diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock new file mode 100644 index 00000000000..785f06dce7c --- /dev/null +++ b/.claude/scheduled_tasks.lock @@ -0,0 +1 @@ +{"sessionId":"a0289491-ebb9-4d03-aa1d-023b0219c585","pid":48965,"procStart":"Fri May 1 19:01:07 2026","acquiredAt":1778687635094} \ No newline at end of file diff --git a/docker-compose-light.yml b/docker-compose-light.yml index 458708bd26a..4eca9648e39 100644 --- a/docker-compose-light.yml +++ b/docker-compose-light.yml @@ -111,6 +111,8 @@ services: superset-init-light: condition: service_completed_successfully volumes: *superset-volumes + ports: + - "${SUPERSET_PORT:-8088}:8088" environment: DATABASE_HOST: db-light DATABASE_DB: superset_light @@ -162,7 +164,7 @@ services: environment: # set this to false if you have perf issues running the npm i; npm run dev in-docker # if you do so, you have to run this manually on the host, which should perform better! - BUILD_SUPERSET_FRONTEND_IN_DOCKER: true + BUILD_SUPERSET_FRONTEND_IN_DOCKER: false NPM_RUN_PRUNE: false SCARF_ANALYTICS: "${SCARF_ANALYTICS:-}" DISABLE_TS_CHECKER: "${DISABLE_TS_CHECKER:-true}" diff --git a/superset-frontend/jest.config.js b/superset-frontend/jest.config.js index d13cd1d03b0..8f9e40a8d7f 100644 --- a/superset-frontend/jest.config.js +++ b/superset-frontend/jest.config.js @@ -27,6 +27,11 @@ module.exports = { '\\.svg$': '/spec/__mocks__/svgrMock.tsx', '^src/(.*)$': '/src/$1', '^spec/(.*)$': '/spec/$1', + // mapping glyph-core to local package source + '^@superset-ui/glyph-core$': + '/packages/superset-ui-glyph-core/src', + '^@superset-ui/glyph-core/(.*)$': + '/packages/superset-ui-glyph-core/src/$1', // mapping plugins of superset-ui to source code '^@superset-ui/([^/]+)/(.*)$': '/node_modules/@superset-ui/$1/src/$2', diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index dfb2e5c5f51..a27cbe26ebb 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -42,6 +42,7 @@ "@luma.gl/gltf": "~9.2.5", "@luma.gl/shadertools": "~9.2.5", "@luma.gl/webgl": "~9.2.5", + "@react-spring/web": "^10.0.3", "@reduxjs/toolkit": "^1.9.3", "@rjsf/antd": "^5.24.13", "@rjsf/core": "^5.24.13", @@ -118,6 +119,7 @@ "nanoid": "^5.1.11", "ol": "^10.9.0", "pretty-ms": "^9.3.0", + "prop-types": "^15.8.1", "query-string": "9.3.1", "re-resizable": "^6.11.2", "react": "^18.2.0", @@ -344,6 +346,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.1.tgz", "integrity": "sha512-12WGKBQzjUAI4ayyF4IAtfw2QR/IDoqk6jTddXDhtYTJF9ASmoE1zst7cVtP0aL/F1jUJL5r+JxKXKEgHNbEUQ==", + "dev": true, "license": "MIT" }, "node_modules/@ant-design/colors": { @@ -402,9 +405,9 @@ "license": "MIT" }, "node_modules/@ant-design/cssinjs/node_modules/stylis": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", - "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.5.tgz", + "integrity": "sha512-K7npNOKGRYuhAFFzkzMGfxFDpN6gDwf8hcMiE+uveTVbBgm93HrNP3ZDUpKqzZ4pG7TP6fmb+EMAQPjq9FqqvA==", "license": "MIT" }, "node_modules/@ant-design/fast-color": { @@ -778,9 +781,9 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.8", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz", - "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==", + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.6.tgz", + "integrity": "sha512-mOAsxeeKkUKayvZR3HeTYD/fICpCPLJrU5ZjelT/PA6WHtNDBOE436YiaEUvHN454bRM3CebhDsIpieCc4texA==", "dev": true, "license": "MIT", "dependencies": { @@ -2495,13 +2498,13 @@ } }, "node_modules/@babel/preset-env/node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz", - "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.0.tgz", + "integrity": "sha512-AvDcMxJ34W4Wgy4KBIIePQTAOP1Ie2WFwkQp3dB7FQ/f0lI5+nM96zUnYEOE1P9sEg0es5VCP0HxiWu5fUHZAQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.8", + "@babel/helper-define-polyfill-provider": "^0.6.6", "core-js-compat": "^3.48.0" }, "peerDependencies": { @@ -2693,26 +2696,19 @@ } }, "node_modules/@bramus/specificity/node_modules/css-tree": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", - "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", "dev": true, "license": "MIT", "dependencies": { - "mdn-data": "2.27.1", - "source-map-js": "^1.2.1" + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" }, "engines": { "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" } }, - "node_modules/@bramus/specificity/node_modules/mdn-data": { - "version": "2.27.1", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", - "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", - "dev": true, - "license": "CC0-1.0" - }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -2756,9 +2752,9 @@ } }, "node_modules/@csstools/css-calc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.0.tgz", - "integrity": "sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", "dev": true, "funding": [ { @@ -2780,9 +2776,9 @@ } }, "node_modules/@csstools/css-color-parser": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.0.tgz", - "integrity": "sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.1.tgz", + "integrity": "sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g==", "dev": true, "funding": [ { @@ -2797,7 +2793,7 @@ "license": "MIT", "dependencies": { "@csstools/color-helpers": "^6.0.2", - "@csstools/css-calc": "^3.2.0" + "@csstools/css-calc": "^3.2.1" }, "engines": { "node": ">=20.19.0" @@ -2830,6 +2826,31 @@ "@csstools/css-tokenizer": "^4.0.0" } }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.4.tgz", + "integrity": "sha512-wgsqt92b7C7tQhIdPNxj0n9zuUbQlvAuI1exyzeNrOKOi62SD7ren8zqszmpVREjAOqg8cD2FqYhQfAuKjk4sw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, "node_modules/@csstools/css-tokenizer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", @@ -2851,13 +2872,13 @@ } }, "node_modules/@deck.gl/aggregation-layers": { - "version": "9.2.11", - "resolved": "https://registry.npmjs.org/@deck.gl/aggregation-layers/-/aggregation-layers-9.2.11.tgz", - "integrity": "sha512-MRFbBHtMcDkOthxXnMPm6nF08DjFDACaIQsJSyHkdWtLUTSLHsWnOTn/8QbB4ka86WyNyfJy3dibLu/m3ei2ow==", + "version": "9.2.6", + "resolved": "https://registry.npmjs.org/@deck.gl/aggregation-layers/-/aggregation-layers-9.2.6.tgz", + "integrity": "sha512-T42ZwB63KI4+0pe2HBwMQS7qnqyv3LlqAQfRSHBlFZMzBq72SxIgk9BzhrT16uBHxFFjjMh6K5g28/UfDOXQEg==", "license": "MIT", "dependencies": { - "@luma.gl/constants": "~9.2.6", - "@luma.gl/shadertools": "~9.2.6", + "@luma.gl/constants": "^9.2.6", + "@luma.gl/shadertools": "^9.2.6", "@math.gl/core": "^4.1.0", "@math.gl/web-mercator": "^4.1.0", "d3-hexbin": "^0.2.1" @@ -2895,13 +2916,13 @@ } }, "node_modules/@deck.gl/extensions": { - "version": "9.2.11", - "resolved": "https://registry.npmjs.org/@deck.gl/extensions/-/extensions-9.2.11.tgz", - "integrity": "sha512-zlpM4Bg1ifBziW1Juiii9NY5gyW2rEhyVTWnhagH/bpTCZ2E73OhnToYt1ouqmoxL6lMtIjhRXz6LPb7tJbHHQ==", + "version": "9.2.6", + "resolved": "https://registry.npmjs.org/@deck.gl/extensions/-/extensions-9.2.6.tgz", + "integrity": "sha512-HNuzo76mD6Ykc/xMEyCMH+to6/Xi+7ehG3VYToSm+R3196Ki5p58pyRHzvq9CrBDvFd3SLMe9QqRm2GTg3wn/w==", "license": "MIT", "dependencies": { - "@luma.gl/constants": "~9.2.6", - "@luma.gl/shadertools": "~9.2.6", + "@luma.gl/constants": "^9.2.6", + "@luma.gl/shadertools": "^9.2.6", "@math.gl/core": "^4.1.0" }, "peerDependencies": { @@ -2966,6 +2987,20 @@ "@luma.gl/engine": "~9.2.4" } }, + "node_modules/@deck.gl/mapbox": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/@deck.gl/mapbox/-/mapbox-9.3.2.tgz", + "integrity": "sha512-+T9pJwsOXwjUxyGN6oiBMfIs28VtDIG1V1Rqz4qqn4TjjNEFFw+xO0olJIg8FO5IAqw2OtePdsrMj0tX8tHdGQ==", + "license": "MIT", + "dependencies": { + "@math.gl/web-mercator": "^4.1.0" + }, + "peerDependencies": { + "@deck.gl/core": "~9.3.0", + "@luma.gl/core": "~9.3.3", + "@math.gl/web-mercator": "^4.1.0" + } + }, "node_modules/@deck.gl/mesh-layers": { "version": "9.2.6", "resolved": "https://registry.npmjs.org/@deck.gl/mesh-layers/-/mesh-layers-9.2.6.tgz", @@ -2997,20 +3032,6 @@ "react-dom": ">=16.3.0" } }, - "node_modules/@deck.gl/widgets": { - "version": "9.2.11", - "resolved": "https://registry.npmjs.org/@deck.gl/widgets/-/widgets-9.2.11.tgz", - "integrity": "sha512-90HWlQPsiRyTPWR4aYfLwnYDrJdHG2mqCzRcyMUKewWBNQLu4upB//l4ewIkUeXXCzAprjjVeRnNb7wdYj2CXQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "preact": "^10.17.0" - }, - "peerDependencies": { - "@deck.gl/core": "~9.2.0", - "@luma.gl/core": "~9.2.6" - } - }, "node_modules/@discoveryjs/json-ext": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", @@ -3075,20 +3096,20 @@ } }, "node_modules/@emnapi/core": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz", - "integrity": "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz", + "integrity": "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==", "dev": true, "license": "MIT", "dependencies": { - "@emnapi/wasi-threads": "1.0.2", + "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", - "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", + "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==", "dev": true, "license": "MIT", "dependencies": { @@ -3096,9 +3117,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.2.tgz", - "integrity": "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz", + "integrity": "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==", "dev": true, "license": "MIT", "dependencies": { @@ -3630,9 +3651,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.1.tgz", + "integrity": "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==", "cpu": [ "arm64" ], @@ -3664,9 +3685,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.1.tgz", + "integrity": "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==", "cpu": [ "arm64" ], @@ -3698,9 +3719,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.1.tgz", + "integrity": "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==", "cpu": [ "arm64" ], @@ -3849,9 +3870,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -3872,6 +3893,22 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@eslint/eslintrc/node_modules/js-yaml": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", @@ -3892,6 +3929,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@eslint/js": { "version": "8.57.1", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", @@ -3903,19 +3953,19 @@ } }, "node_modules/@exodus/bytes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.8.0.tgz", - "integrity": "sha512-8JPn18Bcp8Uo1T82gR8lh2guEOa5KKU/IEKvvdp0sgmi7coPBWf1Doi1EXsGZb2ehc8ym/StJCjffYV+ne7sXQ==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", "dev": true, "license": "MIT", "engines": { "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@exodus/crypto": "^1.0.0-rc.4" + "@noble/hashes": "^1.8.0 || ^2.0.0" }, "peerDependenciesMeta": { - "@exodus/crypto": { + "@noble/hashes": { "optional": true } } @@ -4176,6 +4226,16 @@ } } }, + "node_modules/@inquirer/core/node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/@inquirer/core/node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -4258,9 +4318,9 @@ } }, "node_modules/@inquirer/external-editor/node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4490,9 +4550,9 @@ } }, "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, "license": "MIT", "engines": { @@ -4503,9 +4563,9 @@ } }, "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, "license": "MIT", "engines": { @@ -4534,13 +4594,13 @@ } }, "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.2.2" + "ansi-regex": "^6.0.1" }, "engines": { "node": ">=12" @@ -4747,9 +4807,9 @@ } }, "node_modules/@jest/console/node_modules/@sinclair/typebox": { - "version": "0.34.49", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", - "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", "dev": true, "license": "MIT" }, @@ -4810,9 +4870,9 @@ } }, "node_modules/@jest/console/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -4940,9 +5000,9 @@ } }, "node_modules/@jest/core/node_modules/@sinclair/typebox": { - "version": "0.34.49", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", - "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", "dev": true, "license": "MIT" }, @@ -5003,9 +5063,9 @@ } }, "node_modules/@jest/core/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -5099,9 +5159,9 @@ } }, "node_modules/@jest/create-cache-key-function/node_modules/@sinclair/typebox": { - "version": "0.34.49", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", - "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", "dev": true, "license": "MIT" }, @@ -5123,9 +5183,9 @@ } }, "node_modules/@jest/diff-sequences": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz", - "integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==", + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", "dev": true, "license": "MIT", "engines": { @@ -5175,6 +5235,16 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@jest/expect/node_modules/@jest/diff-sequences": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz", + "integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/expect/node_modules/@jest/expect-utils": { "version": "30.3.0", "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.3.0.tgz", @@ -5221,9 +5291,9 @@ } }, "node_modules/@jest/expect/node_modules/@sinclair/typebox": { - "version": "0.34.49", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", - "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", "dev": true, "license": "MIT" }, @@ -5349,9 +5419,9 @@ } }, "node_modules/@jest/expect/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -5510,16 +5580,16 @@ } }, "node_modules/@jest/globals/node_modules/@sinclair/typebox": { - "version": "0.34.49", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", - "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", "dev": true, "license": "MIT" }, "node_modules/@jest/globals/node_modules/@sinonjs/fake-timers": { - "version": "15.3.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.3.2.tgz", - "integrity": "sha512-mrn35Jl2pCpns+mE3HaZa1yPN5EYCRgiMI+135COjr2hr8Cls9DXqIZ57vZe2cz7y2XVSq92tcs6kGQcT1J8Rw==", + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.1.1.tgz", + "integrity": "sha512-cO5W33JgAPbOh07tvZjUOJ7oWhtaqGHiZw+11DPbyqh2kHTBc3eF/CjJDeQ4205RLQsX6rxCuYOroFQwl7JDRw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -5598,9 +5668,9 @@ } }, "node_modules/@jest/globals/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -5738,20 +5808,33 @@ } }, "node_modules/@jest/reporters/node_modules/@sinclair/typebox": { - "version": "0.34.49", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", - "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", "dev": true, "license": "MIT" }, + "node_modules/@jest/reporters/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/@jest/reporters/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/@jest/reporters/node_modules/chalk": { @@ -5880,13 +5963,13 @@ } }, "node_modules/@jest/reporters/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.7.tgz", + "integrity": "sha512-MOwgjc8tfrpn5QQEvjijjmDVtMw2oL88ugTevzxQnzRLm6l3fVEF2gzU0kYeYYKD8C66+IdGX6peJ4MyUlUnPg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.2" + "brace-expansion": "^5.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -5896,9 +5979,9 @@ } }, "node_modules/@jest/reporters/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -6008,9 +6091,9 @@ } }, "node_modules/@jest/snapshot-utils/node_modules/@sinclair/typebox": { - "version": "0.34.49", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", - "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", "dev": true, "license": "MIT" }, @@ -6095,9 +6178,9 @@ } }, "node_modules/@jest/test-result/node_modules/@sinclair/typebox": { - "version": "0.34.49", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", - "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", "dev": true, "license": "MIT" }, @@ -6203,9 +6286,9 @@ } }, "node_modules/@jest/transform/node_modules/@sinclair/typebox": { - "version": "0.34.49", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", - "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", "dev": true, "license": "MIT" }, @@ -6245,9 +6328,9 @@ } }, "node_modules/@jest/transform/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -6333,9 +6416,9 @@ } }, "node_modules/@jridgewell/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "dev": true, "license": "MIT", "dependencies": { @@ -6350,9 +6433,9 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -6415,369 +6498,17 @@ "tslib": "2" } }, - "node_modules/@jsonjoy.com/buffers": { - "version": "17.67.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.67.0.tgz", - "integrity": "sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/codegen": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", - "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-core": { - "version": "4.57.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.57.2.tgz", - "integrity": "sha512-SVjwklkpIV5wrynpYtuYnfYH1QF4/nDuLBX7VXdb+3miglcAgBVZb/5y0cOsehRV/9Vb+3UqhkMq3/NR3ztdkQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/fs-node-builtins": "4.57.2", - "@jsonjoy.com/fs-node-utils": "4.57.2", - "thingies": "^2.5.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-fsa": { - "version": "4.57.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.57.2.tgz", - "integrity": "sha512-fhO8+iR2I+OCw668ISDJdn1aArc9zx033sWejIyzQ8RBeXa9bDSaUeA3ix0poYOfrj1KdOzytmYNv2/uLDfV6g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/fs-core": "4.57.2", - "@jsonjoy.com/fs-node-builtins": "4.57.2", - "@jsonjoy.com/fs-node-utils": "4.57.2", - "thingies": "^2.5.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-node": { - "version": "4.57.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.57.2.tgz", - "integrity": "sha512-nX2AdL6cOFwLdju9G4/nbRnYevmCJbh7N7hvR3gGm97Cs60uEjyd0rpR+YBS7cTg175zzl22pGKXR5USaQMvKg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/fs-core": "4.57.2", - "@jsonjoy.com/fs-node-builtins": "4.57.2", - "@jsonjoy.com/fs-node-utils": "4.57.2", - "@jsonjoy.com/fs-print": "4.57.2", - "@jsonjoy.com/fs-snapshot": "4.57.2", - "glob-to-regex.js": "^1.0.0", - "thingies": "^2.5.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-node-builtins": { - "version": "4.57.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.57.2.tgz", - "integrity": "sha512-xhiegylRmhw43Ki2HO1ZBL7DQ5ja/qpRsL29VtQ2xuUHiuDGbgf2uD4p9Qd8hJI5P6RCtGYD50IXHXVq/Ocjcg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-node-to-fsa": { - "version": "4.57.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.57.2.tgz", - "integrity": "sha512-18LmWTSONhoAPW+IWRuf8w/+zRolPFGPeGwMxlAhhfY11EKzX+5XHDBPAw67dBF5dxDErHJbl40U+3IXSDRXSQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/fs-fsa": "4.57.2", - "@jsonjoy.com/fs-node-builtins": "4.57.2", - "@jsonjoy.com/fs-node-utils": "4.57.2" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-node-utils": { - "version": "4.57.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.57.2.tgz", - "integrity": "sha512-rsPSJgekz43IlNbLyAM/Ab+ouYLWGp5DDBfYBNNEqDaSpsbXfthBn29Q4muFA9L0F+Z3mKo+CWlgSCXrf+mOyQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/fs-node-builtins": "4.57.2" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-print": { - "version": "4.57.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.57.2.tgz", - "integrity": "sha512-wK9NSow48i4DbDl9F1CQE5TqnyZOJ04elU3WFG5aJ76p+YxO/ulyBBQvKsessPxdo381Bc2pcEoyPujMOhcRqQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/fs-node-utils": "4.57.2", - "tree-dump": "^1.1.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-snapshot": { - "version": "4.57.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.57.2.tgz", - "integrity": "sha512-GdduDZuoP5V/QCgJkx9+BZ6SC0EZ/smXAdTS7PfMqgMTGXLlt/bH/FqMYaqB9JmLf05sJPtO0XRbAwwkEEPbVw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/buffers": "^17.65.0", - "@jsonjoy.com/fs-node-utils": "4.57.2", - "@jsonjoy.com/json-pack": "^17.65.0", - "@jsonjoy.com/util": "^17.65.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/base64": { - "version": "17.67.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-17.67.0.tgz", - "integrity": "sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/codegen": { - "version": "17.67.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-17.67.0.tgz", - "integrity": "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pack": { - "version": "17.67.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-17.67.0.tgz", - "integrity": "sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/base64": "17.67.0", - "@jsonjoy.com/buffers": "17.67.0", - "@jsonjoy.com/codegen": "17.67.0", - "@jsonjoy.com/json-pointer": "17.67.0", - "@jsonjoy.com/util": "17.67.0", - "hyperdyperid": "^1.2.0", - "thingies": "^2.5.0", - "tree-dump": "^1.1.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pointer": { - "version": "17.67.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-17.67.0.tgz", - "integrity": "sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/util": "17.67.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/util": { - "version": "17.67.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-17.67.0.tgz", - "integrity": "sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/buffers": "17.67.0", - "@jsonjoy.com/codegen": "17.67.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, "node_modules/@jsonjoy.com/json-pack": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", - "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.2.0.tgz", + "integrity": "sha512-io1zEbbYcElht3tdlqEOFxZ0dMTYrHz9iMf0gqn1pPjZFTCgM5R4R5IMA20Chb2UPYYsxjzs8CgZ7Nb5n2K2rA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/base64": "^1.1.2", - "@jsonjoy.com/buffers": "^1.2.0", - "@jsonjoy.com/codegen": "^1.0.0", - "@jsonjoy.com/json-pointer": "^1.0.2", - "@jsonjoy.com/util": "^1.9.0", + "@jsonjoy.com/base64": "^1.1.1", + "@jsonjoy.com/util": "^1.1.2", "hyperdyperid": "^1.2.0", - "thingies": "^2.5.0", - "tree-dump": "^1.1.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/json-pack/node_modules/@jsonjoy.com/buffers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", - "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/json-pointer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", - "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/codegen": "^1.0.0", - "@jsonjoy.com/util": "^1.9.0" + "thingies": "^1.20.0" }, "engines": { "node": ">=10.0" @@ -6791,30 +6522,9 @@ } }, "node_modules/@jsonjoy.com/util": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", - "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jsonjoy.com/buffers": "^1.0.0", - "@jsonjoy.com/codegen": "^1.0.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/util/node_modules/@jsonjoy.com/buffers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", - "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.6.0.tgz", + "integrity": "sha512-sw/RMbehRhN68WRtcKCpQOPfnH6lLP4GJfqzi3iYej8tnzpZUDr6UkZYJjcjjC0FWEJOJbyM3PTIwxucUmDG2A==", "dev": true, "license": "Apache-2.0", "engines": { @@ -7163,13 +6873,6 @@ "@loaders.gl/core": "^4.3.0" } }, - "node_modules/@ltd/j-toml": { - "version": "1.38.0", - "resolved": "https://registry.npmjs.org/@ltd/j-toml/-/j-toml-1.38.0.tgz", - "integrity": "sha512-lYtBcmvHustHQtg4X7TXUu1Xa/tbLC3p2wLvgQI+fWVySguVZJF60Snxijw5EiohumxZbR10kWYFFebh1zotiw==", - "dev": true, - "license": "LGPL-3.0" - }, "node_modules/@luma.gl/constants": { "version": "9.2.6", "resolved": "https://registry.npmjs.org/@luma.gl/constants/-/constants-9.2.6.tgz", @@ -7318,9 +7021,9 @@ "license": "ISC" }, "node_modules/@mapbox/tiny-sdf": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.1.0.tgz", - "integrity": "sha512-uFJhNh36BR4OCuWIEiWaEix9CA2WzT6CAIcqVjWYpnx8+QDtS+oC4QehRrx5cX4mgWs37MmKnwUejeHxVymzNg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.2.0.tgz", + "integrity": "sha512-LVL4wgI9YAum5V+LNVQO6QgFBPw7/MIIY4XJPNsPDMrjEwcE+JfKk1LuIl8GnF197ejVdC9QdPaxrx5gfgdGXg==", "license": "BSD-2-Clause" }, "node_modules/@mapbox/unitbezier": { @@ -7348,15 +7051,18 @@ } }, "node_modules/@maplibre/geojson-vt": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-5.0.4.tgz", - "integrity": "sha512-KGg9sma45S+stfH9vPCJk1J0lSDLWZgCT9Y8u8qWZJyjFlP8MNP1WGTxIMYJZjDvVT3PDn05kN1C95Sut1HpgQ==", - "license": "ISC" + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-6.1.0.tgz", + "integrity": "sha512-2eIY4gZxeKIVOZVNkAMb+5NgXhgsMQpOveTQAvnp53LYqHGJZDidk7Ew0Tged9PThidpbS+NFTh0g4zivhPDzQ==", + "license": "ISC", + "dependencies": { + "kdbush": "^4.0.2" + } }, "node_modules/@maplibre/maplibre-gl-style-spec": { - "version": "24.8.1", - "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-24.8.1.tgz", - "integrity": "sha512-zxa92qF96ZNojLxeAjnaRpjVCy+swoUNJvDhtpC90k7u5F0TMr4GmvNqMKvYrMoPB8d7gRSXbMG1hBbmgESIsw==", + "version": "24.8.5", + "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-24.8.5.tgz", + "integrity": "sha512-EzEJmMt6thioRH7GI9LWS7ahXTcAhAPGWCe6oTP2Ps4YnsXOOAfeqx854lZaiDnwURfHmcCKV1mr6oo0i23x6w==", "license": "ISC", "dependencies": { "@mapbox/jsonlint-lines-primitives": "~2.0.2", @@ -7364,7 +7070,6 @@ "json-stringify-pretty-compact": "^4.0.0", "minimist": "^1.2.8", "quickselect": "^3.0.0", - "rw": "^1.3.3", "tinyqueue": "^3.0.0" }, "bin": { @@ -7379,16 +7084,16 @@ "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==", "license": "MIT" }, - "node_modules/@maplibre/maplibre-gl-style-spec/node_modules/rw": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", - "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", - "license": "BSD-3-Clause" + "node_modules/@maplibre/maplibre-gl-style-spec/node_modules/quickselect": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", + "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", + "license": "ISC" }, "node_modules/@maplibre/mlt": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/@maplibre/mlt/-/mlt-1.1.8.tgz", - "integrity": "sha512-8vtfYGidr1rNkv5IwIoU2lfe3Oy+Wa8HluzQYcQi9cveU9K3pweAal/poQj4GJ0K/EW4bTQp2wVAs09g2yDRZg==", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@maplibre/mlt/-/mlt-1.1.9.tgz", + "integrity": "sha512-g/tD8EYJB97udq33ipuJ9a4Q7fcbZnTEnUrgnEc/tLMmEL+zaCbR+X5fkDBO2dgpaAMsLH179qE3UXg2N0Nc/g==", "license": "(MIT OR Apache-2.0)", "dependencies": { "@mapbox/point-geometry": "^1.1.0" @@ -7432,6 +7137,12 @@ "pbf": "^4.0.1" } }, + "node_modules/@maplibre/vt-pbf/node_modules/@maplibre/geojson-vt": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-5.0.4.tgz", + "integrity": "sha512-KGg9sma45S+stfH9vPCJk1J0lSDLWZgCT9Y8u8qWZJyjFlP8MNP1WGTxIMYJZjDvVT3PDn05kN1C95Sut1HpgQ==", + "license": "ISC" + }, "node_modules/@maplibre/vt-pbf/node_modules/pbf": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz", @@ -7697,9 +7408,9 @@ } }, "node_modules/@npmcli/agent/node_modules/lru-cache": { - "version": "11.3.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", - "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", + "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -7765,9 +7476,9 @@ } }, "node_modules/@npmcli/arborist/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -7777,10 +7488,23 @@ "node": "18 || 20 || >=22" } }, + "node_modules/@npmcli/arborist/node_modules/hosted-git-info": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/@npmcli/arborist/node_modules/lru-cache": { - "version": "11.3.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", - "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", + "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -7942,9 +7666,9 @@ } }, "node_modules/@npmcli/git/node_modules/lru-cache": { - "version": "11.3.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", - "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", + "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -8031,9 +7755,9 @@ } }, "node_modules/@npmcli/map-workspaces/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -8062,9 +7786,9 @@ } }, "node_modules/@npmcli/map-workspaces/node_modules/lru-cache": { - "version": "11.3.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", - "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", + "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -8191,9 +7915,9 @@ } }, "node_modules/@npmcli/package-json/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -8228,6 +7952,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@npmcli/package-json/node_modules/hosted-git-info": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/@npmcli/package-json/node_modules/jackspeak": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", @@ -8245,9 +7982,9 @@ } }, "node_modules/@npmcli/package-json/node_modules/lru-cache": { - "version": "11.3.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", - "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", + "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -8438,14 +8175,14 @@ } }, "node_modules/@nx/devkit": { - "version": "22.6.1", - "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-22.6.1.tgz", - "integrity": "sha512-/mwG9zWY1phsWvMKzP0yZ4pE6aH0kLH31DuCYj4eLbhuUu0STL3xSdjPPzhDHf71R4K3YnuvG97e2qiGDbG5Qw==", + "version": "22.7.1", + "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-22.7.1.tgz", + "integrity": "sha512-z2ayFHq406MyVpNtksGnsfHOYZVTSInwQgZeg6u+S4sD21Wvb+oldhqkbYX46jiGJSaw5aUjFdzXJu2l4MYP1A==", "dev": true, "license": "MIT", "dependencies": { "@zkochan/js-yaml": "0.0.7", - "ejs": "^3.1.7", + "ejs": "5.0.1", "enquirer": "~2.3.6", "minimatch": "10.2.4", "semver": "^7.6.3", @@ -8467,9 +8204,9 @@ } }, "node_modules/@nx/devkit/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -8479,6 +8216,32 @@ "node": "18 || 20 || >=22" } }, + "node_modules/@nx/devkit/node_modules/ejs": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-5.0.1.tgz", + "integrity": "sha512-COqBPFMxuPTPspXl2DkVYaDS3HtrD1GpzOGkNTJ1IYkifq/r9h8SVEFrjA3D9/VJGOEoMQcrlhpntcSUrM8k6A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.12.18" + } + }, + "node_modules/@nx/devkit/node_modules/enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/@nx/devkit/node_modules/minimatch": { "version": "10.2.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", @@ -8496,9 +8259,9 @@ } }, "node_modules/@nx/nx-darwin-arm64": { - "version": "22.6.1", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-22.6.1.tgz", - "integrity": "sha512-lixkEBGFdEsUiqEZg9LIyjfiTv12Sg1Es/yUgrdOQUAZu+5oiUPMoybyBwrvINl+fZw+PLh66jOmB4GSP2aUMQ==", + "version": "22.7.1", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-22.7.1.tgz", + "integrity": "sha512-m00ZmBn39VUgb0Ahhu5iY6D56ETdXjDbVnOz0XF3DacJrcLtq9sZ+cg1bj6PshqtvRWVg+zJRrZBU6vL7hGuFQ==", "cpu": [ "arm64" ], @@ -8510,9 +8273,9 @@ ] }, "node_modules/@nx/nx-darwin-x64": { - "version": "22.6.1", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-22.6.1.tgz", - "integrity": "sha512-HvgtOtuWnEf0dpfWb05N0ptdFg040YgzsKFhXg6+qaBJg5Hg0e0AXPKaSgh2PCqCIDlKu40YtwVgF7KXxXAGlA==", + "version": "22.7.1", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-22.7.1.tgz", + "integrity": "sha512-DmD8Qow+Yt7Yrmjlz1AsfiwxW+0kRzg+6MY70+d7qChtD2bTzvA/k0ut8SMy+CxU3kxgUbKhGOtml5JDXoX2ww==", "cpu": [ "x64" ], @@ -8524,9 +8287,9 @@ ] }, "node_modules/@nx/nx-freebsd-x64": { - "version": "22.6.1", - "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-22.6.1.tgz", - "integrity": "sha512-g2wUltGX+7/+mdTV5d6ODa0ylrNu/krgb9YdrsbhW6oZeXYm2LeLOAnYqIlL/Kx140NLrb5Kcz7bi7JrBAw4Ow==", + "version": "22.7.1", + "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-22.7.1.tgz", + "integrity": "sha512-HboVrUCHcuYTXtuX3dMyRszP7JO90ZVBLWgnmaM7jUM7jnllZjmezUMtpNHfN1GQbVFafJf/NBShDWsu9LuaUA==", "cpu": [ "x64" ], @@ -8538,9 +8301,9 @@ ] }, "node_modules/@nx/nx-linux-arm-gnueabihf": { - "version": "22.6.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-22.6.1.tgz", - "integrity": "sha512-TTqisFPAPrj35EihvzotBbajS+0bX++PQggmRVmDmGwSTrpySRJwZnKNHYDqP6s9tigDvkNJOJftK+GkBEFRRA==", + "version": "22.7.1", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-22.7.1.tgz", + "integrity": "sha512-5Gm8Y7L8WXMLUjHhiy1eqGz5/PiRw1YLanFg5audBNkZvH6Jkwzdpoz0dbeKjwMDHz4NmniUV1s76Th8VLWmiQ==", "cpu": [ "arm" ], @@ -8552,9 +8315,9 @@ ] }, "node_modules/@nx/nx-linux-arm64-gnu": { - "version": "22.6.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-22.6.1.tgz", - "integrity": "sha512-uIkPcanSTIcyh7/6LOoX0YpGO/7GkVhMRgyM9Mg/7ItFjCtRaeuPEPrJESsaNeB5zIVVhI4cXbGrM9NDnagiiw==", + "version": "22.7.1", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-22.7.1.tgz", + "integrity": "sha512-GdgPYMfbijBRFJs1absL/9QdSNLsTAGdyKykDf9CaVxEMZ92VB+pncpX9Vn/ZBCSeeWTLF+bSK3UM5v+loIObQ==", "cpu": [ "arm64" ], @@ -8566,9 +8329,9 @@ ] }, "node_modules/@nx/nx-linux-arm64-musl": { - "version": "22.6.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-22.6.1.tgz", - "integrity": "sha512-eqkG8s/7remiRZ1Lo2zIrFLSNsQ/0x9fAj++CV1nqFE+rfykPQhC48F8pqsq6tUQpI5HqRQEfQgv4CnFNpLR+w==", + "version": "22.7.1", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-22.7.1.tgz", + "integrity": "sha512-HyBgPtY1hyNTk8683nt7F29jh3lVdS/zul9vS0NgKeCSoYL3GRM3nLoTPynoHUxyVP/tWYOE3ymvnk92qYwL4Q==", "cpu": [ "arm64" ], @@ -8580,9 +8343,9 @@ ] }, "node_modules/@nx/nx-linux-x64-gnu": { - "version": "22.6.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-22.6.1.tgz", - "integrity": "sha512-6DhSupCcDa6BYzQ48qsMK4LIdIO+y4E+4xuUBkX2YTGOZh58gctELCv7Gi6/FhiC8rzVzM7hDcygOvHCGc30zA==", + "version": "22.7.1", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-22.7.1.tgz", + "integrity": "sha512-bQBgRiEsanNvKcDOjVAUPjvcp0iDLofYYUL2af2iuCDxreLOej+J6MeA5bWTLNly5ly1d4voKGTqa+OsouVyLg==", "cpu": [ "x64" ], @@ -8594,9 +8357,9 @@ ] }, "node_modules/@nx/nx-linux-x64-musl": { - "version": "22.6.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-22.6.1.tgz", - "integrity": "sha512-QqtfaBhdfLRKGucpP8RSv7KJ51XRWpfUcXPhkb/1dKP/b9/Z0kpaCgczGHdrAtX9m6haWw+sQXYGxnStZIg/TQ==", + "version": "22.7.1", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-22.7.1.tgz", + "integrity": "sha512-gcco2GjcAztF/fRcAgFxtWxrWDnQdNmPaAN9FTt1+qQ9RUSLvdL8bQxKx4Kd9N9T+gXPlrWhMkBkKbbV09+X1Q==", "cpu": [ "x64" ], @@ -8608,9 +8371,9 @@ ] }, "node_modules/@nx/nx-win32-arm64-msvc": { - "version": "22.6.1", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-22.6.1.tgz", - "integrity": "sha512-8pTWXphY5IIgY3edZ5SfzP8yPjBqoAxRV5snAYDctF4e0OC1nDOUims70jLesMle8DTSWiHPSfbLVfp2HkU9WQ==", + "version": "22.7.1", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-22.7.1.tgz", + "integrity": "sha512-IT9oEn0YQ83iPH7666aoPyTRsUzBIBJdBLMXeLX4I60fHPXWhUSGpfiLtIsgU2OfeOVb9hU9idwNh1wc4u9rWQ==", "cpu": [ "arm64" ], @@ -8622,9 +8385,9 @@ ] }, "node_modules/@nx/nx-win32-x64-msvc": { - "version": "22.6.1", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-22.6.1.tgz", - "integrity": "sha512-XMYrtsR5O39uNR4fVpFs65rVB09FyLXvUM735r2rO7IUWWHxHWTAgVcc+gqQaAchBPqR9f1q+3u2i1Inub3Cdw==", + "version": "22.7.1", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-22.7.1.tgz", + "integrity": "sha512-P2zeSKXVH2Eiwsb8UfP2rMMS7//cHWpiO4M9zt6q0c4lI/hN1vXBciRKVWruGk9ZrWLHuhaMAhG94+MJtzKuRQ==", "cpu": [ "x64" ], @@ -8643,9 +8406,9 @@ "license": "MIT" }, "node_modules/@oxlint/binding-android-arm-eabi": { - "version": "1.63.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.63.0.tgz", - "integrity": "sha512-A9xLtQt7i0OA1PoB/meog6kikXI9CdwEp7ZwQqmgnpKn3G3b1orvTDy8CQ6T7w1HvDrgWGB78PkFKcWgibcTCg==", + "version": "1.64.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.64.0.tgz", + "integrity": "sha512-2r6Nq3XXGLHEXKkSj8JtmJ6N4gDw431DPFOg0ZoJHlNjnG6HVMm/ksQ10m0HJ8WBvwgMe1L50UHPaYZutCRPCw==", "cpu": [ "arm" ], @@ -8660,9 +8423,9 @@ } }, "node_modules/@oxlint/binding-android-arm64": { - "version": "1.63.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.63.0.tgz", - "integrity": "sha512-SQo+ZMvdR9l3CxZp5W5gFNxSiDxclY6lOzzNpKYLF8asESpm3Pwumx0gER5T7aHLF1/2BAAtLD3DiDkdgy4V1A==", + "version": "1.64.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.64.0.tgz", + "integrity": "sha512-ePJMpePgg7fBv+L/hVx1xXRU5/5gd5m0obLA6hPEfLXF3GjpR8idIDbY1dhQYhyz1ms2wdTccSboo6KEd2Oxtg==", "cpu": [ "arm64" ], @@ -8677,9 +8440,9 @@ } }, "node_modules/@oxlint/binding-darwin-arm64": { - "version": "1.63.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.63.0.tgz", - "integrity": "sha512-6W82XjJDTmMnjg30427l0dufpnyLoq7wEukKdM6/g2VIybRVuQiBVh43EA4b+UxZ3+tLcKm+Or/pXGNgLCEU8g==", + "version": "1.64.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.64.0.tgz", + "integrity": "sha512-U4DMLQd10gJLuoSTLSGbfv3bGjTlUNsScm9Dgb8wwBqmCzidf1pE1pXV4doGNxqwH3KtVng1AGTINA0NvkGLvQ==", "cpu": [ "arm64" ], @@ -8694,9 +8457,9 @@ } }, "node_modules/@oxlint/binding-darwin-x64": { - "version": "1.63.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.63.0.tgz", - "integrity": "sha512-CnWd/YCuVG5W1BYkjJEVbJG11o526O9qAwBEQM+nh8K19CRFUkFdROXCyYkGmroHEYQe4vgQ6+lh3550Lp35Xw==", + "version": "1.64.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.64.0.tgz", + "integrity": "sha512-GoRIL48QWm4/TAvjN8pB1nAG+1/uqc9EdnWT9zqHeb6wsmjZtywj8VRe5aGW47Fdb64YtLOsdLqVxOvQuz98Wg==", "cpu": [ "x64" ], @@ -8711,9 +8474,9 @@ } }, "node_modules/@oxlint/binding-freebsd-x64": { - "version": "1.63.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.63.0.tgz", - "integrity": "sha512-a4eZAqrmtajqcxfdAzC+l7g3PaE3V8hpAYqqeD3fTxLXOMFdK3eNTZrU80n4dDEVm0JXy1aL5PqvqWldBl6zYA==", + "version": "1.64.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.64.0.tgz", + "integrity": "sha512-5dFkv4tkg7PxJJGS9/OjrJwjhuHczrd3OQOkRE0wHcLM+ncUnULtzEPWjqGOxTXxZnLWcB91bGiIznx89TVXyQ==", "cpu": [ "x64" ], @@ -8728,9 +8491,9 @@ } }, "node_modules/@oxlint/binding-linux-arm-gnueabihf": { - "version": "1.63.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.63.0.tgz", - "integrity": "sha512-tYUtU9TdbU3uXF5D62g5zXJ13iniFGhXQx5vp9cyEjGdbSAY3VdFBSaldYvyoDmgMZ0ZYuwQP1Y4t2Fhejwa0w==", + "version": "1.64.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.64.0.tgz", + "integrity": "sha512-jsBqMLl/uOL5+Kq/+BtK9FrmiNGUbx8SiyZXv+WlUxA45KuwcLu9BfiSIL3I3DBDgWM3yZizDITnTK9BcqNBQg==", "cpu": [ "arm" ], @@ -8745,9 +8508,9 @@ } }, "node_modules/@oxlint/binding-linux-arm-musleabihf": { - "version": "1.63.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.63.0.tgz", - "integrity": "sha512-I5r3twFf776UZg9dmRo2xbrKt00tTkORXEVe0ctg4vdTkQvJAjiCHxnbAU2HL1AiJ9cqADA76MAliuilsAWnvg==", + "version": "1.64.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.64.0.tgz", + "integrity": "sha512-1lrj8At/Uuc9GhjrVFBQo0NEjfBrTkzpmtHIGAhNnIXqn1CAyGL+qrztUsXb2GIluJrpl9Q7qRLJOb/NqydacQ==", "cpu": [ "arm" ], @@ -8762,9 +8525,9 @@ } }, "node_modules/@oxlint/binding-linux-arm64-gnu": { - "version": "1.63.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.63.0.tgz", - "integrity": "sha512-t7ltUkg6FFh4b564QyGir8xIj/QZbXu8FlcRkcyW9+ztr/mfRHlvUOFd95pJCXi9s/L5DrUeWWgpXRS+V+6igQ==", + "version": "1.64.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.64.0.tgz", + "integrity": "sha512-HpSQbubwh03mMhAdy2BYtad/fsY8vDFHDAb6bUwuCYg2VD3xCQgn6ArKcO0oZyLCheacKTv4PrF3Mfu5hgoE2g==", "cpu": [ "arm64" ], @@ -8779,9 +8542,9 @@ } }, "node_modules/@oxlint/binding-linux-arm64-musl": { - "version": "1.63.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.63.0.tgz", - "integrity": "sha512-Q5mmZy/XWjuYFUuQyYjOvZ5U/JkKEwnpir6hGxhh6HcdP0V/BKxLo8dqkfF/t7r7AguB17dfS/8+go5AQDRR6g==", + "version": "1.64.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.64.0.tgz", + "integrity": "sha512-00QQ0h0Y7u0G69BgiH3+ky2aaq/QvkDL6DYok8htIuJHxybiux5aQ8jwmg8qIk9wha6UagUP2BAwAzbemcJbpg==", "cpu": [ "arm64" ], @@ -8796,9 +8559,9 @@ } }, "node_modules/@oxlint/binding-linux-ppc64-gnu": { - "version": "1.63.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.63.0.tgz", - "integrity": "sha512-uBGtuZ0TzLB4x5wVa82HGNvYqY8buwDhyCnCP0R0gkk9szqVsP0MeTtD5HX7EsEuFIt+aYmYxuxeVxs3nTSwtQ==", + "version": "1.64.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.64.0.tgz", + "integrity": "sha512-2GaimTV6EMW+s5HS0An3oGbQme3BgHswvfVdGk3EB57Xe9+/gyT+Qd7lNVzb3rtir52vbIPzXfaYArzs5b5zcw==", "cpu": [ "ppc64" ], @@ -8813,9 +8576,9 @@ } }, "node_modules/@oxlint/binding-linux-riscv64-gnu": { - "version": "1.63.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.63.0.tgz", - "integrity": "sha512-h4s6FwxE+9MeA181o0dnDwHP32Y/bG8EiB/vrD6Ib+AMt6haigDc/0bUtI/sLmQDBMJnUfaCmtSSrEAqjtEVrA==", + "version": "1.64.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.64.0.tgz", + "integrity": "sha512-H46AtFb9wypjoVwGdlxrm0DsD809NGmtiK9HiyPKTxkSte2YjhC4S+00rOIrwCaxcyPiGid3Y3OMXp5KMAkGZw==", "cpu": [ "riscv64" ], @@ -8830,9 +8593,9 @@ } }, "node_modules/@oxlint/binding-linux-riscv64-musl": { - "version": "1.63.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.63.0.tgz", - "integrity": "sha512-2EaNcCBR8Mcjl5ARtuN3BdEpVkX7KpjSjMGZ/mJMIeaXgTtdz5ytg2VwygMSStA/k0ixfvZFoZOfjDEcouV5vQ==", + "version": "1.64.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.64.0.tgz", + "integrity": "sha512-HEgsidjjvvyzdg82icYkuFCf7REDV7B9JFwbIMbVwrKLBY0MrXX+bku3POn/hduZ2yW91IyVDUMq0Bf02KwXQw==", "cpu": [ "riscv64" ], @@ -8847,9 +8610,9 @@ } }, "node_modules/@oxlint/binding-linux-s390x-gnu": { - "version": "1.63.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.63.0.tgz", - "integrity": "sha512-p4hlf/fd7TrYYl3QrWWD0GocqJefwMu3cHQhmi2FvEB/YOvFb5DZN3SMBaPi7B1TM5DeypkEtrVib674q1KKPg==", + "version": "1.64.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.64.0.tgz", + "integrity": "sha512-Axvm8qryotmKN00P5w4JapaSjvP2LOSbdbBJiX+2SuHd3QzhW7TUc8skqgw+ahQZ5DmzEYeHCqauvW8f32Ns6Q==", "cpu": [ "s390x" ], @@ -8864,9 +8627,9 @@ } }, "node_modules/@oxlint/binding-linux-x64-gnu": { - "version": "1.63.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.63.0.tgz", - "integrity": "sha512-Vgq9rkRVcPcjbcH+ihYTfpeR7vCXfqpd+z5ItTGc0yYUV59L5ceHYN1iV4H9bKGV7Rn5hkVc7x3mSvHegduENA==", + "version": "1.64.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.64.0.tgz", + "integrity": "sha512-cR60vSd7+m+KRZ3GQGfDxWwahW5RMXg0qlGvAluZr0fTUYvw0H9N9AXAF/M/PMqgytyqvVNmBAkJG9l7U30Y1g==", "cpu": [ "x64" ], @@ -8881,9 +8644,9 @@ } }, "node_modules/@oxlint/binding-linux-x64-musl": { - "version": "1.63.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.63.0.tgz", - "integrity": "sha512-3/Lkq/ncooA61rorrC+ZQed1Bc4VpGj+WnGsp58zmxKgvZ2vhreu+dcVyr3mX8NUpq7mfZ4gDDTou/yrF1Pd7A==", + "version": "1.64.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.64.0.tgz", + "integrity": "sha512-2u/aPZ9pEg7HnvZPDsHxUGNnrpr4qaHi+mCgLgpt+LYRzPrS4Px4wPfkIdRdr2GvKnaYyt+XSlto0Vm5sbStTg==", "cpu": [ "x64" ], @@ -8898,9 +8661,9 @@ } }, "node_modules/@oxlint/binding-openharmony-arm64": { - "version": "1.63.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.63.0.tgz", - "integrity": "sha512-0/EdD/6hDkx5Mfd769PTjvEM8mZ/6Dfukp1dBCL/2PjlIVGEtYdNZyok6ChqYPsT9JcFnlQnUeQzO0/1L/oC9w==", + "version": "1.64.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.64.0.tgz", + "integrity": "sha512-kfhkGfCdoXLSxEkrhDlJrvBYajGmq+ma4EMc53dsOWTq+rIBOlI0vTBmpZNnM5oH2LY/K/w1HAK+UQEgjgpVUg==", "cpu": [ "arm64" ], @@ -8915,9 +8678,9 @@ } }, "node_modules/@oxlint/binding-win32-arm64-msvc": { - "version": "1.63.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.63.0.tgz", - "integrity": "sha512-wb0CUkN8ngwPiRQBjD1Cj0LsHeNvm+Xt6YBHDMtj2DVQVD6Oj8Ri7g6BD+KICf6LaBqZlmzOvy6nF9E/8yyGOg==", + "version": "1.64.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.64.0.tgz", + "integrity": "sha512-r/cNKBFieONoVu2bb1KkVouq9W+edDUgHumXJGphCRRj+U0xaD4nanrw8ZOqo0IsutPkEM4vCcGBpak6x5aXMg==", "cpu": [ "arm64" ], @@ -8932,9 +8695,9 @@ } }, "node_modules/@oxlint/binding-win32-ia32-msvc": { - "version": "1.63.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.63.0.tgz", - "integrity": "sha512-BX5iq+ovdNlVYhSn5qPMUIT0uwAwt2lmEnCnzK+Gkhw4DovIvhGb96OFhV8yzQNUnQxn/xGkOR+X+BLrLDNm8w==", + "version": "1.64.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.64.0.tgz", + "integrity": "sha512-tUw0xUUwEFVZbpJoeCblkv8SJA4Xz3CdXCJbAnBsiNLyxDrk2tLcxEAS6M73Q7hHHDg3OtwI8vZVK3t5RJt4Gw==", "cpu": [ "ia32" ], @@ -8949,9 +8712,9 @@ } }, "node_modules/@oxlint/binding-win32-x64-msvc": { - "version": "1.63.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.63.0.tgz", - "integrity": "sha512-QeN/WELOfsXMeYwxvfgQrl6CbVftYUCZsGXHjXQd5Trccm8+i4gmtxaOui4xbJQaiDlviF8F3yLSBloQUeFsfA==", + "version": "1.64.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.64.0.tgz", + "integrity": "sha512-9CBR+LO0JVST87fNTzzNxS5I29jIUO5gxT9i9+M3SDHHALElj9sY1Prf12tad3vIRC6OD7Ehtvvh+sn13vSwHw==", "cpu": [ "x64" ], @@ -9432,6 +9195,20 @@ "react-dom": ">=16.9.0" } }, + "node_modules/@rc-component/util": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@rc-component/util/-/util-1.10.1.tgz", + "integrity": "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng==", + "license": "MIT", + "dependencies": { + "is-mobile": "^5.0.0", + "react-is": "^18.2.0" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, "node_modules/@react-dnd/asap": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.1.tgz", @@ -9451,81 +9228,75 @@ "license": "MIT" }, "node_modules/@react-spring/animated": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.5.tgz", - "integrity": "sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-10.0.3.tgz", + "integrity": "sha512-7MrxADV3vaUADn2V9iYhaIL6iOWRx9nCJjYrsk2AHD2kwPr6fg7Pt0v+deX5RnCDmCKNnD6W5fasiyM8D+wzJQ==", "license": "MIT", - "peer": true, "dependencies": { - "@react-spring/shared": "~9.7.5", - "@react-spring/types": "~9.7.5" + "@react-spring/shared": "~10.0.3", + "@react-spring/types": "~10.0.3" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/@react-spring/core": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.5.tgz", - "integrity": "sha512-rmEqcxRcu7dWh7MnCcMXLvrf6/SDlSokLaLTxiPlAYi11nN3B5oiCUAblO72o+9z/87j2uzxa2Inm8UbLjXA+w==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-10.0.3.tgz", + "integrity": "sha512-D4DwNO68oohDf/0HG2G0Uragzb9IA1oXblxrd6MZAcBcUQG2EHUWXewjdECMPLNmQvlYVyyBRH6gPxXM5DX7DQ==", "license": "MIT", - "peer": true, "dependencies": { - "@react-spring/animated": "~9.7.5", - "@react-spring/shared": "~9.7.5", - "@react-spring/types": "~9.7.5" + "@react-spring/animated": "~10.0.3", + "@react-spring/shared": "~10.0.3", + "@react-spring/types": "~10.0.3" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/react-spring/donate" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/@react-spring/rafz": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.5.tgz", - "integrity": "sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw==", - "license": "MIT", - "peer": true + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-10.0.3.tgz", + "integrity": "sha512-Ri2/xqt8OnQ2iFKkxKMSF4Nqv0LSWnxXT4jXFzBDsHgeeH/cHxTLupAWUwmV9hAGgmEhBmh5aONtj3J6R/18wg==", + "license": "MIT" }, "node_modules/@react-spring/shared": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.5.tgz", - "integrity": "sha512-wdtoJrhUeeyD/PP/zo+np2s1Z820Ohr/BbuVYv+3dVLW7WctoiN7std8rISoYoHpUXtbkpesSKuPIw/6U1w1Pw==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-10.0.3.tgz", + "integrity": "sha512-geCal66nrkaQzUVhPkGomylo+Jpd5VPK8tPMEDevQEfNSWAQP15swHm+MCRG4wVQrQlTi9lOzKzpRoTL3CA84Q==", "license": "MIT", - "peer": true, "dependencies": { - "@react-spring/rafz": "~9.7.5", - "@react-spring/types": "~9.7.5" + "@react-spring/rafz": "~10.0.3", + "@react-spring/types": "~10.0.3" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/@react-spring/types": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.5.tgz", - "integrity": "sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g==", - "license": "MIT", - "peer": true + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-10.0.3.tgz", + "integrity": "sha512-H5Ixkd2OuSIgHtxuHLTt7aJYfhMXKXT/rK32HPD/kSrOB6q6ooeiWAXkBy7L8F3ZxdkBb9ini9zP9UwnEFzWgQ==", + "license": "MIT" }, "node_modules/@react-spring/web": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.5.tgz", - "integrity": "sha512-lmvqGwpe+CSttsWNZVr+Dg62adtKhauGwLyGE/RRyZ8AAMLgb9x3NDMA5RMElXo+IMyTkPp7nxTB8ZQlmhb6JQ==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-10.0.3.tgz", + "integrity": "sha512-ndU+kWY81rHsT7gTFtCJ6mrVhaJ6grFmgTnENipzmKqot4HGf5smPNK+cZZJqoGeDsj9ZsiWPW4geT/NyD484A==", "license": "MIT", - "peer": true, "dependencies": { - "@react-spring/animated": "~9.7.5", - "@react-spring/core": "~9.7.5", - "@react-spring/shared": "~9.7.5", - "@react-spring/types": "~9.7.5" + "@react-spring/animated": "~10.0.3", + "@react-spring/core": "~10.0.3", + "@react-spring/shared": "~10.0.3", + "@react-spring/types": "~10.0.3" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/@reduxjs/toolkit": { @@ -9614,43 +9385,6 @@ "react-dom": ">=16.9.0" } }, - "node_modules/@rjsf/antd/node_modules/rc-picker/node_modules/rc-trigger": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/rc-trigger/-/rc-trigger-5.3.4.tgz", - "integrity": "sha512-mQv+vas0TwKcjAO2izNPkqR4j86OemLRmvL2nOzdP9OWNWA1ivoTt5hzFqYNW9zACwmTezRiN8bttrC7cZzYSw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.3", - "classnames": "^2.2.6", - "rc-align": "^4.0.0", - "rc-motion": "^2.0.0", - "rc-util": "^5.19.2" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@rjsf/antd/node_modules/rc-picker/node_modules/rc-trigger/node_modules/rc-align": { - "version": "4.0.15", - "resolved": "https://registry.npmjs.org/rc-align/-/rc-align-4.0.15.tgz", - "integrity": "sha512-wqJtVH60pka/nOX7/IspElA8gjPNQKIx/ZqJ6heATCkXpe1Zg4cPVrMD2vC96wjsFFL8WsmhPbx9tdMo1qqlIA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.10.1", - "classnames": "2.x", - "dom-align": "^1.7.0", - "rc-util": "^5.26.0", - "resize-observer-polyfill": "^1.5.1" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, "node_modules/@rjsf/core": { "version": "5.24.13", "resolved": "https://registry.npmjs.org/@rjsf/core/-/core-5.24.13.tgz", @@ -9799,9 +9533,9 @@ } }, "node_modules/@sigstore/protobuf-specs": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.5.0.tgz", - "integrity": "sha512-MM8XIwUjN2bwvCg1QvrMtbBmpcSHrkhFSCu1D11NyPvDQ25HEc4oG5/OcQfd/Tlf/OxmKWERDj0zGE23jQaMwA==", + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.5.1.tgz", + "integrity": "sha512-/ScWUhhoFasJsSRGTVBwId1loQjjnjAfE4djL6ZhrXRpNCmPTnUKF5Jokd58ILseOMjzET3UrMOtJPS9sYeI0g==", "dev": true, "license": "Apache-2.0", "engines": { @@ -9987,24 +9721,12 @@ } }, "node_modules/@sinclair/typebox": { - "version": "0.27.10", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", - "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "dev": true, "license": "MIT" }, - "node_modules/@sindresorhus/merge-streams": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", - "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@sinonjs/commons": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", @@ -10050,7 +9772,7 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "8.6.18" + "storybook": "^8.6.18" } }, "node_modules/@storybook/addon-actions/node_modules/uuid": { @@ -10067,6 +9789,25 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/@storybook/addon-backgrounds": { + "version": "8.6.18", + "resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-8.6.18.tgz", + "integrity": "sha512-froND3WwvSCYzjEBO8QODStaWNL+aGXqxBEbrMnGYejDFST4qEFkvM2IYWMnLBkRgrgJ0yIqTeDQoyH9b9/8uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "memoizerific": "^1.11.3", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.6.18" + } + }, "node_modules/@storybook/addon-controls": { "version": "8.6.18", "resolved": "https://registry.npmjs.org/@storybook/addon-controls/-/addon-controls-8.6.18.tgz", @@ -10083,7 +9824,30 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "8.6.18" + "storybook": "^8.6.18" + } + }, + "node_modules/@storybook/addon-docs": { + "version": "8.6.18", + "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-8.6.18.tgz", + "integrity": "sha512-55ADer0yNmmeR928Y3UAv3r4i7bJSd9LwywsQ+lRol/FNe0ZcwLEz31xL+jVsqQFNnDh/imsDIp8aYapGMtfEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mdx-js/react": "^3.0.0", + "@storybook/blocks": "8.6.18", + "@storybook/csf-plugin": "8.6.18", + "@storybook/react-dom-shim": "8.6.18", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.6.18" } }, "node_modules/@storybook/addon-essentials": { @@ -10109,97 +9873,10 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "8.6.18" + "storybook": "^8.6.18" } }, - "node_modules/@storybook/addon-essentials/node_modules/@storybook/addon-backgrounds": { - "version": "8.6.18", - "resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-8.6.18.tgz", - "integrity": "sha512-froND3WwvSCYzjEBO8QODStaWNL+aGXqxBEbrMnGYejDFST4qEFkvM2IYWMnLBkRgrgJ0yIqTeDQoyH9b9/8uQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@storybook/global": "^5.0.0", - "memoizerific": "^1.11.3", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "storybook": "8.6.18" - } - }, - "node_modules/@storybook/addon-essentials/node_modules/@storybook/addon-docs": { - "version": "8.6.18", - "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-8.6.18.tgz", - "integrity": "sha512-55ADer0yNmmeR928Y3UAv3r4i7bJSd9LwywsQ+lRol/FNe0ZcwLEz31xL+jVsqQFNnDh/imsDIp8aYapGMtfEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@mdx-js/react": "^3.0.0", - "@storybook/blocks": "8.6.18", - "@storybook/csf-plugin": "8.6.18", - "@storybook/react-dom-shim": "8.6.18", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "storybook": "8.6.18" - } - }, - "node_modules/@storybook/addon-essentials/node_modules/@storybook/addon-docs/node_modules/@storybook/blocks": { - "version": "8.6.18", - "resolved": "https://registry.npmjs.org/@storybook/blocks/-/blocks-8.6.18.tgz", - "integrity": "sha512-esZv4msPQ9LxgTb8YUIZhhxVMuI6BPi5bkXtk8c7w7sWuAsqsCe/RnVInn7ooUry2gjnD4hd9+8Eqj0b8oTVoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@storybook/icons": "^1.2.12", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "8.6.18" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, - "node_modules/@storybook/addon-essentials/node_modules/@storybook/addon-docs/node_modules/@storybook/csf-plugin": { - "version": "8.6.18", - "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-8.6.18.tgz", - "integrity": "sha512-x1ioz/L0CwaelCkHci3P31YtvwayN3FBftvwQOPbvRh9qeb4Cpz5IdVDmyvSxxYwXN66uAORNoqgjTi7B4/y5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "unplugin": "^1.3.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "storybook": "8.6.18" - } - }, - "node_modules/@storybook/addon-essentials/node_modules/@storybook/addon-highlight": { + "node_modules/@storybook/addon-highlight": { "version": "8.6.18", "resolved": "https://registry.npmjs.org/@storybook/addon-highlight/-/addon-highlight-8.6.18.tgz", "integrity": "sha512-wTFJ1DPM0C8gK6nGTJxH75byayQj7BPAz02fME4AOmT6clrBpVl1zSTFTkXaSr+k4xOfeMR/xNUfVskaXz6T9w==", @@ -10213,74 +9890,7 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "8.6.18" - } - }, - "node_modules/@storybook/addon-essentials/node_modules/@storybook/addon-measure": { - "version": "8.6.18", - "resolved": "https://registry.npmjs.org/@storybook/addon-measure/-/addon-measure-8.6.18.tgz", - "integrity": "sha512-fMEOJXgPrTm6qHlWoRM+WTLE7Mr1QBIf2ei+pujBQFcWkD6Gjc2pV8zKzvh93d+EA13wD8AmwOq1DEw9J+XH+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@storybook/global": "^5.0.0", - "tiny-invariant": "^1.3.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "storybook": "8.6.18" - } - }, - "node_modules/@storybook/addon-essentials/node_modules/@storybook/addon-outline": { - "version": "8.6.18", - "resolved": "https://registry.npmjs.org/@storybook/addon-outline/-/addon-outline-8.6.18.tgz", - "integrity": "sha512-TErFqfCtlV2xt9B6/kskROt69TPjr6AXdHpMselaRrN1X4WEjcMk9GT9PcNP7FXqL88/VYqUb3uNMiAmpDmS/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@storybook/global": "^5.0.0", - "ts-dedent": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "storybook": "8.6.18" - } - }, - "node_modules/@storybook/addon-essentials/node_modules/@storybook/addon-toolbars": { - "version": "8.6.18", - "resolved": "https://registry.npmjs.org/@storybook/addon-toolbars/-/addon-toolbars-8.6.18.tgz", - "integrity": "sha512-x037KXCEcNfPISGX485DtiP+8Bw/cOT45plcQa8eiAQVrVcUwYaDoLubE9YV5b5CsSAjX8sDviGTme6ALfq7+w==", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "storybook": "8.6.18" - } - }, - "node_modules/@storybook/addon-essentials/node_modules/@storybook/addon-viewport": { - "version": "8.6.18", - "resolved": "https://registry.npmjs.org/@storybook/addon-viewport/-/addon-viewport-8.6.18.tgz", - "integrity": "sha512-z9sDJSkuWQb4BP+Z1+H+y/Q0rFbPSDcw+OBBEhMfRcJPPXavdC2pNQ0GdQNVw+tDwhAXj+U7jehKnMDKaP7TyA==", - "dev": true, - "license": "MIT", - "dependencies": { - "memoizerific": "^1.11.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "storybook": "8.6.18" + "storybook": "^8.6.18" } }, "node_modules/@storybook/addon-links": { @@ -10299,7 +9909,7 @@ }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "storybook": "8.6.18" + "storybook": "^8.6.18" }, "peerDependenciesMeta": { "react": { @@ -10322,7 +9932,1207 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "8.6.18" + "storybook": "^8.6.18" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/mdast-util-find-and-replace": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-2.2.2.tgz", + "integrity": "sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/mdast-util-from-markdown": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", + "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "mdast-util-to-string": "^3.1.0", + "micromark": "^3.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-decode-string": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-stringify-position": "^3.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/mdast-util-gfm": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-2.0.2.tgz", + "integrity": "sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-gfm-autolink-literal": "^1.0.0", + "mdast-util-gfm-footnote": "^1.0.0", + "mdast-util-gfm-strikethrough": "^1.0.0", + "mdast-util-gfm-table": "^1.0.0", + "mdast-util-gfm-task-list-item": "^1.0.0", + "mdast-util-to-markdown": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/mdast-util-gfm-autolink-literal": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-1.0.3.tgz", + "integrity": "sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "ccount": "^2.0.0", + "mdast-util-find-and-replace": "^2.0.0", + "micromark-util-character": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/mdast-util-gfm-footnote": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-1.0.2.tgz", + "integrity": "sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0", + "micromark-util-normalize-identifier": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/mdast-util-gfm-strikethrough": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-1.0.3.tgz", + "integrity": "sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/mdast-util-gfm-table": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-1.0.7.tgz", + "integrity": "sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-to-markdown": "^1.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/mdast-util-gfm-task-list-item": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-1.0.2.tgz", + "integrity": "sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/mdast-util-phrasing": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz", + "integrity": "sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/mdast-util-to-markdown": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz", + "integrity": "sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^3.0.0", + "mdast-util-to-string": "^3.0.0", + "micromark-util-decode-string": "^1.0.0", + "unist-util-visit": "^4.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/mdast-util-to-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/micromark": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", + "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "micromark-core-commonmark": "^1.0.1", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/micromark-core-commonmark": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", + "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-factory-destination": "^1.0.0", + "micromark-factory-label": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-factory-title": "^1.0.0", + "micromark-factory-whitespace": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-html-tag-name": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/micromark-extension-gfm": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-2.0.3.tgz", + "integrity": "sha512-vb9OoHqrhCmbRidQv/2+Bc6pkP0FrtlhurxZofvOEy5o8RtuuvTq+RQ1Vw5ZDNrVraQZu3HixESqbG+0iKk/MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^1.0.0", + "micromark-extension-gfm-footnote": "^1.0.0", + "micromark-extension-gfm-strikethrough": "^1.0.0", + "micromark-extension-gfm-table": "^1.0.0", + "micromark-extension-gfm-tagfilter": "^1.0.0", + "micromark-extension-gfm-task-list-item": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/micromark-extension-gfm-autolink-literal": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-1.0.5.tgz", + "integrity": "sha512-z3wJSLrDf8kRDOh2qBtoTRD53vJ+CWIyo7uyZuxf/JAbNJjiHsOpG1y5wxk8drtv3ETAHutCu6N3thkOOgueWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/micromark-extension-gfm-footnote": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-1.1.2.tgz", + "integrity": "sha512-Yxn7z7SxgyGWRNa4wzf8AhYYWNrwl5q1Z8ii+CSTTIqVkmGZF1CElX2JI8g5yGoM3GAman9/PVCUFUSJ0kB/8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark-core-commonmark": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/micromark-extension-gfm-strikethrough": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-1.0.7.tgz", + "integrity": "sha512-sX0FawVE1o3abGk3vRjOH50L5TTLr3b5XMqnP9YDRb34M0v5OoZhG+OHFz1OffZ9dlwgpTBKaT4XW/AsUVnSDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/micromark-extension-gfm-table": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-1.0.7.tgz", + "integrity": "sha512-3ZORTHtcSnMQEKtAOsBQ9/oHp9096pI/UvdPtN7ehKvrmZZ2+bbWhi0ln+I9drmwXMt5boocn6OlwQzNXeVeqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/micromark-extension-gfm-tagfilter": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-1.0.2.tgz", + "integrity": "sha512-5XWB9GbAUSHTn8VPU8/1DBXMuKYT5uOgEjJb8gN3mW0PNW5OPHpSdojoqf+iq1xo7vWzw/P8bAHY0n6ijpXF7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/micromark-extension-gfm-task-list-item": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-1.0.5.tgz", + "integrity": "sha512-RMFXl2uQ0pNQy6Lun2YBYT9g9INXtWJULgbt01D/x8/6yJ2qpKyzdZD3pi6UIkzF++Da49xAelVKUeUMqd5eIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/micromark-factory-destination": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", + "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/micromark-factory-label": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", + "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/micromark-factory-space": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", + "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/micromark-factory-title": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", + "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/micromark-factory-whitespace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", + "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/micromark-util-character": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", + "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/micromark-util-chunked": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", + "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/micromark-util-classify-character": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", + "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/micromark-util-combine-extensions": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", + "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/micromark-util-decode-numeric-character-reference": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", + "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/micromark-util-decode-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", + "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/micromark-util-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", + "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/micromark-util-html-tag-name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", + "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/micromark-util-normalize-identifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", + "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/micromark-util-resolve-all": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", + "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/micromark-util-sanitize-uri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", + "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/micromark-util-subtokenize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", + "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/micromark-util-symbol": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", + "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/remark-gfm": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-3.0.1.tgz", + "integrity": "sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-gfm": "^2.0.0", + "micromark-extension-gfm": "^2.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@storybook/addon-mdx-gfm/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@storybook/addon-measure": { + "version": "8.6.18", + "resolved": "https://registry.npmjs.org/@storybook/addon-measure/-/addon-measure-8.6.18.tgz", + "integrity": "sha512-fMEOJXgPrTm6qHlWoRM+WTLE7Mr1QBIf2ei+pujBQFcWkD6Gjc2pV8zKzvh93d+EA13wD8AmwOq1DEw9J+XH+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "tiny-invariant": "^1.3.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.6.18" + } + }, + "node_modules/@storybook/addon-outline": { + "version": "8.6.18", + "resolved": "https://registry.npmjs.org/@storybook/addon-outline/-/addon-outline-8.6.18.tgz", + "integrity": "sha512-TErFqfCtlV2xt9B6/kskROt69TPjr6AXdHpMselaRrN1X4WEjcMk9GT9PcNP7FXqL88/VYqUb3uNMiAmpDmS/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.6.18" + } + }, + "node_modules/@storybook/addon-toolbars": { + "version": "8.6.18", + "resolved": "https://registry.npmjs.org/@storybook/addon-toolbars/-/addon-toolbars-8.6.18.tgz", + "integrity": "sha512-x037KXCEcNfPISGX485DtiP+8Bw/cOT45plcQa8eiAQVrVcUwYaDoLubE9YV5b5CsSAjX8sDviGTme6ALfq7+w==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.6.18" + } + }, + "node_modules/@storybook/addon-viewport": { + "version": "8.6.18", + "resolved": "https://registry.npmjs.org/@storybook/addon-viewport/-/addon-viewport-8.6.18.tgz", + "integrity": "sha512-z9sDJSkuWQb4BP+Z1+H+y/Q0rFbPSDcw+OBBEhMfRcJPPXavdC2pNQ0GdQNVw+tDwhAXj+U7jehKnMDKaP7TyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "memoizerific": "^1.11.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.6.18" + } + }, + "node_modules/@storybook/blocks": { + "version": "8.6.18", + "resolved": "https://registry.npmjs.org/@storybook/blocks/-/blocks-8.6.18.tgz", + "integrity": "sha512-esZv4msPQ9LxgTb8YUIZhhxVMuI6BPi5bkXtk8c7w7sWuAsqsCe/RnVInn7ooUry2gjnD4hd9+8Eqj0b8oTVoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/icons": "^1.2.12", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "storybook": "^8.6.18" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/@storybook/builder-webpack5": { + "version": "8.6.18", + "resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-8.6.18.tgz", + "integrity": "sha512-rg73TpqIUzXc66c/AaQ4kuc8yiZ+tStvy5fb1OnFYZ9rAeYQejDD0OIIaI2rqtX5XYuxC+yQEGitMntlIMV0og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/core-webpack": "8.6.18", + "@types/semver": "^7.3.4", + "browser-assert": "^1.2.1", + "case-sensitive-paths-webpack-plugin": "^2.4.0", + "cjs-module-lexer": "^1.2.3", + "constants-browserify": "^1.0.0", + "css-loader": "^6.7.1", + "es-module-lexer": "^1.5.0", + "fork-ts-checker-webpack-plugin": "^8.0.0", + "html-webpack-plugin": "^5.5.0", + "magic-string": "^0.30.5", + "path-browserify": "^1.0.1", + "process": "^0.11.10", + "semver": "^7.3.7", + "style-loader": "^3.3.1", + "terser-webpack-plugin": "^5.3.1", + "ts-dedent": "^2.0.0", + "url": "^0.11.0", + "util": "^0.12.4", + "util-deprecate": "^1.0.2", + "webpack": "5", + "webpack-dev-middleware": "^6.1.2", + "webpack-hot-middleware": "^2.25.1", + "webpack-virtual-modules": "^0.6.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.6.18" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@storybook/builder-webpack5/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@storybook/builder-webpack5/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/@storybook/builder-webpack5/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@storybook/builder-webpack5/node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@storybook/builder-webpack5/node_modules/css-loader": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", + "integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/@storybook/builder-webpack5/node_modules/fork-ts-checker-webpack-plugin": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-8.0.0.tgz", + "integrity": "sha512-mX3qW3idpueT2klaQXBzrIM/pHw+T0B/V9KHEvNrqijTq9NFnMZU6oreVxDYcf33P8a5cW+67PjodNHthGnNVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.16.7", + "chalk": "^4.1.2", + "chokidar": "^3.5.3", + "cosmiconfig": "^7.0.1", + "deepmerge": "^4.2.2", + "fs-extra": "^10.0.0", + "memfs": "^3.4.1", + "minimatch": "^3.0.4", + "node-abort-controller": "^3.0.1", + "schema-utils": "^3.1.1", + "semver": "^7.3.5", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">=12.13.0", + "yarn": ">=1.0.0" + }, + "peerDependencies": { + "typescript": ">3.6.0", + "webpack": "^5.11.0" + } + }, + "node_modules/@storybook/builder-webpack5/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@storybook/builder-webpack5/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@storybook/builder-webpack5/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@storybook/builder-webpack5/node_modules/style-loader": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", + "integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/@storybook/builder-webpack5/node_modules/yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" } }, "node_modules/@storybook/components": { @@ -10412,6 +11222,23 @@ "type-fest": "^2.19.0" } }, + "node_modules/@storybook/csf-plugin": { + "version": "8.6.18", + "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-8.6.18.tgz", + "integrity": "sha512-x1ioz/L0CwaelCkHci3P31YtvwayN3FBftvwQOPbvRh9qeb4Cpz5IdVDmyvSxxYwXN66uAORNoqgjTi7B4/y5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unplugin": "^1.3.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.6.18" + } + }, "node_modules/@storybook/csf-tools": { "version": "8.6.14", "resolved": "https://registry.npmjs.org/@storybook/csf-tools/-/csf-tools-8.6.14.tgz", @@ -10447,6 +11274,24 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta" } }, + "node_modules/@storybook/instrumenter": { + "version": "8.6.18", + "resolved": "https://registry.npmjs.org/@storybook/instrumenter/-/instrumenter-8.6.18.tgz", + "integrity": "sha512-viEC1BGlYyjAzi1Tv3LZjByh7Y3Oh04u6QKsujxdeUbr5rUOH4pa/wCKmxXmY6yWrD4WjcNtojmUvQZN/66FXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "@vitest/utils": "^2.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.6.18" + } + }, "node_modules/@storybook/manager-api": { "version": "8.6.18", "resolved": "https://registry.npmjs.org/@storybook/manager-api/-/manager-api-8.6.18.tgz", @@ -10461,6 +11306,43 @@ "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" } }, + "node_modules/@storybook/preset-react-webpack": { + "version": "8.6.18", + "resolved": "https://registry.npmjs.org/@storybook/preset-react-webpack/-/preset-react-webpack-8.6.18.tgz", + "integrity": "sha512-UkioZsLIyKGQTAdVB3EMx4NyqwIPDRyuDTIQyCwlMcLYCJCs9Ks2ILbM1x1554/iqRIxy8Yv2IBMapK+euCwgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/core-webpack": "8.6.18", + "@storybook/react": "8.6.18", + "@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.0c3f3b7.0", + "@types/semver": "^7.3.4", + "find-up": "^5.0.0", + "magic-string": "^0.30.5", + "react-docgen": "^7.0.0", + "resolve": "^1.22.8", + "semver": "^7.3.7", + "tsconfig-paths": "^4.2.0", + "webpack": "5" + }, + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "storybook": "^8.6.18" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/@storybook/preview-api": { "version": "8.6.18", "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-8.6.18.tgz", @@ -10500,7 +11382,7 @@ "@storybook/test": "8.6.18", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "storybook": "8.6.18", + "storybook": "^8.6.18", "typescript": ">= 4.2.x" }, "peerDependenciesMeta": { @@ -10613,7 +11495,7 @@ "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "storybook": "8.6.18", + "storybook": "^8.6.18", "typescript": ">= 4.2.x" }, "peerDependenciesMeta": { @@ -10622,282 +11504,6 @@ } } }, - "node_modules/@storybook/react-webpack5/node_modules/@storybook/builder-webpack5": { - "version": "8.6.18", - "resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-8.6.18.tgz", - "integrity": "sha512-rg73TpqIUzXc66c/AaQ4kuc8yiZ+tStvy5fb1OnFYZ9rAeYQejDD0OIIaI2rqtX5XYuxC+yQEGitMntlIMV0og==", - "dev": true, - "license": "MIT", - "dependencies": { - "@storybook/core-webpack": "8.6.18", - "@types/semver": "^7.3.4", - "browser-assert": "^1.2.1", - "case-sensitive-paths-webpack-plugin": "^2.4.0", - "cjs-module-lexer": "^1.2.3", - "constants-browserify": "^1.0.0", - "css-loader": "^6.7.1", - "es-module-lexer": "^1.5.0", - "fork-ts-checker-webpack-plugin": "^8.0.0", - "html-webpack-plugin": "^5.5.0", - "magic-string": "^0.30.5", - "path-browserify": "^1.0.1", - "process": "^0.11.10", - "semver": "^7.3.7", - "style-loader": "^3.3.1", - "terser-webpack-plugin": "^5.3.1", - "ts-dedent": "^2.0.0", - "url": "^0.11.0", - "util": "^0.12.4", - "util-deprecate": "^1.0.2", - "webpack": "5", - "webpack-dev-middleware": "^6.1.2", - "webpack-hot-middleware": "^2.25.1", - "webpack-virtual-modules": "^0.6.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "storybook": "8.6.18" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@storybook/react-webpack5/node_modules/@storybook/preset-react-webpack": { - "version": "8.6.18", - "resolved": "https://registry.npmjs.org/@storybook/preset-react-webpack/-/preset-react-webpack-8.6.18.tgz", - "integrity": "sha512-UkioZsLIyKGQTAdVB3EMx4NyqwIPDRyuDTIQyCwlMcLYCJCs9Ks2ILbM1x1554/iqRIxy8Yv2IBMapK+euCwgg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@storybook/core-webpack": "8.6.18", - "@storybook/react": "8.6.18", - "@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.0c3f3b7.0", - "@types/semver": "^7.3.4", - "find-up": "^5.0.0", - "magic-string": "^0.30.5", - "react-docgen": "^7.0.0", - "resolve": "^1.22.8", - "semver": "^7.3.7", - "tsconfig-paths": "^4.2.0", - "webpack": "5" - }, - "engines": { - "node": ">=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "storybook": "8.6.18" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@storybook/react-webpack5/node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@storybook/react-webpack5/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/@storybook/react-webpack5/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@storybook/react-webpack5/node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@storybook/react-webpack5/node_modules/css-loader": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", - "integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==", - "dev": true, - "license": "MIT", - "dependencies": { - "icss-utils": "^5.1.0", - "postcss": "^8.4.33", - "postcss-modules-extract-imports": "^3.1.0", - "postcss-modules-local-by-default": "^4.0.5", - "postcss-modules-scope": "^3.2.0", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/@storybook/react-webpack5/node_modules/fork-ts-checker-webpack-plugin": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-8.0.0.tgz", - "integrity": "sha512-mX3qW3idpueT2klaQXBzrIM/pHw+T0B/V9KHEvNrqijTq9NFnMZU6oreVxDYcf33P8a5cW+67PjodNHthGnNVg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.16.7", - "chalk": "^4.1.2", - "chokidar": "^3.5.3", - "cosmiconfig": "^7.0.1", - "deepmerge": "^4.2.2", - "fs-extra": "^10.0.0", - "memfs": "^3.4.1", - "minimatch": "^3.0.4", - "node-abort-controller": "^3.0.1", - "schema-utils": "^3.1.1", - "semver": "^7.3.5", - "tapable": "^2.2.1" - }, - "engines": { - "node": ">=12.13.0", - "yarn": ">=1.0.0" - }, - "peerDependencies": { - "typescript": ">3.6.0", - "webpack": "^5.11.0" - } - }, - "node_modules/@storybook/react-webpack5/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@storybook/react-webpack5/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@storybook/react-webpack5/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/@storybook/react-webpack5/node_modules/style-loader": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", - "integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, - "node_modules/@storybook/react-webpack5/node_modules/yaml": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", - "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, "node_modules/@storybook/test": { "version": "8.6.18", "resolved": "https://registry.npmjs.org/@storybook/test/-/test-8.6.18.tgz", @@ -10918,7 +11524,7 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "8.6.18" + "storybook": "^8.6.18" } }, "node_modules/@storybook/test-runner": { @@ -11326,10 +11932,22 @@ "node": ">=8" } }, + "node_modules/@storybook/test-runner/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, "node_modules/@storybook/test-runner/node_modules/dedent": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", - "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", + "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -11341,6 +11959,52 @@ } } }, + "node_modules/@storybook/test-runner/node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/@storybook/test-runner/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@storybook/test-runner/node_modules/foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@storybook/test-runner/node_modules/jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", @@ -11846,13 +12510,13 @@ } }, "node_modules/@storybook/test-runner/node_modules/jest-watch-typeahead/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.2.2" + "ansi-regex": "^6.0.1" }, "engines": { "node": ">=12" @@ -11881,6 +12545,185 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@storybook/test-runner/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@storybook/test-runner/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@storybook/test-runner/node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@storybook/test-runner/node_modules/nyc": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", + "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "caching-transform": "^4.0.0", + "convert-source-map": "^1.7.0", + "decamelize": "^1.2.0", + "find-cache-dir": "^3.2.0", + "find-up": "^4.1.0", + "foreground-child": "^2.0.0", + "get-package-type": "^0.1.0", + "glob": "^7.1.6", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-hook": "^3.0.0", + "istanbul-lib-instrument": "^4.0.0", + "istanbul-lib-processinfo": "^2.0.2", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "make-dir": "^3.0.0", + "node-preload": "^0.2.1", + "p-map": "^3.0.0", + "process-on-spawn": "^1.0.0", + "resolve-from": "^5.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "spawn-wrap": "^2.0.0", + "test-exclude": "^6.0.0", + "yargs": "^15.0.2" + }, + "bin": { + "nyc": "bin/nyc.js" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/@storybook/test-runner/node_modules/nyc/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@storybook/test-runner/node_modules/nyc/node_modules/istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@storybook/test-runner/node_modules/nyc/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@storybook/test-runner/node_modules/nyc/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@storybook/test-runner/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@storybook/test-runner/node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@storybook/test-runner/node_modules/p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@storybook/test-runner/node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -11926,6 +12769,23 @@ ], "license": "MIT" }, + "node_modules/@storybook/test-runner/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@storybook/test-runner/node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -11981,6 +12841,13 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/@storybook/test-runner/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true, + "license": "ISC" + }, "node_modules/@storybook/test-runner/node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -12000,22 +12867,81 @@ "node": ">=12" } }, - "node_modules/@storybook/test/node_modules/@storybook/instrumenter": { - "version": "8.6.18", - "resolved": "https://registry.npmjs.org/@storybook/instrumenter/-/instrumenter-8.6.18.tgz", - "integrity": "sha512-viEC1BGlYyjAzi1Tv3LZjByh7Y3Oh04u6QKsujxdeUbr5rUOH4pa/wCKmxXmY6yWrD4WjcNtojmUvQZN/66FXQ==", + "node_modules/@storybook/test-runner/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@storybook/test-runner/node_modules/yargs-parser/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@storybook/test-runner/node_modules/yargs/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@storybook/test-runner/node_modules/yargs/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/global": "^5.0.0", - "@vitest/utils": "^2.1.1" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "storybook": "8.6.18" + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@storybook/test-runner/node_modules/yargs/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/@storybook/test-runner/node_modules/yargs/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" } }, "node_modules/@storybook/test/node_modules/@testing-library/dom": { @@ -12133,6 +13059,10 @@ "resolved": "packages/generator-superset", "link": true }, + "node_modules/@superset-ui/glyph-core": { + "resolved": "packages/superset-ui-glyph-core", + "link": true + }, "node_modules/@superset-ui/legacy-plugin-chart-calendar": { "resolved": "plugins/legacy-plugin-chart-calendar", "link": true @@ -12488,7 +13418,7 @@ "version": "1.15.33", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.33.tgz", "integrity": "sha512-jOlwnFV2xhuuZeAUILGFULeR6vDPfijEJ57evfocwznQldLU3w2cZ9bSDryY9ip+AsM3r1NJKzf47V2NXebkeQ==", - "devOptional": true, + "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -12532,6 +13462,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -12548,6 +13479,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -12564,6 +13496,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -12580,6 +13513,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -12596,6 +13530,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -12612,6 +13547,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -12628,6 +13564,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -12644,6 +13581,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -12660,6 +13598,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -12676,6 +13615,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -12692,6 +13632,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -12708,6 +13649,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -12721,7 +13663,7 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "devOptional": true, + "dev": true, "license": "Apache-2.0" }, "node_modules/@swc/jest": { @@ -12766,7 +13708,7 @@ "version": "0.1.26", "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.26.tgz", "integrity": "sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3" @@ -12776,6 +13718,7 @@ "version": "9.3.4", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", "integrity": "sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.10.4", @@ -12795,6 +13738,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -12811,6 +13755,7 @@ "version": "6.9.1", "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, "license": "MIT", "dependencies": { "@adobe/css-tools": "^4.4.0", @@ -12830,12 +13775,14 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, "license": "MIT" }, "node_modules/@testing-library/react": { "version": "14.3.1", "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.3.1.tgz", "integrity": "sha512-H99XjUhWQw0lTgyMN05W3xQG1Nh4lq574D8keFf1dDoNTJgp66VbJozRaczoF+wsiaPJNt/TcnfpLGufGxSrZQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.5", @@ -12854,6 +13801,7 @@ "version": "12.8.3", "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-12.8.3.tgz", "integrity": "sha512-IR0iWbFkgd56Bu5ZI/ej8yQwrkCv8Qydx6RzwbKz9faXazR/+5tvYKsZQgyXJiwgpcva127YO6JcWy7YlCfofQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.5" @@ -12902,9 +13850,9 @@ } }, "node_modules/@tsconfig/node10": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", - "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", "license": "MIT" }, "node_modules/@tsconfig/node12": { @@ -12960,9 +13908,9 @@ } }, "node_modules/@tufjs/models/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -12998,6 +13946,21 @@ "@turf/invariant": "^5.1.5" } }, + "node_modules/@turf/boolean-clockwise/node_modules/@turf/helpers": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-5.1.5.tgz", + "integrity": "sha512-/lF+JR+qNDHZ8bF9d+Cp58nxtZWJ3sqFe6n3u3Vpj+/0cqkjk4nXKYBSY0azm+GIYB5mWKxUXvuP/m0ZnKj1bw==", + "license": "MIT" + }, + "node_modules/@turf/boolean-clockwise/node_modules/@turf/invariant": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-5.2.0.tgz", + "integrity": "sha512-28RCBGvCYsajVkw2EydpzLdcYyhSA77LovuOvgCJplJWaNVyJYH6BOR3HR9w50MEkPqb/Vc/jdo6I6ermlRtQA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^5.1.5" + } + }, "node_modules/@turf/clone": { "version": "5.1.5", "resolved": "https://registry.npmjs.org/@turf/clone/-/clone-5.1.5.tgz", @@ -13007,21 +13970,12 @@ "@turf/helpers": "^5.1.5" } }, - "node_modules/@turf/helpers": { + "node_modules/@turf/clone/node_modules/@turf/helpers": { "version": "5.1.5", "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-5.1.5.tgz", "integrity": "sha512-/lF+JR+qNDHZ8bF9d+Cp58nxtZWJ3sqFe6n3u3Vpj+/0cqkjk4nXKYBSY0azm+GIYB5mWKxUXvuP/m0ZnKj1bw==", "license": "MIT" }, - "node_modules/@turf/invariant": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-5.2.0.tgz", - "integrity": "sha512-28RCBGvCYsajVkw2EydpzLdcYyhSA77LovuOvgCJplJWaNVyJYH6BOR3HR9w50MEkPqb/Vc/jdo6I6ermlRtQA==", - "license": "MIT", - "dependencies": { - "@turf/helpers": "^5.1.5" - } - }, "node_modules/@turf/rewind": { "version": "5.1.5", "resolved": "https://registry.npmjs.org/@turf/rewind/-/rewind-5.1.5.tgz", @@ -13035,6 +13989,21 @@ "@turf/meta": "^5.1.5" } }, + "node_modules/@turf/rewind/node_modules/@turf/helpers": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-5.1.5.tgz", + "integrity": "sha512-/lF+JR+qNDHZ8bF9d+Cp58nxtZWJ3sqFe6n3u3Vpj+/0cqkjk4nXKYBSY0azm+GIYB5mWKxUXvuP/m0ZnKj1bw==", + "license": "MIT" + }, + "node_modules/@turf/rewind/node_modules/@turf/invariant": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-5.2.0.tgz", + "integrity": "sha512-28RCBGvCYsajVkw2EydpzLdcYyhSA77LovuOvgCJplJWaNVyJYH6BOR3HR9w50MEkPqb/Vc/jdo6I6ermlRtQA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^5.1.5" + } + }, "node_modules/@turf/rewind/node_modules/@turf/meta": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@turf/meta/-/meta-5.2.0.tgz", @@ -13058,6 +14027,7 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, "license": "MIT" }, "node_modules/@types/babel__core": { @@ -13200,9 +14170,10 @@ "license": "MIT" }, "node_modules/@types/d3": { - "version": "3.5.38", - "resolved": "https://registry.npmjs.org/@types/d3/-/d3-3.5.38.tgz", - "integrity": "sha512-O/gRkjWULp3xVX8K85V0H3tsSGole0WYt77KVpGZO2xTGLuVFuvE6JIsIli3fvFHCYBhGFn/8OHEEyMYF+QehA==", + "version": "3.5.53", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-3.5.53.tgz", + "integrity": "sha512-8yKQA9cAS6+wGsJpBysmnhlaaxlN42Qizqkw+h2nILSlS+MAG2z4JdO6p+PJrJ+ACvimkmLJL281h157e52psQ==", + "dev": true, "license": "MIT" }, "node_modules/@types/d3-array": { @@ -13222,9 +14193,10 @@ } }, "node_modules/@types/d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "dev": true, "license": "MIT" }, "node_modules/@types/d3-delaunay": { @@ -13315,9 +14287,9 @@ "license": "MIT" }, "node_modules/@types/debug": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", - "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", "license": "MIT", "dependencies": { "@types/ms": "*" @@ -13366,19 +14338,12 @@ } }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, "license": "MIT" }, - "node_modules/@types/expect": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/@types/expect/-/expect-1.20.4.tgz", - "integrity": "sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg==", - "license": "MIT", - "peer": true - }, "node_modules/@types/express": { "version": "4.17.25", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", @@ -13532,9 +14497,9 @@ } }, "node_modules/@types/jest/node_modules/@jest/expect-utils": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.3.0.tgz", - "integrity": "sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", + "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", "dev": true, "license": "MIT", "dependencies": { @@ -13558,9 +14523,9 @@ } }, "node_modules/@types/jest/node_modules/@jest/types": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", - "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", "dev": true, "license": "MIT", "dependencies": { @@ -13577,9 +14542,9 @@ } }, "node_modules/@types/jest/node_modules/@sinclair/typebox": { - "version": "0.34.49", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", - "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", "dev": true, "license": "MIT" }, @@ -13601,69 +14566,69 @@ } }, "node_modules/@types/jest/node_modules/expect": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.3.0.tgz", - "integrity": "sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/expect-utils": "30.3.0", + "@jest/expect-utils": "30.2.0", "@jest/get-type": "30.1.0", - "jest-matcher-utils": "30.3.0", - "jest-message-util": "30.3.0", - "jest-mock": "30.3.0", - "jest-util": "30.3.0" + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@types/jest/node_modules/jest-diff": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz", - "integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", + "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", "dev": true, "license": "MIT", "dependencies": { - "@jest/diff-sequences": "30.3.0", + "@jest/diff-sequences": "30.0.1", "@jest/get-type": "30.1.0", "chalk": "^4.1.2", - "pretty-format": "30.3.0" + "pretty-format": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@types/jest/node_modules/jest-matcher-utils": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.3.0.tgz", - "integrity": "sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", + "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", "chalk": "^4.1.2", - "jest-diff": "30.3.0", - "pretty-format": "30.3.0" + "jest-diff": "30.2.0", + "pretty-format": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@types/jest/node_modules/jest-message-util": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz", - "integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", + "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@jest/types": "30.3.0", + "@jest/types": "30.2.0", "@types/stack-utils": "^2.0.3", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", - "picomatch": "^4.0.3", - "pretty-format": "30.3.0", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", "slash": "^3.0.0", "stack-utils": "^2.0.6" }, @@ -13672,42 +14637,42 @@ } }, "node_modules/@types/jest/node_modules/jest-mock": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.3.0.tgz", - "integrity": "sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", + "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.3.0", + "@jest/types": "30.2.0", "@types/node": "*", - "jest-util": "30.3.0" + "jest-util": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@types/jest/node_modules/jest-util": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz", - "integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", + "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.3.0", + "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", - "picomatch": "^4.0.3" + "picomatch": "^4.0.2" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@types/jest/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -13718,9 +14683,9 @@ } }, "node_modules/@types/jest/node_modules/pretty-format": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", - "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", "dev": true, "license": "MIT", "dependencies": { @@ -13876,12 +14841,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.7.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.7.0.tgz", - "integrity": "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==", + "version": "25.8.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.8.0.tgz", + "integrity": "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==", "license": "MIT", "dependencies": { - "undici-types": "~7.21.0" + "undici-types": ">=7.24.0 <7.24.7" } }, "node_modules/@types/normalize-package-data": { @@ -13927,9 +14892,9 @@ "license": "MIT" }, "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "version": "15.7.14", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", + "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", "license": "MIT" }, "node_modules/@types/qs": { @@ -13991,6 +14956,7 @@ "version": "5.5.11", "resolved": "https://registry.npmjs.org/@types/react-loadable/-/react-loadable-5.5.11.tgz", "integrity": "sha512-/tq2IJ853MoIFRBmqVOxnGsRRjER5TmEKzsZtaAkiXAWoDeKgR/QNOT1vd9k0p9h/F616X21cpNh3hu4RutzRQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/react": "*", @@ -14001,6 +14967,7 @@ "version": "4.41.40", "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.40.tgz", "integrity": "sha512-u6kMFSBM9HcoTpUXnL6mt2HSzftqb3JgYV6oxIgL2dl6sX6aCa5k6SOkzv5DuZjBTPUE/dJltKtwwuqrkZHpfw==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -14015,6 +14982,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -14096,6 +15064,7 @@ "version": "1.8.8", "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.8.tgz", "integrity": "sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==", + "dev": true, "license": "MIT", "dependencies": { "@types/react": "*" @@ -14209,6 +15178,7 @@ "version": "0.1.6", "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.6.tgz", "integrity": "sha512-5JcVt1u5HDmlXkwOD2nslZVllBBc7HDuOICfiZah2Z0is8M8g+ddAEawbmd3VjedfDHBzxCaXLs07QEmb7y54g==", + "dev": true, "license": "MIT" }, "node_modules/@types/stack-utils": { @@ -14231,12 +15201,14 @@ "version": "1.0.12", "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.12.tgz", "integrity": "sha512-bTHG8fcxEqv1M9+TD14P8ok8hjxoOCkfKc8XXLaaD05kI7ohpeI956jtDOD3XHKBQrlyPughUtzm1jtVhHpA5Q==", + "dev": true, "license": "MIT" }, "node_modules/@types/tinycolor2": { "version": "1.4.6", "resolved": "https://registry.npmjs.org/@types/tinycolor2/-/tinycolor2-1.4.6.tgz", "integrity": "sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==", + "dev": true, "license": "MIT" }, "node_modules/@types/tough-cookie": { @@ -14257,6 +15229,7 @@ "version": "3.17.5", "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.17.5.tgz", "integrity": "sha512-TU+fZFBTBcXj/GpDpDaBmgWk/gn96kMZ+uocaFUlV2f8a6WdMzzI44QBCmGcCiYR0Y6ZlNRiyUyKKt5nl/lbzQ==", + "dev": true, "license": "MIT", "dependencies": { "source-map": "^0.6.1" @@ -14266,6 +15239,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -14308,17 +15282,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/vinyl": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/@types/vinyl/-/vinyl-2.0.12.tgz", - "integrity": "sha512-Sr2fYMBUVGYq8kj3UthXFAu5UN6ZW+rYr4NACjZQJvHvj+c8lYv0CahmZ2P/r7iUkN44gGUBwqxZkrKXYPb7cw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/expect": "^1.20.4", - "@types/node": "*" - } - }, "node_modules/@types/wait-on": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/@types/wait-on/-/wait-on-5.3.4.tgz", @@ -14333,6 +15296,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-3.2.3.tgz", "integrity": "sha512-4nZOdMwSPHZ4pTEZzSp0AsTM4K7Qmu40UKW4tJDiOVs20UzYF9l+qUe4s0ftfN0pin06n+5cWWDJXH+sbhAiDw==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -14784,14 +15748,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.1.tgz", - "integrity": "sha512-+MuHQlHiEr00Of/IQbE/MmEoi44znZHbR/Pz7Opq4HryUOlRi+/44dro9Ycy8Fyo+/024IWtw8m4JUMCGTYxDg==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.0.tgz", + "integrity": "sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.59.1", - "@typescript-eslint/types": "^8.59.1", + "@typescript-eslint/tsconfig-utils": "^8.56.0", + "@typescript-eslint/types": "^8.56.0", "debug": "^4.4.3" }, "engines": { @@ -14802,13 +15766,13 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/project-service/node_modules/@typescript-eslint/types": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.1.tgz", - "integrity": "sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz", + "integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==", "dev": true, "license": "MIT", "engines": { @@ -14838,9 +15802,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.1.tgz", - "integrity": "sha512-/0nEyPbX7gRsk0Uwfe4ALwwgxuA66d/l2mhRDNlAvaj4U3juhUtJNq0DsY8M2AYwwb9rEq2hrC3IcIcEt++iJA==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz", + "integrity": "sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==", "dev": true, "license": "MIT", "engines": { @@ -14851,7 +15815,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/type-utils": { @@ -15412,27 +16376,16 @@ } }, "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", - "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.11.tgz", + "integrity": "sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==", "dev": true, "license": "MIT", "optional": true, "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.10.0" - } - }, - "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" + "@tybys/wasm-util": "^0.9.0" } }, "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { @@ -15478,9 +16431,9 @@ ] }, "node_modules/@vis.gl/react-mapbox": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@vis.gl/react-mapbox/-/react-mapbox-8.1.0.tgz", - "integrity": "sha512-FwvH822oxEjWYOr+pP2L8hpv+7cZB2UsQbHHHT0ryrkvvqzmTgt7qHDhamv0EobKw86e1I+B4ojENdJ5G5BkyQ==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@vis.gl/react-mapbox/-/react-mapbox-8.1.1.tgz", + "integrity": "sha512-KMDTjtWESXxHS4uqWxjsvgQUHvuL3Z6SdKe68o7Nxma2qUfuyH3x4TCkIqGn3FQTrFvZLWvTnSAbGvtm+Kd13A==", "license": "MIT", "peerDependencies": { "mapbox-gl": ">=3.5.0", @@ -15494,9 +16447,9 @@ } }, "node_modules/@vis.gl/react-maplibre": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@vis.gl/react-maplibre/-/react-maplibre-8.1.0.tgz", - "integrity": "sha512-PkAK/gp3mUfhCLhUuc+4gc3PN9zCtVGxTF2hB6R5R5yYUw+hdg84OZ770U5MU4tPMTCG6fbduExuIW6RRKN6qQ==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@vis.gl/react-maplibre/-/react-maplibre-8.1.1.tgz", + "integrity": "sha512-iUOfzJAhFAJwEZp1644tQb7LOTFgi5/GzdaztkhzNgFVuoF2Ez7guvwZjQAKB9CN2TlHTgNuYH8UW85kO7cVhw==", "license": "MIT", "dependencies": { "@maplibre/maplibre-gl-style-spec": "^19.2.1" @@ -15686,25 +16639,6 @@ "integrity": "sha512-I6UrHoJAEVbx3RORQNupgTiX5EzjuZpiwLPxn8L2mI5nfERotPKi1Yus12Cq2WtXqEBR/WgqTnoImlqOXBykcA==", "license": "MIT" }, - "node_modules/@visx/react-spring": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@visx/react-spring/-/react-spring-3.12.0.tgz", - "integrity": "sha512-ehtmjFrUQx3g0mZ684M4LgI9UfQ84ZWD/m9tKfvXhEm1Vl8D4AjaZ4af1tTOg9S7vk2VlpxvVOVN7+t5pu0nSA==", - "license": "MIT", - "dependencies": { - "@types/react": "*", - "@visx/axis": "3.12.0", - "@visx/grid": "3.12.0", - "@visx/scale": "3.12.0", - "@visx/text": "3.12.0", - "classnames": "^2.3.1", - "prop-types": "^15.6.2" - }, - "peerDependencies": { - "@react-spring/web": "^9.4.5", - "react": "^16.3.0-0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/@visx/responsive": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/@visx/responsive/-/responsive-3.12.0.tgz", @@ -15819,6 +16753,12 @@ "integrity": "sha512-Reoy+pKnvsksN0lQUlcH6dOGjRZ/3WRwXR//m+/8lt1BXeI4xyaUZoqULNjyXXRuh0Mj4LNpkCvhUpQlY3X5xQ==", "license": "MIT" }, + "node_modules/@visx/vendor/node_modules/@types/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==", + "license": "MIT" + }, "node_modules/@visx/vendor/node_modules/@types/d3-format": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.1.tgz", @@ -15855,6 +16795,27 @@ "integrity": "sha512-/myT3I7EwlukNOX2xVdMzb8FRgNzRMpsZddwst9Ld/VFe6LyJyRp0s32l/V9XoUzk+Gqu56F/oGk6507+8BxrA==", "license": "MIT" }, + "node_modules/@visx/vendor/node_modules/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-gUY/qeHq/yNqqoCKNq4vtpFLdoCdvyNpWoC/KNjhGbhDuQpAM9sIQQKkXSNpXa9h5KySs/gzm7R88WkUutgwWQ==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@visx/vendor/node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/@visx/voronoi": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/@visx/voronoi/-/voronoi-3.12.0.tgz", @@ -15904,6 +16865,25 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/@visx/xychart/node_modules/@visx/react-spring": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@visx/react-spring/-/react-spring-3.12.0.tgz", + "integrity": "sha512-ehtmjFrUQx3g0mZ684M4LgI9UfQ84ZWD/m9tKfvXhEm1Vl8D4AjaZ4af1tTOg9S7vk2VlpxvVOVN7+t5pu0nSA==", + "license": "MIT", + "dependencies": { + "@types/react": "*", + "@visx/axis": "3.12.0", + "@visx/grid": "3.12.0", + "@visx/scale": "3.12.0", + "@visx/text": "3.12.0", + "classnames": "^2.3.1", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "@react-spring/web": "^9.4.5", + "react": "^16.3.0-0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@visx/xychart/node_modules/d3-shape": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-2.1.0.tgz", @@ -16228,52 +17208,6 @@ "dev": true, "license": "BSD-2-Clause" }, - "node_modules/@yarnpkg/parsers": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@yarnpkg/parsers/-/parsers-3.0.2.tgz", - "integrity": "sha512-/HcYgtUSiJiot/XWGLOlGxPYUG65+/31V8oqk17vZLW1xlCoR4PampyePljOxY2n8/3jz9+tIFzICsyGujJZoA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "js-yaml": "^3.10.0", - "tslib": "^2.4.0" - }, - "engines": { - "node": ">=18.12.0" - } - }, - "node_modules/@yeoman/namespace": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@yeoman/namespace/-/namespace-1.0.1.tgz", - "integrity": "sha512-XGdYL0HCoPvrzW7T8bxD6RbCY/B8uvR2jpOzJc/yEwTueKHwoVhjSLjVXkokQAO0LNl8nQFLVZ1aKfr2eFWZeA==", - "license": "MIT", - "engines": { - "node": "^16.13.0 || >=18.12.0" - } - }, - "node_modules/@yeoman/types": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@yeoman/types/-/types-1.11.1.tgz", - "integrity": "sha512-27CI5hHQAHfq8ohYILmLNzClbdzBJzu+ny9AzUVV6naJO0l4/+t+67QDKlwQvt+TW3oE5j74I/Mh4Kn14rsVXA==", - "license": "MIT", - "peer": true, - "engines": { - "node": "^16.13.0 || >=18.12.0" - }, - "peerDependencies": { - "@types/node": ">=16.18.26", - "@yeoman/adapter": "^1.6.0 || ^2.0.0-beta.0 || ^3.0.0 || ^4.0.0", - "mem-fs": "^3.0.0 || ^4.0.0-beta.1" - }, - "peerDependenciesMeta": { - "@yeoman/adapter": { - "optional": true - }, - "mem-fs": { - "optional": true - } - } - }, "node_modules/@zarrita/storage": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@zarrita/storage/-/storage-0.2.0.tgz", @@ -16345,12 +17279,6 @@ "node": ">= 0.6" } }, - "node_modules/ace-builds": { - "version": "1.44.0", - "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.44.0.tgz", - "integrity": "sha512-PFNMSYqFdEUkul2Ntud0HvA09AgY+F1ag0UYdpMH60wNI/qOA8cB8tlTgoALMEwIdUPJK2CjrIQ7OnbiSS/ugQ==", - "license": "BSD-3-Clause" - }, "node_modules/acorn": { "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", @@ -16363,6 +17291,19 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -16461,9 +17402,9 @@ } }, "node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -16569,6 +17510,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -16578,6 +17520,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -16653,10 +17596,26 @@ "react-dom": ">=16.9.0" } }, + "node_modules/antd/node_modules/rc-input": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.8.0.tgz", + "integrity": "sha512-KXvaTbX+7ha8a/k+eg6SYRVERK0NddX8QX7a7AnRvUa/rEH0CNMlpcBzBkhI0wp2C8C4HlMoYl8TImSN+fuHKA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-util": "^5.18.1" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -16712,6 +17671,7 @@ "version": "5.1.3", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dev": true, "license": "Apache-2.0", "dependencies": { "deep-equal": "^2.0.5" @@ -16965,6 +17925,7 @@ "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, "license": "MIT" }, "node_modules/async-function": { @@ -16999,17 +17960,39 @@ } }, "node_modules/axios": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", - "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz", + "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==", "dev": true, "license": "MIT", "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, + "node_modules/axios/node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/axios/node_modules/proxy-from-env": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", @@ -17200,9 +18183,9 @@ } }, "node_modules/babel-plugin-macros/node_modules/yaml": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", - "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", "license": "ISC", "engines": { "node": ">= 6" @@ -17667,9 +18650,10 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -17942,9 +18926,9 @@ } }, "node_modules/cacache/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -17973,9 +18957,9 @@ } }, "node_modules/cacache/node_modules/lru-cache": { - "version": "11.3.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", - "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", + "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -18552,9 +19536,9 @@ } }, "node_modules/cjs-module-lexer": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", - "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz", + "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==", "dev": true, "license": "MIT" }, @@ -18637,6 +19621,7 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -18651,6 +19636,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -18951,6 +19937,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, "license": "MIT" }, "node_modules/concat-stream": { @@ -19398,9 +20385,9 @@ } }, "node_modules/core-js": { - "version": "3.49.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", - "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.40.0.tgz", + "integrity": "sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==", "hasInstallScript": true, "license": "MIT", "funding": { @@ -19747,9 +20734,9 @@ } }, "node_modules/create-jest/node_modules/dedent": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", - "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", + "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -20075,9 +21062,9 @@ } }, "node_modules/create-jest/node_modules/jest-snapshot/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true, "license": "ISC", "bin": { @@ -20425,9 +21412,9 @@ } }, "node_modules/css-minimizer-webpack-plugin/node_modules/@jest/types": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", - "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", "dev": true, "license": "MIT", "dependencies": { @@ -20444,9 +21431,9 @@ } }, "node_modules/css-minimizer-webpack-plugin/node_modules/@sinclair/typebox": { - "version": "0.34.49", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", - "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", "dev": true, "license": "MIT" }, @@ -20481,33 +21468,33 @@ } }, "node_modules/css-minimizer-webpack-plugin/node_modules/jest-util": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz", - "integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", + "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.3.0", + "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", - "picomatch": "^4.0.3" + "picomatch": "^4.0.2" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/css-minimizer-webpack-plugin/node_modules/jest-worker": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.3.0.tgz", - "integrity": "sha512-DrCKkaQwHexjRUFTmPzs7sHQe0TSj9nvDALKGdwmK5mW9v7j90BudWirKAJHt3QQ9Dhrg1F7DogPzhChppkJpQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", + "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", "@ungap/structured-clone": "^1.3.0", - "jest-util": "30.3.0", + "jest-util": "30.2.0", "merge-stream": "^2.0.0", "supports-color": "^8.1.1" }, @@ -20516,9 +21503,9 @@ } }, "node_modules/css-minimizer-webpack-plugin/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -20545,9 +21532,9 @@ } }, "node_modules/css-select": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", - "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -20575,6 +21562,13 @@ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" } }, + "node_modules/css-tree/node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/css-what": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", @@ -20592,6 +21586,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, "license": "MIT" }, "node_modules/csscolorparser": { @@ -20734,25 +21729,12 @@ "dev": true, "license": "CC0-1.0" }, - "node_modules/cssom": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", - "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", - "dev": true, - "license": "MIT" - }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, - "node_modules/currencyformatter.js": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/currencyformatter.js/-/currencyformatter.js-1.0.5.tgz", - "integrity": "sha512-gNhjgPges50sAHOb56BeEOi33w88sED2nSiY0s9niq1S/64IKB8DB1EmJh8wv5PofFXpHWG91yptoDQAj5GI2w==", - "license": "MIT" - }, "node_modules/cwd": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/cwd/-/cwd-0.10.0.tgz", @@ -20774,17 +21756,20 @@ "license": "BSD-3-Clause" }, "node_modules/d3-array": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.1.tgz", - "integrity": "sha512-gUY/qeHq/yNqqoCKNq4vtpFLdoCdvyNpWoC/KNjhGbhDuQpAM9sIQQKkXSNpXa9h5KySs/gzm7R88WkUutgwWQ==", - "license": "ISC", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", "dependencies": { - "internmap": "1 - 2" - }, - "engines": { - "node": ">=12" + "internmap": "^1.0.0" } }, + "node_modules/d3-array/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, "node_modules/d3-collection": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz", @@ -20819,13 +21804,10 @@ "license": "BSD-3-Clause" }, "node_modules/d3-format": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", - "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", - "license": "ISC", - "engines": { - "node": ">=12" - } + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz", + "integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==", + "license": "BSD-3-Clause" }, "node_modules/d3-geo": { "version": "3.1.0", @@ -21069,6 +22051,12 @@ "topojson": "^1.6.19" } }, + "node_modules/datamaps/node_modules/@types/d3": { + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-3.5.38.tgz", + "integrity": "sha512-O/gRkjWULp3xVX8K85V0H3tsSGole0WYt77KVpGZO2xTGLuVFuvE6JIsIli3fvFHCYBhGFn/8OHEEyMYF+QehA==", + "license": "MIT" + }, "node_modules/date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -21221,6 +22209,7 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", + "dev": true, "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.0", @@ -21690,6 +22679,7 @@ "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, "license": "MIT" }, "node_modules/dom-align": { @@ -21783,10 +22773,11 @@ } }, "node_modules/dompurify": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.1.tgz", - "integrity": "sha512-JahakDAIg1gyOm7dlgWSDjV4n7Ip2PKR55NIT6jrMfIgLFgWo81vdr1/QGqWtFNRqXP9UV71oVePtjqS2ebnPw==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz", + "integrity": "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==", "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true, "optionalDependencies": { "@types/trusted-types": "^2.0.7" } @@ -21844,9 +22835,9 @@ } }, "node_modules/dotenv-expand": { - "version": "11.0.7", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", - "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.3.tgz", + "integrity": "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -22038,19 +23029,15 @@ "license": "MIT" }, "node_modules/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", - "dev": true, + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-5.0.2.tgz", + "integrity": "sha512-IpbUaI/CAW86l3f+T8zN0iggSc0LmMZLcIW5eRVStLVNCoTXkE0YlncbbH50fp8Cl6zHIky0sW2uUbhBqGw0Jw==", "license": "Apache-2.0", - "dependencies": { - "jake": "^10.8.5" - }, "bin": { "ejs": "bin/cli.js" }, "engines": { - "node": ">=0.10.0" + "node": ">=0.12.18" } }, "node_modules/electron-to-chromium": { @@ -22125,9 +23112,9 @@ } }, "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", "license": "MIT", "dependencies": { "once": "^1.4.0" @@ -22146,32 +23133,19 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.20.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", - "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", + "version": "5.21.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.3.tgz", + "integrity": "sha512-QyL119InA+XXEkNLNTPCXPugSvOfhwv0JOlGNzvxs0hZaiHLNvXSpudUWsOlsXGWJh8G6ckCScEkVHfX3kw/2Q==", "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.3.0" + "tapable": "^2.3.3" }, "engines": { "node": ">=10.13.0" } }, - "node_modules/enquirer": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", - "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-colors": "^4.1.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -22331,6 +23305,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.2", @@ -22788,6 +23763,35 @@ "eslint": ">=7" } }, + "node_modules/eslint-plugin-cypress/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-cypress/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint-plugin-i18n-strings": { "resolved": "eslint-rules/eslint-plugin-i18n-strings", "link": true @@ -22989,9 +23993,9 @@ "license": "MIT" }, "node_modules/eslint-plugin-react-you-might-not-need-an-effect": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-you-might-not-need-an-effect/-/eslint-plugin-react-you-might-not-need-an-effect-0.10.0.tgz", - "integrity": "sha512-a4pugbQc2zLiE2NZGuXdTjtMNvlP2984QFPDv71eskUYDzigLFYfBL4QjK+RnRtcboHoXRKOcQqEZKxiK6KegA==", + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-you-might-not-need-an-effect/-/eslint-plugin-react-you-might-not-need-an-effect-0.10.1.tgz", + "integrity": "sha512-IK0s/+ShN0bkur5moKCu/lfx2D/9uIeozje8Wv2/XnYdmswa17pDg02aUuytEPb8Gf0eueiQFf/QsvOHHcvujg==", "dev": true, "license": "MIT", "dependencies": { @@ -23005,9 +24009,9 @@ } }, "node_modules/eslint-plugin-react-you-might-not-need-an-effect/node_modules/globals": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", - "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", "dev": true, "license": "MIT", "engines": { @@ -23064,14 +24068,14 @@ } }, "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/scope-manager": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.1.tgz", - "integrity": "sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz", + "integrity": "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.59.1", - "@typescript-eslint/visitor-keys": "8.59.1" + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -23082,9 +24086,9 @@ } }, "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/types": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.1.tgz", - "integrity": "sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz", + "integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==", "dev": true, "license": "MIT", "engines": { @@ -23096,21 +24100,21 @@ } }, "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.1.tgz", - "integrity": "sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz", + "integrity": "sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.59.1", - "@typescript-eslint/tsconfig-utils": "8.59.1", - "@typescript-eslint/types": "8.59.1", - "@typescript-eslint/visitor-keys": "8.59.1", + "@typescript-eslint/project-service": "8.56.0", + "@typescript-eslint/tsconfig-utils": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", "debug": "^4.4.3", - "minimatch": "^10.2.2", + "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -23120,20 +24124,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/utils": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.1.tgz", - "integrity": "sha512-3pIeoXhCeYH9FSCBI8P3iNwJlGuzPlYKkTlen2O9T1DSeeg8UG8jstq6BLk+Mda0qup7mgk4z4XL4OzRaxZ8LA==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.0.tgz", + "integrity": "sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.59.1", - "@typescript-eslint/types": "8.59.1", - "@typescript-eslint/typescript-estree": "8.59.1" + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -23144,17 +24148,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/eslint-plugin-testing-library/node_modules/@typescript-eslint/visitor-keys": { - "version": "8.59.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.1.tgz", - "integrity": "sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz", + "integrity": "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.59.1", + "@typescript-eslint/types": "8.56.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -23176,9 +24180,9 @@ } }, "node_modules/eslint-plugin-testing-library/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", "dev": true, "license": "MIT", "dependencies": { @@ -23189,9 +24193,9 @@ } }, "node_modules/eslint-plugin-testing-library/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz", + "integrity": "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==", "dev": true, "license": "Apache-2.0", "engines": { @@ -23202,16 +24206,16 @@ } }, "node_modules/eslint-plugin-testing-library/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.7.tgz", + "integrity": "sha512-MOwgjc8tfrpn5QQEvjijjmDVtMw2oL88ugTevzxQnzRLm6l3fVEF2gzU0kYeYYKD8C66+IdGX6peJ4MyUlUnPg==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "ISC", "dependencies": { - "brace-expansion": "^5.0.5" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": ">=16 || 14 >=14.17" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -23256,9 +24260,9 @@ } }, "node_modules/eslint/node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -23339,6 +24343,22 @@ "node": ">=10.13.0" } }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint/node_modules/js-yaml": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", @@ -23359,6 +24379,19 @@ "dev": true, "license": "MIT" }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -23378,9 +24411,9 @@ } }, "node_modules/espree/node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, "license": "MIT", "bin": { @@ -23480,13 +24513,6 @@ "node": ">= 0.6" } }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "dev": true, - "license": "MIT" - }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -23794,9 +24820,9 @@ "license": "BSD-3-Clause" }, "node_modules/fast-xml-builder": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.5.tgz", - "integrity": "sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", "funding": [ { "type": "github", @@ -23805,13 +24831,14 @@ ], "license": "MIT", "dependencies": { - "path-expression-matcher": "^1.1.3" + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" } }, "node_modules/fast-xml-parser": { - "version": "4.5.6", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.6.tgz", - "integrity": "sha512-Yd4vkROfJf8AuJrDIVMVmYfULKmIJszVsMv7Vo71aocsKgFxpdlpSHXSaInvyYfgw2PRuObQSW2GFpVMUjxu9A==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.4.tgz", + "integrity": "sha512-jE8ugADnYOBsu1uaoayVl1tVKAMNOXyjwvv2U6udEA2ORBhDooJDWoGxTkhd4Qn4yh59JVVt/pKXtjPwx9OguQ==", "funding": [ { "type": "github", @@ -23996,36 +25023,6 @@ "url": "https://github.com/sindresorhus/file-type?sponsor=1" } }, - "node_modules/filelist": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", - "license": "Apache-2.0", - "dependencies": { - "minimatch": "^5.0.1" - } - }, - "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -24272,31 +25269,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/find-up-simple": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", - "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/first-chunk-stream": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/first-chunk-stream/-/first-chunk-stream-5.0.0.tgz", - "integrity": "sha512-WdHo4ejd2cG2Dl+sLkW79SctU7mUQDfr4s1i26ffOZRs5mgv+BRttIM9gwcq0rDbemo0KlpVPaa3LBVLqPXzcQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/flat": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", @@ -24322,17 +25294,34 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/flat-cache/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", + "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", "dev": true, "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", - "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "dev": true, "funding": [ { @@ -24424,9 +25413,9 @@ } }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -24616,16 +25605,6 @@ ], "license": "MIT" }, - "node_modules/front-matter": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/front-matter/-/front-matter-4.0.2.tgz", - "integrity": "sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-yaml": "^3.13.1" - } - }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -24688,6 +25667,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, "license": "ISC" }, "node_modules/fsevents": { @@ -25111,13 +26091,13 @@ } }, "node_modules/geostyler-sld-parser": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/geostyler-sld-parser/-/geostyler-sld-parser-8.4.2.tgz", - "integrity": "sha512-NPlTC1VoxgsApUgs/KhEAkGuMhWrUr2slJj8gSyXhd+oJUaYaPOfeGEFiaV+MGk1va+g3XJ1e5gkRD6aHjx8Eg==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/geostyler-sld-parser/-/geostyler-sld-parser-8.5.0.tgz", + "integrity": "sha512-b02BvloFweoC6m47wSKSjuO5jv9xd+JnuNBXLejFUlzAh2lo+VBVRPcThBJ2j3yBRAYJm3aSUfBUfjK/tP4+fA==", "license": "BSD-2-Clause", "dependencies": { "fast-xml-parser": "^5.2.3", - "geostyler-style": "^11.0.2" + "geostyler-style": "^12.0.0" }, "engines": { "node": ">=20.6.0" @@ -25127,9 +26107,9 @@ } }, "node_modules/geostyler-sld-parser/node_modules/fast-xml-parser": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.2.tgz", - "integrity": "sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.8.0.tgz", + "integrity": "sha512-6bIM7fsJxeo3uXv7OncQYsBAMPJ7V16Slahl/6M98C/i2q+vB1+4a0MtrvYwDFEUrwDSbAmeLDRXsOBwrL7yAg==", "funding": [ { "type": "github", @@ -25139,18 +26119,32 @@ "license": "MIT", "dependencies": { "@nodable/entities": "^2.1.0", - "fast-xml-builder": "^1.1.5", + "fast-xml-builder": "^1.2.0", "path-expression-matcher": "^1.5.0", - "strnum": "^2.2.3" + "strnum": "^2.3.0", + "xml-naming": "^0.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, + "node_modules/geostyler-sld-parser/node_modules/geostyler-style": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/geostyler-style/-/geostyler-style-12.0.0.tgz", + "integrity": "sha512-teNaY/a4tVrGAt7vqvrXi0mWBg7OxIdUKAtI0oik/IlO1GfOK4JPERfT/Z+6m1FwNLVNOEx0VvZ2JiTSevPrwQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.6.0", + "npm": ">=10.0.0" + }, + "funding": { + "url": "https://opencollective.com/geostyler" + } + }, "node_modules/geostyler-sld-parser/node_modules/strnum": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz", - "integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", "funding": [ { "type": "github", @@ -25429,9 +26423,9 @@ } }, "node_modules/get-stream": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.0.tgz", - "integrity": "sha512-A1B3Bh1UmL0bidM/YX2NsCOTnGJePL9rO/M+Mw3m9f2gUpfokS0hi5Eah0WSUEWZdZhIZtMjkIYS7mDfOqNHbg==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "dev": true, "license": "MIT", "engines": { @@ -25600,6 +26594,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -25628,23 +26623,6 @@ "node": ">= 6" } }, - "node_modules/glob-to-regex.js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", - "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, "node_modules/glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", @@ -25722,35 +26700,6 @@ "which": "bin/which" } }, - "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globals/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/globalthis": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", @@ -25973,6 +26922,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -26128,9 +27078,9 @@ } }, "node_modules/hast-util-from-parse5/node_modules/vfile-message": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", - "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -26204,9 +27154,9 @@ "license": "MIT" }, "node_modules/hast-util-raw/node_modules/unist-util-visit": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", - "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -26233,9 +27183,9 @@ } }, "node_modules/hast-util-raw/node_modules/vfile-message": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", - "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -26407,27 +27357,6 @@ "node": ">=0.10.0" } }, - "node_modules/hosted-git-info": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.2.tgz", - "integrity": "sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==", - "license": "ISC", - "dependencies": { - "lru-cache": "^11.1.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/hosted-git-info/node_modules/lru-cache": { - "version": "11.3.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", - "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, "node_modules/hpack.js": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", @@ -26789,6 +27718,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/http-proxy/node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT" + }, "node_modules/http-server": { "version": "14.1.1", "resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz", @@ -26981,9 +27917,9 @@ } }, "node_modules/ignore-walk/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -27028,9 +27964,9 @@ "license": "MIT" }, "node_modules/immer": { - "version": "11.1.7", - "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.7.tgz", - "integrity": "sha512-LFVFtAROHcDy1er5UI6nodRFnZ2SgdCXhfNSI+DpObO8N7Pur/muBGsjzH5wpnFHCYhYVQxZskCkV4koQ//3/Q==", + "version": "11.1.8", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.8.tgz", + "integrity": "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==", "license": "MIT", "funding": { "type": "opencollective", @@ -27117,28 +28053,18 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/index-to-position": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", - "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -27242,9 +28168,9 @@ "license": "MIT" }, "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "dev": true, "license": "MIT", "engines": { @@ -27289,6 +28215,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -27578,6 +28505,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -27956,13 +28884,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==", - "license": "MIT", - "peer": true - }, "node_modules/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -28139,6 +29060,23 @@ "node": ">=8" } }, + "node_modules/istanbul-lib-processinfo/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/istanbul-lib-processinfo/node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -28235,40 +29173,6 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/jake": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", - "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", - "license": "Apache-2.0", - "dependencies": { - "async": "^3.2.3", - "chalk": "^4.0.2", - "filelist": "^1.0.4", - "minimatch": "^3.1.2" - }, - "bin": { - "jake": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jake/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/jed": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/jed/-/jed-1.1.1.tgz", @@ -28350,9 +29254,9 @@ } }, "node_modules/jest-changed-files/node_modules/@sinclair/typebox": { - "version": "0.34.49", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", - "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", "dev": true, "license": "MIT" }, @@ -28392,9 +29296,9 @@ } }, "node_modules/jest-changed-files/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -28436,6 +29340,16 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-circus/node_modules/@jest/diff-sequences": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz", + "integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/jest-circus/node_modules/@jest/environment": { "version": "30.3.0", "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.3.0.tgz", @@ -28503,16 +29417,16 @@ } }, "node_modules/jest-circus/node_modules/@sinclair/typebox": { - "version": "0.34.49", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", - "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", "dev": true, "license": "MIT" }, "node_modules/jest-circus/node_modules/@sinonjs/fake-timers": { - "version": "15.3.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.3.2.tgz", - "integrity": "sha512-mrn35Jl2pCpns+mE3HaZa1yPN5EYCRgiMI+135COjr2hr8Cls9DXqIZ57vZe2cz7y2XVSq92tcs6kGQcT1J8Rw==", + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.1.1.tgz", + "integrity": "sha512-cO5W33JgAPbOh07tvZjUOJ7oWhtaqGHiZw+11DPbyqh2kHTBc3eF/CjJDeQ4205RLQsX6rxCuYOroFQwl7JDRw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -28638,9 +29552,9 @@ } }, "node_modules/jest-circus/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -28754,9 +29668,9 @@ } }, "node_modules/jest-cli/node_modules/@sinclair/typebox": { - "version": "0.34.49", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", - "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", "dev": true, "license": "MIT" }, @@ -28796,9 +29710,9 @@ } }, "node_modules/jest-cli/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -28911,16 +29825,16 @@ } }, "node_modules/jest-config/node_modules/@sinclair/typebox": { - "version": "0.34.49", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", - "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", "dev": true, "license": "MIT" }, "node_modules/jest-config/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -29001,9 +29915,9 @@ } }, "node_modules/jest-config/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -29175,9 +30089,9 @@ } }, "node_modules/jest-each/node_modules/@sinclair/typebox": { - "version": "0.34.49", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", - "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", "dev": true, "license": "MIT" }, @@ -29217,9 +30131,9 @@ } }, "node_modules/jest-each/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -29286,9 +30200,9 @@ } }, "node_modules/jest-environment-jsdom/node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, "license": "MIT", "bin": { @@ -29309,6 +30223,13 @@ "acorn-walk": "^8.0.2" } }, + "node_modules/jest-environment-jsdom/node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "dev": true, + "license": "MIT" + }, "node_modules/jest-environment-jsdom/node_modules/cssstyle": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", @@ -29612,16 +30533,16 @@ } }, "node_modules/jest-environment-node/node_modules/@sinclair/typebox": { - "version": "0.34.49", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", - "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", "dev": true, "license": "MIT" }, "node_modules/jest-environment-node/node_modules/@sinonjs/fake-timers": { - "version": "15.3.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.3.2.tgz", - "integrity": "sha512-mrn35Jl2pCpns+mE3HaZa1yPN5EYCRgiMI+135COjr2hr8Cls9DXqIZ57vZe2cz7y2XVSq92tcs6kGQcT1J8Rw==", + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.1.1.tgz", + "integrity": "sha512-cO5W33JgAPbOh07tvZjUOJ7oWhtaqGHiZw+11DPbyqh2kHTBc3eF/CjJDeQ4205RLQsX6rxCuYOroFQwl7JDRw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -29700,9 +30621,9 @@ } }, "node_modules/jest-environment-node/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -29818,9 +30739,9 @@ } }, "node_modules/jest-haste-map/node_modules/@sinclair/typebox": { - "version": "0.34.49", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", - "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", "dev": true, "license": "MIT" }, @@ -29893,9 +30814,9 @@ } }, "node_modules/jest-haste-map/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -29928,9 +30849,9 @@ } }, "node_modules/jest-html-reporter/node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.1.tgz", + "integrity": "sha512-+g/1TKjFuGrf1Hh0QPCv0gISwBxJ+MQSNXmG9zjHy7BmFhtoJ9fdNhWJp3qUKRi93AOZHXtdxZgJ1vAtz6z65w==", "dev": true, "license": "MIT", "dependencies": { @@ -29941,14 +30862,14 @@ } }, "node_modules/jest-html-reporter/node_modules/@jest/types": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", - "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.1.tgz", + "integrity": "sha512-HGwoYRVF0QSKJu1ZQX0o5ZrUrrhj0aOOFA8hXrumD7SIzjouevhawbTjmXdwOmURdGluU9DM/XvGm3NyFoiQjw==", "dev": true, "license": "MIT", "dependencies": { "@jest/pattern": "30.0.1", - "@jest/schemas": "30.0.5", + "@jest/schemas": "30.0.1", "@types/istanbul-lib-coverage": "^2.0.6", "@types/istanbul-reports": "^3.0.4", "@types/node": "*", @@ -29960,9 +30881,9 @@ } }, "node_modules/jest-html-reporter/node_modules/@sinclair/typebox": { - "version": "0.34.49", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", - "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "version": "0.34.37", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.37.tgz", + "integrity": "sha512-2TRuQVgQYfy+EzHRTIvkhv2ADEouJ2xNS/Vq+W5EuuewBdOrvATvljZTxHWZSTYr2sTjTHpGvucaGAt67S2akw==", "dev": true, "license": "MIT" }, @@ -30037,9 +30958,9 @@ } }, "node_modules/jest-leak-detector/node_modules/@sinclair/typebox": { - "version": "0.34.49", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", - "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", "dev": true, "license": "MIT" }, @@ -30406,9 +31327,9 @@ } }, "node_modules/jest-resolve/node_modules/@sinclair/typebox": { - "version": "0.34.49", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", - "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", "dev": true, "license": "MIT" }, @@ -30448,9 +31369,9 @@ } }, "node_modules/jest-resolve/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -30571,16 +31492,16 @@ } }, "node_modules/jest-runner/node_modules/@sinclair/typebox": { - "version": "0.34.49", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", - "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", "dev": true, "license": "MIT" }, "node_modules/jest-runner/node_modules/@sinonjs/fake-timers": { - "version": "15.3.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.3.2.tgz", - "integrity": "sha512-mrn35Jl2pCpns+mE3HaZa1yPN5EYCRgiMI+135COjr2hr8Cls9DXqIZ57vZe2cz7y2XVSq92tcs6kGQcT1J8Rw==", + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.1.1.tgz", + "integrity": "sha512-cO5W33JgAPbOh07tvZjUOJ7oWhtaqGHiZw+11DPbyqh2kHTBc3eF/CjJDeQ4205RLQsX6rxCuYOroFQwl7JDRw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -30692,9 +31613,9 @@ } }, "node_modules/jest-runner/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -30864,16 +31785,16 @@ } }, "node_modules/jest-runtime/node_modules/@sinclair/typebox": { - "version": "0.34.49", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", - "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", "dev": true, "license": "MIT" }, "node_modules/jest-runtime/node_modules/@sinonjs/fake-timers": { - "version": "15.3.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.3.2.tgz", - "integrity": "sha512-mrn35Jl2pCpns+mE3HaZa1yPN5EYCRgiMI+135COjr2hr8Cls9DXqIZ57vZe2cz7y2XVSq92tcs6kGQcT1J8Rw==", + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.1.1.tgz", + "integrity": "sha512-cO5W33JgAPbOh07tvZjUOJ7oWhtaqGHiZw+11DPbyqh2kHTBc3eF/CjJDeQ4205RLQsX6rxCuYOroFQwl7JDRw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -30881,9 +31802,9 @@ } }, "node_modules/jest-runtime/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -31007,9 +31928,9 @@ } }, "node_modules/jest-runtime/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -31100,6 +32021,16 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-snapshot/node_modules/@jest/diff-sequences": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz", + "integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/jest-snapshot/node_modules/@jest/expect-utils": { "version": "30.3.0", "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.3.0.tgz", @@ -31146,9 +32077,9 @@ } }, "node_modules/jest-snapshot/node_modules/@sinclair/typebox": { - "version": "0.34.49", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", - "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", "dev": true, "license": "MIT" }, @@ -31274,9 +32205,9 @@ } }, "node_modules/jest-snapshot/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -31426,9 +32357,9 @@ } }, "node_modules/jest-validate/node_modules/@sinclair/typebox": { - "version": "0.34.49", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", - "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", "dev": true, "license": "MIT" }, @@ -31530,9 +32461,9 @@ } }, "node_modules/jest-watcher/node_modules/@sinclair/typebox": { - "version": "0.34.49", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", - "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", "dev": true, "license": "MIT" }, @@ -31572,9 +32503,9 @@ } }, "node_modules/jest-watcher/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -31660,9 +32591,9 @@ } }, "node_modules/jest/node_modules/@sinclair/typebox": { - "version": "0.34.49", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", - "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", "dev": true, "license": "MIT" }, @@ -31828,64 +32759,6 @@ } } }, - "node_modules/jsdom/node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.3.tgz", - "integrity": "sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "peerDependencies": { - "css-tree": "^3.2.1" - }, - "peerDependenciesMeta": { - "css-tree": { - "optional": true - } - } - }, - "node_modules/jsdom/node_modules/@exodus/bytes": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", - "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@noble/hashes": "^1.8.0 || ^2.0.0" - }, - "peerDependenciesMeta": { - "@noble/hashes": { - "optional": true - } - } - }, - "node_modules/jsdom/node_modules/@noble/hashes": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", - "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/jsdom/node_modules/css-tree": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", @@ -31914,9 +32787,9 @@ } }, "node_modules/jsdom/node_modules/lru-cache": { - "version": "11.3.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", - "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", + "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -31943,6 +32816,39 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/jsdom/node_modules/tldts": { + "version": "7.0.30", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.30.tgz", + "integrity": "sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.30" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/jsdom/node_modules/tldts-core": { + "version": "7.0.30", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.30.tgz", + "integrity": "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom/node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/jsdom/node_modules/webidl-conversions": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", @@ -32145,9 +33051,9 @@ } }, "node_modules/jspdf": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.2.1.tgz", - "integrity": "sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.2.0.tgz", + "integrity": "sha512-hR/hnRevAXXlrjeqU5oahOE+Ln9ORJUB5brLHHqH67A+RBQZuFr5GkbI9XQI8OUFSEezKegsi45QRpc4bGj75Q==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.6", @@ -32399,10 +33305,20 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, + "node_modules/lerna/node_modules/@jest/diff-sequences": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.4.0.tgz", + "integrity": "sha512-zOpzlfUs45l6u7jm39qr87JCHUDsaeCtvL+kQe/Vn9jSnRB4/5IPXISm0h9I1vZW/o00Kn4UTJ2MOlhnUGwv3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/lerna/node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", + "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", "dev": true, "license": "MIT", "dependencies": { @@ -32659,6 +33575,13 @@ } } }, + "node_modules/lerna/node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT" + }, "node_modules/lerna/node_modules/execa": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/execa/-/execa-5.0.0.tgz", @@ -32701,6 +33624,19 @@ } } }, + "node_modules/lerna/node_modules/get-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.0.tgz", + "integrity": "sha512-A1B3Bh1UmL0bidM/YX2NsCOTnGJePL9rO/M+Mw3m9f2gUpfokS0hi5Eah0WSUEWZdZhIZtMjkIYS7mDfOqNHbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lerna/node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -32769,16 +33705,16 @@ } }, "node_modules/lerna/node_modules/jest-diff": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz", - "integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.4.1.tgz", + "integrity": "sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/diff-sequences": "30.3.0", + "@jest/diff-sequences": "30.4.0", "@jest/get-type": "30.1.0", "chalk": "^4.1.2", - "pretty-format": "30.3.0" + "pretty-format": "30.4.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -32814,6 +33750,16 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/lerna/node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/lerna/node_modules/p-queue": { "version": "6.6.2", "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", @@ -32845,9 +33791,9 @@ } }, "node_modules/lerna/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -32858,15 +33804,16 @@ } }, "node_modules/lerna/node_modules/pretty-format": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", - "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", + "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/schemas": "30.0.5", + "@jest/schemas": "30.4.1", "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" + "react-is-18": "npm:react-is@^18.3.1", + "react-is-19": "npm:react-is@^19.2.5" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -33548,6 +34495,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, "license": "MIT", "bin": { "lz-string": "bin/bin.js" @@ -33735,9 +34683,9 @@ } }, "node_modules/mapbox-gl/node_modules/earcut": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz", - "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.1.tgz", + "integrity": "sha512-0l1/0gOjESMeQyYaK5IDiPNvFeu93Z/cO0TjZh9eZ1vyCtZnA7KMZ8rQggpsJHIbGSdrqYq9OhuveadOVHCshw==", "license": "ISC" }, "node_modules/mapbox-gl/node_modules/pbf": { @@ -33752,6 +34700,12 @@ "pbf": "bin/pbf" } }, + "node_modules/mapbox-gl/node_modules/quickselect": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", + "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", + "license": "ISC" + }, "node_modules/maplibre-gl": { "version": "5.24.0", "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.24.0.tgz", @@ -33803,15 +34757,6 @@ "pbf": "^4.0.1" } }, - "node_modules/maplibre-gl/node_modules/@maplibre/geojson-vt": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-6.1.0.tgz", - "integrity": "sha512-2eIY4gZxeKIVOZVNkAMb+5NgXhgsMQpOveTQAvnp53LYqHGJZDidk7Ew0Tged9PThidpbS+NFTh0g4zivhPDzQ==", - "license": "ISC", - "dependencies": { - "kdbush": "^4.0.2" - } - }, "node_modules/maplibre-gl/node_modules/earcut": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz", @@ -33830,6 +34775,12 @@ "pbf": "bin/pbf" } }, + "node_modules/maplibre-gl/node_modules/quickselect": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", + "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", + "license": "ISC" + }, "node_modules/markdown-table": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", @@ -33951,375 +34902,6 @@ "@types/unist": "^2" } }, - "node_modules/mdast-util-find-and-replace": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-2.2.2.tgz", - "integrity": "sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "escape-string-regexp": "^5.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-find-and-replace/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mdast-util-find-and-replace/node_modules/unist-util-is": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", - "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-find-and-replace/node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-from-markdown": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", - "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "decode-named-character-reference": "^1.0.0", - "mdast-util-to-string": "^3.1.0", - "micromark": "^3.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-decode-string": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "unist-util-stringify-position": "^3.0.0", - "uvu": "^0.5.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-from-markdown/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/mdast-util-from-markdown/node_modules/micromark-util-symbol": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", - "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/mdast-util-from-markdown/node_modules/micromark-util-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", - "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/mdast-util-from-markdown/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-2.0.2.tgz", - "integrity": "sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg==", - "license": "MIT", - "dependencies": { - "mdast-util-from-markdown": "^1.0.0", - "mdast-util-gfm-autolink-literal": "^1.0.0", - "mdast-util-gfm-footnote": "^1.0.0", - "mdast-util-gfm-strikethrough": "^1.0.0", - "mdast-util-gfm-table": "^1.0.0", - "mdast-util-gfm-task-list-item": "^1.0.0", - "mdast-util-to-markdown": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-autolink-literal": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-1.0.3.tgz", - "integrity": "sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "ccount": "^2.0.0", - "mdast-util-find-and-replace": "^2.0.0", - "micromark-util-character": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-autolink-literal/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/mdast-util-gfm-autolink-literal/node_modules/micromark-util-character": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", - "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/mdast-util-gfm-autolink-literal/node_modules/micromark-util-symbol": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", - "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/mdast-util-gfm-autolink-literal/node_modules/micromark-util-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", - "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/mdast-util-gfm-footnote": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-1.0.2.tgz", - "integrity": "sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-to-markdown": "^1.3.0", - "micromark-util-normalize-identifier": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-footnote/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/mdast-util-gfm-strikethrough": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-1.0.3.tgz", - "integrity": "sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-to-markdown": "^1.3.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-strikethrough/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/mdast-util-gfm-table": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-1.0.7.tgz", - "integrity": "sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "markdown-table": "^3.0.0", - "mdast-util-from-markdown": "^1.0.0", - "mdast-util-to-markdown": "^1.3.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-table/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/mdast-util-gfm-task-list-item": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-1.0.2.tgz", - "integrity": "sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-to-markdown": "^1.3.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-task-list-item/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/mdast-util-phrasing": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz", - "integrity": "sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-phrasing/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/mdast-util-phrasing/node_modules/unist-util-is": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", - "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/mdast-util-to-hast": { "version": "13.2.1", "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", @@ -34357,9 +34939,9 @@ "license": "MIT" }, "node_modules/mdast-util-to-hast/node_modules/unist-util-visit": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", - "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -34386,9 +34968,9 @@ } }, "node_modules/mdast-util-to-hast/node_modules/vfile-message": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", - "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -34399,61 +34981,10 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/mdast-util-to-markdown": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz", - "integrity": "sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "longest-streak": "^3.0.0", - "mdast-util-phrasing": "^3.0.0", - "mdast-util-to-string": "^3.0.0", - "micromark-util-decode-string": "^1.0.0", - "unist-util-visit": "^4.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-markdown/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/mdast-util-to-string": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", - "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-string/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, "node_modules/mdn-data": { - "version": "2.0.30", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", "dev": true, "license": "CC0-1.0" }, @@ -34467,20 +34998,77 @@ "node": ">= 0.6" } }, - "node_modules/mem-fs": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/mem-fs/-/mem-fs-4.1.4.tgz", - "integrity": "sha512-NlRHmUiEcxDHI7FeDlrrTZP5YFvnoS74wEf5OrQ7NAg83B2Rv3oF+FWr961I0rVdxkKbZMjq2BcV7VFWGFPkog==", + "node_modules/mem-fs-editor": { + "version": "12.0.4", + "resolved": "https://registry.npmjs.org/mem-fs-editor/-/mem-fs-editor-12.0.4.tgz", + "integrity": "sha512-gc8b4VlisaGp5W+ot2f4Xc8jUgKnMn5UR2mKsdm8UdbESYCdSiQKqioktPu8gJ0Uxd8gV/m/M16Pp5n1Ge8pjA==", "license": "MIT", - "peer": true, "dependencies": { - "@types/node": ">=18", - "@types/vinyl": "^2.0.12", - "vinyl": "^3.0.1", - "vinyl-file": "^5.0.0" + "@types/ejs": "^3.1.5", + "@types/picomatch": "^4.0.2", + "binaryextensions": "^6.11.0", + "commondir": "^1.0.1", + "debug": "^4.4.3", + "deep-extend": "^0.6.0", + "ejs": "^5.0.1", + "isbinaryfile": "5.0.7", + "minimatch": "^10.2.4", + "multimatch": "^8.0.0", + "normalize-path": "^3.0.0", + "textextensions": "^6.11.0", + "tinyglobby": "^0.2.15", + "vinyl": "^3.0.1" + }, + "acceptDependencies": { + "isbinaryfile": "^6.0.0" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" + }, + "peerDependencies": { + "@types/node": ">=20", + "mem-fs": "^4.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/mem-fs-editor/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/mem-fs-editor/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/mem-fs-editor/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/memfs": { @@ -34553,11 +35141,17 @@ } }, "node_modules/meow/node_modules/hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", "dev": true, - "license": "ISC" + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } }, "node_modules/meow/node_modules/locate-path": { "version": "5.0.0", @@ -34601,32 +35195,6 @@ "node": ">=10" } }, - "node_modules/meow/node_modules/normalize-package-data/node_modules/hosted-git-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/meow/node_modules/normalize-package-data/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/meow/node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -34700,6 +35268,13 @@ "node": ">=8" } }, + "node_modules/meow/node_modules/read-pkg/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true, + "license": "ISC" + }, "node_modules/meow/node_modules/read-pkg/node_modules/normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -34713,6 +35288,16 @@ "validate-npm-package-license": "^3.0.1" } }, + "node_modules/meow/node_modules/read-pkg/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/meow/node_modules/read-pkg/node_modules/type-fest": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", @@ -34723,16 +35308,6 @@ "node": ">=8" } }, - "node_modules/meow/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, "node_modules/meow/node_modules/type-fest": { "version": "0.18.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", @@ -34823,961 +35398,6 @@ "integrity": "sha512-awNbTOqCxK1DBGjalK3xqWIstBZgN6fxsMSiXLs9/spqWkF2pAhb2rrYCFSsr1/tT7PhcDGjZndG8SWYn0byYA==", "license": "MIT" }, - "node_modules/micromark": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", - "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", - "decode-named-character-reference": "^1.0.0", - "micromark-core-commonmark": "^1.0.1", - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-chunked": "^1.0.0", - "micromark-util-combine-extensions": "^1.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-encode": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-sanitize-uri": "^1.0.0", - "micromark-util-subtokenize": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.1", - "uvu": "^0.5.0" - } - }, - "node_modules/micromark-core-commonmark": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", - "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-factory-destination": "^1.0.0", - "micromark-factory-label": "^1.0.0", - "micromark-factory-space": "^1.0.0", - "micromark-factory-title": "^1.0.0", - "micromark-factory-whitespace": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-chunked": "^1.0.0", - "micromark-util-classify-character": "^1.0.0", - "micromark-util-html-tag-name": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-subtokenize": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.1", - "uvu": "^0.5.0" - } - }, - "node_modules/micromark-core-commonmark/node_modules/micromark-util-character": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", - "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-core-commonmark/node_modules/micromark-util-symbol": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", - "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-core-commonmark/node_modules/micromark-util-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", - "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-extension-gfm": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-2.0.3.tgz", - "integrity": "sha512-vb9OoHqrhCmbRidQv/2+Bc6pkP0FrtlhurxZofvOEy5o8RtuuvTq+RQ1Vw5ZDNrVraQZu3HixESqbG+0iKk/MQ==", - "license": "MIT", - "dependencies": { - "micromark-extension-gfm-autolink-literal": "^1.0.0", - "micromark-extension-gfm-footnote": "^1.0.0", - "micromark-extension-gfm-strikethrough": "^1.0.0", - "micromark-extension-gfm-table": "^1.0.0", - "micromark-extension-gfm-tagfilter": "^1.0.0", - "micromark-extension-gfm-task-list-item": "^1.0.0", - "micromark-util-combine-extensions": "^1.0.0", - "micromark-util-types": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-autolink-literal": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-1.0.5.tgz", - "integrity": "sha512-z3wJSLrDf8kRDOh2qBtoTRD53vJ+CWIyo7uyZuxf/JAbNJjiHsOpG1y5wxk8drtv3ETAHutCu6N3thkOOgueWg==", - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-sanitize-uri": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-autolink-literal/node_modules/micromark-util-character": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", - "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-extension-gfm-autolink-literal/node_modules/micromark-util-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", - "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-extension-gfm-autolink-literal/node_modules/micromark-util-sanitize-uri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", - "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-encode": "^1.0.0", - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/micromark-extension-gfm-autolink-literal/node_modules/micromark-util-symbol": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", - "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-extension-gfm-autolink-literal/node_modules/micromark-util-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", - "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-extension-gfm-footnote": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-1.1.2.tgz", - "integrity": "sha512-Yxn7z7SxgyGWRNa4wzf8AhYYWNrwl5q1Z8ii+CSTTIqVkmGZF1CElX2JI8g5yGoM3GAman9/PVCUFUSJ0kB/8Q==", - "license": "MIT", - "dependencies": { - "micromark-core-commonmark": "^1.0.0", - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-normalize-identifier": "^1.0.0", - "micromark-util-sanitize-uri": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-footnote/node_modules/micromark-util-character": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", - "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-extension-gfm-footnote/node_modules/micromark-util-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", - "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-extension-gfm-footnote/node_modules/micromark-util-sanitize-uri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", - "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-encode": "^1.0.0", - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/micromark-extension-gfm-footnote/node_modules/micromark-util-symbol": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", - "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-extension-gfm-footnote/node_modules/micromark-util-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", - "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-extension-gfm-strikethrough": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-1.0.7.tgz", - "integrity": "sha512-sX0FawVE1o3abGk3vRjOH50L5TTLr3b5XMqnP9YDRb34M0v5OoZhG+OHFz1OffZ9dlwgpTBKaT4XW/AsUVnSDw==", - "license": "MIT", - "dependencies": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-classify-character": "^1.0.0", - "micromark-util-resolve-all": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-strikethrough/node_modules/micromark-util-symbol": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", - "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-extension-gfm-strikethrough/node_modules/micromark-util-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", - "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-extension-gfm-table": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-1.0.7.tgz", - "integrity": "sha512-3ZORTHtcSnMQEKtAOsBQ9/oHp9096pI/UvdPtN7ehKvrmZZ2+bbWhi0ln+I9drmwXMt5boocn6OlwQzNXeVeqw==", - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-table/node_modules/micromark-util-character": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", - "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-extension-gfm-table/node_modules/micromark-util-symbol": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", - "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-extension-gfm-table/node_modules/micromark-util-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", - "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-extension-gfm-tagfilter": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-1.0.2.tgz", - "integrity": "sha512-5XWB9GbAUSHTn8VPU8/1DBXMuKYT5uOgEjJb8gN3mW0PNW5OPHpSdojoqf+iq1xo7vWzw/P8bAHY0n6ijpXF7g==", - "license": "MIT", - "dependencies": { - "micromark-util-types": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-tagfilter/node_modules/micromark-util-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", - "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-extension-gfm-task-list-item": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-1.0.5.tgz", - "integrity": "sha512-RMFXl2uQ0pNQy6Lun2YBYT9g9INXtWJULgbt01D/x8/6yJ2qpKyzdZD3pi6UIkzF++Da49xAelVKUeUMqd5eIQ==", - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-task-list-item/node_modules/micromark-util-character": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", - "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-extension-gfm-task-list-item/node_modules/micromark-util-symbol": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", - "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-extension-gfm-task-list-item/node_modules/micromark-util-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", - "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-extension-gfm/node_modules/micromark-util-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", - "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-factory-destination": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", - "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-factory-destination/node_modules/micromark-util-character": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", - "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-factory-destination/node_modules/micromark-util-symbol": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", - "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-factory-destination/node_modules/micromark-util-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", - "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-factory-label": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", - "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - } - }, - "node_modules/micromark-factory-label/node_modules/micromark-util-character": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", - "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-factory-label/node_modules/micromark-util-symbol": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", - "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-factory-label/node_modules/micromark-util-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", - "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-factory-space": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", - "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-factory-space/node_modules/micromark-util-character": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", - "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-factory-space/node_modules/micromark-util-symbol": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", - "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-factory-space/node_modules/micromark-util-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", - "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-factory-title": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", - "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-factory-title/node_modules/micromark-util-character": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", - "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-factory-title/node_modules/micromark-util-symbol": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", - "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-factory-title/node_modules/micromark-util-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", - "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-factory-whitespace": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", - "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-factory-whitespace/node_modules/micromark-util-character": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", - "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-factory-whitespace/node_modules/micromark-util-symbol": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", - "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-factory-whitespace/node_modules/micromark-util-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", - "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, "node_modules/micromark-util-character": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", @@ -35798,259 +35418,6 @@ "micromark-util-types": "^2.0.0" } }, - "node_modules/micromark-util-chunked": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", - "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/micromark-util-chunked/node_modules/micromark-util-symbol": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", - "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-classify-character": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", - "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-util-classify-character/node_modules/micromark-util-character": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", - "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-util-classify-character/node_modules/micromark-util-symbol": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", - "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-classify-character/node_modules/micromark-util-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", - "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-combine-extensions": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", - "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-util-combine-extensions/node_modules/micromark-util-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", - "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", - "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/micromark-util-decode-numeric-character-reference/node_modules/micromark-util-symbol": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", - "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-decode-string": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", - "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^1.0.0", - "micromark-util-decode-numeric-character-reference": "^1.0.0", - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/micromark-util-decode-string/node_modules/micromark-util-character": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", - "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-util-decode-string/node_modules/micromark-util-symbol": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", - "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-decode-string/node_modules/micromark-util-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", - "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, "node_modules/micromark-util-encode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", @@ -36067,92 +35434,6 @@ ], "license": "MIT" }, - "node_modules/micromark-util-html-tag-name": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", - "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-normalize-identifier": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", - "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/micromark-util-normalize-identifier/node_modules/micromark-util-symbol": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", - "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-resolve-all": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", - "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-util-resolve-all/node_modules/micromark-util-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", - "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, "node_modules/micromark-util-sanitize-uri": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", @@ -36174,60 +35455,6 @@ "micromark-util-symbol": "^2.0.0" } }, - "node_modules/micromark-util-subtokenize": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", - "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-chunked": "^1.0.0", - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0", - "uvu": "^0.5.0" - } - }, - "node_modules/micromark-util-subtokenize/node_modules/micromark-util-symbol": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", - "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-subtokenize/node_modules/micromark-util-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", - "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, "node_modules/micromark-util-symbol": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", @@ -36245,98 +35472,9 @@ "license": "MIT" }, "node_modules/micromark-util-types": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", - "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark/node_modules/micromark-util-character": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", - "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark/node_modules/micromark-util-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", - "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark/node_modules/micromark-util-sanitize-uri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", - "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-encode": "^1.0.0", - "micromark-util-symbol": "^1.0.0" - } - }, - "node_modules/micromark/node_modules/micromark-util-symbol": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", - "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark/node_modules/micromark-util-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", - "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.1.tgz", + "integrity": "sha512-534m2WhVTddrcKVepwmVEVnUAmtrx9bfIjNoQHRqfnvdaHQiFytEhJoTgpWJvDEXCO5gLTQh3wYC1PgOJA4NSQ==", "funding": [ { "type": "GitHub Sponsors", @@ -36412,6 +35550,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -36449,6 +35588,7 @@ "version": "3.1.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.4.tgz", "integrity": "sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==", + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -36485,7 +35625,6 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" @@ -36523,18 +35662,38 @@ } }, "node_modules/minipass-flush": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.6.tgz", - "integrity": "sha512-7Uf5gMJZ2kTkFisE3toGxT991s+cg+vMh42nbZGM2bNxfYVpkpqRudf1QrcOy72a3PwcL4JYqL+4NY7t0Hdd0A==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", + "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "minipass": "^7.1.3" + "minipass": "^3.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">= 8" } }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, "node_modules/minipass-pipeline": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", @@ -36758,9 +35917,9 @@ } }, "node_modules/multimatch/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -36799,16 +35958,6 @@ "mustache": "bin/mustache" } }, - "node_modules/mute-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", - "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/nanoid": { "version": "5.1.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.11.tgz", @@ -36935,21 +36084,21 @@ } }, "node_modules/node-gyp": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.2.0.tgz", - "integrity": "sha512-q23WdzrQv48KozXlr0U1v9dwO/k59NHeSzn6loGcasyf0UnSrtzs8kRxM+mfwJSf0DkX0s43hcqgnSO4/VNthQ==", + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.3.0.tgz", + "integrity": "sha512-QNcUWM+HgJplcPzBvFBZ9VXacyGZ4+VTOb80PwWR+TlVzoHbRKULNEzpRsnaoxG3Wzr7Qh7BYxGDU3CbKib2Yg==", "dev": true, "license": "MIT", "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", - "make-fetch-happen": "^15.0.0", "nopt": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", "tar": "^7.5.4", "tinyglobby": "^0.2.12", + "undici": "^6.25.0", "which": "^6.0.0" }, "bin": { @@ -37005,6 +36154,16 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/node-gyp/node_modules/undici": { + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.25.0.tgz", + "integrity": "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, "node_modules/node-gyp/node_modules/which": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", @@ -37113,20 +36272,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/normalize-package-data": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-8.0.0.tgz", - "integrity": "sha512-RWk+PI433eESQ7ounYxIp67CYuVsS1uYSonX3kA6ps/3LWfjVQa/ptEg6Y3T6uAMq1mWpX9PQ+qx+QaHpsc7gQ==", - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^9.0.0", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -37188,6 +36333,29 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/npm-package-arg/node_modules/hosted-git-info": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-package-arg/node_modules/lru-cache": { + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", + "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/npm-packlist": { "version": "10.0.3", "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-10.0.3.tgz", @@ -37323,65 +36491,139 @@ "license": "MIT" }, "node_modules/nx": { - "version": "22.6.1", - "resolved": "https://registry.npmjs.org/nx/-/nx-22.6.1.tgz", - "integrity": "sha512-b4eo52o5aCVt3oG6LPYvD2Cul3JFBMgr2p9OjMBIo6oU6QfSR693H2/UuUMepLtO6jcIniPKOcIrf6Ue8aXAww==", + "version": "22.7.1", + "resolved": "https://registry.npmjs.org/nx/-/nx-22.7.1.tgz", + "integrity": "sha512-SadJUQY57MiwRIetm9rhZhdpFeOe1Csib2Vg9C423Pw/h0fZE14qUo6+OBby9vLh5QCkRfRZ0WaHkeO5q6yNtA==", "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { - "@ltd/j-toml": "^1.38.0", + "@emnapi/core": "1.4.5", + "@emnapi/runtime": "1.4.5", + "@emnapi/wasi-threads": "1.0.4", + "@jest/diff-sequences": "30.0.1", "@napi-rs/wasm-runtime": "0.2.4", - "@yarnpkg/lockfile": "^1.1.0", - "@yarnpkg/parsers": "3.0.2", + "@tybys/wasm-util": "0.9.0", + "@yarnpkg/lockfile": "1.1.0", "@zkochan/js-yaml": "0.0.7", - "axios": "^1.12.0", + "ansi-colors": "4.1.3", + "ansi-regex": "5.0.1", + "ansi-styles": "4.3.0", + "argparse": "2.0.1", + "asynckit": "0.4.0", + "axios": "1.15.0", + "balanced-match": "4.0.3", + "base64-js": "1.5.1", + "bl": "4.1.0", + "brace-expansion": "5.0.2", + "buffer": "5.7.1", + "call-bind-apply-helpers": "1.0.2", + "chalk": "4.1.2", "cli-cursor": "3.1.0", "cli-spinners": "2.6.1", - "cliui": "^8.0.1", - "dotenv": "~16.4.5", - "dotenv-expand": "~11.0.6", - "ejs": "^3.1.7", - "enquirer": "~2.3.6", + "cliui": "8.0.1", + "clone": "1.0.4", + "color-convert": "2.0.1", + "color-name": "1.1.4", + "combined-stream": "1.0.8", + "defaults": "1.0.4", + "define-lazy-prop": "2.0.0", + "delayed-stream": "1.0.0", + "dotenv": "16.4.7", + "dotenv-expand": "12.0.3", + "dunder-proto": "1.0.1", + "ejs": "5.0.1", + "emoji-regex": "8.0.0", + "end-of-stream": "1.4.5", + "enquirer": "2.3.6", + "es-define-property": "1.0.1", + "es-errors": "1.3.0", + "es-object-atoms": "1.1.1", + "es-set-tostringtag": "2.1.0", + "escalade": "3.2.0", + "escape-string-regexp": "1.0.5", "figures": "3.2.0", - "flat": "^5.0.2", - "front-matter": "^4.0.2", - "ignore": "^7.0.5", - "jest-diff": "^30.0.2", + "flat": "5.0.2", + "follow-redirects": "1.15.11", + "form-data": "4.0.5", + "fs-constants": "1.0.0", + "function-bind": "1.1.2", + "get-caller-file": "2.0.5", + "get-intrinsic": "1.3.0", + "get-proto": "1.0.1", + "gopd": "1.2.0", + "has-flag": "4.0.0", + "has-symbols": "1.1.0", + "has-tostringtag": "1.0.2", + "hasown": "2.0.2", + "ieee754": "1.2.1", + "ignore": "7.0.5", + "inherits": "2.0.4", + "is-docker": "2.2.1", + "is-fullwidth-code-point": "3.0.0", + "is-interactive": "1.0.0", + "is-unicode-supported": "0.1.0", + "is-wsl": "2.2.0", + "json5": "2.2.3", "jsonc-parser": "3.2.0", "lines-and-columns": "2.0.3", + "log-symbols": "4.1.0", + "math-intrinsics": "1.1.0", + "mime-db": "1.52.0", + "mime-types": "2.1.35", + "mimic-fn": "2.1.0", "minimatch": "10.2.4", - "npm-run-path": "^4.0.1", - "open": "^8.4.0", + "minimist": "1.2.8", + "npm-run-path": "4.0.1", + "once": "1.4.0", + "onetime": "5.1.2", + "open": "8.4.2", "ora": "5.3.0", - "picocolors": "^1.1.0", + "path-key": "3.1.1", + "picocolors": "1.1.1", + "proxy-from-env": "2.1.0", + "readable-stream": "3.6.2", + "require-directory": "2.1.1", "resolve.exports": "2.0.3", - "semver": "^7.6.3", - "string-width": "^4.2.3", - "tar-stream": "~2.2.0", - "tmp": "~0.2.1", - "tree-kill": "^1.2.2", - "tsconfig-paths": "^4.1.2", - "tslib": "^2.3.0", - "yaml": "^2.6.0", - "yargs": "^17.6.2", + "restore-cursor": "3.1.0", + "safe-buffer": "5.2.1", + "semver": "7.7.4", + "signal-exit": "3.0.7", + "smol-toml": "1.6.1", + "string_decoder": "1.3.0", + "string-width": "4.2.3", + "strip-ansi": "6.0.1", + "strip-bom": "3.0.0", + "supports-color": "7.2.0", + "tar-stream": "2.2.0", + "tmp": "0.2.4", + "tree-kill": "1.2.2", + "tsconfig-paths": "4.2.0", + "tslib": "2.8.1", + "util-deprecate": "1.0.2", + "wcwidth": "1.0.1", + "wrap-ansi": "7.0.0", + "wrappy": "1.0.2", + "y18n": "5.0.8", + "yaml": "2.8.0", + "yargs": "17.7.2", "yargs-parser": "21.1.1" }, "bin": { - "nx": "bin/nx.js", - "nx-cloud": "bin/nx-cloud.js" + "nx": "dist/bin/nx.js", + "nx-cloud": "dist/bin/nx-cloud.js" }, "optionalDependencies": { - "@nx/nx-darwin-arm64": "22.6.1", - "@nx/nx-darwin-x64": "22.6.1", - "@nx/nx-freebsd-x64": "22.6.1", - "@nx/nx-linux-arm-gnueabihf": "22.6.1", - "@nx/nx-linux-arm64-gnu": "22.6.1", - "@nx/nx-linux-arm64-musl": "22.6.1", - "@nx/nx-linux-x64-gnu": "22.6.1", - "@nx/nx-linux-x64-musl": "22.6.1", - "@nx/nx-win32-arm64-msvc": "22.6.1", - "@nx/nx-win32-x64-msvc": "22.6.1" + "@nx/nx-darwin-arm64": "22.7.1", + "@nx/nx-darwin-x64": "22.7.1", + "@nx/nx-freebsd-x64": "22.7.1", + "@nx/nx-linux-arm-gnueabihf": "22.7.1", + "@nx/nx-linux-arm64-gnu": "22.7.1", + "@nx/nx-linux-arm64-musl": "22.7.1", + "@nx/nx-linux-x64-gnu": "22.7.1", + "@nx/nx-linux-x64-musl": "22.7.1", + "@nx/nx-win32-arm64-msvc": "22.7.1", + "@nx/nx-win32-x64-msvc": "22.7.1" }, "peerDependencies": { "@swc-node/register": "^1.11.1", @@ -37396,47 +36638,46 @@ } } }, - "node_modules/nx/node_modules/@jest/schemas": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", - "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "node_modules/nx/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/nx/node_modules/axios": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", + "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", "dev": true, "license": "MIT", "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" } }, - "node_modules/nx/node_modules/@sinclair/typebox": { - "version": "0.34.49", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", - "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", - "dev": true, - "license": "MIT" - }, "node_modules/nx/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz", + "integrity": "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==", "dev": true, "license": "MIT", "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/nx/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", + "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/nx/node_modules/chalk": { @@ -37456,6 +36697,59 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/nx/node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/nx/node_modules/ejs": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-5.0.1.tgz", + "integrity": "sha512-COqBPFMxuPTPspXl2DkVYaDS3HtrD1GpzOGkNTJ1IYkifq/r9h8SVEFrjA3D9/VJGOEoMQcrlhpntcSUrM8k6A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.12.18" + } + }, + "node_modules/nx/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/nx/node_modules/enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/nx/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/nx/node_modules/ignore": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", @@ -37466,20 +36760,34 @@ "node": ">= 4" } }, - "node_modules/nx/node_modules/jest-diff": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz", - "integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==", + "node_modules/nx/node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nx/node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/diff-sequences": "30.3.0", - "@jest/get-type": "30.1.0", - "chalk": "^4.1.2", - "pretty-format": "30.3.0" + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/nx/node_modules/minimatch": { @@ -37498,32 +36806,52 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/nx/node_modules/pretty-format": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", - "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.0.5", - "ansi-styles": "^5.2.0", - "react-is": "^18.3.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/nx/node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "node_modules/nx/node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", "dev": true, "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/nx/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nx/node_modules/tmp": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.4.tgz", + "integrity": "sha512-UdiSoX6ypifLmrfQ/XfiawN6hkjSBpCjhKxxZcWlUUmoXLaCKQU0bx4HF/tdDK2uzRuchf1txGvrWBzYREssoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/nx/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, "engines": { "node": ">=10" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/nx/node_modules/yargs": { @@ -37545,264 +36873,6 @@ "node": ">=12" } }, - "node_modules/nyc": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", - "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "caching-transform": "^4.0.0", - "convert-source-map": "^1.7.0", - "decamelize": "^1.2.0", - "find-cache-dir": "^3.2.0", - "find-up": "^4.1.0", - "foreground-child": "^2.0.0", - "get-package-type": "^0.1.0", - "glob": "^7.1.6", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-hook": "^3.0.0", - "istanbul-lib-instrument": "^4.0.0", - "istanbul-lib-processinfo": "^2.0.2", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.0.2", - "make-dir": "^3.0.0", - "node-preload": "^0.2.1", - "p-map": "^3.0.0", - "process-on-spawn": "^1.0.0", - "resolve-from": "^5.0.0", - "rimraf": "^3.0.0", - "signal-exit": "^3.0.2", - "spawn-wrap": "^2.0.0", - "test-exclude": "^6.0.0", - "yargs": "^15.0.2" - }, - "bin": { - "nyc": "bin/nyc.js" - }, - "engines": { - "node": ">=8.9" - } - }, - "node_modules/nyc/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/nyc/node_modules/cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "node_modules/nyc/node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true, - "license": "MIT" - }, - "node_modules/nyc/node_modules/find-cache-dir": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", - "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", - "dev": true, - "license": "MIT", - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/avajs/find-cache-dir?sponsor=1" - } - }, - "node_modules/nyc/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/nyc/node_modules/foreground-child": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", - "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/nyc/node_modules/istanbul-lib-instrument": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", - "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.7.5", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.0.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/nyc/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/nyc/node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/nyc/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/nyc/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/nyc/node_modules/p-map": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", - "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/nyc/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/nyc/node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/nyc/node_modules/yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/nyc/node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -37828,6 +36898,7 @@ "version": "1.1.6", "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.7", @@ -38273,9 +37344,9 @@ } }, "node_modules/oxlint": { - "version": "1.63.0", - "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.63.0.tgz", - "integrity": "sha512-9TGXetdjgIHOJ9OiReomP7nnrMkV9HxC1xM2ramJSLQpzxjsAJtQwa4wqkJN2f/uCrqZuJseFuSlWDdvcruveg==", + "version": "1.64.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.64.0.tgz", + "integrity": "sha512-Star3SNpWPeWFPw7kRXIhXUSn6fdiAl25q15CQzH/9WaOtG6e9CWTc25vNZOCr4PE1yEP1GtKJKIKglhj3OmEQ==", "dev": true, "license": "MIT", "bin": { @@ -38288,25 +37359,25 @@ "url": "https://github.com/sponsors/Boshen" }, "optionalDependencies": { - "@oxlint/binding-android-arm-eabi": "1.63.0", - "@oxlint/binding-android-arm64": "1.63.0", - "@oxlint/binding-darwin-arm64": "1.63.0", - "@oxlint/binding-darwin-x64": "1.63.0", - "@oxlint/binding-freebsd-x64": "1.63.0", - "@oxlint/binding-linux-arm-gnueabihf": "1.63.0", - "@oxlint/binding-linux-arm-musleabihf": "1.63.0", - "@oxlint/binding-linux-arm64-gnu": "1.63.0", - "@oxlint/binding-linux-arm64-musl": "1.63.0", - "@oxlint/binding-linux-ppc64-gnu": "1.63.0", - "@oxlint/binding-linux-riscv64-gnu": "1.63.0", - "@oxlint/binding-linux-riscv64-musl": "1.63.0", - "@oxlint/binding-linux-s390x-gnu": "1.63.0", - "@oxlint/binding-linux-x64-gnu": "1.63.0", - "@oxlint/binding-linux-x64-musl": "1.63.0", - "@oxlint/binding-openharmony-arm64": "1.63.0", - "@oxlint/binding-win32-arm64-msvc": "1.63.0", - "@oxlint/binding-win32-ia32-msvc": "1.63.0", - "@oxlint/binding-win32-x64-msvc": "1.63.0" + "@oxlint/binding-android-arm-eabi": "1.64.0", + "@oxlint/binding-android-arm64": "1.64.0", + "@oxlint/binding-darwin-arm64": "1.64.0", + "@oxlint/binding-darwin-x64": "1.64.0", + "@oxlint/binding-freebsd-x64": "1.64.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.64.0", + "@oxlint/binding-linux-arm-musleabihf": "1.64.0", + "@oxlint/binding-linux-arm64-gnu": "1.64.0", + "@oxlint/binding-linux-arm64-musl": "1.64.0", + "@oxlint/binding-linux-ppc64-gnu": "1.64.0", + "@oxlint/binding-linux-riscv64-gnu": "1.64.0", + "@oxlint/binding-linux-riscv64-musl": "1.64.0", + "@oxlint/binding-linux-s390x-gnu": "1.64.0", + "@oxlint/binding-linux-x64-gnu": "1.64.0", + "@oxlint/binding-linux-x64-musl": "1.64.0", + "@oxlint/binding-openharmony-arm64": "1.64.0", + "@oxlint/binding-win32-arm64-msvc": "1.64.0", + "@oxlint/binding-win32-ia32-msvc": "1.64.0", + "@oxlint/binding-win32-x64-msvc": "1.64.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.22.1" @@ -38896,6 +37967,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -38941,9 +38013,9 @@ "license": "ISC" }, "node_modules/path-to-regexp": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", - "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "dev": true, "license": "MIT" }, @@ -39004,9 +38076,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -39189,6 +38261,7 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/polished/-/polished-4.3.1.tgz", "integrity": "sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.17.8" @@ -39267,9 +38340,9 @@ } }, "node_modules/postcss-calc/node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", + "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", "dev": true, "license": "MIT", "dependencies": { @@ -39508,9 +38581,9 @@ } }, "node_modules/postcss-modules-local-by-default/node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", + "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", "dev": true, "license": "MIT", "dependencies": { @@ -39538,9 +38611,9 @@ } }, "node_modules/postcss-modules-scope/node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", + "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", "dev": true, "license": "MIT", "dependencies": { @@ -39814,9 +38887,9 @@ "license": "MIT" }, "node_modules/postcss/node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "dev": true, "funding": [ { @@ -39851,17 +38924,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/preact": { - "version": "10.29.1", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.1.tgz", - "integrity": "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==", - "license": "MIT", - "peer": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/preact" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -39934,6 +38996,7 @@ "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1", @@ -39948,6 +39011,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -39960,6 +39024,7 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, "license": "MIT" }, "node_modules/pretty-ms": { @@ -40164,9 +39229,9 @@ "license": "ISC" }, "node_modules/protocol-buffers-schema": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.1.tgz", - "integrity": "sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", + "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==", "license": "MIT" }, "node_modules/protocols": { @@ -40332,12 +39397,6 @@ "node": ">=8" } }, - "node_modules/quickselect": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", - "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", - "license": "ISC" - }, "node_modules/quote-stream": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/quote-stream/-/quote-stream-1.0.2.tgz", @@ -40441,6 +39500,12 @@ "quickselect": "^3.0.0" } }, + "node_modules/rbush/node_modules/quickselect": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", + "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", + "license": "ISC" + }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -40456,6 +39521,23 @@ "rc": "cli.js" } }, + "node_modules/rc-align": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/rc-align/-/rc-align-4.0.15.tgz", + "integrity": "sha512-wqJtVH60pka/nOX7/IspElA8gjPNQKIx/ZqJ6heATCkXpe1Zg4cPVrMD2vC96wjsFFL8WsmhPbx9tdMo1qqlIA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "dom-align": "^1.7.0", + "rc-util": "^5.26.0", + "resize-observer-polyfill": "^1.5.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, "node_modules/rc-cascader": { "version": "3.34.0", "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.34.0.tgz", @@ -40589,21 +39671,6 @@ "react-dom": ">=16.9.0" } }, - "node_modules/rc-input": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.8.0.tgz", - "integrity": "sha512-KXvaTbX+7ha8a/k+eg6SYRVERK0NddX8QX7a7AnRvUa/rEH0CNMlpcBzBkhI0wp2C8C4HlMoYl8TImSN+fuHKA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.11.1", - "classnames": "^2.2.1", - "rc-util": "^5.18.1" - }, - "peerDependencies": { - "react": ">=16.0.0", - "react-dom": ">=16.0.0" - } - }, "node_modules/rc-input-number": { "version": "9.5.0", "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-9.5.0.tgz", @@ -40621,6 +39688,21 @@ "react-dom": ">=16.9.0" } }, + "node_modules/rc-input-number/node_modules/rc-input": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.8.0.tgz", + "integrity": "sha512-KXvaTbX+7ha8a/k+eg6SYRVERK0NddX8QX7a7AnRvUa/rEH0CNMlpcBzBkhI0wp2C8C4HlMoYl8TImSN+fuHKA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-util": "^5.18.1" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, "node_modules/rc-mentions": { "version": "2.20.0", "resolved": "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.20.0.tgz", @@ -40640,6 +39722,21 @@ "react-dom": ">=16.9.0" } }, + "node_modules/rc-mentions/node_modules/rc-input": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.8.0.tgz", + "integrity": "sha512-KXvaTbX+7ha8a/k+eg6SYRVERK0NddX8QX7a7AnRvUa/rEH0CNMlpcBzBkhI0wp2C8C4HlMoYl8TImSN+fuHKA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-util": "^5.18.1" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, "node_modules/rc-menu": { "version": "9.16.1", "resolved": "https://registry.npmjs.org/rc-menu/-/rc-menu-9.16.1.tgz", @@ -40956,6 +40053,21 @@ "react-dom": ">=16.9.0" } }, + "node_modules/rc-textarea/node_modules/rc-input": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.8.0.tgz", + "integrity": "sha512-KXvaTbX+7ha8a/k+eg6SYRVERK0NddX8QX7a7AnRvUa/rEH0CNMlpcBzBkhI0wp2C8C4HlMoYl8TImSN+fuHKA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-util": "^5.18.1" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, "node_modules/rc-tooltip": { "version": "6.4.0", "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-6.4.0.tgz", @@ -41009,6 +40121,26 @@ "react-dom": "*" } }, + "node_modules/rc-trigger": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/rc-trigger/-/rc-trigger-5.3.4.tgz", + "integrity": "sha512-mQv+vas0TwKcjAO2izNPkqR4j86OemLRmvL2nOzdP9OWNWA1ivoTt5hzFqYNW9zACwmTezRiN8bttrC7cZzYSw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "classnames": "^2.2.6", + "rc-align": "^4.0.0", + "rc-motion": "^2.0.0", + "rc-util": "^5.19.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, "node_modules/rc-upload": { "version": "4.9.2", "resolved": "https://registry.npmjs.org/rc-upload/-/rc-upload-4.9.2.tgz", @@ -41094,28 +40226,10 @@ "node": ">=0.10.0" } }, - "node_modules/react-ace": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/react-ace/-/react-ace-10.1.0.tgz", - "integrity": "sha512-VkvUjZNhdYTuKOKQpMIZi7uzZZVgzCjM7cLYu6F64V0mejY8a2XTyPUIMszC6A4trbeMIHbK5fYFcT/wkP/8VA==", - "license": "MIT", - "peer": true, - "dependencies": { - "ace-builds": "^1.4.14", - "diff-match-patch": "^1.0.5", - "lodash.get": "^4.4.2", - "lodash.isequal": "^4.5.0", - "prop-types": "^15.7.2" - }, - "peerDependencies": { - "react": "^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/react-arborist": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/react-arborist/-/react-arborist-3.5.0.tgz", - "integrity": "sha512-FdXOICSt7P2h+Pxin1ULN02b4qrXJznNcshgwwWVtuYMLWSJcD245PQ4HOSj/Lr2T1uEegmnEm5Lbns2hUUsqg==", + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/react-arborist/-/react-arborist-3.6.1.tgz", + "integrity": "sha512-h2/sPz6PXL79h7mOWjCA6Y5WNUKmA0kL8Uh6RYZQbYk7UOFBd86Jeoga4RjHMBYpOWpBPYrOJOE3HbIPUETp8w==", "license": "MIT", "dependencies": { "react-dnd": "^14.0.3", @@ -41235,9 +40349,9 @@ } }, "node_modules/react-checkbox-tree/node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "funding": [ { "type": "github", @@ -41424,6 +40538,22 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, + "node_modules/react-is-18": { + "name": "react-is", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-is-19": { + "name": "react-is", + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.6.tgz", + "integrity": "sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==", + "dev": true, + "license": "MIT" + }, "node_modules/react-json-tree": { "version": "0.20.0", "resolved": "https://registry.npmjs.org/react-json-tree/-/react-json-tree-0.20.0.tgz", @@ -41460,96 +40590,6 @@ "react": "*" } }, - "node_modules/react-map-gl": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/react-map-gl/-/react-map-gl-8.1.1.tgz", - "integrity": "sha512-aSqFAFoxvY7wxbGI93Dz0E41171mkAb3GcNbnkFIotmu88OFw495os6mIDZSi7irYNT/PZEIOEHUxhun4ToGuQ==", - "license": "MIT", - "dependencies": { - "@vis.gl/react-mapbox": "8.1.1", - "@vis.gl/react-maplibre": "8.1.1" - }, - "peerDependencies": { - "mapbox-gl": ">=1.13.0", - "maplibre-gl": ">=1.13.0", - "react": ">=16.3.0", - "react-dom": ">=16.3.0" - }, - "peerDependenciesMeta": { - "mapbox-gl": { - "optional": true - }, - "maplibre-gl": { - "optional": true - } - } - }, - "node_modules/react-map-gl/node_modules/@maplibre/maplibre-gl-style-spec": { - "version": "19.3.3", - "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-19.3.3.tgz", - "integrity": "sha512-cOZZOVhDSulgK0meTsTkmNXb1ahVvmTmWmfx9gRBwc6hq98wS9JP35ESIoNq3xqEan+UN+gn8187Z6E4NKhLsw==", - "license": "ISC", - "dependencies": { - "@mapbox/jsonlint-lines-primitives": "~2.0.2", - "@mapbox/unitbezier": "^0.0.1", - "json-stringify-pretty-compact": "^3.0.0", - "minimist": "^1.2.8", - "rw": "^1.3.3", - "sort-object": "^3.0.3" - }, - "bin": { - "gl-style-format": "dist/gl-style-format.mjs", - "gl-style-migrate": "dist/gl-style-migrate.mjs", - "gl-style-validate": "dist/gl-style-validate.mjs" - } - }, - "node_modules/react-map-gl/node_modules/@vis.gl/react-mapbox": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/@vis.gl/react-mapbox/-/react-mapbox-8.1.1.tgz", - "integrity": "sha512-KMDTjtWESXxHS4uqWxjsvgQUHvuL3Z6SdKe68o7Nxma2qUfuyH3x4TCkIqGn3FQTrFvZLWvTnSAbGvtm+Kd13A==", - "license": "MIT", - "peerDependencies": { - "mapbox-gl": ">=3.5.0", - "react": ">=16.3.0", - "react-dom": ">=16.3.0" - }, - "peerDependenciesMeta": { - "mapbox-gl": { - "optional": true - } - } - }, - "node_modules/react-map-gl/node_modules/@vis.gl/react-maplibre": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/@vis.gl/react-maplibre/-/react-maplibre-8.1.1.tgz", - "integrity": "sha512-iUOfzJAhFAJwEZp1644tQb7LOTFgi5/GzdaztkhzNgFVuoF2Ez7guvwZjQAKB9CN2TlHTgNuYH8UW85kO7cVhw==", - "license": "MIT", - "dependencies": { - "@maplibre/maplibre-gl-style-spec": "^19.2.1" - }, - "peerDependencies": { - "maplibre-gl": ">=4.0.0", - "react": ">=16.3.0", - "react-dom": ">=16.3.0" - }, - "peerDependenciesMeta": { - "maplibre-gl": { - "optional": true - } - } - }, - "node_modules/react-map-gl/node_modules/json-stringify-pretty-compact": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-3.0.0.tgz", - "integrity": "sha512-Rc2suX5meI0S3bfdZuA7JMFBGkJ875ApfVyq2WHELjBiiG22My/l7/8zPpH/CfFVQHuVLd8NLR0nv6vi0BYYKA==", - "license": "MIT" - }, - "node_modules/react-map-gl/node_modules/rw": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", - "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", - "license": "BSD-3-Clause" - }, "node_modules/react-markdown": { "version": "8.0.7", "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-8.0.7.tgz", @@ -41893,86 +40933,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/read-package-up": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-12.0.0.tgz", - "integrity": "sha512-Q5hMVBYur/eQNWDdbF4/Wqqr9Bjvtrw2kjGxxBbKLbx8bVCL8gcArjTy8zDUuLGQicftpMuU0riQNcAsbtOVsw==", - "license": "MIT", - "dependencies": { - "find-up-simple": "^1.0.1", - "read-pkg": "^10.0.0", - "type-fest": "^5.2.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-package-up/node_modules/parse-json": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", - "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "index-to-position": "^1.1.0", - "type-fest": "^4.39.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-package-up/node_modules/parse-json/node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-package-up/node_modules/read-pkg": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-10.1.0.tgz", - "integrity": "sha512-I8g2lArQiP78ll51UeMZojewtYgIRCKCWqZEgOO8c/uefTI+XDXvCSXu3+YNUaTNvZzobrL5+SqHjBrByRRTdg==", - "license": "MIT", - "dependencies": { - "@types/normalize-package-data": "^2.4.4", - "normalize-package-data": "^8.0.0", - "parse-json": "^8.3.0", - "type-fest": "^5.4.4", - "unicorn-magic": "^0.4.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-package-up/node_modules/type-fest": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.6.0.tgz", - "integrity": "sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==", - "license": "(MIT OR CC0-1.0)", - "dependencies": { - "tagged-tag": "^1.0.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/read-pkg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", @@ -42168,6 +41128,16 @@ "node": ">=4" } }, + "node_modules/read/node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -42239,6 +41209,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, "license": "MIT", "dependencies": { "indent-string": "^4.0.0", @@ -42252,6 +41223,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, "license": "MIT", "dependencies": { "min-indent": "^1.0.0" @@ -42561,9 +41533,9 @@ } }, "node_modules/rehype-raw/node_modules/vfile-message": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", - "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -42620,31 +41592,6 @@ "node": ">=4" } }, - "node_modules/remark-gfm": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-3.0.1.tgz", - "integrity": "sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-gfm": "^2.0.0", - "micromark-extension-gfm": "^2.0.0", - "unified": "^10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-gfm/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, "node_modules/remark-parse": { "version": "10.0.2", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.2.tgz", @@ -42669,6 +41616,498 @@ "@types/unist": "^2" } }, + "node_modules/remark-parse/node_modules/mdast-util-from-markdown": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", + "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "mdast-util-to-string": "^3.1.0", + "micromark": "^3.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-decode-string": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-stringify-position": "^3.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse/node_modules/mdast-util-to-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse/node_modules/micromark": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", + "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "micromark-core-commonmark": "^1.0.1", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/remark-parse/node_modules/micromark-core-commonmark": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", + "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-factory-destination": "^1.0.0", + "micromark-factory-label": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-factory-title": "^1.0.0", + "micromark-factory-whitespace": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-html-tag-name": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/remark-parse/node_modules/micromark-factory-destination": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", + "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/remark-parse/node_modules/micromark-factory-label": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", + "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/remark-parse/node_modules/micromark-factory-space": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", + "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/remark-parse/node_modules/micromark-factory-title": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", + "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/remark-parse/node_modules/micromark-factory-whitespace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", + "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/remark-parse/node_modules/micromark-util-character": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", + "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/remark-parse/node_modules/micromark-util-chunked": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", + "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/remark-parse/node_modules/micromark-util-classify-character": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", + "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/remark-parse/node_modules/micromark-util-combine-extensions": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", + "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/remark-parse/node_modules/micromark-util-decode-numeric-character-reference": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", + "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/remark-parse/node_modules/micromark-util-decode-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", + "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/remark-parse/node_modules/micromark-util-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", + "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/remark-parse/node_modules/micromark-util-html-tag-name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", + "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/remark-parse/node_modules/micromark-util-normalize-identifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", + "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/remark-parse/node_modules/micromark-util-resolve-all": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", + "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/remark-parse/node_modules/micromark-util-sanitize-uri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", + "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/remark-parse/node_modules/micromark-util-subtokenize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", + "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/remark-parse/node_modules/micromark-util-symbol": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", + "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/remark-parse/node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/remark-parse/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-rehype": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-10.1.0.tgz", @@ -42928,6 +42367,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -43121,23 +42561,6 @@ "node": ">= 0.8.15" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/rison": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/rison/-/rison-0.1.1.tgz", @@ -43145,9 +42568,9 @@ "license": "Apache-2.0" }, "node_modules/robust-predicates": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", - "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", "license": "Unlicense" }, "node_modules/run-applescript": { @@ -43845,23 +43268,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/simple-git": { - "version": "3.36.0", - "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.36.0.tgz", - "integrity": "sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q==", - "license": "MIT", - "dependencies": { - "@kwsites/file-exists": "^1.1.1", - "@kwsites/promise-deferred": "^1.1.1", - "@simple-git/args-pathspec": "^1.0.3", - "@simple-git/argv-parser": "^1.1.0", - "debug": "^4.4.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/steveukx/git-js?sponsor=1" - } - }, "node_modules/simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", @@ -43872,9 +43278,9 @@ } }, "node_modules/simple-swizzle/node_modules/is-arrayish": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", - "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", "license": "MIT" }, "node_modules/simple-zstd": { @@ -43941,6 +43347,19 @@ "npm": ">= 3.0.0" } }, + "node_modules/smol-toml": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz", + "integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, "node_modules/snake-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", @@ -43981,13 +43400,13 @@ } }, "node_modules/socks": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", - "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", "dev": true, "license": "MIT", "dependencies": { - "ip-address": "^10.0.1", + "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" }, "engines": { @@ -44148,6 +43567,7 @@ "version": "0.7.6", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">= 12" @@ -44242,6 +43662,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/spawn-wrap/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/spawn-wrap/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -44851,6 +44288,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -44888,6 +44326,7 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, "license": "MIT" }, "node_modules/string.prototype.trim": { @@ -44950,6 +44389,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -44982,39 +44422,6 @@ "node": ">=8" } }, - "node_modules/strip-bom-buf": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-bom-buf/-/strip-bom-buf-3.0.1.tgz", - "integrity": "sha512-iJaWw2WroigLHzQysdc5WWeUc99p7ea7AEgB6JkY8CMyiO1yTVAA1gIlJJgORElUIR+lcZJkNl1OGChMhvc2Cw==", - "license": "MIT", - "peer": true, - "dependencies": { - "is-utf8": "^0.2.1" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-bom-stream": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/strip-bom-stream/-/strip-bom-stream-5.0.0.tgz", - "integrity": "sha512-Yo472mU+3smhzqeKlIxClre4s4pwtYZEvDNQvY/sJpnChdaxmKuwU28UVx/v1ORKNMxkmj1GBuvxJQyBk6wYMQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "first-chunk-stream": "^5.0.0", - "strip-bom-buf": "^3.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/strip-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz", @@ -45152,6 +44559,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -45285,9 +44693,9 @@ } }, "node_modules/tapable": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", "dev": true, "license": "MIT", "engines": { @@ -45394,14 +44802,14 @@ } }, "node_modules/terser": { - "version": "5.37.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", - "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", + "version": "5.47.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.47.1.tgz", + "integrity": "sha512-tPbLXTI6ohPASb/1YViL428oEHu6/qv1OxqYnfaonVCFHqx4+wCd95pHrQWsL5X4pl90CTyW9piSAsS2L0VoMw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", + "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -45603,18 +45011,14 @@ } }, "node_modules/thingies": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.6.0.tgz", - "integrity": "sha512-rMHRjmlFLM1R96UYPvpmnc3LYtdFrT33JIB7L9hetGue1qAPfn1N2LJeEjxUSidu1Iku+haLZXDuEXUHNGO/lg==", + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-1.21.0.tgz", + "integrity": "sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==", "dev": true, - "license": "MIT", + "license": "Unlicense", "engines": { "node": ">=10.18" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, "peerDependencies": { "tslib": "^2" } @@ -45713,13 +45117,13 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.4" + "picomatch": "^4.0.3" }, "engines": { "node": ">=12.0.0" @@ -45746,9 +45150,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", "engines": { "node": ">=12" @@ -45783,36 +45187,6 @@ "node": ">=14.0.0" } }, - "node_modules/tldts": { - "version": "7.0.29", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.29.tgz", - "integrity": "sha512-JIXCerhudr/N6OWLwLF1HVsTTUo7ry6qHa5eWZEkiMuxsIiAACL55tGLfqfHfoH7QaMQUW8fngD7u7TxWexYQg==", - "dev": true, - "license": "MIT", - "dependencies": { - "tldts-core": "^7.0.29" - }, - "bin": { - "tldts": "bin/cli.js" - } - }, - "node_modules/tldts-core": { - "version": "7.0.29", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.29.tgz", - "integrity": "sha512-W99NuU7b1DcG3uJ3v9k9VztCH3WialNbBkBft5wCs8V8mexu0XQqaZEYb9l9RNNzK8+3EJ9PKWB0/RUtTQ/o+Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/tmp": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", - "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.14" - } - }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -45905,19 +45279,6 @@ "node": ">=6" } }, - "node_modules/tough-cookie": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", - "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tldts": "^7.0.5" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/tr46": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", @@ -45959,9 +45320,9 @@ } }, "node_modules/tree-dump": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", - "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.0.3.tgz", + "integrity": "sha512-il+Cv80yVHFBwokQSfd4bldvr1Md951DpgAGfmhydt04L+YzHgubm2tQ7zueWDcGENKHq0ZvGFR/hjvNXilHEg==", "dev": true, "license": "Apache-2.0", "engines": { @@ -46220,9 +45581,9 @@ } }, "node_modules/tscw-config/node_modules/strip-json-comments": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", - "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.1.tgz", + "integrity": "sha512-0fk9zBqO67Nq5M/m45qHCJxylV/DhBlIOVExqgOMiCCrzrhU6tCibRXNqE3jwJLftzE9SNuZtYbpzcO+i9FiKw==", "dev": true, "license": "MIT", "engines": { @@ -46282,9 +45643,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.1.tgz", + "integrity": "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==", "cpu": [ "ppc64" ], @@ -46299,9 +45660,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.1.tgz", + "integrity": "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==", "cpu": [ "arm" ], @@ -46316,9 +45677,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.1.tgz", + "integrity": "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==", "cpu": [ "arm64" ], @@ -46333,9 +45694,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.1.tgz", + "integrity": "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==", "cpu": [ "x64" ], @@ -46350,9 +45711,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.1.tgz", + "integrity": "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==", "cpu": [ "arm64" ], @@ -46367,9 +45728,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.1.tgz", + "integrity": "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==", "cpu": [ "x64" ], @@ -46384,9 +45745,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.1.tgz", + "integrity": "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==", "cpu": [ "arm64" ], @@ -46401,9 +45762,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.1.tgz", + "integrity": "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==", "cpu": [ "x64" ], @@ -46418,9 +45779,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.1.tgz", + "integrity": "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==", "cpu": [ "arm" ], @@ -46435,9 +45796,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.1.tgz", + "integrity": "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==", "cpu": [ "arm64" ], @@ -46452,9 +45813,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.1.tgz", + "integrity": "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==", "cpu": [ "ia32" ], @@ -46469,9 +45830,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.1.tgz", + "integrity": "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==", "cpu": [ "loong64" ], @@ -46486,9 +45847,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.1.tgz", + "integrity": "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==", "cpu": [ "mips64el" ], @@ -46503,9 +45864,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.1.tgz", + "integrity": "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==", "cpu": [ "ppc64" ], @@ -46520,9 +45881,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.1.tgz", + "integrity": "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==", "cpu": [ "riscv64" ], @@ -46537,9 +45898,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.1.tgz", + "integrity": "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==", "cpu": [ "s390x" ], @@ -46554,9 +45915,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.1.tgz", + "integrity": "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==", "cpu": [ "x64" ], @@ -46571,9 +45932,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.1.tgz", + "integrity": "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==", "cpu": [ "x64" ], @@ -46588,9 +45949,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.1.tgz", + "integrity": "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==", "cpu": [ "x64" ], @@ -46605,9 +45966,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.1.tgz", + "integrity": "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==", "cpu": [ "x64" ], @@ -46622,9 +45983,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.1.tgz", + "integrity": "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==", "cpu": [ "arm64" ], @@ -46639,9 +46000,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.1.tgz", + "integrity": "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==", "cpu": [ "ia32" ], @@ -46656,9 +46017,9 @@ } }, "node_modules/tsx/node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.1.tgz", + "integrity": "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==", "cpu": [ "x64" ], @@ -46673,9 +46034,9 @@ } }, "node_modules/tsx/node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.1.tgz", + "integrity": "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -46686,32 +46047,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" + "@esbuild/aix-ppc64": "0.27.1", + "@esbuild/android-arm": "0.27.1", + "@esbuild/android-arm64": "0.27.1", + "@esbuild/android-x64": "0.27.1", + "@esbuild/darwin-arm64": "0.27.1", + "@esbuild/darwin-x64": "0.27.1", + "@esbuild/freebsd-arm64": "0.27.1", + "@esbuild/freebsd-x64": "0.27.1", + "@esbuild/linux-arm": "0.27.1", + "@esbuild/linux-arm64": "0.27.1", + "@esbuild/linux-ia32": "0.27.1", + "@esbuild/linux-loong64": "0.27.1", + "@esbuild/linux-mips64el": "0.27.1", + "@esbuild/linux-ppc64": "0.27.1", + "@esbuild/linux-riscv64": "0.27.1", + "@esbuild/linux-s390x": "0.27.1", + "@esbuild/linux-x64": "0.27.1", + "@esbuild/netbsd-arm64": "0.27.1", + "@esbuild/netbsd-x64": "0.27.1", + "@esbuild/openbsd-arm64": "0.27.1", + "@esbuild/openbsd-x64": "0.27.1", + "@esbuild/openharmony-arm64": "0.27.1", + "@esbuild/sunos-x64": "0.27.1", + "@esbuild/win32-arm64": "0.27.1", + "@esbuild/win32-ia32": "0.27.1", + "@esbuild/win32-x64": "0.27.1" } }, "node_modules/tsyringe": { @@ -46915,6 +46276,7 @@ "version": "5.4.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -46925,38 +46287,116 @@ } }, "node_modules/typescript-json-schema": { - "version": "0.67.1", - "resolved": "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.67.1.tgz", - "integrity": "sha512-vKTZB/RoYTIBdVP7E7vrgHMCssBuhja91wQy498QIVhvfRimaOgjc98uwAXmZ7mbLUytJmOSbF11wPz+ByQeXg==", + "version": "0.67.2", + "resolved": "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.67.2.tgz", + "integrity": "sha512-QRMWWIRu9JF6gL2cQAC4J7k0iEPEjJRq/p1d2xRa8ZwfHP/tNV4p248PdKoRPKcWFro3v4SAsB23XV6HOl3tHQ==", "license": "BSD-3-Clause", "dependencies": { - "@types/json-schema": "^7.0.9", - "@types/node": "^18.11.9", - "glob": "^7.1.7", + "@types/json-schema": "^7.0.15", + "@types/node": "^24.10.2", + "glob": "^13.0.6", "path-equal": "^1.2.5", - "safe-stable-stringify": "^2.2.0", - "ts-node": "^10.9.1", - "typescript": "~5.5.0", - "vm2": "^3.10.0", - "yargs": "^17.1.1" + "safe-stable-stringify": "^2.5.0", + "ts-node": "^10.9.2", + "typescript": "~5.9.3", + "vm2": "^3.10.5", + "yargs": "^18.0.0" }, "bin": { "typescript-json-schema": "bin/typescript-json-schema" } }, "node_modules/typescript-json-schema/node_modules/@types/node": { - "version": "18.19.130", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", - "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "version": "24.12.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz", + "integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==", "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~7.16.0" + } + }, + "node_modules/typescript-json-schema/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/typescript-json-schema/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/typescript-json-schema/node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typescript-json-schema/node_modules/lru-cache": { + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", + "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/typescript-json-schema/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typescript-json-schema/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/typescript-json-schema/node_modules/typescript": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", - "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -46967,29 +46407,11 @@ } }, "node_modules/typescript-json-schema/node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "license": "MIT" }, - "node_modules/typescript-json-schema/node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/typewise": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typewise/-/typewise-1.0.3.tgz", @@ -47079,9 +46501,9 @@ } }, "node_modules/undici-types": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.21.0.tgz", - "integrity": "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { @@ -47128,18 +46550,6 @@ "node": ">=4" } }, - "node_modules/unicorn-magic": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.4.0.tgz", - "integrity": "sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==", - "license": "MIT", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/unified": { "version": "10.1.2", "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", @@ -47225,9 +46635,9 @@ } }, "node_modules/unist-util-is": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", - "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0" @@ -47850,9 +47260,9 @@ } }, "node_modules/vfile-location/node_modules/vfile-message": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", - "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -47918,25 +47328,6 @@ "node": ">=10.13.0" } }, - "node_modules/vinyl-file": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/vinyl-file/-/vinyl-file-5.0.0.tgz", - "integrity": "sha512-MvkPF/yA1EX7c6p+juVIvp9+Lxp70YUfNKzEWeHMKpUNVSnTZh2coaOqLxI0pmOe2V9nB+OkgFaMDkodaJUyGw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/vinyl": "^2.0.7", - "strip-bom-buf": "^3.0.1", - "strip-bom-stream": "^5.0.0", - "vinyl": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/vlq": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/vlq/-/vlq-0.2.3.tgz", @@ -48277,9 +47668,9 @@ } }, "node_modules/webpack-bundle-analyzer/node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, "license": "MIT", "bin": { @@ -48319,6 +47710,28 @@ "dev": true, "license": "MIT" }, + "node_modules/webpack-bundle-analyzer/node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/webpack-cli": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-6.0.1.tgz", @@ -48373,9 +47786,9 @@ } }, "node_modules/webpack-cli/node_modules/envinfo": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", - "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz", + "integrity": "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==", "dev": true, "license": "MIT", "bin": { @@ -48503,9 +47916,9 @@ } }, "node_modules/webpack-dev-server/node_modules/ipaddr.js": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", - "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", "dev": true, "license": "MIT", "engines": { @@ -48513,9 +47926,9 @@ } }, "node_modules/webpack-dev-server/node_modules/is-wsl": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", - "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", "dev": true, "license": "MIT", "dependencies": { @@ -48529,73 +47942,36 @@ } }, "node_modules/webpack-dev-server/node_modules/memfs": { - "version": "4.57.2", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.57.2.tgz", - "integrity": "sha512-2nWzSsJzrukurSDna4Z0WywuScK4Id3tSKejgu74u8KCdW4uNrseKRSIDg75C6Yw5ZRqBe0F0EtMNlTbUq8bAQ==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.17.2.tgz", + "integrity": "sha512-NgYhCOWgovOXSzvYgUW0LQ7Qy72rWQMGGFJDoWg4G30RHd3z77VbYdtJ4fembJXBy8pMIUA31XNAupobOQlwdg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/fs-core": "4.57.2", - "@jsonjoy.com/fs-fsa": "4.57.2", - "@jsonjoy.com/fs-node": "4.57.2", - "@jsonjoy.com/fs-node-builtins": "4.57.2", - "@jsonjoy.com/fs-node-to-fsa": "4.57.2", - "@jsonjoy.com/fs-node-utils": "4.57.2", - "@jsonjoy.com/fs-print": "4.57.2", - "@jsonjoy.com/fs-snapshot": "4.57.2", - "@jsonjoy.com/json-pack": "^1.11.0", - "@jsonjoy.com/util": "^1.9.0", - "glob-to-regex.js": "^1.0.1", - "thingies": "^2.5.0", - "tree-dump": "^1.0.3", + "@jsonjoy.com/json-pack": "^1.0.3", + "@jsonjoy.com/util": "^1.3.0", + "tree-dump": "^1.0.1", "tslib": "^2.0.0" }, + "engines": { + "node": ">= 4.0.0" + }, "funding": { "type": "github", "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/webpack-dev-server/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/webpack-dev-server/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" } }, "node_modules/webpack-dev-server/node_modules/open": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", - "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz", + "integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==", "dev": true, "license": "MIT", "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", - "wsl-utils": "^0.1.0" + "is-wsl": "^3.1.0" }, "engines": { "node": ">=18" @@ -48623,15 +47999,15 @@ } }, "node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { - "version": "7.4.5", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.5.tgz", - "integrity": "sha512-uxQ6YqGdE4hgDKNf7hUiPXOdtkXvBJXrfEGYSx7P7LC8hnUYGK70X6xQXUvXeNyBDDcsiQXpG2m3G9vxowaEuA==", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz", + "integrity": "sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA==", "dev": true, "license": "MIT", "dependencies": { "colorette": "^2.0.10", - "memfs": "^4.43.1", - "mime-types": "^3.0.1", + "memfs": "^4.6.0", + "mime-types": "^2.1.31", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "schema-utils": "^4.0.0" @@ -48652,20 +48028,26 @@ } } }, - "node_modules/webpack-dev-server/node_modules/wsl-utils": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", - "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "node_modules/webpack-dev-server/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "dev": true, "license": "MIT", - "dependencies": { - "is-wsl": "^3.1.0" - }, "engines": { - "node": ">=18" + "node": ">=10.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } } }, "node_modules/webpack-hot-middleware": { @@ -48796,19 +48178,6 @@ "node": ">=0.4.0" } }, - "node_modules/webpack/node_modules/acorn-import-phases": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", - "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - }, - "peerDependencies": { - "acorn": "^8.14.0" - } - }, "node_modules/webpack/node_modules/es-module-lexer": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", @@ -48895,39 +48264,6 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/whatwg-url/node_modules/@exodus/bytes": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", - "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@noble/hashes": "^1.8.0 || ^2.0.0" - }, - "peerDependenciesMeta": { - "@noble/hashes": { - "optional": true - } - } - }, - "node_modules/whatwg-url/node_modules/@noble/hashes": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", - "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/whatwg-url/node_modules/webidl-conversions": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", @@ -49151,9 +48487,9 @@ } }, "node_modules/ws": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", - "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, "license": "MIT", "engines": { @@ -49234,6 +48570,21 @@ "node": ">=18" } }, + "node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/xml-utils": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/xml-utils/-/xml-utils-1.10.2.tgz", @@ -49327,9 +48678,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", "dev": true, "license": "ISC", "bin": { @@ -49337,9 +48688,6 @@ }, "engines": { "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" } }, "node_modules/yargs": { @@ -49363,6 +48711,7 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, "license": "ISC", "engines": { "node": ">=12" @@ -49541,9 +48890,9 @@ } }, "node_modules/yosay/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "license": "MIT", "engines": { "node": ">=12" @@ -49553,9 +48902,9 @@ } }, "node_modules/yosay/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "license": "MIT", "engines": { "node": ">=12" @@ -49565,9 +48914,9 @@ } }, "node_modules/yosay/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" @@ -49589,9 +48938,9 @@ } }, "node_modules/yosay/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", "license": "MIT" }, "node_modules/yosay/node_modules/meow": { @@ -49624,12 +48973,12 @@ } }, "node_modules/yosay/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "license": "MIT", "dependencies": { - "ansi-regex": "^6.2.2" + "ansi-regex": "^6.0.1" }, "engines": { "node": ">=12" @@ -49679,9 +49028,9 @@ } }, "node_modules/zarrita": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/zarrita/-/zarrita-0.7.2.tgz", - "integrity": "sha512-BHP+Z+yemkl9pOogkO1XMOrJ5qI4RNqrmheqJeYtIhpiaW4uvqplYx/jGkMD6edQjIZRQhniFigJZE2oTh7dwQ==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/zarrita/-/zarrita-0.7.3.tgz", + "integrity": "sha512-wChTQ1Ox75INoQCzKAfLWAfB70JJ4KjdW8Sz5x4ZWrFB4Dw+YZdnxHTL0xSdsrB9EmKSeK7fS1Y+I2ibhfGbkw==", "license": "MIT", "dependencies": { "@zarrita/storage": "^0.2.0", @@ -49747,25 +49096,34 @@ "npm": ">= 4.0.0" } }, - "packages/generator-superset/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "packages/generator-superset/node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", "license": "MIT", "engines": { - "node": "18 || 20 || >=22" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "packages/generator-superset/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "packages/generator-superset/node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", "license": "MIT", "dependencies": { - "balanced-match": "^4.0.2" - }, + "@types/ms": "*" + } + }, + "packages/generator-superset/node_modules/@yeoman/namespace": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@yeoman/namespace/-/namespace-2.1.0.tgz", + "integrity": "sha512-/BxsZlALPRp34juAzzh9QUr2hR9w9o+dBSw8sF/iv7NfUU/A/QI0xOyVtQJz2uCPuvtY6nkGDxYxZoKI/lnFQg==", + "license": "MIT", "engines": { - "node": "18 || 20 || >=22" + "node": "^16.13.0 || >=18.12.0" } }, "packages/generator-superset/node_modules/chalk": { @@ -49780,21 +49138,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "packages/generator-superset/node_modules/ejs": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-4.0.1.tgz", - "integrity": "sha512-krvQtxc0btwSm/nvnt1UpnaFDFVJpJ0fdckmALpCgShsr/iGYHTnJiUliZTgmzq/UxTX33TtOQVKaNigMQp/6Q==", - "license": "Apache-2.0", - "dependencies": { - "jake": "^10.9.1" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.12.18" - } - }, "packages/generator-superset/node_modules/execa": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", @@ -49836,6 +49179,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/generator-superset/node_modules/find-up-simple": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", + "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/generator-superset/node_modules/fs-extra": { "version": "11.3.2", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", @@ -49867,6 +49222,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/generator-superset/node_modules/hosted-git-info": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "packages/generator-superset/node_modules/human-signals": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", @@ -49876,6 +49243,18 @@ "node": ">=18.18.0" } }, + "packages/generator-superset/node_modules/index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/generator-superset/node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -49900,56 +49279,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "packages/generator-superset/node_modules/mem-fs-editor": { - "version": "12.0.2", - "resolved": "https://registry.npmjs.org/mem-fs-editor/-/mem-fs-editor-12.0.2.tgz", - "integrity": "sha512-H31IlLheZAEViQpInX2Wv9YdH98hLMOeqiaSmuENIDoXriQBd7KUmqXOgby0Xgy/9MoZvvDly2k9Zm77EdngAA==", - "license": "MIT", - "dependencies": { - "@types/ejs": "^3.1.5", - "@types/picomatch": "^4.0.2", - "binaryextensions": "^6.11.0", - "commondir": "^1.0.1", - "debug": "^4.4.3", - "deep-extend": "^0.6.0", - "ejs": "^4.0.1", - "isbinaryfile": "5.0.7", - "minimatch": "^10.2.4", - "multimatch": "^8.0.0", - "normalize-path": "^3.0.0", - "textextensions": "^6.11.0", - "tinyglobby": "^0.2.15", - "vinyl": "^3.0.1" - }, - "acceptDependencies": { - "isbinaryfile": "^6.0.0" - }, + "packages/generator-superset/node_modules/lru-cache": { + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", + "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@types/node": ">=20", - "mem-fs": "^4.0.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "node": "20 || >=22" } }, - "packages/generator-superset/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "license": "BlueOak-1.0.0", + "packages/generator-superset/node_modules/normalize-package-data": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-8.0.0.tgz", + "integrity": "sha512-RWk+PI433eESQ7ounYxIp67CYuVsS1uYSonX3kA6ps/3LWfjVQa/ptEg6Y3T6uAMq1mWpX9PQ+qx+QaHpsc7gQ==", + "license": "BSD-2-Clause", "dependencies": { - "brace-expansion": "^5.0.5" + "hosted-git-info": "^9.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" }, "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "^20.17.0 || >=22.9.0" } }, "packages/generator-superset/node_modules/npm-run-path": { @@ -49968,6 +49318,35 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/generator-superset/node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/generator-superset/node_modules/parse-json/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/generator-superset/node_modules/path-key": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", @@ -49980,6 +49359,54 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/generator-superset/node_modules/read-package-up": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-12.0.0.tgz", + "integrity": "sha512-Q5hMVBYur/eQNWDdbF4/Wqqr9Bjvtrw2kjGxxBbKLbx8bVCL8gcArjTy8zDUuLGQicftpMuU0riQNcAsbtOVsw==", + "license": "MIT", + "dependencies": { + "find-up-simple": "^1.0.1", + "read-pkg": "^10.0.0", + "type-fest": "^5.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/generator-superset/node_modules/read-pkg": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-10.1.0.tgz", + "integrity": "sha512-I8g2lArQiP78ll51UeMZojewtYgIRCKCWqZEgOO8c/uefTI+XDXvCSXu3+YNUaTNvZzobrL5+SqHjBrByRRTdg==", + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.4", + "normalize-package-data": "^8.0.0", + "parse-json": "^8.3.0", + "type-fest": "^5.4.4", + "unicorn-magic": "^0.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/generator-superset/node_modules/read-pkg/node_modules/unicorn-magic": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.4.0.tgz", + "integrity": "sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/generator-superset/node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -49992,6 +49419,23 @@ "url": "https://github.com/sponsors/isaacs" } }, + "packages/generator-superset/node_modules/simple-git": { + "version": "3.36.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.36.0.tgz", + "integrity": "sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q==", + "license": "MIT", + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "@simple-git/args-pathspec": "^1.0.3", + "@simple-git/argv-parser": "^1.1.0", + "debug": "^4.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/git-js?sponsor=1" + } + }, "packages/generator-superset/node_modules/strip-final-newline": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", @@ -50032,27 +49476,27 @@ } }, "packages/generator-superset/node_modules/yeoman-generator": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/yeoman-generator/-/yeoman-generator-8.1.2.tgz", - "integrity": "sha512-5e9jK2EPm6/c/zaYnuKnCLbRWfD9XwJYup1VzdJMgj010cgDn3YJWDfOIczabLfSoRSXRFxTDHfx1MEdIJEcGQ==", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/yeoman-generator/-/yeoman-generator-8.2.2.tgz", + "integrity": "sha512-GIvRULf09VrTyJ1nMIxCRFTI8gzW9zsAxVXTHOmsWVKZ7QYPdByRQvFtnp0XOObM6dvDSoAwBhuYR6i5inp/ig==", "license": "BSD-2-Clause", "dependencies": { - "@types/debug": "^4.1.12", + "@types/debug": "^4.1.13", "@types/lodash-es": "^4.17.12", - "@yeoman/namespace": "^1.0.1", + "@yeoman/namespace": "^2.1.0", "chalk": "^5.6.2", "debug": "^4.4.3", "execa": "^9.6.1", "latest-version": "^9.0.0", - "lodash-es": "^4.17.23", - "mem-fs-editor": "^12.0.2", + "lodash-es": "^4.18.1", + "mem-fs-editor": "^12.0.4", "minimist": "^1.2.8", "read-package-up": "^12.0.0", "semver": "^7.7.4", - "simple-git": "^3.32.3", + "simple-git": "^3.36.0", "sort-keys": "^6.0.0", "text-table": "^0.2.0", - "type-fest": "^5.4.4" + "type-fest": "^5.6.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" @@ -50275,9 +49719,9 @@ } }, "packages/superset-ui-core/node_modules/@ant-design/icons": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.2.2.tgz", - "integrity": "sha512-zlJtE7AMbG12TeYVPhtBXwNpFInNy8mjLzcIm+0BPw16/b8ODG87YJ1G37VIF5VFscdgfsf6EweAFPTobu/3iQ==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.2.3.tgz", + "integrity": "sha512-Pl3aoAtxQeKryYnt6VvDJtOxMOtA8wrRSACe/pTjOAIG3fdHrWm6Ivb4ku9tsFjYroSXBKirvuxG4QkwBXD9gg==", "license": "MIT", "dependencies": { "@ant-design/colors": "^8.0.1", @@ -50293,18 +49737,37 @@ "react-dom": ">=16.0.0" } }, - "packages/superset-ui-core/node_modules/@ant-design/icons/node_modules/@rc-component/util": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@rc-component/util/-/util-1.10.1.tgz", - "integrity": "sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng==", + "packages/superset-ui-core/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", "license": "MIT", "dependencies": { - "is-mobile": "^5.0.0", - "react-is": "^18.2.0" - }, - "peerDependencies": { - "react": ">=18.0.0", - "react-dom": ">=18.0.0" + "@types/unist": "^2" + } + }, + "packages/superset-ui-core/node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "packages/superset-ui-core/node_modules/ace-builds": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.44.0.tgz", + "integrity": "sha512-PFNMSYqFdEUkul2Ntud0HvA09AgY+F1ag0UYdpMH60wNI/qOA8cB8tlTgoALMEwIdUPJK2CjrIQ7OnbiSS/ugQ==", + "license": "BSD-3-Clause" + }, + "packages/superset-ui-core/node_modules/core-js": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", + "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" } }, "packages/superset-ui-core/node_modules/d3-format": { @@ -50316,6 +49779,771 @@ "node": ">=12" } }, + "packages/superset-ui-core/node_modules/dompurify": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.3.tgz", + "integrity": "sha512-VVwJidIJcp1hpg2OMXML3ZVRPYSZiq4aX7qBh83BSIpOaRDqI+qxhXjjIWnpzkOXhmp0L81lnoME1mnCc9H48A==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "packages/superset-ui-core/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/superset-ui-core/node_modules/mdast-util-find-and-replace": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-2.2.2.tgz", + "integrity": "sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "packages/superset-ui-core/node_modules/mdast-util-from-markdown": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", + "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "mdast-util-to-string": "^3.1.0", + "micromark": "^3.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-decode-string": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-stringify-position": "^3.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "packages/superset-ui-core/node_modules/mdast-util-gfm": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-2.0.2.tgz", + "integrity": "sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-gfm-autolink-literal": "^1.0.0", + "mdast-util-gfm-footnote": "^1.0.0", + "mdast-util-gfm-strikethrough": "^1.0.0", + "mdast-util-gfm-table": "^1.0.0", + "mdast-util-gfm-task-list-item": "^1.0.0", + "mdast-util-to-markdown": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "packages/superset-ui-core/node_modules/mdast-util-gfm-autolink-literal": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-1.0.3.tgz", + "integrity": "sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "ccount": "^2.0.0", + "mdast-util-find-and-replace": "^2.0.0", + "micromark-util-character": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "packages/superset-ui-core/node_modules/mdast-util-gfm-footnote": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-1.0.2.tgz", + "integrity": "sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0", + "micromark-util-normalize-identifier": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "packages/superset-ui-core/node_modules/mdast-util-gfm-strikethrough": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-1.0.3.tgz", + "integrity": "sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "packages/superset-ui-core/node_modules/mdast-util-gfm-table": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-1.0.7.tgz", + "integrity": "sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-to-markdown": "^1.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "packages/superset-ui-core/node_modules/mdast-util-gfm-task-list-item": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-1.0.2.tgz", + "integrity": "sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "packages/superset-ui-core/node_modules/mdast-util-phrasing": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz", + "integrity": "sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "packages/superset-ui-core/node_modules/mdast-util-to-markdown": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz", + "integrity": "sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^3.0.0", + "mdast-util-to-string": "^3.0.0", + "micromark-util-decode-string": "^1.0.0", + "unist-util-visit": "^4.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "packages/superset-ui-core/node_modules/mdast-util-to-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "packages/superset-ui-core/node_modules/micromark": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", + "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "micromark-core-commonmark": "^1.0.1", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "packages/superset-ui-core/node_modules/micromark-core-commonmark": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", + "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-factory-destination": "^1.0.0", + "micromark-factory-label": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-factory-title": "^1.0.0", + "micromark-factory-whitespace": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-html-tag-name": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "packages/superset-ui-core/node_modules/micromark-extension-gfm": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-2.0.3.tgz", + "integrity": "sha512-vb9OoHqrhCmbRidQv/2+Bc6pkP0FrtlhurxZofvOEy5o8RtuuvTq+RQ1Vw5ZDNrVraQZu3HixESqbG+0iKk/MQ==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^1.0.0", + "micromark-extension-gfm-footnote": "^1.0.0", + "micromark-extension-gfm-strikethrough": "^1.0.0", + "micromark-extension-gfm-table": "^1.0.0", + "micromark-extension-gfm-tagfilter": "^1.0.0", + "micromark-extension-gfm-task-list-item": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "packages/superset-ui-core/node_modules/micromark-extension-gfm-autolink-literal": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-1.0.5.tgz", + "integrity": "sha512-z3wJSLrDf8kRDOh2qBtoTRD53vJ+CWIyo7uyZuxf/JAbNJjiHsOpG1y5wxk8drtv3ETAHutCu6N3thkOOgueWg==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "packages/superset-ui-core/node_modules/micromark-extension-gfm-footnote": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-1.1.2.tgz", + "integrity": "sha512-Yxn7z7SxgyGWRNa4wzf8AhYYWNrwl5q1Z8ii+CSTTIqVkmGZF1CElX2JI8g5yGoM3GAman9/PVCUFUSJ0kB/8Q==", + "license": "MIT", + "dependencies": { + "micromark-core-commonmark": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "packages/superset-ui-core/node_modules/micromark-extension-gfm-strikethrough": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-1.0.7.tgz", + "integrity": "sha512-sX0FawVE1o3abGk3vRjOH50L5TTLr3b5XMqnP9YDRb34M0v5OoZhG+OHFz1OffZ9dlwgpTBKaT4XW/AsUVnSDw==", + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "packages/superset-ui-core/node_modules/micromark-extension-gfm-table": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-1.0.7.tgz", + "integrity": "sha512-3ZORTHtcSnMQEKtAOsBQ9/oHp9096pI/UvdPtN7ehKvrmZZ2+bbWhi0ln+I9drmwXMt5boocn6OlwQzNXeVeqw==", + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "packages/superset-ui-core/node_modules/micromark-extension-gfm-tagfilter": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-1.0.2.tgz", + "integrity": "sha512-5XWB9GbAUSHTn8VPU8/1DBXMuKYT5uOgEjJb8gN3mW0PNW5OPHpSdojoqf+iq1xo7vWzw/P8bAHY0n6ijpXF7g==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "packages/superset-ui-core/node_modules/micromark-extension-gfm-task-list-item": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-1.0.5.tgz", + "integrity": "sha512-RMFXl2uQ0pNQy6Lun2YBYT9g9INXtWJULgbt01D/x8/6yJ2qpKyzdZD3pi6UIkzF++Da49xAelVKUeUMqd5eIQ==", + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "packages/superset-ui-core/node_modules/micromark-factory-destination": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", + "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "packages/superset-ui-core/node_modules/micromark-factory-label": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", + "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "packages/superset-ui-core/node_modules/micromark-factory-space": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", + "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "packages/superset-ui-core/node_modules/micromark-factory-title": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", + "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "packages/superset-ui-core/node_modules/micromark-factory-whitespace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", + "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "packages/superset-ui-core/node_modules/micromark-util-character": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", + "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "packages/superset-ui-core/node_modules/micromark-util-chunked": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", + "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "packages/superset-ui-core/node_modules/micromark-util-classify-character": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", + "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "packages/superset-ui-core/node_modules/micromark-util-combine-extensions": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", + "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "packages/superset-ui-core/node_modules/micromark-util-decode-numeric-character-reference": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", + "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "packages/superset-ui-core/node_modules/micromark-util-decode-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", + "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "packages/superset-ui-core/node_modules/micromark-util-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", + "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "packages/superset-ui-core/node_modules/micromark-util-html-tag-name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", + "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "packages/superset-ui-core/node_modules/micromark-util-normalize-identifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", + "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "packages/superset-ui-core/node_modules/micromark-util-resolve-all": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", + "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^1.0.0" + } + }, + "packages/superset-ui-core/node_modules/micromark-util-sanitize-uri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", + "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "packages/superset-ui-core/node_modules/micromark-util-subtokenize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", + "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "packages/superset-ui-core/node_modules/micromark-util-symbol": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", + "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "packages/superset-ui-core/node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, "packages/superset-ui-core/node_modules/react-ace": { "version": "14.0.1", "resolved": "https://registry.npmjs.org/react-ace/-/react-ace-14.0.1.tgz", @@ -50389,6 +50617,22 @@ "react": ">= 0.14.0" } }, + "packages/superset-ui-core/node_modules/remark-gfm": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-3.0.1.tgz", + "integrity": "sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-gfm": "^2.0.0", + "micromark-extension-gfm": "^2.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "packages/superset-ui-core/node_modules/timezone-mock": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/timezone-mock/-/timezone-mock-1.4.2.tgz", @@ -50396,6 +50640,57 @@ "dev": true, "license": "MIT" }, + "packages/superset-ui-core/node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "packages/superset-ui-core/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "packages/superset-ui-core/node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "packages/superset-ui-glyph-core": { + "name": "@superset-ui/glyph-core", + "version": "0.20.3", + "license": "Apache-2.0", + "peerDependencies": { + "@apache-superset/core": "*", + "@superset-ui/chart-controls": "*", + "@superset-ui/core": "*", + "react": "^17.0.2" + } + }, "packages/superset-ui-switchboard": { "name": "@superset-ui/switchboard", "version": "0.20.3", @@ -50416,6 +50711,7 @@ "@emotion/react": "^11.4.1", "@superset-ui/chart-controls": "*", "@superset-ui/core": "*", + "@superset-ui/glyph-core": "*", "react": "^18.2.0" } }, @@ -50452,7 +50748,8 @@ "peerDependencies": { "@apache-superset/core": "*", "@superset-ui/chart-controls": "*", - "@superset-ui/core": "*" + "@superset-ui/core": "*", + "@superset-ui/glyph-core": "*" } }, "plugins/legacy-plugin-chart-chord/node_modules/react": { @@ -50477,6 +50774,7 @@ "@apache-superset/core": "*", "@superset-ui/chart-controls": "*", "@superset-ui/core": "*", + "@superset-ui/glyph-core": "*", "react": "^18.2.0" } }, @@ -50505,6 +50803,7 @@ "@apache-superset/core": "*", "@superset-ui/chart-controls": "*", "@superset-ui/core": "*", + "@superset-ui/glyph-core": "*", "react": "^18.2.0" } }, @@ -50526,6 +50825,26 @@ "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", "license": "ISC" }, + "plugins/legacy-plugin-chart-map-box": { + "name": "@superset-ui/legacy-plugin-chart-map-box", + "version": "0.20.3", + "extraneous": true, + "license": "Apache-2.0", + "dependencies": { + "@math.gl/web-mercator": "^4.1.0", + "prop-types": "^15.8.1", + "react-map-gl": "^6.1.19", + "supercluster": "^8.0.1" + }, + "peerDependencies": { + "@apache-superset/core": "*", + "@superset-ui/chart-controls": "*", + "@superset-ui/core": "*", + "@superset-ui/glyph-core": "*", + "mapbox-gl": "*", + "react": "^17.0.2" + } + }, "plugins/legacy-plugin-chart-paired-t-test": { "name": "@superset-ui/legacy-plugin-chart-paired-t-test", "version": "0.20.3", @@ -50539,6 +50858,7 @@ "@apache-superset/core": "*", "@superset-ui/chart-controls": "*", "@superset-ui/core": "*", + "@superset-ui/glyph-core": "*", "react": "^18.2.0" } }, @@ -50554,6 +50874,7 @@ "@apache-superset/core": "*", "@superset-ui/chart-controls": "*", "@superset-ui/core": "*", + "@superset-ui/glyph-core": "*", "react": "^18.2.0" } }, @@ -50570,6 +50891,7 @@ "@apache-superset/core": "*", "@superset-ui/chart-controls": "*", "@superset-ui/core": "*", + "@superset-ui/glyph-core": "*", "@testing-library/jest-dom": "*", "@testing-library/react": "^14.0.0", "react": "^18.2.0", @@ -50608,6 +50930,7 @@ "@apache-superset/core": "*", "@superset-ui/chart-controls": "*", "@superset-ui/core": "*", + "@superset-ui/glyph-core": "*", "react": "^18.2.0" } }, @@ -50629,13 +50952,13 @@ "extraneous": true, "license": "Apache-2.0", "dependencies": { - "@deck.gl/aggregation-layers": "~9.2.11", + "@deck.gl/aggregation-layers": "~9.2.5", "@deck.gl/core": "~9.2.5", - "@deck.gl/extensions": "~9.2.9", + "@deck.gl/extensions": "~9.2.5", "@deck.gl/geo-layers": "~9.2.5", "@deck.gl/layers": "~9.2.5", "@deck.gl/mesh-layers": "~9.2.5", - "@deck.gl/react": "~9.2.11", + "@deck.gl/react": "~9.2.5", "@luma.gl/constants": "~9.2.5", "@luma.gl/core": "~9.2.5", "@luma.gl/engine": "~9.2.6", @@ -50650,8 +50973,8 @@ "d3-array": "^3.2.4", "d3-color": "^3.1.0", "d3-scale": "^4.0.2", - "handlebars": "^4.7.9", - "lodash": "^4.18.1", + "handlebars": "^4.7.8", + "lodash": "^4.17.23", "mousetrap": "^1.6.5", "ngeohash": "^0.6.3", "prop-types": "^15.8.1", @@ -50669,6 +50992,7 @@ "@apache-superset/core": "*", "@superset-ui/chart-controls": "*", "@superset-ui/core": "*", + "@superset-ui/glyph-core": "*", "dayjs": "^1.11.19", "mapbox-gl": "*", "react": "^17.0.2", @@ -50694,10 +51018,20 @@ "@apache-superset/core": "*", "@superset-ui/chart-controls": "*", "@superset-ui/core": "*", + "@superset-ui/glyph-core": "*", "dayjs": "^1.11.19", "react": "^18.2.0" } }, + "plugins/legacy-preset-chart-nvd3/node_modules/dompurify": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.3.tgz", + "integrity": "sha512-VVwJidIJcp1hpg2OMXML3ZVRPYSZiq4aX7qBh83BSIpOaRDqI+qxhXjjIWnpzkOXhmp0L81lnoME1mnCc9H48A==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "plugins/plugin-chart-ag-grid-table": { "name": "@superset-ui/plugin-chart-ag-grid-table", "version": "0.20.3", @@ -50760,6 +51094,7 @@ "@reduxjs/toolkit": "*", "@superset-ui/chart-controls": "*", "@superset-ui/core": "*", + "@superset-ui/glyph-core": "*", "@types/react-redux": "*", "geostyler": "^18.3.1", "geostyler-data": "^1.0.0", @@ -50788,6 +51123,7 @@ "@apache-superset/core": "*", "@superset-ui/chart-controls": "*", "@superset-ui/core": "*", + "@superset-ui/glyph-core": "*", "dayjs": "^1.11.19", "echarts": "*", "memoize-one": "*", @@ -50819,9 +51155,9 @@ } }, "plugins/plugin-chart-echarts/node_modules/zod": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.1.tgz", - "integrity": "sha512-a6ENMBBGZBsnlSebQ/eKCguSBeGKSf4O7BPnqVPmYGtpBYI7VSqoVqw+QcB7kPRjbqPwhYTpFbVj/RqNz/CT0Q==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -50845,6 +51181,7 @@ "@apache-superset/core": "*", "@superset-ui/chart-controls": "*", "@superset-ui/core": "*", + "@superset-ui/glyph-core": "*", "ace-builds": "^1.4.14", "dayjs": "^1.11.19", "handlebars": "^4.7.8", @@ -50854,6 +51191,12 @@ "react-dom": "^18.2.0" } }, + "plugins/plugin-chart-handlebars/node_modules/currencyformatter.js": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/currencyformatter.js/-/currencyformatter.js-1.0.5.tgz", + "integrity": "sha512-gNhjgPges50sAHOb56BeEOi33w88sED2nSiY0s9niq1S/64IKB8DB1EmJh8wv5PofFXpHWG91yptoDQAj5GI2w==", + "license": "MIT" + }, "plugins/plugin-chart-handlebars/node_modules/just-handlebars-helpers": { "version": "1.0.19", "resolved": "https://registry.npmjs.org/just-handlebars-helpers/-/just-handlebars-helpers-1.0.19.tgz", @@ -50906,13 +51249,13 @@ } }, "plugins/plugin-chart-point-cluster-map/node_modules/react-map-gl": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/react-map-gl/-/react-map-gl-8.1.0.tgz", - "integrity": "sha512-vDx/QXR3Tb+8/ap/z6gdMjJQ8ZEyaZf6+uMSPz7jhWF5VZeIsKsGfPvwHVPPwGF43Ryn+YR4bd09uEFNR5OPdg==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/react-map-gl/-/react-map-gl-8.1.1.tgz", + "integrity": "sha512-aSqFAFoxvY7wxbGI93Dz0E41171mkAb3GcNbnkFIotmu88OFw495os6mIDZSi7irYNT/PZEIOEHUxhun4ToGuQ==", "license": "MIT", "dependencies": { - "@vis.gl/react-mapbox": "8.1.0", - "@vis.gl/react-maplibre": "8.1.0" + "@vis.gl/react-mapbox": "8.1.1", + "@vis.gl/react-maplibre": "8.1.1" }, "peerDependencies": { "mapbox-gl": ">=1.13.0", @@ -51034,6 +51377,7 @@ "@luma.gl/shadertools": "~9.2.6", "@luma.gl/webgl": "~9.2.6", "@mapbox/geojson-extent": "^1.0.1", + "@mapbox/tiny-sdf": "^2.0.7", "@math.gl/web-mercator": "^4.1.0", "@types/d3-array": "^3.2.2", "@types/geojson": "^7946.0.16", @@ -51062,6 +51406,7 @@ "@apache-superset/core": "*", "@superset-ui/chart-controls": "*", "@superset-ui/core": "*", + "@superset-ui/glyph-core": "*", "dayjs": "^1.11.19", "mapbox-gl": ">=1.0.0", "react": "^18.2.0", @@ -51073,18 +51418,39 @@ } } }, - "plugins/preset-chart-deckgl/node_modules/@deck.gl/mapbox": { - "version": "9.3.2", - "resolved": "https://registry.npmjs.org/@deck.gl/mapbox/-/mapbox-9.3.2.tgz", - "integrity": "sha512-+T9pJwsOXwjUxyGN6oiBMfIs28VtDIG1V1Rqz4qqn4TjjNEFFw+xO0olJIg8FO5IAqw2OtePdsrMj0tX8tHdGQ==", + "plugins/preset-chart-deckgl/node_modules/@deck.gl/aggregation-layers": { + "version": "9.2.11", + "resolved": "https://registry.npmjs.org/@deck.gl/aggregation-layers/-/aggregation-layers-9.2.11.tgz", + "integrity": "sha512-MRFbBHtMcDkOthxXnMPm6nF08DjFDACaIQsJSyHkdWtLUTSLHsWnOTn/8QbB4ka86WyNyfJy3dibLu/m3ei2ow==", "license": "MIT", "dependencies": { - "@math.gl/web-mercator": "^4.1.0" + "@luma.gl/constants": "~9.2.6", + "@luma.gl/shadertools": "~9.2.6", + "@math.gl/core": "^4.1.0", + "@math.gl/web-mercator": "^4.1.0", + "d3-hexbin": "^0.2.1" }, "peerDependencies": { - "@deck.gl/core": "~9.3.0", - "@luma.gl/core": "~9.3.3", - "@math.gl/web-mercator": "^4.1.0" + "@deck.gl/core": "~9.2.0", + "@deck.gl/layers": "~9.2.0", + "@luma.gl/core": "~9.2.6", + "@luma.gl/engine": "~9.2.6" + } + }, + "plugins/preset-chart-deckgl/node_modules/@deck.gl/extensions": { + "version": "9.2.11", + "resolved": "https://registry.npmjs.org/@deck.gl/extensions/-/extensions-9.2.11.tgz", + "integrity": "sha512-zlpM4Bg1ifBziW1Juiii9NY5gyW2rEhyVTWnhagH/bpTCZ2E73OhnToYt1ouqmoxL6lMtIjhRXz6LPb7tJbHHQ==", + "license": "MIT", + "dependencies": { + "@luma.gl/constants": "~9.2.6", + "@luma.gl/shadertools": "~9.2.6", + "@math.gl/core": "^4.1.0" + }, + "peerDependencies": { + "@deck.gl/core": "~9.2.0", + "@luma.gl/core": "~9.2.6", + "@luma.gl/engine": "~9.2.6" } }, "plugins/preset-chart-deckgl/node_modules/d3-array": { @@ -51099,6 +51465,30 @@ "node": ">=12" } }, + "plugins/preset-chart-deckgl/node_modules/react-map-gl": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/react-map-gl/-/react-map-gl-8.1.1.tgz", + "integrity": "sha512-aSqFAFoxvY7wxbGI93Dz0E41171mkAb3GcNbnkFIotmu88OFw495os6mIDZSi7irYNT/PZEIOEHUxhun4ToGuQ==", + "license": "MIT", + "dependencies": { + "@vis.gl/react-mapbox": "8.1.1", + "@vis.gl/react-maplibre": "8.1.1" + }, + "peerDependencies": { + "mapbox-gl": ">=1.13.0", + "maplibre-gl": ">=1.13.0", + "react": ">=16.3.0", + "react-dom": ">=16.3.0" + }, + "peerDependenciesMeta": { + "mapbox-gl": { + "optional": true + }, + "maplibre-gl": { + "optional": true + } + } + }, "tools/eslint-i18n-strings": { "version": "1.0.0", "extraneous": true, diff --git a/superset-frontend/package.json b/superset-frontend/package.json index a4655ded486..cfdc070ff5a 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -109,7 +109,9 @@ "@emotion/cache": "^11.4.0", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", + "@fontsource/fira-code": "^5.2.7", "@fontsource/ibm-plex-mono": "^5.2.7", + "@fontsource/inter": "^5.2.8", "@googleapis/sheets": "^13.0.1", "@luma.gl/constants": "~9.2.5", "@luma.gl/core": "~9.2.5", @@ -123,6 +125,7 @@ "@jsonforms/core": "^3.7.0", "@jsonforms/react": "^3.7.0", "@jsonforms/vanilla-renderers": "^3.7.0", + "@react-spring/web": "^10.0.3", "@reduxjs/toolkit": "^1.9.3", "@rjsf/antd": "^5.24.13", "@rjsf/core": "^5.24.13", @@ -199,6 +202,7 @@ "nanoid": "^5.1.11", "ol": "^10.9.0", "pretty-ms": "^9.3.0", + "prop-types": "^15.8.1", "query-string": "9.3.1", "re-resizable": "^6.11.2", "react": "^18.2.0", diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts index e1d656dc144..0165a280f5b 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts @@ -441,6 +441,8 @@ export interface ControlPanelConfig { sectionOverrides?: SectionOverrides; onInit?: (state: ControlStateMapping) => void; formDataOverrides?: (formData: QueryFormData) => QueryFormData; + /** @internal Raw glyph argument definitions from defineChart() – used for native control panel rendering */ + _glyphArgs?: unknown; } export type ControlOverrides = { diff --git a/superset-frontend/packages/superset-ui-core/src/chart/types/Base.ts b/superset-frontend/packages/superset-ui-core/src/chart/types/Base.ts index 15459d11148..4a99bfa34a9 100644 --- a/superset-frontend/packages/superset-ui-core/src/chart/types/Base.ts +++ b/superset-frontend/packages/superset-ui-core/src/chart/types/Base.ts @@ -33,6 +33,14 @@ export enum Behavior { */ DrillToDetail = 'DRILL_TO_DETAIL', DrillBy = 'DRILL_BY', + + /** + * Include `ALLOWS_EMPTY_RESULTS` behavior if the chart handles empty/no data + * gracefully (e.g., showing a drop zone for drag-and-drop configuration). + * Charts with this behavior will receive empty data instead of seeing + * the "No results" message. + */ + AllowsEmptyResults = 'ALLOWS_EMPTY_RESULTS', } export interface ContextMenuFilters { diff --git a/superset-frontend/packages/superset-ui-glyph-core/GLYPH_MIGRATION_GUIDE.md b/superset-frontend/packages/superset-ui-glyph-core/GLYPH_MIGRATION_GUIDE.md new file mode 100644 index 00000000000..b677b6555ab --- /dev/null +++ b/superset-frontend/packages/superset-ui-glyph-core/GLYPH_MIGRATION_GUIDE.md @@ -0,0 +1,335 @@ +# Glyph Pattern Migration Guide + +This guide documents how to migrate traditional Superset chart plugins to the single-file Glyph pattern. + +## Overview + +The Glyph pattern simplifies chart plugin development by: +- **Arguments define BOTH controls AND render props** - No separate files needed +- **No `controlPanel.ts`** - Generated from argument definitions +- **No `transformProps.ts`** - Arguments are passed directly to render +- **No `buildQuery.ts`** - Inferred from Metric/Dimension/Temporal arguments +- **Single file** - Everything in one place (~200 lines vs 500+ across multiple files) + +## Migration Steps + +### 1. Analyze the Existing Chart + +Identify from the original chart: +- **Metrics/Dimensions**: What data does it query? +- **Controls**: What options does the user configure? +- **Styling**: What visual customizations exist? +- **Rendering**: How is the data displayed? + +### 2. Create the Glyph Chart File + +Create a new file: `src/BigNumber/BigNumberGlyph/index.tsx` + +```typescript +import { t } from '@apache-superset/core'; +import { styled } from '@apache-superset/core/ui'; +import { Behavior, getNumberFormatter, CurrencyFormatter } from '@superset-ui/core'; + +import { + defineChart, + Metric, + Select, + Text, + Checkbox, + NumberFormat, + Currency, + TimeFormat, + ConditionalFormatting, +} from '@superset-ui/glyph-core'; +``` + +### 3. Define Arguments (Controls + Props) + +**CRITICAL: Use camelCase for argument names!** + +Superset converts control names to camelCase in `formData`. If you use snake_case (`show_metric_name`), it won't match the camelCase key in formData (`showMetricName`). + +```typescript +arguments: { + // Data arguments + metric: Metric.with({ label: t('Metric') }), + + // Visual arguments - USE CAMELCASE! + headerFontSize: Select.with({ + label: t('Font Size'), + options: [ + { label: t('Small'), value: 0.2 }, + { label: t('Large'), value: 0.4 }, + ], + default: 0.4, + }), + + showMetricName: Checkbox.with({ + label: t('Show Metric Name'), + default: false, + }), + + // Declarative visibility (preferred) + metricNameFontSize: { + arg: Select.with({ ... }), + visibleWhen: { showMetricName: true }, + }, + + // Declarative disabled state + subtitleFontSize: { + arg: Select.with({ ... }), + disabledWhen: { subtitle: '' }, + }, +} +``` + +### 4. Available Argument Types + +| Type | Control Generated | Value Type | Properties | +|------|------------------|------------|------------| +| `Metric` | MetricControl | `{ value, name, formattedValue }` | `label` | +| `Dimension` | GroupByControl | `string[]` | `label` | +| `Temporal` | TemporalControl | `string` | `label` | +| `Select` | SelectControl | `string \| number` | `label`, `description`, `options`, `default` | +| `Text` | TextControl | `string` | `label`, `description`, `default`, `placeholder` | +| `Checkbox` | CheckboxControl | `boolean` | `label`, `description`, `default` | +| `Int` | SliderControl | `number` | `label`, `description`, `default`, `min`, `max`, `step` | +| `Color` | ColorPickerControl | `string` (hex) | `label`, `description`, `default` | +| `NumberFormat` | SelectControl (freeform) | `string` | `label`, `description`, `default` | +| `Currency` | CurrencyControl | `{ symbol?, symbolPosition? }` | `label`, `description`, `default` | +| `TimeFormat` | SelectControl (freeform) | `string` | `label`, `description`, `default` | +| `ConditionalFormatting` | ConditionalFormattingControl | `Rule[]` | `label`, `description` | + +### 5. Declarative Visibility & Disabled States + +Instead of Redux `mapStateToProps`, use declarative conditions: + +```typescript +// Simple equality check - visible when showMetricName is true +metricNameFontSize: { + arg: Select.with({ ... }), + visibleWhen: { showMetricName: true }, +}, + +// Function check - visible when subtitle is not empty +subtitleFontSize: { + arg: Select.with({ ... }), + visibleWhen: { subtitle: (val) => !!val }, +}, + +// Multiple conditions (AND) - visible when both conditions are met +advancedOption: { + arg: Checkbox.with({ ... }), + visibleWhen: { + showMetricName: true, + subtitle: (val) => !!val, + }, +}, + +// Disabled state (control visible but not editable) +formatOption: { + arg: Select.with({ ... }), + disabledWhen: { forceTimestampFormatting: true }, +}, +``` + +### 6. Number, Currency, and Time Formatting + +Use the built-in format argument types: + +```typescript +arguments: { + numberFormat: NumberFormat.with({ + label: t('Number Format'), + description: t('D3 format string'), + default: 'SMART_NUMBER', + }), + + currencyFormat: Currency.with({ + label: t('Currency Format'), + }), + + timeFormat: TimeFormat.with({ + label: t('Date Format'), + default: 'smart_date', + }), +} +``` + +Then use them directly in the render function: + +```typescript +render: ({ numberFormat, currencyFormat, timeFormat, metric }) => { + const formatter = currencyFormat?.symbol + ? new CurrencyFormatter({ + currency: { symbol: currencyFormat.symbol, symbolPosition: currencyFormat.symbolPosition ?? 'prefix' }, + d3Format: numberFormat, + }) + : getNumberFormatter(numberFormat); + + return
{formatter(metric.value)}
; +} +``` + +### 7. Conditional Formatting (Colors) + +Use `ConditionalFormatting` for color-based rules: + +```typescript +import { getColorFormatters } from '@superset-ui/chart-controls'; + +arguments: { + conditionalFormatting: ConditionalFormatting.with({ + label: t('Conditional Formatting'), + description: t('Apply conditional color formatting to metric'), + }), +}, + +render: ({ conditionalFormatting, metric, data, theme }) => { + let numberColor: string | undefined; + + if (conditionalFormatting?.length > 0 && metric.value != null) { + const colorFormatters = getColorFormatters(conditionalFormatting, data, theme, false); + if (colorFormatters) { + for (const formatter of colorFormatters) { + const color = formatter.getColorFromValue(metric.value as number); + if (color) { + numberColor = color; + break; + } + } + } + } + + return {metric.formattedValue}; +} +``` + +### 8. Styled Components + +Use Superset's theme properties with template literal syntax: + +```typescript +const Container = styled.div<{ height: number }>` + ${({ theme, height }) => ` + height: ${height}px; + padding: ${theme.sizeUnit * 4}px; + font-family: ${theme.fontFamily}; + color: ${theme.colorText}; + `} +`; +``` + +**Common theme properties:** +| Property | Description | +|----------|-------------| +| `theme.sizeUnit` | Base spacing unit (typically 4px) | +| `theme.fontFamily` | Default font family | +| `theme.fontWeightNormal` | Normal font weight | +| `theme.fontWeightLight` | Light font weight | +| `theme.fontSizeSM` | Small font size | +| `theme.colorText` | Primary text color | +| `theme.colorTextTertiary` | Muted/secondary text color | +| `theme.borderRadius` | Standard border radius | + +### 9. Render Function + +The render function receives all arguments directly - no formData lookup needed: + +```typescript +render: ({ + metric, + headerFontSize, + showMetricName, + numberFormat, + currencyFormat, + conditionalFormatting, + height, + data, + theme, +}) => { + // All arguments are directly available! + const formatter = currencyFormat?.symbol + ? new CurrencyFormatter({ currency: currencyFormat, d3Format: numberFormat }) + : getNumberFormatter(numberFormat); + + const formattedValue = metric.value != null + ? formatter(metric.value as number) + : t('No data'); + + return ( + + {showMetricName && {metric.name}} + {formattedValue} + + ); +}, +``` + +### 10. Register the Plugin + +In `BigNumber/index.ts`: +```typescript +export { default as BigNumberGlyphChartPlugin } from './BigNumberGlyph'; +``` + +In `plugin-chart-echarts/src/index.ts`: +```typescript +export { BigNumberGlyphChartPlugin } from './BigNumber'; +``` + +In `MainPreset.js`: +```typescript +import { BigNumberGlyphChartPlugin } from '@superset-ui/plugin-chart-echarts'; + +new BigNumberGlyphChartPlugin().configure({ key: 'big_number_glyph' }), +``` + +## Common Pitfalls + +### 1. Snake Case vs Camel Case +- **WRONG**: `show_metric_name` - won't match formData +- **RIGHT**: `showMetricName` - matches Superset's camelCase conversion + +### 2. Theme Undefined +- **WRONG**: `theme.gridUnit` - crashes if theme is undefined +- **RIGHT**: `theme?.gridUnit ?? 4` - safe with fallback + +### 3. Metric Value Extraction +The Glyph core automatically extracts metric values from query results. The `metric` argument provides: +- `metric.value` - The raw numeric value +- `metric.name` - The metric label/name +- `metric.formattedValue` - Basic string representation + +### 4. Visibility vs Legacy Functions +- **Prefer**: `visibleWhen: { showMetricName: true }` - declarative, clean +- **Legacy**: `visibility: ({ controls }) => controls?.showMetricName?.value === true` - still works + +## File Structure Comparison + +### Traditional (5+ files, ~500 lines) +``` +BigNumberTotal/ +├── index.ts # Plugin registration +├── controlPanel.ts # Control definitions (~100 lines) +├── transformProps.ts # Data transformation (~150 lines) +├── buildQuery.ts # Query building (~50 lines) +├── BigNumberViz.tsx # React component (~150 lines) +└── types.ts # TypeScript types (~50 lines) +``` + +### Glyph Pattern (1 file, ~250 lines) +``` +BigNumberGlyph/ +└── index.tsx # Everything in one file! +``` + +## Complete Example + +See `BigNumber/BigNumberGlyph/index.tsx` for a complete working example with: +- Metric display +- Number/currency/time formatting +- Conditional color formatting +- Declarative visibility +- Subtitle support +- Font size controls diff --git a/superset-frontend/packages/superset-ui-glyph-core/package.json b/superset-frontend/packages/superset-ui-glyph-core/package.json new file mode 100644 index 00000000000..cc747c71ac8 --- /dev/null +++ b/superset-frontend/packages/superset-ui-glyph-core/package.json @@ -0,0 +1,38 @@ +{ + "name": "@superset-ui/glyph-core", + "version": "0.20.3", + "description": "Glyph Core - A declarative visualization plugin framework for Apache Superset", + "sideEffects": false, + "main": "lib/index.js", + "module": "esm/index.js", + "files": [ + "esm", + "lib" + ], + "repository": { + "type": "git", + "url": "https://github.com/apache/superset.git", + "directory": "superset-frontend/packages/superset-ui-glyph-core" + }, + "keywords": [ + "superset", + "glyph", + "visualization", + "chart" + ], + "author": "Superset", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/apache/superset/issues" + }, + "homepage": "https://github.com/apache/superset#readme", + "publishConfig": { + "access": "public" + }, + "peerDependencies": { + "@apache-superset/core": "*", + "@superset-ui/chart-controls": "*", + "@superset-ui/core": "*", + "react": "^17.0.2" + } +} diff --git a/superset-frontend/packages/superset-ui-glyph-core/src/arguments.ts b/superset-frontend/packages/superset-ui-glyph-core/src/arguments.ts new file mode 100644 index 00000000000..58c60a55e1c --- /dev/null +++ b/superset-frontend/packages/superset-ui-glyph-core/src/arguments.ts @@ -0,0 +1,646 @@ +/** + * 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 { + ColumnType, + SelectOptions, + SelectOption, + TextOptions, + CheckboxOptions, + IntOptions, + ColorOptions, + MetricOptions, + DimensionOptions, + NumberFormatOptions, + CurrencyOptions, + CurrencyValue, + TimeFormatOptions, + ConditionalFormattingOptions, + ConditionalFormattingRule, + SliderOptions, + BoundsOptions, + BoundsValue, + ColorPickerOptions, + RadioButtonOptions, + RadioOption, + RgbaColor, +} from './types'; + +/** + * Base Argument class - all argument types extend from this. + * + * Arguments define: + * 1. What the chart needs (semantically) + * 2. How to render controls in the control panel + * 3. Default values and validation + */ +export class Argument { + static label: string | null = null; + static description: string | null = null; + static columnType: ColumnType = ColumnType.Argument; + static controlType: string = 'TextControl'; + + value: unknown; + + constructor(value: unknown) { + this.value = value; + } +} + +/** + * Metric - represents a numeric aggregation (SUM, COUNT, AVG, etc.) + * + * Maps to Superset's MetricsControl in the query section. + */ +export class Metric extends Argument { + static override label: string | null = 'Metric'; + static override description: string | null = + 'A numeric aggregation (SUM, COUNT, AVG, etc.)'; + static override columnType = ColumnType.Metric; + static override controlType = 'MetricsControl'; + static multi = false; + + static with(options: MetricOptions): typeof Metric { + const Base = this; + return class extends Base { + static override label = options.label ?? Base.label; + static override description = options.description ?? Base.description; + static override multi = options.multi ?? Base.multi; + }; + } +} + +/** + * Dimension - represents a categorical column for grouping data + * + * Maps to Superset's GroupByControl in the query section. + */ +export class Dimension extends Argument { + static override label: string | null = 'Dimension'; + static override description: string | null = + 'A categorical column for grouping data'; + static override columnType = ColumnType.Dimension; + static override controlType = 'GroupByControl'; + static multi = true; + + static with(options: DimensionOptions): typeof Dimension { + const Base = this; + return class extends Base { + static override label = options.label ?? Base.label; + static override description = options.description ?? Base.description; + static override multi = options.multi ?? Base.multi; + }; + } +} + +/** + * Temporal - represents a time column + * + * Maps to Superset's temporal controls (x_axis, time_grain_sqla). + */ +export class Temporal extends Argument { + static override label: string | null = 'Time Column'; + static override description: string | null = + 'A temporal column for time series data'; + static override columnType = ColumnType.Temporal; + static override controlType = 'TemporalControl'; + + static with(options: { + label?: string; + description?: string; + }): typeof Temporal { + const Base = this; + return class extends Base { + static override label = options.label ?? Base.label; + static override description = options.description ?? Base.description; + }; + } +} + +/** + * Select - dropdown selection from predefined options + * + * Maps to Superset's SelectControl. + */ +export class Select extends Argument { + static override label: string | null = 'Select'; + static override description: string | null = 'Choose from options'; + static override controlType = 'SelectControl'; + static default: string | number = ''; + static options: SelectOption[] = []; + static clearable = false; + + static with(options: SelectOptions): typeof Select { + const Base = this; + return class extends Base { + static override label = options.label ?? Base.label; + static override description = options.description ?? Base.description; + static override default = options.default ?? Base.default; + static override options = options.options ?? Base.options; + }; + } +} + +/** + * Text - free-form text input + * + * Maps to Superset's TextControl. + */ +export class Text extends Argument { + static override label: string | null = 'Text'; + static override description: string | null = 'Text input'; + static override controlType = 'TextControl'; + static default: string = ''; + static placeholder: string = ''; + + static with(options: TextOptions): typeof Text { + const Base = this; + return class extends Base { + static override label = options.label ?? Base.label; + static override description = options.description ?? Base.description; + static override default = options.default ?? Base.default; + static override placeholder = options.placeholder ?? Base.placeholder; + }; + } +} + +/** + * Checkbox - boolean toggle + * + * Maps to Superset's CheckboxControl. + */ +export class Checkbox extends Argument { + static override label: string | null = 'Checkbox'; + static override description: string | null = 'Toggle option'; + static override controlType = 'CheckboxControl'; + static default: boolean = false; + + static with(options: CheckboxOptions): typeof Checkbox { + const Base = this; + return class extends Base { + static override label = options.label ?? Base.label; + static override description = options.description ?? Base.description; + static override default = options.default ?? Base.default; + }; + } +} + +/** + * Int - numeric input with slider + * + * Maps to Superset's SliderControl. + */ +export class Int extends Argument { + static override label: string | null = 'Integer'; + static override description: string | null = 'A numeric value'; + static override controlType = 'SliderControl'; + static default: number = 0; + static min: number = 0; + static max: number = 100; + static step: number = 1; + + static with(options: IntOptions): typeof Int { + const Base = this; + return class extends Base { + static override label = options.label ?? Base.label; + static override description = options.description ?? Base.description; + static override default = options.default ?? Base.default; + static override min = options.min ?? Base.min; + static override max = options.max ?? Base.max; + static override step = options.step ?? Base.step; + }; + } +} + +/** + * Color - color picker + * + * Maps to Superset's ColorPickerControl. + */ +export class Color extends Argument { + static override label: string | null = 'Color'; + static override description: string | null = 'A color value'; + static override controlType = 'ColorPickerControl'; + // eslint-disable-next-line theme-colors/no-literal-colors + static default: string = '#000000'; + + static with(options: ColorOptions): typeof Color { + const Base = this; + return class extends Base { + static override label = options.label ?? Base.label; + static override description = options.description ?? Base.description; + static override default = options.default ?? Base.default; + }; + } +} + +/** + * NumberFormat - D3 number format string selection + * + * Maps to Superset's SelectControl with D3 format options. + * Allows freeform input for custom formats. + */ +export class NumberFormat extends Argument { + static override label: string | null = 'Number Format'; + static override description: string | null = + 'D3 format string for number display (e.g., ".2f", ".1%", ",.0f")'; + static override controlType = 'NumberFormatControl'; + static default: string = 'SMART_NUMBER'; + + // Standard D3 format options + static readonly FORMAT_OPTIONS: SelectOption[] = [ + { label: 'Adaptive formatting', value: 'SMART_NUMBER' }, + { label: 'Original value', value: '~g' }, + { label: '12,345.432', value: ',.3f' }, + { label: '12,345.43', value: ',.2f' }, + { label: '12,345.4', value: ',.1f' }, + { label: '12,345', value: ',.0f' }, + { label: '12345.432', value: '.3f' }, + { label: '12345.43', value: '.2f' }, + { label: '12345.4', value: '.1f' }, + { label: '12345', value: '.0f' }, + { label: '12K', value: '.0s' }, + { label: '12.3K', value: '.1s' }, + { label: '12.35K', value: '.2s' }, + { label: '12.346K', value: '.3s' }, + { label: '1234543.21%', value: '.2%' }, + { label: '1234543%', value: '.0%' }, + { label: '12.34%', value: '.2r' }, + { label: '+12,345.4', value: '+,.1f' }, + { label: '$12,345.43', value: '$,.2f' }, + { label: 'Duration (1m 6s)', value: 'DURATION' }, + { label: 'Duration (1ms 400µs)', value: 'DURATION_SUB' }, + ]; + + static with(options: NumberFormatOptions): typeof NumberFormat { + const Base = this; + return class extends Base { + static override label = options.label ?? Base.label; + static override description = options.description ?? Base.description; + static override default = options.default ?? Base.default; + }; + } +} + +/** + * Currency - currency format with symbol and position + * + * Maps to Superset's CurrencyControl. + * Value is { symbol: 'USD', symbolPosition: 'prefix' | 'suffix' } + */ +export class Currency extends Argument { + static override label: string | null = 'Currency Format'; + static override description: string | null = + 'Currency symbol and position for formatting'; + static override controlType = 'CurrencyControl'; + static default: CurrencyValue = {}; + + static with(options: CurrencyOptions): typeof Currency { + const Base = this; + return class extends Base { + static override label = options.label ?? Base.label; + static override description = options.description ?? Base.description; + static override default = options.default ?? Base.default; + }; + } +} + +/** + * TimeFormat - D3 time format string selection + * + * Maps to Superset's SelectControl with D3 time format options. + * Allows freeform input for custom formats. + */ +export class TimeFormat extends Argument { + static override label: string | null = 'Time Format'; + static override description: string | null = + 'D3 time format string (e.g., "%Y-%m-%d", "%H:%M:%S")'; + static override controlType = 'TimeFormatControl'; + static default: string = 'smart_date'; + + // Standard D3 time format options + static readonly FORMAT_OPTIONS: SelectOption[] = [ + { label: 'Adaptive formatting', value: 'smart_date' }, + { label: '%d/%m/%Y | 14/01/2019', value: '%d/%m/%Y' }, + { label: '%m/%d/%Y | 01/14/2019', value: '%m/%d/%Y' }, + { label: '%d.%m.%Y | 14.01.2019', value: '%d.%m.%Y' }, + { label: '%Y-%m-%d | 2019-01-14', value: '%Y-%m-%d' }, + { + label: '%Y-%m-%d %H:%M:%S | 2019-01-14 01:32:10', + value: '%Y-%m-%d %H:%M:%S', + }, + { + label: '%d-%m-%Y %H:%M:%S | 14-01-2019 01:32:10', + value: '%d-%m-%Y %H:%M:%S', + }, + { label: '%H:%M:%S | 01:32:10', value: '%H:%M:%S' }, + ]; + + static with(options: TimeFormatOptions): typeof TimeFormat { + const Base = this; + return class extends Base { + static override label = options.label ?? Base.label; + static override description = options.description ?? Base.description; + static override default = options.default ?? Base.default; + }; + } +} + +/** + * ConditionalFormatting - apply color rules based on metric values + * + * This is a special argument type that encapsulates the complex + * mapStateToProps logic needed for conditional formatting controls. + * The control automatically receives numeric column options from the chart response. + */ +export class ConditionalFormatting extends Argument { + static override label: string | null = 'Conditional Formatting'; + static override description: string | null = + 'Apply conditional color formatting to metric values'; + static override controlType = 'ConditionalFormattingControl'; + static default: ConditionalFormattingRule[] = []; + + static with( + options: ConditionalFormattingOptions, + ): typeof ConditionalFormatting { + const Base = this; + return class extends Base { + static override label = options.label ?? Base.label; + static override description = options.description ?? Base.description; + }; + } +} + +/** + * Slider - continuous floating point values with min/max/step + * + * Similar to Int but for float values. + * Maps to Superset's SliderControl. + */ +export class Slider extends Argument { + static override label: string | null = 'Slider'; + static override description: string | null = 'A continuous numeric value'; + static override controlType = 'SliderControl'; + static default: number = 0; + static min: number = 0; + static max: number = 1; + static step: number = 0.1; + + static with(options: SliderOptions): typeof Slider { + const Base = this; + return class extends Base { + static override label = options.label ?? Base.label; + static override description = options.description ?? Base.description; + static override default = options.default ?? Base.default; + static override min = options.min ?? Base.min; + static override max = options.max ?? Base.max; + static override step = options.step ?? Base.step; + }; + } +} + +/** + * Bounds - min/max value pairs + * + * Used for axis bounds, value ranges, etc. + * Maps to Superset's BoundsControl. + */ +export class Bounds extends Argument { + static override label: string | null = 'Bounds'; + static override description: string | null = 'Min and max value bounds'; + static override controlType = 'BoundsControl'; + static default: BoundsValue = [null, null]; + + static with(options: BoundsOptions): typeof Bounds { + const Base = this; + return class extends Base { + static override label = options.label ?? Base.label; + static override description = options.description ?? Base.description; + static override default = options.default ?? Base.default; + }; + } +} + +/** + * ColorPicker - RGBA color selection + * + * Different from Color (which uses hex strings). + * Maps to Superset's ColorPickerControl with RGBA format. + */ +export class ColorPicker extends Argument { + static override label: string | null = 'Color'; + static override description: string | null = 'Select a color'; + static override controlType = 'ColorPickerControl'; + static default: RgbaColor = { r: 0, g: 0, b: 0, a: 1 }; + + static with(options: ColorPickerOptions): typeof ColorPicker { + const Base = this; + return class extends Base { + static override label = options.label ?? Base.label; + static override description = options.description ?? Base.description; + static override default = options.default ?? Base.default; + }; + } +} + +/** + * RadioButton - mutually exclusive options + * + * Use for small sets of exclusive choices (2-4 options). + * Maps to Superset's RadioButtonControl. + */ +export class RadioButton extends Argument { + static override label: string | null = 'Option'; + static override description: string | null = 'Select one option'; + static override controlType = 'RadioButtonControl'; + static default: string | boolean = ''; + static options: RadioOption[] = []; + + static with(options: RadioButtonOptions): typeof RadioButton { + const Base = this; + return class extends Base { + static override label = options.label ?? Base.label; + static override description = options.description ?? Base.description; + static override default = options.default ?? Base.default; + static override options = options.options; + }; + } +} + +/** + * Type guard to check if an argument class is a ConditionalFormatting type + */ +export function isConditionalFormattingArg( + argClass: typeof Argument, +): argClass is typeof ConditionalFormatting { + return argClass.controlType === 'ConditionalFormattingControl'; +} + +/** + * Type guard to check if an argument class is a TimeFormat type + */ +export function isTimeFormatArg( + argClass: typeof Argument, +): argClass is typeof TimeFormat { + return argClass.controlType === 'TimeFormatControl'; +} + +/** + * Type guard to check if an argument class is a NumberFormat type + */ +export function isNumberFormatArg( + argClass: typeof Argument, +): argClass is typeof NumberFormat { + return argClass.controlType === 'NumberFormatControl'; +} + +/** + * Type guard to check if an argument class is a Currency type + */ +export function isCurrencyArg( + argClass: typeof Argument, +): argClass is typeof Currency { + return argClass.controlType === 'CurrencyControl'; +} + +/** + * Type guard to check if an argument class is a Select type + */ +export function isSelectArg( + argClass: typeof Argument, +): argClass is typeof Select { + return ( + 'options' in argClass && Array.isArray((argClass as typeof Select).options) + ); +} + +/** + * Type guard to check if an argument class is a Checkbox type + */ +export function isCheckboxArg( + argClass: typeof Argument, +): argClass is typeof Checkbox { + return ( + 'default' in argClass && + typeof (argClass as typeof Checkbox).default === 'boolean' + ); +} + +/** + * Type guard to check if an argument class is a Text type + */ +export function isTextArg(argClass: typeof Argument): argClass is typeof Text { + return ( + argClass.controlType === 'TextControl' || + (argClass.prototype instanceof Text && + !isSelectArg(argClass) && + !isCheckboxArg(argClass)) + ); +} + +/** + * Type guard to check if an argument class is an Int type + */ +export function isIntArg(argClass: typeof Argument): argClass is typeof Int { + return 'min' in argClass && 'max' in argClass; +} + +/** + * Type guard to check if an argument class is a Color type + */ +export function isColorArg( + argClass: typeof Argument, +): argClass is typeof Color { + return ( + argClass.controlType === 'ColorPickerControl' || + argClass.prototype instanceof Color + ); +} + +/** + * Type guard to check if an argument class is a Metric type + */ +export function isMetricArg( + argClass: typeof Argument, +): argClass is typeof Metric { + return argClass.columnType === ColumnType.Metric; +} + +/** + * Type guard to check if an argument class is a Dimension type + */ +export function isDimensionArg( + argClass: typeof Argument, +): argClass is typeof Dimension { + return argClass.columnType === ColumnType.Dimension; +} + +/** + * Type guard to check if an argument class is a Temporal type + */ +export function isTemporalArg( + argClass: typeof Argument, +): argClass is typeof Temporal { + return argClass.columnType === ColumnType.Temporal; +} + +/** + * Type guard to check if an argument class is a Slider type + */ +export function isSliderArg( + argClass: typeof Argument, +): argClass is typeof Slider { + return ( + argClass.controlType === 'SliderControl' && + 'step' in argClass && + typeof (argClass as typeof Slider).step === 'number' + ); +} + +/** + * Type guard to check if an argument class is a Bounds type + */ +export function isBoundsArg( + argClass: typeof Argument, +): argClass is typeof Bounds { + return argClass.controlType === 'BoundsControl'; +} + +/** + * Type guard to check if an argument class is a ColorPicker type + */ +export function isColorPickerArg( + argClass: typeof Argument, +): argClass is typeof ColorPicker { + return ( + argClass.controlType === 'ColorPickerControl' && + 'default' in argClass && + typeof (argClass as typeof ColorPicker).default === 'object' && + 'r' in ((argClass as typeof ColorPicker).default as object) + ); +} + +/** + * Type guard to check if an argument class is a RadioButton type + */ +export function isRadioButtonArg( + argClass: typeof Argument, +): argClass is typeof RadioButton { + return argClass.controlType === 'RadioButtonControl'; +} diff --git a/superset-frontend/packages/superset-ui-glyph-core/src/crossFilter.ts b/superset-frontend/packages/superset-ui-glyph-core/src/crossFilter.ts new file mode 100644 index 00000000000..4f04ccc1482 --- /dev/null +++ b/superset-frontend/packages/superset-ui-glyph-core/src/crossFilter.ts @@ -0,0 +1,245 @@ +/** + * 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. + */ + +/** + * Cross-Filter Utilities for Glyph Charts + * + * This module provides helpers for implementing cross-filtering in Glyph charts. + * Cross-filtering allows charts to filter other charts on the dashboard when + * users click on data points. + * + * ## Quick Start + * + * 1. Add behaviors to metadata: + * ```typescript + * metadata: { + * behaviors: [Behavior.InteractiveChart, Behavior.DrillToDetail, Behavior.DrillBy], + * } + * ``` + * + * 2. Extract cross-filter props in transform: + * ```typescript + * transform: (chartProps) => { + * const crossFilterProps = extractCrossFilterProps(chartProps, groupby, labelMap); + * return { transformedProps: { ...otherProps, ...crossFilterProps } }; + * } + * ``` + * + * 3. Use event handlers in render: + * ```typescript + * render: ({ transformedProps }) => { + * const eventHandlers = allEventHandlers(transformedProps); + * return ; + * } + * ``` + */ + +import type { + ChartProps, + FilterState, + QueryFormColumn, + SetDataMaskHook, + ContextMenuFilters, +} from '@superset-ui/core'; + +/** + * Props needed for cross-filtering in the render component. + * These are typically returned from the transform function and passed to Echart. + */ +export interface CrossFilterRenderProps { + /** Groupby columns used for filtering */ + groupby: QueryFormColumn[]; + /** Maps series names to their groupby column values */ + labelMap: Record; + /** Callback to emit cross-filter data mask */ + setDataMask: SetDataMaskHook; + /** Maps series indices to selected value names */ + selectedValues: Record; + /** Whether cross-filters are enabled for this chart */ + emitCrossFilters?: boolean; + /** Context menu handler for drill actions */ + onContextMenu?: ( + clientX: number, + clientY: number, + filters?: ContextMenuFilters, + ) => void; + /** Column type mapping for formatting */ + coltypeMapping?: Record; +} + +/** + * Create a selectedValues map from filterState. + * + * The selectedValues map is used by the Echart component to track which + * data points are currently selected (for highlighting). + * + * @param filterState - Current filter state from chartProps + * @param seriesNames - Array of series/data point names + * @returns Map of index -> name for selected values + * + * @example + * ```typescript + * const selectedValues = createSelectedValuesMap( + * filterState, + * transformedData.map(d => d.name), + * ); + * ``` + */ +export function createSelectedValuesMap( + filterState: FilterState | undefined, + seriesNames: string[], +): Record { + return (filterState?.selectedValues || []).reduce( + (acc: Record, selectedValue: string) => { + const index = seriesNames.findIndex(name => name === selectedValue); + if (index >= 0) { + return { ...acc, [index]: selectedValue }; + } + return acc; + }, + {}, + ); +} + +/** + * Extract cross-filter related props from ChartProps. + * + * This is a convenience function that extracts all the props needed for + * cross-filtering from the standard ChartProps object. + * + * @param chartProps - The chart props from Superset + * @param groupby - The groupby columns (dimensions) from form data + * @param labelMap - A map from series names to their groupby values + * @param seriesNames - Array of series/data point names for selectedValues mapping + * @param coltypeMapping - Optional column type mapping + * + * @example + * ```typescript + * // In transform function: + * const labelMap = data.reduce((acc, datum) => ({ + * ...acc, + * [extractGroupbyLabel({ datum, groupby })]: groupby.map(col => datum[col]), + * }), {}); + * + * const crossFilterProps = extractCrossFilterProps( + * chartProps, + * groupby, + * labelMap, + * transformedData.map(d => d.name), + * coltypeMapping, + * ); + * + * return { + * transformedProps: { + * echartOptions, + * formData, + * width, + * height, + * refs, + * ...crossFilterProps, + * }, + * }; + * ``` + */ +export function extractCrossFilterProps( + chartProps: ChartProps, + groupby: QueryFormColumn[], + labelMap: Record, + seriesNames: string[], + coltypeMapping?: Record, +): CrossFilterRenderProps { + const { hooks, filterState, emitCrossFilters, formData } = chartProps; + const { setDataMask = () => {}, onContextMenu } = hooks ?? {}; + + const selectedValues = createSelectedValuesMap(filterState, seriesNames); + + return { + groupby, + labelMap, + setDataMask, + selectedValues, + emitCrossFilters, + onContextMenu, + coltypeMapping, + // Also include formData for context menu formatting + formData, + } as CrossFilterRenderProps & { formData: unknown }; +} + +/** + * Check if a data point is currently filtered (should be dimmed). + * + * Use this in the transform function to apply opacity/styling to + * data points that are not part of the current filter selection. + * + * @param filterState - Current filter state from chartProps + * @param name - The name/label of the data point to check + * @returns true if the data point should be dimmed, false otherwise + * + * @example + * ```typescript + * const isFiltered = isDataPointFiltered(filterState, datum.name); + * const opacity = isFiltered ? OpacityEnum.SemiTransparent : OpacityEnum.NonTransparent; + * ``` + */ +export function isDataPointFiltered( + filterState: FilterState | undefined, + name: string, +): boolean { + return Boolean( + filterState?.selectedValues && + filterState.selectedValues.length > 0 && + !filterState.selectedValues.includes(name), + ); +} + +/** + * Create a labelMap from data records. + * + * The labelMap maps series names (like "USA" or "2024-01") to their + * corresponding groupby column values. This is needed for the cross-filter + * event handlers to construct proper filter clauses. + * + * @param data - Array of data records + * @param groupbyLabels - Array of groupby column labels + * @param extractLabel - Function to extract the series label from a datum + * @returns Map of label -> groupby values + * + * @example + * ```typescript + * const labelMap = createLabelMap( + * data, + * groupbyLabels, + * datum => extractGroupbyLabel({ datum, groupby: groupbyLabels, coltypeMapping }), + * ); + * ``` + */ +export function createLabelMap>( + data: T[], + groupbyLabels: string[], + extractLabel: (datum: T) => string, +): Record { + return data.reduce((acc: Record, datum: T) => { + const label = extractLabel(datum); + return { + ...acc, + [label]: groupbyLabels.map(col => datum[col] as string), + }; + }, {}); +} diff --git a/superset-frontend/packages/superset-ui-glyph-core/src/defineChart.tsx b/superset-frontend/packages/superset-ui-glyph-core/src/defineChart.tsx new file mode 100644 index 00000000000..963c7aa1bb1 --- /dev/null +++ b/superset-frontend/packages/superset-ui-glyph-core/src/defineChart.tsx @@ -0,0 +1,1035 @@ +/** + * 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. + */ + +/** + * defineChart - Single-file visualization plugin pattern + * + * This is the core of the Glyph pattern: define a chart with a single function + * where the arguments define both the controls AND the props passed to render. + * + * No more separate files for: + * - controlPanel.ts (generated from arguments) + * - transformProps.ts (not needed - arguments go directly to render) + * - buildQuery.ts (inferred from Metric/Dimension/Temporal arguments) + */ + +import type { FC, ReactElement } from 'react'; +import { t } from '@apache-superset/core/translation'; +import { + ChartPlugin, + ChartMetadata, + Behavior, + ChartProps, + buildQueryContext, + QueryFormData, +} from '@superset-ui/core'; +import type { + ControlPanelConfig, + ControlPanelSectionConfig, + ControlSetRow, +} from '@superset-ui/chart-controls'; +import { sharedControls } from '@superset-ui/chart-controls'; +import { + Argument, + Select, + Text, + Checkbox, + Int, + Color, + Metric, + Dimension, + Temporal, + NumberFormat, + Currency, + TimeFormat, + ConditionalFormatting, + isSelectArg, + isCheckboxArg, + isIntArg, + isColorArg, + isMetricArg, + isDimensionArg, + isTemporalArg, + isNumberFormatArg, + isCurrencyArg, + isTimeFormatArg, + isConditionalFormattingArg, +} from './arguments'; +import type { + VisibilityFn, + RgbaColor, + CurrencyValue, + ArgumentCondition, + ConditionalFormattingRule, +} from './types'; +import { GenericDataType } from '@apache-superset/core/common'; + +// ============================================================================ +// Types +// ============================================================================ + +/** + * Argument definition - either a class or a class with visibility/disabled config + * + * @example Simple argument + * metric: Metric.with({ label: 'Metric' }) + * + * @example With declarative visibility (preferred) + * metricNameFontSize: { + * arg: Select.with({ ... }), + * visibleWhen: { showMetricName: true }, + * } + * + * @example With declarative disabled state + * subtitleFontSize: { + * arg: Select.with({ ... }), + * disabledWhen: { subtitle: (val) => !val }, + * } + * + * @example With legacy visibility function (deprecated) + * metricNameFontSize: { + * arg: Select.with({ ... }), + * visibility: ({ controls }) => controls?.showMetricName?.value === true, + * } + */ +export type ArgDef = + | typeof Argument + | { + arg: typeof Argument; + /** @deprecated Use visibleWhen instead */ + visibility?: VisibilityFn; + /** Declarative condition: show control when condition is met */ + visibleWhen?: ArgumentCondition; + /** Declarative condition: disable control when condition is met */ + disabledWhen?: ArgumentCondition; + /** Reset value when control is hidden */ + resetOnHide?: boolean; + }; + +/** + * Arguments object - keys become formData keys, values define controls + */ +export type ChartArguments = Record; + +/** + * Extract the runtime value type from an argument class + */ +type ArgValue = T extends typeof Checkbox + ? boolean + : T extends typeof Int + ? number + : T extends typeof Select + ? string | number + : T extends typeof Color + ? string + : T extends typeof NumberFormat + ? string + : T extends typeof TimeFormat + ? string + : T extends typeof Currency + ? CurrencyValue + : T extends typeof ConditionalFormatting + ? ConditionalFormattingRule[] + : T extends typeof Metric + ? { value: unknown; name: string; formattedValue: string } + : T extends typeof Dimension + ? string[] + : T extends typeof Temporal + ? string + : T extends { arg: infer A } + ? A extends typeof Argument + ? ArgValue + : unknown + : string; + +/** + * Convert arguments definition to render props type + */ +export type RenderProps = { + [K in keyof TArgs]: ArgValue; +} & { + width: number; + height: number; + data: Record[]; + theme: unknown; + formData: Record; +}; + +/** + * Custom transform function type - processes chartProps before rendering + */ +export type TransformFn< + TArgs extends ChartArguments, + TExtra = Record, +> = (chartProps: ChartProps, argValues: RenderProps) => TExtra; + +/** + * Chart definition options + */ +export interface ChartDefinition< + TArgs extends ChartArguments, + TExtra = Record, +> { + /** Chart metadata */ + metadata: { + name: string; + description?: string; + category?: string; + tags?: string[]; + thumbnail: string; + thumbnailDark?: string; + behaviors?: Behavior[]; + credits?: string[]; + exampleGallery?: Array<{ url: string; urlDark?: string; caption?: string }>; + supportedAnnotationTypes?: string[]; + useLegacyApi?: boolean; + }; + + /** + * Argument definitions - these define both controls AND render props + * Keys become formData keys (use snake_case for Superset convention) + */ + arguments: TArgs; + + /** + * Additional control panel sections (for complex controls that don't fit the argument pattern) + */ + additionalControls?: { + /** Prepended before auto-generated query controls (metric, groupby, adhoc_filters) */ + queryBefore?: ControlSetRow[]; + query?: ControlSetRow[]; + chartOptions?: ControlSetRow[]; + }; + + /** + * Additional complete control panel sections prepended before the Query section. + * Use for a Time section or other sections that should appear first. + */ + prependSections?: ControlPanelSectionConfig[]; + + /** + * Additional complete control panel sections inserted between Query and Chart Options. + * Use for sections like Options or Formatting that should appear before Chart Options. + */ + middleSections?: ControlPanelSectionConfig[]; + + /** + * Additional complete control panel sections appended after Query and Chart Options. + * Use for sections like Time Comparison that need their own label, tab, etc. + */ + additionalSections?: ControlPanelSectionConfig[]; + + /** + * Tab override for the Chart Options section (e.g., 'customize' moves it to the Customize tab). + */ + chartOptionsTabOverride?: 'customize' | 'data'; + + /** + * Suppress the auto-generated Query section entirely. + * Use for charts that have entirely custom sections and no standard query controls. + */ + suppressQuerySection?: boolean; + + /** + * Control overrides for argument-generated controls (labels, descriptions, etc.) + */ + controlOverrides?: Record>; + + /** + * Control overrides for controls in additionalControls (e.g., metric, color_scheme) + * These are merged with controlOverrides in the final control panel config. + */ + additionalControlOverrides?: Record>; + + /** + * Form data overrides function - called to standardize controls + * (e.g., for getStandardizedControls() in cross-filtering) + */ + formDataOverrides?: (formData: QueryFormData) => QueryFormData; + + /** + * Custom buildQuery function - use for charts that need post-processing operators + * If not provided, a default query builder is generated from arguments + */ + buildQuery?: ( + formData: QueryFormData, + ) => ReturnType; + + /** + * Custom transform function - processes raw chartProps to add computed values + * Return value is merged into render props + * + * @example + * transform: (chartProps, argValues) => { + * const trendlineData = computeTrendline(chartProps.queriesData[0].data); + * return { trendlineData, echartOptions: buildEchartOptions(trendlineData) }; + * } + */ + transform?: TransformFn; + + /** + * The render function - receives argument values + transformed data + */ + render: (props: RenderProps & TExtra) => ReactElement; +} + +// ============================================================================ +// Helpers +// ============================================================================ + +function hexToRgba(hex: string): RgbaColor { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + if (result?.[1] && result[2] && result[3]) { + return { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16), + a: 1, + }; + } + return { r: 0, g: 0, b: 0, a: 1 }; +} + +function rgbaToHex(rgba: RgbaColor): string { + const toHex = (n: number) => n.toString(16).padStart(2, '0'); + return `#${toHex(rgba.r)}${toHex(rgba.g)}${toHex(rgba.b)}`; +} + +function getArgClass(argDef: ArgDef): typeof Argument { + return 'arg' in argDef ? argDef.arg : argDef; +} + +/** + * Public API: resolve the argument class from an ArgDef (handles both + * bare class form and `{ arg, visibleWhen, ... }` form). + */ +export const resolveArgClass = getArgClass; + +/** + * Public API: extract the raw visibleWhen condition from an ArgDef (if any). + */ +export function getArgVisibleWhen( + argDef: ArgDef, +): ArgumentCondition | undefined { + return 'arg' in argDef ? argDef.visibleWhen : undefined; +} + +/** + * Convert a declarative ArgumentCondition to a visibility function. + * + * @example + * // { showMetricName: true } becomes: + * ({ controls }) => controls?.showMetricName?.value === true + * + * @example + * // { subtitle: (val) => !!val } becomes: + * ({ controls }) => !!controls?.subtitle?.value + */ +function conditionToVisibilityFn(condition: ArgumentCondition): VisibilityFn { + return ({ controls }) => { + for (const [argName, expectedValue] of Object.entries(condition)) { + const actualValue = controls?.[argName]?.value; + + if (typeof expectedValue === 'function') { + // Function check + if (!expectedValue(actualValue)) { + return false; + } + } else { + // Equality check + if (actualValue !== expectedValue) { + return false; + } + } + } + return true; + }; +} + +/** + * Evaluate an ArgumentCondition directly from formData (no Redux controls state needed). + * + * Use this in rendering code to decide control visibility without going through + * the Redux controls pipeline. + * + * @example + * const isVisible = evaluateGlyphCondition({ showLegend: true }, form_data); + */ +export function evaluateGlyphCondition( + condition: ArgumentCondition, + formData: Record, +): boolean { + for (const [argName, expectedValue] of Object.entries(condition)) { + const actualValue = formData[argName]; + if (typeof expectedValue === 'function') { + if (!(expectedValue as (val: unknown) => boolean)(actualValue)) { + return false; + } + } else if (actualValue !== expectedValue) { + return false; + } + } + return true; +} + +/** + * Convert a declarative ArgumentCondition to a disabled function. + * Similar to visibility but returns true when condition IS met (to disable). + */ +function conditionToDisabledFn( + condition: ArgumentCondition, +): (state: { controls: Record }) => boolean { + return ({ controls }) => { + for (const [argName, expectedValue] of Object.entries(condition)) { + const actualValue = controls?.[argName]?.value; + + if (typeof expectedValue === 'function') { + if (!expectedValue(actualValue)) { + return false; + } + } else { + if (actualValue !== expectedValue) { + return false; + } + } + } + return true; + }; +} + +interface ControlVisibilityConfig { + visibility?: VisibilityFn; + disabled?: (state: { + controls: Record; + }) => boolean; + resetOnHide?: boolean; +} + +function getVisibilityConfig(argDef: ArgDef): ControlVisibilityConfig { + if ('arg' in argDef) { + const config: ControlVisibilityConfig = { + resetOnHide: argDef.resetOnHide, + }; + + // Prefer declarative visibleWhen over legacy visibility function + if (argDef.visibleWhen) { + config.visibility = conditionToVisibilityFn(argDef.visibleWhen); + } else if (argDef.visibility) { + config.visibility = argDef.visibility; + } + + // Handle disabledWhen + if (argDef.disabledWhen) { + config.disabled = conditionToDisabledFn(argDef.disabledWhen); + } + + return config; + } + return {}; +} + +/** + * Generate control config from argument class. + * Public alias: use getGlyphControlConfig() for external callers. + */ +function getControlConfig( + argClass: typeof Argument, + paramName: string, +): Record { + const label = argClass.label || paramName; + const description = argClass.description || ''; + + if (isSelectArg(argClass)) { + return { + type: 'SelectControl', + label, + description, + default: argClass.default, + options: argClass.options, + clearable: false, + renderTrigger: true, + }; + } + + if (isCheckboxArg(argClass)) { + return { + type: 'CheckboxControl', + label, + description, + default: argClass.default, + renderTrigger: true, + }; + } + + if (isIntArg(argClass)) { + return { + type: 'SliderControl', + label, + description, + default: argClass.default, + min: argClass.min, + max: argClass.max, + step: argClass.step ?? 1, + renderTrigger: true, + }; + } + + if (isColorArg(argClass)) { + // eslint-disable-next-line theme-colors/no-literal-colors + const hexDefault = argClass.default ?? '#000000'; + return { + type: 'ColorPickerControl', + label, + description, + default: hexToRgba(hexDefault), + renderTrigger: true, + }; + } + + if (isNumberFormatArg(argClass)) { + const formatClass = argClass as typeof NumberFormat; + return { + type: 'SelectControl', + freeForm: true, + label, + description, + default: formatClass.default, + choices: formatClass.FORMAT_OPTIONS.map(opt => [opt.value, opt.label]), + renderTrigger: true, + tokenSeparators: ['\n', '\t', ';'], + }; + } + + if (isCurrencyArg(argClass)) { + const currencyClass = argClass as typeof Currency; + return { + type: 'CurrencyControl', + label, + description, + default: currencyClass.default, + renderTrigger: true, + }; + } + + if (isTimeFormatArg(argClass)) { + const timeFormatClass = argClass as typeof TimeFormat; + return { + type: 'SelectControl', + freeForm: true, + label, + description, + default: timeFormatClass.default, + choices: timeFormatClass.FORMAT_OPTIONS.map(opt => [ + opt.value, + opt.label, + ]), + renderTrigger: true, + }; + } + + if (isConditionalFormattingArg(argClass)) { + return { + type: 'ConditionalFormattingControl', + renderTrigger: true, + label, + description, + shouldMapStateToProps() { + return true; + }, + mapStateToProps( + explore: { + datasource?: { + verbose_map?: Record; + columns?: Record; + }; + }, + _control: unknown, + chart: { + queriesResponse?: Array<{ + colnames?: string[]; + coltypes?: number[]; + }>; + }, + ) { + const verboseMap = + explore?.datasource?.verbose_map ?? + explore?.datasource?.columns ?? + {}; + const { colnames, coltypes } = chart?.queriesResponse?.[0] ?? {}; + const numericColumns = + Array.isArray(colnames) && Array.isArray(coltypes) + ? colnames + .filter( + (_col: string, index: number) => + coltypes[index] === GenericDataType.Numeric, + ) + .map((colname: string) => ({ + value: colname, + label: + (Array.isArray(verboseMap) + ? verboseMap[colname as unknown as number] + : verboseMap[colname]) ?? colname, + dataType: colnames && coltypes[colnames.indexOf(colname)], + })) + : []; + return { + columnOptions: numericColumns, + verboseMap, + }; + }, + }; + } + + // Default to TextControl + const textClass = argClass as typeof Text; + return { + type: 'TextControl', + label, + description, + default: textClass.default ?? '', + placeholder: textClass.placeholder ?? '', + renderTrigger: true, + }; +} + +/** + * Public API: get the control config object for a glyph argument class. + * Returns the raw config (type, label, description, default, options, etc.) + * that can be used to render a control directly. + */ +export const getGlyphControlConfig = getControlConfig; + +// ============================================================================ +// Control Panel Generator +// ============================================================================ + +function generateControlPanel( + args: TArgs, + additionalControls?: ChartDefinition['additionalControls'], + controlOverrides?: ChartDefinition['controlOverrides'], + additionalControlOverrides?: ChartDefinition['additionalControlOverrides'], + formDataOverrides?: ChartDefinition['formDataOverrides'], + additionalSections?: ControlPanelSectionConfig[], + prependSections?: ControlPanelSectionConfig[], + chartOptionsTabOverride?: 'customize' | 'data', + middleSections?: ControlPanelSectionConfig[], + suppressQuerySection?: boolean, +): ControlPanelConfig { + const queryControls: ControlSetRow[] = []; + const chartOptionsControls: ControlSetRow[] = []; + + for (const [paramName, argDef] of Object.entries(args)) { + const argClass = getArgClass(argDef); + const { visibility, disabled, resetOnHide } = getVisibilityConfig(argDef); + + // Data arguments go in Query section — inline sharedControls directly so + // getSectionsToRender can skip expandControlConfig for these too. + if (isMetricArg(argClass)) { + queryControls.push([{ name: 'metric', config: sharedControls.metric }]); + continue; + } + if (isDimensionArg(argClass)) { + queryControls.push([{ name: 'groupby', config: sharedControls.groupby }]); + continue; + } + if (isTemporalArg(argClass)) { + queryControls.push( + [{ name: 'x_axis', config: sharedControls.x_axis }], + [{ name: 'time_grain_sqla', config: sharedControls.time_grain_sqla }], + ); + continue; + } + + // Visual arguments go in Chart Options + const controlConfig = getControlConfig(argClass, paramName); + if (visibility) { + controlConfig.visibility = visibility; + controlConfig.resetOnHide = resetOnHide ?? false; + } + // Store raw visibleWhen condition so renderers can evaluate directly from formData + if ('arg' in argDef && argDef.visibleWhen) { + controlConfig._glyphVisibleWhen = argDef.visibleWhen; + } + if (disabled) { + // Superset uses shouldMapStateToProps + mapStateToProps for dynamic disabled state + controlConfig.shouldMapStateToProps = () => true; + controlConfig.mapStateToProps = ( + _explore: unknown, + control: { value: unknown }, + chart: unknown, + ) => { + // Get current controls state from the control panel + const controls = + (chart as { controls?: Record }) + ?.controls || {}; + return { + disabled: disabled({ controls }), + value: control?.value, + }; + }; + } + + chartOptionsControls.push([ + { + name: paramName, + config: controlConfig as Record & { type: string }, + }, + ]); + } + + // Add adhoc_filters — inlined so getSectionsToRender skips expandControlConfig + queryControls.push([ + { name: 'adhoc_filters', config: sharedControls.adhoc_filters }, + ]); + + // Merge additional controls + const finalQueryControls = [ + ...(additionalControls?.queryBefore || []), + ...queryControls, + ...(additionalControls?.query || []), + ]; + const finalChartOptionsControls = [ + ...chartOptionsControls, + ...(additionalControls?.chartOptions || []), + ]; + + const config: ControlPanelConfig = { + controlPanelSections: [ + ...(prependSections || []), + ...(suppressQuerySection + ? [] + : [ + { + label: t('Query'), + expanded: true, + controlSetRows: finalQueryControls, + }, + ]), + ...(middleSections || []), + ...(finalChartOptionsControls.length > 0 + ? [ + { + label: t('Chart Options'), + expanded: true, + ...(chartOptionsTabOverride + ? { tabOverride: chartOptionsTabOverride } + : {}), + controlSetRows: finalChartOptionsControls, + }, + ] + : []), + ...(additionalSections || []), + ], + }; + + // Merge both controlOverrides and additionalControlOverrides + const mergedOverrides = { + ...controlOverrides, + ...additionalControlOverrides, + }; + if (Object.keys(mergedOverrides).length > 0) { + config.controlOverrides = mergedOverrides; + } + + if (formDataOverrides) { + config.formDataOverrides = formDataOverrides; + } + + // Store raw glyph args for native rendering (bypasses expandControlConfig pipeline) + config._glyphArgs = args; + + return config; +} + +// ============================================================================ +// Build Query Generator +// ============================================================================ + +function generateBuildQuery(args: TArgs) { + // Check what data arguments we have + const hasTemporal = Object.values(args).some(argDef => + isTemporalArg(getArgClass(argDef)), + ); + + return (formData: QueryFormData) => + buildQueryContext(formData, baseQueryObject => { + const query = { ...baseQueryObject }; + + // Add temporal axis if needed + if (hasTemporal && formData.x_axis) { + query.columns = [ + { + columnType: 'BASE_AXIS', + sqlExpression: formData.x_axis, + label: formData.x_axis, + expressionType: 'SQL', + }, + ...(query.columns || []), + ]; + } + + return [query]; + }); +} + +// ============================================================================ +// Transform Props Generator (Hidden - user never sees this) +// ============================================================================ + +function generateTransformProps< + TArgs extends ChartArguments, + TExtra = Record, +>( + args: TArgs, + RenderComponent: FC & TExtra>, + customTransform?: TransformFn, +) { + return (chartProps: ChartProps) => { + const { width, height, queriesData, formData, theme } = chartProps; + const data = queriesData[0]?.data || []; + + // Build render props from arguments + const renderProps: Record = { + width, + height, + data, + theme, + formData, + }; + + for (const [paramName, argDef] of Object.entries(args)) { + const argClass = getArgClass(argDef); + + // Handle different argument types + if (isMetricArg(argClass)) { + const metricValue = formData.metric || formData.metrics?.[0]; + // Extract metric label from various formats + let metricLabel: string; + if (typeof metricValue === 'string') { + metricLabel = metricValue; + } else if (metricValue?.label) { + metricLabel = metricValue.label; + } else if (metricValue?.column?.column_name && metricValue?.aggregate) { + metricLabel = `${metricValue.aggregate}(${metricValue.column.column_name})`; + } else { + metricLabel = 'value'; + } + + // Find the actual value - try the label first, then look for first numeric column + let rawValue = data[0]?.[metricLabel]; + if (rawValue === undefined && data[0]) { + // Try to find the metric value by looking for numeric columns + const dataKeys = Object.keys(data[0]); + for (const key of dataKeys) { + const val = data[0][key]; + if (typeof val === 'number') { + rawValue = val; + metricLabel = key; + break; + } + } + } + + renderProps[paramName] = { + value: rawValue, + name: metricLabel, + formattedValue: String(rawValue ?? ''), + }; + continue; + } + + if (isDimensionArg(argClass)) { + renderProps[paramName] = formData.groupby || formData.columns || []; + continue; + } + + if (isTemporalArg(argClass)) { + renderProps[paramName] = + formData.x_axis || formData.granularity_sqla || '__timestamp'; + continue; + } + + // Get value from formData - ChartProps converts formData to camelCase via + // convertKeysToCamelCase, so check camelCase first, then snake_case fallback + const camelParamName = paramName.replace(/_([a-z])/g, (_, c: string) => + c.toUpperCase(), + ); + let value = formData[camelParamName] ?? formData[paramName]; + + if (isColorArg(argClass)) { + const colorClass = argClass as typeof Color; + // eslint-disable-next-line theme-colors/no-literal-colors + const defaultRgba = hexToRgba(colorClass.default ?? '#000000'); + const colorValue = value ?? defaultRgba; + if ( + typeof colorValue === 'object' && + colorValue !== null && + 'r' in colorValue + ) { + value = rgbaToHex(colorValue as RgbaColor); + } else if (typeof colorValue !== 'string') { + // eslint-disable-next-line theme-colors/no-literal-colors + value = colorClass.default ?? '#000000'; + } + } else if (isNumberFormatArg(argClass)) { + value = + value ?? (argClass as typeof NumberFormat).default ?? 'SMART_NUMBER'; + } else if (isTimeFormatArg(argClass)) { + value = + value ?? (argClass as typeof TimeFormat).default ?? 'smart_date'; + } else if (isConditionalFormattingArg(argClass)) { + value = + value ?? (argClass as typeof ConditionalFormatting).default ?? []; + } else if (isCurrencyArg(argClass)) { + value = value ?? (argClass as typeof Currency).default ?? {}; + } else if (isSelectArg(argClass)) { + value = value ?? (argClass as typeof Select).default; + } else if (isCheckboxArg(argClass)) { + value = value ?? (argClass as typeof Checkbox).default ?? false; + } else if (isIntArg(argClass)) { + value = value ?? (argClass as typeof Int).default ?? 0; + } else { + value = value ?? (argClass as typeof Text).default ?? ''; + } + + renderProps[paramName] = value; + } + + // Apply custom transform if provided + const baseProps = renderProps as RenderProps; + if (customTransform) { + const extraProps = customTransform(chartProps, baseProps); + return { ...baseProps, ...extraProps } as RenderProps & TExtra; + } + + return baseProps as RenderProps & TExtra; + }; +} + +// ============================================================================ +// The Main Event: defineChart +// ============================================================================ + +/** + * Define a complete chart plugin with a single function. + * + * The arguments define both the control panel AND the props passed to render. + * No separate controlPanel.ts, transformProps.ts, or buildQuery.ts needed. + * + * @example + * ```typescript + * export default defineChart({ + * metadata: { + * name: 'My Chart', + * thumbnail, + * }, + * arguments: { + * metric: Metric.with({ label: 'Metric' }), + * fontSize: Select.with({ + * label: 'Font Size', + * options: [{ label: 'Small', value: 12 }, { label: 'Large', value: 24 }], + * default: 12, + * }), + * }, + * render: ({ metric, fontSize, width, height }) => ( + *
+ * {metric.formattedValue} + *
+ * ), + * }); + * ``` + */ +export function defineChart< + TArgs extends ChartArguments, + TExtra = Record, +>(definition: ChartDefinition): new () => ChartPlugin { + const { + metadata, + arguments: args, + additionalControls, + additionalSections, + prependSections, + middleSections, + chartOptionsTabOverride, + suppressQuerySection, + controlOverrides, + additionalControlOverrides, + formDataOverrides, + buildQuery: customBuildQuery, + transform, + render, + } = definition; + + // Create the chart component that receives transformed props + const ChartComponent: FC & TExtra> = props => + render(props); + + // Generate everything from arguments (or use custom if provided) + const controlPanel = generateControlPanel( + args, + additionalControls, + controlOverrides, + additionalControlOverrides, + formDataOverrides, + additionalSections, + prependSections, + chartOptionsTabOverride, + middleSections, + suppressQuerySection, + ); + const transformProps = generateTransformProps( + args, + ChartComponent, + transform, + ); + const buildQuery = customBuildQuery ?? generateBuildQuery(args); + + // Create metadata + const chartMetadata = new ChartMetadata({ + name: metadata.name, + description: metadata.description, + category: metadata.category, + tags: metadata.tags, + thumbnail: metadata.thumbnail, + thumbnailDark: metadata.thumbnailDark, + behaviors: metadata.behaviors || [Behavior.InteractiveChart], + credits: metadata.credits, + exampleGallery: metadata.exampleGallery, + supportedAnnotationTypes: metadata.supportedAnnotationTypes, + useLegacyApi: metadata.useLegacyApi, + }); + + // Return a ChartPlugin class + return class GlyphChartPlugin extends ChartPlugin { + constructor() { + super({ + metadata: chartMetadata, + loadChart: () => Promise.resolve(ChartComponent), + controlPanel, + transformProps, + buildQuery, + }); + } + }; +} + +// Re-export useful types for custom transforms +export type { ChartProps } from '@superset-ui/core'; + +export default defineChart; diff --git a/superset-frontend/packages/superset-ui-glyph-core/src/generators.ts b/superset-frontend/packages/superset-ui-glyph-core/src/generators.ts new file mode 100644 index 00000000000..17aa5c70761 --- /dev/null +++ b/superset-frontend/packages/superset-ui-glyph-core/src/generators.ts @@ -0,0 +1,419 @@ +/** + * 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 { t } from '@apache-superset/core/translation'; +import type { + ControlPanelConfig, + ControlSetRow, +} from '@superset-ui/chart-controls'; +import type { ChartProps } from '@superset-ui/core'; +import { + Argument, + Select, + Text, + Checkbox, + Int, + Color, + isSelectArg, + isCheckboxArg, + isIntArg, + isColorArg, + isMetricArg, + isDimensionArg, + isTemporalArg, +} from './arguments'; +import type { VisibilityFn, RgbaColor } from './types'; + +/** + * Configuration for a glyph argument with optional visibility control + */ +export interface GlyphArgConfig { + arg: typeof Argument; + visibility?: VisibilityFn; + resetOnHide?: boolean; +} + +/** + * Arguments map - parameter name to argument class or config + */ +export type GlyphArguments = Map; + +/** + * Convert hex color string to RGBA object for Superset's ColorPickerControl + */ +function hexToRgba(hex: string): RgbaColor { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + if (result && result[1] && result[2] && result[3]) { + return { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16), + a: 1, + }; + } + return { r: 0, g: 0, b: 0, a: 1 }; +} + +/** + * Convert RGBA object to hex color string + */ +function rgbaToHex(rgba: RgbaColor): string { + const toHex = (n: number) => n.toString(16).padStart(2, '0'); + return `#${toHex(rgba.r)}${toHex(rgba.g)}${toHex(rgba.b)}`; +} + +/** + * Get the argument class from a config (handles both direct class and config object) + */ +function getArgClass( + argOrConfig: typeof Argument | GlyphArgConfig, +): typeof Argument { + return 'arg' in argOrConfig ? argOrConfig.arg : argOrConfig; +} + +/** + * Get visibility config if present + */ +function getVisibilityConfig(argOrConfig: typeof Argument | GlyphArgConfig): { + visibility?: VisibilityFn; + resetOnHide?: boolean; +} { + if ('arg' in argOrConfig) { + return { + visibility: argOrConfig.visibility, + resetOnHide: argOrConfig.resetOnHide, + }; + } + return {}; +} + +/** + * Generate Superset control config from a glyph Argument class + */ +export function getControlConfig( + argClass: typeof Argument, + paramName: string, +): Record & { type: string } { + const label = argClass.label || paramName; + const description = argClass.description || ''; + + // Select control + if (isSelectArg(argClass)) { + return { + type: 'SelectControl', + label, + description, + default: argClass.default, + options: argClass.options, + clearable: argClass.clearable ?? false, + renderTrigger: true, + }; + } + + // Checkbox control + if (isCheckboxArg(argClass)) { + return { + type: 'CheckboxControl', + label, + description, + default: argClass.default, + renderTrigger: true, + }; + } + + // Int/Slider control + if (isIntArg(argClass)) { + return { + type: 'SliderControl', + label, + description, + default: argClass.default, + min: argClass.min, + max: argClass.max, + step: argClass.step ?? 1, + renderTrigger: true, + }; + } + + // Color control + if (isColorArg(argClass)) { + // eslint-disable-next-line theme-colors/no-literal-colors + const hexDefault = argClass.default ?? '#000000'; + return { + type: 'ColorPickerControl', + label, + description, + default: hexToRgba(hexDefault), + renderTrigger: true, + }; + } + + // Default to TextControl + const textClass = argClass as typeof Text; + return { + type: 'TextControl', + label, + description, + default: textClass.default ?? '', + placeholder: textClass.placeholder ?? '', + renderTrigger: true, + }; +} + +/** + * Options for control panel generation + */ +export interface ControlPanelOptions { + /** Additional control rows for the query section */ + queryControls?: ControlSetRow[]; + /** Additional control rows for the chart options section */ + chartOptionsControls?: ControlSetRow[]; + /** Control overrides */ + controlOverrides?: Record>; + /** Form data overrides function */ + formDataOverrides?: ( + formData: Record, + ) => Record; +} + +/** + * Generate a complete ControlPanelConfig from glyph arguments + * + * This is the core function that converts semantic argument definitions + * into Superset's control panel format. + */ +export function generateControlPanel( + glyphArguments: GlyphArguments, + options: ControlPanelOptions = {}, +): ControlPanelConfig { + const queryControls: ControlSetRow[] = []; + const chartOptionsControls: ControlSetRow[] = []; + + // Process each argument + for (const [paramName, argOrConfig] of glyphArguments) { + const argClass = getArgClass(argOrConfig); + const { visibility, resetOnHide } = getVisibilityConfig(argOrConfig); + + // Data arguments go in Query section + if (isMetricArg(argClass)) { + queryControls.push(['metric']); + continue; + } + + if (isDimensionArg(argClass)) { + queryControls.push(['groupby']); + continue; + } + + if (isTemporalArg(argClass)) { + queryControls.push(['x_axis'], ['time_grain_sqla']); + continue; + } + + // Style/visual arguments go in Chart Options section + const controlConfig = getControlConfig(argClass, paramName); + + // Add visibility if specified + if (visibility) { + controlConfig.visibility = visibility; + controlConfig.resetOnHide = resetOnHide ?? false; + } + + chartOptionsControls.push([ + { + name: paramName, + config: controlConfig, + }, + ]); + } + + // Add adhoc_filters to query section + queryControls.push(['adhoc_filters']); + + // Merge with additional controls from options + const finalQueryControls = [ + ...queryControls, + ...(options.queryControls || []), + ]; + const finalChartOptionsControls = [ + ...chartOptionsControls, + ...(options.chartOptionsControls || []), + ]; + + const config: ControlPanelConfig = { + controlPanelSections: [ + { + label: t('Query'), + expanded: true, + controlSetRows: finalQueryControls, + }, + { + label: t('Chart Options'), + expanded: true, + controlSetRows: finalChartOptionsControls, + }, + ], + }; + + if (options.controlOverrides) { + config.controlOverrides = options.controlOverrides; + } + + if (options.formDataOverrides) { + // Type assertion needed because SqlaFormData is more specific than Record + config.formDataOverrides = + options.formDataOverrides as ControlPanelConfig['formDataOverrides']; + } + + return config; +} + +/** + * Options for transformProps generation + */ +export interface TransformPropsOptions { + /** Custom transformation function that receives extracted values */ + transform?: ( + values: Record, + chartProps: ChartProps, + ) => TResult; + /** Additional props to pass through from chartProps */ + passthrough?: (keyof ChartProps)[]; +} + +/** + * Generate a transformProps function from glyph arguments + * + * This extracts values from formData based on argument definitions, + * applying type conversions as needed (e.g., RGBA to hex for colors). + */ +export function generateTransformProps>( + glyphArguments: GlyphArguments, + options: TransformPropsOptions = {}, +): (chartProps: ChartProps) => TResult { + return (chartProps: ChartProps) => { + const { formData, width, height, queriesData } = chartProps; + const values: Record = { + width, + height, + queriesData, + }; + + // Add passthrough props + if (options.passthrough) { + for (const key of options.passthrough) { + values[key] = chartProps[key]; + } + } + + // Extract values from formData based on argument definitions + for (const [paramName, argOrConfig] of glyphArguments) { + const argClass = getArgClass(argOrConfig); + + // Skip data arguments (metric, dimension, temporal) - these are handled differently + if ( + isMetricArg(argClass) || + isDimensionArg(argClass) || + isTemporalArg(argClass) + ) { + continue; + } + + // Get value from formData, using default if not present + let value = formData[paramName]; + + // Color control: convert RGBA object to hex string + if (isColorArg(argClass)) { + const colorClass = argClass as typeof Color; + // eslint-disable-next-line theme-colors/no-literal-colors + const defaultRgba = hexToRgba(colorClass.default ?? '#000000'); + const colorValue = value ?? defaultRgba; + + if ( + typeof colorValue === 'object' && + colorValue !== null && + 'r' in colorValue + ) { + value = rgbaToHex(colorValue as RgbaColor); + } else if (typeof colorValue === 'string') { + value = colorValue; + } else { + // eslint-disable-next-line theme-colors/no-literal-colors + value = colorClass.default ?? '#000000'; + } + } + // Select control: use default if no value + else if (isSelectArg(argClass)) { + const selectClass = argClass as typeof Select; + value = value ?? selectClass.default; + } + // Checkbox control: use default if no value + else if (isCheckboxArg(argClass)) { + const checkboxClass = argClass as typeof Checkbox; + value = value ?? checkboxClass.default ?? false; + } + // Int control: use default if no value + else if (isIntArg(argClass)) { + const intClass = argClass as typeof Int; + value = value ?? intClass.default ?? 0; + } + // Text control: use default if no value + else { + const textClass = argClass as typeof Text; + value = value ?? textClass.default ?? ''; + } + + values[paramName] = value; + } + + // Apply custom transformation if provided + if (options.transform) { + return options.transform(values, chartProps); + } + + return values as TResult; + }; +} + +/** + * Combined result of creating a glyph plugin + */ +export interface GlyphPluginDef { + controlPanel: ControlPanelConfig; + transformProps: (chartProps: ChartProps) => TProps; +} + +/** + * Create both controlPanel and transformProps from a single argument definition + * + * This is the main entry point for the single-file viz pattern. + */ +export function createGlyphPlugin>( + glyphArguments: GlyphArguments, + controlPanelOptions: ControlPanelOptions = {}, + transformPropsOptions: TransformPropsOptions = {}, +): GlyphPluginDef { + return { + controlPanel: generateControlPanel(glyphArguments, controlPanelOptions), + transformProps: generateTransformProps( + glyphArguments, + transformPropsOptions, + ), + }; +} diff --git a/superset-frontend/packages/superset-ui-glyph-core/src/index.ts b/superset-frontend/packages/superset-ui-glyph-core/src/index.ts new file mode 100644 index 00000000000..7f281876233 --- /dev/null +++ b/superset-frontend/packages/superset-ui-glyph-core/src/index.ts @@ -0,0 +1,71 @@ +/** + * 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. + */ + +/** + * Glyph Core - A declarative visualization plugin framework + * + * This module enables single-file visualization plugins where: + * 1. Arguments define both the chart's inputs AND the control panel + * 2. transformProps is auto-generated from argument definitions + * 3. The chart component is a simple function receiving typed arguments + * + * Features: + * - Single-file chart definitions with defineChart() + * - Declarative argument types (Metric, Dimension, Select, Checkbox, etc.) + * - Conditional visibility with visibleWhen/disabledWhen + * - Cross-filtering support with extractCrossFilterProps() and allEventHandlers() + * - Reusable presets (ShowLegend, HeaderFontSize, etc.) + * + * Example usage: + * ```typescript + * export default defineChart({ + * metadata: { + * name: 'My Chart', + * thumbnail, + * behaviors: [Behavior.InteractiveChart, Behavior.DrillToDetail, Behavior.DrillBy], + * }, + * arguments: { + * metric: Metric.with({ label: 'Metric' }), + * groupby: Dimension.with({ label: 'Breakdowns' }), + * fontSize: Select.with({ + * label: 'Font Size', + * options: [{ label: 'Small', value: 0.2 }, { label: 'Large', value: 0.4 }], + * default: 0.3, + * }), + * }, + * transform: (chartProps, argValues) => { + * // Extract cross-filter props for interactive filtering + * const crossFilterProps = extractCrossFilterProps(chartProps, groupby, labelMap, seriesNames); + * return { transformedProps: { echartOptions, ...crossFilterProps } }; + * }, + * render: ({ transformedProps }) => { + * const eventHandlers = allEventHandlers(transformedProps); + * return ; + * }, + * }); + * ``` + */ + +// Re-export everything +export * from './types'; +export * from './arguments'; +export * from './generators'; +export * from './defineChart'; +export * from './presets'; +export * from './crossFilter'; diff --git a/superset-frontend/packages/superset-ui-glyph-core/src/presets.ts b/superset-frontend/packages/superset-ui-glyph-core/src/presets.ts new file mode 100644 index 00000000000..b445fbf11ec --- /dev/null +++ b/superset-frontend/packages/superset-ui-glyph-core/src/presets.ts @@ -0,0 +1,406 @@ +/** + * 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. + */ + +/** + * Glyph Presets - Reusable argument configurations + * + * This module contains pre-configured arguments that are commonly + * used across multiple visualization types. Charts can import these + * directly or use .with() to customize them further. + * + * Example usage: + * ```typescript + * import { HeaderFontSize, Subtitle } from '../../glyph-core/presets'; + * + * arguments: { + * headerFontSize: HeaderFontSize, + * subtitle: Subtitle, + * // Override defaults when needed: + * customSize: HeaderFontSize.with({ default: 0.5 }), + * } + * ``` + */ + +import { t } from '@apache-superset/core/translation'; +import { Select, Text, Checkbox } from './arguments'; +import { SelectOption } from './types'; + +// ============================================================================ +// Font Size Options +// ============================================================================ + +/** + * Large font size options - for primary/header text elements + * Values are multipliers of container height (0.2 = 20% of height) + */ +export const FONT_SIZE_OPTIONS_LARGE: SelectOption[] = [ + { label: t('Tiny'), value: 0.2 }, + { label: t('Small'), value: 0.3 }, + { label: t('Normal'), value: 0.4 }, + { label: t('Large'), value: 0.5 }, + { label: t('Huge'), value: 0.6 }, +]; + +/** + * Small font size options - for secondary text elements (subtitles, labels) + * Values are multipliers of container height + */ +export const FONT_SIZE_OPTIONS_SMALL: SelectOption[] = [ + { label: t('Tiny'), value: 0.125 }, + { label: t('Small'), value: 0.15 }, + { label: t('Normal'), value: 0.2 }, + { label: t('Large'), value: 0.3 }, + { label: t('Huge'), value: 0.4 }, +]; + +// ============================================================================ +// Pre-configured Arguments +// ============================================================================ + +/** + * Header/primary font size selector + * Used for main display elements like big numbers, titles + */ +export const HeaderFontSize = Select.with({ + label: t('Font Size'), + description: t('Font size for the primary display element'), + options: FONT_SIZE_OPTIONS_LARGE, + default: 0.4, +}); + +/** + * Subheader/secondary font size selector + * Used for subtitles, labels, secondary text + */ +export const SubheaderFontSize = Select.with({ + label: t('Subheader Font Size'), + description: t('Font size for secondary text elements'), + options: FONT_SIZE_OPTIONS_SMALL, + default: 0.15, +}); + +/** + * Subtitle text input + * Generic subtitle/description field used by many chart types + */ +export const Subtitle = Text.with({ + label: t('Subtitle'), + description: t('Description text displayed below the main content'), + default: '', +}); + +/** + * Show legend toggle + * Common toggle for charts with legends + */ +export const ShowLegend = Checkbox.with({ + label: t('Show Legend'), + description: t('Whether to display the chart legend'), + default: true, +}); + +/** + * Force timestamp formatting toggle + * Used when a value might be a timestamp but isn't auto-detected + */ +export const ForceTimestampFormatting = Checkbox.with({ + label: t('Force Date Format'), + description: t( + 'Use date formatting even when the value is not detected as a timestamp', + ), + default: false, +}); + +// ============================================================================ +// Legend Options +// ============================================================================ + +export const LEGEND_TYPE_OPTIONS: SelectOption[] = [ + { label: t('Scroll'), value: 'scroll' }, + { label: t('List'), value: 'plain' }, +]; + +export const LEGEND_ORIENTATION_OPTIONS: SelectOption[] = [ + { label: t('Top'), value: 'top' }, + { label: t('Bottom'), value: 'bottom' }, + { label: t('Left'), value: 'left' }, + { label: t('Right'), value: 'right' }, +]; + +export const LEGEND_SORT_OPTIONS: SelectOption[] = [ + { label: t('No sort'), value: '' }, + { label: t('Ascending'), value: 'asc' }, + { label: t('Descending'), value: 'desc' }, +]; + +/** + * Legend type selector + * Choose between scrollable or plain list legend + */ +export const LegendType = Select.with({ + label: t('Legend Type'), + description: t('Type of legend display'), + options: LEGEND_TYPE_OPTIONS, + default: 'scroll', +}); + +/** + * Legend orientation selector + * Position the legend relative to the chart + */ +export const LegendOrientation = Select.with({ + label: t('Legend Orientation'), + description: t('Position of the legend'), + options: LEGEND_ORIENTATION_OPTIONS, + default: 'top', +}); + +/** + * Legend sort selector + * Sort legend items alphabetically + */ +export const LegendSort = Select.with({ + label: t('Legend Sort'), + description: t('Sort order for legend items'), + options: LEGEND_SORT_OPTIONS, + default: '', +}); + +// ============================================================================ +// Label Presets +// ============================================================================ + +/** + * Show labels toggle + * Common toggle for chart labels + */ +export const ShowLabels = Checkbox.with({ + label: t('Show Labels'), + description: t('Whether to display labels on the chart'), + default: true, +}); + +/** + * Show value toggle + * Common toggle for showing values on chart elements + */ +export const ShowValue = Checkbox.with({ + label: t('Show Value'), + description: t('Whether to display values on the chart'), + default: false, +}); + +// ============================================================================ +// Metric Name Presets +// ============================================================================ + +/** + * Show metric name toggle + * Used in BigNumber charts to optionally show the metric name + */ +export const ShowMetricName = Checkbox.with({ + label: t('Show Metric Name'), + description: t('Whether to display the metric name as a title'), + default: false, +}); + +/** + * Metric name font size selector + * Typically used with visibility tied to ShowMetricName + */ +export const MetricNameFontSize = Select.with({ + label: t('Metric Name Font Size'), + description: t('Font size for the metric name'), + options: FONT_SIZE_OPTIONS_SMALL, + default: 0.15, +}); + +// ============================================================================ +// Label Type Options (shared by Pie, Funnel, etc.) +// ============================================================================ + +/** + * Standard label content type options + * Used by Pie, Funnel, and other category-based charts + */ +export const LABEL_TYPE_OPTIONS: SelectOption[] = [ + { label: t('Category Name'), value: 'key' }, + { label: t('Value'), value: 'value' }, + { label: t('Percentage'), value: 'percent' }, + { label: t('Category and Value'), value: 'key_value' }, + { label: t('Category and Percentage'), value: 'key_percent' }, + { label: t('Category, Value and Percentage'), value: 'key_value_percent' }, + { label: t('Value and Percentage'), value: 'value_percent' }, +]; + +/** + * Label type selector for category-based charts + */ +export const LabelType = Select.with({ + label: t('Label Type'), + description: t('What should be shown on the label?'), + options: LABEL_TYPE_OPTIONS, + default: 'key', +}); + +// ============================================================================ +// Sort Options +// ============================================================================ + +export const SORT_OPTIONS: SelectOption[] = [ + { label: t('Descending'), value: 'descending' }, + { label: t('Ascending'), value: 'ascending' }, + { label: t('None'), value: 'none' }, +]; + +/** + * Sort by metric toggle + * Common for charts that need to sort data by metric value + */ +export const SortByMetric = Checkbox.with({ + label: t('Sort by Metric'), + description: t('Sort results by the selected metric'), + default: true, +}); + +// ============================================================================ +// Label Position Options +// ============================================================================ + +export const LABEL_POSITION_OPTIONS: SelectOption[] = [ + { label: t('Top'), value: 'top' }, + { label: t('Left'), value: 'left' }, + { label: t('Right'), value: 'right' }, + { label: t('Bottom'), value: 'bottom' }, + { label: t('Inside'), value: 'inside' }, + { label: t('Inside Left'), value: 'insideLeft' }, + { label: t('Inside Right'), value: 'insideRight' }, + { label: t('Inside Top'), value: 'insideTop' }, + { label: t('Inside Bottom'), value: 'insideBottom' }, +]; + +/** + * Label position selector + * Position labels relative to chart elements + */ +export const LabelPosition = Select.with({ + label: t('Label Position'), + description: t('Position of labels on the chart'), + options: LABEL_POSITION_OPTIONS, + default: 'top', +}); + +// ============================================================================ +// Simple Label Type (key/value variants only) +// ============================================================================ + +/** + * Simple label type options - for charts with fewer label display options + * Used by Radar, Sunburst, etc. + */ +export const SIMPLE_LABEL_TYPE_OPTIONS: SelectOption[] = [ + { label: t('Category Name'), value: 'key' }, + { label: t('Value'), value: 'value' }, + { label: t('Category and Value'), value: 'key_value' }, +]; + +/** + * Simple label type selector + * For charts that only need key/value/key_value options + */ +export const SimpleLabelType = Select.with({ + label: t('Label Type'), + description: t('What should be shown on the label?'), + options: SIMPLE_LABEL_TYPE_OPTIONS, + default: 'key', +}); + +/** + * Value-only label type options - for charts like Radar + */ +export const VALUE_LABEL_TYPE_OPTIONS: SelectOption[] = [ + { label: t('Value'), value: 'value' }, + { label: t('Category and Value'), value: 'key_value' }, +]; + +/** + * Value label type selector + * For charts that show value or category+value + */ +export const ValueLabelType = Select.with({ + label: t('Label Type'), + description: t('What should be shown on the label?'), + options: VALUE_LABEL_TYPE_OPTIONS, + default: 'value', +}); + +// ============================================================================ +// Totals and Aggregates +// ============================================================================ + +/** + * Show total toggle + * For charts that can display aggregate totals + */ +export const ShowTotal = Checkbox.with({ + label: t('Show Total'), + description: t('Whether to display the aggregate total'), + default: false, +}); + +// ============================================================================ +// Threshold Controls +// ============================================================================ + +/** + * Label percentage threshold + * Minimum percentage for showing labels (avoids clutter on small slices) + */ +export const LabelThreshold = Text.with({ + label: t('Percentage Threshold'), + description: t('Minimum threshold in percentage points for showing labels'), + default: '5', +}); + +// ============================================================================ +// Shape Options +// ============================================================================ + +/** + * Circle shape toggle (used by Radar) + */ +export const CircleShape = Checkbox.with({ + label: t('Circle Shape'), + description: t('Use circular shape instead of polygon'), + default: false, +}); + +// ============================================================================ +// Data Zoom +// ============================================================================ + +/** + * Enable data zoom toggle + * For charts with zoomable data areas + */ +export const DataZoom = Checkbox.with({ + label: t('Data Zoom'), + description: t('Enable data zooming controls'), + default: false, +}); diff --git a/superset-frontend/packages/superset-ui-glyph-core/src/types.ts b/superset-frontend/packages/superset-ui-glyph-core/src/types.ts new file mode 100644 index 00000000000..d90b6c16bd6 --- /dev/null +++ b/superset-frontend/packages/superset-ui-glyph-core/src/types.ts @@ -0,0 +1,306 @@ +/** + * 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 type { ControlPanelConfig } from '@superset-ui/chart-controls'; +import type { ChartProps } from '@superset-ui/core'; + +/** + * Option for Select controls + */ +export interface SelectOption { + label: string; + value: string | number; +} + +/** + * Configuration options for Select argument type + */ +export interface SelectOptions { + label?: string; + description?: string; + default?: string | number; + options?: SelectOption[]; + clearable?: boolean; + renderTrigger?: boolean; +} + +/** + * Configuration options for Text argument type + */ +export interface TextOptions { + label?: string; + description?: string; + default?: string; + placeholder?: string; +} + +/** + * Configuration options for Checkbox argument type + */ +export interface CheckboxOptions { + label?: string; + description?: string; + default?: boolean; +} + +/** + * Configuration options for Int argument type (slider) + */ +export interface IntOptions { + label?: string; + description?: string; + default?: number; + min?: number; + max?: number; + step?: number; +} + +/** + * Configuration options for Color argument type + */ +export interface ColorOptions { + label?: string; + description?: string; + default?: string; +} + +/** + * Configuration options for Metric argument type + */ +export interface MetricOptions { + label?: string; + description?: string; + multi?: boolean; +} + +/** + * Configuration options for Dimension argument type + */ +export interface DimensionOptions { + label?: string; + description?: string; + multi?: boolean; +} + +/** + * Configuration options for NumberFormat argument type + */ +export interface NumberFormatOptions { + label?: string; + description?: string; + default?: string; +} + +/** + * Currency value structure + */ +export interface CurrencyValue { + symbol?: string; + symbolPosition?: 'prefix' | 'suffix'; +} + +/** + * Configuration options for Currency argument type + */ +export interface CurrencyOptions { + label?: string; + description?: string; + default?: CurrencyValue; +} + +/** + * Configuration options for TimeFormat argument type + */ +export interface TimeFormatOptions { + label?: string; + description?: string; + default?: string; +} + +/** + * Configuration options for ConditionalFormatting argument type + */ +export interface ConditionalFormattingOptions { + label?: string; + description?: string; +} + +/** + * Configuration options for Slider argument type (continuous float values) + */ +export interface SliderOptions { + label?: string; + description?: string; + default?: number; + min?: number; + max?: number; + step?: number; +} + +/** + * Configuration options for Bounds argument type (min/max pairs) + */ +export interface BoundsOptions { + label?: string; + description?: string; + default?: [number | null, number | null]; +} + +/** + * Bounds value type - tuple of [min, max] where either can be null + */ +export type BoundsValue = [number | null, number | null]; + +/** + * Configuration options for ColorPicker argument type (RGBA colors) + */ +export interface ColorPickerOptions { + label?: string; + description?: string; + default?: RgbaColor; +} + +/** + * Configuration options for RadioButton argument type + */ +export interface RadioButtonOptions { + label?: string; + description?: string; + default?: string | boolean; + options: RadioOption[]; +} + +/** + * Option for RadioButton controls + */ +export interface RadioOption { + label: string; + value: string | boolean; +} + +/** + * Conditional formatting rule value + */ +export interface ConditionalFormattingRule { + column?: string; + operator?: '<' | '<=' | '>' | '>=' | '==' | '!=' | 'between'; + targetValue?: number; + targetValueLeft?: number; + targetValueRight?: number; + colorScheme?: string; +} + +/** + * Column type enum for data arguments + */ +export enum ColumnType { + Metric = 'metric', + Dimension = 'dimension', + Temporal = 'temporal', + Argument = 'argument', +} + +/** + * Base argument class interface + */ +export interface ArgumentClass { + label: string | null; + description: string | null; + columnType?: ColumnType; + controlType?: string; +} + +/** + * RGBA color format used by Superset's ColorPickerControl + */ +export interface RgbaColor { + r: number; + g: number; + b: number; + a: number; +} + +/** + * Visibility function for conditional control display (legacy) + */ +export type VisibilityFn = (state: { + controls: Record; +}) => boolean; + +/** + * Declarative condition for argument visibility/disabled state. + * + * Keys are argument names, values define the condition: + * - Literal value: equality check (e.g., { showMetricName: true }) + * - Function: custom check (e.g., { subtitle: (val) => !!val }) + * + * Multiple keys are AND'd together. + * + * @example + * // Visible when showMetricName is true + * visibleWhen: { showMetricName: true } + * + * @example + * // Visible when subtitle is not empty + * visibleWhen: { subtitle: (val) => !!val } + * + * @example + * // Visible when showMetricName is true AND subtitle is not empty + * visibleWhen: { showMetricName: true, subtitle: (val) => !!val } + */ +export type ArgumentCondition = Record< + string, + unknown | ((value: unknown) => boolean) +>; + +/** + * Extended control configuration with visibility + */ +export interface ControlConfig { + name: string; + config: Record; + visibility?: VisibilityFn; + resetOnHide?: boolean; +} + +/** + * Glyph chart definition + */ +export interface GlyphChartDef> { + arguments: TArgs; + sections?: { + query?: ControlConfig[][]; + chartOptions?: ControlConfig[][]; + }; +} + +/** + * Result of createGlyphPlugin + */ +export interface GlyphPluginResult { + controlPanel: ControlPanelConfig; + transformProps: (chartProps: ChartProps) => TFormData; +} + +/** + * Type helper to extract form data types from argument definitions + */ +export type ArgumentsToFormData> = { + [K in keyof TArgs]: TArgs[K] extends { default: infer D } ? D : unknown; +}; diff --git a/superset-frontend/packages/superset-ui-glyph-core/tsconfig.json b/superset-frontend/packages/superset-ui-glyph-core/tsconfig.json new file mode 100644 index 00000000000..17a3be1b17a --- /dev/null +++ b/superset-frontend/packages/superset-ui-glyph-core/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + // Path Resolution: Override baseUrl to maintain correct path mappings from parent config + // (e.g., "@apache-superset/core" -> "./packages/superset-core/src") + "baseUrl": "../..", + + // Directory Overrides: Parent config paths are relative to frontend root, + // but packages need paths relative to their own directory + "outDir": "lib", + "rootDir": "src", + "declarationDir": "lib" + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "types/**/*"], + "exclude": ["src/**/*.test.*", "src/**/*.stories.*"], + "references": [ + { "path": "../superset-core" }, + { "path": "../superset-ui-core" }, + { "path": "../superset-ui-chart-controls" } + ] +} diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/package.json b/superset-frontend/plugins/legacy-plugin-chart-calendar/package.json index 58967416345..8296e1ec815 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-calendar/package.json +++ b/superset-frontend/plugins/legacy-plugin-chart-calendar/package.json @@ -32,6 +32,7 @@ "@emotion/react": "^11.4.1", "@superset-ui/chart-controls": "*", "@superset-ui/core": "*", + "@superset-ui/glyph-core": "*", "@apache-superset/core": "*", "react": "^18.2.0" }, diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/controlPanel.ts b/superset-frontend/plugins/legacy-plugin-chart-calendar/src/controlPanel.ts deleted file mode 100644 index 79e5670f7a1..00000000000 --- a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/controlPanel.ts +++ /dev/null @@ -1,205 +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 { t } from '@apache-superset/core/translation'; -import { legacyValidateInteger } from '@superset-ui/core'; -import { - ControlPanelConfig, - D3_FORMAT_DOCS, - D3_TIME_FORMAT_OPTIONS, - getStandardizedControls, -} from '@superset-ui/chart-controls'; - -const config: ControlPanelConfig = { - controlPanelSections: [ - { - label: t('Time'), - expanded: true, - description: t('Time related form attributes'), - controlSetRows: [['granularity_sqla'], ['time_range']], - }, - { - label: t('Query'), - expanded: true, - controlSetRows: [ - [ - { - name: 'domain_granularity', - config: { - type: 'SelectControl', - label: t('Domain'), - default: 'month', - choices: [ - ['hour', t('hour')], - ['day', t('day')], - ['week', t('week')], - ['month', t('month')], - ['year', t('year')], - ], - description: t('The time unit used for the grouping of blocks'), - }, - }, - { - name: 'subdomain_granularity', - config: { - type: 'SelectControl', - label: t('Subdomain'), - default: 'day', - choices: [ - ['min', t('min')], - ['hour', t('hour')], - ['day', t('day')], - ['week', t('week')], - ['month', t('month')], - ], - description: t( - 'The time unit for each block. Should be a smaller unit than ' + - 'domain_granularity. Should be larger or equal to Time Grain', - ), - }, - }, - ], - ['metrics'], - ['adhoc_filters'], - ], - }, - { - label: t('Chart Options'), - expanded: true, - tabOverride: 'customize', - controlSetRows: [ - ['linear_color_scheme'], - [ - { - name: 'cell_size', - config: { - type: 'TextControl', - isInt: true, - default: 10, - validators: [legacyValidateInteger], - renderTrigger: true, - label: t('Cell Size'), - description: t('The size of the square cell, in pixels'), - }, - }, - { - name: 'cell_padding', - config: { - type: 'TextControl', - isInt: true, - validators: [legacyValidateInteger], - renderTrigger: true, - default: 2, - label: t('Cell Padding'), - description: t('The distance between cells, in pixels'), - }, - }, - ], - [ - { - name: 'cell_radius', - config: { - type: 'TextControl', - isInt: true, - validators: [legacyValidateInteger], - renderTrigger: true, - default: 0, - label: t('Cell Radius'), - description: t('The pixel radius'), - }, - }, - { - name: 'steps', - config: { - type: 'TextControl', - isInt: true, - validators: [legacyValidateInteger], - renderTrigger: true, - default: 10, - label: t('Color Steps'), - description: t('The number color "steps"'), - }, - }, - ], - [ - 'y_axis_format', - { - name: 'x_axis_time_format', - config: { - type: 'SelectControl', - freeForm: true, - label: t('Time Format'), - renderTrigger: true, - default: 'smart_date', - choices: D3_TIME_FORMAT_OPTIONS, - description: D3_FORMAT_DOCS, - }, - }, - ], - [ - { - name: 'show_legend', - config: { - type: 'CheckboxControl', - label: t('Legend'), - renderTrigger: true, - default: true, - description: t('Whether to display the legend (toggles)'), - }, - }, - { - name: 'show_values', - config: { - type: 'CheckboxControl', - label: t('Show Values'), - renderTrigger: true, - default: false, - description: t( - 'Whether to display the numerical values within the cells', - ), - }, - }, - ], - [ - { - name: 'show_metric_name', - config: { - type: 'CheckboxControl', - label: t('Show Metric Names'), - renderTrigger: true, - default: true, - description: t('Whether to display the metric name as a title'), - }, - }, - null, - ], - ], - }, - ], - controlOverrides: { - y_axis_format: { - label: t('Number Format'), - }, - }, - formDataOverrides: formData => ({ - ...formData, - metrics: getStandardizedControls().popAllMetrics(), - }), -}; - -export default config; diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/index.ts b/superset-frontend/plugins/legacy-plugin-chart-calendar/src/index.ts deleted file mode 100644 index ea2ce7e47ec..00000000000 --- a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/index.ts +++ /dev/null @@ -1,58 +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 { ChartMetadata, ChartPlugin } from '@superset-ui/core'; -import { t } from '@apache-superset/core/translation'; -import transformProps from './transformProps'; -import example from './images/example.jpg'; -import exampleDark from './images/example-dark.jpg'; -import controlPanel from './controlPanel'; -import thumbnail from './images/thumbnail.png'; -import thumbnailDark from './images/thumbnail-dark.png'; - -const metadata = new ChartMetadata({ - category: t('Correlation'), - credits: ['https://github.com/wa0x6e/cal-heatmap'], - description: t( - "Visualizes how a metric has changed over a time using a color scale and a calendar view. Gray values are used to indicate missing values and the linear color scheme is used to encode the magnitude of each day's value.", - ), - exampleGallery: [{ url: example, urlDark: exampleDark }], - name: t('Calendar Heatmap'), - tags: [ - t('Business'), - t('Comparison'), - t('Intensity'), - t('Pattern'), - t('Report'), - t('Trend'), - ], - thumbnail, - thumbnailDark, - useLegacyApi: true, -}); - -export default class CalendarChartPlugin extends ChartPlugin { - constructor() { - super({ - loadChart: () => import('./ReactCalendar'), - metadata, - transformProps, - controlPanel, - }); - } -} diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/index.tsx b/superset-frontend/plugins/legacy-plugin-chart-calendar/src/index.tsx new file mode 100644 index 00000000000..140babf36ce --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-calendar/src/index.tsx @@ -0,0 +1,241 @@ +/** + * 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 { t } from '@apache-superset/core/translation'; +import { getNumberFormatter } from '@superset-ui/core'; +import { + D3_FORMAT_DOCS, + getStandardizedControls, +} from '@superset-ui/chart-controls'; +import { + defineChart, + Int, + Checkbox, + TimeFormat, +} from '@superset-ui/glyph-core'; +import example from './images/example.jpg'; +import exampleDark from './images/example-dark.jpg'; +import thumbnail from './images/thumbnail.png'; +import thumbnailDark from './images/thumbnail-dark.png'; +import { getFormattedUTCTime } from './utils'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const ReactCalendar = require('./ReactCalendar').default; + +type CalendarExtra = { + timeFormatter: (ts: number | string) => string; + valueFormatter: (val: unknown) => string; + verboseMap: Record; + domainGranularity: string; + subdomainGranularity: string; + linearColorScheme: string; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export default defineChart({ + metadata: { + name: t('Calendar Heatmap'), + description: t( + "Visualizes how a metric has changed over a time using a color scale and a calendar view. Gray values are used to indicate missing values and the linear color scheme is used to encode the magnitude of each day's value.", + ), + category: t('Correlation'), + credits: ['https://github.com/wa0x6e/cal-heatmap'], + tags: [ + t('Business'), + t('Comparison'), + t('Intensity'), + t('Pattern'), + t('Report'), + t('Trend'), + ], + thumbnail, + thumbnailDark, + exampleGallery: [{ url: example, urlDark: exampleDark }], + }, + arguments: { + cell_size: Int.with({ + label: 'Cell Size', + description: 'The size of the square cell, in pixels', + default: 10, + min: 1, + max: 100, + }), + cell_padding: Int.with({ + label: 'Cell Padding', + description: 'The distance between cells, in pixels', + default: 2, + min: 0, + max: 20, + }), + cell_radius: Int.with({ + label: 'Cell Radius', + description: 'The pixel radius', + default: 0, + min: 0, + max: 50, + }), + steps: Int.with({ + label: 'Color Steps', + description: 'The number color "steps"', + default: 10, + min: 1, + max: 50, + }), + x_axis_time_format: TimeFormat.with({ + label: 'Time Format', + description: D3_FORMAT_DOCS, + default: 'smart_date', + }), + show_legend: Checkbox.with({ + label: 'Legend', + description: 'Whether to display the legend (toggles)', + default: true, + }), + show_values: Checkbox.with({ + label: 'Show Values', + description: 'Whether to display the numerical values within the cells', + default: false, + }), + show_metric_name: Checkbox.with({ + label: 'Show Metric Names', + description: 'Whether to display the metric name as a title', + default: true, + }), + }, + prependSections: [ + { + label: t('Time'), + expanded: true, + description: t('Time related form attributes'), + controlSetRows: [['granularity_sqla'], ['time_range']], + }, + ], + additionalControls: { + queryBefore: [ + [ + { + name: 'domain_granularity', + config: { + type: 'SelectControl', + label: t('Domain'), + default: 'month', + choices: [ + ['hour', t('hour')], + ['day', t('day')], + ['week', t('week')], + ['month', t('month')], + ['year', t('year')], + ], + description: t('The time unit used for the grouping of blocks'), + }, + }, + { + name: 'subdomain_granularity', + config: { + type: 'SelectControl', + label: t('Subdomain'), + default: 'day', + choices: [ + ['min', t('min')], + ['hour', t('hour')], + ['day', t('day')], + ['week', t('week')], + ['month', t('month')], + ], + description: t( + 'The time unit for each block. Should be a smaller unit than ' + + 'domain_granularity. Should be larger or equal to Time Grain', + ), + }, + }, + ], + ['metrics'], + ], + chartOptions: [['linear_color_scheme'], ['y_axis_format']], + }, + chartOptionsTabOverride: 'customize', + additionalControlOverrides: { + y_axis_format: { + label: t('Number Format'), + }, + }, + formDataOverrides: formData => ({ + ...formData, + metrics: getStandardizedControls().popAllMetrics(), + }), + transform: (chartProps, { x_axis_time_format }) => { + const { formData, datasource } = chartProps; + const { + domainGranularity, + subdomainGranularity, + linearColorScheme, + yAxisFormat, + } = formData as Record; + + const verboseMap = + (datasource as { verboseMap?: Record })?.verboseMap ?? {}; + const timeFormatter = (ts: number | string) => + getFormattedUTCTime(ts, x_axis_time_format as string); + const valueFormatter = getNumberFormatter(yAxisFormat); + + return { + timeFormatter, + valueFormatter: valueFormatter as (val: unknown) => string, + verboseMap, + domainGranularity: domainGranularity ?? 'month', + subdomainGranularity: subdomainGranularity ?? 'day', + linearColorScheme: linearColorScheme ?? '', + }; + }, + render: ({ + height, + data, + cell_size: cellSize, + cell_padding: cellPadding, + cell_radius: cellRadius, + steps, + show_legend: showLegend, + show_values: showValues, + show_metric_name: showMetricName, + timeFormatter, + valueFormatter, + verboseMap, + domainGranularity, + subdomainGranularity, + linearColorScheme, + }) => ( + + ), +}); diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/transformProps.ts b/superset-frontend/plugins/legacy-plugin-chart-calendar/src/transformProps.ts deleted file mode 100644 index b20773ee70d..00000000000 --- a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/transformProps.ts +++ /dev/null @@ -1,62 +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 { ChartProps, getNumberFormatter } from '@superset-ui/core'; -import { getFormattedUTCTime } from './utils'; - -export default function transformProps(chartProps: ChartProps) { - const { height, formData, queriesData, datasource } = chartProps; - const { - cellPadding, - cellRadius, - cellSize, - domainGranularity, - linearColorScheme, - showLegend, - showMetricName, - showValues, - steps, - subdomainGranularity, - xAxisTimeFormat, - yAxisFormat, - } = formData; - - const { verboseMap } = datasource; - const timeFormatter = (ts: number | string) => - getFormattedUTCTime(ts, xAxisTimeFormat); - const valueFormatter = getNumberFormatter(yAxisFormat); - - return { - height, - data: queriesData[0].data, - cellPadding, - cellRadius, - cellSize, - domainGranularity, - linearColorScheme, - showLegend, - showMetricName, - showValues, - steps, - subdomainGranularity, - timeFormatter, - valueFormatter, - verboseMap, - }; -} diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/tsconfig.json b/superset-frontend/plugins/legacy-plugin-chart-calendar/tsconfig.json index 03190ec5bd4..f0bfbba676d 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-calendar/tsconfig.json +++ b/superset-frontend/plugins/legacy-plugin-chart-calendar/tsconfig.json @@ -20,6 +20,7 @@ "references": [ { "path": "../../packages/superset-core" }, { "path": "../../packages/superset-ui-core" }, - { "path": "../../packages/superset-ui-chart-controls" } + { "path": "../../packages/superset-ui-chart-controls" }, + { "path": "../../packages/superset-ui-glyph-core" } ] } diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/types/external.d.ts b/superset-frontend/plugins/legacy-plugin-chart-calendar/types/external.d.ts index 66677a600a6..ecd4536d29e 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-calendar/types/external.d.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-calendar/types/external.d.ts @@ -16,13 +16,11 @@ * specific language governing permissions and limitations * under the License. */ - declare module '*.png' { - const value: string; + const value: any; export default value; } - declare module '*.jpg' { - const value: string; + const value: any; export default value; } diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/package.json b/superset-frontend/plugins/legacy-plugin-chart-chord/package.json index deda7030485..c9987396917 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-chord/package.json +++ b/superset-frontend/plugins/legacy-plugin-chart-chord/package.json @@ -36,6 +36,7 @@ "peerDependencies": { "@superset-ui/chart-controls": "*", "@superset-ui/core": "*", - "@apache-superset/core": "*" + "@apache-superset/core": "*", + "@superset-ui/glyph-core": "*" } } diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/src/controlPanel.ts b/superset-frontend/plugins/legacy-plugin-chart-chord/src/controlPanel.ts deleted file mode 100644 index 011227e6ff4..00000000000 --- a/superset-frontend/plugins/legacy-plugin-chart-chord/src/controlPanel.ts +++ /dev/null @@ -1,76 +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 { t } from '@apache-superset/core/translation'; -import { ensureIsArray, validateNonEmpty } from '@superset-ui/core'; -import { - ControlPanelConfig, - getStandardizedControls, -} from '@superset-ui/chart-controls'; - -const config: ControlPanelConfig = { - controlPanelSections: [ - { - label: t('Query'), - expanded: true, - controlSetRows: [ - ['groupby'], - ['columns'], - ['metric'], - ['adhoc_filters'], - ['row_limit'], - ['sort_by_metric'], - ], - }, - { - label: t('Chart Options'), - expanded: true, - controlSetRows: [['y_axis_format', null], ['color_scheme']], - }, - ], - controlOverrides: { - y_axis_format: { - label: t('Number format'), - description: t('Choose a number format'), - }, - groupby: { - label: t('Source'), - multi: false, - validators: [validateNonEmpty], - description: t('Choose a source'), - }, - columns: { - label: t('Target'), - multi: false, - validators: [validateNonEmpty], - description: t('Choose a target'), - }, - }, - formDataOverrides: formData => { - const groupby = getStandardizedControls() - .popAllColumns() - .filter(col => !ensureIsArray(formData.columns).includes(col)); - return { - ...formData, - groupby, - metric: getStandardizedControls().shiftMetric(), - }; - }, -}; - -export default config; diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/src/index.ts b/superset-frontend/plugins/legacy-plugin-chart-chord/src/index.ts deleted file mode 100644 index 4f96b2f547b..00000000000 --- a/superset-frontend/plugins/legacy-plugin-chart-chord/src/index.ts +++ /dev/null @@ -1,57 +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 { t } from '@apache-superset/core/translation'; -import { ChartMetadata, ChartPlugin } from '@superset-ui/core'; -import transformProps from './transformProps'; -import example from './images/chord.jpg'; -import exampleDark from './images/chord-dark.jpg'; -import thumbnail from './images/thumbnail.png'; -import thumbnailDark from './images/thumbnail-dark.png'; -import controlPanel from './controlPanel'; - -const metadata = new ChartMetadata({ - category: t('Flow'), - credits: ['https://github.com/d3/d3-chord'], - description: t( - 'Showcases the flow or link between categories using thickness of chords. The value and corresponding thickness can be different for each side.', - ), - exampleGallery: [ - { - url: example, - urlDark: exampleDark, - caption: t('Relationships between community channels'), - }, - ], - name: t('Chord Diagram'), - tags: [t('Circular'), t('Legacy'), t('Proportional'), t('Relational')], - thumbnail, - thumbnailDark, - useLegacyApi: true, -}); - -export default class ChordChartPlugin extends ChartPlugin { - constructor() { - super({ - loadChart: () => import('./ReactChord'), - metadata, - transformProps, - controlPanel, - }); - } -} diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/src/index.tsx b/superset-frontend/plugins/legacy-plugin-chart-chord/src/index.tsx new file mode 100644 index 00000000000..e46d1ba2c7a --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-chord/src/index.tsx @@ -0,0 +1,120 @@ +/** + * 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 { t } from '@apache-superset/core/translation'; +import { ensureIsArray, validateNonEmpty } from '@superset-ui/core'; +import { getStandardizedControls } from '@superset-ui/chart-controls'; +import { defineChart } from '@superset-ui/glyph-core'; +import example from './images/chord.jpg'; +import exampleDark from './images/chord-dark.jpg'; +import thumbnail from './images/thumbnail.png'; +import thumbnailDark from './images/thumbnail-dark.png'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const ReactChord = require('./ReactChord').default; + +type ChordExtra = { + colorScheme: string; + numberFormat: string; + sliceId: number; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export default defineChart({ + metadata: { + name: t('Chord Diagram'), + description: t( + 'Showcases the flow or link between categories using thickness of chords. The value and corresponding thickness can be different for each side.', + ), + category: t('Flow'), + credits: ['https://github.com/d3/d3-chord'], + tags: [t('Circular'), t('Legacy'), t('Proportional'), t('Relational')], + thumbnail, + thumbnailDark, + exampleGallery: [ + { + url: example, + urlDark: exampleDark, + caption: t('Relationships between community channels'), + }, + ], + useLegacyApi: true, + }, + arguments: {}, + additionalControls: { + queryBefore: [['groupby'], ['columns'], ['metric']], + query: [['row_limit'], ['sort_by_metric']], + chartOptions: [['y_axis_format', null], ['color_scheme']], + }, + additionalControlOverrides: { + y_axis_format: { + label: t('Number format'), + description: t('Choose a number format'), + }, + groupby: { + label: t('Source'), + multi: false, + validators: [validateNonEmpty], + description: t('Choose a source'), + }, + columns: { + label: t('Target'), + multi: false, + validators: [validateNonEmpty], + description: t('Choose a target'), + }, + }, + formDataOverrides: formData => { + const groupby = getStandardizedControls() + .popAllColumns() + .filter( + (col: string) => + // eslint-disable-next-line @typescript-eslint/no-explicit-any + !ensureIsArray((formData as any).columns).includes(col), + ); + return { + ...formData, + groupby, + metric: getStandardizedControls().shiftMetric(), + }; + }, + transform: chartProps => { + const { formData } = chartProps; + const { yAxisFormat, colorScheme, sliceId } = formData as Record< + string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + any + >; + return { + colorScheme: colorScheme ?? '', + numberFormat: yAxisFormat ?? '', + sliceId: sliceId ?? 0, + }; + }, + render: ({ width, height, data, colorScheme, numberFormat, sliceId }) => ( + + ), +}); diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/src/transformProps.ts b/superset-frontend/plugins/legacy-plugin-chart-chord/src/transformProps.ts deleted file mode 100644 index 4b8fef75104..00000000000 --- a/superset-frontend/plugins/legacy-plugin-chart-chord/src/transformProps.ts +++ /dev/null @@ -1,33 +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 { ChartProps } from '@superset-ui/core'; - -export default function transformProps(chartProps: ChartProps) { - const { width, height, formData, queriesData } = chartProps; - const { yAxisFormat, colorScheme, sliceId } = formData; - - return { - colorScheme, - data: queriesData[0].data, - height, - numberFormat: yAxisFormat, - width, - sliceId, - }; -} diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/tsconfig.json b/superset-frontend/plugins/legacy-plugin-chart-chord/tsconfig.json index 03190ec5bd4..f0bfbba676d 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-chord/tsconfig.json +++ b/superset-frontend/plugins/legacy-plugin-chart-chord/tsconfig.json @@ -20,6 +20,7 @@ "references": [ { "path": "../../packages/superset-core" }, { "path": "../../packages/superset-ui-core" }, - { "path": "../../packages/superset-ui-chart-controls" } + { "path": "../../packages/superset-ui-chart-controls" }, + { "path": "../../packages/superset-ui-glyph-core" } ] } diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/types/external.d.ts b/superset-frontend/plugins/legacy-plugin-chart-chord/types/external.d.ts index 66677a600a6..ecd4536d29e 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-chord/types/external.d.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-chord/types/external.d.ts @@ -16,13 +16,11 @@ * specific language governing permissions and limitations * under the License. */ - declare module '*.png' { - const value: string; + const value: any; export default value; } - declare module '*.jpg' { - const value: string; + const value: any; export default value; } diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/package.json b/superset-frontend/plugins/legacy-plugin-chart-country-map/package.json index 6297a4597e2..f03f47fd31e 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-country-map/package.json +++ b/superset-frontend/plugins/legacy-plugin-chart-country-map/package.json @@ -34,6 +34,7 @@ "@apache-superset/core": "*", "@superset-ui/chart-controls": "*", "@superset-ui/core": "*", + "@superset-ui/glyph-core": "*", "react": "^18.2.0" } } diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/controlPanel.ts b/superset-frontend/plugins/legacy-plugin-chart-country-map/src/controlPanel.ts deleted file mode 100644 index 16b923be539..00000000000 --- a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/controlPanel.ts +++ /dev/null @@ -1,99 +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 { t } from '@apache-superset/core/translation'; -import { validateNonEmpty } from '@superset-ui/core'; -import { - ControlPanelConfig, - D3_FORMAT_OPTIONS, - D3_FORMAT_DOCS, - getStandardizedControls, -} from '@superset-ui/chart-controls'; -import { countryOptions } from './countries'; - -const config: ControlPanelConfig = { - controlPanelSections: [ - { - label: t('Query'), - expanded: true, - controlSetRows: [ - [ - { - name: 'select_country', - config: { - type: 'SelectControl', - label: t('Country'), - default: null, - choices: countryOptions, - description: t('Which country to plot the map for?'), - validators: [validateNonEmpty], - }, - }, - ], - ['entity'], - ['metric'], - ['adhoc_filters'], - ], - }, - { - label: t('Chart Options'), - expanded: true, - tabOverride: 'customize', - controlSetRows: [ - [ - { - name: 'number_format', - config: { - type: 'SelectControl', - freeForm: true, - label: t('Number format'), - renderTrigger: true, - default: 'SMART_NUMBER', - choices: D3_FORMAT_OPTIONS, - description: D3_FORMAT_DOCS, - }, - }, - ], - ['currency_format'], - ['linear_color_scheme'], - ], - }, - ], - controlOverrides: { - entity: { - label: t('ISO 3166-2 Codes'), - description: t( - 'Column containing ISO 3166-2 codes of region/province/department in your table.', - ), - }, - metric: { - label: t('Metric'), - description: t('Metric to display bottom title'), - }, - linear_color_scheme: { - renderTrigger: false, - }, - }, - formDataOverrides: formData => ({ - ...formData, - entity: getStandardizedControls().shiftColumn(), - metric: getStandardizedControls().shiftMetric(), - }), -}; - -export default config; diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/index.ts b/superset-frontend/plugins/legacy-plugin-chart-country-map/src/index.ts deleted file mode 100644 index e59be4ec4b0..00000000000 --- a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/index.ts +++ /dev/null @@ -1,65 +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 { t } from '@apache-superset/core/translation'; -import { ChartMetadata, ChartPlugin } from '@superset-ui/core'; -import transformProps from './transformProps'; -import exampleUsa from './images/exampleUsa.jpg'; -import exampleUsaDark from './images/exampleUsa-dark.jpg'; -import exampleGermany from './images/exampleGermany.jpg'; -import exampleGermanyDark from './images/exampleGermany-dark.jpg'; -import thumbnail from './images/thumbnail.png'; -import thumbnailDark from './images/thumbnail-dark.png'; -import controlPanel from './controlPanel'; - -const metadata = new ChartMetadata({ - category: t('Map'), - credits: ['https://bl.ocks.org/john-guerra'], - description: t( - "Visualizes how a single metric varies across a country's principal subdivisions (states, provinces, etc) on a choropleth map. Each subdivision's value is elevated when you hover over the corresponding geographic boundary.", - ), - exampleGallery: [ - { url: exampleUsa, urlDark: exampleUsaDark }, - { url: exampleGermany, urlDark: exampleGermanyDark }, - ], - name: t('Country Map'), - tags: [ - t('2D'), - t('Comparison'), - t('Geo'), - t('Range'), - t('Report'), - t('Stacked'), - ], - thumbnail, - thumbnailDark, - useLegacyApi: true, -}); - -export default class CountryMapChartPlugin extends ChartPlugin { - constructor() { - super({ - loadChart: () => import('./ReactCountryMap'), - metadata, - transformProps, - controlPanel, - }); - } -} - -export { default as countries } from './countries'; diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/index.tsx b/superset-frontend/plugins/legacy-plugin-chart-country-map/src/index.tsx new file mode 100644 index 00000000000..9173c0f6233 --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-country-map/src/index.tsx @@ -0,0 +1,170 @@ +/** + * 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 { t } from '@apache-superset/core/translation'; +import { validateNonEmpty } from '@superset-ui/core'; +import { + D3_FORMAT_OPTIONS, + D3_FORMAT_DOCS, + getStandardizedControls, +} from '@superset-ui/chart-controls'; +import { defineChart } from '@superset-ui/glyph-core'; +import exampleUsa from './images/exampleUsa.jpg'; +import exampleUsaDark from './images/exampleUsa-dark.jpg'; +import exampleGermany from './images/exampleGermany.jpg'; +import exampleGermanyDark from './images/exampleGermany-dark.jpg'; +import thumbnail from './images/thumbnail.png'; +import thumbnailDark from './images/thumbnail-dark.png'; +import { countryOptions } from './countries'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const ReactCountryMap = require('./ReactCountryMap').default; + +export { default as countries } from './countries'; + +type CountryMapExtra = { + country: string | null; + linearColorScheme: string; + numberFormat: string; + colorScheme: string; + sliceId: number; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export default defineChart({ + metadata: { + name: t('Country Map'), + description: t( + "Visualizes how a single metric varies across a country's principal subdivisions (states, provinces, etc) on a choropleth map. Each subdivision's value is elevated when you hover over the corresponding geographic boundary.", + ), + category: t('Map'), + credits: ['https://bl.ocks.org/john-guerra'], + tags: [ + t('2D'), + t('Comparison'), + t('Geo'), + t('Range'), + t('Report'), + t('Stacked'), + ], + thumbnail, + thumbnailDark, + exampleGallery: [ + { url: exampleUsa, urlDark: exampleUsaDark }, + { url: exampleGermany, urlDark: exampleGermanyDark }, + ], + useLegacyApi: true, + }, + arguments: {}, + additionalControls: { + queryBefore: [ + [ + { + name: 'select_country', + config: { + type: 'SelectControl', + label: t('Country'), + default: null, + choices: countryOptions, + description: t('Which country to plot the map for?'), + validators: [validateNonEmpty], + }, + }, + ], + ['entity'], + ['metric'], + ], + chartOptions: [ + [ + { + name: 'number_format', + config: { + type: 'SelectControl', + freeForm: true, + label: t('Number format'), + renderTrigger: true, + default: 'SMART_NUMBER', + choices: D3_FORMAT_OPTIONS, + description: D3_FORMAT_DOCS, + }, + }, + ], + ['linear_color_scheme'], + ], + }, + chartOptionsTabOverride: 'customize', + additionalControlOverrides: { + entity: { + label: t('ISO 3166-2 Codes'), + description: t( + 'Column containing ISO 3166-2 codes of region/province/department in your table.', + ), + }, + metric: { + label: t('Metric'), + description: t('Metric to display bottom title'), + }, + linear_color_scheme: { + renderTrigger: false, + }, + }, + formDataOverrides: formData => ({ + ...formData, + entity: getStandardizedControls().shiftColumn(), + metric: getStandardizedControls().shiftMetric(), + }), + transform: chartProps => { + const { formData } = chartProps; + const { + linearColorScheme, + numberFormat, + selectCountry, + colorScheme, + sliceId, + } = formData as Record; + return { + country: selectCountry ? String(selectCountry).toLowerCase() : null, + linearColorScheme: (linearColorScheme as string) ?? '', + numberFormat: (numberFormat as string) ?? '', + colorScheme: (colorScheme as string) ?? '', + sliceId: (sliceId as number) ?? 0, + }; + }, + render: ({ + width, + height, + data, + country, + linearColorScheme, + numberFormat, + colorScheme, + sliceId, + }) => ( + + ), +}); diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/transformProps.ts b/superset-frontend/plugins/legacy-plugin-chart-country-map/src/transformProps.ts deleted file mode 100644 index af32c4ef46d..00000000000 --- a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/transformProps.ts +++ /dev/null @@ -1,63 +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 { ChartProps, getValueFormatter } from '@superset-ui/core'; - -export default function transformProps(chartProps: ChartProps) { - const { width, height, formData, queriesData, datasource } = chartProps; - const { - linearColorScheme, - numberFormat, - currencyFormat, - selectCountry, - colorScheme, - sliceId, - metric, - } = formData; - - const { - currencyFormats = {}, - columnFormats = {}, - currencyCodeColumn, - } = datasource; - const { data, detected_currency: detectedCurrency } = queriesData[0]; - - const formatter = getValueFormatter( - metric, - currencyFormats, - columnFormats, - numberFormat, - currencyFormat, - undefined, // key - not needed for single-metric charts - data, - currencyCodeColumn, - detectedCurrency, - ); - - return { - width, - height, - data: queriesData[0].data, - country: selectCountry ? String(selectCountry).toLowerCase() : null, - linearColorScheme, - numberFormat, // left for backward compatibility - colorScheme, - sliceId, - formatter, - }; -} diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/tsconfig.json b/superset-frontend/plugins/legacy-plugin-chart-country-map/tsconfig.json index 03190ec5bd4..f0bfbba676d 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-country-map/tsconfig.json +++ b/superset-frontend/plugins/legacy-plugin-chart-country-map/tsconfig.json @@ -20,6 +20,7 @@ "references": [ { "path": "../../packages/superset-core" }, { "path": "../../packages/superset-ui-core" }, - { "path": "../../packages/superset-ui-chart-controls" } + { "path": "../../packages/superset-ui-chart-controls" }, + { "path": "../../packages/superset-ui-glyph-core" } ] } diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/types/external.d.ts b/superset-frontend/plugins/legacy-plugin-chart-country-map/types/external.d.ts index 66677a600a6..ecd4536d29e 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-country-map/types/external.d.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-country-map/types/external.d.ts @@ -16,13 +16,11 @@ * specific language governing permissions and limitations * under the License. */ - declare module '*.png' { - const value: string; + const value: any; export default value; } - declare module '*.jpg' { - const value: string; + const value: any; export default value; } diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/package.json b/superset-frontend/plugins/legacy-plugin-chart-horizon/package.json index bf443299c33..1d860c795ad 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-horizon/package.json +++ b/superset-frontend/plugins/legacy-plugin-chart-horizon/package.json @@ -30,6 +30,7 @@ "peerDependencies": { "@superset-ui/chart-controls": "*", "@superset-ui/core": "*", + "@superset-ui/glyph-core": "*", "@apache-superset/core": "*", "react": "^18.2.0" }, diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/src/controlPanel.ts b/superset-frontend/plugins/legacy-plugin-chart-horizon/src/controlPanel.ts deleted file mode 100644 index e778b01129a..00000000000 --- a/superset-frontend/plugins/legacy-plugin-chart-horizon/src/controlPanel.ts +++ /dev/null @@ -1,105 +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 { t } from '@apache-superset/core/translation'; -import { - ControlPanelConfig, - formatSelectOptions, -} from '@superset-ui/chart-controls'; - -const config: ControlPanelConfig = { - controlPanelSections: [ - { - label: t('Time'), - expanded: true, - description: t('Time related form attributes'), - controlSetRows: [['granularity_sqla'], ['time_range']], - }, - { - label: t('Query'), - expanded: true, - controlSetRows: [ - ['metrics'], - ['adhoc_filters'], - ['groupby'], - ['limit', 'timeseries_limit_metric'], - ['order_desc'], - [ - { - name: 'contribution', - config: { - type: 'CheckboxControl', - label: t('Contribution'), - default: false, - description: t('Compute the contribution to the total'), - }, - }, - ], - ['row_limit', null], - ], - }, - { - label: t('Chart Options'), - expanded: true, - controlSetRows: [ - [ - { - name: 'series_height', - config: { - type: 'SelectControl', - renderTrigger: true, - freeForm: true, - label: t('Series Height'), - default: '25', - choices: formatSelectOptions([ - '10', - '25', - '40', - '50', - '75', - '100', - '150', - '200', - ]), - description: t('Pixel height of each series'), - }, - }, - { - name: 'horizon_color_scale', - config: { - type: 'SelectControl', - renderTrigger: true, - label: t('Value Domain'), - choices: [ - ['series', t('series')], - ['overall', t('overall')], - ['change', t('change')], - ], - default: 'series', - description: t( - 'series: Treat each series independently; overall: All series use the same scale; change: Show changes compared to the first data point in each series', - ), - }, - }, - ], - ], - }, - ], -}; - -export default config; diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/src/index.ts b/superset-frontend/plugins/legacy-plugin-chart-horizon/src/index.ts deleted file mode 100644 index ea5a357c4b4..00000000000 --- a/superset-frontend/plugins/legacy-plugin-chart-horizon/src/index.ts +++ /dev/null @@ -1,51 +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 { t } from '@apache-superset/core/translation'; -import { ChartMetadata, ChartPlugin } from '@superset-ui/core'; -import transformProps from './transformProps'; -import example from './images/Horizon_Chart.jpg'; -import exampleDark from './images/Horizon_Chart-dark.jpg'; -import thumbnail from './images/thumbnail.png'; -import thumbnailDark from './images/thumbnail-dark.png'; -import controlPanel from './controlPanel'; - -const metadata = new ChartMetadata({ - category: t('Distribution'), - credits: ['http://kmandov.github.io/d3-horizon-chart/'], - description: t( - 'Compares how a metric changes over time between different groups. Each group is mapped to a row and change over time is visualized bar lengths and color.', - ), - exampleGallery: [{ url: example, urlDark: exampleDark }], - name: t('Horizon Chart'), - tags: [t('Legacy')], - thumbnail, - thumbnailDark, - useLegacyApi: true, -}); - -export default class HorizonChartPlugin extends ChartPlugin { - constructor() { - super({ - loadChart: () => import('./HorizonChart'), - metadata, - transformProps, - controlPanel, - }); - } -} diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/src/index.tsx b/superset-frontend/plugins/legacy-plugin-chart-horizon/src/index.tsx new file mode 100644 index 00000000000..a1594fe9994 --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-horizon/src/index.tsx @@ -0,0 +1,142 @@ +/** + * 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 { t } from '@apache-superset/core/translation'; +import { formatSelectOptions } from '@superset-ui/chart-controls'; +import { defineChart } from '@superset-ui/glyph-core'; +import example from './images/Horizon_Chart.jpg'; +import exampleDark from './images/Horizon_Chart-dark.jpg'; +import thumbnail from './images/thumbnail.png'; +import thumbnailDark from './images/thumbnail-dark.png'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const HorizonChart = require('./HorizonChart').default; + +type HorizonExtra = { + colorScale: string; + seriesHeight: number; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export default defineChart({ + metadata: { + name: t('Horizon Chart'), + description: t( + 'Compares how a metric changes over time between different groups. Each group is mapped to a row and change over time is visualized bar lengths and color.', + ), + category: t('Distribution'), + credits: ['http://kmandov.github.io/d3-horizon-chart/'], + tags: [t('Legacy')], + thumbnail, + thumbnailDark, + exampleGallery: [{ url: example, urlDark: exampleDark }], + useLegacyApi: true, + }, + arguments: {}, + prependSections: [ + { + label: t('Time'), + expanded: true, + description: t('Time related form attributes'), + controlSetRows: [['granularity_sqla'], ['time_range']], + }, + ], + additionalControls: { + queryBefore: [['metrics']], + query: [ + ['groupby'], + ['limit', 'timeseries_limit_metric'], + ['order_desc'], + [ + { + name: 'contribution', + config: { + type: 'CheckboxControl', + label: t('Contribution'), + default: false, + description: t('Compute the contribution to the total'), + }, + }, + ], + ['row_limit', null], + ], + chartOptions: [ + [ + { + name: 'series_height', + config: { + type: 'SelectControl', + renderTrigger: true, + freeForm: true, + label: t('Series Height'), + default: '25', + choices: formatSelectOptions([ + '10', + '25', + '40', + '50', + '75', + '100', + '150', + '200', + ]), + description: t('Pixel height of each series'), + }, + }, + { + name: 'horizon_color_scale', + config: { + type: 'SelectControl', + renderTrigger: true, + label: t('Value Domain'), + choices: [ + ['series', t('series')], + ['overall', t('overall')], + ['change', t('change')], + ], + default: 'series', + description: t( + 'series: Treat each series independently; overall: All series use the same scale; change: Show changes compared to the first data point in each series', + ), + }, + }, + ], + ], + }, + transform: chartProps => { + const { formData } = chartProps; + const { horizonColorScale, seriesHeight } = formData as Record< + string, + string + >; + return { + colorScale: horizonColorScale ?? 'series', + seriesHeight: parseInt(seriesHeight ?? '25', 10), + }; + }, + render: ({ width, height, data, colorScale, seriesHeight }) => ( + + ), +}); diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/src/transformProps.ts b/superset-frontend/plugins/legacy-plugin-chart-horizon/src/transformProps.ts deleted file mode 100644 index cab69e8c568..00000000000 --- a/superset-frontend/plugins/legacy-plugin-chart-horizon/src/transformProps.ts +++ /dev/null @@ -1,38 +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 { ChartProps } from '@superset-ui/core'; - -export default function transformProps(chartProps: ChartProps) { - const { height, width, formData, queriesData } = chartProps; - const { - horizon_color_scale: horizonColorScale, - series_height: seriesHeight, - } = formData; - - // Only include colorScale if defined, otherwise let defaultProps apply - return { - ...(horizonColorScale !== undefined && { - colorScale: horizonColorScale as string, - }), - data: queriesData[0].data, - height, - seriesHeight: parseInt(String(seriesHeight ?? 20), 10), - width, - }; -} diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/tsconfig.json b/superset-frontend/plugins/legacy-plugin-chart-horizon/tsconfig.json index 03190ec5bd4..f0bfbba676d 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-horizon/tsconfig.json +++ b/superset-frontend/plugins/legacy-plugin-chart-horizon/tsconfig.json @@ -20,6 +20,7 @@ "references": [ { "path": "../../packages/superset-core" }, { "path": "../../packages/superset-ui-core" }, - { "path": "../../packages/superset-ui-chart-controls" } + { "path": "../../packages/superset-ui-chart-controls" }, + { "path": "../../packages/superset-ui-glyph-core" } ] } diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/types/external.d.ts b/superset-frontend/plugins/legacy-plugin-chart-horizon/types/external.d.ts index 66677a600a6..30cdb90064f 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-horizon/types/external.d.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-horizon/types/external.d.ts @@ -16,13 +16,11 @@ * specific language governing permissions and limitations * under the License. */ - -declare module '*.png' { - const value: string; +declare module "*.png" { + const value: any; export default value; } - -declare module '*.jpg' { - const value: string; +declare module "*.jpg" { + const value: any; export default value; } diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/package.json b/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/package.json index 892129132ed..e5e656e3042 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/package.json +++ b/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/package.json @@ -30,6 +30,7 @@ "peerDependencies": { "@superset-ui/chart-controls": "*", "@superset-ui/core": "*", + "@superset-ui/glyph-core": "*", "@apache-superset/core": "*", "react": "^18.2.0" }, diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/controlPanel.ts b/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/controlPanel.ts deleted file mode 100644 index 973cc0127a4..00000000000 --- a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/controlPanel.ts +++ /dev/null @@ -1,103 +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 { t } from '@apache-superset/core/translation'; -import { validateNonEmpty } from '@superset-ui/core'; -import { ControlPanelConfig } from '@superset-ui/chart-controls'; - -const config: ControlPanelConfig = { - controlPanelSections: [ - { - label: t('Query'), - expanded: true, - controlSetRows: [ - ['metrics'], - ['adhoc_filters'], - [ - { - name: 'groupby', - override: { - validators: [validateNonEmpty], - }, - }, - ], - ['limit', 'timeseries_limit_metric'], - ['order_desc'], - [ - { - name: 'contribution', - config: { - type: 'CheckboxControl', - label: t('Contribution'), - default: false, - description: t('Compute the contribution to the total'), - }, - }, - ], - ['row_limit', null], - ], - }, - { - label: t('Parameters'), - expanded: false, - controlSetRows: [ - [ - { - name: 'significance_level', - config: { - type: 'TextControl', - label: t('Significance Level'), - default: 0.05, - description: t( - 'Threshold alpha level for determining significance', - ), - }, - }, - ], - [ - { - name: 'pvalue_precision', - config: { - type: 'TextControl', - label: t('p-value precision'), - default: 6, - description: t( - 'Number of decimal places with which to display p-values', - ), - }, - }, - ], - [ - { - name: 'liftvalue_precision', - config: { - type: 'TextControl', - label: t('Lift percent precision'), - default: 4, - description: t( - 'Number of decimal places with which to display lift values', - ), - }, - }, - ], - ], - }, - ], -}; - -export default config; diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/index.ts b/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/index.ts deleted file mode 100644 index a2e28a06114..00000000000 --- a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/index.ts +++ /dev/null @@ -1,50 +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 { t } from '@apache-superset/core/translation'; -import { ChartMetadata, ChartPlugin } from '@superset-ui/core'; -import transformProps from './transformProps'; -import example from './images/example.jpg'; -import exampleDark from './images/example-dark.jpg'; -import thumbnail from './images/thumbnail.png'; -import thumbnailDark from './images/thumbnail-dark.png'; -import controlPanel from './controlPanel'; - -const metadata = new ChartMetadata({ - category: t('Correlation'), - description: t( - 'Table that visualizes paired t-tests, which are used to understand statistical differences between groups.', - ), - exampleGallery: [{ url: example, urlDark: exampleDark }], - name: t('Paired t-test Table'), - tags: [t('Legacy'), t('Statistical'), t('Tabular')], - thumbnail, - thumbnailDark, - useLegacyApi: true, -}); - -export default class PairedTTestChartPlugin extends ChartPlugin { - constructor() { - super({ - loadChart: () => import('./PairedTTest'), - metadata, - transformProps, - controlPanel, - }); - } -} diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/index.tsx b/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/index.tsx new file mode 100644 index 00000000000..83a9b68de77 --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/index.tsx @@ -0,0 +1,158 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { t } from '@apache-superset/core/translation'; +import { validateNonEmpty } from '@superset-ui/core'; +import { defineChart } from '@superset-ui/glyph-core'; +import example from './images/example.jpg'; +import exampleDark from './images/example-dark.jpg'; +import thumbnail from './images/thumbnail.png'; +import thumbnailDark from './images/thumbnail-dark.png'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const PairedTTest = require('./PairedTTest').default; + +type PairedTTestExtra = { + alpha: number; + groups: string[]; + liftValPrec: number; + metrics: string[]; + pValPrec: number; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export default defineChart({ + metadata: { + name: t('Paired t-test Table'), + description: t( + 'Table that visualizes paired t-tests, which are used to understand statistical differences between groups.', + ), + category: t('Correlation'), + tags: [t('Legacy'), t('Statistical'), t('Tabular')], + thumbnail, + thumbnailDark, + exampleGallery: [{ url: example, urlDark: exampleDark }], + useLegacyApi: true, + }, + arguments: {}, + additionalControls: { + queryBefore: [['metrics']], + query: [ + [ + { + name: 'groupby', + override: { + validators: [validateNonEmpty], + }, + }, + ], + ['limit', 'timeseries_limit_metric'], + ['order_desc'], + [ + { + name: 'contribution', + config: { + type: 'CheckboxControl', + label: t('Contribution'), + default: false, + description: t('Compute the contribution to the total'), + }, + }, + ], + ['row_limit', null], + ], + }, + additionalSections: [ + { + label: t('Parameters'), + expanded: false, + controlSetRows: [ + [ + { + name: 'significance_level', + config: { + type: 'TextControl', + label: t('Significance Level'), + default: 0.05, + description: t( + 'Threshold alpha level for determining significance', + ), + }, + }, + ], + [ + { + name: 'pvalue_precision', + config: { + type: 'TextControl', + label: t('p-value precision'), + default: 6, + description: t( + 'Number of decimal places with which to display p-values', + ), + }, + }, + ], + [ + { + name: 'liftvalue_precision', + config: { + type: 'TextControl', + label: t('Lift percent precision'), + default: 4, + description: t( + 'Number of decimal places with which to display lift values', + ), + }, + }, + ], + ], + }, + ], + transform: chartProps => { + const { formData } = chartProps; + const { + groupby, + liftvaluePrecision, + metrics, + pvaluePrecision, + significanceLevel, + } = formData as Record; + + return { + alpha: (significanceLevel as number) ?? 0.05, + groups: (groupby as string[]) ?? [], + liftValPrec: parseInt(String(liftvaluePrecision ?? '4'), 10), + metrics: ((metrics as Array) ?? []).map( + metric => (typeof metric === 'string' ? metric : metric.label), + ), + pValPrec: parseInt(String(pvaluePrecision ?? '6'), 10), + }; + }, + render: ({ alpha, groups, liftValPrec, metrics, pValPrec, data }) => ( + + ), +}); diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/transformProps.ts b/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/transformProps.ts deleted file mode 100644 index 4decf04962c..00000000000 --- a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/transformProps.ts +++ /dev/null @@ -1,42 +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 { ChartProps } from '@superset-ui/core'; - -export default function transformProps(chartProps: ChartProps) { - const { formData, queriesData } = chartProps; - const { - groupby, - liftvaluePrecision, - metrics, - pvaluePrecision, - significanceLevel, - } = formData; - - return { - alpha: significanceLevel, - data: queriesData[0].data, - groups: groupby, - liftValPrec: parseInt(liftvaluePrecision, 10), - metrics: (metrics as (string | { label: string })[]).map( - (metric: string | { label: string }) => - typeof metric === 'string' ? metric : metric.label, - ), - pValPrec: parseInt(pvaluePrecision, 10), - }; -} diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/tsconfig.json b/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/tsconfig.json index 03190ec5bd4..f0bfbba676d 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/tsconfig.json +++ b/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/tsconfig.json @@ -20,6 +20,7 @@ "references": [ { "path": "../../packages/superset-core" }, { "path": "../../packages/superset-ui-core" }, - { "path": "../../packages/superset-ui-chart-controls" } + { "path": "../../packages/superset-ui-chart-controls" }, + { "path": "../../packages/superset-ui-glyph-core" } ] } diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/types/external.d.ts b/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/types/external.d.ts index 40dbbf9ccbd..f93047f0d03 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/types/external.d.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/types/external.d.ts @@ -17,67 +17,12 @@ * under the License. */ declare module '*.png' { - const value: string; + const value: any; export default value; } - declare module '*.jpg' { - const value: string; + const value: any; export default value; } - -declare module 'distributions' { - class Studentt { - constructor(degreesOfFreedom: number); - cdf(x: number): number; - } - const dist: { - Studentt: typeof Studentt; - }; - export default dist; -} - -declare module 'reactable' { - import { ComponentType, ReactNode } from 'react'; - - interface TableProps { - className?: string; - id?: string; - sortable?: ( - | string - | { - column: string; - sortFunction: (a: string, b: string) => number; - } - )[]; - children?: ReactNode; - } - - interface TrProps { - className?: string; - onClick?: () => void; - children?: ReactNode; - } - - interface TdProps { - className?: string; - column?: string; - data?: string | number | boolean; - children?: ReactNode; - } - - interface ThProps { - column?: string; - children?: ReactNode; - } - - interface TheadProps { - children?: ReactNode; - } - - export const Table: ComponentType; - export const Tr: ComponentType; - export const Td: ComponentType; - export const Th: ComponentType; - export const Thead: ComponentType; -} +declare module 'distributions'; +declare module 'reactable'; diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/package.json b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/package.json index 1b22d2e8151..7faf037c525 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/package.json +++ b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/package.json @@ -35,6 +35,7 @@ "peerDependencies": { "@superset-ui/chart-controls": "*", "@superset-ui/core": "*", + "@superset-ui/glyph-core": "*", "@apache-superset/core": "*", "react": "^18.2.0" } diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/controlPanel.ts b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/controlPanel.ts deleted file mode 100644 index 726d06a20e9..00000000000 --- a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/controlPanel.ts +++ /dev/null @@ -1,69 +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 { t } from '@apache-superset/core/translation'; -import { ControlPanelConfig } from '@superset-ui/chart-controls'; - -const config: ControlPanelConfig = { - controlPanelSections: [ - { - label: t('Query'), - expanded: true, - controlSetRows: [ - ['series'], - ['metrics'], - ['secondary_metric'], - ['adhoc_filters'], - ['limit', 'row_limit'], - ['timeseries_limit_metric'], - ['order_desc'], - ], - }, - { - label: t('Options'), - expanded: true, - controlSetRows: [ - [ - { - name: 'show_datatable', - config: { - type: 'CheckboxControl', - label: t('Data Table'), - default: false, - renderTrigger: true, - description: t('Whether to display the interactive data table'), - }, - }, - { - name: 'include_series', - config: { - type: 'CheckboxControl', - label: t('Include Series'), - renderTrigger: true, - default: false, - description: t('Include series name as an axis'), - }, - }, - ], - ['linear_color_scheme'], - ], - }, - ], -}; - -export default config; diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/index.ts b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/index.ts deleted file mode 100644 index f5cb8a5c381..00000000000 --- a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/index.ts +++ /dev/null @@ -1,56 +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 { t } from '@apache-superset/core/translation'; -import { ChartMetadata, ChartPlugin } from '@superset-ui/core'; -import transformProps from './transformProps'; -import thumbnail from './images/thumbnail.png'; -import thumbnailDark from './images/thumbnail-dark.png'; -import example1 from './images/example1.jpg'; -import example1Dark from './images/example1-dark.jpg'; -import example2 from './images/example2.jpg'; -import example2Dark from './images/example2-dark.jpg'; -import controlPanel from './controlPanel'; - -const metadata = new ChartMetadata({ - category: t('Ranking'), - credits: ['https://syntagmatic.github.io/parallel-coordinates'], - description: t( - 'Plots the individual metrics for each row in the data vertically and links them together as a line. This chart is useful for comparing multiple metrics across all of the samples or rows in the data.', - ), - exampleGallery: [ - { url: example1, urlDark: example1Dark }, - { url: example2, urlDark: example2Dark }, - ], - name: t('Parallel Coordinates'), - tags: [t('Directional'), t('Legacy'), t('Relational')], - thumbnail, - thumbnailDark, - useLegacyApi: true, -}); - -export default class ParallelCoordinatesChartPlugin extends ChartPlugin { - constructor() { - super({ - loadChart: () => import('./ReactParallelCoordinates'), - metadata, - transformProps, - controlPanel, - }); - } -} diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/index.tsx b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/index.tsx new file mode 100644 index 00000000000..1e797c2704b --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/index.tsx @@ -0,0 +1,147 @@ +/** + * 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 { t } from '@apache-superset/core/translation'; +import { defineChart } from '@superset-ui/glyph-core'; +import thumbnail from './images/thumbnail.png'; +import thumbnailDark from './images/thumbnail-dark.png'; +import example1 from './images/example1.jpg'; +import example1Dark from './images/example1-dark.jpg'; +import example2 from './images/example2.jpg'; +import example2Dark from './images/example2-dark.jpg'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const ReactParallelCoordinates = require('./ReactParallelCoordinates').default; + +type ParallelCoordinatesExtra = { + includeSeries: boolean; + linearColorScheme: string; + metrics: string[]; + colorMetric: string | undefined; + series: string; + showDatatable: boolean; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export default defineChart({ + metadata: { + name: t('Parallel Coordinates'), + description: t( + 'Plots the individual metrics for each row in the data vertically and links them together as a line. This chart is useful for comparing multiple metrics across all of the samples or rows in the data.', + ), + category: t('Ranking'), + credits: ['https://syntagmatic.github.io/parallel-coordinates'], + tags: [t('Directional'), t('Legacy'), t('Relational')], + thumbnail, + thumbnailDark, + exampleGallery: [ + { url: example1, urlDark: example1Dark }, + { url: example2, urlDark: example2Dark }, + ], + useLegacyApi: true, + }, + arguments: {}, + additionalControls: { + queryBefore: [['series'], ['metrics'], ['secondary_metric']], + query: [ + ['limit', 'row_limit'], + ['timeseries_limit_metric'], + ['order_desc'], + ], + }, + middleSections: [ + { + label: t('Options'), + expanded: true, + controlSetRows: [ + [ + { + name: 'show_datatable', + config: { + type: 'CheckboxControl', + label: t('Data Table'), + default: false, + renderTrigger: true, + description: t('Whether to display the interactive data table'), + }, + }, + { + name: 'include_series', + config: { + type: 'CheckboxControl', + label: t('Include Series'), + renderTrigger: true, + default: false, + description: t('Include series name as an axis'), + }, + }, + ], + ['linear_color_scheme'], + ], + }, + ], + transform: chartProps => { + const { formData } = chartProps; + const { + includeSeries, + linearColorScheme, + metrics, + secondaryMetric, + series, + showDatatable, + } = formData as Record; + + return { + includeSeries: (includeSeries as boolean) ?? false, + linearColorScheme: (linearColorScheme as string) ?? '', + metrics: ((metrics as Array) ?? []).map( + m => (m as { label?: string }).label || (m as string), + ), + colorMetric: + secondaryMetric && (secondaryMetric as { label?: string }).label + ? (secondaryMetric as { label: string }).label + : (secondaryMetric as string | undefined), + series: (series as string) ?? '', + showDatatable: (showDatatable as boolean) ?? false, + }; + }, + render: ({ + width, + height, + data, + includeSeries, + linearColorScheme, + metrics, + colorMetric, + series, + showDatatable, + }) => ( + + ), +}); diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/transformProps.ts b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/transformProps.ts deleted file mode 100644 index 94a065717ed..00000000000 --- a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/transformProps.ts +++ /dev/null @@ -1,48 +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 { ChartProps } from '@superset-ui/core'; -import { isThemeDark } from '@apache-superset/core/theme'; - -export default function transformProps(chartProps: ChartProps) { - const { width, height, formData, queriesData, theme } = chartProps; - const { - includeSeries, - linearColorScheme, - metrics, - secondaryMetric, - series, - showDatatable, - } = formData; - - return { - width, - height, - data: queriesData[0].data, - defaultLineColor: theme.colorTextTertiary, - includeSeries, - isDarkMode: isThemeDark(theme), - linearColorScheme, - metrics: metrics.map((m: { label?: string } | string) => - typeof m === 'string' ? m : m.label || m, - ), - colorMetric: secondaryMetric?.label || secondaryMetric, - series, - showDatatable, - }; -} diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/tsconfig.json b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/tsconfig.json index 03190ec5bd4..f0bfbba676d 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/tsconfig.json +++ b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/tsconfig.json @@ -20,6 +20,7 @@ "references": [ { "path": "../../packages/superset-core" }, { "path": "../../packages/superset-ui-core" }, - { "path": "../../packages/superset-ui-chart-controls" } + { "path": "../../packages/superset-ui-chart-controls" }, + { "path": "../../packages/superset-ui-glyph-core" } ] } diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/types/external.d.ts b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/types/external.d.ts index 3143530e5dc..82bd3e5ba10 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/types/external.d.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/types/external.d.ts @@ -16,18 +16,12 @@ * specific language governing permissions and limitations * under the License. */ - declare module '*.png' { - const value: string; + const value: any; export default value; } - declare module '*.jpg' { - const value: string; + const value: any; export default value; } - -declare module 'd3v3' { - const d3: Record; - export = d3; -} +declare module 'd3v3'; diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/package.json b/superset-frontend/plugins/legacy-plugin-chart-partition/package.json index 7e2a8046f3d..4f2318bee13 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-partition/package.json +++ b/superset-frontend/plugins/legacy-plugin-chart-partition/package.json @@ -31,6 +31,7 @@ "@superset-ui/chart-controls": "*", "@superset-ui/core": "*", "@apache-superset/core": "*", + "@superset-ui/glyph-core": "*", "@testing-library/jest-dom": "*", "@testing-library/react": "^14.0.0", "react": "^18.2.0", diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/src/controlPanel.tsx b/superset-frontend/plugins/legacy-plugin-chart-partition/src/controlPanel.tsx deleted file mode 100644 index ece7cbe0259..00000000000 --- a/superset-frontend/plugins/legacy-plugin-chart-partition/src/controlPanel.tsx +++ /dev/null @@ -1,401 +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 { t } from '@apache-superset/core/translation'; -import { validateNonEmpty } from '@superset-ui/core'; -import { - ColumnMeta, - ControlPanelConfig, - ControlSubSectionHeader, - D3_FORMAT_DOCS, - D3_FORMAT_OPTIONS, - D3_TIME_FORMAT_OPTIONS, - getStandardizedControls, -} from '@superset-ui/chart-controls'; -import OptionDescription from './OptionDescription'; - -const config: ControlPanelConfig = { - controlPanelSections: [ - { - label: t('Query'), - expanded: true, - controlSetRows: [ - ['metrics'], - ['adhoc_filters'], - ['groupby'], - ['limit'], - ['timeseries_limit_metric'], - ['order_desc'], - [ - { - name: 'contribution', - config: { - type: 'CheckboxControl', - label: t('Contribution'), - default: false, - description: t('Compute the contribution to the total'), - }, - }, - ], - ['row_limit'], - ], - }, - { - label: t('Time Series Options'), - expanded: true, - controlSetRows: [ - [ - { - name: 'time_series_option', - config: { - type: 'SelectControl', - label: t('Options'), - validators: [validateNonEmpty], - default: 'not_time', - valueKey: 'value', - options: [ - { - label: t('Not Time Series'), - value: 'not_time', - description: t('Ignore time'), - }, - { - label: t('Time Series'), - value: 'time_series', - description: t('Standard time series'), - }, - { - label: t('Aggregate Mean'), - value: 'agg_mean', - description: t('Mean of values over specified period'), - }, - { - label: t('Aggregate Sum'), - value: 'agg_sum', - description: t('Sum of values over specified period'), - }, - { - label: t('Difference'), - value: 'point_diff', - description: t( - 'Metric change in value from `since` to `until`', - ), - }, - { - label: t('Percent Change'), - value: 'point_percent', - description: t( - 'Metric percent change in value from `since` to `until`', - ), - }, - { - label: t('Factor'), - value: 'point_factor', - description: t( - 'Metric factor change from `since` to `until`', - ), - }, - { - label: t('Advanced Analytics'), - value: 'adv_anal', - description: t('Use the Advanced Analytics options below'), - }, - ], - optionRenderer: (op: ColumnMeta) => ( - - ), - valueRenderer: (op: ColumnMeta) => ( - - ), - description: t('Settings for time series'), - }, - }, - ], - ], - }, - { - label: t('Chart Options'), - expanded: true, - tabOverride: 'customize', - controlSetRows: [ - ['color_scheme'], - [ - { - name: 'number_format', - config: { - type: 'SelectControl', - freeForm: true, - label: t('Number format'), - renderTrigger: true, - default: 'SMART_NUMBER', - choices: D3_FORMAT_OPTIONS, - description: D3_FORMAT_DOCS, - }, - }, - { - name: 'date_time_format', - config: { - type: 'SelectControl', - freeForm: true, - label: t('Date Time Format'), - renderTrigger: true, - default: 'smart_date', - choices: D3_TIME_FORMAT_OPTIONS, - description: D3_FORMAT_DOCS, - }, - }, - ], - [ - { - name: 'partition_limit', - config: { - type: 'TextControl', - label: t('Partition Limit'), - isInt: true, - default: '5', - description: t( - 'The maximum number of subdivisions of each group; ' + - 'lower values are pruned first', - ), - }, - }, - { - name: 'partition_threshold', - config: { - type: 'TextControl', - label: t('Partition Threshold'), - isFloat: true, - default: '0.05', - description: t( - 'Partitions whose height to parent height proportions are ' + - 'below this value are pruned', - ), - }, - }, - ], - [ - { - name: 'log_scale', - config: { - type: 'CheckboxControl', - label: t('Log Scale'), - default: false, - renderTrigger: true, - description: t('Use a log scale'), - }, - }, - ], - [ - { - name: 'equal_date_size', - config: { - type: 'CheckboxControl', - label: t('Equal Date Sizes'), - default: true, - renderTrigger: true, - description: t( - 'Check to force date partitions to have the same height', - ), - }, - }, - ], - [ - { - name: 'rich_tooltip', - config: { - type: 'CheckboxControl', - label: t('Rich Tooltip'), - renderTrigger: true, - default: true, - description: t( - 'The rich tooltip shows a list of all series for that point in time', - ), - }, - }, - ], - ], - }, - { - label: t('Advanced Analytics'), - tabOverride: 'data', - description: t( - 'This section contains options ' + - 'that allow for advanced analytical post processing ' + - 'of query results', - ), - controlSetRows: [ - // eslint-disable-next-line react/jsx-key - [ - - {t('Rolling Window')} - , - ], - [ - { - name: 'rolling_type', - config: { - type: 'SelectControl', - label: t('Rolling Function'), - default: 'None', - choices: [ - ['None', t('None')], - ['mean', t('mean')], - ['sum', t('sum')], - ['std', t('std')], - ['cumsum', t('cumsum')], - ], - description: t( - 'Defines a rolling window function to apply, works along ' + - 'with the [Periods] text box', - ), - }, - }, - ], - [ - { - name: 'rolling_periods', - config: { - type: 'TextControl', - label: t('Periods'), - isInt: true, - description: t( - 'Defines the size of the rolling window function, ' + - 'relative to the time granularity selected', - ), - }, - }, - { - name: 'min_periods', - config: { - type: 'TextControl', - label: t('Min Periods'), - isInt: true, - description: t( - 'The minimum number of rolling periods required to show ' + - 'a value. For instance if you do a cumulative sum on 7 days ' + - 'you may want your "Min Period" to be 7, so that all data points ' + - 'shown are the total of 7 periods. This will hide the "ramp up" ' + - 'taking place over the first 7 periods', - ), - }, - }, - ], - // eslint-disable-next-line react/jsx-key - [ - - {t('Time Comparison')} - , - ], - [ - { - name: 'time_compare', - config: { - type: 'SelectControl', - multi: true, - freeForm: true, - label: t('Time Shift'), - choices: [ - ['1 day', t('1 day')], - ['1 week', t('1 week')], - ['28 days', t('28 days')], - ['30 days', t('30 days')], - ['52 weeks', t('52 weeks')], - ['1 year', t('1 year')], - ['104 weeks', t('104 weeks')], - ['2 years', t('2 years')], - ['156 weeks', t('156 weeks')], - ['3 years', t('3 years')], - ], - description: t( - 'Overlay one or more timeseries from a ' + - 'relative time period. Expects relative time deltas ' + - 'in natural language (example: 24 hours, 7 days, ' + - '52 weeks, 365 days). Free text is supported.', - ), - }, - }, - { - name: 'comparison_type', - config: { - type: 'SelectControl', - label: t('Calculation type'), - default: 'values', - choices: [ - ['values', t('Actual Values')], - ['absolute', t('Difference')], - ['percentage', t('Percentage change')], - ['ratio', t('Ratio')], - ], - description: t( - 'How to display time shifts: as individual lines; as the ' + - 'difference between the main time series and each time shift; ' + - 'as the percentage change; or as the ratio between series and time shifts.', - ), - }, - }, - ], - [{t('Resample')}], - [ - { - name: 'resample_rule', - config: { - type: 'SelectControl', - freeForm: true, - label: t('Rule'), - default: null, - choices: [ - ['1T', t('1T')], - ['1H', t('1H')], - ['1D', t('1D')], - ['7D', t('7D')], - ['1M', t('1M')], - ['1AS', t('1AS')], - ], - description: t('Pandas resample rule'), - }, - }, - { - name: 'resample_method', - config: { - type: 'SelectControl', - freeForm: true, - label: t('Method'), - default: null, - choices: [ - ['asfreq', t('asfreq')], - ['bfill', t('bfill')], - ['ffill', t('ffill')], - ['median', t('median')], - ['mean', t('mean')], - ['sum', t('sum')], - ], - description: t('Pandas resample method'), - }, - }, - ], - ], - }, - ], - formDataOverrides: formData => ({ - ...formData, - groupby: getStandardizedControls().popAllColumns(), - metrics: getStandardizedControls().popAllMetrics(), - }), -}; - -export default config; diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/src/index.ts b/superset-frontend/plugins/legacy-plugin-chart-partition/src/index.ts deleted file mode 100644 index 0ae654105c5..00000000000 --- a/superset-frontend/plugins/legacy-plugin-chart-partition/src/index.ts +++ /dev/null @@ -1,48 +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 { t } from '@apache-superset/core/translation'; -import { ChartMetadata, ChartPlugin } from '@superset-ui/core'; -import transformProps from './transformProps'; -import thumbnail from './images/thumbnail.png'; -import thumbnailDark from './images/thumbnail-dark.png'; -import example from './images/example.jpg'; -import exampleDark from './images/example-dark.jpg'; -import controlPanel from './controlPanel'; - -const metadata = new ChartMetadata({ - category: t('Part of a Whole'), - description: t('Compare the same summarized metric across multiple groups.'), - exampleGallery: [{ url: example, urlDark: exampleDark }], - name: t('Partition Chart'), - tags: [t('Categorical'), t('Comparison'), t('Legacy'), t('Proportional')], - thumbnail, - thumbnailDark, - useLegacyApi: true, -}); - -export default class PartitionChartPlugin extends ChartPlugin { - constructor() { - super({ - loadChart: () => import('./ReactPartition'), - metadata, - transformProps, - controlPanel, - }); - } -} diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/src/transformProps.ts b/superset-frontend/plugins/legacy-plugin-chart-partition/src/transformProps.ts deleted file mode 100644 index 1422701dab2..00000000000 --- a/superset-frontend/plugins/legacy-plugin-chart-partition/src/transformProps.ts +++ /dev/null @@ -1,56 +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 { ChartProps } from '@superset-ui/core'; - -export default function transformProps(chartProps: ChartProps) { - const { width, height, datasource, formData, queriesData } = chartProps; - const { - colorScheme, - dateTimeFormat, - equalDateSize, - groupby, - logScale, - metrics, - numberFormat, - partitionLimit, - partitionThreshold, - richTooltip, - timeSeriesOption, - sliceId, - } = formData; - const { verboseMap = {} } = datasource; - - return { - width, - height, - data: queriesData[0].data, - colorScheme, - dateTimeFormat, - equalDateSize, - levels: groupby.map((g: string) => verboseMap[g] || g), - metrics, - numberFormat, - partitionLimit: partitionLimit && parseInt(partitionLimit, 10), - partitionThreshold: partitionThreshold && parseInt(partitionThreshold, 10), - timeSeriesOption, - useLogScale: logScale, - useRichTooltip: richTooltip, - sliceId, - }; -} diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/tsconfig.json b/superset-frontend/plugins/legacy-plugin-chart-partition/tsconfig.json index 03190ec5bd4..f0bfbba676d 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-partition/tsconfig.json +++ b/superset-frontend/plugins/legacy-plugin-chart-partition/tsconfig.json @@ -20,6 +20,7 @@ "references": [ { "path": "../../packages/superset-core" }, { "path": "../../packages/superset-ui-core" }, - { "path": "../../packages/superset-ui-chart-controls" } + { "path": "../../packages/superset-ui-chart-controls" }, + { "path": "../../packages/superset-ui-glyph-core" } ] } diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/types/external.d.ts b/superset-frontend/plugins/legacy-plugin-chart-partition/types/external.d.ts index 66677a600a6..30cdb90064f 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-partition/types/external.d.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-partition/types/external.d.ts @@ -16,13 +16,11 @@ * specific language governing permissions and limitations * under the License. */ - -declare module '*.png' { - const value: string; +declare module "*.png" { + const value: any; export default value; } - -declare module '*.jpg' { - const value: string; +declare module "*.jpg" { + const value: any; export default value; } diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/tsconfig.json b/superset-frontend/plugins/legacy-plugin-chart-rose/tsconfig.json index 03190ec5bd4..f0bfbba676d 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-rose/tsconfig.json +++ b/superset-frontend/plugins/legacy-plugin-chart-rose/tsconfig.json @@ -20,6 +20,7 @@ "references": [ { "path": "../../packages/superset-core" }, { "path": "../../packages/superset-ui-core" }, - { "path": "../../packages/superset-ui-chart-controls" } + { "path": "../../packages/superset-ui-chart-controls" }, + { "path": "../../packages/superset-ui-glyph-core" } ] } diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/types/external.d.ts b/superset-frontend/plugins/legacy-plugin-chart-rose/types/external.d.ts index 66677a600a6..ecd4536d29e 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-rose/types/external.d.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-rose/types/external.d.ts @@ -16,13 +16,11 @@ * specific language governing permissions and limitations * under the License. */ - declare module '*.png' { - const value: string; + const value: any; export default value; } - declare module '*.jpg' { - const value: string; + const value: any; export default value; } diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/package.json b/superset-frontend/plugins/legacy-plugin-chart-world-map/package.json index 257f1f736f4..748de186b90 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-world-map/package.json +++ b/superset-frontend/plugins/legacy-plugin-chart-world-map/package.json @@ -38,6 +38,7 @@ "peerDependencies": { "@superset-ui/chart-controls": "*", "@superset-ui/core": "*", + "@superset-ui/glyph-core": "*", "@apache-superset/core": "*", "react": "^18.2.0" } diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/controlPanel.ts b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/controlPanel.ts deleted file mode 100644 index 974264e80fb..00000000000 --- a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/controlPanel.ts +++ /dev/null @@ -1,155 +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 { t } from '@apache-superset/core/translation'; -import { - ControlPanelConfig, - formatSelectOptions, - getStandardizedControls, -} from '@superset-ui/chart-controls'; -import { ColorBy } from './utils'; - -const config: ControlPanelConfig = { - controlPanelSections: [ - { - label: t('Query'), - expanded: true, - controlSetRows: [ - ['entity'], - [ - { - name: 'country_fieldtype', - config: { - type: 'SelectControl', - label: t('Country Field Type'), - default: 'cca2', - choices: [ - ['name', t('Full name')], - ['cioc', t('code International Olympic Committee (cioc)')], - ['cca2', t('code ISO 3166-1 alpha-2 (cca2)')], - ['cca3', t('code ISO 3166-1 alpha-3 (cca3)')], - ], - description: t( - 'The country code standard that Superset should expect ' + - 'to find in the [country] column', - ), - }, - }, - ], - ['metric'], - ['adhoc_filters'], - ['row_limit'], - ['sort_by_metric'], - ], - }, - { - label: t('Options'), - expanded: true, - controlSetRows: [ - [ - { - name: 'show_bubbles', - config: { - type: 'CheckboxControl', - label: t('Show Bubbles'), - default: false, - renderTrigger: true, - description: t('Whether to display bubbles on top of countries'), - }, - }, - ], - ['secondary_metric'], - [ - { - name: 'max_bubble_size', - config: { - type: 'SelectControl', - freeForm: true, - label: t('Max Bubble Size'), - default: '25', - choices: formatSelectOptions([ - '5', - '10', - '15', - '25', - '50', - '75', - '100', - ]), - }, - }, - ], - ['color_picker'], - [ - { - name: 'color_by', - config: { - type: 'RadioButtonControl', - label: t('Color by'), - default: ColorBy.Metric, - options: [ - [ColorBy.Metric, t('Metric')], - [ColorBy.Country, t('Country')], - ], - description: t( - 'Choose whether a country should be shaded by the metric, or assigned a color based on a categorical color palette', - ), - }, - }, - ], - ['linear_color_scheme'], - ['color_scheme'], - ], - }, - { - label: t('Chart Options'), - expanded: true, - controlSetRows: [['y_axis_format'], ['currency_format']], - }, - ], - controlOverrides: { - entity: { - label: t('Country Column'), - description: t('3 letter code of the country'), - }, - secondary_metric: { - label: t('Bubble Size'), - description: t('Metric that defines the size of the bubble'), - }, - color_picker: { - label: t('Bubble Color'), - }, - linear_color_scheme: { - label: t('Country Color Scheme'), - visibility: ({ controls }) => - Boolean(controls?.color_by.value === ColorBy.Metric), - }, - color_scheme: { - label: t('Country Color Scheme'), - visibility: ({ controls }) => - Boolean(controls?.color_by.value === ColorBy.Country), - }, - }, - formDataOverrides: formData => ({ - ...formData, - entity: getStandardizedControls().shiftColumn(), - metric: getStandardizedControls().shiftMetric(), - }), -}; - -export default config; diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/index.ts b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/index.ts deleted file mode 100644 index 4b53c5cb9ec..00000000000 --- a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/index.ts +++ /dev/null @@ -1,71 +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 { t } from '@apache-superset/core/translation'; -import { ChartMetadata, ChartPlugin, Behavior } from '@superset-ui/core'; -import transformProps from './transformProps'; -import thumbnail from './images/thumbnail.png'; -import thumbnailDark from './images/thumbnail-dark.png'; -import example1 from './images/WorldMap1.jpg'; -import example1Dark from './images/WorldMap1-dark.jpg'; -import example2 from './images/WorldMap2.jpg'; -import example2Dark from './images/WorldMap2-dark.jpg'; -import controlPanel from './controlPanel'; - -const metadata = new ChartMetadata({ - category: t('Map'), - credits: ['http://datamaps.github.io/'], - description: t( - 'A map of the world, that can indicate values in different countries.', - ), - exampleGallery: [ - { url: example1, urlDark: example1Dark }, - { url: example2, urlDark: example2Dark }, - ], - name: t('World Map'), - tags: [ - t('2D'), - t('Comparison'), - t('Intensity'), - t('Legacy'), - t('Multi-Dimensions'), - t('Multi-Layers'), - t('Multi-Variables'), - t('Scatter'), - t('Featured'), - ], - thumbnail, - thumbnailDark, - useLegacyApi: true, - behaviors: [ - Behavior.InteractiveChart, - Behavior.DrillToDetail, - Behavior.DrillBy, - ], -}); - -export default class WorldMapChartPlugin extends ChartPlugin { - constructor() { - super({ - loadChart: () => import('./ReactWorldMap'), - metadata, - transformProps, - controlPanel, - }); - } -} diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/index.tsx b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/index.tsx new file mode 100644 index 00000000000..78e51728408 --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/index.tsx @@ -0,0 +1,266 @@ +/** + * 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 { t } from '@apache-superset/core/translation'; +import { Behavior, getValueFormatter, Currency } from '@superset-ui/core'; +import { + formatSelectOptions, + getStandardizedControls, +} from '@superset-ui/chart-controls'; +import { defineChart } from '@superset-ui/glyph-core'; +import { rgb } from 'd3-color'; +import thumbnail from './images/thumbnail.png'; +import thumbnailDark from './images/thumbnail-dark.png'; +import example1 from './images/WorldMap1.jpg'; +import example1Dark from './images/WorldMap1-dark.jpg'; +import example2 from './images/WorldMap2.jpg'; +import example2Dark from './images/WorldMap2-dark.jpg'; +import { ColorBy } from './utils'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const ReactWorldMap = require('./ReactWorldMap').default; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type WorldMapExtra = Record; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export default defineChart({ + metadata: { + name: t('World Map'), + description: t( + 'A map of the world, that can indicate values in different countries.', + ), + category: t('Map'), + credits: ['http://datamaps.github.io/'], + tags: [ + t('2D'), + t('Comparison'), + t('Intensity'), + t('Legacy'), + t('Multi-Dimensions'), + t('Multi-Layers'), + t('Multi-Variables'), + t('Scatter'), + t('Featured'), + ], + thumbnail, + thumbnailDark, + exampleGallery: [ + { url: example1, urlDark: example1Dark }, + { url: example2, urlDark: example2Dark }, + ], + useLegacyApi: true, + behaviors: [ + Behavior.InteractiveChart, + Behavior.DrillToDetail, + Behavior.DrillBy, + ], + }, + arguments: {}, + additionalControls: { + queryBefore: [ + ['entity'], + [ + { + name: 'country_fieldtype', + config: { + type: 'SelectControl', + label: t('Country Field Type'), + default: 'cca2', + choices: [ + ['name', t('Full name')], + ['cioc', t('code International Olympic Committee (cioc)')], + ['cca2', t('code ISO 3166-1 alpha-2 (cca2)')], + ['cca3', t('code ISO 3166-1 alpha-3 (cca3)')], + ], + description: t( + 'The country code standard that Superset should expect ' + + 'to find in the [country] column', + ), + }, + }, + ], + ['metric'], + ], + query: [['row_limit'], ['sort_by_metric']], + chartOptions: [['y_axis_format'], ['currency_format']], + }, + middleSections: [ + { + label: t('Options'), + expanded: true, + controlSetRows: [ + [ + { + name: 'show_bubbles', + config: { + type: 'CheckboxControl', + label: t('Show Bubbles'), + default: false, + renderTrigger: true, + description: t('Whether to display bubbles on top of countries'), + }, + }, + ], + ['secondary_metric'], + [ + { + name: 'max_bubble_size', + config: { + type: 'SelectControl', + freeForm: true, + label: t('Max Bubble Size'), + default: '25', + choices: formatSelectOptions([ + '5', + '10', + '15', + '25', + '50', + '75', + '100', + ]), + }, + }, + ], + ['color_picker'], + [ + { + name: 'color_by', + config: { + type: 'RadioButtonControl', + label: t('Color by'), + default: ColorBy.Metric, + options: [ + [ColorBy.Metric, t('Metric')], + [ColorBy.Country, t('Country')], + ], + description: t( + 'Choose whether a country should be shaded by the metric, or assigned a color based on a categorical color palette', + ), + }, + }, + ], + ['linear_color_scheme'], + ['color_scheme'], + ], + }, + ], + additionalControlOverrides: { + entity: { + label: t('Country Column'), + description: t('3 letter code of the country'), + }, + secondary_metric: { + label: t('Bubble Size'), + description: t('Metric that defines the size of the bubble'), + }, + color_picker: { + label: t('Bubble Color'), + }, + linear_color_scheme: { + label: t('Country Color Scheme'), + visibility: ({ + controls, + }: { + controls?: Record; + }) => Boolean(controls?.color_by?.value === ColorBy.Metric), + }, + color_scheme: { + label: t('Country Color Scheme'), + visibility: ({ + controls, + }: { + controls?: Record; + }) => Boolean(controls?.color_by?.value === ColorBy.Country), + }, + }, + formDataOverrides: formData => ({ + ...formData, + entity: getStandardizedControls().shiftColumn(), + metric: getStandardizedControls().shiftMetric(), + }), + transform: chartProps => { + const { + formData, + hooks, + inContextMenu, + filterState, + emitCrossFilters, + datasource, + } = chartProps; + const { onContextMenu, setDataMask } = hooks as Record; + const { + countryFieldtype, + entity, + maxBubbleSize, + showBubbles, + linearColorScheme, + colorPicker, + colorBy, + colorScheme, + sliceId, + metric, + yAxisFormat, + currencyFormat, + } = formData as Record; + + const { r, g, b } = (colorPicker ?? { r: 0, g: 0, b: 0 }) as { + r: number; + g: number; + b: number; + }; + const { currencyFormats = {}, columnFormats = {} } = (datasource ?? {}) as { + currencyFormats?: Record; + columnFormats?: Record; + }; + + const formatter = getValueFormatter( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + metric as any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + currencyFormats as any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + columnFormats as any, + yAxisFormat as string, + currencyFormat as Currency, + ); + + return { + countryFieldtype, + entity, + maxBubbleSize: parseInt(String(maxBubbleSize ?? '25'), 10), + showBubbles, + linearColorScheme, + color: rgb(r, g, b).formatHex(), + colorBy, + colorScheme, + sliceId, + onContextMenu, + setDataMask, + inContextMenu, + filterState, + emitCrossFilters, + formatter, + }; + }, + render: ({ width, height, data, ...extra }) => ( + + ), +}); diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/transformProps.ts b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/transformProps.ts deleted file mode 100644 index 0c541297fcb..00000000000 --- a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/transformProps.ts +++ /dev/null @@ -1,89 +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 { rgb } from 'd3-color'; -import { ChartProps, getValueFormatter } from '@superset-ui/core'; - -export default function transformProps(chartProps: ChartProps) { - const { - width, - height, - formData, - queriesData, - hooks, - inContextMenu, - filterState, - emitCrossFilters, - datasource, - } = chartProps; - const { onContextMenu, setDataMask } = hooks; - const { - countryFieldtype, - entity, - maxBubbleSize, - showBubbles, - linearColorScheme, - colorPicker, - colorBy, - colorScheme, - sliceId, - metric, - yAxisFormat, - currencyFormat, - } = formData; - const { r, g, b } = colorPicker; - const { - currencyFormats = {}, - columnFormats = {}, - currencyCodeColumn, - } = datasource; - const { data, detected_currency: detectedCurrency } = queriesData[0]; - - const formatter = getValueFormatter( - metric, - currencyFormats, - columnFormats, - yAxisFormat, - currencyFormat, - undefined, // key - not needed for single-metric charts - data, - currencyCodeColumn, - detectedCurrency, - ); - - return { - countryFieldtype, - entity, - data, - width, - height, - maxBubbleSize: parseInt(maxBubbleSize, 10), - showBubbles, - linearColorScheme, - color: rgb(r, g, b).hex(), - colorBy, - colorScheme, - sliceId, - onContextMenu, - setDataMask, - inContextMenu, - filterState, - emitCrossFilters, - formatter, - }; -} diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/tsconfig.json b/superset-frontend/plugins/legacy-plugin-chart-world-map/tsconfig.json index 03190ec5bd4..f0bfbba676d 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-world-map/tsconfig.json +++ b/superset-frontend/plugins/legacy-plugin-chart-world-map/tsconfig.json @@ -20,6 +20,7 @@ "references": [ { "path": "../../packages/superset-core" }, { "path": "../../packages/superset-ui-core" }, - { "path": "../../packages/superset-ui-chart-controls" } + { "path": "../../packages/superset-ui-chart-controls" }, + { "path": "../../packages/superset-ui-glyph-core" } ] } diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/types/external.d.ts b/superset-frontend/plugins/legacy-plugin-chart-world-map/types/external.d.ts index 66677a600a6..ecd4536d29e 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-world-map/types/external.d.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-world-map/types/external.d.ts @@ -16,13 +16,11 @@ * specific language governing permissions and limitations * under the License. */ - declare module '*.png' { - const value: string; + const value: any; export default value; } - declare module '*.jpg' { - const value: string; + const value: any; export default value; } diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/package.json b/superset-frontend/plugins/legacy-preset-chart-nvd3/package.json index f61be6cfebb..afb647be373 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/package.json +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/package.json @@ -42,6 +42,7 @@ "@apache-superset/core": "*", "@superset-ui/chart-controls": "*", "@superset-ui/core": "*", + "@superset-ui/glyph-core": "*", "dayjs": "^1.11.19", "react": "^18.2.0" } diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/index.ts b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/index.ts deleted file mode 100644 index bf98ec9a44e..00000000000 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/index.ts +++ /dev/null @@ -1,51 +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 { t } from '@apache-superset/core/translation'; -import { ChartMetadata, ChartPlugin } from '@superset-ui/core'; -import transformProps from '../transformProps'; -import thumbnail from './images/thumbnail.png'; -import thumbnailDark from './images/thumbnail-dark.png'; -import example from './images/example.jpg'; -import exampleDark from './images/example-dark.jpg'; -import controlPanel from './controlPanel'; - -const metadata = new ChartMetadata({ - category: t('Evolution'), - credits: ['http://nvd3.org'], - description: t( - 'Compares metrics between different time periods. Displays time series data across multiple periods (like weeks or months) to show period-over-period trends and patterns.', - ), - exampleGallery: [{ url: example, urlDark: exampleDark }], - name: t('Time-series Period Pivot'), - tags: [t('Legacy'), t('Time'), t('nvd3')], - thumbnail, - thumbnailDark, - useLegacyApi: true, -}); - -export default class TimePivotChartPlugin extends ChartPlugin { - constructor() { - super({ - loadChart: () => import('../ReactNVD3'), - metadata, - transformProps, - controlPanel, - }); - } -} diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/controlPanel.ts b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/index.tsx similarity index 69% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/controlPanel.ts rename to superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/index.tsx index da9aaffe3b6..8b22fdace1b 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/controlPanel.ts +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/index.tsx @@ -18,11 +18,15 @@ */ import { t } from '@apache-superset/core/translation'; import { - ControlPanelConfig, D3_FORMAT_OPTIONS, getStandardizedControls, sections, } from '@superset-ui/chart-controls'; +import { defineChart } from '@superset-ui/glyph-core'; +import thumbnail from './images/thumbnail.png'; +import thumbnailDark from './images/thumbnail-dark.png'; +import example from './images/example.jpg'; +import exampleDark from './images/example-dark.jpg'; import { lineInterpolation, showLegend, @@ -37,8 +41,31 @@ import { leftMargin, } from '../NVD3Controls'; -const config: ControlPanelConfig = { - controlPanelSections: [ +// eslint-disable-next-line @typescript-eslint/no-var-requires +const transformPropsJs = require('../transformProps').default; +// eslint-disable-next-line @typescript-eslint/no-var-requires +const ReactNVD3 = require('../ReactNVD3').default; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type NVD3Extra = Record; + +export default defineChart, NVD3Extra>({ + metadata: { + name: t('Time-series Period Pivot'), + description: t( + 'Compares metrics between different time periods. Displays time series data across multiple periods (like weeks or months) to show period-over-period trends and patterns.', + ), + category: t('Evolution'), + credits: ['http://nvd3.org'], + tags: [t('Legacy'), t('Time'), t('nvd3')], + thumbnail, + thumbnailDark, + exampleGallery: [{ url: example, urlDark: exampleDark }], + useLegacyApi: true, + }, + arguments: {}, + suppressQuerySection: true, + prependSections: [ sections.legacyTimeseriesTime, { label: t('Query'), @@ -119,15 +146,16 @@ const config: ControlPanelConfig = { ], }, ], - controlOverrides: { + additionalControlOverrides: { metric: { clearable: false, }, }, formDataOverrides: formData => ({ ...formData, - metric: getStandardizedControls().shiftMetric, + metric: getStandardizedControls().shiftMetric(), }), -}; - -export default config; + transform: chartProps => transformPropsJs(chartProps), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + render: (props: any) => , +}); diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/tsconfig.json b/superset-frontend/plugins/legacy-preset-chart-nvd3/tsconfig.json index e73ca5d4f87..02f450b2a29 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/tsconfig.json +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/tsconfig.json @@ -16,6 +16,7 @@ "references": [ { "path": "../../packages/superset-core" }, { "path": "../../packages/superset-ui-core" }, - { "path": "../../packages/superset-ui-chart-controls" } + { "path": "../../packages/superset-ui-chart-controls" }, + { "path": "../../packages/superset-ui-glyph-core" } ] } diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/types/external.d.ts b/superset-frontend/plugins/legacy-preset-chart-nvd3/types/external.d.ts index cd1101eb3d0..ecd4536d29e 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/types/external.d.ts +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/types/external.d.ts @@ -17,33 +17,10 @@ * under the License. */ declare module '*.png' { - const value: string; + const value: any; export default value; } - declare module '*.jpg' { - const value: string; + const value: any; export default value; } - -declare module '*.jpeg' { - const value: string; - export default value; -} - -declare module 'd3' { - const d3: Record; - export default d3; -} - -declare module 'nvd3-fork' { - const nv: Record; - export default nv; -} - -declare module 'nvd3-fork/build/nv.d3.css'; - -declare module 'd3-tip' { - const d3tip: () => Record; - export default d3tip; -} diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/tsconfig.json b/superset-frontend/plugins/plugin-chart-ag-grid-table/tsconfig.json index 03190ec5bd4..f0bfbba676d 100644 --- a/superset-frontend/plugins/plugin-chart-ag-grid-table/tsconfig.json +++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/tsconfig.json @@ -20,6 +20,7 @@ "references": [ { "path": "../../packages/superset-core" }, { "path": "../../packages/superset-ui-core" }, - { "path": "../../packages/superset-ui-chart-controls" } + { "path": "../../packages/superset-ui-chart-controls" }, + { "path": "../../packages/superset-ui-glyph-core" } ] } diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/package.json b/superset-frontend/plugins/plugin-chart-cartodiagram/package.json index 76586122cef..6cb31eee221 100644 --- a/superset-frontend/plugins/plugin-chart-cartodiagram/package.json +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/package.json @@ -23,7 +23,7 @@ "homepage": "https://github.com/apache-superset/superset-ui#readme", "contributors": [ "terrestris GmbH & Co. KG (https://www.terrestris.de)", - "meggsimum - Büro für Geoinformatik (https://meggsimum.de)" + "meggsimum - B\u00fcro f\u00fcr Geoinformatik (https://meggsimum.de)" ], "publishConfig": { "access": "public" @@ -39,6 +39,7 @@ "@reduxjs/toolkit": "*", "@superset-ui/chart-controls": "*", "@superset-ui/core": "*", + "@superset-ui/glyph-core": "*", "@types/react-redux": "*", "geostyler": "^18.3.1", "geostyler-data": "^1.0.0", diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/src/CartodiagramPlugin.tsx b/superset-frontend/plugins/plugin-chart-cartodiagram/src/CartodiagramPlugin.tsx deleted file mode 100644 index 77f6eeeba83..00000000000 --- a/superset-frontend/plugins/plugin-chart-cartodiagram/src/CartodiagramPlugin.tsx +++ /dev/null @@ -1,59 +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 { createRef, useState } from 'react'; -import { styled, useTheme } from '@apache-superset/core/theme'; -import OlMap from 'ol/Map'; -import { - CartodiagramPluginProps, - CartodiagramPluginStylesProps, -} from './types'; - -import OlChartMap from './components/OlChartMap'; - -import 'ol/ol.css'; - -// The following Styles component is a
element, which has been styled using Emotion -// For docs, visit https://emotion.sh/docs/styled - -// Theming variables are provided for your use via a ThemeProvider -// imported from @superset-ui/core. For variables available, please visit -// https://github.com/apache-superset/superset-ui/blob/master/packages/superset-ui-core/src/style/index.ts - -const Styles = styled.div` - height: ${({ height }) => height}px; - width: ${({ width }) => width}px; -`; - -export default function CartodiagramPlugin(props: CartodiagramPluginProps) { - const { height, width } = props; - const theme = useTheme(); - - const rootElem = createRef(); - - const [mapId] = useState( - `cartodiagram-plugin-${Math.floor(Math.random() * 1000000)}`, - ); - const [olMap] = useState(new OlMap({})); - - return ( - - - - ); -} diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/src/index.ts b/superset-frontend/plugins/plugin-chart-cartodiagram/src/index.ts deleted file mode 100644 index ca175475ac3..00000000000 --- a/superset-frontend/plugins/plugin-chart-cartodiagram/src/index.ts +++ /dev/null @@ -1,20 +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. - */ -// eslint-disable-next-line import/prefer-default-export -export { default as CartodiagramPlugin } from './plugin'; diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/src/index.tsx b/superset-frontend/plugins/plugin-chart-cartodiagram/src/index.tsx new file mode 100644 index 00000000000..71b217c4e27 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/src/index.tsx @@ -0,0 +1,393 @@ +/** + * 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 { createRef, useState } from 'react'; +import { t } from '@apache-superset/core/translation'; +import { styled, useTheme } from '@apache-superset/core/theme'; +import OlMap from 'ol/Map'; +import { + validateNonEmpty, + ChartPlugin, + QueryFormData, + getChartBuildQueryRegistry, + getChartTransformPropsRegistry, +} from '@superset-ui/core'; +import { defineChart } from '@superset-ui/glyph-core'; +import { + CartodiagramPluginConstructorOpts, + CartodiagramPluginProps, + CartodiagramPluginStylesProps, + LayerConf, +} from './types'; +import OlChartMap from './components/OlChartMap'; +import { parseSelectedChart, getChartConfigs } from './util/transformPropsUtil'; +import { selectedChartMutator } from './util/controlPanelUtil'; +import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL } from './util/zoomUtil'; +import thumbnail from './images/thumbnail.png'; +import thumbnailDark from './images/thumbnail-dark.png'; +import example1 from './images/example1.png'; +import example1Dark from './images/example1-dark.png'; +import example2 from './images/example2.png'; +import example2Dark from './images/example2-dark.png'; + +import 'ol/ol.css'; + +// ── CartodiagramPlugin component ────────────────────────────────────────────── + +const Styles = styled.div` + height: ${({ height }) => height}px; + width: ${({ width }) => width}px; +`; + +function CartodiagramPlugin(props: CartodiagramPluginProps) { + const { height, width } = props; + const theme = useTheme(); + + const rootElem = createRef(); + + const [mapId] = useState( + `cartodiagram-plugin-${Math.floor(Math.random() * 1000000)}`, + ); + const [olMap] = useState(new OlMap({})); + + return ( + + + + ); +} + +// ── buildQuery ──────────────────────────────────────────────────────────────── + +export function buildQuery(formData: QueryFormData) { + const { + selected_chart: selectedChartString, + geom_column: geometryColumn, + extra_form_data: extraFormData, + } = formData; + const selectedChart = JSON.parse(selectedChartString); + const vizType = selectedChart.viz_type; + const chartFormData = JSON.parse(selectedChart.params); + // Pass extra_form_data to chartFormData so that + // dashboard filters will also be applied to the charts + // on the map. + chartFormData.extra_form_data = { + ...chartFormData.extra_form_data, + ...extraFormData, + }; + + // adapt groupby property to ensure geometry column always exists + // and is always at first position + let { groupby } = chartFormData; + if (!groupby) { + groupby = []; + } + // add geometry column at the first place + groupby?.unshift(geometryColumn); + chartFormData.groupby = groupby; + + // TODO: find way to import correct type "InclusiveLoaderResult" + const buildQueryRegistry = getChartBuildQueryRegistry(); + const chartQueryBuilder = buildQueryRegistry.get(vizType) as any; + + const chartQuery = chartQueryBuilder(chartFormData); + return chartQuery; +} + +// ── Plugin definition ───────────────────────────────────────────────────────── + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type CartodiagramExtra = Record; + +// Standalone transformProps exported for testing +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function transformProps(chartProps: any) { + const { formData, hooks, width, height, queriesData } = chartProps; + const { + geomColumn, + selectedChart: selectedChartString, + chartSize, + layerConfigs, + mapView, + chartBackgroundColor, + chartBackgroundBorderRadius, + } = formData as Record; + const { setControlValue = () => {} } = (hooks ?? {}) as { + setControlValue?: (key: string, value: unknown) => void; + }; + const selectedChart = parseSelectedChart(selectedChartString as string); + const transformPropsRegistry = getChartTransformPropsRegistry(); + const chartTransformer = transformPropsRegistry.get(selectedChart.viz_type); + const chartConfigs = getChartConfigs( + selectedChart, + geomColumn as string, + chartProps, + chartTransformer, + ); + + return { + width, + height, + queriesData, + geomColumn, + selectedChart, + chartConfigs, + chartVizType: selectedChart.viz_type, + chartSize, + layerConfigs, + mapView, + chartBackgroundColor, + chartBackgroundBorderRadius, + setControlValue, + }; +} + +export function createCartodiagramPlugin( + opts: CartodiagramPluginConstructorOpts = {}, +): new () => ChartPlugin { + const layerConfigsDefault: LayerConf[] = opts.defaultLayers ?? []; + + return defineChart, CartodiagramExtra>({ + metadata: { + name: t('Cartodiagram'), + description: + 'Display charts on a map. For using this plugin, users first have to create any other chart that can then be placed on the map.', + category: t('Map'), + tags: [t('Geo'), t('2D'), t('Spatial'), t('Experimental')], + thumbnail, + thumbnailDark, + exampleGallery: [ + { + url: example1, + urlDark: example1Dark, + caption: t('Pie charts on a map'), + }, + { + url: example2, + urlDark: example2Dark, + caption: t('Line charts on a map'), + }, + ], + }, + arguments: {}, + buildQuery, + suppressQuerySection: true, + prependSections: [ + { + label: t('Configuration'), + expanded: true, + controlSetRows: [ + [ + { + name: 'selected_chart', + config: { + type: 'SelectAsyncControl', + mutator: selectedChartMutator, + multi: false, + label: t('Chart'), + validators: [validateNonEmpty], + description: t('Choose a chart for displaying on the map'), + placeholder: t('Select chart'), + onAsyncErrorMessage: t('Error while fetching charts'), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mapStateToProps: (state: any) => { + if (state?.datasource?.id) { + const { id: datasourceId } = state.datasource; + const query = { + columns: ['id', 'slice_name', 'params', 'viz_type'], + filters: [ + { + col: 'datasource_id', + opr: 'eq', + value: datasourceId, + }, + ], + page: 0, + page_size: 999, + }; + return { + dataEndpoint: `/api/v1/chart/?q=${JSON.stringify(query)}`, + }; + } + return {}; + }, + }, + }, + ], + [ + { + name: 'geom_column', + config: { + type: 'SelectControl', + label: t('Geometry Column'), + renderTrigger: false, + description: t('The name of the geometry column'), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mapStateToProps: (state: any) => ({ + choices: state.datasource?.columns?.map( + (c: { column_name: string }) => [ + c.column_name, + c.column_name, + ], + ), + }), + validators: [validateNonEmpty], + }, + }, + ], + ], + }, + { + label: t('Map Options'), + expanded: true, + controlSetRows: [ + [ + { + name: 'map_view', + config: { + type: 'MapViewControl', + renderTrigger: true, + description: t( + 'The extent of the map on application start. FIT DATA automatically sets the extent so that all data points are included in the viewport. CUSTOM allows users to define the extent manually.', + ), + label: t('Extent'), + dontRefreshOnChange: true, + default: { + mode: 'FIT_DATA', + }, + }, + }, + ], + [ + { + name: 'layer_configs', + config: { + type: 'LayerConfigsControl', + renderTrigger: true, + label: t('Layers'), + default: layerConfigsDefault, + description: t('The configuration for the map layers'), + }, + }, + ], + ], + }, + { + label: t('Chart Options'), + expanded: true, + controlSetRows: [ + [ + { + name: 'chart_background_color', + config: { + label: t('Background Color'), + description: t('The background color of the charts.'), + type: 'ColorPickerControl', + default: { r: 255, g: 255, b: 255, a: 0.2 }, + renderTrigger: true, + }, + }, + ], + [ + { + name: 'chart_background_border_radius', + config: { + label: t('Corner Radius'), + description: t('The corner radius of the chart background'), + type: 'SliderControl', + default: 10, + min: 0, + step: 1, + max: 100, + renderTrigger: true, + }, + }, + ], + [ + { + name: 'chart_size', + config: { + type: 'ZoomConfigControl', + renderTrigger: true, + default: { + type: 'FIXED', + configs: { + zoom: 6, + width: 100, + height: 100, + slope: 30, + exponent: 2, + }, + values: { + ...Array.from( + { length: MAX_ZOOM_LEVEL - MIN_ZOOM_LEVEL + 1 }, + () => ({ width: 100, height: 100 }), + ), + }, + }, + label: t('Chart size'), + description: t('Configure the chart size for each zoom level'), + }, + }, + ], + ], + }, + ], + transform: chartProps => { + const { formData, hooks } = chartProps; + const { + geomColumn, + selectedChart: selectedChartString, + chartSize, + layerConfigs, + mapView, + chartBackgroundColor, + chartBackgroundBorderRadius, + } = formData as Record; + const { setControlValue = () => {} } = (hooks ?? {}) as { + setControlValue?: (key: string, value: unknown) => void; + }; + const selectedChart = parseSelectedChart(selectedChartString as string); + const transformPropsRegistry = getChartTransformPropsRegistry(); + const chartTransformer = transformPropsRegistry.get( + selectedChart.viz_type, + ); + const chartConfigs = getChartConfigs( + selectedChart, + geomColumn as string, + chartProps, + chartTransformer, + ); + + return { + geomColumn, + selectedChart, + chartConfigs, + chartVizType: selectedChart.viz_type, + chartSize, + layerConfigs, + mapView, + chartBackgroundColor, + chartBackgroundBorderRadius, + setControlValue, + }; + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + render: (props: any) => , + }); +} diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/src/plugin/buildQuery.ts b/superset-frontend/plugins/plugin-chart-cartodiagram/src/plugin/buildQuery.ts deleted file mode 100644 index 87f1d1fd880..00000000000 --- a/superset-frontend/plugins/plugin-chart-cartodiagram/src/plugin/buildQuery.ts +++ /dev/null @@ -1,54 +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 { QueryFormData, getChartBuildQueryRegistry } from '@superset-ui/core'; - -export default function buildQuery(formData: QueryFormData) { - const { - selected_chart: selectedChartString, - geom_column: geometryColumn, - extra_form_data: extraFormData, - } = formData; - const selectedChart = JSON.parse(selectedChartString); - const vizType = selectedChart.viz_type; - const chartFormData = JSON.parse(selectedChart.params); - // Pass extra_form_data to chartFormData so that - // dashboard filters will also be applied to the charts - // on the map. - chartFormData.extra_form_data = { - ...chartFormData.extra_form_data, - ...extraFormData, - }; - - // adapt groupby property to ensure geometry column always exists - // and is always at first position - let { groupby } = chartFormData; - if (!groupby) { - groupby = []; - } - // add geometry column at the first place - groupby?.unshift(geometryColumn); - chartFormData.groupby = groupby; - - // TODO: find way to import correct type "InclusiveLoaderResult" - const buildQueryRegistry = getChartBuildQueryRegistry(); - const chartQueryBuilder = buildQueryRegistry.get(vizType) as any; - - const chartQuery = chartQueryBuilder(chartFormData); - return chartQuery; -} diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/src/plugin/controlPanel.ts b/superset-frontend/plugins/plugin-chart-cartodiagram/src/plugin/controlPanel.ts deleted file mode 100644 index c472ef4a5b0..00000000000 --- a/superset-frontend/plugins/plugin-chart-cartodiagram/src/plugin/controlPanel.ts +++ /dev/null @@ -1,194 +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 { t } from '@apache-superset/core/translation'; -import { validateNonEmpty } from '@superset-ui/core'; -import { ControlPanelConfig } from '@superset-ui/chart-controls'; -import { selectedChartMutator } from '../util/controlPanelUtil'; - -import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL } from '../util/zoomUtil'; - -const config: ControlPanelConfig = { - controlPanelSections: [ - { - label: t('Configuration'), - expanded: true, - controlSetRows: [ - [ - { - name: 'selected_chart', - config: { - type: 'SelectAsyncControl', - mutator: selectedChartMutator, - multi: false, - label: t('Chart'), - validators: [validateNonEmpty], - description: t('Choose a chart for displaying on the map'), - placeholder: t('Select chart'), - onAsyncErrorMessage: t('Error while fetching charts'), - mapStateToProps: state => { - if (state?.datasource?.id) { - const datasourceId = state.datasource.id; - const query = { - columns: ['id', 'slice_name', 'params', 'viz_type'], - filters: [ - { - col: 'datasource_id', - opr: 'eq', - value: datasourceId, - }, - ], - page: 0, - // TODO check why we only retrieve 100 items, even though there are more - page_size: 999, - }; - - const dataEndpoint = `/api/v1/chart/?q=${JSON.stringify( - query, - )}`; - - return { dataEndpoint }; - } - // could not extract datasource from map - return {}; - }, - }, - }, - ], - [ - { - name: 'geom_column', - config: { - type: 'SelectControl', - label: t('Geometry Column'), - renderTrigger: false, - description: t('The name of the geometry column'), - mapStateToProps: state => ({ - choices: state.datasource?.columns.map(c => [ - c.column_name, - c.column_name, - ]), - }), - validators: [validateNonEmpty], - }, - }, - ], - ], - }, - { - label: t('Map Options'), - expanded: true, - controlSetRows: [ - [ - { - name: 'map_view', - config: { - type: 'MapViewControl', - renderTrigger: true, - description: t( - 'The extent of the map on application start. FIT DATA automatically sets the extent so that all data points are included in the viewport. CUSTOM allows users to define the extent manually.', - ), - label: t('Extent'), - dontRefreshOnChange: true, - default: { - mode: 'FIT_DATA', - }, - }, - }, - ], - [ - { - // name is referenced in 'index.ts' for setting default value - name: 'layer_configs', - config: { - type: 'LayerConfigsControl', - renderTrigger: true, - label: t('Layers'), - default: [], - description: t('The configuration for the map layers'), - }, - }, - ], - ], - }, - { - label: t('Chart Options'), - expanded: true, - controlSetRows: [ - [ - { - name: 'chart_background_color', - config: { - label: t('Background Color'), - description: t('The background color of the charts.'), - type: 'ColorPickerControl', - default: { r: 255, g: 255, b: 255, a: 0.2 }, - renderTrigger: true, - }, - }, - ], - [ - { - name: 'chart_background_border_radius', - config: { - label: t('Corner Radius'), - description: t('The corner radius of the chart background'), - type: 'SliderControl', - default: 10, - min: 0, - step: 1, - max: 100, - renderTrigger: true, - }, - }, - ], - [ - { - name: 'chart_size', - config: { - type: 'ZoomConfigControl', - // set this to true, if we are able to render it fast - renderTrigger: true, - default: { - type: 'FIXED', - configs: { - zoom: 6, - width: 100, - height: 100, - slope: 30, - exponent: 2, - }, - // create an object with keys MIN_ZOOM_LEVEL - MAX_ZOOM_LEVEL - // that all contain the same initial value - values: { - ...Array.from( - { length: MAX_ZOOM_LEVEL - MIN_ZOOM_LEVEL + 1 }, - () => ({ width: 100, height: 100 }), - ), - }, - }, - label: t('Chart size'), - description: t('Configure the chart size for each zoom level'), - }, - }, - ], - ], - }, - ], -}; -export default config; diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/src/plugin/index.ts b/superset-frontend/plugins/plugin-chart-cartodiagram/src/plugin/index.ts deleted file mode 100644 index 7bf99854318..00000000000 --- a/superset-frontend/plugins/plugin-chart-cartodiagram/src/plugin/index.ts +++ /dev/null @@ -1,79 +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 { t } from '@apache-superset/core/translation'; -import { ChartMetadata, ChartPlugin } from '@superset-ui/core'; -import buildQuery from './buildQuery'; -import controlPanel from './controlPanel'; -import transformProps from './transformProps'; -import thumbnail from '../images/thumbnail.png'; -import thumbnailDark from '../images/thumbnail-dark.png'; -import example1 from '../images/example1.png'; -import example1Dark from '../images/example1-dark.png'; -import example2 from '../images/example2.png'; -import example2Dark from '../images/example2-dark.png'; -import { CartodiagramPluginConstructorOpts } from '../types'; -import { getLayerConfig } from '../util/controlPanelUtil'; - -export default class CartodiagramPlugin extends ChartPlugin { - constructor(opts: CartodiagramPluginConstructorOpts) { - const metadata = new ChartMetadata({ - description: - 'Display charts on a map. For using this plugin, users first have to create any other chart that can then be placed on the map.', - name: t('Cartodiagram'), - thumbnail, - thumbnailDark, - tags: [t('Geo'), t('2D'), t('Spatial'), t('Experimental')], - category: t('Map'), - exampleGallery: [ - { - url: example1, - urlDark: example1Dark, - caption: t('Pie charts on a map'), - }, - { - url: example2, - urlDark: example2Dark, - caption: t('Line charts on a map'), - }, - ], - }); - - if (opts.defaultLayers) { - const layerConfig = getLayerConfig(controlPanel); - - // set defaults for layer config if found - if (layerConfig) { - layerConfig.config.default = opts.defaultLayers; - } else { - // eslint-disable-next-line no-console - console.warn( - 'Cannot set defaultLayers. layerConfig not found in control panel. Please check if the path to layerConfig should be adjusted.', - ); - } - } - - super({ - buildQuery, - controlPanel, - loadChart: () => import('../CartodiagramPlugin'), - metadata, - transformProps, - }); - } -} diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/src/plugin/transformProps.ts b/superset-frontend/plugins/plugin-chart-cartodiagram/src/plugin/transformProps.ts deleted file mode 100644 index c868591ea17..00000000000 --- a/superset-frontend/plugins/plugin-chart-cartodiagram/src/plugin/transformProps.ts +++ /dev/null @@ -1,63 +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 { ChartProps, getChartTransformPropsRegistry } from '@superset-ui/core'; -import { - getChartConfigs, - parseSelectedChart, -} from '../util/transformPropsUtil'; - -export default function transformProps(chartProps: ChartProps) { - const { width, height, formData, hooks, theme } = chartProps; - const { - geomColumn, - selectedChart: selectedChartString, - chartSize, - layerConfigs, - mapView, - chartBackgroundColor, - chartBackgroundBorderRadius, - } = formData; - const { setControlValue = () => {} } = hooks; - const selectedChart = parseSelectedChart(selectedChartString); - const transformPropsRegistry = getChartTransformPropsRegistry(); - const chartTransformer = transformPropsRegistry.get(selectedChart.viz_type); - - const chartConfigs = getChartConfigs( - selectedChart, - geomColumn, - chartProps, - chartTransformer, - ); - - return { - width, - height, - geomColumn, - selectedChart, - chartConfigs, - chartVizType: selectedChart.viz_type, - chartSize, - layerConfigs, - mapView, - chartBackgroundColor, - chartBackgroundBorderRadius, - setControlValue, - theme, - }; -} diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/src/stories/Cartodiagram.stories.tsx b/superset-frontend/plugins/plugin-chart-cartodiagram/src/stories/Cartodiagram.stories.tsx deleted file mode 100644 index 9cce0e2230e..00000000000 --- a/superset-frontend/plugins/plugin-chart-cartodiagram/src/stories/Cartodiagram.stories.tsx +++ /dev/null @@ -1,208 +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 { - SuperChart, - VizType, - getChartTransformPropsRegistry, -} from '@superset-ui/core'; -import { CartodiagramPlugin } from '@superset-ui/plugin-chart-cartodiagram'; -import { - EchartsPieChartPlugin, - PieTransformProps, -} from '@superset-ui/plugin-chart-echarts'; -import { withResizableChartDemo, dummyDatasource } from '@storybook-shared'; -import { - defaultLayerConfigs, - defaultMapView, - defaultChartBackgroundColor, -} from './data'; - -const VIZ_TYPE = 'cartodiagram'; - -// Register the pie chart plugin and its transformProps (same as Pie stories) -new EchartsPieChartPlugin().configure({ key: VizType.Pie }).register(); -getChartTransformPropsRegistry().registerValue(VizType.Pie, PieTransformProps); - -// Register the cartodiagram plugin -new CartodiagramPlugin({}).configure({ key: VIZ_TYPE }).register(); - -// Sample data: each row has a geom column + the same format as pie chart data -// The cartodiagram will group by geom location and create a pie for each -const SF_GEOM = '{"type":"Point","coordinates":[-122.4194,37.7749]}'; -const LA_GEOM = '{"type":"Point","coordinates":[-118.2437,34.0522]}'; - -const sampleData = [ - // San Francisco data - { geom: SF_GEOM, category: 'Tech', 'SUM(revenue)': 1500000 }, - { geom: SF_GEOM, category: 'Finance', 'SUM(revenue)': 800000 }, - { geom: SF_GEOM, category: 'Healthcare', 'SUM(revenue)': 400000 }, - // Los Angeles data - { geom: LA_GEOM, category: 'Entertainment', 'SUM(revenue)': 3500000 }, - { geom: LA_GEOM, category: 'Tech', 'SUM(revenue)': 2000000 }, - { geom: LA_GEOM, category: 'Finance', 'SUM(revenue)': 1500000 }, -]; - -export default { - title: 'Chart Plugins/plugin-chart-cartodiagram', - decorators: [withResizableChartDemo], - args: { - donut: false, - showLabels: false, - colorScheme: 'supersetColors', - chartWidth: 100, - chartHeight: 100, - borderRadius: 8, - }, - argTypes: { - donut: { - control: 'boolean', - description: 'Display pie charts as donuts', - }, - showLabels: { - control: 'boolean', - description: 'Show labels on embedded charts', - }, - colorScheme: { - control: 'select', - options: [ - 'supersetColors', - 'd3Category10', - 'bnbColors', - 'googleCategory20c', - ], - }, - chartWidth: { - control: { type: 'range', min: 50, max: 200, step: 10 }, - description: 'Width of embedded charts in pixels', - }, - chartHeight: { - control: { type: 'range', min: 50, max: 200, step: 10 }, - description: 'Height of embedded charts in pixels', - }, - borderRadius: { - control: { type: 'range', min: 0, max: 50, step: 2 }, - description: 'Border radius of chart containers', - }, - }, - parameters: { - docs: { - description: { - component: ` -The Cartodiagram plugin displays embedded charts on a map using OpenLayers. -Each GeoJSON point location gets a small chart (pie, bar, etc.) rendered on it. - -### Features -- OpenLayers map with configurable base layers (OSM, WMS, WFS, XYZ) -- Embedded Superset charts at GeoJSON point locations -- Configurable chart size per zoom level -- Chart background color and border radius customization - -### How It Works -1. Data includes a geometry column with GeoJSON Point strings -2. The plugin groups data by location -3. For each location, it calls the embedded chart's transformProps -4. Mini charts are rendered as OpenLayers overlays - `, - }, - }, - }, -}; - -export const BasicMap = ({ - width, - height, - donut, - showLabels, - colorScheme, - chartWidth, - chartHeight, - borderRadius, -}: { - width: number; - height: number; - donut: boolean; - showLabels: boolean; - colorScheme: string; - chartWidth: number; - chartHeight: number; - borderRadius: number; -}) => ( - -); - -BasicMap.parameters = { - initialSize: { - width: 800, - height: 600, - }, - docs: { - description: { - story: ` -Shows an OpenLayers map with embedded pie charts at San Francisco and Los Angeles. -Each pie chart shows the breakdown of revenue by industry sector for that city. - -The map uses OpenStreetMap tiles as the base layer. - `, - }, - }, -}; diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/test/index.test.ts b/superset-frontend/plugins/plugin-chart-cartodiagram/test/index.test.ts deleted file mode 100644 index 24931495640..00000000000 --- a/superset-frontend/plugins/plugin-chart-cartodiagram/test/index.test.ts +++ /dev/null @@ -1,33 +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 { CartodiagramPlugin } from '../src'; - -/** - * The example tests in this file act as a starting point, and - * we encourage you to build more. These tests check that the - * plugin loads properly, and focus on `transformProps` - * to ake sure that data, controls, and props are all - * treated correctly (e.g. formData from plugin controls - * properly transform the data and/or any resulting props). - */ -describe('CartodiagramPlugin', () => { - test('exists', () => { - expect(CartodiagramPlugin).toBeDefined(); - }); -}); diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/test/plugin/buildQuery.test.ts b/superset-frontend/plugins/plugin-chart-cartodiagram/test/plugin/buildQuery.test.ts index a4132d25195..2eab1a7a044 100644 --- a/superset-frontend/plugins/plugin-chart-cartodiagram/test/plugin/buildQuery.test.ts +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/test/plugin/buildQuery.test.ts @@ -17,7 +17,7 @@ * under the License. */ import { getChartBuildQueryRegistry } from '@superset-ui/core'; -import buildQuery from '../../src/plugin/buildQuery'; +import { buildQuery } from '../../src/index'; describe('CartodiagramPlugin buildQuery', () => { const selectedChartParams = { diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/test/plugin/index.test.ts b/superset-frontend/plugins/plugin-chart-cartodiagram/test/plugin/index.test.ts index 0abd0c5b121..ce9296de176 100644 --- a/superset-frontend/plugins/plugin-chart-cartodiagram/test/plugin/index.test.ts +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/test/plugin/index.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import CartodiagramPlugin from '../../src/CartodiagramPlugin'; +import { createCartodiagramPlugin as CartodiagramPlugin } from '../../src/index'; describe('CartodiagramPlugin', () => { test('exists', () => { diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/test/plugin/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-cartodiagram/test/plugin/transformProps.test.ts index f24217a5241..4ece422d1d1 100644 --- a/superset-frontend/plugins/plugin-chart-cartodiagram/test/plugin/transformProps.test.ts +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/test/plugin/transformProps.test.ts @@ -19,7 +19,7 @@ import { ChartProps, getChartTransformPropsRegistry } from '@superset-ui/core'; import { supersetTheme } from '@apache-superset/core/theme'; import { LayerConf, MapViewConfigs, ZoomConfigs } from '../../src/types'; -import transformProps from '../../src/plugin/transformProps'; +import { transformProps } from '../../src/index'; import { groupedTimeseriesChartData, groupedTimeseriesLabelMap, diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/tsconfig.json b/superset-frontend/plugins/plugin-chart-cartodiagram/tsconfig.json index 03190ec5bd4..f0bfbba676d 100644 --- a/superset-frontend/plugins/plugin-chart-cartodiagram/tsconfig.json +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/tsconfig.json @@ -20,6 +20,7 @@ "references": [ { "path": "../../packages/superset-core" }, { "path": "../../packages/superset-ui-core" }, - { "path": "../../packages/superset-ui-chart-controls" } + { "path": "../../packages/superset-ui-chart-controls" }, + { "path": "../../packages/superset-ui-glyph-core" } ] } diff --git a/superset-frontend/plugins/plugin-chart-echarts/package.json b/superset-frontend/plugins/plugin-chart-echarts/package.json index eb68a240a31..30fd233f8eb 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/package.json +++ b/superset-frontend/plugins/plugin-chart-echarts/package.json @@ -35,6 +35,7 @@ "@apache-superset/core": "*", "@superset-ui/chart-controls": "*", "@superset-ui/core": "*", + "@superset-ui/glyph-core": "*", "dayjs": "^1.11.19", "echarts": "*", "memoize-one": "*", diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberGlyph/index.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberGlyph/index.tsx new file mode 100644 index 00000000000..dbedf4f286e --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberGlyph/index.tsx @@ -0,0 +1,257 @@ +/** + * 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. + */ + +/** + * BigNumber Glyph - Single-File Visualization Plugin + * + * This is the Glyph pattern implementation of BigNumber: + * - Arguments define BOTH the controls AND render props + * - No controlPanel.ts, transformProps.ts, or buildQuery.ts needed + * - Just define arguments + render function = complete plugin + * + * Feature parity with BigNumberTotal: + * - Number formatting (D3 formats) + * - Currency formatting + * - Time/date formatting + * - Conditional color formatting + * - Metric name display + * - Subtitle display + */ + +import { t } from '@apache-superset/core/translation'; +import { styled } from '@apache-superset/core/theme'; +import { + Behavior, + getNumberFormatter, + CurrencyFormatter, + getTimeFormatter, + DataRecord, +} from '@superset-ui/core'; +import { + getColorFormatters, + ConditionalFormattingConfig, +} from '@superset-ui/chart-controls'; + +import { + defineChart, + Metric, + NumberFormat, + Currency, + TimeFormat, + ConditionalFormatting, + // Presets - reusable argument configurations + HeaderFontSize, + SubheaderFontSize, + Subtitle, + ForceTimestampFormatting, + ShowMetricName, + MetricNameFontSize, +} from '@superset-ui/glyph-core'; + +import thumbnail from '../BigNumberTotal/images/thumbnail.png'; +import example1 from '../BigNumberTotal/images/BigNumber.jpg'; +import example1Dark from '../BigNumberTotal/images/BigNumber-dark.jpg'; +import example2 from '../BigNumberTotal/images/BigNumber2.jpg'; +import example2Dark from '../BigNumberTotal/images/BigNumber2-dark.jpg'; + +// ============================================================================ +// Styled components for the chart +// ============================================================================ + +const Container = styled.div<{ height: number }>` + ${({ theme, height }) => ` + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + height: ${height}px; + padding: ${theme.sizeUnit * 4}px; + font-family: ${theme.fontFamily}; + `} +`; + +const BigNumberText = styled.div<{ fontSize: number; color?: string }>` + ${({ theme, fontSize, color }) => ` + font-size: ${fontSize}px; + font-weight: ${theme.fontWeightNormal}; + line-height: 1; + color: ${color || theme.colorText}; + `} +`; + +const MetricName = styled.div<{ fontSize: number }>` + ${({ theme, fontSize }) => ` + font-size: ${fontSize}px; + color: ${theme.colorTextTertiary}; + margin-bottom: ${theme.sizeUnit * 2}px; + `} +`; + +const SubtitleText = styled.div<{ fontSize: number }>` + ${({ theme, fontSize }) => ` + font-size: ${fontSize}px; + color: ${theme.colorTextTertiary}; + margin-top: ${theme.sizeUnit * 2}px; + `} +`; + +// ============================================================================ +// THE CHART DEFINITION - This is ALL you need! +// ============================================================================ + +export default defineChart({ + metadata: { + name: t('Big Number'), + description: t( + 'Showcases a single metric front-and-center. Big number is best used to call ' + + 'attention to a KPI or the one thing you want your audience to focus on.', + ), + category: t('KPI'), + tags: [ + t('Additive'), + t('Business'), + t('Percentages'), + t('Featured'), + t('Report'), + ], + thumbnail, + behaviors: [Behavior.DrillToDetail], + exampleGallery: [ + { url: example1, urlDark: example1Dark, caption: t('A Big Number') }, + { url: example2, urlDark: example2Dark, caption: t('With a subheader') }, + ], + }, + + // Arguments define BOTH the control panel AND the props passed to render + // NOTE: Use camelCase - Superset converts snake_case to camelCase in formData + arguments: { + metric: Metric.with({ label: t('Metric') }), + + headerFontSize: HeaderFontSize, + + subtitle: Subtitle, + + subtitleFontSize: SubheaderFontSize, + + showMetricName: ShowMetricName, + + metricNameFontSize: { + arg: MetricNameFontSize, + visibleWhen: { showMetricName: true }, + }, + + numberFormat: NumberFormat, + + currencyFormat: Currency, + + timeFormat: TimeFormat, + + forceTimestampFormatting: ForceTimestampFormatting, + + conditionalFormatting: ConditionalFormatting, + }, + + // The render function receives argument values directly - no transformation needed! + render: ({ + metric, + headerFontSize, + subtitle, + subtitleFontSize, + showMetricName, + metricNameFontSize, + numberFormat, + currencyFormat, + timeFormat, + forceTimestampFormatting, + conditionalFormatting, + height, + data, + theme, + }) => { + // Determine if we should use time formatting + // In a real implementation, we'd check the column type from the data + const useTimeFormat = forceTimestampFormatting; + + // Create formatter based on format type + let formattedValue: string; + if (useTimeFormat && metric.value != null) { + const timeFormatter = getTimeFormatter(timeFormat); + formattedValue = timeFormatter(metric.value as Date); + } else if (currencyFormat?.symbol) { + const formatter = new CurrencyFormatter({ + currency: { + symbol: currencyFormat.symbol, + symbolPosition: currencyFormat.symbolPosition ?? 'prefix', + }, + d3Format: numberFormat, + }); + formattedValue = + metric.value != null ? formatter(metric.value as number) : t('No data'); + } else { + const formatter = getNumberFormatter(numberFormat); + formattedValue = + metric.value != null ? formatter(metric.value as number) : t('No data'); + } + + // Calculate conditional formatting color + let numberColor: string | undefined; + if ( + conditionalFormatting && + conditionalFormatting.length > 0 && + metric.value != null + ) { + const colorFormatters = getColorFormatters( + conditionalFormatting as ConditionalFormattingConfig[], + data as DataRecord[], + theme as Record, + false, + ); + if (colorFormatters) { + for (const formatter of colorFormatters) { + const color = formatter.getColorFromValue(metric.value as number); + if (color) { + numberColor = color; + break; + } + } + } + } + + // Calculate font sizes based on container height + const headerFontPx = Math.floor(height * (headerFontSize as number)); + const subtitleFontPx = Math.floor(height * (subtitleFontSize as number)); + const metricNameFontPx = Math.floor( + height * (metricNameFontSize as number), + ); + + return ( + + {showMetricName && metric.name && ( + {metric.name} + )} + + {formattedValue} + + {subtitle && ( + {subtitle} + )} + + ); + }, +}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/buildQuery.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/buildQuery.ts deleted file mode 100644 index e230097e266..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/buildQuery.ts +++ /dev/null @@ -1,76 +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 { - buildQueryContext, - QueryFormData, - PostProcessingRule, - ensureIsArray, -} 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; - - const queryContextA = buildQueryContext(formData, baseQueryObject => { - const postProcessing: PostProcessingRule[] = []; - postProcessing.push(timeCompareOperator(formData, baseQueryObject)); - - const nonCustomNorInheritShifts = ensureIsArray( - formData.time_compare, - ).filter((shift: string) => shift !== 'custom' && shift !== 'inherit'); - const customOrInheritShifts = ensureIsArray(formData.time_compare).filter( - (shift: string) => shift === 'custom' || shift === 'inherit', - ); - - let timeOffsets: string[] = []; - - // Shifts for non-custom or non inherit time comparison - if (!isEmpty(nonCustomNorInheritShifts)) { - timeOffsets = nonCustomNorInheritShifts; - } - - // Shifts for custom or inherit time comparison - if (!isEmpty(customOrInheritShifts)) { - if (customOrInheritShifts.includes('custom')) { - timeOffsets = timeOffsets.concat([formData.start_date_offset]); - } - if (customOrInheritShifts.includes('inherit')) { - timeOffsets = timeOffsets.concat(['inherit']); - } - } - return [ - { - ...baseQueryObject, - groupby, - post_processing: postProcessing, - time_offsets: isTimeComparison(formData, baseQueryObject) - ? ensureIsArray(timeOffsets) - : [], - }, - ]; - }); - - return { - ...queryContextA, - }; -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/controlPanel.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/controlPanel.ts deleted file mode 100644 index 9da2cfa8e2e..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/controlPanel.ts +++ /dev/null @@ -1,182 +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 { t } from '@apache-superset/core/translation'; -import { GenericDataType } from '@apache-superset/core/common'; -import { - ControlPanelConfig, - getStandardizedControls, - sharedControls, - sections, - ColorSchemeEnum, -} from '@superset-ui/chart-controls'; -import { noop } from 'lodash'; -import { - headerFontSize, - subheaderFontSize, - subtitleControl, - subtitleFontSize, - showMetricNameControl, - metricNameFontSizeWithVisibility, -} from '../sharedControls'; - -const config: ControlPanelConfig = { - controlPanelSections: [ - { - label: t('Query'), - expanded: true, - controlSetRows: [ - ['metric'], - ['adhoc_filters'], - [ - { - name: 'row_limit', - config: sharedControls.row_limit, - }, - ], - ], - }, - { - label: t('Chart Options'), - expanded: true, - controlSetRows: [ - ['y_axis_format'], - [ - { - name: 'percentDifferenceFormat', - config: { - ...sharedControls.y_axis_format, - label: t('Percent Difference format'), - }, - }, - ], - ['currency_format'], - [ - { - ...headerFontSize, - config: { ...headerFontSize.config, default: 0.2 }, - }, - ], - [subtitleControl], - [subtitleFontSize], - [showMetricNameControl], - [metricNameFontSizeWithVisibility], - [ - { - ...subheaderFontSize, - config: { - ...subheaderFontSize.config, - default: 0.125, - label: t('Comparison font size'), - }, - }, - ], - [ - { - name: 'comparison_color_enabled', - config: { - type: 'CheckboxControl', - label: t('Add color for positive/negative change'), - renderTrigger: true, - default: false, - description: t('Add color for positive/negative change'), - }, - }, - ], - [ - { - name: 'comparison_color_scheme', - config: { - type: 'SelectControl', - label: t('color scheme for comparison'), - default: ColorSchemeEnum.Green, - renderTrigger: true, - choices: [ - [ColorSchemeEnum.Green, 'Green for increase, red for decrease'], - [ColorSchemeEnum.Red, 'Red for increase, green for decrease'], - ], - visibility: ({ controls }) => - controls?.comparison_color_enabled?.value === true, - description: t( - 'Adds color to the chart symbols based on the positive or ' + - 'negative change from the comparison value.', - ), - }, - }, - ], - [ - { - name: 'column_config', - config: { - type: 'ColumnConfigControl', - label: t('Customize columns'), - description: t('Further customize how to display each column'), - width: 400, - height: 320, - renderTrigger: true, - configFormLayout: { - [GenericDataType.Numeric]: [ - { - tab: t('General'), - children: [ - ['customColumnName'], - ['displayTypeIcon'], - ['visible'], - ], - }, - ], - }, - shouldMapStateToProps() { - return true; - }, - mapStateToProps(explore, _, chart) { - noop(explore, _, chart); - return { - columnsPropsObject: { - colnames: ['Previous value', 'Delta', 'Percent change'], - coltypes: [ - GenericDataType.Numeric, - GenericDataType.Numeric, - GenericDataType.Numeric, - ], - }, - }; - }, - }, - }, - ], - ], - }, - sections.timeComparisonControls({ - multi: false, - showCalculationType: false, - showFullChoices: false, - }), - ], - controlOverrides: { - y_axis_format: { - label: t('Number format'), - }, - }, - formDataOverrides: formData => ({ - ...formData, - metric: getStandardizedControls().shiftMetric(), - }), -}; - -export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/index.ts deleted file mode 100644 index 77872ac1bc6..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/index.ts +++ /dev/null @@ -1,54 +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 { t } from '@apache-superset/core/translation'; -import { ChartMetadata, ChartPlugin } from '@superset-ui/core'; -import buildQuery from './buildQuery'; -import controlPanel from './controlPanel'; -import transformProps from './transformProps'; -import thumbnail from './images/thumbnail.png'; -import thumbnailDark from './images/thumbnail-dark.png'; - -export default class PopKPIPlugin extends ChartPlugin { - constructor() { - const metadata = new ChartMetadata({ - category: t('KPI'), - description: - 'Showcases a metric along with a comparison of value, change, and percent change for a selected time period.', - name: t('Big Number with Time Period Comparison'), - tags: [ - t('Comparison'), - t('Business'), - t('ECharts'), - t('Percentages'), - t('Report'), - t('Advanced-Analytics'), - ], - thumbnail, - thumbnailDark, - }); - - super({ - buildQuery, - controlPanel, - loadChart: () => import('./PopKPI'), - metadata, - transformProps, - }); - } -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/index.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/index.tsx new file mode 100644 index 00000000000..841b8a443ea --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/index.tsx @@ -0,0 +1,474 @@ +/** + * 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. + */ + +/** + * Big Number with Time Period Comparison - Glyph Pattern Implementation + * + * Showcases a metric along with a comparison of value, change, and percent + * change for a selected time period. + */ + +// Type augmentation for dayjs plugins +import 'dayjs/plugin/utc'; +import { t } from '@apache-superset/core/translation'; +import { + buildQueryContext, + ChartProps, + ensureIsArray, + getMetricLabel, + getNumberFormatter, + getValueFormatter, + PostProcessingRule, + QueryFormData, + SimpleAdhocFilter, +} from '@superset-ui/core'; +import { GenericDataType } from '@apache-superset/core/common'; +import { + getStandardizedControls, + isTimeComparison, + Metric, + sections, + sharedControls, + timeCompareOperator, + ColorSchemeEnum, +} from '@superset-ui/chart-controls'; +import { noop, isEmpty } from 'lodash'; +import { extendedDayjs as dayjs } from '@superset-ui/core/utils/dates'; + +import { defineChart } from '@superset-ui/glyph-core'; + +import { + headerFontSize, + subheaderFontSize, + subtitleControl, + subtitleFontSize, + showMetricNameControl, + metricNameFontSizeWithVisibility, +} from '../sharedControls'; +import { getOriginalLabel } from '../utils'; +import PopKPI from './PopKPI'; +import { FontSizeOptions, PopKPIProps } from './types'; +import { + getComparisonFontSize, + getHeaderFontSize, + getMetricNameFontSize, +} from './utils'; + +import thumbnail from './images/thumbnail.png'; +import thumbnailDark from './images/thumbnail-dark.png'; + +// ============================================================================ +// Build Query +// ============================================================================ + +function buildQuery(formData: QueryFormData) { + const { cols: groupby } = formData; + + const queryContextA = buildQueryContext(formData, baseQueryObject => { + const postProcessing: PostProcessingRule[] = []; + postProcessing.push(timeCompareOperator(formData, baseQueryObject)); + + const nonCustomNorInheritShifts = ensureIsArray( + formData.time_compare, + ).filter((shift: string) => shift !== 'custom' && shift !== 'inherit'); + const customOrInheritShifts = ensureIsArray(formData.time_compare).filter( + (shift: string) => shift === 'custom' || shift === 'inherit', + ); + + let timeOffsets: string[] = []; + + if (!isEmpty(nonCustomNorInheritShifts)) { + timeOffsets = nonCustomNorInheritShifts; + } + + if (!isEmpty(customOrInheritShifts)) { + if (customOrInheritShifts.includes('custom')) { + timeOffsets = timeOffsets.concat([formData.start_date_offset]); + } + if (customOrInheritShifts.includes('inherit')) { + timeOffsets = timeOffsets.concat(['inherit']); + } + } + return [ + { + ...baseQueryObject, + groupby, + post_processing: postProcessing, + time_offsets: isTimeComparison(formData, baseQueryObject) + ? ensureIsArray(timeOffsets) + : [], + }, + ]; + }); + + return { + ...queryContextA, + }; +} + +// ============================================================================ +// Transform +// ============================================================================ + +const parseMetricValue = (metricValue: number | string | null) => { + if (typeof metricValue === 'string') { + const dateObject = dayjs.utc(metricValue, undefined, true); + if (dateObject.isValid()) { + return dateObject.valueOf(); + } + return 0; + } + return metricValue ?? 0; +}; + +interface PopKPITransformResult { + popKPIProps: PopKPIProps; +} + +function transformPopKPI(chartProps: ChartProps): PopKPIProps { + const { + width, + height, + formData, + queriesData, + datasource: { currencyFormats = {}, columnFormats = {} }, + } = chartProps; + const { + boldText, + headerFontSize, + headerText, + metric, + metricNameFontSize, + yAxisFormat, + currencyFormat, + subheaderFontSize, + comparisonColorScheme, + comparisonColorEnabled, + percentDifferenceFormat, + subtitle = '', + subtitleFontSize, + columnConfig = {}, + } = formData; + const { data: dataA = [] } = queriesData[0]; + const data = dataA; + const metricName = metric ? getMetricLabel(metric) : ''; + const metrics = chartProps.datasource?.metrics || []; + const originalLabel = getOriginalLabel(metric, metrics); + const showMetricName = chartProps.rawFormData?.show_metric_name ?? false; + 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]; + + let metricEntry: Metric | undefined; + if (chartProps.datasource?.metrics) { + metricEntry = chartProps.datasource.metrics.find( + metricItem => metricItem.metric_name === metric, + ); + } + + const isCustomOrInherit = + timeComparison === 'custom' || timeComparison === 'inherit'; + let dataOffset: string[] = []; + if (isCustomOrInherit) { + if (timeComparison && timeComparison === 'custom') { + dataOffset = [startDateOffset]; + } else { + dataOffset = ensureIsArray(timeComparison) || []; + } + } + + const { value1, value2 } = data.reduce( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (acc: { value1: number; value2: number }, curr: { [x: string]: any }) => { + Object.keys(curr).forEach(key => { + if ( + key.includes( + `${metricName}__${ + !isCustomOrInherit ? timeComparison : dataOffset[0] + }`, + ) + ) { + acc.value2 += curr[key]; + } else if (key.includes(metricName)) { + acc.value1 += curr[key]; + } + }); + return acc; + }, + { value1: 0, value2: 0 }, + ); + + let bigNumber: number | string = + data.length === 0 ? 0 : parseMetricValue(value1); + let prevNumber: number | string = + data.length === 0 ? 0 : parseMetricValue(value2); + + const numberFormatter = getValueFormatter( + metric, + currencyFormats, + columnFormats, + metricEntry?.d3format || yAxisFormat, + currencyFormat, + ); + + const compTitles = { + r: 'Range' as string, + y: 'Year' as string, + m: 'Month' as string, + w: 'Week' as string, + }; + + const formatPercentChange = getNumberFormatter(percentDifferenceFormat); + + let valueDifference: number | string = bigNumber - prevNumber; + + let percentDifferenceNum; + + if (!bigNumber && !prevNumber) { + percentDifferenceNum = 0; + } else if (!bigNumber || !prevNumber) { + percentDifferenceNum = bigNumber ? 1 : -1; + } else { + percentDifferenceNum = (bigNumber - prevNumber) / Math.abs(prevNumber); + } + + const compType = + compTitles[formData.timeComparison as keyof typeof compTitles]; + bigNumber = numberFormatter(bigNumber); + prevNumber = numberFormatter(prevNumber); + valueDifference = numberFormatter(valueDifference); + const percentDifference: string = formatPercentChange(percentDifferenceNum); + + return { + width, + height, + data, + metrics, + metricName: originalLabel, + bigNumber, + prevNumber, + valueDifference, + percentDifferenceFormattedString: percentDifference, + boldText, + subtitle, + subtitleFontSize, + showMetricName, + metricNameFontSize: getMetricNameFontSize(metricNameFontSize), + headerFontSize: getHeaderFontSize( + headerFontSize, + ) as unknown as FontSizeOptions, + subheaderFontSize: getComparisonFontSize( + subheaderFontSize, + ) as unknown as FontSizeOptions, + headerText, + compType, + comparisonColorEnabled, + comparisonColorScheme, + percentDifferenceNumber: percentDifferenceNum, + currentTimeRangeFilter, + startDateOffset, + shift: timeComparison, + dashboardTimeRange: formData?.extraFormData?.time_range, + columnConfig, + }; +} + +// ============================================================================ +// Chart Definition +// ============================================================================ + +export default defineChart< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + any, + PopKPITransformResult +>({ + metadata: { + name: t('Big Number with Time Period Comparison'), + description: t( + 'Showcases a metric along with a comparison of value, change, and percent change for a selected time period.', + ), + category: t('KPI'), + tags: [ + t('Comparison'), + t('Business'), + t('ECharts'), + t('Percentages'), + t('Report'), + t('Advanced-Analytics'), + ], + thumbnail, + thumbnailDark, + }, + + arguments: {}, + + additionalControls: { + query: [ + ['metric'], + ['adhoc_filters'], + [ + { + name: 'row_limit', + config: sharedControls.row_limit, + }, + ], + ], + chartOptions: [ + ['y_axis_format'], + [ + { + name: 'percentDifferenceFormat', + config: { + ...sharedControls.y_axis_format, + label: t('Percent Difference format'), + }, + }, + ], + ['currency_format'], + [ + { + ...headerFontSize, + config: { ...headerFontSize.config, default: 0.2 }, + }, + ], + [subtitleControl], + [subtitleFontSize], + [showMetricNameControl], + [metricNameFontSizeWithVisibility], + [ + { + ...subheaderFontSize, + config: { + ...subheaderFontSize.config, + default: 0.125, + label: t('Comparison font size'), + }, + }, + ], + [ + { + name: 'comparison_color_enabled', + config: { + type: 'CheckboxControl', + label: t('Add color for positive/negative change'), + renderTrigger: true, + default: false, + description: t('Add color for positive/negative change'), + }, + }, + ], + [ + { + name: 'comparison_color_scheme', + config: { + type: 'SelectControl', + label: t('color scheme for comparison'), + default: ColorSchemeEnum.Green, + renderTrigger: true, + choices: [ + [ColorSchemeEnum.Green, 'Green for increase, red for decrease'], + [ColorSchemeEnum.Red, 'Red for increase, green for decrease'], + ], + visibility: ({ + controls, + }: { + controls?: Record; + }) => controls?.comparison_color_enabled?.value === true, + description: t( + 'Adds color to the chart symbols based on the positive or ' + + 'negative change from the comparison value.', + ), + }, + }, + ], + [ + { + name: 'column_config', + config: { + type: 'ColumnConfigControl', + label: t('Customize columns'), + description: t('Further customize how to display each column'), + width: 400, + height: 320, + renderTrigger: true, + configFormLayout: { + [GenericDataType.Numeric]: [ + { + tab: t('General'), + children: [ + ['customColumnName'], + ['displayTypeIcon'], + ['visible'], + ], + }, + ], + }, + shouldMapStateToProps() { + return true; + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mapStateToProps(explore: any, _: any, chart: any) { + noop(explore, _, chart); + return { + columnsPropsObject: { + colnames: ['Previous value', 'Delta', 'Percent change'], + coltypes: [ + GenericDataType.Numeric, + GenericDataType.Numeric, + GenericDataType.Numeric, + ], + }, + }; + }, + }, + }, + ], + ], + }, + + additionalSections: [ + sections.timeComparisonControls({ + multi: false, + showCalculationType: false, + showFullChoices: false, + }), + ], + + controlOverrides: { + y_axis_format: { + label: t('Number format'), + }, + }, + + formDataOverrides: formData => ({ + ...formData, + metric: getStandardizedControls().shiftMetric(), + }), + + buildQuery, + + transform: (chartProps: ChartProps): PopKPITransformResult => ({ + popKPIProps: transformPopKPI(chartProps), + }), + + render: ({ popKPIProps }) => , +}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/transformProps.ts deleted file mode 100644 index b610d5b35de..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberPeriodOverPeriod/transformProps.ts +++ /dev/null @@ -1,231 +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. - */ -// Type augmentation for dayjs plugins -import 'dayjs/plugin/utc'; -import { Metric } from '@superset-ui/chart-controls'; -import { - ChartProps, - getMetricLabel, - getValueFormatter, - getNumberFormatter, - SimpleAdhocFilter, - ensureIsArray, -} from '@superset-ui/core'; -import { extendedDayjs as dayjs } from '@superset-ui/core/utils/dates'; -import { - getComparisonFontSize, - getHeaderFontSize, - getMetricNameFontSize, -} from './utils'; - -import { getOriginalLabel } from '../utils'; - -export const parseMetricValue = (metricValue: number | string | null) => { - if (typeof metricValue === 'string') { - const dateObject = dayjs.utc(metricValue, undefined, true); - if (dateObject.isValid()) { - return dateObject.valueOf(); - } - return 0; - } - return metricValue ?? 0; -}; - -export default function transformProps(chartProps: ChartProps) { - /** - * This function is called after a successful response has been - * received from the chart data endpoint, and is used to transform - * the incoming data prior to being sent to the Visualization. - * - * The transformProps function is also quite useful to return - * additional/modified props to your data viz component. The formData - * can also be accessed from your CustomViz.tsx file, but - * doing supplying custom props here is often handy for integrating third - * party libraries that rely on specific props. - * - * A description of properties in `chartProps`: - * - `height`, `width`: the height/width of the DOM element in which - * the chart is located - * - `formData`: the chart data request payload that was sent to the - * backend. - * - `queriesData`: the chart data response payload that was received - * from the backend. Some notable properties of `queriesData`: - * - `data`: an array with data, each row with an object mapping - * the column/alias to its value. Example: - * `[{ col1: 'abc', metric1: 10 }, { col1: 'xyz', metric1: 20 }]` - * - `rowcount`: the number of rows in `data` - * - `query`: the query that was issued. - * - * Please note: the transformProps function gets cached when the - * application loads. When making changes to the `transformProps` - * function during development with hot reloading, changes won't - * be seen until restarting the development server. - */ - const { - width, - height, - formData, - queriesData, - datasource: { - currencyFormats = {}, - columnFormats = {}, - currencyCodeColumn, - }, - } = chartProps; - const { - boldText, - headerFontSize, - headerText, - metric, - metricNameFontSize, - yAxisFormat, - currencyFormat, - subheaderFontSize, - comparisonColorScheme, - comparisonColorEnabled, - percentDifferenceFormat, - subtitle = '', - subtitleFontSize, - columnConfig = {}, - } = formData; - const { data: dataA = [], detected_currency: detectedCurrency } = - queriesData[0] || {}; - const data = dataA; - const metricName = metric ? getMetricLabel(metric) : ''; - const metrics = chartProps.datasource?.metrics || []; - const originalLabel = getOriginalLabel(metric, metrics); - const showMetricName = chartProps.rawFormData?.show_metric_name ?? false; - 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]; - - let metricEntry: Metric | undefined; - if (chartProps.datasource?.metrics) { - metricEntry = chartProps.datasource.metrics.find( - metricItem => metricItem.metric_name === metric, - ); - } - - const isCustomOrInherit = - timeComparison === 'custom' || timeComparison === 'inherit'; - let dataOffset: string[] = []; - if (isCustomOrInherit) { - if (timeComparison && timeComparison === 'custom') { - dataOffset = [startDateOffset]; - } else { - dataOffset = ensureIsArray(timeComparison) || []; - } - } - - const { value1, value2 } = data.reduce( - (acc: { value1: number; value2: number }, curr: { [x: string]: any }) => { - Object.keys(curr).forEach(key => { - if ( - key.includes( - `${metricName}__${ - !isCustomOrInherit ? timeComparison : dataOffset[0] - }`, - ) - ) { - acc.value2 += curr[key]; - } else if (key.includes(metricName)) { - acc.value1 += curr[key]; - } - }); - return acc; - }, - { value1: 0, value2: 0 }, - ); - - let bigNumber: number | string = - data.length === 0 ? 0 : parseMetricValue(value1); - let prevNumber: number | string = - data.length === 0 ? 0 : parseMetricValue(value2); - - const numberFormatter = getValueFormatter( - metric, - currencyFormats, - columnFormats, - metricEntry?.d3format || yAxisFormat, - currencyFormat, - undefined, - data, - currencyCodeColumn, - detectedCurrency, - ); - - const compTitles = { - r: 'Range' as string, - y: 'Year' as string, - m: 'Month' as string, - w: 'Week' as string, - }; - - const formatPercentChange = getNumberFormatter(percentDifferenceFormat); - - let valueDifference: number | string = bigNumber - prevNumber; - - let percentDifferenceNum; - - if (!bigNumber && !prevNumber) { - percentDifferenceNum = 0; - } else if (!bigNumber || !prevNumber) { - percentDifferenceNum = bigNumber ? 1 : -1; - } else { - percentDifferenceNum = (bigNumber - prevNumber) / Math.abs(prevNumber); - } - - const compType = - compTitles[formData.timeComparison as keyof typeof compTitles]; - bigNumber = numberFormatter(bigNumber); - prevNumber = numberFormatter(prevNumber); - valueDifference = numberFormatter(valueDifference); - const percentDifference: string = formatPercentChange(percentDifferenceNum); - - return { - width, - height, - data, - metricName: originalLabel, - bigNumber, - prevNumber, - valueDifference, - percentDifferenceFormattedString: percentDifference, - boldText, - subtitle, - subtitleFontSize, - showMetricName, - metricNameFontSize: getMetricNameFontSize(metricNameFontSize), - headerFontSize: getHeaderFontSize(headerFontSize), - subheaderFontSize: getComparisonFontSize(subheaderFontSize), - headerText, - compType, - comparisonColorEnabled, - comparisonColorScheme, - percentDifferenceNumber: percentDifferenceNum, - currentTimeRangeFilter, - startDateOffset, - shift: timeComparison, - dashboardTimeRange: formData?.extraFormData?.time_range, - columnConfig, - }; -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/buildQuery.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/buildQuery.ts deleted file mode 100644 index e714898b024..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/buildQuery.ts +++ /dev/null @@ -1,23 +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 { buildQueryContext, QueryFormData } from '@superset-ui/core'; - -export default function buildQuery(formData: QueryFormData) { - return buildQueryContext(formData, baseQueryObject => [baseQueryObject]); -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/controlPanel.test.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/controlPanel.test.ts deleted file mode 100644 index 7d8e74156d4..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/controlPanel.test.ts +++ /dev/null @@ -1,97 +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 { SqlaFormData } from '@superset-ui/core'; -import * as ChartControls from '@superset-ui/chart-controls'; -import controlPanel from './controlPanel'; - -const { __mockShiftMetric } = ChartControls as any; - -jest.mock('@superset-ui/core', () => ({ - GenericDataType: { Numeric: 'numeric' }, - SMART_DATE_ID: 'SMART_DATE_ID', - t: (str: string) => str, -})); - -jest.mock('@superset-ui/chart-controls', () => { - // Define the mock function inside the factory - const mockShiftMetric = jest.fn(() => 'shiftedMetric'); - return { - ControlPanelConfig: {}, - D3_FORMAT_DOCS: 'Format docs', - D3_TIME_FORMAT_OPTIONS: [['', 'default']], - getStandardizedControls: () => ({ - shiftMetric: mockShiftMetric, - }), - // Optional export to let tests access the mock - __mockShiftMetric: mockShiftMetric, - }; -}); - -describe('BigNumber Total Control Panel Config', () => { - test('should have the required control panel sections', () => { - expect(controlPanel).toHaveProperty('controlPanelSections'); - const sections = controlPanel.controlPanelSections; - expect(Array.isArray(sections)).toBe(true); - expect(sections.length).toBe(2); - - // First section should have label 'Query' and contain rows with metric and adhoc_filters - expect(sections[0]!.label).toBe('Query'); - expect(Array.isArray(sections[0]!.controlSetRows)).toBe(true); - expect(sections[0]!.controlSetRows[0]).toEqual(['metric']); - expect(sections[0]!.controlSetRows[1]).toEqual(['adhoc_filters']); - - // Second section should contain a control named subtitle - const secondSectionRow = sections[1]!.controlSetRows[1]; - expect(secondSectionRow[0]).toHaveProperty('name', 'subtitle'); - - // Second section should include controls for time_format and conditional_formatting - const thirdSection = sections[1]!.controlSetRows; - // Check time_format control exists in one of the rows - const timeFormatRow = thirdSection.find(row => - row.some((control: any) => control.name === 'time_format'), - ); - expect(timeFormatRow).toBeTruthy(); - // Check conditional_formatting control exists in one of the rows - const conditionalFormattingRow = thirdSection.find(row => - row.some((control: any) => control.name === 'conditional_formatting'), - ); - expect(conditionalFormattingRow).toBeTruthy(); - }); - - test('should have y_axis_format override with correct label', () => { - expect(controlPanel).toHaveProperty('controlOverrides'); - expect(controlPanel.controlOverrides).toHaveProperty('y_axis_format'); - expect(controlPanel.controlOverrides!.y_axis_format!.label).toBe( - 'Number format', - ); - }); - - test('should override formData metric using getStandardizedControls', () => { - const dummyFormData = { someProp: 'test' } as unknown as SqlaFormData; - const newFormData = controlPanel.formDataOverrides!(dummyFormData); - - // The original properties are spread correctly. - expect(newFormData.someProp).toBe('test'); - // The metric property should be replaced by the output of shiftMetric. - expect(newFormData.metric).toBe('shiftedMetric'); - - // Ensure that the mockShiftMetric function was called. - expect(__mockShiftMetric).toHaveBeenCalled(); - }); -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/controlPanel.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/controlPanel.ts deleted file mode 100644 index d926f06de01..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/controlPanel.ts +++ /dev/null @@ -1,139 +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 { t } from '@apache-superset/core/translation'; -import { SMART_DATE_ID } from '@superset-ui/core'; -import { GenericDataType } from '@apache-superset/core/common'; -import { - ControlPanelConfig, - D3_FORMAT_DOCS, - D3_TIME_FORMAT_OPTIONS, - Dataset, - getStandardizedControls, -} from '@superset-ui/chart-controls'; -import { - headerFontSize, - subtitleFontSize, - subtitleControl, - showMetricNameControl, - metricNameFontSizeWithVisibility, -} from '../sharedControls'; - -export default { - controlPanelSections: [ - { - label: t('Query'), - expanded: true, - controlSetRows: [['metric'], ['adhoc_filters']], - }, - { - label: t('Chart Options'), - expanded: true, - controlSetRows: [ - [headerFontSize], - [subtitleControl], - [subtitleFontSize], - [showMetricNameControl], - [metricNameFontSizeWithVisibility], - ['y_axis_format'], - ['currency_format'], - [ - { - name: 'time_format', - config: { - type: 'SelectControl', - freeForm: true, - label: t('Date format'), - renderTrigger: true, - choices: D3_TIME_FORMAT_OPTIONS, - description: D3_FORMAT_DOCS, - default: SMART_DATE_ID, - }, - }, - ], - [ - { - name: 'force_timestamp_formatting', - config: { - type: 'CheckboxControl', - label: t('Force date format'), - renderTrigger: true, - default: false, - description: t( - 'Use date formatting even when metric value is not a timestamp', - ), - }, - }, - ], - [ - { - name: 'conditional_formatting', - config: { - type: 'ConditionalFormattingControl', - renderTrigger: true, - label: t('Conditional Formatting'), - description: t('Apply conditional color formatting to metric'), - shouldMapStateToProps() { - return true; - }, - mapStateToProps(explore, _, chart) { - const verboseMap = explore?.datasource?.hasOwnProperty( - 'verbose_map', - ) - ? (explore?.datasource as Dataset)?.verbose_map - : (explore?.datasource?.columns ?? {}); - const { colnames, coltypes } = - chart?.queriesResponse?.[0] ?? {}; - const numericColumns = - Array.isArray(colnames) && Array.isArray(coltypes) - ? colnames - .filter( - (_: string, index: number) => - coltypes[index] === GenericDataType.Numeric, - ) - .map((colname: string | number) => ({ - value: colname, - label: - (Array.isArray(verboseMap) - ? verboseMap[colname as number] - : verboseMap[colname as string]) ?? colname, - dataType: - colnames && coltypes[colnames?.indexOf(colname)], - })) - : []; - return { - columnOptions: numericColumns, - verboseMap, - }; - }, - }, - }, - ], - ], - }, - ], - controlOverrides: { - y_axis_format: { - label: t('Number format'), - }, - }, - formDataOverrides: formData => ({ - ...formData, - metric: getStandardizedControls().shiftMetric(), - }), -} as ControlPanelConfig; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/index.ts deleted file mode 100644 index 7d8458479d1..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/index.ts +++ /dev/null @@ -1,70 +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 { t } from '@apache-superset/core/translation'; -import { Behavior } from '@superset-ui/core'; -import controlPanel from './controlPanel'; -import transformProps from './transformProps'; -import buildQuery from './buildQuery'; -import example1 from './images/BigNumber.jpg'; -import example1Dark from './images/BigNumber-dark.jpg'; -import example2 from './images/BigNumber2.jpg'; -import example2Dark from './images/BigNumber2-dark.jpg'; -import thumbnail from './images/thumbnail.png'; -import thumbnailDark from './images/thumbnail-dark.png'; -import { BigNumberTotalChartProps, BigNumberTotalFormData } from '../types'; -import { EchartsChartPlugin } from '../../types'; - -const metadata = { - category: t('KPI'), - description: t( - 'Showcases a single metric front-and-center. Big number is best used to call attention to a KPI or the one thing you want your audience to focus on.', - ), - exampleGallery: [ - { url: example1, urlDark: example1Dark, caption: t('A Big Number') }, - { url: example2, urlDark: example2Dark, caption: t('With a subheader') }, - ], - name: t('Big Number'), - tags: [ - t('Additive'), - t('Business'), - t('ECharts'), - t('Legacy'), - t('Percentages'), - t('Featured'), - t('Report'), - ], - thumbnail, - thumbnailDark, - behaviors: [Behavior.DrillToDetail], -}; - -export default class BigNumberTotalChartPlugin extends EchartsChartPlugin< - BigNumberTotalFormData, - BigNumberTotalChartProps -> { - constructor() { - super({ - loadChart: () => import('../BigNumberViz'), - metadata, - buildQuery, - transformProps, - controlPanel, - }); - } -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/index.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/index.tsx new file mode 100644 index 00000000000..3098b4f858e --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/index.tsx @@ -0,0 +1,248 @@ +/** + * 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. + */ + +/** + * Big Number (Total) - Glyph Pattern Implementation + * + * Showcases a single metric front-and-center. Best used to call attention + * to a KPI or the one thing you want your audience to focus on. + */ + +import { t } from '@apache-superset/core/translation'; +import { + Behavior, + extractTimegrain, + getMetricLabel, + getValueFormatter, + QueryFormData, +} from '@superset-ui/core'; +import { GenericDataType } from '@apache-superset/core/common'; +import { + ColorFormatters, + getColorFormatters, + getStandardizedControls, +} from '@superset-ui/chart-controls'; + +import { + defineChart, + NumberFormat, + Currency, + TimeFormat, + HeaderFontSize, + SubheaderFontSize, + Subtitle, + ForceTimestampFormatting, + ShowMetricName, + MetricNameFontSize, + ConditionalFormatting, + ChartProps, +} from '@superset-ui/glyph-core'; + +import { getDateFormatter, getOriginalLabel, parseMetricValue } from '../utils'; +import BigNumberViz from '../BigNumberViz'; +import { BigNumberTotalChartProps, BigNumberVizProps } from '../types'; +import { Refs } from '../../types'; + +import example1 from './images/BigNumber.jpg'; +import example1Dark from './images/BigNumber-dark.jpg'; +import example2 from './images/BigNumber2.jpg'; +import example2Dark from './images/BigNumber2-dark.jpg'; +import thumbnail from './images/thumbnail.png'; +import thumbnailDark from './images/thumbnail-dark.png'; + +// ============================================================================ +// Transform +// ============================================================================ + +interface BigNumberTotalTransformResult { + vizProps: BigNumberVizProps; +} + +function transformBigNumberTotal( + chartProps: BigNumberTotalChartProps, +): BigNumberVizProps { + const { + width, + height, + queriesData, + formData, + rawFormData, + hooks, + datasource: { currencyFormats = {}, columnFormats = {} }, + theme, + } = chartProps; + const { + metricNameFontSize, + headerFontSize, + metric = 'value', + subtitle, + subtitleFontSize, + forceTimestampFormatting, + timeFormat, + yAxisFormat, + conditionalFormatting, + currencyFormat, + subheader, + subheaderFontSize, + } = formData; + const refs: Refs = {}; + const { data = [], coltypes = [] } = queriesData[0] || {}; + const granularity = extractTimegrain(rawFormData as QueryFormData); + const metrics = chartProps.datasource?.metrics || []; + const originalLabel = getOriginalLabel(metric, metrics); + const metricName = getMetricLabel(metric); + const showMetricName = chartProps.rawFormData?.show_metric_name ?? false; + const formattedSubtitle = subtitle?.trim() ? subtitle : subheader || ''; + const formattedSubtitleFontSize = subtitle?.trim() + ? (subtitleFontSize ?? 1) + : (subheaderFontSize ?? 1); + const bigNumber = + data.length === 0 ? null : parseMetricValue(data[0][metricName]); + + let metricD3Format: string | undefined; + if (chartProps.datasource?.metrics) { + const metricEntry = chartProps.datasource.metrics.find( + (metricItem: { metric_name?: string }) => + metricItem.metric_name === metric, + ); + metricD3Format = metricEntry?.d3format ?? undefined; + } + + const formatTime = getDateFormatter(timeFormat, granularity, metricD3Format); + + const numberFormatter = getValueFormatter( + metric, + currencyFormats, + columnFormats, + metricD3Format || yAxisFormat, + currencyFormat, + ); + + const headerFormatter = + coltypes[0] === GenericDataType.Temporal || + coltypes[0] === GenericDataType.String || + forceTimestampFormatting + ? formatTime + : numberFormatter; + + const { onContextMenu } = hooks; + + const defaultColorFormatters = [] as ColorFormatters; + + const colorThresholdFormatters = + getColorFormatters(conditionalFormatting, data, theme, false) ?? + defaultColorFormatters; + + return { + width, + height, + bigNumber, + headerFormatter, + headerFontSize, + subheaderFontSize, + subtitleFontSize: formattedSubtitleFontSize, + subtitle: formattedSubtitle, + onContextMenu, + refs, + colorThresholdFormatters, + metricName: originalLabel, + showMetricName, + metricNameFontSize, + }; +} + +// ============================================================================ +// Chart Definition +// ============================================================================ + +export default defineChart< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + any, + BigNumberTotalTransformResult +>({ + metadata: { + name: t('Big Number'), + description: t( + 'Showcases a single metric front-and-center. Big number is best used to call attention to a KPI or the one thing you want your audience to focus on.', + ), + category: t('KPI'), + tags: [ + t('Additive'), + t('Business'), + t('ECharts'), + t('Legacy'), + t('Percentages'), + t('Featured'), + t('Report'), + ], + thumbnail, + thumbnailDark, + behaviors: [Behavior.DrillToDetail], + exampleGallery: [ + { url: example1, urlDark: example1Dark, caption: t('A Big Number') }, + { + url: example2, + urlDark: example2Dark, + caption: t('With a subheader'), + }, + ], + }, + + arguments: { + headerFontSize: HeaderFontSize, + subtitle: Subtitle, + subtitleFontSize: SubheaderFontSize, + showMetricName: ShowMetricName, + metricNameFontSize: { + arg: MetricNameFontSize, + visibleWhen: { showMetricName: true }, + }, + yAxisFormat: NumberFormat.with({ + label: t('Number format'), + }), + currencyFormat: Currency, + timeFormat: TimeFormat, + forceTimestampFormatting: ForceTimestampFormatting, + conditionalFormatting: ConditionalFormatting.with({ + label: t('Conditional Formatting'), + description: t('Apply conditional color formatting to metric'), + }), + }, + + additionalControls: { + query: [['metric'], ['adhoc_filters']], + }, + + controlOverrides: { + y_axis_format: { + label: t('Number format'), + }, + }, + + formDataOverrides: formData => ({ + ...formData, + metric: getStandardizedControls().shiftMetric(), + }), + + transform: (chartProps: ChartProps): BigNumberTotalTransformResult => ({ + vizProps: transformBigNumberTotal(chartProps as BigNumberTotalChartProps), + }), + + render: ({ vizProps }) => , +}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.test.ts deleted file mode 100644 index 2b2f38a0ac2..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.test.ts +++ /dev/null @@ -1,256 +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 { GenericDataType } from '@apache-superset/core/common'; -import { getColorFormatters } from '@superset-ui/chart-controls'; -import { BigNumberTotalChartProps } from '../types'; -import transformProps from './transformProps'; - -jest.mock('@superset-ui/chart-controls', () => ({ - getColorFormatters: jest.fn(), -})); - -jest.mock('@superset-ui/core', () => ({ - GenericDataType: { Temporal: 2, String: 1 }, - getMetricLabel: jest.fn(metric => metric), - extractTimegrain: jest.fn(() => 'P1D'), - getValueFormatter: jest.fn(() => (v: any) => `$${v}`), -})); - -jest.mock('../utils', () => ({ - getDateFormatter: jest.fn(() => (v: any) => `${v}pm`), - parseMetricValue: jest.fn(val => Number(val)), - getOriginalLabel: jest.fn((metric, metrics) => { - console.log(metrics); - return metric; - }), -})); - -describe('BigNumberTotal transformProps', () => { - const onContextMenu = jest.fn(); - const baseFormData = { - headerFontSize: 20, - metric: 'value', - subheader: 'sub header text', - subheaderFontSize: 14, - forceTimestampFormatting: false, - timeFormat: 'YYYY-MM-DD', - yAxisFormat: 'SMART_NUMBER', - conditionalFormatting: [{ color: 'red', op: '>', value: 0 }], - currencyFormat: { symbol: '$', symbolPosition: 'prefix' }, - }; - - const baseDatasource = { - currencyFormats: { value: '$0,0.00' }, - columnFormats: { value: '$0,0.00' }, - metrics: [{ metric_name: 'value', d3format: '.2f' }], - }; - - const baseHooks = { onContextMenu }; - - const baseRawFormData = { dummy: 'raw' }; - - test('should return null bigNumber when no data is provided', () => { - const chartProps = { - width: 400, - height: 300, - queriesData: [{ data: [], coltypes: [] }], - formData: baseFormData, - rawFormData: baseRawFormData, - hooks: baseHooks, - datasource: baseDatasource, - }; - - const result = transformProps( - chartProps as unknown as BigNumberTotalChartProps, - ); - expect(result.bigNumber).toBeNull(); - expect(result.width).toBe(400); - expect(result.height).toBe(300); - expect(result.subtitle).toBe(baseFormData.subheader); - expect(result.onContextMenu).toBe(onContextMenu); - expect(result.refs).toEqual({}); - // headerFormatter should be set even if there's no data - expect(typeof result.headerFormatter).toBe('function'); - // colorThresholdFormatters fallback to empty array when getColorFormatters returns falsy - expect(result.colorThresholdFormatters).toEqual([]); - }); - test('should convert subheader to subtitle', () => { - const chartProps = { - width: 400, - height: 300, - queriesData: [{ data: [], coltypes: [] }], - formData: { ...baseFormData, subheader: 'test' }, - rawFormData: baseRawFormData, - hooks: baseHooks, - datasource: baseDatasource, - }; - const result = transformProps( - chartProps as unknown as BigNumberTotalChartProps, - ); - expect(result.subtitle).toBe('test'); - }); - - const baseChartProps = { - width: 400, - height: 300, - queriesData: [{ data: [], coltypes: [] }], - rawFormData: { dummy: 'raw' }, - hooks: { onContextMenu: jest.fn() }, - datasource: { - currencyFormats: { value: '$0,0.00' }, - columnFormats: { value: '$0,0.00' }, - metrics: [{ metric_name: 'value', d3format: '.2f' }], - }, - }; - - test('uses subtitle font size when subtitle is provided', () => { - const result = transformProps({ - ...baseChartProps, - formData: { - subtitle: 'Subtitle wins', - subheader: 'Fallback subheader', - subtitleFontSize: 0.4, - subheaderFontSize: 0.99, - metric: 'value', - headerFontSize: 0.3, - yAxisFormat: 'SMART_NUMBER', - timeFormat: 'smart_date', - }, - } as unknown as BigNumberTotalChartProps); - - expect(result.subtitle).toBe('Subtitle wins'); - expect(result.subtitleFontSize).toBe(0.4); - }); - - test('should compute bigNumber using parseMetricValue when data exists', () => { - const chartProps = { - width: 500, - height: 400, - queriesData: [ - { data: [{ value: '456' }], coltypes: [GenericDataType.String] }, - ], - formData: { ...baseFormData, forceTimestampFormatting: false }, - rawFormData: baseRawFormData, - hooks: baseHooks, - datasource: baseDatasource, - sortBy: 'value', - }; - - const result = transformProps( - chartProps as unknown as BigNumberTotalChartProps, - ); - // parseMetricValue converts '456' to number 456 by our mock - expect(result.bigNumber).toEqual(456); - }); - - test('should use formatTime as headerFormatter for Temporal or String types or forced formatting', () => { - // Case 1: Temporal type - const chartPropsTemporal = { - width: 600, - height: 450, - queriesData: [ - { data: [{ value: '789' }], coltypes: [GenericDataType.Temporal] }, - ], - formData: { ...baseFormData, forceTimestampFormatting: false }, - rawFormData: baseRawFormData, - hooks: baseHooks, - datasource: baseDatasource, - }; - - const resultTemporal = transformProps( - chartPropsTemporal as unknown as BigNumberTotalChartProps, - ); - expect(resultTemporal.headerFormatter(5)).toBe('5pm'); - - // Case 2: String type regardless of forcing formatting - const chartPropsString = { - width: 600, - height: 450, - queriesData: [ - { data: [{ value: '789' }], coltypes: [GenericDataType.String] }, - ], - formData: { ...baseFormData, forceTimestampFormatting: false }, - rawFormData: baseRawFormData, - hooks: baseHooks, - datasource: baseDatasource, - }; - - const resultString = transformProps( - chartPropsString as unknown as BigNumberTotalChartProps, - ); - expect(resultString.headerFormatter(5)).toBe('5pm'); - - // Case 3: Forced timestamp formatting - const chartPropsForced = { - width: 600, - height: 450, - queriesData: [{ data: [{ value: '789' }], coltypes: [0] }], // non-temporal/non-string - formData: { ...baseFormData, forceTimestampFormatting: true }, - rawFormData: baseRawFormData, - hooks: baseHooks, - datasource: baseDatasource, - }; - - const resultForced = transformProps( - chartPropsForced as unknown as BigNumberTotalChartProps, - ); - expect(resultForced.headerFormatter(5)).toBe('5pm'); - }); - - test('should use numberFormatter as headerFormatter when not Temporal/String and no forced formatting', () => { - const chartProps = { - width: 700, - height: 500, - queriesData: [{ data: [{ value: '321' }], coltypes: [0] }], // non-temporal/non-string - formData: { ...baseFormData, forceTimestampFormatting: false }, - rawFormData: baseRawFormData, - hooks: baseHooks, - datasource: baseDatasource, - }; - - const result = transformProps( - chartProps as unknown as BigNumberTotalChartProps, - ); - expect(result.headerFormatter(500)).toBe('$500'); - }); - - test('should propagate colorThresholdFormatters from getColorFormatters', () => { - // Override the getColorFormatters mock to return specific value - const mockFormatters = [{ formatter: 'red' }]; - (getColorFormatters as jest.Mock).mockReturnValueOnce(mockFormatters); - - const chartProps = { - width: 800, - height: 600, - queriesData: [ - { data: [{ value: '100' }], coltypes: [GenericDataType.Temporal] }, - ], - formData: baseFormData, - rawFormData: baseRawFormData, - hooks: baseHooks, - datasource: baseDatasource, - }; - - const result = transformProps( - chartProps as unknown as BigNumberTotalChartProps, - ); - expect(result.colorThresholdFormatters).toEqual(mockFormatters); - }); -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.ts deleted file mode 100644 index 7c250595962..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberTotal/transformProps.ts +++ /dev/null @@ -1,140 +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 { - ColorFormatters, - getColorFormatters, - Metric, -} from '@superset-ui/chart-controls'; -import { - getMetricLabel, - extractTimegrain, - QueryFormData, - getValueFormatter, -} from '@superset-ui/core'; -import { GenericDataType } from '@apache-superset/core/common'; -import { BigNumberTotalChartProps, BigNumberVizProps } from '../types'; -import { PROPORTION } from '../constants'; -import { getDateFormatter, getOriginalLabel, parseMetricValue } from '../utils'; -import { Refs } from '../../types'; - -export default function transformProps( - chartProps: BigNumberTotalChartProps, -): BigNumberVizProps { - const { - width, - height, - queriesData, - formData, - rawFormData, - hooks, - datasource: { - currencyFormats = {}, - columnFormats = {}, - currencyCodeColumn, - }, - theme, - } = chartProps; - const { - metricNameFontSize, - headerFontSize, - metric = 'value', - subtitle, - subtitleFontSize, - forceTimestampFormatting, - timeFormat, - yAxisFormat, - conditionalFormatting, - currencyFormat, - subheader, - subheaderFontSize, - } = formData; - const refs: Refs = {}; - const { - data = [], - coltypes = [], - detected_currency: detectedCurrency, - } = queriesData[0] || {}; - const granularity = extractTimegrain(rawFormData as QueryFormData); - const metrics = chartProps.datasource?.metrics || []; - const originalLabel = getOriginalLabel(metric, metrics); - const metricName = getMetricLabel(metric); - const showMetricName = chartProps.rawFormData?.show_metric_name ?? false; - const formattedSubtitle = subtitle?.trim() ? subtitle : subheader || ''; - const formattedSubtitleFontSize = subtitle?.trim() - ? (subtitleFontSize ?? PROPORTION.SUBHEADER) - : (subheaderFontSize ?? subtitleFontSize ?? PROPORTION.SUBHEADER); - const bigNumber = - data.length === 0 ? null : parseMetricValue(data[0][metricName]); - - let metricEntry: Metric | undefined; - if (chartProps.datasource?.metrics) { - metricEntry = chartProps.datasource.metrics.find( - metricItem => metricItem.metric_name === metric, - ); - } - - const formatTime = getDateFormatter( - timeFormat, - granularity, - metricEntry?.d3format, - ); - - const numberFormatter = getValueFormatter( - metric, - currencyFormats, - columnFormats, - metricEntry?.d3format || yAxisFormat, - currencyFormat, - undefined, - data, - currencyCodeColumn, - detectedCurrency, - ); - - const headerFormatter = - coltypes[0] === GenericDataType.Temporal || - coltypes[0] === GenericDataType.String || - forceTimestampFormatting - ? formatTime - : numberFormatter; - - const { onContextMenu } = hooks; - - const defaultColorFormatters = [] as ColorFormatters; - - const colorThresholdFormatters = - getColorFormatters(conditionalFormatting, data, theme, false) ?? - defaultColorFormatters; - return { - width, - height, - bigNumber, - headerFormatter, - headerFontSize, - subheaderFontSize, - subtitleFontSize: formattedSubtitleFontSize, - subtitle: formattedSubtitle, - onContextMenu, - refs, - colorThresholdFormatters, - metricName: originalLabel, - showMetricName, - metricNameFontSize, - }; -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/buildQuery.test.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/buildQuery.test.ts deleted file mode 100644 index fa11762d036..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/buildQuery.test.ts +++ /dev/null @@ -1,101 +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 { QueryFormData } from '@superset-ui/core'; -import buildQuery from './buildQuery'; - -jest.mock('@superset-ui/core', () => ({ - ...jest.requireActual('@superset-ui/core'), - getXAxisColumn: jest.fn(() => 'order_date'), - isXAxisSet: jest.fn(() => true), -})); - -jest.mock('@superset-ui/chart-controls', () => ({ - pivotOperator: jest.fn(() => ({ operation: 'pivot' })), - aggregationOperator: jest.fn(formData => { - if (formData.aggregation === 'LAST_VALUE' || !formData.aggregation) { - return undefined; - } - return { - operation: 'aggregation', - options: { operator: formData.aggregation }, - }; - }), - flattenOperator: jest.fn(() => ({ operation: 'flatten' })), - resampleOperator: jest.fn(() => ({ operation: 'resample' })), - rollingWindowOperator: jest.fn(() => ({ operation: 'rolling' })), -})); - -describe('BigNumberWithTrendline buildQuery', () => { - const baseFormData: QueryFormData = { - datasource: '1__table', - viz_type: 'big_number', - metric: 'custom_metric', - aggregation: null, - }; - - test('creates raw metric query when aggregation is "raw"', () => { - const queryContext = buildQuery({ ...baseFormData, aggregation: 'raw' }); - const bigNumberQuery = queryContext.queries[1]; - - expect(bigNumberQuery.post_processing).toEqual([]); - expect(bigNumberQuery.is_timeseries).toBe(false); - expect(bigNumberQuery.columns).toEqual([]); - }); - - test('returns single query for aggregation methods that can be computed client-side', () => { - const queryContext = buildQuery({ ...baseFormData, aggregation: 'sum' }); - - expect(queryContext.queries.length).toBe(1); - expect(queryContext.queries[0].post_processing).toEqual([ - { operation: 'pivot' }, - { operation: 'resample' }, - { operation: 'rolling' }, - { operation: 'flatten' }, - ]); - }); - - test('returns single query for LAST_VALUE aggregation', () => { - const queryContext = buildQuery({ - ...baseFormData, - aggregation: 'LAST_VALUE', - }); - - expect(queryContext.queries.length).toBe(1); - expect(queryContext.queries[0].post_processing).toEqual([ - { operation: 'pivot' }, - { operation: 'resample' }, - { operation: 'rolling' }, - { operation: 'flatten' }, - ]); - }); - - test('returns two queries only for raw aggregation', () => { - const queryContext = buildQuery({ ...baseFormData, aggregation: 'raw' }); - expect(queryContext.queries.length).toBe(2); - - const queryContextLastValue = buildQuery({ - ...baseFormData, - aggregation: 'LAST_VALUE', - }); - expect(queryContextLastValue.queries.length).toBe(1); - - const queryContextSum = buildQuery({ ...baseFormData, aggregation: 'sum' }); - expect(queryContextSum.queries.length).toBe(1); - }); -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/buildQuery.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/buildQuery.ts deleted file mode 100644 index e5c7f822705..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/buildQuery.ts +++ /dev/null @@ -1,75 +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 { - buildQueryContext, - ensureIsArray, - getXAxisColumn, - isXAxisSet, - QueryFormData, -} from '@superset-ui/core'; -import { - aggregationOperator, - flattenOperator, - pivotOperator, - resampleOperator, - rollingWindowOperator, -} from '@superset-ui/chart-controls'; - -export default function buildQuery(formData: QueryFormData) { - const isRawMetric = formData.aggregation === 'raw'; - - const timeColumn = isXAxisSet(formData) - ? ensureIsArray(getXAxisColumn(formData)) - : []; - - return buildQueryContext(formData, baseQueryObject => { - const queries = [ - { - ...baseQueryObject, - columns: [...timeColumn], - ...(timeColumn.length ? {} : { is_timeseries: true }), - post_processing: [ - pivotOperator(formData, baseQueryObject), - resampleOperator(formData, baseQueryObject), - rollingWindowOperator(formData, baseQueryObject), - flattenOperator(formData, baseQueryObject), - ].filter(Boolean), - }, - ]; - - // Only add second query for raw metrics which need different query structure - // All other aggregations (sum, mean, min, max, median, LAST_VALUE) can be computed client-side from trendline data - if (formData.aggregation === 'raw') { - queries.push({ - ...baseQueryObject, - columns: [...(isRawMetric ? [] : timeColumn)], - is_timeseries: !isRawMetric, - post_processing: isRawMetric - ? [] - : ([ - pivotOperator(formData, baseQueryObject), - aggregationOperator(formData, baseQueryObject), - ].filter(Boolean) as any[]), - }); - } - - return queries; - }); -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/controlPanel.tsx deleted file mode 100644 index 9351efabeb4..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/controlPanel.tsx +++ /dev/null @@ -1,365 +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 { t } from '@apache-superset/core/translation'; -import { SMART_DATE_ID } from '@superset-ui/core'; -import { - aggregationControl, - ControlPanelConfig, - ControlPanelsContainerProps, - ControlSubSectionHeader, - D3_FORMAT_DOCS, - D3_TIME_FORMAT_OPTIONS, - getStandardizedControls, - temporalColumnMixin, -} from '@superset-ui/chart-controls'; -import { - headerFontSize, - subheaderFontSize, - subtitleFontSize, - subtitleControl, - showMetricNameControl, - metricNameFontSizeWithVisibility, -} from '../sharedControls'; - -const config: ControlPanelConfig = { - controlPanelSections: [ - { - label: t('Query'), - expanded: true, - controlSetRows: [ - ['x_axis'], - ['time_grain_sqla'], - [aggregationControl], - ['metric'], - ['adhoc_filters'], - ], - }, - { - label: t('Options'), - tabOverride: 'data', - expanded: true, - controlSetRows: [ - [ - { - name: 'compare_lag', - config: { - type: 'TextControl', - label: t('Comparison Period Lag'), - isInt: true, - description: t( - 'Based on granularity, number of time periods to compare against', - ), - }, - }, - ], - [ - { - name: 'compare_suffix', - config: { - type: 'TextControl', - label: t('Comparison suffix'), - description: t('Suffix to apply after the percentage display'), - }, - }, - ], - [ - { - name: 'show_timestamp', - config: { - type: 'CheckboxControl', - label: t('Show Timestamp'), - renderTrigger: true, - default: false, - description: t('Whether to display the timestamp'), - }, - }, - ], - [ - { - name: 'show_trend_line', - config: { - type: 'CheckboxControl', - label: t('Show Trend Line'), - renderTrigger: true, - default: true, - description: t('Whether to display the trend line'), - }, - }, - ], - [ - { - name: 'start_y_axis_at_zero', - config: { - type: 'CheckboxControl', - label: t('Start y-axis at 0'), - renderTrigger: true, - default: true, - description: t( - 'Start y-axis at zero. Uncheck to start y-axis at minimum value in the data.', - ), - }, - }, - ], - [ - { - name: 'time_range_fixed', - config: { - type: 'CheckboxControl', - label: t('Fix to selected Time Range'), - description: t( - 'Fix the trend line to the full time range specified in case filtered results do not include the start or end dates', - ), - renderTrigger: true, - visibility(props) { - const { time_range: timeRange } = props.form_data; - // only display this option when a time range is selected - return !!timeRange && timeRange !== 'No filter'; - }, - }, - }, - ], - ], - }, - { - label: t('Chart Options'), - expanded: true, - controlSetRows: [ - ['color_picker', null], - [headerFontSize], - [subheaderFontSize], - [subtitleControl], - [subtitleFontSize], - [showMetricNameControl], - [metricNameFontSizeWithVisibility], - [{t('X Axis')}], - [ - { - name: 'show_x_axis', - config: { - type: 'CheckboxControl', - label: t('Show X-axis'), - renderTrigger: true, - default: false, - description: t('Whether to display the X Axis'), - }, - }, - ], - [ - { - name: 'show_x_axis_min_max_labels', - config: { - type: 'CheckboxControl', - label: t('Show min/max axis labels'), - renderTrigger: true, - default: false, - description: t( - 'When enabled, the axis will display labels for the minimum and maximum values of your data', - ), - visibility: ({ controls }: ControlPanelsContainerProps) => - Boolean(controls?.show_x_axis?.value), - }, - }, - ], - [{t('Y Axis')}], - [ - { - name: 'show_y_axis', - config: { - type: 'CheckboxControl', - label: t('Show Y-axis'), - renderTrigger: true, - default: false, - description: t('Whether to display the Y Axis'), - }, - }, - ], - [ - { - name: 'show_y_axis_min_max_labels', - config: { - type: 'CheckboxControl', - label: t('Show min/max axis labels'), - renderTrigger: true, - default: false, - description: t( - 'When enabled, the axis will display labels for the minimum and maximum values of your data', - ), - visibility: ({ controls }: ControlPanelsContainerProps) => - Boolean(controls?.show_y_axis?.value), - }, - }, - ], - ['y_axis_format'], - ['currency_format'], - [ - { - name: 'time_format', - config: { - type: 'SelectControl', - freeForm: true, - label: t('Date format'), - renderTrigger: true, - choices: D3_TIME_FORMAT_OPTIONS, - description: D3_FORMAT_DOCS, - default: SMART_DATE_ID, - }, - }, - ], - [ - { - name: 'force_timestamp_formatting', - config: { - type: 'CheckboxControl', - label: t('Force date format'), - renderTrigger: true, - default: false, - description: t( - 'Use date formatting even when metric value is not a timestamp', - ), - }, - }, - ], - ], - }, - { - label: t('Advanced Analytics'), - expanded: false, - controlSetRows: [ - // eslint-disable-next-line react/jsx-key - [ - - {t('Rolling Window')} - , - ], - [ - { - name: 'rolling_type', - config: { - type: 'SelectControl', - label: t('Rolling Function'), - default: 'None', - choices: [ - ['None', t('None')], - ['mean', t('mean')], - ['sum', t('sum')], - ['std', t('std')], - ['cumsum', t('cumsum')], - ], - description: t( - 'Defines a rolling window function to apply, works along ' + - 'with the [Periods] text box', - ), - }, - }, - ], - [ - { - name: 'rolling_periods', - config: { - type: 'TextControl', - label: t('Periods'), - isInt: true, - description: t( - 'Defines the size of the rolling window function, ' + - 'relative to the time granularity selected', - ), - }, - }, - ], - [ - { - name: 'min_periods', - config: { - type: 'TextControl', - label: t('Min Periods'), - isInt: true, - description: t( - 'The minimum number of rolling periods required to show ' + - 'a value. For instance if you do a cumulative sum on 7 days ' + - 'you may want your "Min Period" to be 7, so that all data points ' + - 'shown are the total of 7 periods. This will hide the "ramp up" ' + - 'taking place over the first 7 periods', - ), - }, - }, - ], - [{t('Resample')}], - [ - { - name: 'resample_rule', - config: { - type: 'SelectControl', - freeForm: true, - label: t('Rule'), - default: null, - choices: [ - ['1T', t('1 minutely frequency')], - ['1H', t('1 hourly frequency')], - ['1D', t('1 calendar day frequency')], - ['7D', t('7 calendar day frequency')], - ['1MS', t('1 month start frequency')], - ['1M', t('1 month end frequency')], - ['1AS', t('1 year start frequency')], - ['1A', t('1 year end frequency')], - ], - description: t('Pandas resample rule'), - }, - }, - ], - [ - { - name: 'resample_method', - config: { - type: 'SelectControl', - freeForm: true, - label: t('Fill method'), - default: null, - choices: [ - ['asfreq', t('Null imputation')], - ['zerofill', t('Zero imputation')], - ['linear', t('Linear interpolation')], - ['ffill', t('Forward values')], - ['bfill', t('Backward values')], - ['median', t('Median values')], - ['mean', t('Mean values')], - ['sum', t('Sum values')], - ], - description: t('Pandas resample method'), - }, - }, - ], - ], - }, - ], - controlOverrides: { - y_axis_format: { - label: t('Number format'), - }, - x_axis: { - label: t('Temporal X-Axis'), - ...temporalColumnMixin, - }, - }, - formDataOverrides: formData => ({ - ...formData, - metric: getStandardizedControls().shiftMetric(), - }), -}; - -export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/index.ts deleted file mode 100644 index 74a7da2c043..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/index.ts +++ /dev/null @@ -1,68 +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 { t } from '@apache-superset/core/translation'; -import { Behavior } from '@superset-ui/core'; -import controlPanel from './controlPanel'; -import transformProps from './transformProps'; -import buildQuery from './buildQuery'; -import example from './images/Big_Number_Trendline.jpg'; -import exampleDark from './images/Big_Number_Trendline-dark.jpg'; -import thumbnail from './images/thumbnail.png'; -import thumbnailDark from './images/thumbnail-dark.png'; -import { - BigNumberWithTrendlineChartProps, - BigNumberWithTrendlineFormData, -} from '../types'; -import { EchartsChartPlugin } from '../../types'; - -const metadata = { - category: t('KPI'), - description: t( - 'Showcases a single number accompanied by a simple line chart, to call attention to an important metric along with its change over time or other dimension.', - ), - exampleGallery: [{ url: example, urlDark: exampleDark }], - name: t('Big Number with Trendline'), - tags: [ - t('Advanced-Analytics'), - t('ECharts'), - t('Line'), - t('Percentages'), - t('Featured'), - t('Report'), - t('Trend'), - ], - thumbnail, - thumbnailDark, - behaviors: [Behavior.DrillToDetail], -}; - -export default class BigNumberWithTrendlineChartPlugin extends EchartsChartPlugin< - BigNumberWithTrendlineFormData, - BigNumberWithTrendlineChartProps -> { - constructor() { - super({ - loadChart: () => import('../BigNumberViz'), - metadata, - buildQuery, - transformProps, - controlPanel, - }); - } -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/index.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/index.tsx new file mode 100644 index 00000000000..55ef834634d --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/index.tsx @@ -0,0 +1,752 @@ +/** + * 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. + */ + +/** + * BigNumber with Trendline - Glyph Pattern Implementation + * + * This is the Glyph pattern version of BigNumberWithTrendline. + * Complex charts like this one use custom buildQuery and transform functions + * to handle the data processing, while still benefiting from the + * declarative argument-based control panel generation. + */ + +import { t } from '@apache-superset/core/translation'; +import { + Behavior, + buildQueryContext, + ensureIsArray, + extractTimegrain, + getMetricLabel, + getNumberFormatter, + getValueFormatter, + getXAxisColumn, + getXAxisLabel, + isXAxisSet, + Metric as SupersetMetric, + NumberFormats, + QueryFormData, + tooltipHtml, +} from '@superset-ui/core'; +import { GenericDataType } from '@apache-superset/core/common'; +import { EChartsCoreOption, graphic } from 'echarts/core'; +import { + aggregationControl, + aggregationOperator, + aggregationChoices, + flattenOperator, + pivotOperator, + resampleOperator, + rollingWindowOperator, + temporalColumnMixin, +} from '@superset-ui/chart-controls'; + +import { + defineChart, + Metric, + Temporal, + Select, + Text, + Checkbox, + Color, + NumberFormat, + Currency, + TimeFormat, + HeaderFontSize, + SubheaderFontSize, + Subtitle, + ForceTimestampFormatting, + ShowMetricName, + MetricNameFontSize, + ChartProps, +} from '@superset-ui/glyph-core'; + +import { TIMESERIES_CONSTANTS } from '../../constants'; +import { getXAxisFormatter } from '../../utils/formatters'; +import { getDefaultTooltip } from '../../utils/tooltip'; +import { getDateFormatter, parseMetricValue, getOriginalLabel } from '../utils'; +import BigNumberViz from '../BigNumberViz'; +import { BigNumberDatum, TimeSeriesDatum, BigNumberVizProps } from '../types'; +import { Refs } from '../../types'; + +import example from './images/Big_Number_Trendline.jpg'; +import exampleDark from './images/Big_Number_Trendline-dark.jpg'; +import thumbnail from './images/thumbnail.png'; + +// ============================================================================ +// Build Query - Exported for testing +// ============================================================================ + +export function buildQuery(formData: QueryFormData) { + const isRawMetric = formData.aggregation === 'raw'; + + const timeColumn = isXAxisSet(formData) + ? ensureIsArray(getXAxisColumn(formData)) + : []; + + return buildQueryContext(formData, baseQueryObject => { + const queries = [ + { + ...baseQueryObject, + columns: [...timeColumn], + ...(timeColumn.length ? {} : { is_timeseries: true }), + post_processing: [ + pivotOperator(formData, baseQueryObject), + rollingWindowOperator(formData, baseQueryObject), + resampleOperator(formData, baseQueryObject), + flattenOperator(formData, baseQueryObject), + ].filter(Boolean), + }, + ]; + + if (formData.aggregation === 'raw') { + queries.push({ + ...baseQueryObject, + columns: [...(isRawMetric ? [] : timeColumn)], + is_timeseries: !isRawMetric, + post_processing: isRawMetric + ? [] + : ([ + pivotOperator(formData, baseQueryObject), + aggregationOperator(formData, baseQueryObject), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ].filter(Boolean) as any[]), + }); + } + + return queries; + }); +} + +// ============================================================================ +// Helpers +// ============================================================================ + +const formatPercentChange = getNumberFormatter( + NumberFormats.PERCENT_SIGNED_1_POINT, +); + +function computeClientSideAggregation( + data: [number | null, number | null][], + aggregation: string | undefined | null, +): number | null { + if (!data.length) return null; + + const methodKey = Object.keys(aggregationChoices).find( + key => key.toLowerCase() === (aggregation || '').toLowerCase(), + ); + + const selectedMethod = methodKey + ? aggregationChoices[methodKey as keyof typeof aggregationChoices] + : aggregationChoices.LAST_VALUE; + + const values = data + .map(([, value]) => value) + .filter((v): v is number => v !== null); + + return selectedMethod.compute(values); +} + +// ============================================================================ +// Rolling Window Options +// ============================================================================ + +const ROLLING_TYPE_OPTIONS = [ + { label: t('None'), value: 'None' }, + { label: t('mean'), value: 'mean' }, + { label: t('sum'), value: 'sum' }, + { label: t('std'), value: 'std' }, + { label: t('cumsum'), value: 'cumsum' }, +]; + +const RESAMPLE_RULE_OPTIONS = [ + { label: t('1 minutely frequency'), value: '1T' }, + { label: t('1 hourly frequency'), value: '1H' }, + { label: t('1 calendar day frequency'), value: '1D' }, + { label: t('7 calendar day frequency'), value: '7D' }, + { label: t('1 month start frequency'), value: '1MS' }, + { label: t('1 month end frequency'), value: '1M' }, + { label: t('1 year start frequency'), value: '1AS' }, + { label: t('1 year end frequency'), value: '1A' }, +]; + +const RESAMPLE_METHOD_OPTIONS = [ + { label: t('Null imputation'), value: 'asfreq' }, + { label: t('Zero imputation'), value: 'zerofill' }, + { label: t('Linear interpolation'), value: 'linear' }, + { label: t('Forward values'), value: 'ffill' }, + { label: t('Backward values'), value: 'bfill' }, + { label: t('Median values'), value: 'median' }, + { label: t('Mean values'), value: 'mean' }, + { label: t('Sum values'), value: 'sum' }, +]; + +// ============================================================================ +// Transform Props Type +// ============================================================================ + +interface TrendlineTransformResult { + vizProps: BigNumberVizProps; +} + +// ============================================================================ +// The Chart Definition +// ============================================================================ + +export default defineChart< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + any, + TrendlineTransformResult +>({ + metadata: { + name: t('Big Number with Trendline'), + description: t( + 'Showcases a single number accompanied by a simple line chart, to call attention to ' + + 'an important metric along with its change over time or other dimension.', + ), + category: t('KPI'), + tags: [ + t('Advanced-Analytics'), + t('Line'), + t('Percentages'), + t('Featured'), + t('Report'), + t('Trend'), + ], + thumbnail, + behaviors: [Behavior.DrillToDetail], + exampleGallery: [{ url: example, urlDark: exampleDark }], + }, + + arguments: { + // Query arguments + xAxis: Temporal.with({ label: t('Temporal X-Axis') }), + metric: Metric.with({ label: t('Metric') }), + + // Comparison options + compareLag: Text.with({ + label: t('Comparison Period Lag'), + description: t( + 'Based on granularity, number of time periods to compare against', + ), + }), + + compareSuffix: Text.with({ + label: t('Comparison suffix'), + description: t('Suffix to apply after the percentage display'), + }), + + // Display options + showTimestamp: Checkbox.with({ + label: t('Show Timestamp'), + description: t('Whether to display the timestamp'), + default: false, + }), + + showTrendLine: Checkbox.with({ + label: t('Show Trend Line'), + description: t('Whether to display the trend line'), + default: true, + }), + + startYAxisAtZero: Checkbox.with({ + label: t('Start y-axis at 0'), + description: t( + 'Start y-axis at zero. Uncheck to start y-axis at minimum value in the data.', + ), + default: true, + }), + + timeRangeFixed: { + arg: Checkbox.with({ + label: t('Fix to selected Time Range'), + description: t( + 'Fix the trend line to the full time range specified in case filtered results do not include the start or end dates', + ), + default: false, + }), + // Only show when time range is selected + visibleWhen: {}, // TODO: Need formData access for this + }, + + // Chart styling + colorPicker: Color.with({ + label: t('Color'), + description: t('Color for the trend line'), + // eslint-disable-next-line theme-colors/no-literal-colors + default: '#1f77b4', + }), + + headerFontSize: HeaderFontSize, + subheaderFontSize: SubheaderFontSize, + subtitle: Subtitle, + subtitleFontSize: SubheaderFontSize, + + showMetricName: ShowMetricName, + + metricNameFontSize: { + arg: MetricNameFontSize, + visibleWhen: { showMetricName: true }, + }, + + // X Axis options + showXAxis: Checkbox.with({ + label: t('Show X-axis'), + description: t('Whether to display the X Axis'), + default: false, + }), + + showXAxisMinMaxLabels: { + arg: Checkbox.with({ + label: t('Show min/max axis labels'), + description: t( + 'When enabled, the axis will display labels for the minimum and maximum values of your data', + ), + default: false, + }), + visibleWhen: { showXAxis: true }, + }, + + // Y Axis options + showYAxis: Checkbox.with({ + label: t('Show Y-axis'), + description: t('Whether to display the Y Axis'), + default: false, + }), + + showYAxisMinMaxLabels: { + arg: Checkbox.with({ + label: t('Show min/max axis labels'), + description: t( + 'When enabled, the axis will display labels for the minimum and maximum values of your data', + ), + default: false, + }), + visibleWhen: { showYAxis: true }, + }, + + // Formatting + yAxisFormat: NumberFormat.with({ + label: t('Number format'), + default: 'SMART_NUMBER', + }), + + currencyFormat: Currency, + timeFormat: TimeFormat, + forceTimestampFormatting: ForceTimestampFormatting, + + // Advanced analytics - Rolling window + rollingType: Select.with({ + label: t('Rolling Function'), + description: t( + 'Defines a rolling window function to apply, works along with the [Periods] text box', + ), + options: ROLLING_TYPE_OPTIONS, + default: 'None', + }), + + rollingPeriods: Text.with({ + label: t('Periods'), + description: t( + 'Defines the size of the rolling window function, relative to the time granularity selected', + ), + }), + + minPeriods: Text.with({ + label: t('Min Periods'), + description: t( + 'The minimum number of rolling periods required to show a value', + ), + }), + + // Advanced analytics - Resample + resampleRule: Select.with({ + label: t('Rule'), + description: t('Pandas resample rule'), + options: RESAMPLE_RULE_OPTIONS, + default: '', + }), + + resampleMethod: Select.with({ + label: t('Fill method'), + description: t('Pandas resample method'), + options: RESAMPLE_METHOD_OPTIONS, + default: '', + }), + }, + + // Additional controls that need special handling + additionalControls: { + query: [['x_axis'], ['time_grain_sqla'], [aggregationControl]], + }, + + controlOverrides: { + y_axis_format: { + label: t('Number format'), + }, + x_axis: { + label: t('Temporal X-Axis'), + ...temporalColumnMixin, + }, + }, + + // Custom buildQuery for time-series with post-processing + buildQuery, + + // Custom transform to compute trendline data and ECharts options + transform: (chartProps: ChartProps, _argValues): TrendlineTransformResult => { + const { + width, + height, + queriesData, + formData, + rawFormData, + hooks, + inContextMenu, + theme, + datasource, + } = chartProps; + + const currencyFormats = datasource?.currencyFormats ?? {}; + const columnFormats = datasource?.columnFormats ?? {}; + + const { + colorPicker = { r: 31, g: 119, b: 180 }, + compareLag: compareLag_, + compareSuffix = '', + timeFormat, + metricNameFontSize, + headerFontSize, + metric = 'value', + showTimestamp, + showTrendLine, + subtitle = '', + subtitleFontSize, + aggregation, + startYAxisAtZero, + subheader = '', + subheaderFontSize, + forceTimestampFormatting, + yAxisFormat, + currencyFormat, + timeRangeFixed, + showXAxis = false, + showXAxisMinMaxLabels = false, + showYAxis = false, + showYAxisMinMaxLabels = false, + } = formData; + + const granularity = extractTimegrain(rawFormData as QueryFormData); + const { + data = [], + colnames = [], + coltypes = [], + from_dttm: fromDatetime, + to_dttm: toDatetime, + } = queriesData[0] ?? {}; + + const aggregatedQueryData = queriesData.length > 1 ? queriesData[1] : null; + + const hasAggregatedData = + aggregatedQueryData?.data && + aggregatedQueryData.data.length > 0 && + aggregation !== 'LAST_VALUE'; + + const aggregatedData = hasAggregatedData + ? aggregatedQueryData.data[0] + : null; + const refs: Refs = {}; + const metricName = getMetricLabel(metric); + const metrics = datasource?.metrics || []; + const originalLabel = getOriginalLabel(metric, metrics as SupersetMetric[]); + const showMetricName = rawFormData?.show_metric_name ?? false; + const compareLag = Number(compareLag_) || 0; + let formattedSubheader = subheader; + + const { r, g, b } = colorPicker as { r: number; g: number; b: number }; + const mainColor = `rgb(${r}, ${g}, ${b})`; + + const xAxisLabel = getXAxisLabel(rawFormData as QueryFormData) as string; + let trendLineData: TimeSeriesDatum[] | undefined; + let percentChange = 0; + let bigNumber = + data.length === 0 ? null : (data[0] as BigNumberDatum)[metricName]; + let timestamp = + data.length === 0 ? null : (data[0] as BigNumberDatum)[xAxisLabel]; + let bigNumberFallback: TimeSeriesDatum | undefined; + let sortedData: [number | null, number | null][] = []; + + if (data.length > 0) { + sortedData = (data as BigNumberDatum[]) + .map( + d => + [d[xAxisLabel], parseMetricValue(d[metricName])] as [ + number | null, + number | null, + ], + ) + .sort((a, b) => (a[0] !== null && b[0] !== null ? b[0] - a[0] : 0)); + } + + if (sortedData.length > 0) { + timestamp = sortedData[0][0]; + + if (aggregation === 'raw' && hasAggregatedData && aggregatedData) { + if ( + aggregatedData[metricName] !== null && + aggregatedData[metricName] !== undefined + ) { + bigNumber = aggregatedData[metricName]; + } else { + const metricKeys = Object.keys(aggregatedData).filter( + key => + key !== xAxisLabel && + aggregatedData[key] !== null && + typeof aggregatedData[key] === 'number', + ); + bigNumber = + metricKeys.length > 0 ? aggregatedData[metricKeys[0]] : null; + } + } else { + bigNumber = computeClientSideAggregation(sortedData, aggregation); + } + + if (bigNumber === null) { + const fallback = sortedData.find(d => d[1] !== null); + if (fallback) { + bigNumberFallback = fallback as TimeSeriesDatum; + bigNumber = fallback[1]; + timestamp = fallback[0]; + } + } + } + + if (compareLag > 0 && sortedData.length > 0) { + const compareIndex = compareLag; + if (compareIndex < sortedData.length) { + const compareFromValue = sortedData[compareIndex][1]; + const compareToValue = sortedData[0][1]; + if (compareToValue !== null && compareFromValue !== null) { + percentChange = compareFromValue + ? (Number(compareToValue) - compareFromValue) / + Math.abs(compareFromValue) + : 0; + formattedSubheader = `${formatPercentChange( + percentChange, + )} ${compareSuffix}`; + } + } + } + + if (data.length > 0) { + const reversedData = [...sortedData].reverse(); + trendLineData = showTrendLine + ? (reversedData as TimeSeriesDatum[]) + : undefined; + } + + let className = ''; + if (percentChange > 0) { + className = 'positive'; + } else if (percentChange < 0) { + className = 'negative'; + } + + const metricColtypeIndex = colnames.findIndex( + (name: string) => name === metricName, + ); + const metricColtype = + metricColtypeIndex > -1 ? coltypes[metricColtypeIndex] : null; + + let metricEntry: SupersetMetric | undefined; + if (datasource?.metrics) { + metricEntry = (datasource.metrics as SupersetMetric[]).find( + m => m.metric_name === metric, + ); + } + + const formatTime = getDateFormatter( + timeFormat, + granularity, + metricEntry?.d3format, + ); + + if (trendLineData && timeRangeFixed && fromDatetime) { + const toDatetimeOrToday = toDatetime ?? Date.now(); + if (!trendLineData[0][0] || trendLineData[0][0] > fromDatetime) { + trendLineData.unshift([fromDatetime, null]); + } + if ( + !trendLineData[trendLineData.length - 1][0] || + trendLineData[trendLineData.length - 1][0]! < toDatetimeOrToday + ) { + trendLineData.push([toDatetimeOrToday, null]); + } + } + + const numberFormatter = getValueFormatter( + metric, + currencyFormats, + columnFormats, + metricEntry?.d3format || yAxisFormat, + currencyFormat, + ); + const xAxisFormatter = getXAxisFormatter(timeFormat); + const yAxisFormatter = + metricColtype === GenericDataType.Temporal || + metricColtype === GenericDataType.String || + forceTimestampFormatting + ? formatTime + : numberFormatter; + + const echartOptions: EChartsCoreOption = trendLineData + ? { + series: [ + { + data: trendLineData, + type: 'line', + smooth: true, + symbol: 'circle', + symbolSize: 10, + showSymbol: false, + color: mainColor, + areaStyle: { + color: new graphic.LinearGradient(0, 0, 0, 1, [ + { + offset: 0, + color: mainColor, + }, + { + offset: 1, + // eslint-disable-next-line theme-colors/no-literal-colors + color: + (theme as { colorBgContainer?: string }) + ?.colorBgContainer ?? '#fff', + }, + ]), + }, + }, + ], + xAxis: { + type: 'time', + show: showXAxis, + splitLine: { + show: false, + }, + axisLabel: { + hideOverlap: true, + formatter: xAxisFormatter, + alignMinLabel: 'left', + alignMaxLabel: 'right', + showMinLabel: showXAxisMinMaxLabels, + showMaxLabel: showXAxisMinMaxLabels, + }, + }, + yAxis: { + type: 'value', + show: showYAxis, + scale: !startYAxisAtZero, + splitLine: { + show: false, + }, + axisLabel: { + hideOverlap: true, + formatter: yAxisFormatter, + showMinLabel: showYAxisMinMaxLabels, + showMaxLabel: showYAxisMinMaxLabels, + }, + }, + grid: + showXAxis || showYAxis + ? { + containLabel: true, + bottom: TIMESERIES_CONSTANTS.gridOffsetBottom, + left: TIMESERIES_CONSTANTS.gridOffsetLeft, + right: TIMESERIES_CONSTANTS.gridOffsetRight, + top: TIMESERIES_CONSTANTS.gridOffsetTop, + } + : { + bottom: 0, + left: 0, + right: 0, + top: 0, + }, + tooltip: { + ...getDefaultTooltip(refs), + show: !inContextMenu, + trigger: 'axis', + formatter: (params: { data: TimeSeriesDatum }[]) => + tooltipHtml( + [ + [ + metricName, + params[0].data[1] === null + ? t('N/A') + : yAxisFormatter.format(params[0].data[1]), + ], + ], + formatTime(params[0].data[0]), + ), + }, + aria: { + enabled: true, + label: { + description: `Big number visualization ${subheader}`, + }, + }, + useUTC: true, + } + : {}; + + const { onContextMenu } = hooks ?? {}; + + return { + vizProps: { + width, + height, + bigNumber, + bigNumberFallback, + className, + headerFormatter: yAxisFormatter, + formatTime, + formData: formData as BigNumberVizProps['formData'], + metricName: originalLabel, + showMetricName, + metricNameFontSize, + headerFontSize, + subtitleFontSize, + subtitle, + subheaderFontSize, + mainColor, + showTimestamp, + showTrendLine, + startYAxisAtZero, + subheader: formattedSubheader, + timestamp, + trendLineData, + echartOptions, + onContextMenu, + xValueFormatter: formatTime, + refs, + }, + }; + }, + + // Render using BigNumberViz component + render: ({ vizProps }) => , +}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.test.ts deleted file mode 100644 index 6ce84f5ffe4..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.test.ts +++ /dev/null @@ -1,310 +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 { GenericDataType } from '@apache-superset/core/common'; -import transformProps from './transformProps'; -import { BigNumberWithTrendlineChartProps, BigNumberDatum } from '../types'; - -// Mock chart-controls to avoid styled-components issues in Jest -jest.mock('@superset-ui/chart-controls', () => ({ - aggregationChoices: { - raw: { - label: 'Force server-side aggregation', - compute: (data: number[]) => data[0] ?? null, - }, - LAST_VALUE: { - label: 'Last Value', - compute: (data: number[]) => data[0] ?? null, - }, - sum: { - label: 'Total (Sum)', - compute: (data: number[]) => data.reduce((a, b) => a + b, 0), - }, - mean: { - label: 'Average (Mean)', - compute: (data: number[]) => - data.reduce((a, b) => a + b, 0) / data.length, - }, - min: { label: 'Minimum', compute: (data: number[]) => Math.min(...data) }, - max: { label: 'Maximum', compute: (data: number[]) => Math.max(...data) }, - median: { - label: 'Median', - compute: (data: number[]) => { - const sorted = [...data].sort((a, b) => a - b); - const mid = Math.floor(sorted.length / 2); - return sorted.length % 2 === 0 - ? (sorted[mid - 1] + sorted[mid]) / 2 - : sorted[mid]; - }, - }, - }, -})); - -jest.mock('@superset-ui/core', () => ({ - BRAND_COLOR: '#00A699', - GenericDataType: { Temporal: 2, String: 1 }, - extractTimegrain: jest.fn(() => 'P1D'), - getMetricLabel: jest.fn(metric => metric), - getXAxisLabel: jest.fn(() => '__timestamp'), - getValueFormatter: jest.fn(() => ({ - format: (v: number) => `$${v}`, - })), - getNumberFormatter: jest.fn(() => (v: number) => `${(v * 100).toFixed(1)}%`), - t: jest.fn(v => v), - tooltipHtml: jest.fn(() => '
tooltip
'), - NumberFormats: { - PERCENT_SIGNED_1_POINT: '.1%', - }, -})); - -jest.mock('../utils', () => ({ - getDateFormatter: jest.fn(() => (v: any) => `${v}pm`), - parseMetricValue: jest.fn(val => Number(val)), - getOriginalLabel: jest.fn((metric, metrics) => { - console.log(metrics); - return metric; - }), -})); - -jest.mock('../../utils/tooltip', () => ({ - getDefaultTooltip: jest.fn(() => ({})), -})); - -jest.mock('../../utils/formatters', () => ({ - getXAxisFormatter: jest.fn(() => String), -})); - -jest.mock('../../constants', () => ({ - TIMESERIES_CONSTANTS: { - gridOffsetBottom: 20, - gridOffsetLeft: 20, - gridOffsetRight: 20, - gridOffsetTop: 20, - }, -})); - -describe('BigNumberWithTrendline transformProps', () => { - const onContextMenu = jest.fn(); - const baseFormData = { - headerFontSize: 20, - metric: 'value', - subtitle: 'subtitle message', - subtitleFontSize: 14, - forceTimestampFormatting: false, - timeFormat: 'YYYY-MM-DD', - xAxis: '__timestamp', - yAxisFormat: 'SMART_NUMBER', - compareLag: 1, - compareSuffix: 'WoW', - colorPicker: { r: 0, g: 0, b: 0 }, - currencyFormat: { symbol: '$', symbolPosition: 'prefix' }, - }; - - const baseDatasource = { - currencyFormats: { value: '$0,0.00' }, - columnFormats: { value: '$0,0.00' }, - metrics: [{ metric_name: 'value', d3format: '.2f' }], - }; - - const baseHooks = { onContextMenu }; - const baseRawFormData = { dummy: 'raw' }; - - test('should return null bigNumber when no data is provided', () => { - const chartProps = { - width: 400, - height: 300, - queriesData: [{ data: [] as unknown as BigNumberDatum[], coltypes: [] }], - formData: baseFormData, - rawFormData: baseRawFormData, - hooks: baseHooks, - datasource: baseDatasource, - theme: { colors: { grayscale: { light5: '#eee' } } }, - }; - - const result = transformProps( - chartProps as unknown as BigNumberWithTrendlineChartProps, - ); - expect(result.bigNumber).toBeNull(); - expect(result.subtitle).toBe('subtitle message'); - }); - - test('should calculate subheader as percent change with suffix', () => { - const chartProps = { - width: 500, - height: 400, - queriesData: [ - { - data: [ - { __timestamp: 2, value: 110 }, - { __timestamp: 1, value: 100 }, - ] as unknown as BigNumberDatum[], - colnames: ['__timestamp', 'value'], - coltypes: ['TEMPORAL', 'NUMERIC'], - }, - ], - formData: baseFormData, - rawFormData: baseRawFormData, - hooks: baseHooks, - datasource: baseDatasource, - theme: { colors: { grayscale: { light5: '#eee' } } }, - }; - - const result = transformProps( - chartProps as unknown as BigNumberWithTrendlineChartProps, - ); - expect(result.subheader).toBe('10.0% WoW'); - }); - - test('should compute bigNumber from parseMetricValue', () => { - const chartProps = { - width: 600, - height: 450, - queriesData: [ - { - data: [ - { __timestamp: 2, value: '456' }, - ] as unknown as BigNumberDatum[], - colnames: ['__timestamp', 'value'], - coltypes: [GenericDataType.Temporal, GenericDataType.String], - }, - ], - formData: baseFormData, - rawFormData: baseRawFormData, - hooks: baseHooks, - datasource: baseDatasource, - theme: { colors: { grayscale: { light5: '#eee' } } }, - }; - - const result = transformProps( - chartProps as unknown as BigNumberWithTrendlineChartProps, - ); - expect(result.bigNumber).toEqual(456); - }); - - test('should use formatTime as headerFormatter for Temporal/String or forced', () => { - const formData = { ...baseFormData, forceTimestampFormatting: true }; - const chartProps = { - width: 600, - height: 450, - queriesData: [ - { - data: [ - { __timestamp: 2, value: '123' }, - ] as unknown as BigNumberDatum[], - colnames: ['__timestamp', 'value'], - coltypes: [0, GenericDataType.String], - }, - ], - formData, - rawFormData: baseRawFormData, - hooks: baseHooks, - datasource: baseDatasource, - theme: { colors: { grayscale: { light5: '#eee' } } }, - }; - - const result = transformProps( - chartProps as unknown as BigNumberWithTrendlineChartProps, - ); - expect(result.headerFormatter(5)).toBe('5pm'); - }); - - test('should use numberFormatter when not Temporal/String and not forced', () => { - const formData = { ...baseFormData, forceTimestampFormatting: false }; - const chartProps = { - width: 600, - height: 450, - queriesData: [ - { - data: [{ __timestamp: 2, value: 500 }] as unknown as BigNumberDatum[], - colnames: ['__timestamp', 'value'], - coltypes: [0, 0], - }, - ], - formData, - rawFormData: baseRawFormData, - hooks: baseHooks, - datasource: baseDatasource, - theme: { colors: { grayscale: { light5: '#eee' } } }, - }; - - const result = transformProps( - chartProps as unknown as BigNumberWithTrendlineChartProps, - ); - expect(result.headerFormatter.format(500)).toBe('$500'); - }); - - test('should use last data point for comparison when big number comes from aggregated data', () => { - const chartProps = { - width: 500, - height: 400, - queriesData: [ - { - data: [ - { __timestamp: 3, value: 150 }, - { __timestamp: 2, value: 100 }, - { __timestamp: 1, value: 110 }, - ] as unknown as BigNumberDatum[], - colnames: ['__timestamp', 'value'], - coltypes: ['TEMPORAL', 'NUMERIC'], - }, - { - data: [{ value: 360 }], - colnames: ['value'], - coltypes: ['NUMERIC'], - }, - ], - formData: { ...baseFormData, aggregation: 'sum' }, - rawFormData: baseRawFormData, - hooks: baseHooks, - datasource: baseDatasource, - theme: { colors: { grayscale: { light5: '#eee' } } }, - }; - - const result = transformProps( - chartProps as unknown as BigNumberWithTrendlineChartProps, - ); - expect(result.bigNumber).toBe(360); - expect(result.subheader).toBe('50.0% WoW'); - }); - - test('should not crash and should return undefined mainColor when colorPicker is null', () => { - const chartProps = { - width: 400, - height: 300, - queriesData: [ - { - data: [ - { __timestamp: 1, value: 100 }, - ] as unknown as BigNumberDatum[], - colnames: ['__timestamp', 'value'], - coltypes: ['TEMPORAL', 'NUMERIC'], - }, - ], - formData: { ...baseFormData, colorPicker: null }, - rawFormData: baseRawFormData, - hooks: baseHooks, - datasource: baseDatasource, - theme: { colors: { grayscale: { light5: '#eee' } } }, - }; - - const result = transformProps( - chartProps as unknown as BigNumberWithTrendlineChartProps, - ); - expect(result.mainColor).toBeUndefined(); - }); -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.ts deleted file mode 100644 index 23708f7a41c..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.ts +++ /dev/null @@ -1,412 +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 { t } from '@apache-superset/core/translation'; -import { - BRAND_COLOR, - extractTimegrain, - getNumberFormatter, - NumberFormats, - getMetricLabel, - getXAxisLabel, - Metric, - getValueFormatter, - tooltipHtml, -} from '@superset-ui/core'; -import { GenericDataType } from '@apache-superset/core/common'; -import { EChartsCoreOption, graphic } from 'echarts/core'; -import { aggregationChoices } from '@superset-ui/chart-controls'; -import { TIMESERIES_CONSTANTS } from '../../constants'; -import { getXAxisFormatter } from '../../utils/formatters'; -import { - BigNumberVizProps, - BigNumberDatum, - BigNumberWithTrendlineChartProps, - TimeSeriesDatum, -} from '../types'; -import { getDateFormatter, parseMetricValue, getOriginalLabel } from '../utils'; -import { getDefaultTooltip } from '../../utils/tooltip'; -import { Refs } from '../../types'; - -const formatPercentChange = getNumberFormatter( - NumberFormats.PERCENT_SIGNED_1_POINT, -); - -// Client-side aggregation function using shared aggregationChoices -function computeClientSideAggregation( - data: [number | null, number | null][], - aggregation: string | undefined | null, -): number | null { - if (!data.length) return null; - - // Find the aggregation method, handling case variations - const methodKey = Object.keys(aggregationChoices).find( - key => key.toLowerCase() === (aggregation || '').toLowerCase(), - ); - - // Use the compute method from aggregationChoices, fallback to LAST_VALUE - const selectedMethod = methodKey - ? aggregationChoices[methodKey as keyof typeof aggregationChoices] - : aggregationChoices.LAST_VALUE; - - // Extract values from tuple array and filter out nulls - const values = data - .map(([, value]) => value) - .filter((v): v is number => v !== null); - - return selectedMethod.compute(values); -} - -export default function transformProps( - chartProps: BigNumberWithTrendlineChartProps, -): BigNumberVizProps { - const { - width, - height, - queriesData, - formData, - rawFormData, - hooks, - inContextMenu, - theme, - datasource: { - currencyFormats = {}, - columnFormats = {}, - currencyCodeColumn, - }, - } = chartProps; - const { - colorPicker, - compareLag: compareLag_, - compareSuffix = '', - timeFormat, - metricNameFontSize, - headerFontSize, - metric = 'value', - showTimestamp, - showTrendLine, - subtitle = '', - subtitleFontSize, - aggregation, - startYAxisAtZero, - subheader = '', - subheaderFontSize, - forceTimestampFormatting, - yAxisFormat, - currencyFormat, - timeRangeFixed, - showXAxis = false, - showXAxisMinMaxLabels = false, - showYAxis = false, - showYAxisMinMaxLabels = false, - } = formData; - const granularity = extractTimegrain(rawFormData); - const { - data = [], - colnames = [], - coltypes = [], - from_dttm: fromDatetime, - to_dttm: toDatetime, - detected_currency: detectedCurrency, - } = queriesData[0]; - - const aggregatedQueryData = queriesData.length > 1 ? queriesData[1] : null; - - const hasAggregatedData = - aggregatedQueryData?.data && - aggregatedQueryData.data.length > 0 && - aggregation !== 'LAST_VALUE'; - - const aggregatedData = hasAggregatedData ? aggregatedQueryData.data[0] : null; - const refs: Refs = {}; - const metricName = getMetricLabel(metric); - const metrics = chartProps.datasource?.metrics || []; - const originalLabel = getOriginalLabel(metric, metrics); - const showMetricName = chartProps.rawFormData?.show_metric_name ?? false; - const compareLag = Number(compareLag_) || 0; - let formattedSubheader = subheader; - - const mainColor = colorPicker - ? `rgb(${colorPicker.r}, ${colorPicker.g}, ${colorPicker.b})` - : undefined; - - const xAxisLabel = getXAxisLabel(rawFormData) as string; - let trendLineData: TimeSeriesDatum[] | undefined; - let percentChange = 0; - let bigNumber = data.length === 0 ? null : data[0][metricName]; - let timestamp = data.length === 0 ? null : data[0][xAxisLabel]; - let bigNumberFallback = null; - let sortedData: [number | null, number | null][] = []; - - if (data.length > 0) { - sortedData = (data as BigNumberDatum[]) - .map( - d => - [d[xAxisLabel], parseMetricValue(d[metricName])] as [ - number | null, - number | null, - ], - ) - // sort in time descending order - .sort((a, b) => (a[0] !== null && b[0] !== null ? b[0] - a[0] : 0)); - } - if (sortedData.length > 0) { - timestamp = sortedData[0][0]; - - // Raw aggregation uses server-side data, all others use client-side - if (aggregation === 'raw' && hasAggregatedData && aggregatedData) { - // Use server-side aggregation for raw - if ( - aggregatedData[metricName] !== null && - aggregatedData[metricName] !== undefined - ) { - bigNumber = aggregatedData[metricName]; - } else { - const metricKeys = Object.keys(aggregatedData).filter( - key => - key !== xAxisLabel && - aggregatedData[key] !== null && - typeof aggregatedData[key] === 'number', - ); - bigNumber = - metricKeys.length > 0 ? aggregatedData[metricKeys[0]] : null; - } - } else { - // Use client-side aggregation for all other methods - bigNumber = computeClientSideAggregation(sortedData, aggregation); - } - - // Handle null bigNumber case - if (bigNumber === null) { - bigNumberFallback = sortedData.find(d => d[1] !== null); - bigNumber = bigNumberFallback ? bigNumberFallback[1] : null; - timestamp = bigNumberFallback ? bigNumberFallback[0] : null; - } - } - - if (compareLag > 0 && sortedData.length > 0) { - const compareIndex = compareLag; - if (compareIndex < sortedData.length) { - const compareFromValue = sortedData[compareIndex][1]; - const compareToValue = sortedData[0][1]; - // compare values must both be non-nulls - if (compareToValue !== null && compareFromValue !== null) { - percentChange = compareFromValue - ? (Number(compareToValue) - compareFromValue) / - Math.abs(compareFromValue) - : 0; - formattedSubheader = `${formatPercentChange( - percentChange, - )} ${compareSuffix}`; - } - } - } - - if (data.length > 0 && showTrendLine) { - // Filter out entries with null timestamps and reverse for chronological order - // TimeSeriesDatum requires [number, number | null] - timestamp must be non-null - const validData = sortedData.filter( - (d): d is [number, number | null] => d[0] !== null, - ); - trendLineData = [...validData].reverse(); - } - - let className = ''; - if (percentChange > 0) { - className = 'positive'; - } else if (percentChange < 0) { - className = 'negative'; - } - - const metricColtypeIndex = colnames.findIndex(name => name === metricName); - const metricColtype = - metricColtypeIndex > -1 ? coltypes[metricColtypeIndex] : null; - - let metricEntry: Metric | undefined; - if (chartProps.datasource?.metrics) { - metricEntry = chartProps.datasource.metrics.find( - metricEntry => metricEntry.metric_name === metric, - ); - } - - const formatTime = getDateFormatter( - timeFormat, - granularity, - metricEntry?.d3format, - ); - - if (trendLineData && timeRangeFixed && fromDatetime) { - const toDatetimeOrToday = toDatetime ?? Date.now(); - if (!trendLineData[0][0] || trendLineData[0][0] > fromDatetime) { - trendLineData.unshift([fromDatetime, null]); - } - if ( - !trendLineData[trendLineData.length - 1][0] || - trendLineData[trendLineData.length - 1][0]! < toDatetimeOrToday - ) { - trendLineData.push([toDatetimeOrToday, null]); - } - } - - const numberFormatter = getValueFormatter( - metric, - currencyFormats, - columnFormats, - metricEntry?.d3format || yAxisFormat, - currencyFormat, - undefined, - data, - currencyCodeColumn, - detectedCurrency, - ); - const xAxisFormatter = getXAxisFormatter(timeFormat); - const yAxisFormatter = - metricColtype === GenericDataType.Temporal || - metricColtype === GenericDataType.String || - forceTimestampFormatting - ? formatTime - : numberFormatter; - - const echartOptions: EChartsCoreOption = trendLineData - ? { - series: [ - { - data: trendLineData, - type: 'line', - smooth: true, - symbol: 'circle', - symbolSize: 10, - showSymbol: false, - color: mainColor ?? BRAND_COLOR, - areaStyle: { - color: new graphic.LinearGradient(0, 0, 0, 1, [ - { - offset: 0, - color: mainColor ?? BRAND_COLOR, - }, - { - offset: 1, - color: theme.colorBgContainer, - }, - ]), - }, - }, - ], - xAxis: { - type: 'time', - show: showXAxis, - splitLine: { - show: false, - }, - axisLabel: { - hideOverlap: true, - formatter: xAxisFormatter, - alignMinLabel: 'left', - alignMaxLabel: 'right', - showMinLabel: showXAxisMinMaxLabels, - showMaxLabel: showXAxisMinMaxLabels, - }, - }, - yAxis: { - type: 'value', - show: showYAxis, - scale: !startYAxisAtZero, - splitLine: { - show: false, - }, - axisLabel: { - hideOverlap: true, - formatter: yAxisFormatter, - showMinLabel: showYAxisMinMaxLabels, - showMaxLabel: showYAxisMinMaxLabels, - }, - }, - grid: - showXAxis || showYAxis - ? { - containLabel: true, - bottom: TIMESERIES_CONSTANTS.gridOffsetBottom, - left: TIMESERIES_CONSTANTS.gridOffsetLeft, - right: TIMESERIES_CONSTANTS.gridOffsetRight, - top: TIMESERIES_CONSTANTS.gridOffsetTop, - } - : { - bottom: 0, - left: 0, - right: 0, - top: 0, - }, - tooltip: { - ...getDefaultTooltip(refs), - show: !inContextMenu, - trigger: 'axis', - formatter: (params: { data: TimeSeriesDatum }[]) => - tooltipHtml( - [ - [ - metricName, - params[0].data[1] === null - ? t('N/A') - : yAxisFormatter.format(params[0].data[1]), - ], - ], - formatTime(params[0].data[0]), - ), - }, - aria: { - enabled: true, - label: { - description: `Big number visualization ${subheader}`, - }, - }, - useUTC: true, - } - : {}; - - const { onContextMenu } = hooks; - - return { - width, - height, - bigNumber, - // @ts-expect-error - bigNumberFallback, - className, - headerFormatter: yAxisFormatter, - formatTime, - formData, - metricName: originalLabel, - showMetricName, - metricNameFontSize, - headerFontSize, - subtitleFontSize, - subtitle, - subheaderFontSize, - mainColor, - showTimestamp, - showTrendLine, - startYAxisAtZero, - subheader: formattedSubheader, - timestamp, - trendLineData, - echartOptions, - onContextMenu, - xValueFormatter: formatTime, - refs, - }; -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/index.ts index 334493f4193..bb76a4af489 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/index.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/index.ts @@ -20,3 +20,4 @@ export { default as BigNumberChartPlugin } from './BigNumberWithTrendline'; export { default as BigNumberTotalChartPlugin } from './BigNumberTotal'; export { default as BigNumberPeriodOverPeriodChartPlugin } from './BigNumberPeriodOverPeriod'; +export { default as BigNumberGlyphChartPlugin } from './BigNumberGlyph'; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/EchartsBoxPlot.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/EchartsBoxPlot.tsx deleted file mode 100644 index 637e8236fa8..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/EchartsBoxPlot.tsx +++ /dev/null @@ -1,40 +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 Echart from '../components/Echart'; -import { allEventHandlers } from '../utils/eventHandlers'; -import { BoxPlotChartTransformedProps } from './types'; - -export default function EchartsBoxPlot(props: BoxPlotChartTransformedProps) { - const { height, width, echartOptions, selectedValues, refs, formData } = - props; - - const eventHandlers = allEventHandlers(props); - - return ( - - ); -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/buildQuery.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/buildQuery.ts deleted file mode 100644 index 03b9058c8a3..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/buildQuery.ts +++ /dev/null @@ -1,59 +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 { - AdhocColumn, - buildQueryContext, - ensureIsArray, - isPhysicalColumn, -} from '@superset-ui/core'; -import { boxplotOperator } from '@superset-ui/chart-controls'; -import { BoxPlotQueryFormData } from './types'; - -export default function buildQuery(formData: BoxPlotQueryFormData) { - return buildQueryContext(formData, baseQueryObject => [ - { - ...baseQueryObject, - columns: [ - ...(ensureIsArray(formData.columns).length === 0 && - formData.granularity_sqla - ? [formData.granularity_sqla] // for backwards compatible: if columns control is empty and granularity_sqla was set, the time columns is default distributed column. - : ensureIsArray(formData.columns) - ).map(col => { - if ( - isPhysicalColumn(col) && - formData.time_grain_sqla && - formData?.temporal_columns_lookup?.[col] - ) { - return { - timeGrain: formData.time_grain_sqla, - columnType: 'BASE_AXIS', - sqlExpression: col, - label: col, - expressionType: 'SQL', - } as AdhocColumn; - } - return col; - }), - ...ensureIsArray(formData.groupby), - ], - series_columns: formData.groupby, - post_processing: [boxplotOperator(formData, baseQueryObject)], - }, - ]); -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/controlPanel.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/controlPanel.ts deleted file mode 100644 index d98c135ced8..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/controlPanel.ts +++ /dev/null @@ -1,206 +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 { t } from '@apache-superset/core/translation'; -import { - ensureIsArray, - isAdhocColumn, - isPhysicalColumn, - validateNonEmpty, -} from '@superset-ui/core'; -import { - D3_FORMAT_DOCS, - D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT, - D3_FORMAT_OPTIONS, - D3_TIME_FORMAT_OPTIONS, - sections, - ControlPanelConfig, - getStandardizedControls, - ControlState, - ControlPanelState, - getTemporalColumns, - sharedControls, -} from '@superset-ui/chart-controls'; - -const config: ControlPanelConfig = { - controlPanelSections: [ - { - label: t('Query'), - expanded: true, - controlSetRows: [ - ['columns'], - [ - { - name: 'time_grain_sqla', - config: { - ...sharedControls.time_grain_sqla, - visibility: ({ controls }) => { - const dttmLookup = Object.fromEntries( - ensureIsArray(controls?.columns?.options).map(option => [ - option.column_name, - option.is_dttm, - ]), - ); - - return ensureIsArray(controls?.columns.value) - .map(selection => { - if (isAdhocColumn(selection)) { - return true; - } - if (isPhysicalColumn(selection)) { - return !!dttmLookup[selection]; - } - return false; - }) - .some(Boolean); - }, - }, - }, - 'temporal_columns_lookup', - ], - ['groupby'], - ['metrics'], - ['adhoc_filters'], - ['series_limit'], - ['series_limit_metric'], - ['row_limit'], - [ - { - name: 'whiskerOptions', - config: { - clearable: false, - type: 'SelectControl', - freeForm: true, - label: t('Whisker/outlier options'), - default: 'Tukey', - description: t( - 'Determines how whiskers and outliers are calculated.', - ), - choices: [ - ['Tukey', t('Tukey')], - ['Min/max (no outliers)', t('Min/max (no outliers)')], - ['2/98 percentiles', t('2/98 percentiles')], - ['5/95 percentiles', t('5/95 percentiles')], - ['9/91 percentiles', t('9/91 percentiles')], - ['10/90 percentiles', t('10/90 percentiles')], - ], - }, - }, - ], - ], - }, - sections.titleControls, - { - label: t('Chart Options'), - expanded: true, - controlSetRows: [ - ['color_scheme'], - [ - { - name: 'x_ticks_layout', - config: { - type: 'SelectControl', - label: t('X Tick Layout'), - choices: [ - ['auto', t('auto')], - ['flat', t('flat')], - ['45°', '45°'], - ['90°', '90°'], - ['staggered', t('staggered')], - ], - default: 'auto', - clearable: false, - renderTrigger: true, - description: t('The way the ticks are laid out on the X-axis'), - }, - }, - ], - [ - { - name: 'number_format', - config: { - type: 'SelectControl', - freeForm: true, - label: t('Number format'), - renderTrigger: true, - default: 'SMART_NUMBER', - choices: D3_FORMAT_OPTIONS, - description: `${D3_FORMAT_DOCS} ${D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT}`, - }, - }, - ], - [ - { - name: 'date_format', - config: { - type: 'SelectControl', - freeForm: true, - label: t('Date format'), - renderTrigger: true, - choices: D3_TIME_FORMAT_OPTIONS, - default: 'smart_date', - description: D3_FORMAT_DOCS, - }, - }, - ], - ['zoomable'], - ], - }, - ], - controlOverrides: { - groupby: { - label: t('Dimensions'), - description: t('Categories to group by on the x-axis.'), - }, - columns: { - label: t('Distribute across'), - multi: true, - description: t('Columns to calculate distribution across.'), - initialValue: ( - control: ControlState, - state: ControlPanelState | null, - ) => { - if ( - state && - (!control?.value || - (Array.isArray(control?.value) && control.value.length === 0)) - ) { - return [getTemporalColumns(state.datasource).defaultTemporalColumn]; - } - return control.value; - }, - validators: [validateNonEmpty], - }, - }, - formDataOverrides: formData => { - const groupby = getStandardizedControls().controls.columns.filter( - col => !ensureIsArray(formData.columns).includes(col), - ); - getStandardizedControls().controls.columns = - getStandardizedControls().controls.columns.filter( - col => !groupby.includes(col), - ); - - return { - ...formData, - metrics: getStandardizedControls().popAllMetrics(), - groupby, - }; - }, -}; -export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/index.ts deleted file mode 100644 index cd196b19311..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/index.ts +++ /dev/null @@ -1,70 +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 { t } from '@apache-superset/core/translation'; -import { Behavior } from '@superset-ui/core'; -import buildQuery from './buildQuery'; -import controlPanel from './controlPanel'; -import transformProps from './transformProps'; -import example from './images/BoxPlot.jpg'; -import exampleDark from './images/BoxPlot-dark.jpg'; -import thumbnail from './images/thumbnail.png'; -import thumbnailDark from './images/thumbnail-dark.png'; -import { BoxPlotQueryFormData, EchartsBoxPlotChartProps } from './types'; -import { EchartsChartPlugin } from '../types'; - -export default class EchartsBoxPlotChartPlugin extends EchartsChartPlugin< - BoxPlotQueryFormData, - EchartsBoxPlotChartProps -> { - /** - * The constructor is used to pass relevant metadata and callbacks that get - * registered in respective registries that are used throughout the library - * and application. A more thorough description of each property is given in - * the respective imported file. - * - * It is worth noting that `buildQuery` and is optional, and only needed for - * advanced visualizations that require either post processing operations - * (pivoting, rolling aggregations, sorting etc) or submitting multiple queries. - */ - constructor() { - super({ - buildQuery, - controlPanel, - loadChart: () => import('./EchartsBoxPlot'), - metadata: { - behaviors: [ - Behavior.InteractiveChart, - Behavior.DrillToDetail, - Behavior.DrillBy, - ], - category: t('Distribution'), - credits: ['https://echarts.apache.org'], - description: t( - 'Also known as a box and whisker plot, this visualization compares the distributions of a related metric across multiple groups. The box in the middle emphasizes the mean, median, and inner 2 quartiles. The whiskers around each box visualize the min, max, range, and outer 2 quartiles.', - ), - exampleGallery: [{ url: example, urlDark: exampleDark }], - name: t('Box Plot'), - tags: [t('ECharts'), t('Range'), t('Statistical'), t('Featured')], - thumbnail, - thumbnailDark, - }, - transformProps, - }); - } -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/index.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/index.tsx new file mode 100644 index 00000000000..b5e592b0582 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/index.tsx @@ -0,0 +1,647 @@ +/** + * 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. + */ + +/** + * ECharts Box Plot Chart - Glyph Pattern Implementation + * + * Also known as a box and whisker plot, this visualization compares the + * distributions of a related metric across multiple groups. + */ + +import { t } from '@apache-superset/core/translation'; +import type { EChartsCoreOption } from 'echarts/core'; +import type { BoxplotSeriesOption } from 'echarts/charts'; +import type { CallbackDataParams } from 'echarts/types/src/util/types'; +import { + AdhocColumn, + Behavior, + buildQueryContext, + CategoricalColorNamespace, + DataRecord, + ensureIsArray, + getColumnLabel, + getMetricLabel, + getNumberFormatter, + getTimeFormatter, + isAdhocColumn, + isPhysicalColumn, + QueryFormData, + QueryFormColumn, + SetDataMaskHook, + ContextMenuFilters, + validateNonEmpty, +} from '@superset-ui/core'; +import { + boxplotOperator, + D3_FORMAT_DOCS, + D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT, + sections, + sharedControls, + getStandardizedControls, + ControlState, + ControlPanelState, + getTemporalColumns, +} from '@superset-ui/chart-controls'; + +import { + defineChart, + Metric, + Dimension, + Text, + Select, + Checkbox, + ChartProps, + NumberFormat, + TimeFormat, + createSelectedValuesMap, +} from '@superset-ui/glyph-core'; +import { allEventHandlers } from '../utils/eventHandlers'; + +import { defaultGrid, defaultYAxis } from '../defaults'; +import { + extractGroupbyLabel, + getColtypesMapping, + sanitizeHtml, +} from '../utils/series'; +import { convertInteger } from '../utils/convertInteger'; +import { getPadding } from '../Timeseries/transformers'; +import { OpacityEnum } from '../constants'; +import { getDefaultTooltip } from '../utils/tooltip'; +import Echart from '../components/Echart'; +import { Refs, LegendOrientation } from '../types'; + +import thumbnail from './images/thumbnail.png'; +import thumbnailDark from './images/thumbnail-dark.png'; +import example from './images/BoxPlot.jpg'; +import exampleDark from './images/BoxPlot-dark.jpg'; + +// ============================================================================ +// Constants +// ============================================================================ + +const WHISKER_OPTIONS = [ + { label: t('Tukey'), value: 'Tukey' }, + { label: t('Min/max (no outliers)'), value: 'Min/max (no outliers)' }, + { label: t('2/98 percentiles'), value: '2/98 percentiles' }, + { label: t('5/95 percentiles'), value: '5/95 percentiles' }, + { label: t('9/91 percentiles'), value: '9/91 percentiles' }, + { label: t('10/90 percentiles'), value: '10/90 percentiles' }, +]; + +const X_TICK_LAYOUT_OPTIONS = [ + { label: t('auto'), value: 'auto' }, + { label: t('flat'), value: 'flat' }, + { label: '45°', value: '45°' }, + { label: '90°', value: '90°' }, + { label: t('staggered'), value: 'staggered' }, +]; + +// ============================================================================ +// Types +// ============================================================================ + +interface BoxPlotTransformResult { + transformedProps: { + refs: Refs; + width: number; + height: number; + echartOptions: EChartsCoreOption; + formData: Record; + groupby: QueryFormColumn[]; + labelMap: Record; + setDataMask: SetDataMaskHook; + selectedValues: Record; + emitCrossFilters?: boolean; + onContextMenu?: ( + clientX: number, + clientY: number, + filters?: ContextMenuFilters, + ) => void; + coltypeMapping?: Record; + }; +} + +// ============================================================================ +// Build Query +// ============================================================================ + +export function buildQuery(formData: QueryFormData) { + return buildQueryContext(formData, baseQueryObject => [ + { + ...baseQueryObject, + columns: [ + ...(ensureIsArray(formData.columns).length === 0 && + formData.granularity_sqla + ? [formData.granularity_sqla] + : ensureIsArray(formData.columns) + ).map(col => { + if ( + isPhysicalColumn(col) && + formData.time_grain_sqla && + formData?.temporal_columns_lookup?.[col] + ) { + return { + timeGrain: formData.time_grain_sqla, + columnType: 'BASE_AXIS', + sqlExpression: col, + label: col, + expressionType: 'SQL', + } as AdhocColumn; + } + return col; + }), + ...ensureIsArray(formData.groupby), + ], + series_columns: formData.groupby, + post_processing: [boxplotOperator(formData, baseQueryObject)], + }, + ]); +} + +// ============================================================================ +// Chart Definition +// ============================================================================ + +export default defineChart({ + metadata: { + name: t('Box Plot'), + description: t( + 'Also known as a box and whisker plot, this visualization compares the distributions of a related metric across multiple groups. The box in the middle emphasizes the mean, median, and inner 2 quartiles. The whiskers around each box visualize the min, max, range, and outer 2 quartiles.', + ), + category: t('Distribution'), + tags: [t('ECharts'), t('Range'), t('Statistical'), t('Featured')], + behaviors: [ + Behavior.InteractiveChart, + Behavior.DrillToDetail, + Behavior.DrillBy, + ], + credits: ['https://echarts.apache.org'], + thumbnail, + thumbnailDark, + exampleGallery: [{ url: example, urlDark: exampleDark }], + }, + + arguments: { + // Query section + groupby: Dimension.with({ + label: t('Dimensions'), + description: t('Categories to group by on the x-axis.'), + multi: true, + }), + + metrics: Metric.with({ + label: t('Metrics'), + description: t('Metrics to display'), + multi: true, + }), + + whiskerOptions: Select.with({ + label: t('Whisker/outlier options'), + description: t('Determines how whiskers and outliers are calculated.'), + options: WHISKER_OPTIONS, + default: 'Tukey', + }), + + // Chart options + xTicksLayout: Select.with({ + label: t('X Tick Layout'), + description: t('The way the ticks are laid out on the X-axis'), + options: X_TICK_LAYOUT_OPTIONS, + default: 'auto', + }), + + numberFormat: NumberFormat.with({ + label: t('Number format'), + description: `${D3_FORMAT_DOCS} ${D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT}`, + }), + + dateFormat: TimeFormat.with({ + label: t('Date format'), + description: D3_FORMAT_DOCS, + }), + + zoomable: Checkbox.with({ + label: t('Data Zoom'), + description: t('Enable data zooming controls'), + default: false, + }), + + // Title controls + xAxisTitle: Text.with({ + label: t('X Axis Title'), + description: t('Title for the X axis'), + default: '', + }), + + yAxisTitle: Text.with({ + label: t('Y Axis Title'), + description: t('Title for the Y axis'), + default: '', + }), + }, + + // Complex controls that need mapStateToProps or visibility logic + additionalControls: { + query: [ + ['columns'], + [ + { + name: 'time_grain_sqla', + config: { + ...sharedControls.time_grain_sqla, + visibility: ({ + controls, + }: { + controls: Record; + }) => { + const dttmLookup = Object.fromEntries( + ensureIsArray( + ( + controls?.columns as { + options?: { column_name: string; is_dttm: boolean }[]; + } + )?.options, + ).map(option => [option.column_name, option.is_dttm]), + ); + + return ensureIsArray( + (controls?.columns as { value?: unknown })?.value, + ) + .map(selection => { + if (isAdhocColumn(selection)) { + return true; + } + if (isPhysicalColumn(selection)) { + return !!dttmLookup[selection]; + } + return false; + }) + .some(Boolean); + }, + }, + }, + 'temporal_columns_lookup', + ], + ['adhoc_filters'], + ['series_limit'], + ['series_limit_metric'], + ['row_limit'], + ], + chartOptions: [...sections.titleControls.controlSetRows], + }, + + additionalControlOverrides: { + columns: { + label: t('Distribute across'), + multi: true, + description: t('Columns to calculate distribution across.'), + initialValue: ( + control: ControlState, + state: ControlPanelState | null, + ) => { + if ( + state && + (!control?.value || + (Array.isArray(control?.value) && control.value.length === 0)) + ) { + return [getTemporalColumns(state.datasource).defaultTemporalColumn]; + } + return control.value; + }, + validators: [validateNonEmpty], + }, + }, + + formDataOverrides: (formData: QueryFormData) => { + const groupby = getStandardizedControls().controls.columns.filter( + col => !ensureIsArray(formData.columns).includes(col), + ); + getStandardizedControls().controls.columns = + getStandardizedControls().controls.columns.filter( + col => !groupby.includes(col), + ); + + return { + ...formData, + metrics: getStandardizedControls().popAllMetrics(), + groupby, + }; + }, + + buildQuery, + + transform: (chartProps: ChartProps): BoxPlotTransformResult => { + const { + width, + height, + rawFormData, + hooks, + filterState, + queriesData, + inContextMenu, + emitCrossFilters, + } = chartProps; + + const { data = [] } = queriesData[0]; + const { setDataMask = () => {}, onContextMenu } = hooks ?? {}; + const coltypeMapping = getColtypesMapping( + queriesData[0] as unknown as Parameters[0], + ); + const refs: Refs = {}; + + // Extract form values + const colorScheme = rawFormData.color_scheme as string; + const groupby = (rawFormData.groupby as QueryFormColumn[]) || []; + const metrics = (rawFormData.metrics as string[]) || []; + const numberFormat = + (rawFormData.number_format as string) || 'SMART_NUMBER'; + const dateFormat = (rawFormData.date_format as string) || 'smart_date'; + const xTicksLayout = (rawFormData.x_ticks_layout as string) || 'auto'; + const legendOrientation = + (rawFormData.legend_orientation as LegendOrientation) || 'top'; + const xAxisTitle = (rawFormData.x_axis_title as string) || ''; + const yAxisTitle = (rawFormData.y_axis_title as string) || ''; + const xAxisTitleMargin = rawFormData.x_axis_title_margin as number; + const yAxisTitleMargin = rawFormData.y_axis_title_margin as number; + const yAxisTitlePosition = + (rawFormData.y_axis_title_position as string) || 'Left'; + const sliceId = rawFormData.slice_id as number | undefined; + const zoomable = rawFormData.zoomable as boolean; + + const colorFn = CategoricalColorNamespace.getScale(colorScheme); + const numberFormatter = getNumberFormatter(numberFormat); + const metricLabels = metrics.map(getMetricLabel); + const groupbyLabels = groupby.map(getColumnLabel); + + const transformedData = (data as Record[]) + .map(datum => { + const groupbyLabel = extractGroupbyLabel({ + datum: datum as DataRecord, + groupby: groupbyLabels, + coltypeMapping, + timeFormatter: getTimeFormatter(dateFormat), + }); + return metricLabels.map(metric => { + const name = + metricLabels.length === 1 + ? groupbyLabel + : `${groupbyLabel}, ${metric}`; + const isFiltered = + filterState?.selectedValues && + !filterState.selectedValues.includes(name); + return { + name, + value: [ + datum[`${metric}__min`], + datum[`${metric}__q1`], + datum[`${metric}__median`], + datum[`${metric}__q3`], + datum[`${metric}__max`], + datum[`${metric}__mean`], + datum[`${metric}__count`], + datum[`${metric}__outliers`], + ], + itemStyle: { + color: colorFn(groupbyLabel, sliceId), + opacity: isFiltered ? OpacityEnum.SemiTransparent : 0.6, + borderColor: colorFn(groupbyLabel, sliceId), + }, + }; + }); + }) + .flatMap(row => row); + + const outlierData = (data as Record[]) + .map(datum => + metricLabels.map(metric => { + const groupbyLabel = extractGroupbyLabel({ + datum: datum as DataRecord, + groupby: groupbyLabels, + coltypeMapping, + timeFormatter: getTimeFormatter(dateFormat), + }); + const name = + metricLabels.length === 1 + ? groupbyLabel + : `${groupbyLabel}, ${metric}`; + const outlierDatum = (datum[`${metric}__outliers`] || []) as number[]; + const isFiltered = + filterState?.selectedValues && + !filterState.selectedValues.includes(name); + return { + name: 'outlier', + type: 'scatter', + data: outlierDatum.map(val => [name, val]), + tooltip: { + ...getDefaultTooltip(refs), + formatter: (param: { data: [string, number] }) => { + const [outlierName, stats] = param.data; + const headline = groupbyLabels.length + ? `

${sanitizeHtml(outlierName)}

` + : ''; + return `${headline}${numberFormatter(stats)}`; + }, + }, + itemStyle: { + color: colorFn(groupbyLabel, sliceId), + opacity: isFiltered + ? OpacityEnum.SemiTransparent + : OpacityEnum.NonTransparent, + }, + }; + }), + ) + .flat(2); + + const labelMap: Record = ( + data as Record[] + ).reduce( + (acc: Record, datum) => { + const label = extractGroupbyLabel({ + datum: datum as DataRecord, + groupby: groupbyLabels, + coltypeMapping, + timeFormatter: getTimeFormatter(dateFormat), + }); + return { + ...acc, + [label]: groupbyLabels.map(col => datum[col] as string), + }; + }, + {} as Record, + ) as Record; + + const seriesNames = transformedData.map(d => d.name); + const selectedValues = createSelectedValuesMap(filterState, seriesNames); + + let axisLabel; + if (xTicksLayout === '45°') axisLabel = { rotate: -45 }; + else if (xTicksLayout === '90°') axisLabel = { rotate: -90 }; + else if (xTicksLayout === 'flat') axisLabel = { rotate: 0 }; + else if (xTicksLayout === 'staggered') axisLabel = { rotate: -45 }; + else axisLabel = { show: true }; + + const series: BoxplotSeriesOption[] = [ + { + name: 'boxplot', + type: 'boxplot', + data: transformedData as BoxplotSeriesOption['data'], + tooltip: { + ...getDefaultTooltip(refs), + formatter: (param: CallbackDataParams) => { + const { value, name } = param as unknown as { + value: [ + number, + number, + number, + number, + number, + number, + number, + number, + number[], + ]; + name: string; + }; + const headline = name + ? `

${sanitizeHtml(name)}

` + : ''; + const stats = [ + `Max: ${numberFormatter(value[5])}`, + `3rd Quartile: ${numberFormatter(value[4])}`, + `Mean: ${numberFormatter(value[6])}`, + `Median: ${numberFormatter(value[3])}`, + `1st Quartile: ${numberFormatter(value[2])}`, + `Min: ${numberFormatter(value[1])}`, + `# Observations: ${value[7]}`, + ]; + if (value[8].length > 0) { + stats.push(`# Outliers: ${value[8].length}`); + } + return headline + stats.join('
'); + }, + }, + }, + // @ts-ignore - outlier scatter series + ...outlierData, + ]; + + const addYAxisTitleOffset = !!yAxisTitle; + const addXAxisTitleOffset = !!xAxisTitle; + const chartPadding = getPadding( + true, + legendOrientation, + addYAxisTitleOffset, + false, + null, + addXAxisTitleOffset, + yAxisTitlePosition, + convertInteger(yAxisTitleMargin), + convertInteger(xAxisTitleMargin), + ); + + const echartOptions: EChartsCoreOption = { + grid: { + ...defaultGrid, + ...chartPadding, + }, + xAxis: { + type: 'category', + data: transformedData.map(row => row.name), + axisLabel, + name: xAxisTitle, + nameGap: convertInteger(xAxisTitleMargin), + nameLocation: 'middle', + }, + yAxis: { + ...defaultYAxis, + type: 'value', + axisLabel: { formatter: numberFormatter }, + name: yAxisTitle, + nameGap: convertInteger(yAxisTitleMargin), + nameLocation: yAxisTitlePosition === 'Left' ? 'middle' : 'end', + }, + tooltip: { + ...getDefaultTooltip(refs), + show: !inContextMenu, + trigger: 'item', + axisPointer: { + type: 'shadow', + }, + }, + series, + toolbox: { + show: zoomable, + feature: { + dataZoom: { + title: { + zoom: 'zoom area', + back: 'restore zoom', + }, + }, + }, + }, + dataZoom: zoomable + ? [ + { + type: 'inside', + zoomOnMouseWheel: false, + moveOnMouseWheel: true, + }, + ] + : [], + }; + + return { + transformedProps: { + refs, + width, + height, + echartOptions, + formData: rawFormData, + groupby, + labelMap, + setDataMask, + selectedValues, + emitCrossFilters, + onContextMenu, + coltypeMapping, + }, + }; + }, + + render: ({ transformedProps }) => { + const { height, width, echartOptions, selectedValues, refs, formData } = + transformedProps; + + const eventHandlers = allEventHandlers(transformedProps); + + return ( + + ); + }, +}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/stories/BoxPlot.stories.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/stories/BoxPlot.stories.tsx index 36026d167a3..e8335e10453 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/stories/BoxPlot.stories.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/stories/BoxPlot.stories.tsx @@ -17,11 +17,8 @@ * under the License. */ -import { SuperChart, getChartTransformPropsRegistry } from '@superset-ui/core'; -import { - EchartsBoxPlotChartPlugin, - BoxPlotTransformProps, -} from '@superset-ui/plugin-chart-echarts'; +import { SuperChart } from '@superset-ui/core'; +import { EchartsBoxPlotChartPlugin } from '@superset-ui/plugin-chart-echarts'; import data from './data'; import { withResizableChartDemo } from '@storybook-shared'; @@ -29,11 +26,6 @@ new EchartsBoxPlotChartPlugin() .configure({ key: 'echarts-boxplot' }) .register(); -getChartTransformPropsRegistry().registerValue( - 'echarts-boxplot', - BoxPlotTransformProps, -); - export default { title: 'Chart Plugins/plugin-chart-echarts/BoxPlot', decorators: [withResizableChartDemo], diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/transformProps.ts deleted file mode 100644 index b0aa1e35826..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/transformProps.ts +++ /dev/null @@ -1,326 +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 { - CategoricalColorNamespace, - getColumnLabel, - getMetricLabel, - getNumberFormatter, - getTimeFormatter, -} from '@superset-ui/core'; -import type { EChartsCoreOption } from 'echarts/core'; -import type { BoxplotSeriesOption } from 'echarts/charts'; -import type { CallbackDataParams } from 'echarts/types/src/util/types'; -import { - BoxPlotChartTransformedProps, - BoxPlotQueryFormData, - EchartsBoxPlotChartProps, -} from './types'; -import { - extractGroupbyLabel, - getColtypesMapping, - sanitizeHtml, -} from '../utils/series'; -import { convertInteger } from '../utils/convertInteger'; -import { defaultGrid, defaultYAxis } from '../defaults'; -import { getPadding } from '../Timeseries/transformers'; -import { OpacityEnum } from '../constants'; -import { getDefaultTooltip } from '../utils/tooltip'; -import { Refs } from '../types'; - -export default function transformProps( - chartProps: EchartsBoxPlotChartProps, -): BoxPlotChartTransformedProps { - const { - width, - height, - formData, - hooks, - filterState, - queriesData, - inContextMenu, - emitCrossFilters, - } = chartProps; - const { data = [] } = queriesData[0]; - const { setDataMask = () => {}, onContextMenu } = hooks; - const coltypeMapping = getColtypesMapping(queriesData[0]); - const { - colorScheme, - groupby = [], - metrics = [], - numberFormat, - dateFormat, - xTicksLayout, - legendOrientation = 'top', - xAxisTitle, - yAxisTitle, - xAxisTitleMargin, - yAxisTitleMargin, - yAxisTitlePosition, - sliceId, - zoomable, - } = formData as BoxPlotQueryFormData; - const refs: Refs = {}; - const colorFn = CategoricalColorNamespace.getScale(colorScheme as string); - const numberFormatter = getNumberFormatter(numberFormat); - const metricLabels = metrics.map(getMetricLabel); - const groupbyLabels = groupby.map(getColumnLabel); - - const transformedData = data - .map((datum: any) => { - const groupbyLabel = extractGroupbyLabel({ - datum, - groupby: groupbyLabels, - coltypeMapping, - timeFormatter: getTimeFormatter(dateFormat), - }); - return metricLabels.map(metric => { - const name = - metricLabels.length === 1 - ? groupbyLabel - : `${groupbyLabel}, ${metric}`; - const isFiltered = - filterState.selectedValues && - !filterState.selectedValues.includes(name); - return { - name, - value: [ - datum[`${metric}__min`], - datum[`${metric}__q1`], - datum[`${metric}__median`], - datum[`${metric}__q3`], - datum[`${metric}__max`], - datum[`${metric}__mean`], - datum[`${metric}__count`], - datum[`${metric}__outliers`], - ], - itemStyle: { - color: colorFn(groupbyLabel, sliceId), - opacity: isFiltered ? OpacityEnum.SemiTransparent : 0.6, - borderColor: colorFn(groupbyLabel, sliceId), - }, - }; - }); - }) - .flatMap(row => row); - const outlierData = data - .map(datum => - metricLabels.map(metric => { - const groupbyLabel = extractGroupbyLabel({ - datum, - groupby: groupbyLabels, - coltypeMapping, - timeFormatter: getTimeFormatter(dateFormat), - }); - const name = - metricLabels.length === 1 - ? groupbyLabel - : `${groupbyLabel}, ${metric}`; - // Outlier data is a nested array of numbers (uncommon, therefore no need to add to DataRecordValue) - const outlierDatum = (datum[`${metric}__outliers`] || []) as number[]; - const isFiltered = - filterState.selectedValues && - !filterState.selectedValues.includes(name); - return { - name: 'outlier', - type: 'scatter', - data: outlierDatum.map(val => [name, val]), - tooltip: { - ...getDefaultTooltip(refs), - formatter: (param: { data: [string, number] }) => { - const [outlierName, stats] = param.data; - const headline = groupbyLabels.length - ? `

${sanitizeHtml(outlierName)}

` - : ''; - return `${headline}${numberFormatter(stats)}`; - }, - }, - itemStyle: { - color: colorFn(groupbyLabel, sliceId), - opacity: isFiltered - ? OpacityEnum.SemiTransparent - : OpacityEnum.NonTransparent, - }, - }; - }), - ) - .flat(2); - - const labelMap = data.reduce((acc: Record, datum) => { - const label = extractGroupbyLabel({ - datum, - groupby: groupbyLabels, - coltypeMapping, - timeFormatter: getTimeFormatter(dateFormat), - }); - return { - ...acc, - [label]: groupbyLabels.map(col => datum[col] as string), - }; - }, {}); - - const selectedValues = (filterState.selectedValues || []).reduce( - (acc: Record, selectedValue: string) => { - const index = transformedData.findIndex( - ({ name }) => name === selectedValue, - ); - return { - ...acc, - [index]: selectedValue, - }; - }, - {}, - ); - - let axisLabel; - if (xTicksLayout === '45°') axisLabel = { rotate: -45 }; - else if (xTicksLayout === '90°') axisLabel = { rotate: -90 }; - else if (xTicksLayout === 'flat') axisLabel = { rotate: 0 }; - else if (xTicksLayout === 'staggered') axisLabel = { rotate: -45 }; - else axisLabel = { show: true }; - - const series: BoxplotSeriesOption[] = [ - { - name: 'boxplot', - type: 'boxplot', - data: transformedData, - tooltip: { - ...getDefaultTooltip(refs), - formatter: (param: CallbackDataParams) => { - // @ts-expect-error - const { - value, - name, - }: { - value: [ - number, - number, - number, - number, - number, - number, - number, - number, - number[], - ]; - name: string; - } = param; - const headline = name - ? `

${sanitizeHtml(name)}

` - : ''; - const stats = [ - `Max: ${numberFormatter(value[5])}`, - `3rd Quartile: ${numberFormatter(value[4])}`, - `Mean: ${numberFormatter(value[6])}`, - `Median: ${numberFormatter(value[3])}`, - `1st Quartile: ${numberFormatter(value[2])}`, - `Min: ${numberFormatter(value[1])}`, - `# Observations: ${value[7]}`, - ]; - if (value[8].length > 0) { - stats.push(`# Outliers: ${value[8].length}`); - } - return headline + stats.join('
'); - }, - }, - }, - // @ts-expect-error - ...outlierData, - ]; - const addYAxisTitleOffset = - !!yAxisTitle && convertInteger(yAxisTitleMargin) !== 0; - const addXAxisTitleOffset = - !!xAxisTitle && convertInteger(xAxisTitleMargin) !== 0; - const chartPadding = getPadding( - true, - legendOrientation, - addYAxisTitleOffset, - false, - null, - addXAxisTitleOffset, - yAxisTitlePosition, - convertInteger(yAxisTitleMargin), - convertInteger(xAxisTitleMargin), - ); - const echartOptions: EChartsCoreOption = { - grid: { - ...defaultGrid, - ...chartPadding, - }, - xAxis: { - type: 'category', - data: transformedData.map(row => row.name), - axisLabel, - name: xAxisTitle, - nameGap: convertInteger(xAxisTitleMargin), - nameLocation: 'middle', - }, - yAxis: { - ...defaultYAxis, - type: 'value', - axisLabel: { formatter: numberFormatter }, - name: yAxisTitle, - nameGap: convertInteger(yAxisTitleMargin), - nameLocation: yAxisTitlePosition === 'Left' ? 'middle' : 'end', - }, - tooltip: { - ...getDefaultTooltip(refs), - show: !inContextMenu, - trigger: 'item', - axisPointer: { - type: 'shadow', - }, - }, - series, - toolbox: { - show: zoomable, - feature: { - dataZoom: { - title: { - zoom: 'zoom area', - back: 'restore zoom', - }, - }, - }, - }, - dataZoom: zoomable - ? [ - { - type: 'inside', - zoomOnMouseWheel: false, - moveOnMouseWheel: true, - }, - ] - : [], - }; - - return { - formData, - width, - height, - echartOptions, - setDataMask, - emitCrossFilters, - labelMap, - groupby, - selectedValues, - onContextMenu, - refs, - coltypeMapping, - }; -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/types.ts deleted file mode 100644 index 84c9c0c5a18..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/types.ts +++ /dev/null @@ -1,62 +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 { QueryFormData } from '@superset-ui/core'; -import { - BaseChartProps, - BaseTransformedProps, - ContextMenuTransformedProps, - CrossFilterTransformedProps, - TitleFormData, -} from '../types'; -import { DEFAULT_TITLE_FORM_DATA } from '../constants'; - -export type BoxPlotQueryFormData = QueryFormData & { - numberFormat?: string; - whiskerOptions?: BoxPlotFormDataWhiskerOptions; - xTickLayout?: BoxPlotFormXTickLayout; -} & TitleFormData; - -export type BoxPlotFormDataWhiskerOptions = - | 'Tukey' - | 'Min/max (no outliers)' - | '2/98 percentiles' - | '5/95 percentiles' - | '9/91 percentiles' - | '10/90 percentiles'; - -export type BoxPlotFormXTickLayout = - | '45°' - | '90°' - | 'auto' - | 'flat' - | 'staggered'; - -// @ts-expect-error -export const DEFAULT_FORM_DATA: BoxPlotQueryFormData = { - ...DEFAULT_TITLE_FORM_DATA, -}; - -export interface EchartsBoxPlotChartProps extends BaseChartProps { - formData: BoxPlotQueryFormData; -} - -export type BoxPlotChartTransformedProps = - BaseTransformedProps & - CrossFilterTransformedProps & - ContextMenuTransformedProps; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/EchartsBubble.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/EchartsBubble.tsx deleted file mode 100644 index adfa0acfe30..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/EchartsBubble.tsx +++ /dev/null @@ -1,33 +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 { BubbleChartTransformedProps } from './types'; -import Echart from '../components/Echart'; - -export default function EchartsBubble(props: BubbleChartTransformedProps) { - const { height, width, echartOptions, refs, formData } = props; - return ( - - ); -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/buildQuery.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/buildQuery.ts deleted file mode 100644 index 31cdc0d9f60..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/buildQuery.ts +++ /dev/null @@ -1,40 +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 { - buildQueryContext, - ensureIsArray, - QueryFormData, -} from '@superset-ui/core'; - -export default function buildQuery(formData: QueryFormData) { - const columns = [ - ...ensureIsArray(formData.entity), - ...ensureIsArray(formData.series), - ]; - - return buildQueryContext(formData, baseQueryObject => [ - { - ...baseQueryObject, - columns, - orderby: baseQueryObject.orderby - ? [[baseQueryObject.orderby[0], !baseQueryObject.order_desc]] - : undefined, - }, - ]); -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/constants.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/constants.ts deleted file mode 100644 index 83d5449ea59..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/constants.ts +++ /dev/null @@ -1,39 +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 { DEFAULT_LEGEND_FORM_DATA } from '../constants'; -import { defaultXAxis } from '../defaults'; -import { EchartsBubbleFormData } from './types'; - -export const DEFAULT_FORM_DATA: Partial = { - ...DEFAULT_LEGEND_FORM_DATA, - emitFilter: false, - logXAis: false, - logYAxis: false, - xAxisTitleMargin: 40, - yAxisTitleMargin: 50, - truncateXAxis: false, - truncateYAxis: false, - xAxisBounds: [null, null], - yAxisBounds: [null, null], - xAxisLabelRotation: defaultXAxis.xAxisLabelRotation, - xAxisLabelInterval: defaultXAxis.xAxisLabelInterval, - opacity: 0.6, -}; - -export const MINIMUM_BUBBLE_SIZE = 5; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/controlPanel.tsx deleted file mode 100644 index f0aa4a228fa..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/controlPanel.tsx +++ /dev/null @@ -1,277 +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 { t } from '@apache-superset/core/translation'; -import { - ControlPanelConfig, - formatSelectOptions, - sections, - ControlPanelsContainerProps, - sharedControls, -} from '@superset-ui/chart-controls'; - -import { DEFAULT_FORM_DATA } from './constants'; -import { - legendSection, - truncateXAxis, - xAxisBounds, - xAxisLabelRotation, - xAxisLabelInterval, -} from '../controls'; -import { defaultYAxis } from '../defaults'; - -const { logAxis, truncateYAxis, yAxisBounds, opacity } = DEFAULT_FORM_DATA; - -const config: ControlPanelConfig = { - controlPanelSections: [ - { - label: t('Query'), - expanded: true, - controlSetRows: [ - ['series'], - ['entity'], - ['x'], - ['y'], - ['adhoc_filters'], - ['size'], - ['orderby'], - [ - { - name: 'order_desc', - config: { - ...sharedControls.order_desc, - visibility: ({ controls }) => Boolean(controls.orderby.value), - }, - }, - ], - ['row_limit'], - ], - }, - { - label: t('Chart Options'), - expanded: true, - tabOverride: 'customize', - controlSetRows: [ - ['color_scheme'], - ...legendSection, - [ - { - name: 'max_bubble_size', - config: { - type: 'SelectControl', - renderTrigger: true, - freeForm: true, - label: t('Max Bubble Size'), - default: '25', - choices: formatSelectOptions([ - '5', - '10', - '15', - '25', - '50', - '75', - '100', - ]), - }, - }, - ], - [ - { - name: 'tooltipSizeFormat', - config: { - ...sharedControls.y_axis_format, - label: t('Bubble size number format'), - }, - }, - ], - [ - { - name: 'opacity', - config: { - type: 'SliderControl', - label: t('Bubble Opacity'), - renderTrigger: true, - min: 0, - max: 1, - step: 0.1, - default: opacity, - description: t( - 'Opacity of bubbles, 0 means completely transparent, 1 means opaque', - ), - }, - }, - ], - ], - }, - { - label: t('X Axis'), - expanded: true, - controlSetRows: [ - [ - { - name: 'x_axis_label', - config: { - type: 'TextControl', - label: t('X Axis Title'), - renderTrigger: true, - default: '', - }, - }, - ], - [xAxisLabelRotation], - [xAxisLabelInterval], - [ - { - name: 'x_axis_title_margin', - config: { - type: 'SelectControl', - freeForm: true, - clearable: true, - label: t('X axis title margin'), - renderTrigger: true, - default: sections.TITLE_MARGIN_OPTIONS[3], - choices: formatSelectOptions(sections.TITLE_MARGIN_OPTIONS), - }, - }, - ], - [ - { - name: 'xAxisFormat', - config: { - ...sharedControls.y_axis_format, - label: t('X Axis Format'), - }, - }, - ], - [ - { - name: 'logXAxis', - config: { - type: 'CheckboxControl', - label: t('Logarithmic x-axis'), - renderTrigger: true, - default: logAxis, - description: t('Logarithmic x-axis'), - }, - }, - ], - ], - }, - { - label: t('Y Axis'), - expanded: true, - controlSetRows: [ - [ - { - name: 'y_axis_label', - config: { - type: 'TextControl', - label: t('Y Axis Title'), - renderTrigger: true, - default: '', - }, - }, - ], - [ - { - name: 'yAxisLabelRotation', - config: { - type: 'SelectControl', - freeForm: true, - clearable: false, - label: t('Rotate y axis label'), - choices: [ - [0, '0°'], - [45, '45°'], - ], - default: defaultYAxis.yAxisLabelRotation, - renderTrigger: true, - description: t( - 'Input field supports custom rotation. e.g. 30 for 30°', - ), - }, - }, - ], - [ - { - name: 'y_axis_title_margin', - config: { - type: 'SelectControl', - freeForm: true, - clearable: true, - label: t('Y axis title margin'), - renderTrigger: true, - default: sections.TITLE_MARGIN_OPTIONS[4], - choices: formatSelectOptions(sections.TITLE_MARGIN_OPTIONS), - }, - }, - ], - ['y_axis_format'], - [ - { - name: 'logYAxis', - config: { - type: 'CheckboxControl', - label: t('Logarithmic y-axis'), - renderTrigger: true, - default: logAxis, - description: t('Logarithmic y-axis'), - }, - }, - ], - [truncateXAxis], - [xAxisBounds], - [ - { - name: 'truncateYAxis', - config: { - type: 'CheckboxControl', - label: t('Truncate Y Axis'), - default: truncateYAxis, - renderTrigger: true, - description: t( - 'Truncate Y Axis. Can be overridden by specifying a min or max bound.', - ), - }, - }, - ], - [ - { - name: 'y_axis_bounds', - config: { - type: 'BoundsControl', - label: t('Y Axis Bounds'), - renderTrigger: true, - default: yAxisBounds, - description: t( - 'Bounds for the Y-axis. When left empty, the bounds are ' + - 'dynamically defined based on the min/max of the data. Note that ' + - "this feature will only expand the axis range. It won't " + - "narrow the data's extent.", - ), - visibility: ({ controls }: ControlPanelsContainerProps) => - Boolean(controls?.truncateYAxis?.value), - }, - }, - ], - ], - }, - ], -}; - -export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/index.ts deleted file mode 100644 index 5795acd3cd4..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/index.ts +++ /dev/null @@ -1,68 +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 { t } from '@apache-superset/core/translation'; -import { ChartMetadata, ChartPlugin } from '@superset-ui/core'; -import thumbnail from './images/thumbnail.png'; -import thumbnailDark from './images/thumbnail-dark.png'; -import transformProps from './transformProps'; -import buildQuery from './buildQuery'; -import controlPanel from './controlPanel'; -import example1 from './images/example1.png'; -import example1Dark from './images/example1-dark.png'; -import example2 from './images/example2.png'; -import example2Dark from './images/example2-dark.png'; -import { EchartsBubbleChartProps, EchartsBubbleFormData } from './types'; - -// TODO: Implement cross filtering -export default class EchartsBubbleChartPlugin extends ChartPlugin< - EchartsBubbleFormData, - EchartsBubbleChartProps -> { - constructor() { - super({ - buildQuery, - controlPanel, - loadChart: () => import('./EchartsBubble'), - metadata: new ChartMetadata({ - category: t('Correlation'), - credits: ['https://echarts.apache.org'], - description: t( - 'Visualizes a metric across three dimensions of data in a single chart (X axis, Y axis, and bubble size). Bubbles from the same group can be showcased using bubble color.', - ), - exampleGallery: [ - { url: example1, urlDark: example1Dark }, - { url: example2, urlDark: example2Dark }, - ], - name: t('Bubble Chart'), - tags: [ - t('Multi-Dimensions'), - t('Comparison'), - t('Scatter'), - t('Time'), - t('Trend'), - t('ECharts'), - t('Featured'), - ], - thumbnail, - thumbnailDark, - }), - transformProps, - }); - } -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/index.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/index.tsx new file mode 100644 index 00000000000..03aa154f608 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/index.tsx @@ -0,0 +1,664 @@ +/** + * 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. + */ + +/** + * ECharts Bubble Chart - Glyph Pattern Implementation + * + * Visualizes a metric across three dimensions of data (X axis, Y axis, bubble size). + * Bubbles from the same group can be showcased using bubble color. + */ + +import { t } from '@apache-superset/core/translation'; +import type { EChartsCoreOption } from 'echarts/core'; +import type { ScatterSeriesOption } from 'echarts/charts'; +import { extent } from 'd3-array'; +import { + Behavior, + buildQueryContext, + CategoricalColorNamespace, + ensureIsArray, + getMetricLabel, + getNumberFormatter, + NumberFormatter, + QueryFormData, + QueryFormColumn, + SetDataMaskHook, + ContextMenuFilters, + tooltipHtml, + AxisType, +} from '@superset-ui/core'; + +import { + defineChart, + Metric, + Dimension, + Text, + Checkbox, + Select, + Slider, + Bounds, + ChartProps, + NumberFormat, + ShowLegend, + LegendType as LegendTypePreset, + LegendOrientation as LegendOrientationPreset, + LegendSort, + BoundsValue, + // Cross-filter utilities + isDataPointFiltered, + createSelectedValuesMap, +} from '@superset-ui/glyph-core'; +import { allEventHandlers } from '../utils/eventHandlers'; + +import { defaultGrid } from '../defaults'; +import { getLegendProps, getMinAndMaxFromBounds } from '../utils/series'; +import { parseAxisBound } from '../utils/controls'; +import { getDefaultTooltip } from '../utils/tooltip'; +import { getPadding } from '../Timeseries/transformers'; +import { convertInteger } from '../utils/convertInteger'; +import Echart from '../components/Echart'; +import { Refs, LegendOrientation, LegendType } from '../types'; +import { OpacityEnum, NULL_STRING } from '../constants'; + +import thumbnail from './images/thumbnail.png'; +import thumbnailDark from './images/thumbnail-dark.png'; +import example1 from './images/example1.png'; +import example1Dark from './images/example1-dark.png'; +import example2 from './images/example2.png'; +import example2Dark from './images/example2-dark.png'; + +// ============================================================================ +// Constants +// ============================================================================ + +const MINIMUM_BUBBLE_SIZE = 5; + +const MAX_BUBBLE_SIZE_OPTIONS = [ + { label: '5', value: '5' }, + { label: '10', value: '10' }, + { label: '15', value: '15' }, + { label: '25', value: '25' }, + { label: '50', value: '50' }, + { label: '75', value: '75' }, + { label: '100', value: '100' }, +]; + +const AXIS_TITLE_MARGIN_OPTIONS = [ + { label: '15', value: 15 }, + { label: '30', value: 30 }, + { label: '45', value: 45 }, + { label: '60', value: 60 }, + { label: '75', value: 75 }, + { label: '90', value: 90 }, +]; + +const X_AXIS_ROTATION_OPTIONS = [ + { label: '0°', value: 0 }, + { label: '45°', value: 45 }, +]; + +const Y_AXIS_ROTATION_OPTIONS = [ + { label: '0°', value: 0 }, + { label: '45°', value: 45 }, +]; + +// ============================================================================ +// Types +// ============================================================================ + +interface BubbleTransformResult { + transformedProps: { + refs: Refs; + width: number; + height: number; + echartOptions: EChartsCoreOption; + formData: Record; + // Cross-filter props + groupby: QueryFormColumn[]; + labelMap: Record; + setDataMask: SetDataMaskHook; + selectedValues: Record; + emitCrossFilters?: boolean; + onContextMenu?: ( + clientX: number, + clientY: number, + filters?: ContextMenuFilters, + ) => void; + coltypeMapping?: Record; + }; +} + +// ============================================================================ +// Helper Functions +// ============================================================================ + +const isIterable = (obj: unknown): obj is Iterable => + obj != null && + typeof (obj as Iterable)[Symbol.iterator] === 'function'; + +function normalizeSymbolSize( + nodes: ScatterSeriesOption[], + maxBubbleValue: number, +) { + const [bubbleMinValue, bubbleMaxValue] = extent( + nodes, + x => { + const tmpValue = x.data?.[0]; + const result = isIterable(tmpValue) ? (tmpValue as number[])[2] : null; + if (typeof result === 'number') { + return result; + } + return null; + }, + ); + if (bubbleMinValue !== undefined && bubbleMaxValue !== undefined) { + const nodeSpread = bubbleMaxValue - bubbleMinValue; + nodes.forEach(node => { + const tmpValue = node.data?.[0]; + const calculated = isIterable(tmpValue) + ? (tmpValue as number[])[2] + : null; + if (typeof calculated === 'number') { + // eslint-disable-next-line no-param-reassign + node.symbolSize = + (((calculated - bubbleMinValue) / nodeSpread) * + (maxBubbleValue * 2) || 0) + MINIMUM_BUBBLE_SIZE; + } + }); + } +} + +function formatTooltip( + params: { data: (string | number | null)[] }, + xAxisLabel: string, + yAxisLabel: string, + sizeLabel: string, + xAxisFormatter: NumberFormatter, + yAxisFormatter: NumberFormatter, + tooltipSizeFormatter: NumberFormatter, +) { + const title = params.data[4] + ? `${params.data[4]} (${params.data[3]})` + : String(params.data[3]); + + return tooltipHtml( + [ + [xAxisLabel, xAxisFormatter(params.data[0] as number)], + [yAxisLabel, yAxisFormatter(params.data[1] as number)], + [sizeLabel, tooltipSizeFormatter(params.data[2] as number)], + ], + title, + ); +} + +// ============================================================================ +// Build Query - exported for testing +// ============================================================================ + +export function buildQuery(formData: QueryFormData) { + const columns = [ + ...ensureIsArray(formData.entity), + ...ensureIsArray(formData.series), + ]; + + return buildQueryContext(formData, baseQueryObject => [ + { + ...baseQueryObject, + columns, + orderby: baseQueryObject.orderby + ? [[baseQueryObject.orderby[0], !baseQueryObject.order_desc]] + : undefined, + }, + ]); +} + +// ============================================================================ +// Chart Definition +// ============================================================================ + +export default defineChart({ + metadata: { + name: t('Bubble Chart'), + description: t( + 'Visualizes a metric across three dimensions of data in a single chart (X axis, Y axis, and bubble size). Bubbles from the same group can be showcased using bubble color.', + ), + category: t('Correlation'), + tags: [ + t('Multi-Dimensions'), + t('Comparison'), + t('Scatter'), + t('Time'), + t('Trend'), + t('ECharts'), + t('Featured'), + ], + behaviors: [ + Behavior.InteractiveChart, + Behavior.DrillToDetail, + Behavior.DrillBy, + ], + credits: ['https://echarts.apache.org'], + thumbnail, + thumbnailDark, + exampleGallery: [ + { url: example1, urlDark: example1Dark }, + { url: example2, urlDark: example2Dark }, + ], + }, + + arguments: { + // Query section + series: Dimension.with({ + label: t('Series'), + description: t('Dimension to group bubbles by color'), + multi: false, + }), + + entity: Dimension.with({ + label: t('Entity'), + description: t('Entity column for bubble identification'), + multi: false, + }), + + x: Metric.with({ + label: t('X Axis'), + description: t('Metric for X axis position'), + multi: false, + }), + + y: Metric.with({ + label: t('Y Axis'), + description: t('Metric for Y axis position'), + multi: false, + }), + + size: Metric.with({ + label: t('Bubble Size'), + description: t('Metric that determines bubble size'), + multi: false, + }), + + // Chart Options + maxBubbleSize: Select.with({ + label: t('Max Bubble Size'), + description: t('Maximum size of the bubbles'), + options: MAX_BUBBLE_SIZE_OPTIONS, + default: '25', + }), + + tooltipSizeFormat: NumberFormat.with({ + label: t('Bubble size number format'), + description: t('Number format for bubble size in tooltip'), + }), + + opacity: Slider.with({ + label: t('Bubble Opacity'), + description: t( + 'Opacity of bubbles, 0 means completely transparent, 1 means opaque', + ), + default: 0.6, + min: 0, + max: 1, + step: 0.1, + }), + + // Legend + showLegend: ShowLegend, + legendType: LegendTypePreset, + legendOrientation: LegendOrientationPreset, + legendSort: LegendSort, + + // X Axis + xAxisLabel: Text.with({ + label: t('X Axis Title'), + description: t('Title for the X axis'), + default: '', + }), + + xAxisLabelRotation: Select.with({ + label: t('Rotate x axis label'), + description: t('Rotation angle for X axis labels'), + options: X_AXIS_ROTATION_OPTIONS, + default: 0, + }), + + xAxisTitleMargin: Select.with({ + label: t('X axis title margin'), + description: t('Margin for X axis title'), + options: AXIS_TITLE_MARGIN_OPTIONS, + default: 30, + }), + + xAxisFormat: NumberFormat.with({ + label: t('X Axis Format'), + description: t('Number format for X axis'), + }), + + logXAxis: Checkbox.with({ + label: t('Logarithmic x-axis'), + description: t('Use logarithmic scale for X axis'), + default: false, + }), + + truncateXAxis: Checkbox.with({ + label: t('Truncate X Axis'), + description: t( + 'Truncate X Axis. Can be overridden by specifying a min or max bound.', + ), + default: false, + }), + + xAxisBounds: Bounds.with({ + label: t('X Axis Bounds'), + description: t( + 'Bounds for the X-axis. When left empty, the bounds are dynamically defined based on the min/max of the data.', + ), + default: [null, null], + }), + + // Y Axis + yAxisLabel: Text.with({ + label: t('Y Axis Title'), + description: t('Title for the Y axis'), + default: '', + }), + + yAxisLabelRotation: Select.with({ + label: t('Rotate y axis label'), + description: t('Rotation angle for Y axis labels'), + options: Y_AXIS_ROTATION_OPTIONS, + default: 0, + }), + + yAxisTitleMargin: Select.with({ + label: t('Y axis title margin'), + description: t('Margin for Y axis title'), + options: AXIS_TITLE_MARGIN_OPTIONS, + default: 30, + }), + + yAxisFormat: NumberFormat.with({ + label: t('Y Axis Format'), + description: t('Number format for Y axis'), + }), + + logYAxis: Checkbox.with({ + label: t('Logarithmic y-axis'), + description: t('Use logarithmic scale for Y axis'), + default: false, + }), + + truncateYAxis: Checkbox.with({ + label: t('Truncate Y Axis'), + description: t( + 'Truncate Y Axis. Can be overridden by specifying a min or max bound.', + ), + default: false, + }), + + yAxisBounds: Bounds.with({ + label: t('Y Axis Bounds'), + description: t( + 'Bounds for the Y-axis. When left empty, the bounds are dynamically defined based on the min/max of the data.', + ), + default: [null, null], + }), + }, + + buildQuery, + + transform: (chartProps: ChartProps): BubbleTransformResult => { + const { + height, + width, + queriesData, + rawFormData, + inContextMenu, + theme, + filterState, + hooks, + emitCrossFilters, + } = chartProps; + + const { data = [] } = queriesData[0]; + const refs: Refs = {}; + const { setDataMask = () => {}, onContextMenu } = hooks ?? {}; + + // Extract form values + const x = rawFormData.x as string; + const y = rawFormData.y as string; + const size = rawFormData.size as string; + const entity = rawFormData.entity as string; + const maxBubbleSize = Number(rawFormData.max_bubble_size || '25'); + const colorScheme = rawFormData.color_scheme as string; + const bubbleSeries = rawFormData.series as string | undefined; + const bubbleXAxisTitle = (rawFormData.x_axis_label as string) || ''; + const bubbleYAxisTitle = (rawFormData.y_axis_label as string) || ''; + const xAxisBounds = (rawFormData.x_axis_bounds as BoundsValue) || [ + null, + null, + ]; + const xAxisFormat = (rawFormData.x_axis_format as string) || 'SMART_NUMBER'; + const yAxisFormat = (rawFormData.y_axis_format as string) || 'SMART_NUMBER'; + const yAxisBounds = (rawFormData.y_axis_bounds as BoundsValue) || [ + null, + null, + ]; + const logXAxis = rawFormData.log_x_axis as boolean; + const logYAxis = rawFormData.log_y_axis as boolean; + const xAxisTitleMargin = rawFormData.x_axis_title_margin as number; + const yAxisTitleMargin = rawFormData.y_axis_title_margin as number; + const truncateXAxis = rawFormData.truncate_x_axis as boolean; + const truncateYAxis = rawFormData.truncate_y_axis as boolean; + const xAxisLabelRotation = rawFormData.x_axis_label_rotation as number; + const yAxisLabelRotation = rawFormData.y_axis_label_rotation as number; + const tooltipSizeFormat = + (rawFormData.tooltip_size_format as string) || 'SMART_NUMBER'; + const opacity = (rawFormData.opacity as number) ?? 0.6; + const showLegend = rawFormData.show_legend as boolean; + const legendOrientation = rawFormData.legend_orientation as string; + const legendMargin = rawFormData.legend_margin as number; + const legendType = rawFormData.legend_type as string; + const legendSort = rawFormData.legend_sort as string; + const sliceId = rawFormData.slice_id as number | undefined; + + const colorFn = CategoricalColorNamespace.getScale(colorScheme as string); + + const legends = new Set(); + const series: ScatterSeriesOption[] = []; + const seriesNames: string[] = []; + + const xAxisLabel: string = getMetricLabel(x); + const yAxisLabel: string = getMetricLabel(y); + const sizeLabel: string = getMetricLabel(size); + + // Build groupby from series and/or entity columns for cross-filtering + const groupby: QueryFormColumn[] = []; + if (bubbleSeries) { + groupby.push(bubbleSeries); + } + + // Build labelMap for cross-filtering + const labelMap: Record = {}; + + data.forEach((datum: Record) => { + const dataName = bubbleSeries ? datum[bubbleSeries] : datum[entity]; + const name = dataName ? String(dataName) : NULL_STRING; + const bubbleSeriesValue = bubbleSeries ? datum[bubbleSeries] : null; + + // Build labelMap entry + if (bubbleSeries) { + labelMap[name] = [String(datum[bubbleSeries] ?? '')]; + } + + // Check if this data point is filtered + const isFiltered = isDataPointFiltered(filterState, name); + + series.push({ + name, + data: [ + [ + datum[xAxisLabel] as number, + datum[yAxisLabel] as number, + datum[sizeLabel] as number, + datum[entity] as string | number, + bubbleSeriesValue as string | number | null, + ], + ], + type: 'scatter', + itemStyle: { + color: colorFn(name, sliceId), + opacity: isFiltered ? OpacityEnum.SemiTransparent : opacity, + }, + }); + legends.add(name); + seriesNames.push(name); + }); + + // Create selectedValues map for cross-filtering + const selectedValues = createSelectedValuesMap(filterState, seriesNames); + + normalizeSymbolSize(series, maxBubbleSize); + + const xAxisFormatter = getNumberFormatter(xAxisFormat); + const yAxisFormatter = getNumberFormatter(yAxisFormat); + const tooltipSizeFormatter = getNumberFormatter(tooltipSizeFormat); + + const [xAxisMin, xAxisMax] = (xAxisBounds || []).map(parseAxisBound); + const [yAxisMin, yAxisMax] = (yAxisBounds || []).map(parseAxisBound); + + const padding = getPadding( + showLegend, + legendOrientation as LegendOrientation, + true, + false, + legendMargin, + true, + 'Left', + convertInteger(yAxisTitleMargin), + convertInteger(xAxisTitleMargin), + ); + + const xAxisType = logXAxis ? AxisType.Log : AxisType.Value; + const echartOptions: EChartsCoreOption = { + series, + xAxis: { + axisLabel: { formatter: xAxisFormatter }, + splitLine: { + lineStyle: { + type: 'dashed', + }, + }, + nameRotate: xAxisLabelRotation, + scale: true, + name: bubbleXAxisTitle, + nameLocation: 'middle', + nameTextStyle: { + fontWeight: 'bolder', + }, + nameGap: convertInteger(xAxisTitleMargin), + type: xAxisType, + ...getMinAndMaxFromBounds(xAxisType, truncateXAxis, xAxisMin, xAxisMax), + }, + yAxis: { + axisLabel: { formatter: yAxisFormatter }, + splitLine: { + lineStyle: { + type: 'dashed', + }, + }, + nameRotate: yAxisLabelRotation, + scale: truncateYAxis, + name: bubbleYAxisTitle, + nameLocation: 'middle', + nameTextStyle: { + fontWeight: 'bolder', + }, + nameGap: convertInteger(yAxisTitleMargin), + min: yAxisMin, + max: yAxisMax, + type: logYAxis ? AxisType.Log : AxisType.Value, + }, + legend: { + ...getLegendProps( + legendType as LegendType, + legendOrientation as LegendOrientation, + showLegend, + theme, + ), + data: Array.from(legends).sort((a: string, b: string) => { + if (!legendSort) return 0; + return legendSort === 'asc' ? a.localeCompare(b) : b.localeCompare(a); + }), + }, + tooltip: { + show: !inContextMenu, + ...getDefaultTooltip(refs), + formatter: (params: unknown): string => + formatTooltip( + params as { data: (string | number | null)[] }, + xAxisLabel, + yAxisLabel, + sizeLabel, + xAxisFormatter, + yAxisFormatter, + tooltipSizeFormatter, + ), + }, + grid: { ...defaultGrid, ...padding }, + }; + + return { + transformedProps: { + refs, + height, + width, + echartOptions, + formData: rawFormData, + // Cross-filter props + groupby, + labelMap, + setDataMask, + selectedValues, + emitCrossFilters, + onContextMenu, + }, + }; + }, + + render: ({ transformedProps }) => { + const { height, width, echartOptions, refs, formData, selectedValues } = + transformedProps; + + // Use allEventHandlers for cross-filtering support + const eventHandlers = allEventHandlers(transformedProps); + + return ( + + ); + }, +}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/stories/BubbleChart.stories.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/stories/BubbleChart.stories.tsx index 14fa1d50b24..9e213e8d1e1 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/stories/BubbleChart.stories.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/stories/BubbleChart.stories.tsx @@ -16,25 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -import { - EchartsBubbleChartPlugin, - BubbleTransformProps, -} from '@superset-ui/plugin-chart-echarts'; -import { - SuperChart, - VizType, - getChartTransformPropsRegistry, -} from '@superset-ui/core'; +import { EchartsBubbleChartPlugin } from '@superset-ui/plugin-chart-echarts'; +import { SuperChart, VizType } from '@superset-ui/core'; import { simpleBubbleData } from './data'; import { withResizableChartDemo } from '@storybook-shared'; new EchartsBubbleChartPlugin().configure({ key: VizType.Bubble }).register(); -getChartTransformPropsRegistry().registerValue( - VizType.Bubble, - BubbleTransformProps, -); - export default { title: 'Chart Plugins/plugin-chart-echarts', decorators: [withResizableChartDemo], diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/transformProps.ts deleted file mode 100644 index 2ffeaebf2ab..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/transformProps.ts +++ /dev/null @@ -1,282 +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 type { EChartsCoreOption } from 'echarts/core'; -import type { ScatterSeriesOption } from 'echarts/charts'; -import { extent } from 'd3-array'; -import { - CategoricalColorNamespace, - getNumberFormatter, - AxisType, - getMetricLabel, - NumberFormatter, - tooltipHtml, -} from '@superset-ui/core'; -import { EchartsBubbleChartProps, EchartsBubbleFormData } from './types'; -import { DEFAULT_FORM_DATA, MINIMUM_BUBBLE_SIZE } from './constants'; -import { defaultGrid } from '../defaults'; -import { getLegendProps, getMinAndMaxFromBounds } from '../utils/series'; -import { resolveLegendLayout } from '../utils/legendLayout'; -import { Refs } from '../types'; -import { parseAxisBound } from '../utils/controls'; -import { getDefaultTooltip } from '../utils/tooltip'; -import { getPadding } from '../Timeseries/transformers'; -import { convertInteger } from '../utils/convertInteger'; -import { NULL_STRING } from '../constants'; - -const isIterable = (obj: any): obj is Iterable => - obj != null && typeof obj[Symbol.iterator] === 'function'; - -function normalizeSymbolSize( - nodes: ScatterSeriesOption[], - maxBubbleValue: number, -) { - const [bubbleMinValue, bubbleMaxValue] = extent( - nodes, - x => { - const tmpValue = x.data?.[0]; - const result = isIterable(tmpValue) ? tmpValue[2] : null; - if (typeof result === 'number') { - return result; - } - return null; - }, - ); - if (bubbleMinValue !== undefined && bubbleMaxValue !== undefined) { - const nodeSpread = bubbleMaxValue - bubbleMinValue; - nodes.forEach(node => { - const tmpValue = node.data?.[0]; - const calculated = isIterable(tmpValue) ? tmpValue[2] : null; - if (typeof calculated === 'number') { - // eslint-disable-next-line no-param-reassign - node.symbolSize = - (((calculated - bubbleMinValue) / nodeSpread) * - (maxBubbleValue * 2) || 0) + MINIMUM_BUBBLE_SIZE; - } - }); - } -} - -export function formatTooltip( - params: any, - xAxisLabel: string, - yAxisLabel: string, - sizeLabel: string, - xAxisFormatter: NumberFormatter, - yAxisFormatter: NumberFormatter, - tooltipSizeFormatter: NumberFormatter, -) { - const title = params.data[4] - ? `${params.data[4]} (${params.data[3]})` - : params.data[3]; - - return tooltipHtml( - [ - [xAxisLabel, xAxisFormatter(params.data[0])], - [yAxisLabel, yAxisFormatter(params.data[1])], - [sizeLabel, tooltipSizeFormatter(params.data[2])], - ], - title, - ); -} - -export default function transformProps(chartProps: EchartsBubbleChartProps) { - const { height, width, hooks, queriesData, formData, inContextMenu, theme } = - chartProps; - - const { data = [] } = queriesData[0]; - const { - x, - y, - size, - entity, - maxBubbleSize, - colorScheme, - series: bubbleSeries, - xAxisLabel: bubbleXAxisTitle, - yAxisLabel: bubbleYAxisTitle, - xAxisBounds, - xAxisFormat, - yAxisFormat, - yAxisBounds, - logXAxis, - logYAxis, - xAxisTitleMargin, - yAxisTitleMargin, - truncateXAxis, - truncateYAxis, - xAxisLabelRotation, - xAxisLabelInterval, - yAxisLabelRotation, - tooltipSizeFormat, - opacity, - showLegend, - legendOrientation, - legendMargin, - legendType, - legendSort, - sliceId, - }: EchartsBubbleFormData = { ...DEFAULT_FORM_DATA, ...formData }; - const colorFn = CategoricalColorNamespace.getScale(colorScheme as string); - - const legends = new Set(); - const series: ScatterSeriesOption[] = []; - - const xAxisLabel: string = getMetricLabel(x); - const yAxisLabel: string = getMetricLabel(y); - const sizeLabel: string = getMetricLabel(size); - - const refs: Refs = {}; - - data.forEach(datum => { - const dataName = bubbleSeries ? datum[bubbleSeries] : datum[entity]; - const name = dataName ? String(dataName) : NULL_STRING; - const bubbleSeriesValue = bubbleSeries ? datum[bubbleSeries] : null; - - series.push({ - name, - data: [ - [ - datum[xAxisLabel], - datum[yAxisLabel], - datum[sizeLabel], - datum[entity], - bubbleSeriesValue as any, - ], - ], - type: 'scatter', - itemStyle: { - color: colorFn(name, sliceId), - opacity, - }, - }); - legends.add(name); - }); - - normalizeSymbolSize(series, maxBubbleSize); - - const xAxisFormatter = getNumberFormatter(xAxisFormat); - const yAxisFormatter = getNumberFormatter(yAxisFormat); - const tooltipSizeFormatter = getNumberFormatter(tooltipSizeFormat); - const legendData = Array.from(legends).sort((a: string, b: string) => { - if (!legendSort) return 0; - return legendSort === 'asc' ? a.localeCompare(b) : b.localeCompare(a); - }); - const { effectiveLegendMargin, effectiveLegendType } = resolveLegendLayout({ - chartHeight: height, - chartWidth: width, - legendItems: legendData, - legendMargin, - orientation: legendOrientation, - show: showLegend, - theme, - type: legendType, - }); - - const [xAxisMin, xAxisMax] = (xAxisBounds || []).map(parseAxisBound); - const [yAxisMin, yAxisMax] = (yAxisBounds || []).map(parseAxisBound); - - const padding = getPadding( - showLegend, - legendOrientation, - true, - false, - effectiveLegendMargin, - true, - 'Left', - convertInteger(yAxisTitleMargin), - convertInteger(xAxisTitleMargin), - ); - - const xAxisType = logXAxis ? AxisType.Log : AxisType.Value; - const echartOptions: EChartsCoreOption = { - series, - xAxis: { - axisLabel: { formatter: xAxisFormatter, rotate: xAxisLabelRotation }, - splitLine: { - lineStyle: { - type: 'dashed', - }, - }, - interval: xAxisLabelInterval, - scale: true, - name: bubbleXAxisTitle, - nameLocation: 'middle', - nameTextStyle: { - fontWeight: 'bolder', - }, - nameGap: convertInteger(xAxisTitleMargin), - type: xAxisType, - ...getMinAndMaxFromBounds(xAxisType, truncateXAxis, xAxisMin, xAxisMax), - }, - yAxis: { - axisLabel: { formatter: yAxisFormatter, rotate: yAxisLabelRotation }, - splitLine: { - lineStyle: { - type: 'dashed', - }, - }, - scale: truncateYAxis, - name: bubbleYAxisTitle, - nameLocation: 'middle', - nameTextStyle: { - fontWeight: 'bolder', - }, - nameGap: convertInteger(yAxisTitleMargin), - min: yAxisMin, - max: yAxisMax, - type: logYAxis ? AxisType.Log : AxisType.Value, - }, - legend: { - ...getLegendProps( - effectiveLegendType, - legendOrientation, - showLegend, - theme, - ), - data: legendData, - }, - tooltip: { - show: !inContextMenu, - ...getDefaultTooltip(refs), - formatter: (params: any): string => - formatTooltip( - params, - xAxisLabel, - yAxisLabel, - sizeLabel, - xAxisFormatter, - yAxisFormatter, - tooltipSizeFormatter, - ), - }, - grid: { ...defaultGrid, ...padding }, - }; - - const { onContextMenu, setDataMask = () => {} } = hooks; - - return { - refs, - height, - width, - echartOptions, - onContextMenu, - setDataMask, - formData, - }; -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/types.ts deleted file mode 100644 index db4fc6002b2..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Bubble/types.ts +++ /dev/null @@ -1,56 +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 { - ChartProps, - ChartDataResponseResult, - QueryFormData, -} from '@superset-ui/core'; -import { - LegendFormData, - BaseTransformedProps, - CrossFilterTransformedProps, -} from '../types'; - -export type EchartsBubbleFormData = QueryFormData & - LegendFormData & { - series?: string; - entity: string; - xAxisFormat: string; - yAXisFormat: string; - logXAxis: boolean; - logYAxis: boolean; - xAxisBounds: [number | undefined | null, number | undefined | null]; - yAxisBounds: [number | undefined | null, number | undefined | null]; - xAxisLabel?: string; - colorScheme?: string; - defaultValue?: string[] | null; - dateFormat: string; - emitFilter: boolean; - tooltipFormat: string; - x: string; - y: string; - }; - -export interface EchartsBubbleChartProps extends ChartProps { - formData: EchartsBubbleFormData; - queriesData: ChartDataResponseResult[]; -} - -export type BubbleChartTransformedProps = - BaseTransformedProps & CrossFilterTransformedProps; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/EchartsFunnel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/EchartsFunnel.tsx deleted file mode 100644 index c3b2c81a12a..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/EchartsFunnel.tsx +++ /dev/null @@ -1,40 +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 { FunnelChartTransformedProps } from './types'; -import Echart from '../components/Echart'; -import { allEventHandlers } from '../utils/eventHandlers'; - -export default function EchartsFunnel(props: FunnelChartTransformedProps) { - const { height, width, echartOptions, selectedValues, refs, formData } = - props; - - const eventHandlers = allEventHandlers(props); - - return ( - - ); -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/buildQuery.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/buildQuery.ts deleted file mode 100644 index 8b47fb5e725..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/buildQuery.ts +++ /dev/null @@ -1,29 +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 { buildQueryContext, QueryFormData } from '@superset-ui/core'; - -export default function buildQuery(formData: QueryFormData) { - const { metric, sort_by_metric } = formData; - return buildQueryContext(formData, baseQueryObject => [ - { - ...baseQueryObject, - ...(sort_by_metric && { orderby: [[metric, false]] }), - }, - ]); -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/controlPanel.tsx deleted file mode 100644 index 67910b1b71a..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/controlPanel.tsx +++ /dev/null @@ -1,206 +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 { t } from '@apache-superset/core/translation'; -import { - ControlPanelConfig, - ControlSubSectionHeader, - D3_FORMAT_DOCS, - D3_FORMAT_OPTIONS, - D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT, - getStandardizedControls, - sharedControls, -} from '@superset-ui/chart-controls'; -import { - DEFAULT_FORM_DATA, - EchartsFunnelLabelType, - PercentCalcType, -} from './types'; -import { legendSection } from '../controls'; - -const { labelType, numberFormat, showLabels, defaultTooltipLabel } = - DEFAULT_FORM_DATA; - -const funnelLegendSection = [...legendSection]; -funnelLegendSection.splice(2, 1); - -const config: ControlPanelConfig = { - controlPanelSections: [ - { - label: t('Query'), - expanded: true, - controlSetRows: [ - ['groupby'], - ['metric'], - ['adhoc_filters'], - [ - { - name: 'row_limit', - config: { - ...sharedControls.row_limit, - default: 10, - }, - }, - ], - [ - { - name: 'sort_by_metric', - config: { - ...sharedControls.sort_by_metric, - default: true, - }, - }, - ], - [ - { - name: 'percent_calculation_type', - config: { - type: 'SelectControl', - label: t('% calculation'), - description: t( - 'Display percents in the label and tooltip as the percent of the total value, from the first step of the funnel, or from the previous step in the funnel.', - ), - choices: [ - [PercentCalcType.FirstStep, t('Calculate from first step')], - [ - PercentCalcType.PreviousStep, - t('Calculate from previous step'), - ], - [PercentCalcType.Total, t('Percent of total')], - ], - default: PercentCalcType.FirstStep, - renderTrigger: true, - }, - }, - ], - ], - }, - { - label: t('Chart Options'), - expanded: true, - controlSetRows: [ - ['color_scheme'], - ...funnelLegendSection, - // eslint-disable-next-line react/jsx-key - [{t('Labels')}], - [ - { - name: 'label_type', - config: { - type: 'SelectControl', - label: t('Label Contents'), - default: labelType, - renderTrigger: true, - choices: [ - [EchartsFunnelLabelType.Key, t('Category Name')], - [EchartsFunnelLabelType.Value, t('Value')], - [EchartsFunnelLabelType.Percent, t('Percentage')], - [EchartsFunnelLabelType.KeyValue, t('Category and Value')], - [ - EchartsFunnelLabelType.KeyPercent, - t('Category and Percentage'), - ], - [ - EchartsFunnelLabelType.KeyValuePercent, - t('Category, Value and Percentage'), - ], - [ - EchartsFunnelLabelType.ValuePercent, - t('Value and Percentage'), - ], - ], - description: t('What should be shown as the label'), - }, - }, - ], - [ - { - name: 'tooltip_label_type', - config: { - type: 'SelectControl', - label: t('Tooltip Contents'), - default: defaultTooltipLabel, - renderTrigger: true, - choices: [ - [EchartsFunnelLabelType.Key, t('Category Name')], - [EchartsFunnelLabelType.Value, t('Value')], - [EchartsFunnelLabelType.Percent, t('Percentage')], - [EchartsFunnelLabelType.KeyValue, t('Category and Value')], - [ - EchartsFunnelLabelType.KeyPercent, - t('Category and Percentage'), - ], - [ - EchartsFunnelLabelType.KeyValuePercent, - t('Category, Value and Percentage'), - ], - ], - description: t('What should be shown as the tooltip label'), - }, - }, - ], - [ - { - name: 'number_format', - config: { - type: 'SelectControl', - freeForm: true, - label: t('Number format'), - renderTrigger: true, - default: numberFormat, - choices: D3_FORMAT_OPTIONS, - description: `${D3_FORMAT_DOCS} ${D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT}`, - }, - }, - ], - ['currency_format'], - [ - { - name: 'show_labels', - config: { - type: 'CheckboxControl', - label: t('Show Labels'), - renderTrigger: true, - default: showLabels, - description: t('Whether to display the labels.'), - }, - }, - ], - [ - { - name: 'show_tooltip_labels', - config: { - type: 'CheckboxControl', - label: t('Show Tooltip Labels'), - renderTrigger: true, - default: showLabels, - description: t('Whether to display the tooltip labels.'), - }, - }, - ], - ], - }, - ], - formDataOverrides: formData => ({ - ...formData, - metric: getStandardizedControls().shiftMetric(), - groupby: getStandardizedControls().popAllColumns(), - }), -}; - -export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/index.ts deleted file mode 100644 index 056443ecfb9..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/index.ts +++ /dev/null @@ -1,78 +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 { t } from '@apache-superset/core/translation'; -import { Behavior } from '@superset-ui/core'; -import buildQuery from './buildQuery'; -import controlPanel from './controlPanel'; -import transformProps from './transformProps'; -import thumbnail from './images/thumbnail.png'; -import thumbnailDark from './images/thumbnail-dark.png'; -import example from './images/example.jpg'; -import exampleDark from './images/example-dark.jpg'; -import { EchartsFunnelChartProps, EchartsFunnelFormData } from './types'; -import { EchartsChartPlugin } from '../types'; - -export default class EchartsFunnelChartPlugin extends EchartsChartPlugin< - EchartsFunnelFormData, - EchartsFunnelChartProps -> { - /** - * The constructor is used to pass relevant metadata and callbacks that get - * registered in respective registries that are used throughout the library - * and application. A more thorough description of each property is given in - * the respective imported file. - * - * It is worth noting that `buildQuery` and is optional, and only needed for - * advanced visualizations that require either post processing operations - * (pivoting, rolling aggregations, sorting etc) or submitting multiple queries. - */ - constructor() { - super({ - buildQuery, - controlPanel, - loadChart: () => import('./EchartsFunnel'), - metadata: { - behaviors: [ - Behavior.InteractiveChart, - Behavior.DrillToDetail, - Behavior.DrillBy, - ], - category: t('KPI'), - credits: ['https://echarts.apache.org'], - description: t( - 'Showcases how a metric changes as the funnel progresses. This classic chart is useful for visualizing drop-off between stages in a pipeline or lifecycle.', - ), - exampleGallery: [{ url: example, urlDark: exampleDark }], - name: t('Funnel Chart'), - tags: [ - t('Business'), - t('ECharts'), - t('Progressive'), - t('Report'), - t('Sequential'), - t('Trend'), - t('Featured'), - ], - thumbnail, - thumbnailDark, - }, - transformProps, - }); - } -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/index.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/index.tsx new file mode 100644 index 00000000000..02d4169c517 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/index.tsx @@ -0,0 +1,594 @@ +/** + * 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. + */ + +/** + * ECharts Funnel Chart - Glyph Pattern Implementation + * + * Showcases how a metric changes as the funnel progresses. + * Useful for visualizing drop-off between stages in a pipeline. + */ + +import { t } from '@apache-superset/core/translation'; +import { + Behavior, + buildQueryContext, + CategoricalColorNamespace, + Currency as CurrencyType, + DataRecord, + getColumnLabel, + getMetricLabel, + getNumberFormatter, + getValueFormatter, + NumberFormats, + QueryFormData, + tooltipHtml, + ValueFormatter, + VizType, +} from '@superset-ui/core'; +import type { CallbackDataParams } from 'echarts/types/src/util/types'; +import type { EChartsCoreOption } from 'echarts/core'; +import type { FunnelSeriesOption } from 'echarts/charts'; + +import { + defineChart, + Metric, + Dimension, + Select, + Checkbox, + Int, + NumberFormat, + Currency, + ChartProps, + // Presets + ShowLegend, + ShowLabels, + LabelType, + LegendType as LegendTypeArg, + LegendOrientation as LegendOrientationArg, + LegendSort as LegendSortArg, + SortByMetric, + LABEL_TYPE_OPTIONS, + SORT_OPTIONS, +} from '@superset-ui/glyph-core'; + +import { OpacityEnum } from '../constants'; +import { + extractGroupbyLabel, + getChartPadding, + getColtypesMapping, + getLegendProps, + sanitizeHtml, +} from '../utils/series'; +import { defaultGrid } from '../defaults'; +import { getDefaultTooltip } from '../utils/tooltip'; +import { allEventHandlers } from '../utils/eventHandlers'; +import Echart from '../components/Echart'; +import { Refs, LegendOrientation, LegendType } from '../types'; +import { + EchartsFunnelFormData, + EchartsFunnelLabelType, + FunnelChartTransformedProps, + PercentCalcType, +} from './types'; + +import thumbnail from './images/thumbnail.png'; +import example from './images/example.jpg'; +import exampleDark from './images/example-dark.jpg'; + +// ============================================================================ +// Constants & Helpers +// ============================================================================ + +const percentFormatter = getNumberFormatter(NumberFormats.PERCENT_2_POINT); + +const PERCENT_CALC_OPTIONS = [ + { label: t('Calculate from first step'), value: PercentCalcType.FirstStep }, + { + label: t('Calculate from previous step'), + value: PercentCalcType.PreviousStep, + }, + { label: t('Percent of total'), value: PercentCalcType.Total }, +]; + +const ORIENT_OPTIONS = [ + { label: t('Vertical'), value: 'vertical' }, + { label: t('Horizontal'), value: 'horizontal' }, +]; + +// Map string label types to enum values for formatter +const LABEL_TYPE_MAP: Record = { + key: EchartsFunnelLabelType.Key, + value: EchartsFunnelLabelType.Value, + percent: EchartsFunnelLabelType.Percent, + key_value: EchartsFunnelLabelType.KeyValue, + key_percent: EchartsFunnelLabelType.KeyPercent, + key_value_percent: EchartsFunnelLabelType.KeyValuePercent, + value_percent: EchartsFunnelLabelType.ValuePercent, +}; + +export function parseParams({ + params, + numberFormatter, + percentCalculationType = PercentCalcType.FirstStep, + sanitizeName = false, +}: { + params: Pick; + numberFormatter: ValueFormatter; + percentCalculationType?: PercentCalcType; + sanitizeName?: boolean; +}) { + const { name: rawName = '', value, percent: totalPercent, data } = params; + const name = sanitizeName ? sanitizeHtml(rawName) : rawName; + const formattedValue = numberFormatter(value as number); + const { firstStepPercent, prevStepPercent } = data as { + firstStepPercent: number; + prevStepPercent: number; + }; + let percent; + + if (percentCalculationType === PercentCalcType.Total) { + percent = (totalPercent ?? 0) / 100; + } else if (percentCalculationType === PercentCalcType.PreviousStep) { + percent = prevStepPercent ?? 0; + } else { + percent = firstStepPercent ?? 0; + } + const formattedPercent = percentFormatter(percent); + return [name, formattedValue, formattedPercent]; +} + +// ============================================================================ +// Build Query - exported for testing +// ============================================================================ + +export function buildQuery(formData: QueryFormData) { + const { metric, sort_by_metric: sortByMetric } = formData; + return buildQueryContext(formData, baseQueryObject => [ + { + ...baseQueryObject, + ...(sortByMetric && { orderby: [[metric, false]] }), + }, + ]); +} + +// ============================================================================ +// Transform Result Type +// ============================================================================ + +interface FunnelTransformResult { + transformedProps: FunnelChartTransformedProps; +} + +// ============================================================================ +// The Chart Definition +// ============================================================================ + +export default defineChart< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + any, + FunnelTransformResult +>({ + metadata: { + name: t('Funnel Chart'), + description: t( + 'Showcases how a metric changes as the funnel progresses. This classic chart is useful for visualizing drop-off between stages in a pipeline or lifecycle.', + ), + category: t('KPI'), + tags: [ + t('Business'), + t('ECharts'), + t('Progressive'), + t('Report'), + t('Sequential'), + t('Trend'), + t('Featured'), + ], + thumbnail, + behaviors: [ + Behavior.InteractiveChart, + Behavior.DrillToDetail, + Behavior.DrillBy, + ], + exampleGallery: [{ url: example, urlDark: exampleDark }], + }, + + arguments: { + // Query section + groupby: Dimension.with({ + label: t('Dimensions'), + description: t('Columns to group by'), + }), + + metric: Metric.with({ + label: t('Metric'), + description: t('The metric used to determine funnel size'), + }), + + sortByMetric: SortByMetric, + + percentCalculationType: Select.with({ + label: t('% Calculation'), + description: t( + 'Display percents in the label and tooltip as the percent of the total value, from the first step of the funnel, or from the previous step in the funnel.', + ), + options: PERCENT_CALC_OPTIONS, + default: PercentCalcType.FirstStep, + }), + + // Chart options + sort: Select.with({ + label: t('Sort'), + description: t('How to sort the funnel stages'), + options: SORT_OPTIONS, + default: 'descending', + }), + + orient: Select.with({ + label: t('Orientation'), + description: t('Funnel orientation'), + options: ORIENT_OPTIONS, + default: 'vertical', + }), + + gap: Int.with({ + label: t('Gap'), + description: t('Gap between funnel segments'), + default: 0, + min: 0, + max: 50, + step: 1, + }), + + // Legend section + showLegend: ShowLegend, + + legendType: { arg: LegendTypeArg, visibleWhen: { showLegend: true } }, + legendOrientation: { + arg: LegendOrientationArg, + visibleWhen: { showLegend: true }, + }, + + legendMargin: { + arg: Int.with({ + label: t('Legend Margin'), + description: t('Additional padding for legend'), + default: 0, + min: 0, + max: 100, + step: 1, + }), + visibleWhen: { showLegend: true }, + }, + + legendSort: { arg: LegendSortArg, visibleWhen: { showLegend: true } }, + + // Label section + labelType: LabelType, + + tooltipLabelType: Select.with({ + label: t('Tooltip Contents'), + description: t('What should be shown in the tooltip?'), + options: LABEL_TYPE_OPTIONS, + default: 'key_value_percent', + }), + + numberFormat: NumberFormat, + currencyFormat: Currency, + + showLabels: ShowLabels, + + labelLine: { + arg: Checkbox.with({ + label: t('Label Line'), + description: t('Draw line from funnel to label'), + default: false, + }), + visibleWhen: { showLabels: true }, + }, + + showTooltipLabels: Checkbox.with({ + label: t('Show Tooltip Labels'), + description: t('Whether to display the tooltip labels'), + default: true, + }), + }, + + buildQuery, + + transform: (chartProps: ChartProps): FunnelTransformResult => { + const { + formData, + height, + hooks, + filterState, + queriesData, + width, + theme, + emitCrossFilters, + datasource, + inContextMenu, + } = chartProps; + + const rawFormData = formData as Record; + const data: DataRecord[] = (queriesData[0]?.data as DataRecord[]) || []; + const { colnames = [], coltypes = [] } = + (queriesData[0] as { colnames?: string[]; coltypes?: number[] }) ?? {}; + const coltypeMapping = getColtypesMapping({ colnames, coltypes }); + const { columnFormats = {}, currencyFormats = {} } = datasource ?? {}; + + // Extract form values + const colorScheme = rawFormData.color_scheme as string; + const groupby = (rawFormData.groupby as string[]) || []; + const orient = rawFormData.orient as 'vertical' | 'horizontal' | undefined; + const sort = rawFormData.sort as + | 'descending' + | 'ascending' + | 'none' + | undefined; + const gap = (rawFormData.gap as number) ?? 0; + const labelLine = (rawFormData.label_line as boolean) ?? false; + const labelType = + LABEL_TYPE_MAP[rawFormData.label_type as string] ?? + EchartsFunnelLabelType.Key; + const tooltipLabelType = + LABEL_TYPE_MAP[rawFormData.tooltip_label_type as string] ?? + EchartsFunnelLabelType.KeyValuePercent; + const legendMargin = (rawFormData.legend_margin as number) ?? 0; + const legendOrientation = + (rawFormData.legend_orientation as LegendOrientation) ?? + LegendOrientation.Top; + const legendType = + (rawFormData.legend_type as LegendType) ?? LegendType.Scroll; + const legendSort = (rawFormData.legend_sort as string) ?? ''; + const metric = (rawFormData.metric as string) ?? ''; + const numberFormat = + (rawFormData.number_format as string) ?? 'SMART_NUMBER'; + const currencyFormat = rawFormData.currency_format as + | CurrencyType + | undefined; + const showLabels = (rawFormData.show_labels as boolean) ?? true; + const showTooltipLabels = + (rawFormData.show_tooltip_labels as boolean) ?? true; + const showLegend = (rawFormData.show_legend as boolean) ?? true; + const sliceId = rawFormData.slice_id as number | undefined; + const percentCalculationType = + (rawFormData.percent_calculation_type as PercentCalcType) ?? + PercentCalcType.FirstStep; + + const refs: Refs = {}; + const metricLabel = getMetricLabel(metric); + const groupbyLabels = groupby.map(getColumnLabel); + const keys = data.map(datum => + extractGroupbyLabel({ + datum, + groupby: groupbyLabels, + coltypeMapping: {}, + }), + ); + const labelMap = data.reduce( + (acc: Record, datum: DataRecord) => { + const label = extractGroupbyLabel({ + datum, + groupby: groupbyLabels, + coltypeMapping: {}, + }); + return { + ...acc, + [label]: groupbyLabels.map((col: string) => datum[col] as string), + }; + }, + {}, + ); + + const { setDataMask = () => {}, onContextMenu } = hooks ?? {}; + const colorFn = CategoricalColorNamespace.getScale(colorScheme as string); + const numberFormatter = getValueFormatter( + metric, + currencyFormats, + columnFormats, + numberFormat, + currencyFormat, + ); + + const transformedData: { + value: number; + name: string; + itemStyle: { color: string; opacity: OpacityEnum }; + firstStepPercent: number; + prevStepPercent: number; + }[] = data.map((datum: DataRecord, index: number) => { + const name = extractGroupbyLabel({ + datum, + groupby: groupbyLabels, + coltypeMapping: {}, + }); + const value = datum[metricLabel] as number; + const isFiltered = + filterState?.selectedValues && + !filterState.selectedValues.includes(name); + const firstStepPercent = value / (data[0][metricLabel] as number); + const prevStepPercent = + index === 0 ? 1 : value / (data[index - 1][metricLabel] as number); + return { + value, + name, + itemStyle: { + color: colorFn(name, sliceId), + opacity: isFiltered + ? OpacityEnum.SemiTransparent + : OpacityEnum.NonTransparent, + }, + firstStepPercent, + prevStepPercent, + }; + }); + + const selectedValues = (filterState?.selectedValues || []).reduce( + (acc: Record, selectedValue: string) => { + const index = transformedData.findIndex( + ({ name }) => name === selectedValue, + ); + return { + ...acc, + [index]: selectedValue, + }; + }, + {}, + ); + + const formatter = (params: CallbackDataParams) => { + const [name, formattedValue, formattedPercent] = parseParams({ + params, + numberFormatter, + percentCalculationType, + }); + switch (labelType) { + case EchartsFunnelLabelType.Key: + return name; + case EchartsFunnelLabelType.Value: + return formattedValue; + case EchartsFunnelLabelType.Percent: + return formattedPercent; + case EchartsFunnelLabelType.KeyValue: + return `${name}: ${formattedValue}`; + case EchartsFunnelLabelType.KeyValuePercent: + return `${name}: ${formattedValue} (${formattedPercent})`; + case EchartsFunnelLabelType.KeyPercent: + return `${name}: ${formattedPercent}`; + case EchartsFunnelLabelType.ValuePercent: + return `${formattedValue} (${formattedPercent})`; + default: + return name; + } + }; + + const defaultLabel = { + formatter, + show: showLabels, + // eslint-disable-next-line theme-colors/no-literal-colors + color: (theme as { colorText?: string })?.colorText ?? '#000', + // eslint-disable-next-line theme-colors/no-literal-colors + textBorderColor: + (theme as { colorBgBase?: string })?.colorBgBase ?? '#fff', + textBorderWidth: 1, + }; + + const series: FunnelSeriesOption[] = [ + { + type: VizType.Funnel, + ...getChartPadding(showLegend, legendOrientation, legendMargin), + animation: true, + minSize: '0%', + maxSize: '100%', + sort, + orient, + gap, + funnelAlign: 'center', + labelLine: { show: !!labelLine }, + label: { + ...defaultLabel, + position: labelLine ? 'outer' : 'inner', + }, + emphasis: { + label: { + show: true, + fontWeight: 'bold', + }, + }, + // @ts-ignore + data: transformedData, + }, + ]; + + const echartOptions: EChartsCoreOption = { + grid: { + ...defaultGrid, + }, + tooltip: { + ...getDefaultTooltip(refs), + show: !inContextMenu && showTooltipLabels, + trigger: 'item', + formatter: (params: CallbackDataParams) => { + const [name, formattedValue, formattedPercent] = parseParams({ + params, + numberFormatter, + percentCalculationType, + }); + const row = []; + const enumName = EchartsFunnelLabelType[tooltipLabelType]; + const title = enumName.includes('Key') ? name : undefined; + if (enumName.includes('Value') || enumName.includes('Percent')) { + row.push(metricLabel); + } + if (enumName.includes('Value')) { + row.push(formattedValue); + } + if (enumName.includes('Percent')) { + row.push(formattedPercent); + } + return tooltipHtml([row], title); + }, + }, + legend: { + ...getLegendProps(legendType, legendOrientation, showLegend, theme), + data: keys.sort((a: string, b: string) => { + if (!legendSort) return 0; + return legendSort === 'asc' ? a.localeCompare(b) : b.localeCompare(a); + }), + }, + series, + }; + + return { + transformedProps: { + formData: formData as EchartsFunnelFormData, + width, + height, + echartOptions, + setDataMask, + emitCrossFilters, + labelMap, + groupby, + selectedValues, + onContextMenu, + refs, + coltypeMapping, + }, + }; + }, + + render: ({ transformedProps }) => { + const { height, width, echartOptions, selectedValues, refs, formData } = + transformedProps; + + const eventHandlers = allEventHandlers(transformedProps); + + return ( + + ); + }, +}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/stories/Funnel.stories.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/stories/Funnel.stories.tsx index 01843322903..f842e493723 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/stories/Funnel.stories.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/stories/Funnel.stories.tsx @@ -17,25 +17,13 @@ * under the License. */ -import { - SuperChart, - VizType, - getChartTransformPropsRegistry, -} from '@superset-ui/core'; -import { - EchartsFunnelChartPlugin, - FunnelTransformProps, -} from '@superset-ui/plugin-chart-echarts'; +import { SuperChart, VizType } from '@superset-ui/core'; +import { EchartsFunnelChartPlugin } from '@superset-ui/plugin-chart-echarts'; import { dataSource } from './constants'; import { withResizableChartDemo } from '@storybook-shared'; new EchartsFunnelChartPlugin().configure({ key: VizType.Funnel }).register(); -getChartTransformPropsRegistry().registerValue( - VizType.Funnel, - FunnelTransformProps, -); - export default { title: 'Chart Plugins/plugin-chart-echarts/Funnel', decorators: [withResizableChartDemo], diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts deleted file mode 100644 index 87189c355a3..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts +++ /dev/null @@ -1,341 +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 { - CategoricalColorNamespace, - DataRecord, - getColumnLabel, - getMetricLabel, - getNumberFormatter, - getValueFormatter, - NumberFormats, - tooltipHtml, - ValueFormatter, - VizType, -} from '@superset-ui/core'; -import type { CallbackDataParams } from 'echarts/types/src/util/types'; -import type { EChartsCoreOption } from 'echarts/core'; -import type { FunnelSeriesOption } from 'echarts/charts'; -import { - DEFAULT_FORM_DATA as DEFAULT_FUNNEL_FORM_DATA, - EchartsFunnelChartProps, - EchartsFunnelFormData, - EchartsFunnelLabelType, - FunnelChartTransformedProps, - PercentCalcType, -} from './types'; -import { - extractGroupbyLabel, - getChartPadding, - getColtypesMapping, - getLegendProps, - sanitizeHtml, -} from '../utils/series'; -import { resolveLegendLayout } from '../utils/legendLayout'; -import { defaultGrid } from '../defaults'; -import { DEFAULT_LEGEND_FORM_DATA, OpacityEnum } from '../constants'; -import { getDefaultTooltip } from '../utils/tooltip'; -import { Refs } from '../types'; - -const percentFormatter = getNumberFormatter(NumberFormats.PERCENT_2_POINT); - -export function parseParams({ - params, - numberFormatter, - percentCalculationType = PercentCalcType.FirstStep, - sanitizeName = false, -}: { - params: Pick; - numberFormatter: ValueFormatter; - percentCalculationType?: PercentCalcType; - sanitizeName?: boolean; -}) { - const { name: rawName = '', value, percent: totalPercent, data } = params; - const name = sanitizeName ? sanitizeHtml(rawName) : rawName; - const formattedValue = numberFormatter(value as number); - const { firstStepPercent, prevStepPercent } = data as { - firstStepPercent: number; - prevStepPercent: number; - }; - let percent; - - if (percentCalculationType === PercentCalcType.Total) { - percent = (totalPercent ?? 0) / 100; - } else if (percentCalculationType === PercentCalcType.PreviousStep) { - percent = prevStepPercent ?? 0; - } else { - percent = firstStepPercent ?? 0; - } - const formattedPercent = percentFormatter(percent); - return [name, formattedValue, formattedPercent]; -} - -export default function transformProps( - chartProps: EchartsFunnelChartProps, -): FunnelChartTransformedProps { - const { - formData, - height, - hooks, - filterState, - queriesData, - width, - theme, - emitCrossFilters, - datasource, - } = chartProps; - const data: DataRecord[] = queriesData[0].data || []; - const detectedCurrency = queriesData[0]?.detected_currency; - const coltypeMapping = getColtypesMapping(queriesData[0]); - const { - colorScheme, - groupby, - orient, - sort, - gap, - labelLine, - labelType, - tooltipLabelType, - legendMargin, - legendOrientation, - legendType, - legendSort, - metric = '', - numberFormat, - currencyFormat, - showLabels, - inContextMenu, - showTooltipLabels, - showLegend, - sliceId, - percentCalculationType, - }: EchartsFunnelFormData = { - ...DEFAULT_LEGEND_FORM_DATA, - ...DEFAULT_FUNNEL_FORM_DATA, - ...formData, - }; - const { - currencyFormats = {}, - columnFormats = {}, - currencyCodeColumn, - } = datasource; - const refs: Refs = {}; - const metricLabel = getMetricLabel(metric); - const groupbyLabels = groupby.map(getColumnLabel); - const keys = data.map(datum => - extractGroupbyLabel({ datum, groupby: groupbyLabels, coltypeMapping: {} }), - ); - const labelMap = data.reduce((acc: Record, datum) => { - const label = extractGroupbyLabel({ - datum, - groupby: groupbyLabels, - coltypeMapping: {}, - }); - return { - ...acc, - [label]: groupbyLabels.map(col => datum[col] as string), - }; - }, {}); - - const { setDataMask = () => {}, onContextMenu } = hooks; - const colorFn = CategoricalColorNamespace.getScale(colorScheme as string); - const numberFormatter = getValueFormatter( - metric, - currencyFormats, - columnFormats, - numberFormat, - currencyFormat, - undefined, - data, - currencyCodeColumn, - detectedCurrency, - ); - - const transformedData: { - value: number; - name: string; - itemStyle: { color: string; opacity: OpacityEnum }; - }[] = data.map((datum, index) => { - const name = extractGroupbyLabel({ - datum, - groupby: groupbyLabels, - coltypeMapping: {}, - }); - const value = datum[metricLabel] as number; - const isFiltered = - filterState.selectedValues && !filterState.selectedValues.includes(name); - const firstStepPercent = value / (data[0][metricLabel] as number); - const prevStepPercent = - index === 0 ? 1 : value / (data[index - 1][metricLabel] as number); - return { - value, - name, - itemStyle: { - color: colorFn(name, sliceId), - opacity: isFiltered - ? OpacityEnum.SemiTransparent - : OpacityEnum.NonTransparent, - }, - firstStepPercent, - prevStepPercent, - }; - }); - - const selectedValues = (filterState.selectedValues || []).reduce( - (acc: Record, selectedValue: string) => { - const index = transformedData.findIndex( - ({ name }) => name === selectedValue, - ); - return { - ...acc, - [index]: selectedValue, - }; - }, - {}, - ); - - const formatter = (params: CallbackDataParams) => { - const [name, formattedValue, formattedPercent] = parseParams({ - params, - numberFormatter, - percentCalculationType, - }); - switch (labelType) { - case EchartsFunnelLabelType.Key: - return name; - case EchartsFunnelLabelType.Value: - return formattedValue; - case EchartsFunnelLabelType.Percent: - return formattedPercent; - case EchartsFunnelLabelType.KeyValue: - return `${name}: ${formattedValue}`; - case EchartsFunnelLabelType.KeyValuePercent: - return `${name}: ${formattedValue} (${formattedPercent})`; - case EchartsFunnelLabelType.KeyPercent: - return `${name}: ${formattedPercent}`; - case EchartsFunnelLabelType.ValuePercent: - return `${formattedValue} (${formattedPercent})`; - default: - return name; - } - }; - - const defaultLabel = { - formatter, - show: showLabels, - color: theme.colorText, - textBorderColor: theme.colorBgBase, - textBorderWidth: 1, - }; - const legendData = keys.sort((a: string, b: string) => { - if (!legendSort) return 0; - return legendSort === 'asc' ? a.localeCompare(b) : b.localeCompare(a); - }); - const { effectiveLegendMargin, effectiveLegendType } = resolveLegendLayout({ - chartHeight: height, - chartWidth: width, - legendItems: legendData, - legendMargin, - orientation: legendOrientation, - show: showLegend, - theme, - type: legendType, - }); - - const series: FunnelSeriesOption[] = [ - { - type: VizType.Funnel, - ...getChartPadding(showLegend, legendOrientation, effectiveLegendMargin), - animation: true, - minSize: '0%', - maxSize: '100%', - sort, - orient, - gap, - funnelAlign: 'center', - labelLine: { show: !!labelLine }, - label: { - ...defaultLabel, - position: labelLine ? 'outer' : 'inner', - }, - emphasis: { - label: { - show: true, - fontWeight: 'bold', - }, - }, - data: transformedData, - }, - ]; - - const echartOptions: EChartsCoreOption = { - grid: { - ...defaultGrid, - }, - tooltip: { - ...getDefaultTooltip(refs), - show: !inContextMenu && showTooltipLabels, - trigger: 'item', - formatter: (params: any) => { - const [name, formattedValue, formattedPercent] = parseParams({ - params, - numberFormatter, - percentCalculationType, - }); - const row = []; - const enumName = EchartsFunnelLabelType[tooltipLabelType]; - const title = enumName.includes('Key') ? name : undefined; - if (enumName.includes('Value') || enumName.includes('Percent')) { - row.push(metricLabel); - } - if (enumName.includes('Value')) { - row.push(formattedValue); - } - if (enumName.includes('Percent')) { - row.push(formattedPercent); - } - return tooltipHtml([row], title); - }, - }, - legend: { - ...getLegendProps( - effectiveLegendType, - legendOrientation, - showLegend, - theme, - ), - data: legendData, - }, - series, - }; - - return { - formData, - width, - height, - echartOptions, - setDataMask, - emitCrossFilters, - labelMap, - groupby, - selectedValues, - onContextMenu, - refs, - coltypeMapping, - }; -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Gantt/EchartsGantt.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Gantt/EchartsGantt.tsx deleted file mode 100644 index 48e7e590022..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Gantt/EchartsGantt.tsx +++ /dev/null @@ -1,90 +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 { useEffect, useRef, useState } from 'react'; -import { sharedControlComponents } from '@superset-ui/chart-controls'; -import { t } from '@apache-superset/core/translation'; -import Echart from '../components/Echart'; -import { EchartsGanttChartTransformedProps } from './types'; -import { EventHandlers } from '../types'; - -const { RadioButtonControl } = sharedControlComponents; - -export default function EchartsGantt(props: EchartsGanttChartTransformedProps) { - const { - height, - width, - echartOptions, - selectedValues, - refs, - formData, - setControlValue, - onLegendStateChanged, - } = props; - const extraControlRef = useRef(null); - const [extraHeight, setExtraHeight] = useState(0); - - useEffect(() => { - const updatedHeight = extraControlRef.current?.offsetHeight ?? 0; - setExtraHeight(updatedHeight); - }, [formData.showExtraControls]); - - const eventHandlers: EventHandlers = { - legendselectchanged: payload => { - requestAnimationFrame(() => { - onLegendStateChanged?.(payload.selected); - }); - }, - legendselectall: payload => { - requestAnimationFrame(() => { - onLegendStateChanged?.(payload.selected); - }); - }, - legendinverseselect: payload => { - requestAnimationFrame(() => { - onLegendStateChanged?.(payload.selected); - }); - }, - }; - - return ( - <> -
- {formData.showExtraControls ? ( - setControlValue?.('subcategories', v)} - /> - ) : null} -
- - - ); -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Gantt/buildQuery.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Gantt/buildQuery.ts deleted file mode 100644 index 66153916fe8..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Gantt/buildQuery.ts +++ /dev/null @@ -1,61 +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 { - QueryFormData, - QueryObject, - buildQueryContext, - ensureIsArray, -} from '@superset-ui/core'; - -export default function buildQuery(formData: QueryFormData) { - const { - start_time, - end_time, - y_axis, - series, - tooltip_columns, - tooltip_metrics, - order_by_cols, - } = formData; - - const groupBy = ensureIsArray(series); - const orderby = ensureIsArray(order_by_cols).map( - expr => JSON.parse(expr) as [string, boolean], - ); - const columns = Array.from( - new Set([ - start_time, - end_time, - y_axis, - ...groupBy, - ...ensureIsArray(tooltip_columns), - ...orderby.map(v => v[0]), - ]), - ); - - return buildQueryContext(formData, (baseQueryObject: QueryObject) => [ - { - ...baseQueryObject, - columns, - metrics: ensureIsArray(tooltip_metrics), - orderby, - series_columns: groupBy, - }, - ]); -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Gantt/constants.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Gantt/constants.ts deleted file mode 100644 index e4023f22201..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Gantt/constants.ts +++ /dev/null @@ -1,26 +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. - */ -export const ELEMENT_HEIGHT_SCALE = 0.85 as const; - -export enum Dimension { - StartTime = 'startTime', - EndTime = 'endTime', - Index = 'index', - SeriesCount = 'seriesCount', -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Gantt/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Gantt/controlPanel.tsx deleted file mode 100644 index fd8127d428a..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Gantt/controlPanel.tsx +++ /dev/null @@ -1,145 +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 { - ControlPanelConfig, - ControlSubSectionHeader, - sections, - sharedControls, -} from '@superset-ui/chart-controls'; -import { t } from '@apache-superset/core/translation'; -import { GenericDataType } from '@apache-superset/core/common'; -import { - legendSection, - showExtraControls, - tooltipTimeFormatControl, - tooltipValuesFormatControl, -} from '../controls'; - -const config: ControlPanelConfig = { - controlPanelSections: [ - { - label: t('Query'), - expanded: true, - controlSetRows: [ - [ - { - name: 'start_time', - config: { - ...sharedControls.entity, - label: t('Start Time'), - description: undefined, - allowedDataTypes: [GenericDataType.Temporal], - }, - }, - ], - [ - { - name: 'end_time', - config: { - ...sharedControls.entity, - label: t('End Time'), - description: undefined, - allowedDataTypes: [GenericDataType.Temporal], - }, - }, - ], - [ - { - name: 'y_axis', - config: { - ...sharedControls.x_axis, - label: t('Y-axis'), - description: t('Dimension to use on y-axis.'), - initialValue: () => undefined, - }, - }, - ], - ['series'], - [ - { - name: 'subcategories', - config: { - type: 'CheckboxControl', - label: t('Subcategories'), - description: t( - 'Divides each category into subcategories based on the values in ' + - 'the dimension. It can be used to exclude intersections.', - ), - renderTrigger: true, - default: false, - visibility: ({ controls }) => !!controls?.series?.value, - }, - }, - ], - ['tooltip_metrics'], - ['tooltip_columns'], - ['adhoc_filters'], - ['order_by_cols'], - ['row_limit'], - ], - }, - { - ...sections.titleControls, - controlSetRows: sections.titleControls.controlSetRows.slice(0, -1), - }, - { - label: t('Chart Options'), - expanded: true, - tabOverride: 'customize', - controlSetRows: [ - ['color_scheme'], - ...legendSection, - ['zoomable'], - [showExtraControls], - [ - - {t('X Axis')} - , - ], - [ - { - name: 'x_axis_time_bounds', - config: { - type: 'TimeRangeControl', - label: t('Bounds'), - description: t( - 'Bounds for the X-axis. Selected time merges with ' + - 'min/max date of the data. When left empty, bounds ' + - 'dynamically defined based on the min/max of the data.', - ), - renderTrigger: true, - allowClear: true, - allowEmpty: [true, true], - }, - }, - ], - ['x_axis_time_format'], - [ - - {t('Tooltip')} - , - ], - [tooltipTimeFormatControl], - [tooltipValuesFormatControl], - ], - }, - ], -}; - -export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Gantt/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Gantt/index.ts deleted file mode 100644 index 104afe3a44d..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Gantt/index.ts +++ /dev/null @@ -1,62 +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 { t } from '@apache-superset/core/translation'; -import { Behavior } from '@superset-ui/core'; -import transformProps from './transformProps'; -import controlPanel from './controlPanel'; -import buildQuery from './buildQuery'; -import { EchartsChartPlugin } from '../types'; -import thumbnail from './images/thumbnail.png'; -import thumbnailDark from './images/thumbnail-dark.png'; -import example1 from './images/example1.png'; -import example1Dark from './images/example1-dark.png'; -import example2 from './images/example2.png'; -import example2Dark from './images/example2-dark.png'; - -export default class EchartsGanttChartPlugin extends EchartsChartPlugin { - constructor() { - super({ - buildQuery, - controlPanel, - loadChart: () => import('./EchartsGantt'), - metadata: { - behaviors: [ - Behavior.InteractiveChart, - Behavior.DrillToDetail, - Behavior.DrillBy, - ], - credits: ['https://echarts.apache.org'], - name: t('Gantt Chart'), - description: t( - 'Gantt chart visualizes important events over a time span. ' + - 'Every data point displayed as a separate event along a ' + - 'horizontal line.', - ), - tags: [t('ECharts'), t('Featured'), t('Timeline'), t('Time')], - thumbnail, - thumbnailDark, - exampleGallery: [ - { url: example1, urlDark: example1Dark }, - { url: example2, urlDark: example2Dark }, - ], - }, - transformProps, - }); - } -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Gantt/index.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Gantt/index.tsx new file mode 100644 index 00000000000..574677bd981 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Gantt/index.tsx @@ -0,0 +1,828 @@ +/** + * 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 'dayjs/plugin/utc'; +import { useEffect, useRef, useState } from 'react'; +import { t } from '@apache-superset/core/translation'; +import { GenericDataType } from '@apache-superset/core/common'; +import { + AxisType, + Behavior, + buildQueryContext, + CategoricalColorNamespace, + ChartDataResponseResult, + ChartProps, + DataRecord, + DataRecordValue, + ensureIsArray, + getColumnLabel, + getNumberFormatter, + QueryFormColumn, + QueryFormData, + QueryFormMetric, + QueryObject, + SetDataMaskHook, + tooltipHtml, +} from '@superset-ui/core'; +import { extendedDayjs as dayjs } from '@superset-ui/core/utils/dates'; +import { + ControlSubSectionHeader, + sections, + sharedControlComponents, + sharedControls, +} from '@superset-ui/chart-controls'; +import { + CustomSeriesOption, + CustomSeriesRenderItem, + EChartsCoreOption, + LineSeriesOption, +} from 'echarts'; +import { CallbackDataParams } from 'echarts/types/src/util/types'; + +import { defineChart } from '@superset-ui/glyph-core'; +import { Checkbox } from '@superset-ui/glyph-core'; +import { + ShowLegend, + LegendType, + LegendOrientation, + LegendSort, +} from '@superset-ui/glyph-core'; +import Echart from '../components/Echart'; +import { DEFAULT_FORM_DATA, TIMESERIES_CONSTANTS } from '../constants'; +import { + BaseTransformedProps, + CrossFilterTransformedProps, + EventHandlers, + LegendFormData, + Refs, +} from '../types'; +import { getLegendProps, groupData } from '../utils/series'; +import { + getTooltipTimeFormatter, + getXAxisFormatter, +} from '../utils/formatters'; +import { defaultGrid } from '../defaults'; +import { getPadding } from '../Timeseries/transformers'; +import { convertInteger } from '../utils/convertInteger'; +import { getTooltipLabels } from '../utils/tooltip'; +import { + legendSection, + showExtraControls, + tooltipTimeFormatControl, + tooltipValuesFormatControl, +} from '../controls'; + +import thumbnail from './images/thumbnail.png'; +import thumbnailDark from './images/thumbnail-dark.png'; +import example1 from './images/example1.png'; +import example1Dark from './images/example1-dark.png'; +import example2 from './images/example2.png'; +import example2Dark from './images/example2-dark.png'; + +// ============================================================================ +// Types +// ============================================================================ + +type EchartsGanttChartTransformedProps = + BaseTransformedProps & CrossFilterTransformedProps; + +type EchartsGanttFormData = QueryFormData & + LegendFormData & { + viz_type: 'gantt_chart'; + startTime: QueryFormColumn; + endTime: QueryFormColumn; + yAxis: QueryFormColumn; + tooltipMetrics: QueryFormMetric[]; + tooltipColumns: QueryFormColumn[]; + series?: QueryFormColumn; + xAxisTimeFormat?: string; + tooltipTimeFormat?: string; + tooltipValuesFormat?: string; + colorScheme?: string; + zoomable?: boolean; + xAxisTitle?: string; + xAxisTitleMargin?: number; + yAxisTitle?: string; + yAxisTitleMargin?: number; + yAxisTitlePosition?: string; + xAxisTimeBounds?: [string | null, string | null]; + subcategories?: boolean; + showExtraControls?: boolean; + }; + +interface EchartsGanttChartProps extends ChartProps { + formData: EchartsGanttFormData; + queriesData: ChartDataResponseResult[]; +} + +interface Cartesian2dCoordSys { + type: 'cartesian2d'; + x: number; + y: number; + width: number; + height: number; +} + +// ============================================================================ +// Constants +// ============================================================================ + +const ELEMENT_HEIGHT_SCALE = 0.85; + +enum Dimension { + StartTime = 'startTime', + EndTime = 'endTime', + Index = 'index', + SeriesCount = 'seriesCount', +} + +const { RadioButtonControl } = sharedControlComponents; + +// ============================================================================ +// Custom Series Render Item +// ============================================================================ + +const renderItem: CustomSeriesRenderItem = (params, api) => { + const startX = api.value(Dimension.StartTime); + const endX = api.value(Dimension.EndTime); + const index = Number(api.value(Dimension.Index)); + const seriesCount = Number(api.value(Dimension.SeriesCount)); + + if (Number.isNaN(index)) { + return null; + } + + const startY = seriesCount - 1 - index; + const endY = startY - 1; + + const startCoord = api.coord([startX, startY]); + const endCoord = api.coord([endX, endY]); + + const baseHeight = endCoord[1] - startCoord[1]; + const height = baseHeight * ELEMENT_HEIGHT_SCALE; + + const coordSys = params.coordSys as Cartesian2dCoordSys; + const bounds = [coordSys.x, coordSys.x + coordSys.width]; + + // left bound + startCoord[0] = Math.max(startCoord[0], bounds[0]); + endCoord[0] = Math.max(startCoord[0], endCoord[0]); + // right bound + startCoord[0] = Math.min(startCoord[0], bounds[1]); + endCoord[0] = Math.min(endCoord[0], bounds[1]); + + const width = endCoord[0] - startCoord[0]; + + if (width <= 0 || height <= 0) { + return null; + } + + return { + type: 'rect', + transition: ['shape'], + shape: { + x: startCoord[0], + y: startCoord[1] - height - (baseHeight - height) / 2, + width, + height, + }, + style: api.style(), + }; +}; + +// ============================================================================ +// Gantt Chart Render Component +// ============================================================================ + +function GanttRender({ + transformedProps, +}: { + transformedProps: EchartsGanttChartTransformedProps; +}) { + const { + height, + width, + echartOptions, + selectedValues, + refs, + formData, + setControlValue, + onLegendStateChanged, + } = transformedProps; + + const extraControlRef = useRef(null); + const [extraHeight, setExtraHeight] = useState(0); + + useEffect(() => { + const updatedHeight = extraControlRef.current?.offsetHeight ?? 0; + setExtraHeight(updatedHeight); + }, [formData.showExtraControls]); + + const eventHandlers: EventHandlers = { + legendselectchanged: payload => { + requestAnimationFrame(() => { + onLegendStateChanged?.(payload.selected); + }); + }, + legendselectall: payload => { + requestAnimationFrame(() => { + onLegendStateChanged?.(payload.selected); + }); + }, + legendinverseselect: payload => { + requestAnimationFrame(() => { + onLegendStateChanged?.(payload.selected); + }); + }, + }; + + return ( + <> +
+ {formData.showExtraControls ? ( + setControlValue?.('subcategories', v)} + /> + ) : null} +
+ + + ); +} + +// ============================================================================ +// Chart Definition +// ============================================================================ + +export default defineChart({ + metadata: { + name: t('Gantt Chart'), + description: t( + 'Gantt chart visualizes important events over a time span. ' + + 'Every data point displayed as a separate event along a ' + + 'horizontal line.', + ), + tags: [t('ECharts'), t('Featured'), t('Timeline'), t('Time')], + thumbnail, + thumbnailDark, + exampleGallery: [ + { url: example1, urlDark: example1Dark }, + { url: example2, urlDark: example2Dark }, + ], + behaviors: [ + Behavior.InteractiveChart, + Behavior.DrillToDetail, + Behavior.DrillBy, + ], + }, + + arguments: { + subcategories: { + arg: Checkbox.with({ + label: t('Subcategories'), + description: t( + 'Divides each category into subcategories based on the values in ' + + 'the dimension. It can be used to exclude intersections.', + ), + default: false, + }), + visibleWhen: { series: (value: unknown) => !!value }, + }, + showLegend: ShowLegend, + legendType: { + arg: LegendType, + visibleWhen: { showLegend: true }, + }, + legendOrientation: { + arg: LegendOrientation, + visibleWhen: { showLegend: true }, + }, + legendSort: { + arg: LegendSort, + visibleWhen: { showLegend: true }, + }, + zoomable: Checkbox.with({ + label: t('Data Zoom'), + description: t('Enable data zooming controls'), + default: false, + }), + }, + + additionalControls: { + query: [ + [ + { + name: 'start_time', + config: { + ...sharedControls.entity, + label: t('Start Time'), + description: undefined, + allowedDataTypes: [GenericDataType.Temporal], + }, + }, + ], + [ + { + name: 'end_time', + config: { + ...sharedControls.entity, + label: t('End Time'), + description: undefined, + allowedDataTypes: [GenericDataType.Temporal], + }, + }, + ], + [ + { + name: 'y_axis', + config: { + ...sharedControls.x_axis, + label: t('Y-axis'), + description: t('Dimension to use on y-axis.'), + initialValue: () => undefined, + }, + }, + ], + ['series'], + ['tooltip_metrics'], + ['tooltip_columns'], + ['adhoc_filters'], + ['order_by_cols'], + ['row_limit'], + ], + chartOptions: [ + ['color_scheme'], + ...legendSection, + ['zoomable'], + [showExtraControls], + [ + + {t('X Axis')} + , + ], + [ + { + name: 'x_axis_time_bounds', + config: { + type: 'TimeRangeControl', + label: t('Bounds'), + description: t( + 'Bounds for the X-axis. Selected time merges with ' + + 'min/max date of the data. When left empty, bounds ' + + 'dynamically defined based on the min/max of the data.', + ), + renderTrigger: true, + allowClear: true, + allowEmpty: [true, true], + }, + }, + ], + ['x_axis_time_format'], + [ + + {t('Tooltip')} + , + ], + [tooltipTimeFormatControl], + [tooltipValuesFormatControl], + ], + }, + + // Include title controls from sections (minus the last row) + additionalSections: [ + { + ...sections.titleControls, + controlSetRows: sections.titleControls.controlSetRows.slice(0, -1), + }, + ], + + buildQuery: (formData: QueryFormData) => { + const { + start_time: startTime, + end_time: endTime, + y_axis: yAxis, + series, + tooltip_columns: tooltipColumns, + tooltip_metrics: tooltipMetrics, + order_by_cols: orderByCols, + } = formData; + + const groupBy = ensureIsArray(series); + const orderby = ensureIsArray(orderByCols).map( + expr => JSON.parse(expr) as [string, boolean], + ); + const columns = Array.from( + new Set([ + startTime, + endTime, + yAxis, + ...groupBy, + ...ensureIsArray(tooltipColumns), + ...orderby.map(v => v[0]), + ]), + ); + + return buildQueryContext(formData, (baseQueryObject: QueryObject) => [ + { + ...baseQueryObject, + columns, + metrics: ensureIsArray(tooltipMetrics), + orderby, + series_columns: groupBy, + }, + ]); + }, + + transform: ( + chartProps: EchartsGanttChartProps, + ): { transformedProps: EchartsGanttChartTransformedProps } => { + const { + formData, + queriesData, + height, + hooks, + filterState, + width, + theme, + emitCrossFilters, + legendState, + } = chartProps; + + const { + startTime, + endTime, + yAxis, + series: dimension, + tooltipMetrics, + tooltipColumns, + xAxisTimeFormat, + tooltipTimeFormat, + tooltipValuesFormat, + colorScheme, + sliceId, + zoomable, + legendMargin, + legendOrientation, + legendType, + legendSort, + showLegend, + yAxisTitle, + yAxisTitleMargin, + xAxisTitle, + xAxisTitleMargin, + xAxisTimeBounds, + subcategories, + }: EchartsGanttFormData = { + ...DEFAULT_FORM_DATA, + ...formData, + }; + + const { setControlValue, onLegendStateChanged } = hooks; + + const { data = [], colnames = [], coltypes = [] } = queriesData[0]; + const refs: Refs = {}; + + const startTimeLabel = getColumnLabel(startTime); + const endTimeLabel = getColumnLabel(endTime); + const yAxisLabel = getColumnLabel(yAxis); + const dimensionLabel = dimension ? getColumnLabel(dimension) : undefined; + const tooltipLabels = getTooltipLabels({ tooltipMetrics, tooltipColumns }); + + const seriesMap = groupData(data, dimensionLabel); + + const seriesInCategoriesMap = new Map< + DataRecordValue | undefined, + Map + >(); + data.forEach(datum => { + const category = datum[yAxisLabel]; + let dimensionValue: DataRecordValue | undefined; + if (dimensionLabel) { + if (legendState && !legendState[String(datum[dimensionLabel])]) { + return; + } + if (subcategories) { + dimensionValue = datum[dimensionLabel]; + } + } + const categorySeriesMap = seriesInCategoriesMap.get(category); + if (categorySeriesMap) { + const dimensionMapValue = categorySeriesMap.get(dimensionValue); + if (dimensionMapValue === undefined) { + categorySeriesMap.set(dimensionValue, categorySeriesMap.size); + } + } else { + seriesInCategoriesMap.set(category, new Map([[dimensionValue, 0]])); + } + }); + + let seriesCount = 0; + const categoryAndSeriesToIndexMap: typeof seriesInCategoriesMap = new Map(); + Array.from(seriesInCategoriesMap.entries()).forEach(([key, map]) => { + categoryAndSeriesToIndexMap.set( + key, + new Map( + Array.from(map.entries()).map(([key2, idx]) => [ + key2, + seriesCount + idx, + ]), + ), + ); + seriesCount += map.size; + }); + + const borderLines: { yAxis: number }[] = []; + const categoryLines: { yAxis: number; name?: string }[] = []; + let sum = 0; + let prevSum = 0; + Array.from(seriesInCategoriesMap.entries()).forEach(([key, map]) => { + sum += map.size; + categoryLines.push({ + yAxis: seriesCount - (sum + prevSum) / 2, + name: key ? String(key) : undefined, + }); + borderLines.push({ yAxis: seriesCount - sum }); + prevSum = sum; + }); + + const xAxisFormatter = getXAxisFormatter(xAxisTimeFormat); + const tooltipTimeFormatter = getTooltipTimeFormatter(tooltipTimeFormat); + const tooltipValuesFormatter = getNumberFormatter(tooltipValuesFormat); + + const bounds: [number | undefined, number | undefined] = [ + undefined, + undefined, + ]; + if (xAxisTimeBounds?.[0]) { + const minDate = Math.min( + ...data.map(datum => Number(datum[startTimeLabel] ?? 0)), + ); + const time = dayjs(xAxisTimeBounds[0], 'HH:mm:ss'); + bounds[0] = +dayjs + .utc(minDate) + .hour(time.hour()) + .minute(time.minute()) + .second(time.second()); + } + if (xAxisTimeBounds?.[1]) { + const maxDate = Math.min( + ...data.map(datum => Number(datum[endTimeLabel] ?? 0)), + ); + const time = dayjs(xAxisTimeBounds[1], 'HH:mm:ss'); + bounds[1] = +dayjs + .utc(maxDate) + .hour(time.hour()) + .minute(time.minute()) + .second(time.second()); + } + + const padding = getPadding( + showLegend && seriesMap.size > 1, + legendOrientation, + false, + zoomable, + legendMargin, + !!xAxisTitle, + 'Left', + convertInteger(yAxisTitleMargin), + convertInteger(xAxisTitleMargin), + ); + + const colorScale = CategoricalColorNamespace.getScale( + colorScheme as string, + ); + + const getIndex = (datum: DataRecord) => { + const seriesIndexMap = categoryAndSeriesToIndexMap.get(datum[yAxisLabel]); + const seriesKey = + subcategories && dimensionLabel ? datum[dimensionLabel] : undefined; + return seriesIndexMap ? seriesIndexMap.get(seriesKey) : undefined; + }; + + const series: (CustomSeriesOption | LineSeriesOption)[] = Array.from( + seriesMap.entries(), + ) + .map(([key, seriesData], idx) => ({ + name: key as string | undefined, + progressive: 0, + itemStyle: { + color: colorScale(String(key), sliceId ?? idx), + }, + type: 'custom' as const, + renderItem, + data: seriesData.map(datum => ({ + value: [ + datum[startTimeLabel], + datum[endTimeLabel], + getIndex(datum), + seriesCount, + ...Object.values(datum), + ], + })), + dimensions: [...Object.values(Dimension), ...colnames], + encode: { + x: [0, 1], + }, + })) + .sort((a, b) => String(a.name).localeCompare(String(b.name))); + + series.push( + { + animation: false, + type: 'line' as const, + markLine: { + silent: true, + symbol: ['none', 'none'], + lineStyle: { + type: 'dashed', + // eslint-disable-next-line theme-colors/no-literal-colors + color: '#dbe0ea', + }, + label: { + show: false, + }, + data: borderLines, + }, + }, + { + animation: false, + type: 'line', + markLine: { + silent: true, + symbol: ['none', 'none'], + lineStyle: { + type: 'solid', + // eslint-disable-next-line theme-colors/no-literal-colors + color: '#00000000', + }, + label: { + show: true, + position: 'start', + formatter: '{b}', + color: theme.colorText, + }, + data: categoryLines, + }, + }, + ); + + const legendData = series + .map(entry => { + const { name } = entry; + if (name === null || name === undefined) return ''; + return String(name); + }) + .filter(name => name !== '') + .sort((a, b) => { + if (!legendSort) return 0; + return legendSort === 'asc' ? a.localeCompare(b) : b.localeCompare(a); + }); + + const tooltipFormatterMap = { + [GenericDataType.Numeric]: tooltipValuesFormatter, + [GenericDataType.String]: undefined, + [GenericDataType.Temporal]: tooltipTimeFormatter, + [GenericDataType.Boolean]: undefined, + }; + + const echartOptions: EChartsCoreOption = { + useUTC: true, + tooltip: { + formatter: (params: CallbackDataParams) => + tooltipHtml( + tooltipLabels.map(label => { + const offset = Object.keys(Dimension).length; + const dimensionNames = params.dimensionNames!.slice(offset); + const dataArray = (params.value as unknown[]).slice(offset); + + const idx = dimensionNames.findIndex(v => v === label)!; + const value = dataArray[idx]; + const type = coltypes[idx]; + + return [ + label, + String(tooltipFormatterMap[type]?.(value as number) ?? value), + ]; + }) as string[][], + dimensionLabel ? params.seriesName : undefined, + ), + }, + legend: { + ...getLegendProps( + legendType, + legendOrientation, + showLegend, + theme, + zoomable, + legendState, + ), + data: legendData, + }, + grid: { + ...defaultGrid, + ...padding, + }, + dataZoom: zoomable && [ + { + type: 'slider', + filterMode: 'none', + start: TIMESERIES_CONSTANTS.dataZoomStart, + end: TIMESERIES_CONSTANTS.dataZoomEnd, + bottom: TIMESERIES_CONSTANTS.zoomBottom, + }, + ], + toolbox: { + show: zoomable, + top: TIMESERIES_CONSTANTS.toolboxTop, + right: TIMESERIES_CONSTANTS.toolboxRight, + feature: { + dataZoom: { + yAxisIndex: false, + title: { + zoom: t('zoom area'), + back: t('restore zoom'), + }, + }, + }, + }, + series, + xAxis: { + name: xAxisTitle, + nameLocation: 'middle', + type: AxisType.Time, + nameGap: convertInteger(xAxisTitleMargin), + axisLabel: { + formatter: xAxisFormatter, + hideOverlap: true, + }, + min: bounds[0], + max: bounds[1], + }, + yAxis: { + name: yAxisTitle, + nameGap: convertInteger(yAxisTitleMargin), + nameLocation: 'middle', + axisLabel: { + show: false, + }, + splitLine: { + show: false, + }, + type: AxisType.Value, + min: 0, + max: seriesCount, + }, + }; + + return { + transformedProps: { + formData, + echartOptions, + height, + filterState, + width, + emitCrossFilters, + refs, + setControlValue, + onLegendStateChanged, + // Required by CrossFilterTransformedProps + groupby: [] as QueryFormColumn[], + labelMap: {} as Record, + setDataMask: (hooks.setDataMask ?? (() => {})) as SetDataMaskHook, + selectedValues: {} as Record, + }, + }; + }, + + render: ({ transformedProps }) => ( + + ), +}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Gantt/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Gantt/transformProps.ts deleted file mode 100644 index 1a9b4d565a1..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Gantt/transformProps.ts +++ /dev/null @@ -1,497 +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. - */ -// Type augmentation for dayjs plugins -import 'dayjs/plugin/utc'; -import { - CustomSeriesOption, - CustomSeriesRenderItem, - EChartsCoreOption, - LineSeriesOption, -} from 'echarts'; -import { t } from '@apache-superset/core/translation'; -import { - AxisType, - CategoricalColorNamespace, - DataRecord, - DataRecordValue, - getColumnLabel, - getNumberFormatter, - tooltipHtml, -} from '@superset-ui/core'; -import { extendedDayjs as dayjs } from '@superset-ui/core/utils/dates'; -import { GenericDataType } from '@apache-superset/core/common'; -import { CallbackDataParams } from 'echarts/types/src/util/types'; -import { - Cartesian2dCoordSys, - EchartsGanttChartProps, - EchartsGanttFormData, -} from './types'; -import { DEFAULT_FORM_DATA, TIMESERIES_CONSTANTS } from '../constants'; -import { LegendOrientation, Refs } from '../types'; -import { - getHorizontalLegendAvailableWidth, - getLegendProps, - groupData, -} from '../utils/series'; -import { resolveLegendLayout } from '../utils/legendLayout'; -import { - getTooltipTimeFormatter, - getXAxisFormatter, -} from '../utils/formatters'; -import { defaultGrid } from '../defaults'; -import { getPadding } from '../Timeseries/transformers'; -import { convertInteger } from '../utils/convertInteger'; -import { getTooltipLabels } from '../utils/tooltip'; -import { Dimension, ELEMENT_HEIGHT_SCALE } from './constants'; - -const renderItem: CustomSeriesRenderItem = (params, api) => { - const startX = api.value(Dimension.StartTime); - const endX = api.value(Dimension.EndTime); - const index = Number(api.value(Dimension.Index)); - const seriesCount = Number(api.value(Dimension.SeriesCount)); - - if (Number.isNaN(index)) { - return null; - } - - const startY = seriesCount - 1 - index; - const endY = startY - 1; - - const startCoord = api.coord([startX, startY]); - const endCoord = api.coord([endX, endY]); - - const baseHeight = endCoord[1] - startCoord[1]; - const height = baseHeight * ELEMENT_HEIGHT_SCALE; - - const coordSys = params.coordSys as Cartesian2dCoordSys; - const bounds = [coordSys.x, coordSys.x + coordSys.width]; - - // left bound - startCoord[0] = Math.max(startCoord[0], bounds[0]); - endCoord[0] = Math.max(startCoord[0], endCoord[0]); - // right bound - startCoord[0] = Math.min(startCoord[0], bounds[1]); - endCoord[0] = Math.min(endCoord[0], bounds[1]); - - const width = endCoord[0] - startCoord[0]; - - if (width <= 0 || height <= 0) { - return null; - } - - return { - type: 'rect', - transition: ['shape'], - shape: { - x: startCoord[0], - y: startCoord[1] - height - (baseHeight - height) / 2, - width, - height, - }, - style: api.style(), - }; -}; - -export default function transformProps(chartProps: EchartsGanttChartProps) { - const { - formData, - queriesData, - height, - hooks, - filterState, - width, - theme, - emitCrossFilters, - datasource, - legendState, - } = chartProps; - - const { - startTime, - endTime, - yAxis, - series: dimension, - tooltipMetrics, - tooltipColumns, - xAxisTimeFormat, - tooltipTimeFormat, - tooltipValuesFormat, - colorScheme, - sliceId, - zoomable, - legendMargin, - legendOrientation, - legendType, - legendSort, - showLegend, - yAxisTitle, - yAxisTitleMargin, - xAxisTitle, - xAxisTitleMargin, - xAxisTimeBounds, - subcategories, - }: EchartsGanttFormData = { - ...DEFAULT_FORM_DATA, - ...formData, - }; - - const { setControlValue, onLegendStateChanged } = hooks; - - const { data = [], colnames = [], coltypes = [] } = queriesData[0]; - const refs: Refs = {}; - - const startTimeLabel = getColumnLabel(startTime); - const endTimeLabel = getColumnLabel(endTime); - const yAxisLabel = getColumnLabel(yAxis); - const dimensionLabel = dimension ? getColumnLabel(dimension) : undefined; - const tooltipLabels = getTooltipLabels({ tooltipMetrics, tooltipColumns }); - - const seriesMap = groupData(data, dimensionLabel); - - const seriesInCategoriesMap = new Map< - DataRecordValue | undefined, - Map - >(); - data.forEach(datum => { - const category = datum[yAxisLabel]; - let dimensionValue: DataRecordValue | undefined; - if (dimensionLabel) { - if (legendState && !legendState[String(datum[dimensionLabel])]) { - return; - } - if (subcategories) { - dimensionValue = datum[dimensionLabel]; - } - } - const seriesMap = seriesInCategoriesMap.get(category); - if (seriesMap) { - const dimensionMapValue = seriesMap.get(dimensionValue); - if (dimensionMapValue === undefined) { - seriesMap.set(dimensionValue, seriesMap.size); - } - } else { - seriesInCategoriesMap.set(category, new Map([[dimensionValue, 0]])); - } - }); - - let seriesCount = 0; - const categoryAndSeriesToIndexMap: typeof seriesInCategoriesMap = new Map(); - Array.from(seriesInCategoriesMap.entries()).forEach(([key, map]) => { - categoryAndSeriesToIndexMap.set( - key, - new Map( - Array.from(map.entries()).map(([key2, idx]) => [ - key2, - seriesCount + idx, - ]), - ), - ); - seriesCount += map.size; - }); - - const borderLines: { yAxis: number }[] = []; - const categoryLines: { yAxis: number; name?: string }[] = []; - let sum = 0; - let prevSum = 0; - Array.from(seriesInCategoriesMap.entries()).forEach(([key, map]) => { - sum += map.size; - categoryLines.push({ - yAxis: seriesCount - (sum + prevSum) / 2, - name: key ? String(key) : undefined, - }); - borderLines.push({ yAxis: seriesCount - sum }); - prevSum = sum; - }); - - const xAxisFormatter = getXAxisFormatter(xAxisTimeFormat); - const tooltipTimeFormatter = getTooltipTimeFormatter(tooltipTimeFormat); - const tooltipValuesFormatter = getNumberFormatter(tooltipValuesFormat); - - const bounds: [number | undefined, number | undefined] = [ - undefined, - undefined, - ]; - if (xAxisTimeBounds?.[0]) { - const minDate = Math.min( - ...data.map(datum => Number(datum[startTimeLabel] ?? 0)), - ); - const time = dayjs(xAxisTimeBounds[0], 'HH:mm:ss'); - bounds[0] = +dayjs - .utc(minDate) - .hour(time.hour()) - .minute(time.minute()) - .second(time.second()); - } - if (xAxisTimeBounds?.[1]) { - const maxDate = Math.min( - ...data.map(datum => Number(datum[endTimeLabel] ?? 0)), - ); - const time = dayjs(xAxisTimeBounds[1], 'HH:mm:ss'); - bounds[1] = +dayjs - .utc(maxDate) - .hour(time.hour()) - .minute(time.minute()) - .second(time.second()); - } - - const padding = getPadding( - showLegend, - legendOrientation, - false, - zoomable, - legendMargin, - !!xAxisTitle, - 'Left', - convertInteger(yAxisTitleMargin), - convertInteger(xAxisTitleMargin), - ); - - const colorScale = CategoricalColorNamespace.getScale(colorScheme as string); - - const getIndex = (datum: DataRecord) => { - const seriesMap = categoryAndSeriesToIndexMap.get(datum[yAxisLabel]); - const series = - subcategories && dimensionLabel ? datum[dimensionLabel] : undefined; - return seriesMap ? seriesMap.get(series) : undefined; - }; - - const series: (CustomSeriesOption | LineSeriesOption)[] = Array.from( - seriesMap.entries(), - ) - .map(([key, data], idx) => ({ - name: key as string | undefined, - // For some reason items can visually disappear if progressive enabled. - progressive: 0, - itemStyle: { - color: colorScale(String(key), sliceId ?? idx), - }, - type: 'custom' as const, - renderItem, - data: data.map(datum => ({ - value: [ - datum[startTimeLabel], - datum[endTimeLabel], - getIndex(datum), - seriesCount, - ...Object.values(datum), - ], - })), - dimensions: [...Object.values(Dimension), ...colnames], - encode: { - x: [0, 1], - }, - })) - .sort((a, b) => String(a.name).localeCompare(String(b.name))); - - series.push( - { - animation: false, - type: 'line' as const, - markLine: { - silent: true, - symbol: ['none', 'none'], - lineStyle: { - type: 'dashed', - // eslint-disable-next-line theme-colors/no-literal-colors - color: '#dbe0ea', - }, - label: { - show: false, - }, - data: borderLines, - }, - }, - { - animation: false, - type: 'line', - markLine: { - silent: true, - symbol: ['none', 'none'], - lineStyle: { - type: 'solid', - // eslint-disable-next-line theme-colors/no-literal-colors - color: '#00000000', - }, - label: { - show: true, - position: 'start', - formatter: '{b}', - color: theme.colorText, - }, - data: categoryLines, - }, - }, - ); - - const legendData = series - .map(entry => { - const { name } = entry; - if (name === null || name === undefined) return ''; - return String(name); - }) - .filter(name => name !== '') - .sort((a, b) => { - if (!legendSort) return 0; - return legendSort === 'asc' ? a.localeCompare(b) : b.localeCompare(a); - }); - const { legendLayout, effectiveLegendType } = resolveLegendLayout({ - availableWidth: - legendOrientation === LegendOrientation.Top || - legendOrientation === LegendOrientation.Bottom - ? getHorizontalLegendAvailableWidth({ - chartWidth: width, - orientation: legendOrientation, - padding, - zoomable, - }) - : undefined, - chartHeight: height, - chartWidth: width, - legendItems: legendData, - legendMargin, - orientation: legendOrientation, - show: showLegend, - theme, - type: legendType, - }); - if (legendLayout.effectiveMargin !== undefined) { - const adjustedPadding = getPadding( - showLegend, - legendOrientation, - false, - zoomable, - legendLayout.effectiveMargin, - !!xAxisTitle, - 'Left', - convertInteger(yAxisTitleMargin), - convertInteger(xAxisTitleMargin), - ); - Object.assign(padding, adjustedPadding); - } - - const tooltipFormatterMap = { - [GenericDataType.Numeric]: tooltipValuesFormatter, - [GenericDataType.String]: undefined, - [GenericDataType.Temporal]: tooltipTimeFormatter, - [GenericDataType.Boolean]: undefined, - }; - - const echartOptions: EChartsCoreOption = { - useUTC: true, - tooltip: { - formatter: (params: CallbackDataParams) => - tooltipHtml( - tooltipLabels.map(label => { - const offset = Object.keys(Dimension).length; - const dimensionNames = params.dimensionNames!.slice(offset); - const data = (params.value as any[]).slice(offset); - - const idx = dimensionNames.findIndex(v => v === label)!; - const value = data[idx]; - const type = coltypes[idx]; - - return [label, tooltipFormatterMap[type]?.(value) ?? value]; - }), - dimensionLabel ? params.seriesName : undefined, - ), - }, - legend: { - ...getLegendProps( - effectiveLegendType, - legendOrientation, - showLegend, - theme, - zoomable, - legendState, - padding, - ), - data: legendData, - }, - grid: { - ...defaultGrid, - ...padding, - }, - dataZoom: zoomable && [ - { - type: 'slider', - filterMode: 'none', - start: TIMESERIES_CONSTANTS.dataZoomStart, - end: TIMESERIES_CONSTANTS.dataZoomEnd, - bottom: TIMESERIES_CONSTANTS.zoomBottom, - }, - ], - toolbox: { - show: zoomable, - top: TIMESERIES_CONSTANTS.toolboxTop, - right: TIMESERIES_CONSTANTS.toolboxRight, - feature: { - dataZoom: { - yAxisIndex: false, - title: { - zoom: t('zoom area'), - back: t('restore zoom'), - }, - }, - }, - }, - series, - xAxis: { - name: xAxisTitle, - nameLocation: 'middle', - type: AxisType.Time, - nameGap: convertInteger(xAxisTitleMargin), - axisLabel: { - formatter: xAxisFormatter, - hideOverlap: true, - }, - min: bounds[0], - max: bounds[1], - }, - yAxis: { - name: yAxisTitle, - nameGap: convertInteger(yAxisTitleMargin), - nameLocation: 'middle', - axisLabel: { - show: false, - }, - splitLine: { - show: false, - }, - type: AxisType.Value, - min: 0, - max: seriesCount, - }, - }; - - return { - formData, - queriesData, - echartOptions, - height, - filterState, - width, - theme, - hooks, - emitCrossFilters, - datasource, - refs, - setControlValue, - onLegendStateChanged, - }; -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Gantt/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Gantt/types.ts deleted file mode 100644 index 08d44364bc8..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Gantt/types.ts +++ /dev/null @@ -1,70 +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 { - ChartDataResponseResult, - ChartProps, - QueryFormColumn, - QueryFormData, - QueryFormMetric, -} from '@superset-ui/core'; -import { - BaseTransformedProps, - CrossFilterTransformedProps, - LegendFormData, -} from '../types'; - -export type EchartsGanttChartTransformedProps = - BaseTransformedProps & CrossFilterTransformedProps; - -export type EchartsGanttFormData = QueryFormData & - LegendFormData & { - viz_type: 'gantt_chart'; - startTime: QueryFormColumn; - endTime: QueryFormColumn; - yAxis: QueryFormColumn; - tooltipMetrics: QueryFormMetric[]; - tooltipColumns: QueryFormColumn[]; - series?: QueryFormColumn; - xAxisTimeFormat?: string; - tooltipTimeFormat?: string; - tooltipValuesFormat?: string; - colorScheme?: string; - zoomable?: boolean; - xAxisTitle?: string; - xAxisTitleMargin?: number; - yAxisTitle?: string; - yAxisTitleMargin?: number; - yAxisTitlePosition?: string; - xAxisTimeBounds?: [string | null, string | null]; - subcategories?: boolean; - showExtraControls?: boolean; - }; - -export interface EchartsGanttChartProps extends ChartProps { - formData: EchartsGanttFormData; - queriesData: ChartDataResponseResult[]; -} - -export interface Cartesian2dCoordSys { - type: 'cartesian2d'; - x: number; - y: number; - width: number; - height: number; -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/EchartsGauge.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/EchartsGauge.tsx deleted file mode 100644 index 3482977f83e..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/EchartsGauge.tsx +++ /dev/null @@ -1,40 +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 { GaugeChartTransformedProps } from './types'; -import Echart from '../components/Echart'; -import { allEventHandlers } from '../utils/eventHandlers'; - -export default function EchartsGauge(props: GaugeChartTransformedProps) { - const { height, width, echartOptions, selectedValues, refs, formData } = - props; - - const eventHandlers = allEventHandlers(props); - - return ( - - ); -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/buildQuery.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/buildQuery.ts deleted file mode 100644 index 8b47fb5e725..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/buildQuery.ts +++ /dev/null @@ -1,29 +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 { buildQueryContext, QueryFormData } from '@superset-ui/core'; - -export default function buildQuery(formData: QueryFormData) { - const { metric, sort_by_metric } = formData; - return buildQueryContext(formData, baseQueryObject => [ - { - ...baseQueryObject, - ...(sort_by_metric && { orderby: [[metric, false]] }), - }, - ]); -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/controlPanel.tsx deleted file mode 100644 index 423fa6f4d81..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/controlPanel.tsx +++ /dev/null @@ -1,303 +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 { t } from '@apache-superset/core/translation'; -import { - sharedControls, - ControlPanelConfig, - ControlSubSectionHeader, - D3_FORMAT_OPTIONS, - getStandardizedControls, -} from '@superset-ui/chart-controls'; -import { DEFAULT_FORM_DATA } from './types'; - -const config: ControlPanelConfig = { - controlPanelSections: [ - { - label: t('Query'), - expanded: true, - controlSetRows: [ - [ - { - name: 'groupby', - config: { - ...sharedControls.groupby, - description: t('Columns to group by'), - }, - }, - ], - ['metric'], - ['adhoc_filters'], - [ - { - name: 'row_limit', - config: { - ...sharedControls.row_limit, - choices: [...Array(10).keys()].map(n => n + 1), - default: DEFAULT_FORM_DATA.rowLimit, - }, - }, - ], - ['sort_by_metric'], - ], - }, - { - label: t('Chart Options'), - expanded: true, - controlSetRows: [ - [{t('General')}], - [ - { - name: 'min_val', - config: { - type: 'TextControl', - isInt: true, - default: DEFAULT_FORM_DATA.minVal, - renderTrigger: true, - label: t('Min'), - description: t('Minimum value on the gauge axis'), - }, - }, - { - name: 'max_val', - config: { - type: 'TextControl', - isInt: true, - default: DEFAULT_FORM_DATA.maxVal, - renderTrigger: true, - label: t('Max'), - description: t('Maximum value on the gauge axis'), - }, - }, - ], - [ - { - name: 'start_angle', - config: { - type: 'TextControl', - label: t('Start angle'), - description: t('Angle at which to start progress axis'), - renderTrigger: true, - default: DEFAULT_FORM_DATA.startAngle, - }, - }, - { - name: 'end_angle', - config: { - type: 'TextControl', - label: t('End angle'), - description: t('Angle at which to end progress axis'), - renderTrigger: true, - default: DEFAULT_FORM_DATA.endAngle, - }, - }, - ], - ['color_scheme'], - [ - { - name: 'font_size', - config: { - type: 'SliderControl', - label: t('Font size'), - description: t( - 'Font size for axis labels, detail value and other text elements', - ), - renderTrigger: true, - min: 10, - max: 20, - default: DEFAULT_FORM_DATA.fontSize, - }, - }, - ], - [ - { - name: 'number_format', - config: { - type: 'SelectControl', - label: t('Number format'), - description: t( - 'D3 format syntax: https://github.com/d3/d3-format', - ), - freeForm: true, - renderTrigger: true, - default: DEFAULT_FORM_DATA.numberFormat, - choices: D3_FORMAT_OPTIONS, - }, - }, - ], - ['currency_format'], - [ - { - name: 'value_formatter', - config: { - type: 'TextControl', - label: t('Value format'), - description: t( - 'Additional text to add before or after the value, e.g. unit', - ), - renderTrigger: true, - default: DEFAULT_FORM_DATA.valueFormatter, - }, - }, - ], - [ - { - name: 'show_pointer', - config: { - type: 'CheckboxControl', - label: t('Show pointer'), - description: t('Whether to show the pointer'), - renderTrigger: true, - default: DEFAULT_FORM_DATA.showPointer, - }, - }, - ], - [ - { - name: 'animation', - config: { - type: 'CheckboxControl', - label: t('Animation'), - description: t( - 'Whether to animate the progress and the value or just display them', - ), - renderTrigger: true, - default: DEFAULT_FORM_DATA.animation, - }, - }, - ], - [{t('Axis')}], - [ - { - name: 'show_axis_tick', - config: { - type: 'CheckboxControl', - label: t('Show axis line ticks'), - description: t('Whether to show minor ticks on the axis'), - renderTrigger: true, - default: DEFAULT_FORM_DATA.showAxisTick, - }, - }, - ], - [ - { - name: 'show_split_line', - config: { - type: 'CheckboxControl', - label: t('Show split lines'), - description: t('Whether to show the split lines on the axis'), - renderTrigger: true, - default: DEFAULT_FORM_DATA.showSplitLine, - }, - }, - ], - [ - { - name: 'split_number', - config: { - type: 'SliderControl', - label: t('Split number'), - description: t('Number of split segments on the axis'), - renderTrigger: true, - min: 3, - max: 30, - default: DEFAULT_FORM_DATA.splitNumber, - }, - }, - ], - [{t('Progress')}], - [ - { - name: 'show_progress', - config: { - type: 'CheckboxControl', - label: t('Show progress'), - description: t('Whether to show the progress of gauge chart'), - renderTrigger: true, - default: DEFAULT_FORM_DATA.showProgress, - }, - }, - ], - [ - { - name: 'overlap', - config: { - type: 'CheckboxControl', - label: t('Overlap'), - description: t( - 'Whether the progress bar overlaps when there are multiple groups of data', - ), - renderTrigger: true, - default: DEFAULT_FORM_DATA.overlap, - }, - }, - ], - [ - { - name: 'round_cap', - config: { - type: 'CheckboxControl', - label: t('Round cap'), - description: t( - 'Style the ends of the progress bar with a round cap', - ), - renderTrigger: true, - default: DEFAULT_FORM_DATA.roundCap, - }, - }, - ], - [{t('Intervals')}], - [ - { - name: 'intervals', - config: { - type: 'TextControl', - label: t('Interval bounds'), - description: t( - 'Comma-separated interval bounds, e.g. 2,4,5 for intervals 0-2, 2-4 and 4-5. Last number should match the value provided for MAX.', - ), - renderTrigger: true, - default: DEFAULT_FORM_DATA.intervals, - }, - }, - ], - [ - { - name: 'interval_color_indices', - config: { - type: 'TextControl', - label: t('Interval colors'), - description: t( - 'Comma-separated color picks for the intervals, e.g. 1,2,4. Integers denote colors from the chosen color scheme and are 1-indexed. Length must be matching that of interval bounds.', - ), - renderTrigger: true, - default: DEFAULT_FORM_DATA.intervalColorIndices, - }, - }, - ], - ], - }, - ], - formDataOverrides: formData => ({ - ...formData, - metric: getStandardizedControls().shiftMetric(), - groupby: getStandardizedControls().popAllColumns(), - }), -}; - -export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/index.ts deleted file mode 100644 index 1347ec21dcd..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/index.ts +++ /dev/null @@ -1,72 +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 { t } from '@apache-superset/core/translation'; -import { Behavior } from '@superset-ui/core'; -import controlPanel from './controlPanel'; -import transformProps from './transformProps'; -import thumbnail from './images/thumbnail.png'; -import thumbnailDark from './images/thumbnail-dark.png'; -import example1 from './images/example1.jpg'; -import example1Dark from './images/example1-dark.jpg'; -import example2 from './images/example2.jpg'; -import example2Dark from './images/example2-dark.jpg'; -import buildQuery from './buildQuery'; -import { EchartsGaugeChartProps, EchartsGaugeFormData } from './types'; -import { EchartsChartPlugin } from '../types'; - -export default class EchartsGaugeChartPlugin extends EchartsChartPlugin< - EchartsGaugeFormData, - EchartsGaugeChartProps -> { - constructor() { - super({ - buildQuery, - controlPanel, - loadChart: () => import('./EchartsGauge'), - metadata: { - behaviors: [ - Behavior.InteractiveChart, - Behavior.DrillToDetail, - Behavior.DrillBy, - ], - category: t('KPI'), - credits: ['https://echarts.apache.org'], - description: t( - 'Uses a gauge to showcase progress of a metric towards a target. The position of the dial represents the progress and the terminal value in the gauge represents the target value.', - ), - exampleGallery: [ - { url: example1, urlDark: example1Dark }, - { url: example2, urlDark: example2Dark }, - ], - name: t('Gauge Chart'), - tags: [ - t('Multi-Variables'), - t('Business'), - t('Comparison'), - t('ECharts'), - t('Report'), - t('Featured'), - ], - thumbnail, - thumbnailDark, - }, - transformProps, - }); - } -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/index.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/index.tsx new file mode 100644 index 00000000000..514f789c950 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/index.tsx @@ -0,0 +1,646 @@ +/** + * 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. + */ + +/** + * ECharts Gauge Chart - Glyph Pattern Implementation + * + * Shows a gauge visualization with optional interval coloring, + * progress bars, and multiple data series support. + */ + +import { t } from '@apache-superset/core/translation'; +import { + Behavior, + buildQueryContext, + CategoricalColorNamespace, + CategoricalColorScale, + Currency as CurrencyType, + DataRecord, + getColumnLabel, + getMetricLabel, + getValueFormatter, + QueryFormData, + QueryFormMetric, + tooltipHtml, +} from '@superset-ui/core'; +import type { EChartsCoreOption } from 'echarts/core'; +import type { GaugeSeriesOption } from 'echarts/charts'; +import type { GaugeDataItemOption } from 'echarts/types/src/chart/gauge/GaugeSeries'; +import type { CallbackDataParams } from 'echarts/types/src/util/types'; +import { range } from 'lodash'; + +import { + defineChart, + Metric, + Dimension, + Text, + Checkbox, + Int, + NumberFormat, + Currency, + ChartProps, + SortByMetric, +} from '@superset-ui/glyph-core'; + +import { OpacityEnum } from '../constants'; +import { parseNumbersList } from '../utils/controls'; +import { getColtypesMapping } from '../utils/series'; +import { getDefaultTooltip } from '../utils/tooltip'; +import { allEventHandlers } from '../utils/eventHandlers'; +import Echart from '../components/Echart'; +import { Refs } from '../types'; +import { + EchartsGaugeFormData, + AxisTickLineStyle, + GaugeChartTransformedProps, +} from './types'; +import { + defaultGaugeSeriesOption, + INTERVAL_GAUGE_SERIES_OPTION, + OFFSETS, + FONT_SIZE_MULTIPLIERS, +} from './constants'; + +import thumbnail from './images/thumbnail.png'; +import example from './images/example1.jpg'; +import exampleDark from './images/example1-dark.jpg'; + +// ============================================================================ +// Helpers +// ============================================================================ + +export const getIntervalBoundsAndColors = ( + intervals: string, + intervalColorIndices: string, + colorFn: CategoricalColorScale, + min: number, + max: number, +): Array<[number, string]> => { + let intervalBoundsNonNormalized; + let intervalColorIndicesArray; + try { + intervalBoundsNonNormalized = parseNumbersList(intervals, ','); + intervalColorIndicesArray = parseNumbersList(intervalColorIndices, ','); + } catch (error) { + intervalBoundsNonNormalized = [] as number[]; + intervalColorIndicesArray = [] as number[]; + } + + const intervalBounds = intervalBoundsNonNormalized.map( + bound => (bound - min) / (max - min), + ); + const intervalColors = intervalColorIndicesArray.map( + ind => colorFn.colors[(ind - 1) % colorFn.colors.length], + ); + + return intervalBounds.map((val, idx) => { + const color = intervalColors[idx]; + return [val, color || colorFn.colors[idx]]; + }); +}; + +const calculateAxisLineWidth = ( + data: DataRecord[], + fontSize: number, + overlap: boolean, +): number => (overlap ? fontSize : data.length * fontSize); + +const calculateMin = (data: GaugeDataItemOption[]) => + 2 * Math.min(...data.map(d => d.value as number).concat([0])); + +const calculateMax = (data: GaugeDataItemOption[]) => + 2 * Math.max(...data.map(d => d.value as number).concat([0])); + +// ============================================================================ +// Build Query - exported for testing +// ============================================================================ + +export function buildQuery(formData: QueryFormData) { + const { metric, sort_by_metric: sortByMetric } = formData; + return buildQueryContext(formData, baseQueryObject => [ + { + ...baseQueryObject, + ...(sortByMetric && { orderby: [[metric, false]] }), + }, + ]); +} + +// ============================================================================ +// Transform Result Type +// ============================================================================ + +interface GaugeTransformResult { + transformedProps: GaugeChartTransformedProps; +} + +// ============================================================================ +// The Chart Definition +// ============================================================================ + +export default defineChart< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + any, + GaugeTransformResult +>({ + metadata: { + name: t('Gauge Chart'), + description: t( + 'Uses a gauge to showcase the variation of a metric across one or multiple groups.', + ), + category: t('KPI'), + tags: [ + t('Multi-Variables'), + t('Business'), + t('Comparison'), + t('ECharts'), + t('Report'), + t('Featured'), + ], + thumbnail, + behaviors: [ + Behavior.InteractiveChart, + Behavior.DrillToDetail, + Behavior.DrillBy, + ], + exampleGallery: [{ url: example, urlDark: exampleDark }], + }, + + arguments: { + // Query section + groupby: Dimension.with({ + label: t('Dimensions'), + description: t('Columns to group by'), + }), + + metric: Metric.with({ + label: t('Metric'), + description: t('The metric to display'), + }), + + sortByMetric: SortByMetric, + + // General options + minVal: Text.with({ + label: t('Min'), + description: t('Minimum value on the gauge axis'), + default: '', + }), + + maxVal: Text.with({ + label: t('Max'), + description: t('Maximum value on the gauge axis'), + default: '', + }), + + startAngle: Int.with({ + label: t('Start Angle'), + description: t('Angle at which to start progress axis'), + default: 225, + min: 0, + max: 360, + step: 1, + }), + + endAngle: Int.with({ + label: t('End Angle'), + description: t('Angle at which to end progress axis'), + default: -45, + min: -360, + max: 360, + step: 1, + }), + + fontSize: Int.with({ + label: t('Font Size'), + description: t( + 'Font size for axis labels, detail value and other text elements', + ), + default: 15, + min: 10, + max: 20, + step: 1, + }), + + numberFormat: NumberFormat, + currencyFormat: Currency, + + valueFormatter: Text.with({ + label: t('Value Format'), + description: t( + 'Additional text to add before or after the value, e.g. unit', + ), + default: '{value}', + }), + + showPointer: Checkbox.with({ + label: t('Show Pointer'), + description: t('Whether to show the pointer'), + default: true, + }), + + animation: Checkbox.with({ + label: t('Animation'), + description: t( + 'Whether to animate the progress and the value or just display them', + ), + default: true, + }), + + // Axis options + showAxisTick: Checkbox.with({ + label: t('Show Axis Line Ticks'), + description: t('Whether to show minor ticks on the axis'), + default: false, + }), + + showSplitLine: Checkbox.with({ + label: t('Show Split Lines'), + description: t('Whether to show the split lines on the axis'), + default: false, + }), + + splitNumber: Int.with({ + label: t('Split Number'), + description: t('Number of split segments on the axis'), + default: 10, + min: 3, + max: 30, + step: 1, + }), + + // Progress options + showProgress: Checkbox.with({ + label: t('Show Progress'), + description: t('Whether to show the progress of gauge chart'), + default: true, + }), + + overlap: Checkbox.with({ + label: t('Overlap'), + description: t( + 'Whether the progress bar overlaps when there are multiple groups of data', + ), + default: true, + }), + + roundCap: Checkbox.with({ + label: t('Round Cap'), + description: t('Style the ends of the progress bar with a round cap'), + default: false, + }), + + // Interval options + intervals: Text.with({ + label: t('Interval Bounds'), + description: t( + 'Comma-separated interval bounds, e.g. 2,4,5 for intervals 0-2, 2-4 and 4-5. Last number should match the value provided for MAX.', + ), + default: '', + }), + + intervalColorIndices: Text.with({ + label: t('Interval Colors'), + description: t( + 'Comma-separated color picks for the intervals, e.g. 1,2,4. Integers denote colors from the chosen color scheme and are 1-indexed. Length must be matching that of interval bounds.', + ), + default: '', + }), + }, + + buildQuery, + + transform: (chartProps: ChartProps): GaugeTransformResult => { + const { + width, + height, + formData, + queriesData, + hooks, + filterState, + theme, + emitCrossFilters, + datasource, + } = chartProps; + + const rawFormData = formData as Record; + const gaugeSeriesOptions = defaultGaugeSeriesOption(theme); + const { + verboseMap = {}, + currencyFormats = {}, + columnFormats = {}, + } = datasource ?? {}; + + // Extract form values + const groupby = (rawFormData.groupby as string[]) || []; + const metric = (rawFormData.metric as string) ?? ''; + const minVal = rawFormData.min_val as string | number | null; + const maxVal = rawFormData.max_val as string | number | null; + const colorScheme = rawFormData.color_scheme as string; + const fontSize = (rawFormData.font_size as number) ?? 15; + const numberFormat = + (rawFormData.number_format as string) ?? 'SMART_NUMBER'; + const currencyFormat = rawFormData.currency_format as + | CurrencyType + | undefined; + const animation = (rawFormData.animation as boolean) ?? true; + const showProgress = (rawFormData.show_progress as boolean) ?? true; + const overlap = (rawFormData.overlap as boolean) ?? true; + const roundCap = (rawFormData.round_cap as boolean) ?? false; + const showAxisTick = (rawFormData.show_axis_tick as boolean) ?? false; + const showSplitLine = (rawFormData.show_split_line as boolean) ?? false; + const splitNumber = (rawFormData.split_number as number) ?? 10; + const startAngle = (rawFormData.start_angle as number) ?? 225; + const endAngle = (rawFormData.end_angle as number) ?? -45; + const showPointer = (rawFormData.show_pointer as boolean) ?? true; + const intervals = (rawFormData.intervals as string) ?? ''; + const intervalColorIndices = + (rawFormData.interval_color_indices as string) ?? ''; + const valueFormatter = (rawFormData.value_formatter as string) ?? '{value}'; + const sliceId = rawFormData.slice_id as number | undefined; + + const refs: Refs = {}; + const data = (queriesData[0]?.data || []) as DataRecord[]; + const { colnames = [], coltypes = [] } = + (queriesData[0] as { colnames?: string[]; coltypes?: number[] }) ?? {}; + const coltypeMapping = getColtypesMapping({ colnames, coltypes }); + const numberFormatter = getValueFormatter( + metric, + currencyFormats, + columnFormats, + numberFormat, + currencyFormat, + ); + const colorFn = CategoricalColorNamespace.getScale(colorScheme as string); + const axisLineWidth = calculateAxisLineWidth(data, fontSize, overlap); + const groupbyLabels = groupby.map(getColumnLabel); + const formatValue = (value: number) => + valueFormatter.replace('{value}', numberFormatter(value)); + const axisTickLength = FONT_SIZE_MULTIPLIERS.axisTickLength * fontSize; + const splitLineLength = FONT_SIZE_MULTIPLIERS.splitLineLength * fontSize; + const titleOffsetFromTitle = + FONT_SIZE_MULTIPLIERS.titleOffsetFromTitle * fontSize; + const detailOffsetFromTitle = + FONT_SIZE_MULTIPLIERS.detailOffsetFromTitle * fontSize; + const columnsLabelMap = new Map(); + const metricLabel = getMetricLabel(metric as QueryFormMetric); + + const transformedData: GaugeDataItemOption[] = data.map( + (dataPoint: DataRecord, index: number) => { + const name = groupbyLabels + .map( + (column: string) => + `${verboseMap[column] || column}: ${dataPoint[column]}`, + ) + .join(', '); + const colorLabel = groupbyLabels.map( + (col: string) => dataPoint[col] as string, + ); + columnsLabelMap.set( + name, + groupbyLabels.map((col: string) => dataPoint[col] as string), + ); + let item: GaugeDataItemOption = { + value: dataPoint[metricLabel] as number, + name, + itemStyle: { + color: colorFn(colorLabel, sliceId), + }, + title: { + offsetCenter: [ + '0%', + `${index * titleOffsetFromTitle + OFFSETS.titleFromCenter}%`, + ], + fontSize, + color: + (theme as { colorTextSecondary?: string })?.colorTextSecondary ?? + '#666', + }, + detail: { + offsetCenter: [ + '0%', + `${ + index * titleOffsetFromTitle + + OFFSETS.titleFromCenter + + detailOffsetFromTitle + }%`, + ], + fontSize: FONT_SIZE_MULTIPLIERS.detailFontSize * fontSize, + color: (theme as { colorText?: string })?.colorText ?? '#000', + }, + }; + if ( + filterState?.selectedValues && + !filterState.selectedValues.includes(name) + ) { + item = { + ...item, + itemStyle: { + color: colorFn(index, sliceId), + opacity: OpacityEnum.SemiTransparent, + }, + detail: { + show: false, + }, + title: { + show: false, + }, + }; + } + return item; + }, + ); + + const { setDataMask = () => {}, onContextMenu } = hooks ?? {}; + + const isValidNumber = ( + val: number | null | undefined | string, + ): val is number => { + if (val == null || val === '') return false; + const num = typeof val === 'string' ? Number(val) : val; + return !Number.isNaN(num) && Number.isFinite(num); + }; + + const min = isValidNumber(minVal) + ? Number(minVal) + : calculateMin(transformedData); + const max = isValidNumber(maxVal) + ? Number(maxVal) + : calculateMax(transformedData); + const axisLabels = range(min, max, (max - min) / splitNumber); + const axisLabelLength = Math.max( + ...axisLabels.map(label => numberFormatter(label).length).concat([1]), + ); + const intervalBoundsAndColors = getIntervalBoundsAndColors( + intervals, + intervalColorIndices, + colorFn, + min, + max, + ); + const splitLineDistance = + axisLineWidth + splitLineLength + OFFSETS.ticksFromLine; + const axisLabelDistance = + FONT_SIZE_MULTIPLIERS.axisLabelDistance * + fontSize * + FONT_SIZE_MULTIPLIERS.axisLabelLength * + axisLabelLength + + (showSplitLine ? splitLineLength : 0) + + (showAxisTick ? axisTickLength : 0) + + OFFSETS.ticksFromLine - + axisLineWidth; + const axisTickDistance = + axisLineWidth + axisTickLength + OFFSETS.ticksFromLine; + + const progress = { + show: showProgress, + overlap, + roundCap, + width: fontSize, + }; + const splitLine = { + show: showSplitLine, + distance: -splitLineDistance, + length: splitLineLength, + lineStyle: { + width: FONT_SIZE_MULTIPLIERS.splitLineWidth * fontSize, + color: gaugeSeriesOptions.splitLine?.lineStyle?.color, + }, + }; + const axisLine = { + roundCap, + lineStyle: { + width: axisLineWidth, + color: gaugeSeriesOptions.axisLine?.lineStyle?.color, + }, + }; + const axisLabel = { + distance: -axisLabelDistance, + fontSize, + formatter: numberFormatter, + color: gaugeSeriesOptions.axisLabel?.color, + }; + const axisTick = { + show: showAxisTick, + distance: -axisTickDistance, + length: axisTickLength, + lineStyle: gaugeSeriesOptions.axisTick?.lineStyle as AxisTickLineStyle, + }; + const detail = { + valueAnimation: animation, + formatter: (value: number) => formatValue(value), + color: gaugeSeriesOptions.detail?.color, + }; + const tooltip = { + ...getDefaultTooltip(refs), + formatter: (params: CallbackDataParams) => { + const { name, value } = params; + return tooltipHtml([[metricLabel, formatValue(value as number)]], name); + }, + }; + + let pointer; + if (intervalBoundsAndColors.length) { + splitLine.lineStyle.color = + INTERVAL_GAUGE_SERIES_OPTION.splitLine?.lineStyle?.color; + axisTick.lineStyle.color = INTERVAL_GAUGE_SERIES_OPTION?.axisTick + ?.lineStyle?.color as string; + axisLabel.color = INTERVAL_GAUGE_SERIES_OPTION.axisLabel?.color; + axisLine.lineStyle.color = intervalBoundsAndColors; + pointer = { + show: showPointer, + showAbove: false, + itemStyle: INTERVAL_GAUGE_SERIES_OPTION.pointer?.itemStyle, + }; + } else { + pointer = { + show: showPointer, + showAbove: false, + }; + } + + const series: GaugeSeriesOption[] = [ + { + type: 'gauge', + startAngle, + endAngle, + min, + max, + progress, + animation, + axisLine: axisLine as GaugeSeriesOption['axisLine'], + splitLine, + splitNumber, + axisLabel, + axisTick, + pointer, + detail, + // @ts-ignore + tooltip, + radius: + Math.min(width, height) / 2 - axisLabelDistance - axisTickDistance, + center: ['50%', '55%'], + data: transformedData, + }, + ]; + + const echartOptions: EChartsCoreOption = { + tooltip: { + ...getDefaultTooltip(refs), + trigger: 'item', + }, + series, + }; + + return { + transformedProps: { + formData: formData as EchartsGaugeFormData, + width, + height, + echartOptions, + setDataMask, + emitCrossFilters, + labelMap: Object.fromEntries(columnsLabelMap), + groupby, + selectedValues: filterState?.selectedValues || [], + onContextMenu, + refs, + coltypeMapping, + }, + }; + }, + + render: ({ transformedProps }) => { + const { height, width, echartOptions, selectedValues, refs, formData } = + transformedProps; + + const eventHandlers = allEventHandlers(transformedProps); + + return ( + + ); + }, +}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/stories/Gauge.stories.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/stories/Gauge.stories.tsx deleted file mode 100644 index f90010c28d3..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/stories/Gauge.stories.tsx +++ /dev/null @@ -1,118 +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 { SuperChart, getChartTransformPropsRegistry } from '@superset-ui/core'; -import { - EchartsGaugeChartPlugin, - GaugeTransformProps, -} from '@superset-ui/plugin-chart-echarts'; -import { withResizableChartDemo } from '@storybook-shared'; -import { speed } from './data'; - -new EchartsGaugeChartPlugin().configure({ key: 'echarts-gauge' }).register(); - -getChartTransformPropsRegistry().registerValue( - 'echarts-gauge', - GaugeTransformProps, -); - -export default { - title: 'Chart Plugins/plugin-chart-echarts/Gauge', - decorators: [withResizableChartDemo], - args: { - colorScheme: 'supersetColors', - showProgress: true, - showPointer: true, - splitNumber: 10, - numberFormat: 'SMART_NUMBER', - minVal: 0, - maxVal: 100, - startAngle: 225, - endAngle: -45, - }, - argTypes: { - colorScheme: { - control: 'select', - options: [ - 'supersetColors', - 'd3Category10', - 'bnbColors', - 'googleCategory20c', - ], - }, - showProgress: { control: 'boolean' }, - showPointer: { control: 'boolean' }, - splitNumber: { control: { type: 'range', min: 2, max: 20, step: 1 } }, - numberFormat: { - control: 'select', - options: ['SMART_NUMBER', '.2f', '.0%', '$,.2f', '.3s'], - }, - minVal: { control: 'number' }, - maxVal: { control: 'number' }, - startAngle: { control: { type: 'range', min: 0, max: 360, step: 15 } }, - endAngle: { control: { type: 'range', min: -360, max: 0, step: 15 } }, - }, -}; - -export const Gauge = ({ - width, - height, - colorScheme, - showProgress, - showPointer, - splitNumber, - numberFormat, - minVal, - maxVal, - startAngle, - endAngle, -}: { - width: number; - height: number; - colorScheme: string; - showProgress: boolean; - showPointer: boolean; - splitNumber: number; - numberFormat: string; - minVal: number; - maxVal: number; - startAngle: number; - endAngle: number; -}) => ( - -); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/transformProps.ts deleted file mode 100644 index c879557ba75..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/transformProps.ts +++ /dev/null @@ -1,383 +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 { - QueryFormMetric, - CategoricalColorNamespace, - CategoricalColorScale, - DataRecord, - getMetricLabel, - getColumnLabel, - getValueFormatter, - tooltipHtml, -} from '@superset-ui/core'; -import type { EChartsCoreOption } from 'echarts/core'; -import type { GaugeSeriesOption } from 'echarts/charts'; -import type { GaugeDataItemOption } from 'echarts/types/src/chart/gauge/GaugeSeries'; -import type { CallbackDataParams } from 'echarts/types/src/util/types'; -import { range } from 'lodash'; -import { parseNumbersList } from '../utils/controls'; -import { - DEFAULT_FORM_DATA as DEFAULT_GAUGE_FORM_DATA, - EchartsGaugeFormData, - AxisTickLineStyle, - GaugeChartTransformedProps, - EchartsGaugeChartProps, -} from './types'; -import { - defaultGaugeSeriesOption, - INTERVAL_GAUGE_SERIES_OPTION, - OFFSETS, - FONT_SIZE_MULTIPLIERS, -} from './constants'; -import { OpacityEnum } from '../constants'; -import { getDefaultTooltip } from '../utils/tooltip'; -import { Refs } from '../types'; -import { getColtypesMapping } from '../utils/series'; - -export const getIntervalBoundsAndColors = ( - intervals: string, - intervalColorIndices: string, - colorFn: CategoricalColorScale, - min: number, - max: number, -): Array<[number, string]> => { - let intervalBoundsNonNormalized; - let intervalColorIndicesArray; - try { - intervalBoundsNonNormalized = parseNumbersList(intervals, ','); - intervalColorIndicesArray = parseNumbersList(intervalColorIndices, ','); - } catch (error) { - intervalBoundsNonNormalized = [] as number[]; - intervalColorIndicesArray = [] as number[]; - } - - const intervalBounds = intervalBoundsNonNormalized.map( - bound => (bound - min) / (max - min), - ); - const intervalColors = intervalColorIndicesArray.map( - ind => colorFn.colors[(ind - 1) % colorFn.colors.length], - ); - - return intervalBounds.map((val, idx) => { - const color = intervalColors[idx]; - return [val, color || colorFn.colors[idx]]; - }); -}; - -const calculateAxisLineWidth = ( - data: DataRecord[], - fontSize: number, - overlap: boolean, -): number => (overlap ? fontSize : data.length * fontSize); - -const calculateMin = (data: GaugeDataItemOption[]) => - 2 * Math.min(...data.map(d => d.value as number).concat([0])); - -const calculateMax = (data: GaugeDataItemOption[]) => - 2 * Math.max(...data.map(d => d.value as number).concat([0])); - -export default function transformProps( - chartProps: EchartsGaugeChartProps, -): GaugeChartTransformedProps { - const { - width, - height, - formData, - queriesData, - hooks, - filterState, - theme, - emitCrossFilters, - datasource, - } = chartProps; - - const gaugeSeriesOptions = defaultGaugeSeriesOption(theme); - const { - verboseMap = {}, - currencyFormats = {}, - columnFormats = {}, - currencyCodeColumn, - } = datasource; - const { - groupby, - metric, - minVal, - maxVal, - colorScheme, - fontSize, - numberFormat, - currencyFormat, - animation, - showProgress, - overlap, - roundCap, - showAxisTick, - showSplitLine, - splitNumber, - startAngle, - endAngle, - showPointer, - intervals, - intervalColorIndices, - valueFormatter, - sliceId, - }: EchartsGaugeFormData = { ...DEFAULT_GAUGE_FORM_DATA, ...formData }; - const refs: Refs = {}; - const data = (queriesData[0]?.data || []) as DataRecord[]; - const detectedCurrency = queriesData[0]?.detected_currency; - const coltypeMapping = getColtypesMapping(queriesData[0]); - const numberFormatter = getValueFormatter( - metric, - currencyFormats, - columnFormats, - numberFormat, - currencyFormat, - undefined, - data, - currencyCodeColumn, - detectedCurrency, - ); - const colorFn = CategoricalColorNamespace.getScale(colorScheme as string); - const axisLineWidth = calculateAxisLineWidth(data, fontSize, overlap); - const groupbyLabels = groupby.map(getColumnLabel); - const formatValue = (value: number) => - valueFormatter.replace('{value}', numberFormatter(value)); - const axisTickLength = FONT_SIZE_MULTIPLIERS.axisTickLength * fontSize; - const splitLineLength = FONT_SIZE_MULTIPLIERS.splitLineLength * fontSize; - const titleOffsetFromTitle = - FONT_SIZE_MULTIPLIERS.titleOffsetFromTitle * fontSize; - const detailOffsetFromTitle = - FONT_SIZE_MULTIPLIERS.detailOffsetFromTitle * fontSize; - const columnsLabelMap = new Map(); - const metricLabel = getMetricLabel(metric as QueryFormMetric); - - const transformedData: GaugeDataItemOption[] = data.map( - (data_point, index) => { - const name = groupbyLabels - .map(column => `${verboseMap[column] || column}: ${data_point[column]}`) - .join(', '); - const colorLabel = groupbyLabels.map(col => data_point[col] as string); - columnsLabelMap.set( - name, - groupbyLabels.map(col => data_point[col] as string), - ); - let item: GaugeDataItemOption = { - value: data_point[metricLabel] as number, - name, - itemStyle: { - color: colorFn(colorLabel, sliceId), - }, - title: { - offsetCenter: [ - '0%', - `${index * titleOffsetFromTitle + OFFSETS.titleFromCenter}%`, - ], - fontSize, - color: theme.colorTextSecondary, - }, - detail: { - offsetCenter: [ - '0%', - `${ - index * titleOffsetFromTitle + - OFFSETS.titleFromCenter + - detailOffsetFromTitle - }%`, - ], - fontSize: FONT_SIZE_MULTIPLIERS.detailFontSize * fontSize, - color: theme.colorText, - }, - }; - if ( - filterState.selectedValues && - !filterState.selectedValues.includes(name) - ) { - item = { - ...item, - itemStyle: { - color: colorFn(index, sliceId), - opacity: OpacityEnum.SemiTransparent, - }, - detail: { - show: false, - }, - title: { - show: false, - }, - }; - } - return item; - }, - ); - - const { setDataMask = () => {}, onContextMenu } = hooks; - - const isValidNumber = ( - val: number | null | undefined | string, - ): val is number => { - if (val == null || val === '') return false; - const num = typeof val === 'string' ? Number(val) : val; - return !Number.isNaN(num) && Number.isFinite(num); - }; - - const min = isValidNumber(minVal) - ? Number(minVal) - : calculateMin(transformedData); - const max = isValidNumber(maxVal) - ? Number(maxVal) - : calculateMax(transformedData); - const axisLabels = range(min, max, (max - min) / splitNumber); - const axisLabelLength = Math.max( - ...axisLabels.map(label => numberFormatter(label).length).concat([1]), - ); - const intervalBoundsAndColors = getIntervalBoundsAndColors( - intervals, - intervalColorIndices, - colorFn, - min, - max, - ); - const splitLineDistance = - axisLineWidth + splitLineLength + OFFSETS.ticksFromLine; - const axisLabelDistance = - FONT_SIZE_MULTIPLIERS.axisLabelDistance * - fontSize * - FONT_SIZE_MULTIPLIERS.axisLabelLength * - axisLabelLength + - (showSplitLine ? splitLineLength : 0) + - (showAxisTick ? axisTickLength : 0) + - OFFSETS.ticksFromLine - - axisLineWidth; - const axisTickDistance = - axisLineWidth + axisTickLength + OFFSETS.ticksFromLine; - - const progress = { - show: showProgress, - overlap, - roundCap, - width: fontSize, - }; - const splitLine = { - show: showSplitLine, - distance: -splitLineDistance, - length: splitLineLength, - lineStyle: { - width: FONT_SIZE_MULTIPLIERS.splitLineWidth * fontSize, - color: gaugeSeriesOptions.splitLine?.lineStyle?.color, - }, - }; - const axisLine = { - roundCap, - lineStyle: { - width: axisLineWidth, - color: gaugeSeriesOptions.axisLine?.lineStyle?.color, - }, - }; - const axisLabel = { - distance: -axisLabelDistance, - fontSize, - formatter: numberFormatter, - color: gaugeSeriesOptions.axisLabel?.color, - }; - const axisTick = { - show: showAxisTick, - distance: -axisTickDistance, - length: axisTickLength, - lineStyle: gaugeSeriesOptions.axisTick?.lineStyle as AxisTickLineStyle, - }; - const detail = { - valueAnimation: animation, - formatter: (value: number) => formatValue(value), - color: gaugeSeriesOptions.detail?.color, - }; - const tooltip = { - ...getDefaultTooltip(refs), - formatter: (params: CallbackDataParams) => { - const { name, value } = params; - return tooltipHtml([[metricLabel, formatValue(value as number)]], name); - }, - }; - - let pointer; - if (intervalBoundsAndColors.length) { - splitLine.lineStyle.color = - INTERVAL_GAUGE_SERIES_OPTION.splitLine?.lineStyle?.color; - axisTick.lineStyle.color = INTERVAL_GAUGE_SERIES_OPTION?.axisTick?.lineStyle - ?.color as string; - axisLabel.color = INTERVAL_GAUGE_SERIES_OPTION.axisLabel?.color; - axisLine.lineStyle.color = intervalBoundsAndColors; - pointer = { - show: showPointer, - showAbove: false, - itemStyle: INTERVAL_GAUGE_SERIES_OPTION.pointer?.itemStyle, - }; - } else { - pointer = { - show: showPointer, - showAbove: false, - }; - } - - const series: GaugeSeriesOption[] = [ - { - type: 'gauge', - startAngle, - endAngle, - min, - max, - progress, - animation, - axisLine: axisLine as GaugeSeriesOption['axisLine'], - splitLine, - splitNumber, - axisLabel, - axisTick, - pointer, - detail, - tooltip, - radius: - Math.min(width, height) / 2 - axisLabelDistance - axisTickDistance, - center: ['50%', '55%'], - data: transformedData, - }, - ]; - - const echartOptions: EChartsCoreOption = { - tooltip: { - ...getDefaultTooltip(refs), - trigger: 'item', - }, - series, - }; - - return { - formData, - width, - height, - echartOptions, - setDataMask, - emitCrossFilters, - labelMap: Object.fromEntries(columnsLabelMap), - groupby, - selectedValues: filterState.selectedValues || [], - onContextMenu, - refs, - coltypeMapping, - }; -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/EchartsGraph.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/EchartsGraph.tsx deleted file mode 100644 index b765bb6bc0e..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/EchartsGraph.tsx +++ /dev/null @@ -1,181 +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 { - getColumnLabel, - getNumberFormatter, - getTimeFormatter, -} from '@superset-ui/core'; -import { EventHandlers } from '../types'; -import Echart from '../components/Echart'; -import { GraphChartTransformedProps } from './types'; -import { formatSeriesName } from '../utils/series'; - -type DataRow = { - source?: string; - target?: string; - id?: string; - col: string; - name: string; -}; -type Data = DataRow[]; -type Event = { - name: string; - event: { stop: () => void; event: PointerEvent }; - data: DataRow; - dataType: 'node' | 'edge'; -}; - -export default function EchartsGraph({ - height, - width, - echartOptions, - formData, - onContextMenu, - setDataMask, - filterState, - emitCrossFilters, - refs, - coltypeMapping, -}: GraphChartTransformedProps) { - const getCrossFilterDataMask = (node: DataRow | undefined) => { - if (!node?.name || !node?.col) { - return undefined; - } - const { name, col } = node; - const selected = Object.values( - filterState?.selectedValues || {}, - ) as string[]; - let values: string[]; - if (selected.includes(name)) { - values = selected.filter(v => v !== name); - } else { - values = [name]; - } - return { - dataMask: { - extraFormData: { - filters: values.length - ? [ - { - col, - op: 'IN' as const, - val: values, - }, - ] - : [], - }, - filterState: { - value: values.length ? values : null, - selectedValues: values.length ? values : null, - }, - }, - isCurrentValueSelected: selected.includes(name), - }; - }; - const eventHandlers: EventHandlers = { - click: (e: Event) => { - if (!emitCrossFilters || !setDataMask) { - return; - } - e.event.stop(); - const data = (echartOptions as any).series[0].data as Data; - const node = data.find(item => item.id === e.data.id); - const dataMask = getCrossFilterDataMask(node)?.dataMask; - if (dataMask) { - setDataMask(dataMask); - } - }, - contextmenu: (e: Event) => { - const handleNodeClick = (data: Data) => { - const node = data.find(item => item.id === e.data.id); - if (node?.name) { - return [ - { - col: node.col, - op: '==' as const, - val: node.name, - formattedVal: node.name, - }, - ]; - } - return undefined; - }; - const handleEdgeClick = (data: Data) => { - const sourceValue = data.find(item => item.id === e.data.source)?.name; - const targetValue = data.find(item => item.id === e.data.target)?.name; - if (sourceValue && targetValue) { - return [ - { - col: formData.source, - op: '==' as const, - val: sourceValue, - formattedVal: sourceValue, - }, - { - col: formData.target, - op: '==' as const, - val: targetValue, - formattedVal: targetValue, - }, - ]; - } - return undefined; - }; - if (onContextMenu) { - e.event.stop(); - const pointerEvent = e.event.event; - const data = (echartOptions as any).series[0].data as Data; - const drillToDetailFilters = - e.dataType === 'node' ? handleNodeClick(data) : handleEdgeClick(data); - const node = data.find(item => item.id === e.data.id); - - onContextMenu(pointerEvent.clientX, pointerEvent.clientY, { - drillToDetail: drillToDetailFilters, - crossFilter: getCrossFilterDataMask(node), - drillBy: node && { - filters: [ - { - col: node.col, - op: '==', - val: node.name, - formattedVal: formatSeriesName(node.name, { - timeFormatter: getTimeFormatter(formData.dateFormat), - numberFormatter: getNumberFormatter(formData.numberFormat), - coltype: coltypeMapping?.[getColumnLabel(node.col)], - }), - }, - ], - groupbyFieldName: - node.col === formData.source ? 'source' : 'target', - }, - }); - } - }, - }; - return ( - - ); -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/buildQuery.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/buildQuery.ts deleted file mode 100644 index 78e431ac707..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/buildQuery.ts +++ /dev/null @@ -1,47 +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 { buildQueryContext } from '@superset-ui/core'; -import { EchartsGraphFormData } from './types'; -import { buildColumnsOrderBy, applyOrderBy } from '../utils/orderby'; - -export default function buildQuery(formData: EchartsGraphFormData) { - const { source, target, source_category, target_category, row_limit } = - formData; - const orderby = buildColumnsOrderBy([ - source, - target, - source_category, - target_category, - ]); - - return buildQueryContext(formData, { - queryFields: { - source: 'columns', - target: 'columns', - source_category: 'columns', - target_category: 'columns', - }, - buildQuery: baseQueryObject => [ - { - ...baseQueryObject, - ...applyOrderBy(orderby, row_limit), - }, - ], - }); -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/constants.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/constants.ts deleted file mode 100644 index c193be38eed..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/constants.ts +++ /dev/null @@ -1,57 +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 type { GraphSeriesOption } from 'echarts/charts'; - -export const DEFAULT_GRAPH_SERIES_OPTION: GraphSeriesOption = { - zoom: 0.7, - circular: { rotateLabel: true }, - force: { - initLayout: 'circular', - layoutAnimation: true, - }, - label: { - show: true, - position: 'right', - distance: 5, - rotate: 0, - offset: [0, 0], - fontStyle: 'normal', - fontWeight: 'normal', - fontFamily: 'sans-serif', - fontSize: 12, - padding: [0, 0, 0, 0], - overflow: 'truncate', - formatter: '{b}', - }, - emphasis: { - focus: 'adjacency', - }, - animation: true, - animationDuration: 500, - animationEasing: 'cubicOut', - lineStyle: { color: 'source', curveness: 0.1 }, - select: { - itemStyle: { borderWidth: 3, opacity: 1 }, - label: { fontWeight: 'bolder' }, - }, - // Ref: https://echarts.apache.org/en/option.html#series-graph.data.tooltip.formatter - // - b: data name - // - c: data value - tooltip: { formatter: '{b}: {c}' }, -}; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/controlPanel.tsx deleted file mode 100644 index f382dddfaee..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/controlPanel.tsx +++ /dev/null @@ -1,328 +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 { t } from '@apache-superset/core/translation'; -import { - ControlPanelConfig, - ControlSubSectionHeader, - getStandardizedControls, - sharedControls, -} from '@superset-ui/chart-controls'; -import { DEFAULT_FORM_DATA } from './types'; -import { legendSection } from '../controls'; - -const requiredEntity = { - ...sharedControls.entity, - clearable: false, -}; - -const optionalEntity = { - ...sharedControls.entity, - clearable: true, - validators: [], -}; - -const controlPanel: ControlPanelConfig = { - controlPanelSections: [ - { - label: t('Query'), - expanded: true, - controlSetRows: [ - [ - { - name: 'source', - config: { - ...requiredEntity, - label: t('Source'), - description: t('Name of the source nodes'), - }, - }, - ], - [ - { - name: 'target', - config: { - ...requiredEntity, - label: t('Target'), - description: t('Name of the target nodes'), - }, - }, - ], - ['metric'], - [ - { - name: 'source_category', - config: { - ...optionalEntity, - label: t('Source category'), - description: t( - 'The category of source nodes used to assign colors. ' + - 'If a node is associated with more than one category, only the first will be used.', - ), - }, - }, - ], - [ - { - name: 'target_category', - config: { - ...optionalEntity, - label: t('Target category'), - description: t('Category of target nodes'), - }, - }, - ], - ['adhoc_filters'], - ['row_limit'], - ], - }, - { - label: t('Chart options'), - expanded: true, - controlSetRows: [ - ['color_scheme'], - ...legendSection, - [{t('Layout')}], - [ - { - name: 'layout', - config: { - type: 'RadioButtonControl', - renderTrigger: true, - label: t('Graph layout'), - default: DEFAULT_FORM_DATA.layout, - options: [ - ['force', t('Force')], - ['circular', t('Circular')], - ], - description: t('Layout type of graph'), - }, - }, - ], - [ - { - name: 'edgeSymbol', - config: { - type: 'SelectControl', - renderTrigger: true, - label: t('Edge symbols'), - description: t('Symbol of two ends of edge line'), - default: DEFAULT_FORM_DATA.edgeSymbol, - choices: [ - ['none,none', t('None -> None')], - ['none,arrow', t('None -> Arrow')], - ['circle,arrow', t('Circle -> Arrow')], - ['circle,circle', t('Circle -> Circle')], - ], - }, - }, - ], - [ - { - name: 'draggable', - config: { - type: 'CheckboxControl', - label: t('Enable node dragging'), - renderTrigger: true, - default: DEFAULT_FORM_DATA.draggable, - description: t( - 'Whether to enable node dragging in force layout mode.', - ), - visibility({ form_data: { layout } }) { - return ( - layout === 'force' || - (!layout && DEFAULT_FORM_DATA.layout === 'force') - ); - }, - }, - }, - ], - [ - { - name: 'roam', - config: { - type: 'SelectControl', - label: t('Enable graph roaming'), - renderTrigger: true, - default: DEFAULT_FORM_DATA.roam, - choices: [ - [false, t('Disabled')], - ['scale', t('Scale only')], - ['move', t('Move only')], - [true, t('Scale and Move')], - ], - description: t( - 'Whether to enable changing graph position and scaling.', - ), - }, - }, - ], - [ - { - name: 'selectedMode', - config: { - type: 'SelectControl', - renderTrigger: true, - label: t('Node select mode'), - default: DEFAULT_FORM_DATA.selectedMode, - choices: [ - [false, t('Disabled')], - ['single', t('Single')], - ['multiple', t('Multiple')], - ], - description: t('Allow node selections'), - }, - }, - ], - [ - { - name: 'showSymbolThreshold', - config: { - type: 'TextControl', - label: t('Label threshold'), - renderTrigger: true, - isInt: true, - default: DEFAULT_FORM_DATA.showSymbolThreshold, - description: t( - 'Minimum value for label to be displayed on graph.', - ), - }, - }, - ], - [ - { - name: 'baseNodeSize', - config: { - type: 'TextControl', - label: t('Node size'), - renderTrigger: true, - isFloat: true, - default: DEFAULT_FORM_DATA.baseNodeSize, - description: t( - 'Median node size, the largest node will be 4 times larger than the smallest', - ), - }, - }, - { - name: 'baseEdgeWidth', - config: { - type: 'TextControl', - label: t('Edge width'), - renderTrigger: true, - isFloat: true, - default: DEFAULT_FORM_DATA.baseEdgeWidth, - description: t( - 'Median edge width, the thickest edge will be 4 times thicker than the thinnest.', - ), - }, - }, - ], - [ - { - name: 'edgeLength', - config: { - type: 'SliderControl', - label: t('Edge length'), - renderTrigger: true, - min: 100, - max: 1000, - step: 50, - default: DEFAULT_FORM_DATA.edgeLength, - description: t('Edge length between nodes'), - visibility({ form_data: { layout } }) { - return ( - layout === 'force' || - (!layout && DEFAULT_FORM_DATA.layout === 'force') - ); - }, - }, - }, - ], - [ - { - name: 'gravity', - config: { - type: 'SliderControl', - label: t('Gravity'), - renderTrigger: true, - min: 0.1, - max: 1, - step: 0.1, - default: DEFAULT_FORM_DATA.gravity, - description: t('Strength to pull the graph toward center'), - visibility({ form_data: { layout } }) { - return ( - layout === 'force' || - (!layout && DEFAULT_FORM_DATA.layout === 'force') - ); - }, - }, - }, - ], - [ - { - name: 'repulsion', - config: { - type: 'SliderControl', - label: t('Repulsion'), - renderTrigger: true, - min: 100, - max: 3000, - step: 50, - default: DEFAULT_FORM_DATA.repulsion, - description: t('Repulsion strength between nodes'), - visibility({ form_data: { layout } }) { - return ( - layout === 'force' || - (!layout && DEFAULT_FORM_DATA.layout === 'force') - ); - }, - }, - }, - ], - [ - { - name: 'friction', - config: { - type: 'SliderControl', - label: t('Friction'), - renderTrigger: true, - min: 0.1, - max: 1, - step: 0.1, - default: DEFAULT_FORM_DATA.friction, - description: t('Friction between nodes'), - visibility({ form_data: { layout } }) { - return ( - layout === 'force' || - (!layout && DEFAULT_FORM_DATA.layout === 'force') - ); - }, - }, - }, - ], - ], - }, - ], - formDataOverrides: formData => ({ - ...formData, - metric: getStandardizedControls().popAllMetrics(), - }), -}; - -export default controlPanel; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/index.ts deleted file mode 100644 index 768e4732454..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/index.ts +++ /dev/null @@ -1,65 +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 { t } from '@apache-superset/core/translation'; -import { Behavior } from '@superset-ui/core'; -import controlPanel from './controlPanel'; -import transformProps from './transformProps'; -import thumbnail from './images/thumbnail.png'; -import thumbnailDark from './images/thumbnail-dark.png'; -import example from './images/example.jpg'; -import exampleDark from './images/example-dark.jpg'; -import buildQuery from './buildQuery'; -import { EchartsChartPlugin } from '../types'; - -export default class EchartsGraphChartPlugin extends EchartsChartPlugin { - constructor() { - super({ - buildQuery, - controlPanel, - loadChart: () => import('./EchartsGraph'), - metadata: { - category: t('Flow'), - credits: ['https://echarts.apache.org'], - description: t( - 'Displays connections between entities in a graph structure. Useful for mapping relationships and showing which nodes are important in a network. Graph charts can be configured to be force-directed or circulate. If your data has a geospatial component, try the deck.gl Arc chart.', - ), - exampleGallery: [{ url: example, urlDark: exampleDark }], - name: t('Graph Chart'), - tags: [ - t('Circular'), - t('Comparison'), - t('Directional'), - t('ECharts'), - t('Relational'), - t('Structural'), - t('Transformable'), - t('Featured'), - ], - thumbnail, - thumbnailDark, - behaviors: [ - Behavior.InteractiveChart, - Behavior.DrillToDetail, - Behavior.DrillBy, - ], - }, - transformProps, - }); - } -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/index.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/index.tsx new file mode 100644 index 00000000000..2f43915e28e --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/index.tsx @@ -0,0 +1,948 @@ +/** + * 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. + */ + +/** + * ECharts Graph Chart - Glyph Pattern Implementation + * + * Displays connections between entities in a graph structure. + * Useful for mapping relationships and showing which nodes are important in a network. + * Graph charts can be configured to be force-directed or circular. + */ + +import { t } from '@apache-superset/core/translation'; +import type { EChartsCoreOption } from 'echarts/core'; +import type { GraphSeriesOption } from 'echarts/charts'; +import type { + GraphEdgeItemOption, + GraphNodeItemOption, +} from 'echarts/types/src/chart/graph/GraphSeries'; +import type { SeriesTooltipOption } from 'echarts/types/src/util/types'; +import { extent as d3Extent } from 'd3-array'; +import { + Behavior, + buildQueryContext, + CategoricalColorNamespace, + DataRecord, + DataRecordValue, + getColumnLabel, + getMetricLabel, + getNumberFormatter, + getTimeFormatter, + QueryFormData, + SetDataMaskHook, + ContextMenuFilters, + FilterState, + tooltipHtml, +} from '@superset-ui/core'; +import { + getStandardizedControls, + sharedControls, +} from '@superset-ui/chart-controls'; + +import { + defineChart, + Select, + Checkbox, + Text, + Slider, + RadioButton, + ChartProps, +} from '@superset-ui/glyph-core'; + +import { + getChartPadding, + getColtypesMapping, + getLegendProps, + sanitizeHtml, + formatSeriesName, +} from '../utils/series'; +import { getDefaultTooltip } from '../utils/tooltip'; +import { legendSection } from '../controls'; +import { DEFAULT_LEGEND_FORM_DATA } from '../constants'; +import Echart from '../components/Echart'; +import { EventHandlers, LegendOrientation, LegendType, Refs } from '../types'; + +import thumbnail from './images/thumbnail.png'; +import thumbnailDark from './images/thumbnail-dark.png'; +import example from './images/example.jpg'; +import exampleDark from './images/example-dark.jpg'; + +// ============================================================================ +// Types +// ============================================================================ + +type EdgeSymbol = 'none' | 'circle' | 'arrow'; + +type EChartGraphNode = Omit & { + value: number; + col: string; + tooltip?: Pick; +}; + +type EdgeWithStyles = GraphEdgeItemOption & { + lineStyle: Exclude; + emphasis: Exclude; + select: Exclude; +}; + +const DEFAULT_FORM_DATA = { + ...DEFAULT_LEGEND_FORM_DATA, + source: '', + target: '', + layout: 'force', + roam: true, + draggable: false, + selectedMode: 'single', + showSymbolThreshold: 0, + repulsion: 1000, + gravity: 0.3, + edgeSymbol: 'none,arrow', + edgeLength: 400, + baseEdgeWidth: 3, + baseNodeSize: 20, + friction: 0.2, + legendOrientation: LegendOrientation.Top, + legendType: LegendType.Scroll, +}; + +interface GraphTransformResult { + transformedProps: { + refs: Refs; + width: number; + height: number; + echartOptions: EChartsCoreOption; + formData: Record; + setDataMask: SetDataMaskHook; + filterState?: FilterState; + emitCrossFilters?: boolean; + onContextMenu?: ( + clientX: number, + clientY: number, + filters?: ContextMenuFilters, + ) => void; + coltypeMapping?: Record; + }; +} + +// ============================================================================ +// Constants +// ============================================================================ + +const DEFAULT_GRAPH_SERIES_OPTION: GraphSeriesOption = { + zoom: 0.7, + circular: { rotateLabel: true }, + force: { + initLayout: 'circular', + layoutAnimation: true, + }, + label: { + show: true, + position: 'right', + distance: 5, + rotate: 0, + offset: [0, 0], + fontStyle: 'normal', + fontWeight: 'normal', + fontFamily: 'sans-serif', + fontSize: 12, + padding: [0, 0, 0, 0], + overflow: 'truncate', + formatter: '{b}', + }, + emphasis: { + focus: 'adjacency', + }, + animation: true, + animationDuration: 500, + animationEasing: 'cubicOut', + lineStyle: { color: 'source', curveness: 0.1 }, + select: { + itemStyle: { borderWidth: 3, opacity: 1 }, + label: { fontWeight: 'bolder' }, + }, + tooltip: { formatter: '{b}: {c}' }, +}; + +// ============================================================================ +// Helpers +// ============================================================================ + +function verifyEdgeSymbol(symbol: string): EdgeSymbol { + if (symbol === 'none' || symbol === 'circle' || symbol === 'arrow') { + return symbol; + } + return 'none'; +} + +function parseEdgeSymbol(symbols?: string | null): [EdgeSymbol, EdgeSymbol] { + const [start, end] = (symbols || '').split(','); + return [verifyEdgeSymbol(start), verifyEdgeSymbol(end)]; +} + +function getEmphasizedEdgeWidth(width: number) { + return Math.max(5, Math.min(width * 2, 20)); +} + +function normalizeStyles( + nodes: EChartGraphNode[], + links: EdgeWithStyles[], + { + baseNodeSize, + baseEdgeWidth, + showSymbolThreshold, + }: { + baseNodeSize: number; + baseEdgeWidth: number; + showSymbolThreshold?: number; + }, +) { + const minNodeSize = baseNodeSize * 0.5; + const maxNodeSize = baseNodeSize * 2; + const minEdgeWidth = baseEdgeWidth * 0.5; + const maxEdgeWidth = baseEdgeWidth * 2; + const [nodeMinValue, nodeMaxValue] = d3Extent(nodes, x => x.value) as [ + number, + number, + ]; + + const nodeSpread = nodeMaxValue - nodeMinValue; + nodes.forEach(node => { + node.symbolSize = + (((node.value - nodeMinValue) / nodeSpread) * maxNodeSize || 0) + + minNodeSize; + node.label = { + ...node.label, + show: showSymbolThreshold ? node.value > showSymbolThreshold : true, + }; + }); + + const [linkMinValue, linkMaxValue] = d3Extent(links, x => x.value) as [ + number, + number, + ]; + const linkSpread = linkMaxValue - linkMinValue; + links.forEach(link => { + const lineWidth = + ((link.value! - linkMinValue) / linkSpread) * maxEdgeWidth || + 0 + minEdgeWidth; + link.lineStyle.width = lineWidth; + link.emphasis.lineStyle = { + ...link.emphasis.lineStyle, + width: getEmphasizedEdgeWidth(lineWidth), + }; + link.select.lineStyle = { + ...link.select.lineStyle, + width: getEmphasizedEdgeWidth(lineWidth * 0.8), + opacity: 1, + }; + }); +} + +function getKeyByValue( + object: { [name: string]: number }, + value: number, +): string { + return Object.keys(object).find(key => object[key] === value) as string; +} + +function getCategoryName(columnName: string, name?: DataRecordValue) { + if (name === false) { + return `${columnName}: false`; + } + if (name === true) { + return `${columnName}: true`; + } + if (name == null) { + return 'N/A'; + } + return String(name); +} + +// ============================================================================ +// Build Query +// ============================================================================ + +export function buildQuery(formData: QueryFormData) { + return buildQueryContext(formData, { + queryFields: { + source: 'columns', + target: 'columns', + source_category: 'columns', + target_category: 'columns', + }, + }); +} + +// ============================================================================ +// Chart Definition +// ============================================================================ + +export default defineChart({ + metadata: { + name: t('Graph Chart'), + description: t( + 'Displays connections between entities in a graph structure. Useful for mapping relationships and showing which nodes are important in a network. Graph charts can be configured to be force-directed or circulate. If your data has a geospatial component, try the deck.gl Arc chart.', + ), + category: t('Flow'), + tags: [ + t('Circular'), + t('Comparison'), + t('Directional'), + t('ECharts'), + t('Relational'), + t('Structural'), + t('Transformable'), + t('Featured'), + ], + behaviors: [ + Behavior.InteractiveChart, + Behavior.DrillToDetail, + Behavior.DrillBy, + ], + credits: ['https://echarts.apache.org'], + thumbnail, + thumbnailDark, + exampleGallery: [{ url: example, urlDark: exampleDark }], + }, + + arguments: { + layout: RadioButton.with({ + label: t('Graph layout'), + description: t('Layout type of graph'), + options: [ + { label: t('Force'), value: 'force' }, + { label: t('Circular'), value: 'circular' }, + ], + default: 'force', + }), + + edgeSymbol: Select.with({ + label: t('Edge symbols'), + description: t('Symbol of two ends of edge line'), + options: [ + { label: t('None -> None'), value: 'none,none' }, + { label: t('None -> Arrow'), value: 'none,arrow' }, + { label: t('Circle -> Arrow'), value: 'circle,arrow' }, + { label: t('Circle -> Circle'), value: 'circle,circle' }, + ], + default: 'none,arrow', + }), + + draggable: { + arg: Checkbox.with({ + label: t('Enable node dragging'), + description: t('Whether to enable node dragging in force layout mode.'), + default: false, + }), + visibleWhen: { layout: 'force' }, + }, + + roam: Select.with({ + label: t('Enable graph roaming'), + description: t('Whether to enable changing graph position and scaling.'), + options: [ + { label: t('Disabled'), value: 'false' }, + { label: t('Scale only'), value: 'scale' }, + { label: t('Move only'), value: 'move' }, + { label: t('Scale and Move'), value: 'true' }, + ], + default: 'true', + }), + + selectedMode: Select.with({ + label: t('Node select mode'), + description: t('Allow node selections'), + options: [ + { label: t('Disabled'), value: 'false' }, + { label: t('Single'), value: 'single' }, + { label: t('Multiple'), value: 'multiple' }, + ], + default: 'single', + }), + + showSymbolThreshold: Text.with({ + label: t('Label threshold'), + description: t('Minimum value for label to be displayed on graph.'), + default: '0', + }), + + baseNodeSize: Text.with({ + label: t('Node size'), + description: t( + 'Median node size, the largest node will be 4 times larger than the smallest', + ), + default: '20', + }), + + baseEdgeWidth: Text.with({ + label: t('Edge width'), + description: t( + 'Median edge width, the thickest edge will be 4 times thicker than the thinnest.', + ), + default: '3', + }), + + edgeLength: { + arg: Slider.with({ + label: t('Edge length'), + description: t('Edge length between nodes'), + min: 100, + max: 1000, + step: 50, + default: 400, + }), + visibleWhen: { layout: 'force' }, + }, + + gravity: { + arg: Slider.with({ + label: t('Gravity'), + description: t('Strength to pull the graph toward center'), + min: 0.1, + max: 1, + step: 0.1, + default: 0.3, + }), + visibleWhen: { layout: 'force' }, + }, + + repulsion: { + arg: Slider.with({ + label: t('Repulsion'), + description: t('Repulsion strength between nodes'), + min: 100, + max: 3000, + step: 50, + default: 1000, + }), + visibleWhen: { layout: 'force' }, + }, + + friction: { + arg: Slider.with({ + label: t('Friction'), + description: t('Friction between nodes'), + min: 0.1, + max: 1, + step: 0.1, + default: 0.2, + }), + visibleWhen: { layout: 'force' }, + }, + }, + + additionalControls: { + query: [ + [ + { + name: 'source', + config: { + ...sharedControls.entity, + clearable: false, + label: t('Source'), + description: t('Name of the source nodes'), + }, + }, + ], + [ + { + name: 'target', + config: { + ...sharedControls.entity, + clearable: false, + label: t('Target'), + description: t('Name of the target nodes'), + }, + }, + ], + ['metric'], + [ + { + name: 'source_category', + config: { + ...sharedControls.entity, + clearable: true, + validators: [], + label: t('Source category'), + description: t( + 'The category of source nodes used to assign colors. If a node is associated with more than one category, only the first will be used.', + ), + }, + }, + ], + [ + { + name: 'target_category', + config: { + ...sharedControls.entity, + clearable: true, + validators: [], + label: t('Target category'), + description: t('Category of target nodes'), + }, + }, + ], + ['adhoc_filters'], + ['row_limit'], + ], + chartOptions: [['color_scheme'], ...legendSection], + }, + + formDataOverrides: (formData: QueryFormData) => ({ + ...formData, + metric: getStandardizedControls().popAllMetrics(), + }), + + buildQuery, + + transform: (chartProps: ChartProps): GraphTransformResult => { + const { + width, + height, + rawFormData, + hooks, + filterState, + queriesData, + theme, + inContextMenu, + emitCrossFilters, + } = chartProps; + + const data: DataRecord[] = queriesData[0].data || []; + const coltypeMapping = getColtypesMapping( + queriesData[0] as unknown as Parameters[0], + ); + const { setDataMask = () => {}, onContextMenu } = hooks ?? {}; + const refs: Refs = {}; + + // Extract form values with defaults + const source = (rawFormData.source as string) || DEFAULT_FORM_DATA.source; + const target = (rawFormData.target as string) || DEFAULT_FORM_DATA.target; + const sourceCategory = rawFormData.source_category as string | undefined; + const targetCategory = rawFormData.target_category as string | undefined; + const colorScheme = rawFormData.color_scheme as string; + const metric = (rawFormData.metric as string) || ''; + const layout = + (rawFormData.layout as 'force' | 'circular') || DEFAULT_FORM_DATA.layout; + + // Handle roam - can be boolean or string + let { roam } = DEFAULT_FORM_DATA; + const roamValue = rawFormData.roam; + if (roamValue === 'true' || roamValue === true) roam = true; + else if (roamValue === 'false' || roamValue === false) roam = false; + else if (roamValue === 'scale' || roamValue === 'move') roam = roamValue; + + const draggable = + rawFormData.draggable !== undefined + ? (rawFormData.draggable as boolean) + : DEFAULT_FORM_DATA.draggable; + + // Handle selectedMode - can be boolean or string + let selectedMode: boolean | 'single' | 'multiple' = + DEFAULT_FORM_DATA.selectedMode as 'single'; + const selectedModeValue = rawFormData.selected_mode; + if (selectedModeValue === 'false' || selectedModeValue === false) + selectedMode = false; + else if (selectedModeValue === 'single' || selectedModeValue === 'multiple') + selectedMode = selectedModeValue; + + const showSymbolThreshold = parseInt( + (rawFormData.show_symbol_threshold as string) || + String(DEFAULT_FORM_DATA.showSymbolThreshold), + 10, + ); + const edgeLength = + (rawFormData.edge_length as number) || DEFAULT_FORM_DATA.edgeLength; + const gravity = + (rawFormData.gravity as number) || DEFAULT_FORM_DATA.gravity; + const repulsion = + (rawFormData.repulsion as number) || DEFAULT_FORM_DATA.repulsion; + const friction = + (rawFormData.friction as number) || DEFAULT_FORM_DATA.friction; + const legendMargin = rawFormData.legend_margin as number | undefined; + const legendOrientation = + (rawFormData.legend_orientation as LegendOrientation) || + DEFAULT_FORM_DATA.legendOrientation; + const legendType = + (rawFormData.legend_type as LegendType) || DEFAULT_FORM_DATA.legendType; + const legendSort = rawFormData.legend_sort as string | undefined; + const showLegend = + rawFormData.show_legend !== undefined + ? (rawFormData.show_legend as boolean) + : DEFAULT_FORM_DATA.showLegend; + const baseEdgeWidth = parseFloat( + (rawFormData.base_edge_width as string) || + String(DEFAULT_FORM_DATA.baseEdgeWidth), + ); + const baseNodeSize = parseFloat( + (rawFormData.base_node_size as string) || + String(DEFAULT_FORM_DATA.baseNodeSize), + ); + const edgeSymbol = + (rawFormData.edge_symbol as string) || DEFAULT_FORM_DATA.edgeSymbol; + const sliceId = rawFormData.slice_id as number | undefined; + + const metricLabel = getMetricLabel(metric); + const colorFn = CategoricalColorNamespace.getScale(colorScheme as string); + const firstColor = colorFn.range()[0]; + const nodes: { [name: string]: number } = {}; + const categories: Set = new Set(); + const echartNodes: EChartGraphNode[] = []; + const echartLinks: EdgeWithStyles[] = []; + + function getOrCreateNode( + name: string, + col: string, + category?: string, + color?: string, + ) { + if (!(name in nodes)) { + nodes[name] = echartNodes.length; + echartNodes.push({ + id: String(nodes[name]), + name, + col, + value: 0, + category, + select: DEFAULT_GRAPH_SERIES_OPTION.select, + tooltip: { + ...getDefaultTooltip(refs), + ...DEFAULT_GRAPH_SERIES_OPTION.tooltip, + }, + itemStyle: { color }, + }); + } + const node = echartNodes[nodes[name]]; + if (category) { + categories.add(category); + if (!node.category) { + node.category = category; + } + } + return node; + } + + data.forEach(link => { + const value = link[metricLabel] as number; + if (!value) { + return; + } + const sourceName = link[source] as string; + const targetName = link[target] as string; + const sourceCategoryName = sourceCategory + ? getCategoryName(sourceCategory, link[sourceCategory]) + : undefined; + const targetCategoryName = targetCategory + ? getCategoryName(targetCategory, link[targetCategory]) + : undefined; + const sourceNodeColor = sourceCategoryName + ? colorFn(sourceCategoryName) + : firstColor; + const targetNodeColor = targetCategoryName + ? colorFn(targetCategoryName) + : firstColor; + + const sourceNode = getOrCreateNode( + sourceName, + source, + sourceCategoryName, + sourceNodeColor, + ); + const targetNode = getOrCreateNode( + targetName, + target, + targetCategoryName, + targetNodeColor, + ); + + sourceNode.value += value; + targetNode.value += value; + + echartLinks.push({ + source: sourceNode.id, + target: targetNode.id, + value, + lineStyle: { + color: sourceNodeColor, + }, + emphasis: {}, + select: {}, + }); + }); + + normalizeStyles(echartNodes, echartLinks, { + showSymbolThreshold, + baseEdgeWidth, + baseNodeSize, + }); + + const categoryList = [...categories]; + const series: GraphSeriesOption[] = [ + { + zoom: DEFAULT_GRAPH_SERIES_OPTION.zoom, + type: 'graph', + categories: categoryList.map(c => ({ + name: c, + itemStyle: { + color: colorFn(c, sliceId), + }, + })), + layout, + force: { + ...DEFAULT_GRAPH_SERIES_OPTION.force, + edgeLength, + gravity, + repulsion, + friction, + }, + circular: DEFAULT_GRAPH_SERIES_OPTION.circular, + data: echartNodes, + links: echartLinks, + roam, + draggable, + edgeSymbol: parseEdgeSymbol(edgeSymbol), + edgeSymbolSize: baseEdgeWidth * 2, + selectedMode, + ...getChartPadding(showLegend, legendOrientation, legendMargin), + animation: DEFAULT_GRAPH_SERIES_OPTION.animation, + label: { + ...DEFAULT_GRAPH_SERIES_OPTION.label, + color: theme.colorText, + }, + lineStyle: DEFAULT_GRAPH_SERIES_OPTION.lineStyle, + emphasis: DEFAULT_GRAPH_SERIES_OPTION.emphasis, + }, + ]; + + const echartOptions: EChartsCoreOption = { + animationDuration: DEFAULT_GRAPH_SERIES_OPTION.animationDuration, + animationEasing: DEFAULT_GRAPH_SERIES_OPTION.animationEasing, + tooltip: { + ...getDefaultTooltip(refs), + show: !inContextMenu, + formatter: (params: { + data: { source: string; target: string }; + value: number; + }): string => { + const sourceLabel = sanitizeHtml( + getKeyByValue(nodes, Number(params.data.source)), + ); + const targetLabel = sanitizeHtml( + getKeyByValue(nodes, Number(params.data.target)), + ); + const title = `${sourceLabel} > ${targetLabel}`; + return tooltipHtml([[metricLabel, `${params.value}`]], title); + }, + }, + legend: { + ...getLegendProps(legendType, legendOrientation, showLegend, theme), + data: categoryList.sort((a: string, b: string) => { + if (!legendSort) return 0; + return legendSort === 'asc' ? a.localeCompare(b) : b.localeCompare(a); + }), + }, + series, + }; + + return { + transformedProps: { + refs, + width, + height, + echartOptions, + formData: rawFormData, + setDataMask, + filterState, + emitCrossFilters, + onContextMenu, + coltypeMapping, + }, + }; + }, + + render: ({ transformedProps }) => { + const { + height, + width, + echartOptions, + formData, + onContextMenu, + setDataMask, + filterState, + emitCrossFilters, + refs, + coltypeMapping, + } = transformedProps; + + type DataRow = { + source?: string; + target?: string; + id?: string; + col: string; + name: string; + }; + type Data = DataRow[]; + type Event = { + name: string; + event: { stop: () => void; event: PointerEvent }; + data: DataRow; + dataType: 'node' | 'edge'; + }; + + const getCrossFilterDataMask = (node: DataRow | undefined) => { + if (!node?.name || !node?.col) { + return undefined; + } + const { name, col } = node; + const selected = Object.values( + filterState?.selectedValues || {}, + ) as string[]; + let values: string[]; + if (selected.includes(name)) { + values = selected.filter(v => v !== name); + } else { + values = [name]; + } + return { + dataMask: { + extraFormData: { + filters: values.length + ? [ + { + col, + op: 'IN' as const, + val: values, + }, + ] + : [], + }, + filterState: { + value: values.length ? values : null, + selectedValues: values.length ? values : null, + }, + }, + isCurrentValueSelected: selected.includes(name), + }; + }; + + const eventHandlers: EventHandlers = { + click: (e: Event) => { + if (!emitCrossFilters || !setDataMask) { + return; + } + e.event.stop(); + const { data } = (echartOptions as { series: { data: Data }[] }) + .series[0]; + const node = data.find(item => item.id === e.data.id); + const dataMask = getCrossFilterDataMask(node)?.dataMask; + if (dataMask) { + setDataMask(dataMask); + } + }, + contextmenu: (e: Event) => { + const handleNodeClick = (data: Data) => { + const node = data.find(item => item.id === e.data.id); + if (node?.name) { + return [ + { + col: node.col, + op: '==' as const, + val: node.name, + formattedVal: node.name, + }, + ]; + } + return undefined; + }; + const handleEdgeClick = (data: Data) => { + const sourceValue = data.find( + item => item.id === e.data.source, + )?.name; + const targetValue = data.find( + item => item.id === e.data.target, + )?.name; + if (sourceValue && targetValue) { + return [ + { + col: formData.source as string, + op: '==' as const, + val: sourceValue, + formattedVal: sourceValue, + }, + { + col: formData.target as string, + op: '==' as const, + val: targetValue, + formattedVal: targetValue, + }, + ]; + } + return undefined; + }; + if (onContextMenu) { + e.event.stop(); + const pointerEvent = e.event.event; + const { data } = (echartOptions as { series: { data: Data }[] }) + .series[0]; + const drillToDetailFilters = + e.dataType === 'node' + ? handleNodeClick(data) + : handleEdgeClick(data); + const node = data.find(item => item.id === e.data.id); + + onContextMenu(pointerEvent.clientX, pointerEvent.clientY, { + drillToDetail: drillToDetailFilters, + crossFilter: getCrossFilterDataMask(node), + drillBy: node && { + filters: [ + { + col: node.col, + op: '==', + val: node.name, + formattedVal: formatSeriesName(node.name, { + timeFormatter: getTimeFormatter( + formData.date_format as string, + ), + numberFormatter: getNumberFormatter( + formData.number_format as string, + ), + coltype: coltypeMapping?.[getColumnLabel(node.col)], + }), + }, + ], + groupbyFieldName: + node.col === formData.source ? 'source' : 'target', + }, + }); + } + }, + }; + + return ( + + ); + }, +}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/stories/Graph.stories.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/stories/Graph.stories.tsx deleted file mode 100644 index a33d3d1c3f0..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/stories/Graph.stories.tsx +++ /dev/null @@ -1,138 +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 { SuperChart, getChartTransformPropsRegistry } from '@superset-ui/core'; -import { - EchartsGraphChartPlugin, - GraphTransformProps, -} from '@superset-ui/plugin-chart-echarts'; -import { basic } from './data'; -import { withResizableChartDemo } from '@storybook-shared'; - -new EchartsGraphChartPlugin().configure({ key: 'echarts-graph' }).register(); - -getChartTransformPropsRegistry().registerValue( - 'echarts-graph', - GraphTransformProps, -); - -export default { - title: 'Chart Plugins/plugin-chart-echarts/Graph', - decorators: [withResizableChartDemo], - args: { - colorScheme: 'supersetColors', - layout: 'force', - showLegend: true, - roam: true, - draggable: true, - repulsion: 1000, - gravity: 0.3, - edgeLength: 400, - showSymbolThreshold: 0, - }, - argTypes: { - colorScheme: { - control: 'select', - options: [ - 'supersetColors', - 'd3Category10', - 'bnbColors', - 'googleCategory20c', - ], - }, - layout: { - control: 'select', - options: ['force', 'circular'], - }, - showLegend: { control: 'boolean' }, - roam: { - control: 'boolean', - description: 'Enable zooming and panning', - }, - draggable: { - control: 'boolean', - description: 'Enable dragging nodes', - }, - repulsion: { - control: { type: 'range', min: 100, max: 5000, step: 100 }, - description: 'Force repulsion between nodes', - }, - gravity: { - control: { type: 'range', min: 0, max: 1, step: 0.1 }, - description: 'Gravity towards center', - }, - edgeLength: { - control: { type: 'range', min: 50, max: 1000, step: 50 }, - description: 'Expected edge length', - }, - showSymbolThreshold: { - control: { type: 'range', min: 0, max: 100, step: 10 }, - description: 'Hide labels below this threshold', - }, - }, -}; - -export const Graph = ({ - width, - height, - colorScheme, - layout, - showLegend, - roam, - draggable, - repulsion, - gravity, - edgeLength, - showSymbolThreshold, -}: { - width: number; - height: number; - colorScheme: string; - layout: string; - showLegend: boolean; - roam: boolean; - draggable: boolean; - repulsion: number; - gravity: number; - edgeLength: number; - showSymbolThreshold: number; -}) => ( - -); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/transformProps.ts deleted file mode 100644 index 9fee03ae3d3..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/transformProps.ts +++ /dev/null @@ -1,396 +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 { - CategoricalColorNamespace, - getMetricLabel, - DataRecord, - DataRecordValue, - tooltipHtml, -} from '@superset-ui/core'; -import type { EChartsCoreOption } from 'echarts/core'; -import type { GraphSeriesOption } from 'echarts/charts'; -import type { GraphEdgeItemOption } from 'echarts/types/src/chart/graph/GraphSeries'; -import { extent as d3Extent } from 'd3-array'; -import { - EchartsGraphFormData, - EChartGraphNode, - DEFAULT_FORM_DATA as DEFAULT_GRAPH_FORM_DATA, - EdgeSymbol, - GraphChartTransformedProps, - EchartsGraphChartProps, -} from './types'; -import { DEFAULT_GRAPH_SERIES_OPTION } from './constants'; -import { - getChartPadding, - getColtypesMapping, - getLegendProps, - sanitizeHtml, -} from '../utils/series'; -import { resolveLegendLayout } from '../utils/legendLayout'; -import { getDefaultTooltip } from '../utils/tooltip'; -import { Refs } from '../types'; - -type EdgeWithStyles = GraphEdgeItemOption & { - lineStyle: Exclude; - emphasis: Exclude; - select: Exclude; -}; - -function verifyEdgeSymbol(symbol: string): EdgeSymbol { - if (symbol === 'none' || symbol === 'circle' || symbol === 'arrow') { - return symbol; - } - return 'none'; -} - -function parseEdgeSymbol(symbols?: string | null): [EdgeSymbol, EdgeSymbol] { - const [start, end] = (symbols || '').split(','); - return [verifyEdgeSymbol(start), verifyEdgeSymbol(end)]; -} - -/** - * Emphasized edge width with a min and max. - */ -function getEmphasizedEdgeWidth(width: number) { - return Math.max(5, Math.min(width * 2, 20)); -} - -/** - * Normalize node size, edge width, and apply label visibility thresholds. - */ -function normalizeStyles( - nodes: EChartGraphNode[], - links: EdgeWithStyles[], - { - baseNodeSize, - baseEdgeWidth, - showSymbolThreshold, - }: { - baseNodeSize: number; - baseEdgeWidth: number; - showSymbolThreshold?: number; - }, -) { - const minNodeSize = baseNodeSize * 0.5; - const maxNodeSize = baseNodeSize * 2; - const minEdgeWidth = baseEdgeWidth * 0.5; - const maxEdgeWidth = baseEdgeWidth * 2; - const [nodeMinValue, nodeMaxValue] = d3Extent(nodes, x => x.value) as [ - number, - number, - ]; - - const nodeSpread = nodeMaxValue - nodeMinValue; - nodes.forEach(node => { - // eslint-disable-next-line no-param-reassign - node.symbolSize = - (((node.value - nodeMinValue) / nodeSpread) * maxNodeSize || 0) + - minNodeSize; - // eslint-disable-next-line no-param-reassign - node.label = { - ...node.label, - show: showSymbolThreshold ? node.value > showSymbolThreshold : true, - }; - }); - - const [linkMinValue, linkMaxValue] = d3Extent(links, x => x.value) as [ - number, - number, - ]; - const linkSpread = linkMaxValue - linkMinValue; - links.forEach(link => { - const lineWidth = - ((link.value! - linkMinValue) / linkSpread) * maxEdgeWidth || - 0 + minEdgeWidth; - // eslint-disable-next-line no-param-reassign - link.lineStyle.width = lineWidth; - // eslint-disable-next-line no-param-reassign - link.emphasis.lineStyle = { - ...link.emphasis.lineStyle, - width: getEmphasizedEdgeWidth(lineWidth), - }; - // eslint-disable-next-line no-param-reassign - link.select.lineStyle = { - ...link.select.lineStyle, - width: getEmphasizedEdgeWidth(lineWidth * 0.8), - opacity: 1, - }; - }); -} - -function getKeyByValue( - object: { [name: string]: number }, - value: number, -): string { - return Object.keys(object).find(key => object[key] === value) as string; -} - -function getCategoryName(columnName: string, name?: DataRecordValue) { - if (name === false) { - return `${columnName}: false`; - } - if (name === true) { - return `${columnName}: true`; - } - if (name == null) { - return 'N/A'; - } - return String(name); -} - -export default function transformProps( - chartProps: EchartsGraphChartProps, -): GraphChartTransformedProps { - const { - width, - height, - formData, - queriesData, - hooks, - inContextMenu, - filterState, - emitCrossFilters, - theme, - } = chartProps; - const data: DataRecord[] = queriesData[0].data || []; - const coltypeMapping = getColtypesMapping(queriesData[0]); - const { - source, - target, - sourceCategory, - targetCategory, - colorScheme, - metric = '', - layout, - roam, - draggable, - selectedMode, - showSymbolThreshold, - edgeLength, - gravity, - repulsion, - friction, - legendMargin, - legendOrientation, - legendType, - legendSort, - showLegend, - baseEdgeWidth, - baseNodeSize, - edgeSymbol, - sliceId, - }: EchartsGraphFormData = { ...DEFAULT_GRAPH_FORM_DATA, ...formData }; - - const refs: Refs = {}; - const metricLabel = getMetricLabel(metric); - const colorFn = CategoricalColorNamespace.getScale(colorScheme as string); - const firstColor = colorFn.range()[0]; - const nodes: { [name: string]: number } = {}; - const categories: Set = new Set(); - const echartNodes: EChartGraphNode[] = []; - const echartLinks: EdgeWithStyles[] = []; - - /** - * Get the node id of an existing node, - * or create a new node if it doesn't exist. - */ - function getOrCreateNode( - name: string, - col: string, - category?: string, - color?: string, - ) { - if (!(name in nodes)) { - nodes[name] = echartNodes.length; - echartNodes.push({ - id: String(nodes[name]), - name, - col, - value: 0, - category, - select: DEFAULT_GRAPH_SERIES_OPTION.select, - tooltip: { - ...getDefaultTooltip(refs), - ...DEFAULT_GRAPH_SERIES_OPTION.tooltip, - }, - itemStyle: { color }, - }); - } - const node = echartNodes[nodes[name]]; - if (category) { - categories.add(category); - // category may be empty when one of `sourceCategory` - // or `targetCategory` is not set. - if (!node.category) { - node.category = category; - } - } - return node; - } - - data.forEach(link => { - const value = link[metricLabel] as number; - if (!value) { - return; - } - const sourceName = link[source] as string; - const targetName = link[target] as string; - const sourceCategoryName = sourceCategory - ? getCategoryName(sourceCategory, link[sourceCategory]) - : undefined; - const targetCategoryName = targetCategory - ? getCategoryName(targetCategory, link[targetCategory]) - : undefined; - const sourceNodeColor = sourceCategoryName - ? colorFn(sourceCategoryName) - : firstColor; - const targetNodeColor = targetCategoryName - ? colorFn(targetCategoryName) - : firstColor; - - const sourceNode = getOrCreateNode( - sourceName, - source, - sourceCategoryName, - sourceNodeColor, - ); - const targetNode = getOrCreateNode( - targetName, - target, - targetCategoryName, - targetNodeColor, - ); - - sourceNode.value += value; - targetNode.value += value; - - echartLinks.push({ - source: sourceNode.id, - target: targetNode.id, - value, - lineStyle: { - color: sourceNodeColor, - }, - emphasis: {}, - select: {}, - }); - }); - - normalizeStyles(echartNodes, echartLinks, { - showSymbolThreshold, - baseEdgeWidth, - baseNodeSize, - }); - - const categoryList = [...categories]; - const legendData = categoryList.sort((a: string, b: string) => { - if (!legendSort) return 0; - return legendSort === 'asc' ? a.localeCompare(b) : b.localeCompare(a); - }); - const { effectiveLegendMargin, effectiveLegendType } = resolveLegendLayout({ - chartHeight: height, - chartWidth: width, - legendItems: legendData, - legendMargin, - orientation: legendOrientation, - show: showLegend, - theme, - type: legendType, - }); - const series: GraphSeriesOption[] = [ - { - zoom: DEFAULT_GRAPH_SERIES_OPTION.zoom, - type: 'graph', - categories: categoryList.map(c => ({ - name: c, - itemStyle: { - color: colorFn(c, sliceId), - }, - })), - layout, - force: { - ...DEFAULT_GRAPH_SERIES_OPTION.force, - edgeLength, - gravity, - repulsion, - friction, - }, - circular: DEFAULT_GRAPH_SERIES_OPTION.circular, - data: echartNodes, - links: echartLinks, - roam, - draggable, - edgeSymbol: parseEdgeSymbol(edgeSymbol), - edgeSymbolSize: baseEdgeWidth * 2, - selectedMode, - ...getChartPadding(showLegend, legendOrientation, effectiveLegendMargin), - animation: DEFAULT_GRAPH_SERIES_OPTION.animation, - label: { - ...DEFAULT_GRAPH_SERIES_OPTION.label, - color: theme.colorText, - }, - lineStyle: DEFAULT_GRAPH_SERIES_OPTION.lineStyle, - emphasis: DEFAULT_GRAPH_SERIES_OPTION.emphasis, - }, - ]; - - const echartOptions: EChartsCoreOption = { - animationDuration: DEFAULT_GRAPH_SERIES_OPTION.animationDuration, - animationEasing: DEFAULT_GRAPH_SERIES_OPTION.animationEasing, - tooltip: { - ...getDefaultTooltip(refs), - show: !inContextMenu, - formatter: (params: any): string => { - const source = sanitizeHtml( - getKeyByValue(nodes, Number(params.data.source)), - ); - const target = sanitizeHtml( - getKeyByValue(nodes, Number(params.data.target)), - ); - const title = `${source} > ${target}`; - return tooltipHtml([[metricLabel, `${params.value}`]], title); - }, - }, - legend: { - ...getLegendProps( - effectiveLegendType, - legendOrientation, - showLegend, - theme, - ), - data: legendData, - }, - series, - }; - - const { onContextMenu, setDataMask } = hooks; - - return { - width, - height, - formData, - echartOptions, - onContextMenu, - setDataMask, - filterState, - refs, - emitCrossFilters, - coltypeMapping, - }; -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/types.ts deleted file mode 100644 index d86e7bf6a20..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/types.ts +++ /dev/null @@ -1,92 +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 { QueryFormData } from '@superset-ui/core'; -import type { GraphNodeItemOption } from 'echarts/types/src/chart/graph/GraphSeries'; -import type { SeriesTooltipOption } from 'echarts/types/src/util/types'; -import { - BaseChartProps, - BaseTransformedProps, - ContextMenuTransformedProps, - LegendFormData, - LegendOrientation, - LegendType, -} from '../types'; -import { DEFAULT_LEGEND_FORM_DATA } from '../constants'; - -export type EdgeSymbol = 'none' | 'circle' | 'arrow'; - -export type EchartsGraphFormData = QueryFormData & - LegendFormData & { - source: string; - target: string; - sourceCategory?: string; - targetCategory?: string; - colorScheme?: string; - metric?: string; - layout?: 'none' | 'circular' | 'force'; - roam: boolean | 'scale' | 'move'; - draggable: boolean; - selectedMode?: boolean | 'multiple' | 'single'; - showSymbolThreshold: number; - repulsion: number; - gravity: number; - baseNodeSize: number; - baseEdgeWidth: number; - edgeLength: number; - edgeSymbol: string; - friction: number; - }; - -export type EChartGraphNode = Omit & { - value: number; - col: string; - tooltip?: Pick; -}; - -// @ts-expect-error -export const DEFAULT_FORM_DATA: EchartsGraphFormData = { - ...DEFAULT_LEGEND_FORM_DATA, - source: '', - target: '', - layout: 'force', - roam: true, - draggable: false, - selectedMode: 'single', - showSymbolThreshold: 0, - repulsion: 1000, - gravity: 0.3, - edgeSymbol: 'none,arrow', - edgeLength: 400, - baseEdgeWidth: 3, - baseNodeSize: 20, - friction: 0.2, - legendOrientation: LegendOrientation.Top, - legendType: LegendType.Scroll, -}; - -export type tooltipFormatParams = { - data: { [name: string]: string }; -}; - -export interface EchartsGraphChartProps extends BaseChartProps { - formData: EchartsGraphFormData; -} - -export type GraphChartTransformedProps = - BaseTransformedProps & ContextMenuTransformedProps; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Heatmap/Heatmap.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Heatmap/Heatmap.tsx deleted file mode 100644 index 96bd0ed4139..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Heatmap/Heatmap.tsx +++ /dev/null @@ -1,32 +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 { HeatmapTransformedProps } from './types'; -import Echart from '../components/Echart'; - -export default function Heatmap(props: HeatmapTransformedProps) { - const { height, width, echartOptions, refs } = props; - return ( - - ); -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Heatmap/buildQuery.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Heatmap/buildQuery.ts deleted file mode 100644 index 858a8ddb3e6..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Heatmap/buildQuery.ts +++ /dev/null @@ -1,71 +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 { - QueryFormColumn, - QueryFormData, - QueryFormOrderBy, - buildQueryContext, - ensureIsArray, - getColumnLabel, - getMetricLabel, - getXAxisColumn, -} from '@superset-ui/core'; -import { rankOperator } from '@superset-ui/chart-controls'; - -export default function buildQuery(formData: QueryFormData) { - const { groupby, normalize_across, sort_x_axis, sort_y_axis, x_axis } = - formData; - const metric = getMetricLabel(formData.metric); - const columns = [ - ...ensureIsArray(getXAxisColumn(formData)), - ...ensureIsArray(groupby), - ]; - const orderby: QueryFormOrderBy[] = []; - if (sort_x_axis) { - orderby.push([ - sort_x_axis.includes('value') ? metric : columns[0], - sort_x_axis.includes('asc'), - ]); - } - if (sort_y_axis) { - orderby.push([ - sort_y_axis.includes('value') ? metric : columns[1], - sort_y_axis.includes('asc'), - ]); - } - const group_by = - normalize_across === 'x' - ? getColumnLabel(x_axis) - : normalize_across === 'y' - ? getColumnLabel(groupby as unknown as QueryFormColumn) - : undefined; - return buildQueryContext(formData, baseQueryObject => [ - { - ...baseQueryObject, - columns, - orderby, - post_processing: [ - rankOperator(formData, baseQueryObject, { - metric, - group_by, - }), - ], - }, - ]); -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Heatmap/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Heatmap/controlPanel.tsx deleted file mode 100644 index 5ac5440f08c..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Heatmap/controlPanel.tsx +++ /dev/null @@ -1,329 +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 { t } from '@apache-superset/core/translation'; -import { validateNonEmpty } from '@superset-ui/core'; -import { - ControlPanelConfig, - formatSelectOptionsForRange, - getStandardizedControls, -} from '@superset-ui/chart-controls'; -import { xAxisLabelRotation } from '../controls'; - -const sortAxisChoices = [ - ['alpha_asc', t('Axis ascending')], - ['alpha_desc', t('Axis descending')], - ['value_asc', t('Metric ascending')], - ['value_desc', t('Metric descending')], -]; - -const config: ControlPanelConfig = { - controlPanelSections: [ - { - label: t('Query'), - expanded: true, - controlSetRows: [ - ['x_axis'], - ['time_grain_sqla'], - ['groupby'], - ['metric'], - ['adhoc_filters'], - ['row_limit'], - [ - { - name: 'sort_x_axis', - config: { - type: 'SelectControl', - label: t('Sort X Axis'), - choices: sortAxisChoices, - renderTrigger: false, - clearable: true, - }, - }, - ], - [ - { - name: 'sort_y_axis', - config: { - type: 'SelectControl', - label: t('Sort Y Axis'), - choices: sortAxisChoices, - renderTrigger: false, - clearable: true, - }, - }, - ], - [ - { - name: 'normalize_across', - config: { - type: 'SelectControl', - label: t('Normalize Across'), - choices: [ - ['heatmap', t('heatmap')], - ['x', t('x')], - ['y', t('y')], - ], - default: 'heatmap', - renderTrigger: false, - description: ( - <> -
- {t( - 'Color will be shaded based the normalized (0% to 100%) value of a given cell against the other cells in the selected range: ', - )} -
-
    -
  • {t('x: values are normalized within each column')}
  • -
  • {t('y: values are normalized within each row')}
  • -
  • - {t( - 'heatmap: values are normalized across the entire heatmap', - )} -
  • -
- - ), - }, - }, - ], - ], - }, - { - label: t('Chart Options'), - expanded: true, - controlSetRows: [ - [ - { - name: 'legend_type', - config: { - type: 'SelectControl', - label: t('Legend Type'), - renderTrigger: true, - choices: [ - ['continuous', t('Continuous')], - ['piecewise', t('Piecewise')], - ], - default: 'continuous', - clearable: false, - }, - }, - ], - ['linear_color_scheme'], - [ - { - name: 'border_color', - config: { - type: 'ColorPickerControl', - label: t('Border color'), - renderTrigger: true, - description: t('The color of the elements border'), - default: { r: 0, g: 0, b: 0, a: 1 }, - }, - }, - { - name: 'border_width', - config: { - type: 'SliderControl', - label: t('Border width'), - renderTrigger: true, - min: 0, - max: 2, - default: 0, - step: 0.1, - description: t('The width of the elements border'), - }, - }, - ], - [ - { - name: 'xscale_interval', - config: { - type: 'SelectControl', - label: t('X-scale interval'), - renderTrigger: true, - choices: [[-1, t('Auto')]].concat( - formatSelectOptionsForRange(1, 50), - ), - default: -1, - clearable: false, - description: t( - 'Number of steps to take between ticks when displaying the X scale', - ), - }, - }, - ], - [ - { - name: 'yscale_interval', - config: { - type: 'SelectControl', - label: t('Y-scale interval'), - choices: [[-1, t('Auto')]].concat( - formatSelectOptionsForRange(1, 50), - ), - default: -1, - clearable: false, - renderTrigger: true, - description: t( - 'Number of steps to take between ticks when displaying the Y scale', - ), - }, - }, - ], - [ - { - name: 'left_margin', - config: { - type: 'SelectControl', - freeForm: true, - clearable: false, - label: t('Left Margin'), - choices: [ - ['auto', t('Auto')], - [50, '50'], - [75, '75'], - [100, '100'], - [125, '125'], - [150, '150'], - [200, '200'], - ], - default: 'auto', - renderTrigger: true, - description: t( - 'Left margin, in pixels, allowing for more room for axis labels', - ), - }, - }, - ], - [ - { - name: 'bottom_margin', - config: { - type: 'SelectControl', - clearable: false, - freeForm: true, - label: t('Bottom Margin'), - choices: [ - ['auto', t('Auto')], - [50, '50'], - [75, '75'], - [100, '100'], - [125, '125'], - [150, '150'], - [200, '200'], - ], - default: 'auto', - renderTrigger: true, - description: t( - 'Bottom margin, in pixels, allowing for more room for axis labels', - ), - }, - }, - ], - [ - { - name: 'value_bounds', - config: { - type: 'BoundsControl', - label: t('Value bounds'), - renderTrigger: true, - default: [null, null], - description: t('Hard value bounds applied for color coding.'), - }, - }, - ], - ['y_axis_format'], - ['x_axis_time_format'], - [xAxisLabelRotation], - ['currency_format'], - [ - { - name: 'show_legend', - config: { - type: 'CheckboxControl', - label: t('Legend'), - renderTrigger: true, - default: true, - description: t('Whether to display the legend (toggles)'), - }, - }, - ], - [ - { - name: 'show_percentage', - config: { - type: 'CheckboxControl', - label: t('Show percentage'), - renderTrigger: true, - description: t( - 'Whether to include the percentage in the tooltip', - ), - default: true, - }, - }, - ], - [ - { - name: 'show_values', - config: { - type: 'CheckboxControl', - label: t('Show Values'), - renderTrigger: true, - default: false, - description: t( - 'Whether to display the numerical values within the cells', - ), - }, - }, - ], - [ - { - name: 'normalized', - config: { - type: 'CheckboxControl', - label: t('Normalized'), - renderTrigger: true, - description: t( - 'Whether to apply a normal distribution based on rank on the color scale', - ), - default: false, - }, - }, - ], - ], - }, - ], - controlOverrides: { - groupby: { - label: t('Y-Axis'), - description: t('Dimension to use on y-axis.'), - multi: false, - validators: [validateNonEmpty], - }, - y_axis_format: { - label: t('Value Format'), - }, - }, - formDataOverrides: formData => ({ - ...formData, - metric: getStandardizedControls().shiftMetric(), - }), -}; - -export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Heatmap/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Heatmap/index.ts deleted file mode 100644 index 56965454a11..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Heatmap/index.ts +++ /dev/null @@ -1,66 +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 { t } from '@apache-superset/core/translation'; -import { ChartMetadata, ChartPlugin } from '@superset-ui/core'; -import transformProps from './transformProps'; -import buildQuery from './buildQuery'; -import example1 from './images/example1.png'; -import example1Dark from './images/example1-dark.png'; -import example2 from './images/example2.png'; -import example2Dark from './images/example2-dark.png'; -import example3 from './images/example3.png'; -import example3Dark from './images/example3-dark.png'; -import thumbnail from './images/thumbnail.png'; -import thumbnailDark from './images/thumbnail-dark.png'; -import controlPanel from './controlPanel'; - -const metadata = new ChartMetadata({ - category: t('Correlation'), - description: t( - 'Visualize a related metric across pairs of groups. Heatmaps excel at showcasing the correlation or strength between two groups. Color is used to emphasize the strength of the link between each pair of groups.', - ), - exampleGallery: [ - { url: example1, urlDark: example1Dark }, - { url: example2, urlDark: example2Dark }, - { url: example3, urlDark: example3Dark }, - ], - name: t('Heatmap'), - tags: [ - t('Business'), - t('Intensity'), - t('Density'), - t('Single Metric'), - t('ECharts'), - t('Featured'), - ], - thumbnail, - thumbnailDark, -}); - -export default class EchartsHeatmapChartPlugin extends ChartPlugin { - constructor() { - super({ - buildQuery, - loadChart: () => import('./Heatmap'), - metadata, - transformProps, - controlPanel, - }); - } -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Heatmap/index.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Heatmap/index.tsx new file mode 100644 index 00000000000..f3dede0869a --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Heatmap/index.tsx @@ -0,0 +1,792 @@ +/** + * 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 { logging } from '@apache-superset/core/utils'; +import { t } from '@apache-superset/core/translation'; +import { GenericDataType } from '@apache-superset/core/common'; +import { + buildQueryContext, + Currency, + DataRecordValue, + ensureIsArray, + getColumnLabel, + getMetricLabel, + getSequentialSchemeRegistry, + getTimeFormatter, + getValueFormatter, + getXAxisColumn, + NumberFormats, + QueryFormColumn, + QueryFormData, + QueryFormMetric, + QueryFormOrderBy, + rgbToHex, + addAlpha, + RgbaColor, + tooltipHtml, + validateNonEmpty, +} from '@superset-ui/core'; +import { + formatSelectOptionsForRange, + getStandardizedControls, + rankOperator, +} from '@superset-ui/chart-controls'; +import memoizeOne from 'memoize-one'; +import { maxBy, minBy } from 'lodash'; +import type { ComposeOption } from 'echarts/core'; +import type { HeatmapSeriesOption } from 'echarts/charts'; +import type { CallbackDataParams } from 'echarts/types/src/util/types'; + +import { defineChart } from '@superset-ui/glyph-core'; +import { Checkbox, Select, Slider } from '@superset-ui/glyph-core'; +import { ShowLegend } from '@superset-ui/glyph-core'; +import Echart from '../components/Echart'; +import { getDefaultTooltip } from '../utils/tooltip'; +import { Refs, BaseChartProps, BaseTransformedProps } from '../types'; +import { parseAxisBound } from '../utils/controls'; +import { getPercentFormatter } from '../utils/formatters'; +import { xAxisLabelRotation } from '../controls'; + +import thumbnail from './images/thumbnail.png'; +import thumbnailDark from './images/thumbnail-dark.png'; +import example1 from './images/example1.png'; +import example1Dark from './images/example1-dark.png'; +import example2 from './images/example2.png'; +import example2Dark from './images/example2-dark.png'; +import example3 from './images/example3.png'; +import example3Dark from './images/example3-dark.png'; + +// ============================================================================ +// Types +// ============================================================================ + +type EChartsOption = ComposeOption; + +interface HeatmapFormData extends QueryFormData { + bottomMargin: string; + currencyFormat?: Currency; + leftMargin: string; + legendType: 'continuous' | 'piecewise'; + linearColorScheme?: string; + metric: QueryFormMetric; + normalizeAcross: 'heatmap' | 'x' | 'y'; + normalized?: boolean; + borderColor: RgbaColor; + borderWidth: number; + showLegend?: boolean; + showPercentage?: boolean; + showValues?: boolean; + sortXAxis?: string; + sortYAxis?: string; + timeFormat?: string; + xAxis: QueryFormColumn; + xAxisLabelRotation: number; + xscaleInterval: number; + valueBounds: [number | undefined | null, number | undefined | null]; + yAxisFormat?: string; + yscaleInterval: number; +} + +interface HeatmapChartProps extends BaseChartProps { + formData: HeatmapFormData; +} + +type HeatmapTransformedProps = BaseTransformedProps; + +// ============================================================================ +// Constants +// ============================================================================ + +const DEFAULT_ECHARTS_BOUNDS = [0, 200]; + +const SORT_AXIS_OPTIONS = [ + { label: t('Axis ascending'), value: 'alpha_asc' }, + { label: t('Axis descending'), value: 'alpha_desc' }, + { label: t('Metric ascending'), value: 'value_asc' }, + { label: t('Metric descending'), value: 'value_desc' }, +]; + +const NORMALIZE_ACROSS_OPTIONS = [ + { label: t('heatmap'), value: 'heatmap' }, + { label: t('x'), value: 'x' }, + { label: t('y'), value: 'y' }, +]; + +const LEGEND_TYPE_OPTIONS = [ + { label: t('Continuous'), value: 'continuous' }, + { label: t('Piecewise'), value: 'piecewise' }, +]; + +// ============================================================================ +// Helper Functions +// ============================================================================ + +/** + * Extract unique values for an axis from the data. + * Filters out null and undefined values. + */ +function extractUniqueValues( + data: Record[], + columnName: string, +): DataRecordValue[] { + const uniqueSet = new Set(); + data.forEach(row => { + const value = row[columnName]; + if (value !== null && value !== undefined) { + uniqueSet.add(value); + } + }); + return Array.from(uniqueSet); +} + +/** + * Sort axis values based on the sort configuration. + * Supports alphabetical (with numeric awareness) and metric value-based sorting. + */ +function sortAxisValues( + values: DataRecordValue[], + data: Record[], + sortOption: string | undefined, + metricLabel: string, + axisColumn: string, +): DataRecordValue[] { + if (!sortOption) { + return values; + } + + const isAscending = sortOption.includes('asc'); + const isValueSort = sortOption.includes('value'); + + if (isValueSort) { + const valueMap = new Map(); + data.forEach(row => { + const axisValue = row[axisColumn]; + const metricValue = row[metricLabel]; + if ( + axisValue !== null && + axisValue !== undefined && + typeof metricValue === 'number' + ) { + const current = valueMap.get(axisValue) || 0; + valueMap.set(axisValue, current + metricValue); + } + }); + + return [...values].sort((a, b) => { + const aValue = valueMap.get(a) || 0; + const bValue = valueMap.get(b) || 0; + return isAscending ? aValue - bValue : bValue - aValue; + }); + } + + // Alphabetical/lexicographic sort + return [...values].sort((a, b) => { + const aNum = typeof a === 'number' ? a : Number(a); + const bNum = typeof b === 'number' ? b : Number(b); + const aIsNumeric = Number.isFinite(aNum); + const bIsNumeric = Number.isFinite(bNum); + + if (aIsNumeric && bIsNumeric) { + return isAscending ? aNum - bNum : bNum - aNum; + } + + const aStr = String(a); + const bStr = String(b); + const comparison = aStr.localeCompare(bStr, undefined, { numeric: true }); + return isAscending ? comparison : -comparison; + }); +} + +// Calculated totals per x and y categories plus total +const calculateTotals = memoizeOne( + ( + data: Record[], + xAxis: string, + groupby: string, + metric: string, + ) => + data.reduce( + ( + acc: { + x: Record; + y: Record; + total: number; + }, + row, + ) => { + const value = row[metric]; + if (typeof value !== 'number') { + return acc; + } + const x = row[xAxis] as string; + const y = row[groupby] as string; + const xTotal = acc.x[x] || 0; + const yTotal = acc.y[y] || 0; + return { + x: { ...acc.x, [x]: xTotal + value }, + y: { ...acc.y, [y]: yTotal + value }, + total: acc.total + value, + }; + }, + { + x: {} as Record, + y: {} as Record, + total: 0, + }, + ), +); + +// ============================================================================ +// Chart Definition +// ============================================================================ + +export default defineChart({ + metadata: { + name: t('Heatmap'), + description: t( + 'Visualize a related metric across pairs of groups. Heatmaps excel at showcasing the correlation or strength between two groups. Color is used to emphasize the strength of the link between each pair of groups.', + ), + category: t('Correlation'), + tags: [ + t('Business'), + t('Intensity'), + t('Density'), + t('Single Metric'), + t('ECharts'), + t('Featured'), + ], + thumbnail, + thumbnailDark, + exampleGallery: [ + { url: example1, urlDark: example1Dark }, + { url: example2, urlDark: example2Dark }, + { url: example3, urlDark: example3Dark }, + ], + }, + + arguments: { + // Query section controls are in additionalControls + sortXAxis: Select.with({ + label: t('Sort X Axis'), + options: SORT_AXIS_OPTIONS, + default: undefined, + clearable: true, + renderTrigger: false, + }), + sortYAxis: Select.with({ + label: t('Sort Y Axis'), + options: SORT_AXIS_OPTIONS, + default: undefined, + clearable: true, + renderTrigger: false, + }), + normalizeAcross: Select.with({ + label: t('Normalize Across'), + description: t( + 'Color will be shaded based the normalized (0% to 100%) value of a given cell against the other cells in the selected range.', + ), + options: NORMALIZE_ACROSS_OPTIONS, + default: 'heatmap', + renderTrigger: false, + }), + // Chart Options + legendType: Select.with({ + label: t('Legend Type'), + options: LEGEND_TYPE_OPTIONS, + default: 'continuous', + }), + showLegend: ShowLegend, + borderWidth: Slider.with({ + label: t('Border width'), + description: t('The width of the elements border'), + min: 0, + max: 2, + step: 0.1, + default: 0, + }), + showPercentage: Checkbox.with({ + label: t('Show percentage'), + description: t('Whether to include the percentage in the tooltip'), + default: true, + }), + showValues: Checkbox.with({ + label: t('Show Values'), + description: t( + 'Whether to display the numerical values within the cells', + ), + default: false, + }), + normalized: Checkbox.with({ + label: t('Normalized'), + description: t( + 'Whether to apply a normal distribution based on rank on the color scale', + ), + default: false, + }), + }, + + // Many Heatmap controls use special control types (ColorPicker, Bounds, etc.) + // that need additionalControls + additionalControls: { + query: [ + ['x_axis'], + ['time_grain_sqla'], + ['groupby'], + ['metric'], + ['adhoc_filters'], + ['row_limit'], + ], + chartOptions: [ + ['linear_color_scheme'], + [ + { + name: 'border_color', + config: { + type: 'ColorPickerControl', + label: t('Border color'), + renderTrigger: true, + description: t('The color of the elements border'), + default: { r: 0, g: 0, b: 0, a: 1 }, + }, + }, + ], + [ + { + name: 'xscale_interval', + config: { + type: 'SelectControl', + label: t('X-scale interval'), + renderTrigger: true, + choices: [[-1, t('Auto')]].concat( + formatSelectOptionsForRange(1, 50), + ), + default: -1, + clearable: false, + description: t( + 'Number of steps to take between ticks when displaying the X scale', + ), + }, + }, + ], + [ + { + name: 'yscale_interval', + config: { + type: 'SelectControl', + label: t('Y-scale interval'), + choices: [[-1, t('Auto')]].concat( + formatSelectOptionsForRange(1, 50), + ), + default: -1, + clearable: false, + renderTrigger: true, + description: t( + 'Number of steps to take between ticks when displaying the Y scale', + ), + }, + }, + ], + [ + { + name: 'left_margin', + config: { + type: 'SelectControl', + freeForm: true, + clearable: false, + label: t('Left Margin'), + choices: [ + ['auto', t('Auto')], + [50, '50'], + [75, '75'], + [100, '100'], + [125, '125'], + [150, '150'], + [200, '200'], + ], + default: 'auto', + renderTrigger: true, + description: t( + 'Left margin, in pixels, allowing for more room for axis labels', + ), + }, + }, + ], + [ + { + name: 'bottom_margin', + config: { + type: 'SelectControl', + clearable: false, + freeForm: true, + label: t('Bottom Margin'), + choices: [ + ['auto', t('Auto')], + [50, '50'], + [75, '75'], + [100, '100'], + [125, '125'], + [150, '150'], + [200, '200'], + ], + default: 'auto', + renderTrigger: true, + description: t( + 'Bottom margin, in pixels, allowing for more room for axis labels', + ), + }, + }, + ], + [ + { + name: 'value_bounds', + config: { + type: 'BoundsControl', + label: t('Value bounds'), + renderTrigger: true, + default: [null, null], + description: t('Hard value bounds applied for color coding.'), + }, + }, + ], + ['y_axis_format'], + ['x_axis_time_format'], + [xAxisLabelRotation], + ['currency_format'], + ], + }, + + controlOverrides: { + groupby: { + label: t('Y-Axis'), + description: t('Dimension to use on y-axis.'), + multi: false, + validators: [validateNonEmpty], + }, + y_axis_format: { + label: t('Value Format'), + }, + }, + + formDataOverrides: formData => ({ + ...formData, + metric: getStandardizedControls().shiftMetric(), + }), + + buildQuery: (formData: QueryFormData) => { + const { groupby, x_axis: xAxis } = formData; + const normalizeAcross = formData.normalize_across; + const sortXAxis = formData.sort_x_axis; + const sortYAxis = formData.sort_y_axis; + const metric = getMetricLabel(formData.metric); + const columns = [ + ...ensureIsArray(getXAxisColumn(formData)), + ...ensureIsArray(groupby), + ]; + const orderby: QueryFormOrderBy[] = []; + if (sortXAxis) { + orderby.push([ + sortXAxis.includes('value') ? metric : columns[0], + sortXAxis.includes('asc'), + ]); + } + if (sortYAxis) { + orderby.push([ + sortYAxis.includes('value') ? metric : columns[1], + sortYAxis.includes('asc'), + ]); + } + const groupBy = + normalizeAcross === 'x' + ? getColumnLabel(xAxis) + : normalizeAcross === 'y' + ? getColumnLabel(groupby as unknown as QueryFormColumn) + : undefined; + return buildQueryContext(formData, baseQueryObject => [ + { + ...baseQueryObject, + columns, + orderby, + post_processing: [ + rankOperator(formData, baseQueryObject, { + metric, + group_by: groupBy, + }), + ], + }, + ]); + }, + + transform: ( + chartProps: HeatmapChartProps, + ): { transformedProps: HeatmapTransformedProps } => { + const refs: Refs = {}; + const { width, height, formData, queriesData, datasource, theme } = + chartProps; + const { + bottomMargin, + xAxis, + groupby, + linearColorScheme, + leftMargin, + legendType = 'continuous', + metric = '', + normalizeAcross, + normalized, + borderColor, + borderWidth = 0, + showLegend, + showPercentage, + showValues, + xscaleInterval, + yscaleInterval, + valueBounds, + yAxisFormat, + xAxisTimeFormat, + xAxisLabelRotation: rotation, + currencyFormat, + sortXAxis, + sortYAxis, + } = formData; + const metricLabel = getMetricLabel(metric); + const xAxisLabel = getColumnLabel(xAxis); + const yAxisLabel = getColumnLabel(groupby as unknown as QueryFormColumn); + const { data, colnames, coltypes } = queriesData[0]; + const { columnFormats = {}, currencyFormats = {} } = datasource; + const colorColumn = normalized ? 'rank' : metricLabel; + const colors = getSequentialSchemeRegistry().get(linearColorScheme)?.colors; + const getAxisFormatter = + (colType: GenericDataType) => (value: number | string) => { + if (colType === GenericDataType.Temporal) { + if (typeof value === 'string') { + return getTimeFormatter(xAxisTimeFormat)( + Number.parseInt(value, 10), + ); + } + return getTimeFormatter(xAxisTimeFormat)(value); + } + return String(value); + }; + + const xAxisFormatter = getAxisFormatter(coltypes[0]); + const yAxisFormatter = getAxisFormatter(coltypes[1]); + const percentFormatter = getPercentFormatter(NumberFormats.PERCENT_2_POINT); + const valueFormatter = getValueFormatter( + metric, + currencyFormats, + columnFormats, + yAxisFormat, + currencyFormat, + ); + + let [min, max] = (valueBounds || []).map(parseAxisBound); + if (min === undefined) { + min = + (minBy(data, row => row[colorColumn])?.[colorColumn] as number) || + DEFAULT_ECHARTS_BOUNDS[0]; + } + if (max === undefined) { + max = + (maxBy(data, row => row[colorColumn])?.[colorColumn] as number) || + DEFAULT_ECHARTS_BOUNDS[1]; + } + + // Extract and sort unique axis values + const xAxisColumnName = colnames[0]; + const yAxisColumnName = colnames[1]; + + const xAxisValues = extractUniqueValues(data, xAxisColumnName); + const yAxisValues = extractUniqueValues(data, yAxisColumnName); + + const sortedXAxisValues = sortAxisValues( + xAxisValues, + data, + sortXAxis, + metricLabel, + xAxisColumnName, + ); + const sortedYAxisValues = sortAxisValues( + yAxisValues, + data, + sortYAxis, + metricLabel, + yAxisColumnName, + ); + + // Create lookup maps for axis indices + const xAxisIndexMap = new Map( + sortedXAxisValues.map((value, index) => [value, index]), + ); + const yAxisIndexMap = new Map( + sortedYAxisValues.map((value, index) => [value, index]), + ); + + const series: HeatmapSeriesOption[] = [ + { + name: metricLabel, + type: 'heatmap', + data: data.flatMap(row => { + const xValue = row[xAxisColumnName]; + const yValue = row[yAxisColumnName]; + const metricValue = row[metricLabel]; + + const xIndex = xAxisIndexMap.get(xValue); + const yIndex = yAxisIndexMap.get(yValue); + + if (xIndex === undefined || yIndex === undefined) { + logging.warn( + `Heatmap: Skipping row due to missing axis value. xValue: ${xValue}, yValue: ${yValue}, metricValue: ${metricValue}`, + row, + ); + return []; + } + return [[xIndex, yIndex, metricValue]]; + }) as unknown as HeatmapSeriesOption['data'], + label: { + show: showValues, + formatter: (params: CallbackDataParams) => { + const paramsValue = params.value as (string | number)[]; + return valueFormatter( + paramsValue?.[2] as number | null | undefined, + ); + }, + }, + itemStyle: { + borderColor: addAlpha( + rgbToHex(borderColor.r, borderColor.g, borderColor.b), + borderColor.a, + ), + borderWidth, + }, + emphasis: { + itemStyle: { + borderColor: 'transparent', + shadowBlur: 10, + shadowColor: addAlpha(theme.colorText, 0.3), + }, + }, + }, + ]; + + const echartOptions: EChartsOption = { + grid: { + containLabel: true, + bottom: bottomMargin, + left: leftMargin, + }, + series, + tooltip: { + ...getDefaultTooltip(refs), + formatter: (params: CallbackDataParams) => { + const totals = calculateTotals( + data, + xAxisLabel, + yAxisLabel, + metricLabel, + ) as { + x: Record; + y: Record; + total: number; + }; + const paramsValue = params.value as (string | number)[]; + const x = paramsValue?.[0]; + const y = paramsValue?.[1]; + const value = paramsValue?.[2] as number | null | undefined; + const formattedX = xAxisFormatter(x); + const formattedY = yAxisFormatter(y); + const formattedValue = valueFormatter(value); + let percentage = 0; + let suffix = 'heatmap'; + if (typeof value === 'number') { + if (normalizeAcross === 'x') { + percentage = value / totals.x[String(x)]; + suffix = formattedX; + } else if (normalizeAcross === 'y') { + percentage = value / totals.y[String(y)]; + suffix = formattedY; + } else { + percentage = value / totals.total; + suffix = 'heatmap'; + } + } + const title = `${formattedX} (${formattedY})`; + const row = [colnames[2], formattedValue]; + if (showPercentage) { + row.push(`${percentFormatter(percentage)} (${suffix})`); + } + return tooltipHtml([row], title); + }, + }, + visualMap: { + type: legendType, + min, + max, + calculable: true, + orient: 'horizontal', + right: 0, + top: 0, + itemHeight: legendType === 'continuous' ? 300 : 14, + itemWidth: 15, + formatter: (minVal: number) => valueFormatter(minVal), + inRange: { + color: colors, + }, + show: showLegend, + dimension: normalized ? 3 : 2, + }, + xAxis: { + type: 'category', + data: sortedXAxisValues, + axisLabel: { + formatter: xAxisFormatter, + interval: xscaleInterval === -1 ? 'auto' : xscaleInterval - 1, + rotate: rotation, + }, + }, + yAxis: { + type: 'category', + data: sortedYAxisValues, + axisLabel: { + formatter: yAxisFormatter, + interval: yscaleInterval === -1 ? 'auto' : yscaleInterval - 1, + }, + }, + }; + return { + transformedProps: { + refs, + echartOptions, + width, + height, + formData, + }, + }; + }, + + render: ({ transformedProps }) => { + const { height, width, echartOptions, refs, formData } = transformedProps; + return ( + + ); + }, +}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Heatmap/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Heatmap/transformProps.ts deleted file mode 100644 index cb2d250dd04..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Heatmap/transformProps.ts +++ /dev/null @@ -1,456 +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 { - NumberFormats, - QueryFormColumn, - getColumnLabel, - getMetricLabel, - getSequentialSchemeRegistry, - getTimeFormatter, - getValueFormatter, - rgbToHex, - addAlpha, - tooltipHtml, - DataRecordValue, -} from '@superset-ui/core'; -import { logging } from '@apache-superset/core/utils'; -import { GenericDataType } from '@apache-superset/core/common'; -import memoizeOne from 'memoize-one'; -import { maxBy, minBy } from 'lodash'; -import type { ComposeOption } from 'echarts/core'; -import type { HeatmapSeriesOption } from 'echarts/charts'; -import type { CallbackDataParams } from 'echarts/types/src/util/types'; -import { HeatmapChartProps, HeatmapTransformedProps } from './types'; -import { getDefaultTooltip } from '../utils/tooltip'; -import { Refs } from '../types'; -import { parseAxisBound } from '../utils/controls'; -import { getPercentFormatter } from '../utils/formatters'; - -type EChartsOption = ComposeOption; - -const DEFAULT_ECHARTS_BOUNDS = [0, 200]; - -/** - * Column name for the rank values added by the backend's rank post-processing operation. - * This is used when the heatmap is in normalized mode to color cells by percentile rank. - */ -const RANK_COLUMN_NAME = 'rank'; - -/** - * Extract unique values for an axis from the data. - * Filters out null and undefined values. - * - * @param data - The dataset to extract values from - * @param columnName - The column to extract unique values from - * @returns Array of unique values from the specified column - */ -function extractUniqueValues( - data: Record[], - columnName: string, -): DataRecordValue[] { - const uniqueSet = new Set(); - data.forEach(row => { - const value = row[columnName]; - if (value !== null && value !== undefined) { - uniqueSet.add(value); - } - }); - return Array.from(uniqueSet); -} - -/** - * Sort axis values based on the sort configuration. - * Supports alphabetical (with numeric awareness) and metric value-based sorting. - * - * @param values - The unique values to sort - * @param data - The full dataset - * @param sortOption - Sort option string (e.g., 'alpha_asc', 'value_desc') - * @param metricLabel - Label of the metric for value-based sorting - * @param axisColumn - Column name for the axis being sorted - * @returns Sorted array of values - */ -function sortAxisValues( - values: DataRecordValue[], - data: Record[], - sortOption: string | undefined, - metricLabel: string, - axisColumn: string, -): DataRecordValue[] { - if (!sortOption) { - // No sorting specified, return values as they appear in the data - return values; - } - - const isAscending = sortOption.includes('asc'); - const isValueSort = sortOption.includes('value'); - - if (isValueSort) { - // Sort by metric value - aggregate metric values for each axis category - const valueMap = new Map(); - data.forEach(row => { - const axisValue = row[axisColumn]; - const metricValue = row[metricLabel]; - if ( - axisValue !== null && - axisValue !== undefined && - typeof metricValue === 'number' - ) { - const current = valueMap.get(axisValue) || 0; - valueMap.set(axisValue, current + metricValue); - } - }); - - return [...values].sort((a, b) => { - const aValue = valueMap.get(a) || 0; - const bValue = valueMap.get(b) || 0; - return isAscending ? aValue - bValue : bValue - aValue; - }); - } - - // Alphabetical/lexicographic sort - return [...values].sort((a, b) => { - // Check if both values are numeric for proper numeric sorting - const aNum = typeof a === 'number' ? a : Number(a); - const bNum = typeof b === 'number' ? b : Number(b); - const aIsNumeric = Number.isFinite(aNum); - const bIsNumeric = Number.isFinite(bNum); - - if (aIsNumeric && bIsNumeric) { - // Both are numeric, sort numerically - return isAscending ? aNum - bNum : bNum - aNum; - } - - // At least one is non-numeric, use locale-aware string comparison - const aStr = String(a); - const bStr = String(b); - const comparison = aStr.localeCompare(bStr, undefined, { numeric: true }); - return isAscending ? comparison : -comparison; - }); -} - -// Calculated totals per x and y categories plus total -const calculateTotals = memoizeOne( - ( - data: Record[], - xAxis: string, - groupby: string, - metric: string, - ) => - data.reduce( - (acc, row) => { - const value = row[metric]; - if (typeof value !== 'number') { - return acc; - } - const x = row[xAxis] as string; - const y = row[groupby] as string; - const xTotal = acc.x[x] || 0; - const yTotal = acc.y[y] || 0; - return { - x: { ...acc.x, [x]: xTotal + value }, - y: { ...acc.y, [y]: yTotal + value }, - total: acc.total + value, - }; - }, - { x: {}, y: {}, total: 0 }, - ), -); - -export default function transformProps( - chartProps: HeatmapChartProps, -): HeatmapTransformedProps { - const refs: Refs = {}; - const { width, height, formData, queriesData, datasource, theme } = - chartProps; - const { - bottomMargin, - xAxis, - groupby, - linearColorScheme, - leftMargin, - legendType = 'continuous', - metric = '', - normalizeAcross, - normalized, - borderColor, - borderWidth = 0, - showLegend, - showPercentage, - showValues, - xscaleInterval, - yscaleInterval, - valueBounds, - yAxisFormat, - xAxisTimeFormat, - xAxisLabelRotation, - currencyFormat, - sortXAxis, - sortYAxis, - } = formData; - const metricLabel = getMetricLabel(metric); - const xAxisLabel = getColumnLabel(xAxis); - // groupby is overridden to be a single value - const yAxisLabel = getColumnLabel(groupby as unknown as QueryFormColumn); - const { - data, - colnames, - coltypes, - detected_currency: detectedCurrency, - } = queriesData[0]; - const { - columnFormats = {}, - currencyFormats = {}, - currencyCodeColumn, - } = datasource; - const colorColumn = normalized ? RANK_COLUMN_NAME : metricLabel; - const colors = getSequentialSchemeRegistry().get(linearColorScheme)?.colors; - const getAxisFormatter = - (colType: GenericDataType) => (value: number | string) => { - if (colType === GenericDataType.Temporal) { - if (typeof value === 'string') { - return getTimeFormatter(xAxisTimeFormat)(Number.parseInt(value, 10)); - } - return getTimeFormatter(xAxisTimeFormat)(value); - } - return String(value); - }; - - const xAxisFormatter = getAxisFormatter(coltypes[0]); - const yAxisFormatter = getAxisFormatter(coltypes[1]); - const percentFormatter = getPercentFormatter(NumberFormats.PERCENT_2_POINT); - const valueFormatter = getValueFormatter( - metric, - currencyFormats, - columnFormats, - yAxisFormat, - currencyFormat, - undefined, - data, - currencyCodeColumn, - detectedCurrency, - ); - - let [min, max] = (valueBounds || []).map(parseAxisBound); - if (min === undefined) { - min = - (minBy(data, row => row[colorColumn])?.[colorColumn] as number) || - DEFAULT_ECHARTS_BOUNDS[0]; - } - if (max === undefined) { - max = - (maxBy(data, row => row[colorColumn])?.[colorColumn] as number) || - DEFAULT_ECHARTS_BOUNDS[1]; - } - - // Extract and sort unique axis values - // Use colnames to get the actual column names in the data - const xAxisColumnName = colnames[0]; - const yAxisColumnName = colnames[1]; - - const xAxisValues = extractUniqueValues(data, xAxisColumnName); - const yAxisValues = extractUniqueValues(data, yAxisColumnName); - - const sortedXAxisValues = sortAxisValues( - xAxisValues, - data, - sortXAxis, - metricLabel, - xAxisColumnName, - ); - const sortedYAxisValues = sortAxisValues( - yAxisValues, - data, - sortYAxis, - metricLabel, - yAxisColumnName, - ); - - // Create lookup maps for axis indices - const xAxisIndexMap = new Map( - sortedXAxisValues.map((value, index) => [value, index]), - ); - const yAxisIndexMap = new Map( - sortedYAxisValues.map((value, index) => [value, index]), - ); - - const series: HeatmapSeriesOption[] = [ - { - name: metricLabel, - type: 'heatmap', - data: data.flatMap(row => { - const xValue = row[xAxisColumnName]; - const yValue = row[yAxisColumnName]; - const metricValue = row[metricLabel]; - const rankValue = row[RANK_COLUMN_NAME]; - - // Convert to axis indices for ECharts when explicit axis data is provided - const xIndex = xAxisIndexMap.get(xValue); - const yIndex = yAxisIndexMap.get(yValue); - - if (xIndex === undefined || yIndex === undefined) { - // Log a warning for debugging - logging.warn( - `Heatmap: Skipping row due to missing axis value. xValue: ${xValue}, yValue: ${yValue}, metricValue: ${metricValue}`, - row, - ); - return []; - } - if (normalized && rankValue === undefined) { - logging.error( - `Heatmap: Skipping row due to missing rank value. xValue: ${xValue}, yValue: ${yValue}, metricValue: ${metricValue}`, - row, - ); - return []; - } - - // Include rank as 4th dimension when normalized is enabled - // This allows visualMap to use dimension: 3 to color by rank percentile - if (normalized) { - return [[xIndex, yIndex, metricValue, rankValue]]; - } - return [[xIndex, yIndex, metricValue]]; - }) as any, - label: { - show: showValues, - formatter: (params: CallbackDataParams) => { - const paramsValue = params.value as (string | number)[]; - return valueFormatter(paramsValue?.[2] as number | null | undefined); - }, - }, - itemStyle: { - borderColor: addAlpha( - rgbToHex(borderColor.r, borderColor.g, borderColor.b), - borderColor.a, - ), - borderWidth, - }, - emphasis: { - itemStyle: { - borderColor: 'transparent', - shadowBlur: 10, - shadowColor: addAlpha(theme.colorText, 0.3), - }, - }, - }, - ]; - - const echartOptions: EChartsOption = { - grid: { - containLabel: true, - bottom: bottomMargin, - left: leftMargin, - }, - legend: { - show: false, - }, - series, - tooltip: { - ...getDefaultTooltip(refs), - formatter: (params: CallbackDataParams) => { - const totals = calculateTotals( - data, - xAxisLabel, - yAxisLabel, - metricLabel, - ); - const paramsValue = params.value as (string | number)[]; - // paramsValue contains [xIndex, yIndex, metricValue, rankValue?] - // We need to look up the actual axis values from the sorted arrays - const xIndex = paramsValue?.[0] as number; - const yIndex = paramsValue?.[1] as number; - const value = paramsValue?.[2] as number | null | undefined; - const xValue = sortedXAxisValues[xIndex]; - const yValue = sortedYAxisValues[yIndex]; - // Format the axis values for display (handle null/undefined with empty string fallback) - // Convert to string/number for formatter compatibility - const formattedX = - xValue !== null && xValue !== undefined - ? xAxisFormatter(xValue as string | number) - : ''; - const formattedY = - yValue !== null && yValue !== undefined - ? yAxisFormatter(yValue as string | number) - : ''; - const formattedValue = valueFormatter(value); - let percentage = 0; - let suffix = 'heatmap'; - if (typeof value === 'number') { - if (normalizeAcross === 'x') { - percentage = value / totals.x[String(xValue)]; - suffix = formattedX; - } else if (normalizeAcross === 'y') { - percentage = value / totals.y[String(yValue)]; - suffix = formattedY; - } else { - percentage = value / totals.total; - suffix = 'heatmap'; - } - } - const title = `${formattedX} (${formattedY})`; - const row = [colnames[2], formattedValue]; - if (showPercentage) { - row.push(`${percentFormatter(percentage)} (${suffix})`); - } - return tooltipHtml([row], title); - }, - }, - visualMap: { - type: legendType, - min, - max, - calculable: true, - orient: 'horizontal', - right: 0, - top: 0, - itemHeight: legendType === 'continuous' ? 300 : 14, - itemWidth: 15, - formatter: (min: number) => valueFormatter(min), - inRange: { - color: colors, - }, - show: showLegend, - // By default, ECharts uses the last dimension which is rank - dimension: normalized ? 3 : 2, - }, - xAxis: { - type: 'category', - data: sortedXAxisValues, - axisLabel: { - formatter: xAxisFormatter, - interval: xscaleInterval === -1 ? 'auto' : xscaleInterval - 1, - rotate: xAxisLabelRotation, - }, - }, - yAxis: { - type: 'category', - data: sortedYAxisValues, - axisLabel: { - formatter: yAxisFormatter, - interval: yscaleInterval === -1 ? 'auto' : yscaleInterval - 1, - }, - }, - }; - return { - refs, - echartOptions, - width, - height, - formData, - }; -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Heatmap/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Heatmap/types.ts deleted file mode 100644 index cb7217a4abe..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Heatmap/types.ts +++ /dev/null @@ -1,57 +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 { - Currency, - QueryFormColumn, - QueryFormData, - QueryFormMetric, - RgbaColor, -} from '@superset-ui/core'; -import { BaseChartProps, BaseTransformedProps } from '../types'; - -export interface HeatmapFormData extends QueryFormData { - bottomMargin: string; - currencyFormat?: Currency; - leftMargin: string; - legendType: 'continuous' | 'piecewise'; - linearColorScheme?: string; - metric: QueryFormMetric; - normalizeAcross: 'heatmap' | 'x' | 'y'; - normalized?: boolean; - borderColor: RgbaColor; - borderWidth: number; - showLegend?: boolean; - showPercentage?: boolean; - showValues?: boolean; - sortXAxis?: string; - sortYAxis?: string; - timeFormat?: string; - xAxis: QueryFormColumn; - xAxisLabelRotation: number; - xscaleInterval: number; - valueBounds: [number | undefined | null, number | undefined | null]; - yAxisFormat?: string; - yscaleInterval: number; -} - -export interface HeatmapChartProps extends BaseChartProps { - formData: HeatmapFormData; -} - -export type HeatmapTransformedProps = BaseTransformedProps; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Histogram/Histogram.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Histogram/Histogram.tsx deleted file mode 100644 index 4f7b5c220d7..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Histogram/Histogram.tsx +++ /dev/null @@ -1,62 +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 { HistogramTransformedProps } from './types'; -import Echart from '../components/Echart'; -import { EventHandlers } from '../types'; - -export default function Histogram(props: HistogramTransformedProps) { - const { - height, - width, - echartOptions, - onFocusedSeries, - onLegendStateChanged, - refs, - formData, - } = props; - - const eventHandlers: EventHandlers = { - legendselectchanged: payload => { - onLegendStateChanged?.(payload.selected); - }, - legendselectall: payload => { - onLegendStateChanged?.(payload.selected); - }, - legendinverseselect: payload => { - onLegendStateChanged?.(payload.selected); - }, - mouseout: () => { - onFocusedSeries(undefined); - }, - mouseover: params => { - onFocusedSeries(params.seriesIndex); - }, - }; - - return ( - - ); -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Histogram/buildQuery.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Histogram/buildQuery.ts deleted file mode 100644 index 4afcb1e4af3..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Histogram/buildQuery.ts +++ /dev/null @@ -1,33 +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 { buildQueryContext } from '@superset-ui/core'; -import { histogramOperator } from '@superset-ui/chart-controls'; -import { HistogramFormData } from './types'; - -export default function buildQuery(formData: HistogramFormData) { - const { column, groupby = [] } = formData; - return buildQueryContext(formData, baseQueryObject => [ - { - ...baseQueryObject, - columns: [...groupby, column], - post_processing: [histogramOperator(formData, baseQueryObject)], - metrics: undefined, - }, - ]); -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Histogram/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Histogram/controlPanel.tsx deleted file mode 100644 index 889f9c04edd..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Histogram/controlPanel.tsx +++ /dev/null @@ -1,173 +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 { t } from '@apache-superset/core/translation'; -import { - validateInteger, - validateNonEmpty, - withLabel, -} from '@superset-ui/core'; -import { GenericDataType } from '@apache-superset/core/common'; -import { - ControlPanelConfig, - formatSelectOptionsForRange, - dndGroupByControl, - columnsByType, - D3_FORMAT_OPTIONS, - D3_FORMAT_DOCS, - D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT, -} from '@superset-ui/chart-controls'; -import { showLegendControl, showValueControl } from '../controls'; - -const config: ControlPanelConfig = { - controlPanelSections: [ - { - label: t('Query'), - expanded: true, - controlSetRows: [ - [ - { - name: 'column', - config: { - ...dndGroupByControl, - label: t('Column'), - multi: false, - description: t('Numeric column used to calculate the histogram.'), - validators: [validateNonEmpty], - freeForm: false, - disabledTabs: new Set(['saved', 'sqlExpression']), - mapStateToProps: ({ datasource }) => ({ - options: columnsByType(datasource, GenericDataType.Numeric), - }), - }, - }, - ], - ['groupby'], - ['adhoc_filters'], - ['row_limit'], - [ - { - name: 'bins', - config: { - type: 'SelectControl', - freeForm: true, - label: t('Bins'), - default: 5, - choices: formatSelectOptionsForRange(5, 20, 5), - description: t('The number of bins for the histogram'), - validators: [withLabel(validateInteger, t('Bins'))], - }, - }, - ], - [ - { - name: 'normalize', - config: { - type: 'CheckboxControl', - label: t('Normalize'), - description: t(` - The normalize option transforms the histogram values into proportions or - probabilities by dividing each bin's count by the total count of data points. - This normalization process ensures that the resulting values sum up to 1, - enabling a relative comparison of the data's distribution and providing a - clearer understanding of the proportion of data points within each bin.`), - default: false, - }, - }, - ], - [ - { - name: 'cumulative', - config: { - type: 'CheckboxControl', - label: t('Cumulative'), - description: t(` - The cumulative option allows you to see how your data accumulates over different - values. When enabled, the histogram bars represent the running total of frequencies - up to each bin. This helps you understand how likely it is to encounter values - below a certain point. Keep in mind that enabling cumulative doesn't change your - original data, it just changes the way the histogram is displayed.`), - default: false, - }, - }, - ], - ], - }, - { - label: t('Chart Options'), - expanded: true, - controlSetRows: [ - ['color_scheme'], - [showValueControl], - [showLegendControl], - [ - { - name: 'x_axis_title', - config: { - type: 'TextControl', - label: t('X Axis Title'), - renderTrigger: true, - default: '', - }, - }, - ], - [ - { - name: 'x_axis_format', - config: { - type: 'SelectControl', - freeForm: true, - label: t('X Axis Format'), - renderTrigger: true, - default: 'SMART_NUMBER', - choices: D3_FORMAT_OPTIONS, - description: `${D3_FORMAT_DOCS} ${D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT}`, - }, - }, - ], - [ - { - name: 'y_axis_title', - config: { - type: 'TextControl', - label: t('Y Axis Title'), - renderTrigger: true, - default: '', - }, - }, - ], - [ - { - name: 'y_axis_format', - config: { - type: 'SelectControl', - freeForm: true, - label: t('Y Axis Format'), - renderTrigger: true, - default: 'SMART_NUMBER', - choices: D3_FORMAT_OPTIONS, - description: `${D3_FORMAT_DOCS} ${D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT}`, - }, - }, - ], - ], - }, - ], -}; - -export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Histogram/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Histogram/index.ts deleted file mode 100644 index f0e0d8905b8..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Histogram/index.ts +++ /dev/null @@ -1,74 +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 - * regardin - * g 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 { t } from '@apache-superset/core/translation'; -import { ChartMetadata, ChartPlugin } from '@superset-ui/core'; -import buildQuery from './buildQuery'; -import controlPanel from './controlPanel'; -import transformProps from './transformProps'; -import thumbnail from './images/thumbnail.png'; -import thumbnailDark from './images/thumbnail-dark.png'; -import example1 from './images/example1.png'; -import example2 from './images/example2.png'; -import example1Dark from './images/example1-dark.png'; -import example2Dark from './images/example2-dark.png'; -import { HistogramChartProps, HistogramFormData } from './types'; - -// TODO: Implement cross filtering -export default class EchartsHistogramChartPlugin extends ChartPlugin< - HistogramFormData, - HistogramChartProps -> { - /** - * The constructor is used to pass relevant metadata and callbacks that get - * registered in respective registries that are used throughout the library - * and application. A more thorough description of each property is given in - * the respective imported file. - * - * It is worth noting that `buildQuery` and is optional, and only needed for - * advanced visualizations that require either post processing operations - * (pivoting, rolling aggregations, sorting etc) or submitting multiple queries. - */ - constructor() { - super({ - buildQuery, - controlPanel, - loadChart: () => import('./Histogram'), - metadata: new ChartMetadata({ - credits: ['https://echarts.apache.org'], - category: t('Distribution'), - description: t( - `The histogram chart displays the distribution of a dataset by - representing the frequency or count of values within different ranges or bins. - It helps visualize patterns, clusters, and outliers in the data and provides - insights into its shape, central tendency, and spread.`, - ), - exampleGallery: [ - { url: example1, urlDark: example1Dark }, - { url: example2, urlDark: example2Dark }, - ], - name: t('Histogram'), - tags: [t('Comparison'), t('ECharts'), t('Pattern'), t('Range')], - thumbnail, - thumbnailDark, - }), - transformProps, - }); - } -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Histogram/index.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Histogram/index.tsx new file mode 100644 index 00000000000..6285b7f2d3d --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Histogram/index.tsx @@ -0,0 +1,460 @@ +/** + * 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. + */ + +/** + * ECharts Histogram Chart - Glyph Pattern Implementation + * + * Displays the distribution of a dataset by representing the frequency + * or count of values within different ranges or bins. + */ + +import { t } from '@apache-superset/core/translation'; +import type { EChartsCoreOption } from 'echarts/core'; +import type { CallbackDataParams } from 'echarts/types/src/util/types'; +import { isEmpty } from 'lodash'; +import { + buildQueryContext, + CategoricalColorNamespace, + getColumnLabel, + getValueFormatter, + NumberFormats, + QueryFormData, + tooltipHtml, +} from '@superset-ui/core'; +import { + histogramOperator, + formatSelectOptionsForRange, + dndGroupByControl, + columnsByType, +} from '@superset-ui/chart-controls'; +import { GenericDataType } from '@apache-superset/core/common'; + +import { + defineChart, + Dimension, + Text, + Checkbox, + Select, + ChartProps, + NumberFormat, + ShowLegend, + ShowValue, +} from '@superset-ui/glyph-core'; + +import { defaultGrid, defaultYAxis } from '../defaults'; +import { getLegendProps } from '../utils/series'; +import { getDefaultTooltip } from '../utils/tooltip'; +import { getPercentFormatter } from '../utils/formatters'; +import Echart from '../components/Echart'; +import { Refs, LegendOrientation, LegendType, EventHandlers } from '../types'; + +import thumbnail from './images/thumbnail.png'; +import thumbnailDark from './images/thumbnail-dark.png'; +import example1 from './images/example1.png'; +import example1Dark from './images/example1-dark.png'; +import example2 from './images/example2.png'; +import example2Dark from './images/example2-dark.png'; + +// ============================================================================ +// Constants +// ============================================================================ + +const BINS_OPTIONS = formatSelectOptionsForRange(5, 20, 5).map( + ([value, label]) => ({ + label: String(label), + value: Number(value), + }), +); + +// ============================================================================ +// Types +// ============================================================================ + +interface HistogramTransformResult { + transformedProps: { + refs: Refs; + width: number; + height: number; + echartOptions: EChartsCoreOption; + formData: Record; + onFocusedSeries: (index: number | undefined) => void; + onLegendStateChanged?: (selected: Record) => void; + }; +} + +// ============================================================================ +// Build Query +// ============================================================================ + +export function buildQuery(formData: QueryFormData) { + const { column, groupby = [] } = formData; + return buildQueryContext(formData, baseQueryObject => [ + { + ...baseQueryObject, + columns: [...(groupby as string[]), column as string], + post_processing: [histogramOperator(formData, baseQueryObject)], + metrics: undefined, + }, + ]); +} + +// ============================================================================ +// Chart Definition +// ============================================================================ + +export default defineChart({ + metadata: { + name: t('Histogram'), + description: t( + `The histogram chart displays the distribution of a dataset by + representing the frequency or count of values within different ranges or bins. + It helps visualize patterns, clusters, and outliers in the data and provides + insights into its shape, central tendency, and spread.`, + ), + category: t('Distribution'), + tags: [t('Comparison'), t('ECharts'), t('Pattern'), t('Range')], + credits: ['https://echarts.apache.org'], + thumbnail, + thumbnailDark, + exampleGallery: [ + { url: example1, urlDark: example1Dark }, + { url: example2, urlDark: example2Dark }, + ], + }, + + arguments: { + // Query section - groupby is handled via additionalControls + groupby: Dimension.with({ + label: t('Breakdowns'), + description: t('Defines how the histogram data is grouped.'), + multi: true, + }), + + bins: Select.with({ + label: t('Bins'), + description: t('The number of bins for the histogram'), + options: BINS_OPTIONS, + default: 5, + }), + + normalize: Checkbox.with({ + label: t('Normalize'), + description: t(` + The normalize option transforms the histogram values into proportions or + probabilities by dividing each bin's count by the total count of data points. + This normalization process ensures that the resulting values sum up to 1, + enabling a relative comparison of the data's distribution and providing a + clearer understanding of the proportion of data points within each bin.`), + default: false, + }), + + cumulative: Checkbox.with({ + label: t('Cumulative'), + description: t(` + The cumulative option allows you to see how your data accumulates over different + values. When enabled, the histogram bars represent the running total of frequencies + up to each bin. This helps you understand how likely it is to encounter values + below a certain point. Keep in mind that enabling cumulative doesn't change your + original data, it just changes the way the histogram is displayed.`), + default: false, + }), + + // Chart options + showValue: ShowValue, + showLegend: ShowLegend, + + xAxisTitle: Text.with({ + label: t('X Axis Title'), + description: t('Title for the X axis'), + default: '', + }), + + xAxisFormat: NumberFormat.with({ + label: t('X Axis Format'), + description: t('Number format for X axis'), + }), + + yAxisTitle: Text.with({ + label: t('Y Axis Title'), + description: t('Title for the Y axis'), + default: '', + }), + + yAxisFormat: NumberFormat.with({ + label: t('Y Axis Format'), + description: t('Number format for Y axis'), + }), + }, + + // Column control needs special handling with mapStateToProps + additionalControls: { + query: [ + [ + { + name: 'column', + config: { + ...dndGroupByControl, + label: t('Column'), + multi: false, + description: t('Numeric column used to calculate the histogram.'), + freeForm: false, + disabledTabs: new Set(['saved', 'sqlExpression']), + mapStateToProps: ({ datasource }: { datasource?: unknown }) => ({ + options: columnsByType( + datasource as Parameters[0], + GenericDataType.Numeric, + ), + }), + }, + }, + ], + ], + }, + + buildQuery, + + transform: (chartProps: ChartProps): HistogramTransformResult => { + const { + height, + width, + queriesData, + rawFormData, + hooks, + theme, + legendState = {}, + datasource, + } = chartProps; + + const refs: Refs = {}; + let focusedSeries: number | undefined; + + const { onLegendStateChanged } = hooks; + const { data = [] } = queriesData[0]; + + // Get datasource info for formatting + const { currencyFormats = {}, columnFormats = {} } = datasource ?? {}; + + // Extract form values + const colorScheme = rawFormData.color_scheme as string; + const column = rawFormData.column as string; + const groupby = (rawFormData.groupby as string[]) || []; + const normalize = rawFormData.normalize as boolean; + const showLegend = rawFormData.show_legend as boolean; + const showValue = rawFormData.show_value as boolean; + const sliceId = rawFormData.slice_id as number | undefined; + const xAxisFormat = (rawFormData.x_axis_format as string) || 'SMART_NUMBER'; + const xAxisTitle = (rawFormData.x_axis_title as string) || ''; + const yAxisTitle = (rawFormData.y_axis_title as string) || ''; + const yAxisFormat = (rawFormData.y_axis_format as string) || 'SMART_NUMBER'; + + const colorFn = CategoricalColorNamespace.getScale(colorScheme); + + const formatter = (format: string) => + getValueFormatter( + column, + currencyFormats, + columnFormats, + format, + undefined, + ); + const xAxisFormatter = formatter(xAxisFormat); + const yAxisFormatter = formatter(yAxisFormat); + + const percentFormatter = getPercentFormatter(NumberFormats.PERCENT_2_POINT); + const groupbySet = new Set(groupby); + + // Build X axis data from histogram bin ranges + const xAxisData: string[] = Object.keys(data[0] || {}) + .filter(key => !groupbySet.has(key)) + .map(key => { + const array = key.split(' - ').map(value => parseFloat(value)); + return `${xAxisFormatter(array[0])} - ${xAxisFormatter(array[1])}`; + }); + + // Build bar series + const barSeries = (data as Record[]).map(datum => { + const seriesName = + groupby.length > 0 + ? groupby.map(key => datum[getColumnLabel(key)]).join(', ') + : getColumnLabel(column); + const seriesData = Object.keys(datum) + .filter(key => !groupbySet.has(key)) + .map(key => datum[key] as number); + return { + name: seriesName, + type: 'bar' as const, + data: seriesData, + itemStyle: { + color: colorFn(seriesName, sliceId), + }, + label: { + show: showValue, + position: 'top' as const, + formatter: (params: { value: number | number[] }) => { + const { value } = params; + return yAxisFormatter.format(value as number); + }, + }, + }; + }); + + // Setup legend + const legendOptions = barSeries.map(series => series.name as string); + const currentLegendState = { ...legendState }; + if (isEmpty(currentLegendState)) { + legendOptions.forEach(legend => { + currentLegendState[legend] = true; + }); + } + + // Tooltip formatter + const tooltipFormatter = (params: CallbackDataParams[]) => { + const title = params[0].name; + const rows = params.map(param => { + const { marker, seriesName, value } = param; + return [ + `${marker}${seriesName}`, + yAxisFormatter.format(value as number), + ]; + }); + if (groupby.length > 0) { + const total = params.reduce( + (acc, param) => acc + (param.value as number), + 0, + ); + if (!normalize) { + rows.forEach((row, i) => + row.push( + percentFormatter.format( + (params[i].value as number) / (total || 1), + ), + ), + ); + } + const totalRow = ['Total', yAxisFormatter.format(total)]; + if (!normalize) { + totalRow.push(percentFormatter.format(1)); + } + rows.push(totalRow); + } + return tooltipHtml(rows, title, focusedSeries); + }; + + const onFocusedSeries = (index: number | undefined) => { + focusedSeries = index; + }; + + const echartOptions: EChartsCoreOption = { + grid: { + ...defaultGrid, + left: '5%', + right: '5%', + top: '10%', + bottom: '10%', + }, + xAxis: { + data: xAxisData, + name: xAxisTitle, + nameGap: 35, + type: 'category', + nameLocation: 'middle', + }, + yAxis: { + ...defaultYAxis, + name: yAxisTitle, + nameGap: normalize ? 55 : 40, + type: 'value', + nameLocation: 'middle', + axisLabel: { + formatter: (value: number) => yAxisFormatter.format(value), + }, + }, + series: barSeries, + legend: { + ...getLegendProps( + LegendType.Scroll, + LegendOrientation.Top, + showLegend, + theme, + false, + currentLegendState, + ), + data: legendOptions, + }, + tooltip: { + ...getDefaultTooltip(refs), + trigger: 'axis', + formatter: tooltipFormatter, + }, + }; + + return { + transformedProps: { + refs, + height, + width, + echartOptions, + formData: rawFormData, + onFocusedSeries, + onLegendStateChanged, + }, + }; + }, + + render: ({ transformedProps }) => { + const { + height, + width, + echartOptions, + refs, + formData, + onFocusedSeries, + onLegendStateChanged, + } = transformedProps; + + const eventHandlers: EventHandlers = { + legendselectchanged: payload => { + onLegendStateChanged?.(payload.selected); + }, + legendselectall: payload => { + onLegendStateChanged?.(payload.selected); + }, + legendinverseselect: payload => { + onLegendStateChanged?.(payload.selected); + }, + mouseout: () => { + onFocusedSeries(undefined); + }, + mouseover: params => { + onFocusedSeries(params.seriesIndex); + }, + }; + + return ( + + ); + }, +}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Histogram/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Histogram/transformProps.ts deleted file mode 100644 index 28d2a93e6d1..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Histogram/transformProps.ts +++ /dev/null @@ -1,208 +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 type { ComposeOption } from 'echarts/core'; -import type { BarSeriesOption } from 'echarts/charts'; -import type { GridComponentOption } from 'echarts/components'; -import type { CallbackDataParams } from 'echarts/types/src/util/types'; -import { isEmpty } from 'lodash'; -import { - CategoricalColorNamespace, - NumberFormats, - getColumnLabel, - getValueFormatter, - tooltipHtml, -} from '@superset-ui/core'; -import { HistogramChartProps, HistogramTransformedProps } from './types'; -import { LegendOrientation, LegendType, Refs } from '../types'; -import { defaultGrid, defaultYAxis } from '../defaults'; -import { getLegendProps } from '../utils/series'; -import { getDefaultTooltip } from '../utils/tooltip'; -import { getPercentFormatter } from '../utils/formatters'; - -export default function transformProps( - chartProps: HistogramChartProps, -): HistogramTransformedProps { - const refs: Refs = {}; - let focusedSeries: number | undefined; - const { - datasource: { currencyFormats = {}, columnFormats = {} }, - formData, - height, - hooks, - legendState = {}, - queriesData, - theme, - width, - } = chartProps; - const { onLegendStateChanged } = hooks; - const { - colorScheme, - column, - groupby = [], - normalize, - showLegend, - showValue, - sliceId, - xAxisFormat, - xAxisTitle, - yAxisTitle, - yAxisFormat, - } = formData; - const { data } = queriesData[0]; - const colorFn = CategoricalColorNamespace.getScale(colorScheme); - - const formatter = (format: string) => - getValueFormatter( - column, - currencyFormats, - columnFormats, - format, - undefined, - ); - const xAxisFormatter = formatter(xAxisFormat); - const yAxisFormatter = formatter(yAxisFormat); - - const percentFormatter = getPercentFormatter(NumberFormats.PERCENT_2_POINT); - const groupbySet = new Set(groupby); - const xAxisData: string[] = Object.keys(data[0]) - .filter(key => !groupbySet.has(key)) - .map(key => { - const array = key.split(' - ').map(value => parseFloat(value)); - return `${xAxisFormatter(array[0])} - ${xAxisFormatter(array[1])}`; - }); - const barSeries: BarSeriesOption[] = data.map(datum => { - const seriesName = - groupby.length > 0 - ? groupby.map(key => datum[getColumnLabel(key)]).join(', ') - : getColumnLabel(column); - const seriesData = Object.keys(datum) - .filter(key => groupbySet.has(key) === false) - .map(key => datum[key] as number); - return { - name: seriesName, - type: 'bar', - data: seriesData, - itemStyle: { - color: colorFn(seriesName, sliceId), - }, - label: { - show: showValue, - position: 'top', - formatter: params => { - const { value } = params; - return yAxisFormatter.format(value as number); - }, - }, - }; - }); - - const legendOptions = barSeries.map(series => series.name as string); - if (isEmpty(legendState)) { - legendOptions.forEach(legend => { - legendState[legend] = true; - }); - } - - const tooltipFormatter = (params: CallbackDataParams[]) => { - const title = params[0].name; - const rows = params.map(param => { - const { marker, seriesName, value } = param; - return [`${marker}${seriesName}`, yAxisFormatter.format(value as number)]; - }); - if (groupby.length > 0) { - const total = params.reduce( - (acc, param) => acc + (param.value as number), - 0, - ); - if (!normalize) { - rows.forEach((row, i) => - row.push( - percentFormatter.format((params[i].value as number) / (total || 1)), - ), - ); - } - const totalRow = ['Total', yAxisFormatter.format(total)]; - if (!normalize) { - totalRow.push(percentFormatter.format(1)); - } - rows.push(totalRow); - } - return tooltipHtml(rows, title, focusedSeries); - }; - - const onFocusedSeries = (index?: number | undefined) => { - focusedSeries = index; - }; - - type EChartsOption = ComposeOption; - - const echartOptions: EChartsOption = { - grid: { - ...defaultGrid, - left: '5%', - right: '5%', - top: '10%', - bottom: '10%', - }, - xAxis: { - data: xAxisData, - name: xAxisTitle, - nameGap: 35, - type: 'category', - nameLocation: 'middle', - }, - yAxis: { - ...defaultYAxis, - name: yAxisTitle, - nameGap: normalize ? 55 : 40, - type: 'value', - nameLocation: 'middle', - axisLabel: { - formatter: (value: number) => yAxisFormatter.format(value), - }, - }, - series: barSeries, - legend: { - ...getLegendProps( - LegendType.Scroll, - LegendOrientation.Top, - showLegend, - theme, - false, - legendState, - ), - data: legendOptions, - }, - tooltip: { - ...getDefaultTooltip(refs), - trigger: 'axis', - formatter: tooltipFormatter, - }, - }; - - return { - refs, - formData, - width, - height, - echartOptions, - onFocusedSeries, - onLegendStateChanged, - }; -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Histogram/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Histogram/types.ts deleted file mode 100644 index ab3d4b75c2e..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Histogram/types.ts +++ /dev/null @@ -1,44 +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 { QueryFormColumn, QueryFormData } from '@superset-ui/core'; -import { BaseChartProps, BaseTransformedProps } from '../types'; - -export type HistogramFormData = QueryFormData & { - bins: number; - column: QueryFormColumn; - colorScheme?: string; - cumulative: boolean; - normalize: boolean; - sliceId: number; - showLegend: boolean; - showValue: boolean; - xAxisFormat: string; - xAxisTitle: string; - yAxisFormat: string; - yAxisTitle: string; -}; - -export interface HistogramChartProps extends BaseChartProps { - formData: HistogramFormData; -} - -export type HistogramTransformedProps = - BaseTransformedProps & { - onFocusedSeries: (index: number | undefined) => void; - }; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/EchartsMixedTimeseries.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/EchartsMixedTimeseries.tsx deleted file mode 100644 index 05e87dacd16..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/EchartsMixedTimeseries.tsx +++ /dev/null @@ -1,220 +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 { useCallback } from 'react'; -import { - AxisType, - BinaryQueryObjectFilterClause, - DTTM_ALIAS, - DataRecordValue, - getColumnLabel, - getNumberFormatter, - getTimeFormatter, -} from '@superset-ui/core'; -import { EchartsMixedTimeseriesChartTransformedProps } from './types'; -import Echart from '../components/Echart'; -import { EventHandlers } from '../types'; -import { formatSeriesName } from '../utils/series'; - -export default function EchartsMixedTimeseries({ - height, - width, - echartOptions, - setDataMask, - labelMap, - labelMapB, - groupby, - groupbyB, - selectedValues, - formData, - emitCrossFilters, - seriesBreakdown, - onContextMenu, - onFocusedSeries, - xValueFormatter, - xAxis, - refs, - coltypeMapping, -}: EchartsMixedTimeseriesChartTransformedProps) { - const isFirstQuery = useCallback( - (seriesIndex: number) => seriesIndex < seriesBreakdown, - [seriesBreakdown], - ); - - const getCrossFilterDataMask = useCallback( - (seriesName: string, seriesIndex: number) => { - const selected: string[] = Object.values(selectedValues || {}); - let values: string[]; - if (selected.includes(seriesName)) { - values = selected.filter(v => v !== seriesName); - } else { - values = [seriesName]; - } - - const currentGroupBy = isFirstQuery(seriesIndex) ? groupby : groupbyB; - const currentLabelMap = isFirstQuery(seriesIndex) ? labelMap : labelMapB; - const groupbyValues = values - .map(value => currentLabelMap?.[value]) - .filter(value => !!value); - - return { - dataMask: { - extraFormData: { - filters: - values.length === 0 - ? [] - : currentGroupBy.map((col, idx) => { - const val: DataRecordValue[] = groupbyValues.map( - v => v[idx], - ); - if (val === null || val === undefined) - return { - col, - op: 'IS NULL' as const, - }; - return { - col, - op: 'IN' as const, - val: val as (string | number | boolean)[], - }; - }), - }, - filterState: { - value: !groupbyValues.length ? null : groupbyValues, - selectedValues: values.length ? values : null, - }, - }, - isCurrentValueSelected: selected.includes(seriesName), - }; - }, - [groupby, groupbyB, isFirstQuery, labelMap, labelMapB, selectedValues], - ); - - const handleChange = useCallback( - (seriesName: string, seriesIndex: number) => { - const isFirst = isFirstQuery(seriesIndex); - if ( - !emitCrossFilters || - (isFirst && groupby.length === 0) || - (!isFirst && groupbyB.length === 0) - ) { - return; - } - - setDataMask(getCrossFilterDataMask(seriesName, seriesIndex).dataMask); - }, - [ - isFirstQuery, - emitCrossFilters, - groupby.length, - groupbyB.length, - setDataMask, - getCrossFilterDataMask, - ], - ); - - const eventHandlers: EventHandlers = { - click: props => { - const { seriesName, seriesIndex } = props; - handleChange(seriesName, seriesIndex); - }, - mouseout: () => { - onFocusedSeries(null); - }, - mouseover: params => { - onFocusedSeries(params.seriesName); - }, - contextmenu: async eventParams => { - if (onContextMenu) { - eventParams.event.stop(); - const { data, seriesName, seriesIndex } = eventParams; - const pointerEvent = eventParams.event.event; - const drillToDetailFilters: BinaryQueryObjectFilterClause[] = []; - const drillByFilters: BinaryQueryObjectFilterClause[] = []; - const isFirst = isFirstQuery(seriesIndex); - const values = [ - ...(eventParams.name ? [eventParams.name] : []), - ...((isFirst ? labelMap : labelMapB)[eventParams.seriesName] || []), - ]; - if (data && xAxis.type === AxisType.Time) { - drillToDetailFilters.push({ - col: - xAxis.label === DTTM_ALIAS - ? formData.granularitySqla - : xAxis.label, - grain: formData.timeGrainSqla, - op: '==', - val: data[0], - formattedVal: xValueFormatter(data[0]), - }); - } - [ - ...(data && xAxis.type === AxisType.Category ? [xAxis.label] : []), - ...(isFirst ? formData.groupby : formData.groupbyB), - ].forEach((dimension, i) => - drillToDetailFilters.push({ - col: dimension, - op: '==', - val: values[i], - formattedVal: String(values[i]), - }), - ); - - [...(isFirst ? formData.groupby : formData.groupbyB)].forEach( - (dimension, i) => - drillByFilters.push({ - col: dimension, - op: '==', - val: values[i], - formattedVal: formatSeriesName(values[i], { - timeFormatter: getTimeFormatter(formData.dateFormat), - numberFormatter: getNumberFormatter(formData.numberFormat), - coltype: coltypeMapping?.[getColumnLabel(dimension)], - }), - }), - ); - const hasCrossFilter = - (isFirst && groupby.length > 0) || (!isFirst && groupbyB.length > 0); - - onContextMenu(pointerEvent.clientX, pointerEvent.clientY, { - drillToDetail: drillToDetailFilters, - crossFilter: hasCrossFilter - ? getCrossFilterDataMask(seriesName, seriesIndex) - : undefined, - drillBy: { - filters: drillByFilters, - groupbyFieldName: isFirst ? 'groupby' : 'groupby_b', - adhocFilterFieldName: isFirst ? 'adhoc_filters' : 'adhoc_filters_b', - }, - }); - } - }, - }; - - return ( - - ); -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx deleted file mode 100644 index 1945302d031..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx +++ /dev/null @@ -1,537 +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 { t } from '@apache-superset/core/translation'; -import { ensureIsArray } from '@superset-ui/core'; -import { cloneDeep } from 'lodash'; -import { - ControlPanelsContainerProps, - ControlPanelConfig, - ControlPanelSectionConfig, - ControlSetRow, - ControlSubSectionHeader, - CustomControlItem, - getStandardizedControls, - sections, - sharedControls, - DEFAULT_SORT_SERIES_DATA, - SORT_SERIES_CHOICES, -} from '@superset-ui/chart-controls'; - -import { DEFAULT_FORM_DATA } from './types'; -import { EchartsTimeseriesSeriesType } from '../Timeseries/types'; -import { - legendSection, - minorTicks, - richTooltipSection, - truncateXAxis, - xAxisBounds, - xAxisLabelRotation, - xAxisLabelInterval, - forceMaxInterval, -} from '../controls'; - -const { - area, - logAxis, - markerEnabled, - markerSize, - minorSplitLine, - opacity, - orderDesc, - rowLimit, - seriesType, - showValues, - stack, - truncateYAxis, - yAxisBounds, - yAxisIndex, -} = DEFAULT_FORM_DATA; - -function createQuerySection( - label: string, - controlSuffix: string, -): ControlPanelSectionConfig { - return { - label, - expanded: true, - controlSetRows: [ - [ - { - name: `metrics${controlSuffix}`, - config: sharedControls.metrics, - }, - ], - [ - { - name: `groupby${controlSuffix}`, - config: sharedControls.groupby, - }, - ], - [ - { - name: `adhoc_filters${controlSuffix}`, - config: sharedControls.adhoc_filters, - }, - ], - [ - { - name: `limit${controlSuffix}`, - config: sharedControls.limit, - }, - ], - [ - { - name: `timeseries_limit_metric${controlSuffix}`, - config: sharedControls.timeseries_limit_metric, - }, - ], - [ - { - name: `order_desc${controlSuffix}`, - config: { - type: 'CheckboxControl', - label: t('Sort Descending'), - default: orderDesc, - description: t('Whether to sort descending or ascending'), - }, - }, - ], - [ - { - name: `row_limit${controlSuffix}`, - config: { - ...sharedControls.row_limit, - default: rowLimit, - }, - }, - ], - [ - { - name: `truncate_metric${controlSuffix}`, - config: { - ...sharedControls.truncate_metric, - default: sharedControls.truncate_metric.default, - }, - }, - ], - ], - }; -} - -function createCustomizeSection( - label: string, - controlSuffix: string, -): ControlSetRow[] { - return [ - [{label}], - [ - { - name: `seriesType${controlSuffix}`, - config: { - type: 'SelectControl', - label: t('Series type'), - renderTrigger: true, - default: seriesType, - choices: [ - [EchartsTimeseriesSeriesType.Line, t('Line')], - [EchartsTimeseriesSeriesType.Scatter, t('Scatter')], - [EchartsTimeseriesSeriesType.Smooth, t('Smooth Line')], - [EchartsTimeseriesSeriesType.Bar, t('Bar')], - [EchartsTimeseriesSeriesType.Start, t('Step - start')], - [EchartsTimeseriesSeriesType.Middle, t('Step - middle')], - [EchartsTimeseriesSeriesType.End, t('Step - end')], - ], - description: t('Series chart type (line, bar etc)'), - }, - }, - ], - [ - { - name: `stack${controlSuffix}`, - config: { - type: 'CheckboxControl', - label: t('Stack series'), - renderTrigger: true, - default: stack, - description: t('Stack series on top of each other'), - }, - }, - ], - [ - { - name: `area${controlSuffix}`, - config: { - type: 'CheckboxControl', - label: t('Area chart'), - renderTrigger: true, - default: area, - description: t( - 'Draw area under curves. Only applicable for line types.', - ), - }, - }, - ], - [ - { - name: `show_value${controlSuffix}`, - config: { - type: 'CheckboxControl', - label: t('Show Values'), - renderTrigger: true, - default: showValues, - description: t( - 'Whether to display the numerical values within the cells', - ), - }, - }, - ], - [ - { - name: `only_total${controlSuffix}`, - config: { - type: 'CheckboxControl', - label: t('Only Total'), - default: true, - renderTrigger: true, - description: t( - 'Only show the total value on the stacked chart, and not show on the selected category', - ), - visibility: ({ controls }: ControlPanelsContainerProps) => - Boolean(controls?.show_value?.value) && - Boolean(controls?.stack?.value), - }, - }, - ], - [ - { - name: `opacity${controlSuffix}`, - config: { - type: 'SliderControl', - label: t('Opacity'), - renderTrigger: true, - min: 0, - max: 1, - step: 0.1, - default: opacity, - description: t('Opacity of area chart.'), - }, - }, - ], - [ - { - name: `markerEnabled${controlSuffix}`, - config: { - type: 'CheckboxControl', - label: t('Marker'), - renderTrigger: true, - default: markerEnabled, - description: t( - 'Draw a marker on data points. Only applicable for line types.', - ), - }, - }, - ], - [ - { - name: `markerSize${controlSuffix}`, - config: { - type: 'SliderControl', - label: t('Marker size'), - renderTrigger: true, - min: 0, - max: 100, - default: markerSize, - description: t( - 'Size of marker. Also applies to forecast observations.', - ), - }, - }, - ], - [ - { - name: `yAxisIndex${controlSuffix}`, - config: { - type: 'SelectControl', - label: t('Y Axis'), - choices: [ - [0, t('Primary')], - [1, t('Secondary')], - ], - default: yAxisIndex, - clearable: false, - renderTrigger: true, - description: t('Primary or secondary y-axis'), - }, - }, - ], - [{t('Series Order')}], - [ - { - name: `sort_series_type${controlSuffix}`, - config: { - type: 'SelectControl', - freeForm: false, - label: t('Sort Series By'), - choices: SORT_SERIES_CHOICES, - default: DEFAULT_SORT_SERIES_DATA.sort_series_type, - renderTrigger: true, - description: t( - 'Based on what should series be ordered on the chart and legend', - ), - }, - }, - ], - [ - { - name: `sort_series_ascending${controlSuffix}`, - config: { - type: 'CheckboxControl', - label: t('Sort Series Ascending'), - default: DEFAULT_SORT_SERIES_DATA.sort_series_ascending, - renderTrigger: true, - description: t('Sort series in ascending order'), - }, - }, - ], - ]; -} - -function createAdvancedAnalyticsSection( - label: string, - controlSuffix: string, -): ControlPanelSectionConfig { - const aaWithSuffix = cloneDeep(sections.advancedAnalyticsControls); - aaWithSuffix.label = label; - if (!controlSuffix) { - return aaWithSuffix; - } - aaWithSuffix.controlSetRows.forEach(row => - row.forEach((control: CustomControlItem) => { - if (control?.name) { - // eslint-disable-next-line no-param-reassign - control.name = `${control.name}${controlSuffix}`; - } - }), - ); - return aaWithSuffix; -} - -const config: ControlPanelConfig = { - controlPanelSections: [ - { - label: t('Shared query fields'), - expanded: true, - controlSetRows: [['x_axis'], ['time_grain_sqla']], - }, - createQuerySection(t('Query A'), ''), - createAdvancedAnalyticsSection(t('Advanced analytics Query A'), ''), - createQuerySection(t('Query B'), '_b'), - createAdvancedAnalyticsSection(t('Advanced analytics Query B'), '_b'), - sections.annotationsAndLayersControls, - sections.titleControls, - { - label: t('Chart Options'), - expanded: true, - controlSetRows: [ - ['color_scheme'], - ['time_shift_color'], - ...createCustomizeSection(t('Query A'), ''), - ...createCustomizeSection(t('Query B'), 'B'), - ['zoomable'], - [minorTicks], - ...legendSection, - [{t('X Axis')}], - ['x_axis_time_format'], - [xAxisLabelRotation], - [xAxisLabelInterval], - [forceMaxInterval], - [{t('Tooltip')}], - [ - { - name: 'show_query_identifiers', - config: { - type: 'CheckboxControl', - label: t('Show query identifiers'), - description: t( - 'Add Query A and Query B identifiers to tooltips to help differentiate series', - ), - default: false, - renderTrigger: true, - }, - }, - ], - ...richTooltipSection.slice(1), // Skip the tooltip header since we added our own - // eslint-disable-next-line react/jsx-key - [{t('Y Axis')}], - [ - { - name: 'minorSplitLine', - config: { - type: 'CheckboxControl', - label: t('Minor Split Line'), - renderTrigger: true, - default: minorSplitLine, - description: t('Draw split lines for minor y-axis ticks'), - }, - }, - ], - [truncateXAxis], - [xAxisBounds], - [ - { - name: 'truncateYAxis', - config: { - type: 'CheckboxControl', - label: t('Truncate Y Axis'), - default: truncateYAxis, - renderTrigger: true, - description: t( - 'Truncate Y Axis. Can be overridden by specifying a min or max bound.', - ), - }, - }, - ], - [ - { - name: 'y_axis_bounds', - config: { - type: 'BoundsControl', - label: t('Primary y-axis Bounds'), - renderTrigger: true, - default: yAxisBounds, - description: t( - 'Bounds for the primary Y-axis. When left empty, the bounds are ' + - 'dynamically defined based on the min/max of the data. Note that ' + - "this feature will only expand the axis range. It won't " + - "narrow the data's extent.", - ), - }, - }, - ], - [ - { - name: `y_axis_format`, - config: { - ...sharedControls.y_axis_format, - label: t('Primary y-axis format'), - }, - }, - ], - ['currency_format'], - [ - { - name: 'logAxis', - config: { - type: 'CheckboxControl', - label: t('Logarithmic y-axis'), - renderTrigger: true, - default: logAxis, - description: t('Logarithmic scale on primary y-axis'), - }, - }, - ], - [ - { - name: 'y_axis_bounds_secondary', - config: { - type: 'BoundsControl', - label: t('Secondary y-axis Bounds'), - renderTrigger: true, - default: yAxisBounds, - description: t( - `Bounds for the secondary Y-axis. Only works when Independent Y-axis - bounds are enabled. When left empty, the bounds are dynamically defined - based on the min/max of the data. Note that this feature will only expand - the axis range. It won't narrow the data's extent.`, - ), - }, - }, - ], - [ - { - name: `y_axis_format_secondary`, - config: { - ...sharedControls.y_axis_format, - label: t('Secondary y-axis format'), - }, - }, - ], - [ - { - name: 'currency_format_secondary', - config: { - ...sharedControls.currency_format, - label: t('Secondary currency format'), - }, - }, - ], - [ - { - name: 'yAxisTitleSecondary', - config: { - type: 'TextControl', - label: t('Secondary y-axis title'), - renderTrigger: true, - default: '', - description: t('Logarithmic y-axis'), - }, - }, - ], - [ - { - name: 'logAxisSecondary', - config: { - type: 'CheckboxControl', - label: t('Logarithmic y-axis'), - renderTrigger: true, - default: logAxis, - description: t('Logarithmic scale on secondary y-axis'), - }, - }, - ], - ['echart_options'], - ], - }, - ], - formDataOverrides: formData => { - const groupby = getStandardizedControls().controls.columns.filter( - col => !ensureIsArray(formData.groupby_b).includes(col), - ); - getStandardizedControls().controls.columns = - getStandardizedControls().controls.columns.filter( - col => !groupby.includes(col), - ); - - const metrics = getStandardizedControls().controls.metrics.filter( - metric => !ensureIsArray(formData.metrics_b).includes(metric), - ); - getStandardizedControls().controls.metrics = - getStandardizedControls().controls.metrics.filter( - col => !metrics.includes(col), - ); - - return { - ...formData, - metrics, - groupby, - }; - }, -}; - -export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/index.ts deleted file mode 100644 index 82c5496efe0..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/index.ts +++ /dev/null @@ -1,88 +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 { t } from '@apache-superset/core/translation'; -import { AnnotationType, Behavior } from '@superset-ui/core'; -import buildQuery from './buildQuery'; -import controlPanel from './controlPanel'; -import transformProps from './transformProps'; -import thumbnail from './images/thumbnail.png'; -import thumbnailDark from './images/thumbnail-dark.png'; -import example from './images/example.jpg'; -import exampleDark from './images/example-dark.jpg'; -import { - EchartsMixedTimeseriesFormData, - EchartsMixedTimeseriesProps, -} from './types'; -import { EchartsChartPlugin } from '../types'; - -export default class EchartsTimeseriesChartPlugin extends EchartsChartPlugin< - EchartsMixedTimeseriesFormData, - EchartsMixedTimeseriesProps -> { - /** - * The constructor is used to pass relevant metadata and callbacks that get - * registered in respective registries that are used throughout the library - * and application. A more thorough description of each property is given in - * the respective imported file. - * - * It is worth noting that `buildQuery` and is optional, and only needed for - * advanced visualizations that require either post processing operations - * (pivoting, rolling aggregations, sorting etc) or submitting multiple queries. - */ - constructor() { - super({ - buildQuery, - controlPanel, - loadChart: () => import('./EchartsMixedTimeseries'), - metadata: { - behaviors: [ - Behavior.InteractiveChart, - Behavior.DrillToDetail, - Behavior.DrillBy, - ], - category: t('Evolution'), - credits: ['https://echarts.apache.org'], - description: t( - 'Visualize two different series using the same x-axis. Note that both series can be visualized with a different chart type (e.g. 1 using bars and 1 using a line).', - ), - supportedAnnotationTypes: [ - AnnotationType.Event, - AnnotationType.Formula, - AnnotationType.Interval, - AnnotationType.Timeseries, - ], - exampleGallery: [{ url: example, urlDark: exampleDark }], - name: t('Mixed Chart'), - thumbnail, - thumbnailDark, - tags: [ - t('Advanced-Analytics'), - t('ECharts'), - t('Line'), - t('Multi-Variables'), - t('Time'), - t('Transformable'), - t('Featured'), - ], - queryObjectCount: 2, - }, - transformProps, - }); - } -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/index.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/index.tsx new file mode 100644 index 00000000000..5fcde678ea7 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/index.tsx @@ -0,0 +1,1631 @@ +/** + * 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. + */ + +/** + * Mixed Timeseries Chart - Glyph Pattern Implementation + * + * Visualize two different series using the same x-axis. Note that + * both series can be visualized with a different chart type + * (e.g. 1 using bars and 1 using a line). + * + * Key characteristics: + * - Two independent queries (A and B) with separate metrics/groupby + * - Dual y-axes (primary and secondary) with independent formatting + * - Per-query series type, stack, area, markers, opacity, sort order + * - Per-query y-axis assignment (primary or secondary) + * - Custom buildQuery that splits form data by `_b` suffix + * - queryObjectCount: 2 + * - No orientation (horizontal mode not supported) + * - No ExtraControls / stack radio buttons + * - Supports cross-filtering, drill-to-detail, and drill-by + * - Supports annotations (event, formula, interval, timeseries) + */ + +import { useCallback } from 'react'; +import { invert, cloneDeep } from 'lodash'; +import { t } from '@apache-superset/core/translation'; +import { + AnnotationLayer, + AnnotationType, + AxisType, + Behavior, + BinaryQueryObjectFilterClause, + buildCustomFormatters, + CategoricalColorNamespace, + ChartProps, + CurrencyFormatter, + DataRecordValue, + DTTM_ALIAS, + ensureIsArray, + getColumnLabel, + getCustomFormatter, + getNumberFormatter, + getTimeFormatter, + getXAxisLabel, + isDefined, + isEventAnnotationLayer, + isFormulaAnnotationLayer, + isIntervalAnnotationLayer, + isPhysicalColumn, + isTimeseriesAnnotationLayer, + QueryFormData, + QueryFormMetric, + TimeseriesChartDataResponseResult, + TimeseriesDataRecord, + tooltipHtml, + ValueFormatter, +} from '@superset-ui/core'; +import { GenericDataType } from '@apache-superset/core/common'; +import { + ControlPanelSectionConfig, + ControlPanelsContainerProps, + ControlSetRow, + ControlSubSectionHeader, + CustomControlItem, + DEFAULT_SORT_SERIES_DATA, + getOriginalSeries, + getStandardizedControls, + sections, + sharedControls, + SORT_SERIES_CHOICES, +} from '@superset-ui/chart-controls'; +import type { EChartsCoreOption } from 'echarts/core'; +import type { SeriesOption } from 'echarts'; + +import { defineChart } from '@superset-ui/glyph-core'; +import { + EchartsTimeseriesSeriesType, + ForecastSeriesEnum, + Refs, +} from '../types'; +import { EventHandlers } from '../types'; +import Echart from '../components/Echart'; +import { formatSeriesName } from '../utils/series'; +import { parseAxisBound } from '../utils/controls'; +import { + dedupSeries, + extractDataTotalValues, + extractSeries, + extractShowValueIndexes, + extractTooltipKeys, + getAxisType, + getColtypesMapping, + getLegendProps, + getMinAndMaxFromBounds, + getOverMaxHiddenFormatter, +} from '../utils/series'; +import { + extractAnnotationLabels, + getAnnotationData, +} from '../utils/annotation'; +import { + extractForecastSeriesContext, + extractForecastValuesFromTooltipParams, + formatForecastTooltipSeries, + rebaseForecastDatum, + reorderForecastSeries, +} from '../utils/forecast'; +import { convertInteger } from '../utils/convertInteger'; +import { defaultGrid, defaultYAxis } from '../defaults'; +import { + getPadding, + transformEventAnnotation, + transformFormulaAnnotation, + transformIntervalAnnotation, + transformSeries, + transformTimeseriesAnnotation, +} from '../Timeseries/transformers'; +import { TIMEGRAIN_TO_TIMESTAMP, TIMESERIES_CONSTANTS } from '../constants'; +import { getDefaultTooltip } from '../utils/tooltip'; +import { + getTooltipTimeFormatter, + getXAxisFormatter, + getYAxisFormatter, +} from '../utils/formatters'; +import { getMetricDisplayName } from '../utils/metricDisplayName'; +import { + DEFAULT_FORM_DATA, + EchartsMixedTimeseriesChartTransformedProps, + EchartsMixedTimeseriesFormData, + EchartsMixedTimeseriesProps, +} from './types'; +import buildQuery from './buildQuery'; +import { + legendSection, + minorTicks, + richTooltipSection, + truncateXAxis, + xAxisBounds, + xAxisLabelRotation, + xAxisLabelInterval, + forceMaxInterval, +} from '../controls'; + +import thumbnail from './images/thumbnail.png'; +import thumbnailDark from './images/thumbnail-dark.png'; +import example from './images/example.jpg'; +import exampleDark from './images/example-dark.jpg'; + +// ============================================================================ +// Types +// ============================================================================ + +interface MixedTransformResult { + transformedProps: EchartsMixedTimeseriesChartTransformedProps; +} + +// ============================================================================ +// Constants +// ============================================================================ + +const { + area, + logAxis, + markerEnabled, + markerSize, + minorSplitLine, + opacity, + orderDesc, + rowLimit, + seriesType, + showValue, + stack, + truncateYAxis, + yAxisBounds, + yAxisIndex, +} = DEFAULT_FORM_DATA; + +// ============================================================================ +// Control Panel Helpers +// ============================================================================ + +function createQuerySection( + label: string, + controlSuffix: string, +): ControlPanelSectionConfig { + return { + label, + expanded: true, + controlSetRows: [ + [ + { + name: `metrics${controlSuffix}`, + config: sharedControls.metrics, + }, + ], + [ + { + name: `groupby${controlSuffix}`, + config: sharedControls.groupby, + }, + ], + [ + { + name: `adhoc_filters${controlSuffix}`, + config: sharedControls.adhoc_filters, + }, + ], + [ + { + name: `limit${controlSuffix}`, + config: sharedControls.limit, + }, + ], + [ + { + name: `timeseries_limit_metric${controlSuffix}`, + config: sharedControls.timeseries_limit_metric, + }, + ], + [ + { + name: `order_desc${controlSuffix}`, + config: { + type: 'CheckboxControl', + label: t('Sort Descending'), + default: orderDesc, + description: t('Whether to sort descending or ascending'), + }, + }, + ], + [ + { + name: `row_limit${controlSuffix}`, + config: { + ...sharedControls.row_limit, + default: rowLimit, + }, + }, + ], + [ + { + name: `truncate_metric${controlSuffix}`, + config: { + ...sharedControls.truncate_metric, + default: sharedControls.truncate_metric.default, + }, + }, + ], + ], + }; +} + +function createCustomizeSection( + label: string, + controlSuffix: string, +): ControlSetRow[] { + return [ + [{label}], + [ + { + name: `seriesType${controlSuffix}`, + config: { + type: 'SelectControl', + label: t('Series type'), + renderTrigger: true, + default: seriesType, + choices: [ + [EchartsTimeseriesSeriesType.Line, t('Line')], + [EchartsTimeseriesSeriesType.Scatter, t('Scatter')], + [EchartsTimeseriesSeriesType.Smooth, t('Smooth Line')], + [EchartsTimeseriesSeriesType.Bar, t('Bar')], + [EchartsTimeseriesSeriesType.Start, t('Step - start')], + [EchartsTimeseriesSeriesType.Middle, t('Step - middle')], + [EchartsTimeseriesSeriesType.End, t('Step - end')], + ], + description: t('Series chart type (line, bar etc)'), + }, + }, + ], + [ + { + name: `stack${controlSuffix}`, + config: { + type: 'CheckboxControl', + label: t('Stack series'), + renderTrigger: true, + default: stack, + description: t('Stack series on top of each other'), + }, + }, + ], + [ + { + name: `area${controlSuffix}`, + config: { + type: 'CheckboxControl', + label: t('Area chart'), + renderTrigger: true, + default: area, + description: t( + 'Draw area under curves. Only applicable for line types.', + ), + }, + }, + ], + [ + { + name: `show_value${controlSuffix}`, + config: { + type: 'CheckboxControl', + label: t('Show Values'), + renderTrigger: true, + default: showValue, + description: t( + 'Whether to display the numerical values within the cells', + ), + }, + }, + ], + [ + { + name: `only_total${controlSuffix}`, + config: { + type: 'CheckboxControl', + label: t('Only Total'), + default: true, + renderTrigger: true, + description: t( + 'Only show the total value on the stacked chart, and not show on the selected category', + ), + visibility: ({ controls }: ControlPanelsContainerProps) => + Boolean(controls?.show_value?.value) && + Boolean(controls?.stack?.value), + }, + }, + ], + [ + { + name: `opacity${controlSuffix}`, + config: { + type: 'SliderControl', + label: t('Opacity'), + renderTrigger: true, + min: 0, + max: 1, + step: 0.1, + default: opacity, + description: t('Opacity of area chart.'), + }, + }, + ], + [ + { + name: `markerEnabled${controlSuffix}`, + config: { + type: 'CheckboxControl', + label: t('Marker'), + renderTrigger: true, + default: markerEnabled, + description: t( + 'Draw a marker on data points. Only applicable for line types.', + ), + }, + }, + ], + [ + { + name: `markerSize${controlSuffix}`, + config: { + type: 'SliderControl', + label: t('Marker size'), + renderTrigger: true, + min: 0, + max: 100, + default: markerSize, + description: t( + 'Size of marker. Also applies to forecast observations.', + ), + }, + }, + ], + [ + { + name: `yAxisIndex${controlSuffix}`, + config: { + type: 'SelectControl', + label: t('Y Axis'), + choices: [ + [0, t('Primary')], + [1, t('Secondary')], + ], + default: yAxisIndex, + clearable: false, + renderTrigger: true, + description: t('Primary or secondary y-axis'), + }, + }, + ], + [{t('Series Order')}], + [ + { + name: `sort_series_type${controlSuffix}`, + config: { + type: 'SelectControl', + freeForm: false, + label: t('Sort Series By'), + choices: SORT_SERIES_CHOICES, + default: DEFAULT_SORT_SERIES_DATA.sort_series_type, + renderTrigger: true, + description: t( + 'Based on what should series be ordered on the chart and legend', + ), + }, + }, + ], + [ + { + name: `sort_series_ascending${controlSuffix}`, + config: { + type: 'CheckboxControl', + label: t('Sort Series Ascending'), + default: DEFAULT_SORT_SERIES_DATA.sort_series_ascending, + renderTrigger: true, + description: t('Sort series in ascending order'), + }, + }, + ], + ]; +} + +function createAdvancedAnalyticsSection( + label: string, + controlSuffix: string, +): ControlPanelSectionConfig { + const aaWithSuffix = cloneDeep(sections.advancedAnalyticsControls); + aaWithSuffix.label = label; + if (!controlSuffix) { + return aaWithSuffix; + } + aaWithSuffix.controlSetRows.forEach(row => + row.forEach((control: CustomControlItem) => { + if (control?.name) { + // eslint-disable-next-line no-param-reassign + control.name = `${control.name}${controlSuffix}`; + } + }), + ); + return aaWithSuffix; +} + +// ============================================================================ +// Transform Helpers +// ============================================================================ + +const getFormatter = ( + customFormatters: Record, + defaultFormatter: ValueFormatter, + metrics: QueryFormMetric[], + formatterKey: string, + forcePercentFormat: boolean, +) => { + if (forcePercentFormat) { + return getNumberFormatter(',.0%'); + } + return ( + getCustomFormatter(customFormatters, metrics, formatterKey) ?? + defaultFormatter + ); +}; + +// ============================================================================ +// Transform Function +// ============================================================================ + +function transformMixedProps( + chartProps: EchartsMixedTimeseriesProps, +): EchartsMixedTimeseriesChartTransformedProps { + const { + width, + height, + formData, + queriesData, + hooks, + filterState, + datasource, + theme, + inContextMenu, + emitCrossFilters, + legendState, + } = chartProps; + + let focusedSeries: string | null = null; + + const { + verboseMap = {}, + currencyFormats = {}, + columnFormats = {}, + } = datasource; + const { label_map: labelMap } = + queriesData[0] as TimeseriesChartDataResponseResult; + const { label_map: labelMapB } = + queriesData[1] as TimeseriesChartDataResponseResult; + const data1 = (queriesData[0].data || []) as TimeseriesDataRecord[]; + const data2 = (queriesData[1].data || []) as TimeseriesDataRecord[]; + const annotationData = getAnnotationData(chartProps); + const coltypeMapping = { + ...getColtypesMapping(queriesData[0]), + ...getColtypesMapping(queriesData[1]), + }; + const { + area, + areaB, + annotationLayers, + colorScheme, + timeShiftColor, + contributionMode, + legendOrientation, + legendMargin, + legendType, + legendSort, + logAxis, + logAxisSecondary, + markerEnabled, + markerEnabledB, + markerSize, + markerSizeB, + opacity, + opacityB, + minorSplitLine, + minorTicks, + seriesType, + seriesTypeB, + showLegend, + showValue, + showValueB, + onlyTotal, + onlyTotalB, + stack, + stackB, + truncateXAxis, + truncateYAxis, + tooltipTimeFormat, + yAxisFormat, + currencyFormat, + yAxisFormatSecondary, + currencyFormatSecondary, + xAxisTimeFormat, + yAxisBounds, + yAxisBoundsSecondary, + yAxisIndex, + yAxisIndexB, + yAxisTitleSecondary, + zoomable, + richTooltip, + tooltipSortByMetric, + xAxisBounds, + xAxisLabelRotation, + xAxisLabelInterval, + groupby, + groupbyB, + xAxis: xAxisOrig, + xAxisForceCategorical, + xAxisTitle, + yAxisTitle, + xAxisTitleMargin, + yAxisTitleMargin, + yAxisTitlePosition, + sliceId, + sortSeriesType, + sortSeriesTypeB, + sortSeriesAscending, + sortSeriesAscendingB, + timeGrainSqla, + forceMaxInterval, + percentageThreshold, + showQueryIdentifiers = false, + metrics = [], + metricsB = [], + }: EchartsMixedTimeseriesFormData = { ...DEFAULT_FORM_DATA, ...formData }; + + const refs: Refs = {}; + const colorScale = CategoricalColorNamespace.getScale(colorScheme as string); + + let xAxisLabel = getXAxisLabel( + chartProps.rawFormData as QueryFormData, + ) as string; + if ( + isPhysicalColumn(chartProps.rawFormData?.x_axis) && + isDefined(verboseMap[xAxisLabel]) + ) { + xAxisLabel = verboseMap[xAxisLabel]; + } + + const rebasedDataA = rebaseForecastDatum(data1, verboseMap); + const { totalStackedValues, thresholdValues } = extractDataTotalValues( + rebasedDataA, + { + stack, + percentageThreshold, + xAxisCol: xAxisLabel, + }, + ); + + const MetricDisplayNameA = getMetricDisplayName(metrics[0], verboseMap); + const MetricDisplayNameB = getMetricDisplayName(metricsB[0], verboseMap); + + const dataTypes = getColtypesMapping(queriesData[0]); + const xAxisDataType = dataTypes?.[xAxisLabel] ?? dataTypes?.[xAxisOrig]; + const xAxisType = getAxisType(stack, xAxisForceCategorical, xAxisDataType); + + const [rawSeriesA, sortedTotalValuesA] = extractSeries(rebasedDataA, { + fillNeighborValue: stack ? 0 : undefined, + xAxis: xAxisLabel, + sortSeriesType, + sortSeriesAscending, + stack, + totalStackedValues, + xAxisType, + }); + const rebasedDataB = rebaseForecastDatum(data2, verboseMap); + const { + totalStackedValues: totalStackedValuesB, + thresholdValues: thresholdValuesB, + } = extractDataTotalValues(rebasedDataB, { + stack: Boolean(stackB), + percentageThreshold, + xAxisCol: xAxisLabel, + }); + const [rawSeriesB, sortedTotalValuesB] = extractSeries(rebasedDataB, { + fillNeighborValue: stackB ? 0 : undefined, + xAxis: xAxisLabel, + sortSeriesType: sortSeriesTypeB, + sortSeriesAscending: sortSeriesAscendingB, + stack: Boolean(stackB), + totalStackedValues: totalStackedValuesB, + xAxisType, + }); + const series: SeriesOption[] = []; + const formatter = contributionMode + ? getNumberFormatter(',.0%') + : currencyFormat?.symbol + ? new CurrencyFormatter({ + d3Format: yAxisFormat, + currency: currencyFormat, + }) + : getNumberFormatter(yAxisFormat); + const formatterSecondary = contributionMode + ? getNumberFormatter(',.0%') + : currencyFormatSecondary?.symbol + ? new CurrencyFormatter({ + d3Format: yAxisFormatSecondary, + currency: currencyFormatSecondary, + }) + : getNumberFormatter(yAxisFormatSecondary); + const customFormatters = buildCustomFormatters( + [...ensureIsArray(metrics), ...ensureIsArray(metricsB)], + currencyFormats, + columnFormats, + yAxisFormat, + currencyFormat, + ); + const customFormattersSecondary = buildCustomFormatters( + [...ensureIsArray(metrics), ...ensureIsArray(metricsB)], + currencyFormats, + columnFormats, + yAxisFormatSecondary, + currencyFormatSecondary, + ); + + const primarySeries = new Set(); + const secondarySeries = new Set(); + const mapSeriesIdToAxis = ( + seriesOption: SeriesOption, + index?: number, + ): void => { + if (index === 1) { + secondarySeries.add(seriesOption.id as string); + } else { + primarySeries.add(seriesOption.id as string); + } + }; + const showValueIndexesA = extractShowValueIndexes(rawSeriesA, { + stack, + onlyTotal, + }); + const showValueIndexesB = extractShowValueIndexes(rawSeriesB, { + stack, + onlyTotal, + }); + + annotationLayers + .filter((layer: AnnotationLayer) => layer.show) + .forEach((layer: AnnotationLayer) => { + if (isFormulaAnnotationLayer(layer)) + series.push( + transformFormulaAnnotation( + layer, + data1, + xAxisLabel, + xAxisType, + colorScale, + sliceId, + ), + ); + else if (isIntervalAnnotationLayer(layer)) { + series.push( + ...transformIntervalAnnotation( + layer, + data1, + annotationData, + colorScale, + theme, + sliceId, + ), + ); + } else if (isEventAnnotationLayer(layer)) { + series.push( + ...transformEventAnnotation( + layer, + data1, + annotationData, + colorScale, + theme, + sliceId, + ), + ); + } else if (isTimeseriesAnnotationLayer(layer)) { + series.push( + ...transformTimeseriesAnnotation( + layer, + markerSize, + data1, + annotationData, + colorScale, + sliceId, + ), + ); + } + }); + + const [xAxisMin, xAxisMax] = (xAxisBounds || []).map(parseAxisBound); + let [yAxisMin, yAxisMax] = (yAxisBounds || []).map(parseAxisBound); + let [minSecondary, maxSecondary] = (yAxisBoundsSecondary || []).map( + parseAxisBound, + ); + + const array = ensureIsArray(chartProps.rawFormData?.time_compare); + const inverted = invert(verboseMap); + + rawSeriesA.forEach(entry => { + const entryName = String(entry.name || ''); + const seriesName = inverted[entryName] || entryName; + const colorScaleKey = getOriginalSeries(seriesName, array); + + let displayName: string; + + if (groupby.length > 0) { + const metricPart = showQueryIdentifiers + ? `${MetricDisplayNameA} (Query A)` + : MetricDisplayNameA; + displayName = `${metricPart}, ${entryName}`; + } else { + displayName = showQueryIdentifiers ? `${entryName} (Query A)` : entryName; + } + + const seriesFormatter = getFormatter( + customFormatters, + formatter, + metrics, + labelMap?.[seriesName]?.[0], + !!contributionMode, + ); + + const transformedSeries = transformSeries( + { + ...entry, + id: `${displayName || ''}`, + name: `${displayName || ''}`, + }, + colorScale, + colorScaleKey, + { + area, + markerEnabled, + markerSize, + areaOpacity: opacity, + seriesType, + showValue, + onlyTotal, + stack: Boolean(stack), + stackIdSuffix: '\na', + yAxisIndex, + filterState, + seriesKey: entry.name, + sliceId, + queryIndex: 0, + formatter: + seriesType === EchartsTimeseriesSeriesType.Bar + ? getOverMaxHiddenFormatter({ + max: yAxisMax, + formatter: seriesFormatter, + }) + : seriesFormatter, + totalStackedValues: sortedTotalValuesA, + showValueIndexes: showValueIndexesA, + thresholdValues, + timeShiftColor, + theme, + }, + ); + + if (transformedSeries) { + series.push(transformedSeries); + mapSeriesIdToAxis(transformedSeries, yAxisIndex); + } + }); + + rawSeriesB.forEach(entry => { + const entryName = String(entry.name || ''); + const seriesEntry = inverted[entryName] || entryName; + const colorScaleKey = getOriginalSeries(seriesEntry, array); + + let displayName: string; + + if (groupbyB.length > 0) { + const metricPart = showQueryIdentifiers + ? `${MetricDisplayNameB} (Query B)` + : MetricDisplayNameB; + displayName = `${metricPart}, ${entryName}`; + } else { + displayName = showQueryIdentifiers ? `${entryName} (Query B)` : entryName; + } + + const seriesFormatter = getFormatter( + customFormattersSecondary, + formatterSecondary, + metricsB, + labelMapB?.[seriesEntry]?.[0], + !!contributionMode, + ); + + const transformedSeries = transformSeries( + { + ...entry, + id: `${displayName || ''}`, + name: `${displayName || ''}`, + }, + + colorScale, + colorScaleKey, + { + area: areaB, + markerEnabled: markerEnabledB, + markerSize: markerSizeB, + areaOpacity: opacityB, + seriesType: seriesTypeB, + showValue: showValueB, + onlyTotal: onlyTotalB, + stack: Boolean(stackB), + stackIdSuffix: '\nb', + yAxisIndex: yAxisIndexB, + filterState, + seriesKey: entry.name, + sliceId, + queryIndex: 1, + formatter: + seriesTypeB === EchartsTimeseriesSeriesType.Bar + ? getOverMaxHiddenFormatter({ + max: maxSecondary, + formatter: seriesFormatter, + }) + : seriesFormatter, + totalStackedValues: sortedTotalValuesB, + showValueIndexes: showValueIndexesB, + thresholdValues: thresholdValuesB, + timeShiftColor, + theme, + }, + ); + + if (transformedSeries) { + series.push(transformedSeries); + mapSeriesIdToAxis(transformedSeries, yAxisIndexB); + } + }); + + if (contributionMode === 'row' && stack) { + if (yAxisMin === undefined) yAxisMin = 0; + if (yAxisMax === undefined) yAxisMax = 1; + if (minSecondary === undefined) minSecondary = 0; + if (maxSecondary === undefined) maxSecondary = 1; + } + + const tooltipFormatter = + xAxisDataType === GenericDataType.Temporal + ? getTooltipTimeFormatter(tooltipTimeFormat) + : String; + const xAxisFormatter = + xAxisDataType === GenericDataType.Temporal + ? getXAxisFormatter(xAxisTimeFormat) + : String; + + const addYAxisTitleOffset = !!(yAxisTitle || yAxisTitleSecondary); + const addXAxisTitleOffset = !!xAxisTitle; + + const chartPadding = getPadding( + showLegend, + legendOrientation, + addYAxisTitleOffset, + zoomable, + legendMargin, + addXAxisTitleOffset, + yAxisTitlePosition, + convertInteger(yAxisTitleMargin), + convertInteger(xAxisTitleMargin), + ); + + const { setDataMask = () => {}, onContextMenu } = hooks; + const alignTicks = yAxisIndex !== yAxisIndexB; + + const echartOptions: EChartsCoreOption = { + useUTC: true, + grid: { + ...defaultGrid, + ...chartPadding, + }, + xAxis: { + type: xAxisType, + name: xAxisTitle, + nameGap: convertInteger(xAxisTitleMargin), + nameLocation: 'middle', + axisLabel: { + formatter: xAxisFormatter, + rotate: xAxisLabelRotation, + interval: xAxisLabelInterval, + }, + minorTick: { show: minorTicks }, + minInterval: + xAxisType === AxisType.Time && timeGrainSqla && !forceMaxInterval + ? TIMEGRAIN_TO_TIMESTAMP[ + timeGrainSqla as keyof typeof TIMEGRAIN_TO_TIMESTAMP + ] + : 0, + maxInterval: + xAxisType === AxisType.Time && timeGrainSqla && forceMaxInterval + ? TIMEGRAIN_TO_TIMESTAMP[ + timeGrainSqla as keyof typeof TIMEGRAIN_TO_TIMESTAMP + ] + : undefined, + ...getMinAndMaxFromBounds( + xAxisType, + truncateXAxis, + xAxisMin, + xAxisMax, + seriesType === EchartsTimeseriesSeriesType.Bar || + seriesTypeB === EchartsTimeseriesSeriesType.Bar + ? EchartsTimeseriesSeriesType.Bar + : undefined, + ), + }, + yAxis: [ + { + ...defaultYAxis, + type: logAxis ? 'log' : 'value', + min: yAxisMin, + max: yAxisMax, + minorTick: { show: minorTicks }, + minorSplitLine: { show: minorSplitLine }, + axisLabel: { + formatter: getYAxisFormatter( + metrics, + !!contributionMode, + customFormatters, + formatter, + yAxisFormat, + ), + }, + scale: truncateYAxis, + name: yAxisTitle, + nameGap: convertInteger(yAxisTitleMargin), + nameLocation: yAxisTitlePosition === 'Left' ? 'middle' : 'end', + alignTicks, + }, + { + ...defaultYAxis, + type: logAxisSecondary ? 'log' : 'value', + min: minSecondary, + max: maxSecondary, + minorTick: { show: minorTicks }, + splitLine: { show: false }, + minorSplitLine: { show: minorSplitLine }, + axisLabel: { + formatter: getYAxisFormatter( + metricsB, + !!contributionMode, + customFormattersSecondary, + formatterSecondary, + yAxisFormatSecondary, + ), + }, + scale: truncateYAxis, + name: yAxisTitleSecondary, + alignTicks, + }, + ], + tooltip: { + ...getDefaultTooltip(refs), + show: !inContextMenu, + trigger: richTooltip ? 'axis' : 'item', + formatter: (params: any) => { + const xValue: number = richTooltip + ? params[0].value[0] + : params.value[0]; + const forecastValue: any[] = richTooltip ? params : [params]; + + const sortedKeys = extractTooltipKeys( + forecastValue, + 1, + richTooltip, + tooltipSortByMetric, + ); + + const rows: string[][] = []; + const forecastValues = + extractForecastValuesFromTooltipParams(forecastValue); + + const keys = Object.keys(forecastValues); + let focusedRow; + sortedKeys + .filter(key => keys.includes(key)) + .forEach(key => { + const value = forecastValues[key]; + let formatterKey; + if (primarySeries.has(key)) { + formatterKey = + groupby.length === 0 ? inverted[key] : labelMap[key]?.[0]; + } else { + formatterKey = + groupbyB.length === 0 ? inverted[key] : labelMapB[key]?.[0]; + } + const tooltipFormatter = getFormatter( + customFormatters, + formatter, + metrics, + formatterKey, + !!contributionMode, + ); + const tooltipFormatterSecondary = getFormatter( + customFormattersSecondary, + formatterSecondary, + metricsB, + formatterKey, + !!contributionMode, + ); + const row = formatForecastTooltipSeries({ + ...value, + seriesName: key, + formatter: primarySeries.has(key) + ? tooltipFormatter + : tooltipFormatterSecondary, + }); + rows.push(row); + if (key === focusedSeries) { + focusedRow = rows.length - 1; + } + }); + return tooltipHtml(rows, tooltipFormatter(xValue), focusedRow); + }, + }, + legend: { + ...getLegendProps( + legendType, + legendOrientation, + showLegend, + theme, + zoomable, + legendState, + chartPadding, + ), + // @ts-ignore + data: series + .filter( + entry => + extractForecastSeriesContext((entry.name || '') as string).type === + ForecastSeriesEnum.Observation, + ) + .map(entry => entry.id || entry.name || '') + .concat(extractAnnotationLabels(annotationLayers)) + .sort((a: string, b: string) => { + if (!legendSort) return 0; + return legendSort === 'asc' ? a.localeCompare(b) : b.localeCompare(a); + }), + }, + series: dedupSeries(reorderForecastSeries(series) as SeriesOption[]), + toolbox: { + show: zoomable, + top: TIMESERIES_CONSTANTS.toolboxTop, + right: TIMESERIES_CONSTANTS.toolboxRight, + feature: { + dataZoom: { + yAxisIndex: false, + title: { + zoom: 'zoom area', + back: 'restore zoom', + }, + }, + }, + }, + dataZoom: zoomable + ? [ + { + type: 'slider', + start: TIMESERIES_CONSTANTS.dataZoomStart, + end: TIMESERIES_CONSTANTS.dataZoomEnd, + bottom: TIMESERIES_CONSTANTS.zoomBottom, + }, + ] + : [], + }; + + const onFocusedSeries = (seriesName: string | null) => { + focusedSeries = seriesName; + }; + + return { + formData, + width, + height, + echartOptions, + setDataMask, + emitCrossFilters, + labelMap, + labelMapB, + groupby, + groupbyB, + seriesBreakdown: rawSeriesA.length, + selectedValues: filterState.selectedValues || [], + onContextMenu, + onFocusedSeries, + xValueFormatter: tooltipFormatter, + xAxis: { + label: xAxisLabel, + type: xAxisType, + }, + refs, + coltypeMapping, + }; +} + +// ============================================================================ +// Render Component +// ============================================================================ + +function MixedRender({ + transformedProps, +}: { + transformedProps: EchartsMixedTimeseriesChartTransformedProps; +}) { + const { + height, + width, + echartOptions, + setDataMask, + labelMap, + labelMapB, + groupby, + groupbyB, + selectedValues, + formData, + emitCrossFilters, + seriesBreakdown, + onContextMenu, + onFocusedSeries, + xValueFormatter, + xAxis, + refs, + coltypeMapping, + } = transformedProps; + + const isFirstQuery = useCallback( + (seriesIndex: number) => seriesIndex < seriesBreakdown, + [seriesBreakdown], + ); + + const getCrossFilterDataMask = useCallback( + (seriesName: string, seriesIndex: number) => { + const selected: string[] = Object.values(selectedValues || {}); + let values: string[]; + if (selected.includes(seriesName)) { + values = selected.filter(v => v !== seriesName); + } else { + values = [seriesName]; + } + + const currentGroupBy = isFirstQuery(seriesIndex) ? groupby : groupbyB; + const currentLabelMap = isFirstQuery(seriesIndex) ? labelMap : labelMapB; + const groupbyValues = values + .map(value => currentLabelMap?.[value]) + .filter(value => !!value); + + return { + dataMask: { + extraFormData: { + // @ts-ignore + filters: + values.length === 0 + ? [] + : currentGroupBy.map((col, idx) => { + const val: DataRecordValue[] = groupbyValues.map( + v => v[idx], + ); + if (val === null || val === undefined) + return { + col, + op: 'IS NULL' as const, + }; + return { + col, + op: 'IN' as const, + val: val as (string | number | boolean)[], + }; + }), + }, + filterState: { + value: !groupbyValues.length ? null : groupbyValues, + selectedValues: values.length ? values : null, + }, + }, + isCurrentValueSelected: selected.includes(seriesName), + }; + }, + [groupby, groupbyB, isFirstQuery, labelMap, labelMapB, selectedValues], + ); + + const handleChange = useCallback( + (seriesName: string, seriesIndex: number) => { + const isFirst = isFirstQuery(seriesIndex); + if ( + !emitCrossFilters || + (isFirst && groupby.length === 0) || + (!isFirst && groupbyB.length === 0) + ) { + return; + } + + setDataMask(getCrossFilterDataMask(seriesName, seriesIndex).dataMask); + }, + [ + isFirstQuery, + emitCrossFilters, + groupby.length, + groupbyB.length, + setDataMask, + getCrossFilterDataMask, + ], + ); + + const eventHandlers: EventHandlers = { + click: props => { + const { seriesName, seriesIndex } = props; + handleChange(seriesName, seriesIndex); + }, + mouseout: () => { + onFocusedSeries(null); + }, + mouseover: params => { + onFocusedSeries(params.seriesName); + }, + contextmenu: async eventParams => { + if (onContextMenu) { + eventParams.event.stop(); + const { data, seriesName, seriesIndex } = eventParams; + const pointerEvent = eventParams.event.event; + const drillToDetailFilters: BinaryQueryObjectFilterClause[] = []; + const drillByFilters: BinaryQueryObjectFilterClause[] = []; + const isFirst = isFirstQuery(seriesIndex); + const values = [ + ...(eventParams.name ? [eventParams.name] : []), + ...((isFirst ? labelMap : labelMapB)[eventParams.seriesName] || []), + ]; + if (data && xAxis.type === AxisType.Time) { + drillToDetailFilters.push({ + col: + xAxis.label === DTTM_ALIAS + ? formData.granularitySqla + : xAxis.label, + grain: formData.timeGrainSqla, + op: '==', + val: data[0], + formattedVal: xValueFormatter(data[0]), + }); + } + [ + ...(data && xAxis.type === AxisType.Category ? [xAxis.label] : []), + ...(isFirst ? formData.groupby : formData.groupbyB), + ].forEach((dimension, i) => + drillToDetailFilters.push({ + col: dimension, + op: '==', + val: values[i], + formattedVal: String(values[i]), + }), + ); + + [...(isFirst ? formData.groupby : formData.groupbyB)].forEach( + (dimension, i) => + drillByFilters.push({ + col: dimension, + op: '==', + val: values[i], + formattedVal: formatSeriesName(values[i], { + timeFormatter: getTimeFormatter(formData.dateFormat), + numberFormatter: getNumberFormatter(formData.numberFormat), + coltype: coltypeMapping?.[getColumnLabel(dimension)], + }), + }), + ); + const hasCrossFilter = + (isFirst && groupby.length > 0) || (!isFirst && groupbyB.length > 0); + + onContextMenu(pointerEvent.clientX, pointerEvent.clientY, { + drillToDetail: drillToDetailFilters, + crossFilter: hasCrossFilter + ? getCrossFilterDataMask(seriesName, seriesIndex) + : undefined, + drillBy: { + filters: drillByFilters, + groupbyFieldName: isFirst ? 'groupby' : 'groupby_b', + adhocFilterFieldName: isFirst ? 'adhoc_filters' : 'adhoc_filters_b', + }, + }); + } + }, + }; + + return ( + + ); +} + +// ============================================================================ +// Chart Definition +// ============================================================================ + +export default defineChart({ + metadata: { + name: t('Mixed Chart'), + description: t( + 'Visualize two different series using the same x-axis. Note that both series can be visualized with a different chart type (e.g. 1 using bars and 1 using a line).', + ), + category: t('Evolution'), + tags: [ + t('Advanced-Analytics'), + t('ECharts'), + t('Line'), + t('Multi-Variables'), + t('Time'), + t('Transformable'), + t('Featured'), + ], + credits: ['https://echarts.apache.org'], + thumbnail, + thumbnailDark, + exampleGallery: [{ url: example, urlDark: exampleDark }], + behaviors: [ + Behavior.InteractiveChart, + Behavior.DrillToDetail, + Behavior.DrillBy, + ], + supportedAnnotationTypes: [ + AnnotationType.Event, + AnnotationType.Formula, + AnnotationType.Interval, + AnnotationType.Timeseries, + ], + }, + + arguments: {}, + + additionalControls: { + query: [ + // Shared query fields + ...([['x_axis'], ['time_grain_sqla']] as ControlSetRow[]), + // Query A + ...createQuerySection(t('Query A'), '').controlSetRows, + ...createAdvancedAnalyticsSection(t('Advanced analytics Query A'), '') + .controlSetRows, + // Query B + ...createQuerySection(t('Query B'), '_b').controlSetRows, + ...createAdvancedAnalyticsSection(t('Advanced analytics Query B'), '_b') + .controlSetRows, + ...sections.annotationsAndLayersControls.controlSetRows, + ], + chartOptions: [ + ...sections.titleControls.controlSetRows, + [ + + {t('Chart Options')} + , + ], + ['color_scheme'], + ['time_shift_color'], + ...createCustomizeSection(t('Query A'), ''), + ...createCustomizeSection(t('Query B'), 'B'), + ['zoomable'], + [minorTicks], + ...legendSection, + [ + + {t('X Axis')} + , + ], + ['x_axis_time_format'], + [xAxisLabelRotation], + [xAxisLabelInterval], + [forceMaxInterval], + [ + + {t('Tooltip')} + , + ], + [ + { + name: 'show_query_identifiers', + config: { + type: 'CheckboxControl', + label: t('Show query identifiers'), + description: t( + 'Add Query A and Query B identifiers to tooltips to help differentiate series', + ), + default: false, + renderTrigger: true, + }, + }, + ], + ...richTooltipSection.slice(1), + [ + + {t('Y Axis')} + , + ], + [ + { + name: 'minorSplitLine', + config: { + type: 'CheckboxControl', + label: t('Minor Split Line'), + renderTrigger: true, + default: minorSplitLine, + description: t('Draw split lines for minor y-axis ticks'), + }, + }, + ], + [truncateXAxis], + [xAxisBounds], + [ + { + name: 'truncateYAxis', + config: { + type: 'CheckboxControl', + label: t('Truncate Y Axis'), + default: truncateYAxis, + renderTrigger: true, + description: t( + 'Truncate Y Axis. Can be overridden by specifying a min or max bound.', + ), + }, + }, + ], + [ + { + name: 'y_axis_bounds', + config: { + type: 'BoundsControl', + label: t('Primary y-axis Bounds'), + renderTrigger: true, + default: yAxisBounds, + description: t( + 'Bounds for the primary Y-axis. When left empty, the bounds are ' + + 'dynamically defined based on the min/max of the data. Note that ' + + "this feature will only expand the axis range. It won't " + + "narrow the data's extent.", + ), + }, + }, + ], + [ + { + name: `y_axis_format`, + config: { + ...sharedControls.y_axis_format, + label: t('Primary y-axis format'), + }, + }, + ], + ['currency_format'], + [ + { + name: 'logAxis', + config: { + type: 'CheckboxControl', + label: t('Logarithmic y-axis'), + renderTrigger: true, + default: logAxis, + description: t('Logarithmic scale on primary y-axis'), + }, + }, + ], + [ + { + name: 'y_axis_bounds_secondary', + config: { + type: 'BoundsControl', + label: t('Secondary y-axis Bounds'), + renderTrigger: true, + default: yAxisBounds, + description: t( + `Bounds for the secondary Y-axis. Only works when Independent Y-axis + bounds are enabled. When left empty, the bounds are dynamically defined + based on the min/max of the data. Note that this feature will only expand + the axis range. It won't narrow the data's extent.`, + ), + }, + }, + ], + [ + { + name: `y_axis_format_secondary`, + config: { + ...sharedControls.y_axis_format, + label: t('Secondary y-axis format'), + }, + }, + ], + [ + { + name: 'currency_format_secondary', + config: { + ...sharedControls.currency_format, + label: t('Secondary currency format'), + }, + }, + ], + [ + { + name: 'yAxisTitleSecondary', + config: { + type: 'TextControl', + label: t('Secondary y-axis title'), + renderTrigger: true, + default: '', + description: t('Logarithmic y-axis'), + }, + }, + ], + [ + { + name: 'logAxisSecondary', + config: { + type: 'CheckboxControl', + label: t('Logarithmic y-axis'), + renderTrigger: true, + default: logAxis, + description: t('Logarithmic scale on secondary y-axis'), + }, + }, + ], + ], + }, + + formDataOverrides: formData => { + const groupby = getStandardizedControls().controls.columns.filter( + col => !ensureIsArray(formData.groupby_b).includes(col), + ); + getStandardizedControls().controls.columns = + getStandardizedControls().controls.columns.filter( + col => !groupby.includes(col), + ); + + const metrics = getStandardizedControls().controls.metrics.filter( + metric => !ensureIsArray(formData.metrics_b).includes(metric), + ); + getStandardizedControls().controls.metrics = + getStandardizedControls().controls.metrics.filter( + col => !metrics.includes(col), + ); + + return { + ...formData, + metrics, + groupby, + }; + }, + + buildQuery, + + transform: (chartProps: ChartProps): MixedTransformResult => { + const transformedProps = transformMixedProps( + chartProps as unknown as EchartsMixedTimeseriesProps, + ); + return { transformedProps }; + }, + + render: ({ transformedProps }) => ( + + ), +}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/stories/MixedSeries.stories.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/stories/MixedSeries.stories.tsx deleted file mode 100644 index cc368e48cda..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/stories/MixedSeries.stories.tsx +++ /dev/null @@ -1,334 +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 { SuperChart, getChartTransformPropsRegistry } from '@superset-ui/core'; -import { - EchartsTimeseriesChartPlugin, - MixedTimeseriesTransformProps, -} from '@superset-ui/plugin-chart-echarts'; -import data from '../../Timeseries/stories/data'; -import negativeNumData from './negativeData'; -import { withResizableChartDemo } from '@storybook-shared'; - -new EchartsTimeseriesChartPlugin() - .configure({ key: 'mixed-timeseries' }) - .register(); - -getChartTransformPropsRegistry().registerValue( - 'mixed-timeseries', - MixedTimeseriesTransformProps, -); - -export default { - title: 'Chart Plugins/plugin-chart-echarts/MixedTimeseries', - decorators: [withResizableChartDemo], -}; - -export const Timeseries = ({ - zoomable, - logAxis, - yAxisFormat, - yAxisTitle, - yAxisIndexB, - minorSplitLine, - seriesType, - stack, - area, - markerEnabled, - markerSize, - opacity, - seriesTypeB, - stackB, - areaB, - markerEnabledB, - markerSizeB, - opacityB, - width, - height, -}: { - zoomable: boolean; - logAxis: boolean; - yAxisFormat: string; - yAxisTitle: string; - yAxisIndexB: number; - minorSplitLine: boolean; - seriesType: string; - stack: boolean; - area: boolean; - markerEnabled: boolean; - markerSize: number; - opacity: number; - seriesTypeB: string; - stackB: boolean; - areaB: boolean; - markerEnabledB: boolean; - markerSizeB: number; - opacityB: number; - width: number; - height: number; -}) => { - const queriesData = [ - { - data: data - .map(row => ({ - // eslint-disable-next-line no-underscore-dangle - __timestamp: row.__timestamp, - Boston: row.Boston, - })) - .filter(row => !!row.Boston), - colnames: ['__timestamp'], - coltypes: [2], - }, - { - data: data - .map(row => ({ - // eslint-disable-next-line no-underscore-dangle - __timestamp: row.__timestamp, - California: row.California, - WestTexNewMexico: row.WestTexNewMexico, - })) - .filter(row => !!row.California), - }, - ]; - return ( - - ); -}; - -Timeseries.args = { - zoomable: false, - logAxis: false, - yAxisFormat: '$,.2f', - yAxisTitle: '', - yAxisIndexB: 1, - minorSplitLine: false, - seriesType: 'line', - stack: false, - area: false, - markerSize: 6, - opacity: 0.2, - seriesTypeB: 'bar', - stackB: false, - areaB: false, - markerEnabledB: false, - markerSizeB: 6, - opacityB: 0.2, -}; - -Timeseries.argTypes = { - zoomable: { - control: 'boolean', - description: 'Zoomable', - }, - logAxis: { - control: 'boolean', - description: 'Log axis', - }, - yAxisFormat: { - control: 'select', - description: 'Y Axis format', - options: ['$,.2f', 'SMART_NUMBER'], - }, - yAxisTitle: { - control: 'text', - description: 'Y Axis title', - }, - yAxisIndexB: { - control: 'select', - description: 'Y Axis index for Query 2', - options: [0, 1], - }, - minorSplitLine: { - control: 'boolean', - description: 'Query 1: Minor splitline', - }, - seriesType: { - control: 'select', - description: 'Query 1: Line type', - options: ['line', 'scatter', 'smooth', 'bar', 'start', 'middle', 'end'], - }, - stack: { - control: 'boolean', - description: 'Query 1: Stack', - }, - area: { - control: 'boolean', - description: 'Query 1: Area chart', - }, - markerEnabled: { - control: 'boolean', - description: 'Query 1: Enable markers', - }, - markerSize: { - control: 'number', - description: 'Query 1: Marker Size', - }, - opacity: { - control: 'number', - description: 'Query 1: Opacity', - }, - seriesTypeB: { - control: 'select', - description: 'Query 2: Line type', - options: ['line', 'scatter', 'smooth', 'bar', 'start', 'middle', 'end'], - }, - stackB: { - control: 'boolean', - description: 'Query 2: Stack', - }, - areaB: { - control: 'boolean', - description: 'Query 2: Area chart', - }, - markerEnabledB: { - control: 'boolean', - description: 'Query 2: Enable markers', - }, - markerSizeB: { - control: 'number', - description: 'Query 2: Marker Size', - }, - opacityB: { - control: 'number', - description: 'Query 2: Opacity', - }, -}; - -export const WithNegativeNumbers = ({ - seriesType, - yAxisFormat, - showValue, - showValueB, - yAxisIndexB, - width, - height, -}: { - seriesType: string; - yAxisFormat: string; - showValue: boolean; - showValueB: boolean; - yAxisIndexB: number; - width: number; - height: number; -}) => ( - ({ - __timestamp, - avgRate: Boston / 100, - })), - }, - ]} - formData={{ - contributionMode: undefined, - colorScheme: 'supersetColors', - seriesType, - xAxisTimeFormat: 'smart_date', - yAxisFormat, - stack: true, - showValue, - showValueB, - showLegend: true, - markerEnabledB: true, - yAxisIndexB, - metrics: [{ label: 'Boston' }], - metricsB: [{ label: 'avgRate' }], - }} - /> -); - -WithNegativeNumbers.args = { - seriesType: 'line', - yAxisFormat: '$,.2f', - showValue: true, - showValueB: false, - yAxisIndexB: 1, -}; - -WithNegativeNumbers.argTypes = { - seriesType: { - control: 'select', - options: ['line', 'scatter', 'smooth', 'bar', 'start', 'middle', 'end'], - }, - yAxisFormat: { - control: 'select', - options: { - 'Original value': '~g', - 'Smart number': 'SMART_NUMBER', - '(12345.432 => $12,345.43)': '$,.2f', - }, - }, - showValue: { - control: 'boolean', - description: 'Query 1: Show Value', - }, - showValueB: { - control: 'boolean', - description: 'Query 2: Show Value', - }, - yAxisIndexB: { - control: 'select', - description: 'Query 2: Y Axis', - options: { - Primary: 0, - Secondary: 1, - }, - }, -}; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts deleted file mode 100644 index 33df597f923..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts +++ /dev/null @@ -1,916 +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. - */ -/* eslint-disable camelcase */ -import { invert } from 'lodash'; -import { - AnnotationLayer, - AxisType, - buildCustomFormatters, - CategoricalColorNamespace, - CurrencyFormatter, - ensureIsArray, - getCustomFormatter, - getNumberFormatter, - getXAxisLabel, - isDefined, - isEventAnnotationLayer, - isFormulaAnnotationLayer, - isIntervalAnnotationLayer, - isPhysicalColumn, - isTimeseriesAnnotationLayer, - QueryFormData, - QueryFormMetric, - resolveAutoCurrency, - TimeseriesChartDataResponseResult, - TimeseriesDataRecord, - tooltipHtml, - ValueFormatter, -} from '@superset-ui/core'; -import { GenericDataType } from '@apache-superset/core/common'; -import { getOriginalSeries } from '@superset-ui/chart-controls'; -import type { EChartsCoreOption } from 'echarts/core'; -import type { SeriesOption } from 'echarts'; -import { - DEFAULT_FORM_DATA, - EchartsMixedTimeseriesChartTransformedProps, - EchartsMixedTimeseriesFormData, - EchartsMixedTimeseriesProps, -} from './types'; -import { - EchartsTimeseriesSeriesType, - ForecastSeriesEnum, - LegendOrientation, - Refs, -} from '../types'; -import { parseAxisBound } from '../utils/controls'; -import { safeParseEChartOptions } from '../utils/safeEChartOptionsParser'; -import { - dedupSeries, - extractDataTotalValues, - extractSeries, - extractShowValueIndexes, - extractTooltipKeys, - getAxisType, - getColtypesMapping, - getHorizontalLegendAvailableWidth, - getLegendProps, - getMinAndMaxFromBounds, - getOverMaxHiddenFormatter, -} from '../utils/series'; -import { resolveLegendLayout } from '../utils/legendLayout'; -import { - extractAnnotationLabels, - getAnnotationData, -} from '../utils/annotation'; -import { - extractForecastSeriesContext, - extractForecastValuesFromTooltipParams, - formatForecastTooltipSeries, - rebaseForecastDatum, - reorderForecastSeries, -} from '../utils/forecast'; -import { convertInteger } from '../utils/convertInteger'; -import { defaultGrid, defaultYAxis } from '../defaults'; -import { - getPadding, - transformEventAnnotation, - transformFormulaAnnotation, - transformIntervalAnnotation, - transformSeries, - transformTimeseriesAnnotation, -} from '../Timeseries/transformers'; -import { TIMEGRAIN_TO_TIMESTAMP, TIMESERIES_CONSTANTS } from '../constants'; -import { getDefaultTooltip } from '../utils/tooltip'; -import { - getTooltipTimeFormatter, - getXAxisFormatter, - getYAxisFormatter, -} from '../utils/formatters'; -import { getMetricDisplayName } from '../utils/metricDisplayName'; -import { mergeCustomEChartOptions } from '../utils/mergeCustomEChartOptions'; - -const getFormatter = ( - customFormatters: Record, - defaultFormatter: ValueFormatter, - metrics: QueryFormMetric[], - formatterKey: string, - forcePercentFormat: boolean, -) => { - if (forcePercentFormat) { - return getNumberFormatter(',.0%'); - } - return ( - getCustomFormatter(customFormatters, metrics, formatterKey) ?? - defaultFormatter - ); -}; - -export default function transformProps( - chartProps: EchartsMixedTimeseriesProps, -): EchartsMixedTimeseriesChartTransformedProps { - const { - width, - height, - formData: { echartOptions: _echartOptions, ...formData }, - queriesData, - hooks, - filterState, - datasource, - theme, - inContextMenu, - emitCrossFilters, - legendState, - } = chartProps; - - let focusedSeries: string | null = null; - - const { - verboseMap = {}, - currencyFormats = {}, - columnFormats = {}, - currencyCodeColumn, - } = datasource; - const { label_map: labelMap, detected_currency: backendDetectedCurrency } = - queriesData[0] as TimeseriesChartDataResponseResult; - const { label_map: labelMapB, detected_currency: backendDetectedCurrencyB } = - queriesData[1] as TimeseriesChartDataResponseResult; - const data1 = (queriesData[0].data || []) as TimeseriesDataRecord[]; - const data2 = (queriesData[1].data || []) as TimeseriesDataRecord[]; - const annotationData = getAnnotationData(chartProps); - const coltypeMapping = { - ...getColtypesMapping(queriesData[0]), - ...getColtypesMapping(queriesData[1]), - }; - const { - area, - areaB, - annotationLayers, - colorScheme, - timeShiftColor, - contributionMode, - legendOrientation, - legendMargin, - legendType, - legendSort, - logAxis, - logAxisSecondary, - markerEnabled, - markerEnabledB, - markerSize, - markerSizeB, - opacity, - opacityB, - minorSplitLine, - minorTicks, - seriesType, - seriesTypeB, - showLegend, - showValue, - showValueB, - onlyTotal, - onlyTotalB, - stack, - stackB, - truncateXAxis, - truncateYAxis, - tooltipTimeFormat, - yAxisFormat, - currencyFormat, - yAxisFormatSecondary, - currencyFormatSecondary, - xAxisTimeFormat, - yAxisBounds, - yAxisBoundsSecondary, - yAxisIndex, - yAxisIndexB, - yAxisTitleSecondary, - zoomable, - richTooltip, - tooltipSortByMetric, - xAxisBounds, - xAxisLabelRotation, - xAxisLabelInterval, - groupby, - groupbyB, - xAxis: xAxisOrig, - xAxisForceCategorical, - xAxisTitle, - yAxisTitle, - xAxisTitleMargin, - yAxisTitleMargin, - yAxisTitlePosition, - sliceId, - sortSeriesType, - sortSeriesTypeB, - sortSeriesAscending, - sortSeriesAscendingB, - timeGrainSqla, - forceMaxInterval, - percentageThreshold, - showQueryIdentifiers = false, - metrics = [], - metricsB = [], - }: EchartsMixedTimeseriesFormData = { ...DEFAULT_FORM_DATA, ...formData }; - - const refs: Refs = {}; - const colorScale = CategoricalColorNamespace.getScale(colorScheme as string); - - let xAxisLabel = getXAxisLabel( - chartProps.rawFormData as QueryFormData, - ) as string; - if ( - isPhysicalColumn(chartProps.rawFormData?.x_axis) && - isDefined(verboseMap[xAxisLabel]) - ) { - xAxisLabel = verboseMap[xAxisLabel]; - } - - const rebasedDataA = rebaseForecastDatum(data1, verboseMap); - const { totalStackedValues, thresholdValues } = extractDataTotalValues( - rebasedDataA, - { - stack, - percentageThreshold, - xAxisCol: xAxisLabel, - }, - ); - - const MetricDisplayNameA: string = - getMetricDisplayName(metrics[0], verboseMap) || ''; - const MetricDisplayNameB: string = - getMetricDisplayName(metricsB[0], verboseMap) || ''; - - const dataTypes = getColtypesMapping(queriesData[0]); - const xAxisDataType = dataTypes?.[xAxisLabel] ?? dataTypes?.[xAxisOrig]; - const xAxisType = getAxisType( - stack, - xAxisForceCategorical, - xAxisDataType, - seriesType === EchartsTimeseriesSeriesType.Bar || - seriesTypeB === EchartsTimeseriesSeriesType.Bar - ? EchartsTimeseriesSeriesType.Bar - : seriesType, - ); - - const [rawSeriesA, sortedTotalValuesA] = extractSeries(rebasedDataA, { - fillNeighborValue: stack ? 0 : undefined, - xAxis: xAxisLabel, - sortSeriesType, - sortSeriesAscending, - stack, - totalStackedValues, - xAxisType, - }); - const rebasedDataB = rebaseForecastDatum(data2, verboseMap); - const { - totalStackedValues: totalStackedValuesB, - thresholdValues: thresholdValuesB, - } = extractDataTotalValues(rebasedDataB, { - stack: Boolean(stackB), - percentageThreshold, - xAxisCol: xAxisLabel, - }); - const [rawSeriesB, sortedTotalValuesB] = extractSeries(rebasedDataB, { - fillNeighborValue: stackB ? 0 : undefined, - xAxis: xAxisLabel, - sortSeriesType: sortSeriesTypeB, - sortSeriesAscending: sortSeriesAscendingB, - stack: Boolean(stackB), - totalStackedValues: totalStackedValuesB, - xAxisType, - }); - const series: SeriesOption[] = []; - - const resolvedCurrency = resolveAutoCurrency( - currencyFormat, - backendDetectedCurrency, - data1, - currencyCodeColumn, - ); - const resolvedCurrencySecondary = resolveAutoCurrency( - currencyFormatSecondary, - backendDetectedCurrencyB, - data2, - currencyCodeColumn, - ); - - const formatter = contributionMode - ? getNumberFormatter(',.0%') - : resolvedCurrency?.symbol - ? new CurrencyFormatter({ - d3Format: yAxisFormat, - currency: resolvedCurrency, - }) - : getNumberFormatter(yAxisFormat); - const formatterSecondary = contributionMode - ? getNumberFormatter(',.0%') - : resolvedCurrencySecondary?.symbol - ? new CurrencyFormatter({ - d3Format: yAxisFormatSecondary, - currency: resolvedCurrencySecondary, - }) - : getNumberFormatter(yAxisFormatSecondary); - const customFormatters = buildCustomFormatters( - [...ensureIsArray(metrics), ...ensureIsArray(metricsB)], - currencyFormats, - columnFormats, - yAxisFormat, - resolvedCurrency, - data1, - currencyCodeColumn, - ); - const customFormattersSecondary = buildCustomFormatters( - [...ensureIsArray(metrics), ...ensureIsArray(metricsB)], - currencyFormats, - columnFormats, - yAxisFormatSecondary, - resolvedCurrencySecondary, - data2, - currencyCodeColumn, - ); - - const primarySeries = new Set(); - const secondarySeries = new Set(); - const mapSeriesIdToAxis = ( - seriesOption: SeriesOption, - index?: number, - ): void => { - if (index === 1) { - secondarySeries.add(seriesOption.id as string); - } else { - primarySeries.add(seriesOption.id as string); - } - }; - const showValueIndexesA = extractShowValueIndexes(rawSeriesA, { - stack, - onlyTotal, - }); - const showValueIndexesB = extractShowValueIndexes(rawSeriesB, { - stack, - onlyTotal, - }); - - annotationLayers - .filter((layer: AnnotationLayer) => layer.show) - .forEach((layer: AnnotationLayer) => { - if (isFormulaAnnotationLayer(layer)) - series.push( - transformFormulaAnnotation( - layer, - rebasedDataA as TimeseriesDataRecord[], - xAxisLabel, - xAxisType, - colorScale, - sliceId, - ), - ); - else if (isIntervalAnnotationLayer(layer)) { - series.push( - ...transformIntervalAnnotation( - layer, - data1, - annotationData, - colorScale, - theme, - sliceId, - ), - ); - } else if (isEventAnnotationLayer(layer)) { - series.push( - ...transformEventAnnotation( - layer, - data1, - annotationData, - colorScale, - theme, - sliceId, - ), - ); - } else if (isTimeseriesAnnotationLayer(layer)) { - series.push( - ...transformTimeseriesAnnotation( - layer, - markerSize, - data1, - annotationData, - colorScale, - sliceId, - ), - ); - } - }); - - // yAxisBounds need to be parsed to replace incompatible values with undefined - const [xAxisMin, xAxisMax] = (xAxisBounds || []).map(parseAxisBound); - let [yAxisMin, yAxisMax] = (yAxisBounds || []).map(parseAxisBound); - let [minSecondary, maxSecondary] = (yAxisBoundsSecondary || []).map( - parseAxisBound, - ); - - const array = ensureIsArray(chartProps.rawFormData?.time_compare); - const inverted = invert(verboseMap); - - rawSeriesA.forEach(entry => { - const entryName = String(entry.name || ''); - const seriesName = inverted[entryName] || entryName; - const colorScaleKey = getOriginalSeries(seriesName, array); - - let displayName: string; - - if (groupby.length > 0) { - // When we have groupby, format as "metric, dimension" - const metricPart: string = showQueryIdentifiers - ? `${MetricDisplayNameA} (Query A)` - : MetricDisplayNameA; - displayName = entryName.includes(metricPart) - ? entryName - : `${metricPart}, ${entryName}`; - } else { - // When no groupby, format as just the entry name with optional query identifier - displayName = showQueryIdentifiers ? `${entryName} (Query A)` : entryName; - } - - const seriesFormatter = getFormatter( - customFormatters, - formatter, - metrics, - labelMap?.[seriesName]?.[0], - !!contributionMode, - ); - - const transformedSeries = transformSeries( - { - ...entry, - id: `${displayName || ''}`, - name: `${displayName || ''}`, - }, - colorScale, - colorScaleKey, - { - area, - markerEnabled, - markerSize, - areaOpacity: opacity, - seriesType, - showValue, - onlyTotal, - stack: Boolean(stack), - stackIdSuffix: '\na', - yAxisIndex, - filterState, - seriesKey: entry.name, - sliceId, - queryIndex: 0, - formatter: - seriesType === EchartsTimeseriesSeriesType.Bar - ? getOverMaxHiddenFormatter({ - max: yAxisMax, - formatter: seriesFormatter, - }) - : seriesFormatter, - totalStackedValues: sortedTotalValuesA, - showValueIndexes: showValueIndexesA, - thresholdValues, - timeShiftColor, - theme, - }, - ); - - if (transformedSeries) { - series.push(transformedSeries); - mapSeriesIdToAxis(transformedSeries, yAxisIndex); - } - }); - - rawSeriesB.forEach(entry => { - const entryName = String(entry.name || ''); - const seriesEntry = inverted[entryName] || entryName; - const seriesName = `${seriesEntry} (1)`; - const colorScaleKey = getOriginalSeries(seriesEntry, array); - - let displayName: string; - - if (groupbyB.length > 0) { - // When we have groupby, format as "metric, dimension" - const metricPart: string = showQueryIdentifiers - ? `${MetricDisplayNameB} (Query B)` - : MetricDisplayNameB; - displayName = entryName.includes(metricPart) - ? entryName - : `${metricPart}, ${entryName}`; - } else { - // When no groupby, format as just the entry name with optional query identifier - displayName = showQueryIdentifiers ? `${entryName} (Query B)` : entryName; - } - - const seriesFormatter = getFormatter( - customFormattersSecondary, - formatterSecondary, - metricsB, - labelMapB?.[seriesName]?.[0], - !!contributionMode, - ); - - const transformedSeries = transformSeries( - { - ...entry, - id: `${displayName || ''}`, - name: `${displayName || ''}`, - }, - - colorScale, - colorScaleKey, - { - area: areaB, - markerEnabled: markerEnabledB, - markerSize: markerSizeB, - areaOpacity: opacityB, - seriesType: seriesTypeB, - showValue: showValueB, - onlyTotal: onlyTotalB, - stack: Boolean(stackB), - stackIdSuffix: '\nb', - yAxisIndex: yAxisIndexB, - filterState, - seriesKey: entry.name, - sliceId, - queryIndex: 1, - formatter: - seriesTypeB === EchartsTimeseriesSeriesType.Bar - ? getOverMaxHiddenFormatter({ - max: maxSecondary, - formatter: seriesFormatter, - }) - : seriesFormatter, - totalStackedValues: sortedTotalValuesB, - showValueIndexes: showValueIndexesB, - thresholdValues: thresholdValuesB, - timeShiftColor, - theme, - }, - ); - - if (transformedSeries) { - series.push(transformedSeries); - mapSeriesIdToAxis(transformedSeries, yAxisIndexB); - } - }); - - // default to 0-100% range when doing row-level contribution chart - if (contributionMode === 'row' && stack) { - if (yAxisMin === undefined) yAxisMin = 0; - if (yAxisMax === undefined) yAxisMax = 1; - if (minSecondary === undefined) minSecondary = 0; - if (maxSecondary === undefined) maxSecondary = 1; - } - - const tooltipFormatter = - xAxisDataType === GenericDataType.Temporal - ? getTooltipTimeFormatter(tooltipTimeFormat) - : String; - const xAxisFormatter = - xAxisDataType === GenericDataType.Temporal - ? getXAxisFormatter(xAxisTimeFormat, timeGrainSqla) - : String; - - const showMaxLabel = xAxisType === AxisType.Time && xAxisLabelRotation === 0; - const deduplicatedFormatter = showMaxLabel - ? (() => { - let lastLabel: string | undefined; - const wrapper = (value: number | string) => { - const label = - typeof xAxisFormatter === 'function' - ? (xAxisFormatter as Function)(value) - : String(value); - if (label === lastLabel) { - return ''; - } - lastLabel = label; - return label; - }; - if (typeof xAxisFormatter === 'function' && 'id' in xAxisFormatter) { - (wrapper as any).id = (xAxisFormatter as any).id; - } - return wrapper; - })() - : xAxisFormatter; - - const addYAxisTitleOffset = - !!(yAxisTitle || yAxisTitleSecondary) && - convertInteger(yAxisTitleMargin) !== 0; - const addXAxisTitleOffset = - !!xAxisTitle && convertInteger(xAxisTitleMargin) !== 0; - const baseChartPadding = getPadding( - showLegend, - legendOrientation, - addYAxisTitleOffset, - zoomable, - legendMargin, - addXAxisTitleOffset, - yAxisTitlePosition, - convertInteger(yAxisTitleMargin), - convertInteger(xAxisTitleMargin), - ); - const legendData = series - .filter( - entry => - extractForecastSeriesContext((entry.name || '') as string).type === - ForecastSeriesEnum.Observation, - ) - .map(entry => entry.name) - .filter((name): name is string => Boolean(name)) - .concat(extractAnnotationLabels(annotationLayers)) - .sort((a: string, b: string) => { - if (!legendSort) return 0; - return legendSort === 'asc' ? a.localeCompare(b) : b.localeCompare(a); - }); - const { effectiveLegendMargin, effectiveLegendType } = resolveLegendLayout({ - availableWidth: - legendOrientation === LegendOrientation.Top || - legendOrientation === LegendOrientation.Bottom - ? getHorizontalLegendAvailableWidth({ - chartWidth: width, - orientation: legendOrientation, - padding: baseChartPadding, - zoomable, - }) - : undefined, - chartHeight: height, - chartWidth: width, - legendItems: legendData, - legendMargin, - orientation: legendOrientation, - show: showLegend, - theme, - type: legendType, - }); - - const chartPadding = getPadding( - showLegend, - legendOrientation, - addYAxisTitleOffset, - zoomable, - effectiveLegendMargin, - addXAxisTitleOffset, - yAxisTitlePosition, - convertInteger(yAxisTitleMargin), - convertInteger(xAxisTitleMargin), - ); - - const { setDataMask = () => {}, onContextMenu } = hooks; - const alignTicks = yAxisIndex !== yAxisIndexB; - - const echartOptions: EChartsCoreOption = { - useUTC: true, - grid: { - ...defaultGrid, - ...chartPadding, - }, - xAxis: { - type: xAxisType, - name: xAxisTitle, - nameGap: convertInteger(xAxisTitleMargin), - nameLocation: 'middle', - axisLabel: { - hideOverlap: !(xAxisType === AxisType.Time && xAxisLabelRotation !== 0), - formatter: deduplicatedFormatter, - rotate: xAxisLabelRotation, - interval: xAxisLabelInterval, - ...(showMaxLabel && { - showMaxLabel: true, - alignMaxLabel: 'right', - }), - }, - minorTick: { show: minorTicks }, - minInterval: - xAxisType === AxisType.Time && timeGrainSqla && !forceMaxInterval - ? TIMEGRAIN_TO_TIMESTAMP[ - timeGrainSqla as keyof typeof TIMEGRAIN_TO_TIMESTAMP - ] - : 0, - maxInterval: - xAxisType === AxisType.Time && timeGrainSqla && forceMaxInterval - ? TIMEGRAIN_TO_TIMESTAMP[ - timeGrainSqla as keyof typeof TIMEGRAIN_TO_TIMESTAMP - ] - : undefined, - ...getMinAndMaxFromBounds( - xAxisType, - truncateXAxis, - xAxisMin, - xAxisMax, - seriesType === EchartsTimeseriesSeriesType.Bar || - seriesTypeB === EchartsTimeseriesSeriesType.Bar - ? EchartsTimeseriesSeriesType.Bar - : undefined, - ), - }, - yAxis: [ - { - ...defaultYAxis, - type: logAxis ? 'log' : 'value', - min: yAxisMin, - max: yAxisMax, - minorTick: { show: minorTicks }, - minorSplitLine: { show: minorSplitLine }, - axisLabel: { - formatter: getYAxisFormatter( - metrics, - !!contributionMode, - customFormatters, - formatter, - yAxisFormat, - ), - }, - scale: truncateYAxis, - name: yAxisTitle, - nameGap: convertInteger(yAxisTitleMargin), - nameLocation: yAxisTitlePosition === 'Left' ? 'middle' : 'end', - alignTicks, - }, - { - ...defaultYAxis, - type: logAxisSecondary ? 'log' : 'value', - min: minSecondary, - max: maxSecondary, - minorTick: { show: minorTicks }, - splitLine: { show: false }, - minorSplitLine: { show: minorSplitLine }, - axisLabel: { - formatter: getYAxisFormatter( - metricsB, - !!contributionMode, - customFormattersSecondary, - formatterSecondary, - yAxisFormatSecondary, - ), - }, - scale: truncateYAxis, - name: yAxisTitleSecondary, - alignTicks, - }, - ], - tooltip: { - ...getDefaultTooltip(refs), - show: !inContextMenu, - trigger: richTooltip ? 'axis' : 'item', - formatter: (params: any) => { - const xValue: number = richTooltip - ? params[0].value[0] - : params.value[0]; - const forecastValue: any[] = richTooltip ? params : [params]; - - const sortedKeys = extractTooltipKeys( - forecastValue, - // horizontal mode is not supported in mixed series chart - 1, - richTooltip, - tooltipSortByMetric, - ); - - const rows: string[][] = []; - const forecastValues = - extractForecastValuesFromTooltipParams(forecastValue); - - const keys = Object.keys(forecastValues); - let focusedRow; - sortedKeys - .filter(key => keys.includes(key)) - .forEach(key => { - const value = forecastValues[key]; - // if there are no dimensions, key is a verbose name of a metric, - // otherwise it is a comma separated string where the first part is metric name - let formatterKey; - if (primarySeries.has(key)) { - formatterKey = - groupby.length === 0 ? inverted[key] : labelMap[key]?.[0]; - } else { - formatterKey = - groupbyB.length === 0 ? inverted[key] : labelMapB[key]?.[0]; - } - const tooltipFormatter = getFormatter( - customFormatters, - formatter, - metrics, - formatterKey, - !!contributionMode, - ); - const tooltipFormatterSecondary = getFormatter( - customFormattersSecondary, - formatterSecondary, - metricsB, - formatterKey, - !!contributionMode, - ); - const row = formatForecastTooltipSeries({ - ...value, - seriesName: key, - formatter: primarySeries.has(key) - ? tooltipFormatter - : tooltipFormatterSecondary, - }); - rows.push(row); - if (key === focusedSeries) { - focusedRow = rows.length - 1; - } - }); - return tooltipHtml(rows, tooltipFormatter(xValue), focusedRow); - }, - }, - legend: { - ...getLegendProps( - effectiveLegendType, - legendOrientation, - showLegend, - theme, - zoomable, - legendState, - chartPadding, - ), - data: legendData, - }, - series: dedupSeries(reorderForecastSeries(series) as SeriesOption[]), - toolbox: { - show: zoomable, - top: TIMESERIES_CONSTANTS.toolboxTop, - right: TIMESERIES_CONSTANTS.toolboxRight, - feature: { - dataZoom: { - yAxisIndex: false, - title: { - zoom: 'zoom area', - back: 'restore zoom', - }, - }, - }, - }, - dataZoom: zoomable - ? [ - { - type: 'slider', - start: TIMESERIES_CONSTANTS.dataZoomStart, - end: TIMESERIES_CONSTANTS.dataZoomEnd, - bottom: TIMESERIES_CONSTANTS.zoomBottom, - }, - ] - : [], - }; - - const onFocusedSeries = (seriesName: string | null) => { - focusedSeries = seriesName; - }; - - let customEchartOptions; - try { - // Parse custom EChart options safely using AST analysis - // This replaces the unsafe `new Function()` approach with a secure parser - // that only allows static data structures (no function callbacks) - customEchartOptions = safeParseEChartOptions(_echartOptions); - } catch (_) { - customEchartOptions = undefined; - } - - const mergedEchartOptions = customEchartOptions - ? mergeCustomEChartOptions(echartOptions, customEchartOptions) - : echartOptions; - - return { - formData, - width, - height, - echartOptions: mergedEchartOptions, - setDataMask, - emitCrossFilters, - labelMap, - labelMapB, - groupby, - groupbyB, - seriesBreakdown: rawSeriesA.length, - selectedValues: filterState.selectedValues || [], - onContextMenu, - onFocusedSeries, - xValueFormatter: tooltipFormatter, - xAxis: { - label: xAxisLabel, - type: xAxisType, - }, - refs, - coltypeMapping, - }; -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/EchartsPie.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/EchartsPie.tsx deleted file mode 100644 index 3f4d4f27747..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/EchartsPie.tsx +++ /dev/null @@ -1,40 +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 { PieChartTransformedProps } from './types'; -import Echart from '../components/Echart'; -import { allEventHandlers } from '../utils/eventHandlers'; - -export default function EchartsPie(props: PieChartTransformedProps) { - const { height, width, echartOptions, selectedValues, refs, formData } = - props; - - const eventHandlers = allEventHandlers(props); - - return ( - - ); -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/buildQuery.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/buildQuery.ts deleted file mode 100644 index 6985eb92868..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/buildQuery.ts +++ /dev/null @@ -1,45 +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 { - buildQueryContext, - getMetricLabel, - QueryFormData, -} from '@superset-ui/core'; -import { getContributionLabel } from './utils'; - -export default function buildQuery(formData: QueryFormData) { - const { metric, sort_by_metric } = formData; - const metricLabel = getMetricLabel(metric); - - return buildQueryContext(formData, baseQueryObject => [ - { - ...baseQueryObject, - ...(sort_by_metric && { orderby: [[metric, false]] }), - post_processing: [ - { - operation: 'contribution', - options: { - columns: [metricLabel], - rename_columns: [getContributionLabel(metricLabel)], - }, - }, - ], - }, - ]); -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/controlPanel.tsx deleted file mode 100644 index 08f84c7bce3..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/controlPanel.tsx +++ /dev/null @@ -1,317 +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 { t } from '@apache-superset/core/translation'; -import { ensureIsInt, validateNonEmpty } from '@superset-ui/core'; -import { - ControlPanelConfig, - ControlPanelsContainerProps, - ControlSubSectionHeader, - D3_FORMAT_DOCS, - D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT, - D3_FORMAT_OPTIONS, - D3_TIME_FORMAT_OPTIONS, - getStandardizedControls, - sharedControls, -} from '@superset-ui/chart-controls'; -import { DEFAULT_FORM_DATA } from './types'; -import { legendSection } from '../controls'; - -const { - donut, - innerRadius, - labelsOutside, - labelType, - labelLine, - outerRadius, - numberFormat, - showLabels, - roseType, -} = DEFAULT_FORM_DATA; - -const config: ControlPanelConfig = { - controlPanelSections: [ - { - label: t('Query'), - expanded: true, - controlSetRows: [ - ['groupby'], - ['metric'], - ['adhoc_filters'], - ['row_limit'], - [ - { - name: 'sort_by_metric', - config: { - ...sharedControls.sort_by_metric, - default: true, - }, - }, - ], - ], - }, - { - label: t('Chart Options'), - expanded: true, - controlSetRows: [ - ['color_scheme'], - [ - { - name: 'show_labels_threshold', - config: { - type: 'TextControl', - label: t('Percentage threshold'), - renderTrigger: true, - isFloat: true, - default: 5, - description: t( - 'Minimum threshold in percentage points for showing labels.', - ), - }, - }, - ], - [ - { - name: 'threshold_for_other', - config: { - type: 'NumberControl', - label: t('Threshold for Other'), - min: 0, - step: 0.5, - max: 100, - default: 0, - renderTrigger: true, - description: t( - 'Values less than this percentage will be grouped into the Other category.', - ), - }, - }, - ], - [ - { - name: 'roseType', - config: { - type: 'SelectControl', - label: t('Rose Type'), - default: roseType, - renderTrigger: true, - choices: [ - ['area', t('Area')], - ['radius', t('Radius')], - [null, t('None')], - ], - description: t('Whether to show as Nightingale chart.'), - }, - }, - ], - ...legendSection, - // eslint-disable-next-line react/jsx-key - [{t('Labels')}], - [ - { - name: 'label_type', - config: { - type: 'SelectControl', - label: t('Label Type'), - default: labelType, - renderTrigger: true, - choices: [ - ['key', t('Category Name')], - ['value', t('Value')], - ['percent', t('Percentage')], - ['key_value', t('Category and Value')], - ['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', - config: { - type: 'SelectControl', - freeForm: true, - label: t('Number format'), - renderTrigger: true, - default: numberFormat, - choices: D3_FORMAT_OPTIONS, - description: `${D3_FORMAT_DOCS} ${D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT}`, - tokenSeparators: ['\n', '\t', ';'], - }, - }, - ], - ['currency_format'], - [ - { - name: 'date_format', - config: { - type: 'SelectControl', - freeForm: true, - label: t('Date format'), - renderTrigger: true, - choices: D3_TIME_FORMAT_OPTIONS, - default: 'smart_date', - description: D3_FORMAT_DOCS, - }, - }, - ], - [ - { - name: 'show_labels', - config: { - type: 'CheckboxControl', - label: t('Show Labels'), - renderTrigger: true, - default: showLabels, - description: t('Whether to display the labels.'), - }, - }, - ], - [ - { - name: 'labels_outside', - config: { - type: 'CheckboxControl', - label: t('Put labels outside'), - default: labelsOutside, - renderTrigger: true, - description: t('Put the labels outside of the pie?'), - visibility: ({ controls }: ControlPanelsContainerProps) => - Boolean(controls?.show_labels?.value), - }, - }, - ], - [ - { - name: 'label_line', - config: { - type: 'CheckboxControl', - label: t('Label Line'), - default: labelLine, - renderTrigger: true, - description: t( - 'Draw line from Pie to label when labels outside?', - ), - visibility: ({ controls }: ControlPanelsContainerProps) => - Boolean(controls?.show_labels?.value), - }, - }, - ], - [ - { - name: 'show_total', - config: { - type: 'CheckboxControl', - label: t('Show Total'), - default: false, - renderTrigger: true, - description: t('Whether to display the aggregate count'), - }, - }, - ], - // eslint-disable-next-line react/jsx-key - [{t('Pie shape')}], - [ - { - name: 'outerRadius', - config: { - type: 'SliderControl', - label: t('Outer Radius'), - renderTrigger: true, - min: 10, - max: 100, - step: 1, - default: outerRadius, - description: t('Outer edge of Pie chart'), - }, - }, - ], - [ - { - name: 'donut', - config: { - type: 'CheckboxControl', - label: t('Donut'), - default: donut, - renderTrigger: true, - description: t('Do you want a donut or a pie?'), - }, - }, - ], - [ - { - name: 'innerRadius', - config: { - type: 'SliderControl', - label: t('Inner Radius'), - renderTrigger: true, - min: 0, - max: 100, - step: 1, - default: innerRadius, - description: t('Inner radius of donut hole'), - visibility: ({ controls }: ControlPanelsContainerProps) => - Boolean(controls?.donut?.value), - }, - }, - ], - ], - }, - ], - controlOverrides: { - series: { - validators: [validateNonEmpty], - clearable: false, - }, - row_limit: { - default: 100, - }, - }, - formDataOverrides: formData => ({ - ...formData, - metric: getStandardizedControls().shiftMetric(), - groupby: getStandardizedControls().popAllColumns(), - row_limit: - ensureIsInt(formData.row_limit, 100) >= 100 ? 100 : formData.row_limit, - }), -}; - -export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/index.ts deleted file mode 100644 index 389bcb5f19c..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/index.ts +++ /dev/null @@ -1,91 +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 { t } from '@apache-superset/core/translation'; -import { Behavior } from '@superset-ui/core'; -import buildQuery from './buildQuery'; -import controlPanel from './controlPanel'; -import transformProps from './transformProps'; -import thumbnail from './images/thumbnail.png'; -import thumbnailDark from './images/thumbnail-dark.png'; -import example1 from './images/Pie1.jpg'; -import example1Dark from './images/Pie1-dark.jpg'; -import example2 from './images/Pie2.jpg'; -import example2Dark from './images/Pie2-dark.jpg'; -import example3 from './images/Pie3.jpg'; -import example3Dark from './images/Pie3-dark.jpg'; -import example4 from './images/Pie4.jpg'; -import example4Dark from './images/Pie4-dark.jpg'; -import { EchartsPieChartProps, EchartsPieFormData } from './types'; -import { EchartsChartPlugin } from '../types'; - -export default class EchartsPieChartPlugin extends EchartsChartPlugin< - EchartsPieFormData, - EchartsPieChartProps -> { - /** - * The constructor is used to pass relevant metadata and callbacks that get - * registered in respective registries that are used throughout the library - * and application. A more thorough description of each property is given in - * the respective imported file. - * - * It is worth noting that `buildQuery` and is optional, and only needed for - * advanced visualizations that require either post processing operations - * (pivoting, rolling aggregations, sorting etc) or submitting multiple queries. - */ - constructor() { - super({ - buildQuery, - controlPanel, - loadChart: () => import('./EchartsPie'), - metadata: { - behaviors: [ - Behavior.InteractiveChart, - Behavior.DrillToDetail, - Behavior.DrillBy, - ], - category: t('Part of a Whole'), - credits: ['https://echarts.apache.org'], - description: - t(`The classic. Great for showing how much of a company each investor gets, what demographics follow your blog, or what portion of the budget goes to the military industrial complex. - - Pie charts can be difficult to interpret precisely. If clarity of relative proportion is important, consider using a bar or other chart type instead.`), - exampleGallery: [ - { url: example1, urlDark: example1Dark }, - { url: example2, urlDark: example2Dark }, - { url: example3, urlDark: example3Dark }, - { url: example4, urlDark: example4Dark }, - ], - name: t('Pie Chart'), - tags: [ - t('Categorical'), - t('Circular'), - t('Comparison'), - t('Percentages'), - t('Featured'), - t('Proportional'), - t('ECharts'), - t('Nightingale'), - ], - thumbnail, - thumbnailDark, - }, - transformProps, - }); - } -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/index.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/index.tsx new file mode 100644 index 00000000000..1818a139a01 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/index.tsx @@ -0,0 +1,787 @@ +/** + * 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. + */ + +/** + * ECharts Pie Chart - Glyph Pattern Implementation + * + * A classic pie/donut chart for showing proportions of a whole. + * Supports Nightingale (rose) charts, custom labels, and "Other" grouping. + */ + +import { t } from '@apache-superset/core/translation'; +import { + Behavior, + buildQueryContext, + CategoricalColorNamespace, + DataRecord, + getColumnLabel, + getMetricLabel, + getNumberFormatter, + getTimeFormatter, + getValueFormatter, + NumberFormats, + QueryFormData, + tooltipHtml, + ValueFormatter, +} from '@superset-ui/core'; +import type { CallbackDataParams } from 'echarts/types/src/util/types'; +import type { EChartsCoreOption } from 'echarts/core'; +import type { PieSeriesOption } from 'echarts/charts'; + +import { + defineChart, + Metric, + Dimension, + Select, + Text, + Checkbox, + Int, + NumberFormat, + Currency, + TimeFormat, + ChartProps, + // Presets + ShowLegend, + ShowLabels, + LegendType as LegendTypeArg, + LegendOrientation as LegendOrientationArg, + LegendSort as LegendSortArg, +} from '@superset-ui/glyph-core'; + +import { OpacityEnum } from '../constants'; +import { + extractGroupbyLabel, + getChartPadding, + getColtypesMapping, + getLegendProps, + sanitizeHtml, +} from '../utils/series'; +import { defaultGrid } from '../defaults'; +import { convertInteger } from '../utils/convertInteger'; +import { getDefaultTooltip } from '../utils/tooltip'; +import { allEventHandlers } from '../utils/eventHandlers'; +import Echart from '../components/Echart'; +import { Refs, LegendOrientation, LegendType } from '../types'; +import { CONTRIBUTION_SUFFIX } from './constants'; +import { + EchartsPieFormData, + EchartsPieLabelType, + PieChartDataItem, + PieChartTransformedProps, +} from './types'; + +import thumbnail from './images/thumbnail.png'; +import example1 from './images/Pie1.jpg'; +import example1Dark from './images/Pie1-dark.jpg'; +import example2 from './images/Pie2.jpg'; +import example2Dark from './images/Pie2-dark.jpg'; +import example3 from './images/Pie3.jpg'; +import example3Dark from './images/Pie3-dark.jpg'; +import example4 from './images/Pie4.jpg'; +import example4Dark from './images/Pie4-dark.jpg'; + +// ============================================================================ +// Constants & Helpers +// ============================================================================ + +const percentFormatter = getNumberFormatter(NumberFormats.PERCENT_2_POINT); + +const getContributionLabel = (metricLabel: string) => + `${metricLabel}${CONTRIBUTION_SUFFIX}`; + +function parseParams({ + params, + numberFormatter, + sanitizeName = false, +}: { + params: Pick; + numberFormatter: ValueFormatter; + sanitizeName?: boolean; +}): string[] { + const { name: rawName = '', value, percent } = params; + const name = sanitizeName ? sanitizeHtml(rawName) : rawName; + const formattedValue = numberFormatter(value as number); + const formattedPercent = percentFormatter((percent as number) / 100); + return [name, formattedValue, formattedPercent]; +} + +function getTotalValuePadding({ + chartPadding, + donut, + width, + height, +}: { + chartPadding: { bottom: number; left: number; right: number; top: number }; + donut: boolean; + width: number; + height: number; +}) { + const padding: { left?: string; top?: string } = { + top: donut ? 'middle' : '0', + left: 'center', + }; + if (chartPadding.top) { + padding.top = donut + ? `${50 + (chartPadding.top / height / 2) * 100}%` + : `${(chartPadding.top / height) * 100}%`; + } + if (chartPadding.bottom) { + padding.top = donut + ? `${50 - (chartPadding.bottom / height / 2) * 100}%` + : '0'; + } + if (chartPadding.left) { + const leftPaddingPercent = (chartPadding.left / width) * 100; + const adjustedLeftPercent = 50 + leftPaddingPercent * 0.25; + padding.left = `${adjustedLeftPercent}%`; + } + if (chartPadding.right) { + const rightPaddingPercent = (chartPadding.right / width) * 100; + const adjustedLeftPercent = 50 - rightPaddingPercent * 0.75; + padding.left = `${adjustedLeftPercent}%`; + } + return padding; +} + +// ============================================================================ +// Build Query - exported for testing +// ============================================================================ + +export function buildQuery(formData: QueryFormData) { + const { metric, sort_by_metric } = formData; + const metricLabel = getMetricLabel(metric); + + return buildQueryContext(formData, baseQueryObject => [ + { + ...baseQueryObject, + ...(sort_by_metric && { orderby: [[metric, false]] }), + post_processing: [ + { + operation: 'contribution', + options: { + columns: [metricLabel], + rename_columns: [getContributionLabel(metricLabel)], + }, + }, + ], + }, + ]); +} + +// ============================================================================ +// Select Options +// ============================================================================ + +const ROSE_TYPE_OPTIONS = [ + { label: t('None'), value: '' }, + { label: t('Area'), value: 'area' }, + { label: t('Radius'), value: 'radius' }, +]; + +const LABEL_TYPE_OPTIONS = [ + { label: t('Category Name'), value: 'key' }, + { label: t('Value'), value: 'value' }, + { label: t('Percentage'), value: 'percent' }, + { label: t('Category and Value'), value: 'key_value' }, + { label: t('Category and Percentage'), value: 'key_percent' }, + { label: t('Category, Value and Percentage'), value: 'key_value_percent' }, + { label: t('Value and Percentage'), value: 'value_percent' }, + { label: t('Template'), value: 'template' }, +]; + +// Legend options imported from glyph-core/presets + +// ============================================================================ +// Transform Result Type +// ============================================================================ + +interface PieTransformResult { + transformedProps: PieChartTransformedProps; +} + +// ============================================================================ +// The Chart Definition +// ============================================================================ + +export default defineChart< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + any, + PieTransformResult +>({ + metadata: { + name: t('Pie Chart'), + description: t( + `The classic. Great for showing how much of a company each investor gets, what demographics follow your blog, or what portion of the budget goes to the military industrial complex. + + Pie charts can be difficult to interpret precisely. If clarity of relative proportion is important, consider using a bar or other chart type instead.`, + ), + category: t('Part of a Whole'), + tags: [ + t('Categorical'), + t('Circular'), + t('Comparison'), + t('Percentages'), + t('Featured'), + t('Proportional'), + t('Nightingale'), + ], + thumbnail, + behaviors: [ + Behavior.InteractiveChart, + Behavior.DrillToDetail, + Behavior.DrillBy, + ], + exampleGallery: [ + { url: example1, urlDark: example1Dark }, + { url: example2, urlDark: example2Dark }, + { url: example3, urlDark: example3Dark }, + { url: example4, urlDark: example4Dark }, + ], + }, + + arguments: { + // Query section + groupby: Dimension.with({ + label: t('Dimensions'), + description: t('Columns to group by on the pie slices'), + }), + + metric: Metric.with({ + label: t('Metric'), + description: t('The metric used to determine slice size'), + }), + + sortByMetric: Checkbox.with({ + label: t('Sort by metric'), + description: t('Sort slices by the metric value'), + default: true, + }), + + // Chart options + showLabelsThreshold: Text.with({ + label: t('Percentage threshold'), + description: t( + 'Minimum threshold in percentage points for showing labels.', + ), + default: '5', + }), + + thresholdForOther: Int.with({ + label: t('Threshold for Other'), + description: t( + 'Values less than this percentage will be grouped into the Other category.', + ), + default: 0, + min: 0, + max: 100, + step: 1, + }), + + roseType: Select.with({ + label: t('Rose Type'), + description: t('Whether to show as Nightingale chart.'), + options: ROSE_TYPE_OPTIONS, + default: '', + }), + + // Legend section + showLegend: ShowLegend, + + legendType: { arg: LegendTypeArg, visibleWhen: { showLegend: true } }, + legendOrientation: { + arg: LegendOrientationArg, + visibleWhen: { showLegend: true }, + }, + + legendMargin: { + arg: Text.with({ + label: t('Legend Margin'), + description: t('Additional padding for legend.'), + default: '', + }), + visibleWhen: { showLegend: true }, + }, + + legendSort: { arg: LegendSortArg, visibleWhen: { showLegend: true } }, + + // Label section + labelType: Select.with({ + label: t('Label Type'), + description: t('What should be shown on the label?'), + options: LABEL_TYPE_OPTIONS, + default: 'key', + }), + + labelTemplate: { + arg: Text.with({ + label: t('Label Template'), + description: t( + 'Format data labels. Use variables: {name}, {value}, {percent}. ' + + '\\n represents a new line.', + ), + default: '', + }), + visibleWhen: { labelType: 'template' }, + }, + + numberFormat: NumberFormat, + currencyFormat: Currency, + dateFormat: TimeFormat, + + showLabels: ShowLabels, + + labelsOutside: { + arg: Checkbox.with({ + label: t('Put labels outside'), + description: t('Put the labels outside of the pie?'), + default: true, + }), + visibleWhen: { showLabels: true }, + }, + + labelLine: { + arg: Checkbox.with({ + label: t('Label Line'), + description: t('Draw line from Pie to label when labels outside?'), + default: false, + }), + visibleWhen: { showLabels: true }, + }, + + showTotal: Checkbox.with({ + label: t('Show Total'), + description: t('Whether to display the aggregate count'), + default: false, + }), + + // Pie shape section + outerRadius: Int.with({ + label: t('Outer Radius'), + description: t('Outer edge of Pie chart'), + default: 70, + min: 10, + max: 100, + step: 1, + }), + + donut: Checkbox.with({ + label: t('Donut'), + description: t('Do you want a donut or a pie?'), + default: false, + }), + + innerRadius: { + arg: Int.with({ + label: t('Inner Radius'), + description: t('Inner radius of donut hole'), + default: 30, + min: 0, + max: 100, + step: 1, + }), + visibleWhen: { donut: true }, + }, + }, + + additionalControls: { + query: [['groupby'], ['adhoc_filters'], ['row_limit']], + chartOptions: [['color_scheme']], + }, + + controlOverrides: { + row_limit: { + default: 100, + }, + }, + + buildQuery, + + transform: (chartProps: ChartProps, _argValues): PieTransformResult => { + const { + formData, + height, + hooks, + filterState, + queriesData, + width, + theme, + inContextMenu, + emitCrossFilters, + datasource, + } = chartProps; + + const { columnFormats = {}, currencyFormats = {} } = datasource ?? {}; + const { + data: rawData = [], + colnames = [], + coltypes = [], + } = queriesData[0] ?? {}; + const coltypeMapping = getColtypesMapping({ colnames, coltypes }); + + const { + colorScheme, + donut = false, + groupby = [], + innerRadius = 30, + labelsOutside = true, + labelLine = false, + labelType = EchartsPieLabelType.Key, + labelTemplate, + legendMargin, + legendOrientation = LegendOrientation.Top, + legendType = LegendType.Scroll, + legendSort, + metric = '', + numberFormat = 'SMART_NUMBER', + currencyFormat, + dateFormat = 'smart_date', + outerRadius = 70, + showLabels = true, + showLegend = true, + showLabelsThreshold = 5, + sliceId, + showTotal = false, + roseType, + thresholdForOther = 0, + } = formData; + + const refs: Refs = {}; + const metricLabel = getMetricLabel(metric); + const contributionLabel = getContributionLabel(metricLabel); + const groupbyLabels = (groupby || []).map(getColumnLabel); + const minShowLabelAngle = (showLabelsThreshold || 0) * 3.6; + + const numberFormatter = getValueFormatter( + metric, + currencyFormats, + columnFormats, + numberFormat, + currencyFormat, + ); + + let data = rawData; + const otherRows: DataRecord[] = []; + const otherTooltipData: string[][] = []; + let otherDatum: PieChartDataItem | null = null; + let otherSum = 0; + + if (thresholdForOther) { + let contributionSum = 0; + data = data.filter((datum: DataRecord) => { + const contribution = datum[contributionLabel] as number; + if (!contribution || contribution * 100 >= thresholdForOther) { + return true; + } + otherSum += datum[metricLabel] as number; + contributionSum += contribution; + otherRows.push(datum); + otherTooltipData.push([ + extractGroupbyLabel({ + datum, + groupby: groupbyLabels, + coltypeMapping, + timeFormatter: getTimeFormatter(dateFormat), + }), + numberFormatter(datum[metricLabel] as number), + percentFormatter(contribution), + ]); + return false; + }); + const otherName = t('Other'); + otherTooltipData.push([ + t('Total'), + numberFormatter(otherSum), + percentFormatter(contributionSum), + ]); + if (otherSum) { + otherDatum = { + name: otherName, + value: otherSum, + itemStyle: { + // eslint-disable-next-line theme-colors/no-literal-colors + color: (theme as { colorText?: string })?.colorText ?? '#000', + opacity: + filterState?.selectedValues && + !filterState.selectedValues.includes(otherName) + ? OpacityEnum.SemiTransparent + : OpacityEnum.NonTransparent, + }, + isOther: true, + }; + } + } + + const labelMap = data.reduce( + (acc: Record, datum: DataRecord) => { + const label = extractGroupbyLabel({ + datum, + groupby: groupbyLabels, + coltypeMapping, + timeFormatter: getTimeFormatter(dateFormat), + }); + return { + ...acc, + [label]: groupbyLabels.map((col: string) => datum[col] as string), + }; + }, + {}, + ); + + const { setDataMask = () => {}, onContextMenu } = hooks ?? {}; + const colorFn = CategoricalColorNamespace.getScale(colorScheme as string); + + let totalValue = 0; + + const transformedData: PieSeriesOption[] = data.map((datum: DataRecord) => { + const name = extractGroupbyLabel({ + datum, + groupby: groupbyLabels, + coltypeMapping, + timeFormatter: getTimeFormatter(dateFormat), + }); + + const isFiltered = + filterState?.selectedValues && + !filterState.selectedValues.includes(name); + const value = datum[metricLabel]; + + if (typeof value === 'number' || typeof value === 'string') { + totalValue += convertInteger(value); + } + + return { + value, + name, + itemStyle: { + color: colorFn(name, sliceId), + opacity: isFiltered + ? OpacityEnum.SemiTransparent + : OpacityEnum.NonTransparent, + }, + }; + }); + + if (otherDatum) { + transformedData.push(otherDatum); + totalValue += otherSum; + } + + const selectedValues = (filterState?.selectedValues || []).reduce( + (acc: Record, selectedValue: string) => { + const index = transformedData.findIndex( + ({ name }) => name === selectedValue, + ); + return { + ...acc, + [index]: selectedValue, + }; + }, + {}, + ); + + const formatTemplate = ( + template: string, + formattedParams: { name: string; value: string; percent: string }, + rawParams: CallbackDataParams, + ) => { + 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, + numberFormatter, + }); + switch (labelType) { + case EchartsPieLabelType.Key: + return name; + case EchartsPieLabelType.Value: + return formattedValue; + case EchartsPieLabelType.Percent: + return formattedPercent; + case EchartsPieLabelType.KeyValue: + return `${name}: ${formattedValue}`; + case EchartsPieLabelType.KeyValuePercent: + return `${name}: ${formattedValue} (${formattedPercent})`; + case EchartsPieLabelType.KeyPercent: + 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; + } + }; + + const defaultLabel = { + formatter, + show: showLabels, + // eslint-disable-next-line theme-colors/no-literal-colors + color: (theme as { colorText?: string })?.colorText ?? '#000', + }; + + const chartPadding = getChartPadding( + showLegend, + legendOrientation, + legendMargin, + ); + + const series: PieSeriesOption[] = [ + { + type: 'pie', + ...chartPadding, + animation: false, + roseType: roseType || undefined, + radius: [`${donut ? innerRadius : 0}%`, `${outerRadius}%`], + center: ['50%', '50%'], + avoidLabelOverlap: true, + labelLine: + labelsOutside && labelLine ? { show: true } : { show: false }, + minShowLabelAngle, + label: labelsOutside + ? { + ...defaultLabel, + position: 'outer', + alignTo: 'none', + bleedMargin: 5, + } + : { + ...defaultLabel, + position: 'inner', + }, + emphasis: { + label: { + show: true, + fontWeight: 'bold', + // eslint-disable-next-line theme-colors/no-literal-colors + backgroundColor: + (theme as { colorBgContainer?: string })?.colorBgContainer ?? + '#fff', + }, + }, + data: transformedData, + }, + ]; + + const echartOptions: EChartsCoreOption = { + grid: { ...defaultGrid }, + tooltip: { + ...getDefaultTooltip(refs), + show: !inContextMenu, + trigger: 'item', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + formatter: (params: any) => { + const [name, formattedValue, formattedPercent] = parseParams({ + params, + numberFormatter, + sanitizeName: true, + }); + if (params?.data?.isOther) { + return tooltipHtml(otherTooltipData, name); + } + return tooltipHtml( + [[metricLabel, formattedValue, formattedPercent]], + name, + ); + }, + }, + legend: { + ...getLegendProps(legendType, legendOrientation, showLegend, theme), + data: transformedData + .map(datum => datum.name) + .sort((a: string, b: string) => { + if (!legendSort) return 0; + return legendSort === 'asc' + ? a.localeCompare(b) + : b.localeCompare(a); + }), + }, + graphic: showTotal + ? { + type: 'text', + ...getTotalValuePadding({ chartPadding, donut, width, height }), + style: { + text: t('Total: %s', numberFormatter(totalValue)), + fontSize: 16, + fontWeight: 'bold', + // eslint-disable-next-line theme-colors/no-literal-colors + fill: (theme as { colorText?: string })?.colorText ?? '#000', + }, + z: 10, + } + : null, + series, + }; + + return { + transformedProps: { + formData: formData as EchartsPieFormData, + width, + height, + echartOptions, + setDataMask, + labelMap, + groupby, + selectedValues, + onContextMenu, + refs, + emitCrossFilters, + coltypeMapping, + }, + }; + }, + + render: ({ transformedProps }) => { + const { height, width, echartOptions, selectedValues, refs, formData } = + transformedProps; + + const eventHandlers = allEventHandlers(transformedProps); + + return ( + + ); + }, +}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/stories/Pie.stories.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/stories/Pie.stories.tsx index c09144ccbb9..4e49845381a 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/stories/Pie.stories.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/stories/Pie.stories.tsx @@ -17,22 +17,13 @@ * under the License. */ -import { - SuperChart, - VizType, - getChartTransformPropsRegistry, -} from '@superset-ui/core'; -import { - EchartsPieChartPlugin, - PieTransformProps, -} from '@superset-ui/plugin-chart-echarts'; +import { SuperChart, VizType } from '@superset-ui/core'; +import { EchartsPieChartPlugin } from '@superset-ui/plugin-chart-echarts'; import { weekday, population, sales } from './data'; import { withResizableChartDemo } from '@storybook-shared'; new EchartsPieChartPlugin().configure({ key: VizType.Pie }).register(); -getChartTransformPropsRegistry().registerValue(VizType.Pie, PieTransformProps); - export default { title: 'Chart Plugins/plugin-chart-echarts/Pie', decorators: [withResizableChartDemo], diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/transformProps.ts deleted file mode 100644 index 6a355f6e753..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/transformProps.ts +++ /dev/null @@ -1,502 +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 { t } from '@apache-superset/core/translation'; -import { - CategoricalColorNamespace, - getColumnLabel, - getMetricLabel, - getNumberFormatter, - getTimeFormatter, - NumberFormats, - ValueFormatter, - getValueFormatter, - tooltipHtml, - DataRecord, -} from '@superset-ui/core'; -import type { CallbackDataParams } from 'echarts/types/src/util/types'; -import type { EChartsCoreOption } from 'echarts/core'; -import type { PieSeriesOption } from 'echarts/charts'; -import { - DEFAULT_FORM_DATA as DEFAULT_PIE_FORM_DATA, - EchartsPieChartProps, - EchartsPieFormData, - EchartsPieLabelType, - PieChartDataItem, - PieChartTransformedProps, -} from './types'; -import { DEFAULT_LEGEND_FORM_DATA, OpacityEnum } from '../constants'; -import { - extractGroupbyLabel, - getChartPadding, - getColtypesMapping, - getLegendProps, - sanitizeHtml, -} from '../utils/series'; -import { resolveLegendLayout } from '../utils/legendLayout'; -import { defaultGrid } from '../defaults'; -import { convertInteger } from '../utils/convertInteger'; -import { getDefaultTooltip } from '../utils/tooltip'; -import { Refs } from '../types'; -import { getContributionLabel } from './utils'; - -const percentFormatter = getNumberFormatter(NumberFormats.PERCENT_2_POINT); - -export function parseParams({ - params, - numberFormatter, - sanitizeName = false, -}: { - params: Pick; - numberFormatter: ValueFormatter; - sanitizeName?: boolean; -}): string[] { - const { name: rawName = '', value, percent } = params; - const name = sanitizeName ? sanitizeHtml(rawName) : rawName; - const formattedValue = numberFormatter(value as number); - const formattedPercent = percentFormatter((percent as number) / 100); - return [name, formattedValue, formattedPercent]; -} - -function getTotalValuePadding({ - chartPadding, - donut, - width, - height, -}: { - chartPadding: { - bottom: number; - left: number; - right: number; - top: number; - }; - donut: boolean; - width: number; - height: number; -}) { - const padding: { - left?: string; - top?: string; - } = { - top: donut ? 'middle' : '0', - left: 'center', - }; - if (chartPadding.top) { - padding.top = donut - ? `${50 + (chartPadding.top / height / 2) * 100}%` - : `${(chartPadding.top / height) * 100}%`; - } - if (chartPadding.bottom) { - padding.top = donut - ? `${50 - (chartPadding.bottom / height / 2) * 100}%` - : '0'; - } - if (chartPadding.left) { - // When legend is on the left, shift text right to center it in the available space - const leftPaddingPercent = (chartPadding.left / width) * 100; - const adjustedLeftPercent = 50 + leftPaddingPercent * 0.25; - padding.left = `${adjustedLeftPercent}%`; - } - if (chartPadding.right) { - // When legend is on the right, shift text left to center it in the available space - const rightPaddingPercent = (chartPadding.right / width) * 100; - const adjustedLeftPercent = 50 - rightPaddingPercent * 0.75; - padding.left = `${adjustedLeftPercent}%`; - } - return padding; -} - -export default function transformProps( - chartProps: EchartsPieChartProps, -): PieChartTransformedProps { - const { - formData, - height, - hooks, - filterState, - queriesData, - width, - theme, - inContextMenu, - emitCrossFilters, - datasource, - } = chartProps; - const { - columnFormats = {}, - currencyFormats = {}, - currencyCodeColumn, - } = datasource; - const { data: rawData = [], detected_currency: detectedCurrency } = - queriesData[0]; - const coltypeMapping = getColtypesMapping(queriesData[0]); - - const { - colorScheme, - donut, - groupby, - innerRadius, - labelsOutside, - labelLine, - labelType, - labelTemplate, - legendMargin, - legendOrientation, - legendType, - legendSort, - metric = '', - numberFormat, - currencyFormat, - dateFormat, - outerRadius, - showLabels, - showLegend, - showLabelsThreshold, - sliceId, - showTotal, - roseType, - thresholdForOther, - }: EchartsPieFormData = { - ...DEFAULT_LEGEND_FORM_DATA, - ...DEFAULT_PIE_FORM_DATA, - ...formData, - }; - const refs: Refs = {}; - const metricLabel = getMetricLabel(metric); - const contributionLabel = getContributionLabel(metricLabel); - const groupbyLabels = groupby.map(getColumnLabel); - const minShowLabelAngle = (showLabelsThreshold || 0) * 3.6; - - const numberFormatter = getValueFormatter( - metric, - currencyFormats, - columnFormats, - numberFormat, - currencyFormat, - undefined, - rawData, - currencyCodeColumn, - detectedCurrency, - ); - - let data = rawData; - const otherRows: DataRecord[] = []; - const otherTooltipData: string[][] = []; - let otherDatum: PieChartDataItem | null = null; - let otherSum = 0; - if (thresholdForOther) { - let contributionSum = 0; - data = data.filter(datum => { - const contribution = datum[contributionLabel] as number; - if (!contribution || contribution * 100 >= thresholdForOther) { - return true; - } - otherSum += datum[metricLabel] as number; - contributionSum += contribution; - otherRows.push(datum); - otherTooltipData.push([ - extractGroupbyLabel({ - datum, - groupby: groupbyLabels, - coltypeMapping, - timeFormatter: getTimeFormatter(dateFormat), - }), - numberFormatter(datum[metricLabel] as number), - percentFormatter(contribution), - ]); - return false; - }); - const otherName = t('Other'); - otherTooltipData.push([ - t('Total'), - numberFormatter(otherSum), - percentFormatter(contributionSum), - ]); - if (otherSum) { - otherDatum = { - name: otherName, - value: otherSum, - itemStyle: { - color: theme.colorText, - opacity: - filterState.selectedValues && - !filterState.selectedValues.includes(otherName) - ? OpacityEnum.SemiTransparent - : OpacityEnum.NonTransparent, - }, - isOther: true, - }; - } - } - - const labelMap = data.reduce((acc: Record, datum) => { - const label = extractGroupbyLabel({ - datum, - groupby: groupbyLabels, - coltypeMapping, - timeFormatter: getTimeFormatter(dateFormat), - }); - return { - ...acc, - [label]: groupbyLabels.map(col => datum[col] as string), - }; - }, {}); - - const { setDataMask = () => {}, onContextMenu } = hooks; - const colorFn = CategoricalColorNamespace.getScale(colorScheme as string); - - let totalValue = 0; - - const transformedData: PieSeriesOption[] = data.map(datum => { - const name = extractGroupbyLabel({ - datum, - groupby: groupbyLabels, - coltypeMapping, - timeFormatter: getTimeFormatter(dateFormat), - }); - - const isFiltered = - filterState.selectedValues && !filterState.selectedValues.includes(name); - const value = datum[metricLabel]; - - if (typeof value === 'number' || typeof value === 'string') { - totalValue += convertInteger(value); - } - - return { - value, - name, - itemStyle: { - color: colorFn(name, sliceId), - opacity: isFiltered - ? OpacityEnum.SemiTransparent - : OpacityEnum.NonTransparent, - }, - }; - }); - if (otherDatum) { - transformedData.push(otherDatum); - totalValue += otherSum; - } - - const selectedValues = (filterState.selectedValues || []).reduce( - (acc: Record, selectedValue: string) => { - const index = transformedData.findIndex( - ({ name }) => name === selectedValue, - ); - return { - ...acc, - [index]: selectedValue, - }; - }, - {}, - ); - - 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, - numberFormatter, - }); - switch (labelType) { - case EchartsPieLabelType.Key: - return name; - case EchartsPieLabelType.Value: - return formattedValue; - case EchartsPieLabelType.Percent: - return formattedPercent; - case EchartsPieLabelType.KeyValue: - return `${name}: ${formattedValue}`; - case EchartsPieLabelType.KeyValuePercent: - return `${name}: ${formattedValue} (${formattedPercent})`; - case EchartsPieLabelType.KeyPercent: - 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; - } - }; - - const defaultLabel = { - formatter, - show: showLabels, - color: theme.colorText, - }; - const legendData = transformedData - .map(datum => datum.name) - .sort((a: string, b: string) => { - if (!legendSort) return 0; - return legendSort === 'asc' ? a.localeCompare(b) : b.localeCompare(a); - }); - const { effectiveLegendMargin, effectiveLegendType } = resolveLegendLayout({ - chartHeight: height, - chartWidth: width, - legendItems: legendData, - legendMargin, - orientation: legendOrientation, - show: showLegend, - theme, - type: legendType, - }); - - const chartPadding = getChartPadding( - showLegend, - legendOrientation, - effectiveLegendMargin, - ); - - const series: PieSeriesOption[] = [ - { - type: 'pie', - ...chartPadding, - animation: false, - roseType: roseType || undefined, - radius: [`${donut ? innerRadius : 0}%`, `${outerRadius}%`], - center: ['50%', '50%'], - avoidLabelOverlap: true, - labelLine: labelsOutside && labelLine ? { show: true } : { show: false }, - minShowLabelAngle, - label: labelsOutside - ? { - ...defaultLabel, - position: 'outer', - alignTo: 'none', - bleedMargin: 5, - } - : { - ...defaultLabel, - position: 'inner', - }, - emphasis: { - label: { - show: true, - fontWeight: 'bold', - backgroundColor: theme.colorBgContainer, - }, - }, - data: transformedData, - }, - ]; - - const echartOptions: EChartsCoreOption = { - grid: { - ...defaultGrid, - }, - tooltip: { - ...getDefaultTooltip(refs), - show: !inContextMenu, - trigger: 'item', - formatter: (params: any) => { - const [name, formattedValue, formattedPercent] = parseParams({ - params, - numberFormatter, - sanitizeName: true, - }); - if (params?.data?.isOther) { - return tooltipHtml(otherTooltipData, name); - } - return tooltipHtml( - [[metricLabel, formattedValue, formattedPercent]], - name, - ); - }, - }, - legend: { - ...getLegendProps( - effectiveLegendType, - legendOrientation, - showLegend, - theme, - ), - data: legendData, - }, - graphic: showTotal - ? { - type: 'text', - ...getTotalValuePadding({ chartPadding, donut, width, height }), - style: { - text: t('Total: %s', numberFormatter(totalValue)), - fontSize: 16, - fontWeight: 'bold', - fill: theme.colorText, - }, - z: 10, - } - : null, - series, - }; - - return { - formData, - width, - height, - echartOptions, - setDataMask, - labelMap, - groupby, - selectedValues, - onContextMenu, - refs, - emitCrossFilters, - coltypeMapping, - }; -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/utils.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/utils.ts deleted file mode 100644 index 36b67e43201..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/utils.ts +++ /dev/null @@ -1,22 +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 { CONTRIBUTION_SUFFIX } from './constants'; - -export const getContributionLabel = (metricLabel: string) => - `${metricLabel}${CONTRIBUTION_SUFFIX}`; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/EchartsRadar.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/EchartsRadar.tsx deleted file mode 100644 index e97b06000e6..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/EchartsRadar.tsx +++ /dev/null @@ -1,39 +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 { RadarChartTransformedProps } from './types'; -import Echart from '../components/Echart'; -import { allEventHandlers } from '../utils/eventHandlers'; - -export default function EchartsRadar(props: RadarChartTransformedProps) { - const { height, width, echartOptions, selectedValues, refs, formData } = - props; - const eventHandlers = allEventHandlers(props); - - return ( - - ); -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/buildQuery.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/buildQuery.ts deleted file mode 100644 index 604f7f2872e..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/buildQuery.ts +++ /dev/null @@ -1,47 +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 { - buildQueryContext, - QueryFormData, - ensureIsArray, -} from '@superset-ui/core'; - -export default function buildQuery(formData: QueryFormData) { - const { series_limit_metric } = formData; - const sortByMetric = ensureIsArray(series_limit_metric)[0]; - - return buildQueryContext(formData, baseQueryObject => { - let { metrics, orderby = [] } = baseQueryObject; - metrics = metrics || []; - // override orderby with timeseries metric - if (sortByMetric) { - orderby = [[sortByMetric, false]]; - } else if (metrics?.length > 0) { - // default to ordering by first metric in descending order - // when no "sort by" metric is set (regardless if "SORT DESC" is set to true) - orderby = [[metrics[0], false]]; - } - return [ - { - ...baseQueryObject, - orderby, - }, - ]; - }); -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/controlPanel.tsx deleted file mode 100644 index 0925962dde0..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/controlPanel.tsx +++ /dev/null @@ -1,256 +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 { t } from '@apache-superset/core/translation'; -import { - ChartDataResponseResult, - QueryFormMetric, - validateNumber, -} from '@superset-ui/core'; -import { GenericDataType } from '@apache-superset/core/common'; -import { - ControlPanelConfig, - ControlSubSectionHeader, - D3_FORMAT_DOCS, - D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT, - D3_FORMAT_OPTIONS, - D3_TIME_FORMAT_OPTIONS, - sharedControls, - ControlFormItemSpec, - getStandardizedControls, -} from '@superset-ui/chart-controls'; -import { DEFAULT_FORM_DATA } from './types'; -import { LabelPositionEnum } from '../types'; -import { legendSection } from '../controls'; - -const { labelType, labelPosition, numberFormat, showLabels, isCircle } = - DEFAULT_FORM_DATA; - -const radarMetricMaxValue: { name: string; config: ControlFormItemSpec } = { - name: 'radarMetricMaxValue', - config: { - controlType: 'InputNumber', - label: t('Max'), - description: t( - 'The maximum value of metrics. It is an optional configuration', - ), - width: 120, - placeholder: t('auto'), - debounceDelay: 400, - validators: [validateNumber], - }, -}; - -const radarMetricMinValue: { name: string; config: ControlFormItemSpec } = { - name: 'radarMetricMinValue', - config: { - controlType: 'InputNumber', - label: t('Min'), - description: t( - 'The minimum value of metrics. It is an optional configuration. If not set, it will be the minimum value of the data', - ), - defaultValue: '0', - width: 120, - placeholder: t('auto'), - debounceDelay: 400, - validators: [validateNumber], - }, -}; - -const getLabelPositionOptions = (): [LabelPositionEnum, string][] => [ - [LabelPositionEnum.Top, t('Top')], - [LabelPositionEnum.Left, t('Left')], - [LabelPositionEnum.Right, t('Right')], - [LabelPositionEnum.Bottom, t('Bottom')], - [LabelPositionEnum.Inside, t('Inside')], - [LabelPositionEnum.InsideLeft, t('Inside left')], - [LabelPositionEnum.InsideRight, t('Inside right')], - [LabelPositionEnum.InsideTop, t('Inside top')], - [LabelPositionEnum.InsideBottom, t('Inside bottom')], - [LabelPositionEnum.InsideTopLeft, t('Inside top left')], - [LabelPositionEnum.InsideBottomLeft, t('Inside bottom left')], - [LabelPositionEnum.InsideTopRight, t('Inside top right')], - [LabelPositionEnum.InsideBottomRight, t('Inside bottom right')], -]; - -const config: ControlPanelConfig = { - controlPanelSections: [ - { - label: t('Query'), - expanded: true, - controlSetRows: [ - ['groupby'], - ['metrics'], - ['timeseries_limit_metric'], - ['adhoc_filters'], - [ - { - name: 'row_limit', - config: { - ...sharedControls.row_limit, - default: 10, - }, - }, - ], - ], - }, - { - label: t('Chart Options'), - expanded: true, - controlSetRows: [ - ['color_scheme'], - ...legendSection, - [{t('Labels')}], - [ - { - name: 'show_labels', - config: { - type: 'CheckboxControl', - label: t('Show Labels'), - renderTrigger: true, - default: showLabels, - description: t('Whether to display the labels.'), - }, - }, - ], - [ - { - name: 'label_type', - config: { - type: 'SelectControl', - label: t('Label Type'), - default: labelType, - renderTrigger: true, - choices: [ - ['value', t('Value')], - ['key_value', t('Category and Value')], - ], - description: t('What should be shown on the label?'), - }, - }, - ], - [ - { - name: 'label_position', - config: { - type: 'SelectControl', - freeForm: false, - label: t('Label position'), - renderTrigger: true, - choices: getLabelPositionOptions(), - default: labelPosition, - description: D3_FORMAT_DOCS, - }, - }, - ], - [ - { - name: 'number_format', - config: { - type: 'SelectControl', - freeForm: true, - label: t('Number format'), - renderTrigger: true, - default: numberFormat, - choices: D3_FORMAT_OPTIONS, - description: `${D3_FORMAT_DOCS} ${D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT}`, - }, - }, - ], - [ - { - name: 'date_format', - config: { - type: 'SelectControl', - freeForm: true, - label: t('Date format'), - renderTrigger: true, - choices: D3_TIME_FORMAT_OPTIONS, - default: 'smart_date', - description: D3_FORMAT_DOCS, - }, - }, - ], - [{t('Radar')}], - [ - { - name: 'column_config', - config: { - type: 'ColumnConfigControl', - label: t('Customize Metrics'), - description: t('Further customize how to display each metric'), - renderTrigger: true, - configFormLayout: { - [GenericDataType.Numeric]: [ - [radarMetricMinValue, radarMetricMaxValue], - ], - }, - shouldMapStateToProps() { - return true; - }, - mapStateToProps(explore, _, chart) { - const values = - (explore?.controls?.metrics?.value as QueryFormMetric[]) ?? - []; - const metricColumn = values.map(value => { - if (typeof value === 'string') { - return value; - } - return value.label; - }); - const { colnames: _colnames, coltypes: _coltypes } = - chart?.queriesResponse?.[0] ?? {}; - const colnames: string[] = _colnames || []; - const coltypes: GenericDataType[] = _coltypes || []; - - return { - queryResponse: chart?.queriesResponse?.[0] as - | ChartDataResponseResult - | undefined, - appliedColumnNames: metricColumn, - columnsPropsObject: { colnames, coltypes }, - }; - }, - }, - }, - ], - [ - { - name: 'is_circle', - config: { - type: 'CheckboxControl', - label: t('Circle radar shape'), - renderTrigger: true, - default: isCircle, - description: t( - "Radar render type, whether to display 'circle' shape.", - ), - }, - }, - ], - ], - }, - ], - formDataOverrides: formData => ({ - ...formData, - metrics: getStandardizedControls().popAllMetrics(), - groupby: getStandardizedControls().popAllColumns(), - }), -}; - -export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/index.ts deleted file mode 100644 index 357ab9ae790..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/index.ts +++ /dev/null @@ -1,84 +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 - * g 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 { t } from '@apache-superset/core/translation'; -import { Behavior } from '@superset-ui/core'; -import buildQuery from './buildQuery'; -import controlPanel from './controlPanel'; -import transformProps from './transformProps'; -import thumbnail from './images/thumbnail.png'; -import thumbnailDark from './images/thumbnail-dark.png'; -import example1 from './images/example1.jpg'; -import example1Dark from './images/example1-dark.jpg'; -import example2 from './images/example2.jpg'; -import example2Dark from './images/example2-dark.jpg'; -import { EchartsRadarChartProps, EchartsRadarFormData } from './types'; -import { EchartsChartPlugin } from '../types'; - -export default class EchartsRadarChartPlugin extends EchartsChartPlugin< - EchartsRadarFormData, - EchartsRadarChartProps -> { - /** - * The constructor is used to pass relevant metadata and callbacks that get - * registered in respective registries that are used throughout the library - * and application. A more thorough description of each property is given in - * the respective imported file. - * - * It is worth noting that `buildQuery` and is optional, and only needed for - * advanced visualizations that require either post processing operations - * (pivoting, rolling aggregations, sorting etc) or submitting multiple queries. - */ - constructor() { - super({ - buildQuery, - controlPanel, - loadChart: () => import('./EchartsRadar'), - metadata: { - behaviors: [ - Behavior.InteractiveChart, - Behavior.DrillToDetail, - Behavior.DrillBy, - ], - category: t('Ranking'), - credits: ['https://echarts.apache.org'], - description: t( - 'Visualize a parallel set of metrics across multiple groups. Each group is visualized using its own line of points and each metric is represented as an edge in the chart.', - ), - exampleGallery: [ - { url: example1, urlDark: example1Dark }, - { url: example2, urlDark: example2Dark }, - ], - name: t('Radar Chart'), - tags: [ - t('Business'), - t('Comparison'), - t('Multi-Variables'), - t('Report'), - t('Web'), - t('ECharts'), - t('Featured'), - ], - thumbnail, - thumbnailDark, - }, - transformProps, - }); - } -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/index.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/index.tsx new file mode 100644 index 00000000000..83d6e8890a8 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/index.tsx @@ -0,0 +1,842 @@ +/** + * 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. + */ + +/** + * ECharts Radar Chart - Glyph Pattern Implementation + * + * Visualize a parallel set of metrics across multiple groups. + * Each group is visualized using its own line of points and + * each metric is represented as an edge in the chart. + */ + +import { t } from '@apache-superset/core/translation'; +import type { EChartsCoreOption } from 'echarts/core'; +import type { CallbackDataParams } from 'echarts/types/src/util/types'; +import type { RadarSeriesDataItemOption } from 'echarts/types/src/chart/radar/RadarSeries'; +import type { RadarSeriesOption } from 'echarts/charts'; +import { + Behavior, + buildQueryContext, + CategoricalColorNamespace, + DataRecord, + ensureIsArray, + ensureIsInt, + getColumnLabel, + getMetricLabel, + getNumberFormatter, + getTimeFormatter, + isDefined, + NumberFormatter, + QueryFormColumn, + QueryFormData, + QueryFormMetric, + SetDataMaskHook, + ContextMenuFilters, + validateNumber, + ChartDataResponseResult, +} from '@superset-ui/core'; +import { GenericDataType } from '@apache-superset/core/common'; +import { + ControlFormItemSpec, + D3_FORMAT_DOCS, + D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT, + D3_FORMAT_OPTIONS, + D3_TIME_FORMAT_OPTIONS, + getStandardizedControls, + sharedControls, +} from '@superset-ui/chart-controls'; + +import { + defineChart, + Metric, + Dimension, + Select, + Checkbox, + ChartProps, + createSelectedValuesMap, +} from '@superset-ui/glyph-core'; +import { allEventHandlers } from '../utils/eventHandlers'; + +import { defaultGrid } from '../defaults'; +import { + extractGroupbyLabel, + getChartPadding, + getColtypesMapping, + getLegendProps, +} from '../utils/series'; +import { DEFAULT_LEGEND_FORM_DATA, OpacityEnum } from '../constants'; +import { LabelPositionEnum } from '../types'; + +const LABEL_POSITION: [LabelPositionEnum, string][] = [ + [LabelPositionEnum.Top, t('Top')], + [LabelPositionEnum.Left, t('Left')], + [LabelPositionEnum.Right, t('Right')], + [LabelPositionEnum.Bottom, t('Bottom')], + [LabelPositionEnum.Inside, t('Inside')], + [LabelPositionEnum.InsideLeft, t('Inside left')], + [LabelPositionEnum.InsideRight, t('Inside right')], + [LabelPositionEnum.InsideTop, t('Inside top')], + [LabelPositionEnum.InsideBottom, t('Inside bottom')], + [LabelPositionEnum.InsideTopLeft, t('Inside top left')], + [LabelPositionEnum.InsideBottomLeft, t('Inside bottom left')], + [LabelPositionEnum.InsideTopRight, t('Inside top right')], + [LabelPositionEnum.InsideBottomRight, t('Inside bottom right')], +]; +import { getDefaultTooltip } from '../utils/tooltip'; +import { legendSection } from '../controls'; +import Echart from '../components/Echart'; +import { LegendOrientation, LegendType, Refs } from '../types'; + +import thumbnail from './images/thumbnail.png'; +import thumbnailDark from './images/thumbnail-dark.png'; +import example1 from './images/example1.jpg'; +import example1Dark from './images/example1-dark.jpg'; +import example2 from './images/example2.jpg'; +import example2Dark from './images/example2-dark.jpg'; + +// ============================================================================ +// Types +// ============================================================================ + +enum EchartsRadarLabelType { + Value = 'value', + KeyValue = 'key_value', +} + +type RadarColumnConfig = Record< + string, + { radarMetricMaxValue?: number | null; radarMetricMinValue?: number } +>; + +interface SeriesNormalizedMap { + [seriesName: string]: { [normalized: string]: number }; +} + +const DEFAULT_FORM_DATA = { + ...DEFAULT_LEGEND_FORM_DATA, + groupby: [], + labelType: EchartsRadarLabelType.Value, + labelPosition: LabelPositionEnum.Top, + legendOrientation: LegendOrientation.Top, + legendType: LegendType.Scroll, + numberFormat: 'SMART_NUMBER', + showLabels: true, + dateFormat: 'smart_date', + isCircle: false, +}; + +interface RadarTransformResult { + transformedProps: { + refs: Refs; + width: number; + height: number; + echartOptions: EChartsCoreOption; + formData: Record; + groupby: QueryFormColumn[]; + labelMap: Record; + setDataMask: SetDataMaskHook; + selectedValues: Record; + emitCrossFilters?: boolean; + onContextMenu?: ( + clientX: number, + clientY: number, + filters?: ContextMenuFilters, + ) => void; + coltypeMapping?: Record; + }; +} + +// ============================================================================ +// Helpers +// ============================================================================ + +function findGlobalMax( + data: Record[], + metrics: string[], +): number { + if (!data?.length || !metrics?.length) return 0; + + return data.reduce((globalMax, row) => { + const rowMax = metrics.reduce((max, metric) => { + const value = row[metric]; + return typeof value === 'number' && + Number.isFinite(value) && + !Number.isNaN(value) + ? Math.max(max, value) + : max; + }, 0); + + return Math.max(globalMax, rowMax); + }, 0); +} + +function renderNormalizedTooltip( + params: { color: string; name?: string; value: number[] }, + metrics: string[], + getDenormalizedValue: (seriesName: string, value: string) => number, + metricsWithCustomBounds: Set, +): string { + const { color, name = '', value: values } = params; + const seriesName = name || 'series0'; + + const colorDot = ``; + + const metricValues = metrics.map((metric, index) => { + const value = values[index]; + const originalValue = metricsWithCustomBounds.has(metric) + ? value + : getDenormalizedValue(name, String(value)); + + return { metric, value: originalValue }; + }); + + const tooltipRows = metricValues + .map( + ({ metric, value }) => ` +
+
${colorDot}${metric}:
+
${value}
+
+ `, + ) + .join(''); + + return ` +
${seriesName}
+ ${tooltipRows} + `; +} + +function formatLabel({ + params, + labelType, + numberFormatter, + getDenormalizedSeriesValue, + metricsWithCustomBounds, + metricLabels, +}: { + params: CallbackDataParams; + labelType: EchartsRadarLabelType; + numberFormatter: NumberFormatter; + getDenormalizedSeriesValue: (seriesName: string, value: string) => number; + metricsWithCustomBounds: Set; + metricLabels: string[]; +}): string { + const { name = '', value, dimensionIndex = 0 } = params; + const metricLabel = metricLabels[dimensionIndex]; + + const formattedValue = numberFormatter( + metricsWithCustomBounds.has(metricLabel) + ? (value as number) + : (getDenormalizedSeriesValue(name, String(value)) as number), + ); + + switch (labelType) { + case EchartsRadarLabelType.Value: + return formattedValue; + case EchartsRadarLabelType.KeyValue: + return `${name}: ${formattedValue}`; + default: + return name; + } +} + +// ============================================================================ +// Additional Controls Configuration +// ============================================================================ + +const radarMetricMaxValue: { name: string; config: ControlFormItemSpec } = { + name: 'radarMetricMaxValue', + config: { + controlType: 'InputNumber', + label: t('Max'), + description: t( + 'The maximum value of metrics. It is an optional configuration', + ), + width: 120, + placeholder: t('auto'), + debounceDelay: 400, + validators: [validateNumber], + }, +}; + +const radarMetricMinValue: { name: string; config: ControlFormItemSpec } = { + name: 'radarMetricMinValue', + config: { + controlType: 'InputNumber', + label: t('Min'), + description: t( + 'The minimum value of metrics. It is an optional configuration. If not set, it will be the minimum value of the data', + ), + defaultValue: '0', + width: 120, + placeholder: t('auto'), + debounceDelay: 400, + validators: [validateNumber], + }, +}; + +// ============================================================================ +// Build Query +// ============================================================================ + +export function buildQuery(formData: QueryFormData) { + const seriesLimitMetric = formData.series_limit_metric; + const sortByMetric = ensureIsArray(seriesLimitMetric)[0]; + + return buildQueryContext(formData, baseQueryObject => { + let { metrics, orderby = [] } = baseQueryObject; + metrics = metrics || []; + if (sortByMetric) { + orderby = [[sortByMetric, false]]; + } else if (metrics?.length > 0) { + orderby = [[metrics[0], false]]; + } + return [ + { + ...baseQueryObject, + orderby, + }, + ]; + }); +} + +// ============================================================================ +// Chart Definition +// ============================================================================ + +export default defineChart({ + metadata: { + name: t('Radar Chart'), + description: t( + 'Visualize a parallel set of metrics across multiple groups. Each group is visualized using its own line of points and each metric is represented as an edge in the chart.', + ), + category: t('Ranking'), + tags: [ + t('Business'), + t('Comparison'), + t('Multi-Variables'), + t('Report'), + t('Web'), + t('ECharts'), + t('Featured'), + ], + behaviors: [ + Behavior.InteractiveChart, + Behavior.DrillToDetail, + Behavior.DrillBy, + ], + credits: ['https://echarts.apache.org'], + thumbnail, + thumbnailDark, + exampleGallery: [ + { url: example1, urlDark: example1Dark }, + { url: example2, urlDark: example2Dark }, + ], + }, + + arguments: { + groupby: Dimension.with({ + label: t('Dimensions'), + description: t('Columns to group by'), + multi: true, + }), + + metrics: Metric.with({ + label: t('Metrics'), + description: t('Metrics to display'), + multi: true, + }), + + showLabels: Checkbox.with({ + label: t('Show Labels'), + description: t('Whether to display the labels.'), + default: true, + }), + + labelType: Select.with({ + label: t('Label Type'), + description: t('What should be shown on the label?'), + options: [ + { label: t('Value'), value: 'value' }, + { label: t('Category and Value'), value: 'key_value' }, + ], + default: 'value', + }), + + labelPosition: Select.with({ + label: t('Label position'), + description: D3_FORMAT_DOCS, + options: LABEL_POSITION.map(([value, label]) => ({ + value: value as string, + label: label as string, + })), + default: 'top', + }), + + numberFormat: Select.with({ + label: t('Number format'), + description: `${D3_FORMAT_DOCS} ${D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT}`, + options: D3_FORMAT_OPTIONS.map(([value, label]) => ({ + value: value as string, + label: label as string, + })), + default: 'SMART_NUMBER', + }), + + dateFormat: Select.with({ + label: t('Date format'), + description: D3_FORMAT_DOCS, + options: D3_TIME_FORMAT_OPTIONS.map(([value, label]) => ({ + value: value as string, + label: label as string, + })), + default: 'smart_date', + }), + + isCircle: Checkbox.with({ + label: t('Circle radar shape'), + description: t("Radar render type, whether to display 'circle' shape."), + default: false, + }), + }, + + additionalControls: { + query: [ + ['timeseries_limit_metric'], + ['adhoc_filters'], + [ + { + name: 'row_limit', + config: { + ...sharedControls.row_limit, + default: 10, + }, + }, + ], + ], + chartOptions: [ + ['color_scheme'], + ...legendSection, + [ + { + name: 'column_config', + config: { + type: 'ColumnConfigControl', + label: t('Customize Metrics'), + description: t('Further customize how to display each metric'), + renderTrigger: true, + configFormLayout: { + [GenericDataType.Numeric]: [ + [radarMetricMinValue, radarMetricMaxValue], + ], + }, + shouldMapStateToProps() { + return true; + }, + mapStateToProps( + explore: { + controls?: { metrics?: { value?: QueryFormMetric[] } }; + }, + _: unknown, + chart: { queriesResponse?: ChartDataResponseResult[] }, + ) { + const values = explore?.controls?.metrics?.value ?? []; + const metricColumn = values.map(value => { + if (typeof value === 'string') { + return value; + } + return value.label; + }); + const { colnames: _colnames, coltypes: _coltypes } = + chart?.queriesResponse?.[0] ?? {}; + const colnames: string[] = _colnames || []; + const coltypes: GenericDataType[] = _coltypes || []; + + return { + queryResponse: chart?.queriesResponse?.[0] as + | ChartDataResponseResult + | undefined, + appliedColumnNames: metricColumn, + columnsPropsObject: { colnames, coltypes }, + }; + }, + }, + }, + ], + ], + }, + + formDataOverrides: (formData: QueryFormData) => ({ + ...formData, + metrics: getStandardizedControls().popAllMetrics(), + groupby: getStandardizedControls().popAllColumns(), + }), + + buildQuery, + + transform: (chartProps: ChartProps): RadarTransformResult => { + const { + width, + height, + rawFormData, + hooks, + filterState, + queriesData, + theme, + inContextMenu, + emitCrossFilters, + } = chartProps; + + const refs: Refs = {}; + const { data = [] } = queriesData[0]; + const globalMax = findGlobalMax( + data as Record[], + Object.keys(data[0] || {}), + ); + const coltypeMapping = getColtypesMapping( + queriesData[0] as unknown as Parameters[0], + ); + const { setDataMask = () => {}, onContextMenu } = hooks ?? {}; + + // Extract form values with defaults + const colorScheme = rawFormData.color_scheme as string; + const groupby = (rawFormData.groupby as QueryFormColumn[]) || []; + const metrics = (rawFormData.metrics as QueryFormMetric[]) || []; + const labelType = + (rawFormData.label_type as EchartsRadarLabelType) || + DEFAULT_FORM_DATA.labelType; + const labelPosition = + (rawFormData.label_position as LabelPositionEnum) || + DEFAULT_FORM_DATA.labelPosition; + const legendOrientation = + (rawFormData.legend_orientation as LegendOrientation) || + DEFAULT_FORM_DATA.legendOrientation; + const legendType = + (rawFormData.legend_type as LegendType) || DEFAULT_FORM_DATA.legendType; + const legendMargin = rawFormData.legend_margin as number | undefined; + const numberFormat = + (rawFormData.number_format as string) || DEFAULT_FORM_DATA.numberFormat; + const dateFormat = + (rawFormData.date_format as string) || DEFAULT_FORM_DATA.dateFormat; + const showLabels = + rawFormData.show_labels !== undefined + ? (rawFormData.show_labels as boolean) + : DEFAULT_FORM_DATA.showLabels; + const showLegend = + rawFormData.show_legend !== undefined + ? (rawFormData.show_legend as boolean) + : DEFAULT_FORM_DATA.showLegend; + const legendSort = rawFormData.legend_sort as string | undefined; + const isCircle = + rawFormData.is_circle !== undefined + ? (rawFormData.is_circle as boolean) + : DEFAULT_FORM_DATA.isCircle; + const columnConfig = rawFormData.column_config as + | RadarColumnConfig + | undefined; + const sliceId = rawFormData.slice_id as number | undefined; + + const colorFn = CategoricalColorNamespace.getScale(colorScheme as string); + const numberFormatter = getNumberFormatter(numberFormat); + const denormalizedSeriesValues: SeriesNormalizedMap = {}; + + const getDenormalizedSeriesValue = ( + seriesName: string, + normalizedValue: string, + ): number => + denormalizedSeriesValues?.[seriesName]?.[normalizedValue] ?? + Number(normalizedValue); + + const metricLabels = metrics.map(getMetricLabel); + + const metricsWithCustomBounds = new Set( + metricLabels.filter(metricLabel => { + const config = columnConfig?.[metricLabel]; + const hasMax = !!isDefined(config?.radarMetricMaxValue); + const hasMin = + isDefined(config?.radarMetricMinValue) && + config?.radarMetricMinValue !== 0; + return hasMax || hasMin; + }), + ); + + const formatter = (params: CallbackDataParams) => + formatLabel({ + params, + numberFormatter, + labelType, + getDenormalizedSeriesValue, + metricsWithCustomBounds, + metricLabels, + }); + + const groupbyLabels = groupby.map(getColumnLabel); + + const metricLabelAndMaxValueMap = new Map(); + const metricLabelAndMinValueMap = new Map(); + const columnsLabelMap = new Map(); + const transformedData: RadarSeriesDataItemOption[] = []; + + (data as Record[]).forEach(datum => { + const joinedName = extractGroupbyLabel({ + datum: datum as DataRecord, + groupby: groupbyLabels, + coltypeMapping, + timeFormatter: getTimeFormatter(dateFormat), + }); + + columnsLabelMap.set( + joinedName, + groupbyLabels.map(col => datum[col] as string), + ); + + for (const [metricLabel, value] of Object.entries(datum)) { + if (metricLabelAndMaxValueMap.has(metricLabel)) { + metricLabelAndMaxValueMap.set( + metricLabel, + Math.max( + value as number, + ensureIsInt( + metricLabelAndMaxValueMap.get(metricLabel), + Number.MIN_SAFE_INTEGER, + ), + ), + ); + } else { + metricLabelAndMaxValueMap.set(metricLabel, value as number); + } + + if (metricLabelAndMinValueMap.has(metricLabel)) { + metricLabelAndMinValueMap.set( + metricLabel, + Math.min( + value as number, + ensureIsInt( + metricLabelAndMinValueMap.get(metricLabel), + Number.MAX_SAFE_INTEGER, + ), + ), + ); + } else { + metricLabelAndMinValueMap.set(metricLabel, value as number); + } + } + + const isFiltered = + filterState?.selectedValues && + !filterState.selectedValues.includes(joinedName); + + transformedData.push({ + value: metricLabels.map(metricLabel => datum[metricLabel]), + name: joinedName, + itemStyle: { + color: colorFn(joinedName, sliceId), + opacity: isFiltered + ? OpacityEnum.Transparent + : OpacityEnum.NonTransparent, + }, + lineStyle: { + opacity: isFiltered + ? OpacityEnum.SemiTransparent + : OpacityEnum.NonTransparent, + }, + label: { + show: showLabels, + position: labelPosition, + formatter, + }, + } as RadarSeriesDataItemOption); + }); + + const seriesNames = transformedData.map(d => d.name as string); + const selectedValues = createSelectedValuesMap(filterState, seriesNames); + + const normalizeArray = (arr: number[], decimals = 10, seriesName: string) => + arr.map((value, index) => { + const metricLabel = metricLabels[index]; + if (metricsWithCustomBounds.has(metricLabel)) { + return value; + } + + const max = Math.max(...arr); + const normalizedValue = Number((value / max).toFixed(decimals)); + + denormalizedSeriesValues[seriesName][String(normalizedValue)] = value; + return normalizedValue; + }); + + const normalizedTransformedData = transformedData.map(series => { + if (Array.isArray(series.value)) { + const seriesName = String(series?.name || ''); + denormalizedSeriesValues[seriesName] = {}; + + return { + ...series, + value: normalizeArray(series.value as number[], 10, seriesName), + }; + } + return series; + }); + + const indicator = metricLabels.map(metricLabel => { + const isMetricWithCustomBounds = metricsWithCustomBounds.has(metricLabel); + if (!isMetricWithCustomBounds) { + return { + name: metricLabel, + max: 1, + min: 0, + }; + } + const maxValueInControl = + columnConfig?.[metricLabel]?.radarMetricMaxValue; + const minValueInControl = + columnConfig?.[metricLabel]?.radarMetricMinValue; + + const maxValue = + metricLabelAndMaxValueMap.get(metricLabel) === 0 + ? Number.MAX_SAFE_INTEGER + : globalMax; + const max = isDefined(maxValueInControl) ? maxValueInControl : maxValue; + + let min: number; + if (isDefined(minValueInControl)) { + min = minValueInControl; + } else { + min = 0; + } + + return { + name: metricLabel, + max, + min, + }; + }); + + const series: RadarSeriesOption[] = [ + { + type: 'radar', + ...getChartPadding(showLegend, legendOrientation, legendMargin), + animation: false, + emphasis: { + label: { + show: true, + fontWeight: 'bold', + }, + }, + data: normalizedTransformedData, + }, + ]; + + const normalizedTooltipFormatter = ( + params: CallbackDataParams & { + color: string; + name: string; + value: number[]; + }, + ) => + renderNormalizedTooltip( + params, + metricLabels, + getDenormalizedSeriesValue, + metricsWithCustomBounds, + ); + + const echartOptions: EChartsCoreOption = { + grid: { + ...defaultGrid, + }, + tooltip: { + ...getDefaultTooltip(refs), + show: !inContextMenu, + trigger: 'item', + formatter: normalizedTooltipFormatter, + }, + legend: { + ...getLegendProps(legendType, legendOrientation, showLegend, theme), + data: Array.from(columnsLabelMap.keys()).sort( + (a: string, b: string) => { + if (!legendSort) return 0; + return legendSort === 'asc' + ? a.localeCompare(b) + : b.localeCompare(a); + }, + ), + }, + series, + radar: { + shape: isCircle ? 'circle' : 'polygon', + indicator, + splitLine: { + show: true, + lineStyle: { + color: theme.colorSplit, + }, + }, + splitArea: { + show: true, + areaStyle: { + color: [theme.colorBgLayout, theme.colorBgContainer], + }, + }, + axisLine: { + lineStyle: { + color: theme.colorSplit, + }, + }, + }, + }; + + return { + transformedProps: { + refs, + width, + height, + echartOptions, + formData: rawFormData, + groupby, + labelMap: Object.fromEntries(columnsLabelMap), + setDataMask, + selectedValues, + emitCrossFilters, + onContextMenu, + coltypeMapping, + }, + }; + }, + + render: ({ transformedProps }) => { + const { height, width, echartOptions, selectedValues, refs, formData } = + transformedProps; + + const eventHandlers = allEventHandlers(transformedProps); + + return ( + + ); + }, +}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/stories/Radar.stories.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/stories/Radar.stories.tsx deleted file mode 100644 index 37eeadb2551..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/stories/Radar.stories.tsx +++ /dev/null @@ -1,128 +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 { - SuperChart, - VizType, - getChartTransformPropsRegistry, -} from '@superset-ui/core'; -import { - EchartsRadarChartPlugin, - RadarTransformProps, -} from '@superset-ui/plugin-chart-echarts'; -import { withResizableChartDemo } from '@storybook-shared'; -import { basic } from './data'; - -new EchartsRadarChartPlugin().configure({ key: VizType.Radar }).register(); - -getChartTransformPropsRegistry().registerValue( - VizType.Radar, - RadarTransformProps, -); - -export default { - title: 'Chart Plugins/plugin-chart-echarts/Radar', - decorators: [withResizableChartDemo], - args: { - colorScheme: 'supersetColors', - showLegend: true, - isCircle: false, - labelType: 'key', - showLabels: true, - numberFormat: 'SMART_NUMBER', - }, - argTypes: { - colorScheme: { - control: 'select', - options: [ - 'supersetColors', - 'd3Category10', - 'bnbColors', - 'googleCategory20c', - ], - }, - showLegend: { control: 'boolean' }, - isCircle: { - control: 'boolean', - description: 'If true, radar shape is circle; otherwise polygon', - }, - labelType: { - control: 'select', - options: ['key', 'value', 'percent', 'key_value', 'key_percent'], - }, - showLabels: { control: 'boolean' }, - numberFormat: { - control: 'select', - options: ['SMART_NUMBER', '.2f', '.0%', '$,.2f', '.3s'], - }, - }, -}; - -export const Radar = ({ - width, - height, - colorScheme, - showLegend, - isCircle, - labelType, - showLabels, - numberFormat, -}: { - width: number; - height: number; - colorScheme: string; - showLegend: boolean; - isCircle: boolean; - labelType: string; - showLabels: boolean; - numberFormat: string; -}) => ( - -); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/transformProps.ts deleted file mode 100644 index a906ee9b177..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/transformProps.ts +++ /dev/null @@ -1,420 +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 { - CategoricalColorNamespace, - ensureIsInt, - getColumnLabel, - getMetricLabel, - getNumberFormatter, - getTimeFormatter, - NumberFormatter, - isDefined, -} from '@superset-ui/core'; -import type { CallbackDataParams } from 'echarts/types/src/util/types'; -import type { RadarSeriesDataItemOption } from 'echarts/types/src/chart/radar/RadarSeries'; -import type { EChartsCoreOption } from 'echarts/core'; -import type { RadarSeriesOption } from 'echarts/charts'; -import { - DEFAULT_FORM_DATA as DEFAULT_RADAR_FORM_DATA, - EchartsRadarChartProps, - EchartsRadarFormData, - EchartsRadarLabelType, - RadarChartTransformedProps, - SeriesNormalizedMap, -} from './types'; -import { DEFAULT_LEGEND_FORM_DATA, OpacityEnum } from '../constants'; -import { - extractGroupbyLabel, - getChartPadding, - getColtypesMapping, - getLegendProps, -} from '../utils/series'; -import { resolveLegendLayout } from '../utils/legendLayout'; -import { defaultGrid } from '../defaults'; -import { Refs } from '../types'; -import { getDefaultTooltip } from '../utils/tooltip'; -import { findGlobalMax, renderNormalizedTooltip } from './utils'; - -export function formatLabel({ - params, - labelType, - numberFormatter, - getDenormalizedSeriesValue, - metricsWithCustomBounds, - metricLabels, -}: { - params: CallbackDataParams; - labelType: EchartsRadarLabelType; - numberFormatter: NumberFormatter; - getDenormalizedSeriesValue: (seriesName: string, value: string) => number; - metricsWithCustomBounds: Set; - metricLabels: string[]; -}): string { - const { name = '', value, dimensionIndex = 0 } = params; - const metricLabel = metricLabels[dimensionIndex]; - - const formattedValue = numberFormatter( - metricsWithCustomBounds.has(metricLabel) - ? (value as number) - : (getDenormalizedSeriesValue(name, String(value)) as number), - ); - - switch (labelType) { - case EchartsRadarLabelType.Value: - return formattedValue; - case EchartsRadarLabelType.KeyValue: - return `${name}: ${formattedValue}`; - default: - return name; - } -} - -export default function transformProps( - chartProps: EchartsRadarChartProps, -): RadarChartTransformedProps { - const { - formData, - height, - hooks, - filterState, - queriesData, - width, - theme, - inContextMenu, - emitCrossFilters, - } = chartProps; - const refs: Refs = {}; - const { data = [] } = queriesData[0]; - const globalMax = findGlobalMax(data, Object.keys(data[0] || {})); - const coltypeMapping = getColtypesMapping(queriesData[0]); - - const { - colorScheme, - groupby, - labelType, - labelPosition, - legendOrientation, - legendType, - legendMargin, - metrics = [], - numberFormat, - dateFormat, - showLabels, - showLegend, - legendSort, - isCircle, - columnConfig, - sliceId, - }: EchartsRadarFormData = { - ...DEFAULT_LEGEND_FORM_DATA, - ...DEFAULT_RADAR_FORM_DATA, - ...formData, - }; - const { setDataMask = () => {}, onContextMenu } = hooks; - const colorFn = CategoricalColorNamespace.getScale(colorScheme as string); - const numberFormatter = getNumberFormatter(numberFormat); - const denormalizedSeriesValues: SeriesNormalizedMap = {}; - - const getDenormalizedSeriesValue = ( - seriesName: string, - normalizedValue: string, - ): number => - denormalizedSeriesValues?.[seriesName]?.[normalizedValue] ?? - Number(normalizedValue); - - const metricLabels = metrics.map(getMetricLabel); - - const metricsWithCustomBounds = new Set( - metricLabels.filter(metricLabel => { - const config = columnConfig?.[metricLabel]; - const hasMax = !!isDefined(config?.radarMetricMaxValue); - const hasMin = - isDefined(config?.radarMetricMinValue) && - config?.radarMetricMinValue !== 0; - return hasMax || hasMin; - }), - ); - - const formatter = (params: CallbackDataParams) => - formatLabel({ - params, - numberFormatter, - labelType, - getDenormalizedSeriesValue, - metricsWithCustomBounds, - metricLabels, - }); - - const groupbyLabels = groupby.map(getColumnLabel); - - const metricLabelAndMaxValueMap = new Map(); - const metricLabelAndMinValueMap = new Map(); - const columnsLabelMap = new Map(); - const transformedData: RadarSeriesDataItemOption[] = []; - data.forEach(datum => { - const joinedName = extractGroupbyLabel({ - datum, - groupby: groupbyLabels, - coltypeMapping, - timeFormatter: getTimeFormatter(dateFormat), - }); - // map(joined_name: [columnLabel_1, columnLabel_2, ...]) - columnsLabelMap.set( - joinedName, - groupbyLabels.map(col => datum[col] as string), - ); - - // put max value of series into metricLabelAndMaxValueMap - // eslint-disable-next-line no-restricted-syntax - for (const [metricLabel, value] of Object.entries(datum)) { - if (metricLabelAndMaxValueMap.has(metricLabel)) { - metricLabelAndMaxValueMap.set( - metricLabel, - Math.max( - value as number, - ensureIsInt( - metricLabelAndMaxValueMap.get(metricLabel), - Number.MIN_SAFE_INTEGER, - ), - ), - ); - } else { - metricLabelAndMaxValueMap.set(metricLabel, value as number); - } - - if (metricLabelAndMinValueMap.has(metricLabel)) { - metricLabelAndMinValueMap.set( - metricLabel, - Math.min( - value as number, - ensureIsInt( - metricLabelAndMinValueMap.get(metricLabel), - Number.MAX_SAFE_INTEGER, - ), - ), - ); - } else { - metricLabelAndMinValueMap.set(metricLabel, value as number); - } - } - - const isFiltered = - filterState.selectedValues && - !filterState.selectedValues.includes(joinedName); - - // generate transformedData - transformedData.push({ - value: metricLabels.map(metricLabel => datum[metricLabel]), - name: joinedName, - itemStyle: { - color: colorFn(joinedName, sliceId), - opacity: isFiltered - ? OpacityEnum.Transparent - : OpacityEnum.NonTransparent, - }, - lineStyle: { - opacity: isFiltered - ? OpacityEnum.SemiTransparent - : OpacityEnum.NonTransparent, - }, - label: { - show: showLabels, - position: labelPosition, - formatter, - }, - } as RadarSeriesDataItemOption); - }); - - const selectedValues = (filterState.selectedValues || []).reduce( - (acc: Record, selectedValue: string) => { - const index = transformedData.findIndex( - ({ name }) => name === selectedValue, - ); - return { - ...acc, - [index]: selectedValue, - }; - }, - {}, - ); - - const normalizeArray = (arr: number[], decimals = 10, seriesName: string) => - arr.map((value, index) => { - const metricLabel = metricLabels[index]; - if (metricsWithCustomBounds.has(metricLabel)) { - return value; - } - - const max = Math.max(...arr); - const normalizedValue = Number((value / max).toFixed(decimals)); - - denormalizedSeriesValues[seriesName][String(normalizedValue)] = value; - return normalizedValue; - }); - - // Normalize the transformed data - const normalizedTransformedData = transformedData.map(series => { - if (Array.isArray(series.value)) { - const seriesName = String(series?.name || ''); - denormalizedSeriesValues[seriesName] = {}; - - return { - ...series, - value: normalizeArray(series.value as number[], 10, seriesName), - }; - } - return series; - }); - - const indicator = metricLabels.map(metricLabel => { - const isMetricWithCustomBounds = metricsWithCustomBounds.has(metricLabel); - if (!isMetricWithCustomBounds) { - return { - name: metricLabel, - max: 1, - min: 0, - }; - } - const maxValueInControl = columnConfig?.[metricLabel]?.radarMetricMaxValue; - const minValueInControl = columnConfig?.[metricLabel]?.radarMetricMinValue; - - // Ensure that 0 is at the center of the polar coordinates - const maxValue = - metricLabelAndMaxValueMap.get(metricLabel) === 0 - ? Number.MAX_SAFE_INTEGER - : globalMax; - const max = isDefined(maxValueInControl) ? maxValueInControl : maxValue; - - let min: number; - if (isDefined(minValueInControl)) { - min = minValueInControl; - } else { - min = 0; - } - - return { - name: metricLabel, - max, - min, - }; - }); - const legendData = Array.from(columnsLabelMap.keys()).sort( - (a: string, b: string) => { - if (!legendSort) return 0; - return legendSort === 'asc' ? a.localeCompare(b) : b.localeCompare(a); - }, - ); - const { effectiveLegendMargin, effectiveLegendType } = resolveLegendLayout({ - chartHeight: height, - chartWidth: width, - legendItems: legendData, - legendMargin, - orientation: legendOrientation, - show: showLegend, - theme, - type: legendType, - }); - - const series: RadarSeriesOption[] = [ - { - type: 'radar', - ...getChartPadding(showLegend, legendOrientation, effectiveLegendMargin), - animation: false, - emphasis: { - label: { - show: true, - fontWeight: 'bold', - }, - }, - data: normalizedTransformedData, - }, - ]; - - const NormalizedTooltipFormater = ( - params: CallbackDataParams & { - color: string; - name: string; - value: number[]; - }, - ) => - renderNormalizedTooltip( - params, - metricLabels, - getDenormalizedSeriesValue, - metricsWithCustomBounds, - ); - - const echartOptions: EChartsCoreOption = { - grid: { - ...defaultGrid, - }, - tooltip: { - ...getDefaultTooltip(refs), - show: !inContextMenu, - trigger: 'item', - formatter: NormalizedTooltipFormater, - }, - legend: { - ...getLegendProps( - effectiveLegendType, - legendOrientation, - showLegend, - theme, - ), - data: legendData, - }, - series, - radar: { - shape: isCircle ? 'circle' : 'polygon', - indicator, - splitLine: { - show: true, - lineStyle: { - color: theme.colorSplit, - }, - }, - splitArea: { - show: true, - areaStyle: { - color: [theme.colorBgLayout, theme.colorBgContainer], - }, - }, - axisLine: { - lineStyle: { - color: theme.colorSplit, - }, - }, - }, - }; - - return { - formData, - width, - height, - echartOptions, - emitCrossFilters, - setDataMask, - labelMap: Object.fromEntries(columnsLabelMap), - groupby, - selectedValues, - onContextMenu, - refs, - coltypeMapping, - }; -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/types.ts deleted file mode 100644 index c3d5dd6cee4..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/types.ts +++ /dev/null @@ -1,99 +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 { - QueryFormColumn, - QueryFormData, - QueryFormMetric, -} from '@superset-ui/core'; -import { - BaseChartProps, - BaseTransformedProps, - ContextMenuTransformedProps, - CrossFilterTransformedProps, - LegendFormData, - LabelPositionEnum, - LegendOrientation, - LegendType, -} from '../types'; -import { DEFAULT_LEGEND_FORM_DATA } from '../constants'; - -type RadarColumnConfig = Record< - string, - { radarMetricMaxValue?: number | null; radarMetricMinValue?: number } ->; - -export type EchartsRadarFormData = QueryFormData & - LegendFormData & { - colorScheme?: string; - columnConfig?: RadarColumnConfig; - currentOwnValue?: string[] | null; - currentValue?: string[] | null; - defaultValue?: string[] | null; - groupby: QueryFormColumn[]; - labelType: EchartsRadarLabelType; - labelPosition: LabelPositionEnum; - metrics: QueryFormMetric[]; - showLabels: boolean; - isCircle: boolean; - numberFormat: string; - dateFormat: string; - isNormalized: boolean; - }; - -export enum EchartsRadarLabelType { - Value = 'value', - KeyValue = 'key_value', -} - -export interface EchartsRadarChartProps extends BaseChartProps { - formData: EchartsRadarFormData; -} - -// @ts-expect-error -export const DEFAULT_FORM_DATA: EchartsRadarFormData = { - ...DEFAULT_LEGEND_FORM_DATA, - groupby: [], - labelType: EchartsRadarLabelType.Value, - labelPosition: LabelPositionEnum.Top, - legendOrientation: LegendOrientation.Top, - legendType: LegendType.Scroll, - numberFormat: 'SMART_NUMBER', - showLabels: true, - dateFormat: 'smart_date', - isCircle: false, -}; - -export type RadarChartTransformedProps = - BaseTransformedProps & - ContextMenuTransformedProps & - CrossFilterTransformedProps; - -/** - * Represents a mapping from a normalized value (as string) to an original numeric value. - */ -interface NormalizedValueMap { - [normalized: string]: number; -} - -/** - * Represents a collection of series, each containing its own NormalizedValueMap. - */ -export interface SeriesNormalizedMap { - [seriesName: string]: NormalizedValueMap; -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/utils.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/utils.ts deleted file mode 100644 index 343d9bbd392..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/utils.ts +++ /dev/null @@ -1,92 +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. - */ -/* - function for finding the max metric values among all series data for Radar Chart -*/ -export const findGlobalMax = ( - data: Record[], - metrics: string[], -): number => { - if (!data?.length || !metrics?.length) return 0; - - return data.reduce((globalMax, row) => { - const rowMax = metrics.reduce((max, metric) => { - const value = row[metric]; - return typeof value === 'number' && - Number.isFinite(value) && - !Number.isNaN(value) - ? Math.max(max, value) - : max; - }, 0); - - return Math.max(globalMax, rowMax); - }, 0); -}; - -interface TooltipParams { - color: string; - name?: string; - value: number[]; -} - -interface TooltipMetricValue { - metric: string; - value: number; -} - -export const renderNormalizedTooltip = ( - params: TooltipParams, - metrics: string[], - getDenormalizedValue: (seriesName: string, value: string) => number, - metricsWithCustomBounds: Set, -): string => { - const { color, name = '', value: values } = params; - const seriesName = name || 'series0'; - - const colorDot = ``; - - // Get metric values with denormalization if needed - const metricValues: TooltipMetricValue[] = metrics.map((metric, index) => { - const value = values[index]; - const originalValue = metricsWithCustomBounds.has(metric) - ? value - : getDenormalizedValue(name, String(value)); - - return { - metric, - value: originalValue, - }; - }); - - const tooltipRows = metricValues - .map( - ({ metric, value }) => ` -
-
${colorDot}${metric}:
-
${value}
-
- `, - ) - .join(''); - - return ` -
${seriesName}
- ${tooltipRows} - `; -}; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/Sankey.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/Sankey.tsx deleted file mode 100644 index 88c5b14f93d..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/Sankey.tsx +++ /dev/null @@ -1,34 +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 { SankeyTransformedProps } from './types'; -import Echart from '../components/Echart'; - -export default function Sankey(props: SankeyTransformedProps) { - const { height, width, echartOptions, refs, formData } = props; - - return ( - - ); -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/buildQuery.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/buildQuery.ts deleted file mode 100644 index 5a573e29af8..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/buildQuery.ts +++ /dev/null @@ -1,45 +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 { buildQueryContext, QueryFormOrderBy } from '@superset-ui/core'; -import { SankeyFormData } from './types'; - -export default function buildQuery(formData: SankeyFormData) { - const { metric, sort_by_metric, source, target, row_limit } = formData; - const groupby = [source, target]; - const orderby: QueryFormOrderBy[] = []; - const shouldApplyOrderBy = - row_limit !== undefined && row_limit !== null && row_limit !== 0; - - if (sort_by_metric && metric) { - orderby.push([metric, false]); - } - [source, target].forEach(column => { - if (column) { - orderby.push([column, true]); - } - }); - - return buildQueryContext(formData, baseQueryObject => [ - { - ...baseQueryObject, - groupby, - ...(shouldApplyOrderBy && orderby.length > 0 && { orderby }), - }, - ]); -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/controlPanel.tsx deleted file mode 100644 index 4135e1097fa..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/controlPanel.tsx +++ /dev/null @@ -1,76 +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 { t } from '@apache-superset/core/translation'; -import { validateNonEmpty } from '@superset-ui/core'; -import { - ControlPanelConfig, - dndGroupByControl, -} from '@superset-ui/chart-controls'; - -const config: ControlPanelConfig = { - controlPanelSections: [ - { - label: t('Query'), - expanded: true, - controlSetRows: [ - [ - { - name: 'source', - config: { - ...dndGroupByControl, - label: t('Source'), - multi: false, - description: t( - 'The column to be used as the source of the edge.', - ), - validators: [validateNonEmpty], - freeForm: false, - }, - }, - ], - [ - { - name: 'target', - config: { - ...dndGroupByControl, - label: t('Target'), - multi: false, - description: t( - 'The column to be used as the target of the edge.', - ), - validators: [validateNonEmpty], - freeForm: false, - }, - }, - ], - ['metric'], - ['adhoc_filters'], - ['row_limit'], - ['sort_by_metric'], - ], - }, - { - label: t('Chart Options'), - expanded: true, - controlSetRows: [['color_scheme']], - }, - ], -}; - -export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/index.ts deleted file mode 100644 index e473bf096b4..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/index.ts +++ /dev/null @@ -1,74 +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 - * regardin - * g 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 { t } from '@apache-superset/core/translation'; -import { ChartMetadata, ChartPlugin } from '@superset-ui/core'; -import buildQuery from './buildQuery'; -import controlPanel from './controlPanel'; -import transformProps from './transformProps'; -import thumbnail from './images/thumbnail.png'; -import thumbnailDark from './images/thumbnail-dark.png'; -import example1 from './images/example1.png'; -import example1Dark from './images/example1-dark.png'; -import example2 from './images/example2.png'; -import example2Dark from './images/example2-dark.png'; -import { SankeyChartProps, SankeyFormData } from './types'; - -// TODO: Implement cross filtering -export default class EchartsSankeyChartPlugin extends ChartPlugin< - SankeyFormData, - SankeyChartProps -> { - /** - * The constructor is used to pass relevant metadata and callbacks that get - * registered in respective registries that are used throughout the library - * and application. A more thorough description of each property is given in - * the respective imported file. - * - * It is worth noting that `buildQuery` and is optional, and only needed for - * advanced visualizations that require either post processing operations - * (pivoting, rolling aggregations, sorting etc) or submitting multiple queries. - */ - constructor() { - super({ - buildQuery, - controlPanel, - loadChart: () => import('./Sankey'), - metadata: new ChartMetadata({ - credits: ['https://echarts.apache.org'], - category: t('Flow'), - description: t( - `The Sankey chart visually tracks the movement and transformation of values across - system stages. Nodes represent stages, connected by links depicting value flow. Node - height corresponds to the visualized metric, providing a clear representation of - value distribution and transformation.`, - ), - exampleGallery: [ - { url: example1, urlDark: example1Dark }, - { url: example2, urlDark: example2Dark }, - ], - name: t('Sankey Chart'), - tags: [t('Directional'), t('ECharts'), t('Distribution'), t('Flow')], - thumbnail, - thumbnailDark, - }), - transformProps, - }); - } -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/index.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/index.tsx new file mode 100644 index 00000000000..020ca638c05 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/index.tsx @@ -0,0 +1,273 @@ +/** + * 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. + */ + +/** + * ECharts Sankey Chart - Glyph Pattern Implementation + * + * Visualizes flow between nodes using proportional link widths. + * Each link represents a value flowing from source to target. + */ + +import { t } from '@apache-superset/core/translation'; +import type { ComposeOption } from 'echarts/core'; +import type { SankeySeriesOption } from 'echarts/charts'; +import type { CallbackDataParams } from 'echarts/types/src/util/types'; +import { + buildQueryContext, + CategoricalColorNamespace, + getColumnLabel, + getMetricLabel, + getNumberFormatter, + NumberFormats, + QueryFormData, + tooltipHtml, +} from '@superset-ui/core'; + +import { + defineChart, + Metric, + Dimension, + ChartProps, + SortByMetric, +} from '@superset-ui/glyph-core'; + +import { getDefaultTooltip } from '../utils/tooltip'; +import { getPercentFormatter } from '../utils/formatters'; +import Echart from '../components/Echart'; +import { Refs } from '../types'; + +import thumbnail from './images/thumbnail.png'; +import thumbnailDark from './images/thumbnail-dark.png'; +import example1 from './images/example1.png'; +import example1Dark from './images/example1-dark.png'; +import example2 from './images/example2.png'; +import example2Dark from './images/example2-dark.png'; + +// ============================================================================ +// Types +// ============================================================================ + +type Link = { source: string; target: string; value: number }; +type EChartsOption = ComposeOption; + +interface SankeyTransformResult { + transformedProps: { + refs: Refs; + width: number; + height: number; + echartOptions: EChartsOption; + formData: Record; + }; +} + +// ============================================================================ +// Build Query - exported for testing +// ============================================================================ + +export function buildQuery(formData: QueryFormData) { + const { metric, sort_by_metric: sortByMetric, source, target } = formData; + const groupby = [source, target]; + return buildQueryContext(formData, baseQueryObject => [ + { + ...baseQueryObject, + groupby, + ...(sortByMetric && { orderby: [[metric, false]] }), + }, + ]); +} + +// ============================================================================ +// Chart Definition +// ============================================================================ + +export default defineChart({ + metadata: { + name: t('Sankey Chart'), + description: t( + `The Sankey chart visually tracks the movement and transformation of values across + system stages. Nodes represent stages, connected by links depicting value flow. Node + height corresponds to the visualized metric, providing a clear representation of + value distribution and transformation.`, + ), + category: t('Flow'), + tags: [t('Directional'), t('ECharts'), t('Distribution'), t('Flow')], + credits: ['https://echarts.apache.org'], + thumbnail, + thumbnailDark, + exampleGallery: [ + { url: example1, urlDark: example1Dark }, + { url: example2, urlDark: example2Dark }, + ], + }, + + arguments: { + source: Dimension.with({ + label: t('Source'), + description: t('The column to be used as the source of the edge.'), + multi: false, + }), + + target: Dimension.with({ + label: t('Target'), + description: t('The column to be used as the target of the edge.'), + multi: false, + }), + + metric: Metric.with({ + label: t('Metric'), + description: t('The value that determines link width.'), + multi: false, + }), + + sortByMetric: SortByMetric, + }, + + buildQuery, + + transform: (chartProps: ChartProps): SankeyTransformResult => { + const refs: Refs = {}; + const { height, queriesData, width, theme, rawFormData } = chartProps; + const { data } = queriesData[0]; + + const colorScheme = rawFormData.color_scheme as string; + const metric = rawFormData.metric as string; + const source = rawFormData.source as string; + const target = rawFormData.target as string; + const sliceId = rawFormData.slice_id as number | undefined; + + const colorFn = CategoricalColorNamespace.getScale(colorScheme); + const metricLabel = getMetricLabel(metric); + const valueFormatter = getNumberFormatter(NumberFormats.FLOAT_2_POINT); + const percentFormatter = getPercentFormatter(NumberFormats.PERCENT_2_POINT); + + const links: Link[] = []; + const set = new Set(); + + data.forEach((datum: Record) => { + const sourceName = String(datum[getColumnLabel(source)]); + const targetName = String(datum[getColumnLabel(target)]); + const value = datum[metricLabel] as number; + set.add(sourceName); + set.add(targetName); + links.push({ + source: sourceName, + target: targetName, + value, + }); + }); + + const seriesData: NonNullable = Array.from( + set, + ).map(name => ({ + name, + itemStyle: { + color: colorFn(name, sliceId), + }, + label: { + color: (theme as { colorText?: string })?.colorText, + textShadow: (theme as { colorBgBase?: string })?.colorBgBase, + }, + })); + + // Stores a map with the total values for each node considering the links + const incomingFlows = new Map(); + const outgoingFlows = new Map(); + const allNodeNames = new Set(); + + links.forEach(link => { + const { source: linkSource, target: linkTarget, value } = link; + allNodeNames.add(linkSource); + allNodeNames.add(linkTarget); + incomingFlows.set( + linkTarget, + (incomingFlows.get(linkTarget) || 0) + value, + ); + outgoingFlows.set( + linkSource, + (outgoingFlows.get(linkSource) || 0) + value, + ); + }); + + const nodeValues = new Map(); + + allNodeNames.forEach(nodeName => { + const totalIncoming = incomingFlows.get(nodeName) || 0; + const totalOutgoing = outgoingFlows.get(nodeName) || 0; + nodeValues.set(nodeName, Math.max(totalIncoming, totalOutgoing)); + }); + + const tooltipFormatter = (params: CallbackDataParams) => { + const { name, data: paramData } = params; + const value = params.value as number; + const rows = [[metricLabel, valueFormatter.format(value)]]; + const { source: linkSource, target: linkTarget } = paramData as Link; + if (linkSource && linkTarget) { + rows.push([ + `% (${linkSource})`, + percentFormatter.format(value / nodeValues.get(linkSource)!), + ]); + rows.push([ + `% (${linkTarget})`, + percentFormatter.format(value / nodeValues.get(linkTarget)!), + ]); + } + return tooltipHtml(rows, name); + }; + + const echartOptions: EChartsOption = { + series: { + animation: false, + data: seriesData, + lineStyle: { + color: 'source', + }, + links, + type: 'sankey', + }, + tooltip: { + ...getDefaultTooltip(refs), + formatter: tooltipFormatter, + }, + }; + + return { + transformedProps: { + refs, + formData: rawFormData, + width, + height, + echartOptions, + }, + }; + }, + + render: ({ transformedProps }) => { + const { height, width, echartOptions, refs, formData } = transformedProps; + + return ( + + ); + }, +}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/transformProps.ts deleted file mode 100644 index 3581492466f..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/transformProps.ts +++ /dev/null @@ -1,143 +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 type { ComposeOption } from 'echarts/core'; -import type { SankeySeriesOption } from 'echarts/charts'; -import type { CallbackDataParams } from 'echarts/types/src/util/types'; -import { - CategoricalColorNamespace, - NumberFormats, - getColumnLabel, - getMetricLabel, - getNumberFormatter, - tooltipHtml, -} from '@superset-ui/core'; -import { SankeyChartProps, SankeyTransformedProps } from './types'; -import { Refs } from '../types'; -import { getDefaultTooltip } from '../utils/tooltip'; -import { getPercentFormatter } from '../utils/formatters'; - -type Link = { source: string; target: string; value: number }; -type EChartsOption = ComposeOption; - -export default function transformProps( - chartProps: SankeyChartProps, -): SankeyTransformedProps { - const refs: Refs = {}; - const { formData, height, hooks, queriesData, width, theme } = chartProps; - const { onLegendStateChanged } = hooks; - const { colorScheme, metric, source, target, sliceId } = formData; - const { data } = queriesData[0]; - const colorFn = CategoricalColorNamespace.getScale(colorScheme); - const metricLabel = getMetricLabel(metric); - const valueFormatter = getNumberFormatter(NumberFormats.FLOAT_2_POINT); - const percentFormatter = getPercentFormatter(NumberFormats.PERCENT_2_POINT); - - const links: Link[] = []; - const set = new Set(); - data.forEach(datum => { - const sourceName = String(datum[getColumnLabel(source)]); - const targetName = String(datum[getColumnLabel(target)]); - const value = datum[metricLabel] as number; - set.add(sourceName); - set.add(targetName); - links.push({ - source: sourceName, - target: targetName, - value, - }); - }); - - const seriesData: NonNullable = Array.from( - set, - ).map(name => ({ - name, - itemStyle: { - color: colorFn(name, sliceId), - }, - label: { - color: theme.colorText, - textShadow: theme.colorBgBase, - }, - })); - - // stores a map with the total values for each node considering the links - const incomingFlows = new Map(); - const outgoingFlows = new Map(); - const allNodeNames = new Set(); - - links.forEach(link => { - const { source, target, value } = link; - allNodeNames.add(source); - allNodeNames.add(target); - incomingFlows.set(target, (incomingFlows.get(target) || 0) + value); - outgoingFlows.set(source, (outgoingFlows.get(source) || 0) + value); - }); - - const nodeValues = new Map(); - - allNodeNames.forEach(nodeName => { - const totalIncoming = incomingFlows.get(nodeName) || 0; - const totalOutgoing = outgoingFlows.get(nodeName) || 0; - - nodeValues.set(nodeName, Math.max(totalIncoming, totalOutgoing)); - }); - - const tooltipFormatter = (params: CallbackDataParams) => { - const { name, data } = params; - const value = params.value as number; - const rows = [[metricLabel, valueFormatter.format(value)]]; - const { source, target } = data as Link; - if (source && target) { - rows.push([ - `% (${source})`, - percentFormatter.format(value / nodeValues.get(source)!), - ]); - rows.push([ - `% (${target})`, - percentFormatter.format(value / nodeValues.get(target)!), - ]); - } - return tooltipHtml(rows, name); - }; - - const echartOptions: EChartsOption = { - series: { - animation: false, - data: seriesData, - lineStyle: { - color: 'source', - }, - links, - type: 'sankey', - }, - tooltip: { - ...getDefaultTooltip(refs), - formatter: tooltipFormatter, - }, - }; - - return { - refs, - formData, - width, - height, - echartOptions, - onLegendStateChanged, - }; -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/types.ts deleted file mode 100644 index 323bd1612ca..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Sankey/types.ts +++ /dev/null @@ -1,37 +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 { - QueryFormColumn, - QueryFormData, - QueryFormMetric, -} from '@superset-ui/core'; -import { BaseChartProps, BaseTransformedProps } from '../types'; - -export type SankeyFormData = QueryFormData & { - colorScheme: string; - metric: QueryFormMetric; - source: QueryFormColumn; - target: QueryFormColumn; -}; - -export interface SankeyChartProps extends BaseChartProps { - formData: SankeyFormData; -} - -export type SankeyTransformedProps = BaseTransformedProps & {}; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/EchartsSunburst.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/EchartsSunburst.tsx deleted file mode 100644 index 8ba09fd5d8f..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/EchartsSunburst.tsx +++ /dev/null @@ -1,166 +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 { useCallback } from 'react'; -import { - BinaryQueryObjectFilterClause, - getColumnLabel, - getNumberFormatter, - getTimeFormatter, -} from '@superset-ui/core'; -import { SunburstTransformedProps } from './types'; -import Echart from '../components/Echart'; -import { EventHandlers, TreePathInfo } from '../types'; -import { formatSeriesName } from '../utils/series'; - -export const extractTreePathInfo = (treePathInfo: TreePathInfo[] | undefined) => - (treePathInfo ?? []) - .map(pathInfo => pathInfo?.name || '') - .filter(path => path !== ''); - -export default function EchartsSunburst(props: SunburstTransformedProps) { - const { - height, - width, - echartOptions, - setDataMask, - selectedValues, - formData, - onContextMenu, - refs, - emitCrossFilters, - coltypeMapping, - } = props; - const { columns } = formData; - - const getCrossFilterDataMask = useCallback( - (treePathInfo: TreePathInfo[]) => { - const treePath = extractTreePathInfo(treePathInfo); - const joinedTreePath = treePath.join(','); - const value = treePath[treePath.length - 1]; - - const isCurrentValueSelected = - Object.values(selectedValues).includes(joinedTreePath); - - if (!columns?.length || isCurrentValueSelected) { - return { - dataMask: { - extraFormData: { - filters: [], - }, - filterState: { - value: null, - selectedValues: [], - }, - }, - isCurrentValueSelected, - }; - } - - return { - dataMask: { - extraFormData: { - filters: [ - { - col: columns[treePath.length - 1], - op: '==' as const, - val: value, - }, - ], - }, - filterState: { - value, - selectedValues: [joinedTreePath], - }, - }, - isCurrentValueSelected, - }; - }, - [columns, selectedValues], - ); - - const handleChange = useCallback( - (treePathInfo: TreePathInfo[]) => { - if (!emitCrossFilters || !columns?.length) { - return; - } - - setDataMask(getCrossFilterDataMask(treePathInfo).dataMask); - }, - [emitCrossFilters, columns?.length, setDataMask, getCrossFilterDataMask], - ); - - const eventHandlers: EventHandlers = { - click: props => { - const { treePathInfo } = props; - handleChange(treePathInfo); - }, - contextmenu: async eventParams => { - if (onContextMenu) { - eventParams.event.stop(); - const { data, treePathInfo } = eventParams; - const { records } = data; - const treePath = extractTreePathInfo(eventParams.treePathInfo); - const pointerEvent = eventParams.event.event; - const drillToDetailFilters: BinaryQueryObjectFilterClause[] = []; - const drillByFilters: BinaryQueryObjectFilterClause[] = []; - if (columns?.length) { - treePath.forEach((path, i) => - drillToDetailFilters.push({ - col: columns[i], - op: '==', - val: records[i], - formattedVal: path, - }), - ); - const val = treePath[treePath.length - 1]; - drillByFilters.push({ - col: columns[treePath.length - 1], - op: '==', - val, - formattedVal: formatSeriesName(val, { - timeFormatter: getTimeFormatter(formData.dateFormat), - numberFormatter: getNumberFormatter(formData.numberFormat), - coltype: - coltypeMapping?.[getColumnLabel(columns[treePath.length - 1])], - }), - }); - } - onContextMenu(pointerEvent.clientX, pointerEvent.clientY, { - drillToDetail: drillToDetailFilters, - crossFilter: columns?.length - ? getCrossFilterDataMask(treePathInfo) - : undefined, - drillBy: { filters: drillByFilters, groupbyFieldName: 'columns' }, - }); - } - }, - }; - - return ( - - ); -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/buildQuery.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/buildQuery.ts deleted file mode 100644 index 8b47fb5e725..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/buildQuery.ts +++ /dev/null @@ -1,29 +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 { buildQueryContext, QueryFormData } from '@superset-ui/core'; - -export default function buildQuery(formData: QueryFormData) { - const { metric, sort_by_metric } = formData; - return buildQueryContext(formData, baseQueryObject => [ - { - ...baseQueryObject, - ...(sort_by_metric && { orderby: [[metric, false]] }), - }, - ]); -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/controlPanel.tsx deleted file mode 100644 index b19bc2a2fa1..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/controlPanel.tsx +++ /dev/null @@ -1,193 +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 { t } from '@apache-superset/core/translation'; -import { - ControlPanelConfig, - ControlPanelsContainerProps, - ControlSubSectionHeader, - D3_FORMAT_DOCS, - D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT, - D3_FORMAT_OPTIONS, - D3_TIME_FORMAT_OPTIONS, - getStandardizedControls, -} from '@superset-ui/chart-controls'; -import { DEFAULT_FORM_DATA } from './types'; - -const { labelType, numberFormat, showLabels } = DEFAULT_FORM_DATA; - -const config: ControlPanelConfig = { - controlPanelSections: [ - { - label: t('Query'), - expanded: true, - controlSetRows: [ - ['columns'], - ['metric'], - ['secondary_metric'], - ['adhoc_filters'], - ['row_limit'], - ['sort_by_metric'], - ], - }, - { - label: t('Chart Options'), - expanded: true, - controlSetRows: [ - ['color_scheme'], - ['linear_color_scheme'], - [{t('Labels')}], - [ - { - name: 'show_labels', - config: { - type: 'CheckboxControl', - label: t('Show Labels'), - renderTrigger: true, - default: showLabels, - description: t('Whether to display the labels.'), - }, - }, - ], - [ - { - name: 'show_labels_threshold', - config: { - type: 'TextControl', - label: t('Percentage threshold'), - renderTrigger: true, - isFloat: true, - default: 5, - description: t( - 'Minimum threshold in percentage points for showing labels.', - ), - }, - }, - ], - [ - { - name: 'show_total', - config: { - type: 'CheckboxControl', - label: t('Show Total'), - default: false, - renderTrigger: true, - description: t('Whether to display the aggregate count'), - }, - }, - ], - [ - { - name: 'label_type', - config: { - type: 'SelectControl', - label: t('Label Type'), - default: labelType, - renderTrigger: true, - choices: [ - ['key', t('Category Name')], - ['value', t('Value')], - ['key_value', t('Category and Value')], - ], - description: t('What should be shown on the label?'), - }, - }, - ], - [ - { - name: 'number_format', - config: { - type: 'SelectControl', - freeForm: true, - label: t('Number format'), - renderTrigger: true, - default: numberFormat, - choices: D3_FORMAT_OPTIONS, - description: `${D3_FORMAT_DOCS} ${D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT}`, - }, - }, - ], - ['currency_format'], - [ - { - name: 'date_format', - config: { - type: 'SelectControl', - freeForm: true, - label: t('Date format'), - renderTrigger: true, - choices: D3_TIME_FORMAT_OPTIONS, - default: 'smart_date', - description: D3_FORMAT_DOCS, - }, - }, - ], - ], - }, - ], - controlOverrides: { - metric: { - label: t('Primary Metric'), - description: t( - 'The primary metric is used to define the arc segment sizes', - ), - }, - secondary_metric: { - label: t('Secondary Metric'), - default: null, - description: t( - '[optional] this secondary metric is used to ' + - 'define the color as a ratio against the primary metric. ' + - 'When omitted, the color is categorical and based on labels', - ), - }, - color_scheme: { - description: t( - 'When only a primary metric is provided, a categorical color scale is used.', - ), - visibility: ({ controls }: ControlPanelsContainerProps) => - Boolean( - !controls?.secondary_metric?.value || - controls?.secondary_metric?.value === controls?.metric.value, - ), - }, - linear_color_scheme: { - description: t( - 'When a secondary metric is provided, a linear color scale is used.', - ), - visibility: ({ controls }: ControlPanelsContainerProps) => - Boolean( - controls?.secondary_metric?.value && - controls?.secondary_metric?.value !== controls?.metric.value, - ), - }, - columns: { - label: t('Hierarchy'), - description: t(`Sets the hierarchy levels of the chart. Each level is - represented by one ring with the innermost circle as the top of the hierarchy.`), - }, - }, - formDataOverrides: formData => ({ - ...formData, - groupby: getStandardizedControls().popAllColumns(), - metric: getStandardizedControls().shiftMetric(), - secondary_metric: getStandardizedControls().shiftMetric(), - }), -}; - -export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/index.ts deleted file mode 100644 index 36646e69060..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/index.ts +++ /dev/null @@ -1,66 +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 { t } from '@apache-superset/core/translation'; -import { Behavior } from '@superset-ui/core'; -import transformProps from './transformProps'; -import thumbnail from './images/thumbnail.png'; -import thumbnailDark from './images/thumbnail-dark.png'; -import controlPanel from './controlPanel'; -import buildQuery from './buildQuery'; -import example1 from './images/Sunburst1.png'; -import example1Dark from './images/Sunburst1-dark.png'; -import example2 from './images/Sunburst2.png'; -import example2Dark from './images/Sunburst2-dark.png'; -import { EchartsChartPlugin } from '../types'; - -export default class EchartsSunburstChartPlugin extends EchartsChartPlugin { - constructor() { - super({ - buildQuery, - controlPanel, - loadChart: () => import('./EchartsSunburst'), - metadata: { - behaviors: [ - Behavior.InteractiveChart, - Behavior.DrillToDetail, - Behavior.DrillBy, - ], - category: t('Part of a Whole'), - credits: ['https://echarts.apache.org'], - description: t( - 'Uses circles to visualize the flow of data through different stages of a system. Hover over individual paths in the visualization to understand the stages a value took. Useful for multi-stage, multi-group visualizing funnels and pipelines.', - ), - exampleGallery: [ - { url: example1, urlDark: example1Dark }, - { url: example2, urlDark: example2Dark }, - ], - name: t('Sunburst Chart'), - tags: [ - t('ECharts'), - t('Multi-Levels'), - t('Proportional'), - t('Featured'), - ], - thumbnail, - thumbnailDark, - }, - transformProps, - }); - } -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/index.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/index.tsx new file mode 100644 index 00000000000..863804f01b4 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/index.tsx @@ -0,0 +1,832 @@ +/** + * 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. + */ + +/** + * ECharts Sunburst Chart - Glyph Pattern Implementation + * + * Uses circles to visualize the flow of data through different stages of a system. + * Hover over individual paths in the visualization to understand the stages a value took. + * Useful for multi-stage, multi-group visualizing funnels and pipelines. + */ + +import { useCallback } from 'react'; +import { t } from '@apache-superset/core/translation'; +import type { EChartsCoreOption } from 'echarts/core'; +import type { CallbackDataParams } from 'echarts/types/src/util/types'; +import type { SunburstSeriesNodeItemOption } from 'echarts/types/src/chart/sunburst/SunburstSeries'; +import { + Behavior, + BinaryQueryObjectFilterClause, + buildQueryContext, + CategoricalColorNamespace, + DataRecord, + DataRecordValue, + getColumnLabel, + getMetricLabel, + getNumberFormatter, + getSequentialSchemeRegistry, + getTimeFormatter, + getValueFormatter, + NumberFormats, + QueryFormColumn, + QueryFormData, + SetDataMaskHook, + ContextMenuFilters, + tooltipHtml, + ValueFormatter, +} from '@superset-ui/core'; +import { + D3_FORMAT_DOCS, + D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT, + D3_FORMAT_OPTIONS, + D3_TIME_FORMAT_OPTIONS, + getStandardizedControls, + ControlPanelsContainerProps, +} from '@superset-ui/chart-controls'; + +import { + defineChart, + Metric, + Select, + Checkbox, + Text, + ChartProps, +} from '@superset-ui/glyph-core'; + +import { defaultGrid } from '../defaults'; +import { formatSeriesName, getColtypesMapping } from '../utils/series'; +import { treeBuilder, TreeNode } from '../utils/treeBuilder'; +import { NULL_STRING, OpacityEnum } from '../constants'; +import { getDefaultTooltip } from '../utils/tooltip'; +import Echart from '../components/Echart'; +import { EventHandlers, Refs, TreePathInfo } from '../types'; + +import thumbnail from './images/thumbnail.png'; +import thumbnailDark from './images/thumbnail-dark.png'; +import example1 from './images/Sunburst1.png'; +import example1Dark from './images/Sunburst1-dark.png'; +import example2 from './images/Sunburst2.png'; +import example2Dark from './images/Sunburst2-dark.png'; + +// ============================================================================ +// Types +// ============================================================================ + +enum EchartsSunburstLabelType { + Key = 'key', + Value = 'value', + KeyValue = 'key_value', +} + +type NodeItemOption = SunburstSeriesNodeItemOption & { + records: DataRecordValue[]; + secondaryValue: number; +}; + +interface SunburstTransformResult { + transformedProps: { + refs: Refs; + width: number; + height: number; + echartOptions: EChartsCoreOption; + formData: Record; + columns: QueryFormColumn[]; + columnsLabelMap: Map; + setDataMask: SetDataMaskHook; + selectedValues: string[]; + emitCrossFilters?: boolean; + onContextMenu?: ( + clientX: number, + clientY: number, + filters?: ContextMenuFilters, + ) => void; + coltypeMapping?: Record; + }; +} + +// ============================================================================ +// Helpers +// ============================================================================ + +function getLinearDomain( + treeData: TreeNode[], + callback: (treeNode: TreeNode) => number, +): [number, number] { + let min = 0; + let max = 0; + let temp = null; + function traverse(tree: TreeNode[]) { + tree.forEach(treeNode => { + if (treeNode.children?.length) { + traverse(treeNode.children); + } + temp = callback(treeNode); + if (temp !== null) { + if (min > temp) min = temp; + if (max < temp) max = temp; + } + }); + } + traverse(treeData); + return [min, max]; +} + +function formatLabel({ + params, + labelType, + numberFormatter, +}: { + params: CallbackDataParams; + labelType: EchartsSunburstLabelType; + numberFormatter: ValueFormatter; +}): string { + const { name = '', value } = params; + const formattedValue = numberFormatter(value as number); + + switch (labelType) { + case EchartsSunburstLabelType.Key: + return name; + case EchartsSunburstLabelType.Value: + return formattedValue; + case EchartsSunburstLabelType.KeyValue: + return `${name}: ${formattedValue}`; + default: + return name; + } +} + +function formatTooltip({ + params, + primaryValueFormatter, + secondaryValueFormatter, + colorByCategory, + totalValue, + metricLabel, + secondaryMetricLabel, +}: { + params: CallbackDataParams & { + treePathInfo: { + name: string; + dataIndex: number; + value: number; + }[]; + }; + primaryValueFormatter: ValueFormatter; + secondaryValueFormatter: ValueFormatter | undefined; + colorByCategory: boolean; + totalValue: number; + metricLabel: string; + secondaryMetricLabel?: string; +}): string { + const { data, treePathInfo = [] } = params; + const node = data as TreeNode; + const formattedValue = primaryValueFormatter(node.value); + const formattedSecondaryValue = secondaryValueFormatter?.( + node.secondaryValue, + ); + + const percentFormatter = getNumberFormatter(NumberFormats.PERCENT_2_POINT); + const compareValuePercentage = percentFormatter( + node.secondaryValue / node.value, + ); + const absolutePercentage = percentFormatter(node.value / totalValue); + const parentNode = + treePathInfo.length > 2 ? treePathInfo[treePathInfo.length - 2] : undefined; + + const title = (node.name || NULL_STRING) + .toString() + .replaceAll('<', '<') + .replaceAll('>', '>'); + const rows: [string, string][] = [[t('% of total'), absolutePercentage]]; + if (parentNode) { + const conditionalPercentage = percentFormatter( + node.value / parentNode.value, + ); + rows.push([t('% of parent'), conditionalPercentage]); + } + rows.push([metricLabel, formattedValue]); + if (!colorByCategory) { + rows.push([ + secondaryMetricLabel || NULL_STRING, + formattedSecondaryValue || NULL_STRING, + ]); + rows.push([ + `${metricLabel}/${secondaryMetricLabel}`, + compareValuePercentage, + ]); + } + return tooltipHtml(rows, title); +} + +const extractTreePathInfo = (treePathInfo: TreePathInfo[] | undefined) => + (treePathInfo ?? []) + .map(pathInfo => pathInfo?.name || '') + .filter(path => path !== ''); + +// ============================================================================ +// Render Component +// ============================================================================ + +function SunburstRender({ + transformedProps, +}: { + transformedProps: SunburstTransformResult['transformedProps']; +}) { + const { + height, + width, + echartOptions, + setDataMask, + selectedValues, + formData, + onContextMenu, + refs, + emitCrossFilters, + coltypeMapping, + columns, + } = transformedProps; + + const getCrossFilterDataMask = useCallback( + (treePathInfo: TreePathInfo[]) => { + const treePath = extractTreePathInfo(treePathInfo); + const joinedTreePath = treePath.join(','); + const value = treePath[treePath.length - 1]; + + const isCurrentValueSelected = + Object.values(selectedValues).includes(joinedTreePath); + + if (!columns?.length || isCurrentValueSelected) { + return { + dataMask: { + extraFormData: { + filters: [], + }, + filterState: { + value: null, + selectedValues: [], + }, + }, + isCurrentValueSelected, + }; + } + + return { + dataMask: { + extraFormData: { + filters: [ + { + col: columns[treePath.length - 1], + op: '==' as const, + val: value, + }, + ], + }, + filterState: { + value, + selectedValues: [joinedTreePath], + }, + }, + isCurrentValueSelected, + }; + }, + [columns, selectedValues], + ); + + const handleChange = useCallback( + (treePathInfo: TreePathInfo[]) => { + if (!emitCrossFilters || !columns?.length) { + return; + } + + setDataMask(getCrossFilterDataMask(treePathInfo).dataMask); + }, + [emitCrossFilters, columns?.length, setDataMask, getCrossFilterDataMask], + ); + + const eventHandlers: EventHandlers = { + click: (props: { treePathInfo: TreePathInfo[] }) => { + const { treePathInfo } = props; + handleChange(treePathInfo); + }, + contextmenu: async (eventParams: { + event: { + stop: () => void; + event: { clientX: number; clientY: number }; + }; + data: { records: DataRecordValue[] }; + treePathInfo: TreePathInfo[]; + }) => { + if (onContextMenu) { + eventParams.event.stop(); + const { data, treePathInfo } = eventParams; + const { records } = data; + const treePath = extractTreePathInfo(eventParams.treePathInfo); + const pointerEvent = eventParams.event.event; + const drillToDetailFilters: BinaryQueryObjectFilterClause[] = []; + const drillByFilters: BinaryQueryObjectFilterClause[] = []; + if (columns?.length) { + treePath.forEach((path, i) => + drillToDetailFilters.push({ + col: columns[i], + op: '==', + val: records[i], + formattedVal: path, + }), + ); + const val = treePath[treePath.length - 1]; + drillByFilters.push({ + col: columns[treePath.length - 1], + op: '==', + val, + formattedVal: formatSeriesName(val, { + timeFormatter: getTimeFormatter(formData.date_format as string), + numberFormatter: getNumberFormatter( + formData.number_format as string, + ), + coltype: + coltypeMapping?.[getColumnLabel(columns[treePath.length - 1])], + }), + }); + } + onContextMenu(pointerEvent.clientX, pointerEvent.clientY, { + drillToDetail: drillToDetailFilters, + crossFilter: columns?.length + ? getCrossFilterDataMask(treePathInfo) + : undefined, + drillBy: { filters: drillByFilters, groupbyFieldName: 'columns' }, + }); + } + }, + }; + + return ( + + ); +} + +// ============================================================================ +// Build Query +// ============================================================================ + +export function buildQuery(formData: QueryFormData) { + const { metric } = formData; + const sortByMetric = formData.sort_by_metric; + return buildQueryContext(formData, baseQueryObject => [ + { + ...baseQueryObject, + ...(sortByMetric && { orderby: [[metric, false]] }), + }, + ]); +} + +// ============================================================================ +// Chart Definition +// ============================================================================ + +export default defineChart({ + metadata: { + name: t('Sunburst Chart'), + description: t( + 'Uses circles to visualize the flow of data through different stages of a system. Hover over individual paths in the visualization to understand the stages a value took. Useful for multi-stage, multi-group visualizing funnels and pipelines.', + ), + category: t('Part of a Whole'), + tags: [t('ECharts'), t('Multi-Levels'), t('Proportional'), t('Featured')], + behaviors: [ + Behavior.InteractiveChart, + Behavior.DrillToDetail, + Behavior.DrillBy, + ], + credits: ['https://echarts.apache.org'], + thumbnail, + thumbnailDark, + exampleGallery: [ + { url: example1, urlDark: example1Dark }, + { url: example2, urlDark: example2Dark }, + ], + }, + + arguments: { + // Labels section + showLabels: Checkbox.with({ + label: t('Show Labels'), + description: t('Whether to display the labels.'), + default: false, + }), + + showLabelsThreshold: Text.with({ + label: t('Percentage threshold'), + description: t( + 'Minimum threshold in percentage points for showing labels.', + ), + default: '5', + }), + + showTotal: Checkbox.with({ + label: t('Show Total'), + description: t('Whether to display the aggregate count'), + default: false, + }), + + labelType: Select.with({ + label: t('Label Type'), + description: t('What should be shown on the label?'), + options: [ + { label: t('Category Name'), value: 'key' }, + { label: t('Value'), value: 'value' }, + { label: t('Category and Value'), value: 'key_value' }, + ], + default: 'key', + }), + + numberFormat: Select.with({ + label: t('Number format'), + description: `${D3_FORMAT_DOCS} ${D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT}`, + options: D3_FORMAT_OPTIONS.map(([value, label]) => ({ + label: label as string, + value: value as string, + })), + default: 'SMART_NUMBER', + }), + + dateFormat: Select.with({ + label: t('Date format'), + description: D3_FORMAT_DOCS, + options: D3_TIME_FORMAT_OPTIONS.map(([value, label]) => ({ + label: label as string, + value: value as string, + })), + default: 'smart_date', + }), + + secondaryMetric: Metric.with({ + label: t('Secondary Metric'), + description: t( + '[optional] this secondary metric is used to define the color as a ratio against the primary metric. When omitted, the color is categorical and based on labels', + ), + multi: false, + }), + }, + + additionalControls: { + query: [ + ['columns'], + ['metric'], + ['secondary_metric'], + ['adhoc_filters'], + ['row_limit'], + ['sort_by_metric'], + ], + chartOptions: [ + ['color_scheme'], + ['linear_color_scheme'], + ['currency_format'], + ], + }, + + additionalControlOverrides: { + metric: { + label: t('Primary Metric'), + description: t( + 'The primary metric is used to define the arc segment sizes', + ), + }, + secondary_metric: { + label: t('Secondary Metric'), + default: null, + description: t( + '[optional] this secondary metric is used to define the color as a ratio against the primary metric. When omitted, the color is categorical and based on labels', + ), + }, + color_scheme: { + description: t( + 'When only a primary metric is provided, a categorical color scale is used.', + ), + visibility: ({ controls }: ControlPanelsContainerProps) => + Boolean( + !controls?.secondary_metric?.value || + controls?.secondary_metric?.value === controls?.metric.value, + ), + }, + linear_color_scheme: { + description: t( + 'When a secondary metric is provided, a linear color scale is used.', + ), + visibility: ({ controls }: ControlPanelsContainerProps) => + Boolean( + controls?.secondary_metric?.value && + controls?.secondary_metric?.value !== controls?.metric.value, + ), + }, + columns: { + label: t('Hierarchy'), + description: t(`Sets the hierarchy levels of the chart. Each level is + represented by one ring with the innermost circle as the top of the hierarchy.`), + }, + }, + + formDataOverrides: (formData: QueryFormData) => ({ + ...formData, + groupby: getStandardizedControls().popAllColumns(), + metric: getStandardizedControls().shiftMetric(), + secondary_metric: getStandardizedControls().shiftMetric(), + }), + + buildQuery, + + transform: (chartProps: ChartProps): SunburstTransformResult => { + const { + width, + height, + rawFormData, + hooks, + filterState, + queriesData, + theme, + inContextMenu, + emitCrossFilters, + datasource, + } = chartProps; + + const { data = [] } = queriesData[0]; + const { setDataMask = () => {}, onContextMenu } = hooks ?? {}; + const coltypeMapping = getColtypesMapping( + queriesData[0] as unknown as Parameters[0], + ); + const refs: Refs = {}; + + // Extract form values + const columns = (rawFormData.columns as QueryFormColumn[]) || []; + const metric = rawFormData.metric || ''; + const secondaryMetric = rawFormData.secondary_metric || ''; + const colorScheme = rawFormData.color_scheme as string; + const linearColorScheme = rawFormData.linear_color_scheme as string; + const labelType = + (rawFormData.label_type as EchartsSunburstLabelType) || + EchartsSunburstLabelType.Key; + const numberFormat = + (rawFormData.number_format as string) || 'SMART_NUMBER'; + const currencyFormat = rawFormData.currency_format; + const dateFormat = (rawFormData.date_format as string) || 'smart_date'; + const showLabels = rawFormData.show_labels as boolean; + const showLabelsThreshold = parseFloat( + (rawFormData.show_labels_threshold as string) || '5', + ); + const showTotal = rawFormData.show_total as boolean; + const sliceId = rawFormData.slice_id as number | undefined; + + const { + currencyFormats = {}, + columnFormats = {}, + verboseMap = {}, + } = datasource ?? {}; + + const primaryValueFormatter = getValueFormatter( + metric, + currencyFormats, + columnFormats, + numberFormat, + currencyFormat, + ); + const secondaryValueFormatter = secondaryMetric + ? getValueFormatter( + secondaryMetric, + currencyFormats, + columnFormats, + numberFormat, + currencyFormat, + ) + : undefined; + + const numberFormatter = getNumberFormatter(numberFormat); + const formatter = (params: CallbackDataParams) => + formatLabel({ + params, + numberFormatter: primaryValueFormatter, + labelType, + }); + const minShowLabelAngle = (showLabelsThreshold || 0) * 3.6; + const padding = { + top: theme.sizeUnit * 3, + right: theme.sizeUnit, + bottom: theme.sizeUnit * 3, + left: theme.sizeUnit, + }; + const containerWidth = width; + const containerHeight = height; + const visWidth = containerWidth - padding.left - padding.right; + const visHeight = containerHeight - padding.top - padding.bottom; + const radius = Math.min(visWidth, visHeight) / 2; + + const columnsLabelMap = new Map(); + const metricLabel = getMetricLabel(metric); + const secondaryMetricLabel = secondaryMetric + ? getMetricLabel(secondaryMetric) + : undefined; + const columnLabels = columns.map(getColumnLabel); + const treeData = treeBuilder( + data as DataRecord[], + columnLabels, + metricLabel, + secondaryMetricLabel, + ); + const totalValue = treeData.reduce( + (result, treeNode) => result + treeNode.value, + 0, + ); + const totalSecondaryValue = treeData.reduce( + (result, treeNode) => result + treeNode.secondaryValue, + 0, + ); + + const categoricalColorScale = CategoricalColorNamespace.getScale( + colorScheme as string, + ); + let linearColorScale: ((value: number) => string) | undefined; + let colorByCategory = true; + if (secondaryMetric && metric !== secondaryMetric) { + const domain = getLinearDomain( + treeData, + node => node.secondaryValue / node.value, + ); + colorByCategory = false; + linearColorScale = getSequentialSchemeRegistry() + ?.get(linearColorScheme) + ?.createLinearScale(domain) as ((value: number) => string) | undefined; + } + + // Add a base color to keep feature parity + if (colorByCategory) { + categoricalColorScale(metricLabel, sliceId); + } else if (linearColorScale) { + linearColorScale(totalSecondaryValue / totalValue); + } + + const labelProps = { + color: theme.colorText, + textBorderColor: theme.colorBgBase, + textBorderWidth: 1, + }; + + const traverse = ( + treeNodes: TreeNode[], + path: string[], + pathRecords?: DataRecordValue[], + ): NodeItemOption[] => + treeNodes.map(treeNode => { + const { name: nodeName, value, secondaryValue, groupBy } = treeNode; + const records = [...(pathRecords || []), nodeName]; + let name = formatSeriesName(nodeName, { + numberFormatter, + timeFormatter: getTimeFormatter(dateFormat), + ...(coltypeMapping[groupBy] && { + coltype: coltypeMapping[groupBy], + }), + }); + const newPath = path.concat(name); + let item: NodeItemOption = { + records, + name, + value, + secondaryValue, + itemStyle: { + color: colorByCategory + ? categoricalColorScale(name, sliceId) + : linearColorScale?.(secondaryValue / value), + }, + }; + if (treeNode.children?.length) { + item.children = traverse( + treeNode.children, + newPath, + records, + ) as NodeItemOption['children']; + } else { + name = newPath.join(','); + } + columnsLabelMap.set(name, newPath); + if (filterState?.selectedValues?.[0]?.includes(name) === false) { + item = { + ...item, + itemStyle: { + ...item.itemStyle, + opacity: OpacityEnum.SemiTransparent, + }, + label: { + ...labelProps, + }, + }; + } + return item; + }); + + const echartOptions: EChartsCoreOption = { + grid: { + ...defaultGrid, + }, + tooltip: { + ...getDefaultTooltip(refs), + show: !inContextMenu, + trigger: 'item', + formatter: (params: CallbackDataParams) => + formatTooltip({ + params: params as CallbackDataParams & { + treePathInfo: { + name: string; + dataIndex: number; + value: number; + }[]; + }, + primaryValueFormatter, + secondaryValueFormatter, + colorByCategory, + totalValue, + metricLabel: verboseMap[metricLabel] || metricLabel, + secondaryMetricLabel: secondaryMetricLabel + ? verboseMap[secondaryMetricLabel] || secondaryMetricLabel + : undefined, + }), + }, + series: [ + { + type: 'sunburst', + ...padding, + nodeClick: false, + emphasis: { + focus: 'ancestor', + label: { + show: showLabels, + }, + }, + label: { + ...labelProps, + width: (radius * 0.6) / (columns.length || 1), + show: showLabels, + formatter, + minAngle: minShowLabelAngle, + overflow: 'breakAll', + }, + radius: [radius * 0.3, radius], + data: traverse(treeData, []), + }, + ], + graphic: showTotal + ? { + type: 'text', + top: 'center', + left: 'center', + style: { + text: t('Total: %s', primaryValueFormatter(totalValue)), + fontSize: 16, + fontWeight: 'bold', + }, + z: 10, + } + : null, + }; + + return { + transformedProps: { + refs, + width, + height, + echartOptions, + formData: rawFormData, + columns, + columnsLabelMap, + setDataMask, + selectedValues: (filterState?.selectedValues as string[]) || [], + emitCrossFilters, + onContextMenu, + coltypeMapping, + }, + }; + }, + + render: ({ transformedProps }) => ( + + ), +}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/stories/Sunburst.stories.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/stories/Sunburst.stories.tsx deleted file mode 100644 index 193a9f5cec7..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/stories/Sunburst.stories.tsx +++ /dev/null @@ -1,73 +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 { SuperChart, getChartTransformPropsRegistry } from '@superset-ui/core'; -import { - EchartsSunburstChartPlugin, - SunburstTransformProps, -} from '@superset-ui/plugin-chart-echarts'; -import { withResizableChartDemo } from '@storybook-shared'; -import data from './data'; - -new EchartsSunburstChartPlugin() - .configure({ key: 'echarts-sunburst' }) - .register(); - -getChartTransformPropsRegistry().registerValue( - 'echarts-sunburst', - SunburstTransformProps, -); - -export default { - title: 'Chart Plugins/plugin-chart-echarts/Sunburst', - decorators: [withResizableChartDemo], -}; - -export const Sunburst = ({ - showLabels, - showTotal, - width, - height, -}: { - showLabels: boolean; - showTotal: boolean; - width: number; - height: number; -}) => ( - -); -Sunburst.args = { - showLabels: true, - showTotal: true, -}; -Sunburst.argTypes = { - showLabels: { control: 'boolean' }, - showTotal: { control: 'boolean' }, -}; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/transformProps.ts deleted file mode 100644 index 8ffde2656ac..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/transformProps.ts +++ /dev/null @@ -1,410 +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 { t } from '@apache-superset/core/translation'; -import { - CategoricalColorNamespace, - DataRecordValue, - getColumnLabel, - getMetricLabel, - getNumberFormatter, - getSequentialSchemeRegistry, - getTimeFormatter, - getValueFormatter, - NumberFormats, - tooltipHtml, - ValueFormatter, -} from '@superset-ui/core'; -import type { EChartsCoreOption } from 'echarts/core'; -import type { CallbackDataParams } from 'echarts/types/src/util/types'; -import { NULL_STRING, OpacityEnum } from '../constants'; -import { defaultGrid } from '../defaults'; -import { Refs } from '../types'; -import { formatSeriesName, getColtypesMapping } from '../utils/series'; -import { treeBuilder, TreeNode } from '../utils/treeBuilder'; -import { - EchartsSunburstChartProps, - EchartsSunburstLabelType, - NodeItemOption, - SunburstTransformedProps, -} from './types'; -import { getDefaultTooltip } from '../utils/tooltip'; - -export function getLinearDomain( - treeData: TreeNode[], - callback: (treeNode: TreeNode) => number, -) { - let min = 0; - let max = 0; - let temp = null; - function traverse(tree: TreeNode[]) { - tree.forEach(treeNode => { - if (treeNode.children?.length) { - traverse(treeNode.children); - } - temp = callback(treeNode); - if (temp !== null) { - if (min > temp) min = temp; - if (max < temp) max = temp; - } - }); - } - traverse(treeData); - return [min, max]; -} - -export function formatLabel({ - params, - labelType, - numberFormatter, -}: { - params: CallbackDataParams; - labelType: EchartsSunburstLabelType; - numberFormatter: ValueFormatter; -}): string { - const { name = '', value } = params; - const formattedValue = numberFormatter(value as number); - - switch (labelType) { - case EchartsSunburstLabelType.Key: - return name; - case EchartsSunburstLabelType.Value: - return formattedValue; - case EchartsSunburstLabelType.KeyValue: - return `${name}: ${formattedValue}`; - default: - return name; - } -} - -export function formatTooltip({ - params, - primaryValueFormatter, - secondaryValueFormatter, - colorByCategory, - totalValue, - metricLabel, - secondaryMetricLabel, -}: { - params: CallbackDataParams & { - treePathInfo: { - name: string; - dataIndex: number; - value: number; - }[]; - }; - primaryValueFormatter: ValueFormatter; - secondaryValueFormatter: ValueFormatter | undefined; - colorByCategory: boolean; - totalValue: number; - metricLabel: string; - secondaryMetricLabel?: string; -}): string { - const { data, treePathInfo = [] } = params; - const node = data as TreeNode; - const formattedValue = primaryValueFormatter(node.value); - const formattedSecondaryValue = secondaryValueFormatter?.( - node.secondaryValue, - ); - - const percentFormatter = getNumberFormatter(NumberFormats.PERCENT_2_POINT); - const compareValuePercentage = percentFormatter( - node.secondaryValue / node.value, - ); - const absolutePercentage = percentFormatter(node.value / totalValue); - const parentNode = - treePathInfo.length > 2 ? treePathInfo[treePathInfo.length - 2] : undefined; - - const title = (node.name || NULL_STRING) - .toString() - .replaceAll('<', '<') - .replaceAll('>', '>'); - const rows = [[t('% of total'), absolutePercentage]]; - if (parentNode) { - const conditionalPercentage = percentFormatter( - node.value / parentNode.value, - ); - rows.push([t('% of parent'), conditionalPercentage]); - } - rows.push([metricLabel, formattedValue]); - if (!colorByCategory) { - rows.push([ - secondaryMetricLabel || NULL_STRING, - formattedSecondaryValue || NULL_STRING, - ]); - rows.push([ - `${metricLabel}/${secondaryMetricLabel}`, - compareValuePercentage, - ]); - } - return tooltipHtml(rows, title); -} - -export default function transformProps( - chartProps: EchartsSunburstChartProps, -): SunburstTransformedProps { - const { - formData, - height, - hooks, - filterState, - queriesData, - width, - theme, - inContextMenu, - emitCrossFilters, - datasource, - } = chartProps; - const { data = [], detected_currency: detectedCurrency } = queriesData[0]; - const coltypeMapping = getColtypesMapping(queriesData[0]); - const { - groupby = [], - columns = [], - metric = '', - secondaryMetric = '', - colorScheme, - linearColorScheme, - labelType, - numberFormat, - currencyFormat, - dateFormat, - showLabels, - showLabelsThreshold, - showTotal, - sliceId, - } = formData; - const { - currencyFormats = {}, - columnFormats = {}, - verboseMap = {}, - currencyCodeColumn, - } = datasource; - const refs: Refs = {}; - const primaryValueFormatter = getValueFormatter( - metric, - currencyFormats, - columnFormats, - numberFormat, - currencyFormat, - undefined, - data, - currencyCodeColumn, - detectedCurrency, - ); - const secondaryValueFormatter = secondaryMetric - ? getValueFormatter( - secondaryMetric, - currencyFormats, - columnFormats, - numberFormat, - currencyFormat, - undefined, - data, - currencyCodeColumn, - detectedCurrency, - ) - : undefined; - - const numberFormatter = getNumberFormatter(numberFormat); - const formatter = (params: CallbackDataParams) => - formatLabel({ - params, - numberFormatter: primaryValueFormatter, - labelType, - }); - const minShowLabelAngle = (showLabelsThreshold || 0) * 3.6; - const padding = { - top: theme.sizeUnit * 3, - right: theme.sizeUnit, - bottom: theme.sizeUnit * 3, - left: theme.sizeUnit, - }; - const containerWidth = width; - const containerHeight = height; - const visWidth = containerWidth - padding.left - padding.right; - const visHeight = containerHeight - padding.top - padding.bottom; - const radius = Math.min(visWidth, visHeight) / 2; - const { setDataMask = () => {}, onContextMenu } = hooks; - const columnsLabelMap = new Map(); - const metricLabel = getMetricLabel(metric); - const secondaryMetricLabel = secondaryMetric - ? getMetricLabel(secondaryMetric) - : undefined; - const columnLabels = columns.map(getColumnLabel); - const treeData = treeBuilder( - data, - columnLabels, - metricLabel, - secondaryMetricLabel, - ); - const totalValue = treeData.reduce( - (result, treeNode) => result + treeNode.value, - 0, - ); - const totalSecondaryValue = treeData.reduce( - (result, treeNode) => result + treeNode.secondaryValue, - 0, - ); - - const categoricalColorScale = CategoricalColorNamespace.getScale( - colorScheme as string, - ); - let linearColorScale: any; - let colorByCategory = true; - if (secondaryMetric && metric !== secondaryMetric) { - const domain = getLinearDomain( - treeData, - node => node.secondaryValue / node.value, - ); - colorByCategory = false; - linearColorScale = getSequentialSchemeRegistry() - ?.get(linearColorScheme) - ?.createLinearScale(domain); - } - - // add a base color to keep feature parity - if (colorByCategory) { - categoricalColorScale(metricLabel, sliceId); - } else { - linearColorScale(totalSecondaryValue / totalValue); - } - const labelProps = { - color: theme.colorText, - }; - const traverse = ( - treeNodes: TreeNode[], - path: string[], - pathRecords?: DataRecordValue[], - ) => - treeNodes.map(treeNode => { - const { name: nodeName, value, secondaryValue, groupBy } = treeNode; - const records = [...(pathRecords || []), nodeName]; - let name = formatSeriesName(nodeName, { - numberFormatter, - timeFormatter: getTimeFormatter(dateFormat), - ...(coltypeMapping[groupBy] && { - coltype: coltypeMapping[groupBy], - }), - }); - const newPath = path.concat(name); - let item: NodeItemOption = { - records, - name, - value, - secondaryValue, - itemStyle: { - color: colorByCategory - ? categoricalColorScale(name, sliceId) - : linearColorScale(secondaryValue / value), - }, - }; - if (treeNode.children?.length) { - item.children = traverse(treeNode.children, newPath, records); - } else { - name = newPath.join(','); - } - columnsLabelMap.set(name, newPath); - if (filterState.selectedValues?.[0]?.includes(name) === false) { - item = { - ...item, - itemStyle: { - ...item.itemStyle, - opacity: OpacityEnum.SemiTransparent, - }, - label: { - ...labelProps, - }, - }; - } - return item; - }); - - const echartOptions: EChartsCoreOption = { - grid: { - ...defaultGrid, - }, - tooltip: { - ...getDefaultTooltip(refs), - show: !inContextMenu, - trigger: 'item', - formatter: (params: any) => - formatTooltip({ - params, - primaryValueFormatter, - secondaryValueFormatter, - colorByCategory, - totalValue, - metricLabel: verboseMap[metricLabel] || metricLabel, - secondaryMetricLabel: secondaryMetricLabel - ? verboseMap[secondaryMetricLabel] || secondaryMetricLabel - : undefined, - }), - }, - series: [ - { - type: 'sunburst', - ...padding, - nodeClick: false, - emphasis: { - focus: 'ancestor', - label: { - show: showLabels, - }, - }, - label: { - ...labelProps, - width: (radius * 0.6) / (columns.length || 1), - show: showLabels, - formatter, - minAngle: minShowLabelAngle, - overflow: 'breakAll', - }, - radius: [radius * 0.3, radius], - data: traverse(treeData, []), - }, - ], - graphic: showTotal - ? { - type: 'text', - top: 'center', - left: 'center', - style: { - text: t('Total: %s', primaryValueFormatter(totalValue)), - fontSize: 16, - fontWeight: 'bold', - fill: theme.colorText, - }, - z: 10, - } - : null, - }; - return { - formData, - width, - height, - echartOptions, - setDataMask, - emitCrossFilters, - labelMap: Object.fromEntries(columnsLabelMap), - groupby, - selectedValues: filterState.selectedValues || [], - onContextMenu, - refs, - coltypeMapping, - }; -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/types.ts deleted file mode 100644 index ef278abd6e2..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Sunburst/types.ts +++ /dev/null @@ -1,70 +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 { - ChartDataResponseResult, - ChartProps, - DataRecordValue, - QueryFormColumn, - QueryFormData, - QueryFormMetric, -} from '@superset-ui/core'; -import type { SunburstSeriesNodeItemOption } from 'echarts/types/src/chart/sunburst/SunburstSeries'; -import { - BaseTransformedProps, - ContextMenuTransformedProps, - CrossFilterTransformedProps, -} from '../types'; - -export type EchartsSunburstFormData = QueryFormData & { - groupby: QueryFormColumn[]; - metric: QueryFormMetric; - secondaryMetric?: QueryFormMetric; - colorScheme?: string; - linearColorScheme?: string; -}; - -export enum EchartsSunburstLabelType { - Key = 'key', - Value = 'value', - KeyValue = 'key_value', -} - -export const DEFAULT_FORM_DATA: Partial = { - groupby: [], - numberFormat: 'SMART_NUMBER', - labelType: EchartsSunburstLabelType.Key, - showLabels: false, - dateFormat: 'smart_date', -}; - -export interface EchartsSunburstChartProps extends ChartProps { - formData: EchartsSunburstFormData; - queriesData: ChartDataResponseResult[]; -} - -export type SunburstTransformedProps = - BaseTransformedProps & - ContextMenuTransformedProps & - CrossFilterTransformedProps; - -export type NodeItemOption = SunburstSeriesNodeItemOption & { - records: DataRecordValue[]; - secondaryValue: number; -}; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx deleted file mode 100644 index 5c12e526e9b..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx +++ /dev/null @@ -1,297 +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 { t } from '@apache-superset/core/translation'; -import { getColumnLabel, QueryFormColumn } from '@superset-ui/core'; -import { GenericDataType } from '@apache-superset/core/common'; -import { - checkColumnType, - ControlPanelConfig, - ControlPanelsContainerProps, - ControlSubSectionHeader, - D3_TIME_FORMAT_DOCS, - getStandardizedControls, - sections, - sharedControls, -} from '@superset-ui/chart-controls'; - -import { EchartsTimeseriesSeriesType } from '../types'; -import { DEFAULT_FORM_DATA, TIME_SERIES_DESCRIPTION_TEXT } from '../constants'; -import { - legendSection, - onlyTotalControl, - showValueControl, - richTooltipSection, - seriesOrderSection, - percentageThresholdControl, - xAxisLabelRotation, - xAxisLabelInterval, - truncateXAxis, - xAxisBounds, - minorTicks, - forceMaxInterval, -} from '../../controls'; -import { AreaChartStackControlOptions } from '../../constants'; - -const { - logAxis, - markerEnabled, - markerSize, - minorSplitLine, - opacity, - rowLimit, - seriesType, - truncateYAxis, - yAxisBounds, -} = DEFAULT_FORM_DATA; -const config: ControlPanelConfig = { - controlPanelSections: [ - sections.echartsTimeSeriesQueryWithXAxisSort, - sections.advancedAnalyticsControls, - sections.annotationsAndLayersControls, - sections.forecastIntervalControls, - sections.titleControls, - { - label: t('Chart Options'), - expanded: true, - controlSetRows: [ - ...seriesOrderSection, - ['color_scheme'], - ['time_shift_color'], - [ - { - name: 'seriesType', - config: { - type: 'SelectControl', - label: t('Series Style'), - renderTrigger: true, - default: seriesType, - choices: [ - [EchartsTimeseriesSeriesType.Line, t('Line')], - [EchartsTimeseriesSeriesType.Smooth, t('Smooth Line')], - [EchartsTimeseriesSeriesType.Start, t('Step - start')], - [EchartsTimeseriesSeriesType.Middle, t('Step - middle')], - [EchartsTimeseriesSeriesType.End, t('Step - end')], - ], - description: t('Series chart type (line, bar etc)'), - }, - }, - ], - [ - { - name: 'opacity', - config: { - type: 'SliderControl', - label: t('Area chart opacity'), - renderTrigger: true, - min: 0, - max: 1, - step: 0.1, - default: opacity, - description: t( - 'Opacity of Area Chart. Also applies to confidence band.', - ), - }, - }, - ], - [showValueControl], - [ - { - name: 'stack', - config: { - type: 'SelectControl', - label: t('Stacked Style'), - renderTrigger: true, - choices: AreaChartStackControlOptions, - default: null, - description: t('Stack series on top of each other'), - }, - }, - ], - [onlyTotalControl], - [percentageThresholdControl], - [ - { - name: 'show_extra_controls', - config: { - type: 'CheckboxControl', - label: t('Extra Controls'), - renderTrigger: true, - default: false, - description: t( - 'Whether to show extra controls or not. Extra controls ' + - 'include things like making multiBar charts stacked ' + - 'or side by side.', - ), - }, - }, - ], - [ - { - name: 'markerEnabled', - config: { - type: 'CheckboxControl', - label: t('Marker'), - renderTrigger: true, - default: markerEnabled, - description: t( - 'Draw a marker on data points. Only applicable for line types.', - ), - }, - }, - ], - [ - { - name: 'markerSize', - config: { - type: 'SliderControl', - label: t('Marker Size'), - renderTrigger: true, - min: 0, - max: 20, - default: markerSize, - description: t( - 'Size of marker. Also applies to forecast observations.', - ), - visibility: ({ controls }: ControlPanelsContainerProps) => - Boolean(controls?.markerEnabled?.value), - }, - }, - ], - [minorTicks], - ['zoomable'], - ...legendSection, - [{t('X Axis')}], - [ - { - name: 'x_axis_time_format', - config: { - ...sharedControls.x_axis_time_format, - default: 'smart_date', - description: `${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`, - visibility: ({ controls }: ControlPanelsContainerProps) => - checkColumnType( - getColumnLabel(controls?.x_axis?.value as QueryFormColumn), - controls?.datasource?.datasource, - [GenericDataType.Temporal], - ), - disableStash: true, - resetOnHide: false, - }, - }, - ], - [ - { - name: 'x_axis_number_format', - config: { - ...sharedControls.x_axis_number_format, - default: '~g', - mapStateToProps: undefined, - visibility: ({ controls }: ControlPanelsContainerProps) => - checkColumnType( - getColumnLabel(controls?.x_axis?.value as QueryFormColumn), - controls?.datasource?.datasource, - [GenericDataType.Numeric], - ), - }, - }, - ], - [xAxisLabelRotation], - [xAxisLabelInterval], - [forceMaxInterval], - ...richTooltipSection, - // eslint-disable-next-line react/jsx-key - [{t('Y Axis')}], - ['y_axis_format'], - ['currency_format'], - [ - { - name: 'logAxis', - config: { - type: 'CheckboxControl', - label: t('Logarithmic y-axis'), - renderTrigger: true, - default: logAxis, - description: t('Logarithmic y-axis'), - }, - }, - ], - [ - { - name: 'minorSplitLine', - config: { - type: 'CheckboxControl', - label: t('Minor Split Line'), - renderTrigger: true, - default: minorSplitLine, - description: t('Draw split lines for minor y-axis ticks'), - }, - }, - ], - [truncateXAxis], - [xAxisBounds], - [ - { - name: 'truncateYAxis', - config: { - type: 'CheckboxControl', - label: t('Truncate Y Axis'), - default: truncateYAxis, - renderTrigger: true, - description: t( - 'Truncate Y Axis. Can be overridden by specifying a min or max bound.', - ), - }, - }, - ], - [ - { - name: 'y_axis_bounds', - config: { - type: 'BoundsControl', - label: t('Y Axis Bounds'), - renderTrigger: true, - default: yAxisBounds, - description: t( - 'Bounds for the Y-axis. When left empty, the bounds are ' + - 'dynamically defined based on the min/max of the data. Note that ' + - "this feature will only expand the axis range. It won't " + - "narrow the data's extent.", - ), - visibility: ({ controls }: ControlPanelsContainerProps) => - Boolean(controls?.truncateYAxis?.value), - }, - }, - ], - ['echart_options'], - ], - }, - ], - controlOverrides: { - row_limit: { - default: rowLimit, - }, - }, - formDataOverrides: formData => ({ - ...formData, - metrics: getStandardizedControls().popAllMetrics(), - groupby: getStandardizedControls().popAllColumns(), - }), -}; - -export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/index.ts deleted file mode 100644 index b860f9638e6..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/index.ts +++ /dev/null @@ -1,84 +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 { t } from '@apache-superset/core/translation'; -import { AnnotationType, Behavior } from '@superset-ui/core'; -import buildQuery from '../buildQuery'; -import controlPanel from './controlPanel'; -import transformProps from '../transformProps'; -import thumbnail from './images/thumbnail.png'; -import thumbnailDark from './images/thumbnail-dark.png'; -import { - EchartsTimeseriesChartProps, - EchartsTimeseriesFormData, -} from '../types'; -import example1 from './images/Area1.png'; -import example1Dark from './images/Area1-dark.png'; -import { EchartsChartPlugin } from '../../types'; - -const areaTransformProps = (chartProps: EchartsTimeseriesChartProps) => - transformProps({ - ...chartProps, - formData: { ...chartProps.formData, area: true }, - }); - -export default class EchartsAreaChartPlugin extends EchartsChartPlugin< - EchartsTimeseriesFormData, - EchartsTimeseriesChartProps -> { - constructor() { - super({ - buildQuery, - controlPanel, - loadChart: () => import('../EchartsTimeseries'), - metadata: { - behaviors: [ - Behavior.InteractiveChart, - Behavior.DrillToDetail, - Behavior.DrillBy, - ], - category: t('Evolution'), - credits: ['https://echarts.apache.org'], - description: t( - 'Area charts are similar to line charts in that they represent variables with the same scale, but area charts stack the metrics on top of each other.', - ), - exampleGallery: [{ url: example1, urlDark: example1Dark }], - supportedAnnotationTypes: [ - AnnotationType.Event, - AnnotationType.Formula, - AnnotationType.Interval, - AnnotationType.Timeseries, - ], - name: t('Area Chart'), - tags: [ - t('ECharts'), - t('Predictive'), - t('Advanced-Analytics'), - t('Time'), - t('Line'), - t('Transformable'), - t('Stacked'), - t('Featured'), - ], - thumbnail, - thumbnailDark, - }, - transformProps: areaTransformProps, - }); - } -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/index.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/index.tsx new file mode 100644 index 00000000000..bd11921503e --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/index.tsx @@ -0,0 +1,431 @@ +/** + * 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. + */ + +/** + * Timeseries Area Chart - Glyph Pattern Implementation + * + * Area charts are similar to line charts in that they represent variables + * with the same scale, but area charts stack the metrics on top of each other. + * + * Key characteristics: + * - area is always enabled (hardcoded true) + * - seriesType is configurable (Line, Smooth, Step variants) + * - Supports stacking (Stack, Stream, Expand modes) + * - Supports configurable area opacity + * - Markers are optional (toggle with markerEnabled) + * - Supports cross-filtering, drill-to-detail, and drill-by + * - Has ExtraControls for area stack radio buttons + */ + +import { t } from '@apache-superset/core/translation'; +import { AnnotationType, Behavior, ChartProps } from '@superset-ui/core'; +import { + ControlPanelsContainerProps, + ControlSubSectionHeader, + D3_TIME_FORMAT_DOCS, + DEFAULT_SORT_SERIES_DATA, + getStandardizedControls, + sharedControls, + SORT_SERIES_CHOICES, +} from '@superset-ui/chart-controls'; + +import { defineChart } from '@superset-ui/glyph-core'; +import { Checkbox, Slider, Select } from '@superset-ui/glyph-core'; +import { + EchartsTimeseriesChartProps, + EchartsTimeseriesSeriesType, + TimeseriesChartTransformedProps, +} from '../types'; +import { DEFAULT_FORM_DATA, TIME_SERIES_DESCRIPTION_TEXT } from '../constants'; +import buildQuery from '../buildQuery'; +import { + legendSection, + minorTicks, + onlyTotalControl, + percentageThresholdControl, + richTooltipSection, + showValueControl, + truncateXAxis, + xAxisBounds, + xAxisLabelRotation, + xAxisLabelInterval, + forceMaxInterval, +} from '../../controls'; +import { AreaChartStackControlOptions } from '../../constants'; +import { + transformFullTimeseriesProps, + TimeseriesRender, + timeseriesQueryControls, + timeseriesBaseChartOptions, + markerConditionalRows, +} from '../shared'; + +import thumbnail from './images/thumbnail.png'; +import thumbnailDark from './images/thumbnail-dark.png'; +import example1 from './images/Area1.png'; +import example1Dark from './images/Area1-dark.png'; + +// ============================================================================ +// Types +// ============================================================================ + +interface AreaTransformResult { + transformedProps: TimeseriesChartTransformedProps; +} + +// ============================================================================ +// Constants +// ============================================================================ + +const { + logAxis, + markerEnabled, + markerSize, + minorSplitLine, + opacity, + rowLimit, + seriesType, + truncateYAxis, + yAxisBounds, +} = DEFAULT_FORM_DATA; + +// ============================================================================ +// Chart Definition +// ============================================================================ + +export default defineChart({ + metadata: { + name: t('Area Chart'), + description: t( + 'Area charts are similar to line charts in that they represent variables with the same scale, but area charts stack the metrics on top of each other.', + ), + category: t('Evolution'), + tags: [ + t('ECharts'), + t('Predictive'), + t('Advanced-Analytics'), + t('Time'), + t('Line'), + t('Transformable'), + t('Stacked'), + t('Featured'), + ], + credits: ['https://echarts.apache.org'], + thumbnail, + thumbnailDark, + exampleGallery: [{ url: example1, urlDark: example1Dark }], + behaviors: [ + Behavior.InteractiveChart, + Behavior.DrillToDetail, + Behavior.DrillBy, + ], + supportedAnnotationTypes: [ + AnnotationType.Event, + AnnotationType.Formula, + AnnotationType.Interval, + AnnotationType.Timeseries, + ], + }, + + arguments: { + // Series style + seriesType: Select.with({ + label: t('Series Style'), + description: t('Series chart type (line, bar etc)'), + options: [ + { value: EchartsTimeseriesSeriesType.Line, label: t('Line') }, + { value: EchartsTimeseriesSeriesType.Smooth, label: t('Smooth Line') }, + { value: EchartsTimeseriesSeriesType.Start, label: t('Step - start') }, + { + value: EchartsTimeseriesSeriesType.Middle, + label: t('Step - middle'), + }, + { value: EchartsTimeseriesSeriesType.End, label: t('Step - end') }, + ], + default: seriesType, + }), + + // Marker controls + markerEnabled: Checkbox.with({ + label: t('Marker'), + description: t( + 'Draw a marker on data points. Only applicable for line types.', + ), + default: markerEnabled, + }), + + markerSize: { + arg: Slider.with({ + label: t('Marker Size'), + description: t( + 'Size of marker. Also applies to forecast observations.', + ), + default: markerSize, + min: 0, + max: 20, + step: 1, + }), + visibleWhen: { markerEnabled: true }, + }, + + // Show value control + showValue: Checkbox.with({ + label: t('Show Value'), + description: t('Show series values on the chart'), + default: false, + }), + + // Zoom control + zoomable: Checkbox.with({ + label: t('Data Zoom'), + description: t('Enable data zooming controls'), + default: false, + }), + + // Y-Axis controls + logAxis: Checkbox.with({ + label: t('Logarithmic Y-axis'), + description: t('Logarithmic y-axis'), + default: logAxis, + }), + + minorSplitLine: Checkbox.with({ + label: t('Minor Split Line'), + description: t('Draw split lines for minor y-axis ticks'), + default: minorSplitLine, + }), + + truncateYAxis: Checkbox.with({ + label: t('Truncate Y Axis'), + description: t( + 'Truncate Y Axis. Can be overridden by specifying a min or max bound.', + ), + default: truncateYAxis, + }), + + // Series order controls + sortSeriesType: Select.with({ + label: t('Sort Series By'), + description: t( + 'Based on what should series be ordered on the chart and legend', + ), + options: SORT_SERIES_CHOICES.map(([value, label]) => ({ + value, + label, + })), + default: DEFAULT_SORT_SERIES_DATA.sort_series_type, + }), + + sortSeriesAscending: Checkbox.with({ + label: t('Sort Series Ascending'), + description: t('Sort series in ascending order'), + default: DEFAULT_SORT_SERIES_DATA.sort_series_ascending, + }), + }, + + additionalControls: { + query: timeseriesQueryControls, + chartOptions: [ + ...timeseriesBaseChartOptions, + [ + { + name: 'seriesType', + config: { + type: 'SelectControl', + label: t('Series Style'), + renderTrigger: true, + default: seriesType, + choices: [ + [EchartsTimeseriesSeriesType.Line, t('Line')], + [EchartsTimeseriesSeriesType.Smooth, t('Smooth Line')], + [EchartsTimeseriesSeriesType.Start, t('Step - start')], + [EchartsTimeseriesSeriesType.Middle, t('Step - middle')], + [EchartsTimeseriesSeriesType.End, t('Step - end')], + ], + description: t('Series chart type (line, bar etc)'), + }, + }, + ], + [ + { + name: 'opacity', + config: { + type: 'SliderControl', + label: t('Area chart opacity'), + renderTrigger: true, + min: 0, + max: 1, + step: 0.1, + default: opacity, + description: t( + 'Opacity of Area Chart. Also applies to confidence band.', + ), + }, + }, + ], + [showValueControl], + [ + { + name: 'stack', + config: { + type: 'SelectControl', + label: t('Stacked Style'), + renderTrigger: true, + choices: AreaChartStackControlOptions, + default: null, + description: t('Stack series on top of each other'), + }, + }, + ], + [onlyTotalControl], + [percentageThresholdControl], + [ + { + name: 'show_extra_controls', + config: { + type: 'CheckboxControl', + label: t('Extra Controls'), + renderTrigger: true, + default: false, + description: t( + 'Whether to show extra controls or not. Extra controls ' + + 'include things like making multiBar charts stacked ' + + 'or side by side.', + ), + }, + }, + ], + ...markerConditionalRows, + [minorTicks], + ['zoomable'], + ...legendSection, + [ + + {t('X Axis')} + , + ], + [ + { + name: 'x_axis_time_format', + config: { + ...sharedControls.x_axis_time_format, + default: 'smart_date', + description: `${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`, + }, + }, + ], + [xAxisLabelRotation], + [xAxisLabelInterval], + [forceMaxInterval], + ...richTooltipSection, + [ + + {t('Y Axis')} + , + ], + ['y_axis_format'], + ['currency_format'], + [ + { + name: 'logAxis', + config: { + type: 'CheckboxControl', + label: t('Logarithmic y-axis'), + renderTrigger: true, + default: logAxis, + description: t('Logarithmic y-axis'), + }, + }, + ], + [ + { + name: 'minorSplitLine', + config: { + type: 'CheckboxControl', + label: t('Minor Split Line'), + renderTrigger: true, + default: minorSplitLine, + description: t('Draw split lines for minor y-axis ticks'), + }, + }, + ], + [truncateXAxis], + [xAxisBounds], + [ + { + name: 'truncateYAxis', + config: { + type: 'CheckboxControl', + label: t('Truncate Y Axis'), + default: truncateYAxis, + renderTrigger: true, + description: t( + 'Truncate Y Axis. Can be overridden by specifying a min or max bound.', + ), + }, + }, + ], + [ + { + name: 'y_axis_bounds', + config: { + type: 'BoundsControl', + label: t('Y Axis Bounds'), + renderTrigger: true, + default: yAxisBounds, + description: t( + 'Bounds for the Y-axis. When left empty, the bounds are ' + + 'dynamically defined based on the min/max of the data. Note that ' + + "this feature will only expand the axis range. It won't " + + "narrow the data's extent.", + ), + visibility: ({ controls }: ControlPanelsContainerProps) => + Boolean(controls?.truncateYAxis?.value), + }, + }, + ], + ], + }, + + controlOverrides: { + row_limit: { + default: rowLimit, + }, + }, + + formDataOverrides: formData => ({ + ...formData, + metrics: getStandardizedControls().popAllMetrics(), + groupby: getStandardizedControls().popAllColumns(), + }), + + buildQuery, + + transform: (chartProps: ChartProps): AreaTransformResult => { + const transformedProps = transformFullTimeseriesProps( + chartProps as unknown as EchartsTimeseriesChartProps, + { area: true }, + ); + return { transformedProps }; + }, + + render: ({ transformedProps }) => ( + + ), +}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Bar/images/Bar1-dark.png b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Bar/images/Bar1-dark.png new file mode 100644 index 00000000000..e029ab6a9d4 Binary files /dev/null and b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Bar/images/Bar1-dark.png differ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Bar/images/Bar1.png b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Bar/images/Bar1.png new file mode 100644 index 00000000000..94dd5c58600 Binary files /dev/null and b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Bar/images/Bar1.png differ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Bar/images/Bar2-dark.png b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Bar/images/Bar2-dark.png new file mode 100644 index 00000000000..64b7cd02435 Binary files /dev/null and b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Bar/images/Bar2-dark.png differ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Bar/images/Bar2.png b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Bar/images/Bar2.png new file mode 100644 index 00000000000..5c028c5d85d Binary files /dev/null and b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Bar/images/Bar2.png differ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Bar/images/Bar3-dark.png b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Bar/images/Bar3-dark.png new file mode 100644 index 00000000000..b284026af37 Binary files /dev/null and b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Bar/images/Bar3-dark.png differ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Bar/images/Bar3.png b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Bar/images/Bar3.png new file mode 100644 index 00000000000..84f826fc86e Binary files /dev/null and b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Bar/images/Bar3.png differ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Bar/images/thumbnail-dark.png b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Bar/images/thumbnail-dark.png new file mode 100644 index 00000000000..d533a049400 Binary files /dev/null and b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Bar/images/thumbnail-dark.png differ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Bar/images/thumbnail.png b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Bar/images/thumbnail.png new file mode 100644 index 00000000000..29eed8d1d40 Binary files /dev/null and b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Bar/images/thumbnail.png differ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Bar/index.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Bar/index.tsx new file mode 100644 index 00000000000..2b004d7e12a --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Bar/index.tsx @@ -0,0 +1,444 @@ +/** + * 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. + */ + +/** + * Timeseries Bar Chart - Glyph Pattern Implementation + * + * Bar Charts are used to show metrics as a series of bars. + * Supports both vertical and horizontal orientation. + * + * Key characteristics: + * - seriesType is fixed to 'bar' + * - Supports vertical and horizontal orientation + * - Supports stacking (Stack, Stream modes) with stackDimension + * - No area fills or markers (bars are the visual) + * - Supports cross-filtering, drill-to-detail, and drill-by + * - Supports annotations (event, formula, interval, timeseries) + * - Has ExtraControls for stack radio buttons + * + * Control panel simplification: + * Uses "Primary Axis (Categories)" / "Secondary Axis (Values)" grouping + * instead of orientation-dependent X/Y visibility toggling. The transform's + * isHorizontal axis swap handles physical axis mapping transparently. + */ + +import { t } from '@apache-superset/core/translation'; +import { + AnnotationType, + Behavior, + ChartProps, + ensureIsArray, + JsonArray, +} from '@superset-ui/core'; +import { + ControlPanelsContainerProps, + ControlSubSectionHeader, + D3_TIME_FORMAT_DOCS, + DEFAULT_SORT_SERIES_DATA, + formatSelectOptions, + getStandardizedControls, + sections, + sharedControls, + SORT_SERIES_CHOICES, +} from '@superset-ui/chart-controls'; + +import { defineChart } from '@superset-ui/glyph-core'; +import { Checkbox, Select } from '@superset-ui/glyph-core'; +import { + EchartsTimeseriesChartProps, + EchartsTimeseriesSeriesType, + OrientationType, + TimeseriesChartTransformedProps, +} from '../types'; +import { DEFAULT_FORM_DATA, TIME_SERIES_DESCRIPTION_TEXT } from '../constants'; +import buildQuery from '../buildQuery'; +import { + legendSection, + minorTicks, + richTooltipSection, + seriesOrderSection, + showValueSection, + truncateXAxis, + xAxisBounds, + xAxisLabelRotation, + xAxisLabelInterval, + forceMaxInterval, +} from '../../controls'; +import { StackControlsValue } from '../../constants'; +import { + transformFullTimeseriesProps, + TimeseriesRender, + timeseriesQueryControls, +} from '../shared'; + +import thumbnail from './images/thumbnail.png'; +import thumbnailDark from './images/thumbnail-dark.png'; +import example1 from './images/Bar1.png'; +import example1Dark from './images/Bar1-dark.png'; +import example2 from './images/Bar2.png'; +import example2Dark from './images/Bar2-dark.png'; +import example3 from './images/Bar3.png'; +import example3Dark from './images/Bar3-dark.png'; + +// ============================================================================ +// Types +// ============================================================================ + +interface BarTransformResult { + transformedProps: TimeseriesChartTransformedProps; +} + +// ============================================================================ +// Constants +// ============================================================================ + +const { logAxis, minorSplitLine, orientation, truncateYAxis, yAxisBounds } = + DEFAULT_FORM_DATA; + +// ============================================================================ +// Chart Definition +// ============================================================================ + +export default defineChart({ + metadata: { + name: t('Bar Chart'), + description: t('Bar Charts are used to show metrics as a series of bars.'), + category: t('Evolution'), + tags: [ + t('ECharts'), + t('Predictive'), + t('Advanced-Analytics'), + t('Time'), + t('Transformable'), + t('Stacked'), + t('Bar'), + t('Featured'), + ], + credits: ['https://echarts.apache.org'], + thumbnail, + thumbnailDark, + exampleGallery: [ + { url: example1, urlDark: example1Dark }, + { url: example2, urlDark: example2Dark }, + { url: example3, urlDark: example3Dark }, + ], + behaviors: [ + Behavior.InteractiveChart, + Behavior.DrillToDetail, + Behavior.DrillBy, + ], + supportedAnnotationTypes: [ + AnnotationType.Event, + AnnotationType.Formula, + AnnotationType.Interval, + AnnotationType.Timeseries, + ], + }, + + arguments: { + // Show value control + showValue: Checkbox.with({ + label: t('Show Value'), + description: t('Show series values on the chart'), + default: false, + }), + + // Zoom control + zoomable: Checkbox.with({ + label: t('Data Zoom'), + description: t('Enable data zooming controls'), + default: false, + }), + + // Value axis controls + logAxis: Checkbox.with({ + label: t('Logarithmic axis'), + description: t('Logarithmic axis'), + default: logAxis, + }), + + minorSplitLine: Checkbox.with({ + label: t('Minor Split Line'), + description: t('Draw split lines for minor axis ticks'), + default: minorSplitLine, + }), + + truncateYAxis: Checkbox.with({ + label: t('Truncate Axis'), + description: t("It's not recommended to truncate axis in Bar chart."), + default: truncateYAxis, + }), + + // Series order controls + sortSeriesType: Select.with({ + label: t('Sort Series By'), + description: t( + 'Based on what should series be ordered on the chart and legend', + ), + options: SORT_SERIES_CHOICES.map(([value, label]) => ({ + value, + label, + })), + default: DEFAULT_SORT_SERIES_DATA.sort_series_type, + }), + + sortSeriesAscending: Checkbox.with({ + label: t('Sort Series Ascending'), + description: t('Sort series in ascending order'), + default: DEFAULT_SORT_SERIES_DATA.sort_series_ascending, + }), + }, + + additionalControls: { + query: timeseriesQueryControls, + chartOptions: [ + // Orientation + [ + { + name: 'orientation', + config: { + type: 'RadioButtonControl', + renderTrigger: true, + label: t('Bar orientation'), + default: orientation, + options: [ + [OrientationType.Vertical, t('Vertical')], + [OrientationType.Horizontal, t('Horizontal')], + ], + description: t('Orientation of bar chart'), + }, + }, + ], + // Chart Options + [ + + {t('Chart Options')} + , + ], + ...seriesOrderSection, + ['color_scheme'], + ['time_shift_color'], + ...showValueSection, + [ + { + name: 'stackDimension', + config: { + type: 'SelectControl', + label: t('Split stack by'), + visibility: ({ controls }: ControlPanelsContainerProps) => + controls?.stack?.value === StackControlsValue.Stack, + renderTrigger: true, + description: t( + 'Stack in groups, where each group corresponds to a dimension', + ), + shouldMapStateToProps: () => true, + mapStateToProps: (state: Record) => { + const value: JsonArray = ensureIsArray( + state.controls.groupby?.value, + ) as JsonArray; + const valueAsStringArr: string[][] = value.map(v => { + if (v) return [v.toString(), v.toString()]; + return ['', '']; + }); + return { + choices: valueAsStringArr, + }; + }, + }, + }, + ], + [minorTicks], + ['zoomable'], + ...legendSection, + // Primary Axis (Categories) - controls the dimension/time axis + // The transform's isHorizontal swap handles physical axis mapping + [ + + {t('Primary Axis (Categories)')} + , + ], + [ + { + name: 'x_axis_title', + config: { + type: 'TextControl', + label: t('Axis Title'), + renderTrigger: true, + default: '', + }, + }, + ], + [ + { + name: 'x_axis_title_margin', + config: { + type: 'SelectControl', + freeForm: true, + clearable: true, + label: t('Axis title margin'), + renderTrigger: true, + default: sections.TITLE_MARGIN_OPTIONS[0], + choices: formatSelectOptions(sections.TITLE_MARGIN_OPTIONS), + }, + }, + ], + [ + { + name: 'x_axis_time_format', + config: { + ...sharedControls.x_axis_time_format, + default: 'smart_date', + description: `${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`, + }, + }, + ], + [xAxisLabelRotation], + [xAxisLabelInterval], + [forceMaxInterval], + [truncateXAxis], + [xAxisBounds], + // Rich tooltip + ...richTooltipSection, + // Secondary Axis (Values) - controls the metric/value axis + [ + + {t('Secondary Axis (Values)')} + , + ], + [ + { + name: 'y_axis_title', + config: { + type: 'TextControl', + label: t('Axis Title'), + renderTrigger: true, + default: '', + }, + }, + ], + [ + { + name: 'y_axis_title_margin', + config: { + type: 'SelectControl', + freeForm: true, + clearable: true, + label: t('Axis title margin'), + renderTrigger: true, + default: sections.TITLE_MARGIN_OPTIONS[1], + choices: formatSelectOptions(sections.TITLE_MARGIN_OPTIONS), + }, + }, + ], + [ + { + name: 'y_axis_title_position', + config: { + type: 'SelectControl', + freeForm: true, + clearable: false, + label: t('Axis title position'), + renderTrigger: true, + default: sections.TITLE_POSITION_OPTIONS[0][0], + choices: sections.TITLE_POSITION_OPTIONS, + }, + }, + ], + ['y_axis_format'], + ['currency_format'], + [ + { + name: 'logAxis', + config: { + type: 'CheckboxControl', + label: t('Logarithmic axis'), + renderTrigger: true, + default: logAxis, + description: t('Logarithmic axis'), + }, + }, + ], + [ + { + name: 'minorSplitLine', + config: { + type: 'CheckboxControl', + label: t('Minor Split Line'), + renderTrigger: true, + default: minorSplitLine, + description: t('Draw split lines for minor axis ticks'), + }, + }, + ], + [ + { + name: 'truncateYAxis', + config: { + type: 'CheckboxControl', + label: t('Truncate Axis'), + default: truncateYAxis, + renderTrigger: true, + description: t( + "It's not recommended to truncate axis in Bar chart.", + ), + }, + }, + ], + [ + { + name: 'y_axis_bounds', + config: { + type: 'BoundsControl', + label: t('Axis Bounds'), + renderTrigger: true, + default: yAxisBounds, + description: t( + 'Bounds for the axis. When left empty, the bounds are ' + + 'dynamically defined based on the min/max of the data. Note that ' + + "this feature will only expand the axis range. It won't " + + "narrow the data's extent.", + ), + visibility: ({ controls }: ControlPanelsContainerProps) => + Boolean(controls?.truncateYAxis?.value), + }, + }, + ], + ], + }, + + formDataOverrides: formData => ({ + ...formData, + metrics: getStandardizedControls().popAllMetrics(), + groupby: getStandardizedControls().popAllColumns(), + }), + + buildQuery, + + transform: (chartProps: ChartProps): BarTransformResult => { + const transformedProps = transformFullTimeseriesProps( + chartProps as unknown as EchartsTimeseriesChartProps, + { seriesType: EchartsTimeseriesSeriesType.Bar }, + ); + return { transformedProps }; + }, + + render: ({ transformedProps }) => ( + + ), +}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.test.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.test.tsx deleted file mode 100644 index 6804f9c9784..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.test.tsx +++ /dev/null @@ -1,398 +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 { - render, - waitFor, - cleanup, -} from '../../../../spec/helpers/testing-library'; -import { AxisType } from '@superset-ui/core'; -import type { EChartsCoreOption } from 'echarts/core'; -import type { ReactNode } from 'react'; -import { - LegendOrientation, - LegendType, - type EchartsHandler, - type EchartsProps, -} from '../types'; -import EchartsTimeseries from './EchartsTimeseries'; -import { - EchartsTimeseriesSeriesType, - OrientationType, - type EchartsTimeseriesFormData, - type TimeseriesChartTransformedProps, -} from './types'; - -const mockEchart = jest.fn(); - -jest.mock('../components/Echart', () => { - const { forwardRef } = jest.requireActual('react'); - const MockEchart = forwardRef( - (props, _ref) => { - mockEchart(props); - return null; - }, - ); - MockEchart.displayName = 'MockEchart'; - return { - __esModule: true, - default: MockEchart, - }; -}); - -jest.mock('../components/ExtraControls', () => ({ - ExtraControls: ({ children }: { children?: ReactNode }) => ( -
{children}
- ), -})); - -const originalResizeObserver = globalThis.ResizeObserver; -const offsetHeightDescriptor = Object.getOwnPropertyDescriptor( - HTMLElement.prototype, - 'offsetHeight', -); - -let mockOffsetHeight = 0; - -beforeAll(() => { - Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { - configurable: true, - get() { - return mockOffsetHeight; - }, - }); -}); - -afterAll(() => { - if (offsetHeightDescriptor) { - Object.defineProperty( - HTMLElement.prototype, - 'offsetHeight', - offsetHeightDescriptor, - ); - } else { - delete (HTMLElement.prototype as { offsetHeight?: number }).offsetHeight; - } -}); - -afterEach(() => { - cleanup(); - mockEchart.mockReset(); - (globalThis as { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = - originalResizeObserver; -}); - -const defaultFormData: EchartsTimeseriesFormData & { - vizType: string; - dateFormat: string; - numberFormat: string; - granularitySqla?: string; -} = { - annotationLayers: [], - area: false, - colorScheme: undefined, - timeShiftColor: false, - contributionMode: undefined, - forecastEnabled: false, - forecastPeriods: 0, - forecastInterval: 0, - forecastSeasonalityDaily: null, - forecastSeasonalityWeekly: null, - forecastSeasonalityYearly: null, - logAxis: false, - markerEnabled: false, - markerSize: 1, - metrics: [], - minorSplitLine: false, - minorTicks: false, - opacity: 1, - orderDesc: false, - rowLimit: 0, - seriesType: EchartsTimeseriesSeriesType.Line, - stack: null, - stackDimension: '', - timeCompare: [], - tooltipTimeFormat: undefined, - showTooltipTotal: false, - showTooltipPercentage: false, - truncateXAxis: false, - truncateYAxis: false, - yAxisFormat: undefined, - xAxisForceCategorical: false, - xAxisTimeFormat: undefined, - timeGrainSqla: undefined, - forceMaxInterval: false, - xAxisBounds: [null, null], - yAxisBounds: [null, null], - zoomable: false, - richTooltip: false, - xAxisLabelRotation: 0, - xAxisLabelInterval: 0, - showValue: false, - onlyTotal: false, - showExtraControls: true, - percentageThreshold: 0, - orientation: OrientationType.Vertical, - datasource: '1__table', - viz_type: 'echarts_timeseries', - legendMargin: 0, - legendOrientation: LegendOrientation.Top, - legendType: LegendType.Plain, - showLegend: false, - legendSort: null, - xAxisTitle: '', - xAxisTitleMargin: 40, - yAxisTitle: '', - yAxisTitleMargin: 50, - yAxisTitlePosition: '', - time_range: 'No filter', - granularity: undefined, - granularity_sqla: undefined, - sql: '', - url_params: {}, - custom_params: {}, - extra_form_data: {}, - adhoc_filters: [], - order_desc: false, - row_limit: 0, - row_offset: 0, - time_grain_sqla: undefined, - vizType: 'echarts_timeseries', - dateFormat: 'smart_date', - numberFormat: 'SMART_NUMBER', -}; - -const defaultProps: TimeseriesChartTransformedProps = { - echartOptions: {} as EChartsCoreOption, - formData: defaultFormData, - height: 400, - width: 800, - onContextMenu: jest.fn(), - setDataMask: jest.fn(), - onLegendStateChanged: jest.fn(), - refs: {}, - emitCrossFilters: false, - coltypeMapping: {}, - onLegendScroll: jest.fn(), - groupby: [], - labelMap: {}, - setControlValue: jest.fn(), - selectedValues: {}, - legendData: [], - xValueFormatter: String, - xAxis: { - label: 'x', - type: AxisType.Time, - }, - onFocusedSeries: jest.fn(), -}; - -function getLatestHeight() { - const lastCall = mockEchart.mock.calls.at(-1); - expect(lastCall).toBeDefined(); - const [props] = lastCall as [EchartsProps]; - return props.height; -} - -test('observes extra control height changes when ResizeObserver is available', async () => { - const disconnectSpy = jest.fn(); - const observeSpy = jest.fn(); - - class MockResizeObserver implements ResizeObserver { - private static latestInstance: MockResizeObserver | null = null; - private readonly callback: ResizeObserverCallback; - - constructor(callback: ResizeObserverCallback) { - this.callback = callback; - MockResizeObserver.latestInstance = this; - } - - observe = (target: Element) => { - observeSpy(target); - }; - - unobserve(_target: Element): void {} - - disconnect = () => { - disconnectSpy(); - }; - - trigger(entries: ResizeObserverEntry[] = []) { - this.callback(entries, this); - } - - static getLatestInstance() { - return this.latestInstance; - } - } - - (globalThis as { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = - MockResizeObserver as unknown as typeof ResizeObserver; - - mockOffsetHeight = 42; - const { unmount } = render(); - - await waitFor(() => { - expect(getLatestHeight()).toBe(defaultProps.height - mockOffsetHeight); - }); - - expect(observeSpy).toHaveBeenCalledWith(expect.any(HTMLElement)); - - mockOffsetHeight = 24; - MockResizeObserver.getLatestInstance()?.trigger(); - - await waitFor(() => { - expect(getLatestHeight()).toBe(defaultProps.height - mockOffsetHeight); - }); - - expect(disconnectSpy).not.toHaveBeenCalled(); - - expect(MockResizeObserver.getLatestInstance()).not.toBeNull(); - - unmount(); - - expect(disconnectSpy).toHaveBeenCalled(); -}); - -test('falls back to window resize listener when ResizeObserver is unavailable', async () => { - (globalThis as { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = - undefined; - - const addEventListenerSpy = jest.spyOn(window, 'addEventListener'); - const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener'); - - mockOffsetHeight = 30; - - const { unmount } = render(); - - await waitFor(() => { - expect(getLatestHeight()).toBe(defaultProps.height - mockOffsetHeight); - }); - - expect(addEventListenerSpy).toHaveBeenCalledWith( - 'resize', - expect.any(Function), - ); - - mockOffsetHeight = 10; - window.dispatchEvent(new Event('resize')); - - await waitFor(() => { - expect(getLatestHeight()).toBe(defaultProps.height - mockOffsetHeight); - }); - - unmount(); - - expect(removeEventListenerSpy).toHaveBeenCalledWith( - 'resize', - expect.any(Function), - ); - - addEventListenerSpy.mockRestore(); - removeEventListenerSpy.mockRestore(); -}); - -// Test for issue #25334: Bar chart cross-filter without dimensions -test('emits cross-filter on X-axis value when no dimensions and categorical X-axis', async () => { - const setDataMaskMock = jest.fn(); - - const propsWithCategoricalXAxis: TimeseriesChartTransformedProps = { - ...defaultProps, - emitCrossFilters: true, - setDataMask: setDataMaskMock, - groupby: [], // No dimensions - xAxis: { - label: 'category_column', - type: AxisType.Category, // Categorical X-axis - }, - }; - - render(); - - // Get the click handler from the mock - const lastCall = mockEchart.mock.calls.at(-1); - expect(lastCall).toBeDefined(); - const [props] = lastCall as [EchartsProps]; - expect(props.eventHandlers).toBeDefined(); - expect(props.eventHandlers?.click).toBeDefined(); - - // Simulate a click event with X-axis data - const clickHandler = props.eventHandlers?.click; - if (clickHandler) { - clickHandler({ - seriesName: 'Sales', // This is the metric name - data: ['Product A', 100], // X-axis value is 'Product A' - name: 'Product A', - dataIndex: 0, - }); - - // Wait for the timer (TIMER_DURATION = 300ms) - await waitFor( - () => { - expect(setDataMaskMock).toHaveBeenCalled(); - }, - { timeout: 500 }, - ); - - // Verify the cross-filter uses the X-axis column and value, not the metric - const dataMaskCall = setDataMaskMock.mock.calls[0][0]; - expect(dataMaskCall.extraFormData.filters).toEqual([ - { - col: 'category_column', // X-axis column - op: 'IN', - val: ['Product A'], // X-axis value, not 'Sales' (metric) - }, - ]); - } -}); - -test('does not emit cross-filter when no dimensions and time-based X-axis', async () => { - const setDataMaskMock = jest.fn(); - - const propsWithTimeXAxis: TimeseriesChartTransformedProps = { - ...defaultProps, - emitCrossFilters: true, - setDataMask: setDataMaskMock, - groupby: [], // No dimensions - xAxis: { - label: '__timestamp', - type: AxisType.Time, // Time-based X-axis (not categorical) - }, - }; - - render(); - - const lastCall = mockEchart.mock.calls.at(-1); - expect(lastCall).toBeDefined(); - const [props] = lastCall as [EchartsProps]; - - // Simulate a click event - const clickHandler = props.eventHandlers?.click; - if (clickHandler) { - clickHandler({ - seriesName: 'Sales', - data: [1609459200000, 100], // Timestamp - name: '2021-01-01', - dataIndex: 0, - }); - - // Wait a bit and verify setDataMask was NOT called - await new Promise(resolve => setTimeout(resolve, 400)); - expect(setDataMaskMock).not.toHaveBeenCalled(); - } -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Line/images/Line1-dark.png b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Line/images/Line1-dark.png new file mode 100644 index 00000000000..c7835b3e182 Binary files /dev/null and b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Line/images/Line1-dark.png differ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Line/images/Line1.png b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Line/images/Line1.png new file mode 100644 index 00000000000..d19150b1d5d Binary files /dev/null and b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Line/images/Line1.png differ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Line/images/Line2-dark.png b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Line/images/Line2-dark.png new file mode 100644 index 00000000000..bfbb188bd36 Binary files /dev/null and b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Line/images/Line2-dark.png differ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Line/images/Line2.png b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Line/images/Line2.png new file mode 100644 index 00000000000..30a44d9e57f Binary files /dev/null and b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Line/images/Line2.png differ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Line/images/thumbnail-dark.png b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Line/images/thumbnail-dark.png new file mode 100644 index 00000000000..6ed00c42846 Binary files /dev/null and b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Line/images/thumbnail-dark.png differ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Line/images/thumbnail.png b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Line/images/thumbnail.png new file mode 100644 index 00000000000..eb0cd50dbb0 Binary files /dev/null and b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Line/images/thumbnail.png differ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Line/index.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Line/index.tsx new file mode 100644 index 00000000000..f9c9d773c83 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Line/index.tsx @@ -0,0 +1,252 @@ +/** + * 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. + */ + +/** + * Timeseries Line Chart - Glyph Pattern Implementation + * + * A standard line chart for time series data. Data points are + * connected by straight line segments. + * + * Key characteristics: + * - seriesType is fixed to 'line' + * - Supports stacking (Stack, Stream modes) + * - Supports area fills with configurable opacity (toggle) + * - Markers are optional (toggle with markerEnabled) + * - Supports cross-filtering, drill-to-detail, and drill-by + * - Supports annotations (event, formula, interval, timeseries) + * - Can be used as annotation source (canBeAnnotationTypes: Timeseries) + * - Has ExtraControls for stack radio buttons + */ + +import { t } from '@apache-superset/core/translation'; +import { AnnotationType, Behavior, ChartProps } from '@superset-ui/core'; +import { + DEFAULT_SORT_SERIES_DATA, + getStandardizedControls, + SORT_SERIES_CHOICES, +} from '@superset-ui/chart-controls'; + +import { defineChart } from '@superset-ui/glyph-core'; +import { Checkbox, Slider, Select } from '@superset-ui/glyph-core'; +import { + EchartsTimeseriesChartProps, + EchartsTimeseriesSeriesType, + TimeseriesChartTransformedProps, +} from '../types'; +import { DEFAULT_FORM_DATA } from '../constants'; +import buildQuery from '../buildQuery'; +import { showValueSection } from '../../controls'; +import { + transformFullTimeseriesProps, + TimeseriesRender, + timeseriesQueryControls, + timeseriesBaseChartOptions, + areaOpacityRows, + markerConditionalRows, + legendZoomRows, + xAxisRows, + richTooltipSection, + yAxisRows, +} from '../shared'; + +import thumbnail from './images/thumbnail.png'; +import thumbnailDark from './images/thumbnail-dark.png'; +import example1 from './images/Line1.png'; +import example1Dark from './images/Line1-dark.png'; +import example2 from './images/Line2.png'; +import example2Dark from './images/Line2-dark.png'; + +// ============================================================================ +// Types +// ============================================================================ + +interface LineTransformResult { + transformedProps: TimeseriesChartTransformedProps; +} + +// ============================================================================ +// Constants +// ============================================================================ + +const { + logAxis, + markerEnabled, + markerSize, + minorSplitLine, + rowLimit, + truncateYAxis, +} = DEFAULT_FORM_DATA; + +// ============================================================================ +// Chart Definition +// ============================================================================ + +export default defineChart({ + metadata: { + name: t('Line Chart'), + description: t( + 'Line chart is used to visualize measurements taken over a given category. Line chart is a type of chart which displays information as a series of data points connected by straight line segments. It is a basic type of chart common in many fields.', + ), + category: t('Evolution'), + tags: [ + t('ECharts'), + t('Predictive'), + t('Advanced-Analytics'), + t('Line'), + t('Featured'), + ], + credits: ['https://echarts.apache.org'], + thumbnail, + thumbnailDark, + exampleGallery: [ + { url: example1, urlDark: example1Dark }, + { url: example2, urlDark: example2Dark }, + ], + behaviors: [ + Behavior.InteractiveChart, + Behavior.DrillToDetail, + Behavior.DrillBy, + ], + supportedAnnotationTypes: [ + AnnotationType.Event, + AnnotationType.Formula, + AnnotationType.Interval, + AnnotationType.Timeseries, + ], + }, + + arguments: { + // Marker controls + markerEnabled: Checkbox.with({ + label: t('Marker'), + description: t( + 'Draw a marker on data points. Only applicable for line types.', + ), + default: markerEnabled, + }), + + markerSize: { + arg: Slider.with({ + label: t('Marker Size'), + description: t( + 'Size of marker. Also applies to forecast observations.', + ), + default: markerSize, + min: 0, + max: 20, + step: 1, + }), + visibleWhen: { markerEnabled: true }, + }, + + // Show value control + showValue: Checkbox.with({ + label: t('Show Value'), + description: t('Show series values on the chart'), + default: false, + }), + + // Zoom control + zoomable: Checkbox.with({ + label: t('Data Zoom'), + description: t('Enable data zooming controls'), + default: false, + }), + + // Y-Axis controls + logAxis: Checkbox.with({ + label: t('Logarithmic Y-axis'), + description: t('Logarithmic y-axis'), + default: logAxis, + }), + + minorSplitLine: Checkbox.with({ + label: t('Minor Split Line'), + description: t('Draw split lines for minor y-axis ticks'), + default: minorSplitLine, + }), + + truncateYAxis: Checkbox.with({ + label: t('Truncate Y Axis'), + description: t( + 'Truncate Y Axis. Can be overridden by specifying a min or max bound.', + ), + default: truncateYAxis, + }), + + // Series order controls + sortSeriesType: Select.with({ + label: t('Sort Series By'), + description: t( + 'Based on what should series be ordered on the chart and legend', + ), + options: SORT_SERIES_CHOICES.map(([value, label]) => ({ + value, + label, + })), + default: DEFAULT_SORT_SERIES_DATA.sort_series_type, + }), + + sortSeriesAscending: Checkbox.with({ + label: t('Sort Series Ascending'), + description: t('Sort series in ascending order'), + default: DEFAULT_SORT_SERIES_DATA.sort_series_ascending, + }), + }, + + additionalControls: { + query: timeseriesQueryControls, + chartOptions: [ + ...timeseriesBaseChartOptions, + ...showValueSection, + ...areaOpacityRows, + ...markerConditionalRows, + ...legendZoomRows, + ...xAxisRows, + ...richTooltipSection, + ...yAxisRows, + ], + }, + + controlOverrides: { + row_limit: { + default: rowLimit, + }, + }, + + formDataOverrides: formData => ({ + ...formData, + metrics: getStandardizedControls().popAllMetrics(), + groupby: getStandardizedControls().popAllColumns(), + }), + + buildQuery, + + transform: (chartProps: ChartProps): LineTransformResult => { + const transformedProps = transformFullTimeseriesProps( + chartProps as unknown as EchartsTimeseriesChartProps, + { seriesType: EchartsTimeseriesSeriesType.Line }, + ); + return { transformedProps }; + }, + + render: ({ transformedProps }) => ( + + ), +}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx deleted file mode 100644 index da95df2d891..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx +++ /dev/null @@ -1,425 +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 { t } from '@apache-superset/core/translation'; -import { - ensureIsArray, - getColumnLabel, - JsonArray, - QueryFormColumn, -} from '@superset-ui/core'; -import { GenericDataType } from '@apache-superset/core/common'; -import { - checkColumnType, - ControlPanelConfig, - ControlPanelsContainerProps, - ControlSetRow, - ControlStateMapping, - ControlSubSectionHeader, - D3_TIME_FORMAT_DOCS, - formatSelectOptions, - getStandardizedControls, - sections, - sharedControls, -} from '@superset-ui/chart-controls'; -import { - legendSection, - minorTicks, - richTooltipSection, - seriesOrderSection, - showValueSectionWithoutStream, - truncateXAxis, - xAxisBounds, - xAxisLabelRotation, - xAxisLabelInterval, - forceMaxInterval, - colorByPrimaryAxisSection, -} from '../../../controls'; - -import { OrientationType } from '../../types'; -import { - DEFAULT_FORM_DATA, - TIME_SERIES_DESCRIPTION_TEXT, -} from '../../constants'; -import { StackControlsValue } from '../../../constants'; - -const { logAxis, minorSplitLine, truncateYAxis, yAxisBounds, orientation } = - DEFAULT_FORM_DATA; - -function createAxisTitleControl(axis: 'x' | 'y'): ControlSetRow[] { - const isXAxis = axis === 'x'; - const isVertical = (controls: ControlStateMapping) => - Boolean(controls?.orientation.value === OrientationType.Vertical); - const isHorizontal = (controls: ControlStateMapping) => - Boolean(controls?.orientation.value === OrientationType.Horizontal); - return [ - [ - { - name: 'x_axis_title', - config: { - type: 'TextControl', - label: t('Axis Title'), - renderTrigger: true, - default: '', - visibility: ({ controls }: ControlPanelsContainerProps) => - isXAxis ? isVertical(controls) : isHorizontal(controls), - disableStash: true, - resetOnHide: false, - }, - }, - ], - [ - { - name: 'x_axis_title_margin', - config: { - type: 'SelectControl', - freeForm: true, - clearable: true, - label: t('Axis title margin'), - renderTrigger: true, - default: sections.TITLE_MARGIN_OPTIONS[3], - choices: formatSelectOptions(sections.TITLE_MARGIN_OPTIONS), - visibility: ({ controls }: ControlPanelsContainerProps) => - isXAxis ? isVertical(controls) : isHorizontal(controls), - disableStash: true, - resetOnHide: false, - }, - }, - ], - [ - { - name: 'y_axis_title', - config: { - type: 'TextControl', - label: t('Axis Title'), - renderTrigger: true, - default: '', - visibility: ({ controls }: ControlPanelsContainerProps) => - isXAxis ? isHorizontal(controls) : isVertical(controls), - disableStash: true, - resetOnHide: false, - }, - }, - ], - [ - { - name: 'y_axis_title_margin', - config: { - type: 'SelectControl', - freeForm: true, - clearable: true, - label: t('Axis title margin'), - renderTrigger: true, - default: sections.TITLE_MARGIN_OPTIONS[4], - choices: formatSelectOptions(sections.TITLE_MARGIN_OPTIONS), - visibility: ({ controls }: ControlPanelsContainerProps) => - isXAxis ? isHorizontal(controls) : isVertical(controls), - disableStash: true, - resetOnHide: false, - }, - }, - ], - [ - { - name: 'y_axis_title_position', - config: { - type: 'SelectControl', - freeForm: true, - clearable: false, - label: t('Axis title position'), - renderTrigger: true, - default: sections.TITLE_POSITION_OPTIONS[0][0], - choices: sections.TITLE_POSITION_OPTIONS, - visibility: ({ controls }: ControlPanelsContainerProps) => - isXAxis ? isHorizontal(controls) : isVertical(controls), - disableStash: true, - resetOnHide: false, - }, - }, - ], - ]; -} - -function createAxisControl(axis: 'x' | 'y'): ControlSetRow[] { - const isXAxis = axis === 'x'; - const isVertical = (controls: ControlStateMapping) => - Boolean(controls?.orientation.value === OrientationType.Vertical); - const isHorizontal = (controls: ControlStateMapping) => - Boolean(controls?.orientation.value === OrientationType.Horizontal); - const isNumericXAxis = (controls: ControlStateMapping) => - checkColumnType( - getColumnLabel(controls?.x_axis?.value as QueryFormColumn), - controls?.datasource?.datasource, - [GenericDataType.Numeric], - ); - - return [ - [ - { - name: 'x_axis_time_format', - config: { - ...sharedControls.x_axis_time_format, - default: 'smart_date', - description: `${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`, - visibility: ({ controls }: ControlPanelsContainerProps) => - (isXAxis ? isVertical(controls) : isHorizontal(controls)) && - !isNumericXAxis(controls), - disableStash: true, - resetOnHide: false, - }, - }, - ], - [ - { - name: 'x_axis_number_format', - config: { - ...sharedControls.x_axis_number_format, - default: '~g', - mapStateToProps: undefined, - visibility: ({ controls }: ControlPanelsContainerProps) => - (isXAxis ? isVertical(controls) : isHorizontal(controls)) && - isNumericXAxis(controls), - disableStash: true, - resetOnHide: false, - }, - }, - ], - [ - { - name: xAxisLabelRotation.name, - config: { - ...xAxisLabelRotation.config, - visibility: ({ controls }: ControlPanelsContainerProps) => - isXAxis ? isVertical(controls) : isHorizontal(controls), - disableStash: true, - resetOnHide: false, - }, - }, - ], - [ - { - name: xAxisLabelInterval.name, - config: { - ...xAxisLabelInterval.config, - visibility: ({ controls }: ControlPanelsContainerProps) => - isXAxis ? isVertical(controls) : isHorizontal(controls), - disableStash: true, - resetOnHide: false, - }, - }, - ], - [ - { - name: 'y_axis_format', - config: { - ...sharedControls.y_axis_format, - label: t('Axis Format'), - visibility: ({ controls }: ControlPanelsContainerProps) => - isXAxis ? isHorizontal(controls) : isVertical(controls), - disableStash: true, - resetOnHide: false, - }, - }, - ], - ['currency_format'], - [ - { - name: 'logAxis', - config: { - type: 'CheckboxControl', - label: t('Logarithmic axis'), - renderTrigger: true, - default: logAxis, - description: t('Logarithmic axis'), - visibility: ({ controls }: ControlPanelsContainerProps) => - isXAxis ? isHorizontal(controls) : isVertical(controls), - disableStash: true, - resetOnHide: false, - }, - }, - ], - [ - { - name: 'minorSplitLine', - config: { - type: 'CheckboxControl', - label: t('Minor Split Line'), - renderTrigger: true, - default: minorSplitLine, - description: t('Draw split lines for minor axis ticks'), - visibility: ({ controls }: ControlPanelsContainerProps) => - isXAxis ? isHorizontal(controls) : isVertical(controls), - disableStash: true, - resetOnHide: false, - }, - }, - ], - [ - { - name: 'truncateYAxis', - config: { - type: 'CheckboxControl', - label: t('Truncate Y Axis'), - default: truncateYAxis, - renderTrigger: true, - description: t( - 'It’s not recommended to truncate Y axis in Bar chart.', - ), - visibility: ({ controls }: ControlPanelsContainerProps) => - isXAxis ? isHorizontal(controls) : isVertical(controls), - disableStash: true, - resetOnHide: false, - }, - }, - ], - [ - { - name: 'y_axis_bounds', - config: { - type: 'BoundsControl', - label: t('Axis Bounds'), - renderTrigger: true, - default: yAxisBounds, - description: t( - 'Bounds for the axis. When left empty, the bounds are ' + - 'dynamically defined based on the min/max of the data. Note that ' + - "this feature will only expand the axis range. It won't " + - "narrow the data's extent.", - ), - visibility: ({ controls }: ControlPanelsContainerProps) => - Boolean(controls?.truncateYAxis?.value) && - (isXAxis ? isHorizontal(controls) : isVertical(controls)), - disableStash: true, - resetOnHide: false, - }, - }, - ], - ]; -} - -const config: ControlPanelConfig = { - controlPanelSections: [ - sections.echartsTimeSeriesQueryWithXAxisSort, - sections.advancedAnalyticsControls, - sections.annotationsAndLayersControls, - sections.forecastIntervalControls, - { - label: t('Chart Orientation'), - expanded: true, - controlSetRows: [ - [ - { - name: 'orientation', - config: { - type: 'RadioButtonControl', - renderTrigger: true, - label: t('Bar orientation'), - default: orientation, - options: [ - [OrientationType.Vertical, t('Vertical')], - [OrientationType.Horizontal, t('Horizontal')], - ], - description: t('Orientation of bar chart'), - }, - }, - ], - ], - }, - { - label: t('Chart Title'), - tabOverride: 'customize', - expanded: true, - controlSetRows: [ - [{t('X Axis')}], - ...createAxisTitleControl('x'), - [{t('Y Axis')}], - ...createAxisTitleControl('y'), - ], - }, - { - label: t('Chart Options'), - expanded: true, - controlSetRows: [ - ...seriesOrderSection, - ['color_scheme'], - ['time_shift_color'], - ...showValueSectionWithoutStream, - ...colorByPrimaryAxisSection, - [ - { - name: 'stackDimension', - config: { - type: 'SelectControl', - label: t('Split stack by'), - visibility: ({ controls }) => - controls?.stack?.value === StackControlsValue.Stack, - renderTrigger: true, - description: t( - 'Stack in groups, where each group corresponds to a dimension', - ), - shouldMapStateToProps: ( - prevState, - state, - controlState, - chartState, - ) => true, - mapStateToProps: (state, controlState, chartState) => { - const value: JsonArray = ensureIsArray( - state.controls.groupby?.value, - ) as JsonArray; - const valueAsStringArr: string[][] = value.map(v => { - if (v) return [v.toString(), v.toString()]; - return ['', '']; - }); - return { - choices: valueAsStringArr, - }; - }, - }, - }, - ], - [minorTicks], - ['zoomable'], - ...legendSection, - [{t('X Axis')}], - ...createAxisControl('x'), - [truncateXAxis], - [xAxisBounds], - [forceMaxInterval], - ...richTooltipSection, - [{t('Y Axis')}], - ...createAxisControl('y'), - ['echart_options'], - ], - }, - ], - formDataOverrides: formData => { - // Reset stack to null if it's Stream when switching to Bar chart - const formDataWithStack = formData as Record; - return { - ...formData, - metrics: getStandardizedControls().popAllMetrics(), - groupby: getStandardizedControls().popAllColumns(), - ...(formDataWithStack.stack === StackControlsValue.Stream && { - stack: null, - }), - }; - }, -}; - -export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/index.ts deleted file mode 100644 index 604cd60a876..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/index.ts +++ /dev/null @@ -1,96 +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 { t } from '@apache-superset/core/translation'; -import { AnnotationType, Behavior } from '@superset-ui/core'; -import { - EchartsTimeseriesChartProps, - EchartsTimeseriesFormData, - EchartsTimeseriesSeriesType, -} from '../../types'; -import { EchartsChartPlugin } from '../../../types'; -import buildQuery from '../../buildQuery'; -import controlPanel from './controlPanel'; -import transformProps from '../../transformProps'; -import thumbnail from './images/thumbnail.png'; -import thumbnailDark from './images/thumbnail-dark.png'; -import example1 from './images/Bar1.png'; -import example1Dark from './images/Bar1-dark.png'; -import example2 from './images/Bar2.png'; -import example2Dark from './images/Bar2-dark.png'; -import example3 from './images/Bar3.png'; -import example3Dark from './images/Bar3-dark.png'; - -const barTransformProps = (chartProps: EchartsTimeseriesChartProps) => - transformProps({ - ...chartProps, - formData: { - ...chartProps.formData, - seriesType: EchartsTimeseriesSeriesType.Bar, - }, - }); - -export default class EchartsTimeseriesBarChartPlugin extends EchartsChartPlugin< - EchartsTimeseriesFormData, - EchartsTimeseriesChartProps -> { - constructor() { - super({ - buildQuery, - controlPanel, - loadChart: () => import('../../EchartsTimeseries'), - metadata: { - behaviors: [ - Behavior.InteractiveChart, - Behavior.DrillToDetail, - Behavior.DrillBy, - ], - category: t('Evolution'), - credits: ['https://echarts.apache.org'], - description: t( - 'Bar Charts are used to show metrics as a series of bars.', - ), - exampleGallery: [ - { url: example1, urlDark: example1Dark }, - { url: example2, urlDark: example2Dark }, - { url: example3, urlDark: example3Dark }, - ], - supportedAnnotationTypes: [ - AnnotationType.Event, - AnnotationType.Formula, - AnnotationType.Interval, - AnnotationType.Timeseries, - ], - name: t('Bar Chart'), - tags: [ - t('ECharts'), - t('Predictive'), - t('Advanced-Analytics'), - t('Time'), - t('Transformable'), - t('Stacked'), - t('Bar'), - t('Featured'), - ], - thumbnail, - thumbnailDark, - }, - transformProps: barTransformProps, - }); - } -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/index.ts deleted file mode 100644 index e9eda463d7c..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/index.ts +++ /dev/null @@ -1,91 +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 { t } from '@apache-superset/core/translation'; -import { AnnotationType, Behavior } from '@superset-ui/core'; -import { - EchartsTimeseriesChartProps, - EchartsTimeseriesFormData, - EchartsTimeseriesSeriesType, -} from '../../types'; -import buildQuery from '../../buildQuery'; -import controlPanel from './controlPanel'; -import transformProps from '../../transformProps'; -import thumbnail from './images/thumbnail.png'; -import thumbnailDark from './images/thumbnail-dark.png'; -import example1 from './images/Line1.png'; -import example1Dark from './images/Line1-dark.png'; -import example2 from './images/Line2.png'; -import example2Dark from './images/Line2-dark.png'; -import { EchartsChartPlugin } from '../../../types'; - -const lineTransformProps = (chartProps: EchartsTimeseriesChartProps) => - transformProps({ - ...chartProps, - formData: { - ...chartProps.formData, - seriesType: EchartsTimeseriesSeriesType.Line, - }, - }); - -export default class EchartsTimeseriesLineChartPlugin extends EchartsChartPlugin< - EchartsTimeseriesFormData, - EchartsTimeseriesChartProps -> { - constructor() { - super({ - buildQuery, - controlPanel, - loadChart: () => import('../../EchartsTimeseries'), - metadata: { - behaviors: [ - Behavior.InteractiveChart, - Behavior.DrillToDetail, - Behavior.DrillBy, - ], - category: t('Evolution'), - credits: ['https://echarts.apache.org'], - description: t( - 'Line chart is used to visualize measurements taken over a given category. Line chart is a type of chart which displays information as a series of data points connected by straight line segments. It is a basic type of chart common in many fields.', - ), - exampleGallery: [ - { url: example1, urlDark: example1Dark }, - { url: example2, urlDark: example2Dark }, - ], - canBeAnnotationTypes: [AnnotationType.Timeseries], - supportedAnnotationTypes: [ - AnnotationType.Event, - AnnotationType.Formula, - AnnotationType.Interval, - AnnotationType.Timeseries, - ], - name: t('Line Chart'), - tags: [ - t('ECharts'), - t('Predictive'), - t('Advanced-Analytics'), - t('Line'), - t('Featured'), - ], - thumbnail, - thumbnailDark, - }, - transformProps: lineTransformProps, - }); - } -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx deleted file mode 100644 index a955da37d52..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx +++ /dev/null @@ -1,226 +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 { t } from '@apache-superset/core/translation'; -import { getColumnLabel, QueryFormColumn } from '@superset-ui/core'; -import { GenericDataType } from '@apache-superset/core/common'; -import { - checkColumnType, - ControlPanelConfig, - ControlPanelsContainerProps, - ControlSubSectionHeader, - D3_TIME_FORMAT_DOCS, - getStandardizedControls, - sections, - sharedControls, -} from '@superset-ui/chart-controls'; - -import { - DEFAULT_FORM_DATA, - TIME_SERIES_DESCRIPTION_TEXT, -} from '../../constants'; -import { - legendSection, - minorTicks, - richTooltipSection, - seriesOrderSection, - showValueSection, - truncateXAxis, - xAxisBounds, - xAxisLabelRotation, - xAxisLabelInterval, - forceMaxInterval, -} from '../../../controls'; - -const { - logAxis, - markerEnabled, - markerSize, - minorSplitLine, - rowLimit, - truncateYAxis, - yAxisBounds, -} = DEFAULT_FORM_DATA; -const config: ControlPanelConfig = { - controlPanelSections: [ - sections.echartsTimeSeriesQueryWithXAxisSort, - sections.advancedAnalyticsControls, - sections.annotationsAndLayersControls, - sections.forecastIntervalControls, - sections.titleControls, - { - label: t('Chart Options'), - expanded: true, - controlSetRows: [ - ...seriesOrderSection, - ['color_scheme'], - ['time_shift_color'], - ...showValueSection, - [ - { - name: 'markerEnabled', - config: { - type: 'CheckboxControl', - label: t('Marker'), - renderTrigger: true, - default: markerEnabled, - description: t( - 'Draw a marker on data points. Only applicable for line types.', - ), - }, - }, - ], - [ - { - name: 'markerSize', - config: { - type: 'SliderControl', - label: t('Marker Size'), - renderTrigger: true, - min: 0, - max: 100, - default: markerSize, - description: t( - 'Size of marker. Also applies to forecast observations.', - ), - visibility: ({ controls }: ControlPanelsContainerProps) => - Boolean(controls?.markerEnabled?.value), - }, - }, - ], - ['zoomable'], - [minorTicks], - ...legendSection, - [{t('X Axis')}], - - [ - { - name: 'x_axis_time_format', - config: { - ...sharedControls.x_axis_time_format, - default: 'smart_date', - description: `${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`, - visibility: ({ controls }: ControlPanelsContainerProps) => - checkColumnType( - getColumnLabel(controls?.x_axis?.value as QueryFormColumn), - controls?.datasource?.datasource, - [GenericDataType.Temporal], - ), - disableStash: true, - resetOnHide: false, - }, - }, - { - name: 'x_axis_number_format', - config: { - ...sharedControls.x_axis_number_format, - default: '~g', - mapStateToProps: undefined, - visibility: ({ controls }: ControlPanelsContainerProps) => - checkColumnType( - getColumnLabel(controls?.x_axis?.value as QueryFormColumn), - controls?.datasource?.datasource, - [GenericDataType.Numeric], - ), - }, - }, - ], - [xAxisLabelRotation], - [xAxisLabelInterval], - [forceMaxInterval], - // eslint-disable-next-line react/jsx-key - ...richTooltipSection, - // eslint-disable-next-line react/jsx-key - [{t('Y Axis')}], - ['y_axis_format'], - ['currency_format'], - [ - { - name: 'logAxis', - config: { - type: 'CheckboxControl', - label: t('Logarithmic y-axis'), - renderTrigger: true, - default: logAxis, - description: t('Logarithmic y-axis'), - }, - }, - ], - [ - { - name: 'minorSplitLine', - config: { - type: 'CheckboxControl', - label: t('Minor Split Line'), - renderTrigger: true, - default: minorSplitLine, - description: t('Draw split lines for minor y-axis ticks'), - }, - }, - ], - [truncateXAxis], - [xAxisBounds], - [ - { - name: 'truncateYAxis', - config: { - type: 'CheckboxControl', - label: t('Truncate Y Axis'), - default: truncateYAxis, - renderTrigger: true, - description: t( - 'Truncate Y Axis. Can be overridden by specifying a min or max bound.', - ), - }, - }, - ], - [ - { - name: 'y_axis_bounds', - config: { - type: 'BoundsControl', - label: t('Y Axis Bounds'), - renderTrigger: true, - default: yAxisBounds, - description: t( - 'Bounds for the Y-axis. When left empty, the bounds are ' + - 'dynamically defined based on the min/max of the data. Note that ' + - "this feature will only expand the axis range. It won't " + - "narrow the data's extent.", - ), - visibility: ({ controls }: ControlPanelsContainerProps) => - Boolean(controls?.truncateYAxis?.value), - }, - }, - ], - ], - }, - ], - controlOverrides: { - row_limit: { - default: rowLimit, - }, - }, - formDataOverrides: formData => ({ - ...formData, - metrics: getStandardizedControls().popAllMetrics(), - groupby: getStandardizedControls().popAllColumns(), - }), -}; - -export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/index.ts deleted file mode 100644 index d001ad3fe0a..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/index.ts +++ /dev/null @@ -1,87 +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 { t } from '@apache-superset/core/translation'; -import { AnnotationType, Behavior } from '@superset-ui/core'; -import { - EchartsTimeseriesChartProps, - EchartsTimeseriesFormData, - EchartsTimeseriesSeriesType, -} from '../../types'; -import buildQuery from '../../buildQuery'; -import controlPanel from './controlPanel'; -import transformProps from '../../transformProps'; -import thumbnail from './images/thumbnail.png'; -import thumbnailDark from './images/thumbnail-dark.png'; -import example1 from './images/Scatter1.png'; -import example1Dark from './images/Scatter1-dark.png'; -import { EchartsChartPlugin } from '../../../types'; - -const scatterTransformProps = (chartProps: EchartsTimeseriesChartProps) => - transformProps({ - ...chartProps, - formData: { - ...chartProps.formData, - seriesType: EchartsTimeseriesSeriesType.Scatter, - }, - }); - -export default class EchartsTimeseriesScatterChartPlugin extends EchartsChartPlugin< - EchartsTimeseriesFormData, - EchartsTimeseriesChartProps -> { - constructor() { - super({ - buildQuery, - controlPanel, - loadChart: () => import('../../EchartsTimeseries'), - metadata: { - behaviors: [ - Behavior.InteractiveChart, - Behavior.DrillToDetail, - Behavior.DrillBy, - ], - category: t('Evolution'), - credits: ['https://echarts.apache.org'], - description: t( - 'Scatter Plot has the horizontal axis in linear units, and the points are connected in order. It shows a statistical relationship between two variables.', - ), - exampleGallery: [{ url: example1, urlDark: example1Dark }], - supportedAnnotationTypes: [ - AnnotationType.Event, - AnnotationType.Formula, - AnnotationType.Interval, - AnnotationType.Timeseries, - ], - name: t('Scatter Plot'), - tags: [ - t('ECharts'), - t('Predictive'), - t('Advanced-Analytics'), - t('Time'), - t('Transformable'), - t('Scatter'), - t('Featured'), - ], - thumbnail, - thumbnailDark, - }, - transformProps: scatterTransformProps, - }); - } -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/controlPanel.tsx deleted file mode 100644 index 45128037fca..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/controlPanel.tsx +++ /dev/null @@ -1,229 +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 { t } from '@apache-superset/core/translation'; -import { getColumnLabel, QueryFormColumn } from '@superset-ui/core'; -import { GenericDataType } from '@apache-superset/core/common'; -import { - checkColumnType, - ControlPanelConfig, - ControlPanelsContainerProps, - ControlSubSectionHeader, - D3_TIME_FORMAT_DOCS, - getStandardizedControls, - sections, - sharedControls, -} from '@superset-ui/chart-controls'; - -import { - DEFAULT_FORM_DATA, - TIME_SERIES_DESCRIPTION_TEXT, -} from '../../constants'; -import { - legendSection, - minorTicks, - richTooltipSection, - seriesOrderSection, - showValueSectionWithoutStack, - truncateXAxis, - xAxisBounds, - xAxisLabelRotation, - xAxisLabelInterval, - forceMaxInterval, -} from '../../../controls'; - -const { - logAxis, - markerEnabled, - markerSize, - minorSplitLine, - rowLimit, - truncateYAxis, - yAxisBounds, -} = DEFAULT_FORM_DATA; -const config: ControlPanelConfig = { - controlPanelSections: [ - sections.echartsTimeSeriesQueryWithXAxisSort, - sections.advancedAnalyticsControls, - sections.annotationsAndLayersControls, - sections.forecastIntervalControls, - sections.titleControls, - { - label: t('Chart Options'), - expanded: true, - controlSetRows: [ - ...seriesOrderSection, - ['color_scheme'], - ['time_shift_color'], - ...showValueSectionWithoutStack, - [ - { - name: 'markerEnabled', - config: { - type: 'CheckboxControl', - label: t('Marker'), - renderTrigger: true, - default: markerEnabled, - description: t( - 'Draw a marker on data points. Only applicable for line types.', - ), - }, - }, - ], - [ - { - name: 'markerSize', - config: { - type: 'SliderControl', - label: t('Marker Size'), - renderTrigger: true, - min: 0, - max: 20, - default: markerSize, - description: t( - 'Size of marker. Also applies to forecast observations.', - ), - visibility: ({ controls }: ControlPanelsContainerProps) => - Boolean(controls?.markerEnabled?.value), - }, - }, - ], - ['zoomable'], - [minorTicks], - ...legendSection, - [{t('X Axis')}], - [ - { - name: 'x_axis_time_format', - config: { - ...sharedControls.x_axis_time_format, - default: 'smart_date', - description: `${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`, - visibility: ({ controls }: ControlPanelsContainerProps) => - checkColumnType( - getColumnLabel(controls?.x_axis?.value as QueryFormColumn), - controls?.datasource?.datasource, - [GenericDataType.Temporal], - ), - disableStash: true, - resetOnHide: false, - }, - }, - ], - [ - { - name: 'x_axis_number_format', - config: { - ...sharedControls.x_axis_number_format, - default: '~g', - mapStateToProps: undefined, - visibility: ({ controls }: ControlPanelsContainerProps) => - checkColumnType( - getColumnLabel(controls?.x_axis?.value as QueryFormColumn), - controls?.datasource?.datasource, - [GenericDataType.Numeric], - ), - }, - }, - ], - [xAxisLabelRotation], - [xAxisLabelInterval], - [forceMaxInterval], - // eslint-disable-next-line react/jsx-key - ...richTooltipSection, - // eslint-disable-next-line react/jsx-key - [{t('Y Axis')}], - - ['y_axis_format'], - ['currency_format'], - [ - { - name: 'logAxis', - config: { - type: 'CheckboxControl', - label: t('Logarithmic y-axis'), - renderTrigger: true, - default: logAxis, - description: t('Logarithmic y-axis'), - }, - }, - ], - [ - { - name: 'minorSplitLine', - config: { - type: 'CheckboxControl', - label: t('Minor Split Line'), - renderTrigger: true, - default: minorSplitLine, - description: t('Draw split lines for minor y-axis ticks'), - }, - }, - ], - [truncateXAxis], - [xAxisBounds], - [ - { - name: 'truncateYAxis', - config: { - type: 'CheckboxControl', - label: t('Truncate Y Axis'), - default: truncateYAxis, - renderTrigger: true, - description: t( - 'Truncate Y Axis. Can be overridden by specifying a min or max bound.', - ), - }, - }, - ], - [ - { - name: 'y_axis_bounds', - config: { - type: 'BoundsControl', - label: t('Y Axis Bounds'), - renderTrigger: true, - default: yAxisBounds, - description: t( - 'Bounds for the Y-axis. When left empty, the bounds are ' + - 'dynamically defined based on the min/max of the data. Note that ' + - "this feature will only expand the axis range. It won't " + - "narrow the data's extent.", - ), - visibility: ({ controls }: ControlPanelsContainerProps) => - Boolean(controls?.truncateYAxis?.value), - }, - }, - ], - ['echart_options'], - ], - }, - ], - controlOverrides: { - row_limit: { - default: rowLimit, - }, - }, - formDataOverrides: formData => ({ - ...formData, - metrics: getStandardizedControls().popAllMetrics(), - groupby: getStandardizedControls().popAllColumns(), - }), -}; - -export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/index.ts deleted file mode 100644 index 44665bf5db4..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/index.ts +++ /dev/null @@ -1,86 +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 { t } from '@apache-superset/core/translation'; -import { AnnotationType, Behavior } from '@superset-ui/core'; -import { - EchartsTimeseriesChartProps, - EchartsTimeseriesFormData, - EchartsTimeseriesSeriesType, -} from '../../types'; -import buildQuery from '../../buildQuery'; -import controlPanel from './controlPanel'; -import transformProps from '../../transformProps'; -import thumbnail from './images/thumbnail.png'; -import thumbnailDark from './images/thumbnail-dark.png'; -import example1 from './images/SmoothLine1.png'; -import example1Dark from './images/SmoothLine1-dark.png'; -import { EchartsChartPlugin } from '../../../types'; - -const smoothTransformProps = (chartProps: EchartsTimeseriesChartProps) => - transformProps({ - ...chartProps, - formData: { - ...chartProps.formData, - seriesType: EchartsTimeseriesSeriesType.Smooth, - }, - }); - -export default class EchartsTimeseriesSmoothLineChartPlugin extends EchartsChartPlugin< - EchartsTimeseriesFormData, - EchartsTimeseriesChartProps -> { - constructor() { - super({ - buildQuery, - controlPanel, - loadChart: () => import('../../EchartsTimeseries'), - metadata: { - behaviors: [ - Behavior.InteractiveChart, - Behavior.DrillToDetail, - Behavior.DrillBy, - ], - category: t('Evolution'), - credits: ['https://echarts.apache.org'], - description: t( - 'Smooth-line is a variation of the line chart. Without angles and hard edges, Smooth-line sometimes looks smarter and more professional.', - ), - exampleGallery: [{ url: example1, urlDark: example1Dark }], - supportedAnnotationTypes: [ - AnnotationType.Event, - AnnotationType.Formula, - AnnotationType.Interval, - AnnotationType.Timeseries, - ], - name: t('Smooth Line'), - tags: [ - t('ECharts'), - t('Predictive'), - t('Advanced-Analytics'), - t('Time'), - t('Line'), - t('Transformable'), - ], - thumbnail, - thumbnailDark, - }, - transformProps: smoothTransformProps, - }); - } -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Scatter/images/Scatter1-dark.png b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Scatter/images/Scatter1-dark.png new file mode 100644 index 00000000000..38824252bc5 Binary files /dev/null and b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Scatter/images/Scatter1-dark.png differ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Scatter/images/Scatter1.png b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Scatter/images/Scatter1.png new file mode 100644 index 00000000000..145b69a23d1 Binary files /dev/null and b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Scatter/images/Scatter1.png differ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Scatter/images/thumbnail-dark.png b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Scatter/images/thumbnail-dark.png new file mode 100644 index 00000000000..5c23de422a1 Binary files /dev/null and b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Scatter/images/thumbnail-dark.png differ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Scatter/images/thumbnail.png b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Scatter/images/thumbnail.png new file mode 100644 index 00000000000..1692f3bd9df Binary files /dev/null and b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Scatter/images/thumbnail.png differ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Scatter/index.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Scatter/index.tsx new file mode 100644 index 00000000000..7047a66c243 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Scatter/index.tsx @@ -0,0 +1,217 @@ +/** + * 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. + */ + +/** + * Timeseries Scatter Chart - Glyph Pattern Implementation + * + * A scatter plot for time series data. Points are placed based on + * x-axis (typically time) and y-axis (metric value) coordinates. + * + * Key characteristics: + * - seriesType is fixed to 'scatter' (not configurable) + * - No stack support (scatter plots don't stack) + * - No area support (scatter plots are discrete points) + * - Markers are always shown (they ARE the data points) + * - Supports cross-filtering, drill-to-detail, and drill-by + */ + +import { t } from '@apache-superset/core/translation'; +import { AnnotationType, Behavior, ChartProps } from '@superset-ui/core'; +import { + DEFAULT_SORT_SERIES_DATA, + SORT_SERIES_CHOICES, +} from '@superset-ui/chart-controls'; + +import { defineChart } from '@superset-ui/glyph-core'; +import { Checkbox, Slider, Select } from '@superset-ui/glyph-core'; +import { + EchartsTimeseriesChartProps, + EchartsTimeseriesSeriesType, + TimeseriesChartTransformedProps, +} from '../types'; +import { DEFAULT_FORM_DATA } from '../constants'; +import buildQuery from '../buildQuery'; +import { + transformSimpleTimeseriesProps, + TimeseriesRender, + timeseriesQueryControls, + timeseriesBaseChartOptions, + markerDirectRow, + legendZoomRows, + xAxisRows, + richTooltipSection, + yAxisRows, +} from '../shared'; + +import thumbnail from './images/thumbnail.png'; +import thumbnailDark from './images/thumbnail-dark.png'; +import example1 from './images/Scatter1.png'; +import example1Dark from './images/Scatter1-dark.png'; + +// ============================================================================ +// Types +// ============================================================================ + +interface ScatterTransformResult { + transformedProps: TimeseriesChartTransformedProps; +} + +// ============================================================================ +// Constants +// ============================================================================ + +const { logAxis, markerSize, minorSplitLine, rowLimit, truncateYAxis } = + DEFAULT_FORM_DATA; + +// ============================================================================ +// Chart Definition +// ============================================================================ + +export default defineChart({ + metadata: { + name: t('Scatter Plot'), + description: t( + 'Scatter Plot has the horizontal axis in linear units, and the points are connected in order. It shows a statistical relationship between two variables.', + ), + category: t('Evolution'), + tags: [ + t('ECharts'), + t('Predictive'), + t('Advanced-Analytics'), + t('Time'), + t('Transformable'), + t('Scatter'), + t('Featured'), + ], + credits: ['https://echarts.apache.org'], + thumbnail, + thumbnailDark, + exampleGallery: [{ url: example1, urlDark: example1Dark }], + behaviors: [ + Behavior.InteractiveChart, + Behavior.DrillToDetail, + Behavior.DrillBy, + ], + supportedAnnotationTypes: [ + AnnotationType.Event, + AnnotationType.Formula, + AnnotationType.Interval, + AnnotationType.Timeseries, + ], + }, + + arguments: { + // Marker size control (scatter always shows markers) + markerSize: Slider.with({ + label: t('Marker Size'), + description: t('Size of marker. Also applies to forecast observations.'), + default: markerSize, + min: 0, + max: 100, + step: 1, + }), + + // Show value control + showValue: Checkbox.with({ + label: t('Show Value'), + description: t('Show series values on the chart'), + default: false, + }), + + // Zoom control + zoomable: Checkbox.with({ + label: t('Data Zoom'), + description: t('Enable data zooming controls'), + default: false, + }), + + // Y-Axis controls + logAxis: Checkbox.with({ + label: t('Logarithmic Y-axis'), + description: t('Logarithmic y-axis'), + default: logAxis, + }), + + minorSplitLine: Checkbox.with({ + label: t('Minor Split Line'), + description: t('Draw split lines for minor y-axis ticks'), + default: minorSplitLine, + }), + + truncateYAxis: Checkbox.with({ + label: t('Truncate Y Axis'), + description: t( + 'Truncate Y Axis. Can be overridden by specifying a min or max bound.', + ), + default: truncateYAxis, + }), + + // Series order controls + sortSeriesType: Select.with({ + label: t('Sort Series By'), + description: t( + 'Based on what should series be ordered on the chart and legend', + ), + options: SORT_SERIES_CHOICES.map(([value, label]) => ({ + value, + label, + })), + default: DEFAULT_SORT_SERIES_DATA.sort_series_type, + }), + + sortSeriesAscending: Checkbox.with({ + label: t('Sort Series Ascending'), + description: t('Sort series in ascending order'), + default: DEFAULT_SORT_SERIES_DATA.sort_series_ascending, + }), + }, + + additionalControls: { + query: timeseriesQueryControls, + chartOptions: [ + ...timeseriesBaseChartOptions, + ...markerDirectRow, + ...legendZoomRows, + ...xAxisRows, + ...richTooltipSection, + ...yAxisRows, + ], + }, + + controlOverrides: { + row_limit: { + default: rowLimit, + }, + }, + + buildQuery, + + transform: (chartProps: ChartProps): ScatterTransformResult => { + const transformedProps = transformSimpleTimeseriesProps( + chartProps as unknown as EchartsTimeseriesChartProps, + EchartsTimeseriesSeriesType.Scatter, + { alwaysShowMarkers: true }, + ); + return { transformedProps }; + }, + + render: ({ transformedProps }) => ( + + ), +}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/SmoothLine/images/SmoothLine1-dark.png b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/SmoothLine/images/SmoothLine1-dark.png new file mode 100644 index 00000000000..456ab5849c1 Binary files /dev/null and b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/SmoothLine/images/SmoothLine1-dark.png differ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/SmoothLine/images/SmoothLine1.png b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/SmoothLine/images/SmoothLine1.png new file mode 100644 index 00000000000..d0f1cb9e349 Binary files /dev/null and b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/SmoothLine/images/SmoothLine1.png differ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/SmoothLine/images/thumbnail-dark.png b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/SmoothLine/images/thumbnail-dark.png new file mode 100644 index 00000000000..4502993d8fd Binary files /dev/null and b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/SmoothLine/images/thumbnail-dark.png differ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/SmoothLine/images/thumbnail.png b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/SmoothLine/images/thumbnail.png new file mode 100644 index 00000000000..9105dd3ece3 Binary files /dev/null and b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/SmoothLine/images/thumbnail.png differ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/SmoothLine/index.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/SmoothLine/index.tsx new file mode 100644 index 00000000000..0d057ba8692 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/SmoothLine/index.tsx @@ -0,0 +1,244 @@ +/** + * 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. + */ + +/** + * Timeseries Smooth Line Chart - Glyph Pattern Implementation + * + * A variation of the line chart using smooth curves instead of + * straight line segments between data points. + * + * Key characteristics: + * - seriesType is fixed to 'smooth' (not configurable) + * - No stack support (uses showValueSectionWithoutStack) + * - No area support + * - Markers are optional (toggle with markerEnabled) + * - Supports cross-filtering, drill-to-detail, and drill-by + * - Supports annotations (event, formula, interval, timeseries) + */ + +import { t } from '@apache-superset/core/translation'; +import { AnnotationType, Behavior, ChartProps } from '@superset-ui/core'; +import { + DEFAULT_SORT_SERIES_DATA, + getStandardizedControls, + SORT_SERIES_CHOICES, +} from '@superset-ui/chart-controls'; + +import { defineChart } from '@superset-ui/glyph-core'; +import { Checkbox, Slider, Select } from '@superset-ui/glyph-core'; +import { + EchartsTimeseriesChartProps, + EchartsTimeseriesSeriesType, + TimeseriesChartTransformedProps, +} from '../types'; +import { DEFAULT_FORM_DATA } from '../constants'; +import buildQuery from '../buildQuery'; +import { showValueSectionWithoutStack } from '../../controls'; +import { + transformSimpleTimeseriesProps, + TimeseriesRender, + timeseriesQueryControls, + timeseriesBaseChartOptions, + markerConditionalRows, + legendZoomRows, + xAxisRows, + richTooltipSection, + yAxisRows, +} from '../shared'; + +import thumbnail from './images/thumbnail.png'; +import thumbnailDark from './images/thumbnail-dark.png'; +import example1 from './images/SmoothLine1.png'; +import example1Dark from './images/SmoothLine1-dark.png'; + +// ============================================================================ +// Types +// ============================================================================ + +interface SmoothLineTransformResult { + transformedProps: TimeseriesChartTransformedProps; +} + +// ============================================================================ +// Constants +// ============================================================================ + +const { + logAxis, + markerEnabled, + markerSize, + minorSplitLine, + rowLimit, + truncateYAxis, +} = DEFAULT_FORM_DATA; + +// ============================================================================ +// Chart Definition +// ============================================================================ + +export default defineChart({ + metadata: { + name: t('Smooth Line'), + description: t( + 'Smooth-line is a variation of the line chart. Without angles and hard edges, Smooth-line sometimes looks smarter and more professional.', + ), + category: t('Evolution'), + tags: [ + t('ECharts'), + t('Predictive'), + t('Advanced-Analytics'), + t('Time'), + t('Line'), + t('Transformable'), + ], + credits: ['https://echarts.apache.org'], + thumbnail, + thumbnailDark, + exampleGallery: [{ url: example1, urlDark: example1Dark }], + behaviors: [ + Behavior.InteractiveChart, + Behavior.DrillToDetail, + Behavior.DrillBy, + ], + supportedAnnotationTypes: [ + AnnotationType.Event, + AnnotationType.Formula, + AnnotationType.Interval, + AnnotationType.Timeseries, + ], + }, + + arguments: { + // Marker controls + markerEnabled: Checkbox.with({ + label: t('Marker'), + description: t( + 'Draw a marker on data points. Only applicable for line types.', + ), + default: markerEnabled, + }), + + markerSize: { + arg: Slider.with({ + label: t('Marker Size'), + description: t( + 'Size of marker. Also applies to forecast observations.', + ), + default: markerSize, + min: 0, + max: 20, + step: 1, + }), + visibleWhen: { markerEnabled: true }, + }, + + // Show value control (no stack for smooth line) + showValue: Checkbox.with({ + label: t('Show Value'), + description: t('Show series values on the chart'), + default: false, + }), + + // Zoom control + zoomable: Checkbox.with({ + label: t('Data Zoom'), + description: t('Enable data zooming controls'), + default: false, + }), + + // Y-Axis controls + logAxis: Checkbox.with({ + label: t('Logarithmic Y-axis'), + description: t('Logarithmic y-axis'), + default: logAxis, + }), + + minorSplitLine: Checkbox.with({ + label: t('Minor Split Line'), + description: t('Draw split lines for minor y-axis ticks'), + default: minorSplitLine, + }), + + truncateYAxis: Checkbox.with({ + label: t('Truncate Y Axis'), + description: t( + 'Truncate Y Axis. Can be overridden by specifying a min or max bound.', + ), + default: truncateYAxis, + }), + + // Series order controls + sortSeriesType: Select.with({ + label: t('Sort Series By'), + description: t( + 'Based on what should series be ordered on the chart and legend', + ), + options: SORT_SERIES_CHOICES.map(([value, label]) => ({ + value, + label, + })), + default: DEFAULT_SORT_SERIES_DATA.sort_series_type, + }), + + sortSeriesAscending: Checkbox.with({ + label: t('Sort Series Ascending'), + description: t('Sort series in ascending order'), + default: DEFAULT_SORT_SERIES_DATA.sort_series_ascending, + }), + }, + + additionalControls: { + query: timeseriesQueryControls, + chartOptions: [ + ...timeseriesBaseChartOptions, + ...showValueSectionWithoutStack, + ...markerConditionalRows, + ...legendZoomRows, + ...xAxisRows, + ...richTooltipSection, + ...yAxisRows, + ], + }, + + controlOverrides: { + row_limit: { + default: rowLimit, + }, + }, + + formDataOverrides: formData => ({ + ...formData, + metrics: getStandardizedControls().popAllMetrics(), + groupby: getStandardizedControls().popAllColumns(), + }), + + buildQuery, + + transform: (chartProps: ChartProps): SmoothLineTransformResult => { + const transformedProps = transformSimpleTimeseriesProps( + chartProps as unknown as EchartsTimeseriesChartProps, + EchartsTimeseriesSeriesType.Smooth, + ); + return { transformedProps }; + }, + + render: ({ transformedProps }) => ( + + ), +}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx deleted file mode 100644 index 87bcb0adc21..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx +++ /dev/null @@ -1,278 +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 { t } from '@apache-superset/core/translation'; -import { getColumnLabel, QueryFormColumn } from '@superset-ui/core'; -import { GenericDataType } from '@apache-superset/core/common'; -import { - checkColumnType, - ControlPanelConfig, - ControlPanelsContainerProps, - ControlSubSectionHeader, - D3_TIME_FORMAT_DOCS, - getStandardizedControls, - sections, - sharedControls, -} from '@superset-ui/chart-controls'; - -import { EchartsTimeseriesSeriesType } from '../../types'; -import { DEFAULT_FORM_DATA, TIME_SERIES_DESCRIPTION_TEXT } from '../constants'; -import { - legendSection, - minorTicks, - richTooltipSection, - seriesOrderSection, - showValueSection, - truncateXAxis, - xAxisBounds, - xAxisLabelRotation, - xAxisLabelInterval, - forceMaxInterval, -} from '../../controls'; - -const { - area, - logAxis, - markerEnabled, - markerSize, - minorSplitLine, - opacity, - rowLimit, - truncateYAxis, - yAxisBounds, -} = DEFAULT_FORM_DATA; -const config: ControlPanelConfig = { - controlPanelSections: [ - sections.echartsTimeSeriesQueryWithXAxisSort, - sections.advancedAnalyticsControls, - sections.annotationsAndLayersControls, - sections.forecastIntervalControls, - sections.titleControls, - { - label: t('Chart Options'), - expanded: true, - controlSetRows: [ - ...seriesOrderSection, - ['color_scheme'], - ['time_shift_color'], - [ - { - name: 'seriesType', - config: { - type: 'SelectControl', - label: t('Step type'), - renderTrigger: true, - default: EchartsTimeseriesSeriesType.Start, - choices: [ - [EchartsTimeseriesSeriesType.Start, t('Start')], - [EchartsTimeseriesSeriesType.Middle, t('Middle')], - [EchartsTimeseriesSeriesType.End, t('End')], - ], - description: t( - 'Defines whether the step should appear at the beginning, middle or end between two data points', - ), - }, - }, - ], - ...showValueSection, - [ - { - name: 'area', - config: { - type: 'CheckboxControl', - label: t('Area Chart'), - renderTrigger: true, - default: area, - description: t( - 'Draw area under curves. Only applicable for line types.', - ), - }, - }, - ], - [ - { - name: 'opacity', - config: { - type: 'SliderControl', - label: t('Area chart opacity'), - renderTrigger: true, - min: 0, - max: 1, - step: 0.1, - default: opacity, - description: t( - 'Opacity of Area Chart. Also applies to confidence band.', - ), - visibility: ({ controls }: ControlPanelsContainerProps) => - Boolean(controls?.area?.value), - }, - }, - ], - [ - { - name: 'markerEnabled', - config: { - type: 'CheckboxControl', - label: t('Marker'), - renderTrigger: true, - default: markerEnabled, - description: t( - 'Draw a marker on data points. Only applicable for line types.', - ), - }, - }, - ], - [ - { - name: 'markerSize', - config: { - type: 'SliderControl', - label: t('Marker Size'), - renderTrigger: true, - min: 0, - max: 20, - default: markerSize, - description: t( - 'Size of marker. Also applies to forecast observations.', - ), - visibility: ({ controls }: ControlPanelsContainerProps) => - Boolean(controls?.markerEnabled?.value), - }, - }, - ], - ['zoomable'], - [minorTicks], - ...legendSection, - [{t('X Axis')}], - [ - { - name: 'x_axis_time_format', - config: { - ...sharedControls.x_axis_time_format, - default: 'smart_date', - description: `${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`, - visibility: ({ controls }: ControlPanelsContainerProps) => - checkColumnType( - getColumnLabel(controls?.x_axis?.value as QueryFormColumn), - controls?.datasource?.datasource, - [GenericDataType.Temporal], - ), - disableStash: true, - resetOnHide: false, - }, - }, - ], - [ - { - name: 'x_axis_number_format', - config: { - ...sharedControls.x_axis_number_format, - default: '~g', - mapStateToProps: undefined, - visibility: ({ controls }: ControlPanelsContainerProps) => - checkColumnType( - getColumnLabel(controls?.x_axis?.value as QueryFormColumn), - controls?.datasource?.datasource, - [GenericDataType.Numeric], - ), - }, - }, - ], - [xAxisLabelRotation], - [xAxisLabelInterval], - [forceMaxInterval], - ...richTooltipSection, - // eslint-disable-next-line react/jsx-key - [{t('Y Axis')}], - ['y_axis_format'], - ['currency_format'], - [ - { - name: 'logAxis', - config: { - type: 'CheckboxControl', - label: t('Logarithmic y-axis'), - renderTrigger: true, - default: logAxis, - description: t('Logarithmic y-axis'), - }, - }, - ], - [ - { - name: 'minorSplitLine', - config: { - type: 'CheckboxControl', - label: t('Minor Split Line'), - renderTrigger: true, - default: minorSplitLine, - description: t('Draw split lines for minor y-axis ticks'), - }, - }, - ], - [truncateXAxis], - [xAxisBounds], - [ - { - name: 'truncateYAxis', - config: { - type: 'CheckboxControl', - label: t('Truncate Y Axis'), - default: truncateYAxis, - renderTrigger: true, - description: t( - 'Truncate Y Axis. Can be overridden by specifying a min or max bound.', - ), - }, - }, - ], - [ - { - name: 'y_axis_bounds', - config: { - type: 'BoundsControl', - label: t('Y Axis Bounds'), - renderTrigger: true, - default: yAxisBounds, - description: t( - 'Bounds for the Y-axis. When left empty, the bounds are ' + - 'dynamically defined based on the min/max of the data. Note that ' + - "this feature will only expand the axis range. It won't " + - "narrow the data's extent.", - ), - visibility: ({ controls }: ControlPanelsContainerProps) => - Boolean(controls?.truncateYAxis?.value), - }, - }, - ], - ], - }, - ], - controlOverrides: { - row_limit: { - default: rowLimit, - }, - }, - formDataOverrides: formData => ({ - ...formData, - metrics: getStandardizedControls().popAllMetrics(), - groupby: getStandardizedControls().popAllColumns(), - }), -}; - -export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/index.ts deleted file mode 100644 index 6b0e656d0bf..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/index.ts +++ /dev/null @@ -1,78 +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 { t } from '@apache-superset/core/translation'; -import { AnnotationType, Behavior } from '@superset-ui/core'; -import { EchartsTimeseriesChartProps, EchartsTimeseriesFormData } from '../..'; -import buildQuery from '../buildQuery'; -import controlPanel from './controlPanel'; -import transformProps from '../transformProps'; -import thumbnail from './images/thumbnail.png'; -import thumbnailDark from './images/thumbnail-dark.png'; -import example1 from './images/Step1.png'; -import example1Dark from './images/Step1-dark.png'; -import example2 from './images/Step2.png'; -import example2Dark from './images/Step2-dark.png'; -import { EchartsChartPlugin } from '../../types'; - -export default class EchartsTimeseriesStepChartPlugin extends EchartsChartPlugin< - EchartsTimeseriesFormData, - EchartsTimeseriesChartProps -> { - constructor() { - super({ - buildQuery, - controlPanel, - loadChart: () => import('../EchartsTimeseries'), - metadata: { - behaviors: [ - Behavior.InteractiveChart, - Behavior.DrillToDetail, - Behavior.DrillBy, - ], - category: t('Evolution'), - credits: ['https://echarts.apache.org'], - description: t( - 'Stepped-line graph (also called step chart) is a variation of line chart but with the line forming a series of steps between data points. A step chart can be useful when you want to show the changes that occur at irregular intervals.', - ), - exampleGallery: [ - { url: example1, urlDark: example1Dark }, - { url: example2, urlDark: example2Dark }, - ], - supportedAnnotationTypes: [ - AnnotationType.Event, - AnnotationType.Formula, - AnnotationType.Interval, - AnnotationType.Timeseries, - ], - name: t('Stepped Line'), - tags: [ - t('ECharts'), - t('Predictive'), - t('Advanced-Analytics'), - t('Time'), - t('Transformable'), - t('Stacked'), - ], - thumbnail, - thumbnailDark, - }, - transformProps, - }); - } -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/index.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/index.tsx new file mode 100644 index 00000000000..b5a6f91250b --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/index.tsx @@ -0,0 +1,285 @@ +/** + * 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. + */ + +/** + * Timeseries Stepped Line Chart - Glyph Pattern Implementation + * + * A variation of the line chart where data points are connected + * by horizontal and vertical line segments, forming a staircase pattern. + * + * Key characteristics: + * - seriesType is configurable (Start, Middle, End step placement) + * - Supports stacking (Stack, Stream, Expand modes) + * - Supports area fills with configurable opacity + * - Markers are optional (toggle with markerEnabled) + * - Supports cross-filtering, drill-to-detail, and drill-by + * - Supports annotations (event, formula, interval, timeseries) + * - Has ExtraControls for area stack radio buttons + */ + +import { t } from '@apache-superset/core/translation'; +import { AnnotationType, Behavior, ChartProps } from '@superset-ui/core'; +import { + DEFAULT_SORT_SERIES_DATA, + getStandardizedControls, + SORT_SERIES_CHOICES, +} from '@superset-ui/chart-controls'; + +import { defineChart } from '@superset-ui/glyph-core'; +import { Checkbox, Slider, Select } from '@superset-ui/glyph-core'; +import { + EchartsTimeseriesChartProps, + EchartsTimeseriesSeriesType, + TimeseriesChartTransformedProps, +} from '../types'; +import { DEFAULT_FORM_DATA } from '../constants'; +import buildQuery from '../buildQuery'; +import { showValueSection } from '../../controls'; +import { + transformFullTimeseriesProps, + TimeseriesRender, + timeseriesQueryControls, + timeseriesBaseChartOptions, + areaOpacityRows, + markerConditionalRows, + legendZoomRows, + xAxisRows, + richTooltipSection, + yAxisRows, +} from '../shared'; + +import thumbnail from './images/thumbnail.png'; +import thumbnailDark from './images/thumbnail-dark.png'; +import example1 from './images/Step1.png'; +import example1Dark from './images/Step1-dark.png'; +import example2 from './images/Step2.png'; +import example2Dark from './images/Step2-dark.png'; + +// ============================================================================ +// Types +// ============================================================================ + +interface StepTransformResult { + transformedProps: TimeseriesChartTransformedProps; +} + +// ============================================================================ +// Constants +// ============================================================================ + +const { + logAxis, + markerEnabled, + markerSize, + minorSplitLine, + rowLimit, + truncateYAxis, +} = DEFAULT_FORM_DATA; + +// ============================================================================ +// Chart Definition +// ============================================================================ + +export default defineChart({ + metadata: { + name: t('Stepped Line'), + description: t( + 'Stepped-line graph (also called step chart) is a variation of line chart but with the line forming a series of steps between data points. A step chart can be useful when you want to show the changes that occur at irregular intervals.', + ), + category: t('Evolution'), + tags: [ + t('ECharts'), + t('Predictive'), + t('Advanced-Analytics'), + t('Time'), + t('Transformable'), + t('Stacked'), + ], + credits: ['https://echarts.apache.org'], + thumbnail, + thumbnailDark, + exampleGallery: [ + { url: example1, urlDark: example1Dark }, + { url: example2, urlDark: example2Dark }, + ], + behaviors: [ + Behavior.InteractiveChart, + Behavior.DrillToDetail, + Behavior.DrillBy, + ], + supportedAnnotationTypes: [ + AnnotationType.Event, + AnnotationType.Formula, + AnnotationType.Interval, + AnnotationType.Timeseries, + ], + }, + + arguments: { + // Step type + seriesType: Select.with({ + label: t('Step type'), + description: t( + 'Defines whether the step should appear at the beginning, middle or end between two data points', + ), + options: [ + { value: EchartsTimeseriesSeriesType.Start, label: t('Start') }, + { value: EchartsTimeseriesSeriesType.Middle, label: t('Middle') }, + { value: EchartsTimeseriesSeriesType.End, label: t('End') }, + ], + default: EchartsTimeseriesSeriesType.Start, + }), + + // Marker controls + markerEnabled: Checkbox.with({ + label: t('Marker'), + description: t( + 'Draw a marker on data points. Only applicable for line types.', + ), + default: markerEnabled, + }), + + markerSize: { + arg: Slider.with({ + label: t('Marker Size'), + description: t( + 'Size of marker. Also applies to forecast observations.', + ), + default: markerSize, + min: 0, + max: 20, + step: 1, + }), + visibleWhen: { markerEnabled: true }, + }, + + // Show value control + showValue: Checkbox.with({ + label: t('Show Value'), + description: t('Show series values on the chart'), + default: false, + }), + + // Zoom control + zoomable: Checkbox.with({ + label: t('Data Zoom'), + description: t('Enable data zooming controls'), + default: false, + }), + + // Y-Axis controls + logAxis: Checkbox.with({ + label: t('Logarithmic Y-axis'), + description: t('Logarithmic y-axis'), + default: logAxis, + }), + + minorSplitLine: Checkbox.with({ + label: t('Minor Split Line'), + description: t('Draw split lines for minor y-axis ticks'), + default: minorSplitLine, + }), + + truncateYAxis: Checkbox.with({ + label: t('Truncate Y Axis'), + description: t( + 'Truncate Y Axis. Can be overridden by specifying a min or max bound.', + ), + default: truncateYAxis, + }), + + // Series order controls + sortSeriesType: Select.with({ + label: t('Sort Series By'), + description: t( + 'Based on what should series be ordered on the chart and legend', + ), + options: SORT_SERIES_CHOICES.map(([value, label]) => ({ + value, + label, + })), + default: DEFAULT_SORT_SERIES_DATA.sort_series_type, + }), + + sortSeriesAscending: Checkbox.with({ + label: t('Sort Series Ascending'), + description: t('Sort series in ascending order'), + default: DEFAULT_SORT_SERIES_DATA.sort_series_ascending, + }), + }, + + additionalControls: { + query: timeseriesQueryControls, + chartOptions: [ + ...timeseriesBaseChartOptions, + [ + { + name: 'seriesType', + config: { + type: 'SelectControl', + label: t('Step type'), + renderTrigger: true, + default: EchartsTimeseriesSeriesType.Start, + choices: [ + [EchartsTimeseriesSeriesType.Start, t('Start')], + [EchartsTimeseriesSeriesType.Middle, t('Middle')], + [EchartsTimeseriesSeriesType.End, t('End')], + ], + description: t( + 'Defines whether the step should appear at the beginning, middle or end between two data points', + ), + }, + }, + ], + ...showValueSection, + ...areaOpacityRows, + ...markerConditionalRows, + ...legendZoomRows, + ...xAxisRows, + ...richTooltipSection, + ...yAxisRows, + ], + }, + + controlOverrides: { + row_limit: { + default: rowLimit, + }, + }, + + formDataOverrides: formData => ({ + ...formData, + metrics: getStandardizedControls().popAllMetrics(), + groupby: getStandardizedControls().popAllColumns(), + }), + + buildQuery, + + transform: (chartProps: ChartProps): StepTransformResult => { + // Step chart uses no formData patch - seriesType comes from user's selection + const transformedProps = transformFullTimeseriesProps( + chartProps as unknown as EchartsTimeseriesChartProps, + ); + return { transformedProps }; + }, + + render: ({ transformedProps }) => ( + + ), +}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/shared.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/shared.tsx new file mode 100644 index 00000000000..30aacf41c52 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/shared.tsx @@ -0,0 +1,1998 @@ +/** + * 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. + */ + +/** + * Shared utilities for Timeseries chart variants. + * + * Exports: + * - transformFullTimeseriesProps: transform for Line, Area, Step, Bar, Generic + * - transformSimpleTimeseriesProps: transform for Scatter, SmoothLine + * - TimeseriesRender: unified render component + * - Shared additionalControls pieces (query rows, chart option rows, etc.) + */ + +import { useCallback, useEffect, useRef, useState } from 'react'; +import { invert } from 'lodash'; +import { t } from '@apache-superset/core/translation'; +import { + AnnotationLayer, + AnnotationType, + AxisType, + BinaryQueryObjectFilterClause, + buildCustomFormatters, + CategoricalColorNamespace, + CurrencyFormatter, + DTTM_ALIAS, + ensureIsArray, + getCustomFormatter, + getColumnLabel, + getMetricLabel, + getNumberFormatter, + getTimeFormatter, + getXAxisLabel, + isDefined, + isEventAnnotationLayer, + isFormulaAnnotationLayer, + isIntervalAnnotationLayer, + isPhysicalColumn, + isTimeseriesAnnotationLayer, + LegendState, + NumberFormats, + TimeseriesChartDataResponseResult, + tooltipHtml, +} from '@superset-ui/core'; +import { GenericDataType } from '@apache-superset/core/common'; +import { + ControlPanelsContainerProps, + ControlSetRow, + ControlSubSectionHeader, + D3_TIME_FORMAT_DOCS, + extractExtraMetrics, + getOriginalSeries, + isDerivedSeries, + sections, + sharedControls, +} from '@superset-ui/chart-controls'; +import type { EChartsCoreOption } from 'echarts/core'; +import type { + CallbackDataParams, + LineStyleOption, +} from 'echarts/types/src/util/types'; +import type { SeriesOption } from 'echarts'; +import type { ViewRootGroup } from 'echarts/types/src/util/types'; +import type GlobalModel from 'echarts/types/src/model/Global'; +import type ComponentModel from 'echarts/types/src/model/Component'; + +import { + EchartsHandler, + EventHandlers, + ForecastSeriesEnum, + ForecastValue, + Refs, +} from '../types'; +import Echart from '../components/Echart'; +import { ExtraControls } from '../components/ExtraControls'; +import { formatSeriesName } from '../utils/series'; +import { parseAxisBound } from '../utils/controls'; +import { + calculateLowerLogTick, + dedupSeries, + extractDataTotalValues, + extractSeries, + extractShowValueIndexes, + extractTooltipKeys, + getAxisType, + getColtypesMapping, + getLegendProps, + getMinAndMaxFromBounds, +} from '../utils/series'; +import { + extractAnnotationLabels, + getAnnotationData, +} from '../utils/annotation'; +import { + extractForecastSeriesContext, + extractForecastSeriesContexts, + extractForecastValuesFromTooltipParams, + formatForecastTooltipSeries, + rebaseForecastDatum, + reorderForecastSeries, +} from '../utils/forecast'; +import { convertInteger } from '../utils/convertInteger'; +import { defaultGrid, defaultYAxis } from '../defaults'; +import { + getBaselineSeriesForStream, + getPadding, + transformEventAnnotation, + transformFormulaAnnotation, + transformIntervalAnnotation, + transformSeries, + transformTimeseriesAnnotation, +} from './transformers'; +import { + OpacityEnum, + StackControlsValue, + TIMEGRAIN_TO_TIMESTAMP, + TIMESERIES_CONSTANTS, +} from '../constants'; +import { getDefaultTooltip } from '../utils/tooltip'; +import { + getPercentFormatter, + getTooltipTimeFormatter, + getXAxisFormatter, + getYAxisFormatter, +} from '../utils/formatters'; +import { DEFAULT_FORM_DATA, TIME_SERIES_DESCRIPTION_TEXT } from './constants'; +import { + EchartsTimeseriesChartProps, + EchartsTimeseriesFormData, + EchartsTimeseriesSeriesType, + OrientationType, + TimeseriesChartTransformedProps, +} from './types'; +import { + legendSection, + minorTicks, + richTooltipSection, + seriesOrderSection, + truncateXAxis, + xAxisBounds, + xAxisLabelRotation, + xAxisLabelInterval, + forceMaxInterval, +} from '../controls'; + +// ============================================================================ +// Constants +// ============================================================================ + +export const TIMER_DURATION = 300; + +const { + area, + logAxis, + markerEnabled, + markerSize, + minorSplitLine, + opacity, + truncateYAxis, + yAxisBounds, +} = DEFAULT_FORM_DATA; + +// ============================================================================ +// Shared Transform: Full (Line, Area, Step, Bar, Generic) +// ============================================================================ + +/** + * Shared transform for "full" timeseries charts (Line, Area, Step, Bar, Generic). + * Supports stacking, area fills, orientation, ExtraControls, stackDimension. + * + * @param chartProps - The chart props from the glyph system + * @param formDataPatch - Optional overrides merged into formData before processing. + * Line uses `{ seriesType: EchartsTimeseriesSeriesType.Line }` + * Area uses `{ area: true }` + * Bar uses `{ seriesType: EchartsTimeseriesSeriesType.Bar }` + * Step and Generic pass nothing (or `{}`) + */ +export function transformFullTimeseriesProps( + chartProps: EchartsTimeseriesChartProps, + formDataPatch?: Partial, +): TimeseriesChartTransformedProps { + // Apply optional patch to formData + const effectiveProps = + formDataPatch && Object.keys(formDataPatch).length > 0 + ? { + ...chartProps, + formData: { ...chartProps.formData, ...formDataPatch }, + } + : chartProps; + + const { + width, + height, + filterState, + legendState, + formData, + hooks, + queriesData, + datasource, + theme, + inContextMenu, + emitCrossFilters, + legendIndex, + } = effectiveProps; + + let focusedSeries: string | null = null; + + const { + verboseMap = {}, + columnFormats = {}, + currencyFormats = {}, + } = datasource; + const [queryData] = queriesData; + const { data = [], label_map = {} } = + queryData as TimeseriesChartDataResponseResult; + + const dataTypes = getColtypesMapping(queryData); + const annotationData = getAnnotationData(effectiveProps); + + const { + area: formArea, + annotationLayers, + colorScheme, + contributionMode, + forecastEnabled, + groupby, + legendOrientation, + legendType, + legendMargin, + legendSort, + logAxis: useLogAxis, + markerEnabled: formMarkerEnabled, + markerSize: formMarkerSize, + metrics, + minorSplitLine: showMinorSplitLine, + minorTicks: showMinorTicks, + onlyTotal, + opacity: formOpacity, + orientation, + percentageThreshold, + richTooltip, + seriesType, + showLegend, + showValue, + sliceId, + sortSeriesType, + sortSeriesAscending, + timeGrainSqla, + forceMaxInterval: useForceMaxInterval, + timeCompare, + timeShiftColor, + stack, + tooltipTimeFormat, + tooltipSortByMetric, + showTooltipTotal, + showTooltipPercentage, + truncateXAxis: shouldTruncateXAxis, + truncateYAxis: shouldTruncateYAxis, + xAxis: xAxisOrig, + xAxisBounds: formXAxisBounds, + xAxisForceCategorical, + xAxisLabelRotation, + xAxisLabelInterval, + xAxisSort, + xAxisSortAsc, + xAxisTimeFormat, + xAxisNumberFormat, + xAxisTitle, + xAxisTitleMargin, + yAxisBounds: formYAxisBounds, + yAxisFormat, + currencyFormat, + yAxisTitle, + yAxisTitleMargin, + yAxisTitlePosition, + zoomable, + stackDimension, + }: EchartsTimeseriesFormData = { ...DEFAULT_FORM_DATA, ...formData }; + + const refs: Refs = {}; + const groupBy = ensureIsArray(groupby); + const labelMap: { [key: string]: string[] } = Object.entries( + label_map, + ).reduce((acc, entry) => { + if ( + entry[1].length > groupBy.length && + Array.isArray(timeCompare) && + timeCompare.includes(entry[1][0]) + ) { + entry[1].shift(); + } + return { ...acc, [entry[0]]: entry[1] }; + }, {}); + const colorScale = CategoricalColorNamespace.getScale(colorScheme as string); + const rebasedData = rebaseForecastDatum(data, verboseMap); + let xAxisLabel = getXAxisLabel(effectiveProps.rawFormData) as string; + if ( + isPhysicalColumn(effectiveProps.rawFormData?.x_axis) && + isDefined(verboseMap[xAxisLabel]) + ) { + xAxisLabel = verboseMap[xAxisLabel]; + } + const isHorizontal = orientation === OrientationType.Horizontal; + const { totalStackedValues, thresholdValues } = extractDataTotalValues( + rebasedData, + { + stack, + percentageThreshold, + xAxisCol: xAxisLabel, + legendState, + }, + ); + const extraMetricLabels = extractExtraMetrics(effectiveProps.rawFormData).map( + getMetricLabel, + ); + + const isMultiSeries = groupBy.length || metrics?.length > 1; + const xAxisDataType = dataTypes?.[xAxisLabel] ?? dataTypes?.[xAxisOrig]; + const xAxisType = getAxisType(stack, xAxisForceCategorical, xAxisDataType); + + const [rawSeries, sortedTotalValues, minPositiveValue] = extractSeries( + rebasedData, + { + fillNeighborValue: stack && !forecastEnabled ? 0 : undefined, + xAxis: xAxisLabel, + extraMetricLabels, + stack, + totalStackedValues, + isHorizontal, + sortSeriesType, + sortSeriesAscending, + xAxisSortSeries: isMultiSeries ? xAxisSort : undefined, + xAxisSortSeriesAscending: isMultiSeries ? xAxisSortAsc : undefined, + xAxisType, + }, + ); + const showValueIndexes = extractShowValueIndexes(rawSeries, { + stack, + onlyTotal, + isHorizontal, + legendState, + }); + const seriesContexts = extractForecastSeriesContexts( + rawSeries.map(series => series.name as string), + ); + const isAreaExpand = stack === StackControlsValue.Expand; + const series: SeriesOption[] = []; + + const forcePercentFormatter = Boolean(contributionMode || isAreaExpand); + const percentFormatter = forcePercentFormatter + ? getPercentFormatter(yAxisFormat) + : getPercentFormatter(NumberFormats.PERCENT_2_POINT); + const defaultFormatter = currencyFormat?.symbol + ? new CurrencyFormatter({ d3Format: yAxisFormat, currency: currencyFormat }) + : getNumberFormatter(yAxisFormat); + const customFormatters = buildCustomFormatters( + metrics, + currencyFormats, + columnFormats, + yAxisFormat, + currencyFormat, + ); + + const array = ensureIsArray(effectiveProps.rawFormData?.time_compare); + const inverted = invert(verboseMap); + + let patternIncrement = 0; + + rawSeries.forEach(entry => { + const derivedSeries = isDerivedSeries(entry, effectiveProps.rawFormData); + const lineStyle: LineStyleOption = {}; + if (derivedSeries) { + patternIncrement += 1; + lineStyle.type = [(patternIncrement % 5) + 1, (patternIncrement % 3) + 1]; + lineStyle.opacity = OpacityEnum.DerivedSeries; + } + + const entryName = String(entry.name || ''); + const seriesName = inverted[entryName] || entryName; + + let colorScaleKey = getOriginalSeries(seriesName, array); + + if (array && array.includes(seriesName)) { + const originalSeries = rawSeries.find(s => { + const sName = inverted[String(s.name || '')] || String(s.name || ''); + return !array.includes(sName); + }); + if (originalSeries) { + const originalSeriesName = + inverted[String(originalSeries.name || '')] || + String(originalSeries.name || ''); + colorScaleKey = getOriginalSeries(originalSeriesName, array); + } + } + + const transformedSeries = transformSeries( + entry, + colorScale, + colorScaleKey, + { + area: formArea, + connectNulls: derivedSeries, + filterState, + seriesContexts, + markerEnabled: formMarkerEnabled, + markerSize: formMarkerSize, + areaOpacity: formOpacity, + seriesType, + legendState, + stack, + formatter: forcePercentFormatter + ? percentFormatter + : (getCustomFormatter( + customFormatters, + metrics, + labelMap?.[seriesName]?.[0], + ) ?? defaultFormatter), + showValue, + onlyTotal, + totalStackedValues: sortedTotalValues, + showValueIndexes, + thresholdValues, + richTooltip, + sliceId, + isHorizontal, + lineStyle, + timeCompare: array, + timeShiftColor, + theme, + }, + ); + if (transformedSeries) { + if (stack === StackControlsValue.Stream) { + series.push({ + ...transformedSeries, + data: (transformedSeries.data as any).map( + (row: [string | number, number]) => [row[0], row[1] ?? 0], + ), + }); + } else { + series.push(transformedSeries); + } + } + }); + + if (stack === StackControlsValue.Stream) { + const baselineSeries = getBaselineSeriesForStream( + series.map(entry => entry.data) as [string | number, number][][], + seriesType, + ); + series.unshift(baselineSeries); + } + + const selectedValues = (filterState.selectedValues || []).reduce( + (acc: Record, selectedValue: string) => { + const index = series.findIndex(({ name }) => name === selectedValue); + return { + ...acc, + [index]: selectedValue, + }; + }, + {}, + ); + + annotationLayers + .filter((layer: AnnotationLayer) => layer.show) + .forEach((layer: AnnotationLayer) => { + if (isFormulaAnnotationLayer(layer)) + series.push( + transformFormulaAnnotation( + layer, + data, + xAxisLabel, + xAxisType, + colorScale, + sliceId, + orientation, + ), + ); + else if (isIntervalAnnotationLayer(layer)) { + series.push( + ...transformIntervalAnnotation( + layer, + data, + annotationData, + colorScale, + theme, + sliceId, + orientation, + ), + ); + } else if (isEventAnnotationLayer(layer)) { + series.push( + ...transformEventAnnotation( + layer, + data, + annotationData, + colorScale, + theme, + sliceId, + orientation, + ), + ); + } else if (isTimeseriesAnnotationLayer(layer)) { + series.push( + ...transformTimeseriesAnnotation( + layer, + formMarkerSize, + data, + annotationData, + colorScale, + sliceId, + orientation, + ), + ); + } + }); + + if ( + stack === StackControlsValue.Stack && + stackDimension && + effectiveProps.rawFormData.groupby + ) { + const idxSelectedDimension = + formData.metrics.length > 1 + ? 1 + : 0 + effectiveProps.rawFormData.groupby.indexOf(stackDimension); + for (const s of series) { + if (s.id) { + const columnsArr = labelMap[s.id]; + (s as any).stack = columnsArr[idxSelectedDimension]; + } + } + } + + const [xAxisMin, xAxisMax] = (formXAxisBounds || []).map(parseAxisBound); + let [yAxisMin, yAxisMax] = (formYAxisBounds || []).map(parseAxisBound); + + if ((contributionMode === 'row' || isAreaExpand) && stack) { + if (yAxisMin === undefined) yAxisMin = 0; + if (yAxisMax === undefined) yAxisMax = 1; + } else if ( + useLogAxis && + yAxisMin === undefined && + minPositiveValue !== undefined + ) { + yAxisMin = calculateLowerLogTick(minPositiveValue); + } + + const tooltipFormatter = + xAxisDataType === GenericDataType.Temporal + ? getTooltipTimeFormatter(tooltipTimeFormat) + : String; + const xAxisFormatter = + xAxisDataType === GenericDataType.Temporal + ? getXAxisFormatter(xAxisTimeFormat) + : xAxisDataType === GenericDataType.Numeric + ? getNumberFormatter(xAxisNumberFormat) + : String; + + const { + setDataMask = () => {}, + setControlValue = () => {}, + onContextMenu, + onLegendStateChanged, + onLegendScroll, + } = hooks; + + const addYAxisLabelOffset = !!yAxisTitle; + const addXAxisLabelOffset = !!xAxisTitle; + const padding = getPadding( + showLegend, + legendOrientation, + addYAxisLabelOffset, + zoomable, + legendMargin, + addXAxisLabelOffset, + yAxisTitlePosition, + convertInteger(yAxisTitleMargin), + convertInteger(xAxisTitleMargin), + isHorizontal, + ); + + const legendData = rawSeries + .filter( + entry => + extractForecastSeriesContext(entry.name || '').type === + ForecastSeriesEnum.Observation, + ) + .map(entry => entry.name || '') + .concat(extractAnnotationLabels(annotationLayers)); + + let xAxis: any = { + type: xAxisType, + name: xAxisTitle, + nameGap: convertInteger(xAxisTitleMargin), + nameLocation: 'middle', + axisLabel: { + hideOverlap: true, + formatter: xAxisFormatter, + rotate: xAxisLabelRotation, + interval: xAxisLabelInterval, + }, + minorTick: { show: showMinorTicks }, + minInterval: + xAxisType === AxisType.Time && timeGrainSqla && !useForceMaxInterval + ? TIMEGRAIN_TO_TIMESTAMP[ + timeGrainSqla as keyof typeof TIMEGRAIN_TO_TIMESTAMP + ] + : 0, + maxInterval: + xAxisType === AxisType.Time && timeGrainSqla && useForceMaxInterval + ? TIMEGRAIN_TO_TIMESTAMP[ + timeGrainSqla as keyof typeof TIMEGRAIN_TO_TIMESTAMP + ] + : undefined, + ...getMinAndMaxFromBounds( + xAxisType, + shouldTruncateXAxis, + xAxisMin, + xAxisMax, + seriesType, + ), + }; + + let yAxis: any = { + ...defaultYAxis, + type: useLogAxis ? AxisType.Log : AxisType.Value, + min: yAxisMin, + max: yAxisMax, + minorTick: { show: showMinorTicks }, + minorSplitLine: { show: showMinorSplitLine }, + axisLabel: { + formatter: getYAxisFormatter( + metrics, + forcePercentFormatter, + customFormatters, + defaultFormatter, + yAxisFormat, + ), + }, + scale: shouldTruncateYAxis, + name: yAxisTitle, + nameGap: convertInteger(yAxisTitleMargin), + nameLocation: yAxisTitlePosition === 'Left' ? 'middle' : 'end', + }; + + if (isHorizontal) { + [xAxis, yAxis] = [yAxis, xAxis]; + [padding.bottom, padding.left] = [padding.left, padding.bottom]; + } + + const echartOptions: EChartsCoreOption = { + useUTC: true, + grid: { + ...defaultGrid, + ...padding, + }, + xAxis, + yAxis, + tooltip: { + ...getDefaultTooltip(refs), + show: !inContextMenu, + trigger: richTooltip ? 'axis' : 'item', + formatter: (params: any) => { + const [xIndex, yIndex] = isHorizontal ? [1, 0] : [0, 1]; + const xValue: number = richTooltip + ? params[0].value[xIndex] + : params.value[xIndex]; + const forecastValue: CallbackDataParams[] = richTooltip + ? params + : [params]; + const sortedKeys = extractTooltipKeys( + forecastValue, + yIndex, + richTooltip, + tooltipSortByMetric, + ); + const filteredForecastValue = forecastValue.filter( + (item: CallbackDataParams) => + !annotationLayers.some( + (annotation: AnnotationLayer) => + item.seriesName === annotation.name, + ), + ); + const forecastValues: Record = + extractForecastValuesFromTooltipParams(forecastValue, isHorizontal); + + const filteredForecastValues: Record = + extractForecastValuesFromTooltipParams( + filteredForecastValue, + isHorizontal, + ); + + const isForecast = Object.values(forecastValues).some( + value => + value.forecastTrend || value.forecastLower || value.forecastUpper, + ); + + const formatter = forcePercentFormatter + ? percentFormatter + : (getCustomFormatter(customFormatters, metrics) ?? defaultFormatter); + + const rows: string[][] = []; + const total = Object.values(filteredForecastValues).reduce( + (acc, value) => + value.observation !== undefined ? acc + value.observation : acc, + 0, + ); + const allowTotal = Boolean(isMultiSeries) && richTooltip && !isForecast; + const showPercentage = + allowTotal && !forcePercentFormatter && showTooltipPercentage; + const keys = Object.keys(forecastValues); + let focusedRow; + sortedKeys + .filter(key => keys.includes(key)) + .forEach(key => { + const value = forecastValues[key]; + if (value.observation === 0 && stack) { + return; + } + const row = formatForecastTooltipSeries({ + ...value, + seriesName: key, + formatter, + }); + + const annotationRow = annotationLayers.some( + item => item.name === key, + ); + + if ( + showPercentage && + value.observation !== undefined && + !annotationRow + ) { + row.push( + percentFormatter.format(value.observation / (total || 1)), + ); + } + rows.push(row); + if (key === focusedSeries) { + focusedRow = rows.length - 1; + } + }); + if (stack) { + rows.reverse(); + if (focusedRow !== undefined) { + focusedRow = rows.length - focusedRow - 1; + } + } + if (allowTotal && showTooltipTotal) { + const totalRow = ['Total', formatter.format(total)]; + if (showPercentage) { + totalRow.push(percentFormatter.format(1)); + } + rows.push(totalRow); + } + return tooltipHtml(rows, tooltipFormatter(xValue), focusedRow); + }, + }, + legend: { + ...getLegendProps( + legendType, + legendOrientation, + showLegend, + theme, + zoomable, + legendState, + padding, + ), + scrollDataIndex: legendIndex || 0, + data: legendData.sort((a: string, b: string) => { + if (!legendSort) return 0; + return legendSort === 'asc' ? a.localeCompare(b) : b.localeCompare(a); + }) as string[], + }, + series: dedupSeries(reorderForecastSeries(series) as SeriesOption[]), + toolbox: { + show: zoomable, + top: TIMESERIES_CONSTANTS.toolboxTop, + right: TIMESERIES_CONSTANTS.toolboxRight, + feature: { + dataZoom: { + ...(stack ? { yAxisIndex: false } : {}), + title: { + zoom: t('zoom area'), + back: t('restore zoom'), + }, + }, + }, + }, + dataZoom: zoomable + ? [ + { + type: 'slider', + start: TIMESERIES_CONSTANTS.dataZoomStart, + end: TIMESERIES_CONSTANTS.dataZoomEnd, + bottom: TIMESERIES_CONSTANTS.zoomBottom, + yAxisIndex: isHorizontal ? 0 : undefined, + }, + { + type: 'inside', + yAxisIndex: 0, + zoomOnMouseWheel: false, + moveOnMouseWheel: true, + }, + { + type: 'inside', + xAxisIndex: 0, + zoomOnMouseWheel: false, + moveOnMouseWheel: true, + }, + ] + : [], + }; + + const onFocusedSeries = (seriesName: string | null) => { + focusedSeries = seriesName; + }; + + return { + echartOptions, + emitCrossFilters, + formData, + groupby: groupBy, + height, + labelMap, + selectedValues, + setDataMask, + setControlValue, + width, + legendData, + onContextMenu, + onLegendStateChanged, + onFocusedSeries, + xValueFormatter: tooltipFormatter, + xAxis: { + label: xAxisLabel, + type: xAxisType, + }, + refs, + coltypeMapping: dataTypes, + onLegendScroll, + }; +} + +// ============================================================================ +// Shared Transform: Simple (Scatter, SmoothLine) +// ============================================================================ + +/** + * Shared transform for "simple" timeseries charts (Scatter, SmoothLine). + * No stacking, no area, no orientation swap. + * + * @param chartProps - The chart props from the glyph system + * @param seriesType - The series type (Scatter or Smooth) + * @param opts - Optional settings + * alwaysShowMarkers: if true, markerEnabled=true and onlyTotal=false (for Scatter) + */ +export function transformSimpleTimeseriesProps( + chartProps: EchartsTimeseriesChartProps, + seriesType: EchartsTimeseriesSeriesType, + opts?: { alwaysShowMarkers?: boolean }, +): TimeseriesChartTransformedProps { + const alwaysShowMarkers = opts?.alwaysShowMarkers ?? false; + + const { + width, + height, + filterState, + legendState, + formData, + hooks, + queriesData, + datasource, + theme, + inContextMenu, + emitCrossFilters, + legendIndex, + } = chartProps; + + let focusedSeries: string | null = null; + + const { + verboseMap = {}, + columnFormats = {}, + currencyFormats = {}, + } = datasource; + const [queryData] = queriesData; + const { data = [], label_map = {} } = + queryData as TimeseriesChartDataResponseResult; + + const dataTypes = getColtypesMapping(queryData); + const annotationData = getAnnotationData(chartProps); + + const { + annotationLayers, + colorScheme, + groupby, + legendOrientation, + legendType, + legendMargin, + legendSort, + logAxis: useLogAxis, + markerEnabled: formMarkerEnabled, + markerSize: formMarkerSize, + metrics, + minorSplitLine: showMinorSplitLine, + minorTicks: showMinorTicks, + onlyTotal: formOnlyTotal, + richTooltip, + showLegend, + showValue, + sliceId, + sortSeriesType, + sortSeriesAscending, + timeGrainSqla, + forceMaxInterval: useForceMaxInterval, + timeCompare, + timeShiftColor, + tooltipTimeFormat, + tooltipSortByMetric, + showTooltipTotal, + showTooltipPercentage, + truncateXAxis: shouldTruncateXAxis, + truncateYAxis: shouldTruncateYAxis, + xAxis: xAxisOrig, + xAxisBounds: formXAxisBounds, + xAxisForceCategorical, + xAxisLabelRotation, + xAxisLabelInterval, + xAxisSort, + xAxisSortAsc, + xAxisTimeFormat, + xAxisNumberFormat, + xAxisTitle, + xAxisTitleMargin, + yAxisBounds: formYAxisBounds, + yAxisFormat, + currencyFormat, + yAxisTitle, + yAxisTitleMargin, + yAxisTitlePosition, + zoomable, + }: EchartsTimeseriesFormData = { ...DEFAULT_FORM_DATA, ...formData }; + + const resolvedMarkerEnabled = alwaysShowMarkers ? true : formMarkerEnabled; + const resolvedOnlyTotal = alwaysShowMarkers ? false : formOnlyTotal; + + const refs: Refs = {}; + const groupBy = ensureIsArray(groupby); + const labelMap: { [key: string]: string[] } = Object.entries( + label_map, + ).reduce((acc, entry) => { + if ( + entry[1].length > groupBy.length && + Array.isArray(timeCompare) && + timeCompare.includes(entry[1][0]) + ) { + entry[1].shift(); + } + return { ...acc, [entry[0]]: entry[1] }; + }, {}); + const colorScale = CategoricalColorNamespace.getScale(colorScheme as string); + const rebasedData = rebaseForecastDatum(data, verboseMap); + let xAxisLabel = getXAxisLabel(chartProps.rawFormData) as string; + if ( + isPhysicalColumn(chartProps.rawFormData?.x_axis) && + isDefined(verboseMap[xAxisLabel]) + ) { + xAxisLabel = verboseMap[xAxisLabel]; + } + + const extraMetricLabels = extractExtraMetrics(chartProps.rawFormData).map( + getMetricLabel, + ); + + const isMultiSeries = groupBy.length || metrics?.length > 1; + const xAxisDataType = dataTypes?.[xAxisLabel] ?? dataTypes?.[xAxisOrig]; + const xAxisType = getAxisType(false, xAxisForceCategorical, xAxisDataType); + + // Simple charts don't use stack, so no fillNeighborValue or totalStackedValues + const [rawSeries, , minPositiveValue] = extractSeries(rebasedData, { + xAxis: xAxisLabel, + extraMetricLabels, + sortSeriesType, + sortSeriesAscending, + xAxisSortSeries: isMultiSeries ? xAxisSort : undefined, + xAxisSortSeriesAscending: isMultiSeries ? xAxisSortAsc : undefined, + xAxisType, + }); + + const seriesContexts = extractForecastSeriesContexts( + rawSeries.map(series => series.name as string), + ); + const series: SeriesOption[] = []; + + const defaultFormatter = currencyFormat?.symbol + ? new CurrencyFormatter({ d3Format: yAxisFormat, currency: currencyFormat }) + : getNumberFormatter(yAxisFormat); + const customFormatters = buildCustomFormatters( + metrics, + currencyFormats, + columnFormats, + yAxisFormat, + currencyFormat, + ); + + const array = ensureIsArray(chartProps.rawFormData?.time_compare); + const inverted = invert(verboseMap); + + let patternIncrement = 0; + + rawSeries.forEach(entry => { + const derivedSeries = isDerivedSeries(entry, chartProps.rawFormData); + const lineStyle: LineStyleOption = {}; + if (derivedSeries) { + patternIncrement += 1; + lineStyle.type = [(patternIncrement % 5) + 1, (patternIncrement % 3) + 1]; + lineStyle.opacity = OpacityEnum.DerivedSeries; + } + + const entryName = String(entry.name || ''); + const seriesName = inverted[entryName] || entryName; + + let colorScaleKey = getOriginalSeries(seriesName, array); + + if (array && array.includes(seriesName)) { + const originalSeries = rawSeries.find(s => { + const sName = inverted[String(s.name || '')] || String(s.name || ''); + return !array.includes(sName); + }); + if (originalSeries) { + const originalSeriesName = + inverted[String(originalSeries.name || '')] || + String(originalSeries.name || ''); + colorScaleKey = getOriginalSeries(originalSeriesName, array); + } + } + + const transformedSeries = transformSeries( + entry, + colorScale, + colorScaleKey, + { + area: false, + connectNulls: derivedSeries, + filterState, + seriesContexts, + markerEnabled: resolvedMarkerEnabled, + markerSize: formMarkerSize, + areaOpacity: 0, + seriesType, + legendState, + stack: undefined, + formatter: + getCustomFormatter( + customFormatters, + metrics, + labelMap?.[seriesName]?.[0], + ) ?? defaultFormatter, + showValue, + onlyTotal: resolvedOnlyTotal, + totalStackedValues: undefined, + showValueIndexes: undefined, + thresholdValues: undefined, + richTooltip, + sliceId, + isHorizontal: false, + lineStyle, + timeCompare: array, + timeShiftColor, + theme, + }, + ); + if (transformedSeries) { + series.push(transformedSeries); + } + }); + + const selectedValues = (filterState.selectedValues || []).reduce( + (acc: Record, selectedValue: string) => { + const index = series.findIndex(({ name }) => name === selectedValue); + return { + ...acc, + [index]: selectedValue, + }; + }, + {}, + ); + + // Handle annotations (no orientation param for simple charts) + annotationLayers + .filter((layer: AnnotationLayer) => layer.show) + .forEach((layer: AnnotationLayer) => { + if (isFormulaAnnotationLayer(layer)) + series.push( + transformFormulaAnnotation( + layer, + data, + xAxisLabel, + xAxisType, + colorScale, + sliceId, + ), + ); + else if (isIntervalAnnotationLayer(layer)) { + series.push( + ...transformIntervalAnnotation( + layer, + data, + annotationData, + colorScale, + theme, + sliceId, + ), + ); + } else if (isEventAnnotationLayer(layer)) { + series.push( + ...transformEventAnnotation( + layer, + data, + annotationData, + colorScale, + theme, + sliceId, + ), + ); + } else if (isTimeseriesAnnotationLayer(layer)) { + series.push( + ...transformTimeseriesAnnotation( + layer, + formMarkerSize, + data, + annotationData, + colorScale, + sliceId, + ), + ); + } + }); + + // Axis bounds + const [xAxisMin, xAxisMax] = (formXAxisBounds || []).map(parseAxisBound); + let [yAxisMin, yAxisMax] = (formYAxisBounds || []).map(parseAxisBound); + + if (useLogAxis && yAxisMin === undefined && minPositiveValue !== undefined) { + yAxisMin = calculateLowerLogTick(minPositiveValue); + } + + const tooltipFormatter = + xAxisDataType === GenericDataType.Temporal + ? getTooltipTimeFormatter(tooltipTimeFormat) + : String; + const xAxisFormatter = + xAxisDataType === GenericDataType.Temporal + ? getXAxisFormatter(xAxisTimeFormat) + : xAxisDataType === GenericDataType.Numeric + ? getNumberFormatter(xAxisNumberFormat) + : String; + + const { + setDataMask = () => {}, + setControlValue = () => {}, + onContextMenu, + onLegendStateChanged, + onLegendScroll, + } = hooks; + + const addYAxisLabelOffset = !!yAxisTitle; + const addXAxisLabelOffset = !!xAxisTitle; + const padding = getPadding( + showLegend, + legendOrientation, + addYAxisLabelOffset, + zoomable, + legendMargin, + addXAxisLabelOffset, + yAxisTitlePosition, + convertInteger(yAxisTitleMargin), + convertInteger(xAxisTitleMargin), + false, // isHorizontal - simple charts are always vertical + ); + + const legendData = rawSeries + .filter( + entry => + extractForecastSeriesContext(entry.name || '').type === + ForecastSeriesEnum.Observation, + ) + .map(entry => entry.name || '') + .concat(extractAnnotationLabels(annotationLayers)); + + const xAxis: any = { + type: xAxisType, + name: xAxisTitle, + nameGap: convertInteger(xAxisTitleMargin), + nameLocation: 'middle', + axisLabel: { + hideOverlap: true, + formatter: xAxisFormatter, + rotate: xAxisLabelRotation, + interval: xAxisLabelInterval, + }, + minorTick: { show: showMinorTicks }, + minInterval: + xAxisType === AxisType.Time && timeGrainSqla && !useForceMaxInterval + ? TIMEGRAIN_TO_TIMESTAMP[ + timeGrainSqla as keyof typeof TIMEGRAIN_TO_TIMESTAMP + ] + : 0, + maxInterval: + xAxisType === AxisType.Time && timeGrainSqla && useForceMaxInterval + ? TIMEGRAIN_TO_TIMESTAMP[ + timeGrainSqla as keyof typeof TIMEGRAIN_TO_TIMESTAMP + ] + : undefined, + ...getMinAndMaxFromBounds( + xAxisType, + shouldTruncateXAxis, + xAxisMin, + xAxisMax, + seriesType, + ), + }; + + const yAxis: any = { + ...defaultYAxis, + type: useLogAxis ? AxisType.Log : AxisType.Value, + min: yAxisMin, + max: yAxisMax, + minorTick: { show: showMinorTicks }, + minorSplitLine: { show: showMinorSplitLine }, + axisLabel: { + formatter: getYAxisFormatter( + metrics, + false, // forcePercentFormatter - simple charts don't use percent + customFormatters, + defaultFormatter, + yAxisFormat, + ), + }, + scale: shouldTruncateYAxis, + name: yAxisTitle, + nameGap: convertInteger(yAxisTitleMargin), + nameLocation: yAxisTitlePosition === 'Left' ? 'middle' : 'end', + }; + + const echartOptions: EChartsCoreOption = { + useUTC: true, + grid: { + ...defaultGrid, + ...padding, + }, + xAxis, + yAxis, + tooltip: { + ...getDefaultTooltip(refs), + show: !inContextMenu, + trigger: richTooltip ? 'axis' : 'item', + formatter: (params: any) => { + const xValue: number = richTooltip + ? params[0].value[0] + : params.value[0]; + const forecastValue: CallbackDataParams[] = richTooltip + ? params + : [params]; + const sortedKeys = extractTooltipKeys( + forecastValue, + 1, // yIndex + richTooltip, + tooltipSortByMetric, + ); + const filteredForecastValue = forecastValue.filter( + (item: CallbackDataParams) => + !annotationLayers.some( + (annotation: AnnotationLayer) => + item.seriesName === annotation.name, + ), + ); + const forecastValues: Record = + extractForecastValuesFromTooltipParams(forecastValue, false); + + const filteredForecastValues: Record = + extractForecastValuesFromTooltipParams(filteredForecastValue, false); + + const isForecast = Object.values(forecastValues).some( + value => + value.forecastTrend || value.forecastLower || value.forecastUpper, + ); + + const formatter = + getCustomFormatter(customFormatters, metrics) ?? defaultFormatter; + + const rows: string[][] = []; + const total = Object.values(filteredForecastValues).reduce( + (acc, value) => + value.observation !== undefined ? acc + value.observation : acc, + 0, + ); + const allowTotal = Boolean(isMultiSeries) && richTooltip && !isForecast; + const showPercentage = allowTotal && showTooltipPercentage; + const keys = Object.keys(forecastValues); + let focusedRow; + sortedKeys + .filter(key => keys.includes(key)) + .forEach(key => { + const value = forecastValues[key]; + const row = formatForecastTooltipSeries({ + ...value, + seriesName: key, + formatter, + }); + + const annotationRow = annotationLayers.some( + item => item.name === key, + ); + + if ( + showPercentage && + value.observation !== undefined && + !annotationRow + ) { + row.push( + getNumberFormatter(',.1%').format( + value.observation / (total || 1), + ), + ); + } + rows.push(row); + if (key === focusedSeries) { + focusedRow = rows.length - 1; + } + }); + + if (allowTotal && showTooltipTotal) { + const totalRow = ['Total', formatter.format(total)]; + if (showPercentage) { + totalRow.push(getNumberFormatter(',.1%').format(1)); + } + rows.push(totalRow); + } + return tooltipHtml(rows, tooltipFormatter(xValue), focusedRow); + }, + }, + legend: { + ...getLegendProps( + legendType, + legendOrientation, + showLegend, + theme, + zoomable, + legendState, + padding, + ), + scrollDataIndex: legendIndex || 0, + data: legendData.sort((a: string, b: string) => { + if (!legendSort) return 0; + return legendSort === 'asc' ? a.localeCompare(b) : b.localeCompare(a); + }) as string[], + }, + series: dedupSeries(reorderForecastSeries(series) as SeriesOption[]), + toolbox: { + show: zoomable, + top: TIMESERIES_CONSTANTS.toolboxTop, + right: TIMESERIES_CONSTANTS.toolboxRight, + feature: { + dataZoom: { + title: { + zoom: t('zoom area'), + back: t('restore zoom'), + }, + }, + }, + }, + dataZoom: zoomable + ? [ + { + type: 'slider', + start: TIMESERIES_CONSTANTS.dataZoomStart, + end: TIMESERIES_CONSTANTS.dataZoomEnd, + bottom: TIMESERIES_CONSTANTS.zoomBottom, + }, + { + type: 'inside', + yAxisIndex: 0, + zoomOnMouseWheel: false, + moveOnMouseWheel: true, + }, + { + type: 'inside', + xAxisIndex: 0, + zoomOnMouseWheel: false, + moveOnMouseWheel: true, + }, + ] + : [], + }; + + const onFocusedSeries = (seriesName: string | null) => { + focusedSeries = seriesName; + }; + + return { + echartOptions, + emitCrossFilters, + formData, + groupby: groupBy, + height, + labelMap, + selectedValues, + setDataMask, + setControlValue, + width, + legendData, + onContextMenu, + onLegendStateChanged, + onFocusedSeries, + xValueFormatter: tooltipFormatter, + xAxis: { + label: xAxisLabel, + type: xAxisType, + }, + refs, + coltypeMapping: dataTypes, + onLegendScroll, + }; +} + +// ============================================================================ +// Shared Render Component +// ============================================================================ + +/** + * Unified render component for all timeseries chart variants. + * + * When hasExtraControls=true (Line, Area, Step, Bar, Generic): + * - Renders ExtraControls wrapper above the chart + * - Uses ResizeObserver to track extra controls height + * - Subtracts extra controls height from chart height + * - Includes ec-polygon check in dblclick handler + * + * When hasExtraControls=false (Scatter, SmoothLine): + * - No ExtraControls wrapper + * - Full height used for chart + */ +export function TimeseriesRender({ + transformedProps, + hasExtraControls = false, +}: { + transformedProps: TimeseriesChartTransformedProps; + hasExtraControls?: boolean; +}) { + const { + formData, + height, + width, + echartOptions, + groupby, + labelMap, + selectedValues, + setDataMask, + setControlValue, + legendData = [], + onContextMenu, + onLegendStateChanged, + onFocusedSeries, + xValueFormatter, + xAxis, + refs, + emitCrossFilters, + coltypeMapping, + onLegendScroll, + } = transformedProps; + + const { stack } = formData; + const echartRef = useRef(null); + // eslint-disable-next-line no-param-reassign + refs.echartRef = echartRef; + const clickTimer = useRef>(); + const extraControlRef = useRef(null); + const [extraControlHeight, setExtraControlHeight] = useState(0); + + useEffect(() => { + if (!hasExtraControls) return; + + const element = extraControlRef.current; + if (!element) { + setExtraControlHeight(0); + return; + } + + const updateHeight = () => { + setExtraControlHeight(element.offsetHeight || 0); + }; + + updateHeight(); + + if (typeof ResizeObserver === 'function') { + const resizeObserver = new ResizeObserver(() => { + updateHeight(); + }); + resizeObserver.observe(element); + return () => { + resizeObserver.disconnect(); + }; + } + + window.addEventListener('resize', updateHeight); + return () => { + window.removeEventListener('resize', updateHeight); + }; + }, [hasExtraControls, formData.showExtraControls]); + + const hasDimensions = ensureIsArray(groupby).length > 0; + + const getModelInfo = (target: ViewRootGroup, globalModel: GlobalModel) => { + let el = target; + let model: ComponentModel | null = null; + while (el) { + // eslint-disable-next-line no-underscore-dangle + const modelInfo = el.__ecComponentInfo; + if (modelInfo != null) { + model = globalModel.getComponent(modelInfo.mainType, modelInfo.index); + break; + } + el = el.parent; + } + return model; + }; + + const getCrossFilterDataMask = useCallback( + (value: string) => { + const selected: string[] = Object.values(selectedValues); + let values: string[]; + if (selected.includes(value)) { + values = selected.filter(v => v !== value); + } else { + values = [value]; + } + const groupbyValues = values.map(value => labelMap[value]); + return { + dataMask: { + extraFormData: { + filters: + values.length === 0 + ? [] + : groupby.map((col, idx) => { + const val = groupbyValues.map(v => v[idx]); + if (val === null || val === undefined) + return { + col, + op: 'IS NULL' as const, + }; + return { + col, + op: 'IN' as const, + val: val as (string | number | boolean)[], + }; + }), + }, + filterState: { + label: groupbyValues.length ? groupbyValues : undefined, + value: groupbyValues.length ? groupbyValues : null, + selectedValues: values.length ? values : null, + }, + }, + isCurrentValueSelected: selected.includes(value), + }; + }, + [groupby, labelMap, selectedValues], + ); + + const handleChange = useCallback( + (value: string) => { + if (!emitCrossFilters) { + return; + } + setDataMask(getCrossFilterDataMask(value).dataMask); + }, + [emitCrossFilters, setDataMask, getCrossFilterDataMask], + ); + + const eventHandlers: EventHandlers = { + click: props => { + if (!hasDimensions) { + return; + } + if (clickTimer.current) { + clearTimeout(clickTimer.current); + } + clickTimer.current = setTimeout(() => { + const { seriesName: name } = props; + handleChange(name); + }, TIMER_DURATION); + }, + mouseout: () => { + onFocusedSeries(null); + }, + mouseover: params => { + onFocusedSeries(params.seriesName); + }, + legendscroll: payload => { + onLegendScroll?.(payload.scrollDataIndex); + }, + legendselectchanged: payload => { + onLegendStateChanged?.(payload.selected); + }, + legendselectall: payload => { + onLegendStateChanged?.(payload.selected); + }, + legendinverseselect: payload => { + onLegendStateChanged?.(payload.selected); + }, + contextmenu: async eventParams => { + if (onContextMenu) { + eventParams.event.stop(); + const { data, seriesName } = eventParams; + const drillToDetailFilters: BinaryQueryObjectFilterClause[] = []; + const drillByFilters: BinaryQueryObjectFilterClause[] = []; + const pointerEvent = eventParams.event.event; + const values = [ + ...(eventParams.name ? [eventParams.name] : []), + ...(labelMap[seriesName] ?? []), + ]; + const groupBy = ensureIsArray(formData.groupby); + if (data && xAxis.type === AxisType.Time) { + drillToDetailFilters.push({ + col: + xAxis.label === DTTM_ALIAS + ? formData.granularitySqla + : xAxis.label, + grain: formData.timeGrainSqla, + op: '==', + val: data[0], + formattedVal: xValueFormatter(data[0]), + }); + } + [ + ...(xAxis.type === AxisType.Category && data ? [xAxis.label] : []), + ...groupBy, + ].forEach((dimension, i) => + drillToDetailFilters.push({ + col: dimension, + op: '==', + val: values[i], + formattedVal: String(values[i]), + }), + ); + groupBy.forEach((dimension, i) => { + const dimensionValues = labelMap[seriesName] ?? []; + const metricsCount = dimensionValues.length - groupBy.length; + const val = dimensionValues[metricsCount + i]; + + drillByFilters.push({ + col: dimension, + op: '==', + val, + formattedVal: formatSeriesName(val, { + timeFormatter: getTimeFormatter(formData.dateFormat), + numberFormatter: getNumberFormatter(formData.numberFormat), + coltype: coltypeMapping?.[getColumnLabel(dimension)], + }), + }); + }); + + onContextMenu(pointerEvent.clientX, pointerEvent.clientY, { + drillToDetail: drillToDetailFilters, + drillBy: { filters: drillByFilters, groupbyFieldName: 'groupby' }, + crossFilter: hasDimensions + ? getCrossFilterDataMask(seriesName) + : undefined, + }); + } + }, + }; + + const zrEventHandlers: EventHandlers = { + dblclick: params => { + if (clickTimer.current) { + clearTimeout(clickTimer.current); + } + const pointInPixel = [params.offsetX, params.offsetY]; + const echartInstance = echartRef.current?.getEchartInstance(); + if (echartInstance?.containPixel('grid', pointInPixel)) { + if (hasExtraControls && !stack && params.target?.type === 'ec-polygon') + return; + // @ts-ignore + const globalModel = echartInstance.getModel(); + const model = getModelInfo(params.target, globalModel); + if (model) { + const { name } = model; + const legendState: LegendState = legendData.reduce( + (previous, datum) => ({ + ...previous, + [datum]: datum === name, + }), + {}, + ); + onLegendStateChanged?.(legendState); + } + } + }, + }; + + const echart = ( + + ); + + if (!hasExtraControls) { + return echart; + } + + return ( + <> +
+ +
+ {echart} + + ); +} + +// ============================================================================ +// Shared additionalControls pieces +// ============================================================================ + +/** + * Shared query section rows - identical across all 7 charts. + */ +export const timeseriesQueryControls: ControlSetRow[] = [ + ...sections.echartsTimeSeriesQueryWithXAxisSort.controlSetRows, + ...sections.advancedAnalyticsControls.controlSetRows, + ...sections.annotationsAndLayersControls.controlSetRows, + ...sections.forecastIntervalControls.controlSetRows, +]; + +/** + * Shared chart options header + seriesOrder + color rows - base for all 7 charts. + */ +export const timeseriesBaseChartOptions: ControlSetRow[] = [ + ...sections.titleControls.controlSetRows, + [ + + {t('Chart Options')} + , + ], + ...seriesOrderSection, + ['color_scheme'], + ['time_shift_color'], +]; + +/** + * Area checkbox + opacity slider rows (with visibility condition). + * Used by Line, Step, Generic charts. + */ +export const areaOpacityRows: ControlSetRow[] = [ + [ + { + name: 'area', + config: { + type: 'CheckboxControl', + label: t('Area Chart'), + renderTrigger: true, + default: area, + description: t( + 'Draw area under curves. Only applicable for line types.', + ), + }, + }, + ], + [ + { + name: 'opacity', + config: { + type: 'SliderControl', + label: t('Area chart opacity'), + renderTrigger: true, + min: 0, + max: 1, + step: 0.1, + default: opacity, + description: t( + 'Opacity of Area Chart. Also applies to confidence band.', + ), + visibility: ({ controls }: ControlPanelsContainerProps) => + Boolean(controls?.area?.value), + }, + }, + ], +]; + +/** + * markerEnabled + markerSize rows with conditional visibility. + * Used by Line, Area, Step, Generic, SmoothLine charts. + */ +export const markerConditionalRows: ControlSetRow[] = [ + [ + { + name: 'markerEnabled', + config: { + type: 'CheckboxControl', + label: t('Marker'), + renderTrigger: true, + default: markerEnabled, + description: t( + 'Draw a marker on data points. Only applicable for line types.', + ), + }, + }, + ], + [ + { + name: 'markerSize', + config: { + type: 'SliderControl', + label: t('Marker Size'), + renderTrigger: true, + min: 0, + max: 20, + default: markerSize, + description: t( + 'Size of marker. Also applies to forecast observations.', + ), + visibility: ({ controls }: ControlPanelsContainerProps) => + Boolean(controls?.markerEnabled?.value), + }, + }, + ], +]; + +/** + * Direct markerSize row without conditional visibility. + * Used by Scatter chart (always shows markers). + */ +export const markerDirectRow: ControlSetRow[] = [ + [ + { + name: 'markerSize', + config: { + type: 'SliderControl', + label: t('Marker Size'), + renderTrigger: true, + min: 0, + max: 100, + default: markerSize, + description: t( + 'Size of marker. Also applies to forecast observations.', + ), + }, + }, + ], +]; + +/** + * Standard X Axis rows. + * Used by Line, Area, Step, Generic, SmoothLine, Scatter charts. + */ +export const xAxisRows: ControlSetRow[] = [ + [ + + {t('X Axis')} + , + ], + [ + { + name: 'x_axis_time_format', + config: { + ...sharedControls.x_axis_time_format, + default: 'smart_date', + description: `${D3_TIME_FORMAT_DOCS}. ${TIME_SERIES_DESCRIPTION_TEXT}`, + }, + }, + ], + [xAxisLabelRotation], + [xAxisLabelInterval], + [forceMaxInterval], +]; + +/** + * Standard Y Axis rows (logAxis, minorSplitLine, truncateXAxis, xAxisBounds, + * truncateYAxis, y_axis_bounds with conditional visibility). + * Used by Line, Area, Step, Generic, SmoothLine, Scatter charts. + */ +export const yAxisRows: ControlSetRow[] = [ + [ + + {t('Y Axis')} + , + ], + ['y_axis_format'], + ['currency_format'], + [ + { + name: 'logAxis', + config: { + type: 'CheckboxControl', + label: t('Logarithmic y-axis'), + renderTrigger: true, + default: logAxis, + description: t('Logarithmic y-axis'), + }, + }, + ], + [ + { + name: 'minorSplitLine', + config: { + type: 'CheckboxControl', + label: t('Minor Split Line'), + renderTrigger: true, + default: minorSplitLine, + description: t('Draw split lines for minor y-axis ticks'), + }, + }, + ], + [truncateXAxis], + [xAxisBounds], + [ + { + name: 'truncateYAxis', + config: { + type: 'CheckboxControl', + label: t('Truncate Y Axis'), + default: truncateYAxis, + renderTrigger: true, + description: t( + 'Truncate Y Axis. Can be overridden by specifying a min or max bound.', + ), + }, + }, + ], + [ + { + name: 'y_axis_bounds', + config: { + type: 'BoundsControl', + label: t('Y Axis Bounds'), + renderTrigger: true, + default: yAxisBounds, + description: t( + 'Bounds for the Y-axis. When left empty, the bounds are ' + + 'dynamically defined based on the min/max of the data. Note that ' + + "this feature will only expand the axis range. It won't " + + "narrow the data's extent.", + ), + visibility: ({ controls }: ControlPanelsContainerProps) => + Boolean(controls?.truncateYAxis?.value), + }, + }, + ], +]; + +/** + * Legend section + zoomable + minorTicks rows. + * Used by all non-Bar charts (Bar uses its own ordering). + */ +export const legendZoomRows: ControlSetRow[] = [ + ['zoomable'], + [minorTicks], + ...legendSection, +]; + +/** + * richTooltipSection re-exported for use in chart files. + */ +export { richTooltipSection }; + +/** + * AnnotationType re-exported for metadata in chart files. + */ +export type { AnnotationType }; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/stories/AreaSeries/AreaSeries.stories.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/stories/AreaSeries/AreaSeries.stories.tsx index 92c8b8722bc..f499ca91a06 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/stories/AreaSeries/AreaSeries.stories.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/stories/AreaSeries/AreaSeries.stories.tsx @@ -17,25 +17,13 @@ * under the License. */ -import { - SuperChart, - getChartTransformPropsRegistry, - VizType, -} from '@superset-ui/core'; -import { - EchartsAreaChartPlugin, - TimeseriesTransformProps, -} from '@superset-ui/plugin-chart-echarts'; +import { SuperChart, VizType } from '@superset-ui/core'; +import { EchartsAreaChartPlugin } from '@superset-ui/plugin-chart-echarts'; import data from './data'; import { withResizableChartDemo } from '@storybook-shared'; new EchartsAreaChartPlugin().configure({ key: VizType.Area }).register(); -getChartTransformPropsRegistry().registerValue( - VizType.Area, - TimeseriesTransformProps, -); - export default { title: 'Chart Plugins/plugin-chart-echarts', decorators: [withResizableChartDemo], diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/stories/Timeseries.stories.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/stories/Timeseries.stories.tsx index 6ebd4be2dfd..91304c47f93 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/stories/Timeseries.stories.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/stories/Timeseries.stories.tsx @@ -17,11 +17,8 @@ * under the License. */ -import { SuperChart, getChartTransformPropsRegistry } from '@superset-ui/core'; -import { - EchartsTimeseriesChartPlugin, - TimeseriesTransformProps, -} from '@superset-ui/plugin-chart-echarts'; +import { SuperChart } from '@superset-ui/core'; +import { EchartsTimeseriesChartPlugin } from '@superset-ui/plugin-chart-echarts'; import data from './data'; import negativeNumData from './negativeNumData'; import confbandData from './confbandData'; @@ -32,11 +29,6 @@ new EchartsTimeseriesChartPlugin() .configure({ key: 'echarts-timeseries' }) .register(); -getChartTransformPropsRegistry().registerValue( - 'echarts-timeseries', - TimeseriesTransformProps, -); - export default { title: 'Chart Plugins/plugin-chart-echarts/SeriesChart', decorators: [withResizableChartDemo], diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Tree/EchartsTree.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Tree/EchartsTree.tsx deleted file mode 100644 index cb1ef9b904c..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Tree/EchartsTree.tsx +++ /dev/null @@ -1,38 +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 { TreeTransformedProps } from './types'; -import Echart from '../components/Echart'; - -export default function EchartsTree({ - echartOptions, - height, - refs, - width, - formData, -}: TreeTransformedProps) { - return ( - - ); -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Tree/buildQuery.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Tree/buildQuery.ts deleted file mode 100644 index 09d99b28dd9..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Tree/buildQuery.ts +++ /dev/null @@ -1,40 +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 { buildQueryContext } from '@superset-ui/core'; -import { EchartsTreeFormData } from './types'; -import { buildColumnsOrderBy, applyOrderBy } from '../utils/orderby'; - -export default function buildQuery(formData: EchartsTreeFormData) { - const { id, parent, name, row_limit } = formData; - const orderby = buildColumnsOrderBy([parent, id, name]); - - return buildQueryContext(formData, { - queryFields: { - id: 'columns', - parent: 'columns', - name: 'columns', - }, - buildQuery: baseQueryObject => [ - { - ...baseQueryObject, - ...applyOrderBy(orderby, row_limit), - }, - ], - }); -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Tree/constants.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Tree/constants.ts deleted file mode 100644 index eb0b5f9bc80..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Tree/constants.ts +++ /dev/null @@ -1,46 +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 type { TreeSeriesOption } from 'echarts/charts'; -import { EchartsTreeFormData } from './types'; - -export const DEFAULT_TREE_SERIES_OPTION: TreeSeriesOption = { - label: { - position: 'left', - fontSize: 15, - }, - animation: true, - animationDuration: 500, - animationEasing: 'cubicOut', -}; - -export const DEFAULT_FORM_DATA: Partial = { - id: '', - parent: '', - name: '', - rootNodeId: '', - layout: 'orthogonal', - orient: 'LR', - symbol: 'emptyCircle', - symbolSize: 7, - roam: true, - nodeLabelPosition: 'left', - childLabelPosition: 'bottom', - emphasis: 'descendant', - initialTreeDepth: 2, -}; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Tree/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Tree/controlPanel.tsx deleted file mode 100644 index 49190078aaf..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Tree/controlPanel.tsx +++ /dev/null @@ -1,308 +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 { t } from '@apache-superset/core/translation'; -import { - ControlPanelConfig, - ControlSubSectionHeader, - getStandardizedControls, - sharedControls, -} from '@superset-ui/chart-controls'; -import { DEFAULT_FORM_DATA } from './constants'; - -const requiredEntity = { - ...sharedControls.entity, - clearable: false, -}; -const optionalEntity = { - ...sharedControls.entity, - clearable: true, - validators: [], -}; - -const controlPanel: ControlPanelConfig = { - controlPanelSections: [ - { - label: t('Query'), - expanded: true, - controlSetRows: [ - [ - { - name: 'id', - config: { - ...requiredEntity, - label: t('Id'), - description: t('Name of the id column'), - }, - }, - ], - [ - { - name: 'parent', - config: { - ...requiredEntity, - label: t('Parent'), - description: t( - 'Name of the column containing the id of the parent node', - ), - }, - }, - ], - [ - { - name: 'name', - config: { - ...optionalEntity, - label: t('Name'), - description: t('Optional name of the data column.'), - }, - }, - ], - [ - { - name: 'root_node_id', - config: { - ...optionalEntity, - renderTrigger: true, - type: 'TextControl', - label: t('Root node id'), - description: t('Id of root node of the tree.'), - }, - }, - ], - [ - { - name: 'metric', - config: { - ...sharedControls.metric, - clearable: true, - validators: [], - description: t('Metric for node values'), - }, - }, - ], - ['adhoc_filters'], - ['row_limit'], - ], - }, - { - label: t('Chart options'), - expanded: true, - controlSetRows: [ - [{t('Layout')}], - [ - { - name: 'layout', - config: { - type: 'RadioButtonControl', - renderTrigger: true, - label: t('Tree layout'), - default: DEFAULT_FORM_DATA.layout, - options: [ - ['orthogonal', t('Orthogonal')], - ['radial', t('Radial')], - ], - description: t('Layout type of tree'), - }, - }, - ], - - [ - { - name: 'orient', - config: { - type: 'RadioButtonControl', - renderTrigger: true, - label: t('Tree orientation'), - default: DEFAULT_FORM_DATA.orient, - options: [ - ['LR', t('Left to Right')], - ['RL', t('Right to Left')], - ['TB', t('Top to Bottom')], - ['BT', t('Bottom to Top')], - ], - description: t('Orientation of tree'), - visibility({ form_data: { layout } }) { - return (layout || DEFAULT_FORM_DATA.layout) === 'orthogonal'; - }, - }, - }, - ], - [ - { - name: 'node_label_position', - config: { - type: 'RadioButtonControl', - renderTrigger: true, - label: t('Node label position'), - default: DEFAULT_FORM_DATA.nodeLabelPosition, - options: [ - ['left', t('left')], - ['top', t('top')], - ['right', t('right')], - ['bottom', t('bottom')], - ], - description: t('Position of intermediate node label on tree'), - }, - }, - ], - [ - { - name: 'child_label_position', - config: { - type: 'RadioButtonControl', - renderTrigger: true, - label: t('Child label position'), - default: DEFAULT_FORM_DATA.childLabelPosition, - options: [ - ['left', t('left')], - ['top', t('top')], - ['right', t('right')], - ['bottom', t('bottom')], - ], - description: t('Position of child node label on tree'), - }, - }, - ], - [ - { - name: 'emphasis', - config: { - type: 'RadioButtonControl', - renderTrigger: true, - label: t('Emphasis'), - default: DEFAULT_FORM_DATA.emphasis, - options: [ - ['ancestor', t('ancestor')], - ['descendant', t('descendant')], - ], - description: t('Which relatives to highlight on hover'), - visibility({ form_data: { layout } }) { - return (layout || DEFAULT_FORM_DATA.layout) === 'orthogonal'; - }, - }, - }, - ], - [ - { - name: 'symbol', - config: { - type: 'SelectControl', - renderTrigger: true, - label: t('Symbol'), - default: DEFAULT_FORM_DATA.symbol, - options: [ - { - label: t('Empty circle'), - value: 'emptyCircle', - }, - { - label: t('Circle'), - value: 'circle', - }, - { - label: t('Rectangle'), - value: 'rect', - }, - { - label: t('Triangle'), - value: 'triangle', - }, - { - label: t('Diamond'), - value: 'diamond', - }, - { - label: t('Pin'), - value: 'pin', - }, - { - label: t('Arrow'), - value: 'arrow', - }, - { - label: t('None'), - value: 'none', - }, - ], - description: t('Layout type of tree'), - }, - }, - ], - [ - { - name: 'symbolSize', - config: { - type: 'SliderControl', - label: t('Symbol size'), - renderTrigger: true, - min: 5, - max: 30, - step: 2, - default: DEFAULT_FORM_DATA.symbolSize, - description: t('Size of edge symbols'), - }, - }, - ], - [ - { - name: 'roam', - config: { - type: 'SelectControl', - label: t('Enable graph roaming'), - renderTrigger: true, - default: DEFAULT_FORM_DATA.roam, - choices: [ - [false, t('Disabled')], - ['scale', t('Scale only')], - ['move', t('Move only')], - [true, t('Scale and Move')], - ], - description: t( - 'Whether to enable changing graph position and scaling.', - ), - }, - }, - ], - [ - { - name: 'initialTreeDepth', - config: { - type: 'NumberControl', - label: t('Initial tree depth'), - min: -1, - step: 1, - max: 10, - default: DEFAULT_FORM_DATA.initialTreeDepth, - renderTrigger: true, - description: t( - 'The initial level (depth) of the tree. If set as -1 all nodes are expanded.', - ), - }, - }, - ], - ], - }, - ], - formDataOverrides: formData => ({ - ...formData, - metric: getStandardizedControls().shiftMetric(), - }), -}; - -export default controlPanel; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Tree/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Tree/index.ts deleted file mode 100644 index c1807aec3e2..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Tree/index.ts +++ /dev/null @@ -1,57 +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 { t } from '@apache-superset/core/translation'; -import controlPanel from './controlPanel'; -import transformProps from './transformProps'; -import thumbnail from './images/thumbnail.png'; -import thumbnailDark from './images/thumbnail-dark.png'; -import example from './images/tree.png'; -import exampleDark from './images/tree-dark.png'; -import buildQuery from './buildQuery'; -import { EchartsChartPlugin } from '../types'; - -export default class EchartsTreeChartPlugin extends EchartsChartPlugin { - constructor() { - super({ - buildQuery, - controlPanel, - loadChart: () => import('./EchartsTree'), - metadata: { - category: t('Part of a Whole'), - credits: ['https://echarts.apache.org'], - description: t( - 'Visualize multiple levels of hierarchy using a familiar tree-like structure.', - ), - exampleGallery: [{ url: example, urlDark: exampleDark }], - name: t('Tree Chart'), - tags: [ - t('Categorical'), - t('ECharts'), - t('Multi-Levels'), - t('Relational'), - t('Structural'), - t('Featured'), - ], - thumbnail, - thumbnailDark, - }, - transformProps, - }); - } -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Tree/index.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Tree/index.tsx new file mode 100644 index 00000000000..88828edce4c --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Tree/index.tsx @@ -0,0 +1,559 @@ +/** + * 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. + */ + +/** + * ECharts Tree Chart - Glyph Pattern Implementation + * + * Visualizes hierarchical data using a tree structure with nodes and edges. + */ + +import { t } from '@apache-superset/core/translation'; +import type { EChartsCoreOption } from 'echarts/core'; +import type { TreeSeriesOption } from 'echarts/charts'; +import type { + TreeSeriesCallbackDataParams, + TreeSeriesNodeItemOption, +} from 'echarts/types/src/chart/tree/TreeSeries'; +import type { OptionName } from 'echarts/types/src/util/types'; +import { + buildQueryContext, + DataRecordValue, + getMetricLabel, + QueryFormData, + tooltipHtml, +} from '@superset-ui/core'; +import { sharedControls } from '@superset-ui/chart-controls'; + +import { + defineChart, + Metric, + Text, + Select, + Int, + RadioButton, + ChartProps, +} from '@superset-ui/glyph-core'; + +import { getDefaultTooltip } from '../utils/tooltip'; +import Echart from '../components/Echart'; +import { Refs } from '../types'; + +import thumbnail from './images/thumbnail.png'; +import thumbnailDark from './images/thumbnail-dark.png'; +import example from './images/tree.png'; +import exampleDark from './images/tree-dark.png'; + +// ============================================================================ +// Constants +// ============================================================================ + +const DEFAULT_TREE_SERIES_OPTION: Partial = { + label: { + position: 'left', + fontSize: 15, + }, + animation: true, + animationDuration: 500, + animationEasing: 'cubicOut', +}; + +const DEFAULT_FORM_DATA = { + layout: 'orthogonal', + orient: 'LR', + symbol: 'emptyCircle', + symbolSize: 7, + roam: true, + nodeLabelPosition: 'left', + childLabelPosition: 'bottom', + emphasis: 'descendant', + initialTreeDepth: 2, +}; + +const SYMBOL_OPTIONS = [ + { label: t('Empty circle'), value: 'emptyCircle' }, + { label: t('Circle'), value: 'circle' }, + { label: t('Rectangle'), value: 'rect' }, + { label: t('Triangle'), value: 'triangle' }, + { label: t('Diamond'), value: 'diamond' }, + { label: t('Pin'), value: 'pin' }, + { label: t('Arrow'), value: 'arrow' }, + { label: t('None'), value: 'none' }, +]; + +const ROAM_OPTIONS = [ + { label: t('Disabled'), value: 'false' }, + { label: t('Scale only'), value: 'scale' }, + { label: t('Move only'), value: 'move' }, + { label: t('Scale and Move'), value: 'true' }, +]; + +const LAYOUT_OPTIONS = [ + { label: t('Orthogonal'), value: 'orthogonal' }, + { label: t('Radial'), value: 'radial' }, +]; + +const ORIENT_OPTIONS = [ + { label: t('Left to Right'), value: 'LR' }, + { label: t('Right to Left'), value: 'RL' }, + { label: t('Top to Bottom'), value: 'TB' }, + { label: t('Bottom to Top'), value: 'BT' }, +]; + +const POSITION_OPTIONS = [ + { label: t('left'), value: 'left' }, + { label: t('top'), value: 'top' }, + { label: t('right'), value: 'right' }, + { label: t('bottom'), value: 'bottom' }, +]; + +const EMPHASIS_OPTIONS = [ + { label: t('ancestor'), value: 'ancestor' }, + { label: t('descendant'), value: 'descendant' }, +]; + +// ============================================================================ +// Types +// ============================================================================ + +type TreeDataRecord = Record & { + children?: TreeSeriesNodeItemOption[]; +}; + +interface TreeTransformResult { + transformedProps: { + refs: Refs; + width: number; + height: number; + echartOptions: EChartsCoreOption; + formData: Record; + }; +} + +// ============================================================================ +// Helper Functions +// ============================================================================ + +function formatTooltip({ + params, + metricLabel, +}: { + params: TreeSeriesCallbackDataParams; + metricLabel: string; +}): string { + const { value, treeAncestors } = params; + const treePath = (treeAncestors ?? []) + .map(pathInfo => pathInfo?.name || '') + .filter(path => path !== ''); + const row = value ? [metricLabel, String(value)] : []; + return tooltipHtml([row], treePath.join(' ▸ ')); +} + +// ============================================================================ +// Build Query +// ============================================================================ + +export function buildQuery(formData: QueryFormData) { + return buildQueryContext(formData, { + queryFields: { + id: 'columns', + parent: 'columns', + name: 'columns', + }, + }); +} + +// ============================================================================ +// Chart Definition +// ============================================================================ + +export default defineChart({ + metadata: { + name: t('Tree Chart'), + description: t( + 'Visualize multiple levels of hierarchy using a familiar tree-like structure.', + ), + category: t('Part of a Whole'), + tags: [ + t('Categorical'), + t('ECharts'), + t('Multi-Levels'), + t('Relational'), + t('Structural'), + t('Featured'), + ], + credits: ['https://echarts.apache.org'], + thumbnail, + thumbnailDark, + exampleGallery: [{ url: example, urlDark: exampleDark }], + }, + + arguments: { + // Chart Options + layout: RadioButton.with({ + label: t('Tree layout'), + description: t('Layout type of tree'), + options: LAYOUT_OPTIONS, + default: DEFAULT_FORM_DATA.layout, + }), + + orient: { + arg: RadioButton.with({ + label: t('Tree orientation'), + description: t('Orientation of tree'), + options: ORIENT_OPTIONS, + default: DEFAULT_FORM_DATA.orient, + }), + visibleWhen: { layout: 'orthogonal' }, + }, + + nodeLabelPosition: RadioButton.with({ + label: t('Node label position'), + description: t('Position of intermediate node label on tree'), + options: POSITION_OPTIONS, + default: DEFAULT_FORM_DATA.nodeLabelPosition, + }), + + childLabelPosition: RadioButton.with({ + label: t('Child label position'), + description: t('Position of child node label on tree'), + options: POSITION_OPTIONS, + default: DEFAULT_FORM_DATA.childLabelPosition, + }), + + emphasis: { + arg: RadioButton.with({ + label: t('Emphasis'), + description: t('Which relatives to highlight on hover'), + options: EMPHASIS_OPTIONS, + default: DEFAULT_FORM_DATA.emphasis, + }), + visibleWhen: { layout: 'orthogonal' }, + }, + + symbol: Select.with({ + label: t('Symbol'), + description: t('Shape of node symbol'), + options: SYMBOL_OPTIONS, + default: DEFAULT_FORM_DATA.symbol, + }), + + symbolSize: Int.with({ + label: t('Symbol size'), + description: t('Size of edge symbols'), + default: DEFAULT_FORM_DATA.symbolSize, + min: 5, + max: 30, + step: 2, + }), + + roam: Select.with({ + label: t('Enable graph roaming'), + description: t('Whether to enable changing graph position and scaling.'), + options: ROAM_OPTIONS, + default: 'true', + }), + + initialTreeDepth: Int.with({ + label: t('Initial tree depth'), + description: t( + 'The initial level (depth) of the tree. If set as -1 all nodes are expanded.', + ), + default: DEFAULT_FORM_DATA.initialTreeDepth, + min: -1, + max: 10, + step: 1, + }), + + // Metric for node values + metric: Metric.with({ + label: t('Metric'), + description: t('Metric for node values'), + multi: false, + }), + + rootNodeId: Text.with({ + label: t('Root node id'), + description: t('Id of root node of the tree.'), + default: '', + }), + }, + + // Entity controls need special handling + additionalControls: { + query: [ + [ + { + name: 'id', + config: { + ...sharedControls.entity, + clearable: false, + label: t('Id'), + description: t('Name of the id column'), + }, + }, + ], + [ + { + name: 'parent', + config: { + ...sharedControls.entity, + clearable: false, + label: t('Parent'), + description: t( + 'Name of the column containing the id of the parent node', + ), + }, + }, + ], + [ + { + name: 'name', + config: { + ...sharedControls.entity, + clearable: true, + validators: [], + label: t('Name'), + description: t('Optional name of the data column.'), + }, + }, + ], + ], + }, + + buildQuery, + + transform: (chartProps: ChartProps): TreeTransformResult => { + const { height, width, queriesData, rawFormData, theme } = chartProps; + + const refs: Refs = {}; + const data: TreeDataRecord[] = + (queriesData[0]?.data as TreeDataRecord[]) || []; + + // Extract form values + const id = rawFormData.id as string; + const parent = rawFormData.parent as string; + const name = rawFormData.name as string; + const metric = (rawFormData.metric as string) || ''; + const rootNodeId = rawFormData.root_node_id as string | undefined; + const layout = (rawFormData.layout as string) || DEFAULT_FORM_DATA.layout; + const orient = (rawFormData.orient as string) || DEFAULT_FORM_DATA.orient; + const symbol = (rawFormData.symbol as string) || DEFAULT_FORM_DATA.symbol; + const symbolSize = + (rawFormData.symbol_size as number) || DEFAULT_FORM_DATA.symbolSize; + const roamValue = rawFormData.roam as string | boolean; + const nodeLabelPosition = + (rawFormData.node_label_position as string) || + DEFAULT_FORM_DATA.nodeLabelPosition; + const childLabelPosition = + (rawFormData.child_label_position as string) || + DEFAULT_FORM_DATA.childLabelPosition; + const emphasis = + (rawFormData.emphasis as string) || DEFAULT_FORM_DATA.emphasis; + const initialTreeDepth = + (rawFormData.initial_tree_depth as number) ?? + DEFAULT_FORM_DATA.initialTreeDepth; + + // Parse roam value + let roam: boolean | 'scale' | 'move' = true; + if (roamValue === 'false' || roamValue === false) { + roam = false; + } else if (roamValue === 'scale') { + roam = 'scale'; + } else if (roamValue === 'move') { + roam = 'move'; + } else { + roam = true; + } + + const metricLabel = getMetricLabel(metric); + const nameColumn = name || id; + + function findNodeName(nodeId: DataRecordValue): OptionName { + let nodeName: DataRecordValue = ''; + data.some(node => { + if (node[id]?.toString() === nodeId) { + nodeName = node[nameColumn]; + return true; + } + return false; + }); + return nodeName; + } + + function getTotalChildren(tree: TreeSeriesNodeItemOption): number { + let totalChildren = 0; + function traverse(node: TreeSeriesNodeItemOption) { + (node.children || []).forEach(child => { + traverse(child); + }); + totalChildren += 1; + } + traverse(tree); + return totalChildren; + } + + function createTree(nodeId: DataRecordValue): TreeSeriesNodeItemOption { + const rootNodeName = findNodeName(nodeId); + const tree: TreeSeriesNodeItemOption = { + name: rootNodeName, + children: [], + }; + const children: TreeSeriesNodeItemOption[][] = []; + const indexMap: { [name: string]: number } = {}; + + if (!rootNodeName) { + return tree; + } + + // Index map with node ids + for (let i = 0; i < data.length; i += 1) { + const nodeIdVal = data[i][id] as number; + indexMap[nodeIdVal] = i; + children[i] = []; + } + + // Generate tree + for (let i = 0; i < data.length; i += 1) { + const node = data[i]; + if (node[parent]?.toString() === nodeId) { + tree.children?.push({ + name: node[nameColumn], + children: children[i], + value: node[metricLabel], + }); + } else { + const parentId = node[parent]; + if (data[indexMap[parentId as string]]) { + const parentIndex = indexMap[parentId as string]; + children[parentIndex].push({ + name: node[nameColumn], + children: children[i], + value: node[metricLabel], + }); + } + } + } + + return tree; + } + + let finalTree: TreeSeriesNodeItemOption = { name: '', children: [] }; + + if (rootNodeId) { + finalTree = createTree(rootNodeId); + } else { + // Auto-select root node + const parentChildMap: { [name: string]: { id: unknown }[] } = {}; + data.forEach(node => { + const parentId = node[parent] as string; + if (parentId in parentChildMap) { + parentChildMap[parentId].push({ id: node[id] }); + } else { + parentChildMap[parentId] = [{ id: node[id] }]; + } + }); + + let maxChildren = 0; + Object.keys(parentChildMap).forEach(key => { + if (parentChildMap[key].length === 1) { + const tree = createTree(parentChildMap[key][0].id as string); + const totalChildren = getTotalChildren(tree); + if (totalChildren > maxChildren) { + maxChildren = totalChildren; + finalTree = tree; + } + } + }); + } + + const series: TreeSeriesOption[] = [ + { + type: 'tree', + data: [finalTree], + label: { + ...DEFAULT_TREE_SERIES_OPTION.label, + position: nodeLabelPosition as 'left' | 'right' | 'top' | 'bottom', + color: theme.colorText, + }, + emphasis: { focus: emphasis as 'ancestor' | 'descendant' }, + animation: DEFAULT_TREE_SERIES_OPTION.animation, + layout: layout as 'orthogonal' | 'radial', + orient: orient as 'LR' | 'RL' | 'TB' | 'BT', + symbol, + roam, + symbolSize, + lineStyle: { + color: theme.colorText, + width: 1.5, + }, + select: { + itemStyle: { + borderColor: theme.colorText, + }, + }, + leaves: { + label: { + position: childLabelPosition as 'left' | 'right' | 'top' | 'bottom', + }, + }, + initialTreeDepth, + }, + ]; + + const echartOptions: EChartsCoreOption = { + animationDuration: DEFAULT_TREE_SERIES_OPTION.animationDuration, + animationEasing: DEFAULT_TREE_SERIES_OPTION.animationEasing, + series, + tooltip: { + ...getDefaultTooltip(refs), + trigger: 'item', + triggerOn: 'mousemove', + formatter: (params: unknown) => + formatTooltip({ + params: params as TreeSeriesCallbackDataParams, + metricLabel, + }), + }, + }; + + return { + transformedProps: { + refs, + height, + width, + echartOptions, + formData: rawFormData, + }, + }; + }, + + render: ({ transformedProps }) => { + const { height, width, echartOptions, refs, formData } = transformedProps; + + return ( + + ); + }, +}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Tree/stories/Tree.stories.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Tree/stories/Tree.stories.tsx index fb6155f3bfb..a77433fcdad 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Tree/stories/Tree.stories.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Tree/stories/Tree.stories.tsx @@ -17,21 +17,13 @@ * under the License. */ -import { SuperChart, getChartTransformPropsRegistry } from '@superset-ui/core'; -import { - EchartsTreeChartPlugin, - TreeTransformProps, -} from '@superset-ui/plugin-chart-echarts'; +import { SuperChart } from '@superset-ui/core'; +import { EchartsTreeChartPlugin } from '@superset-ui/plugin-chart-echarts'; import data from './data'; import { withResizableChartDemo } from '@storybook-shared'; new EchartsTreeChartPlugin().configure({ key: 'echarts-tree' }).register(); -getChartTransformPropsRegistry().registerValue( - 'echarts-tree', - TreeTransformProps, -); - export default { title: 'Chart Plugins/plugin-chart-echarts/Tree', decorators: [withResizableChartDemo], diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Tree/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Tree/transformProps.ts deleted file mode 100644 index b44f603a2b0..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Tree/transformProps.ts +++ /dev/null @@ -1,240 +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 { - getMetricLabel, - DataRecordValue, - tooltipHtml, -} from '@superset-ui/core'; -import type { EChartsCoreOption } from 'echarts/core'; -import type { TreeSeriesOption } from 'echarts/charts'; -import type { - TreeSeriesCallbackDataParams, - TreeSeriesNodeItemOption, -} from 'echarts/types/src/chart/tree/TreeSeries'; -import type { OptionName } from 'echarts/types/src/util/types'; -import { - EchartsTreeChartProps, - EchartsTreeFormData, - TreeDataRecord, - TreeTransformedProps, -} from './types'; -import { DEFAULT_FORM_DATA, DEFAULT_TREE_SERIES_OPTION } from './constants'; -import { Refs } from '../types'; -import { getDefaultTooltip } from '../utils/tooltip'; - -export function formatTooltip({ - params, - metricLabel, -}: { - params: TreeSeriesCallbackDataParams; - metricLabel: string; -}): string { - const { value, treeAncestors } = params; - const treePath = (treeAncestors ?? []) - .map(pathInfo => pathInfo?.name || '') - .filter(path => path !== ''); - const row = value ? [metricLabel, String(value)] : []; - return tooltipHtml([row], treePath.join(' ▸ ')); -} - -export default function transformProps( - chartProps: EchartsTreeChartProps, -): TreeTransformedProps { - const { width, height, formData, queriesData, theme, isRefreshing } = - chartProps; - const refs: Refs = {}; - const data: TreeDataRecord[] = queriesData[0].data || []; - - const { - id, - parent, - name, - metric = '', - rootNodeId, - layout, - orient, - symbol, - symbolSize, - roam, - nodeLabelPosition, - childLabelPosition, - emphasis, - initialTreeDepth, - }: EchartsTreeFormData = { ...DEFAULT_FORM_DATA, ...formData }; - const metricLabel = getMetricLabel(metric); - - const nameColumn = name || id; - - function findNodeName(rootNodeId: DataRecordValue): OptionName { - let nodeName: DataRecordValue = ''; - data.some(node => { - if (node[id]!.toString() === rootNodeId) { - nodeName = node[nameColumn]; - return true; - } - return false; - }); - return nodeName; - } - - function getTotalChildren(tree: TreeSeriesNodeItemOption) { - let totalChildren = 0; - - function traverse(tree: TreeSeriesNodeItemOption) { - tree.children!.forEach(node => { - traverse(node); - }); - totalChildren += 1; - } - traverse(tree); - return totalChildren; - } - - function createTree(rootNodeId: DataRecordValue): TreeSeriesNodeItemOption { - const rootNodeName = findNodeName(rootNodeId); - const tree: TreeSeriesNodeItemOption = { name: rootNodeName, children: [] }; - const children: TreeSeriesNodeItemOption[][] = []; - const indexMap: { [name: string]: number } = {}; - - if (!rootNodeName) { - return tree; - } - - // index indexMap with node ids - for (let i = 0; i < data.length; i += 1) { - const nodeId = data[i][id] as number; - indexMap[nodeId] = i; - children[i] = []; - } - - // generate tree - for (let i = 0; i < data.length; i += 1) { - const node = data[i]; - if (node[parent]?.toString() === rootNodeId) { - tree.children?.push({ - name: node[nameColumn], - children: children[i], - value: node[metricLabel], - }); - } else { - const parentId = node[parent]; - if (data[indexMap[parentId]]) { - const parentIndex = indexMap[parentId]; - children[parentIndex].push({ - name: node[nameColumn], - children: children[i], - value: node[metricLabel], - }); - } - } - } - - return tree; - } - - let finalTree = {}; - - if (rootNodeId) { - finalTree = createTree(rootNodeId); - } else { - /* - to select root node, - 1.find parent nodes with only 1 child. - 2.build tree for each such child nodes as root - 3.select tree with most children - */ - // create map of parent:children - const parentChildMap: { [name: string]: { [name: string]: any } } = {}; - data.forEach(node => { - const parentId = node[parent] as string; - if (parentId in parentChildMap) { - parentChildMap[parentId].push({ id: node[id] }); - } else { - parentChildMap[parentId] = [{ id: node[id] }]; - } - }); - - // for each parent node which has only 1 child,find tree and select node with max number of children. - let maxChildren = 0; - Object.keys(parentChildMap).forEach(key => { - if (parentChildMap[key].length === 1) { - const tree = createTree(parentChildMap[key][0].id); - const totalChildren = getTotalChildren(tree); - if (totalChildren > maxChildren) { - maxChildren = totalChildren; - finalTree = tree; - } - } - }); - } - // Disable animation during refresh to prevent expand/collapse layout animation - const seriesAnimation = isRefreshing - ? false - : DEFAULT_TREE_SERIES_OPTION.animation; - - const series: TreeSeriesOption[] = [ - { - type: 'tree', - data: [finalTree], - label: { - ...DEFAULT_TREE_SERIES_OPTION.label, - position: nodeLabelPosition, - color: theme.colorText, - }, - emphasis: { focus: emphasis }, - animation: seriesAnimation, - layout, - orient, - symbol, - roam, - symbolSize, - lineStyle: { - color: theme.colorText, - width: 1.5, - }, - select: DEFAULT_TREE_SERIES_OPTION.select, - leaves: { label: { position: childLabelPosition } }, - initialTreeDepth, - }, - ]; - - const echartOptions: EChartsCoreOption = { - animationDuration: DEFAULT_TREE_SERIES_OPTION.animationDuration, - animationEasing: DEFAULT_TREE_SERIES_OPTION.animationEasing, - series, - tooltip: { - ...getDefaultTooltip(refs), - trigger: 'item', - triggerOn: 'mousemove', - formatter: (params: any) => - formatTooltip({ - params, - metricLabel, - }), - }, - }; - - return { - formData, - width, - height, - echartOptions, - refs, - }; -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Tree/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Tree/types.ts deleted file mode 100644 index 8d4858a5eba..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Tree/types.ts +++ /dev/null @@ -1,55 +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 type { OptionName } from 'echarts/types/src/util/types'; -import type { TreeSeriesNodeItemOption } from 'echarts/types/src/chart/tree/TreeSeries'; -import { ChartDataResponseResult, QueryFormData } from '@superset-ui/core'; -import { BaseChartProps, BaseTransformedProps } from '../types'; - -export type EchartsTreeFormData = QueryFormData & { - id: string; - parent: string; - name: string; - rootNodeId?: string | number; - orient: 'LR' | 'RL' | 'TB' | 'BT'; - symbol: string; - symbolSize: number; - colorScheme?: string; - metric?: string; - layout: 'orthogonal' | 'radial'; - roam: boolean | 'scale' | 'move'; - nodeLabelPosition: 'top' | 'bottom' | 'left' | 'right'; - childLabelPosition: 'top' | 'bottom' | 'left' | 'right'; - emphasis: 'none' | 'ancestor' | 'descendant'; - initialTreeDepth: number; -}; - -export interface TreeChartDataResponseResult extends ChartDataResponseResult { - data: TreeDataRecord[]; -} - -export interface EchartsTreeChartProps extends BaseChartProps { - formData: EchartsTreeFormData; - queriesData: TreeChartDataResponseResult[]; -} - -export type TreeDataRecord = Record & { - children?: TreeSeriesNodeItemOption[]; -}; - -export type TreeTransformedProps = BaseTransformedProps; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/EchartsTreemap.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/EchartsTreemap.tsx deleted file mode 100644 index 57fdef6aaae..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/EchartsTreemap.tsx +++ /dev/null @@ -1,169 +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 { - DataRecordValue, - BinaryQueryObjectFilterClause, - getTimeFormatter, - getColumnLabel, - getNumberFormatter, -} from '@superset-ui/core'; -import { useCallback } from 'react'; -import Echart from '../components/Echart'; -import { NULL_STRING } from '../constants'; -import { EventHandlers, TreePathInfo } from '../types'; -import { extractTreePathInfo } from './constants'; -import { TreemapTransformedProps } from './types'; -import { formatSeriesName } from '../utils/series'; - -export default function EchartsTreemap({ - echartOptions, - emitCrossFilters, - groupby, - height, - labelMap, - onContextMenu, - refs, - setDataMask, - selectedValues, - width, - formData, - coltypeMapping, -}: TreemapTransformedProps) { - const getCrossFilterDataMask = useCallback( - (data: Record, treePathInfo: TreePathInfo[]) => { - if (data?.children) { - return undefined; - } - const { treePath } = extractTreePathInfo(treePathInfo); - const name = treePath.join(','); - const selected = Object.values(selectedValues); - let values: string[]; - if (selected.includes(name)) { - values = selected.filter(v => v !== name); - } else { - values = [name]; - } - - const groupbyValues = values.map(value => labelMap[value]); - - return { - dataMask: { - extraFormData: { - filters: - values.length === 0 - ? [] - : groupby.map((col, idx) => { - const val: DataRecordValue[] = groupbyValues.map( - v => v[idx], - ); - if (val === null || val === undefined) - return { - col, - op: 'IS NULL' as const, - }; - return { - col, - op: 'IN' as const, - val: val as (string | number | boolean)[], - }; - }), - }, - filterState: { - value: groupbyValues.length ? groupbyValues : null, - selectedValues: values.length ? values : null, - }, - }, - isCurrentValueSelected: selected.includes(name), - }; - }, - [groupby, labelMap, selectedValues], - ); - - const handleChange = useCallback( - (data: Record, treePathInfo: TreePathInfo[]) => { - if (!emitCrossFilters || groupby.length === 0) { - return; - } - - const dataMask = getCrossFilterDataMask(data, treePathInfo)?.dataMask; - if (dataMask) { - setDataMask(dataMask); - } - }, - [emitCrossFilters, getCrossFilterDataMask, setDataMask, groupby.length], - ); - - const eventHandlers: EventHandlers = { - click: props => { - const { data, treePathInfo } = props; - handleChange(data, treePathInfo); - }, - contextmenu: async eventParams => { - if (onContextMenu) { - eventParams.event.stop(); - const { data, treePathInfo } = eventParams; - const { treePath } = extractTreePathInfo(treePathInfo); - if (treePath.length > 0) { - const pointerEvent = eventParams.event.event; - const drillToDetailFilters: BinaryQueryObjectFilterClause[] = []; - const drillByFilters: BinaryQueryObjectFilterClause[] = []; - treePath.forEach((path, i) => { - const val = path === 'null' ? NULL_STRING : path; - drillToDetailFilters.push({ - col: groupby[i], - op: '==', - val, - formattedVal: path, - }); - drillByFilters.push({ - col: groupby[i], - op: '==', - val, - formattedVal: formatSeriesName(val, { - timeFormatter: getTimeFormatter(formData.dateFormat), - numberFormatter: getNumberFormatter(formData.numberFormat), - coltype: coltypeMapping?.[getColumnLabel(groupby[i])], - }), - }); - }); - onContextMenu(pointerEvent.clientX, pointerEvent.clientY, { - drillToDetail: drillToDetailFilters, - crossFilter: - groupby.length > 0 - ? getCrossFilterDataMask(data, treePathInfo) - : undefined, - drillBy: { filters: drillByFilters, groupbyFieldName: 'groupby' }, - }); - } - } - }, - }; - - return ( - - ); -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/buildQuery.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/buildQuery.ts deleted file mode 100644 index e6e76ac7ad3..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/buildQuery.ts +++ /dev/null @@ -1,40 +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 { - buildQueryContext, - QueryFormData, - QueryFormOrderBy, -} from '@superset-ui/core'; -import { buildColumnsOrderBy, applyOrderBy } from '../utils/orderby'; - -export default function buildQuery(formData: QueryFormData) { - const { metric, sort_by_metric, groupby = [], row_limit } = formData; - const orderby: QueryFormOrderBy[] = []; - if (sort_by_metric && metric) { - orderby.push([metric, false]); - } - orderby.push(...buildColumnsOrderBy(groupby)); - - return buildQueryContext(formData, baseQueryObject => [ - { - ...baseQueryObject, - ...applyOrderBy(orderby, row_limit), - }, - ]); -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/constants.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/constants.ts deleted file mode 100644 index 72dc714c78d..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/constants.ts +++ /dev/null @@ -1,37 +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 { TreePathInfo } from '../types'; - -export const COLOR_SATURATION = [0.7, 0.4]; -export const LABEL_FONTSIZE = 11; -export const BORDER_WIDTH = 2; -export const GAP_WIDTH = 2; - -export const extractTreePathInfo = ( - treePathInfo: TreePathInfo[] | undefined, -) => { - const treePath = (treePathInfo ?? []) - .map(pathInfo => pathInfo?.name || '') - .filter(path => path !== ''); - - // the 1st tree path is metric label - const metricLabel = treePath.shift() || ''; - return { metricLabel, treePath }; -}; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/controlPanel.tsx deleted file mode 100644 index aaee57adfa8..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/controlPanel.tsx +++ /dev/null @@ -1,133 +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 { t } from '@apache-superset/core/translation'; -import { - ControlPanelConfig, - ControlSubSectionHeader, - D3_FORMAT_DOCS, - D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT, - D3_FORMAT_OPTIONS, - D3_TIME_FORMAT_OPTIONS, - getStandardizedControls, -} from '@superset-ui/chart-controls'; -import { DEFAULT_FORM_DATA } from './types'; - -const { labelType, numberFormat, showLabels, showUpperLabels, dateFormat } = - DEFAULT_FORM_DATA; - -const config: ControlPanelConfig = { - controlPanelSections: [ - { - label: t('Query'), - expanded: true, - controlSetRows: [ - ['groupby'], - ['metric'], - ['row_limit'], - ['sort_by_metric'], - ['adhoc_filters'], - ], - }, - { - label: t('Chart Options'), - expanded: true, - controlSetRows: [ - ['color_scheme'], - [{t('Labels')}], - [ - { - name: 'show_labels', - config: { - type: 'CheckboxControl', - label: t('Show Labels'), - renderTrigger: true, - default: showLabels, - description: t('Whether to display the labels.'), - }, - }, - ], - [ - { - name: 'show_upper_labels', - config: { - type: 'CheckboxControl', - label: t('Show Upper Labels'), - renderTrigger: true, - default: showUpperLabels, - description: t('Show labels when the node has children.'), - }, - }, - ], - [ - { - name: 'label_type', - config: { - type: 'SelectControl', - label: t('Label Type'), - default: labelType, - renderTrigger: true, - choices: [ - ['Key', t('Key')], - ['value', t('Value')], - ['key_value', t('Category and Value')], - ], - description: t('What should be shown on the label?'), - }, - }, - ], - [ - { - name: 'number_format', - config: { - type: 'SelectControl', - freeForm: true, - label: t('Number format'), - renderTrigger: true, - default: numberFormat, - choices: D3_FORMAT_OPTIONS, - description: `${D3_FORMAT_DOCS} ${D3_NUMBER_FORMAT_DESCRIPTION_VALUES_TEXT}`, - }, - }, - ], - ['currency_format'], - [ - { - name: 'date_format', - config: { - type: 'SelectControl', - freeForm: true, - label: t('Date format'), - renderTrigger: true, - choices: D3_TIME_FORMAT_OPTIONS, - default: dateFormat, - description: D3_FORMAT_DOCS, - }, - }, - ], - ], - }, - ], - formDataOverrides: formData => ({ - ...formData, - metric: getStandardizedControls().shiftMetric(), - groupby: getStandardizedControls().popAllColumns(), - }), -}; - -export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/index.ts deleted file mode 100644 index 57264b20f1e..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/index.ts +++ /dev/null @@ -1,84 +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 - * g 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 { t } from '@apache-superset/core/translation'; -import { Behavior } from '@superset-ui/core'; -import buildQuery from './buildQuery'; -import controlPanel from './controlPanel'; -import transformProps from './transformProps'; -import thumbnail from './images/thumbnail.png'; -import thumbnailDark from './images/thumbnail-dark.png'; -import example1 from './images/treemap_v2_1.png'; -import example1Dark from './images/treemap_v2_1-dark.png'; -import example2 from './images/treemap_v2_2.jpg'; -import example2Dark from './images/treemap_v2_2-dark.jpg'; -import { EchartsTreemapChartProps, EchartsTreemapFormData } from './types'; -import { EchartsChartPlugin } from '../types'; - -export default class EchartsTreemapChartPlugin extends EchartsChartPlugin< - EchartsTreemapFormData, - EchartsTreemapChartProps -> { - /** - * The constructor is used to pass relevant metadata and callbacks that get - * registered in respective registries that are used throughout the library - * and application. A more thorough description of each property is given in - * the respective imported file. - * - * It is worth noting that `buildQuery` and is optional, and only needed for - * advanced visualizations that require either post processing operations - * (pivoting, rolling aggregations, sorting etc) or submitting multiple queries. - */ - constructor() { - super({ - buildQuery, - controlPanel, - loadChart: () => import('./EchartsTreemap'), - metadata: { - behaviors: [ - Behavior.InteractiveChart, - Behavior.DrillToDetail, - Behavior.DrillBy, - ], - category: t('Part of a Whole'), - credits: ['https://echarts.apache.org'], - description: t( - 'Show hierarchical relationships of data, with the value represented by area, showing proportion and contribution to the whole.', - ), - exampleGallery: [ - { url: example1, urlDark: example1Dark }, - { url: example2, urlDark: example2Dark }, - ], - name: t('Treemap'), - tags: [ - t('Categorical'), - t('Comparison'), - t('ECharts'), - t('Multi-Levels'), - t('Percentages'), - t('Proportional'), - t('Featured'), - ], - thumbnail, - thumbnailDark, - }, - transformProps, - }); - } -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/index.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/index.tsx new file mode 100644 index 00000000000..d96b1e0f235 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/index.tsx @@ -0,0 +1,685 @@ +/** + * 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. + */ + +/** + * ECharts Treemap Chart - Glyph Pattern Implementation + * + * Show hierarchical relationships of data, with the value represented by area, + * showing proportion and contribution to the whole. + */ + +import { useCallback } from 'react'; +import { t } from '@apache-superset/core/translation'; +import type { EChartsCoreOption } from 'echarts/core'; +import type { TreemapSeriesNodeItemOption } from 'echarts/types/src/chart/treemap/TreemapSeries'; +import type { TreemapSeriesOption } from 'echarts/charts'; +import type { CallbackDataParams } from 'echarts/types/src/util/types'; +import { + Behavior, + BinaryQueryObjectFilterClause, + buildQueryContext, + CategoricalColorNamespace, + DataRecord, + DataRecordValue, + getColumnLabel, + getMetricLabel, + getNumberFormatter, + getTimeFormatter, + getValueFormatter, + NumberFormats, + QueryFormColumn, + QueryFormData, + QueryFormMetric, + SetDataMaskHook, + ContextMenuFilters, + tooltipHtml, + ValueFormatter, +} from '@superset-ui/core'; +import { getStandardizedControls } from '@superset-ui/chart-controls'; + +import { + defineChart, + Metric, + Dimension, + Checkbox, + ChartProps, + SimpleLabelType, + ShowLabels, +} from '@superset-ui/glyph-core'; + +import { formatSeriesName, getColtypesMapping } from '../utils/series'; +import { treeBuilder, TreeNode } from '../utils/treeBuilder'; +import { NULL_STRING, OpacityEnum } from '../constants'; +import { getDefaultTooltip } from '../utils/tooltip'; +import Echart from '../components/Echart'; +import { EventHandlers, LabelPositionEnum, Refs, TreePathInfo } from '../types'; + +import thumbnail from './images/thumbnail.png'; +import thumbnailDark from './images/thumbnail-dark.png'; +import example1 from './images/treemap_v2_1.png'; +import example1Dark from './images/treemap_v2_1-dark.png'; +import example2 from './images/treemap_v2_2.jpg'; +import example2Dark from './images/treemap_v2_2-dark.jpg'; + +// ============================================================================ +// Types +// ============================================================================ + +enum EchartsTreemapLabelType { + Key = 'key', + Value = 'value', + KeyValue = 'key_value', +} + +interface TreemapSeriesCallbackDataParams extends CallbackDataParams { + treePathInfo?: TreePathInfo[]; +} + +interface TreemapTransformResult { + transformedProps: { + refs: Refs; + width: number; + height: number; + echartOptions: EChartsCoreOption; + formData: Record; + groupby: QueryFormColumn[]; + labelMap: Record; + setDataMask: SetDataMaskHook; + selectedValues: string[]; + emitCrossFilters?: boolean; + onContextMenu?: ( + clientX: number, + clientY: number, + filters?: ContextMenuFilters, + ) => void; + coltypeMapping?: Record; + }; +} + +// ============================================================================ +// Constants +// ============================================================================ + +const COLOR_SATURATION = [0.7, 0.4]; +const LABEL_FONTSIZE = 11; +const BORDER_WIDTH = 2; +const GAP_WIDTH = 2; + +const DEFAULT_FORM_DATA = { + groupby: [], + labelType: EchartsTreemapLabelType.KeyValue, + labelPosition: LabelPositionEnum.InsideTopLeft, + numberFormat: 'SMART_NUMBER', + showLabels: true, + showUpperLabels: true, + dateFormat: 'smart_date', +}; + +// ============================================================================ +// Helpers +// ============================================================================ + +function extractTreePathInfo(treePathInfo: TreePathInfo[] | undefined) { + const treePath = (treePathInfo ?? []) + .map(pathInfo => pathInfo?.name || '') + .filter(path => path !== ''); + + const metricLabel = treePath.shift() || ''; + return { metricLabel, treePath }; +} + +function formatLabel({ + params, + labelType, + numberFormatter, +}: { + params: TreemapSeriesCallbackDataParams; + labelType: EchartsTreemapLabelType; + numberFormatter: ValueFormatter; +}): string { + const { name = '', value } = params; + const formattedValue = numberFormatter(value as number); + + switch (labelType) { + case EchartsTreemapLabelType.Key: + return name; + case EchartsTreemapLabelType.Value: + return formattedValue; + case EchartsTreemapLabelType.KeyValue: + return `${name}: ${formattedValue}`; + default: + return name; + } +} + +function formatTooltip({ + params, + numberFormatter, +}: { + params: TreemapSeriesCallbackDataParams; + numberFormatter: ValueFormatter; +}): string { + const { value, treePathInfo = [] } = params; + const formattedValue = numberFormatter(value as number); + const { metricLabel, treePath } = extractTreePathInfo(treePathInfo); + const percentFormatter = getNumberFormatter(NumberFormats.PERCENT_2_POINT); + + let formattedPercent = ''; + const currentNode = treePathInfo[treePathInfo.length - 1]; + const parentNode = treePathInfo[treePathInfo.length - 2]; + if (parentNode) { + const percent: number = parentNode.value + ? (currentNode.value as number) / (parentNode.value as number) + : 0; + formattedPercent = percentFormatter(percent); + } + const row = [metricLabel, formattedValue]; + if (formattedPercent) { + row.push(formattedPercent); + } + return tooltipHtml([row], treePath.join(' ▸ ')); +} + +// ============================================================================ +// Render Component +// ============================================================================ + +function TreemapRender({ + transformedProps, +}: { + transformedProps: TreemapTransformResult['transformedProps']; +}) { + const { + echartOptions, + emitCrossFilters, + groupby, + height, + labelMap, + onContextMenu, + refs, + setDataMask, + selectedValues, + width, + formData, + coltypeMapping, + } = transformedProps; + + const getCrossFilterDataMask = useCallback( + ( + data: { children?: unknown }, + treePathInfo: TreePathInfo[] | undefined, + ) => { + if (data?.children) { + return undefined; + } + const { treePath } = extractTreePathInfo(treePathInfo); + const name = treePath.join(','); + const selected = Object.values(selectedValues); + let values: string[]; + if (selected.includes(name)) { + values = selected.filter(v => v !== name); + } else { + values = [name]; + } + + const groupbyValues = values.map(value => labelMap[value]); + + return { + dataMask: { + extraFormData: { + filters: + values.length === 0 + ? [] + : groupby.map((col, idx) => { + const val: DataRecordValue[] = groupbyValues.map( + v => v[idx], + ); + if (val === null || val === undefined) + return { + col, + op: 'IS NULL' as const, + }; + return { + col, + op: 'IN' as const, + val: val as (string | number | boolean)[], + }; + }), + }, + filterState: { + value: groupbyValues.length ? groupbyValues : null, + selectedValues: values.length ? values : null, + }, + }, + isCurrentValueSelected: selected.includes(name), + }; + }, + [groupby, labelMap, selectedValues], + ); + + const handleChange = useCallback( + ( + data: { children?: unknown }, + treePathInfo: TreePathInfo[] | undefined, + ) => { + if (!emitCrossFilters || groupby.length === 0) { + return; + } + + const dataMask = getCrossFilterDataMask(data, treePathInfo)?.dataMask; + if (dataMask) { + setDataMask(dataMask); + } + }, + [emitCrossFilters, getCrossFilterDataMask, groupby.length, setDataMask], + ); + + const eventHandlers: EventHandlers = { + click: (props: { + data: { children?: unknown }; + treePathInfo: TreePathInfo[]; + }) => { + const { data, treePathInfo } = props; + handleChange(data, treePathInfo); + }, + contextmenu: async (eventParams: { + event: { stop: () => void; event: { clientX: number; clientY: number } }; + data: { children?: unknown }; + treePathInfo: TreePathInfo[]; + }) => { + if (onContextMenu) { + eventParams.event.stop(); + const { data, treePathInfo } = eventParams; + const { treePath } = extractTreePathInfo(treePathInfo); + if (treePath.length > 0) { + const pointerEvent = eventParams.event.event; + const drillToDetailFilters: BinaryQueryObjectFilterClause[] = []; + const drillByFilters: BinaryQueryObjectFilterClause[] = []; + treePath.forEach((path, i) => { + const val = path === 'null' ? NULL_STRING : path; + drillToDetailFilters.push({ + col: groupby[i], + op: '==', + val, + formattedVal: path, + }); + drillByFilters.push({ + col: groupby[i], + op: '==', + val, + formattedVal: formatSeriesName(val, { + timeFormatter: getTimeFormatter(formData.date_format as string), + numberFormatter: getNumberFormatter( + formData.number_format as string, + ), + coltype: coltypeMapping?.[getColumnLabel(groupby[i])], + }), + }); + }); + onContextMenu(pointerEvent.clientX, pointerEvent.clientY, { + drillToDetail: drillToDetailFilters, + crossFilter: + groupby.length > 0 + ? getCrossFilterDataMask(data, treePathInfo) + : undefined, + drillBy: { filters: drillByFilters, groupbyFieldName: 'groupby' }, + }); + } + } + }, + }; + + return ( + + ); +} + +// ============================================================================ +// Build Query +// ============================================================================ + +export function buildQuery(formData: QueryFormData) { + const { metric } = formData; + const sortByMetric = formData.sort_by_metric; + + return buildQueryContext(formData, baseQueryObject => [ + { + ...baseQueryObject, + ...(sortByMetric && { orderby: [[metric, false]] }), + }, + ]); +} + +// ============================================================================ +// Chart Definition +// ============================================================================ + +export default defineChart({ + metadata: { + name: t('Treemap'), + description: t( + 'Show hierarchical relationships of data, with the value represented by area, showing proportion and contribution to the whole.', + ), + category: t('Part of a Whole'), + tags: [ + t('Categorical'), + t('Comparison'), + t('ECharts'), + t('Multi-Levels'), + t('Percentages'), + t('Proportional'), + t('Featured'), + ], + behaviors: [ + Behavior.InteractiveChart, + Behavior.DrillToDetail, + Behavior.DrillBy, + ], + credits: ['https://echarts.apache.org'], + thumbnail, + thumbnailDark, + exampleGallery: [ + { url: example1, urlDark: example1Dark }, + { url: example2, urlDark: example2Dark }, + ], + }, + + arguments: { + groupby: Dimension.with({ + label: t('Dimensions'), + description: t('Columns to group by on the rows'), + multi: true, + }), + + metric: Metric.with({ + label: t('Metric'), + description: t('Metric used to calculate the area of each rectangle'), + multi: false, + }), + + showLabels: ShowLabels, + + showUpperLabels: Checkbox.with({ + label: t('Show Upper Labels'), + description: t('Show labels when the node has children.'), + default: true, + }), + + labelType: SimpleLabelType.with({ + default: 'key_value', + }), + }, + + additionalControls: { + query: [['row_limit'], ['sort_by_metric'], ['adhoc_filters']], + chartOptions: [['color_scheme'], ['currency_format']], + }, + + formDataOverrides: (formData: QueryFormData) => ({ + ...formData, + metric: getStandardizedControls().shiftMetric(), + groupby: getStandardizedControls().popAllColumns(), + }), + + buildQuery, + + transform: (chartProps: ChartProps): TreemapTransformResult => { + const { + width, + height, + rawFormData, + hooks, + filterState, + queriesData, + theme, + inContextMenu, + emitCrossFilters, + datasource, + } = chartProps; + + const { data = [] } = queriesData[0]; + const { columnFormats = {}, currencyFormats = {} } = datasource ?? {}; + const { setDataMask = () => {}, onContextMenu } = hooks ?? {}; + const coltypeMapping = getColtypesMapping( + queriesData[0] as unknown as Parameters[0], + ); + const BORDER_COLOR = theme.colorBgBase; + const refs: Refs = {}; + + // Extract form values with defaults + const colorScheme = rawFormData.color_scheme as string; + const groupby = (rawFormData.groupby as QueryFormColumn[]) || []; + const metric = (rawFormData.metric as QueryFormMetric) || ''; + const labelType = + (rawFormData.label_type as EchartsTreemapLabelType) || + DEFAULT_FORM_DATA.labelType; + const labelPosition = + (rawFormData.label_position as LabelPositionEnum) || + DEFAULT_FORM_DATA.labelPosition; + const numberFormat = + (rawFormData.number_format as string) || DEFAULT_FORM_DATA.numberFormat; + const currencyFormat = rawFormData.currency_format; + const dateFormat = + (rawFormData.date_format as string) || DEFAULT_FORM_DATA.dateFormat; + const showLabels = + rawFormData.show_labels !== undefined + ? (rawFormData.show_labels as boolean) + : DEFAULT_FORM_DATA.showLabels; + const showUpperLabels = + rawFormData.show_upper_labels !== undefined + ? (rawFormData.show_upper_labels as boolean) + : DEFAULT_FORM_DATA.showUpperLabels; + const dashboardId = rawFormData.dashboard_id as number | undefined; + const sliceId = rawFormData.slice_id as number | undefined; + + const colorFn = CategoricalColorNamespace.getScale(colorScheme as string); + const numberFormatter = getValueFormatter( + metric, + currencyFormats, + columnFormats, + numberFormat, + currencyFormat, + ); + + const formatter = (params: TreemapSeriesCallbackDataParams) => + formatLabel({ + params, + numberFormatter, + labelType, + }); + + const columnsLabelMap = new Map(); + const metricLabel = getMetricLabel(metric); + const groupbyLabels = groupby.map(getColumnLabel); + const treeData = treeBuilder( + data as DataRecord[], + groupbyLabels, + metricLabel, + ); + + const labelProps = { + color: theme.colorText, + borderColor: theme.colorBgBase, + borderWidth: 1, + }; + + const traverse = ( + treeNodes: TreeNode[], + path: string[], + ): TreemapSeriesNodeItemOption[] => + treeNodes.map(treeNode => { + const { name: nodeName, value, groupBy } = treeNode; + const name = formatSeriesName(nodeName, { + timeFormatter: getTimeFormatter(dateFormat), + ...(coltypeMapping[groupBy] && { + coltype: coltypeMapping[groupBy], + }), + }); + const newPath = path.concat(name); + let item: TreemapSeriesNodeItemOption = { + name, + value, + colorSaturation: COLOR_SATURATION, + itemStyle: { + borderColor: BORDER_COLOR, + color: colorFn(name, sliceId), + borderWidth: BORDER_WIDTH, + gapWidth: GAP_WIDTH, + }, + }; + if (treeNode.children?.length) { + item = { + ...item, + children: traverse(treeNode.children, newPath), + }; + } else { + const joinedName = newPath.join(','); + columnsLabelMap.set(joinedName, newPath); + if ( + filterState?.selectedValues && + !filterState.selectedValues.includes(joinedName) + ) { + item = { + ...item, + itemStyle: { + colorAlpha: OpacityEnum.SemiTransparent, + color: theme.colorText, + borderColor: theme.colorBgBase, + borderWidth: 2, + }, + label: { + ...labelProps, + }, + }; + } + } + return item; + }); + + const transformedData: TreemapSeriesNodeItemOption[] = [ + { + name: metricLabel, + colorSaturation: COLOR_SATURATION, + itemStyle: { + borderColor: BORDER_COLOR, + color: colorFn(`${metricLabel}`, sliceId), + borderWidth: BORDER_WIDTH, + gapWidth: GAP_WIDTH, + }, + upperLabel: { + show: false, + }, + children: traverse(treeData, []), + }, + ]; + + const levels = [ + { + upperLabel: { + show: false, + }, + label: { + show: false, + }, + itemStyle: { + color: theme.colorPrimary, + }, + }, + ]; + + const series: TreemapSeriesOption[] = [ + { + type: 'treemap', + width: '100%', + height: '100%', + nodeClick: undefined, + roam: !dashboardId, + breadcrumb: { + show: false, + emptyItemWidth: 25, + }, + emphasis: { + label: { + ...labelProps, + show: true, + }, + }, + levels, + label: { + ...labelProps, + show: showLabels, + position: labelPosition, + formatter, + fontSize: LABEL_FONTSIZE, + }, + upperLabel: { + ...labelProps, + show: showUpperLabels, + formatter, + textBorderColor: 'transparent', + fontSize: LABEL_FONTSIZE, + }, + data: transformedData, + }, + ]; + + const echartOptions: EChartsCoreOption = { + tooltip: { + ...getDefaultTooltip(refs), + show: !inContextMenu, + trigger: 'item', + formatter: (params: TreemapSeriesCallbackDataParams) => + formatTooltip({ + params, + numberFormatter, + }), + }, + series, + }; + + return { + transformedProps: { + refs, + width, + height, + echartOptions, + formData: rawFormData, + groupby, + labelMap: Object.fromEntries(columnsLabelMap), + setDataMask, + selectedValues: (filterState?.selectedValues as string[]) || [], + emitCrossFilters, + onContextMenu, + coltypeMapping, + }, + }; + }, + + render: ({ transformedProps }) => ( + + ), +}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/stories/Treemap.stories.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/stories/Treemap.stories.tsx index 376c5b44f33..be21fd33963 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/stories/Treemap.stories.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/stories/Treemap.stories.tsx @@ -17,11 +17,8 @@ * under the License. */ -import { SuperChart, getChartTransformPropsRegistry } from '@superset-ui/core'; -import { - EchartsTreemapChartPlugin, - TreemapTransformProps, -} from '@superset-ui/plugin-chart-echarts'; +import { SuperChart } from '@superset-ui/core'; +import { EchartsTreemapChartPlugin } from '@superset-ui/plugin-chart-echarts'; import data from './data'; import { withResizableChartDemo } from '@storybook-shared'; @@ -29,11 +26,6 @@ new EchartsTreemapChartPlugin() .configure({ key: 'echarts-treemap' }) .register(); -getChartTransformPropsRegistry().registerValue( - 'echarts-treemap', - TreemapTransformProps, -); - export default { title: 'Chart Plugins/plugin-chart-echarts/Treemap', decorators: [withResizableChartDemo], diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/transformProps.ts deleted file mode 100644 index 05d7b1b6a9c..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/transformProps.ts +++ /dev/null @@ -1,323 +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 { - CategoricalColorNamespace, - getColumnLabel, - getMetricLabel, - getNumberFormatter, - getTimeFormatter, - NumberFormats, - ValueFormatter, - getValueFormatter, - tooltipHtml, -} from '@superset-ui/core'; -import type { TreemapSeriesNodeItemOption } from 'echarts/types/src/chart/treemap/TreemapSeries'; -import type { EChartsCoreOption } from 'echarts/core'; -import type { TreemapSeriesOption } from 'echarts/charts'; -import { - DEFAULT_FORM_DATA as DEFAULT_TREEMAP_FORM_DATA, - EchartsTreemapChartProps, - EchartsTreemapFormData, - EchartsTreemapLabelType, - TreemapSeriesCallbackDataParams, - TreemapTransformedProps, -} from './types'; -import { formatSeriesName, getColtypesMapping } from '../utils/series'; -import { - COLOR_SATURATION, - BORDER_WIDTH, - GAP_WIDTH, - LABEL_FONTSIZE, - extractTreePathInfo, -} from './constants'; -import { OpacityEnum } from '../constants'; -import { getDefaultTooltip } from '../utils/tooltip'; -import { Refs } from '../types'; -import { treeBuilder, TreeNode } from '../utils/treeBuilder'; - -export function formatLabel({ - params, - labelType, - numberFormatter, -}: { - params: TreemapSeriesCallbackDataParams; - labelType: EchartsTreemapLabelType; - numberFormatter: ValueFormatter; -}): string { - const { name = '', value } = params; - const formattedValue = numberFormatter(value as number); - - switch (labelType) { - case EchartsTreemapLabelType.Key: - return name; - case EchartsTreemapLabelType.Value: - return formattedValue; - case EchartsTreemapLabelType.KeyValue: - return `${name}: ${formattedValue}`; - default: - return name; - } -} - -export function formatTooltip({ - params, - numberFormatter, -}: { - params: TreemapSeriesCallbackDataParams; - numberFormatter: ValueFormatter; -}): string { - const { value, treePathInfo = [] } = params; - const formattedValue = numberFormatter(value as number); - const { metricLabel, treePath } = extractTreePathInfo(treePathInfo); - const percentFormatter = getNumberFormatter(NumberFormats.PERCENT_2_POINT); - - let formattedPercent = ''; - // the last item is current node, here we should find the parent node - const currentNode = treePathInfo[treePathInfo.length - 1]; - const parentNode = treePathInfo[treePathInfo.length - 2]; - if (parentNode) { - const percent: number = parentNode.value - ? (currentNode.value as number) / (parentNode.value as number) - : 0; - formattedPercent = percentFormatter(percent); - } - const row = [metricLabel, formattedValue]; - if (formattedPercent) { - row.push(formattedPercent); - } - return tooltipHtml([row], treePath.join(' ▸ ')); -} - -export default function transformProps( - chartProps: EchartsTreemapChartProps, -): TreemapTransformedProps { - const { - formData, - height, - queriesData, - width, - hooks, - filterState, - theme, - inContextMenu, - emitCrossFilters, - datasource, - } = chartProps; - const { data = [], detected_currency: detectedCurrency } = queriesData[0]; - const { - columnFormats = {}, - currencyFormats = {}, - currencyCodeColumn, - } = datasource; - const { setDataMask = () => {}, onContextMenu } = hooks; - const coltypeMapping = getColtypesMapping(queriesData[0]); - const BORDER_COLOR = theme.colorBgBase; - - const { - colorScheme, - groupby = [], - metric = '', - labelType, - labelPosition, - numberFormat, - currencyFormat, - dateFormat, - showLabels, - showUpperLabels, - dashboardId, - sliceId, - }: EchartsTreemapFormData = { - ...DEFAULT_TREEMAP_FORM_DATA, - ...formData, - }; - const refs: Refs = {}; - const colorFn = CategoricalColorNamespace.getScale(colorScheme as string); - const numberFormatter = getValueFormatter( - metric, - currencyFormats, - columnFormats, - numberFormat, - currencyFormat, - undefined, - data, - currencyCodeColumn, - detectedCurrency, - ); - - const formatter = (params: TreemapSeriesCallbackDataParams) => - formatLabel({ - params, - numberFormatter, - labelType, - }); - - const columnsLabelMap = new Map(); - const metricLabel = getMetricLabel(metric); - const groupbyLabels = groupby.map(getColumnLabel); - const treeData = treeBuilder(data, groupbyLabels, metricLabel); - const labelProps = { - color: theme.colorText, - }; - const traverse = (treeNodes: TreeNode[], path: string[]) => - treeNodes.map(treeNode => { - const { name: nodeName, value, groupBy } = treeNode; - const name = formatSeriesName(nodeName, { - timeFormatter: getTimeFormatter(dateFormat), - ...(coltypeMapping[groupBy] && { - coltype: coltypeMapping[groupBy], - }), - }); - const newPath = path.concat(name); - let item: TreemapSeriesNodeItemOption = { - name, - value, - colorSaturation: COLOR_SATURATION, - itemStyle: { - borderColor: BORDER_COLOR, - color: colorFn(name, sliceId), - borderWidth: BORDER_WIDTH, - gapWidth: GAP_WIDTH, - }, - }; - if (treeNode.children?.length) { - item = { - ...item, - children: traverse(treeNode.children, newPath), - }; - } else { - const joinedName = newPath.join(','); - // map(joined_name: [columnLabel_1, columnLabel_2, ...]) - columnsLabelMap.set(joinedName, newPath); - if ( - filterState.selectedValues && - !filterState.selectedValues.includes(joinedName) - ) { - item = { - ...item, - itemStyle: { - colorAlpha: OpacityEnum.SemiTransparent, - color: theme.colorText, - borderColor: theme.colorBgBase, - borderWidth: 2, - }, - label: { - ...labelProps, - }, - }; - } - } - return item; - }); - - const transformedData: TreemapSeriesNodeItemOption[] = [ - { - name: metricLabel, - colorSaturation: COLOR_SATURATION, - itemStyle: { - borderColor: BORDER_COLOR, - color: colorFn(`${metricLabel}`, sliceId), - borderWidth: BORDER_WIDTH, - gapWidth: GAP_WIDTH, - }, - upperLabel: { - show: false, - }, - children: traverse(treeData, []), - }, - ]; - - // set a default color when metric values are 0 over all. - const levels = [ - { - upperLabel: { - show: false, - }, - label: { - show: false, - }, - itemStyle: { - color: theme.colorPrimary, - }, - }, - ]; - - const series: TreemapSeriesOption[] = [ - { - type: 'treemap', - width: '100%', - height: '100%', - nodeClick: undefined, - roam: !dashboardId, - breadcrumb: { - show: false, - emptyItemWidth: 25, - }, - emphasis: { - label: { - ...labelProps, - show: true, - }, - }, - levels, - label: { - ...labelProps, - show: showLabels, - position: labelPosition, - formatter, - fontSize: LABEL_FONTSIZE, - }, - upperLabel: { - ...labelProps, - show: showUpperLabels, - formatter, - textBorderColor: 'transparent', - fontSize: LABEL_FONTSIZE, - }, - data: transformedData, - }, - ]; - - const echartOptions: EChartsCoreOption = { - tooltip: { - ...getDefaultTooltip(refs), - show: !inContextMenu, - trigger: 'item', - formatter: (params: any) => - formatTooltip({ - params, - numberFormatter, - }), - }, - series, - }; - return { - formData, - width, - height, - echartOptions, - setDataMask, - emitCrossFilters, - labelMap: Object.fromEntries(columnsLabelMap), - groupby, - selectedValues: filterState.selectedValues || [], - onContextMenu, - refs, - coltypeMapping, - }; -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/types.ts deleted file mode 100644 index d03e166a2bb..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/types.ts +++ /dev/null @@ -1,75 +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 { - ChartDataResponseResult, - ChartProps, - QueryFormColumn, - QueryFormData, - QueryFormMetric, -} from '@superset-ui/core'; -import type { CallbackDataParams } from 'echarts/types/src/util/types'; -import { - BaseTransformedProps, - ContextMenuTransformedProps, - CrossFilterTransformedProps, - LabelPositionEnum, - TreePathInfo, -} from '../types'; - -export type EchartsTreemapFormData = QueryFormData & { - colorScheme?: string; - groupby: QueryFormColumn[]; - metric?: QueryFormMetric; - labelType: EchartsTreemapLabelType; - labelPosition: LabelPositionEnum; - showLabels: boolean; - showUpperLabels: boolean; - numberFormat: string; - dateFormat: string; - dashboardId?: number; -}; - -export enum EchartsTreemapLabelType { - Key = 'key', - Value = 'value', - KeyValue = 'key_value', -} - -export interface EchartsTreemapChartProps extends ChartProps { - formData: EchartsTreemapFormData; - queriesData: ChartDataResponseResult[]; -} - -export const DEFAULT_FORM_DATA: Partial = { - groupby: [], - labelType: EchartsTreemapLabelType.KeyValue, - labelPosition: LabelPositionEnum.InsideTopLeft, - numberFormat: 'SMART_NUMBER', - showLabels: true, - showUpperLabels: true, - dateFormat: 'smart_date', -}; -export interface TreemapSeriesCallbackDataParams extends CallbackDataParams { - treePathInfo?: TreePathInfo[]; -} - -export type TreemapTransformedProps = - BaseTransformedProps & - ContextMenuTransformedProps & - CrossFilterTransformedProps; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/EchartsWaterfall.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/EchartsWaterfall.tsx deleted file mode 100644 index 58c86a87841..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/EchartsWaterfall.tsx +++ /dev/null @@ -1,51 +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 Echart from '../components/Echart'; -import { WaterfallChartTransformedProps } from './types'; -import { EventHandlers } from '../types'; - -export default function EchartsWaterfall( - props: WaterfallChartTransformedProps, -) { - const { height, width, echartOptions, refs, onLegendStateChanged, formData } = - props; - - const eventHandlers: EventHandlers = { - legendselectchanged: payload => { - onLegendStateChanged?.(payload.selected); - }, - legendselectall: payload => { - onLegendStateChanged?.(payload.selected); - }, - legendinverseselect: payload => { - onLegendStateChanged?.(payload.selected); - }, - }; - - return ( - - ); -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/buildQuery.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/buildQuery.ts deleted file mode 100644 index deb3571938b..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/buildQuery.ts +++ /dev/null @@ -1,38 +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 { - buildQueryContext, - ensureIsArray, - QueryFormData, -} from '@superset-ui/core'; - -export default function buildQuery(formData: QueryFormData) { - const { x_axis, granularity_sqla, groupby } = formData; - const columns = [ - ...ensureIsArray(x_axis || granularity_sqla), - ...ensureIsArray(groupby), - ]; - return buildQueryContext(formData, baseQueryObject => [ - { - ...baseQueryObject, - columns, - orderby: columns?.map(column => [column, true]), - }, - ]); -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/index.ts deleted file mode 100644 index 7a67fc90e2a..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/index.ts +++ /dev/null @@ -1,76 +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 - * regardin - * g 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 { t } from '@apache-superset/core/translation'; -import { ChartMetadata, ChartPlugin } from '@superset-ui/core'; -import buildQuery from './buildQuery'; -import controlPanel from './controlPanel'; -import transformProps from './transformProps'; -import thumbnail from './images/thumbnail.png'; -import thumbnailDark from './images/thumbnail-dark.png'; -import example1 from './images/example1.png'; -import example2 from './images/example2.png'; -import example3 from './images/example3.png'; -import example1Dark from './images/example1-dark.png'; -import example2Dark from './images/example2-dark.png'; -import example3Dark from './images/example3-dark.png'; -import { EchartsWaterfallChartProps, EchartsWaterfallFormData } from './types'; - -// TODO: Implement cross filtering -export default class EchartsWaterfallChartPlugin extends ChartPlugin< - EchartsWaterfallFormData, - EchartsWaterfallChartProps -> { - /** - * The constructor is used to pass relevant metadata and callbacks that get - * registered in respective registries that are used throughout the library - * and application. A more thorough description of each property is given in - * the respective imported file. - * - * It is worth noting that `buildQuery` and is optional, and only needed for - * advanced visualizations that require either post processing operations - * (pivoting, rolling aggregations, sorting etc) or submitting multiple queries. - */ - constructor() { - super({ - buildQuery, - controlPanel, - loadChart: () => import('./EchartsWaterfall'), - metadata: new ChartMetadata({ - credits: ['https://echarts.apache.org'], - category: t('Evolution'), - description: t( - `A waterfall chart is a form of data visualization that helps in understanding - the cumulative effect of sequentially introduced positive or negative values. - These intermediate values can either be time based or category based.`, - ), - exampleGallery: [ - { url: example1, urlDark: example1Dark }, - { url: example2, urlDark: example2Dark }, - { url: example3, urlDark: example3Dark }, - ], - name: t('Waterfall Chart'), - tags: [t('Categorical'), t('Comparison'), t('ECharts'), t('Featured')], - thumbnail, - thumbnailDark, - }), - transformProps, - }); - } -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/index.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/index.tsx new file mode 100644 index 00000000000..ea6dee811ed --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/index.tsx @@ -0,0 +1,795 @@ +/** + * 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. + */ + +/** + * ECharts Waterfall Chart - Glyph Pattern Implementation + * + * Visualizes cumulative effect of sequentially introduced positive or negative values. + * Uses stacked bars to show how values increase or decrease over categories/time. + */ + +import { t } from '@apache-superset/core/translation'; +import type { ComposeOption } from 'echarts/core'; +import type { BarSeriesOption } from 'echarts/charts'; +import type { BarDataItemOption } from 'echarts/types/src/chart/bar/BarSeries'; +import type { CallbackDataParams } from 'echarts/types/src/util/types'; +import { + buildQueryContext, + CurrencyFormatter, + DataRecord, + ensureIsArray, + getMetricLabel, + getNumberFormatter, + getTimeFormatter, + isAdhocColumn, + NumberFormatter, + QueryFormData, + rgbToHex, + RgbaColor, + tooltipHtml, +} from '@superset-ui/core'; +import { GenericDataType } from '@apache-superset/core/common'; + +import { + defineChart, + Metric, + Dimension, + Temporal, + Text, + Checkbox, + Select, + ColorPicker, + ChartProps, + NumberFormat, + Currency, + TimeFormat, + ShowLegend, +} from '@superset-ui/glyph-core'; + +import { getDefaultTooltip } from '../utils/tooltip'; +import { defaultGrid, defaultYAxis } from '../defaults'; +import { getColtypesMapping } from '../utils/series'; +import Echart from '../components/Echart'; +import { EventHandlers, Refs } from '../types'; +import { NULL_STRING } from '../constants'; +import { ASSIST_MARK, LEGEND, TOKEN, TOTAL_MARK } from './constants'; + +import thumbnail from './images/thumbnail.png'; +import thumbnailDark from './images/thumbnail-dark.png'; +import example1 from './images/example1.png'; +import example1Dark from './images/example1-dark.png'; +import example2 from './images/example2.png'; +import example2Dark from './images/example2-dark.png'; +import example3 from './images/example3.png'; +import example3Dark from './images/example3-dark.png'; + +// ============================================================================ +// Types +// ============================================================================ + +type EChartsOption = ComposeOption; + +type ISeriesData = { + originalValue?: number; + totalSum?: number; +} & BarDataItemOption; + +type ICallbackDataParams = CallbackDataParams & { + axisValueLabel: string; + data: ISeriesData; +}; + +interface WaterfallTransformResult { + transformedProps: { + refs: Refs; + width: number; + height: number; + echartOptions: EChartsOption; + formData: Record; + onLegendStateChanged?: (state: Record) => void; + }; +} + +// ============================================================================ +// X Ticks Layout Options +// ============================================================================ + +const X_TICKS_LAYOUT_OPTIONS = [ + { label: t('auto'), value: 'auto' }, + { label: t('flat'), value: 'flat' }, + { label: '45°', value: '45°' }, + { label: '90°', value: '90°' }, + { label: t('staggered'), value: 'staggered' }, +]; + +// ============================================================================ +// Build Query - exported for testing +// ============================================================================ + +export function buildQuery(formData: QueryFormData) { + const { + x_axis: xAxis, + granularity_sqla: granularitySqla, + groupby, + } = formData; + const columns = [ + ...ensureIsArray(xAxis || granularitySqla), + ...ensureIsArray(groupby), + ]; + return buildQueryContext(formData, baseQueryObject => [ + { + ...baseQueryObject, + columns, + orderby: columns?.map(column => [column, true]), + }, + ]); +} + +// ============================================================================ +// Helper Functions +// ============================================================================ + +function formatTooltip({ + params, + breakdownName, + defaultFormatter, + xAxisFormatter, + totalMark, +}: { + params: ICallbackDataParams[]; + breakdownName?: string; + defaultFormatter: NumberFormatter | CurrencyFormatter; + xAxisFormatter: (value: number | string, index: number) => string; + totalMark: string; +}) { + const series = params.find( + param => param.seriesName !== ASSIST_MARK && param.data.value !== TOKEN, + ); + + if (!series) { + return ''; + } + + const isTotal = series?.seriesName === totalMark; + if (!series) { + return NULL_STRING; + } + + const title = + !isTotal || breakdownName + ? xAxisFormatter(series.name, series.dataIndex) + : undefined; + const rows: string[][] = []; + if (!isTotal) { + rows.push([ + series.seriesName!, + defaultFormatter(series.data.originalValue), + ]); + } + rows.push([totalMark, defaultFormatter(series.data.totalSum)]); + return tooltipHtml(rows, title); +} + +function transformer({ + data, + xAxis, + metric, + breakdown, + totalMark, + showTotal, +}: { + data: DataRecord[]; + xAxis: string; + metric: string; + breakdown?: string; + totalMark: string; + showTotal: boolean; +}) { + const groupedData = data.reduce((acc, cur) => { + const categoryLabel = cur[xAxis] as string; + const categoryData = acc.get(categoryLabel) || []; + categoryData.push(cur); + acc.set(categoryLabel, categoryData); + return acc; + }, new Map()); + + const transformedData: DataRecord[] = []; + + if (breakdown) { + groupedData.forEach((value, key) => { + const tempValue = value; + const sum = tempValue.reduce( + (acc, cur) => acc + ((cur[metric] as number) ?? 0), + 0, + ); + if (showTotal) { + tempValue.push({ + [xAxis]: key, + [breakdown]: totalMark, + [metric]: sum, + }); + } + transformedData.push(...tempValue); + }); + } else { + let total = 0; + groupedData.forEach((value, key) => { + const sum = value.reduce( + (acc, cur) => acc + ((cur[metric] as number) ?? 0), + 0, + ); + transformedData.push({ + [xAxis]: key, + [metric]: sum, + }); + total += sum; + }); + if (showTotal) { + transformedData.push({ + [xAxis]: totalMark, + [metric]: total, + }); + } + } + + return transformedData; +} + +// ============================================================================ +// Chart Definition +// ============================================================================ + +export default defineChart({ + metadata: { + name: t('Waterfall Chart'), + description: t( + `A waterfall chart is a form of data visualization that helps in understanding + the cumulative effect of sequentially introduced positive or negative values. + These intermediate values can either be time based or category based.`, + ), + category: t('Evolution'), + tags: [t('Categorical'), t('Comparison'), t('ECharts'), t('Featured')], + credits: ['https://echarts.apache.org'], + thumbnail, + thumbnailDark, + exampleGallery: [ + { url: example1, urlDark: example1Dark }, + { url: example2, urlDark: example2Dark }, + { url: example3, urlDark: example3Dark }, + ], + }, + + arguments: { + // Query section + xAxis: Temporal.with({ + label: t('X-axis'), + description: t('The time or category column for the x-axis'), + }), + + groupby: Dimension.with({ + label: t('Breakdowns'), + description: t( + `Breaks down the series by the category specified in this control. + This can help viewers understand how each category affects the overall value.`, + ), + multi: false, + }), + + metric: Metric.with({ + label: t('Metric'), + description: t('The metric value to display'), + multi: false, + }), + + // Chart Options + showValue: Checkbox.with({ + label: t('Show Values'), + description: t('Whether to display values on the bars'), + default: false, + }), + + showLegend: ShowLegend, + + // Series settings - Increase + increaseColor: ColorPicker.with({ + label: t('Increase color'), + description: t( + 'Select the color used for values that indicate an increase in the chart', + ), + default: { r: 90, g: 193, b: 137, a: 1 }, + }), + + increaseLabel: Text.with({ + label: t('Increase label'), + description: t( + 'Customize the label displayed for increasing values in the chart tooltips and legend.', + ), + default: '', + }), + + // Series settings - Decrease + decreaseColor: ColorPicker.with({ + label: t('Decrease color'), + description: t( + 'Select the color used for values that indicate a decrease in the chart.', + ), + default: { r: 224, g: 67, b: 85, a: 1 }, + }), + + decreaseLabel: Text.with({ + label: t('Decrease label'), + description: t( + 'Customize the label displayed for decreasing values in the chart tooltips and legend.', + ), + default: '', + }), + + // Series settings - Total + showTotal: Checkbox.with({ + label: t('Show total'), + description: t('Display cumulative total at end'), + default: true, + }), + + totalColor: ColorPicker.with({ + label: t('Total color'), + description: t( + 'Select the color used for values that represent total bars in the chart', + ), + default: { r: 102, g: 102, b: 102, a: 1 }, + }), + + totalLabel: Text.with({ + label: t('Total label'), + description: t( + 'Customize the label displayed for total values in the chart tooltips, legend, and chart axis.', + ), + default: '', + }), + + // X Axis + xAxisLabel: Text.with({ + label: t('X Axis Label'), + description: t('Label for the x-axis'), + default: '', + }), + + xAxisTimeFormat: TimeFormat.with({ + label: t('X Axis Time Format'), + description: t('Time format for the x-axis labels'), + }), + + xTicksLayout: Select.with({ + label: t('X Tick Layout'), + description: t('The way the ticks are laid out on the X-axis'), + options: X_TICKS_LAYOUT_OPTIONS, + default: 'auto', + }), + + // Y Axis + yAxisLabel: Text.with({ + label: t('Y Axis Label'), + description: t('Label for the y-axis'), + default: '', + }), + + yAxisFormat: NumberFormat, + + currencyFormat: Currency, + }, + + buildQuery, + + transform: (chartProps: ChartProps): WaterfallTransformResult => { + const { + width, + height, + rawFormData, + legendState, + queriesData, + hooks, + theme, + inContextMenu, + } = chartProps; + const refs: Refs = {}; + const { data = [] } = queriesData[0]; + const { colnames = [], coltypes = [] } = + (queriesData[0] as { colnames?: string[]; coltypes?: number[] }) ?? {}; + const coltypeMapping = getColtypesMapping({ colnames, coltypes }); + const { onLegendStateChanged } = hooks; + + // Extract form values + const currencyFormat = rawFormData.currency_format as + | { symbol?: string; symbolPosition?: string } + | undefined; + const granularitySqla = (rawFormData.granularity_sqla as string) || ''; + const { groupby } = rawFormData; + const increaseColor = (rawFormData.increase_color as RgbaColor) || { + r: 90, + g: 193, + b: 137, + }; + const decreaseColor = (rawFormData.decrease_color as RgbaColor) || { + r: 224, + g: 67, + b: 85, + }; + const totalColor = (rawFormData.total_color as RgbaColor) || { + r: 102, + g: 102, + b: 102, + }; + const metric = rawFormData.metric || ''; + const xAxis = rawFormData.x_axis; + const xTicksLayout = rawFormData.x_ticks_layout as string; + const xAxisTimeFormat = rawFormData.x_axis_time_format as string; + const showLegend = rawFormData.show_legend as boolean; + const yAxisLabel = (rawFormData.y_axis_label as string) || ''; + const xAxisLabel = (rawFormData.x_axis_label as string) || ''; + const yAxisFormat = (rawFormData.y_axis_format as string) || 'SMART_NUMBER'; + const showValue = rawFormData.show_value as boolean; + const showTotal = rawFormData.show_total as boolean; + const totalLabelValue = rawFormData.total_label as string; + const increaseLabelValue = rawFormData.increase_label as string; + const decreaseLabelValue = rawFormData.decrease_label as string; + + const defaultFormatter = currencyFormat?.symbol + ? new CurrencyFormatter({ + d3Format: yAxisFormat, + currency: { + symbol: currencyFormat.symbol, + symbolPosition: currencyFormat.symbolPosition || 'prefix', + }, + }) + : getNumberFormatter(yAxisFormat); + + const totalMark = totalLabelValue || TOTAL_MARK; + const legendNames = { + INCREASE: increaseLabelValue || LEGEND.INCREASE, + DECREASE: decreaseLabelValue || LEGEND.DECREASE, + TOTAL: totalLabelValue || LEGEND.TOTAL, + }; + + const seriesFormatter = (params: ICallbackDataParams) => { + const { data: paramData } = params; + const { originalValue } = paramData; + return defaultFormatter(originalValue as number); + }; + + const groupbyArray = ensureIsArray(groupby); + const breakdownColumn = groupbyArray.length ? groupbyArray[0] : undefined; + const breakdownName = isAdhocColumn(breakdownColumn) + ? breakdownColumn.label! + : (breakdownColumn as string); + const xAxisColumn = xAxis || granularitySqla; + const xAxisName = isAdhocColumn(xAxisColumn) + ? xAxisColumn.label! + : (xAxisColumn as string); + const metricLabel = getMetricLabel(metric); + + const transformedData = transformer({ + data, + breakdown: breakdownName, + xAxis: xAxisName, + metric: metricLabel, + totalMark, + showTotal, + }); + + const assistData: ISeriesData[] = []; + const increaseData: ISeriesData[] = []; + const decreaseData: ISeriesData[] = []; + const totalData: ISeriesData[] = []; + + let previousTotal = 0; + + transformedData.forEach((datum, index, self) => { + const totalSum = self.slice(0, index + 1).reduce((prev, cur, i) => { + if (breakdownName) { + if (cur[breakdownName] !== totalMark || i === 0) { + return prev + ((cur[metricLabel] as number) ?? 0); + } + } else if (cur[xAxisName] !== totalMark) { + return prev + ((cur[metricLabel] as number) ?? 0); + } + return prev; + }, 0); + + const isTotal = + (breakdownName && datum[breakdownName] === totalMark) || + datum[xAxisName] === totalMark; + + const originalValue = datum[metricLabel] as number; + let value = originalValue; + const oppositeSigns = Math.sign(previousTotal) !== Math.sign(totalSum); + if (oppositeSigns) { + value = Math.sign(value) * (Math.abs(value) - Math.abs(previousTotal)); + } + + if (isTotal) { + increaseData.push({ value: TOKEN }); + decreaseData.push({ value: TOKEN }); + totalData.push({ + value: totalSum, + originalValue: totalSum, + totalSum, + }); + } else if (value < 0) { + increaseData.push({ value: TOKEN }); + decreaseData.push({ + value: totalSum < 0 ? value : -value, + originalValue, + totalSum, + }); + totalData.push({ value: TOKEN }); + } else { + increaseData.push({ + value: totalSum > 0 ? value : -value, + originalValue, + totalSum, + }); + decreaseData.push({ value: TOKEN }); + totalData.push({ value: TOKEN }); + } + + const color = oppositeSigns + ? value > 0 + ? rgbToHex(increaseColor.r, increaseColor.g, increaseColor.b) + : rgbToHex(decreaseColor.r, decreaseColor.g, decreaseColor.b) + : 'transparent'; + + let opacity = 1; + if (legendState?.[legendNames.INCREASE] === false && value > 0) { + opacity = 0; + } else if (legendState?.[legendNames.DECREASE] === false && value < 0) { + opacity = 0; + } + + if (isTotal) { + assistData.push({ value: TOKEN }); + } else if (index === 0) { + assistData.push({ + value: 0, + }); + } else if ( + oppositeSigns || + Math.abs(totalSum) > Math.abs(previousTotal) + ) { + assistData.push({ + value: previousTotal, + itemStyle: { color, opacity }, + }); + } else { + assistData.push({ + value: totalSum, + itemStyle: { color, opacity }, + }); + } + + previousTotal = totalSum; + }); + + const xAxisColumns: string[] = []; + const xAxisData = transformedData.map(row => { + let column = xAxisName; + let rowValue = row[xAxisName]; + if (breakdownName && row[breakdownName] !== totalMark) { + column = breakdownName; + rowValue = row[breakdownName]; + } + if (!rowValue) { + rowValue = NULL_STRING; + } + if (typeof rowValue !== 'string' && typeof rowValue !== 'number') { + rowValue = String(rowValue); + } + xAxisColumns.push(column); + return rowValue; + }); + + const xAxisFormatter = (axisValue: number | string, index: number) => { + if (axisValue === totalMark) { + return totalMark; + } + if (coltypeMapping[xAxisColumns[index]] === GenericDataType.Temporal) { + if (typeof axisValue === 'string') { + return getTimeFormatter(xAxisTimeFormat)( + Number.parseInt(axisValue, 10), + ); + } + return getTimeFormatter(xAxisTimeFormat)(axisValue); + } + return String(axisValue); + }; + + let axisLabel: { + rotate?: number; + hideOverlap?: boolean; + show?: boolean; + formatter?: typeof xAxisFormatter; + }; + if (xTicksLayout === '45°') { + axisLabel = { rotate: -45 }; + } else if (xTicksLayout === '90°') { + axisLabel = { rotate: -90 }; + } else if (xTicksLayout === 'flat') { + axisLabel = { rotate: 0 }; + } else if (xTicksLayout === 'staggered') { + axisLabel = { rotate: -45 }; + } else { + axisLabel = { show: true }; + } + axisLabel.formatter = xAxisFormatter; + axisLabel.hideOverlap = false; + + const seriesProps: Pick = { + type: 'bar', + stack: 'stack', + emphasis: { + disabled: true, + }, + }; + const labelProps = { + show: showValue, + formatter: seriesFormatter, + color: (theme as { colorText?: string })?.colorText, + borderColor: (theme as { colorBgBase?: string })?.colorBgBase, + borderWidth: 1, + }; + const barSeries: BarSeriesOption[] = [ + { + ...seriesProps, + name: ASSIST_MARK, + data: assistData, + }, + { + ...seriesProps, + name: legendNames.INCREASE, + label: { + ...labelProps, + position: 'top', + }, + itemStyle: { + color: rgbToHex(increaseColor.r, increaseColor.g, increaseColor.b), + }, + data: increaseData, + }, + { + ...seriesProps, + name: legendNames.DECREASE, + label: { + ...labelProps, + position: 'bottom', + }, + itemStyle: { + color: rgbToHex(decreaseColor.r, decreaseColor.g, decreaseColor.b), + }, + data: decreaseData, + }, + { + ...seriesProps, + name: legendNames.TOTAL, + label: { + ...labelProps, + position: 'top', + }, + itemStyle: { + color: rgbToHex(totalColor.r, totalColor.g, totalColor.b), + }, + data: totalData, + }, + ]; + + const sizeUnit = (theme as { sizeUnit?: number })?.sizeUnit ?? 4; + const echartOptions: EChartsOption = { + grid: { + ...defaultGrid, + top: sizeUnit * 7, + bottom: sizeUnit * 7, + left: sizeUnit * 5, + right: sizeUnit * 7, + }, + legend: { + show: showLegend, + selected: legendState, + data: [legendNames.INCREASE, legendNames.DECREASE, legendNames.TOTAL], + }, + xAxis: { + data: xAxisData, + type: 'category', + name: xAxisLabel, + nameTextStyle: { + padding: [sizeUnit * 4, 0, 0, 0], + }, + nameLocation: 'middle', + axisLabel, + }, + yAxis: { + ...defaultYAxis, + type: 'value', + nameTextStyle: { + padding: [0, 0, sizeUnit * 5, 0], + }, + nameLocation: 'middle', + name: yAxisLabel, + axisLabel: { formatter: defaultFormatter }, + }, + tooltip: { + ...getDefaultTooltip(refs), + appendToBody: true, + trigger: 'axis', + show: !inContextMenu, + formatter: (params: unknown) => + formatTooltip({ + params: params as ICallbackDataParams[], + breakdownName, + defaultFormatter, + xAxisFormatter, + totalMark, + }), + }, + series: barSeries, + }; + + return { + transformedProps: { + refs, + formData: rawFormData, + width, + height, + echartOptions, + onLegendStateChanged, + }, + }; + }, + + render: ({ transformedProps }) => { + const { + height, + width, + echartOptions, + refs, + onLegendStateChanged, + formData, + } = transformedProps; + + const eventHandlers: EventHandlers = { + legendselectchanged: payload => { + onLegendStateChanged?.(payload.selected); + }, + legendselectall: payload => { + onLegendStateChanged?.(payload.selected); + }, + legendinverseselect: payload => { + onLegendStateChanged?.(payload.selected); + }, + }; + + return ( + + ); + }, +}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/stories/Waterfall.stories.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/stories/Waterfall.stories.tsx index d59a31aef6c..33e9d5c5ac8 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/stories/Waterfall.stories.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/stories/Waterfall.stories.tsx @@ -16,15 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -import { - SuperChart, - VizType, - getChartTransformPropsRegistry, -} from '@superset-ui/core'; -import { - EchartsWaterfallChartPlugin, - WaterfallTransformProps, -} from '@superset-ui/plugin-chart-echarts'; +import { SuperChart, VizType } from '@superset-ui/core'; +import { EchartsWaterfallChartPlugin } from '@superset-ui/plugin-chart-echarts'; import data from './data'; import { withResizableChartDemo } from '@storybook-shared'; @@ -32,11 +25,6 @@ new EchartsWaterfallChartPlugin() .configure({ key: VizType.Waterfall }) .register(); -getChartTransformPropsRegistry().registerValue( - VizType.Waterfall, - WaterfallTransformProps, -); - export default { title: 'Chart Plugins/plugin-chart-echarts/Waterfall', decorators: [withResizableChartDemo], diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/transformProps.ts deleted file mode 100644 index 4a82498fd59..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/transformProps.ts +++ /dev/null @@ -1,492 +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 { - CurrencyFormatter, - DataRecord, - ensureIsArray, - getMetricLabel, - getNumberFormatter, - getTimeFormatter, - isAdhocColumn, - NumberFormatter, - rgbToHex, - tooltipHtml, -} from '@superset-ui/core'; -import { GenericDataType } from '@apache-superset/core/common'; -import type { ComposeOption } from 'echarts/core'; -import type { BarSeriesOption } from 'echarts/charts'; -import { - EchartsWaterfallChartProps, - ISeriesData, - WaterfallChartTransformedProps, - ICallbackDataParams, -} from './types'; -import { getDefaultTooltip } from '../utils/tooltip'; -import { defaultGrid, defaultYAxis } from '../defaults'; -import { ASSIST_MARK, LEGEND, TOKEN, TOTAL_MARK } from './constants'; -import { getColtypesMapping } from '../utils/series'; -import { Refs } from '../types'; -import { NULL_STRING } from '../constants'; - -type EChartsOption = ComposeOption; - -function formatTooltip({ - params, - breakdownName, - defaultFormatter, - xAxisFormatter, - totalMark, -}: { - params: ICallbackDataParams[]; - breakdownName?: string; - defaultFormatter: NumberFormatter | CurrencyFormatter; - xAxisFormatter: (value: number | string, index: number) => string; - totalMark: string; -}) { - const series = params.find( - param => param.seriesName !== ASSIST_MARK && param.data.value !== TOKEN, - ); - - // We may have no matching series depending on the legend state - if (!series) { - return ''; - } - - const isTotal = series?.seriesName === totalMark; - if (!series) { - return NULL_STRING; - } - - const title = - !isTotal || breakdownName - ? xAxisFormatter(series.name, series.dataIndex) - : undefined; - const rows: string[][] = []; - if (!isTotal) { - rows.push([ - series.seriesName!, - defaultFormatter(series.data.originalValue), - ]); - } - rows.push([totalMark, defaultFormatter(series.data.totalSum)]); - return tooltipHtml(rows, title); -} - -function transformer({ - data, - xAxis, - metric, - breakdown, - totalMark, - showTotal, -}: { - data: DataRecord[]; - xAxis: string; - metric: string; - breakdown?: string; - totalMark: string; - showTotal: boolean; -}) { - // Group by series (temporary map) - const groupedData = data.reduce((acc, cur) => { - const categoryLabel = cur[xAxis] as string; - const categoryData = acc.get(categoryLabel) || []; - categoryData.push(cur); - acc.set(categoryLabel, categoryData); - return acc; - }, new Map()); - - const transformedData: DataRecord[] = []; - - if (breakdown) { - groupedData.forEach((value, key) => { - const tempValue = value; - // Calc total per period - const sum = tempValue.reduce( - (acc, cur) => acc + ((cur[metric] as number) ?? 0), - 0, - ); - // Push total per period to the end of period values array - if (showTotal) { - tempValue.push({ - [xAxis]: key, - [breakdown]: totalMark, - [metric]: sum, - }); - } - transformedData.push(...tempValue); - }); - } else { - let total = 0; - groupedData.forEach((value, key) => { - const sum = value.reduce( - (acc, cur) => acc + ((cur[metric] as number) ?? 0), - 0, - ); - transformedData.push({ - [xAxis]: key, - [metric]: sum, - }); - total += sum; - }); - if (showTotal) { - transformedData.push({ - [xAxis]: totalMark, - [metric]: total, - }); - } - } - - return transformedData; -} - -export default function transformProps( - chartProps: EchartsWaterfallChartProps, -): WaterfallChartTransformedProps { - const { - width, - height, - formData, - legendState, - queriesData, - hooks, - theme, - inContextMenu, - } = chartProps; - const refs: Refs = {}; - const { data = [] } = queriesData[0]; - const coltypeMapping = getColtypesMapping(queriesData[0]); - const { setDataMask = () => {}, onContextMenu, onLegendStateChanged } = hooks; - const { - currencyFormat, - granularitySqla = '', - groupby, - increaseColor = { r: 90, g: 193, b: 137 }, - decreaseColor = { r: 224, g: 67, b: 85 }, - totalColor = { r: 102, g: 102, b: 102 }, - metric = '', - xAxis, - xTicksLayout, - xAxisTimeFormat, - showLegend, - yAxisLabel, - xAxisLabel, - yAxisFormat, - showValue, - showTotal, - totalLabel, - increaseLabel, - decreaseLabel, - } = formData; - const defaultFormatter = currencyFormat?.symbol - ? new CurrencyFormatter({ d3Format: yAxisFormat, currency: currencyFormat }) - : getNumberFormatter(yAxisFormat); - - const totalMark = totalLabel || TOTAL_MARK; - const legendNames = { - INCREASE: increaseLabel || LEGEND.INCREASE, - DECREASE: decreaseLabel || LEGEND.DECREASE, - TOTAL: totalLabel || LEGEND.TOTAL, - }; - - const seriesformatter = (params: ICallbackDataParams) => { - const { data } = params; - const { originalValue } = data; - return defaultFormatter(originalValue as number); - }; - const groupbyArray = ensureIsArray(groupby); - const breakdownColumn = groupbyArray.length ? groupbyArray[0] : undefined; - const breakdownName = isAdhocColumn(breakdownColumn) - ? breakdownColumn.label! - : breakdownColumn; - const xAxisColumn = xAxis || granularitySqla; - const xAxisName = isAdhocColumn(xAxisColumn) - ? xAxisColumn.label! - : xAxisColumn; - const metricLabel = getMetricLabel(metric); - - const transformedData = transformer({ - data, - breakdown: breakdownName, - xAxis: xAxisName, - metric: metricLabel, - totalMark, - showTotal, - }); - - const assistData: ISeriesData[] = []; - const increaseData: ISeriesData[] = []; - const decreaseData: ISeriesData[] = []; - const totalData: ISeriesData[] = []; - - let previousTotal = 0; - - transformedData.forEach((datum, index, self) => { - const totalSum = self.slice(0, index + 1).reduce((prev, cur, i) => { - if (breakdownName) { - if (cur[breakdownName] !== totalMark || i === 0) { - return prev + ((cur[metricLabel] as number) ?? 0); - } - } else if (cur[xAxisName] !== totalMark) { - return prev + ((cur[metricLabel] as number) ?? 0); - } - return prev; - }, 0); - - const isTotal = - (breakdownName && datum[breakdownName] === totalMark) || - datum[xAxisName] === totalMark; - - const originalValue = datum[metricLabel] as number; - let value = originalValue; - const oppositeSigns = Math.sign(previousTotal) !== Math.sign(totalSum); - if (oppositeSigns) { - value = Math.sign(value) * (Math.abs(value) - Math.abs(previousTotal)); - } - - if (isTotal) { - increaseData.push({ value: TOKEN }); - decreaseData.push({ value: TOKEN }); - totalData.push({ - value: totalSum, - originalValue: totalSum, - totalSum, - }); - } else if (value < 0) { - increaseData.push({ value: TOKEN }); - decreaseData.push({ - value: totalSum < 0 ? value : -value, - originalValue, - totalSum, - }); - totalData.push({ value: TOKEN }); - } else { - increaseData.push({ - value: totalSum > 0 ? value : -value, - originalValue, - totalSum, - }); - decreaseData.push({ value: TOKEN }); - totalData.push({ value: TOKEN }); - } - - const color = oppositeSigns - ? value > 0 - ? rgbToHex(increaseColor.r, increaseColor.g, increaseColor.b) - : rgbToHex(decreaseColor.r, decreaseColor.g, decreaseColor.b) - : 'transparent'; - - let opacity = 1; - if (legendState?.[legendNames.INCREASE] === false && value > 0) { - opacity = 0; - } else if (legendState?.[legendNames.DECREASE] === false && value < 0) { - opacity = 0; - } - - if (isTotal) { - assistData.push({ value: TOKEN }); - } else if (index === 0) { - assistData.push({ - value: 0, - }); - } else if (oppositeSigns || Math.abs(totalSum) > Math.abs(previousTotal)) { - assistData.push({ - value: previousTotal, - itemStyle: { color, opacity }, - }); - } else { - assistData.push({ - value: totalSum, - itemStyle: { color, opacity }, - }); - } - - previousTotal = totalSum; - }); - - const xAxisColumns: string[] = []; - const xAxisData = transformedData.map(row => { - let column = xAxisName; - let value = row[xAxisName]; - if (breakdownName && row[breakdownName] !== totalMark) { - column = breakdownName; - value = row[breakdownName]; - } - if (!value) { - value = NULL_STRING; - } - if (typeof value !== 'string' && typeof value !== 'number') { - value = String(value); - } - xAxisColumns.push(column); - return value; - }); - - const xAxisFormatter = (value: number | string, index: number) => { - if (value === totalMark) { - return totalMark; - } - if (coltypeMapping[xAxisColumns[index]] === GenericDataType.Temporal) { - if (typeof value === 'string') { - return getTimeFormatter(xAxisTimeFormat)(Number.parseInt(value, 10)); - } - return getTimeFormatter(xAxisTimeFormat)(value); - } - return String(value); - }; - - let axisLabel: { - rotate?: number; - hideOverlap?: boolean; - show?: boolean; - formatter?: typeof xAxisFormatter; - }; - if (xTicksLayout === '45°') { - axisLabel = { rotate: -45 }; - } else if (xTicksLayout === '90°') { - axisLabel = { rotate: -90 }; - } else if (xTicksLayout === 'flat') { - axisLabel = { rotate: 0 }; - } else if (xTicksLayout === 'staggered') { - axisLabel = { rotate: -45 }; - } else { - axisLabel = { show: true }; - } - axisLabel.formatter = xAxisFormatter; - axisLabel.hideOverlap = false; - - const seriesProps: Pick = { - type: 'bar', - stack: 'stack', - emphasis: { - disabled: true, - }, - }; - const labelProps = { - show: showValue, - formatter: seriesformatter, - color: theme.colorText, - borderColor: theme.colorBgBase, - borderWidth: 1, - }; - const barSeries: BarSeriesOption[] = [ - { - ...seriesProps, - name: ASSIST_MARK, - data: assistData, - }, - { - ...seriesProps, - name: legendNames.INCREASE, - label: { - ...labelProps, - position: 'top', - }, - itemStyle: { - color: rgbToHex(increaseColor.r, increaseColor.g, increaseColor.b), - }, - data: increaseData, - }, - { - ...seriesProps, - name: legendNames.DECREASE, - label: { - ...labelProps, - position: 'bottom', - }, - itemStyle: { - color: rgbToHex(decreaseColor.r, decreaseColor.g, decreaseColor.b), - }, - data: decreaseData, - }, - { - ...seriesProps, - name: legendNames.TOTAL, - label: { - ...labelProps, - position: 'top', - }, - itemStyle: { - color: rgbToHex(totalColor.r, totalColor.g, totalColor.b), - }, - data: totalData, - }, - ]; - - const echartOptions: EChartsOption = { - grid: { - ...defaultGrid, - top: theme.sizeUnit * 7, - bottom: theme.sizeUnit * 7, - left: theme.sizeUnit * 5, - right: theme.sizeUnit * 7, - }, - legend: { - show: showLegend, - selected: legendState, - data: [legendNames.INCREASE, legendNames.DECREASE, legendNames.TOTAL], - }, - xAxis: { - data: xAxisData, - type: 'category', - name: xAxisLabel, - nameTextStyle: { - padding: [theme.sizeUnit * 4, 0, 0, 0], - }, - nameLocation: 'middle', - axisLabel, - }, - yAxis: { - ...defaultYAxis, - type: 'value', - nameTextStyle: { - padding: [0, 0, theme.sizeUnit * 5, 0], - }, - nameLocation: 'middle', - name: yAxisLabel, - axisLabel: { formatter: defaultFormatter }, - }, - tooltip: { - ...getDefaultTooltip(refs), - appendToBody: true, - trigger: 'axis', - show: !inContextMenu, - formatter: (params: any) => - formatTooltip({ - params, - breakdownName, - defaultFormatter, - xAxisFormatter, - totalMark, - }), - }, - series: barSeries, - }; - - return { - refs, - formData, - width, - height, - echartOptions, - setDataMask, - onContextMenu, - onLegendStateChanged, - }; -} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/types.ts deleted file mode 100644 index 9ca53991486..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Waterfall/types.ts +++ /dev/null @@ -1,76 +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 { - ChartDataResponseResult, - ChartProps, - QueryFormColumn, - QueryFormData, - QueryFormMetric, - RgbaColor, -} from '@superset-ui/core'; -import type { BarDataItemOption } from 'echarts/types/src/chart/bar/BarSeries'; -import type { CallbackDataParams } from 'echarts/types/src/util/types'; -import { BaseTransformedProps, LegendFormData } from '../types'; - -export type WaterfallFormXTicksLayout = - | '45°' - | '90°' - | 'auto' - | 'flat' - | 'staggered'; - -export type ISeriesData = { - originalValue?: number; - totalSum?: number; -} & BarDataItemOption; - -export type ICallbackDataParams = CallbackDataParams & { - axisValueLabel: string; - data: ISeriesData; -}; - -export type EchartsWaterfallFormData = QueryFormData & - LegendFormData & { - increaseColor: RgbaColor; - decreaseColor: RgbaColor; - totalColor: RgbaColor; - metric: QueryFormMetric; - xAxis: QueryFormColumn; - xAxisLabel: string; - xAxisTimeFormat?: string; - xTicksLayout?: WaterfallFormXTicksLayout; - yAxisLabel: string; - yAxisFormat: string; - increaseLabel?: string; - decreaseLabel?: string; - totalLabel?: string; - showTotal: boolean; - }; - -export const DEFAULT_FORM_DATA: Partial = { - showLegend: true, -}; - -export interface EchartsWaterfallChartProps extends ChartProps { - formData: EchartsWaterfallFormData; - queriesData: ChartDataResponseResult[]; -} - -export type WaterfallChartTransformedProps = - BaseTransformedProps; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/index.ts index abb57d9e6e8..ee485b00181 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/index.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/index.ts @@ -19,10 +19,10 @@ export { default as EchartsBoxPlotChartPlugin } from './BoxPlot'; export { default as EchartsTimeseriesChartPlugin } from './Timeseries'; export { default as EchartsAreaChartPlugin } from './Timeseries/Area'; -export { default as EchartsTimeseriesBarChartPlugin } from './Timeseries/Regular/Bar'; -export { default as EchartsTimeseriesLineChartPlugin } from './Timeseries/Regular/Line'; -export { default as EchartsTimeseriesScatterChartPlugin } from './Timeseries/Regular/Scatter'; -export { default as EchartsTimeseriesSmoothLineChartPlugin } from './Timeseries/Regular/SmoothLine'; +export { default as EchartsTimeseriesBarChartPlugin } from './Timeseries/Bar'; +export { default as EchartsTimeseriesLineChartPlugin } from './Timeseries/Line'; +export { default as EchartsTimeseriesScatterChartPlugin } from './Timeseries/Scatter'; +export { default as EchartsTimeseriesSmoothLineChartPlugin } from './Timeseries/SmoothLine'; export { default as EchartsTimeseriesStepChartPlugin } from './Timeseries/Step'; export { default as EchartsMixedTimeseriesChartPlugin } from './MixedTimeseries'; export { default as EchartsPieChartPlugin } from './Pie'; @@ -38,6 +38,7 @@ export { BigNumberChartPlugin, BigNumberTotalChartPlugin, BigNumberPeriodOverPeriodChartPlugin, + BigNumberGlyphChartPlugin, } from './BigNumber'; export { default as EchartsSunburstChartPlugin } from './Sunburst'; export { default as EchartsBubbleChartPlugin } from './Bubble'; @@ -45,35 +46,9 @@ export { default as EchartsSankeyChartPlugin } from './Sankey'; export { default as EchartsWaterfallChartPlugin } from './Waterfall'; export { default as EchartsGanttChartPlugin } from './Gantt'; -export { default as BoxPlotTransformProps } from './BoxPlot/transformProps'; -export { default as FunnelTransformProps } from './Funnel/transformProps'; -export { default as GaugeTransformProps } from './Gauge/transformProps'; -export { default as GraphTransformProps } from './Graph/transformProps'; -export { default as MixedTimeseriesTransformProps } from './MixedTimeseries/transformProps'; -export { default as PieTransformProps } from './Pie/transformProps'; -export { default as RadarTransformProps } from './Radar/transformProps'; -export { default as TimeseriesTransformProps } from './Timeseries/transformProps'; -export { default as TreeTransformProps } from './Tree/transformProps'; -export { default as TreemapTransformProps } from './Treemap/transformProps'; -export { default as HeatmapTransformProps } from './Heatmap/transformProps'; -export { default as SunburstTransformProps } from './Sunburst/transformProps'; -export { default as BubbleTransformProps } from './Bubble/transformProps'; -export { default as WaterfallTransformProps } from './Waterfall/transformProps'; -export { default as HistogramTransformProps } from './Histogram/transformProps'; -export { default as SankeyTransformProps } from './Sankey/transformProps'; -export { default as GanttTransformProps } from './Gantt/transformProps'; - export { DEFAULT_FORM_DATA as TimeseriesDefaultFormData } from './Timeseries/constants'; export * from './utils/eChartOptionsSchema'; export * from './utils/safeEChartOptionsParser'; export * from './types'; - -/** - * Note: this file exports the default export from EchartsTimeseries.tsx. - * If you want to export multiple visualization modules, you will need to - * either add additional plugin folders (similar in structure to ./plugin) - * OR export multiple instances of `ChartPlugin` extensions in ./plugin/index.ts - * which in turn load exports from EchartsTimeseries.tsx - */ diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/BigNumber/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/BigNumber/transformProps.test.ts deleted file mode 100644 index 08912e4cbfb..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/test/BigNumber/transformProps.test.ts +++ /dev/null @@ -1,561 +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 { DatasourceType, TimeGranularity, VizType } from '@superset-ui/core'; -import { supersetTheme } from '@apache-superset/core/theme'; -import transformProps from '../../src/BigNumber/BigNumberWithTrendline/transformProps'; -import { - BigNumberDatum, - BigNumberWithTrendlineChartProps, - BigNumberWithTrendlineFormData, -} from '../../src/BigNumber/types'; -import { TIMESERIES_CONSTANTS } from '../../src/constants'; - -const formData = { - metric: 'value', - colorPicker: { - r: 0, - g: 122, - b: 135, - a: 1, - }, - compareLag: 1, - xAxis: '__timestamp', - timeGrainSqla: TimeGranularity.QUARTER, - granularitySqla: 'ds', - compareSuffix: 'over last quarter', - viz_type: VizType.BigNumber, - yAxisFormat: '.3s', - datasource: 'test_datasource', -}; - -const rawFormData: BigNumberWithTrendlineFormData = { - colorPicker: { b: 0, g: 0, r: 0 }, - datasource: '1__table', - metric: 'value', - color_picker: { - r: 0, - g: 122, - b: 135, - a: 1, - }, - compare_lag: 1, - x_axis: '__timestamp', - time_grain_sqla: TimeGranularity.QUARTER, - granularity_sqla: 'ds', - compare_suffix: 'over last quarter', - viz_type: VizType.BigNumber, - y_axis_format: '.3s', - xAxis: '__timestamp', -}; - -function generateProps( - data: BigNumberDatum[], - extraFormData = {}, - extraQueryData: any = {}, -): BigNumberWithTrendlineChartProps { - return { - width: 200, - height: 500, - annotationData: {}, - datasource: { - id: 0, - name: '', - type: DatasourceType.Table, - columns: [], - metrics: [], - columnFormats: {}, - verboseMap: {}, - }, - rawDatasource: {}, - rawFormData, - hooks: {}, - initialValues: {}, - formData: { - ...formData, - ...extraFormData, - }, - queriesData: [ - { - data, - ...extraQueryData, - }, - ], - ownState: {}, - filterState: {}, - behaviors: [], - theme: supersetTheme, - }; -} - -describe('BigNumberWithTrendline', () => { - const props = generateProps( - [ - { - __timestamp: 0, - value: 1.2345, - }, - { - __timestamp: 100, - value: null, - }, - ], - { showTrendLine: true }, - ); - - describe('transformProps()', () => { - test('should fallback and format time', () => { - const transformed = transformProps(props); - // the first item is the last item sorted by __timestamp - const lastDatum = transformed.trendLineData?.pop(); - - // should use last available value - expect(lastDatum?.[0]).toStrictEqual(100); - expect(lastDatum?.[1]).toBeNull(); - - // should get the last non-null value - expect(transformed.bigNumber).toStrictEqual(1.2345); - // bigNumberFallback is only set when bigNumber is null after aggregation - expect(transformed.bigNumberFallback).toBeNull(); - - // should successfully formatTime by granularity - // @ts-expect-error - expect(transformed.formatTime(new Date('2020-01-01'))).toStrictEqual( - '2020-01-01 00:00:00', - ); - }); - - test('should respect datasource d3 format', () => { - const propsWithDatasource = { - ...props, - datasource: { - ...props.datasource, - metrics: [ - { - label: 'value', - metric_name: 'value', - d3format: '.2f', - uuid: '1', - }, - ], - }, - }; - const transformed = transformProps(propsWithDatasource); - // @ts-expect-error - expect(transformed.headerFormatter(transformed.bigNumber)).toStrictEqual( - '1.23', - ); - }); - - test('should format with datasource currency', () => { - const propsWithDatasource = { - ...props, - datasource: { - ...props.datasource, - currencyFormats: { - value: { symbol: 'USD', symbolPosition: 'prefix' }, - }, - metrics: [ - { - label: 'value', - metric_name: 'value', - d3format: '.2f', - currency: { symbol: 'USD', symbolPosition: 'prefix' }, - uuid: '1', - }, - ], - }, - }; - const transformed = transformProps(propsWithDatasource); - // @ts-expect-error - expect(transformed.headerFormatter(transformed.bigNumber)).toStrictEqual( - '$ 1.23', - ); - }); - - test('should show X axis when showXAxis is true', () => { - const transformed = transformProps({ - ...props, - formData: { - ...props.formData, - showXAxis: true, - }, - }); - expect((transformed.echartOptions!.xAxis as { show: boolean }).show).toBe( - true, - ); - }); - - test('should not show X axis when showXAxis is false', () => { - const transformed = transformProps({ - ...props, - formData: { - ...props.formData, - showXAxis: false, - }, - }); - expect((transformed.echartOptions!.xAxis as { show: boolean }).show).toBe( - false, - ); - }); - - test('should show Y axis when showYAxis is true', () => { - const transformed = transformProps({ - ...props, - formData: { - ...props.formData, - showYAxis: true, - }, - }); - expect((transformed.echartOptions!.yAxis as { show: boolean }).show).toBe( - true, - ); - }); - - test('should not show Y axis when showYAxis is false', () => { - const transformed = transformProps({ - ...props, - formData: { - ...props.formData, - showYAxis: false, - }, - }); - expect((transformed.echartOptions!.yAxis as { show: boolean }).show).toBe( - false, - ); - }); - }); - - test('should respect min/max label visibility settings', () => { - const transformed = transformProps({ - ...props, - formData: { - ...props.formData, - showXAxisMinMaxLabels: false, - showYAxisMinMaxLabels: true, - }, - }); - const xAxis = transformed.echartOptions?.xAxis as any; - const yAxis = transformed.echartOptions?.yAxis as any; - - expect(xAxis.axisLabel.showMinLabel).toBe(false); - expect(xAxis.axisLabel.showMaxLabel).toBe(false); - expect(yAxis.axisLabel.showMinLabel).toBe(true); - expect(yAxis.axisLabel.showMaxLabel).toBe(true); - }); - - test('should use minimal grid when both axes are hidden', () => { - const transformed = transformProps({ - ...props, - formData: { - ...props.formData, - showXAxis: false, - showYAxis: false, - }, - }); - - expect(transformed.echartOptions?.grid).toEqual({ - bottom: 0, - left: 0, - right: 0, - top: 0, - }); - }); - - test('should use expanded grid when either axis is shown', () => { - const expandedGrid = { - containLabel: true, - bottom: TIMESERIES_CONSTANTS.gridOffsetBottom, - left: TIMESERIES_CONSTANTS.gridOffsetLeft, - right: TIMESERIES_CONSTANTS.gridOffsetRight, - top: TIMESERIES_CONSTANTS.gridOffsetTop, - }; - - expect( - transformProps({ - ...props, - formData: { - ...props.formData, - showXAxis: true, - showYAxis: false, - }, - }).echartOptions?.grid, - ).toEqual(expandedGrid); - expect( - transformProps({ - ...props, - formData: { - ...props.formData, - showXAxis: false, - showYAxis: true, - }, - }).echartOptions?.grid, - ).toEqual(expandedGrid); - expect( - transformProps({ - ...props, - formData: { - ...props.formData, - showXAxis: true, - showYAxis: true, - }, - }).echartOptions?.grid, - ).toEqual(expandedGrid); - }); -}); - -describe('BigNumberWithTrendline - Aggregation Tests', () => { - const baseProps = { - width: 800, - height: 600, - formData: { - colorPicker: { r: 0, g: 0, b: 0, a: 1 }, - metric: 'metric', - aggregation: 'LAST_VALUE', - }, - queriesData: [ - { - data: [ - { __timestamp: 1607558400000, metric: 10 }, - { __timestamp: 1607558500000, metric: 30 }, - { __timestamp: 1607558600000, metric: 50 }, - { __timestamp: 1607558700000, metric: 60 }, - ], - colnames: ['__timestamp', 'metric'], - coltypes: ['TIMESTAMP', 'BIGINT'], - }, - ], - hooks: {}, - filterState: {}, - datasource: { - columnFormats: {}, - currencyFormats: {}, - }, - rawDatasource: {}, - rawFormData: {}, - theme: { - colors: { - grayscale: { - light5: '#fafafa', - }, - }, - }, - } as unknown as BigNumberWithTrendlineChartProps; - - const propsWithEvenData = { - ...baseProps, - queriesData: [ - { - data: [ - { __timestamp: 1607558400000, metric: 10 }, - { __timestamp: 1607558500000, metric: 20 }, - { __timestamp: 1607558600000, metric: 30 }, - { __timestamp: 1607558700000, metric: 40 }, - ], - colnames: ['__timestamp', 'metric'], - coltypes: ['TIMESTAMP', 'BIGINT'], - }, - ], - } as unknown as BigNumberWithTrendlineChartProps; - - test('should correctly calculate SUM', () => { - const props = { - ...baseProps, - formData: { ...baseProps.formData, aggregation: 'sum' }, - queriesData: [ - baseProps.queriesData[0], - { - data: [{ metric: 150 }], - colnames: ['metric'], - coltypes: ['BIGINT'], - }, - ], - } as unknown as BigNumberWithTrendlineChartProps; - - const transformed = transformProps(props); - expect(transformed.bigNumber).toStrictEqual(150); - }); - - test('should correctly calculate AVG', () => { - const props = { - ...baseProps, - formData: { ...baseProps.formData, aggregation: 'mean' }, - queriesData: [ - baseProps.queriesData[0], - { - data: [{ metric: 37.5 }], - colnames: ['metric'], - coltypes: ['BIGINT'], - }, - ], - } as unknown as BigNumberWithTrendlineChartProps; - - const transformed = transformProps(props); - expect(transformed.bigNumber).toStrictEqual(37.5); - }); - - test('should correctly calculate MIN', () => { - const props = { - ...baseProps, - formData: { ...baseProps.formData, aggregation: 'min' }, - queriesData: [ - baseProps.queriesData[0], - { - data: [{ metric: 10 }], - colnames: ['metric'], - coltypes: ['BIGINT'], - }, - ], - } as unknown as BigNumberWithTrendlineChartProps; - - const transformed = transformProps(props); - expect(transformed.bigNumber).toStrictEqual(10); - }); - - test('should correctly calculate MAX', () => { - const props = { - ...baseProps, - formData: { ...baseProps.formData, aggregation: 'max' }, - queriesData: [ - baseProps.queriesData[0], - { - data: [{ metric: 60 }], - colnames: ['metric'], - coltypes: ['BIGINT'], - }, - ], - } as unknown as BigNumberWithTrendlineChartProps; - - const transformed = transformProps(props); - expect(transformed.bigNumber).toStrictEqual(60); - }); - - test('should correctly calculate MEDIAN (odd count)', () => { - const oddCountProps = { - ...baseProps, - queriesData: [ - { - data: [ - { __timestamp: 1607558300000, metric: 10 }, - { __timestamp: 1607558400000, metric: 20 }, - { __timestamp: 1607558500000, metric: 30 }, - { __timestamp: 1607558600000, metric: 40 }, - { __timestamp: 1607558700000, metric: 50 }, - ], - colnames: ['__timestamp', 'metric'], - coltypes: ['TIMESTAMP', 'BIGINT'], - }, - ], - } as unknown as BigNumberWithTrendlineChartProps; - - const props = { - ...oddCountProps, - formData: { ...oddCountProps.formData, aggregation: 'median' }, - queriesData: [ - oddCountProps.queriesData[0], - { - data: [{ metric: 30 }], - colnames: ['metric'], - coltypes: ['BIGINT'], - }, - ], - } as unknown as BigNumberWithTrendlineChartProps; - - const transformed = transformProps(props); - expect(transformed.bigNumber).toStrictEqual(30); - }); - - test('should correctly calculate MEDIAN (even count)', () => { - const props = { - ...propsWithEvenData, - formData: { ...propsWithEvenData.formData, aggregation: 'median' }, - queriesData: [ - propsWithEvenData.queriesData[0], - { - data: [{ metric: 25 }], - colnames: ['metric'], - coltypes: ['BIGINT'], - }, - ], - } as unknown as BigNumberWithTrendlineChartProps; - - const transformed = transformProps(props); - expect(transformed.bigNumber).toStrictEqual(25); - }); - - test('should return the LAST_VALUE correctly', () => { - const transformed = transformProps(baseProps); - expect(transformed.bigNumber).toStrictEqual(10); - }); -}); - -test('BigNumberWithTrendline AUTO mode should detect single currency', () => { - const props = generateProps( - [ - { __timestamp: 1607558400000, value: 1000, currency_code: 'USD' }, - { __timestamp: 1607558500000, value: 2000, currency_code: 'USD' }, - ], - { - yAxisFormat: ',.2f', - currencyFormat: { symbol: 'AUTO', symbolPosition: 'prefix' }, - }, - ); - props.datasource.currencyCodeColumn = 'currency_code'; - - const transformed = transformProps(props); - // The headerFormatter should include $ for USD - expect(transformed.headerFormatter(1000)).toContain('$'); -}); - -test('BigNumberWithTrendline AUTO mode should use neutral formatting for mixed currencies', () => { - const props = generateProps( - [ - { __timestamp: 1607558400000, value: 1000, currency_code: 'USD' }, - { __timestamp: 1607558500000, value: 2000, currency_code: 'EUR' }, - ], - { - yAxisFormat: ',.2f', - currencyFormat: { symbol: 'AUTO', symbolPosition: 'prefix' }, - }, - ); - props.datasource.currencyCodeColumn = 'currency_code'; - - const transformed = transformProps(props); - // With mixed currencies, should not show currency symbol - const formatted = transformed.headerFormatter(1000); - expect(formatted).not.toContain('$'); - expect(formatted).not.toContain('€'); -}); - -test('BigNumberWithTrendline should preserve static currency format', () => { - const props = generateProps( - [ - { __timestamp: 1607558400000, value: 1000, currency_code: 'USD' }, - { __timestamp: 1607558500000, value: 2000, currency_code: 'EUR' }, - ], - { - yAxisFormat: ',.2f', - currencyFormat: { symbol: 'GBP', symbolPosition: 'prefix' }, - }, - ); - props.datasource.currencyCodeColumn = 'currency_code'; - - const transformed = transformProps(props); - // Static mode should always show £ - expect(transformed.headerFormatter(1000)).toContain('£'); -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/BoxPlot/buildQuery.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/BoxPlot/buildQuery.test.ts deleted file mode 100644 index e7633e421a4..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/test/BoxPlot/buildQuery.test.ts +++ /dev/null @@ -1,62 +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 { - isPostProcessingBoxplot, - PostProcessingBoxplot, -} from '@superset-ui/core'; -import { DEFAULT_TITLE_FORM_DATA } from '../../src/constants'; -import buildQuery from '../../src/BoxPlot/buildQuery'; -import { BoxPlotQueryFormData } from '../../src/BoxPlot/types'; - -describe('BoxPlot buildQuery', () => { - const formData: BoxPlotQueryFormData = { - ...DEFAULT_TITLE_FORM_DATA, - columns: [], - datasource: '5__table', - granularity_sqla: 'ds', - groupby: ['bar'], - metrics: ['foo'], - time_grain_sqla: 'P1Y', - viz_type: 'my_chart', - whiskerOptions: 'Tukey', - yAxisFormat: 'SMART_NUMBER', - }; - - test('should build timeseries when series columns is empty', () => { - const queryContext = buildQuery(formData); - const [query] = queryContext.queries; - expect(query.metrics).toEqual(['foo']); - expect(query.columns).toEqual(['ds', 'bar']); - expect(query.series_columns).toEqual(['bar']); - const [rule] = query.post_processing || []; - expect(isPostProcessingBoxplot(rule)).toEqual(true); - expect((rule as PostProcessingBoxplot)?.options?.groupby).toEqual(['bar']); - }); - - test('should build non-timeseries query object when columns is defined', () => { - const queryContext = buildQuery({ ...formData, columns: ['qwerty'] }); - const [query] = queryContext.queries; - expect(query.metrics).toEqual(['foo']); - expect(query.columns).toEqual(['qwerty', 'bar']); - expect(query.series_columns).toEqual(['bar']); - const [rule] = query.post_processing || []; - expect(isPostProcessingBoxplot(rule)).toEqual(true); - expect((rule as PostProcessingBoxplot)?.options?.groupby).toEqual(['bar']); - }); -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/BoxPlot/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/BoxPlot/transformProps.test.ts deleted file mode 100644 index cdf52dd1ae8..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/test/BoxPlot/transformProps.test.ts +++ /dev/null @@ -1,128 +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 { ChartProps, SqlaFormData } from '@superset-ui/core'; -import { supersetTheme } from '@apache-superset/core/theme'; -import { EchartsBoxPlotChartProps } from '../../src/BoxPlot/types'; -import transformProps from '../../src/BoxPlot/transformProps'; - -describe('BoxPlot transformProps', () => { - const formData: SqlaFormData = { - datasource: '5__table', - granularity_sqla: 'ds', - time_grain_sqla: 'P1Y', - columns: [], - metrics: ['AVG(averageprice)'], - groupby: ['type', 'region'], - whiskerOptions: 'Tukey', - yAxisFormat: 'SMART_NUMBER', - viz_type: 'my_chart', - zoomable: true, - }; - const chartProps = new ChartProps({ - formData, - width: 800, - height: 600, - queriesData: [ - { - data: [ - { - type: 'organic', - region: 'Charlotte', - 'AVG(averageprice)__mean': 1.9405512820512825, - 'AVG(averageprice)__median': 1.9025, - 'AVG(averageprice)__max': 2.505, - 'AVG(averageprice)__min': 1.4775, - 'AVG(averageprice)__q1': 1.73875, - 'AVG(averageprice)__q3': 2.105, - 'AVG(averageprice)__count': 39, - 'AVG(averageprice)__outliers': [2.735], - }, - { - type: 'organic', - region: 'Hartford Springfield', - 'AVG(averageprice)__mean': 2.231141025641026, - 'AVG(averageprice)__median': 2.265, - 'AVG(averageprice)__max': 2.595, - 'AVG(averageprice)__min': 1.862, - 'AVG(averageprice)__q1': 2.1285, - 'AVG(averageprice)__q3': 2.32625, - 'AVG(averageprice)__count': 39, - 'AVG(averageprice)__outliers': [], - }, - ], - }, - ], - theme: supersetTheme, - }); - - test('should transform chart props for viz', () => { - expect(transformProps(chartProps as EchartsBoxPlotChartProps)).toEqual( - expect.objectContaining({ - width: 800, - height: 600, - echartOptions: expect.objectContaining({ - dataZoom: expect.arrayContaining([ - { - moveOnMouseWheel: true, - type: 'inside', - zoomOnMouseWheel: false, - }, - ]), - series: expect.arrayContaining([ - expect.objectContaining({ - name: 'boxplot', - data: expect.arrayContaining([ - expect.objectContaining({ - name: 'organic, Charlotte', - value: [ - 1.4775, - 1.73875, - 1.9025, - 2.105, - 2.505, - 1.9405512820512825, - 39, - [2.735], - ], - }), - expect.objectContaining({ - name: 'organic, Hartford Springfield', - value: [ - 1.862, - 2.1285, - 2.265, - 2.32625, - 2.595, - 2.231141025641026, - 39, - [], - ], - }), - ]), - }), - expect.objectContaining({ - name: 'outlier', - data: [['organic, Charlotte', 2.735]], - }), - ]), - }), - }), - ); - }); -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Bubble/buildQuery.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Bubble/buildQuery.test.ts deleted file mode 100644 index 05a617b2365..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Bubble/buildQuery.test.ts +++ /dev/null @@ -1,93 +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 buildQuery from '../../src/Bubble/buildQuery'; - -describe('Bubble buildQuery', () => { - const formData = { - datasource: '1__table', - viz_type: 'echarts_bubble', - entity: 'customer_name', - x: 'count', - y: { - aggregate: 'sum', - column: { - column_name: 'price_each', - }, - expressionType: 'simple', - label: 'SUM(price_each)', - }, - size: { - aggregate: 'sum', - column: { - column_name: 'sales', - }, - expressionType: 'simple', - label: 'SUM(sales)', - }, - }; - - test('Should build query without dimension', () => { - const queryContext = buildQuery(formData); - const [query] = queryContext.queries; - expect(query.columns).toEqual(['customer_name']); - expect(query.metrics).toEqual([ - 'count', - { - aggregate: 'sum', - column: { - column_name: 'price_each', - }, - expressionType: 'simple', - label: 'SUM(price_each)', - }, - { - aggregate: 'sum', - column: { - column_name: 'sales', - }, - expressionType: 'simple', - label: 'SUM(sales)', - }, - ]); - }); - test('Should build query with dimension', () => { - const queryContext = buildQuery({ ...formData, series: 'state' }); - const [query] = queryContext.queries; - expect(query.columns).toEqual(['customer_name', 'state']); - expect(query.metrics).toEqual([ - 'count', - { - aggregate: 'sum', - column: { - column_name: 'price_each', - }, - expressionType: 'simple', - label: 'SUM(price_each)', - }, - { - aggregate: 'sum', - column: { - column_name: 'sales', - }, - expressionType: 'simple', - label: 'SUM(sales)', - }, - ]); - }); -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Bubble/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Bubble/transformProps.test.ts deleted file mode 100644 index 297fbad5656..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Bubble/transformProps.test.ts +++ /dev/null @@ -1,251 +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 { - ChartProps, - ChartPropsConfig, - getNumberFormatter, - SqlaFormData, -} from '@superset-ui/core'; -import { supersetTheme } from '@apache-superset/core/theme'; -import { EchartsBubbleChartProps } from '../../src/Bubble/types'; -import transformProps, { formatTooltip } from '../../src/Bubble/transformProps'; - -const defaultFormData: SqlaFormData = { - datasource: '1__table', - viz_type: 'echarts_bubble', - entity: 'customer_name', - x: 'count', - y: { - aggregate: 'sum', - column: { - column_name: 'price_each', - }, - expressionType: 'simple', - label: 'SUM(price_each)', - }, - size: { - aggregate: 'sum', - column: { - column_name: 'sales', - }, - expressionType: 'simple', - label: 'SUM(sales)', - }, - xAxisBounds: [null, null], - yAxisBounds: [null, null], -}; - -const queriesData = [ - { - data: [ - { - customer_name: 'AV Stores, Co.', - count: 10, - 'SUM(price_each)': 20, - 'SUM(sales)': 30, - }, - { - customer_name: 'Alpha Cognac', - count: 40, - 'SUM(price_each)': 50, - 'SUM(sales)': 60, - }, - { - customer_name: 'Amica Models & Co.', - count: 70, - 'SUM(price_each)': 80, - 'SUM(sales)': 90, - }, - ], - }, -]; - -const chartConfig: ChartPropsConfig = { - formData: defaultFormData, - height: 800, - width: 800, - queriesData, - theme: supersetTheme, -}; - -describe('Bubble transformProps', () => { - test('Should transform props for viz', () => { - const chartProps = new ChartProps(chartConfig); - expect(transformProps(chartProps as EchartsBubbleChartProps)).toEqual( - expect.objectContaining({ - width: 800, - height: 800, - echartOptions: expect.objectContaining({ - series: expect.arrayContaining([ - expect.objectContaining({ - data: expect.arrayContaining([ - [10, 20, 30, 'AV Stores, Co.', null], - ]), - }), - expect.objectContaining({ - data: expect.arrayContaining([ - [40, 50, 60, 'Alpha Cognac', null], - ]), - }), - expect.objectContaining({ - data: expect.arrayContaining([ - [70, 80, 90, 'Amica Models & Co.', null], - ]), - }), - ]), - }), - }), - ); - }); - - test('Should transform props with undefined control values', () => { - const formData: SqlaFormData = { - ...defaultFormData, - xAxisBounds: undefined, - yAxisBounds: undefined, - }; - const chartProps = new ChartProps({ - ...chartConfig, - formData, - }); - - expect(transformProps(chartProps as EchartsBubbleChartProps)).toEqual( - expect.objectContaining({ - width: 800, - height: 800, - echartOptions: expect.objectContaining({ - series: expect.arrayContaining([ - expect.objectContaining({ - data: expect.arrayContaining([ - [10, 20, 30, 'AV Stores, Co.', null], - ]), - }), - expect.objectContaining({ - data: expect.arrayContaining([ - [40, 50, 60, 'Alpha Cognac', null], - ]), - }), - expect.objectContaining({ - data: expect.arrayContaining([ - [70, 80, 90, 'Amica Models & Co.', null], - ]), - }), - ]), - }), - }), - ); - }); -}); - -describe('Bubble formatTooltip', () => { - const dollerFormatter = getNumberFormatter('$,.2f'); - const percentFormatter = getNumberFormatter(',.1%'); - - test('Should generate correct bubble label content with dimension', () => { - const params = { - data: [10000, 20000, 3, 'bubble title', 'bubble dimension'], - }; - - const html = formatTooltip( - params, - 'x-axis-label', - 'y-axis-label', - 'size-label', - dollerFormatter, - dollerFormatter, - percentFormatter, - ); - expect(html).toContain('bubble title'); - expect(html).toContain('bubble dimension'); - expect(html).toContain('x-axis-label'); - expect(html).toContain('y-axis-label'); - expect(html).toContain('size-label'); - expect(html).toContain('$10,000.00'); - expect(html).toContain('$20,000.00'); - expect(html).toContain('300.0%'); - }); - test('Should generate correct bubble label content without dimension', () => { - const params = { - data: [10000, 25000, 3, 'bubble title', null], - }; - const html = formatTooltip( - params, - 'x-axis-label', - 'y-axis-label', - 'size-label', - dollerFormatter, - dollerFormatter, - percentFormatter, - ); - expect(html).toContain('bubble title'); - expect(html).not.toContain('bubble dimension'); - expect(html).toContain('x-axis-label'); - expect(html).toContain('y-axis-label'); - expect(html).toContain('size-label'); - expect(html).toContain('$10,000.00'); - expect(html).toContain('$25,000.00'); - expect(html).toContain('300.0%'); - }); -}); - -describe('legend sorting', () => { - const createChartProps = (overrides = {}) => - new ChartProps({ - ...chartConfig, - formData: { - ...defaultFormData, - ...overrides, - }, - }); - test('preserves original data order when no sort specified', () => { - const props = createChartProps({ legendSort: null }); - const result = transformProps(props as EchartsBubbleChartProps); - - const legendData = (result.echartOptions.legend as any).data; - expect(legendData).toEqual([ - 'AV Stores, Co.', - 'Alpha Cognac', - 'Amica Models & Co.', - ]); - }); - - test('sorts alphabetically ascending when legendSort is "asc"', () => { - const props = createChartProps({ legendSort: 'asc' }); - const result = transformProps(props as EchartsBubbleChartProps); - - const legendData = (result.echartOptions.legend as any).data; - expect(legendData).toEqual([ - 'Alpha Cognac', - 'Amica Models & Co.', - 'AV Stores, Co.', - ]); - }); - - test('sorts alphabetically descending when legendSort is "desc"', () => { - const props = createChartProps({ legendSort: 'desc' }); - const result = transformProps(props as EchartsBubbleChartProps); - - const legendData = (result.echartOptions.legend as any).data; - expect(legendData).toEqual([ - 'AV Stores, Co.', - 'Amica Models & Co.', - 'Alpha Cognac', - ]); - }); -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Funnel/buildQuery.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Funnel/buildQuery.test.ts deleted file mode 100644 index 6d63513a35f..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Funnel/buildQuery.test.ts +++ /dev/null @@ -1,36 +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 buildQuery from '../../src/Funnel/buildQuery'; - -describe('Funnel buildQuery', () => { - const formData = { - datasource: '5__table', - granularity_sqla: 'ds', - metric: 'foo', - groupby: ['bar'], - viz_type: 'my_chart', - }; - - test('should build query fields from form data', () => { - const queryContext = buildQuery(formData); - const [query] = queryContext.queries; - expect(query.metrics).toEqual(['foo']); - expect(query.columns).toEqual(['bar']); - }); -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Funnel/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Funnel/transformProps.test.ts deleted file mode 100644 index b2e36c42bba..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Funnel/transformProps.test.ts +++ /dev/null @@ -1,169 +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 { ChartProps, getNumberFormatter } from '@superset-ui/core'; -import { supersetTheme } from '@apache-superset/core/theme'; -import transformProps, { parseParams } from '../../src/Funnel/transformProps'; -import { - EchartsFunnelChartProps, - PercentCalcType, -} from '../../src/Funnel/types'; - -const formData = { - colorScheme: 'bnbColors', - datasource: '3__table', - granularity_sqla: 'ds', - metric: 'sum__num', - groupby: ['foo', 'bar'], -}; -const queriesData = [ - { - data: [ - { foo: 'Sylvester', bar: 1, sum__num: 10 }, - { foo: 'Arnold', bar: 2, sum__num: 2.5 }, - ], - }, -]; -const chartProps = new ChartProps({ - formData, - width: 800, - height: 600, - queriesData, - theme: supersetTheme, -}); - -describe('Funnel transformProps', () => { - test('should transform chart props for viz', () => { - expect(transformProps(chartProps as EchartsFunnelChartProps)).toEqual( - expect.objectContaining({ - width: 800, - height: 600, - echartOptions: expect.objectContaining({ - series: [ - expect.objectContaining({ - data: expect.arrayContaining([ - expect.objectContaining({ - name: 'Arnold, 2', - value: 2.5, - }), - expect.objectContaining({ - name: 'Sylvester, 1', - value: 10, - }), - ]), - }), - ], - }), - }), - ); - }); -}); - -describe('formatFunnelLabel', () => { - test('should generate a valid funnel chart label', () => { - const numberFormatter = getNumberFormatter(); - const params = { - name: 'My Label', - value: 1234, - percent: 12.34, - data: { firstStepPercent: 0.5, prevStepPercent: 0.85 }, - }; - expect( - parseParams({ - params, - numberFormatter, - percentCalculationType: PercentCalcType.Total, - }), - ).toEqual(['My Label', '1.23k', '12.34%']); - expect( - parseParams({ - params, - numberFormatter, - percentCalculationType: PercentCalcType.FirstStep, - }), - ).toEqual(['My Label', '1.23k', '50.00%']); - expect( - parseParams({ - params, - numberFormatter, - percentCalculationType: PercentCalcType.PreviousStep, - }), - ).toEqual(['My Label', '1.23k', '85.00%']); - expect( - parseParams({ - params: { ...params, name: '' }, - numberFormatter, - percentCalculationType: PercentCalcType.Total, - }), - ).toEqual(['', '1.23k', '12.34%']); - expect( - parseParams({ - params: { ...params, name: '' }, - numberFormatter, - percentCalculationType: PercentCalcType.Total, - sanitizeName: true, - }), - ).toEqual(['<NULL>', '1.23k', '12.34%']); - }); -}); - -describe('legend sorting', () => { - const legendQueriesData = [ - { - data: [ - { foo: 'Sylvester', sum__num: 10 }, - { foo: 'Arnold', sum__num: 2.5 }, - { foo: 'Mark', sum__num: 13 }, - ], - }, - ]; - const createChartProps = (overrides = {}) => - new ChartProps({ - ...chartProps, - formData: { - ...formData, - groupby: ['foo'], - ...overrides, - }, - queriesData: legendQueriesData, - }); - - test('preserves original data order when no sort specified', () => { - const props = createChartProps({ legendSort: null }); - const result = transformProps(props as EchartsFunnelChartProps); - - const legendData = (result.echartOptions.legend as any).data; - expect(legendData).toEqual(['Sylvester', 'Arnold', 'Mark']); - }); - - test('sorts alphabetically ascending when legendSort is "asc"', () => { - const props = createChartProps({ legendSort: 'asc' }); - const result = transformProps(props as EchartsFunnelChartProps); - - const legendData = (result.echartOptions.legend as any).data; - expect(legendData).toEqual(['Arnold', 'Mark', 'Sylvester']); - }); - - test('sorts alphabetically descending when legendSort is "desc"', () => { - const props = createChartProps({ legendSort: 'desc' }); - const result = transformProps(props as EchartsFunnelChartProps); - - const legendData = (result.echartOptions.legend as any).data; - expect(legendData).toEqual(['Sylvester', 'Mark', 'Arnold']); - }); -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Gantt/buildQuery.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Gantt/buildQuery.test.ts deleted file mode 100644 index f8aa7c41ba2..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Gantt/buildQuery.test.ts +++ /dev/null @@ -1,64 +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 { QueryFormData } from '@superset-ui/core'; -import buildQuery from '../../src/Gantt/buildQuery'; - -describe('Gantt buildQuery', () => { - const formData: QueryFormData = { - datasource: '1__table', - viz_type: 'gantt_chart', - start_time: 'start_time', - end_time: 'end_time', - y_axis: { - label: 'Y Axis', - sqlExpression: 'SELECT 1', - expressionType: 'SQL', - }, - series: 'series', - tooltip_metrics: ['tooltip_metric'], - tooltip_columns: ['tooltip_column'], - order_by_cols: [ - JSON.stringify(['start_time', true]), - JSON.stringify(['order_col', false]), - ], - }; - - test('should build query', () => { - const queryContext = buildQuery(formData); - const [query] = queryContext.queries; - expect(query.metrics).toStrictEqual(['tooltip_metric']); - expect(query.columns).toStrictEqual([ - 'start_time', - 'end_time', - { - label: 'Y Axis', - sqlExpression: 'SELECT 1', - expressionType: 'SQL', - }, - 'series', - 'tooltip_column', - 'order_col', - ]); - expect(query.series_columns).toStrictEqual(['series']); - expect(query.orderby).toStrictEqual([ - ['start_time', true], - ['order_col', false], - ]); - }); -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Gantt/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Gantt/transformProps.test.ts deleted file mode 100644 index f260959e4d2..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Gantt/transformProps.test.ts +++ /dev/null @@ -1,371 +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 { AxisType, ChartProps } from '@superset-ui/core'; -import { supersetTheme } from '@apache-superset/core/theme'; -import { - LegendOrientation, - LegendType, -} from '@superset-ui/plugin-chart-echarts'; -import transformProps from '../../src/Gantt/transformProps'; -import { - EchartsGanttChartProps, - EchartsGanttFormData, -} from '../../src/Gantt/types'; - -const formData: EchartsGanttFormData = { - viz_type: 'gantt_chart', - datasource: '1__table', - - startTime: 'startTime', - endTime: 'endTime', - yAxis: { - label: 'Y Axis', - sqlExpression: 'y_axis', - expressionType: 'SQL', - }, - tooltipMetrics: ['tooltip_metric'], - tooltipColumns: ['tooltip_column'], - series: 'series', - xAxisTimeFormat: '%H:%M', - tooltipTimeFormat: '%H:%M', - tooltipValuesFormat: 'DURATION_SEC', - colorScheme: 'bnbColors', - zoomable: true, - xAxisTitleMargin: undefined, - yAxisTitleMargin: undefined, - xAxisTimeBounds: [null, '19:00:00'], - subcategories: true, - legendMargin: 0, - legendOrientation: LegendOrientation.Top, - legendType: LegendType.Scroll, - showLegend: true, - sortSeriesAscending: true, - legendSort: null, -}; -const queriesData = [ - { - data: [ - { - startTime: Date.UTC(2025, 1, 1, 13, 0, 0), - endTime: Date.UTC(2025, 1, 1, 14, 0, 0), - 'Y Axis': 'first', - tooltip_column: 'tooltip value 1', - series: 'series value 1', - }, - { - startTime: Date.UTC(2025, 1, 1, 18, 0, 0), - endTime: Date.UTC(2025, 1, 1, 20, 0, 0), - 'Y Axis': 'second', - tooltip_column: 'tooltip value 2', - series: 'series value 2', - }, - ], - colnames: ['startTime', 'endTime', 'Y Axis', 'tooltip_column', 'series'], - }, -]; -const chartPropsConfig = { - formData, - queriesData, - theme: supersetTheme, -}; - -describe('Gantt transformProps', () => { - test('should transform chart props', () => { - const chartProps = new ChartProps(chartPropsConfig); - const transformedProps = transformProps( - chartProps as EchartsGanttChartProps, - ); - - expect(transformedProps.echartOptions.series).toHaveLength(4); - const series = transformedProps.echartOptions.series as any[]; - const series0 = series[0]; - const series1 = series[1]; - - // exclude renderItem because it can't be serialized - expect(typeof series0.renderItem).toBe('function'); - delete series0.renderItem; - expect(typeof series1.renderItem).toBe('function'); - delete series1.renderItem; - delete transformedProps.echartOptions.series; - - expect(transformedProps).toEqual( - expect.objectContaining({ - echartOptions: expect.objectContaining({ - useUTC: true, - xAxis: { - name: '', - nameGap: 0, - nameLocation: 'middle', - max: Date.UTC(2025, 1, 1, 19, 0, 0), - min: undefined, - type: AxisType.Time, - axisLabel: { - hideOverlap: true, - formatter: expect.anything(), - }, - }, - yAxis: { - name: '', - nameGap: 0, - nameLocation: 'middle', - type: AxisType.Value, - // always 0 - min: 0, - // equals unique categories count - max: 2, - axisLabel: { - show: false, - }, - splitLine: { - show: false, - }, - }, - legend: expect.objectContaining({ - show: true, - type: 'scroll', - }), - tooltip: { - formatter: expect.anything(), - }, - dataZoom: [ - expect.objectContaining({ - type: 'slider', - filterMode: 'none', - }), - ], - }), - }), - ); - - expect(series0).toEqual({ - name: 'series value 1', - type: 'custom', - progressive: 0, - itemStyle: { - color: expect.anything(), - }, - data: [ - { - value: [ - Date.UTC(2025, 1, 1, 13, 0, 0), - Date.UTC(2025, 1, 1, 14, 0, 0), - 0, - 2, - Date.UTC(2025, 1, 1, 13, 0, 0), - Date.UTC(2025, 1, 1, 14, 0, 0), - 'first', - 'tooltip value 1', - 'series value 1', - ], - }, - ], - dimensions: [ - 'startTime', - 'endTime', - 'index', - 'seriesCount', - 'startTime', - 'endTime', - 'Y Axis', - 'tooltip_column', - 'series', - ], - encode: { - x: [0, 1], - }, - }); - - expect(series1).toEqual({ - name: 'series value 2', - type: 'custom', - progressive: 0, - itemStyle: { - color: expect.anything(), - }, - data: [ - { - value: [ - Date.UTC(2025, 1, 1, 18, 0, 0), - Date.UTC(2025, 1, 1, 20, 0, 0), - 1, - 2, - Date.UTC(2025, 1, 1, 18, 0, 0), - Date.UTC(2025, 1, 1, 20, 0, 0), - 'second', - 'tooltip value 2', - 'series value 2', - ], - }, - ], - dimensions: [ - 'startTime', - 'endTime', - 'index', - 'seriesCount', - 'startTime', - 'endTime', - 'Y Axis', - 'tooltip_column', - 'series', - ], - encode: { - x: [0, 1], - }, - }); - expect(series[2]).toEqual({ - // just for markLines - type: 'line', - animation: false, - markLine: { - data: [{ yAxis: 1 }, { yAxis: 0 }], - label: { - show: false, - }, - silent: true, - symbol: ['none', 'none'], - lineStyle: { - type: 'dashed', - color: '#dbe0ea', - }, - }, - }); - expect(series[3]).toEqual({ - type: 'line', - animation: false, - markLine: { - data: [ - { yAxis: 1.5, name: 'first' }, - { yAxis: 0.5, name: 'second' }, - ], - label: { - show: true, - position: 'start', - formatter: '{b}', - color: 'rgba(0,0,0,0.88)', - }, - lineStyle: expect.objectContaining({ - color: '#00000000', - type: 'solid', - }), - silent: true, - symbol: ['none', 'none'], - }, - }); - }); -}); - -describe('legend sorting', () => { - const createChartProps = (overrides = {}) => - new ChartProps({ - ...chartPropsConfig, - formData: { - ...formData, - ...overrides, - }, - }); - - test('preserves original data order when no sort specified', () => { - const props = createChartProps({ legendSort: null }); - const result = transformProps(props as EchartsGanttChartProps); - - const legendData = (result.echartOptions.legend as any).data; - expect(legendData).toEqual(['series value 1', 'series value 2']); - }); - - test('sorts alphabetically ascending when legendSort is "asc"', () => { - const props = createChartProps({ legendSort: 'asc' }); - const result = transformProps(props as EchartsGanttChartProps); - - const legendData = (result.echartOptions.legend as any).data; - expect(legendData).toEqual(['series value 1', 'series value 2']); - }); - - test('sorts alphabetically descending when legendSort is "desc"', () => { - const props = createChartProps({ legendSort: 'desc' }); - const result = transformProps(props as EchartsGanttChartProps); - - const legendData = (result.echartOptions.legend as any).data; - expect(legendData).toEqual(['series value 2', 'series value 1']); - }); - - test('falls back to scroll for plain legends with an overlong legend item', () => { - const props = new ChartProps({ - ...chartPropsConfig, - width: 320, - formData: { - ...formData, - legendType: LegendType.Plain, - }, - queriesData: [ - { - data: [ - { - startTime: Date.UTC(2025, 1, 1, 13, 0, 0), - endTime: Date.UTC(2025, 1, 1, 14, 0, 0), - 'Y Axis': 'first', - tooltip_column: 'tooltip value 1', - series: - 'This is a ridiculously long legend label that should switch to scroll', - }, - { - startTime: Date.UTC(2025, 1, 1, 18, 0, 0), - endTime: Date.UTC(2025, 1, 1, 20, 0, 0), - 'Y Axis': 'second', - tooltip_column: 'tooltip value 2', - series: 'short label', - }, - ], - colnames: [ - 'startTime', - 'endTime', - 'Y Axis', - 'tooltip_column', - 'series', - ], - }, - ], - }); - - const result = transformProps(props as EchartsGanttChartProps); - - expect((result.echartOptions.legend as any).type).toBe(LegendType.Scroll); - }); - - test('keeps legend visibility driven by showLegend for single-series charts', () => { - const props = new ChartProps({ - ...chartPropsConfig, - queriesData: [ - { - data: [queriesData[0].data[0]], - colnames: [ - 'startTime', - 'endTime', - 'Y Axis', - 'tooltip_column', - 'series', - ], - }, - ], - }); - - const result = transformProps(props as EchartsGanttChartProps); - - expect((result.echartOptions.legend as any).show).toBe(true); - }); -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Gauge/buildQuery.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Gauge/buildQuery.test.ts deleted file mode 100644 index b85e934beb2..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Gauge/buildQuery.test.ts +++ /dev/null @@ -1,48 +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 buildQuery from '../../src/Gauge/buildQuery'; - -describe('Gauge buildQuery', () => { - const baseFormData = { - datasource: '5__table', - metric: 'foo', - viz_type: 'my_chart', - }; - - test('should build query fields with no group by column', () => { - const formData = { ...baseFormData, groupby: undefined }; - const queryContext = buildQuery(formData); - const [query] = queryContext.queries; - expect(query.columns).toEqual([]); - }); - - test('should build query fields with single group by column', () => { - const formData = { ...baseFormData, groupby: ['foo'] }; - const queryContext = buildQuery(formData); - const [query] = queryContext.queries; - expect(query.columns).toEqual(['foo']); - }); - - test('should build query fields with multiple group by columns', () => { - const formData = { ...baseFormData, groupby: ['foo', 'bar'] }; - const queryContext = buildQuery(formData); - const [query] = queryContext.queries; - expect(query.columns).toEqual(['foo', 'bar']); - }); -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Gauge/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Gauge/transformProps.test.ts deleted file mode 100644 index b8fd46d0527..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Gauge/transformProps.test.ts +++ /dev/null @@ -1,727 +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 { - CategoricalColorNamespace, - ChartProps, - SqlaFormData, - VizType, -} from '@superset-ui/core'; -import { supersetTheme } from '@apache-superset/core/theme'; -import transformProps, { - getIntervalBoundsAndColors, -} from '../../src/Gauge/transformProps'; -import { EchartsGaugeChartProps } from '../../src/Gauge/types'; - -describe('Echarts Gauge transformProps', () => { - const baseFormData: SqlaFormData = { - datasource: '26__table', - viz_type: VizType.Gauge, - metric: 'count', - adhocFilters: [], - rowLimit: 10, - minVal: 0, - maxVal: 100, - startAngle: 225, - endAngle: -45, - colorScheme: 'SUPERSET_DEFAULT', - fontSize: 14, - numberFormat: 'SMART_NUMBER', - valueFormatter: '{value}', - showPointer: true, - animation: true, - showAxisTick: false, - showSplitLine: false, - splitNumber: 10, - showProgress: true, - overlap: true, - roundCap: false, - }; - - test('should transform chart props for no group by column', () => { - const formData: SqlaFormData = { ...baseFormData, groupby: [] }; - const queriesData = [ - { - colnames: ['count'], - data: [ - { - count: 16595, - }, - ], - }, - ]; - - const chartPropsConfig = { - formData, - width: 800, - height: 600, - queriesData, - theme: supersetTheme, - }; - - const chartProps = new ChartProps(chartPropsConfig); - const result = transformProps(chartProps as EchartsGaugeChartProps); - - // Test core properties - expect(result.width).toBe(800); - expect(result.height).toBe(600); - - // Test series data - const seriesData = (result.echartOptions as any).series[0].data; - expect(seriesData).toHaveLength(1); - expect(seriesData[0].value).toBe(16595); - expect(seriesData[0].name).toBe(''); - expect(seriesData[0].itemStyle.color).toBe('#1f77b4'); - - // Test detail and title positions - expect(seriesData[0].title.offsetCenter).toEqual(['0%', '20%']); - expect(seriesData[0].title.fontSize).toBe(14); - expect(seriesData[0].detail.offsetCenter).toEqual(['0%', '32.6%']); - expect(seriesData[0].detail.fontSize).toBe(16.8); - }); - - test('should transform chart props for single group by column', () => { - const formData: SqlaFormData = { - ...baseFormData, - groupby: ['year'], - }; - const queriesData = [ - { - colnames: ['year', 'count'], - data: [ - { - year: 1988, - count: 15, - }, - { - year: 1995, - count: 219, - }, - ], - }, - ]; - - const chartPropsConfig = { - formData, - width: 800, - height: 600, - queriesData, - theme: supersetTheme, - }; - - const chartProps = new ChartProps(chartPropsConfig); - const result = transformProps(chartProps as EchartsGaugeChartProps); - - // Test core properties - expect(result.width).toBe(800); - expect(result.height).toBe(600); - - // Test series data - const seriesData = (result.echartOptions as any).series[0].data; - expect(seriesData).toHaveLength(2); - - // First data point - expect(seriesData[0].value).toBe(15); - expect(seriesData[0].name).toBe('year: 1988'); - expect(seriesData[0].itemStyle.color).toBe('#1f77b4'); - expect(seriesData[0].title.offsetCenter).toEqual(['0%', '20%']); - expect(seriesData[0].title.fontSize).toBe(14); - expect(seriesData[0].detail.offsetCenter).toEqual(['0%', '32.6%']); - expect(seriesData[0].detail.fontSize).toBe(16.8); - - // Second data point - expect(seriesData[1].value).toBe(219); - expect(seriesData[1].name).toBe('year: 1995'); - expect(seriesData[1].itemStyle.color).toBe('#ff7f0e'); - expect(seriesData[1].title.offsetCenter).toEqual(['0%', '48%']); - expect(seriesData[1].title.fontSize).toBe(14); - expect(seriesData[1].detail.offsetCenter).toEqual(['0%', '60.6%']); - expect(seriesData[1].detail.fontSize).toBe(16.8); - }); - - test('should transform chart props for multiple group by columns', () => { - const formData: SqlaFormData = { - ...baseFormData, - groupby: ['year', 'platform'], - }; - const queriesData = [ - { - colnames: ['year', 'platform', 'count'], - data: [ - { - year: 2011, - platform: 'PC', - count: 140, - }, - { - year: 2008, - platform: 'PC', - count: 76, - }, - ], - }, - ]; - - const chartPropsConfig = { - formData, - width: 800, - height: 600, - queriesData, - theme: supersetTheme, - }; - - const chartProps = new ChartProps(chartPropsConfig); - const result = transformProps(chartProps as EchartsGaugeChartProps); - - // Test core properties - expect(result.width).toBe(800); - expect(result.height).toBe(600); - - // Test series data - const seriesData = (result.echartOptions as any).series[0].data; - expect(seriesData).toHaveLength(2); - - // First data point - expect(seriesData[0].value).toBe(140); - expect(seriesData[0].name).toBe('year: 2011, platform: PC'); - expect(seriesData[0].itemStyle.color).toBe('#1f77b4'); - expect(seriesData[0].title.offsetCenter).toEqual(['0%', '20%']); - expect(seriesData[0].title.fontSize).toBe(14); - expect(seriesData[0].detail.offsetCenter).toEqual(['0%', '32.6%']); - expect(seriesData[0].detail.fontSize).toBe(16.8); - - // Second data point - expect(seriesData[1].value).toBe(76); - expect(seriesData[1].name).toBe('year: 2008, platform: PC'); - expect(seriesData[1].itemStyle.color).toBe('#ff7f0e'); - expect(seriesData[1].title.offsetCenter).toEqual(['0%', '48%']); - expect(seriesData[1].title.fontSize).toBe(14); - expect(seriesData[1].detail.offsetCenter).toEqual(['0%', '60.6%']); - expect(seriesData[1].detail.fontSize).toBe(16.8); - }); - - test('should transform chart props for intervals', () => { - const formData: SqlaFormData = { - ...baseFormData, - groupby: ['year', 'platform'], - intervals: '60,100', - intervalColorIndices: '1,2', - minVal: 20, - }; - const queriesData = [ - { - colnames: ['year', 'platform', 'count'], - data: [ - { - year: 2011, - platform: 'PC', - count: 140, - }, - { - year: 2008, - platform: 'PC', - count: 76, - }, - ], - }, - ]; - - const chartPropsConfig = { - formData, - width: 800, - height: 600, - queriesData, - theme: supersetTheme, - }; - - const chartProps = new ChartProps(chartPropsConfig); - const result = transformProps(chartProps as EchartsGaugeChartProps); - - // Test core properties - expect(result.width).toBe(800); - expect(result.height).toBe(600); - - // Test axisLine intervals - const { axisLine } = (result.echartOptions as any).series[0]; - expect(axisLine.roundCap).toBe(false); - expect(axisLine.lineStyle.width).toBe(14); - expect(axisLine.lineStyle.color).toEqual([ - [0.5, '#1f77b4'], - [1, '#ff7f0e'], - ]); - - // Test series data - const seriesData = (result.echartOptions.series as any)[0].data; - expect(seriesData).toHaveLength(2); - - // First data point - expect(seriesData[0].value).toBe(140); - expect(seriesData[0].name).toBe('year: 2011, platform: PC'); - expect(seriesData[0].itemStyle.color).toBe('#1f77b4'); - - // Second data point - expect(seriesData[1].value).toBe(76); - expect(seriesData[1].name).toBe('year: 2008, platform: PC'); - expect(seriesData[1].itemStyle.color).toBe('#ff7f0e'); - }); -}); - -describe('Min/Max calculation and axis labels', () => { - const baseFormData: SqlaFormData = { - datasource: '26__table', - viz_type: VizType.Gauge, - metric: 'count', - adhocFilters: [], - rowLimit: 10, - startAngle: 225, - endAngle: -45, - colorScheme: 'SUPERSET_DEFAULT', - fontSize: 14, - numberFormat: 'SMART_NUMBER', - valueFormatter: '{value}', - showPointer: true, - animation: true, - showAxisTick: false, - showSplitLine: false, - splitNumber: 10, - showProgress: true, - overlap: true, - roundCap: false, - groupby: [], - }; - - test('should use provided minVal and maxVal when valid numbers', () => { - const formData: SqlaFormData = { - ...baseFormData, - minVal: 10, - maxVal: 100, - }; - const queriesData = [ - { - colnames: ['count'], - data: [{ count: 50 }, { count: 75 }], - }, - ]; - - const chartProps = new ChartProps({ - formData, - width: 800, - height: 600, - queriesData, - theme: supersetTheme, - }); - - const result = transformProps(chartProps as EchartsGaugeChartProps); - const series = (result.echartOptions as any).series[0]; - - expect(series.min).toBe(10); - expect(series.max).toBe(100); - }); - - test('should calculate min/max from data when minVal is null', () => { - const formData: SqlaFormData = { - ...baseFormData, - minVal: null, - maxVal: 100, - }; - const queriesData = [ - { - colnames: ['count'], - data: [{ count: 20 }, { count: 80 }], - }, - ]; - - const chartProps = new ChartProps({ - formData, - width: 800, - height: 600, - queriesData, - theme: supersetTheme, - }); - - const result = transformProps(chartProps as EchartsGaugeChartProps); - const series = (result.echartOptions as any).series[0]; - - expect(series.min).toBe(0); - expect(series.max).toBe(100); - }); - - test('should calculate min/max from data when maxVal is null', () => { - const formData: SqlaFormData = { - ...baseFormData, - minVal: 0, - maxVal: null, - }; - const queriesData = [ - { - colnames: ['count'], - data: [{ count: 20 }, { count: 80 }], - }, - ]; - - const chartProps = new ChartProps({ - formData, - width: 800, - height: 600, - queriesData, - theme: supersetTheme, - }); - - const result = transformProps(chartProps as EchartsGaugeChartProps); - const series = (result.echartOptions as any).series[0]; - - expect(series.min).toBe(0); - expect(series.max).toBe(160); - }); - - test('should calculate min/max from data when both are null', () => { - const formData: SqlaFormData = { - ...baseFormData, - minVal: null, - maxVal: null, - }; - const queriesData = [ - { - colnames: ['count'], - data: [{ count: 15 }, { count: 45 }], - }, - ]; - - const chartProps = new ChartProps({ - formData, - width: 800, - height: 600, - queriesData, - theme: supersetTheme, - }); - - const result = transformProps(chartProps as EchartsGaugeChartProps); - const series = (result.echartOptions as any).series[0]; - - expect(series.min).toBe(0); - expect(series.max).toBe(90); - }); - - test('should calculate min/max from data when minVal is empty string', () => { - const formData: SqlaFormData = { - ...baseFormData, - minVal: '' as any, - maxVal: 200, - }; - const queriesData = [ - { - colnames: ['count'], - data: [{ count: 30 }, { count: 60 }], - }, - ]; - - const chartProps = new ChartProps({ - formData, - width: 800, - height: 600, - queriesData, - theme: supersetTheme, - }); - - const result = transformProps(chartProps as EchartsGaugeChartProps); - const series = (result.echartOptions as any).series[0]; - - expect(series.min).toBe(0); - expect(series.max).toBe(200); - }); - - test('should calculate min/max from data when maxVal is invalid string', () => { - const formData: SqlaFormData = { - ...baseFormData, - minVal: 0, - maxVal: 'invalid' as any, - }; - const queriesData = [ - { - colnames: ['count'], - data: [{ count: 25 }, { count: 75 }], - }, - ]; - - const chartProps = new ChartProps({ - formData, - width: 800, - height: 600, - queriesData, - theme: supersetTheme, - }); - - const result = transformProps(chartProps as EchartsGaugeChartProps); - const series = (result.echartOptions as any).series[0]; - - expect(series.min).toBe(0); - expect(series.max).toBe(150); - }); - - test('should handle negative values in min/max calculation', () => { - const formData: SqlaFormData = { - ...baseFormData, - minVal: null, - maxVal: null, - }; - const queriesData = [ - { - colnames: ['count'], - data: [{ count: -20 }, { count: 40 }], - }, - ]; - - const chartProps = new ChartProps({ - formData, - width: 800, - height: 600, - queriesData, - theme: supersetTheme, - }); - - const result = transformProps(chartProps as EchartsGaugeChartProps); - const series = (result.echartOptions as any).series[0]; - - expect(series.min).toBe(-40); - expect(series.max).toBe(80); - }); - - test('should generate axis labels correctly based on min, max, and splitNumber', () => { - const formData: SqlaFormData = { - ...baseFormData, - minVal: 0, - maxVal: 100, - splitNumber: 5, - }; - const queriesData = [ - { - colnames: ['count'], - data: [{ count: 50 }], - }, - ]; - - const chartProps = new ChartProps({ - formData, - width: 800, - height: 600, - queriesData, - theme: supersetTheme, - }); - - const result = transformProps(chartProps as EchartsGaugeChartProps); - const series = (result.echartOptions as any).series[0]; - - expect(series.min).toBe(0); - expect(series.max).toBe(100); - expect(series.splitNumber).toBe(5); - expect(series.axisLabel).toBeDefined(); - expect(series.axisLabel.formatter).toBeDefined(); - }); - - test('should calculate axis label length correctly for different number formats', () => { - const formData: SqlaFormData = { - ...baseFormData, - minVal: 0, - maxVal: 1000, - splitNumber: 10, - numberFormat: ',d', - }; - const queriesData = [ - { - colnames: ['count'], - data: [{ count: 500 }], - }, - ]; - - const chartProps = new ChartProps({ - formData, - width: 800, - height: 600, - queriesData, - theme: supersetTheme, - }); - - const result = transformProps(chartProps as EchartsGaugeChartProps); - const series = (result.echartOptions as any).series[0]; - - expect(series.axisLabel).toBeDefined(); - expect(series.axisLabel.formatter).toBeDefined(); - expect(typeof series.axisLabel.formatter).toBe('function'); - }); - - test('should integrate interval bounds and colors with calculated min/max', () => { - const formData: SqlaFormData = { - ...baseFormData, - minVal: null, - maxVal: null, - intervals: '20,60', - intervalColorIndices: '1,2', - }; - const queriesData = [ - { - colnames: ['count'], - data: [{ count: 10 }, { count: 50 }], - }, - ]; - - const chartProps = new ChartProps({ - formData, - width: 800, - height: 600, - queriesData, - theme: supersetTheme, - }); - - const result = transformProps(chartProps as EchartsGaugeChartProps); - const series = (result.echartOptions as any).series[0]; - - expect(series.min).toBe(0); - expect(series.max).toBe(100); - - const { axisLine } = series; - expect(axisLine.lineStyle.color).toEqual( - expect.arrayContaining([ - expect.arrayContaining([expect.any(Number), expect.any(String)]), - ]), - ); - }); - - test('should handle zero values in data correctly', () => { - const formData: SqlaFormData = { - ...baseFormData, - minVal: null, - maxVal: null, - }; - const queriesData = [ - { - colnames: ['count'], - data: [{ count: 0 }, { count: 0 }], - }, - ]; - - const chartProps = new ChartProps({ - formData, - width: 800, - height: 600, - queriesData, - theme: supersetTheme, - }); - - const result = transformProps(chartProps as EchartsGaugeChartProps); - const series = (result.echartOptions as any).series[0]; - - expect(series.min).toBe(0); - expect(series.max).toBe(0); - }); - - test('should handle string minVal/maxVal that can be converted to numbers', () => { - const formData: SqlaFormData = { - ...baseFormData, - minVal: '10' as any, - maxVal: '200' as any, - }; - const queriesData = [ - { - colnames: ['count'], - data: [{ count: 50 }], - }, - ]; - - const chartProps = new ChartProps({ - formData, - width: 800, - height: 600, - queriesData, - theme: supersetTheme, - }); - - const result = transformProps(chartProps as EchartsGaugeChartProps); - const series = (result.echartOptions as any).series[0]; - - expect(series.min).toBe(10); - expect(series.max).toBe(200); - }); - - test('should handle different splitNumber values', () => { - const formData: SqlaFormData = { - ...baseFormData, - minVal: 0, - maxVal: 100, - splitNumber: 20, - }; - const queriesData = [ - { - colnames: ['count'], - data: [{ count: 50 }], - }, - ]; - - const chartProps = new ChartProps({ - formData, - width: 800, - height: 600, - queriesData, - theme: supersetTheme, - }); - - const result = transformProps(chartProps as EchartsGaugeChartProps); - const series = (result.echartOptions as any).series[0]; - - expect(series.splitNumber).toBe(20); - }); -}); - -describe('getIntervalBoundsAndColors', () => { - test('should generate correct interval bounds and colors', () => { - const colorFn = CategoricalColorNamespace.getScale( - 'supersetColors' as string, - ); - expect(getIntervalBoundsAndColors('', '', colorFn, 0, 10)).toEqual([]); - expect(getIntervalBoundsAndColors('4, 10', '1, 2', colorFn, 0, 10)).toEqual( - [ - [0.4, '#1f77b4'], - [1, '#ff7f0e'], - ], - ); - expect( - getIntervalBoundsAndColors('4, 8, 10', '9, 8, 7', colorFn, 0, 10), - ).toEqual([ - [0.4, '#bcbd22'], - [0.8, '#7f7f7f'], - [1, '#e377c2'], - ]); - expect(getIntervalBoundsAndColors('4, 10', '1, 2', colorFn, 2, 10)).toEqual( - [ - [0.25, '#1f77b4'], - [1, '#ff7f0e'], - ], - ); - expect( - getIntervalBoundsAndColors('-4, 0', '1, 2', colorFn, -10, 0), - ).toEqual([ - [0.6, '#1f77b4'], - [1, '#ff7f0e'], - ]); - expect( - getIntervalBoundsAndColors('-4, -2', '1, 2', colorFn, -10, -2), - ).toEqual([ - [0.75, '#1f77b4'], - [1, '#ff7f0e'], - ]); - }); -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Graph/buildQuery.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Graph/buildQuery.test.ts deleted file mode 100644 index 272acd99c9b..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Graph/buildQuery.test.ts +++ /dev/null @@ -1,97 +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 buildQuery from '../../src/Graph/buildQuery'; -import { DEFAULT_FORM_DATA } from '../../src/Graph/types'; - -describe('Graph buildQuery', () => { - const formData = { - ...DEFAULT_FORM_DATA, - datasource: '5__table', - granularity_sqla: 'ds', - source: 'dummy_source', - target: 'dummy_target', - metrics: ['foo', 'bar'], - viz_type: 'my_chart', - }; - - test('should build groupby with source and target categories', () => { - const formDataWithCategories = { - ...formData, - source: 'dummy_source', - target: 'dummy_target', - source_category: 'dummy_source_category', - target_category: 'dummy_target_category', - }; - const queryContext = buildQuery(formDataWithCategories); - const [query] = queryContext.queries; - expect(query.columns).toEqual([ - 'dummy_source', - 'dummy_target', - 'dummy_source_category', - 'dummy_target_category', - ]); - expect(query.metrics).toEqual(['foo', 'bar']); - }); - - test('should build groupby with source category', () => { - const formDataWithCategories = { - ...formData, - source: 'dummy_source', - target: 'dummy_target', - source_category: 'dummy_source_category', - }; - const queryContext = buildQuery(formDataWithCategories); - const [query] = queryContext.queries; - expect(query.columns).toEqual([ - 'dummy_source', - 'dummy_target', - 'dummy_source_category', - ]); - expect(query.metrics).toEqual(['foo', 'bar']); - }); - - test('should build groupby with target category', () => { - const formDataWithCategories = { - ...formData, - source: 'dummy_source', - target: 'dummy_target', - target_category: 'dummy_target_category', - }; - const queryContext = buildQuery(formDataWithCategories); - const [query] = queryContext.queries; - expect(query.columns).toEqual([ - 'dummy_source', - 'dummy_target', - 'dummy_target_category', - ]); - expect(query.metrics).toEqual(['foo', 'bar']); - }); - - test('should build groupby without any category', () => { - const formDataWithCategories = { - ...formData, - source: 'dummy_source', - target: 'dummy_target', - }; - const queryContext = buildQuery(formDataWithCategories); - const [query] = queryContext.queries; - expect(query.columns).toEqual(['dummy_source', 'dummy_target']); - expect(query.metrics).toEqual(['foo', 'bar']); - }); -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Graph/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Graph/transformProps.test.ts deleted file mode 100644 index 5b700ed1396..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Graph/transformProps.test.ts +++ /dev/null @@ -1,354 +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 { ChartProps, SqlaFormData } from '@superset-ui/core'; -import { supersetTheme } from '@apache-superset/core/theme'; -import transformProps from '../../src/Graph/transformProps'; -import { DEFAULT_GRAPH_SERIES_OPTION } from '../../src/Graph/constants'; -import { EchartsGraphChartProps } from '../../src/Graph/types'; - -const formData: SqlaFormData = { - colorScheme: 'bnbColors', - datasource: '3__table', - granularity_sqla: 'ds', - metric: 'count', - source: 'source_column', - target: 'target_column', - category: null, - viz_type: 'graph', -}; -const queriesData = [ - { - colnames: ['source_column', 'target_column', 'count'], - data: [ - { - source_column: 'source_value_1', - target_column: 'target_value_1', - count: 6, - }, - { - source_column: 'source_value_2', - target_column: 'target_value_2', - count: 5, - }, - ], - }, -]; -const chartPropsConfig = { - formData, - width: 800, - height: 600, - queriesData, - theme: supersetTheme, -}; - -describe('EchartsGraph transformProps', () => { - test('should transform chart props for viz without category', () => { - const chartProps = new ChartProps(chartPropsConfig); - expect(transformProps(chartProps as EchartsGraphChartProps)).toEqual( - expect.objectContaining({ - width: 800, - height: 600, - echartOptions: expect.objectContaining({ - legend: expect.objectContaining({ - data: [], - }), - series: expect.arrayContaining([ - expect.objectContaining({ - data: [ - { - col: 'source_column', - category: undefined, - id: '0', - itemStyle: { - color: '#1f77b4', - }, - label: { show: true }, - name: 'source_value_1', - select: { - itemStyle: { borderWidth: 3, opacity: 1 }, - label: { fontWeight: 'bolder' }, - }, - symbolSize: 50, - tooltip: expect.anything(), - value: 6, - }, - { - col: 'target_column', - category: undefined, - id: '1', - itemStyle: { - color: '#1f77b4', - }, - label: { show: true }, - name: 'target_value_1', - select: { - itemStyle: { borderWidth: 3, opacity: 1 }, - label: { fontWeight: 'bolder' }, - }, - symbolSize: 50, - tooltip: expect.anything(), - value: 6, - }, - { - col: 'source_column', - category: undefined, - id: '2', - itemStyle: { - color: '#1f77b4', - }, - label: { show: true }, - name: 'source_value_2', - select: { - itemStyle: { borderWidth: 3, opacity: 1 }, - label: { fontWeight: 'bolder' }, - }, - symbolSize: 10, - tooltip: expect.anything(), - value: 5, - }, - { - col: 'target_column', - category: undefined, - id: '3', - itemStyle: { - color: '#1f77b4', - }, - label: { show: true }, - name: 'target_value_2', - select: { - itemStyle: { borderWidth: 3, opacity: 1 }, - label: { fontWeight: 'bolder' }, - }, - symbolSize: 10, - tooltip: expect.anything(), - value: 5, - }, - ], - }), - expect.objectContaining({ - links: [ - { - emphasis: { lineStyle: { width: 12 } }, - lineStyle: { width: 6, color: '#1f77b4' }, - select: { - lineStyle: { opacity: 1, width: 9.600000000000001 }, - }, - source: '0', - target: '1', - value: 6, - }, - { - emphasis: { lineStyle: { width: 5 } }, - lineStyle: { width: 1.5, color: '#1f77b4' }, - select: { lineStyle: { opacity: 1, width: 5 } }, - source: '2', - target: '3', - value: 5, - }, - ], - }), - ]), - }), - }), - ); - }); - - test('should transform chart props for viz with category and falsy normalization', () => { - const formData: SqlaFormData = { - colorScheme: 'bnbColors', - datasource: '3__table', - granularity_sqla: 'ds', - metric: 'count', - source: 'source_column', - target: 'target_column', - sourceCategory: 'source_category_column', - targetCategory: 'target_category_column', - viz_type: 'graph', - }; - const queriesData = [ - { - colnames: [ - 'source_column', - 'target_column', - 'source_category_column', - 'target_category_column', - 'count', - ], - data: [ - { - source_column: 'source_value', - target_column: 'target_value', - source_category_column: 'category_value_1', - target_category_column: 'category_value_2', - count: 6, - }, - { - source_column: 'source_value', - target_column: 'target_value', - source_category_column: 'category_value_1', - target_category_column: 'category_value_2', - count: 5, - }, - ], - }, - ]; - const chartPropsConfig = { - formData, - width: 800, - height: 600, - queriesData, - theme: supersetTheme, - }; - - const chartProps = new ChartProps(chartPropsConfig); - expect(transformProps(chartProps as EchartsGraphChartProps)).toEqual( - expect.objectContaining({ - width: 800, - height: 600, - echartOptions: expect.objectContaining({ - legend: expect.objectContaining({ - data: ['category_value_1', 'category_value_2'], - }), - series: expect.arrayContaining([ - expect.objectContaining({ - data: [ - { - id: '0', - itemStyle: { - color: '#1f77b4', - }, - col: 'source_column', - name: 'source_value', - value: 11, - symbolSize: 10, - category: 'category_value_1', - select: DEFAULT_GRAPH_SERIES_OPTION.select, - tooltip: expect.anything(), - label: { show: true }, - }, - { - id: '1', - itemStyle: { - color: '#ff7f0e', - }, - col: 'target_column', - name: 'target_value', - value: 11, - symbolSize: 10, - category: 'category_value_2', - select: DEFAULT_GRAPH_SERIES_OPTION.select, - tooltip: expect.anything(), - label: { show: true }, - }, - ], - }), - ]), - }), - }), - ); - }); -}); - -describe('legend sorting', () => { - const queriesData = [ - { - colnames: [ - 'source_column', - 'target_column', - 'source_category_column', - 'target_category_column', - 'count', - ], - data: [ - { - source_column: 'source_value', - target_column: 'target_value', - source_category_column: 'category_value_1', - target_category_column: 'category_value_3', - count: 6, - }, - { - source_column: 'source_value', - target_column: 'target_value', - source_category_column: 'category_value_3', - target_category_column: 'category_value_2', - count: 5, - }, - { - source_column: 'source_value', - target_column: 'target_value', - source_category_column: 'category_value_2', - target_category_column: 'category_value_1', - count: 4, - }, - ], - }, - ]; - - const getChartProps = (overrides = {}) => - new ChartProps({ - ...chartPropsConfig, - formData: { - ...formData, - ...overrides, - sourceCategory: 'source_category_column', - targetCategory: 'target_category_column', - }, - queriesData, - }); - - test('sort legend by data', () => { - const chartProps = getChartProps({ - legendSort: null, - }); - const transformed = transformProps(chartProps as EchartsGraphChartProps); - - expect((transformed.echartOptions.legend as any).data).toEqual([ - 'category_value_1', - 'category_value_3', - 'category_value_2', - ]); - }); - - test('sort legend by label ascending', () => { - const chartProps = getChartProps({ - legendSort: 'asc', - }); - const transformed = transformProps(chartProps as EchartsGraphChartProps); - - expect((transformed.echartOptions.legend as any).data).toEqual([ - 'category_value_1', - 'category_value_2', - 'category_value_3', - ]); - }); - - test('sort legend by label descending', () => { - const chartProps = getChartProps({ - legendSort: 'desc', - }); - const transformed = transformProps(chartProps as EchartsGraphChartProps); - - expect((transformed.echartOptions.legend as any).data).toEqual([ - 'category_value_3', - 'category_value_2', - 'category_value_1', - ]); - }); -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Heatmap/buildQuery.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Heatmap/buildQuery.test.ts deleted file mode 100644 index 75786f761e5..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Heatmap/buildQuery.test.ts +++ /dev/null @@ -1,81 +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 { isPostProcessingRank, QueryFormData } from '@superset-ui/core'; -import buildQuery from '../../src/Heatmap/buildQuery'; - -const baseFormData = { - datasource: '5__table', - granularity_sqla: 'ds', - metric: 'count', - x_axis: 'category', - groupby: ['region'], - viz_type: 'heatmap', -} as QueryFormData; - -const getQuery = (formData: QueryFormData) => buildQuery(formData).queries[0]; -const getRankOperation = (formData: QueryFormData) => - getQuery(formData).post_processing?.find(isPostProcessingRank); - -test('adds X axis orderby when sorting alphabetically ascending', () => { - const query = getQuery({ - ...baseFormData, - sort_x_axis: 'alpha_asc', - }); - - expect(query.orderby).toEqual([['category', true]]); -}); - -test('adds Y axis orderby when sorting alphabetically descending', () => { - const query = getQuery({ - ...baseFormData, - sort_y_axis: 'alpha_desc', - }); - - expect(query.orderby).toEqual([['region', false]]); -}); - -test('should ALWAYS include rank operation when normalized=true', () => { - const rankOperation = getRankOperation({ - ...baseFormData, - normalized: true, - }); - - expect(rankOperation).toBeDefined(); - expect(rankOperation?.operation).toBe('rank'); -}); - -test('should ALWAYS include rank operation when normalized=false', () => { - const rankOperation = getRankOperation({ - ...baseFormData, - normalized: false, - }); - - expect(rankOperation).toBeDefined(); - expect(rankOperation?.operation).toBe('rank'); -}); - -test('should ALWAYS include rank operation when normalized is undefined', () => { - const rankOperation = getRankOperation({ - ...baseFormData, - // normalized not set - }); - - expect(rankOperation).toBeDefined(); - expect(rankOperation?.operation).toBe('rank'); -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Heatmap/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Heatmap/transformProps.test.ts deleted file mode 100644 index d91e8ec935a..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Heatmap/transformProps.test.ts +++ /dev/null @@ -1,362 +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 { ChartProps } from '@superset-ui/core'; -import { supersetTheme } from '@apache-superset/core/theme'; -import { HeatmapChartProps, HeatmapFormData } from '../../src/Heatmap/types'; -import transformProps from '../../src/Heatmap/transformProps'; - -describe('Heatmap transformProps', () => { - const baseFormData: HeatmapFormData = { - datasource: '5__table', - viz_type: 'heatmap', - xAxis: 'day_of_week', - groupby: ['hour'], - metric: 'count', - linearColorScheme: 'blue_white_yellow', - normalized: false, - normalizeAcross: 'heatmap', - borderColor: { r: 255, g: 255, b: 255, a: 1 }, - borderWidth: 1, - showLegend: true, - showValues: false, - showPercentage: false, - legendType: 'continuous', - bottomMargin: 'auto', - leftMargin: 'auto', - xscaleInterval: 1, - yscaleInterval: 1, - xAxisLabelRotation: 0, - valueBounds: [null, null], - }; - - const sparseData = [ - { day_of_week: 'Monday', hour: 9, count: 10 }, - { day_of_week: 'Monday', hour: 14, count: 15 }, - { day_of_week: 'Wednesday', hour: 11, count: 8 }, - { day_of_week: 'Friday', hour: 16, count: 20 }, - { day_of_week: 'Tuesday', hour: 10, count: 12 }, - { day_of_week: 'Thursday', hour: 15, count: 18 }, - ]; - - const createChartProps = ( - formDataOverrides: Partial = {}, - data: Record[] = sparseData, - ) => - new ChartProps({ - formData: { ...baseFormData, ...formDataOverrides }, - width: 800, - height: 600, - queriesData: [ - { - data, - colnames: ['day_of_week', 'hour', 'count'], - coltypes: [0, 0, 0], - }, - ], - theme: supersetTheme, - }); - - test('should sort axes alphabetically in both directions', () => { - // X-axis ascending - const xAscProps = createChartProps({ sortXAxis: 'alpha_asc' }); - const xAscResult = transformProps(xAscProps as HeatmapChartProps); - expect(xAscResult.echartOptions.xAxis).toHaveProperty('data'); - expect((xAscResult.echartOptions.xAxis as any).data).toEqual([ - 'Friday', - 'Monday', - 'Thursday', - 'Tuesday', - 'Wednesday', - ]); - - // X-axis descending - const xDescProps = createChartProps({ sortXAxis: 'alpha_desc' }); - const xDescResult = transformProps(xDescProps as HeatmapChartProps); - expect((xDescResult.echartOptions.xAxis as any).data).toEqual([ - 'Wednesday', - 'Tuesday', - 'Thursday', - 'Monday', - 'Friday', - ]); - - // Y-axis ascending (numeric) - const yAscProps = createChartProps({ sortYAxis: 'alpha_asc' }); - const yAscResult = transformProps(yAscProps as HeatmapChartProps); - // Hours are numbers, so they should be sorted numerically - expect((yAscResult.echartOptions.yAxis as any).data).toEqual([ - 9, 10, 11, 14, 15, 16, - ]); - - // Y-axis descending (numeric) - const yDescProps = createChartProps({ sortYAxis: 'alpha_desc' }); - const yDescResult = transformProps(yDescProps as HeatmapChartProps); - // Numeric descending order - expect((yDescResult.echartOptions.yAxis as any).data).toEqual([ - 16, 15, 14, 11, 10, 9, - ]); - }); - - test('should sort axes by metric value', () => { - const chartPropsXAsc = createChartProps({ sortXAxis: 'value_asc' }); - const resultXAsc = transformProps(chartPropsXAsc as HeatmapChartProps); - // Wednesday(8) < Tuesday(12) < Thursday(18) < Friday(20) < Monday(25=10+15) - expect((resultXAsc.echartOptions.xAxis as any).data).toEqual([ - 'Wednesday', - 'Tuesday', - 'Thursday', - 'Friday', - 'Monday', - ]); - - const chartPropsXDesc = createChartProps({ sortXAxis: 'value_desc' }); - const resultXDesc = transformProps(chartPropsXDesc as HeatmapChartProps); - // Monday(25) > Friday(20) > Thursday(18) > Tuesday(12) > Wednesday(8) - expect((resultXDesc.echartOptions.xAxis as any).data).toEqual([ - 'Monday', - 'Friday', - 'Thursday', - 'Tuesday', - 'Wednesday', - ]); - - const chartPropsYAsc = createChartProps({ sortYAxis: 'value_asc' }); - const resultYAsc = transformProps(chartPropsYAsc as HeatmapChartProps); - // 11(8) < 9(10) < 10(12) < 14(15) < 15(18) < 16(20) - expect((resultYAsc.echartOptions.yAxis as any).data).toEqual([ - 11, 9, 10, 14, 15, 16, - ]); - - const chartPropsYDesc = createChartProps({ sortYAxis: 'value_desc' }); - const resultYDesc = transformProps(chartPropsYDesc as HeatmapChartProps); - // 16(20) > 15(18) > 14(15) > 10(12) > 9(10) > 11(8) - expect((resultYDesc.echartOptions.yAxis as any).data).toEqual([ - 16, 15, 14, 10, 9, 11, - ]); - }); - - test('should handle no sort option specified', () => { - const chartProps = createChartProps({}); - const result = transformProps(chartProps as HeatmapChartProps); - - const xAxisData = (result.echartOptions.xAxis as any).data; - const yAxisData = (result.echartOptions.yAxis as any).data; - - // Should maintain order of first appearance - expect(xAxisData).toEqual([ - 'Monday', - 'Wednesday', - 'Friday', - 'Tuesday', - 'Thursday', - ]); - expect(yAxisData).toEqual([9, 14, 11, 16, 10, 15]); - }); - - test('should aggregate metric values for value-based sorting', () => { - const dataWithDuplicates = [ - { day_of_week: 'Monday', hour: 9, count: 10 }, - { day_of_week: 'Monday', hour: 10, count: 15 }, - { day_of_week: 'Tuesday', hour: 9, count: 5 }, - { day_of_week: 'Tuesday', hour: 10, count: 3 }, - { day_of_week: 'Wednesday', hour: 9, count: 20 }, - ]; - - const chartProps = createChartProps( - { sortXAxis: 'value_asc' }, - dataWithDuplicates, - ); - const result = transformProps(chartProps as HeatmapChartProps); - - const xAxisData = (result.echartOptions.xAxis as any).data; - // Tuesday(8) < Wednesday(20) < Monday(25) - expect(xAxisData).toEqual(['Tuesday', 'Wednesday', 'Monday']); - }); - - test('should handle data with null values', () => { - const dataWithNulls: Record[] = [ - { day_of_week: 'Monday', hour: 9, count: 10 }, - { day_of_week: null, hour: 10, count: 15 }, - { day_of_week: 'Tuesday', hour: null, count: 8 }, - ]; - - const chartProps = createChartProps( - { sortXAxis: 'alpha_asc' }, - dataWithNulls, - ); - const result = transformProps(chartProps as HeatmapChartProps); - - const xAxisData = (result.echartOptions.xAxis as any).data; - // Only non-null values should appear - expect(xAxisData).toEqual(['Monday', 'Tuesday']); - }); - - test('should sort numeric values numerically not alphabetically', () => { - const numericData = [ - { hour: 1, day: 'Mon', count: 10 }, - { hour: 10, day: 'Mon', count: 15 }, - { hour: 2, day: 'Tue', count: 8 }, - { hour: 20, day: 'Wed', count: 12 }, - { hour: 3, day: 'Thu', count: 18 }, - ]; - - const chartProps = createChartProps( - { sortXAxis: 'alpha_asc', xAxis: 'hour', groupby: ['day'] }, - numericData, - ); - - // Override colnames to match the new data structure - (chartProps as any).queriesData[0].colnames = ['hour', 'day', 'count']; - - const result = transformProps(chartProps as HeatmapChartProps); - - const xAxisData = (result.echartOptions.xAxis as any).data; - // Should be numeric order: 1, 2, 3, 10, 20 - // NOT alphabetical order: 1, 10, 2, 20, 3 - expect(xAxisData).toEqual([1, 2, 3, 10, 20]); - }); - - test('should convert series data to axis indices', () => { - const chartProps = createChartProps({ - sortXAxis: 'alpha_asc', - sortYAxis: 'alpha_asc', - }); - const result = transformProps(chartProps as HeatmapChartProps); - - const seriesData = (result.echartOptions.series as any)[0].data; - - // Each data point should be [xIndex, yIndex, value] - expect(Array.isArray(seriesData)).toBe(true); - expect(seriesData.length).toBeGreaterThan(0); - - // Check that data points use indices (numbers starting from 0) - seriesData.forEach((point: any) => { - expect(Array.isArray(point)).toBe(true); - expect(point.length).toBe(3); - // Indices should be numbers - expect(typeof point[0]).toBe('number'); - expect(typeof point[1]).toBe('number'); - // Indices should be >= 0 - expect(point[0]).toBeGreaterThanOrEqual(0); - expect(point[1]).toBeGreaterThanOrEqual(0); - }); - }); - - test('should handle mixed numeric and string values in axes', () => { - const mixedData = [ - { category: 'A', value: 1, count: 10 }, - { category: 'B', value: 10, count: 15 }, - { category: 'C', value: 2, count: 8 }, - ]; - - const chartProps = createChartProps( - { - sortXAxis: 'alpha_asc', - sortYAxis: 'alpha_asc', - xAxis: 'category', - groupby: ['value'], - }, - mixedData, - ); - - (chartProps as any).queriesData[0].colnames = [ - 'category', - 'value', - 'count', - ]; - - const result = transformProps(chartProps as HeatmapChartProps); - - const xAxisData = (result.echartOptions.xAxis as any).data; - const yAxisData = (result.echartOptions.yAxis as any).data; - - // X-axis: strings sorted alphabetically - expect(xAxisData).toEqual(['A', 'B', 'C']); - // Y-axis: numbers sorted numerically (1, 2, 10 NOT 1, 10, 2) - expect(yAxisData).toEqual([1, 2, 10]); - }); - - test('should include rank as 4th dimension when normalized is true', () => { - const dataWithRank = [ - { day_of_week: 'Monday', hour: 9, count: 10, rank: 0.33 }, - { day_of_week: 'Monday', hour: 14, count: 15, rank: 0.67 }, - { day_of_week: 'Wednesday', hour: 11, count: 8, rank: 0.17 }, - { day_of_week: 'Friday', hour: 16, count: 20, rank: 1.0 }, - ]; - - const chartProps = createChartProps({ normalized: true }, dataWithRank); - - const result = transformProps(chartProps as HeatmapChartProps); - - const seriesData = (result.echartOptions.series as any)[0].data; - - // Each data point should be [xIndex, yIndex, metricValue, rankValue] - expect(Array.isArray(seriesData)).toBe(true); - expect(seriesData.length).toBe(4); - - // Check that data points have 4 dimensions when normalized - seriesData.forEach((point: any) => { - expect(Array.isArray(point)).toBe(true); - expect(point.length).toBe(4); - // First two should be indices (numbers) - expect(typeof point[0]).toBe('number'); - expect(typeof point[1]).toBe('number'); - // Third should be the metric value - expect(typeof point[2]).toBe('number'); - // Fourth should be the rank value - expect(typeof point[3]).toBe('number'); - expect(point[3]).toBeGreaterThanOrEqual(0); - expect(point[3]).toBeLessThanOrEqual(1); - }); - - // visualMap should use dimension 3 (4th element) for coloring - expect((result.echartOptions.visualMap as any).dimension).toBe(3); - }); - - test('should use 3 dimensions when normalized is false', () => { - const chartProps = createChartProps({ normalized: false }); - const result = transformProps(chartProps as HeatmapChartProps); - - const seriesData = (result.echartOptions.series as any)[0].data; - - // Each data point should be [xIndex, yIndex, metricValue] - seriesData.forEach((point: any) => { - expect(point.length).toBe(3); - }); - - // visualMap should use dimension 2 (3rd element) for coloring - expect((result.echartOptions.visualMap as any).dimension).toBe(2); - }); - - test('should always hide legend regardless of showLegend setting', () => { - // Test with showLegend: true - const chartPropsWithLegend = createChartProps({ showLegend: true }); - const resultWithLegend = transformProps( - chartPropsWithLegend as HeatmapChartProps, - ); - expect((resultWithLegend.echartOptions.legend as any).show).toBe(false); - - // Test with showLegend: false - const chartPropsWithoutLegend = createChartProps({ showLegend: false }); - const resultWithoutLegend = transformProps( - chartPropsWithoutLegend as HeatmapChartProps, - ); - expect((resultWithoutLegend.echartOptions.legend as any).show).toBe(false); - }); -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/MixedTimeseries/buildQuery.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/MixedTimeseries/buildQuery.test.ts deleted file mode 100644 index 7c265bb31e7..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/test/MixedTimeseries/buildQuery.test.ts +++ /dev/null @@ -1,376 +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 { - ComparisonType, - FreeFormAdhocFilter, - RollingType, - TimeGranularity, -} from '@superset-ui/core'; -import buildQuery from '../../src/MixedTimeseries/buildQuery'; - -const formDataMixedChart = { - datasource: 'dummy', - viz_type: 'my_chart', - // query - // -- common - time_range: '1980 : 2000', - time_grain_sqla: TimeGranularity.WEEK, - granularity_sqla: 'ds', - // -- query a - groupby: ['foo'], - metrics: ['sum(sales)'], - adhoc_filters: [ - { - clause: 'WHERE', - expressionType: 'SQL', - sqlExpression: "foo in ('a', 'b')", - } as FreeFormAdhocFilter, - ], - limit: 5, - row_limit: 10, - timeseries_limit_metric: 'count', - order_desc: true, - truncate_metric: true, - show_empty_columns: true, - // -- query b - groupby_b: [], - metrics_b: ['count'], - adhoc_filters_b: [ - { - clause: 'WHERE', - expressionType: 'SQL', - sqlExpression: "name in ('c', 'd')", - } as FreeFormAdhocFilter, - ], - limit_b: undefined, - row_limit_b: 100, - timeseries_limit_metric_b: undefined, - order_desc_b: false, - truncate_metric_b: true, - show_empty_columns_b: true, - // chart configs - show_value: false, - show_valueB: undefined, -}; -const formDataMixedChartWithAA = { - ...formDataMixedChart, - rolling_type: RollingType.Cumsum, - time_compare: ['1 years ago'], - comparison_type: ComparisonType.Values, - resample_rule: '1AS', - resample_method: 'zerofill', - - rolling_type_b: RollingType.Sum, - rolling_periods_b: 1, - min_periods_b: 1, - comparison_type_b: ComparisonType.Difference, - time_compare_b: ['3 years ago'], - resample_rule_b: '1A', - resample_method_b: 'asfreq', -}; - -test('should compile query object A', () => { - const query = buildQuery(formDataMixedChart).queries[0]; - expect(query).toEqual({ - time_range: '1980 : 2000', - since: undefined, - until: undefined, - granularity: 'ds', - filters: [], - extras: { - having: '', - time_grain_sqla: 'P1W', - where: "(foo in ('a', 'b'))", - }, - applied_time_extras: {}, - columns: ['foo'], - metrics: ['sum(sales)'], - annotation_layers: [], - row_limit: 10, - row_offset: undefined, - series_columns: ['foo'], - series_limit: 5, - series_limit_metric: undefined, - group_others_when_limit_reached: false, - url_params: {}, - custom_params: {}, - custom_form_data: {}, - is_timeseries: true, - time_offsets: [], - post_processing: [ - { - operation: 'pivot', - options: { - aggregates: { - 'sum(sales)': { - operator: 'mean', - }, - }, - columns: ['foo'], - drop_missing_columns: false, - index: ['__timestamp'], - }, - }, - { - operation: 'rename', - options: { - columns: { - 'sum(sales)': null, - }, - inplace: true, - level: 0, - }, - }, - { - operation: 'flatten', - }, - ], - orderby: [['count', false]], - }); -}); - -test('should compile query object B', () => { - const query = buildQuery(formDataMixedChart).queries[1]; - expect(query).toEqual({ - time_range: '1980 : 2000', - since: undefined, - until: undefined, - granularity: 'ds', - filters: [], - extras: { - having: '', - time_grain_sqla: 'P1W', - where: "(name in ('c', 'd'))", - }, - applied_time_extras: {}, - columns: [], - metrics: ['count'], - annotation_layers: [], - row_limit: 100, - row_offset: undefined, - series_columns: [], - series_limit: 0, - series_limit_metric: undefined, - group_others_when_limit_reached: false, - url_params: {}, - custom_params: {}, - custom_form_data: {}, - is_timeseries: true, - time_offsets: [], - post_processing: [ - { - operation: 'pivot', - options: { - aggregates: { - count: { - operator: 'mean', - }, - }, - columns: [], - drop_missing_columns: false, - index: ['__timestamp'], - }, - }, - { - operation: 'flatten', - }, - ], - orderby: [['count', true]], - }); -}); - -test('should compile AA in query A', () => { - const query = buildQuery(formDataMixedChartWithAA).queries[0]; - // time comparison - expect(query.time_offsets).toEqual(['1 years ago']); - - // pivot - expect( - query.post_processing?.find(operator => operator?.operation === 'pivot'), - ).toEqual({ - operation: 'pivot', - options: { - index: ['__timestamp'], - columns: ['foo'], - drop_missing_columns: false, - aggregates: { - 'sum(sales)': { operator: 'mean' }, - 'sum(sales)__1 years ago': { operator: 'mean' }, - }, - }, - }); - // cumsum - expect( - // prettier-ignore - query - .post_processing - ?.find(operator => operator?.operation === 'cum') - ?.operation, - ).toEqual('cum'); - - // resample - expect( - // prettier-ignore - query - .post_processing - ?.find(operator => operator?.operation === 'resample'), - ).toEqual({ - operation: 'resample', - options: { - method: 'asfreq', - rule: '1AS', - fill_value: 0, - }, - }); -}); - -test('should compile AA in query B', () => { - const query = buildQuery(formDataMixedChartWithAA).queries[1]; - // time comparison - expect(query.time_offsets).toEqual(['3 years ago']); - - // rolling total - expect( - // prettier-ignore - query - .post_processing - ?.find(operator => operator?.operation === 'rolling'), - ).toEqual({ - operation: 'rolling', - options: { - rolling_type: 'sum', - window: 1, - min_periods: 1, - columns: { - count: 'count', - 'count__3 years ago': 'count__3 years ago', - }, - }, - }); - - // resample - expect( - // prettier-ignore - query - .post_processing - ?.find(operator => operator?.operation === 'resample'), - ).toEqual({ - operation: 'resample', - options: { - method: 'asfreq', - rule: '1A', - fill_value: null, - }, - }); -}); - -test("shouldn't convert a queryObject with axis", () => { - const { queries } = buildQuery(formDataMixedChart); - expect(queries[0]).toEqual( - expect.objectContaining({ - granularity: 'ds', - columns: ['foo'], - series_columns: ['foo'], - metrics: ['sum(sales)'], - is_timeseries: true, - extras: { - time_grain_sqla: 'P1W', - having: '', - where: "(foo in ('a', 'b'))", - }, - post_processing: [ - { - operation: 'pivot', - options: { - aggregates: { - 'sum(sales)': { - operator: 'mean', - }, - }, - columns: ['foo'], - drop_missing_columns: false, - index: ['__timestamp'], - }, - }, - { - operation: 'rename', - options: { columns: { 'sum(sales)': null }, inplace: true, level: 0 }, - }, - { - operation: 'flatten', - }, - ], - }), - ); - expect(queries[1]).toEqual( - expect.objectContaining({ - granularity: 'ds', - columns: [], - series_columns: [], - metrics: ['count'], - is_timeseries: true, - extras: { - time_grain_sqla: 'P1W', - having: '', - where: "(name in ('c', 'd'))", - }, - post_processing: [ - { - operation: 'pivot', - options: { - aggregates: { - count: { - operator: 'mean', - }, - }, - columns: [], - drop_missing_columns: false, - index: ['__timestamp'], - }, - }, - { - operation: 'flatten', - }, - ], - }), - ); -}); - -test('ensure correct pivot columns', () => { - const query = buildQuery({ ...formDataMixedChartWithAA, x_axis: 'ds' }) - .queries[0]; - - expect(query.time_offsets).toEqual(['1 years ago']); - - // pivot - expect( - query.post_processing?.find(operator => operator?.operation === 'pivot'), - ).toEqual({ - operation: 'pivot', - options: { - index: ['ds'], - columns: ['foo'], - drop_missing_columns: false, - aggregates: { - 'sum(sales)': { operator: 'mean' }, - 'sum(sales)__1 years ago': { operator: 'mean' }, - }, - }, - }); -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/MixedTimeseries/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/MixedTimeseries/transformProps.test.ts deleted file mode 100644 index 7d9deaa7a69..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/test/MixedTimeseries/transformProps.test.ts +++ /dev/null @@ -1,630 +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 { - AnnotationStyle, - AnnotationType, - AnnotationSourceType, - AxisType, - DataRecord, - FormulaAnnotationLayer, - IntervalAnnotationLayer, - VizType, - ChartDataResponseResult, -} from '@superset-ui/core'; -import { GenericDataType } from '@apache-superset/core/common'; -import { - LegendOrientation, - LegendType, - EchartsTimeseriesSeriesType, -} from '../../src'; -import transformProps from '../../src/MixedTimeseries/transformProps'; -import { - EchartsMixedTimeseriesFormData, - EchartsMixedTimeseriesProps, -} from '../../src/MixedTimeseries/types'; -import { DEFAULT_FORM_DATA } from '../../src/MixedTimeseries/types'; -import { createEchartsTimeseriesTestChartProps } from '../helpers'; -import type { SeriesOption } from 'echarts'; - -/** - * Creates a partial ChartDataResponseResult for testing. - * Only includes the fields needed for tests, with sensible defaults for required fields. - */ -function createTestQueryData( - data: unknown[], - overrides?: Partial & { - label_map?: Record; - }, -): ChartDataResponseResult { - return { - annotation_data: null, - cache_key: null, - cache_timeout: null, - cached_dttm: null, - queried_dttm: null, - data: data as DataRecord[], - colnames: [], - coltypes: [], - error: null, - is_cached: false, - query: '', - rowcount: data.length, - sql_rowcount: data.length, - stacktrace: null, - status: 'success', - from_dttm: null, - to_dttm: null, - label_map: {}, - ...overrides, - } as ChartDataResponseResult & { label_map?: Record }; -} - -/** Defaults for createEchartsTimeseriesTestChartProps in Mixed Timeseries tests. */ -const MIXED_TIMESERIES_CHART_PROPS_DEFAULTS = { - defaultFormData: DEFAULT_FORM_DATA, - defaultVizType: 'mixed_timeseries' as const, -}; - -const formData: EchartsMixedTimeseriesFormData = { - annotationLayers: [], - area: false, - areaB: false, - legendMargin: null, - logAxis: false, - logAxisSecondary: false, - markerEnabled: false, - markerEnabledB: false, - markerSize: 0, - markerSizeB: 0, - minorSplitLine: false, - minorTicks: false, - opacity: 0, - opacityB: 0, - orderDesc: false, - orderDescB: false, - richTooltip: false, - rowLimit: 0, - rowLimitB: 0, - legendOrientation: LegendOrientation.Top, - legendType: LegendType.Scroll, - showLegend: false, - showValue: false, - showValueB: false, - stack: true, - stackB: true, - truncateYAxis: false, - truncateYAxisSecondary: false, - xAxisLabelRotation: 0, - xAxisTitle: '', - xAxisTitleMargin: 40, - yAxisBounds: [undefined, undefined], - yAxisBoundsSecondary: [undefined, undefined], - yAxisTitle: '', - yAxisTitleMargin: 50, - yAxisTitlePosition: '', - yAxisTitleSecondary: '', - zoomable: false, - colorScheme: 'bnbColors', - datasource: '3__table', - x_axis: 'ds', - metrics: ['sum__num'], - metricsB: ['sum__num'], - groupby: ['gender'], - groupbyB: ['gender'], - seriesType: EchartsTimeseriesSeriesType.Line, - seriesTypeB: EchartsTimeseriesSeriesType.Bar, - viz_type: VizType.MixedTimeseries, - forecastEnabled: false, - forecastPeriods: [], - forecastInterval: 0, - forecastSeasonalityDaily: 0, - legendSort: null, -}; - -const defaultQueryRows = [ - { boy: 1, girl: 2, ds: 599616000000 }, - { boy: 3, girl: 4, ds: 599916000000 }, -]; -const defaultLabelMap = { ds: ['ds'], boy: ['boy'], girl: ['girl'] }; - -const queriesData: ChartDataResponseResult[] = [ - createTestQueryData(defaultQueryRows, { label_map: defaultLabelMap }), - createTestQueryData(defaultQueryRows, { label_map: defaultLabelMap }), -]; - -test('should transform chart props for viz with showQueryIdentifiers=false', () => { - const chartProps = createEchartsTimeseriesTestChartProps< - EchartsMixedTimeseriesFormData, - EchartsMixedTimeseriesProps - >({ - ...MIXED_TIMESERIES_CHART_PROPS_DEFAULTS, - defaultQueriesData: queriesData, - formData: { ...formData, showQueryIdentifiers: false }, - queriesData, - }); - const transformed = transformProps(chartProps); - - // Check that series IDs don't include query identifiers - const seriesIds = (transformed.echartOptions.series as any[]).map( - (s: any) => s.id, - ); - expect(seriesIds).toContain('sum__num, girl'); - expect(seriesIds).toContain('sum__num, boy'); - expect(seriesIds).not.toContain('sum__num (Query A), girl'); - expect(seriesIds).not.toContain('sum__num (Query A), boy'); - expect(seriesIds).not.toContain('sum__num (Query B), girl'); - expect(seriesIds).not.toContain('sum__num (Query B), boy'); - - // Check that series name include query identifiers - const seriesName = (transformed.echartOptions.series as any[]).map( - (s: any) => s.name, - ); - expect(seriesName).toContain('sum__num, girl'); - expect(seriesName).toContain('sum__num, boy'); - expect(seriesName).not.toContain('sum__num (Query A), girl'); - expect(seriesName).not.toContain('sum__num (Query A), boy'); - expect(seriesName).not.toContain('sum__num (Query B), girl'); - expect(seriesName).not.toContain('sum__num (Query B), boy'); - - expect((transformed.echartOptions.legend as any).data).toEqual([ - 'sum__num, girl', - 'sum__num, boy', - 'sum__num, girl', - 'sum__num, boy', - ]); -}); - -test('should transform chart props for viz with showQueryIdentifiers=true', () => { - const chartProps = createEchartsTimeseriesTestChartProps< - EchartsMixedTimeseriesFormData, - EchartsMixedTimeseriesProps - >({ - ...MIXED_TIMESERIES_CHART_PROPS_DEFAULTS, - defaultQueriesData: queriesData, - formData: { ...formData, showQueryIdentifiers: true }, - queriesData, - }); - const transformed = transformProps(chartProps); - - // Check that series IDs include query identifiers - const seriesIds = (transformed.echartOptions.series as any[]).map( - (s: any) => s.id, - ); - expect(seriesIds).toContain('sum__num (Query A), girl'); - expect(seriesIds).toContain('sum__num (Query A), boy'); - expect(seriesIds).toContain('sum__num (Query B), girl'); - expect(seriesIds).toContain('sum__num (Query B), boy'); - expect(seriesIds).not.toContain('sum__num, girl'); - expect(seriesIds).not.toContain('sum__num, boy'); - - // Check that series name include query identifiers - const seriesName = (transformed.echartOptions.series as any[]).map( - (s: any) => s.name, - ); - expect(seriesName).toContain('sum__num (Query A), girl'); - expect(seriesName).toContain('sum__num (Query A), boy'); - expect(seriesName).toContain('sum__num (Query B), girl'); - expect(seriesName).toContain('sum__num (Query B), boy'); - expect(seriesName).not.toContain('sum__num, girl'); - expect(seriesName).not.toContain('sum__num, boy'); - - expect((transformed.echartOptions.legend as any).data).toEqual([ - 'sum__num (Query A), girl', - 'sum__num (Query A), boy', - 'sum__num (Query B), girl', - 'sum__num (Query B), boy', - ]); -}); - -describe('legend sorting', () => { - const getChartProps = (overrides = {}) => - createEchartsTimeseriesTestChartProps< - EchartsMixedTimeseriesFormData, - EchartsMixedTimeseriesProps - >({ - ...MIXED_TIMESERIES_CHART_PROPS_DEFAULTS, - defaultQueriesData: queriesData, - formData: { - ...formData, - ...overrides, - showQueryIdentifiers: true, - }, - queriesData, - }); - - test('sort legend by data', () => { - const chartProps = getChartProps({ - legendSort: null, - }); - const transformed = transformProps(chartProps); - - expect((transformed.echartOptions.legend as any).data).toEqual([ - 'sum__num (Query A), girl', - 'sum__num (Query A), boy', - 'sum__num (Query B), girl', - 'sum__num (Query B), boy', - ]); - }); - - test('sort legend by label ascending', () => { - const chartProps = getChartProps({ - legendSort: 'asc', - }); - const transformed = transformProps(chartProps); - - expect((transformed.echartOptions.legend as any).data).toEqual([ - 'sum__num (Query A), boy', - 'sum__num (Query A), girl', - 'sum__num (Query B), boy', - 'sum__num (Query B), girl', - ]); - }); - - test('sort legend by label descending', () => { - const chartProps = getChartProps({ - legendSort: 'desc', - }); - const transformed = transformProps(chartProps); - - expect((transformed.echartOptions.legend as any).data).toEqual([ - 'sum__num (Query B), girl', - 'sum__num (Query B), boy', - 'sum__num (Query A), girl', - 'sum__num (Query A), boy', - ]); - }); -}); - -test('legend margin: top orientation sets grid.top correctly', () => { - const chartProps = createEchartsTimeseriesTestChartProps< - EchartsMixedTimeseriesFormData, - EchartsMixedTimeseriesProps - >({ - ...MIXED_TIMESERIES_CHART_PROPS_DEFAULTS, - defaultQueriesData: queriesData, - formData: { - ...formData, - legendMargin: 250, - showLegend: true, - }, - queriesData, - }); - const transformed = transformProps(chartProps); - - expect((transformed.echartOptions.grid as any).top).toEqual(270); -}); - -test('legend margin: bottom orientation sets grid.bottom correctly', () => { - const chartProps = createEchartsTimeseriesTestChartProps< - EchartsMixedTimeseriesFormData, - EchartsMixedTimeseriesProps - >({ - ...MIXED_TIMESERIES_CHART_PROPS_DEFAULTS, - defaultQueriesData: queriesData, - formData: { - ...formData, - legendMargin: 250, - showLegend: true, - legendOrientation: LegendOrientation.Bottom, - }, - queriesData, - }); - const transformed = transformProps(chartProps); - - expect((transformed.echartOptions.grid as any).bottom).toEqual(270); -}); - -test('legend margin: left orientation sets grid.left correctly', () => { - const chartProps = createEchartsTimeseriesTestChartProps< - EchartsMixedTimeseriesFormData, - EchartsMixedTimeseriesProps - >({ - ...MIXED_TIMESERIES_CHART_PROPS_DEFAULTS, - defaultQueriesData: queriesData, - formData: { - ...formData, - legendMargin: 250, - showLegend: true, - legendOrientation: LegendOrientation.Left, - }, - queriesData, - }); - const transformed = transformProps(chartProps); - - expect((transformed.echartOptions.grid as any).left).toEqual(270); -}); - -test('legend margin: right orientation sets grid.right correctly', () => { - const chartProps = createEchartsTimeseriesTestChartProps< - EchartsMixedTimeseriesFormData, - EchartsMixedTimeseriesProps - >({ - ...MIXED_TIMESERIES_CHART_PROPS_DEFAULTS, - defaultQueriesData: queriesData, - formData: { - ...formData, - legendMargin: 270, - showLegend: true, - legendOrientation: LegendOrientation.Right, - }, - queriesData, - }); - const transformed = transformProps(chartProps); - - expect((transformed.echartOptions.grid as any).right).toEqual(270); -}); - -test('should exclude unnamed annotation helper series from legend data', () => { - const interval: IntervalAnnotationLayer = { - annotationType: AnnotationType.Interval, - name: 'My Interval', - show: true, - showLabel: true, - sourceType: AnnotationSourceType.Table, - titleColumn: '', - timeColumn: 'start', - intervalEndColumn: 'end', - descriptionColumns: [], - style: AnnotationStyle.Dashed, - value: 2, - }; - - const annotationData = { - 'My Interval': { - columns: ['start', 'end', 'title'], - records: [ - { - start: 2000, - end: 3000, - title: 'My Title', - }, - ], - }, - }; - - const chartProps = createEchartsTimeseriesTestChartProps< - EchartsMixedTimeseriesFormData, - EchartsMixedTimeseriesProps - >({ - ...MIXED_TIMESERIES_CHART_PROPS_DEFAULTS, - defaultQueriesData: [], - formData: { - ...formData, - annotationLayers: [interval], - showLegend: true, - showQueryIdentifiers: true, - }, - queriesData: [ - createTestQueryData(defaultQueryRows, { - label_map: defaultLabelMap, - annotation_data: annotationData, - }), - createTestQueryData(defaultQueryRows, { - label_map: defaultLabelMap, - annotation_data: annotationData, - }), - ], - }); - const transformed = transformProps(chartProps); - - expect((transformed.echartOptions.legend as any).data).toEqual([ - 'sum__num (Query A), girl', - 'sum__num (Query A), boy', - 'sum__num (Query B), girl', - 'sum__num (Query B), boy', - ]); -}); - -test('should add a formula annotation when X-axis column has dataset-level label', () => { - const formula: FormulaAnnotationLayer = { - name: 'My Formula', - annotationType: AnnotationType.Formula, - value: 'x*2', - style: AnnotationStyle.Solid, - show: true, - showLabel: true, - }; - const timeColumnName = 'ds'; - const timeColumnLabel = 'Time Label'; - const testData = [ - { - [timeColumnLabel]: 599616000000, - boy: 1, - girl: 2, - }, - { - [timeColumnLabel]: 599916000000, - boy: 3, - girl: 4, - }, - ]; - const chartProps = createEchartsTimeseriesTestChartProps< - EchartsMixedTimeseriesFormData, - EchartsMixedTimeseriesProps - >({ - ...MIXED_TIMESERIES_CHART_PROPS_DEFAULTS, - defaultQueriesData: [], - formData: { - ...formData, - x_axis: timeColumnName, - annotationLayers: [formula], - }, - queriesData: [ - createTestQueryData(testData, { - label_map: { - [timeColumnName]: [timeColumnLabel], - boy: ['boy'], - girl: ['girl'], - }, - }), - createTestQueryData(testData, { - label_map: { - [timeColumnName]: [timeColumnLabel], - boy: ['boy'], - girl: ['girl'], - }, - }), - ], - datasource: { - verboseMap: { - [timeColumnName]: timeColumnLabel, - }, - columnFormats: {}, - currencyFormats: {}, - }, - }); - const result = transformProps(chartProps); - const formulaSeries = ( - result.echartOptions.series as SeriesOption[] | undefined - )?.find((s: SeriesOption) => s.name === 'My Formula'); - expect(formulaSeries).toBeDefined(); - expect(formulaSeries?.data).toBeDefined(); - expect(Array.isArray(formulaSeries?.data)).toBe(true); - expect((formulaSeries!.data as unknown[]).length).toBeGreaterThan(0); -}); - -test('numeric x coltype never gets silently coerced to the Time axis', () => { - // Regression guard for echarts-timeseries-epoch-x-axis-labels investigation. - // Mixed Timeseries must follow the reported coltype: Numeric values stay - // off the Time axis and are not silently reinterpreted as Date instances. - // A future change that coerces Numeric → Time would bring back the "NaN" - // label symptom we were investigating. We also assert that whichever - // formatter is picked, it produces a string and does not emit "NaN". - const ts1 = 1745784000000; - const ts2 = 1745870400000; - const epochRows = [ - { __timestamp: ts1, metric: 10 }, - { __timestamp: ts2, metric: 20 }, - ]; - const epochQueryData = createTestQueryData(epochRows, { - colnames: ['__timestamp', 'metric'], - coltypes: [GenericDataType.Numeric, GenericDataType.Numeric], - label_map: { __timestamp: ['__timestamp'], metric: ['metric'] }, - }); - - const chartProps = createEchartsTimeseriesTestChartProps< - EchartsMixedTimeseriesFormData, - EchartsMixedTimeseriesProps - >({ - ...MIXED_TIMESERIES_CHART_PROPS_DEFAULTS, - defaultQueriesData: [epochQueryData, epochQueryData], - formData: { - ...formData, - x_axis: '__timestamp', - metrics: ['metric'], - metricsB: ['metric'], - groupby: [], - groupbyB: [], - }, - queriesData: [epochQueryData, epochQueryData], - }); - - const { echartOptions } = transformProps(chartProps); - const xAxis = echartOptions.xAxis as { - type: string; - axisLabel: { formatter: (v: number) => string }; - }; - - expect(xAxis.type).not.toBe(AxisType.Time); - const label = xAxis.axisLabel.formatter(ts1); - expect(typeof label).toBe('string'); - expect(label).not.toMatch(/NaN/); -}); - -test('xAxisForceCategorical forces Category axis regardless of Numeric coltype', () => { - const ts1 = 1745784000000; - const ts2 = 1745870400000; - const epochRows = [ - { __timestamp: ts1, metric: 10 }, - { __timestamp: ts2, metric: 20 }, - ]; - const epochQueryData = createTestQueryData(epochRows, { - colnames: ['__timestamp', 'metric'], - coltypes: [GenericDataType.Numeric, GenericDataType.Numeric], - label_map: { __timestamp: ['__timestamp'], metric: ['metric'] }, - }); - - const chartProps = createEchartsTimeseriesTestChartProps< - EchartsMixedTimeseriesFormData, - EchartsMixedTimeseriesProps - >({ - ...MIXED_TIMESERIES_CHART_PROPS_DEFAULTS, - defaultQueriesData: [epochQueryData, epochQueryData], - formData: { - ...formData, - x_axis: '__timestamp', - metrics: ['metric'], - metricsB: ['metric'], - groupby: [], - groupbyB: [], - xAxisForceCategorical: true, - }, - queriesData: [epochQueryData, epochQueryData], - }); - - const { echartOptions } = transformProps(chartProps); - const xAxis = echartOptions.xAxis as { type: string }; - - expect(xAxis.type).toBe(AxisType.Category); -}); - -test('temporal x coltype wires the time formatter and Time axis', () => { - // Regression guard: the happy path for mixed-timeseries charts. Ensures - // Temporal coltype still routes through the TimeFormatter so the time axis - // rendering path is exercised by the test suite. - const ts1 = 1745784000000; - const ts2 = 1745870400000; - const temporalRows = [ - { __timestamp: ts1, metric: 10 }, - { __timestamp: ts2, metric: 20 }, - ]; - const temporalQueryData = createTestQueryData(temporalRows, { - colnames: ['__timestamp', 'metric'], - coltypes: [GenericDataType.Temporal, GenericDataType.Numeric], - label_map: { __timestamp: ['__timestamp'], metric: ['metric'] }, - }); - - const chartProps = createEchartsTimeseriesTestChartProps< - EchartsMixedTimeseriesFormData, - EchartsMixedTimeseriesProps - >({ - ...MIXED_TIMESERIES_CHART_PROPS_DEFAULTS, - defaultQueriesData: [temporalQueryData, temporalQueryData], - formData: { - ...formData, - x_axis: '__timestamp', - metrics: ['metric'], - metricsB: ['metric'], - groupby: [], - groupbyB: [], - }, - queriesData: [temporalQueryData, temporalQueryData], - }); - - const { echartOptions } = transformProps(chartProps); - const xAxis = echartOptions.xAxis as { - type: string; - axisLabel: { formatter: (v: Date) => string }; - }; - - expect(xAxis.type).toBe(AxisType.Time); - const label = xAxis.axisLabel.formatter(new Date(ts1)); - expect(typeof label).toBe('string'); - expect(label).not.toMatch(/NaN/); -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Pie/buildQuery.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Pie/buildQuery.test.ts deleted file mode 100644 index 249a55671c4..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Pie/buildQuery.test.ts +++ /dev/null @@ -1,36 +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 buildQuery from '../../src/Pie/buildQuery'; - -describe('Pie buildQuery', () => { - const formData = { - datasource: '5__table', - granularity_sqla: 'ds', - metric: 'foo', - groupby: ['bar'], - viz_type: 'my_chart', - }; - - test('should build query fields from form data', () => { - const queryContext = buildQuery(formData); - const [query] = queryContext.queries; - expect(query.metrics).toEqual(['foo']); - expect(query.columns).toEqual(['bar']); - }); -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Pie/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Pie/transformProps.test.ts deleted file mode 100644 index 5f2c508562c..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Pie/transformProps.test.ts +++ /dev/null @@ -1,593 +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 { - ChartProps, - getNumberFormatter, - SqlaFormData, -} from '@superset-ui/core'; -import { supersetTheme } from '@apache-superset/core/theme'; -import type { PieSeriesOption } from 'echarts/charts'; -import type { - LabelFormatterCallback, - CallbackDataParams, -} from 'echarts/types/src/util/types'; -import transformProps, { parseParams } from '../../src/Pie/transformProps'; -import { EchartsPieChartProps, PieChartDataItem } from '../../src/Pie/types'; -import { LegendOrientation, LegendType } from '../../src/types'; - -describe('Pie transformProps', () => { - const formData: SqlaFormData = { - colorScheme: 'bnbColors', - datasource: '3__table', - granularity_sqla: 'ds', - metric: 'sum__num', - groupby: ['foo', 'bar'], - viz_type: 'my_viz', - }; - const chartProps = new ChartProps({ - formData, - width: 800, - height: 600, - queriesData: [ - { - data: [ - { - foo: 'Sylvester', - bar: 1, - sum__num: 10, - sum__num__contribution: 0.8, - }, - { foo: 'Arnold', bar: 2, sum__num: 2.5, sum__num__contribution: 0.2 }, - ], - }, - ], - theme: supersetTheme, - }); - - test('should transform chart props for viz', () => { - expect(transformProps(chartProps as EchartsPieChartProps)).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, - }), - ]), - }), - ], - }), - }), - ); - }); - - test('falls back to scroll for plain legends with overlong labels', () => { - const longLegendChartProps = new ChartProps({ - formData: { - colorScheme: 'bnbColors', - datasource: '3__table', - granularity_sqla: 'ds', - metric: 'sum__num', - groupby: ['category'], - viz_type: 'pie', - legendType: LegendType.Plain, - legendOrientation: LegendOrientation.Top, - showLegend: true, - } as SqlaFormData, - width: 320, - height: 600, - queriesData: [ - { - data: [ - { - category: 'This is a very long pie legend label one', - sum__num: 10, - }, - { - category: 'This is a very long pie legend label two', - sum__num: 20, - }, - { - category: 'This is a very long pie legend label three', - sum__num: 30, - }, - ], - }, - ], - theme: supersetTheme, - }); - - const transformed = transformProps( - longLegendChartProps as EchartsPieChartProps, - ); - - expect((transformed.echartOptions.legend as any).type).toBe( - LegendType.Scroll, - ); - }); -}); - -describe('formatPieLabel', () => { - test('should generate a valid pie chart label', () => { - const numberFormatter = getNumberFormatter(); - const params = { name: 'My Label', value: 1234, percent: 12.34 }; - expect( - parseParams({ - params, - numberFormatter, - }), - ).toEqual(['My Label', '1.23k', '12.34%']); - expect( - parseParams({ - params: { ...params, name: '' }, - numberFormatter, - }), - ).toEqual(['', '1.23k', '12.34%']); - expect( - parseParams({ - params: { ...params, name: '' }, - numberFormatter, - sanitizeName: true, - }), - ).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): 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) => { - 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); - }; - - test('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%'); - }); - - test('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%'); - }); - - test('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'); - }); -}); - -describe('Total value positioning with legends', () => { - const getChartPropsWithLegend = ( - showTotal = true, - showLegend = true, - legendOrientation = 'right', - donut = true, - ): EchartsPieChartProps => { - const formData: SqlaFormData = { - colorScheme: 'bnbColors', - datasource: '3__table', - granularity_sqla: 'ds', - metric: 'sum__num', - groupby: ['category'], - viz_type: 'pie', - show_total: showTotal, - show_legend: showLegend, - legend_orientation: legendOrientation, - donut, - }; - - return new ChartProps({ - formData, - width: 800, - height: 600, - queriesData: [ - { - data: [ - { category: 'A', sum__num: 10, sum__num__contribution: 0.4 }, - { category: 'B', sum__num: 15, sum__num__contribution: 0.6 }, - ], - }, - ], - theme: supersetTheme, - }) as EchartsPieChartProps; - }; - - test('should center total text when legend is on the right', () => { - const props = getChartPropsWithLegend(true, true, 'right', true); - const transformed = transformProps(props); - - expect(transformed.echartOptions.graphic).toEqual( - expect.objectContaining({ - type: 'text', - left: expect.stringMatching(/^\d+(\.\d+)?%$/), - top: 'middle', - style: expect.objectContaining({ - text: expect.stringContaining('Total:'), - }), - }), - ); - - // The left position should be less than 50% (shifted left) - const leftValue = parseFloat( - (transformed.echartOptions.graphic as any).left.replace('%', ''), - ); - expect(leftValue).toBeLessThan(50); - expect(leftValue).toBeGreaterThan(30); // Should be reasonable positioning - }); - - test('should center total text when legend is on the left', () => { - const props = getChartPropsWithLegend(true, true, 'left', true); - const transformed = transformProps(props); - - expect(transformed.echartOptions.graphic).toEqual( - expect.objectContaining({ - type: 'text', - left: expect.stringMatching(/^\d+(\.\d+)?%$/), - top: 'middle', - }), - ); - - // The left position should be greater than 50% (shifted right) - const leftValue = parseFloat( - (transformed.echartOptions.graphic as any).left.replace('%', ''), - ); - expect(leftValue).toBeGreaterThan(50); - expect(leftValue).toBeLessThan(70); // Should be reasonable positioning - }); - - test('should center total text when legend is on top', () => { - const props = getChartPropsWithLegend(true, true, 'top', true); - const transformed = transformProps(props); - - expect(transformed.echartOptions.graphic).toEqual( - expect.objectContaining({ - type: 'text', - left: 'center', - top: expect.stringMatching(/^\d+(\.\d+)?%$/), - }), - ); - - // The top position should be adjusted for top legend - const topValue = parseFloat( - (transformed.echartOptions.graphic as any).top.replace('%', ''), - ); - expect(topValue).toBeGreaterThan(50); // Shifted down for top legend - }); - - test('should center total text when legend is on bottom', () => { - const props = getChartPropsWithLegend(true, true, 'bottom', true); - const transformed = transformProps(props); - - expect(transformed.echartOptions.graphic).toEqual( - expect.objectContaining({ - type: 'text', - left: 'center', - top: expect.stringMatching(/^\d+(\.\d+)?%$/), - }), - ); - - // The top position should be adjusted for bottom legend - const topValue = parseFloat( - (transformed.echartOptions.graphic as any).top.replace('%', ''), - ); - expect(topValue).toBeLessThan(50); // Shifted up for bottom legend - }); - - test('should use default positioning when no legend is shown', () => { - const props = getChartPropsWithLegend(true, false, 'right', true); - const transformed = transformProps(props); - - expect(transformed.echartOptions.graphic).toEqual( - expect.objectContaining({ - type: 'text', - left: 'center', - top: 'middle', - }), - ); - }); - - test('should handle regular pie chart (non-donut) positioning', () => { - const props = getChartPropsWithLegend(true, true, 'right', false); - const transformed = transformProps(props); - - expect(transformed.echartOptions.graphic).toEqual( - expect.objectContaining({ - type: 'text', - top: '0', // Non-donut charts use '0' as default top position - left: expect.stringMatching(/^\d+(\.\d+)?%$/), // Should still adjust left for right legend - }), - ); - }); - - test('should not show total graphic when showTotal is false', () => { - const props = getChartPropsWithLegend(false, true, 'right', true); - const transformed = transformProps(props); - - expect(transformed.echartOptions.graphic).toBeNull(); - }); -}); - -describe('Other category', () => { - const defaultFormData: SqlaFormData = { - colorScheme: 'bnbColors', - datasource: '3__table', - granularity_sqla: 'ds', - metric: 'metric', - groupby: ['foo', 'bar'], - viz_type: 'my_viz', - }; - - const getChartProps = (formData: Partial) => - new ChartProps({ - formData: { - ...defaultFormData, - ...formData, - }, - width: 800, - height: 600, - queriesData: [ - { - data: [ - { - foo: 'foo 1', - bar: 'bar 1', - metric: 1, - metric__contribution: 1 / 15, // 6.7% - }, - { - foo: 'foo 2', - bar: 'bar 2', - metric: 2, - metric__contribution: 2 / 15, // 13.3% - }, - { - foo: 'foo 3', - bar: 'bar 3', - metric: 3, - metric__contribution: 3 / 15, // 20% - }, - { - foo: 'foo 4', - bar: 'bar 4', - metric: 4, - metric__contribution: 4 / 15, // 26.7% - }, - { - foo: 'foo 5', - bar: 'bar 5', - metric: 5, - metric__contribution: 5 / 15, // 33.3% - }, - ], - }, - ], - theme: supersetTheme, - }); - - test('generates Other category', () => { - const chartProps = getChartProps({ - threshold_for_other: 20, - }); - const transformed = transformProps(chartProps as EchartsPieChartProps); - const series = transformed.echartOptions.series as PieSeriesOption[]; - const data = series[0].data as PieChartDataItem[]; - expect(data).toHaveLength(4); - expect(data[0].value).toBe(3); - expect(data[1].value).toBe(4); - expect(data[2].value).toBe(5); - expect(data[3].value).toBe(1 + 2); - expect(data[3].name).toBe('Other'); - expect(data[3].isOther).toBe(true); - }); -}); - -describe('legend sorting', () => { - const defaultFormData: SqlaFormData = { - colorScheme: 'bnbColors', - datasource: '3__table', - granularity_sqla: 'ds', - metric: 'metric', - groupby: ['foo', 'bar'], - viz_type: 'my_viz', - }; - - const getChartProps = (formData: Partial) => - new ChartProps({ - formData: { - ...defaultFormData, - ...formData, - }, - width: 800, - height: 600, - queriesData: [ - { - data: [ - { - foo: 'A foo', - bar: 'A bar', - metric: 1, - }, - { - foo: 'D foo', - bar: 'D bar', - metric: 2, - }, - - { - foo: 'C foo', - bar: 'C bar', - metric: 3, - }, - { - foo: 'B foo', - bar: 'B bar', - metric: 4, - }, - - { - foo: 'E foo', - bar: 'E bar', - metric: 5, - }, - ], - }, - ], - theme: supersetTheme, - }); - - test('sort legend by data', () => { - const chartProps = getChartProps({ - legendSort: null, - }); - const transformed = transformProps(chartProps as EchartsPieChartProps); - - expect((transformed.echartOptions.legend as any).data).toEqual([ - 'A foo, A bar', - 'D foo, D bar', - 'C foo, C bar', - 'B foo, B bar', - 'E foo, E bar', - ]); - }); - - test('sort legend by label ascending', () => { - const chartProps = getChartProps({ - legendSort: 'asc', - }); - const transformed = transformProps(chartProps as EchartsPieChartProps); - - expect((transformed.echartOptions.legend as any).data).toEqual([ - 'A foo, A bar', - 'B foo, B bar', - 'C foo, C bar', - 'D foo, D bar', - 'E foo, E bar', - ]); - }); - - test('sort legend by label descending', () => { - const chartProps = getChartProps({ - legendSort: 'desc', - }); - const transformed = transformProps(chartProps as EchartsPieChartProps); - - expect((transformed.echartOptions.legend as any).data).toEqual([ - 'E foo, E bar', - 'D foo, D bar', - 'C foo, C bar', - 'B foo, B bar', - 'A foo, A bar', - ]); - }); -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Radar/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Radar/transformProps.test.ts deleted file mode 100644 index d51f3304f40..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Radar/transformProps.test.ts +++ /dev/null @@ -1,204 +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 { ChartProps } from '@superset-ui/core'; -import { supersetTheme } from '@apache-superset/core/theme'; -import { RadarSeriesOption } from 'echarts/charts'; -import transformProps from '../../src/Radar/transformProps'; -import { - EchartsRadarChartProps, - EchartsRadarFormData, -} from '../../src/Radar/types'; - -interface RadarIndicator { - name: string; - max: number; - min: number; -} - -type RadarShape = 'circle' | 'polygon'; - -interface RadarChartConfig { - shape: RadarShape; - indicator: RadarIndicator[]; -} - -interface RadarSeriesData { - value: number[]; - name: string; -} - -const formData: Partial = { - colorScheme: 'supersetColors', - datasource: '3__table', - granularity_sqla: 'ds', - columnConfig: { - 'MAX(na_sales)': { - radarMetricMaxValue: null, - radarMetricMinValue: 0, - }, - 'SUM(eu_sales)': { - radarMetricMaxValue: 5000, - }, - }, - groupby: [], - metrics: [ - 'MAX(na_sales)', - 'SUM(jp_sales)', - 'SUM(other_sales)', - 'SUM(eu_sales)', - ], - viz_type: 'radar', - numberFormat: 'SMART_NUMBER', - dateFormat: 'smart_date', - showLegend: true, - showLabels: true, - isCircle: false, -}; - -const queriesData = [ - { - data: [ - { - 'MAX(na_sales)': 41.49, - 'SUM(jp_sales)': 1290.99, - 'SUM(other_sales)': 797.73, - 'SUM(eu_sales)': 2434.13, - }, - ], - }, -]; - -const chartProps = new ChartProps({ - formData, - width: 800, - height: 600, - queriesData, - theme: supersetTheme, -}); - -describe('Radar transformProps', () => { - test('should transform chart props for normalized radar chart & normalize all metrics except the ones with custom min & max', () => { - const transformedProps = transformProps( - chartProps as EchartsRadarChartProps, - ); - const series = transformedProps.echartOptions.series as RadarSeriesOption[]; - const radar = transformedProps.echartOptions.radar as RadarChartConfig; - - expect((series[0].data as RadarSeriesData[])[0].value).toEqual([ - 0.0170451044, 0.5303701939, 0.3277269497, 2434.13, - ]); - - expect(radar.indicator).toEqual([ - { - name: 'MAX(na_sales)', - max: 1, - min: 0, - }, - { - name: 'SUM(jp_sales)', - max: 1, - min: 0, - }, - { - name: 'SUM(other_sales)', - max: 1, - min: 0, - }, - { - name: 'SUM(eu_sales)', - max: 5000, - min: 0, - }, - ]); - }); -}); - -describe('legend sorting', () => { - const legendSortData = [ - { - data: [ - { - name: 'Sylvester sales', - 'SUM(jp_sales)': 1290.99, - 'SUM(other_sales)': 797.73, - 'SUM(eu_sales)': 2434.13, - }, - { - name: 'Arnold sales', - 'SUM(jp_sales)': 290.99, - 'SUM(other_sales)': 627.73, - 'SUM(eu_sales)': 434.13, - }, - { - name: 'Mark sales', - 'SUM(jp_sales)': 2290.99, - 'SUM(other_sales)': 1297.73, - 'SUM(eu_sales)': 934.13, - }, - ], - }, - ]; - const createChartProps = (overrides = {}) => - new ChartProps({ - ...chartProps, - formData: { - ...formData, - groupby: ['name'], - metrics: ['SUM(jp_sales)', 'SUM(other_sales)', 'SUM(eu_sales)'], - ...overrides, - }, - queriesData: legendSortData, - }); - - test('preserves original data order when no sort specified', () => { - const props = createChartProps({ legendSort: null }); - const result = transformProps(props as EchartsRadarChartProps); - - const legendData = (result.echartOptions.legend as any).data; - expect(legendData).toEqual([ - 'Sylvester sales', - 'Arnold sales', - 'Mark sales', - ]); - }); - - test('sorts alphabetically ascending when legendSort is "asc"', () => { - const props = createChartProps({ legendSort: 'asc' }); - const result = transformProps(props as EchartsRadarChartProps); - - const legendData = (result.echartOptions.legend as any).data; - expect(legendData).toEqual([ - 'Arnold sales', - 'Mark sales', - 'Sylvester sales', - ]); - }); - - test('sorts alphabetically descending when legendSort is "desc"', () => { - const props = createChartProps({ legendSort: 'desc' }); - const result = transformProps(props as EchartsRadarChartProps); - - const legendData = (result.echartOptions.legend as any).data; - expect(legendData).toEqual([ - 'Sylvester sales', - 'Mark sales', - 'Arnold sales', - ]); - }); -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Sunburst/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Sunburst/transformProps.test.ts deleted file mode 100644 index 043b0395a64..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Sunburst/transformProps.test.ts +++ /dev/null @@ -1,53 +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 { ChartProps } from '@superset-ui/core'; -import { supersetTheme } from '@apache-superset/core/theme'; -import { EchartsSunburstChartProps } from '../../src/Sunburst/types'; -import transformProps from '../../src/Sunburst/transformProps'; - -const formData = { - colorScheme: 'bnbColors', - datasource: '3__table', - groupby: ['category'], - metric: 'sum__value', -}; - -const chartProps = new ChartProps({ - formData, - width: 800, - height: 600, - queriesData: [ - { - data: [ - { category: 'A', sum__value: 10 }, - { category: 'B', sum__value: 20 }, - ], - }, - ], - theme: supersetTheme, -}); - -test('series label has no textBorderColor or textBorderWidth', () => { - const { echartOptions } = transformProps( - chartProps as EchartsSunburstChartProps, - ); - const series = (echartOptions as any).series[0]; - expect(series.label).not.toHaveProperty('textBorderColor'); - expect(series.label).not.toHaveProperty('textBorderWidth'); -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/Area/controlPanel.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/Area/controlPanel.test.ts deleted file mode 100644 index 20a36341694..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/Area/controlPanel.test.ts +++ /dev/null @@ -1,115 +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 { ControlPanelsContainerProps } from '@superset-ui/chart-controls/types'; -import { GenericDataType } from '@apache-superset/core/common'; -import controlPanel from '../../../src/Timeseries/Area/controlPanel'; - -const config = controlPanel; - -const getControl = (controlName: string) => { - for (const section of config.controlPanelSections) { - if (section && section.controlSetRows) { - for (const row of section.controlSetRows) { - for (const control of row) { - if ( - typeof control === 'object' && - control !== null && - 'name' in control && - control.name === controlName - ) { - return control; - } - } - } - } - } - - return null; -}; - -const mockControls = ( - xAxisColumn: string | null, - typeGeneric: GenericDataType | null, -): ControlPanelsContainerProps => { - const columns = - xAxisColumn && typeGeneric !== null - ? [{ column_name: xAxisColumn, type_generic: typeGeneric }] - : []; - - return { - controls: { - // @ts-expect-error - x_axis: { - value: xAxisColumn, - }, - // @ts-expect-error - datasource: { - datasource: { columns }, - }, - }, - }; -}; - -const timeFormatControl: any = getControl('x_axis_time_format'); -const numberFormatControl: any = getControl('x_axis_number_format'); - -test('should include x_axis_time_format control', () => { - expect(timeFormatControl).toBeDefined(); - expect(timeFormatControl.config.default).toBe('smart_date'); -}); - -test('should include x_axis_number_format control', () => { - expect(numberFormatControl).toBeDefined(); - expect(numberFormatControl.config.default).toBe('~g'); -}); - -test('x_axis_time_format should be visible for temporal columns', () => { - const visibilityFn = timeFormatControl?.config?.visibility; - expect(visibilityFn(mockControls('date', GenericDataType.Temporal))).toBe( - true, - ); -}); - -test('x_axis_time_format should be hidden for numeric columns', () => { - const visibilityFn = timeFormatControl?.config?.visibility; - expect(visibilityFn(mockControls('year', GenericDataType.Numeric))).toBe( - false, - ); -}); - -test('x_axis_number_format should be visible for numeric columns', () => { - const visibilityFn = numberFormatControl?.config?.visibility; - expect(visibilityFn(mockControls('year', GenericDataType.Numeric))).toBe( - true, - ); -}); - -test('x_axis_number_format should be hidden for temporal columns', () => { - const visibilityFn = numberFormatControl?.config?.visibility; - expect(visibilityFn(mockControls('date', GenericDataType.Temporal))).toBe( - false, - ); -}); - -test('x_axis_number_format should be hidden for string columns', () => { - const visibilityFn = numberFormatControl?.config?.visibility; - expect(visibilityFn(mockControls('name', GenericDataType.String))).toBe( - false, - ); -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/Bar/controlPanel.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/Bar/controlPanel.test.ts deleted file mode 100644 index 01bb1db740c..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/Bar/controlPanel.test.ts +++ /dev/null @@ -1,294 +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 { ControlPanelsContainerProps } from '@superset-ui/chart-controls/types'; -import { GenericDataType } from '@apache-superset/core/common'; -import controlPanel from '../../../src/Timeseries/Regular/Bar/controlPanel'; -import { - StackControlOptionsWithoutStream, - StackControlsValue, -} from '../../../src/constants'; -import { OrientationType } from '../../../src/Timeseries/types'; - -const config = controlPanel; - -const getControl = (controlName: string) => { - for (const section of config.controlPanelSections) { - if (section && section.controlSetRows) { - for (const row of section.controlSetRows) { - for (const control of row) { - if ( - typeof control === 'object' && - control !== null && - 'name' in control && - control.name === controlName - ) { - return control; - } - } - } - } - } - - return null; -}; - -// Mock getStandardizedControls -jest.mock('@superset-ui/chart-controls', () => { - const actual = jest.requireActual('@superset-ui/chart-controls'); - return { - ...actual, - getStandardizedControls: jest.fn(() => ({ - popAllMetrics: jest.fn(() => []), - popAllColumns: jest.fn(() => []), - })), - }; -}); - -test('should include x_axis_time_format control in the panel', () => { - const timeFormatControl = getControl('x_axis_time_format'); - expect(timeFormatControl).toBeDefined(); -}); - -test('should have correct default value for x_axis_time_format', () => { - const timeFormatControl: any = getControl('x_axis_time_format'); - expect(timeFormatControl).toBeDefined(); - expect(timeFormatControl.config).toBeDefined(); - expect(timeFormatControl.config.default).toBe('smart_date'); -}); - -test('should have visibility function for x_axis_time_format', () => { - const timeFormatControl: any = getControl('x_axis_time_format'); - expect(timeFormatControl).toBeDefined(); - expect(timeFormatControl.config.visibility).toBeDefined(); - expect(typeof timeFormatControl.config.visibility).toBe('function'); -}); - -test('should have proper control configuration for x_axis_time_format', () => { - const timeFormatControl: any = getControl('x_axis_time_format'); - expect(timeFormatControl).toBeDefined(); - expect(timeFormatControl.config).toMatchObject({ - default: 'smart_date', - disableStash: true, - resetOnHide: false, - }); - expect(timeFormatControl.config.description).toContain('D3'); -}); - -test('should have Chart Orientation section', () => { - const orientationSection = config.controlPanelSections.find( - section => section && section.label === 'Chart Orientation', - ); - expect(orientationSection).toBeDefined(); - expect(orientationSection!.expanded).toBe(true); -}); - -test('should have Chart Options section with X Axis controls', () => { - const chartOptionsSection = config.controlPanelSections.find( - section => section && section.label === 'Chart Options', - ); - expect(chartOptionsSection).toBeDefined(); - expect(chartOptionsSection!.expanded).toBe(true); - expect(chartOptionsSection!.controlSetRows).toBeDefined(); - expect(chartOptionsSection!.controlSetRows!.length).toBeGreaterThan(0); -}); - -test('should have proper form data overrides', () => { - expect(config.formDataOverrides).toBeDefined(); - expect(typeof config.formDataOverrides).toBe('function'); - - const mockFormData = { - datasource: '1__table', - viz_type: 'echarts_timeseries_bar', - metrics: ['test_metric'], - groupby: ['test_column'], - other_field: 'test', - }; - - const result = config.formDataOverrides!(mockFormData); - - expect(result).toHaveProperty('metrics'); - expect(result).toHaveProperty('groupby'); - expect(result).toHaveProperty('other_field', 'test'); -}); - -test('should include stack control in the panel', () => { - const stackControl = getControl('stack'); - expect(stackControl).toBeDefined(); -}); - -test('should use StackControlOptionsWithoutStream for stack control', () => { - const stackControl: any = getControl('stack'); - expect(stackControl).toBeDefined(); - expect(stackControl.config).toBeDefined(); - expect(stackControl.config.choices).toBe(StackControlOptionsWithoutStream); -}); - -test('should not include Stream option in stack control choices', () => { - const stackControl: any = getControl('stack'); - expect(stackControl).toBeDefined(); - const { choices } = stackControl.config; - const streamOption = choices.find( - (choice: any[]) => choice[0] === StackControlsValue.Stream, - ); - expect(streamOption).toBeUndefined(); -}); - -test('should include None and Stack options in stack control choices', () => { - const stackControl: any = getControl('stack'); - expect(stackControl).toBeDefined(); - const { choices } = stackControl.config; - const noneOption = choices.find((choice: any[]) => choice[0] === null); - const stackOption = choices.find( - (choice: any[]) => choice[0] === StackControlsValue.Stack, - ); - expect(noneOption).toBeDefined(); - expect(stackOption).toBeDefined(); -}); - -test('should have correct default value for stack control', () => { - const stackControl: any = getControl('stack'); - expect(stackControl).toBeDefined(); - expect(stackControl.config.default).toBe(null); -}); - -test('should reset stack to null when formData has Stream value', () => { - const mockFormData = { - datasource: '1__table', - viz_type: 'echarts_timeseries_bar', - metrics: ['test_metric'], - groupby: ['test_column'], - stack: StackControlsValue.Stream, - }; - - const result = config.formDataOverrides!(mockFormData); - - expect(result.stack).toBe(null); -}); - -test('should preserve stack value when formData has Stack value', () => { - const mockFormData = { - datasource: '1__table', - viz_type: 'echarts_timeseries_bar', - metrics: ['test_metric'], - groupby: ['test_column'], - stack: StackControlsValue.Stack, - }; - - const result = config.formDataOverrides!(mockFormData); - - expect(result.stack).toBe(StackControlsValue.Stack); -}); - -test('should preserve stack value when formData has null value', () => { - const mockFormData = { - datasource: '1__table', - viz_type: 'echarts_timeseries_bar', - metrics: ['test_metric'], - groupby: ['test_column'], - stack: null, - }; - - const result = config.formDataOverrides!(mockFormData); - - expect(result.stack).toBe(null); -}); - -test('should preserve stack value when formData does not have stack property', () => { - const mockFormData = { - datasource: '1__table', - viz_type: 'echarts_timeseries_bar', - metrics: ['test_metric'], - groupby: ['test_column'], - }; - - const result = config.formDataOverrides!(mockFormData); - - expect(result).not.toHaveProperty('stack'); -}); - -// x_axis_number_format visibility tests - -const mockBarControls = ( - xAxisColumn: string | null, - typeGeneric: GenericDataType | null, - orientation: string = OrientationType.Vertical, -): ControlPanelsContainerProps => { - const columns = - xAxisColumn && typeGeneric !== null - ? [{ column_name: xAxisColumn, type_generic: typeGeneric }] - : []; - - return { - controls: { - // @ts-expect-error - x_axis: { - value: xAxisColumn, - }, - // @ts-expect-error - orientation: { - value: orientation, - }, - // @ts-expect-error - datasource: { - datasource: { columns }, - }, - }, - }; -}; - -const numberFormatControl: any = getControl('x_axis_number_format'); -const timeFormatControl: any = getControl('x_axis_time_format'); - -test('should include x_axis_number_format control in the panel', () => { - expect(numberFormatControl).toBeDefined(); -}); - -test('x_axis_number_format should be visible for numeric columns in vertical orientation', () => { - const visibilityFn = numberFormatControl?.config?.visibility; - expect(visibilityFn(mockBarControls('year', GenericDataType.Numeric))).toBe( - true, - ); - expect(visibilityFn(mockBarControls('price', GenericDataType.Numeric))).toBe( - true, - ); -}); - -test('x_axis_number_format should be hidden for time columns', () => { - const visibilityFn = numberFormatControl?.config?.visibility; - expect(visibilityFn(mockBarControls('date', GenericDataType.Temporal))).toBe( - false, - ); -}); - -test('x_axis_number_format should be hidden for non-numeric columns', () => { - const visibilityFn = numberFormatControl?.config?.visibility; - expect(visibilityFn(mockBarControls('name', GenericDataType.String))).toBe( - false, - ); - expect(visibilityFn(mockBarControls('flag', GenericDataType.Boolean))).toBe( - false, - ); -}); - -test('x_axis_time_format should be hidden for numeric columns', () => { - const visibilityFn = timeFormatControl?.config?.visibility; - expect(visibilityFn(mockBarControls('year', GenericDataType.Numeric))).toBe( - false, - ); -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/Bar/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/Bar/transformProps.test.ts deleted file mode 100644 index 80e434294c1..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/Bar/transformProps.test.ts +++ /dev/null @@ -1,997 +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 { - ChartDataResponseResult, - ChartProps, - DataRecord, - SqlaFormData, -} from '@superset-ui/core'; -import { GenericDataType } from '@apache-superset/core/common'; -import { supersetTheme } from '@apache-superset/core/theme'; -import { StackControlsValue } from '../../../src/constants'; -import type { - GridComponentOption, - LegendComponentOption, -} from 'echarts/components'; -import { - EchartsTimeseriesChartProps, - LegendOrientation, - LegendType, -} from '../../../src/types'; -import transformProps from '../../../src/Timeseries/transformProps'; -import { DEFAULT_FORM_DATA } from '../../../src/Timeseries/constants'; -import { - EchartsTimeseriesFormData, - OrientationType, - EchartsTimeseriesSeriesType, -} from '../../../src/Timeseries/types'; -import { getPadding } from '../../../src/Timeseries/transformers'; -import { - getHorizontalLegendAvailableWidth, - getLegendLayoutResult, -} from '../../../src/utils/series'; -import { createEchartsTimeseriesTestChartProps } from '../../helpers'; - -function createTestQueryData( - data: DataRecord[], - overrides?: Partial, -): ChartDataResponseResult { - return { - annotation_data: null, - cache_key: null, - cache_timeout: null, - cached_dttm: null, - queried_dttm: null, - data, - colnames: [], - coltypes: [], - error: null, - is_cached: false, - query: '', - rowcount: data.length, - sql_rowcount: data.length, - stacktrace: null, - status: 'success', - from_dttm: null, - to_dttm: null, - ...overrides, - }; -} - -describe('Bar Chart X-axis Time Formatting', () => { - const baseFormData: SqlaFormData = { - ...DEFAULT_FORM_DATA, - colorScheme: 'bnbColors', - datasource: '3__table', - granularity_sqla: '__timestamp', - metric: ['Sales', 'Marketing', 'Operations'], - groupby: [], - viz_type: 'echarts_timeseries_bar', - seriesType: EchartsTimeseriesSeriesType.Bar, - orientation: 'vertical', - }; - - const timeseriesData = [ - { - data: [ - { Sales: 100, __timestamp: 1609459200000 }, // 2021-01-01 - { Marketing: 150, __timestamp: 1612137600000 }, // 2021-02-01 - { Operations: 200, __timestamp: 1614556800000 }, // 2021-03-01 - ], - colnames: ['Sales', 'Marketing', 'Operations', '__timestamp'], - coltypes: ['BIGINT', 'BIGINT', 'BIGINT', 'TIMESTAMP'], - }, - ]; - - const baseChartPropsConfig = { - width: 800, - height: 600, - queriesData: timeseriesData, - theme: supersetTheme, - }; - - describe('Default xAxisTimeFormat', () => { - test('should use smart_date as default xAxisTimeFormat', () => { - const chartProps = new ChartProps({ - ...baseChartPropsConfig, - formData: baseFormData, - }); - - const transformedProps = transformProps( - chartProps as EchartsTimeseriesChartProps, - ); - - // Check that the x-axis has a formatter applied - expect(transformedProps.echartOptions.xAxis).toHaveProperty('axisLabel'); - const xAxis = transformedProps.echartOptions.xAxis as any; - expect(xAxis.axisLabel).toHaveProperty('formatter'); - expect(typeof xAxis.axisLabel.formatter).toBe('function'); - }); - - test('should apply xAxisTimeFormat from DEFAULT_FORM_DATA when not explicitly set', () => { - const formDataWithoutTimeFormat = { - ...baseFormData, - }; - delete formDataWithoutTimeFormat.xAxisTimeFormat; - - const chartProps = new ChartProps({ - ...baseChartPropsConfig, - formData: formDataWithoutTimeFormat, - }); - - const transformedProps = transformProps( - chartProps as EchartsTimeseriesChartProps, - ); - - // Should still have a formatter since DEFAULT_FORM_DATA includes xAxisTimeFormat - expect(transformedProps.echartOptions.xAxis).toHaveProperty('axisLabel'); - const xAxis = transformedProps.echartOptions.xAxis as any; - expect(xAxis.axisLabel).toHaveProperty('formatter'); - }); - }); - - describe('Custom xAxisTimeFormat', () => { - test('should respect custom xAxisTimeFormat when explicitly set', () => { - const customFormData = { - ...baseFormData, - xAxisTimeFormat: '%Y-%m-%d', - }; - - const chartProps = new ChartProps({ - ...baseChartPropsConfig, - formData: customFormData, - }); - - const transformedProps = transformProps( - chartProps as EchartsTimeseriesChartProps, - ); - - // Verify the formatter function exists and is applied - expect(transformedProps.echartOptions.xAxis).toHaveProperty('axisLabel'); - const xAxis = transformedProps.echartOptions.xAxis as any; - expect(xAxis.axisLabel).toHaveProperty('formatter'); - expect(typeof xAxis.axisLabel.formatter).toBe('function'); - - // The key test is that a formatter exists - the actual formatting is handled by d3-time-format - const { formatter } = xAxis.axisLabel; - expect(formatter).toBeDefined(); - expect(typeof formatter).toBe('function'); - }); - - test('should handle different time format options', () => { - const timeFormats = [ - '%Y-%m-%d', - '%Y/%m/%d', - '%m/%d/%Y', - '%b %d, %Y', - 'smart_date', - ]; - - timeFormats.forEach(timeFormat => { - const customFormData = { - ...baseFormData, - xAxisTimeFormat: timeFormat, - }; - - const chartProps = new ChartProps({ - ...baseChartPropsConfig, - formData: customFormData, - }); - - const transformedProps = transformProps( - chartProps as EchartsTimeseriesChartProps, - ); - - const xAxis = transformedProps.echartOptions.xAxis as any; - expect(xAxis.axisLabel).toHaveProperty('formatter'); - expect(typeof xAxis.axisLabel.formatter).toBe('function'); - }); - }); - }); - - describe('Orientation-specific behavior', () => { - test('should apply time formatting to x-axis in vertical bar charts', () => { - const verticalFormData = { - ...baseFormData, - orientation: 'vertical', - xAxisTimeFormat: '%Y-%m', - }; - - const chartProps = new ChartProps({ - ...baseChartPropsConfig, - formData: verticalFormData, - }); - - const transformedProps = transformProps( - chartProps as EchartsTimeseriesChartProps, - ); - - // In vertical orientation, time should be on x-axis - const xAxis = transformedProps.echartOptions.xAxis as any; - expect(xAxis.axisLabel).toHaveProperty('formatter'); - expect(typeof xAxis.axisLabel.formatter).toBe('function'); - }); - - test('should apply time formatting to y-axis in horizontal bar charts', () => { - const horizontalFormData = { - ...baseFormData, - orientation: 'horizontal', - xAxisTimeFormat: '%Y-%m', - }; - - const chartProps = new ChartProps({ - ...baseChartPropsConfig, - formData: horizontalFormData, - }); - - const transformedProps = transformProps( - chartProps as EchartsTimeseriesChartProps, - ); - - // In horizontal orientation, axes are swapped, so time should be on y-axis - const yAxis = transformedProps.echartOptions.yAxis as any; - expect(yAxis.axisLabel).toHaveProperty('formatter'); - expect(typeof yAxis.axisLabel.formatter).toBe('function'); - }); - }); - - describe('Integration with existing features', () => { - test('should work with axis bounds', () => { - const formDataWithBounds = { - ...baseFormData, - xAxisTimeFormat: '%Y-%m-%d', - truncateXAxis: true, - xAxisBounds: [null, null] as [number | null, number | null], - }; - - const chartProps = new ChartProps({ - ...baseChartPropsConfig, - formData: formDataWithBounds, - }); - - const transformedProps = transformProps( - chartProps as EchartsTimeseriesChartProps, - ); - - const xAxis = transformedProps.echartOptions.xAxis as any; - expect(xAxis.axisLabel).toHaveProperty('formatter'); - // The xAxis should be configured with the time formatting - expect(transformedProps.echartOptions.xAxis).toBeDefined(); - }); - - test('should work with label rotation', () => { - const formDataWithRotation = { - ...baseFormData, - xAxisTimeFormat: '%Y-%m-%d', - xAxisLabelRotation: 45, - }; - - const chartProps = new ChartProps({ - ...baseChartPropsConfig, - formData: formDataWithRotation, - }); - - const transformedProps = transformProps( - chartProps as EchartsTimeseriesChartProps, - ); - - const xAxis = transformedProps.echartOptions.xAxis as any; - expect(xAxis.axisLabel).toHaveProperty('formatter'); - expect(xAxis.axisLabel).toHaveProperty('rotate', 45); - }); - - test('should maintain time formatting consistency with tooltip', () => { - const formDataWithTooltip = { - ...baseFormData, - xAxisTimeFormat: '%Y-%m-%d', - tooltipTimeFormat: '%Y-%m-%d', - }; - - const chartProps = new ChartProps({ - ...baseChartPropsConfig, - formData: formDataWithTooltip, - }); - - const transformedProps = transformProps( - chartProps as EchartsTimeseriesChartProps, - ); - - // Both axis and tooltip should have formatters - const xAxis = transformedProps.echartOptions.xAxis as any; - expect(xAxis.axisLabel).toHaveProperty('formatter'); - expect(transformedProps.xValueFormatter).toBeDefined(); - expect(typeof transformedProps.xValueFormatter).toBe('function'); - }); - }); - - describe('Regression test for Issue #30373', () => { - test('should not be stuck on adaptive formatting', () => { - // Test the exact scenario described in the issue - const issueFormData = { - ...baseFormData, - xAxisTimeFormat: '%Y-%m-%d %H:%M:%S', // Non-adaptive format - }; - - const chartProps = new ChartProps({ - ...baseChartPropsConfig, - formData: issueFormData, - }); - - const transformedProps = transformProps( - chartProps as EchartsTimeseriesChartProps, - ); - - // Verify formatter exists - this is the key fix, ensuring xAxisTimeFormat is used - const xAxis = transformedProps.echartOptions.xAxis as any; - const { formatter } = xAxis.axisLabel; - - expect(formatter).toBeDefined(); - expect(typeof formatter).toBe('function'); - - // The important part is that the xAxisTimeFormat is being used from formData - // The actual formatting is handled by the underlying time formatter - }); - - test('should allow changing from smart_date to other formats', () => { - // First create with smart_date (default) - const smartDateFormData = { - ...baseFormData, - xAxisTimeFormat: 'smart_date', - }; - - const smartDateChartProps = new ChartProps({ - ...baseChartPropsConfig, - formData: smartDateFormData, - }); - - const smartDateProps = transformProps( - smartDateChartProps as EchartsTimeseriesChartProps, - ); - - // Then change to a different format - const customFormatFormData = { - ...baseFormData, - xAxisTimeFormat: '%b %Y', - }; - - const customFormatChartProps = new ChartProps({ - ...baseChartPropsConfig, - formData: customFormatFormData, - }); - - const customFormatProps = transformProps( - customFormatChartProps as EchartsTimeseriesChartProps, - ); - - // Both should have formatters - the key is that they're not undefined - const smartDateXAxis = smartDateProps.echartOptions.xAxis as any; - const customFormatXAxis = customFormatProps.echartOptions.xAxis as any; - - expect(smartDateXAxis.axisLabel.formatter).toBeDefined(); - expect(customFormatXAxis.axisLabel.formatter).toBeDefined(); - - // Both should be functions that can format time - expect(typeof smartDateXAxis.axisLabel.formatter).toBe('function'); - expect(typeof customFormatXAxis.axisLabel.formatter).toBe('function'); - }); - - test('should have xAxisTimeFormat in formData by default', () => { - // This test specifically verifies our fix - that DEFAULT_FORM_DATA includes xAxisTimeFormat - const chartProps = new ChartProps({ - ...baseChartPropsConfig, - formData: baseFormData, - }); - - expect(chartProps.formData.xAxisTimeFormat).toBeDefined(); - expect(chartProps.formData.xAxisTimeFormat).toBe('smart_date'); - }); - }); - - describe('Color By X-Axis Feature', () => { - const categoricalData = [ - { - data: [ - { category: 'A', value: 100 }, - { category: 'B', value: 150 }, - { category: 'C', value: 200 }, - ], - colnames: ['category', 'value'], - coltypes: ['STRING', 'BIGINT'], - }, - ]; - - test('should apply color by x-axis when enabled with no dimensions', () => { - const formData = { - ...baseFormData, - colorByPrimaryAxis: true, - groupby: [], - x_axis: 'category', - metric: 'value', - }; - - const chartProps = new ChartProps({ - ...baseChartPropsConfig, - queriesData: categoricalData, - formData, - }); - - const transformedProps = transformProps( - chartProps as unknown as EchartsTimeseriesChartProps, - ); - - // Should have hidden legend series for each x-axis value - const series = transformedProps.echartOptions.series as any[]; - expect(series.length).toBeGreaterThan(3); // Original series + hidden legend series - - // Check that legend data contains x-axis values - const legendData = transformedProps.legendData as string[]; - expect(legendData).toContain('A'); - expect(legendData).toContain('B'); - expect(legendData).toContain('C'); - - // Check that legend items have roundRect icons - const legend = transformedProps.echartOptions.legend as any; - expect(legend.data).toBeDefined(); - expect(Array.isArray(legend.data)).toBe(true); - if (legend.data.length > 0 && typeof legend.data[0] === 'object') { - expect(legend.data[0].icon).toBe('roundRect'); - } - }); - - test('should NOT apply color by x-axis when dimensions are present', () => { - const formData = { - ...baseFormData, - colorByPrimaryAxis: true, - groupby: ['region'], - x_axis: 'category', - metric: 'value', - }; - - const chartProps = new ChartProps({ - ...baseChartPropsConfig, - queriesData: categoricalData, - formData, - }); - - const transformedProps = transformProps( - chartProps as unknown as EchartsTimeseriesChartProps, - ); - - // Legend data should NOT contain x-axis values when dimensions exist - const legendData = transformedProps.legendData as string[]; - // Should use series names, not x-axis values - expect(legendData.length).toBeLessThan(10); - }); - - test('should use x-axis values as color keys for consistent colors', () => { - const formData = { - ...baseFormData, - colorByPrimaryAxis: true, - groupby: [], - x_axis: 'category', - metric: 'value', - }; - - const chartProps = new ChartProps({ - ...baseChartPropsConfig, - queriesData: categoricalData, - formData, - }); - - const transformedProps = transformProps( - chartProps as unknown as EchartsTimeseriesChartProps, - ); - - const series = transformedProps.echartOptions.series as any[]; - - // Find the data series (not the hidden legend series) - const dataSeries = series.find( - s => s.data && s.data.length > 0 && s.type === 'bar', - ); - expect(dataSeries).toBeDefined(); - - // Check that data points have individual itemStyle with colors - if (dataSeries && Array.isArray(dataSeries.data)) { - const dataPoint = dataSeries.data[0]; - if ( - dataPoint && - typeof dataPoint === 'object' && - 'itemStyle' in dataPoint - ) { - expect(dataPoint.itemStyle).toBeDefined(); - expect(dataPoint.itemStyle.color).toBeDefined(); - } - } - }); - - test('should disable legend selection when color by x-axis is enabled', () => { - const formData = { - ...baseFormData, - colorByPrimaryAxis: true, - groupby: [], - x_axis: 'category', - metric: 'value', - }; - - const chartProps = new ChartProps({ - ...baseChartPropsConfig, - queriesData: categoricalData, - formData, - }); - - const transformedProps = transformProps( - chartProps as unknown as EchartsTimeseriesChartProps, - ); - - const legend = transformedProps.echartOptions.legend as any; - expect(legend.selectedMode).toBe(false); - expect(legend.selector).toBe(false); - }); - - test('should work without stacking enabled', () => { - const formData = { - ...baseFormData, - colorByPrimaryAxis: true, - groupby: [], - stack: null, - x_axis: 'category', - metric: 'value', - }; - - const chartProps = new ChartProps({ - ...baseChartPropsConfig, - queriesData: categoricalData, - formData, - }); - - const transformedProps = transformProps( - chartProps as unknown as EchartsTimeseriesChartProps, - ); - - // Should still create legend with x-axis values - const legendData = transformedProps.legendData as string[]; - expect(legendData.length).toBeGreaterThan(0); - expect(legendData).toContain('A'); - }); - - test('should handle when colorByPrimaryAxis is disabled', () => { - const formData = { - ...baseFormData, - colorByPrimaryAxis: false, - groupby: [], - x_axis: 'category', - metric: 'value', - }; - - const chartProps = new ChartProps({ - ...baseChartPropsConfig, - queriesData: categoricalData, - formData, - }); - - const transformedProps = transformProps( - chartProps as unknown as EchartsTimeseriesChartProps, - ); - - // Legend should not be disabled when feature is off - const legend = transformedProps.echartOptions.legend as any; - expect(legend.selectedMode).not.toBe(false); - }); - - test('should use category axis (Y) as color key for horizontal bar charts', () => { - const formData = { - ...baseFormData, - colorByPrimaryAxis: true, - groupby: [], - orientation: 'horizontal', - x_axis: 'category', - metric: 'value', - }; - - const chartProps = new ChartProps({ - ...baseChartPropsConfig, - queriesData: categoricalData, - formData, - }); - - const transformedProps = transformProps( - chartProps as unknown as EchartsTimeseriesChartProps, - ); - - // Legend should contain category values (A, B, C), not numeric values - const legendData = transformedProps.legendData as string[]; - expect(legendData).toContain('A'); - expect(legendData).toContain('B'); - expect(legendData).toContain('C'); - }); - - test('should preserve source order for color-by-primary-axis legends when label sorting is enabled', () => { - const unsortedCategoricalData = [ - { - data: [ - { category: 'Zulu', value: 100 }, - { category: 'Alpha', value: 150 }, - { category: 'Mike', value: 200 }, - ], - colnames: ['category', 'value'], - coltypes: ['STRING', 'BIGINT'], - }, - ]; - - const formData = { - ...baseFormData, - colorByPrimaryAxis: true, - groupby: [], - legendSort: 'asc', - x_axis: 'category', - metric: 'value', - }; - - const chartProps = new ChartProps({ - ...baseChartPropsConfig, - queriesData: unsortedCategoricalData, - formData, - }); - - const transformedProps = transformProps( - chartProps as unknown as EchartsTimeseriesChartProps, - ); - - const legend = transformedProps.echartOptions.legend as { - data: { name: string }[]; - }; - expect(legend.data.map(item => item.name)).toEqual([ - 'Zulu', - 'Alpha', - 'Mike', - ]); - }); - - test('should deduplicate legend entries when x-axis has repeated values', () => { - const repeatedData = [ - { - data: [ - { category: 'A', value: 100 }, - { category: 'A', value: 200 }, - { category: 'B', value: 150 }, - ], - colnames: ['category', 'value'], - coltypes: ['STRING', 'BIGINT'], - }, - ]; - - const formData = { - ...baseFormData, - colorByPrimaryAxis: true, - groupby: [], - x_axis: 'category', - metric: 'value', - }; - - const chartProps = new ChartProps({ - ...baseChartPropsConfig, - queriesData: repeatedData, - formData, - }); - - const transformedProps = transformProps( - chartProps as unknown as EchartsTimeseriesChartProps, - ); - - const legendData = transformedProps.legendData as string[]; - // 'A' should appear only once despite being in the data twice - expect(legendData.filter(v => v === 'A').length).toBe(1); - expect(legendData).toContain('B'); - }); - - test('should create exactly one hidden legend series per unique category', () => { - const formData = { - ...baseFormData, - colorByPrimaryAxis: true, - groupby: [], - x_axis: 'category', - metric: 'value', - }; - - const chartProps = new ChartProps({ - ...baseChartPropsConfig, - queriesData: categoricalData, - formData, - }); - - const transformedProps = transformProps( - chartProps as unknown as EchartsTimeseriesChartProps, - ); - - const series = transformedProps.echartOptions.series as any[]; - const hiddenSeries = series.filter( - s => s.type === 'line' && Array.isArray(s.data) && s.data.length === 0, - ); - // One hidden series per unique category (A, B, C) - expect(hiddenSeries.length).toBe(3); - }); - }); - - describe('Horizontal stacked bar chart axis bounds', () => { - // Dataset where each series max = 4 but stacked total max = 8 - const stackedData: ChartDataResponseResult[] = [ - createTestQueryData( - [ - { team: 'Team A', High: 2, Low: 2, Medium: 4 }, - { team: 'Team B', High: null, Low: null, Medium: 3 }, - { team: 'Team C', High: null, Low: null, Medium: 1 }, - ], - { - colnames: ['team', 'High', 'Low', 'Medium'], - coltypes: [ - GenericDataType.String, - GenericDataType.Numeric, - GenericDataType.Numeric, - GenericDataType.Numeric, - ], - }, - ), - ]; - - const horizontalStackedFormData: EchartsTimeseriesFormData = { - ...(baseFormData as EchartsTimeseriesFormData), - x_axis: 'team', - metric: ['High', 'Low', 'Medium'], - groupby: [], - orientation: OrientationType.Horizontal, - seriesType: EchartsTimeseriesSeriesType.Bar, - stack: StackControlsValue.Stack, - truncateYAxis: true, - }; - - test('xAxis.max uses stacked total, not individual series max', () => { - // Individual series max = 4 (Medium), stacked total for Team A = 8 - // Without the fix, xAxis.max would be 4, clipping bars and duplicating labels - const chartProps = createEchartsTimeseriesTestChartProps< - EchartsTimeseriesFormData, - EchartsTimeseriesChartProps - >({ - defaultFormData: horizontalStackedFormData, - defaultVizType: 'echarts_timeseries_bar', - defaultQueriesData: stackedData, - }); - - const { echartOptions } = transformProps(chartProps); - const xAxis = echartOptions.xAxis as any; - - // xAxis.max must be >= stacked total (8), not capped at individual series max (4) - expect(xAxis.max).toBeGreaterThanOrEqual(8); - }); - - test('xAxis.max is not set to individual series max when stacking', () => { - const chartProps = createEchartsTimeseriesTestChartProps< - EchartsTimeseriesFormData, - EchartsTimeseriesChartProps - >({ - defaultFormData: horizontalStackedFormData, - defaultVizType: 'echarts_timeseries_bar', - defaultQueriesData: stackedData, - }); - - const { echartOptions } = transformProps(chartProps); - const xAxis = echartOptions.xAxis as any; - - // 4 is the individual series max — the axis should not be clipped there - expect(xAxis.max).not.toBe(4); - }); - - test('non-stacked horizontal bar chart still uses individual series max', () => { - const nonStackedFormData: EchartsTimeseriesFormData = { - ...horizontalStackedFormData, - stack: null, - }; - - const chartProps = createEchartsTimeseriesTestChartProps< - EchartsTimeseriesFormData, - EchartsTimeseriesChartProps - >({ - defaultFormData: nonStackedFormData, - defaultVizType: 'echarts_timeseries_bar', - defaultQueriesData: stackedData, - }); - - const { echartOptions } = transformProps(chartProps); - const xAxis = echartOptions.xAxis as any; - - // Without stacking, xAxis.max should be based on individual series values - expect(xAxis.max).toBe(4); - }); - }); - - describe('Legend layout regressions', () => { - const getBottomLegendLayout = ( - chartWidth: number, - legendItems: string[], - legendMargin?: string | number | null, - ) => - getLegendLayoutResult({ - availableWidth: getHorizontalLegendAvailableWidth({ - chartWidth, - orientation: LegendOrientation.Bottom, - padding: getPadding( - true, - LegendOrientation.Bottom, - false, - false, - legendMargin, - false, - undefined, - undefined, - undefined, - true, - ), - }), - chartHeight: baseChartPropsConfig.height, - chartWidth, - legendItems, - legendMargin, - orientation: LegendOrientation.Bottom, - show: true, - theme: supersetTheme, - type: LegendType.Plain, - }); - - test('should fall back to scroll for horizontal bottom legends after margin expansion reduces available width', () => { - const legendLabels = [ - 'This is a long sales legend', - 'This is a long marketing legend', - 'This is a long operations legend', - ]; - const longLegendData: ChartDataResponseResult[] = [ - createTestQueryData( - [ - { - [legendLabels[0]]: 100, - [legendLabels[1]]: null, - [legendLabels[2]]: null, - __timestamp: 1609459200000, - }, - { - [legendLabels[0]]: null, - [legendLabels[1]]: 150, - [legendLabels[2]]: null, - __timestamp: 1612137600000, - }, - { - [legendLabels[0]]: null, - [legendLabels[1]]: null, - [legendLabels[2]]: 200, - __timestamp: 1614556800000, - }, - ], - { - colnames: [...legendLabels, '__timestamp'], - coltypes: [ - GenericDataType.Numeric, - GenericDataType.Numeric, - GenericDataType.Numeric, - GenericDataType.Temporal, - ], - }, - ), - ]; - const regressionFormData: EchartsTimeseriesFormData = { - ...(baseFormData as EchartsTimeseriesFormData), - metric: legendLabels, - orientation: OrientationType.Horizontal, - legendOrientation: LegendOrientation.Bottom, - legendType: LegendType.Plain, - showLegend: true, - }; - const baselineChartProps = createEchartsTimeseriesTestChartProps< - EchartsTimeseriesFormData, - EchartsTimeseriesChartProps - >({ - defaultFormData: regressionFormData, - defaultVizType: 'echarts_timeseries_bar', - defaultQueriesData: longLegendData, - width: baseChartPropsConfig.width, - height: baseChartPropsConfig.height, - }); - const baselineTransformed = transformProps(baselineChartProps); - const legendItems = ( - (baselineTransformed.echartOptions.legend as LegendComponentOption) - .data as Array - ).map(item => (typeof item === 'string' ? item : item.name)); - let chartWidth: number | undefined; - let expandedLegendMargin: number | null = null; - - for (let width = 300; width <= 700; width += 1) { - const initialLayout = getBottomLegendLayout(width, legendItems, null); - - if (initialLayout.effectiveType !== LegendType.Plain) { - continue; - } - - const refinedLayout = getBottomLegendLayout( - width, - legendItems, - initialLayout.effectiveMargin ?? null, - ); - - if (refinedLayout.effectiveType === LegendType.Scroll) { - chartWidth = width; - expandedLegendMargin = initialLayout.effectiveMargin ?? null; - break; - } - } - - expect(chartWidth).toBeDefined(); - expect(expandedLegendMargin).not.toBeNull(); - const resolvedChartWidth = chartWidth ?? baseChartPropsConfig.width; - - const chartProps = createEchartsTimeseriesTestChartProps< - EchartsTimeseriesFormData, - EchartsTimeseriesChartProps - >({ - defaultFormData: regressionFormData, - defaultVizType: 'echarts_timeseries_bar', - defaultQueriesData: longLegendData, - width: resolvedChartWidth, - height: baseChartPropsConfig.height, - }); - - const transformedProps = transformProps(chartProps); - const legend = transformedProps.echartOptions - .legend as LegendComponentOption; - const grid = transformedProps.echartOptions.grid as GridComponentOption; - const expectedPadding = getPadding( - true, - LegendOrientation.Bottom, - false, - false, - null, - false, - undefined, - undefined, - undefined, - true, - ); - [expectedPadding.bottom, expectedPadding.left] = [ - expectedPadding.left, - expectedPadding.bottom, - ]; - const expandedPadding = getPadding( - true, - LegendOrientation.Bottom, - false, - false, - expandedLegendMargin, - false, - undefined, - undefined, - undefined, - true, - ); - [expandedPadding.bottom, expandedPadding.left] = [ - expandedPadding.left, - expandedPadding.bottom, - ]; - - expect(legend.type).toBe(LegendType.Scroll); - expect(grid.bottom).toBe(expectedPadding.bottom); - expect(grid.bottom).not.toBe(expandedPadding.bottom); - }); - }); -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/Scatter/controlPanel.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/Scatter/controlPanel.test.ts deleted file mode 100644 index 943badfe02f..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/Scatter/controlPanel.test.ts +++ /dev/null @@ -1,150 +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 { ControlPanelsContainerProps } from '@superset-ui/chart-controls/types'; -import { GenericDataType } from '@apache-superset/core/common'; -import controlPanel from '../../../src/Timeseries/Regular/Scatter/controlPanel'; - -const config = controlPanel; - -const getControl = (controlName: string) => { - for (const section of config.controlPanelSections) { - if (section && section.controlSetRows) { - for (const row of section.controlSetRows) { - for (const control of row) { - if ( - typeof control === 'object' && - control !== null && - 'name' in control && - control.name === controlName - ) { - return control; - } - } - } - } - } - - return null; -}; - -const mockControls = ( - xAxisColumn: string | null, - typeGeneric: GenericDataType | null, -): ControlPanelsContainerProps => { - const columns = - xAxisColumn && typeGeneric !== null - ? [{ column_name: xAxisColumn, type_generic: typeGeneric }] - : []; - - return { - controls: { - // @ts-expect-error - x_axis: { - value: xAxisColumn, - }, - // @ts-expect-error - datasource: { - datasource: { columns }, - }, - }, - }; -}; - -// tests for x_axis_time_format control -const timeFormatControl: any = getControl('x_axis_time_format'); - -test('scatter chart control panel should include x_axis_time_format control in the panel', () => { - expect(timeFormatControl).toBeDefined(); -}); - -test('scatter chart control panel should have correct default value for x_axis_time_format', () => { - expect(timeFormatControl).toBeDefined(); - expect(timeFormatControl.config).toBeDefined(); - expect(timeFormatControl.config.default).toBe('smart_date'); -}); - -test('scatter chart control panel should have visibility function for x_axis_time_format', () => { - expect(timeFormatControl).toBeDefined(); - expect(timeFormatControl.config.visibility).toBeDefined(); - expect(typeof timeFormatControl.config.visibility).toBe('function'); - - // The visibility function exists - the exact logic is tested implicitly through UI behavior - // The important part is that the control has proper visibility configuration -}); - -const isTimeVisible = ( - xAxisColumn: string | null, - xAxisType: GenericDataType | null, -): boolean => { - const props = mockControls(xAxisColumn, xAxisType); - const visibilityFn = timeFormatControl?.config?.visibility; - return visibilityFn ? visibilityFn(props) : false; -}; - -test('x_axis_time_format control should be visible for temporal data types', () => { - expect(isTimeVisible('time_column', GenericDataType.Temporal)).toBe(true); -}); - -test('x_axis_time_format control should be hidden for non-temporal data types', () => { - expect(isTimeVisible(null, null)).toBe(false); - expect(isTimeVisible('float_column', GenericDataType.Numeric)).toBe(false); - expect(isTimeVisible('name_column', GenericDataType.String)).toBe(false); -}); - -// tests for x_axis_number_format control -const numberFormatControl: any = getControl('x_axis_number_format'); - -test('scatter chart control panel should include x_axis_number_format control in the panel', () => { - expect(numberFormatControl).toBeDefined(); -}); - -test('scatter chart control panel should have correct default value for x_axis_number_format', () => { - expect(numberFormatControl).toBeDefined(); - expect(numberFormatControl.config).toBeDefined(); - expect(numberFormatControl.config.default).toBe('~g'); -}); - -test('scatter chart control panel should have visibility function for x_axis_number_format', () => { - expect(numberFormatControl).toBeDefined(); - expect(numberFormatControl.config.visibility).toBeDefined(); - expect(typeof numberFormatControl.config.visibility).toBe('function'); - - // The visibility function exists - the exact logic is tested implicitly through UI behavior - // The important part is that the control has proper visibility configuration -}); - -const isNumberVisible = ( - xAxisColumn: string | null, - xAxisType: GenericDataType | null, -): boolean => { - const props = mockControls(xAxisColumn, xAxisType); - const visibilityFn = numberFormatControl?.config?.visibility; - return visibilityFn ? visibilityFn(props) : false; -}; - -test('x_axis_number_format control should be visible for numeric data types', () => { - expect(isNumberVisible('float_column', GenericDataType.Numeric)).toBe(true); - expect(isNumberVisible('int_column', GenericDataType.Numeric)).toBe(true); -}); - -test('x_axis_number_format control should be hidden for non-numeric data types', () => { - expect(isNumberVisible('string_column', GenericDataType.String)).toBe(false); - expect(isNumberVisible(null, null)).toBe(false); - expect(isNumberVisible('time_column', GenericDataType.Temporal)).toBe(false); -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/Scatter/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/Scatter/transformProps.test.ts deleted file mode 100644 index 401f85ed5cf..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/Scatter/transformProps.test.ts +++ /dev/null @@ -1,177 +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 { ChartProps, SMART_DATE_ID } from '@superset-ui/core'; -import transformProps from '../../../src/Timeseries/transformProps'; -import { DEFAULT_FORM_DATA } from '../../../src/Timeseries/constants'; -import { - EchartsTimeseriesSeriesType, - EchartsTimeseriesFormData, - EchartsTimeseriesChartProps, -} from '../../../src/Timeseries/types'; -import { GenericDataType } from '@apache-superset/core/common'; -import { - D3_FORMAT_OPTIONS, - D3_TIME_FORMAT_OPTIONS, -} from '@superset-ui/chart-controls'; -import { supersetTheme } from '@apache-superset/core/theme'; - -describe('Scatter Chart X-axis Time Formatting', () => { - const baseFormData: EchartsTimeseriesFormData = { - ...DEFAULT_FORM_DATA, - colorScheme: 'supersetColors', - datasource: '1__table', - granularity_sqla: '__timestamp', - metric: ['column 1'], - groupby: [], - viz_type: 'echarts_timeseries_scatter', - seriesType: EchartsTimeseriesSeriesType.Scatter, - }; - - const timeseriesData = [ - { - data: [ - { column_1: 0.72099, __timestamp: 1609459200000 }, - { column_1: 0.77954, __timestamp: 1612137600000 }, - { column_1: 2.83434, __timestamp: 1614556800000 }, - ], - colnames: ['column_1', '__timestamp'], - coltypes: [GenericDataType.Numeric, GenericDataType.Temporal], - }, - ]; - - const baseChartPropsConfig = { - width: 800, - height: 600, - queriesData: timeseriesData, - theme: supersetTheme, - }; - - test('xAxisTimeFormat has no default formatter', () => { - const chartProps = new ChartProps({ - ...baseChartPropsConfig, - formData: baseFormData, - }); - - const transformedProps = transformProps( - chartProps as EchartsTimeseriesChartProps, - ); - - expect(transformedProps.echartOptions.xAxis).toHaveProperty('axisLabel'); - const xAxis = transformedProps.echartOptions.xAxis as any; - expect(xAxis.axisLabel).toHaveProperty('formatter'); - expect(typeof xAxis.axisLabel.formatter).toBe('function'); - }); - - test.each(D3_TIME_FORMAT_OPTIONS.map(([id]) => id))( - 'should handle %s format', - format => { - const chartProps = new ChartProps({ - ...baseChartPropsConfig, - formData: { - ...baseFormData, - xAxisTimeFormat: format, - }, - }); - - const transformedProps = transformProps( - chartProps as EchartsTimeseriesChartProps, - ); - - const xAxis = transformedProps.echartOptions.xAxis as any; - expect(xAxis.axisLabel).toHaveProperty('formatter'); - expect(typeof xAxis.axisLabel.formatter).toBe('function'); - if (format !== SMART_DATE_ID) { - expect(xAxis.axisLabel.formatter.id).toBe(format); - } - }, - ); -}); - -describe('Scatter Chart X-axis Number Formatting', () => { - const baseFormData: EchartsTimeseriesFormData = { - ...DEFAULT_FORM_DATA, - colorScheme: 'supersetColors', - datasource: '1__table', - metric: ['column_1'], - x_axis: 'column_2', - groupby: [], - viz_type: 'echarts_timeseries_scatter', - seriesType: EchartsTimeseriesSeriesType.Scatter, - }; - - const timeseriesData = [ - { - data: [ - { column_1: 0.72099, column_2: 3.01699 }, - { column_1: 0.77954, column_2: 3.44802 }, - { column_1: 2.83434, column_2: 3.58095 }, - ], - colnames: ['column_1', 'column_2'], - coltypes: [GenericDataType.Numeric, GenericDataType.Numeric], - }, - ]; - - const baseChartPropsConfig = { - width: 800, - height: 600, - queriesData: timeseriesData, - theme: supersetTheme, - }; - - test('should use SMART_NUMBER as default xAxisNumberFormat', () => { - const chartProps = new ChartProps({ - ...baseChartPropsConfig, - formData: baseFormData, - }); - - const transformedProps = transformProps( - chartProps as EchartsTimeseriesChartProps, - ); - - expect(transformedProps.echartOptions.xAxis).toHaveProperty('axisLabel'); - const xAxis = transformedProps.echartOptions.xAxis as any; - expect(xAxis.axisLabel).toHaveProperty('formatter'); - expect(typeof xAxis.axisLabel.formatter).toBe('function'); - expect(xAxis.axisLabel.formatter.id).toBe('SMART_NUMBER'); - }); - - test.each(D3_FORMAT_OPTIONS.map(([id]) => id))( - 'should handle %s format', - format => { - const chartProps = new ChartProps({ - ...baseChartPropsConfig, - formData: { - ...baseFormData, - xAxisNumberFormat: format, - }, - }); - - const transformedProps = transformProps( - chartProps as EchartsTimeseriesChartProps, - ); - - expect(transformedProps.echartOptions.xAxis).toHaveProperty('axisLabel'); - const xAxis = transformedProps.echartOptions.xAxis as any; - expect(xAxis.axisLabel).toHaveProperty('formatter'); - expect(typeof xAxis.axisLabel.formatter).toBe('function'); - expect(xAxis.axisLabel.formatter.id).toBe(format); - }, - ); -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/SmoothLine/controlPanel.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/SmoothLine/controlPanel.test.ts deleted file mode 100644 index 1c1a634db3d..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/SmoothLine/controlPanel.test.ts +++ /dev/null @@ -1,101 +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 { ControlPanelsContainerProps } from '@superset-ui/chart-controls/types'; -import { GenericDataType } from '@apache-superset/core/common'; -import controlPanel from '../../../src/Timeseries/Regular/SmoothLine/controlPanel'; - -const config = controlPanel; - -const getControl = (controlName: string) => { - for (const section of config.controlPanelSections) { - if (section && section.controlSetRows) { - for (const row of section.controlSetRows) { - for (const control of row) { - if ( - typeof control === 'object' && - control !== null && - 'name' in control && - control.name === controlName - ) { - return control; - } - } - } - } - } - - return null; -}; - -const mockControls = ( - xAxisColumn: string | null, - typeGeneric: GenericDataType | null, -): ControlPanelsContainerProps => { - const columns = - xAxisColumn && typeGeneric !== null - ? [{ column_name: xAxisColumn, type_generic: typeGeneric }] - : []; - - return { - controls: { - // @ts-expect-error - x_axis: { - value: xAxisColumn, - }, - // @ts-expect-error - datasource: { - datasource: { columns }, - }, - }, - }; -}; - -const timeFormatControl: any = getControl('x_axis_time_format'); -const numberFormatControl: any = getControl('x_axis_number_format'); - -test('should include x_axis_time_format control', () => { - expect(timeFormatControl).toBeDefined(); - expect(timeFormatControl.config.default).toBe('smart_date'); -}); - -test('should include x_axis_number_format control', () => { - expect(numberFormatControl).toBeDefined(); - expect(numberFormatControl.config.default).toBe('~g'); -}); - -test('x_axis_number_format should be visible for numeric columns', () => { - const visibilityFn = numberFormatControl?.config?.visibility; - expect(visibilityFn(mockControls('year', GenericDataType.Numeric))).toBe( - true, - ); -}); - -test('x_axis_number_format should be hidden for temporal columns', () => { - const visibilityFn = numberFormatControl?.config?.visibility; - expect(visibilityFn(mockControls('date', GenericDataType.Temporal))).toBe( - false, - ); -}); - -test('x_axis_number_format should be hidden for string columns', () => { - const visibilityFn = numberFormatControl?.config?.visibility; - expect(visibilityFn(mockControls('name', GenericDataType.String))).toBe( - false, - ); -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/Step/controlPanel.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/Step/controlPanel.test.ts deleted file mode 100644 index dcd36ebacd2..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/Step/controlPanel.test.ts +++ /dev/null @@ -1,115 +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 { ControlPanelsContainerProps } from '@superset-ui/chart-controls/types'; -import { GenericDataType } from '@apache-superset/core/common'; -import controlPanel from '../../../src/Timeseries/Step/controlPanel'; - -const config = controlPanel; - -const getControl = (controlName: string) => { - for (const section of config.controlPanelSections) { - if (section && section.controlSetRows) { - for (const row of section.controlSetRows) { - for (const control of row) { - if ( - typeof control === 'object' && - control !== null && - 'name' in control && - control.name === controlName - ) { - return control; - } - } - } - } - } - - return null; -}; - -const mockControls = ( - xAxisColumn: string | null, - typeGeneric: GenericDataType | null, -): ControlPanelsContainerProps => { - const columns = - xAxisColumn && typeGeneric !== null - ? [{ column_name: xAxisColumn, type_generic: typeGeneric }] - : []; - - return { - controls: { - // @ts-expect-error - x_axis: { - value: xAxisColumn, - }, - // @ts-expect-error - datasource: { - datasource: { columns }, - }, - }, - }; -}; - -const timeFormatControl: any = getControl('x_axis_time_format'); -const numberFormatControl: any = getControl('x_axis_number_format'); - -test('should include x_axis_time_format control', () => { - expect(timeFormatControl).toBeDefined(); - expect(timeFormatControl.config.default).toBe('smart_date'); -}); - -test('should include x_axis_number_format control', () => { - expect(numberFormatControl).toBeDefined(); - expect(numberFormatControl.config.default).toBe('~g'); -}); - -test('x_axis_time_format should be visible for temporal columns', () => { - const visibilityFn = timeFormatControl?.config?.visibility; - expect(visibilityFn(mockControls('date', GenericDataType.Temporal))).toBe( - true, - ); -}); - -test('x_axis_time_format should be hidden for numeric columns', () => { - const visibilityFn = timeFormatControl?.config?.visibility; - expect(visibilityFn(mockControls('year', GenericDataType.Numeric))).toBe( - false, - ); -}); - -test('x_axis_number_format should be visible for numeric columns', () => { - const visibilityFn = numberFormatControl?.config?.visibility; - expect(visibilityFn(mockControls('year', GenericDataType.Numeric))).toBe( - true, - ); -}); - -test('x_axis_number_format should be hidden for temporal columns', () => { - const visibilityFn = numberFormatControl?.config?.visibility; - expect(visibilityFn(mockControls('date', GenericDataType.Temporal))).toBe( - false, - ); -}); - -test('x_axis_number_format should be hidden for string columns', () => { - const visibilityFn = numberFormatControl?.config?.visibility; - expect(visibilityFn(mockControls('name', GenericDataType.String))).toBe( - false, - ); -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/buildQuery.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/buildQuery.test.ts deleted file mode 100644 index 8caef0d58fe..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/buildQuery.test.ts +++ /dev/null @@ -1,135 +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 { SqlaFormData, VizType } from '@superset-ui/core'; -import buildQuery from '../../src/Timeseries/buildQuery'; - -describe('Timeseries buildQuery', () => { - const formData = { - datasource: '5__table', - granularity_sqla: 'ds', - metrics: ['bar', 'baz'], - viz_type: 'my_chart', - }; - - test('should build groupby with series in form data', () => { - const queryContext = buildQuery(formData); - const [query] = queryContext.queries; - expect(query.metrics).toEqual(['bar', 'baz']); - }); - - test('should order by timeseries limit if orderby unspecified', () => { - const queryContext = buildQuery({ - ...formData, - timeseries_limit_metric: 'bar', - order_desc: true, - }); - const [query] = queryContext.queries; - expect(query.metrics).toEqual(['bar', 'baz']); - expect(query.series_limit_metric).toEqual('bar'); - expect(query.order_desc).toEqual(true); - expect(query.orderby).toEqual([['bar', false]]); - }); - - test('should not order by timeseries limit if orderby provided', () => { - const queryContext = buildQuery({ - ...formData, - timeseries_limit_metric: 'bar', - order_desc: true, - orderby: [['foo', true]], - }); - const [query] = queryContext.queries; - expect(query.metrics).toEqual(['bar', 'baz']); - expect(query.series_limit_metric).toEqual('bar'); - expect(query.order_desc).toEqual(true); - expect(query.orderby).toEqual([['foo', true]]); - }); -}); - -describe('queryObject conversion', () => { - const formData: SqlaFormData = { - datasource: '5__table', - viz_type: VizType.Table, - granularity_sqla: 'time_column', - time_grain_sqla: 'P1Y', - time_range: '1 year ago : 2013', - groupby: ['col1'], - metrics: ['count(*)'], - }; - - test("shouldn't convert queryObject", () => { - const { queries } = buildQuery(formData); - expect(queries[0]).toEqual( - expect.objectContaining({ - granularity: 'time_column', - time_range: '1 year ago : 2013', - extras: { time_grain_sqla: 'P1Y', having: '', where: '' }, - columns: ['col1'], - series_columns: ['col1'], - metrics: ['count(*)'], - is_timeseries: true, - post_processing: [ - { - operation: 'pivot', - options: { - aggregates: { 'count(*)': { operator: 'mean' } }, - columns: ['col1'], - drop_missing_columns: true, - index: ['__timestamp'], - }, - }, - { operation: 'flatten' }, - ], - }), - ); - }); - - test('should convert queryObject', () => { - const { queries } = buildQuery({ ...formData, x_axis: 'time_column' }); - expect(queries[0]).toMatchObject({ - granularity: 'time_column', - time_range: '1 year ago : 2013', - extras: { having: '', where: '', time_grain_sqla: 'P1Y' }, - columns: [ - { - columnType: 'BASE_AXIS', - expressionType: 'SQL', - label: 'time_column', - sqlExpression: 'time_column', - timeGrain: 'P1Y', - isColumnReference: true, - }, - 'col1', - ], - series_columns: ['col1'], - metrics: ['count(*)'], - post_processing: [ - { - operation: 'pivot', - options: { - aggregates: { 'count(*)': { operator: 'mean' } }, - columns: ['col1'], - drop_missing_columns: true, - index: ['time_column'], - }, - }, - { operation: 'flatten' }, - ], - }); - }); -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformProps.test.ts deleted file mode 100644 index a43610f3348..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformProps.test.ts +++ /dev/null @@ -1,1635 +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 { - AnnotationSourceType, - AnnotationStyle, - AnnotationType, - AxisType, - ComparisonType, - DataRecord, - EventAnnotationLayer, - FormulaAnnotationLayer, - IntervalAnnotationLayer, - SqlaFormData, - TimeseriesAnnotationLayer, - ChartDataResponseResult, - TimeGranularity, -} from '@superset-ui/core'; -import { GenericDataType } from '@apache-superset/core/common'; -import { EchartsTimeseriesChartProps } from '../../src/types'; -import type { SeriesOption } from 'echarts'; -import transformProps from '../../src/Timeseries/transformProps'; -import { - EchartsTimeseriesSeriesType, - OrientationType, - EchartsTimeseriesFormData, -} from '../../src/Timeseries/types'; -import { StackControlsValue, TIMESERIES_CONSTANTS } from '../../src/constants'; -import { LegendOrientation, LegendType } from '../../src/types'; -import { DEFAULT_FORM_DATA } from '../../src/Timeseries/constants'; -import { createEchartsTimeseriesTestChartProps } from '../helpers'; -import { BASE_TIMESTAMP, createTestData } from './helpers'; - -/** - * Creates a partial ChartDataResponseResult for testing. - * Only includes the fields needed for tests, with sensible defaults for required fields. - */ -function createTestQueryData( - data: unknown[], - overrides?: Partial & { - label_map?: Record; - }, -): ChartDataResponseResult { - return { - annotation_data: null, - cache_key: null, - cache_timeout: null, - cached_dttm: null, - queried_dttm: null, - data: data as DataRecord[], - colnames: [], - coltypes: [], - error: null, - is_cached: false, - query: '', - rowcount: data.length, - sql_rowcount: data.length, - stacktrace: null, - status: 'success', - from_dttm: null, - to_dttm: null, - label_map: {}, - ...overrides, - } as ChartDataResponseResult & { label_map?: Record }; -} - -type YAxisFormatter = (value: number, index: number) => string; - -function getYAxisFormatter( - transformed: ReturnType, -): YAxisFormatter { - const yAxis = transformed.echartOptions.yAxis as { - axisLabel?: { formatter?: YAxisFormatter }; - }; - expect(yAxis).toBeDefined(); - expect(yAxis.axisLabel).toBeDefined(); - expect(yAxis.axisLabel?.formatter).toBeDefined(); - return yAxis.axisLabel!.formatter!; -} - -const queriesData: ChartDataResponseResult[] = [ - createTestQueryData( - createTestData( - [ - { 'San Francisco': 1, 'New York': 2 }, - { 'San Francisco': 3, 'New York': 4 }, - ], - { intervalMs: 300000000 }, - ), - ), -]; - -/** - * Creates a properly typed EchartsTimeseriesChartProps for testing. - * Uses shared createEchartsTimeseriesTestChartProps with Timeseries defaults. - */ -function createTestChartProps(config: { - formData?: Partial; - queriesData?: ChartDataResponseResult[]; - annotationData?: Record; - datasource?: { - verboseMap?: Record; - columnFormats?: Record; - currencyFormats?: Record< - string, - { symbol: string; symbolPosition: string } - >; - currencyCodeColumn?: string; - }; - width?: number; - height?: number; -}): EchartsTimeseriesChartProps { - return createEchartsTimeseriesTestChartProps< - EchartsTimeseriesFormData, - EchartsTimeseriesChartProps - >({ - defaultFormData: DEFAULT_FORM_DATA, - defaultVizType: 'my_viz', - defaultQueriesData: queriesData, - ...config, - }); -} - -const formData: SqlaFormData = { - colorScheme: 'bnbColors', - datasource: '3__table', - granularity_sqla: 'ds', - metric: 'sum__num', - groupby: ['foo', 'bar'], - viz_type: 'my_viz', -}; - -describe('EchartsTimeseries transformProps', () => { - test('should transform chart props for viz', () => { - const chartProps = createTestChartProps({}); - expect(transformProps(chartProps)).toEqual( - expect.objectContaining({ - width: 800, - height: 600, - echartOptions: expect.objectContaining({ - legend: expect.objectContaining({ - data: ['San Francisco', 'New York'], - }), - series: expect.arrayContaining([ - expect.objectContaining({ - data: [ - [BASE_TIMESTAMP, 1], - [BASE_TIMESTAMP + 300000000, 3], - ], - name: 'San Francisco', - }), - expect.objectContaining({ - data: [ - [BASE_TIMESTAMP, 2], - [BASE_TIMESTAMP + 300000000, 4], - ], - name: 'New York', - }), - ]), - }), - }), - ); - }); - - test('should transform chart props for horizontal viz', () => { - const chartProps = createTestChartProps({ - formData: { - ...formData, - orientation: OrientationType.Horizontal, - }, - }); - expect(transformProps(chartProps)).toEqual( - expect.objectContaining({ - width: 800, - height: 600, - echartOptions: expect.objectContaining({ - legend: expect.objectContaining({ - data: ['San Francisco', 'New York'], - }), - series: expect.arrayContaining([ - expect.objectContaining({ - data: [ - [1, BASE_TIMESTAMP], - [3, BASE_TIMESTAMP + 300000000], - ], - name: 'San Francisco', - }), - expect.objectContaining({ - data: [ - [2, BASE_TIMESTAMP], - [4, BASE_TIMESTAMP + 300000000], - ], - name: 'New York', - }), - ]), - }), - }), - ); - }); - - test('should add a formula annotation to viz', () => { - const formula: FormulaAnnotationLayer = { - name: 'My Formula', - annotationType: AnnotationType.Formula, - value: 'x+1', - style: AnnotationStyle.Solid, - show: true, - showLabel: true, - }; - const chartProps = createTestChartProps({ - formData: { - ...formData, - annotationLayers: [formula], - }, - }); - expect(transformProps(chartProps)).toEqual( - expect.objectContaining({ - width: 800, - height: 600, - echartOptions: expect.objectContaining({ - legend: expect.objectContaining({ - data: ['San Francisco', 'New York', 'My Formula'], - }), - series: expect.arrayContaining([ - expect.objectContaining({ - data: [ - [BASE_TIMESTAMP, 1], - [BASE_TIMESTAMP + 300000000, 3], - ], - name: 'San Francisco', - }), - expect.objectContaining({ - data: [ - [BASE_TIMESTAMP, 2], - [BASE_TIMESTAMP + 300000000, 4], - ], - name: 'New York', - }), - expect.objectContaining({ - data: [ - [BASE_TIMESTAMP, BASE_TIMESTAMP + 1], - [BASE_TIMESTAMP + 300000000, BASE_TIMESTAMP + 300000000 + 1], - ], - name: 'My Formula', - }), - ]), - }), - }), - ); - }); - - test('should add a formula annotation when X-axis column has dataset-level label', () => { - const formula: FormulaAnnotationLayer = { - name: 'My Formula', - annotationType: AnnotationType.Formula, - value: 'x*2', - style: AnnotationStyle.Solid, - show: true, - showLabel: true, - }; - const timeColumnName = 'ds'; - const timeColumnLabel = 'Time Label'; - const testData = [ - { - [timeColumnLabel]: new Date(BASE_TIMESTAMP).toISOString(), - 'San Francisco': 1, - 'New York': 2, - }, - { - [timeColumnLabel]: new Date(BASE_TIMESTAMP + 300000000).toISOString(), - 'San Francisco': 3, - 'New York': 4, - }, - ]; - const chartProps = createTestChartProps({ - formData: { - ...formData, - x_axis: timeColumnName, - granularity_sqla: timeColumnName, - annotationLayers: [formula], - }, - queriesData: [createTestQueryData(testData)], - datasource: { - verboseMap: { - [timeColumnName]: timeColumnLabel, - }, - columnFormats: {}, - currencyFormats: {}, - }, - }); - const result = transformProps(chartProps); - const formulaSeries = ( - result.echartOptions.series as SeriesOption[] | undefined - )?.find((s: SeriesOption) => s.name === 'My Formula'); - expect(formulaSeries).toBeDefined(); - expect(formulaSeries?.data).toBeDefined(); - expect(Array.isArray(formulaSeries?.data)).toBe(true); - expect((formulaSeries!.data as unknown[]).length).toBeGreaterThan(0); - const firstDataPoint = (formulaSeries!.data as [number, number][])[0]; - expect(firstDataPoint).toBeDefined(); - expect(firstDataPoint[1]).toBe(firstDataPoint[0] * 2); - }); - - test('should add a formula annotation when X-axis column has dataset-level label and verboseMap is empty (backward compatibility)', () => { - const formula: FormulaAnnotationLayer = { - name: 'My Formula', - annotationType: AnnotationType.Formula, - value: 'x+1', - style: AnnotationStyle.Solid, - show: true, - showLabel: true, - }; - const chartProps = createTestChartProps({ - formData: { - ...formData, - annotationLayers: [formula], - }, - datasource: { - verboseMap: {}, - columnFormats: {}, - currencyFormats: {}, - }, - }); - const result = transformProps(chartProps); - const formulaSeries = ( - result.echartOptions.series as SeriesOption[] | undefined - )?.find((s: SeriesOption) => s.name === 'My Formula'); - expect(formulaSeries).toBeDefined(); - expect(formulaSeries?.data).toBeDefined(); - expect(Array.isArray(formulaSeries?.data)).toBe(true); - }); - - test('should add a formula annotation when X-axis column has dataset-level label in horizontal orientation', () => { - const formula: FormulaAnnotationLayer = { - name: 'My Formula', - annotationType: AnnotationType.Formula, - value: 'x*2', - style: AnnotationStyle.Solid, - show: true, - showLabel: true, - }; - const timeColumnName = 'ds'; - const timeColumnLabel = 'Time Label'; - const testData = [ - { - [timeColumnLabel]: new Date(BASE_TIMESTAMP).toISOString(), - 'San Francisco': 1, - 'New York': 2, - }, - { - [timeColumnLabel]: new Date(BASE_TIMESTAMP + 300000000).toISOString(), - 'San Francisco': 3, - 'New York': 4, - }, - ]; - const chartProps = createTestChartProps({ - formData: { - ...formData, - x_axis: timeColumnName, - granularity_sqla: timeColumnName, - orientation: OrientationType.Horizontal, - annotationLayers: [formula], - }, - queriesData: [createTestQueryData(testData)], - datasource: { - verboseMap: { - [timeColumnName]: timeColumnLabel, - }, - columnFormats: {}, - currencyFormats: {}, - }, - }); - const result = transformProps(chartProps); - const formulaSeries = ( - result.echartOptions.series as SeriesOption[] | undefined - )?.find((s: SeriesOption) => s.name === 'My Formula'); - expect(formulaSeries).toBeDefined(); - const firstDataPoint = (formulaSeries!.data as [number, number][])[0]; - expect(firstDataPoint).toBeDefined(); - expect(firstDataPoint[0]).toBe(firstDataPoint[1] * 2); - }); - - test('should add an interval, event and timeseries annotation to viz', () => { - const event: EventAnnotationLayer = { - annotationType: AnnotationType.Event, - name: 'My Event', - show: true, - showLabel: true, - sourceType: AnnotationSourceType.Native, - style: AnnotationStyle.Solid, - value: 1, - }; - - const interval: IntervalAnnotationLayer = { - annotationType: AnnotationType.Interval, - name: 'My Interval', - show: true, - showLabel: true, - sourceType: AnnotationSourceType.Table, - titleColumn: '', - timeColumn: 'start', - intervalEndColumn: '', - descriptionColumns: [], - style: AnnotationStyle.Dashed, - value: 2, - }; - - const timeseries: TimeseriesAnnotationLayer = { - annotationType: AnnotationType.Timeseries, - name: 'My Timeseries', - show: true, - showLabel: true, - sourceType: AnnotationSourceType.Line, - style: AnnotationStyle.Solid, - titleColumn: '', - value: 3, - }; - const annotationData = { - 'My Event': { - columns: [ - 'start_dttm', - 'end_dttm', - 'short_descr', - 'long_descr', - 'json_metadata', - ], - records: [ - { - start_dttm: 0, - end_dttm: 1000, - short_descr: '', - long_descr: '', - json_metadata: null, - }, - ], - }, - 'My Interval': { - columns: ['start', 'end', 'title'], - records: [ - { - start: 2000, - end: 3000, - title: 'My Title', - }, - ], - }, - 'My Timeseries': { - records: [ - { x: 10000, y: 11000 }, - { x: 20000, y: 21000 }, - ], - }, - }; - const chartProps = createTestChartProps({ - formData: { - ...formData, - annotationLayers: [event, interval, timeseries], - }, - annotationData, - queriesData: [ - { - ...(queriesData[0] as ChartDataResponseResult), - annotation_data: annotationData, - }, - ], - }); - expect(transformProps(chartProps)).toEqual( - expect.objectContaining({ - echartOptions: expect.objectContaining({ - legend: expect.objectContaining({ - data: ['San Francisco', 'New York', 'My Timeseries'], - }), - series: expect.arrayContaining([ - expect.objectContaining({ - type: 'line', - id: 'My Timeseries', - }), - expect.objectContaining({ - type: 'line', - id: 'Event - My Event', - }), - expect.objectContaining({ - type: 'line', - id: 'Interval - My Interval', - }), - ]), - }), - }), - ); - }); - - test('Should add a baseline series for stream graph', () => { - const streamQueriesDataTyped: ChartDataResponseResult[] = [ - createTestQueryData( - createTestData( - [ - { - 'San Francisco': 120, - 'New York': 220, - Boston: 150, - Miami: 270, - Denver: 800, - }, - { - 'San Francisco': 150, - 'New York': 190, - Boston: 240, - Miami: 350, - Denver: 700, - }, - { - 'San Francisco': 130, - 'New York': 300, - Boston: 250, - Miami: 410, - Denver: 650, - }, - { - 'San Francisco': 90, - 'New York': 340, - Boston: 300, - Miami: 480, - Denver: 590, - }, - { - 'San Francisco': 260, - 'New York': 200, - Boston: 420, - Miami: 490, - Denver: 760, - }, - { - 'San Francisco': 250, - 'New York': 250, - Boston: 380, - Miami: 360, - Denver: 400, - }, - { - 'San Francisco': 160, - 'New York': 210, - Boston: 330, - Miami: 440, - Denver: 580, - }, - ], - { intervalMs: 1 }, - ), - ), - ]; - const streamFormData: Partial = { - ...formData, - stack: StackControlsValue.Stream, - }; - const chartProps = createTestChartProps({ - formData: streamFormData, - queriesData: streamQueriesDataTyped, - }); - expect( - (transformProps(chartProps).echartOptions.series as any[])[0], - ).toEqual({ - areaStyle: { - opacity: 0, - }, - lineStyle: { - opacity: 0, - }, - name: 'baseline', - showSymbol: false, - silent: true, - smooth: false, - stack: 'obs', - stackStrategy: 'all', - step: undefined, - tooltip: { - show: false, - }, - type: 'line', - data: [ - [BASE_TIMESTAMP, -415.7692307692308], - [BASE_TIMESTAMP + 1, -403.6219915054271], - [BASE_TIMESTAMP + 2, -476.32314093071443], - [BASE_TIMESTAMP + 3, -514.2120298196033], - [BASE_TIMESTAMP + 4, -485.7378514158475], - [BASE_TIMESTAMP + 5, -419.6402904402378], - [BASE_TIMESTAMP + 6, -442.9833136960517], - ], - }); - }); -}); - -describe('Does transformProps transform series correctly', () => { - type seriesDataType = [Date, number]; - type labelFormatterType = (params: { - value: seriesDataType; - dataIndex: number; - seriesIndex: number; - }) => string; - type seriesType = { - label: { show: boolean; formatter: labelFormatterType }; - data: seriesDataType[]; - name: string; - }; - - const formData: SqlaFormData = { - viz_type: 'my_viz', - colorScheme: 'bnbColors', - datasource: '3__table', - granularity_sqla: 'ds', - metric: 'sum__num', - groupby: ['foo', 'bar'], - showValue: true, - stack: true, - onlyTotal: false, - percentageThreshold: 50, - }; - const queriesData: ChartDataResponseResult[] = [ - createTestQueryData( - createTestData( - [ - { - 'San Francisco': 1, - 'New York': 2, - Boston: 1, - }, - { - 'San Francisco': 3, - 'New York': 4, - Boston: 1, - }, - { - 'San Francisco': 5, - 'New York': 8, - Boston: 6, - }, - { - 'San Francisco': 2, - 'New York': 7, - Boston: 2, - }, - ], - { intervalMs: 300000000 }, - ), - ), - ]; - - const totalStackedValues = queriesData[0].data.reduce( - (totals, currentStack) => { - const total = Object.keys(currentStack).reduce((stackSum, key) => { - if (key === '__timestamp') return stackSum; - const val = currentStack[key as keyof typeof currentStack]; - return stackSum + (typeof val === 'number' ? val : 0); - }, 0); - totals.push(total); - return totals; - }, - [] as number[], - ); - - test('should show labels when showValue is true', () => { - const chartProps = createTestChartProps({ formData, queriesData }); - - const transformedSeries = transformProps(chartProps).echartOptions - .series as seriesType[]; - - transformedSeries.forEach(series => { - expect(series.label.show).toBe(true); - }); - }); - - test('should not show labels when showValue is false', () => { - const chartProps = createTestChartProps({ - formData: { ...formData, showValue: false }, - queriesData, - }); - - const transformedSeries = transformProps(chartProps).echartOptions - .series as seriesType[]; - - transformedSeries.forEach(series => { - expect(series.label.show).toBe(false); - }); - }); - - test('should show only totals when onlyTotal is true', () => { - const chartProps = createTestChartProps({ - formData: { ...formData, onlyTotal: true }, - queriesData, - }); - - const transformedSeries = transformProps(chartProps).echartOptions - .series as seriesType[]; - - const showValueIndexes: number[] = []; - - transformedSeries.forEach((entry, seriesIndex) => { - const { data = [] } = entry; - (data as [Date, number][]).forEach((datum, dataIndex) => { - if (datum[1] !== null) { - showValueIndexes[dataIndex] = seriesIndex; - } - }); - }); - - transformedSeries.forEach((series, seriesIndex) => { - expect(series.label.show).toBe(true); - series.data.forEach((value, dataIndex) => { - const params = { - value, - dataIndex, - seriesIndex, - }; - - let expectedLabel: string; - - if (seriesIndex === showValueIndexes[dataIndex]) { - expectedLabel = String(totalStackedValues[dataIndex]); - } else { - expectedLabel = ''; - } - - expect(series.label.formatter(params)).toBe(expectedLabel); - }); - }); - }); - - test('should show labels on values >= percentageThreshold if onlyTotal is false', () => { - const chartProps = createTestChartProps({ formData, queriesData }); - - const transformedSeries = transformProps(chartProps).echartOptions - .series as seriesType[]; - - const expectedThresholds = totalStackedValues.map( - total => ((formData.percentageThreshold || 0) / 100) * total, - ); - - transformedSeries.forEach((series, seriesIndex) => { - expect(series.label.show).toBe(true); - series.data.forEach((value, dataIndex) => { - const params = { - value, - dataIndex, - seriesIndex, - }; - const expectedLabel = - value[1] >= expectedThresholds[dataIndex] ? String(value[1]) : ''; - expect(series.label.formatter(params)).toBe(expectedLabel); - }); - }); - }); - - test('should not apply percentage threshold when showValue is true and stack is false', () => { - const chartProps = createTestChartProps({ - formData: { ...formData, stack: false }, - queriesData, - }); - - const transformedSeries = transformProps(chartProps).echartOptions - .series as seriesType[]; - - transformedSeries.forEach((series, seriesIndex) => { - expect(series.label.show).toBe(true); - series.data.forEach((value, dataIndex) => { - const params = { - value, - dataIndex, - seriesIndex, - }; - const expectedLabel = String(value[1]); - expect(series.label.formatter(params)).toBe(expectedLabel); - }); - }); - }); - - test('should remove time shift labels from label_map', () => { - const chartProps = createTestChartProps({ - formData: { - ...formData, - timeCompare: ['1 year ago'], - }, - queriesData: [ - createTestQueryData(queriesData[0].data as DataRecord[], { - label_map: { - '1 year ago, foo1, bar1': ['1 year ago', 'foo1', 'bar1'], - '1 year ago, foo2, bar2': ['1 year ago', 'foo2', 'bar2'], - 'foo1, bar1': ['foo1', 'bar1'], - 'foo2, bar2': ['foo2', 'bar2'], - }, - }), - ], - }); - const transformedProps = transformProps(chartProps); - expect(transformedProps.labelMap).toEqual({ - '1 year ago, foo1, bar1': ['foo1', 'bar1'], - '1 year ago, foo2, bar2': ['foo2', 'bar2'], - 'foo1, bar1': ['foo1', 'bar1'], - 'foo2, bar2': ['foo2', 'bar2'], - }); - }); -}); - -describe('legend sorting', () => { - const legendSortData: ChartDataResponseResult[] = [ - createTestQueryData( - createTestData( - [ - { - Milton: 40, - 'San Francisco': 1, - 'New York': 2, - Boston: 1, - }, - { - Milton: 20, - 'San Francisco': 3, - 'New York': 4, - Boston: 1, - }, - { - Milton: 60, - 'San Francisco': 5, - 'New York': 8, - Boston: 6, - }, - { - Milton: 10, - 'San Francisco': 2, - 'New York': 7, - Boston: 2, - }, - ], - { intervalMs: 300000000 }, - ), - ), - ]; - - const getChartProps = (formDataOverrides: Partial) => - createTestChartProps({ - formData: { ...formData, ...formDataOverrides }, - queriesData: legendSortData, - }); - - test('sort legend by data', () => { - const chartProps = getChartProps({ - legendSort: null, - sortSeriesType: 'min', - sortSeriesAscending: true, - }); - const transformed = transformProps(chartProps); - - expect((transformed.echartOptions.legend as any).data).toEqual([ - 'Boston', - 'San Francisco', - 'New York', - 'Milton', - ]); - }); - - test('sort legend by label ascending', () => { - const chartProps = getChartProps({ - legendSort: 'asc', - sortSeriesType: 'min', - sortSeriesAscending: true, - }); - const transformed = transformProps(chartProps); - - expect((transformed.echartOptions.legend as any).data).toEqual([ - 'Boston', - 'Milton', - 'New York', - 'San Francisco', - ]); - }); - - test('sort legend by label descending', () => { - const chartProps = getChartProps({ - legendSort: 'desc', - sortSeriesType: 'min', - sortSeriesAscending: true, - }); - const transformed = transformProps(chartProps); - - expect((transformed.echartOptions.legend as any).data).toEqual([ - 'San Francisco', - 'New York', - 'Milton', - 'Boston', - ]); - }); - - test('falls back to scroll for zoomable top legends when toolbox space reduces available width', () => { - const narrowLegendData = [ - createTestQueryData( - createTestData( - [ - { - Alpha: 1, - Beta: 2, - Gamma: 3, - }, - ], - { intervalMs: 300000000 }, - ), - ), - ]; - const chartProps = createTestChartProps({ - width: 190 + TIMESERIES_CONSTANTS.legendTopRightOffset, - formData: { - ...formData, - legendType: LegendType.Plain, - legendOrientation: LegendOrientation.Top, - showLegend: true, - zoomable: true, - }, - queriesData: narrowLegendData, - }); - - const transformed = transformProps(chartProps); - - expect((transformed.echartOptions.legend as any).type).toBe( - LegendType.Scroll, - ); - }); -}); - -const timeCompareFormData: SqlaFormData = { - colorScheme: 'bnbColors', - datasource: '3__table', - granularity_sqla: 'ds', - metric: 'sum__num', - viz_type: 'my_viz', -}; - -test('should apply dashed line style to time comparison series with single metric', () => { - const queriesDataWithTimeCompare = [ - createTestQueryData([ - { sum__num: 100, '1 week ago': 80, __timestamp: 599616000000 }, - { sum__num: 150, '1 week ago': 120, __timestamp: 599916000000 }, - ]), - ]; - - const chartProps = createTestChartProps({ - formData: { - ...timeCompareFormData, - time_compare: ['1 week ago'], - timeShiftColor: true, - comparison_type: ComparisonType.Values, - }, - queriesData: queriesDataWithTimeCompare, - }); - - const transformed = transformProps(chartProps); - const series = (transformed.echartOptions.series as SeriesOption[]) || []; - - const mainSeries = series.find(s => s.name === 'sum__num') as - | (SeriesOption & { lineStyle?: { type?: number[] | string } }) - | undefined; - const comparisonSeries = series.find(s => s.name === '1 week ago') as - | (SeriesOption & { lineStyle?: { type?: number[] | string } }) - | undefined; - - expect(mainSeries).toBeDefined(); - expect(comparisonSeries).toBeDefined(); - // Main series should not have a dash pattern array - expect(Array.isArray(mainSeries?.lineStyle?.type)).toBe(false); - expect(mainSeries?.lineStyle?.type).not.toBe('dotted'); - // Comparison series should have a visible dash pattern - expect(comparisonSeries?.lineStyle?.type).toBe('dotted'); -}); - -test('should apply dashed line style to time comparison series with metric__offset pattern', () => { - const queriesDataWithTimeCompare = [ - createTestQueryData([ - { - sum__num: 100, - 'sum__num__1 week ago': 80, - __timestamp: 599616000000, - }, - { - sum__num: 150, - 'sum__num__1 week ago': 120, - __timestamp: 599916000000, - }, - ]), - ]; - - const chartProps = createTestChartProps({ - formData: { - ...timeCompareFormData, - time_compare: ['1 week ago'], - timeShiftColor: true, - comparison_type: ComparisonType.Values, - }, - queriesData: queriesDataWithTimeCompare, - }); - - const transformed = transformProps(chartProps); - const series = (transformed.echartOptions.series as SeriesOption[]) || []; - - const mainSeries = series.find(s => s.name === 'sum__num') as - | (SeriesOption & { lineStyle?: { type?: number[] | string } }) - | undefined; - const comparisonSeries = series.find( - s => s.name === 'sum__num__1 week ago', - ) as - | (SeriesOption & { lineStyle?: { type?: number[] | string } }) - | undefined; - - expect(mainSeries).toBeDefined(); - expect(comparisonSeries).toBeDefined(); - // Main series should not have a dash pattern array - expect(Array.isArray(mainSeries?.lineStyle?.type)).toBe(false); - // Comparison series should have a visible dash pattern - expect(comparisonSeries?.lineStyle?.type).toBe('dotted'); -}); - -test('should apply connectNulls to time comparison series', () => { - const queriesDataWithNulls = [ - createTestQueryData([ - { sum__num: 100, '1 week ago': null, __timestamp: 599616000000 }, - { sum__num: 150, '1 week ago': 120, __timestamp: 599916000000 }, - { sum__num: 200, '1 week ago': null, __timestamp: 600216000000 }, - ]), - ]; - - const chartProps = createTestChartProps({ - formData: { - ...timeCompareFormData, - time_compare: ['1 week ago'], - comparison_type: ComparisonType.Values, - }, - queriesData: queriesDataWithNulls, - }); - - const transformed = transformProps(chartProps); - const series = (transformed.echartOptions.series as SeriesOption[]) || []; - - const comparisonSeries = series.find(s => s.name === '1 week ago') as - | (SeriesOption & { connectNulls?: boolean }) - | undefined; - - expect(comparisonSeries).toBeDefined(); - expect(comparisonSeries?.connectNulls).toBe(true); -}); - -test('should not apply dashed line style for non-Values comparison types', () => { - const queriesDataWithTimeCompare = [ - createTestQueryData([ - { sum__num: 100, '1 week ago': 80, __timestamp: 599616000000 }, - { sum__num: 150, '1 week ago': 120, __timestamp: 599916000000 }, - ]), - ]; - - const chartProps = createTestChartProps({ - formData: { - ...timeCompareFormData, - time_compare: ['1 week ago'], - comparison_type: ComparisonType.Difference, - }, - queriesData: queriesDataWithTimeCompare, - }); - - const transformed = transformProps(chartProps); - const series = (transformed.echartOptions.series as SeriesOption[]) || []; - - const comparisonSeries = series.find(s => s.name === '1 week ago') as - | (SeriesOption & { - lineStyle?: { type?: number[] | string }; - connectNulls?: boolean; - }) - | undefined; - - expect(comparisonSeries).toBeDefined(); - // Non-Values comparison types don't get dashed styling (isDerivedSeries returns false) - expect(Array.isArray(comparisonSeries?.lineStyle?.type)).toBe(false); - expect(comparisonSeries?.connectNulls).toBeFalsy(); -}); - -test('EchartsTimeseries AUTO mode should detect single currency and format with $ for USD', () => { - const chartProps = createTestChartProps({ - formData: { - ...formData, - metrics: ['sum__num'], - currencyFormat: { symbol: 'AUTO', symbolPosition: 'prefix' }, - }, - datasource: { - currencyCodeColumn: 'currency_code', - columnFormats: {}, - currencyFormats: {}, - verboseMap: {}, - }, - queriesData: [ - createTestQueryData( - [ - { - 'San Francisco': 1000, - __timestamp: 599616000000, - currency_code: 'USD', - }, - { - 'San Francisco': 2000, - __timestamp: 599916000000, - currency_code: 'USD', - }, - ], - { detected_currency: 'USD' }, - ), - ], - }); - - const transformed = transformProps(chartProps); - - const formatter = getYAxisFormatter(transformed); - expect(formatter(1000, 0)).toContain('$'); -}); - -test('EchartsTimeseries AUTO mode should use neutral formatting for mixed currencies', () => { - const chartProps = createTestChartProps({ - formData: { - ...formData, - metrics: ['sum__num'], - currencyFormat: { symbol: 'AUTO', symbolPosition: 'prefix' }, - }, - datasource: { - currencyCodeColumn: 'currency_code', - columnFormats: {}, - currencyFormats: {}, - verboseMap: {}, - }, - queriesData: [ - createTestQueryData([ - { - 'San Francisco': 1000, - __timestamp: 599616000000, - currency_code: 'USD', - }, - { - 'San Francisco': 2000, - __timestamp: 599916000000, - currency_code: 'EUR', - }, - ]), - ], - }); - - const transformed = transformProps(chartProps); - - // With mixed currencies, Y-axis should use neutral formatting - const formatter = getYAxisFormatter(transformed); - const formatted = formatter(1000, 0); - expect(formatted).not.toContain('$'); - expect(formatted).not.toContain('€'); -}); - -test('EchartsTimeseries should preserve static currency format with £ for GBP', () => { - const chartProps = createTestChartProps({ - formData: { - ...formData, - metrics: ['sum__num'], - currencyFormat: { symbol: 'GBP', symbolPosition: 'prefix' }, - }, - datasource: { - currencyCodeColumn: 'currency_code', - columnFormats: {}, - currencyFormats: {}, - verboseMap: {}, - }, - queriesData: [ - createTestQueryData([ - { - 'San Francisco': 1000, - __timestamp: 599616000000, - currency_code: 'USD', - }, - { - 'San Francisco': 2000, - __timestamp: 599916000000, - currency_code: 'EUR', - }, - ]), - ], - }); - - const transformed = transformProps(chartProps); - - // Static mode should always show £ - const formatter = getYAxisFormatter(transformed); - expect(formatter(1000, 0)).toContain('£'); -}); - -const baseFormDataHorizontalBar: SqlaFormData = { - colorScheme: 'bnbColors', - datasource: '3__table', - granularity_sqla: '__timestamp', - metric: 'sum__num', - groupby: [], - viz_type: 'echarts_timeseries', - seriesType: EchartsTimeseriesSeriesType.Bar, - orientation: OrientationType.Horizontal, - truncateYAxis: true, - yAxisBounds: [null, null], -}; - -test('should set yAxis max to actual data max for horizontal bar charts', () => { - const queriesData: ChartDataResponseResult[] = [ - createTestQueryData( - createTestData( - [{ 'Series A': 15000 }, { 'Series A': 20000 }, { 'Series A': 18000 }], - { intervalMs: 300000000 }, - ), - ), - ]; - - const chartProps = createTestChartProps({ - formData: baseFormDataHorizontalBar, - queriesData, - }); - - const transformedProps = transformProps(chartProps); - - // In horizontal orientation, axes are swapped, so yAxis becomes xAxis - const xAxisRaw = transformedProps.echartOptions.xAxis as any; - expect(xAxisRaw.max).toBe(20000); // Should be the actual max value, not rounded -}); - -test('should set yAxis min and max for diverging horizontal bar charts', () => { - const queriesData: ChartDataResponseResult[] = [ - createTestQueryData( - createTestData( - [{ 'Series A': -21000 }, { 'Series A': 20000 }, { 'Series A': 18000 }], - { intervalMs: 300000000 }, - ), - ), - ]; - - const chartProps = createTestChartProps({ - formData: baseFormDataHorizontalBar, - queriesData, - }); - - const transformedProps = transformProps(chartProps); - - // In horizontal orientation, axes are swapped, so yAxis becomes xAxis - const xAxisRaw = transformedProps.echartOptions.xAxis as any; - expect(xAxisRaw.max).toBe(20000); // Should be the actual max value - expect(xAxisRaw.min).toBe(-21000); // Should be the actual min value for diverging bars -}); - -test('should not override explicit yAxisBounds for horizontal bar charts', () => { - const queriesData: ChartDataResponseResult[] = [ - createTestQueryData( - createTestData( - [{ 'Series A': 15000 }, { 'Series A': 20000 }, { 'Series A': 18000 }], - { intervalMs: 300000000 }, - ), - ), - ]; - - const chartProps = createTestChartProps({ - formData: { - ...baseFormDataHorizontalBar, - yAxisBounds: [0, 25000], // Explicit bounds - }, - queriesData, - }); - - const transformedProps = transformProps(chartProps); - - // In horizontal orientation, axes are swapped, so yAxis becomes xAxis - const xAxisRaw = transformedProps.echartOptions.xAxis as any; - expect(xAxisRaw.max).toBe(25000); // Should respect explicit bound - expect(xAxisRaw.min).toBe(0); // Should respect explicit bound -}); - -test('should not apply axis bounds calculation when truncateYAxis is false for horizontal bar charts', () => { - const queriesData: ChartDataResponseResult[] = [ - createTestQueryData( - createTestData( - [{ 'Series A': 15000 }, { 'Series A': 20000 }, { 'Series A': 18000 }], - { intervalMs: 300000000 }, - ), - ), - ]; - - const chartProps = createTestChartProps({ - formData: { - ...baseFormDataHorizontalBar, - truncateYAxis: false, - }, - queriesData, - }); - - const transformedProps = transformProps(chartProps); - - // In horizontal orientation, axes are swapped, so yAxis becomes xAxis - const xAxis = transformedProps.echartOptions.xAxis as any; - // Should not have explicit max set when truncateYAxis is false - expect(xAxis.max).toBeUndefined(); -}); - -test('should not apply axis bounds calculation when seriesType is not Bar for horizontal charts', () => { - const queriesData: ChartDataResponseResult[] = [ - createTestQueryData( - createTestData( - [{ 'Series A': 15000 }, { 'Series A': 20000 }, { 'Series A': 18000 }], - { intervalMs: 300000000 }, - ), - ), - ]; - - const chartProps = createTestChartProps({ - formData: { - ...baseFormDataHorizontalBar, - seriesType: EchartsTimeseriesSeriesType.Line, - }, - queriesData, - }); - - const transformedProps = transformProps(chartProps); - - // In horizontal orientation, axes are swapped, so yAxis becomes xAxis - const xAxisRaw = transformedProps.echartOptions.xAxis as any; - // Should not have explicit max set when seriesType is not Bar - expect(xAxisRaw.max).toBeUndefined(); -}); - -test('legend is visible on tall charts when enabled by the user', () => { - const chartProps = createTestChartProps({ - height: 400, - formData: { showLegend: true }, - }); - const { legend } = transformProps(chartProps).echartOptions as any; - - expect(legend.show).toBe(true); -}); - -test('legend is hidden on small charts even when enabled by the user', () => { - const chartProps = createTestChartProps({ - height: 80, - formData: { showLegend: true }, - }); - const { legend } = transformProps(chartProps).echartOptions as any; - - expect(legend.show).toBe(false); -}); - -test('y-axis labels remain visible on small charts for scale reference', () => { - const chartProps = createTestChartProps({ height: 80 }); - const { yAxis } = transformProps(chartProps).echartOptions as any; - - expect(yAxis.axisLabel.show).toBe(true); -}); - -test('y-axis labels are hidden on micro charts for a sparkline view', () => { - const chartProps = createTestChartProps({ height: 40 }); - const { yAxis } = transformProps(chartProps).echartOptions as any; - - expect(yAxis.axisLabel.show).toBe(false); -}); - -test('y-axis tick count scales with chart height', () => { - const short = transformProps(createTestChartProps({ height: 200 })); - const tall = transformProps(createTestChartProps({ height: 500 })); - const shortYAxis = short.echartOptions.yAxis as any; - const tallYAxis = tall.echartOptions.yAxis as any; - - expect(tallYAxis.splitNumber).toBeGreaterThan(shortYAxis.splitNumber); -}); - -test('small chart y-axis uses splitNumber=1 to show only boundary labels', () => { - const chartProps = createTestChartProps({ height: 80 }); - const { yAxis } = transformProps(chartProps).echartOptions as any; - - expect(yAxis.splitNumber).toBe(1); -}); - -test('zoomable small chart preserves bottom padding for the dataZoom slider', () => { - const chartProps = createTestChartProps({ - height: 80, - formData: { zoomable: true }, - }); - const result = transformProps(chartProps); - const grid = result.echartOptions.grid as any; - - expect(grid.bottom).toBeGreaterThan(5); -}); - -test('boundary: height at exactly 100px uses full axis behavior', () => { - const chartProps = createTestChartProps({ height: 100 }); - const { yAxis } = transformProps(chartProps).echartOptions as any; - - expect(yAxis.axisLabel.show).toBe(true); - expect(yAxis.splitNumber).toBeGreaterThanOrEqual(3); -}); - -test('boundary: height at 99px triggers small chart behavior', () => { - const chartProps = createTestChartProps({ - height: 99, - formData: { showLegend: true }, - }); - const { yAxis, legend } = transformProps(chartProps).echartOptions as any; - - expect(yAxis.splitNumber).toBe(1); - expect(legend.show).toBe(false); -}); - -test('boundary: height at exactly 60px shows labels but uses compact axis', () => { - const chartProps = createTestChartProps({ height: 60 }); - const { yAxis } = transformProps(chartProps).echartOptions as any; - - expect(yAxis.axisLabel.show).toBe(true); - expect(yAxis.splitNumber).toBe(1); -}); - -test('boundary: height at 59px triggers micro chart behavior', () => { - const chartProps = createTestChartProps({ height: 59 }); - const { yAxis } = transformProps(chartProps).echartOptions as any; - - expect(yAxis.axisLabel.show).toBe(false); -}); - -test('x-axis formatter deduplicates consecutive identical labels for coarse time grains', () => { - const yearData = [ - { __timestamp: Date.UTC(2003, 0, 1), sales: 100 }, - { __timestamp: Date.UTC(2004, 0, 1), sales: 200 }, - { __timestamp: Date.UTC(2005, 0, 1), sales: 300 }, - ]; - - const chartProps = createTestChartProps({ - formData: { - granularity_sqla: 'ds', - time_grain_sqla: TimeGranularity.YEAR, - xAxisTimeFormat: '%Y', - }, - queriesData: [ - createTestQueryData(yearData, { - colnames: ['__timestamp', 'sales'], - coltypes: [GenericDataType.Temporal, GenericDataType.Numeric], - }), - ], - }); - - const transformedProps = transformProps(chartProps); - const xAxisResult = transformedProps.echartOptions.xAxis as any; - const { formatter } = xAxisResult.axisLabel; - - expect(typeof formatter).toBe('function'); - expect(xAxisResult.axisLabel.showMaxLabel).toBe(true); - - const label1 = formatter(Date.UTC(2003, 0, 1)); - const label2 = formatter(Date.UTC(2004, 0, 1)); - const label3 = formatter(Date.UTC(2005, 0, 1)); - const label4 = formatter(Date.UTC(2005, 6, 1)); - - expect(label1).toBe('2003'); - expect(label2).toBe('2004'); - expect(label3).toBe('2005'); - expect(label4).toBe(''); -}); - -test('numeric x coltype routes through the number formatter (not the time formatter)', () => { - // Regression guard for echarts-timeseries-epoch-x-axis-labels investigation. - // When the query reports a Numeric x-axis coltype (including epoch-ms-like - // values), Timeseries transformProps must pick the Value axis and run the - // label through getNumberFormatter, not the time formatter. If this ever - // changes, epoch-ms values that arrive as Numeric would suddenly be treated - // as Date instances and could render "NaN" — the symptom that prompted this - // investigation. - const ts1 = 1745784000000; - const ts2 = 1745870400000; - const chartProps = createTestChartProps({ - formData: { - metrics: ['metric'], - granularity_sqla: 'ds', - x_axis: '__timestamp', - }, - queriesData: [ - createTestQueryData( - [ - { __timestamp: ts1, metric: 10 }, - { __timestamp: ts2, metric: 20 }, - ], - { - colnames: ['__timestamp', 'metric'], - coltypes: [GenericDataType.Numeric, GenericDataType.Numeric], - }, - ), - ], - }); - - const { echartOptions } = transformProps(chartProps); - const xAxis = echartOptions.xAxis as { - type: string; - axisLabel: { formatter: (v: number) => string }; - }; - - expect(xAxis.type).toBe(AxisType.Value); - const label = xAxis.axisLabel.formatter(ts1); - expect(typeof label).toBe('string'); - expect(label).not.toMatch(/NaN/); -}); - -test('xAxisForceCategorical forces Category axis regardless of Numeric coltype', () => { - const ts1 = 1745784000000; - const ts2 = 1745870400000; - const chartProps = createTestChartProps({ - formData: { - metrics: ['metric'], - granularity_sqla: 'ds', - x_axis: '__timestamp', - xAxisForceCategorical: true, - }, - queriesData: [ - createTestQueryData( - [ - { __timestamp: ts1, metric: 10 }, - { __timestamp: ts2, metric: 20 }, - ], - { - colnames: ['__timestamp', 'metric'], - coltypes: [GenericDataType.Numeric, GenericDataType.Numeric], - }, - ), - ], - }); - - const { echartOptions } = transformProps(chartProps); - const xAxis = echartOptions.xAxis as { type: string }; - - expect(xAxis.type).toBe(AxisType.Category); -}); - -test('temporal x coltype wires the time formatter and Time axis', () => { - // Regression guard: the happy path for time-series charts. Ensures that - // Temporal coltype keeps routing through the TimeFormatter so a refactor - // does not accidentally drop Date handling (the feared regression that - // sparked this investigation). - const ts1 = 1745784000000; - const ts2 = 1745870400000; - const chartProps = createTestChartProps({ - formData: { - metrics: ['metric'], - granularity_sqla: 'ds', - x_axis: '__timestamp', - }, - queriesData: [ - createTestQueryData( - [ - { __timestamp: ts1, metric: 10 }, - { __timestamp: ts2, metric: 20 }, - ], - { - colnames: ['__timestamp', 'metric'], - coltypes: [GenericDataType.Temporal, GenericDataType.Numeric], - }, - ), - ], - }); - - const { echartOptions } = transformProps(chartProps); - const xAxis = echartOptions.xAxis as { - type: string; - axisLabel: { formatter: (v: Date) => string }; - }; - - expect(xAxis.type).toBe(AxisType.Time); - const label = xAxis.axisLabel.formatter(new Date(ts1)); - expect(typeof label).toBe('string'); - expect(label).not.toMatch(/NaN/); - expect(label).not.toBe(String(ts1)); -}); - -test('should assign distinct dash patterns for multiple time offsets consistently', () => { - const queriesDataWithMultipleOffsets = [ - createTestQueryData([ - { - sum__num: 100, - '1 year ago': 80, - '2 years ago': 60, - __timestamp: 599616000000, - }, - { - sum__num: 150, - '1 year ago': 120, - '2 years ago': 90, - __timestamp: 599916000000, - }, - ]), - ]; - - const chartProps = createTestChartProps({ - formData: { - ...timeCompareFormData, - time_compare: ['1 year ago', '2 years ago'], - comparison_type: ComparisonType.Values, - timeShiftColor: true, - }, - queriesData: queriesDataWithMultipleOffsets, - }); - - const transformed = transformProps(chartProps); - const series = (transformed.echartOptions.series as SeriesOption[]) || []; - - const series1 = series.find(s => s.name === '1 year ago') as any; - const series2 = series.find(s => s.name === '2 years ago') as any; - - expect(series1).toBeDefined(); - expect(series2).toBeDefined(); - - const pattern1 = series1.lineStyle?.type; - const symbol1 = series1.symbol; - const pattern2 = series2.lineStyle?.type; - const symbol2 = series2.symbol; - - // must be different patterns - expect(pattern1).not.toEqual(pattern2); - - // must be different patterns - expect(symbol1).not.toEqual(symbol2); -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformers.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformers.test.ts deleted file mode 100644 index 54da9c14ea0..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformers.test.ts +++ /dev/null @@ -1,529 +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 { CategoricalColorScale, ChartProps } from '@superset-ui/core'; -import { GenericDataType } from '@apache-superset/core/common'; -import { supersetTheme } from '@apache-superset/core/theme'; -import type { SeriesOption } from 'echarts'; -import { EchartsTimeseriesSeriesType } from '../../src'; -import { TIMESERIES_CONSTANTS } from '../../src/constants'; -import { LegendOrientation } from '../../src/types'; -import { - transformSeries, - transformNegativeLabelsPosition, - getPadding, -} from '../../src/Timeseries/transformers'; -import transformProps from '../../src/Timeseries/transformProps'; -import { EchartsTimeseriesChartProps } from '../../src/types'; -import * as seriesUtils from '../../src/utils/series'; - -// Mock the colorScale function -const mockColorScale = jest.fn( - (key: string, sliceId?: number) => `color-for-${key}-${sliceId}`, -) as unknown as CategoricalColorScale; - -describe('transformSeries', () => { - const series = { name: 'test-series' }; - - test('should use the colorScaleKey if timeShiftColor is enabled', () => { - const opts = { - timeShiftColor: true, - colorScaleKey: 'test-key', - sliceId: 1, - }; - - const result = transformSeries(series, mockColorScale, 'test-key', opts); - - expect((result as any)?.itemStyle.color).toBe('color-for-test-key-1'); - }); - - test('should use seriesKey if timeShiftColor is not enabled', () => { - const opts = { - timeShiftColor: false, - seriesKey: 'series-key', - sliceId: 2, - }; - - const result = transformSeries(series, mockColorScale, 'test-key', opts); - - expect((result as any)?.itemStyle.color).toBe('color-for-series-key-2'); - }); - - test('should apply border styles for bar series with connectNulls', () => { - const opts = { - seriesType: EchartsTimeseriesSeriesType.Bar, - connectNulls: true, - timeShiftColor: false, - }; - - const result = transformSeries(series, mockColorScale, 'test-key', opts); - - expect((result as any).itemStyle.borderWidth).toBe(1.5); - expect((result as any).itemStyle.borderType).toBe('dotted'); - expect((result as any).itemStyle.borderColor).toBe( - (result as any).itemStyle.color, - ); - }); - - test('should not apply border styles for non-bar series', () => { - const opts = { - seriesType: EchartsTimeseriesSeriesType.Line, - connectNulls: true, - timeShiftColor: false, - }; - - const result = transformSeries(series, mockColorScale, 'test-key', opts); - - expect((result as any).itemStyle.borderWidth).toBe(0); - expect((result as any).itemStyle.borderType).toBeUndefined(); - expect((result as any).itemStyle.borderColor).toBeUndefined(); - }); - - test('should dim series when selectedValues does not include series name (dimension-based filtering)', () => { - const opts = { - filterState: { selectedValues: ['other-series'] }, - hasDimensions: true, - seriesType: EchartsTimeseriesSeriesType.Bar, - timeShiftColor: false, - }; - - const result = transformSeries(series, mockColorScale, 'test-key', opts); - - // OpacityEnum.SemiTransparent = 0.3 - expect((result as any).itemStyle.opacity).toBe(0.3); - }); - - test('should not dim series when hasDimensions is false (X-axis cross-filtering)', () => { - const opts = { - filterState: { selectedValues: ['Product A'] }, - hasDimensions: false, - seriesType: EchartsTimeseriesSeriesType.Bar, - timeShiftColor: false, - }; - - const result = transformSeries(series, mockColorScale, 'test-key', opts); - - // OpacityEnum.NonTransparent = 1 (not dimmed) - expect((result as any).itemStyle.opacity).toBe(1); - }); -}); - -describe('transformNegativeLabelsPosition', () => { - test('label position bottom of negative value no Horizontal', () => { - const isHorizontal = false; - const series: SeriesOption = { - data: [ - [2020, 1], - [2021, 3], - [2022, -2], - [2023, -5], - [2024, 4], - ], - type: EchartsTimeseriesSeriesType.Bar, - stack: undefined, - }; - const result = - Array.isArray(series.data) && series.type === 'bar' && !series.stack - ? transformNegativeLabelsPosition(series, isHorizontal) - : series.data; - expect((result as any)[0].label).toBe(undefined); - expect((result as any)[1].label).toBe(undefined); - expect((result as any)[2].label.position).toBe('outside'); - expect((result as any)[3].label.position).toBe('outside'); - expect((result as any)[4].label).toBe(undefined); - }); - - test('label position left of negative value is Horizontal', () => { - const isHorizontal = true; - const series: SeriesOption = { - data: [ - [1, 2020], - [-3, 2021], - [2, 2022], - [-4, 2023], - [-6, 2024], - ], - type: EchartsTimeseriesSeriesType.Bar, - stack: undefined, - }; - - const result = - Array.isArray(series.data) && series.type === 'bar' && !series.stack - ? transformNegativeLabelsPosition(series, isHorizontal) - : series.data; - expect((result as any)[0].label).toBe(undefined); - expect((result as any)[1].label.position).toBe('outside'); - expect((result as any)[2].label).toBe(undefined); - expect((result as any)[3].label.position).toBe('outside'); - expect((result as any)[4].label.position).toBe('outside'); - }); - - test('label position to line type', () => { - const isHorizontal = false; - const series: SeriesOption = { - data: [ - [2020, 1], - [2021, 3], - [2022, -2], - [2023, -5], - [2024, 4], - ], - type: EchartsTimeseriesSeriesType.Line, - stack: undefined, - }; - - const result = - Array.isArray(series.data) && - !series.stack && - series.type !== 'line' && - series.type === 'bar' - ? transformNegativeLabelsPosition(series, isHorizontal) - : series.data; - expect((result as any)[0].label).toBe(undefined); - expect((result as any)[1].label).toBe(undefined); - expect((result as any)[2].label).toBe(undefined); - expect((result as any)[3].label).toBe(undefined); - expect((result as any)[4].label).toBe(undefined); - }); - - test('label position to bar type and stack', () => { - const isHorizontal = false; - const series: SeriesOption = { - data: [ - [2020, 1], - [2021, 3], - [2022, -2], - [2023, -5], - [2024, 4], - ], - type: EchartsTimeseriesSeriesType.Bar, - stack: 'obs', - }; - - const result = - Array.isArray(series.data) && series.type === 'bar' && !series.stack - ? transformNegativeLabelsPosition(series, isHorizontal) - : series.data; - expect((result as any)[0].label).toBe(undefined); - expect((result as any)[1].label).toBe(undefined); - expect((result as any)[2].label).toBe(undefined); - expect((result as any)[3].label).toBe(undefined); - expect((result as any)[4].label).toBe(undefined); - }); -}); - -function buildTimeseriesChartProps( - overrides: Record = {}, -): EchartsTimeseriesChartProps { - return new ChartProps({ - formData: { - colorScheme: 'bnbColors', - datasource: '3__table', - granularity_sqla: 'ds', - metric: 'sum__num', - viz_type: 'my_viz', - ...overrides, - }, - width: 800, - height: 600, - queriesData: [ - { - data: [ - { sum__num: 100, __timestamp: new Date('2026-01-01').getTime() }, - { sum__num: 200, __timestamp: new Date('2026-04-01').getTime() }, - { sum__num: 300, __timestamp: new Date('2026-07-01').getTime() }, - { sum__num: 400, __timestamp: new Date('2026-10-01').getTime() }, - { sum__num: 500, __timestamp: new Date('2026-12-01').getTime() }, - ], - colnames: ['sum__num', '__timestamp'], - coltypes: [GenericDataType.Numeric, GenericDataType.Temporal], - }, - ], - theme: supersetTheme, - }) as unknown as EchartsTimeseriesChartProps; -} - -test('should configure time axis labels to show max label for last month visibility', () => { - const formData = { - colorScheme: 'bnbColors', - datasource: '3__table', - granularity_sqla: 'ds', - metric: 'sum__num', - viz_type: 'my_viz', - }; - const queriesData = [ - { - data: [ - { sum__num: 100, __timestamp: new Date('2026-01-01').getTime() }, - { sum__num: 200, __timestamp: new Date('2026-02-01').getTime() }, - { sum__num: 300, __timestamp: new Date('2026-03-01').getTime() }, - { sum__num: 400, __timestamp: new Date('2026-04-01').getTime() }, - { sum__num: 500, __timestamp: new Date('2026-05-01').getTime() }, - ], - colnames: ['sum__num', '__timestamp'], - coltypes: [GenericDataType.Numeric, GenericDataType.Temporal], - }, - ]; - const chartProps = new ChartProps({ - formData, - width: 800, - height: 600, - queriesData, - theme: supersetTheme, - }); - - const result = transformProps( - chartProps as unknown as EchartsTimeseriesChartProps, - ); - - expect(result.echartOptions.xAxis).toEqual( - expect.objectContaining({ - axisLabel: expect.objectContaining({ - showMaxLabel: true, - alignMaxLabel: 'right', - }), - }), - ); -}); - -test('x-axis dates do not overlap and last label stays visible at 0° rotation', () => { - const result = transformProps(buildTimeseriesChartProps()); - const { axisLabel } = result.echartOptions.xAxis as Record; - - expect(axisLabel.hideOverlap).toBe(true); - // showMaxLabel forces the last data point label to render even - // when hideOverlap is active, preventing the #37181 regression. - expect(axisLabel.showMaxLabel).toBe(true); - expect(axisLabel.alignMaxLabel).toBe('right'); -}); - -test('last x-axis date is visible and not cut off when rotated -45°', () => { - const lastDataPointTimestamp = new Date('2026-12-01').getTime(); - const result = transformProps( - buildTimeseriesChartProps({ - xAxisLabelRotation: -45, - x_axis_time_format: '%d-%m-%Y %H:%M:%S', - }), - ); - const { xAxis, grid } = result.echartOptions as Record; - const { axisLabel } = xAxis; - - // The formatter renders the last data point's date as a full string - const lastDateLabel = axisLabel.formatter(lastDataPointTimestamp); - expect(lastDateLabel).toMatch(/01-12-2026/); - expect(lastDateLabel).not.toBe(''); - - // Labels are not aggressively hidden so the last date stays visible - expect(axisLabel.hideOverlap).toBe(false); - expect(axisLabel.rotate).toBe(-45); - // No phantom label at a position that doesn't correspond to any bar - expect(axisLabel.showMaxLabel).toBeUndefined(); - // Enough right padding so the last rotated label is not clipped - expect(grid.right).toBeGreaterThan(TIMESERIES_CONSTANTS.gridOffsetRight); -}); - -test('last x-axis date is visible and not cut off when rotated 45°', () => { - const lastDataPointTimestamp = new Date('2026-12-01').getTime(); - const result = transformProps( - buildTimeseriesChartProps({ - xAxisLabelRotation: 45, - x_axis_time_format: '%d-%m-%Y %H:%M:%S', - }), - ); - const { xAxis, grid } = result.echartOptions as Record; - - const lastDateLabel = xAxis.axisLabel.formatter(lastDataPointTimestamp); - expect(lastDateLabel).toMatch(/01-12-2026/); - expect(lastDateLabel).not.toBe(''); - - expect(xAxis.axisLabel.hideOverlap).toBe(false); - expect(xAxis.axisLabel.rotate).toBe(45); - expect(grid.right).toBeGreaterThan(TIMESERIES_CONSTANTS.gridOffsetRight); -}); - -test('no phantom date label appears at the axis boundary', () => { - const result = transformProps( - buildTimeseriesChartProps({ xAxisLabelRotation: -45 }), - ); - const { axisLabel } = result.echartOptions.xAxis as Record; - - expect(axisLabel.showMaxLabel).toBeUndefined(); - expect(axisLabel.showMinLabel).toBeUndefined(); -}); - -function setupGetChartPaddingMock(): jest.SpyInstance { - // Mock getChartPadding to return the padding object as-is for easier testing - const getChartPaddingSpy = jest.spyOn(seriesUtils, 'getChartPadding'); - getChartPaddingSpy.mockImplementation( - ( - show: boolean, - orientation: LegendOrientation, - margin: string | number | null | undefined, - padding: - | { - bottom?: number; - left?: number; - right?: number; - top?: number; - } - | undefined, - ) => ({ - bottom: padding?.bottom ?? 0, - left: padding?.left ?? 0, - right: padding?.right ?? 0, - top: padding?.top ?? 0, - }), - ); - return getChartPaddingSpy; -} - -test('getPadding should only affect left margin when Y axis title position is Left', () => { - const getChartPaddingSpy = setupGetChartPaddingMock(); - try { - const result = getPadding( - false, // showLegend - LegendOrientation.Top, // legendOrientation - true, // addYAxisTitleOffset - false, // zoomable - null, // margin - false, // addXAxisTitleOffset - 'Left', // yAxisTitlePosition - 30, // yAxisTitleMargin - 0, // xAxisTitleMargin - false, // isHorizontal - ); - - // Top should be base value, not affected by Left position - expect(result.top).toBe(TIMESERIES_CONSTANTS.gridOffsetTop); - // Left should include the margin - expect(result.left).toBe(TIMESERIES_CONSTANTS.gridOffsetLeft + 30); - // Bottom should be base value - expect(result.bottom).toBe(TIMESERIES_CONSTANTS.gridOffsetBottom); - // Right should be base value - expect(result.right).toBe(TIMESERIES_CONSTANTS.gridOffsetRight); - } finally { - getChartPaddingSpy.mockRestore(); - } -}); - -test('getPadding should only affect top margin when Y axis title position is Top', () => { - const getChartPaddingSpy = setupGetChartPaddingMock(); - try { - const result = getPadding( - false, // showLegend - LegendOrientation.Top, // legendOrientation - true, // addYAxisTitleOffset - false, // zoomable - null, // margin - false, // addXAxisTitleOffset - 'Top', // yAxisTitlePosition - 30, // yAxisTitleMargin - 0, // xAxisTitleMargin - false, // isHorizontal - ); - - // Top should include the margin - expect(result.top).toBe(TIMESERIES_CONSTANTS.gridOffsetTop + 30); - // Left should be base value, not affected by Top position - expect(result.left).toBe(TIMESERIES_CONSTANTS.gridOffsetLeft); - // Bottom should be base value - expect(result.bottom).toBe(TIMESERIES_CONSTANTS.gridOffsetBottom); - // Right should be base value - expect(result.right).toBe(TIMESERIES_CONSTANTS.gridOffsetRight); - } finally { - getChartPaddingSpy.mockRestore(); - } -}); - -test('getPadding should use yAxisOffset for top when position is not specified and addYAxisTitleOffset is true', () => { - const getChartPaddingSpy = setupGetChartPaddingMock(); - try { - const result = getPadding( - false, // showLegend - LegendOrientation.Top, // legendOrientation - true, // addYAxisTitleOffset - false, // zoomable - null, // margin - false, // addXAxisTitleOffset - undefined, // yAxisTitlePosition (not specified) - 0, // yAxisTitleMargin - 0, // xAxisTitleMargin - false, // isHorizontal - ); - - // Top should include yAxisOffset - expect(result.top).toBe( - TIMESERIES_CONSTANTS.gridOffsetTop + - TIMESERIES_CONSTANTS.yAxisLabelTopOffset, - ); - // Left should be base value - expect(result.left).toBe(TIMESERIES_CONSTANTS.gridOffsetLeft); - } finally { - getChartPaddingSpy.mockRestore(); - } -}); - -test('getPadding should not add yAxisOffset when addYAxisTitleOffset is false', () => { - const getChartPaddingSpy = setupGetChartPaddingMock(); - try { - const result = getPadding( - false, // showLegend - LegendOrientation.Top, // legendOrientation - false, // addYAxisTitleOffset - false, // zoomable - null, // margin - false, // addXAxisTitleOffset - undefined, // yAxisTitlePosition - 0, // yAxisTitleMargin - 0, // xAxisTitleMargin - false, // isHorizontal - ); - - // Top should be base value only - expect(result.top).toBe(TIMESERIES_CONSTANTS.gridOffsetTop); - // Left should be base value - expect(result.left).toBe(TIMESERIES_CONSTANTS.gridOffsetLeft); - } finally { - getChartPaddingSpy.mockRestore(); - } -}); - -test('getPadding should handle Left position with zero margin correctly', () => { - const getChartPaddingSpy = setupGetChartPaddingMock(); - try { - const result = getPadding( - false, // showLegend - LegendOrientation.Top, // legendOrientation - true, // addYAxisTitleOffset - false, // zoomable - null, // margin - false, // addXAxisTitleOffset - 'Left', // yAxisTitlePosition - 0, // yAxisTitleMargin (zero) - 0, // xAxisTitleMargin - false, // isHorizontal - ); - - // Top should be base value, not affected - expect(result.top).toBe(TIMESERIES_CONSTANTS.gridOffsetTop); - // Left should be base value only (margin is 0) - expect(result.left).toBe(TIMESERIES_CONSTANTS.gridOffsetLeft); - } finally { - getChartPaddingSpy.mockRestore(); - } -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Tree/buildQuery.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Tree/buildQuery.test.ts deleted file mode 100644 index 9dea35e82dc..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Tree/buildQuery.test.ts +++ /dev/null @@ -1,68 +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 buildQuery from '../../src/Tree/buildQuery'; -import { EchartsTreeFormData } from '../../src/Tree/types'; - -const BASE_FORM_DATA: EchartsTreeFormData = { - datasource: '5__table', - granularity_sqla: 'ds', - viz_type: 'my_chart', - id: '', - parent: '', - name: '', - orient: 'LR', - symbol: 'emptyCircle', - symbolSize: 7, - layout: 'orthogonal', - roam: true, - nodeLabelPosition: 'left', - childLabelPosition: 'bottom', - emphasis: 'descendant', - initialTreeDepth: 2, - metrics: [], -}; - -describe('Tree buildQuery', () => { - test('should build query', () => { - const formData: EchartsTreeFormData = { - ...BASE_FORM_DATA, - id: 'id_col', - parent: 'relation_col', - name: 'name_col', - metrics: ['foo', 'bar'], - }; - const queryContext = buildQuery(formData); - const [query] = queryContext.queries; - expect(query.columns).toEqual(['id_col', 'relation_col', 'name_col']); - expect(query.metrics).toEqual(['foo', 'bar']); - }); - test('should build query without name column', () => { - const formData: EchartsTreeFormData = { - ...BASE_FORM_DATA, - id: 'id_col', - parent: 'relation_col', - name: '', - metrics: ['foo', 'bar'], - }; - const queryContext = buildQuery(formData); - const [query] = queryContext.queries; - expect(query.columns).toEqual(['id_col', 'relation_col']); - expect(query.metrics).toEqual(['foo', 'bar']); - }); -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Tree/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Tree/transformProps.test.ts deleted file mode 100644 index f4a7f15636a..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Tree/transformProps.test.ts +++ /dev/null @@ -1,426 +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 { ChartProps } from '@superset-ui/core'; -import { supersetTheme } from '@apache-superset/core/theme'; -import transformProps from '../../src/Tree/transformProps'; -import { EchartsTreeChartProps } from '../../src/Tree/types'; - -describe('EchartsTree transformProps', () => { - const formData = { - colorScheme: 'bnbColors', - datasource: '3__table', - granularity_sqla: 'ds', - metric: 'count', - id: 'id_column', - parent: 'relation_column', - name: 'name_column', - rootNodeId: '1', - }; - const chartPropsConfig = { - formData, - width: 800, - height: 600, - theme: supersetTheme, - }; - test('should transform when parent present before child', () => { - const queriesData = [ - { - colnames: ['id_column', 'relation_column', 'name_column', 'count'], - data: [ - { - id_column: '1', - relation_column: null, - name_column: 'root', - count: 10, - }, - { - id_column: '2', - relation_column: '1', - name_column: 'first_child', - count: 10, - }, - { - id_column: '3', - relation_column: '2', - name_column: 'second_child', - count: 10, - }, - { - id_column: '4', - relation_column: '3', - name_column: 'third_child', - count: 10, - }, - ], - }, - ]; - - const chartProps = new ChartProps({ ...chartPropsConfig, queriesData }); - expect(transformProps(chartProps as EchartsTreeChartProps)).toEqual( - expect.objectContaining({ - width: 800, - height: 600, - echartOptions: expect.objectContaining({ - series: expect.arrayContaining([ - expect.objectContaining({ - data: [ - { - name: 'root', - children: [ - { - name: 'first_child', - value: 10, - children: [ - { - name: 'second_child', - value: 10, - children: [ - { name: 'third_child', value: 10, children: [] }, - ], - }, - ], - }, - ], - }, - ], - }), - ]), - }), - }), - ); - }); - test('should transform when child is present before parent', () => { - const queriesData = [ - { - colnames: ['id_column', 'relation_column', 'name_column', 'count'], - data: [ - { - id_column: '1', - relation_column: null, - name_column: 'root', - count: 10, - }, - { - id_column: '2', - relation_column: '4', - name_column: 'second_child', - count: 20, - }, - { - id_column: '3', - relation_column: '4', - name_column: 'second_child', - count: 30, - }, - { - id_column: '4', - relation_column: '1', - name_column: 'first_child', - count: 40, - }, - ], - }, - ]; - - const chartProps = new ChartProps({ ...chartPropsConfig, queriesData }); - expect(transformProps(chartProps as EchartsTreeChartProps)).toEqual( - expect.objectContaining({ - width: 800, - height: 600, - echartOptions: expect.objectContaining({ - series: expect.arrayContaining([ - expect.objectContaining({ - data: [ - { - name: 'root', - children: [ - { - name: 'first_child', - value: 40, - children: [ - { - name: 'second_child', - value: 20, - children: [], - }, - { - name: 'second_child', - value: 30, - children: [], - }, - ], - }, - ], - }, - ], - }), - ]), - }), - }), - ); - }); - test('ignore node if not attached to root', () => { - const formData = { - colorScheme: 'bnbColors', - datasource: '3__table', - granularity_sqla: 'ds', - metric: 'count', - id: 'id_column', - parent: 'relation_column', - name: 'name_column', - rootNodeId: '2', - }; - const chartPropsConfig = { - formData, - width: 800, - height: 600, - theme: supersetTheme, - }; - const queriesData = [ - { - colnames: ['id_column', 'relation_column', 'name_column', 'count'], - data: [ - { - id_column: '1', - relation_column: null, - name_column: 'root', - count: 10, - }, - { - id_column: '2', - relation_column: '1', - name_column: 'first_child', - count: 10, - }, - { - id_column: '3', - relation_column: '2', - name_column: 'second_child', - count: 10, - }, - { - id_column: '4', - relation_column: '3', - name_column: 'third_child', - count: 20, - }, - ], - }, - ]; - - const chartProps = new ChartProps({ ...chartPropsConfig, queriesData }); - expect(transformProps(chartProps as EchartsTreeChartProps)).toEqual( - expect.objectContaining({ - width: 800, - height: 600, - echartOptions: expect.objectContaining({ - series: expect.arrayContaining([ - expect.objectContaining({ - data: [ - { - name: 'first_child', - children: [ - { - name: 'second_child', - value: 10, - children: [ - { - name: 'third_child', - value: 20, - children: [], - }, - ], - }, - ], - }, - ], - }), - ]), - }), - }), - ); - }); - test('should transform props if name column is not specified', () => { - const formData = { - colorScheme: 'bnbColors', - datasource: '3__table', - granularity_sqla: 'ds', - metric: 'count', - id: 'id_column', - parent: 'relation_column', - rootNodeId: '1', - }; - const chartPropsConfig = { - formData, - width: 800, - height: 600, - theme: supersetTheme, - }; - const queriesData = [ - { - colnames: ['id_column', 'relation_column', 'count'], - data: [ - { - id_column: '1', - relation_column: null, - count: 10, - }, - { - id_column: '2', - relation_column: '1', - count: 10, - }, - { - id_column: '3', - relation_column: '2', - count: 10, - }, - { - id_column: '4', - relation_column: '3', - count: 20, - }, - ], - }, - ]; - - const chartProps = new ChartProps({ ...chartPropsConfig, queriesData }); - expect(transformProps(chartProps as EchartsTreeChartProps)).toEqual( - expect.objectContaining({ - width: 800, - height: 600, - echartOptions: expect.objectContaining({ - series: expect.arrayContaining([ - expect.objectContaining({ - data: [ - { - name: '1', - children: [ - { - name: '2', - value: 10, - children: [ - { - name: '3', - value: 10, - children: [ - { - name: '4', - value: 20, - children: [], - }, - ], - }, - ], - }, - ], - }, - ], - }), - ]), - }), - }), - ); - }); - test('should find root node with null parent when root node name is not provided', () => { - const formData = { - colorScheme: 'bnbColors', - datasource: '3__table', - granularity_sqla: 'ds', - metric: 'count', - id: 'id_column', - parent: 'relation_column', - name: 'name_column', - }; - const chartPropsConfig = { - formData, - width: 800, - height: 600, - theme: supersetTheme, - }; - const queriesData = [ - { - colnames: ['id_column', 'relation_column', 'name_column', 'count'], - data: [ - { - id_column: '2', - relation_column: '4', - name_column: 'second_child', - count: 20, - }, - { - id_column: '3', - relation_column: '4', - name_column: 'second_child', - count: 30, - }, - { - id_column: '4', - relation_column: '1', - name_column: 'first_child', - count: 40, - }, - { - id_column: '1', - relation_column: null, - name_column: 'root', - count: 10, - }, - ], - }, - ]; - - const chartProps = new ChartProps({ ...chartPropsConfig, queriesData }); - expect(transformProps(chartProps as EchartsTreeChartProps)).toEqual( - expect.objectContaining({ - width: 800, - height: 600, - echartOptions: expect.objectContaining({ - series: expect.arrayContaining([ - expect.objectContaining({ - data: [ - { - name: 'root', - children: [ - { - name: 'first_child', - value: 40, - children: [ - { - name: 'second_child', - value: 20, - children: [], - }, - { - name: 'second_child', - value: 30, - children: [], - }, - ], - }, - ], - }, - ], - }), - ]), - }), - }), - ); - }); -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Treemap/buildQuery.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Treemap/buildQuery.test.ts deleted file mode 100644 index f09419e867e..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Treemap/buildQuery.test.ts +++ /dev/null @@ -1,36 +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 buildQuery from '../../src/Treemap/buildQuery'; - -describe('Treemap buildQuery', () => { - const formData = { - datasource: '5__table', - granularity_sqla: 'ds', - metric: 'foo', - groupby: ['bar'], - viz_type: 'my_chart', - }; - - test('should build query fields from form data', () => { - const queryContext = buildQuery(formData); - const [query] = queryContext.queries; - expect(query.metrics).toEqual(['foo']); - expect(query.columns).toEqual(['bar']); - }); -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Treemap/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Treemap/transformProps.test.ts deleted file mode 100644 index 387c9199ef1..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Treemap/transformProps.test.ts +++ /dev/null @@ -1,77 +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 { ChartProps } from '@superset-ui/core'; -import { supersetTheme } from '@apache-superset/core/theme'; -import { EchartsTreemapChartProps } from '../../src/Treemap/types'; -import transformProps from '../../src/Treemap/transformProps'; - -describe('Treemap transformProps', () => { - const formData = { - colorScheme: 'bnbColors', - datasource: '3__table', - granularity_sqla: 'ds', - metric: 'sum__num', - groupby: ['foo', 'bar'], - }; - const chartProps = new ChartProps({ - formData, - width: 800, - height: 600, - queriesData: [ - { - data: [ - { foo: 'Sylvester', bar: 'bar1', sum__num: 10 }, - { foo: 'Arnold', bar: 'bar2', sum__num: 2.5 }, - ], - }, - ], - theme: supersetTheme, - }); - - test('should transform chart props for viz', () => { - expect(transformProps(chartProps as EchartsTreemapChartProps)).toEqual( - expect.objectContaining({ - width: 800, - height: 600, - echartOptions: expect.objectContaining({ - series: [ - expect.objectContaining({ - data: expect.arrayContaining([ - expect.objectContaining({ - name: 'sum__num', - children: expect.arrayContaining([ - expect.objectContaining({ - name: 'Sylvester', - children: expect.arrayContaining([ - expect.objectContaining({ - name: 'bar1', - value: 10, - }), - ]), - }), - ]), - }), - ]), - }), - ], - }), - }), - ); - }); -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Waterfall/buildQuery.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Waterfall/buildQuery.test.ts deleted file mode 100644 index 5eace89fcc7..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Waterfall/buildQuery.test.ts +++ /dev/null @@ -1,41 +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 { SqlaFormData, VizType } from '@superset-ui/core'; -import buildQuery from '../../src/Waterfall/buildQuery'; - -describe('Waterfall buildQuery', () => { - const formData = { - datasource: '5__table', - granularity_sqla: 'ds', - metric: 'foo', - x_axis: 'bar', - groupby: ['baz'], - viz_type: VizType.Waterfall, - }; - - test('should build query fields from form data', () => { - const queryContext = buildQuery(formData as unknown as SqlaFormData); - const [query] = queryContext.queries; - expect(query.metrics).toEqual(['foo']); - expect(query.columns?.[0]).toEqual( - expect.objectContaining({ sqlExpression: 'bar' }), - ); - expect(query.columns?.[1]).toEqual('baz'); - }); -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Waterfall/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Waterfall/transformProps.test.ts deleted file mode 100644 index ab5cbd8de43..00000000000 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Waterfall/transformProps.test.ts +++ /dev/null @@ -1,168 +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 { ChartProps } from '@superset-ui/core'; -import { supersetTheme } from '@apache-superset/core/theme'; -import { - EchartsWaterfallChartProps, - WaterfallChartTransformedProps, -} from '../../src/Waterfall/types'; -import transformProps from '../../src/Waterfall/transformProps'; - -const extractSeries = (props: WaterfallChartTransformedProps) => { - const { echartOptions } = props; - const { series } = echartOptions as unknown as { - series: [{ data: [{ value: number }] }]; - }; - return series.map(item => item.data).map(item => item.map(i => i.value)); -}; - -const extractSeriesName = (props: WaterfallChartTransformedProps) => { - const { echartOptions } = props; - const { series } = echartOptions as unknown as { - series: [{ name: string }]; - }; - return series.map(item => item.name); -}; - -const data = [ - { year: '2019', name: 'Sylvester', sum: 10 }, - { year: '2019', name: 'Arnold', sum: 3 }, - { year: '2020', name: 'Sylvester', sum: -10 }, - { year: '2020', name: 'Arnold', sum: 5 }, -]; - -const formData = { - colorScheme: 'bnbColors', - datasource: '3__table', - x_axis: 'year', - metric: 'sum', - increaseColor: { r: 0, b: 0, g: 0 }, - decreaseColor: { r: 0, b: 0, g: 0 }, - totalColor: { r: 0, b: 0, g: 0 }, - showTotal: true, -}; - -test('should tranform chart props for viz when breakdown not exist', () => { - const chartProps = new ChartProps({ - formData: { ...formData, series: 'bar' }, - width: 800, - height: 600, - queriesData: [ - { - data, - }, - ], - theme: supersetTheme, - }); - const transformedProps = transformProps( - chartProps as unknown as EchartsWaterfallChartProps, - ); - expect(extractSeries(transformedProps)).toEqual([ - [0, 8, '-'], - [13, '-', '-'], - ['-', 5, '-'], - ['-', '-', 8], - ]); -}); - -test('should tranform chart props for viz when breakdown exist', () => { - const chartProps = new ChartProps({ - formData: { ...formData, groupby: 'name' }, - width: 800, - height: 600, - queriesData: [ - { - data, - }, - ], - theme: supersetTheme, - }); - const transformedProps = transformProps( - chartProps as unknown as EchartsWaterfallChartProps, - ); - expect(extractSeries(transformedProps)).toEqual([ - [0, 10, '-', 3, 3, '-'], - [10, 3, '-', '-', 5, '-'], - ['-', '-', '-', 10, '-', '-'], - ['-', '-', 13, '-', '-', 8], - ]); -}); - -test('renaming series names, checking legend and X axis labels', () => { - const chartProps = new ChartProps({ - formData: { - ...formData, - increaseLabel: 'sale increase', - decreaseLabel: 'sale decrease', - totalLabel: 'sale total', - }, - width: 800, - height: 600, - queriesData: [ - { - data, - }, - ], - theme: supersetTheme, - }); - const transformedProps = transformProps( - chartProps as unknown as EchartsWaterfallChartProps, - ); - expect((transformedProps.echartOptions.legend as any).data).toEqual([ - 'sale increase', - 'sale decrease', - 'sale total', - ]); - - expect((transformedProps.echartOptions.xAxis as any).data).toEqual([ - '2019', - '2020', - 'sale total', - ]); - - expect(extractSeriesName(transformedProps)).toEqual([ - 'Assist', - 'sale increase', - 'sale decrease', - 'sale total', - ]); -}); - -test('hide totals', () => { - const chartProps = new ChartProps({ - formData: { ...formData, series: 'bar', showTotal: false }, - width: 800, - height: 600, - queriesData: [ - { - data, - }, - ], - theme: supersetTheme, - }); - const transformedProps = transformProps( - chartProps as unknown as EchartsWaterfallChartProps, - ); - expect(extractSeries(transformedProps)).toEqual([ - [0, 8], - [13, '-'], - ['-', 5], - ['-', '-'], - ]); -}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/index.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/index.test.ts index 7061dc89076..dfb2084272f 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/test/index.test.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/test/index.test.ts @@ -125,6 +125,6 @@ test('@superset-ui/plugin-chart-echarts-parsemethod-validation', () => { ]; plugins.forEach(plugin => { - expect(plugin.metadata.parseMethod).toEqual('json'); + expect(['json', 'json-bigint']).toContain(plugin.metadata.parseMethod); }); }); diff --git a/superset-frontend/plugins/plugin-chart-echarts/tsconfig.json b/superset-frontend/plugins/plugin-chart-echarts/tsconfig.json index 03190ec5bd4..f0bfbba676d 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/tsconfig.json +++ b/superset-frontend/plugins/plugin-chart-echarts/tsconfig.json @@ -20,6 +20,7 @@ "references": [ { "path": "../../packages/superset-core" }, { "path": "../../packages/superset-ui-core" }, - { "path": "../../packages/superset-ui-chart-controls" } + { "path": "../../packages/superset-ui-chart-controls" }, + { "path": "../../packages/superset-ui-glyph-core" } ] } diff --git a/superset-frontend/plugins/plugin-chart-handlebars/package.json b/superset-frontend/plugins/plugin-chart-handlebars/package.json index a2aa22da45a..ae69712cd2d 100644 --- a/superset-frontend/plugins/plugin-chart-handlebars/package.json +++ b/superset-frontend/plugins/plugin-chart-handlebars/package.json @@ -35,6 +35,7 @@ "@superset-ui/chart-controls": "*", "@apache-superset/core": "*", "@superset-ui/core": "*", + "@superset-ui/glyph-core": "*", "ace-builds": "^1.4.14", "handlebars": "^4.7.8", "lodash": "^4.18.1", diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/Handlebars.tsx b/superset-frontend/plugins/plugin-chart-handlebars/src/Handlebars.tsx deleted file mode 100644 index 54d6de1d346..00000000000 --- a/superset-frontend/plugins/plugin-chart-handlebars/src/Handlebars.tsx +++ /dev/null @@ -1,49 +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 { styled } from '@apache-superset/core/theme'; -import { createRef } from 'react'; -import { HandlebarsViewer } from './components/Handlebars/HandlebarsViewer'; -import { HandlebarsProps, HandlebarsStylesProps } from './types'; - -const Styles = styled.div` - padding: ${({ theme }) => theme.sizeUnit * 4}px; - border-radius: ${({ theme }) => theme.borderRadius}px; - height: ${({ height }) => height}px; - width: ${({ width }) => width}px; - overflow: auto; -`; - -export default function Handlebars(props: HandlebarsProps) { - const { data, height, width, formData } = props; - const styleTemplateSource = formData.styleTemplate - ? `` - : ''; - const handlebarTemplateSource = formData.handlebarsTemplate - ? formData.handlebarsTemplate - : '{{data}}'; - const templateSource = `${handlebarTemplateSource}\n${styleTemplateSource} `; - - const rootElem = createRef(); - - return ( - - - - ); -} diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/components/CodeEditor/CodeEditor.tsx b/superset-frontend/plugins/plugin-chart-handlebars/src/components/CodeEditor/CodeEditor.tsx deleted file mode 100644 index 099f7c56503..00000000000 --- a/superset-frontend/plugins/plugin-chart-handlebars/src/components/CodeEditor/CodeEditor.tsx +++ /dev/null @@ -1,25 +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. - */ - -export { - CodeEditor, - type CodeEditorProps, - type CodeEditorMode, - type CodeEditorTheme, -} from '@superset-ui/core/components'; diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/components/ControlHeader/controlHeader.tsx b/superset-frontend/plugins/plugin-chart-handlebars/src/components/ControlHeader/controlHeader.tsx deleted file mode 100644 index 9bdb452f7a7..00000000000 --- a/superset-frontend/plugins/plugin-chart-handlebars/src/components/ControlHeader/controlHeader.tsx +++ /dev/null @@ -1,31 +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 { ReactNode } from 'react'; - -interface ControlHeaderProps { - children: ReactNode; -} - -export const ControlHeader = ({ - children, -}: ControlHeaderProps): JSX.Element => ( -
-
{children}
-
-); diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/components/Handlebars/HandlebarsViewer.tsx b/superset-frontend/plugins/plugin-chart-handlebars/src/components/Handlebars/HandlebarsViewer.tsx deleted file mode 100644 index 557290af42f..00000000000 --- a/superset-frontend/plugins/plugin-chart-handlebars/src/components/Handlebars/HandlebarsViewer.tsx +++ /dev/null @@ -1,118 +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 { t } from '@apache-superset/core/translation'; -import { styled } from '@apache-superset/core/theme'; -import { SafeMarkdown } from '@superset-ui/core/components'; -import { extendedDayjs as dayjs } from '@superset-ui/core/utils/dates'; -import Handlebars from 'handlebars'; -import { useMemo, useState } from 'react'; -import { isPlainObject } from 'lodash'; -import Helpers from 'just-handlebars-helpers'; -import HandlebarsGroupBy from 'handlebars-group-by'; - -export interface HandlebarsViewerProps { - templateSource: string; - data: any; -} - -export const HandlebarsViewer = ({ - templateSource, - data, -}: HandlebarsViewerProps) => { - const [renderedTemplate, setRenderedTemplate] = useState(''); - const [error, setError] = useState(''); - const appContainer = document.getElementById('app'); - const { common } = JSON.parse( - appContainer?.getAttribute('data-bootstrap') || '{}', - ); - const htmlSanitization = common?.conf?.HTML_SANITIZATION ?? true; - const htmlSchemaOverrides = - common?.conf?.HTML_SANITIZATION_SCHEMA_EXTENSIONS || {}; - - useMemo(() => { - try { - const template = Handlebars.compile(templateSource); - const result = template(data); - setRenderedTemplate(result); - setError(''); - } catch (error) { - setRenderedTemplate(''); - setError(error.message); - } - }, [templateSource, data]); - - const Error = styled.pre` - white-space: pre-wrap; - `; - - if (error) { - return {error}; - } - - if (renderedTemplate) { - return ( - - ); - } - return

{t('Loading...')}

; -}; - -// usage: {{ dateFormat my_date format="MMMM YYYY" }} -Handlebars.registerHelper('dateFormat', function (context, block) { - const f = block.hash.format || 'YYYY-MM-DD'; - return dayjs(context).format(f); -}); - -// usage: {{ }} -Handlebars.registerHelper('stringify', (obj: any, obj2: any) => { - // calling without an argument - if (obj2 === undefined) - throw new Error('Please call with an object. Example: `stringify myObj`'); - return isPlainObject(obj) ? JSON.stringify(obj) : String(obj); -}); - -Handlebars.registerHelper( - 'formatNumber', - function (number: any, locale = 'en-US') { - if (typeof number !== 'number') { - return number; - } - return number.toLocaleString(locale); - }, -); - -// usage: {{parseJson jsonString}} -Handlebars.registerHelper('parseJson', (jsonString: string) => { - try { - return JSON.parse(jsonString); - } catch (error) { - if (error instanceof Error) { - error.message = `Invalid JSON string: ${error.message}`; - throw error; - } - throw new Error(`Invalid JSON string: ${String(error)}`); - } -}); - -Helpers.registerHelpers(Handlebars); -HandlebarsGroupBy.register(Handlebars); diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/consts.ts b/superset-frontend/plugins/plugin-chart-handlebars/src/consts.ts deleted file mode 100644 index 8b722af5435..00000000000 --- a/superset-frontend/plugins/plugin-chart-handlebars/src/consts.ts +++ /dev/null @@ -1,25 +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 { debounce } from 'lodash'; -import { Constants } from '@superset-ui/core/components'; - -export const debounceFunc = debounce( - (func: (val: string) => void, source: string) => func(source), - Constants.SLOW_DEBOUNCE, -); diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/index.ts b/superset-frontend/plugins/plugin-chart-handlebars/src/index.ts deleted file mode 100644 index c39fe12b952..00000000000 --- a/superset-frontend/plugins/plugin-chart-handlebars/src/index.ts +++ /dev/null @@ -1,27 +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. - */ -// eslint-disable-next-line import/prefer-default-export -export { default as HandlebarsChartPlugin } from './plugin'; -/** - * Note: this file exports the default export from Handlebars.tsx. - * If you want to export multiple visualization modules, you will need to - * either add additional plugin folders (similar in structure to ./plugin) - * OR export multiple instances of `ChartPlugin` extensions in ./plugin/index.ts - * which in turn load exports from Handlebars.tsx - */ diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/index.tsx b/superset-frontend/plugins/plugin-chart-handlebars/src/index.tsx new file mode 100644 index 00000000000..dfd66219ee1 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-handlebars/src/index.tsx @@ -0,0 +1,676 @@ +/** + * 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. + */ + +/** + * Handlebars Chart - Glyph Pattern Implementation + * + * Renders data via a user-supplied Handlebars template with optional CSS. + * Supports both Aggregate and Raw query modes. + */ + +import { t } from '@apache-superset/core/translation'; +import { styled, useTheme } from '@apache-superset/core/theme'; +import { + buildQueryContext, + ensureIsArray, + normalizeOrderBy, + QueryFormColumn, + QueryFormData, + QueryFormMetric, + QueryMode, + TimeGranularity, + TimeseriesDataRecord, + validateNonEmpty, +} from '@superset-ui/core'; +import { + CodeEditor, + Constants, + InfoTooltip, + SafeMarkdown, +} from '@superset-ui/core/components'; +import { extendedDayjs as dayjs } from '@superset-ui/core/utils/dates'; +import { + ColumnMeta, + ControlConfig, + ControlPanelState, + ControlPanelsContainerProps, + ControlSetItem, + ControlState, + ControlStateMapping, + CustomControlConfig, + Dataset, + defineSavedMetrics, + ExtraControlProps, + getStandardizedControls, + QueryModeLabel, + sharedControls, +} from '@superset-ui/chart-controls'; +import { defineChart } from '@superset-ui/glyph-core'; +import Handlebars from 'handlebars'; +import { debounce, isEmpty, isPlainObject } from 'lodash'; +import Helpers from 'just-handlebars-helpers'; +import HandlebarsGroupBy from 'handlebars-group-by'; +import { createRef, ReactNode, useMemo, useState } from 'react'; + +import thumbnail from './images/thumbnail.png'; +import thumbnailDark from './images/thumbnail-dark.png'; +import example1 from './images/example1.jpg'; +import example1Dark from './images/example1-dark.jpg'; +import example2 from './images/example2.jpg'; +import example2Dark from './images/example2-dark.jpg'; + +// ─── Types ─────────────────────────────────────────────────────────────────── + +export type HandlebarsQueryFormData = QueryFormData & { + height: number; + width: number; + handlebarsTemplate?: string; + styleTemplate?: string; + align_pn?: boolean; + color_pn?: boolean; + include_time?: boolean; + include_search?: boolean; + query_mode?: QueryMode; + page_length?: string | number | null; + metrics?: QueryFormMetric[] | null; + percent_metrics?: QueryFormMetric[] | null; + timeseries_limit_metric?: QueryFormMetric[] | QueryFormMetric | null; + groupby?: QueryFormMetric[] | null; + all_columns?: QueryFormMetric[] | null; + order_desc?: boolean; + table_timestamp_format?: string; + granularitySqla?: string; + time_grain_sqla?: TimeGranularity; +}; + +type HandlebarsProps = { + height: number; + width: number; + data: TimeseriesDataRecord[]; + formData: HandlebarsQueryFormData; +}; + +// ─── Helpers & Utilities ───────────────────────────────────────────────────── + +const debounceFunc = debounce( + (func: (val: string) => void, source: string) => func(source), + Constants.SLOW_DEBOUNCE, +); + +const ControlHeader = ({ children }: { children: ReactNode }) => ( +
+
{children}
+
+); + +// ─── Handlebars Template Engine Setup ──────────────────────────────────────── + +// usage: {{ dateFormat my_date format="MMMM YYYY" }} +Handlebars.registerHelper('dateFormat', function (context, block) { + const f = block.hash.format || 'YYYY-MM-DD'; + return dayjs(context).format(f); +}); + +// usage: {{ stringify myObj }} +// eslint-disable-next-line @typescript-eslint/no-explicit-any +Handlebars.registerHelper('stringify', (obj: any, obj2: any) => { + if (obj2 === undefined) + throw new Error('Please call with an object. Example: `stringify myObj`'); + return isPlainObject(obj) ? JSON.stringify(obj) : String(obj); +}); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +Handlebars.registerHelper( + 'formatNumber', + function (number: any, locale = 'en-US') { + if (typeof number !== 'number') return number; + return number.toLocaleString(locale); + }, +); + +// usage: {{parseJson jsonString}} +Handlebars.registerHelper('parseJson', (jsonString: string) => { + try { + return JSON.parse(jsonString); + } catch (error) { + if (error instanceof Error) { + error.message = `Invalid JSON string: ${error.message}`; + throw error; + } + throw new Error(`Invalid JSON string: ${String(error)}`); + } +}); + +Helpers.registerHelpers(Handlebars); +HandlebarsGroupBy.register(Handlebars); + +// ─── HandlebarsViewer ──────────────────────────────────────────────────────── + +const TemplateError = styled.pre` + white-space: pre-wrap; +`; + +function HandlebarsViewer({ + templateSource, + data, +}: { + templateSource: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data: any; +}) { + const [renderedTemplate, setRenderedTemplate] = useState(''); + const [error, setError] = useState(''); + const appContainer = document.getElementById('app'); + const { common } = JSON.parse( + appContainer?.getAttribute('data-bootstrap') || '{}', + ); + const htmlSanitization = common?.conf?.HTML_SANITIZATION ?? true; + const htmlSchemaOverrides = + common?.conf?.HTML_SANITIZATION_SCHEMA_EXTENSIONS || {}; + + useMemo(() => { + try { + const template = Handlebars.compile(templateSource); + const result = template(data); + setRenderedTemplate(result); + setError(''); + } catch (err) { + setRenderedTemplate(''); + setError((err as Error).message); + } + }, [templateSource, data]); + + if (error) return {error}; + if (renderedTemplate) { + return ( + + ); + } + return

{t('Loading...')}

; +} + +// ─── Render Component ──────────────────────────────────────────────────────── + +const HandlebarsStyles = styled.div<{ height: number; width: number }>` + padding: ${({ theme }) => theme.sizeUnit * 4}px; + border-radius: ${({ theme }) => theme.borderRadius}px; + height: ${({ height }) => height}px; + width: ${({ width }) => width}px; + overflow: auto; +`; + +function HandlebarsChart(props: HandlebarsProps) { + const { data, height, width, formData } = props; + const styleTemplateSource = formData.styleTemplate + ? `` + : ''; + const templateSource = `${formData.handlebarsTemplate || '{{data}}'}\n${styleTemplateSource} `; + const rootElem = createRef(); + return ( + + + + ); +} + +// ─── Query Mode Utilities ───────────────────────────────────────────────────── + +function getQueryMode(controls: ControlStateMapping): QueryMode { + const mode = controls?.query_mode?.value; + if (mode === QueryMode.Aggregate || mode === QueryMode.Raw) { + return mode as QueryMode; + } + const rawColumns = controls?.all_columns?.value as + | QueryFormColumn[] + | undefined; + return rawColumns?.length ? QueryMode.Raw : QueryMode.Aggregate; +} + +function isQueryMode(mode: QueryMode) { + return ({ controls }: Pick) => + getQueryMode(controls) === mode; +} + +const isAggMode = isQueryMode(QueryMode.Aggregate); +const isRawMode = isQueryMode(QueryMode.Raw); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function validateAggControlValues( + controls: ControlStateMapping, + values: any[], +) { + const areControlsEmpty = values.every(val => ensureIsArray(val).length === 0); + return areControlsEmpty && isAggMode({ controls }) + ? [t('Group By, Metrics or Percentage Metrics must have a value')] + : []; +} + +// ─── Controls ───────────────────────────────────────────────────────────────── + +const queryModeControlSetItem: ControlSetItem = { + name: 'query_mode', + config: { + type: 'RadioButtonControl', + label: t('Query mode'), + default: null, + options: [ + [QueryMode.Aggregate, QueryModeLabel[QueryMode.Aggregate]], + [QueryMode.Raw, QueryModeLabel[QueryMode.Raw]], + ], + mapStateToProps: ({ controls }) => ({ value: getQueryMode(controls) }), + rerender: ['all_columns', 'groupby', 'metrics', 'percent_metrics'], + } as ControlConfig<'RadioButtonControl'>, +}; + +const groupByControlSetItem: ControlSetItem = { + name: 'groupby', + override: { + visibility: isAggMode, + resetOnHide: false, + mapStateToProps: (state: ControlPanelState, controlState: ControlState) => { + const { controls } = state; + const originalMapStateToProps = sharedControls?.groupby?.mapStateToProps; + const newState = originalMapStateToProps?.(state, controlState) ?? {}; + newState.externalValidationErrors = validateAggControlValues(controls, [ + controls.metrics?.value, + controls.percent_metrics?.value, + controlState.value, + ]); + return newState; + }, + rerender: ['metrics', 'percent_metrics'], + }, +}; + +const allColumnsControlSetItem: ControlSetItem = { + name: 'all_columns', + config: { + type: 'DndColumnSelect', + label: t('Columns'), + description: t('Columns to display'), + default: [], + mapStateToProps({ datasource, controls }, controlState) { + const newState: ExtraControlProps = {}; + if (datasource) { + if (datasource?.columns[0]?.hasOwnProperty('filterable')) { + newState.options = (datasource as Dataset)?.columns?.filter( + (c: ColumnMeta) => c.filterable, + ); + } else { + newState.options = datasource.columns; + } + } + newState.queryMode = getQueryMode(controls); + newState.externalValidationErrors = + isRawMode({ controls }) && + ensureIsArray(controlState?.value).length === 0 + ? [t('must have a value')] + : []; + return newState; + }, + visibility: isRawMode, + resetOnHide: false, + } as typeof sharedControls.groupby, +}; + +const metricsControlSetItem: ControlSetItem = { + name: 'metrics', + override: { + validators: [], + visibility: isAggMode, + mapStateToProps: ( + { controls, datasource, form_data }: ControlPanelState, + controlState: ControlState, + ) => ({ + columns: datasource?.columns[0]?.hasOwnProperty('filterable') + ? (datasource as Dataset)?.columns?.filter( + (c: ColumnMeta) => c.filterable, + ) + : datasource?.columns, + savedMetrics: defineSavedMetrics(datasource), + selectedMetrics: + form_data.metrics || (form_data.metric ? [form_data.metric] : []), + datasource, + externalValidationErrors: validateAggControlValues(controls, [ + controls.groupby?.value, + controls.percent_metrics?.value, + controlState.value, + ]), + }), + rerender: ['groupby', 'percent_metrics'], + resetOnHide: false, + }, +}; + +const percentMetricsControlSetItem: ControlSetItem = { + name: 'percent_metrics', + config: { + type: 'DndMetricSelect', + label: t('Percentage metrics'), + description: t( + 'Select one or many metrics to display, that will be displayed in the percentages of total. ' + + 'Percentage metrics will be calculated only from data within the row limit. ' + + 'You can use an aggregation function on a column or write custom SQL to create a percentage metric.', + ), + multi: true, + visibility: isAggMode, + resetOnHide: false, + mapStateToProps: ({ datasource, controls }, controlState) => ({ + columns: datasource?.columns || [], + savedMetrics: defineSavedMetrics(datasource), + datasource, + datasourceType: datasource?.type, + queryMode: getQueryMode(controls), + externalValidationErrors: validateAggControlValues(controls, [ + controls.groupby?.value, + controls.metrics?.value, + controlState?.value, + ]), + }), + rerender: ['groupby', 'metrics'], + default: [], + validators: [], + }, +}; + +const showTotalsControlSetItem: ControlSetItem = { + name: 'show_totals', + config: { + type: 'CheckboxControl', + label: t('Show summary'), + default: false, + description: t( + 'Show total aggregations of selected metrics. Note that row limit does not apply to the result.', + ), + visibility: isAggMode, + resetOnHide: false, + }, +}; + +const rowLimitControlSetItem: ControlSetItem = { + name: 'row_limit', + override: { + visibility: ({ controls }: ControlPanelsContainerProps) => + !controls?.server_pagination?.value, + }, +}; + +const timeSeriesLimitMetricControlSetItem: ControlSetItem = { + name: 'timeseries_limit_metric', + override: { + visibility: isAggMode, + resetOnHide: false, + }, +}; + +const orderByControlSetItem: ControlSetItem = { + name: 'order_by_cols', + config: { + type: 'SelectControl', + label: t('Ordering'), + description: t('Order results by selected columns'), + multi: true, + default: [], + mapStateToProps: ({ datasource }) => ({ + choices: datasource?.hasOwnProperty('order_by_choices') + ? (datasource as Dataset)?.order_by_choices + : datasource?.columns || [], + }), + visibility: isRawMode, + resetOnHide: false, + }, +}; + +const orderDescendingControlSetItem: ControlSetItem = { + name: 'order_desc', + config: { + type: 'CheckboxControl', + label: t('Sort descending'), + default: true, + description: t('Whether to sort descending or ascending'), + visibility: ({ controls }) => + !!( + isAggMode({ controls }) && + controls?.timeseries_limit_metric.value && + !isEmpty(controls?.timeseries_limit_metric.value) + ), + resetOnHide: false, + }, +}; + +const includeTimeControlSetItem: ControlSetItem = { + name: 'include_time', + config: { + type: 'CheckboxControl', + label: t('Include time'), + description: t( + 'Whether to include the time granularity as defined in the time section', + ), + default: false, + visibility: isAggMode, + resetOnHide: false, + }, +}; + +// ─── Code Editor Controls ───────────────────────────────────────────────────── + +const HandlebarsTemplateControl = ( + props: CustomControlConfig<{ value: string }>, +) => { + const theme = useTheme(); + const val = String(props?.value || props?.default || ''); + + const helperDescriptionsHeader = t( + 'Available Handlebars Helpers in Superset:', + ); + const helperDescriptions = [ + { key: 'dateFormat', descKey: 'Formats a date using a specified format.' }, + { key: 'stringify', descKey: 'Converts an object to a JSON string.' }, + { + key: 'formatNumber', + descKey: 'Formats a number using locale-specific formatting.', + }, + { + key: 'parseJson', + descKey: 'Parses a JSON string into a JavaScript object.', + }, + ]; + const helpersTooltipContent = ` +${helperDescriptionsHeader} + +${helperDescriptions + .map(({ key, descKey }) => `- **${key}**: ${t(descKey)}`) + .join('\n')} +`; + + return ( +
+ +
+ {props.label as ReactNode} + } + /> +
+
+ { + debounceFunc(props.onChange, source || ''); + }} + /> +
+ ); +}; + +const handlebarsTemplateControlSetItem: ControlSetItem = { + name: 'handlebarsTemplate', + config: { + ...sharedControls.entity, + type: HandlebarsTemplateControl, + label: t('Handlebars Template'), + description: t('A handlebars template that is applied to the data'), + default: `
    + {{#each data}} +
  • {{stringify this}}
  • + {{/each}} +
`, + isInt: false, + renderTrigger: true, + valueKey: null, + validators: [validateNonEmpty], + mapStateToProps: ({ controls }) => ({ + value: controls?.handlebars_template?.value, + }), + }, +}; + +const StyleControl = (props: CustomControlConfig<{ value: string }>) => { + const theme = useTheme(); + const defaultValue = props?.value + ? undefined + : `/* + .data-list { + background-color: yellow; + } +*/`; + + return ( +
+ +
+ {props.label as ReactNode} + +
+
+ { + debounceFunc(props.onChange, source || ''); + }} + /> +
+ ); +}; + +const styleControlSetItem: ControlSetItem = { + name: 'styleTemplate', + config: { + ...sharedControls.entity, + type: StyleControl, + label: t('CSS Styles'), + description: t('CSS applied to the chart'), + isInt: false, + renderTrigger: true, + valueKey: null, + validators: [], + mapStateToProps: ({ controls }) => ({ + value: controls?.handlebars_template?.value, + }), + }, +}; + +// ─── Plugin Definition ──────────────────────────────────────────────────────── + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type HandlebarsExtra = Record; + +// Standalone exports for testing +export function buildQuery(formData: QueryFormData) { + return buildQueryContext(formData, baseQueryObject => [ + { + ...baseQueryObject, + orderby: normalizeOrderBy(baseQueryObject).orderby, + }, + ]); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function transformProps(chartProps: any) { + const { formData, queriesData, width, height } = chartProps; + const { data } = queriesData[0]; + return { width, height, data, formData }; +} + +export default defineChart, HandlebarsExtra>({ + metadata: { + name: t('Handlebars'), + description: t('Write a handlebars template to render the data'), + thumbnail, + thumbnailDark, + exampleGallery: [ + { url: example1, urlDark: example1Dark }, + { url: example2, urlDark: example2Dark }, + ], + }, + arguments: {}, + buildQuery: (formData: QueryFormData) => + buildQueryContext(formData, baseQueryObject => [ + { + ...baseQueryObject, + orderby: normalizeOrderBy(baseQueryObject).orderby, + }, + ]), + suppressQuerySection: true, + prependSections: [ + { + label: t('Query'), + expanded: true, + controlSetRows: [ + [queryModeControlSetItem], + [groupByControlSetItem], + [metricsControlSetItem, allColumnsControlSetItem], + [percentMetricsControlSetItem], + [timeSeriesLimitMetricControlSetItem, orderByControlSetItem], + [orderDescendingControlSetItem], + [rowLimitControlSetItem], + [includeTimeControlSetItem], + [showTotalsControlSetItem], + ['adhoc_filters'], + ], + }, + { + label: t('Options'), + expanded: true, + controlSetRows: [ + [handlebarsTemplateControlSetItem], + [styleControlSetItem], + ], + }, + ], + formDataOverrides: formData => ({ + ...formData, + groupby: getStandardizedControls().popAllColumns(), + metrics: getStandardizedControls().popAllMetrics(), + }), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + render: (props: any) => , +}); diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/buildQuery.ts b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/buildQuery.ts deleted file mode 100644 index c741e6c4657..00000000000 --- a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/buildQuery.ts +++ /dev/null @@ -1,32 +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 { - buildQueryContext, - normalizeOrderBy, - QueryFormData, -} from '@superset-ui/core'; - -export default function buildQuery(formData: QueryFormData) { - return buildQueryContext(formData, baseQueryObject => [ - { - ...baseQueryObject, - orderby: normalizeOrderBy(baseQueryObject).orderby, - }, - ]); -} diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controlPanel.tsx deleted file mode 100644 index c161b46778f..00000000000 --- a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controlPanel.tsx +++ /dev/null @@ -1,78 +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 { - ControlPanelConfig, - getStandardizedControls, -} from '@superset-ui/chart-controls'; -import { t } from '@apache-superset/core/translation'; -import { allColumnsControlSetItem } from './controls/columns'; -import { groupByControlSetItem } from './controls/groupBy'; -import { handlebarsTemplateControlSetItem } from './controls/handlebarTemplate'; -import { includeTimeControlSetItem } from './controls/includeTime'; -import { - rowLimitControlSetItem, - timeSeriesLimitMetricControlSetItem, -} from './controls/limits'; -import { - metricsControlSetItem, - percentMetricsControlSetItem, - showTotalsControlSetItem, -} from './controls/metrics'; -import { - orderByControlSetItem, - orderDescendingControlSetItem, -} from './controls/orderBy'; -import { queryModeControlSetItem } from './controls/queryMode'; -import { styleControlSetItem } from './controls/style'; - -const config: ControlPanelConfig = { - controlPanelSections: [ - { - label: t('Query'), - expanded: true, - controlSetRows: [ - [queryModeControlSetItem], - [groupByControlSetItem], - [metricsControlSetItem, allColumnsControlSetItem], - [percentMetricsControlSetItem], - [timeSeriesLimitMetricControlSetItem, orderByControlSetItem], - [orderDescendingControlSetItem], - [rowLimitControlSetItem], - [includeTimeControlSetItem], - [showTotalsControlSetItem], - ['adhoc_filters'], - ], - }, - { - label: t('Options'), - expanded: true, - controlSetRows: [ - [handlebarsTemplateControlSetItem], - [styleControlSetItem], - ], - }, - ], - formDataOverrides: formData => ({ - ...formData, - groupby: getStandardizedControls().popAllColumns(), - metrics: getStandardizedControls().popAllMetrics(), - }), -}; - -export default config; diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/columns.tsx b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/columns.tsx deleted file mode 100644 index bd7d30a139f..00000000000 --- a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/columns.tsx +++ /dev/null @@ -1,58 +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 { - ControlSetItem, - ExtraControlProps, - sharedControls, - Dataset, - ColumnMeta, -} from '@superset-ui/chart-controls'; -import { t } from '@apache-superset/core/translation'; -import { ensureIsArray } from '@superset-ui/core'; -import { getQueryMode, isRawMode } from './shared'; - -const dndAllColumns: typeof sharedControls.groupby = { - type: 'DndColumnSelect', - label: t('Columns'), - description: t('Columns to display'), - default: [], - mapStateToProps({ datasource, controls }, controlState) { - const newState: ExtraControlProps = {}; - if (datasource) { - if (datasource?.columns[0]?.hasOwnProperty('filterable')) { - newState.options = (datasource as Dataset)?.columns?.filter( - (c: ColumnMeta) => c.filterable, - ); - } else newState.options = datasource.columns; - } - newState.queryMode = getQueryMode(controls); - newState.externalValidationErrors = - isRawMode({ controls }) && ensureIsArray(controlState?.value).length === 0 - ? [t('must have a value')] - : []; - return newState; - }, - visibility: isRawMode, - resetOnHide: false, -}; - -export const allColumnsControlSetItem: ControlSetItem = { - name: 'all_columns', - config: dndAllColumns, -}; diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/groupBy.tsx b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/groupBy.tsx deleted file mode 100644 index e3bea44b64c..00000000000 --- a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/groupBy.tsx +++ /dev/null @@ -1,45 +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 { - ControlPanelState, - ControlSetItem, - ControlState, - sharedControls, -} from '@superset-ui/chart-controls'; -import { isAggMode, validateAggControlValues } from './shared'; - -export const groupByControlSetItem: ControlSetItem = { - name: 'groupby', - override: { - visibility: isAggMode, - resetOnHide: false, - mapStateToProps: (state: ControlPanelState, controlState: ControlState) => { - const { controls } = state; - const originalMapStateToProps = sharedControls?.groupby?.mapStateToProps; - const newState = originalMapStateToProps?.(state, controlState) ?? {}; - newState.externalValidationErrors = validateAggControlValues(controls, [ - controls.metrics?.value, - controls.percent_metrics?.value, - controlState.value, - ]); - return newState; - }, - rerender: ['metrics', 'percent_metrics'], - }, -}; diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/handlebarTemplate.tsx b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/handlebarTemplate.tsx deleted file mode 100644 index 21a27553526..00000000000 --- a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/handlebarTemplate.tsx +++ /dev/null @@ -1,98 +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 { - ControlSetItem, - CustomControlConfig, - sharedControls, -} from '@superset-ui/chart-controls'; -import { t } from '@apache-superset/core/translation'; -import { validateNonEmpty } from '@superset-ui/core'; -import { useTheme } from '@apache-superset/core/theme'; -import { InfoTooltip } from '@superset-ui/core/components'; -import { CodeEditor } from '../../components/CodeEditor/CodeEditor'; -import { ControlHeader } from '../../components/ControlHeader/controlHeader'; -import { debounceFunc } from '../../consts'; - -interface HandlebarsCustomControlProps { - value: string; -} - -const HandlebarsTemplateControl = ( - props: CustomControlConfig, -) => { - const theme = useTheme(); - const val = String( - props?.value ? props?.value : props?.default ? props?.default : '', - ); - - return ( -
- - - - { - debounceFunc(props.onChange, source || ''); - }} - /> -
- ); -}; - -export const handlebarsTemplateControlSetItem: ControlSetItem = { - name: 'handlebarsTemplate', - config: { - ...sharedControls.entity, - type: HandlebarsTemplateControl, - label: t('Handlebars Template'), - description: t('A handlebars template that is applied to the data'), - default: `
    - {{#each data}} -
  • {{stringify this}}
  • - {{/each}} -
`, - isInt: false, - renderTrigger: true, - valueKey: null, - validators: [validateNonEmpty], - mapStateToProps: ({ form_data }) => ({ - value: form_data?.handlebarsTemplate ?? form_data?.handlebars_template, - }), - }, -}; diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/includeTime.ts b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/includeTime.ts deleted file mode 100644 index b02530049fc..00000000000 --- a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/includeTime.ts +++ /dev/null @@ -1,35 +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 { ControlSetItem } from '@superset-ui/chart-controls'; -import { t } from '@apache-superset/core/translation'; -import { isAggMode } from './shared'; - -export const includeTimeControlSetItem: ControlSetItem = { - name: 'include_time', - config: { - type: 'CheckboxControl', - label: t('Include time'), - description: t( - 'Whether to include the time granularity as defined in the time section', - ), - default: false, - visibility: isAggMode, - resetOnHide: false, - }, -}; diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/limits.ts b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/limits.ts deleted file mode 100644 index 2c28d92742b..00000000000 --- a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/limits.ts +++ /dev/null @@ -1,39 +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 { - ControlPanelsContainerProps, - ControlSetItem, -} from '@superset-ui/chart-controls'; -import { isAggMode } from './shared'; - -export const rowLimitControlSetItem: ControlSetItem = { - name: 'row_limit', - override: { - visibility: ({ controls }: ControlPanelsContainerProps) => - !controls?.server_pagination?.value, - }, -}; - -export const timeSeriesLimitMetricControlSetItem: ControlSetItem = { - name: 'timeseries_limit_metric', - override: { - visibility: isAggMode, - resetOnHide: false, - }, -}; diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/metrics.tsx b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/metrics.tsx deleted file mode 100644 index de33013527b..00000000000 --- a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/metrics.tsx +++ /dev/null @@ -1,113 +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 { - ControlPanelState, - ControlSetItem, - ControlState, - sharedControls, - Dataset, - ColumnMeta, - defineSavedMetrics, -} from '@superset-ui/chart-controls'; -import { t } from '@apache-superset/core/translation'; -import { getQueryMode, isAggMode, validateAggControlValues } from './shared'; - -const percentMetrics: typeof sharedControls.metrics = { - type: 'MetricsControl', - label: t('Percentage metrics'), - description: t( - 'Select one or many metrics to display, that will be displayed in the percentages of total. ' + - 'Percentage metrics will be calculated only from data within the row limit. ' + - 'You can use an aggregation function on a column or write custom SQL to create a percentage metric.', - ), - multi: true, - visibility: isAggMode, - resetOnHide: false, - mapStateToProps: ({ datasource, controls }, controlState) => ({ - columns: datasource?.columns || [], - savedMetrics: defineSavedMetrics(datasource), - datasource, - datasourceType: datasource?.type, - queryMode: getQueryMode(controls), - externalValidationErrors: validateAggControlValues(controls, [ - controls.groupby?.value, - controls.metrics?.value, - controlState?.value, - ]), - }), - rerender: ['groupby', 'metrics'], - default: [], - validators: [], -}; - -const dndPercentMetrics = { - ...percentMetrics, - type: 'DndMetricSelect', -}; - -export const percentMetricsControlSetItem: ControlSetItem = { - name: 'percent_metrics', - config: { - ...dndPercentMetrics, - }, -}; - -export const metricsControlSetItem: ControlSetItem = { - name: 'metrics', - override: { - validators: [], - visibility: isAggMode, - mapStateToProps: ( - { controls, datasource, form_data }: ControlPanelState, - controlState: ControlState, - ) => ({ - columns: datasource?.columns[0]?.hasOwnProperty('filterable') - ? (datasource as Dataset)?.columns?.filter( - (c: ColumnMeta) => c.filterable, - ) - : datasource?.columns, - savedMetrics: defineSavedMetrics(datasource), - // current active adhoc metrics - selectedMetrics: - form_data.metrics || (form_data.metric ? [form_data.metric] : []), - datasource, - externalValidationErrors: validateAggControlValues(controls, [ - controls.groupby?.value, - controls.percent_metrics?.value, - controlState.value, - ]), - }), - rerender: ['groupby', 'percent_metrics'], - resetOnHide: false, - }, -}; - -export const showTotalsControlSetItem: ControlSetItem = { - name: 'show_totals', - config: { - type: 'CheckboxControl', - label: t('Show summary'), - default: false, - description: t( - 'Show total aggregations of selected metrics. Note that row limit does not apply to the result.', - ), - visibility: isAggMode, - resetOnHide: false, - }, -}; diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/orderBy.tsx b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/orderBy.tsx deleted file mode 100644 index cd46e8a3880..00000000000 --- a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/orderBy.tsx +++ /dev/null @@ -1,57 +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 { ControlSetItem, Dataset } from '@superset-ui/chart-controls'; -import { t } from '@apache-superset/core/translation'; -import { isEmpty } from 'lodash'; -import { isAggMode, isRawMode } from './shared'; - -export const orderByControlSetItem: ControlSetItem = { - name: 'order_by_cols', - config: { - type: 'SelectControl', - label: t('Ordering'), - description: t('Order results by selected columns'), - multi: true, - default: [], - mapStateToProps: ({ datasource }) => ({ - choices: datasource?.hasOwnProperty('order_by_choices') - ? (datasource as Dataset)?.order_by_choices - : datasource?.columns || [], - }), - visibility: isRawMode, - resetOnHide: false, - }, -}; - -export const orderDescendingControlSetItem: ControlSetItem = { - name: 'order_desc', - config: { - type: 'CheckboxControl', - label: t('Sort descending'), - default: true, - description: t('Whether to sort descending or ascending'), - visibility: ({ controls }) => - !!( - isAggMode({ controls }) && - controls?.timeseries_limit_metric.value && - !isEmpty(controls?.timeseries_limit_metric.value) - ), - resetOnHide: false, - }, -}; diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/queryMode.tsx b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/queryMode.tsx deleted file mode 100644 index 9d25c154304..00000000000 --- a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/queryMode.tsx +++ /dev/null @@ -1,43 +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 { - ControlConfig, - ControlSetItem, - QueryModeLabel, -} from '@superset-ui/chart-controls'; -import { t } from '@apache-superset/core/translation'; -import { QueryMode } from '@superset-ui/core'; -import { getQueryMode } from './shared'; - -const queryMode: ControlConfig<'RadioButtonControl'> = { - type: 'RadioButtonControl', - label: t('Query mode'), - default: null, - options: [ - [QueryMode.Aggregate, QueryModeLabel[QueryMode.Aggregate]], - [QueryMode.Raw, QueryModeLabel[QueryMode.Raw]], - ], - mapStateToProps: ({ controls }) => ({ value: getQueryMode(controls) }), - rerender: ['all_columns', 'groupby', 'metrics', 'percent_metrics'], -}; - -export const queryModeControlSetItem: ControlSetItem = { - name: 'query_mode', - config: queryMode, -}; diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/shared.ts b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/shared.ts deleted file mode 100644 index 895e7f70d3e..00000000000 --- a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/shared.ts +++ /dev/null @@ -1,57 +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 { - ControlPanelsContainerProps, - ControlStateMapping, -} from '@superset-ui/chart-controls'; -import { t } from '@apache-superset/core/translation'; -import { ensureIsArray, QueryFormColumn, QueryMode } from '@superset-ui/core'; - -export function getQueryMode(controls: ControlStateMapping): QueryMode { - const mode = controls?.query_mode?.value; - if (mode === QueryMode.Aggregate || mode === QueryMode.Raw) { - return mode as QueryMode; - } - const rawColumns = controls?.all_columns?.value as - | QueryFormColumn[] - | undefined; - const hasRawColumns = rawColumns && rawColumns.length > 0; - return hasRawColumns ? QueryMode.Raw : QueryMode.Aggregate; -} - -/** - * Visibility check - */ -export function isQueryMode(mode: QueryMode) { - return ({ controls }: Pick) => - getQueryMode(controls) === mode; -} - -export const isAggMode = isQueryMode(QueryMode.Aggregate); -export const isRawMode = isQueryMode(QueryMode.Raw); - -export const validateAggControlValues = ( - controls: ControlStateMapping, - values: any[], -) => { - const areControlsEmpty = values.every(val => ensureIsArray(val).length === 0); - return areControlsEmpty && isAggMode({ controls }) - ? [t('Group By, Metrics or Percentage Metrics must have a value')] - : []; -}; diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/style.tsx b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/style.tsx deleted file mode 100644 index 6ff62dac406..00000000000 --- a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/controls/style.tsx +++ /dev/null @@ -1,95 +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 { - ControlSetItem, - CustomControlConfig, - sharedControls, -} from '@superset-ui/chart-controls'; -import { t } from '@apache-superset/core/translation'; -import { useTheme } from '@apache-superset/core/theme'; -import { InfoTooltip } from '@superset-ui/core/components'; -import { CodeEditor } from '../../components/CodeEditor/CodeEditor'; -import { ControlHeader } from '../../components/ControlHeader/controlHeader'; -import { debounceFunc } from '../../consts'; - -interface StyleCustomControlProps { - value: string; - htmlSanitization: boolean; -} - -const StyleControl = (props: CustomControlConfig) => { - const theme = useTheme(); - const htmlSanitization = props.htmlSanitization ?? true; - - const defaultValue = props?.value - ? undefined - : `/* - .data-list { - background-color: yellow; - } -*/`; - - return ( -
- -
- {typeof props.label === 'function' ? null : props.label} - {htmlSanitization && ( - - )} -
-
- { - debounceFunc(props.onChange, source || ''); - }} - /> -
- ); -}; - -export const styleControlSetItem: ControlSetItem = { - name: 'styleTemplate', - config: { - ...sharedControls.entity, - type: StyleControl, - label: t('CSS Styles'), - description: t('CSS applied to the chart'), - isInt: false, - renderTrigger: true, - valueKey: null, - - validators: [], - mapStateToProps: ({ form_data, common }) => ({ - value: form_data?.styleTemplate ?? form_data?.style_template, - htmlSanitization: common?.conf?.HTML_SANITIZATION ?? true, - }), - }, -}; diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/index.ts b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/index.ts deleted file mode 100644 index f8e28f2ad31..00000000000 --- a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/index.ts +++ /dev/null @@ -1,62 +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 { t } from '@apache-superset/core/translation'; -import { ChartMetadata, ChartPlugin } from '@superset-ui/core'; -import thumbnail from '../images/thumbnail.png'; -import thumbnailDark from '../images/thumbnail-dark.png'; -import example1 from '../images/example1.jpg'; -import example1Dark from '../images/example1-dark.jpg'; -import example2 from '../images/example2.jpg'; -import example2Dark from '../images/example2-dark.jpg'; -import buildQuery from './buildQuery'; -import controlPanel from './controlPanel'; -import transformProps from './transformProps'; - -export default class HandlebarsChartPlugin extends ChartPlugin { - /** - * The constructor is used to pass relevant metadata and callbacks that get - * registered in respective registries that are used throughout the library - * and application. A more thorough description of each property is given in - * the respective imported file. - * - * It is worth noting that `buildQuery` and is optional, and only needed for - * advanced visualizations that require either post processing operations - * (pivoting, rolling aggregations, sorting etc) or submitting multiple queries. - */ - constructor() { - const metadata = new ChartMetadata({ - description: t('Write a handlebars template to render the data'), - name: t('Handlebars'), - thumbnail, - thumbnailDark, - exampleGallery: [ - { url: example1, urlDark: example1Dark }, - { url: example2, urlDark: example2Dark }, - ], - }); - - super({ - buildQuery, - controlPanel, - loadChart: () => import('../Handlebars'), - metadata, - transformProps, - }); - } -} diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/transformProps.ts b/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/transformProps.ts deleted file mode 100644 index fe0e5329a7c..00000000000 --- a/superset-frontend/plugins/plugin-chart-handlebars/src/plugin/transformProps.ts +++ /dev/null @@ -1,31 +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 { ChartProps, TimeseriesDataRecord } from '@superset-ui/core'; - -export default function transformProps(chartProps: ChartProps) { - const { width, height, formData, queriesData } = chartProps; - const data = queriesData[0].data as TimeseriesDataRecord[]; - - return { - width, - height, - data, - formData, - }; -} diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/stories/Handlebars.stories.tsx b/superset-frontend/plugins/plugin-chart-handlebars/src/stories/Handlebars.stories.tsx deleted file mode 100644 index bef0bc99441..00000000000 --- a/superset-frontend/plugins/plugin-chart-handlebars/src/stories/Handlebars.stories.tsx +++ /dev/null @@ -1,268 +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 { SuperChart, getChartTransformPropsRegistry } from '@superset-ui/core'; -import { HandlebarsChartPlugin } from '@superset-ui/plugin-chart-handlebars'; -import { kpiData, leaderboardData, timelineData } from './data'; -import { withResizableChartDemo } from '@storybook-shared'; - -const VIZ_TYPE = 'handlebars'; - -new HandlebarsChartPlugin().configure({ key: VIZ_TYPE }).register(); - -getChartTransformPropsRegistry().registerValue( - VIZ_TYPE, - (chartProps: { - width: number; - height: number; - formData: object; - queriesData: { data: unknown[] }[]; - }) => { - const { width, height, formData, queriesData } = chartProps; - const { data } = queriesData[0]; - return { width, height, data, formData }; - }, -); - -// KPI Dashboard template - uses inline styles for Storybook compatibility -const kpiTemplate = ` -
- {{#each data}} -
-
{{icon}}
-
{{metric}}
-
{{formatNumber value}}
-
- {{#if (gt change 0)}}{{else}}{{/if}} - {{change}}% -
-
Target: {{formatNumber target}}
-
- {{/each}} -
-`; - -// Leaderboard template - dark theme with inline styles -const leaderboardTemplate = ` -
-

🏆 Top Performers

- {{#each data}} -
-
{{rank}}
-
{{avatar}}
-
-
{{name}}
-
{{team}}
-
-
-
{{formatNumber score}}
-
- {{#if (eq trend 'up')}}↑{{/if}}{{#if (eq trend 'down')}}↓{{/if}}{{#if (eq trend 'same')}}→{{/if}} -
-
-
- {{/each}} -
-`; - -// Timeline template with inline styles -const timelineTemplate = ` -
-
- {{#each data}} -
-
-
-
{{date}}
-
{{event}}
-
{{description}}
-
-
- {{/each}} -
-`; - -// Simple editable template for the interactive demo -const simpleTemplate = `
-

{{title}}

-
    - {{#each data}} -
  • - {{metric}}: {{formatNumber value}} -
  • - {{/each}} -
-
`; - -// Simple CSS for the interactive demo -const simpleCSS = `.handlebars-container { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; -} - -.handlebars-container h2 { - color: #333; - margin-bottom: 16px; -} - -.handlebars-container ul { - list-style: none; - padding: 0; -} - -.handlebars-container li { - padding: 12px; - margin: 8px 0; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: white; - border-radius: 8px; -}`; - -export default { - title: 'Chart Plugins/plugin-chart-handlebars', - decorators: [withResizableChartDemo], -}; - -export const InteractiveHandlebars = ({ - handlebarsTemplate, - styleTemplate, - width, - height, -}: { - handlebarsTemplate: string; - styleTemplate: string; - width: number; - height: number; -}) => ( - -); - -InteractiveHandlebars.args = { - handlebarsTemplate: simpleTemplate, - styleTemplate: simpleCSS, -}; - -InteractiveHandlebars.argTypes = { - handlebarsTemplate: { - control: { type: 'text' }, - description: 'Handlebars template for rendering data', - }, - styleTemplate: { - control: { type: 'text' }, - description: 'CSS styles to apply to the chart', - }, -}; - -InteractiveHandlebars.parameters = { - initialSize: { - width: 600, - height: 400, - }, -}; - -export const KPIDashboard = ({ - width, - height, -}: { - width: number; - height: number; -}) => ( - -); - -KPIDashboard.parameters = { - initialSize: { - width: 900, - height: 280, - }, -}; - -export const Leaderboard = ({ - width, - height, -}: { - width: number; - height: number; -}) => ( - -); - -Leaderboard.parameters = { - initialSize: { - width: 450, - height: 420, - }, -}; - -export const Timeline = ({ - width, - height, -}: { - width: number; - height: number; -}) => ( - -); - -Timeline.parameters = { - initialSize: { - width: 500, - height: 500, - }, -}; diff --git a/superset-frontend/plugins/plugin-chart-handlebars/src/types.ts b/superset-frontend/plugins/plugin-chart-handlebars/src/types.ts deleted file mode 100644 index ff66f1a133b..00000000000 --- a/superset-frontend/plugins/plugin-chart-handlebars/src/types.ts +++ /dev/null @@ -1,62 +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 { - QueryFormData, - QueryFormMetric, - QueryMode, - TimeGranularity, - TimeseriesDataRecord, -} from '@superset-ui/core'; - -export interface HandlebarsStylesProps { - height: number; - width: number; -} - -interface HandlebarsCustomizeProps { - handlebarsTemplate?: string; - styleTemplate?: string; -} - -export type HandlebarsQueryFormData = QueryFormData & - HandlebarsStylesProps & - HandlebarsCustomizeProps & { - align_pn?: boolean; - color_pn?: boolean; - include_time?: boolean; - include_search?: boolean; - query_mode?: QueryMode; - page_length?: string | number | null; // null means auto-paginate - metrics?: QueryFormMetric[] | null; - percent_metrics?: QueryFormMetric[] | null; - timeseries_limit_metric?: QueryFormMetric[] | QueryFormMetric | null; - groupby?: QueryFormMetric[] | null; - all_columns?: QueryFormMetric[] | null; - order_desc?: boolean; - table_timestamp_format?: string; - granularitySqla?: string; - time_grain_sqla?: TimeGranularity; - }; - -export type HandlebarsProps = HandlebarsStylesProps & - HandlebarsCustomizeProps & { - data: TimeseriesDataRecord[]; - // add typing here for the props you pass in from transformProps.ts! - formData: HandlebarsQueryFormData; - }; diff --git a/superset-frontend/plugins/plugin-chart-handlebars/test/index.test.ts b/superset-frontend/plugins/plugin-chart-handlebars/test/index.test.ts deleted file mode 100644 index 16df5aecd58..00000000000 --- a/superset-frontend/plugins/plugin-chart-handlebars/test/index.test.ts +++ /dev/null @@ -1,33 +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 { HandlebarsChartPlugin } from '../src'; - -/** - * The example tests in this file act as a starting point, and - * we encourage you to build more. These tests check that the - * plugin loads properly, and focus on `transformProps` - * to ake sure that data, controls, and props are all - * treated correctly (e.g. formData from plugin controls - * properly transform the data and/or any resulting props). - */ -describe('@superset-ui/plugin-chart-handlebars', () => { - test('exists', () => { - expect(HandlebarsChartPlugin).toBeDefined(); - }); -}); diff --git a/superset-frontend/plugins/plugin-chart-handlebars/test/plugin/buildQuery.test.ts b/superset-frontend/plugins/plugin-chart-handlebars/test/plugin/buildQuery.test.ts index d9bc12497c4..b79e1c7a8cc 100644 --- a/superset-frontend/plugins/plugin-chart-handlebars/test/plugin/buildQuery.test.ts +++ b/superset-frontend/plugins/plugin-chart-handlebars/test/plugin/buildQuery.test.ts @@ -16,8 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { HandlebarsQueryFormData } from '../../src/types'; -import buildQuery from '../../src/plugin/buildQuery'; +import { HandlebarsQueryFormData, buildQuery } from '../../src/index'; describe('Handlebars buildQuery', () => { const formData: HandlebarsQueryFormData = { diff --git a/superset-frontend/plugins/plugin-chart-handlebars/test/plugin/controls.test.ts b/superset-frontend/plugins/plugin-chart-handlebars/test/plugin/controls.test.ts deleted file mode 100644 index ae92c1a3f3f..00000000000 --- a/superset-frontend/plugins/plugin-chart-handlebars/test/plugin/controls.test.ts +++ /dev/null @@ -1,116 +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 { - ControlPanelState, - ControlState, - CustomControlItem, -} from '@superset-ui/chart-controls'; -import { QueryFormData } from '@superset-ui/core'; -import { handlebarsTemplateControlSetItem } from '../../src/plugin/controls/handlebarTemplate'; -import { styleControlSetItem } from '../../src/plugin/controls/style'; - -const handlebarsConfig = (handlebarsTemplateControlSetItem as CustomControlItem) - .config; -const styleConfig = (styleControlSetItem as CustomControlItem).config; - -const buildState = (form_data: Partial) => - ({ - form_data: form_data as QueryFormData, - controls: {}, - datasource: null, - common: { conf: { HTML_SANITIZATION: true } }, - slice: { slice_id: 1 }, - }) as unknown as ControlPanelState; - -const CUSTOM = '
custom template
'; -const CUSTOM_CSS = '.foo { color: red; }'; - -test('handlebarsTemplate mapStateToProps reads snake_case handlebars_template (MCP-created charts)', () => { - const result = handlebarsConfig.mapStateToProps!( - buildState({ handlebars_template: CUSTOM } as Partial), - {} as ControlState, - ); - expect(result.value).toBe(CUSTOM); -}); - -test('handlebarsTemplate mapStateToProps reads camelCase handlebarsTemplate (UI-created charts)', () => { - const result = handlebarsConfig.mapStateToProps!( - buildState({ handlebarsTemplate: CUSTOM } as Partial), - {} as ControlState, - ); - expect(result.value).toBe(CUSTOM); -}); - -test('handlebarsTemplate mapStateToProps prefers camelCase when both keys present (latest edit wins over legacy snake_case)', () => { - const result = handlebarsConfig.mapStateToProps!( - buildState({ - handlebars_template: 'stale legacy value', - handlebarsTemplate: 'latest edit', - } as Partial), - {} as ControlState, - ); - expect(result.value).toBe('latest edit'); -}); - -test('handlebarsTemplate mapStateToProps returns undefined when no template stored (allows default)', () => { - const result = handlebarsConfig.mapStateToProps!( - buildState({}), - {} as ControlState, - ); - expect(result.value).toBeUndefined(); -}); - -test('styleTemplate mapStateToProps reads camelCase styleTemplate (MCP and UI charts)', () => { - const result = styleConfig.mapStateToProps!( - buildState({ styleTemplate: CUSTOM_CSS } as Partial), - {} as ControlState, - ); - expect(result.value).toBe(CUSTOM_CSS); - expect(result.htmlSanitization).toBe(true); -}); - -test('styleTemplate mapStateToProps prefers camelCase when both keys present', () => { - const result = styleConfig.mapStateToProps!( - buildState({ - style_template: 'stale', - styleTemplate: 'latest', - } as Partial), - {} as ControlState, - ); - expect(result.value).toBe('latest'); -}); - -test('styleTemplate mapStateToProps reads snake_case style_template as fallback', () => { - const result = styleConfig.mapStateToProps!( - buildState({ style_template: CUSTOM_CSS } as Partial), - {} as ControlState, - ); - expect(result.value).toBe(CUSTOM_CSS); -}); - -test('styleTemplate mapStateToProps uses HTML_SANITIZATION=false from config', () => { - const result = styleConfig.mapStateToProps!( - { - ...buildState({}), - common: { conf: { HTML_SANITIZATION: false } }, - } as unknown as ControlPanelState, - {} as ControlState, - ); - expect(result.htmlSanitization).toBe(false); -}); diff --git a/superset-frontend/plugins/plugin-chart-handlebars/test/plugin/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-handlebars/test/plugin/transformProps.test.ts deleted file mode 100644 index c47308e8f87..00000000000 --- a/superset-frontend/plugins/plugin-chart-handlebars/test/plugin/transformProps.test.ts +++ /dev/null @@ -1,53 +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 { ChartProps, QueryFormData, VizType } from '@superset-ui/core'; -import { supersetTheme } from '@apache-superset/core/theme'; -import { HandlebarsQueryFormData } from '../../src/types'; -import transformProps from '../../src/plugin/transformProps'; - -describe('Handlebars transformProps', () => { - const formData: HandlebarsQueryFormData = { - colorScheme: 'bnbColors', - datasource: '3__table', - granularitySqla: 'ds', - metric: 'sum__num', - groupby: ['name'], - width: 500, - height: 500, - viz_type: VizType.Handlebars, - }; - const data = [{ name: 'Hulk', sum__num: 1, __timestamp: 599616000000 }]; - const chartProps = new ChartProps({ - formData, - width: 800, - height: 600, - queriesData: [{ data }], - theme: supersetTheme, - }); - - test('should transform chart props for viz', () => { - expect(transformProps(chartProps)).toEqual( - expect.objectContaining({ - width: 800, - height: 600, - data, - }), - ); - }); -}); diff --git a/superset-frontend/plugins/plugin-chart-handlebars/tsconfig.json b/superset-frontend/plugins/plugin-chart-handlebars/tsconfig.json index 03190ec5bd4..f0bfbba676d 100644 --- a/superset-frontend/plugins/plugin-chart-handlebars/tsconfig.json +++ b/superset-frontend/plugins/plugin-chart-handlebars/tsconfig.json @@ -20,6 +20,7 @@ "references": [ { "path": "../../packages/superset-core" }, { "path": "../../packages/superset-ui-core" }, - { "path": "../../packages/superset-ui-chart-controls" } + { "path": "../../packages/superset-ui-chart-controls" }, + { "path": "../../packages/superset-ui-glyph-core" } ] } diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/buildQuery.test.ts b/superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/buildQuery.test.ts deleted file mode 100644 index 3bf887c0b60..00000000000 --- a/superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/buildQuery.test.ts +++ /dev/null @@ -1,134 +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 { TimeGranularity } from '@superset-ui/core'; -import buildQuery from '../../src/plugin/buildQuery'; -import { PivotTableQueryFormData } from '../../src/types'; - -const formData: PivotTableQueryFormData = { - groupbyRows: ['row1', 'row2'], - groupbyColumns: ['col1', 'col2'], - metrics: ['metric1', 'metric2'], - tableRenderer: 'Table With Subtotal', - colOrder: 'key_a_to_z', - rowOrder: 'key_a_to_z', - aggregateFunction: 'Sum', - transposePivot: true, - rowSubtotalPosition: true, - colSubtotalPosition: true, - colTotals: true, - colSubTotals: true, - rowTotals: true, - rowSubTotals: true, - valueFormat: 'SMART_NUMBER', - datasource: '5__table', - viz_type: 'my_chart', - width: 800, - height: 600, - combineMetric: false, - verboseMap: {}, - columnFormats: {}, - currencyFormats: {}, - metricColorFormatters: [], - dateFormatters: {}, - setDataMask: () => {}, - legacy_order_by: 'count', - order_desc: true, - margin: 0, - time_grain_sqla: TimeGranularity.MONTH, - temporal_columns_lookup: { col1: true }, - currencyFormat: { symbol: 'USD', symbolPosition: 'prefix' }, -}; - -test('should build groupby with series in form data', () => { - const queryContext = buildQuery(formData); - const [query] = queryContext.queries; - expect(query.columns).toEqual([ - { - columnType: 'BASE_AXIS', - expressionType: 'SQL', - label: 'col1', - sqlExpression: 'col1', - timeGrain: 'P1M', - }, - 'col2', - 'row1', - 'row2', - ]); -}); - -test('should work with old charts', () => { - const modifiedFormData = { - ...formData, - time_grain_sqla: TimeGranularity.MONTH, - granularity_sqla: 'col1', - }; - const queryContext = buildQuery(modifiedFormData); - const [query] = queryContext.queries; - expect(query.columns).toEqual([ - { - timeGrain: 'P1M', - columnType: 'BASE_AXIS', - sqlExpression: 'col1', - label: 'col1', - expressionType: 'SQL', - }, - 'col2', - 'row1', - 'row2', - ]); -}); - -test('should prefer extra_form_data.time_grain_sqla over formData.time_grain_sqla', () => { - const modifiedFormData = { - ...formData, - extra_form_data: { time_grain_sqla: TimeGranularity.QUARTER }, - }; - const queryContext = buildQuery(modifiedFormData); - const [query] = queryContext.queries; - expect(query.columns?.[0]).toEqual({ - timeGrain: TimeGranularity.QUARTER, - columnType: 'BASE_AXIS', - sqlExpression: 'col1', - label: 'col1', - expressionType: 'SQL', - }); -}); - -test('should fallback to formData.time_grain_sqla if extra_form_data.time_grain_sqla is not set', () => { - const queryContext = buildQuery(formData); - const [query] = queryContext.queries; - expect(query.columns?.[0]).toEqual({ - timeGrain: formData.time_grain_sqla, - columnType: 'BASE_AXIS', - sqlExpression: 'col1', - label: 'col1', - expressionType: 'SQL', - }); -}); - -test('should not omit extras.time_grain_sqla from queryContext so dashboards apply them', () => { - const modifiedFormData = { - ...formData, - extra_form_data: { time_grain_sqla: TimeGranularity.QUARTER }, - }; - const queryContext = buildQuery(modifiedFormData); - const [query] = queryContext.queries; - expect(query.extras?.time_grain_sqla).toEqual(TimeGranularity.QUARTER); -}); diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/transformProps.test.ts deleted file mode 100644 index a900e43fb0e..00000000000 --- a/superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/transformProps.test.ts +++ /dev/null @@ -1,337 +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 { ChartProps, QueryFormData } from '@superset-ui/core'; -import { supersetTheme } from '@apache-superset/core/theme'; -import transformProps from '../../src/plugin/transformProps'; -import { MetricsLayoutEnum } from '../../src/types'; - -const setDataMask = jest.fn(); -const formData = { - groupbyRows: ['row1', 'row2'], - groupbyColumns: ['col1', 'col2'], - metrics: ['metric1', 'metric2'], - tableRenderer: 'Table With Subtotal', - colOrder: 'key_a_to_z', - rowOrder: 'key_a_to_z', - aggregateFunction: 'Sum', - transposePivot: true, - combineMetric: true, - rowSubtotalPosition: true, - colSubtotalPosition: true, - colTotals: true, - rowTotals: true, - valueFormat: 'SMART_NUMBER', - metricsLayout: MetricsLayoutEnum.COLUMNS, - viz_type: '', - datasource: '', - conditionalFormatting: [], - dateFormat: '', - legacy_order_by: 'count', - order_desc: true, - currencyFormat: { symbol: 'USD', symbolPosition: 'prefix' }, -}; -const chartProps = new ChartProps({ - formData, - width: 800, - height: 600, - queriesData: [ - { - data: [{ name: 'Hulk', sum__num: 1, __timestamp: 599616000000 }], - colnames: ['name', 'sum__num', '__timestamp'], - coltypes: [1, 0, 2], - }, - ], - hooks: { setDataMask }, - filterState: { selectedFilters: {} }, - datasource: { verboseMap: {}, columnFormats: {} }, - theme: supersetTheme, -}); - -test('should transform chart props for viz', () => { - expect(transformProps(chartProps)).toEqual({ - width: 800, - height: 600, - groupbyRows: ['row1', 'row2'], - groupbyColumns: ['col1', 'col2'], - metrics: ['metric1', 'metric2'], - tableRenderer: 'Table With Subtotal', - colOrder: 'key_a_to_z', - rowOrder: 'key_a_to_z', - aggregateFunction: 'Sum', - transposePivot: true, - combineMetric: true, - rowSubtotalPosition: true, - colSubtotalPosition: true, - colTotals: true, - rowTotals: true, - valueFormat: 'SMART_NUMBER', - data: [{ name: 'Hulk', sum__num: 1, __timestamp: 599616000000 }], - setDataMask, - selectedFilters: {}, - verboseMap: {}, - metricsLayout: MetricsLayoutEnum.COLUMNS, - metricColorFormatters: [], - dateFormatters: {}, - emitCrossFilters: false, - columnFormats: {}, - currencyFormats: {}, - currencyFormat: { symbol: 'USD', symbolPosition: 'prefix' }, - }); -}); - -test('should pass AUTO mode through for per-cell detection (single currency data)', () => { - const autoFormData = { - ...formData, - currencyFormat: { symbol: 'AUTO', symbolPosition: 'prefix' }, - }; - const autoChartProps = new ChartProps({ - formData: autoFormData, - width: 800, - height: 600, - queriesData: [ - { - data: [ - { country: 'USA', currency: 'USD', revenue: 100 }, - { country: 'Canada', currency: 'USD', revenue: 200 }, - { country: 'Mexico', currency: 'usd', revenue: 150 }, - ], - colnames: ['country', 'currency', 'revenue'], - coltypes: [1, 1, 0], - }, - ], - hooks: { setDataMask }, - filterState: { selectedFilters: {} }, - datasource: { - verboseMap: {}, - columnFormats: {}, - currencyCodeColumn: 'currency', - }, - theme: supersetTheme, - }); - - const result = transformProps(autoChartProps); - // AUTO mode should be preserved for per-cell detection in PivotTableChart - expect(result.currencyFormat).toEqual({ - symbol: 'AUTO', - symbolPosition: 'prefix', - }); - // currencyCodeColumn should be passed through for per-cell detection - expect(result.currencyCodeColumn).toBe('currency'); -}); - -test('should pass AUTO mode through for per-cell detection (mixed currency data)', () => { - const autoFormData = { - ...formData, - currencyFormat: { symbol: 'AUTO', symbolPosition: 'prefix' }, - }; - const autoChartProps = new ChartProps({ - formData: autoFormData, - width: 800, - height: 600, - queriesData: [ - { - data: [ - { country: 'USA', currency: 'USD', revenue: 100 }, - { country: 'UK', currency: 'GBP', revenue: 200 }, - { country: 'France', currency: 'EUR', revenue: 150 }, - ], - colnames: ['country', 'currency', 'revenue'], - coltypes: [1, 1, 0], - }, - ], - hooks: { setDataMask }, - filterState: { selectedFilters: {} }, - datasource: { - verboseMap: {}, - columnFormats: {}, - currencyCodeColumn: 'currency', - }, - theme: supersetTheme, - }); - - const result = transformProps(autoChartProps); - // AUTO mode should be preserved - per-cell detection happens in PivotTableChart - expect(result.currencyFormat).toEqual({ - symbol: 'AUTO', - symbolPosition: 'prefix', - }); - expect(result.currencyCodeColumn).toBe('currency'); -}); - -test('should pass AUTO mode through when no currency column is defined', () => { - const autoFormData = { - ...formData, - currencyFormat: { symbol: 'AUTO', symbolPosition: 'prefix' }, - }; - const autoChartProps = new ChartProps({ - formData: autoFormData, - width: 800, - height: 600, - queriesData: [ - { - data: [ - { country: 'USA', revenue: 100 }, - { country: 'UK', revenue: 200 }, - ], - colnames: ['country', 'revenue'], - coltypes: [1, 0], - }, - ], - hooks: { setDataMask }, - filterState: { selectedFilters: {} }, - datasource: { - verboseMap: {}, - columnFormats: {}, - // No currencyCodeColumn defined - }, - theme: supersetTheme, - }); - - const result = transformProps(autoChartProps); - expect(result.currencyFormat).toEqual({ - symbol: 'AUTO', - symbolPosition: 'prefix', - }); - // currencyCodeColumn should be undefined when not configured - expect(result.currencyCodeColumn).toBeUndefined(); -}); - -test('should handle empty data gracefully in AUTO mode', () => { - const autoFormData = { - ...formData, - currencyFormat: { symbol: 'AUTO', symbolPosition: 'prefix' }, - }; - const autoChartProps = new ChartProps({ - formData: autoFormData, - width: 800, - height: 600, - queriesData: [ - { - data: [], - colnames: ['country', 'currency', 'revenue'], - coltypes: [1, 1, 0], - }, - ], - hooks: { setDataMask }, - filterState: { selectedFilters: {} }, - datasource: { - verboseMap: {}, - columnFormats: {}, - currencyCodeColumn: 'currency', - }, - theme: supersetTheme, - }); - - const result = transformProps(autoChartProps); - expect(result.currencyFormat).toEqual({ - symbol: 'AUTO', - symbolPosition: 'prefix', - }); - expect(result.currencyCodeColumn).toBe('currency'); -}); - -test('should preserve static currency format when not using AUTO mode', () => { - const staticFormData = { - ...formData, - currencyFormat: { symbol: 'EUR', symbolPosition: 'suffix' }, - }; - const staticChartProps = new ChartProps({ - formData: staticFormData, - width: 800, - height: 600, - queriesData: [ - { - data: [ - { country: 'USA', currency: 'USD', revenue: 100 }, - { country: 'UK', currency: 'GBP', revenue: 200 }, - ], - colnames: ['country', 'currency', 'revenue'], - coltypes: [1, 1, 0], - }, - ], - hooks: { setDataMask }, - filterState: { selectedFilters: {} }, - datasource: { - verboseMap: {}, - columnFormats: {}, - currencyCodeColumn: 'currency', - }, - theme: supersetTheme, - }); - - const result = transformProps(staticChartProps); - expect(result.currencyFormat).toEqual({ - symbol: 'EUR', - symbolPosition: 'suffix', - }); -}); - -test('should map conditional formatting rules to metricColorFormatters with correct colors', () => { - const formattingFormData = { - ...formData, - conditionalFormatting: [ - { - colorScheme: '#ACE1C4', - column: 'country', - operator: '=', - targetValue: 'country', - }, - { - colorScheme: '#5ac189', - column: 'revenue', - operator: '=', - targetValue: 'revenue', - }, - ], - }; - const formattingChartProps = new ChartProps({ - formData: formattingFormData, - width: 800, - height: 600, - queriesData: [ - { - data: [ - { country: 'USA', currency: 'USD', revenue: 100 }, - { country: 'UK', currency: 'GBP', revenue: 200 }, - ], - colnames: ['country', 'currency', 'revenue'], - coltypes: [1, 1, 0], - }, - ], - hooks: { setDataMask }, - filterState: { selectedFilters: {} }, - datasource: { - verboseMap: {}, - columnFormats: {}, - currencyCodeColumn: 'currency', - }, - theme: supersetTheme, - }); - - const result = transformProps(formattingChartProps); - const column1Formatting = result.metricColorFormatters[0].column; - const column2Formatting = result.metricColorFormatters[1].column; - expect( - result.metricColorFormatters[0].getColorFromValue(column1Formatting), - ).toEqual('#ACE1C4FF'); - expect( - result.metricColorFormatters[1].getColorFromValue(column2Formatting), - ).toEqual('#5ac189FF'); -}); diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/tsconfig.json b/superset-frontend/plugins/plugin-chart-pivot-table/tsconfig.json index e73ca5d4f87..02f450b2a29 100644 --- a/superset-frontend/plugins/plugin-chart-pivot-table/tsconfig.json +++ b/superset-frontend/plugins/plugin-chart-pivot-table/tsconfig.json @@ -16,6 +16,7 @@ "references": [ { "path": "../../packages/superset-core" }, { "path": "../../packages/superset-ui-core" }, - { "path": "../../packages/superset-ui-chart-controls" } + { "path": "../../packages/superset-ui-chart-controls" }, + { "path": "../../packages/superset-ui-glyph-core" } ] } diff --git a/superset-frontend/plugins/plugin-chart-point-cluster-map/test/controlPanel.test.ts b/superset-frontend/plugins/plugin-chart-point-cluster-map/test/controlPanel.test.ts deleted file mode 100644 index 2a72db46366..00000000000 --- a/superset-frontend/plugins/plugin-chart-point-cluster-map/test/controlPanel.test.ts +++ /dev/null @@ -1,81 +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 type { - ControlPanelConfig, - CustomControlItem, -} from '@superset-ui/chart-controls'; -import controlPanel from '../src/controlPanel'; - -type ControlConfig = Required; - -function isCustomControlItem( - controlItem: unknown, -): controlItem is CustomControlItem & { config: ControlConfig } { - return ( - typeof controlItem === 'object' && - controlItem !== null && - 'name' in controlItem && - 'config' in controlItem - ); -} - -function getControl( - panel: ControlPanelConfig, - controlName: string, -): CustomControlItem & { config: ControlConfig } { - const item = (panel.controlPanelSections || []) - .flatMap(section => section?.controlSetRows || []) - .flat() - .find( - controlItem => - isCustomControlItem(controlItem) && controlItem.name === controlName, - ); - - if (!isCustomControlItem(item)) { - throw new Error(`Control "${controlName}" not found`); - } - - return item; -} - -test('viewport controls default to empty values and rerender without query refresh', () => { - const longitudeControl = getControl(controlPanel, 'viewport_longitude'); - const latitudeControl = getControl(controlPanel, 'viewport_latitude'); - const zoomControl = getControl(controlPanel, 'viewport_zoom'); - - expect(longitudeControl.config.default).toBe(''); - expect(latitudeControl.config.default).toBe(''); - expect(zoomControl.config.default).toBe(''); - - expect(longitudeControl.config.renderTrigger).toBe(true); - expect(latitudeControl.config.renderTrigger).toBe(true); - expect(zoomControl.config.renderTrigger).toBe(true); - - expect(longitudeControl.config.dontRefreshOnChange).toBe(true); - expect(latitudeControl.config.dontRefreshOnChange).toBe(true); - expect(zoomControl.config.dontRefreshOnChange).toBe(true); -}); - -test('opacity control rerenders immediately when changed', () => { - const opacityControl = getControl(controlPanel, 'global_opacity'); - - expect(opacityControl.config.default).toBe(1); - expect(opacityControl.config.renderTrigger).toBe(true); - expect(opacityControl.config.isFloat).toBe(true); -}); diff --git a/superset-frontend/plugins/plugin-chart-point-cluster-map/test/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-point-cluster-map/test/transformProps.test.ts deleted file mode 100644 index dc5a460c5f9..00000000000 --- a/superset-frontend/plugins/plugin-chart-point-cluster-map/test/transformProps.test.ts +++ /dev/null @@ -1,232 +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 { ChartProps } from '@superset-ui/core'; -import { supersetTheme } from '@apache-superset/core/theme'; - -jest.mock('supercluster', () => { - const MockSupercluster = jest.fn().mockImplementation(() => ({ - load: jest.fn(), - getClusters: jest.fn().mockReturnValue([]), - })); - return { __esModule: true, default: MockSupercluster }; -}); - -// Import after mocking supercluster to avoid ESM parse error -// eslint-disable-next-line import/first -import transformProps from '../src/transformProps'; - -type TransformPropsResult = { - globalOpacity?: number; - onViewportChange?: (viewport: { - latitude: number; - longitude: number; - zoom: number; - }) => void; - viewportLongitude?: number; - viewportLatitude?: number; - viewportZoom?: number; - rgb?: string[] | null; -}; - -const baseFormData = { - all_columns_x: 'lon', - all_columns_y: 'lat', - clustering_radius: 60, - global_opacity: 0.8, - map_color: 'rgb(0, 139, 139)', - map_renderer: 'maplibre', - maplibre_style: 'https://tiles.openfreemap.org/styles/liberty', - pandas_aggfunc: 'sum', - point_radius_unit: 'Pixels', - render_while_dragging: true, - viewport_longitude: -73.935242, - viewport_latitude: 40.73061, - viewport_zoom: 9, -}; - -const baseQueriesData = [ - { - data: { - bounds: [ - [-74.0, 40.7], - [-73.9, 40.8], - ] as [[number, number], [number, number]], - geoJSON: { features: [] }, - hasCustomMetric: false, - }, - }, -]; - -function createChartProps(overrides: Record = {}) { - return new ChartProps({ - formData: { ...baseFormData, ...overrides }, - width: 800, - height: 600, - queriesData: baseQueriesData, - theme: supersetTheme, - }); -} - -function getTransformPropsResult( - overrides: Record = {}, -): TransformPropsResult { - return transformProps(createChartProps(overrides)) as TransformPropsResult; -} - -test('extracts globalOpacity from formData', () => { - const result = getTransformPropsResult({ global_opacity: 0.5 }); - expect(result.globalOpacity).toBe(0.5); -}); - -test('extracts viewport values from formData', () => { - const result = getTransformPropsResult({ - viewport_longitude: -122.4, - viewport_latitude: 37.8, - viewport_zoom: 12, - }); - expect(result).toEqual( - expect.objectContaining({ - viewportLongitude: -122.4, - viewportLatitude: 37.8, - viewportZoom: 12, - }), - ); -}); - -test('clamps viewport values to safe map ranges', () => { - const result = getTransformPropsResult({ - viewport_longitude: 190, - viewport_latitude: -100, - viewport_zoom: 99, - }); - expect(result).toEqual( - expect.objectContaining({ - viewportLongitude: 180, - viewportLatitude: -90, - viewportZoom: 16, - }), - ); -}); - -test('provides onViewportChange callback that updates control values', () => { - const setControlValue = jest.fn(); - const chartProps = new ChartProps({ - formData: baseFormData, - width: 800, - height: 600, - queriesData: baseQueriesData, - hooks: { setControlValue }, - theme: supersetTheme, - }); - const result = transformProps(chartProps) as TransformPropsResult; - expect(result.onViewportChange).toBeDefined(); - - result.onViewportChange!({ - latitude: 51.5, - longitude: -0.12, - zoom: 10, - }); - - expect(setControlValue).toHaveBeenCalledWith('viewport_longitude', -0.12); - expect(setControlValue).toHaveBeenCalledWith('viewport_latitude', 51.5); - expect(setControlValue).toHaveBeenCalledWith('viewport_zoom', 10); -}); - -test('normalizes string viewport values to numbers', () => { - const result = getTransformPropsResult({ - viewport_longitude: '-122.4', - viewport_latitude: '37.8', - viewport_zoom: '12', - }); - expect(result.viewportLongitude).toBe(-122.4); - expect(result.viewportLatitude).toBe(37.8); - expect(result.viewportZoom).toBe(12); -}); - -test('normalizes empty viewport values to undefined', () => { - const result = getTransformPropsResult({ - viewport_longitude: '', - viewport_latitude: '', - viewport_zoom: '', - }); - expect(result.viewportLongitude).toBeUndefined(); - expect(result.viewportLatitude).toBeUndefined(); - expect(result.viewportZoom).toBeUndefined(); -}); - -test('normalizes whitespace-only viewport values to undefined', () => { - const result = getTransformPropsResult({ - viewport_longitude: ' ', - viewport_latitude: '\t', - viewport_zoom: ' \n ', - }); - expect(result.viewportLongitude).toBeUndefined(); - expect(result.viewportLatitude).toBeUndefined(); - expect(result.viewportZoom).toBeUndefined(); -}); - -test('normalizes string opacity to number', () => { - const result = getTransformPropsResult({ global_opacity: '0.5' }); - expect(result.globalOpacity).toBe(0.5); -}); - -test('defaults empty opacity to 1', () => { - const result = getTransformPropsResult({ global_opacity: '' }); - expect(result.globalOpacity).toBe(1); -}); - -test('defaults whitespace-only opacity to 1', () => { - const result = getTransformPropsResult({ global_opacity: ' ' }); - expect(result.globalOpacity).toBe(1); -}); - -test('clamps opacity to [0, 1] range', () => { - expect(getTransformPropsResult({ global_opacity: 5 }).globalOpacity).toBe(1); - expect(getTransformPropsResult({ global_opacity: -1 }).globalOpacity).toBe(0); -}); - -test('passes through numeric values unchanged', () => { - const result = getTransformPropsResult({ - viewport_longitude: -122.4, - viewport_latitude: 37.8, - viewport_zoom: 12, - global_opacity: 0.8, - }); - expect(result.viewportLongitude).toBe(-122.4); - expect(result.viewportLatitude).toBe(37.8); - expect(result.viewportZoom).toBe(12); - expect(result.globalOpacity).toBe(0.8); -}); - -test('calls onError and falls back to black for invalid color', () => { - const onError = jest.fn(); - const chartProps = new ChartProps({ - formData: { ...baseFormData, map_color: 'invalid-color' }, - width: 800, - height: 600, - queriesData: baseQueriesData, - hooks: { onError }, - theme: supersetTheme, - }); - const result = transformProps(chartProps) as TransformPropsResult; - expect(onError).toHaveBeenCalled(); - // Falls back to black instead of returning empty object - expect(result.rgb).toEqual(['', '0', '0', '0']); -}); diff --git a/superset-frontend/plugins/plugin-chart-point-cluster-map/tsconfig.json b/superset-frontend/plugins/plugin-chart-point-cluster-map/tsconfig.json index 1abdcd42c58..9a2be170c1b 100644 --- a/superset-frontend/plugins/plugin-chart-point-cluster-map/tsconfig.json +++ b/superset-frontend/plugins/plugin-chart-point-cluster-map/tsconfig.json @@ -11,6 +11,7 @@ "references": [ { "path": "../../packages/superset-core" }, { "path": "../../packages/superset-ui-core" }, - { "path": "../../packages/superset-ui-chart-controls" } + { "path": "../../packages/superset-ui-chart-controls" }, + { "path": "../../packages/superset-ui-glyph-core" } ] } diff --git a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx index 1b9c77c33ee..6b8556ad2b7 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx +++ b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx @@ -1304,7 +1304,7 @@ export default function TableChart( col.toggleSortBy(); } }} - role="columnheader button" + onClick={onClick} data-column-name={col.id} {...(allowRearrangeColumns && { diff --git a/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx b/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx deleted file mode 100644 index 120ee273b6e..00000000000 --- a/superset-frontend/plugins/plugin-chart-table/test/TableChart.test.tsx +++ /dev/null @@ -1,2435 +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 '@testing-library/jest-dom'; -import { - getTextColorForBackground, - ObjectFormattingEnum, -} from '@superset-ui/chart-controls'; -import { supersetTheme } from '@apache-superset/core/theme'; -import { - render, - screen, - fireEvent, - waitFor, - within, -} from '@superset-ui/core/spec'; -import { cloneDeep } from 'lodash'; -import { - QueryMode, - TimeGranularity, - SMART_DATE_ID, - getTimeFormatterForGranularity, -} from '@superset-ui/core'; -import { CellProps, Column, HeaderProps } from 'react-table'; -import DataTable from '../src/DataTable/DataTable'; -import TableChart, { sanitizeHeaderId } from '../src/TableChart'; -import { GenericDataType } from '@apache-superset/core/common'; -import transformProps from '../src/transformProps'; -import DateWithFormatter from '../src/utils/DateWithFormatter'; -import testData from './testData'; -import { ProviderWrapper } from './testHelpers'; - -const expectValidAriaLabels = (container: HTMLElement) => { - const allCells = container.querySelectorAll('tbody td'); - const cellsWithLabels = container.querySelectorAll( - 'tbody td[aria-labelledby]', - ); - - // Table must render data cells (catch empty table regression) - expect(allCells.length).toBeGreaterThan(0); - - // ALL data cells must have aria-labelledby (no unlabeled cells) - expect(cellsWithLabels.length).toBe(allCells.length); - - // ALL aria-labelledby values should be valid - cellsWithLabels.forEach(cell => { - const labelledBy = cell.getAttribute('aria-labelledby'); - expect(labelledBy).not.toBeNull(); - expect(labelledBy).toEqual(expect.stringMatching(/\S/)); - const labelledByValue = labelledBy as string; - expect(labelledByValue).not.toMatch(/\s/); - expect(labelledByValue).not.toMatch(/[%#△]/); - const referencedHeader = container.querySelector( - `#${CSS.escape(labelledByValue)}`, - ); - expect(referencedHeader).toBeTruthy(); - }); -}; - -test('sanitizeHeaderId should sanitize percent sign', () => { - expect(sanitizeHeaderId('%pct_nice')).toBe('percentpct_nice'); -}); - -test('sanitizeHeaderId should sanitize hash/pound sign', () => { - expect(sanitizeHeaderId('# metric_1')).toBe('hash_metric_1'); -}); - -test('sanitizeHeaderId should sanitize delta symbol', () => { - expect(sanitizeHeaderId('△ delta')).toBe('delta_delta'); -}); - -test('sanitizeHeaderId should replace spaces with underscores', () => { - expect(sanitizeHeaderId('Main metric_1')).toBe('Main_metric_1'); - expect(sanitizeHeaderId('multiple spaces')).toBe('multiple_spaces'); -}); - -test('sanitizeHeaderId should handle multiple special characters', () => { - expect(sanitizeHeaderId('% #△ test')).toBe('percent_hashdelta_test'); - expect(sanitizeHeaderId('% # △ test')).toBe('percent_hash_delta_test'); -}); - -test('sanitizeHeaderId should preserve alphanumeric, underscore, and hyphen', () => { - expect(sanitizeHeaderId('valid-name_123')).toBe('valid-name_123'); -}); - -test('sanitizeHeaderId should replace other special characters with underscore', () => { - expect(sanitizeHeaderId('col@name!test')).toBe('col_name_test'); - expect(sanitizeHeaderId('test.column')).toBe('test_column'); -}); - -test('sanitizeHeaderId should handle edge cases', () => { - expect(sanitizeHeaderId('')).toBe(''); - expect(sanitizeHeaderId('simple')).toBe('simple'); -}); - -test('sanitizeHeaderId should collapse consecutive underscores', () => { - expect(sanitizeHeaderId('test @@ space')).toBe('test_space'); - expect(sanitizeHeaderId('col___name')).toBe('col_name'); - expect(sanitizeHeaderId('a b c')).toBe('a_b_c'); - expect(sanitizeHeaderId('test@@name')).toBe('test_name'); -}); - -test('sanitizeHeaderId should remove leading underscores', () => { - expect(sanitizeHeaderId('@col')).toBe('col'); - expect(sanitizeHeaderId('!revenue')).toBe('revenue'); - expect(sanitizeHeaderId('@@test')).toBe('test'); - expect(sanitizeHeaderId(' leading_spaces')).toBe('leading_spaces'); -}); - -test('sanitizeHeaderId should remove trailing underscores', () => { - expect(sanitizeHeaderId('col@')).toBe('col'); - expect(sanitizeHeaderId('revenue!')).toBe('revenue'); - expect(sanitizeHeaderId('test@@')).toBe('test'); - expect(sanitizeHeaderId('trailing_spaces ')).toBe('trailing_spaces'); -}); - -test('sanitizeHeaderId should remove leading and trailing underscores', () => { - expect(sanitizeHeaderId('@col@')).toBe('col'); - expect(sanitizeHeaderId('!test!')).toBe('test'); - expect(sanitizeHeaderId(' spaced ')).toBe('spaced'); - expect(sanitizeHeaderId('@@multiple@@')).toBe('multiple'); -}); - -test('sanitizeHeaderId should handle inputs with only special characters', () => { - expect(sanitizeHeaderId('@')).toBe(''); - expect(sanitizeHeaderId('@@')).toBe(''); - expect(sanitizeHeaderId(' ')).toBe(''); - expect(sanitizeHeaderId('!@$')).toBe(''); - expect(sanitizeHeaderId('!@#$')).toBe('hash'); // # is replaced with 'hash' (semantic replacement) - // Semantic replacements produce readable output even when alone - expect(sanitizeHeaderId('%')).toBe('percent'); - expect(sanitizeHeaderId('#')).toBe('hash'); - expect(sanitizeHeaderId('△')).toBe('delta'); - expect(sanitizeHeaderId('% # △')).toBe('percent_hash_delta'); -}); - -describe('plugin-chart-table', () => { - describe('transformProps', () => { - test('should parse pageLength to pageSize', () => { - expect(transformProps(testData.basic).pageSize).toBe(20); - expect( - transformProps({ - ...testData.basic, - rawFormData: { ...testData.basic.rawFormData, page_length: '20' }, - }).pageSize, - ).toBe(20); - expect( - transformProps({ - ...testData.basic, - rawFormData: { ...testData.basic.rawFormData, page_length: '' }, - }).pageSize, - ).toBe(0); - }); - - test('should memoize data records', () => { - expect(transformProps(testData.basic).data).toBe( - transformProps(testData.basic).data, - ); - }); - - test('should memoize columns meta', () => { - expect(transformProps(testData.basic).columns).toBe( - transformProps({ - ...testData.basic, - rawFormData: { ...testData.basic.rawFormData, pageLength: null }, - }).columns, - ); - }); - - test('should format timestamp', () => { - // eslint-disable-next-line no-underscore-dangle - const parsedDate = transformProps(testData.basic).data[0] - .__timestamp as DateWithFormatter; - expect(String(parsedDate)).toBe('2020-01-01 12:34:56'); - expect(parsedDate.getTime()).toBe(1577882096000); - }); - test('should process comparison columns when time_compare and comparison_type are set', () => { - const transformedProps = transformProps(testData.comparison); - const comparisonColumns = transformedProps.columns.filter( - col => - col.originalLabel === 'metric_1' || - col.originalLabel === 'metric_2' || - col.label === '#' || - col.label === '△' || - col.label === '%', - ); - expect(comparisonColumns.length).toBeGreaterThan(0); - expect( - comparisonColumns.some(col => col.originalLabel === 'metric_1'), - ).toBe(true); - expect( - comparisonColumns.some(col => col.originalLabel === 'metric_2'), - ).toBe(true); - expect(comparisonColumns.some(col => col.label === '#')).toBe(true); - expect(comparisonColumns.some(col => col.label === '△')).toBe(true); - expect(comparisonColumns.some(col => col.label === '%')).toBe(true); - }); - - test('should not process comparison columns when time_compare is empty', () => { - const propsWithoutTimeCompare = { - ...testData.comparison, - rawFormData: { - ...testData.comparison.rawFormData, - time_compare: [], - }, - }; - - const transformedProps = transformProps(propsWithoutTimeCompare); - - // Check if comparison columns are not processed - const comparisonColumns = transformedProps.columns.filter( - col => - col.label === 'Main' || - col.label === '#' || - col.label === '△' || - col.label === '%', - ); - - expect(comparisonColumns.length).toBe(0); - }); - - test('should correctly apply column configuration for comparison columns', () => { - const transformedProps = transformProps(testData.comparisonWithConfig); - - const comparisonColumns = transformedProps.columns.filter( - col => - col.key.startsWith('Main') || - col.key.startsWith('#') || - col.key.startsWith('△') || - col.key.startsWith('%'), - ); - - expect(comparisonColumns).toHaveLength(4); - - const mainMetricConfig = comparisonColumns.find( - col => col.key === 'Main metric_1', - ); - expect(mainMetricConfig).toBeDefined(); - expect(mainMetricConfig?.config).toEqual({ d3NumberFormat: '.2f' }); - - const hashMetricConfig = comparisonColumns.find( - col => col.key === '# metric_1', - ); - expect(hashMetricConfig).toBeDefined(); - expect(hashMetricConfig?.config).toEqual({ d3NumberFormat: '.1f' }); - - const deltaMetricConfig = comparisonColumns.find( - col => col.key === '△ metric_1', - ); - expect(deltaMetricConfig).toBeDefined(); - expect(deltaMetricConfig?.config).toEqual({ d3NumberFormat: '.0f' }); - - const percentMetricConfig = comparisonColumns.find( - col => col.key === '% metric_1', - ); - expect(percentMetricConfig).toBeDefined(); - expect(percentMetricConfig?.config).toEqual({ d3NumberFormat: '.3f' }); - }); - - test('should correctly format comparison columns using getComparisonColFormatter', () => { - const transformedProps = transformProps(testData.comparisonWithConfig); - const comparisonColumns = transformedProps.columns.filter( - col => - col.key.startsWith('Main') || - col.key.startsWith('#') || - col.key.startsWith('△') || - col.key.startsWith('%'), - ); - - const formattedMainMetric = comparisonColumns - .find(col => col.key === 'Main metric_1') - ?.formatter?.(12345.678); - expect(formattedMainMetric).toBe('12345.68'); - - const formattedHashMetric = comparisonColumns - .find(col => col.key === '# metric_1') - ?.formatter?.(12345.678); - expect(formattedHashMetric).toBe('12345.7'); - - const formattedDeltaMetric = comparisonColumns - .find(col => col.key === '△ metric_1') - ?.formatter?.(12345.678); - expect(formattedDeltaMetric).toBe('12346'); - - const formattedPercentMetric = comparisonColumns - .find(col => col.key === '% metric_1') - ?.formatter?.(0.123456); - expect(formattedPercentMetric).toBe('0.123'); - }); - - test('should set originalLabel for comparison columns when time_compare and comparison_type are set', () => { - const transformedProps = transformProps(testData.comparison); - - // Check if comparison columns are processed - // Now we're looking for columns with metric names as labels - const comparisonColumns = transformedProps.columns.filter( - col => - col.originalLabel === 'metric_1' || - col.originalLabel === 'metric_2' || - col.label === '#' || - col.label === '△' || - col.label === '%', - ); - - expect(comparisonColumns.length).toBeGreaterThan(0); - expect( - comparisonColumns.some(col => col.originalLabel === 'metric_1'), - ).toBe(true); - expect( - comparisonColumns.some(col => col.originalLabel === 'metric_2'), - ).toBe(true); - expect(comparisonColumns.some(col => col.label === '#')).toBe(true); - expect(comparisonColumns.some(col => col.label === '△')).toBe(true); - expect(comparisonColumns.some(col => col.label === '%')).toBe(true); - // Verify originalLabel for metric_1 comparison columns - const metric1Column = transformedProps.columns.find( - col => - col.originalLabel === 'metric_1' && - !col.key.startsWith('#') && - !col.key.startsWith('△') && - !col.key.startsWith('%'), - ); - expect(metric1Column).toBeDefined(); - expect(metric1Column?.originalLabel).toBe('metric_1'); - expect(metric1Column?.label).toBe('Main'); - - const hashMetric1 = transformedProps.columns.find( - col => col.key === '# metric_1', - ); - expect(hashMetric1).toBeDefined(); - expect(hashMetric1?.originalLabel).toBe('metric_1'); - - const deltaMetric1 = transformedProps.columns.find( - col => col.key === '△ metric_1', - ); - expect(deltaMetric1).toBeDefined(); - expect(deltaMetric1?.originalLabel).toBe('metric_1'); - - const percentMetric1 = transformedProps.columns.find( - col => col.key === '% metric_1', - ); - expect(percentMetric1).toBeDefined(); - expect(percentMetric1?.originalLabel).toBe('metric_1'); - - // Verify originalLabel for metric_2 comparison columns - const metric2Column = transformedProps.columns.find( - col => - col.originalLabel === 'metric_2' && - !col.key.startsWith('#') && - !col.key.startsWith('△') && - !col.key.startsWith('%'), - ); - expect(metric2Column).toBeDefined(); - expect(metric2Column?.originalLabel).toBe('metric_2'); - - expect(metric2Column?.label).toBe('Main'); - - const hashMetric2 = transformedProps.columns.find( - col => col.key === '# metric_2', - ); - expect(hashMetric2).toBeDefined(); - expect(hashMetric2?.originalLabel).toBe('metric_2'); - - const deltaMetric2 = transformedProps.columns.find( - col => col.key === '△ metric_2', - ); - expect(deltaMetric2).toBeDefined(); - expect(deltaMetric2?.originalLabel).toBe('metric_2'); - - const percentMetric2 = transformedProps.columns.find( - col => col.key === '% metric_2', - ); - expect(percentMetric2).toBeDefined(); - expect(percentMetric2?.originalLabel).toBe('metric_2'); - }); - - test('should not apply time grain formatting in Raw Records mode', () => { - const rawRecordsProps = { - ...testData.basic, - rawFormData: { - ...testData.basic.rawFormData, - query_mode: QueryMode.Raw, - time_grain_sqla: TimeGranularity.MONTH, - table_timestamp_format: SMART_DATE_ID, - }, - }; - - const transformedProps = transformProps(rawRecordsProps); - const timestampColumn = transformedProps.columns.find( - col => col.key === '__timestamp', - ); - - expect(timestampColumn).toBeDefined(); - const testValue = new Date('2023-01-15T10:30:45'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const formatted = (timestampColumn?.formatter as any)?.(testValue); - const granularityFormatted = getTimeFormatterForGranularity( - TimeGranularity.MONTH, - )(testValue as number | Date | null); - expect(formatted).not.toBe(granularityFormatted); - expect(typeof formatted).toBe('string'); - expect(formatted).toContain('2023'); - }); - - test('should handle null/undefined timestamp values correctly', () => { - const rawRecordsProps = { - ...testData.basic, - rawFormData: { - ...testData.basic.rawFormData, - query_mode: QueryMode.Raw, - }, - }; - - const transformedProps = transformProps(rawRecordsProps); - expect(transformedProps.isRawRecords).toBe(true); - - const timestampColumn = transformedProps.columns.find( - col => col.key === '__timestamp', - ); - expect(timestampColumn).toBeDefined(); - }); - - describe('TableChart', () => { - test('render basic data', () => { - render( - , - ); - - const firstDataRow = screen.getAllByRole('rowgroup')[1]; - const cells = firstDataRow.querySelectorAll('td'); - expect(cells).toHaveLength(12); - // Date is rendered as ISO string format - expect(cells[0]).toHaveTextContent('2020-01-01T12:34:56'); - expect(cells[1]).toHaveTextContent('Michael'); - // number is not in `metrics` list, so it should output raw value - // (in real world Superset, this would mean the column is used in GROUP BY) - expect(cells[2]).toHaveTextContent('2467063'); - // should not render column with `.` in name as `undefined` - expect(cells[3]).toHaveTextContent('foo'); - expect(cells[6]).toHaveTextContent('2467'); - expect(cells[8]).toHaveTextContent('N/A'); - }); - - test('render advanced data', () => { - render( - , - ); - const secondColumnHeader = screen.getByText('Sum of Num'); - expect(secondColumnHeader).toBeInTheDocument(); - expect(secondColumnHeader?.getAttribute('data-column-name')).toEqual( - '1', - ); - - const firstDataRow = screen.getAllByRole('rowgroup')[1]; - const cells = firstDataRow.querySelectorAll('td'); - expect(cells[0]).toHaveTextContent('Michael'); - expect(cells[2]).toHaveTextContent('12.346%'); - expect(cells[4]).toHaveTextContent('2.47k'); - }); - - test('render advanced data with currencies', () => { - render( - ProviderWrapper({ - children: ( - - ), - }), - ); - const cells = document.querySelectorAll('td'); - expect(document.querySelectorAll('th')[1]).toHaveTextContent( - 'Sum of Num', - ); - expect(cells[0]).toHaveTextContent('Michael'); - expect(cells[2]).toHaveTextContent('12.346%'); - expect(cells[4]).toHaveTextContent('$ 2.47k'); - }); - - test('render data with a bigint value in a raw record mode', () => { - render( - ProviderWrapper({ - children: ( - - ), - }), - ); - const cells = document.querySelectorAll('td'); - expect(document.querySelectorAll('th')[0]).toHaveTextContent('name'); - expect(document.querySelectorAll('th')[1]).toHaveTextContent('id'); - expect(cells[0]).toHaveTextContent('Michael'); - expect(cells[1]).toHaveTextContent('4312'); - expect(cells[2]).toHaveTextContent('John'); - expect(cells[3]).toHaveTextContent('1234567890123456789'); - }); - - test('render raw data', () => { - const props = transformProps({ - ...testData.raw, - rawFormData: { ...testData.raw.rawFormData }, - }); - render( - ProviderWrapper({ - children: , - }), - ); - const cells = document.querySelectorAll('td'); - expect(document.querySelectorAll('th')[0]).toHaveTextContent('num'); - expect(cells[0]).toHaveTextContent('1234'); - expect(cells[1]).toHaveTextContent('10000'); - expect(cells[1]).toHaveTextContent('0'); - }); - - test('render raw data with currencies', () => { - const props = transformProps({ - ...testData.raw, - rawFormData: { - ...testData.raw.rawFormData, - column_config: { - num: { - currencyFormat: { symbol: 'USD', symbolPosition: 'prefix' }, - }, - }, - }, - }); - render( - ProviderWrapper({ - children: , - }), - ); - const cells = document.querySelectorAll('td'); - - expect(document.querySelectorAll('th')[0]).toHaveTextContent('num'); - expect(cells[0]).toHaveTextContent('$ 1.23k'); - expect(cells[1]).toHaveTextContent('$ 10k'); - expect(cells[2]).toHaveTextContent('$ 0'); - }); - - test('render small formatted data with currencies', () => { - const props = transformProps({ - ...testData.raw, - rawFormData: { - ...testData.raw.rawFormData, - column_config: { - num: { - d3SmallNumberFormat: '.2r', - currencyFormat: { symbol: 'USD', symbolPosition: 'prefix' }, - }, - }, - }, - queriesData: [ - { - ...testData.raw.queriesData[0], - data: [ - { - num: 1234, - }, - { - num: 0.5, - }, - { - num: 0.61234, - }, - ], - }, - ], - }); - render( - ProviderWrapper({ - children: , - }), - ); - const cells = document.querySelectorAll('td'); - - expect(document.querySelectorAll('th')[0]).toHaveTextContent('num'); - expect(cells[0]).toHaveTextContent('$ 1.23k'); - expect(cells[1]).toHaveTextContent('$ 0.50'); - expect(cells[2]).toHaveTextContent('$ 0.61'); - }); - - test('render empty data', () => { - render( - , - ); - expect(screen.getByText('No records found')).toBeInTheDocument(); - }); - - test('render color with column color formatter', () => { - render( - ProviderWrapper({ - children: ( - ', - targetValue: 2467, - }, - ], - }, - })} - /> - ), - }), - ); - - expect(getComputedStyle(screen.getByTitle('2467063')).background).toBe( - 'rgba(172, 225, 196, 1)', - ); - expect(getComputedStyle(screen.getByTitle('2467')).background).toBe(''); - }); - - test('render cell without color', () => { - const dataWithEmptyCell = cloneDeep(testData.advanced.queriesData[0]); - dataWithEmptyCell.data.push({ - __timestamp: null, - name: 'Noah', - sum__num: null, - '%pct_nice': 0.643, - 'abc.com': 'bazzinga', - }); - - render( - ProviderWrapper({ - children: ( - - ), - }), - ); - expect(getComputedStyle(screen.getByTitle('2467')).background).toBe( - 'rgba(172, 225, 196, 0.812)', - ); - expect(getComputedStyle(screen.getByTitle('2467063')).background).toBe( - '', - ); - expect(getComputedStyle(screen.getByText('N/A')).background).toBe(''); - }); - - test('preserves muted null styling when no formatter resolves text color', () => { - const dataWithEmptyCell = cloneDeep(testData.advanced.queriesData[0]); - dataWithEmptyCell.data.push({ - __timestamp: null, - name: 'Noah', - sum__num: null, - '%pct_nice': 0.643, - 'abc.com': 'bazzinga', - }); - - render( - ProviderWrapper({ - children: ( - - ), - }), - ); - - const noahRow = screen.getByText('Noah').closest('tr'); - expect(noahRow).not.toBeNull(); - - const nullCell = noahRow?.querySelector('td.dt-is-null'); - expect(nullCell).not.toBeNull(); - expect((nullCell as HTMLElement).style.color).toBe(''); - expect(getComputedStyle(nullCell as Element).color).toBe( - 'rgba(0, 0, 0, 0.45)', - ); - }); - - test('should display original label in grouped headers', () => { - const props = transformProps(testData.comparison); - - render(); - const groupHeaders = screen.getAllByRole('columnheader'); - expect(groupHeaders.length).toBeGreaterThan(0); - const hasMetricHeaders = groupHeaders.some( - header => - header.textContent && - (header.textContent.includes('metric') || - header.textContent.includes('Metric')), - ); - expect(hasMetricHeaders).toBe(true); - }); - - test('should set meaningful header IDs for time-comparison columns', () => { - // Test time-comparison columns have proper IDs - // Uses originalLabel (e.g., "metric_1") which is sanitized for CSS safety - const props = transformProps(testData.comparison); - - render(); - - const headers = screen.getAllByRole('columnheader'); - - // All headers should have IDs - const headersWithIds = headers.filter(header => header.id); - expect(headersWithIds.length).toBeGreaterThan(0); - - // None should have "header-undefined" - const undefinedHeaders = headersWithIds.filter(header => - header.id.includes('undefined'), - ); - expect(undefinedHeaders).toHaveLength(0); - - // Should have IDs based on sanitized originalLabel (e.g., "metric_1") - const hasMetricHeaders = headersWithIds.some( - header => - header.id.includes('metric_1') || header.id.includes('metric_2'), - ); - expect(hasMetricHeaders).toBe(true); - - // CRITICAL: Verify sanitization - no spaces or special chars in any header ID - headersWithIds.forEach(header => { - // IDs must not contain spaces (would break CSS selectors and ARIA) - expect(header.id).not.toMatch(/\s/); - // IDs must not contain special chars like %, #, △ - expect(header.id).not.toMatch(/[%#△]/); - // IDs should only contain valid characters: alphanumeric, underscore, hyphen - expect(header.id).toMatch(/^header-[a-zA-Z0-9_-]+$/); - }); - }); - - test('should validate ARIA references for time-comparison table cells', () => { - // Test that ALL cells with aria-labelledby have valid references - // This is critical for screen reader accessibility - const props = transformProps(testData.comparison); - - const { container } = render(); - - expectValidAriaLabels(container); - }); - - test('should align group headers correctly when some comparison columns are hidden (#37074)', () => { - // Test that group headers align correctly when columns have visible: false - // This reproduces issue #37074 where headers became misaligned - const props = transformProps(testData.comparisonWithHiddenColumns); - - const { container } = render(); - - // Get all header rows - first row contains group headers, second row contains column headers - const headerRows = container.querySelectorAll('thead tr'); - expect(headerRows.length).toBe(2); - - // Get group headers from the first row (th elements with colSpan > 1 or group headers) - const groupHeaderRow = headerRows[0]; - const groupHeaders = groupHeaderRow.querySelectorAll('th'); - - // Extract group header text content (filter out empty placeholder headers) - const groupHeaderTexts = Array.from(groupHeaders) - .map(th => th.textContent?.trim()) - .filter(text => text && text.length > 0); - - // Verify metric_1 group header appears before metric_2 - // With hidden columns: metric_1 has 2 visible columns (△, %), metric_2 has 4 (Main, #, △, %) - const metric1Index = groupHeaderTexts.findIndex( - text => text?.includes('metric_1') || text?.includes('Metric 1'), - ); - const metric2Index = groupHeaderTexts.findIndex( - text => text?.includes('metric_2') || text?.includes('Metric 2'), - ); - - // Both headers should exist and metric_1 should come before metric_2 - expect(metric1Index).toBeGreaterThanOrEqual(0); - expect(metric2Index).toBeGreaterThanOrEqual(0); - expect(metric1Index).toBeLessThan(metric2Index); - - // Verify colSpan values match the number of visible columns - const metric1Header = Array.from(groupHeaders).find( - th => - th.textContent?.includes('metric_1') || - th.textContent?.includes('Metric 1'), - ); - const metric2Header = Array.from(groupHeaders).find( - th => - th.textContent?.includes('metric_2') || - th.textContent?.includes('Metric 2'), - ); - - // metric_1 should span 2 columns (△ and % are visible, Main and # are hidden) - expect(metric1Header?.getAttribute('colspan')).toBe('2'); - // metric_2 should span 4 columns (all visible) - expect(metric2Header?.getAttribute('colspan')).toBe('4'); - - // Verify ARIA labels are still valid after filtering - expectValidAriaLabels(container); - }); - - test('should set meaningful header IDs for regular table columns', () => { - // Test regular (non-time-comparison) columns have proper IDs - // Uses fallback to column.key since originalLabel is undefined - const props = transformProps(testData.advanced); - - const { container } = render( - ProviderWrapper({ - children: , - }), - ); - - const headers = screen.getAllByRole('columnheader'); - - // Test 1: "name" column (regular string column) - const nameHeader = headers.find(header => - header.textContent?.includes('name'), - ); - expect(nameHeader).toBeDefined(); - expect(nameHeader?.id).toBe('header-name'); // Falls back to column.key - - // Verify cells reference this header correctly - const nameCells = container.querySelectorAll( - 'td[aria-labelledby="header-name"]', - ); - expect(nameCells.length).toBeGreaterThan(0); - - // Test 2: "sum__num" column (metric with verbose map "Sum of Num") - const sumHeader = headers.find(header => - header.textContent?.includes('Sum of Num'), - ); - expect(sumHeader).toBeDefined(); - expect(sumHeader?.id).toBe('header-sum_num'); // Falls back to column.key, consecutive underscores collapsed - - // Verify cells reference this header correctly - const sumCells = container.querySelectorAll( - 'td[aria-labelledby="header-sum_num"]', - ); - expect(sumCells.length).toBeGreaterThan(0); - - // Test 3: Verify NO headers have "undefined" in their ID - const undefinedHeaders = headers.filter(header => - header.id?.includes('undefined'), - ); - expect(undefinedHeaders).toHaveLength(0); - - // Test 4: Verify ALL headers have proper IDs (no missing IDs) - const headersWithIds = headers.filter(header => header.id); - expect(headersWithIds.length).toBe(headers.length); - - // Test 5: Verify ALL header IDs are properly sanitized - headersWithIds.forEach(header => { - // IDs must not contain spaces - expect(header.id).not.toMatch(/\s/); - // IDs must not contain special chars like % (from %pct_nice column) - expect(header.id).not.toMatch(/[%#△]/); - // IDs should only contain valid CSS selector characters - expect(header.id).toMatch(/^header-[a-zA-Z0-9_-]+$/); - }); - }); - - test('should validate ARIA references for regular table cells', () => { - // Test that ALL cells with aria-labelledby have valid references - // This is critical for screen reader accessibility - const props = transformProps(testData.advanced); - - const { container } = render( - ProviderWrapper({ - children: , - }), - ); - - expectValidAriaLabels(container); - }); - - test('render cell bars properly, and only when it is toggled on in both regular and percent metrics', () => { - const props = transformProps({ - ...testData.raw, - rawFormData: { ...testData.raw.rawFormData }, - }); - - props.columns[0].isMetric = true; - - render( - ProviderWrapper({ - children: , - }), - ); - let cells = document.querySelectorAll('div.cell-bar'); - cells.forEach(cell => { - expect(cell).toHaveClass('positive'); - }); - props.columns[0].isMetric = false; - props.columns[0].isPercentMetric = true; - - render( - ProviderWrapper({ - children: , - }), - ); - cells = document.querySelectorAll('div.cell-bar'); - cells.forEach(cell => { - expect(cell).toHaveClass('positive'); - }); - - props.showCellBars = false; - - render( - ProviderWrapper({ - children: , - }), - ); - cells = document.querySelectorAll('td'); - - props.columns[0].isPercentMetric = false; - props.columns[0].isMetric = true; - - render( - ProviderWrapper({ - children: , - }), - ); - cells = document.querySelectorAll('td'); - }); - - test('render cell bars even when column contains NULL values', () => { - const props = transformProps({ - ...testData.raw, - queriesData: [ - { - ...testData.raw.queriesData[0], - colnames: ['category', 'value1', 'value2', 'value3', 'value4'], - coltypes: [ - GenericDataType.String, - GenericDataType.Numeric, - GenericDataType.Numeric, - GenericDataType.Numeric, - GenericDataType.Numeric, - ], - data: [ - { - category: 'Category A', - value1: 10, - value2: 20, - value3: 30, - value4: null, - }, - { - category: 'Category B', - value1: 15, - value2: 25, - value3: 35, - value4: 100, - }, - { - category: 'Category C', - value1: 18, - value2: 28, - value3: 38, - value4: null, - }, - ], - }, - ], - rawFormData: { - ...testData.raw.rawFormData, - show_cell_bars: true, - metrics: ['value1', 'value2', 'value3', 'value4'], - }, - }); - - const { container } = render( - ProviderWrapper({ - children: , - }), - ); - - // Get all cell bars - should exist for both columns with and without NULL values - const cellBars = container.querySelectorAll('div.cell-bar'); - - // Should have cell bars in all numeric columns, even those with NULL values - // value1, value2, value3 all have 3 values, value4 has 1 non-NULL value - // Total: 3 + 3 + 3 + 1 = 10 cell bars - expect(cellBars.length).toBeGreaterThan(0); - - // Specifically check that value4 column (which has NULLs) still renders bars for non-NULL cells - const rows = container.querySelectorAll('tbody tr'); - expect(rows.length).toBe(3); - - // Row 2 should have a cell bar in value4 column (value: 100) - const row2Cells = rows[1].querySelectorAll('td'); - const value4Cell = row2Cells[4]; // 5th column (0-indexed) - const value4Bar = value4Cell.querySelector('div.cell-bar'); - expect(value4Bar).toBeTruthy(); - }); - - test('render color with string column color formatter(operator begins with)', () => { - render( - ProviderWrapper({ - children: ( - - ), - }), - ); - - expect(getComputedStyle(screen.getByText('Joe')).background).toBe( - 'rgba(172, 225, 196, 1)', - ); - expect(getComputedStyle(screen.getByText('Michael')).background).toBe( - '', - ); - }); - - test('render color with string column color formatter (operator ends with)', () => { - render( - ProviderWrapper({ - children: ( - - ), - }), - ); - expect(getComputedStyle(screen.getByText('Maria')).background).toBe( - 'rgba(172, 225, 196, 1)', - ); - expect(getComputedStyle(screen.getByText('Joe')).background).toBe(''); - }); - - test('render color with string column color formatter (operator containing)', () => { - render( - ProviderWrapper({ - children: ( - - ), - }), - ); - expect(getComputedStyle(screen.getByText('Michael')).background).toBe( - 'rgba(172, 225, 196, 1)', - ); - expect(getComputedStyle(screen.getByText('Joe')).background).toBe(''); - }); - - test('render color with string column color formatter (operator not containing)', () => { - render( - ProviderWrapper({ - children: ( - - ), - }), - ); - expect(getComputedStyle(screen.getByText('Joe')).background).toBe( - 'rgba(172, 225, 196, 1)', - ); - expect(getComputedStyle(screen.getByText('Michael')).background).toBe( - '', - ); - }); - - test('render color with string column color formatter (operator =)', () => { - render( - ProviderWrapper({ - children: ( - - ), - }), - ); - expect(getComputedStyle(screen.getByText('Joe')).background).toBe( - 'rgba(172, 225, 196, 1)', - ); - expect(getComputedStyle(screen.getByText('Michael')).background).toBe( - '', - ); - }); - - test('render color with string column color formatter (operator None)', () => { - render( - ProviderWrapper({ - children: ( - - ), - }), - ); - expect(getComputedStyle(screen.getByText('Joe')).background).toBe( - 'rgba(172, 225, 196, 1)', - ); - expect(getComputedStyle(screen.getByText('Michael')).background).toBe( - 'rgba(172, 225, 196, 1)', - ); - expect(getComputedStyle(screen.getByText('Maria')).background).toBe( - 'rgba(172, 225, 196, 1)', - ); - }); - - test('render color with boolean column color formatter (operator is true)', () => { - render( - ProviderWrapper({ - children: ( - - ), - }), - ); - expect(getComputedStyle(screen.getByText('true')).background).toBe( - 'rgba(172, 225, 196, 1)', - ); - expect(getComputedStyle(screen.getByText('false')).background).toBe(''); - }); - - test('render color with boolean column color formatter (operator is false)', () => { - render( - ProviderWrapper({ - children: ( - - ), - }), - ); - expect(getComputedStyle(screen.getByText('false')).background).toBe( - 'rgba(172, 225, 196, 1)', - ); - expect(getComputedStyle(screen.getByText('true')).background).toBe(''); - }); - - test('render color with boolean column color formatter (operator is null)', () => { - render( - ProviderWrapper({ - children: ( - - ), - }), - ); - expect(getComputedStyle(screen.getByText('N/A')).background).toBe( - 'rgba(172, 225, 196, 1)', - ); - expect(getComputedStyle(screen.getByText('true')).background).toBe(''); - expect(getComputedStyle(screen.getByText('false')).background).toBe(''); - }); - - test('render color with boolean column color formatter (operator is not null)', () => { - render( - ProviderWrapper({ - children: ( - - ), - }), - ); - const trueElements = screen.getAllByText('true'); - const falseElements = screen.getAllByText('false'); - expect(getComputedStyle(trueElements[0]).background).toBe( - 'rgba(172, 225, 196, 1)', - ); - expect(getComputedStyle(falseElements[0]).background).toBe( - 'rgba(172, 225, 196, 1)', - ); - expect(getComputedStyle(screen.getByText('N/A')).background).toBe(''); - }); - - test('render color with column color formatter to entire row', () => { - render( - ProviderWrapper({ - children: ( - ', - targetValue: 2467, - columnFormatting: ObjectFormattingEnum.ENTIRE_ROW, - }, - ], - }, - })} - /> - ), - }), - ); - - expect(getComputedStyle(screen.getByText('Michael')).background).toBe( - 'rgba(172, 225, 196, 1)', - ); - expect(getComputedStyle(screen.getByTitle('2467063')).background).toBe( - 'rgba(172, 225, 196, 1)', - ); - expect(getComputedStyle(screen.getByTitle('0.123456')).background).toBe( - 'rgba(172, 225, 196, 1)', - ); - }); - - test('display text color using column color formatter', () => { - render( - ProviderWrapper({ - children: ( - ', - targetValue: 2467, - objectFormatting: ObjectFormattingEnum.TEXT_COLOR, - }, - ], - }, - })} - /> - ), - }), - ); - - expect(getComputedStyle(screen.getByTitle('2467063')).color).toBe( - 'rgb(172, 225, 196)', - ); - expect((screen.getByTitle('2467') as HTMLElement).style.color).toBe(''); - }); - - test('display text color using column color formatter for entire row', () => { - render( - ProviderWrapper({ - children: ( - ', - targetValue: 2467, - columnFormatting: ObjectFormattingEnum.ENTIRE_ROW, - objectFormatting: ObjectFormattingEnum.TEXT_COLOR, - }, - ], - }, - })} - /> - ), - }), - ); - - expect(getComputedStyle(screen.getByText('Michael')).color).toBe( - 'rgb(172, 225, 196)', - ); - expect(getComputedStyle(screen.getByTitle('2467063')).color).toBe( - 'rgb(172, 225, 196)', - ); - expect(getComputedStyle(screen.getByTitle('0.123456')).color).toBe( - 'rgb(172, 225, 196)', - ); - }); - - test('derive readable text color from dark background formatting', () => { - render( - ProviderWrapper({ - children: ( - ', - targetValue: 2467, - useGradient: false, - }, - ], - }, - })} - /> - ), - }), - ); - - expect(getComputedStyle(screen.getByTitle('2467063')).background).toBe( - 'rgb(17, 17, 17)', - ); - expect(getComputedStyle(screen.getByTitle('2467063')).color).toBe( - 'rgb(255, 255, 255)', - ); - }); - - test('keep explicit text color over adaptive contrast', () => { - render( - ProviderWrapper({ - children: ( - ', - targetValue: 2467, - useGradient: false, - }, - { - colorScheme: '#ACE1C4', - column: 'sum__num', - operator: '>', - targetValue: 2467, - objectFormatting: ObjectFormattingEnum.TEXT_COLOR, - }, - ], - }, - })} - /> - ), - }), - ); - - expect(getComputedStyle(screen.getByTitle('2467063')).background).toBe( - 'rgb(17, 17, 17)', - ); - expect(getComputedStyle(screen.getByTitle('2467063')).color).toBe( - 'rgb(172, 225, 196)', - ); - }); - - test('support legacy toTextColor formatters', () => { - render( - ProviderWrapper({ - children: ( - ', - targetValue: 2467, - useGradient: false, - }, - { - colorScheme: '#ACE1C4', - column: 'sum__num', - operator: '>', - targetValue: 2467, - toTextColor: true, - }, - ], - }, - })} - /> - ), - }), - ); - - expect(getComputedStyle(screen.getByTitle('2467063')).background).toBe( - 'rgb(17, 17, 17)', - ); - expect(getComputedStyle(screen.getByTitle('2467063')).color).toBe( - 'rgb(172, 225, 196)', - ); - }); - - test('use striped row surface when deriving adaptive text color', () => { - const backgroundColor = Array.from( - { length: 0xff }, - (_, index) => `#000000${(index + 1).toString(16).padStart(2, '0')}`, - ).find(candidate => { - const baseColor = getTextColorForBackground( - { backgroundColor: candidate }, - supersetTheme.colorBgBase, - ); - const layoutColor = getTextColorForBackground( - { backgroundColor: candidate }, - supersetTheme.colorBgLayout, - ); - return baseColor !== layoutColor; - }); - - expect(backgroundColor).toBeDefined(); - - render( - ProviderWrapper({ - children: ( - ', - targetValue: 2000, - useGradient: false, - }, - ], - }, - })} - /> - ), - }), - ); - - expect(getComputedStyle(screen.getByTitle('2467063')).color).toBe( - getTextColorForBackground( - { backgroundColor }, - supersetTheme.colorBgLayout, - ), - ); - expect(getComputedStyle(screen.getByTitle('2467')).color).toBe( - getTextColorForBackground( - { backgroundColor }, - supersetTheme.colorBgBase, - ), - ); - }); - - test('render color with useGradient false returns solid color', () => { - render( - ProviderWrapper({ - children: ( - ', - targetValue: 2467, - useGradient: false, - }, - ], - }, - })} - /> - ), - }), - ); - - // When useGradient is false, should return solid color (no opacity variation) - // The color should be the same for all matching values - expect(getComputedStyle(screen.getByTitle('2467063')).background).toBe( - 'rgb(172, 225, 196)', - ); - expect(getComputedStyle(screen.getByTitle('2467')).background).toBe(''); - }); - - test('render color with useGradient true returns gradient color', () => { - render( - ProviderWrapper({ - children: ( - ', - targetValue: 2467, - useGradient: true, - }, - ], - }, - })} - /> - ), - }), - ); - - // When useGradient is true, should return gradient color with opacity - expect(getComputedStyle(screen.getByTitle('2467063')).background).toBe( - 'rgba(172, 225, 196, 1)', - ); - expect(getComputedStyle(screen.getByTitle('2467')).background).toBe(''); - }); - - test('render color with useGradient undefined defaults to gradient (backward compatibility)', () => { - render( - ProviderWrapper({ - children: ( - ', - targetValue: 2467, - }, - ], - }, - })} - /> - ), - }), - ); - - // When useGradient is undefined, should default to gradient for backward compatibility - expect(getComputedStyle(screen.getByTitle('2467063')).background).toBe( - 'rgba(172, 225, 196, 1)', - ); - expect(getComputedStyle(screen.getByTitle('2467')).background).toBe(''); - }); - - test('render color with useGradient false and None operator returns solid color', () => { - render( - ProviderWrapper({ - children: ( - - ), - }), - ); - - // When useGradient is false with None operator, all values should have solid color - expect(getComputedStyle(screen.getByTitle('2467063')).background).toBe( - 'rgb(172, 225, 196)', - ); - expect(getComputedStyle(screen.getByTitle('2467')).background).toBe( - 'rgb(172, 225, 196)', - ); - }); - - test('clicking a cell emits cross-filter, clicking again clears it', () => { - const setDataMask = jest.fn(); - const props = transformProps({ - ...testData.basic, - hooks: { setDataMask }, - emitCrossFilters: true, - }); - const { rerender } = render( - - - , - ); - - // Click a string cell to apply cross-filter - const nameCell = screen.getByText('Michael'); - fireEvent.click(nameCell); - - // Find the cross-filter call (not the ownState call) - const crossFilterCall = setDataMask.mock.calls.find( - (call: any[]) => call[0]?.filterState?.filters, - ); - expect(crossFilterCall).toBeDefined(); - const firstCallArg = crossFilterCall![0]; - // Should set the filter - expect(firstCallArg.filterState.filters).toEqual({ - name: ['Michael'], - }); - expect(firstCallArg.extraFormData.filters).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - col: 'name', - op: 'IN', - val: ['Michael'], - }), - ]), - ); - - // Now simulate Redux updating the filters prop (as would happen in dashboard) - setDataMask.mockClear(); - rerender( - - - , - ); - - // The cell should now have the active filter class - const activeCells = document.querySelectorAll('.dt-is-active-filter'); - expect(activeCells.length).toBeGreaterThan(0); - - // Click same cell again to clear cross-filter - setDataMask.mockClear(); - const sameCellAgain = screen.getByText('Michael'); - fireEvent.click(sameCellAgain); - - // Find the cross-filter clearing call - const clearCall = setDataMask.mock.calls.find( - (call: any[]) => call[0]?.filterState !== undefined, - ); - expect(clearCall).toBeDefined(); - const secondCallArg = clearCall![0]; - // Should clear the filter - expect(secondCallArg.filterState.filters).toBeNull(); - expect(secondCallArg.extraFormData.filters).toEqual([]); - }); - - test('cross-filter toggle works with DateWithFormatter values', () => { - const setDataMask = jest.fn(); - const props = transformProps({ - ...testData.basic, - hooks: { setDataMask }, - emitCrossFilters: true, - }); - - // The data has a __timestamp column with DateWithFormatter values - // after processDataRecords. Let's verify this. - const timestampVal = props.data[0].__timestamp; - expect(timestampVal).toBeInstanceOf(DateWithFormatter); - - const { rerender } = render( - - - , - ); - - // Click a timestamp cell - find it by text content - const timestampCell = screen.getByText('2020-01-01 12:34:56'); - fireEvent.click(timestampCell); - - const crossFilterCall = setDataMask.mock.calls.find( - (call: any[]) => call[0]?.filterState?.filters, - ); - expect(crossFilterCall).toBeDefined(); - const firstCallArg = crossFilterCall![0]; - - // Now re-render with the filters from the first click - // This simulates what happens via Redux in the real app - setDataMask.mockClear(); - rerender( - - - , - ); - - // The timestamp cell should be active - const activeCells = document.querySelectorAll('.dt-is-active-filter'); - expect(activeCells.length).toBeGreaterThan(0); - - // Click the same timestamp cell again to clear - setDataMask.mockClear(); - const sameCell = screen.getByText('2020-01-01 12:34:56'); - fireEvent.click(sameCell); - - const clearCall = setDataMask.mock.calls.find( - (call: any[]) => call[0]?.filterState !== undefined, - ); - expect(clearCall).toBeDefined(); - // Should CLEAR the filter (not re-apply it) - expect(clearCall![0].filterState.filters).toBeNull(); - expect(clearCall![0].extraFormData.filters).toEqual([]); - }); - - test('cross-filter toggle clears when DateWithFormatter references differ', () => { - // Regression test: when memoizeOne cache misses between renders, - // new DateWithFormatter instances are created with different references. - // isActiveFilterValue must compare by time value, not reference. - const setDataMask = jest.fn(); - const props = transformProps({ - ...testData.basic, - hooks: { setDataMask }, - emitCrossFilters: true, - }); - - const timestampVal = props.data[0].__timestamp as DateWithFormatter; - expect(timestampVal).toBeInstanceOf(DateWithFormatter); - - // Build filters with a DIFFERENT DateWithFormatter instance (same time value) - const filterKey = '__timestamp'; - const differentRef = new DateWithFormatter(timestampVal.input, { - formatter: timestampVal.formatter, - }); - expect(differentRef).not.toBe(timestampVal); // different reference - expect(differentRef.getTime()).toBe(timestampVal.getTime()); // same time - - const { container } = render( - - - , - ); - - // The cell should show active filter despite different reference - const activeCells = container.querySelectorAll('.dt-is-active-filter'); - expect(activeCells.length).toBeGreaterThan(0); - - // Clicking should CLEAR the filter, not re-apply it - setDataMask.mockClear(); - const timestampCell = screen.getByText('2020-01-01 12:34:56'); - fireEvent.click(timestampCell); - - const clearCall = setDataMask.mock.calls.find( - (call: any[]) => call[0]?.filterState !== undefined, - ); - expect(clearCall).toBeDefined(); - expect(clearCall![0].filterState.filters).toBeNull(); - expect(clearCall![0].extraFormData.filters).toEqual([]); - }); - - test('page size selector arrow stays above resize handles (#39305)', () => { - // .resize-handle elements in dashboard ResizableContainer sit at - // z-index: 10 — the page size arrow must stack above them or it - // gets covered on dashboard charts. - const { container } = render( - ProviderWrapper({ - children: ( - - ), - }), - ); - - const arrow = container.querySelector( - '.dt-select-page-size .ant-select .ant-select-arrow', - ); - expect(arrow).not.toBeNull(); - expect(getComputedStyle(arrow as HTMLElement).zIndex).toBe('11'); - }); - - test('recalculates totals when user filters data', async () => { - const formDataWithTotals = { - ...testData.basic.formData, - show_totals: true, - include_search: true, - server_pagination: false, - metrics: ['sum__num'], - }; - - const { data } = testData.basic.queriesData[0]; - const totalBeforeFilter = data.reduce( - (sum, row) => sum + Number(row.sum__num || 0), - 0, - ); - const totalAfterFilter = - data.find(item => item.name === 'Michael')?.sum__num || 0; - - const props = transformProps({ - ...testData.basic, - formData: formDataWithTotals, - }); - props.totals = { sum__num: totalBeforeFilter }; - props.includeSearch = true; - render( - - - , - ); - - const table = screen.getByRole('table'); - const totalCellBefore = within(table).getByText( - String(totalBeforeFilter), - ); - expect(totalCellBefore).toBeInTheDocument(); - - const searchInput = screen.getByRole('textbox'); - fireEvent.change(searchInput, { target: { value: 'Michael' } }); - - await waitFor(() => { - const totalCellAfter = within(table).getByText( - String(totalAfterFilter), - ); - expect(totalCellAfter).toBeInTheDocument(); - }); - }); - - test('preserves client-side search text across temporal table rerenders', async () => { - const formDataWithSearch = { - ...testData.basic.formData, - include_search: true, - server_pagination: false, - }; - - const renderChart = () => { - const props = transformProps({ - ...testData.basic, - formData: formDataWithSearch, - }); - props.includeSearch = true; - - return ( - - - - ); - }; - - const { rerender } = render(renderChart()); - - const searchInput = screen.getByRole('textbox'); - fireEvent.change(searchInput, { target: { value: 'Michael' } }); - - await waitFor(() => { - expect(searchInput).toHaveValue('Michael'); - expect(screen.getByText('Michael')).toBeInTheDocument(); - }); - - rerender(renderChart()); - - await waitFor(() => { - expect(screen.getByRole('textbox')).toHaveValue('Michael'); - expect(screen.getByText('Michael')).toBeInTheDocument(); - }); - }); - - test('preserves client-side search text when rerendered with empty data', async () => { - const formDataWithSearch = { - ...testData.basic.formData, - include_search: true, - server_pagination: false, - }; - - const renderChart = (data = testData.basic.queriesData[0].data) => { - const props = transformProps({ - ...testData.basic, - formData: formDataWithSearch, - queriesData: [ - { - ...testData.basic.queriesData[0], - data, - }, - ], - }); - props.includeSearch = true; - - return ( - - - - ); - }; - - const { rerender } = render(renderChart()); - - const searchInput = screen.getByRole('textbox'); - fireEvent.change(searchInput, { target: { value: 'Michael' } }); - - await waitFor(() => { - expect(searchInput).toHaveValue('Michael'); - expect(screen.getByText('Michael')).toBeInTheDocument(); - }); - - rerender(renderChart([])); - - await waitFor(() => { - expect(screen.getByRole('textbox')).toHaveValue('Michael'); - expect(screen.getByLabelText('Search 0 records')).toHaveValue( - 'Michael', - ); - }); - }); - - test('preserves client-side search text for function accessor columns', async () => { - type DataRow = { - city: string; - firstName: string; - }; - - const makeColumns = (): Column[] => [ - { - Header: ({ column }: HeaderProps) => ( - First name - ), - Cell: ({ value }: CellProps) => {value}, - id: 'firstName', - accessor: ((row: DataRow) => row.firstName) as never, - }, - { - Header: ({ column }: HeaderProps) => ( - City - ), - Cell: ({ value }: CellProps) => {value}, - id: 'city', - accessor: ((row: DataRow) => row.city) as never, - }, - ]; - - const data: DataRow[] = [ - { firstName: 'Michael', city: 'Paris' }, - { firstName: 'Jordan', city: 'London' }, - ]; - - const renderDataTable = () => ( - - - columns={makeColumns()} - data={data} - rowCount={data.length} - serverPagination={false} - serverPaginationData={{}} - onServerPaginationChange={jest.fn()} - handleSortByChange={jest.fn()} - sortByFromParent={[]} - onSearchColChange={jest.fn()} - searchOptions={[]} - sticky={false} - /> - - ); - - const { rerender } = render(renderDataTable()); - - const searchInput = screen.getByRole('textbox'); - fireEvent.change(searchInput, { target: { value: 'Michael' } }); - - await waitFor(() => { - expect(searchInput).toHaveValue('Michael'); - expect(screen.getByText('Michael')).toBeInTheDocument(); - }); - - rerender(renderDataTable()); - - await waitFor(() => { - expect(screen.getByRole('textbox')).toHaveValue('Michael'); - expect(screen.getByText('Michael')).toBeInTheDocument(); - }); - }); - - test('preserves client-side search text for string accessor columns without ids', async () => { - type DataRow = { - city: string; - firstName: string; - }; - - const makeColumns = (): Column[] => [ - { - Header: ({ column }: HeaderProps) => ( - First name - ), - Cell: ({ value }: CellProps) => {value}, - accessor: 'firstName', - }, - { - Header: ({ column }: HeaderProps) => ( - City - ), - Cell: ({ value }: CellProps) => {value}, - accessor: 'city', - }, - ]; - - const data: DataRow[] = [ - { firstName: 'Michael', city: 'Paris' }, - { firstName: 'Jordan', city: 'London' }, - ]; - - const renderDataTable = () => ( - - - columns={makeColumns()} - data={data} - rowCount={data.length} - serverPagination={false} - serverPaginationData={{}} - onServerPaginationChange={jest.fn()} - handleSortByChange={jest.fn()} - sortByFromParent={[]} - onSearchColChange={jest.fn()} - searchOptions={[]} - sticky={false} - /> - - ); - - const { rerender } = render(renderDataTable()); - - const searchInput = screen.getByRole('textbox'); - fireEvent.change(searchInput, { target: { value: 'Michael' } }); - - await waitFor(() => { - expect(searchInput).toHaveValue('Michael'); - expect(screen.getByText('Michael')).toBeInTheDocument(); - }); - - rerender(renderDataTable()); - - await waitFor(() => { - expect(screen.getByRole('textbox')).toHaveValue('Michael'); - expect(screen.getByText('Michael')).toBeInTheDocument(); - }); - }); - }); - - test('should build columnLabelToNameMap for adhoc columns with custom labels', () => { - const result = transformProps({ - ...testData.basic, - rawFormData: { - ...testData.basic.rawFormData, - query_mode: QueryMode.Aggregate, - groupby: [ - { - sqlExpression: 'name', - label: 'Name_Renamed', - expressionType: 'SQL', - }, - ], - metrics: ['sum__num'], - }, - emitCrossFilters: true, - queriesData: [ - { - ...testData.basic.queriesData[0], - colnames: ['Name_Renamed', 'sum__num'], - coltypes: [GenericDataType.String, GenericDataType.Numeric], - data: [{ Name_Renamed: 'Michael', sum__num: 2467063 }], - }, - ], - }); - expect(result.columnLabelToNameMap).toEqual({ - Name_Renamed: 'name', - }); - }); - - test('should not populate columnLabelToNameMap for physical columns', () => { - const result = transformProps({ - ...testData.basic, - rawFormData: { - ...testData.basic.rawFormData, - query_mode: QueryMode.Aggregate, - groupby: ['name'], - metrics: ['sum__num'], - }, - emitCrossFilters: true, - queriesData: [ - { - ...testData.basic.queriesData[0], - colnames: ['name', 'sum__num'], - coltypes: [GenericDataType.String, GenericDataType.Numeric], - data: [{ name: 'Michael', sum__num: 2467063 }], - }, - ], - }); - expect(result.columnLabelToNameMap).toEqual({}); - }); - - test('should not populate columnLabelToNameMap when adhoc label matches sqlExpression', () => { - const result = transformProps({ - ...testData.basic, - rawFormData: { - ...testData.basic.rawFormData, - query_mode: QueryMode.Aggregate, - groupby: [ - { - sqlExpression: 'name', - label: 'name', - expressionType: 'SQL', - }, - ], - metrics: ['sum__num'], - }, - emitCrossFilters: true, - queriesData: [ - { - ...testData.basic.queriesData[0], - colnames: ['name', 'sum__num'], - coltypes: [GenericDataType.String, GenericDataType.Numeric], - data: [{ name: 'Michael', sum__num: 2467063 }], - }, - ], - }); - expect(result.columnLabelToNameMap).toEqual({}); - }); - - test('cross-filter on adhoc column with custom label emits original column name', () => { - const setDataMask = jest.fn(); - const baseProps = transformProps({ - ...testData.basic, - rawFormData: { - ...testData.basic.rawFormData, - query_mode: QueryMode.Aggregate, - groupby: [ - { - sqlExpression: 'name', - label: 'Name_Renamed', - expressionType: 'SQL', - }, - ], - metrics: ['sum__num'], - }, - filterState: { filters: {} }, - ownState: {}, - hooks: { - onAddFilter: jest.fn(), - setDataMask, - onContextMenu: jest.fn(), - }, - emitCrossFilters: true, - queriesData: [ - { - ...testData.basic.queriesData[0], - colnames: ['Name_Renamed', 'sum__num'], - coltypes: [GenericDataType.String, GenericDataType.Numeric], - data: [ - { Name_Renamed: 'Michael', sum__num: 2467063 }, - { Name_Renamed: 'Joe', sum__num: 2467 }, - ], - }, - ], - }); - - render( - - - , - ); - - // Verify the table rendered with data - expect(screen.getByText('Michael')).toBeInTheDocument(); - - // Find the td cell containing "Michael" and click it - const cell = screen.getByText('Michael').closest('td')!; - fireEvent.click(cell); - - expect(setDataMask).toHaveBeenCalled(); - const lastCall = - setDataMask.mock.calls[setDataMask.mock.calls.length - 1][0]; - const { filters } = lastCall.extraFormData; - expect(filters).toHaveLength(1); - // Should emit the original column name, not the label - expect(filters[0].col).toBe('name'); - expect(filters[0].val).toEqual(['Michael']); - }); - }); -}); - -/** - * DRILL-TO-DETAIL FIX VERIFICATION (#23847) - */ -describe('Drill-to-Detail Temporal Range Logic', () => { - const renderChartAndOpenContextMenu = ( - timeGrain?: TimeGranularity, - timestampValue?: string | number | null, - ) => { - const onContextMenu = jest.fn(); - const data = cloneDeep(testData.basic); - - if (timestampValue !== undefined) { - data.queriesData[0].data[0].__timestamp = timestampValue; - } - - const props = transformProps({ - ...data, - rawFormData: { - ...data.rawFormData, - ...(timeGrain ? { time_grain_sqla: timeGrain } : {}), - }, - hooks: { onAddFilter: jest.fn(), onContextMenu, setDataMask: jest.fn() }, - }); - render(); - - const tbody = screen.getAllByRole('rowgroup')[1]; - fireEvent.contextMenu(tbody.querySelectorAll('td')[0]); - - const [, , { drillToDetail }] = onContextMenu.mock.calls[0]; - return drillToDetail.find((f: any) => f.col === '__timestamp'); - }; - - test('uses TEMPORAL_RANGE for monthly grain', () => { - const filter = renderChartAndOpenContextMenu(TimeGranularity.MONTH); - - expect(filter.op).toBe('TEMPORAL_RANGE'); - expect(filter.val).toContain( - '2020-01-01T12:34:56.000Z : 2020-02-01T00:00:00.000Z', - ); - }); - - test('uses the full bucket for week ending sunday grain', () => { - const filter = renderChartAndOpenContextMenu( - TimeGranularity.WEEK_ENDING_SUNDAY, - '2020-01-05T00:00:00', - ); - - expect(filter.op).toBe('TEMPORAL_RANGE'); - expect(filter.val).toBe( - '2019-12-30T00:00:00.000Z : 2020-01-06T00:00:00.000Z', - ); - }); - - test('uses the full bucket for week ending saturday grain', () => { - const filter = renderChartAndOpenContextMenu( - TimeGranularity.WEEK_ENDING_SATURDAY, - '2020-01-04T00:00:00', - ); - - expect(filter.op).toBe('TEMPORAL_RANGE'); - expect(filter.val).toBe( - '2019-12-29T00:00:00.000Z : 2020-01-05T00:00:00.000Z', - ); - }); - - test('correctly handles NULL values by emitting IS NULL instead of 1970 timestamp', () => { - const filter = renderChartAndOpenContextMenu(TimeGranularity.MONTH, null); - - expect(filter.op).toBe('IS NULL'); - expect(filter.val).toBeNull(); - }); -}); diff --git a/superset-frontend/plugins/plugin-chart-table/test/controlPanel.test.ts b/superset-frontend/plugins/plugin-chart-table/test/controlPanel.test.ts deleted file mode 100644 index e09034fe650..00000000000 --- a/superset-frontend/plugins/plugin-chart-table/test/controlPanel.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -/* eslint-disable camelcase */ -/** - * 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 - */ - -import { - ControlPanelConfig, - ControlPanelsContainerProps, - ControlState, - CustomControlItem, -} from '@superset-ui/chart-controls'; -import config from '../src/controlPanel'; - -type VisibilityFn = ( - props: ControlPanelsContainerProps, - control?: ControlState, -) => boolean; - -function isControlWithVisibility( - controlItem: unknown, -): controlItem is CustomControlItem & { - config: Required & { visibility: VisibilityFn }; -} { - return ( - typeof controlItem === 'object' && - controlItem !== null && - 'name' in controlItem && - 'config' in controlItem && - typeof (controlItem as CustomControlItem).config?.visibility === 'function' - ); -} - -function getVisibility( - panel: ControlPanelConfig, - controlName: string, -): VisibilityFn { - const item = (panel.controlPanelSections || []) - .flatMap(section => section?.controlSetRows || []) - .flat() - .find(c => isControlWithVisibility(c) && c.name === controlName); - - if (!isControlWithVisibility(item)) { - throw new Error(`Control "${controlName}" with visibility not found`); - } - return item.config.visibility; -} - -function mkProps( - groupbyValue: string[], - options = [ - { column_name: 'ORDERDATE', is_dttm: true }, - { column_name: 'some_other_col', is_dttm: false }, - ], -): ControlPanelsContainerProps { - return { - controls: { - groupby: { value: groupbyValue, options }, - }, - } as unknown as ControlPanelsContainerProps; -} - -test('time_grain_sqla visibility should be case-insensitive', () => { - const vis = getVisibility(config, 'time_grain_sqla'); - const controlState = {} as ControlState; - - expect(vis(mkProps(['orderdate']), controlState)).toBe(true); - expect(vis(mkProps(['ORDERDATE']), controlState)).toBe(true); - expect(vis(mkProps(['some_other_col']), controlState)).toBe(false); -}); diff --git a/superset-frontend/plugins/plugin-chart-table/test/controlPanel.test.tsx b/superset-frontend/plugins/plugin-chart-table/test/controlPanel.test.tsx deleted file mode 100644 index 9a35494e78a..00000000000 --- a/superset-frontend/plugins/plugin-chart-table/test/controlPanel.test.tsx +++ /dev/null @@ -1,351 +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 { GenericDataType } from '@apache-superset/core/common'; -import { QueryFormData } from '@superset-ui/core'; -import { - Dataset, - isCustomControlItem, - ControlConfig, - ControlPanelState, - ControlState, - ColorSchemeEnum, - ObjectFormattingEnum, -} from '@superset-ui/chart-controls'; -import config from '../src/controlPanel'; - -const findConditionalFormattingControl = (): ControlConfig | null => { - for (const section of config.controlPanelSections) { - if (!section) continue; - for (const row of section.controlSetRows) { - for (const control of row) { - if ( - isCustomControlItem(control) && - control.name === 'conditional_formatting' - ) { - return control.config; - } - } - } - } - return null; -}; - -const createMockControlState = (value: string[] | undefined): ControlState => ({ - type: 'SelectControl', - value, - label: '', - default: undefined, - renderTrigger: false, -}); - -const createMockExplore = ( - timeCompareValue: string[] | undefined, - datasourceColumns: Partial['columns'] = [], -): ControlPanelState => ({ - slice: { slice_id: 123 }, - datasource: { - verbose_map: { col1: 'Column 1', col2: 'Column 2' }, - columns: datasourceColumns, - } as Partial as Dataset, - controls: { - time_compare: createMockControlState(timeCompareValue), - }, - form_data: { - time_compare: timeCompareValue, - datasource: 'test', - viz_type: 'table', - } as QueryFormData, - common: {}, - metadata: {}, -}); - -const createMockChart = () => ({ - chartStatus: 'success' as const, - queriesResponse: [ - { - colnames: ['col1', 'col2'], - coltypes: [GenericDataType.Numeric, GenericDataType.Numeric], - }, - ], -}); - -const createMockControlStateForConditionalFormatting = (): ControlState => ({ - type: 'CollectionControl', - value: [], - label: '', - default: undefined, - renderTrigger: false, -}); - -test('extraColorChoices not included when time comparison is disabled', () => { - const controlConfig = findConditionalFormattingControl(); - expect(controlConfig).toBeTruthy(); - expect(controlConfig?.mapStateToProps).toBeTruthy(); - - const explore = createMockExplore(undefined); - const chart = createMockChart(); - const result = controlConfig!.mapStateToProps!( - explore, - createMockControlStateForConditionalFormatting(), - chart, - ); - - expect(result.extraColorChoices).toEqual([]); - expect(result.columnOptions).toEqual( - expect.arrayContaining([ - expect.objectContaining({ value: 'col1' }), - expect.objectContaining({ value: 'col2' }), - ]), - ); -}); - -test('extraColorChoices included when time comparison is enabled', () => { - const controlConfig = findConditionalFormattingControl(); - expect(controlConfig).toBeTruthy(); - - const explore = createMockExplore(['P1D']); - const chart = createMockChart(); - const result = controlConfig!.mapStateToProps!( - explore, - createMockControlStateForConditionalFormatting(), - chart, - ); - - expect(result.extraColorChoices).toEqual([ - { - value: ColorSchemeEnum.Green, - label: expect.stringContaining('Green for increase'), - }, - { - value: ColorSchemeEnum.Red, - label: expect.stringContaining('Red for increase'), - }, - ]); - expect(result.columnOptions).not.toEqual( - expect.arrayContaining([expect.objectContaining({ value: 'col1' })]), - ); -}); - -test('extraColorChoices not included when time_compare is empty array', () => { - const controlConfig = findConditionalFormattingControl(); - expect(controlConfig).toBeTruthy(); - - const explore = createMockExplore([]); - const chart = createMockChart(); - const result = controlConfig!.mapStateToProps!( - explore, - createMockControlStateForConditionalFormatting(), - chart, - ); - - expect(result.extraColorChoices).toEqual([]); -}); - -test('consistency between extraColorChoices and columnOptions', () => { - const controlConfig = findConditionalFormattingControl(); - expect(controlConfig).toBeTruthy(); - - const explore = createMockExplore(['P1D']); - const chart = createMockChart(); - const result = controlConfig!.mapStateToProps!( - explore, - createMockControlStateForConditionalFormatting(), - chart, - ); - - const hasExtraColorChoices = result.extraColorChoices.length > 0; - const hasComparisonColumns = result.columnOptions.some( - (col: { value: string }) => - col.value.includes('Main') || - col.value.includes('#') || - col.value.includes('△'), - ); - - expect(hasExtraColorChoices).toBe(true); - expect(hasComparisonColumns).toBe(true); -}); - -test('uses controls.time_compare.value not form_data.time_compare', () => { - const controlConfig = findConditionalFormattingControl(); - expect(controlConfig).toBeTruthy(); - - const explore: ControlPanelState = { - ...createMockExplore(undefined), - form_data: { - ...createMockExplore(undefined).form_data, - time_compare: ['P1D'], - }, - }; - const chart = createMockChart(); - const result = controlConfig!.mapStateToProps!( - explore, - createMockControlStateForConditionalFormatting(), - chart, - ); - - expect(result.extraColorChoices).toEqual([]); -}); - -test('static extraColorChoices removed from config', () => { - const controlConfig = findConditionalFormattingControl(); - expect(controlConfig).toBeTruthy(); - - expect(controlConfig?.extraColorChoices).toBeUndefined(); -}); - -test('columnOptions falls back to datasource columns when queriesResponse is empty', () => { - const controlConfig = findConditionalFormattingControl(); - expect(controlConfig).toBeTruthy(); - - const datasourceColumns = [ - { column_name: 'revenue', type_generic: GenericDataType.Numeric }, - { column_name: 'name', type_generic: GenericDataType.String }, - ]; - const explore = createMockExplore(undefined, datasourceColumns); - const chart = { chartStatus: 'success' as const, queriesResponse: null }; - const result = controlConfig!.mapStateToProps!( - explore, - createMockControlStateForConditionalFormatting(), - chart, - ); - - expect(result.columnOptions).toEqual( - expect.arrayContaining([ - expect.objectContaining({ value: 'revenue' }), - expect.objectContaining({ value: 'name' }), - ]), - ); - expect(result.allColumns).toEqual( - expect.arrayContaining([ - expect.objectContaining({ value: 'revenue' }), - expect.objectContaining({ value: 'name' }), - ]), - ); -}); - -test('columnOptions prefers queriesResponse over datasource columns', () => { - const controlConfig = findConditionalFormattingControl(); - expect(controlConfig).toBeTruthy(); - - const datasourceColumns = [ - { column_name: 'revenue', type_generic: GenericDataType.Numeric }, - { column_name: 'extra_col', type_generic: GenericDataType.String }, - ]; - const explore = createMockExplore(undefined, datasourceColumns); - const chart = createMockChart(); - const result = controlConfig!.mapStateToProps!( - explore, - createMockControlStateForConditionalFormatting(), - chart, - ); - - expect(result.columnOptions).toEqual( - expect.arrayContaining([ - expect.objectContaining({ value: 'col1' }), - expect.objectContaining({ value: 'col2' }), - ]), - ); - expect(result.columnOptions).not.toEqual( - expect.arrayContaining([expect.objectContaining({ value: 'extra_col' })]), - ); -}); - -test('columnOptions falls back to datasource when queriesResponse has empty colnames', () => { - const controlConfig = findConditionalFormattingControl(); - expect(controlConfig).toBeTruthy(); - - const datasourceColumns = [ - { column_name: 'revenue', type_generic: GenericDataType.Numeric }, - ]; - const explore = createMockExplore(undefined, datasourceColumns); - const chart = { - chartStatus: 'success' as const, - queriesResponse: [{ colnames: [], coltypes: [] }], - }; - const result = controlConfig!.mapStateToProps!( - explore, - createMockControlStateForConditionalFormatting(), - chart, - ); - - expect(result.columnOptions).toEqual( - expect.arrayContaining([expect.objectContaining({ value: 'revenue' })]), - ); -}); - -test('columnOptions returns empty when both queriesResponse and datasource have no columns', () => { - const controlConfig = findConditionalFormattingControl(); - expect(controlConfig).toBeTruthy(); - - const explore = createMockExplore(undefined, []); - const chart = { chartStatus: 'success' as const, queriesResponse: null }; - const result = controlConfig!.mapStateToProps!( - explore, - createMockControlStateForConditionalFormatting(), - chart, - ); - - expect(result.columnOptions).toEqual([]); - expect(result.allColumns).toEqual([]); -}); - -test('allColumns includes ENTIRE_ROW when falling back to datasource columns', () => { - const controlConfig = findConditionalFormattingControl(); - expect(controlConfig).toBeTruthy(); - - const datasourceColumns = [ - { column_name: 'revenue', type_generic: GenericDataType.Numeric }, - ]; - const explore = createMockExplore(undefined, datasourceColumns); - const chart = { chartStatus: 'success' as const, queriesResponse: null }; - const result = controlConfig!.mapStateToProps!( - explore, - createMockControlStateForConditionalFormatting(), - chart, - ); - - expect(result.allColumns).toEqual( - expect.arrayContaining([ - expect.objectContaining({ value: ObjectFormattingEnum.ENTIRE_ROW }), - ]), - ); -}); - -test('columnOptions defaults type_generic to String when missing from datasource columns', () => { - const controlConfig = findConditionalFormattingControl(); - expect(controlConfig).toBeTruthy(); - - const datasourceColumns = [{ column_name: 'untyped_col' }]; - const explore = createMockExplore(undefined, datasourceColumns); - const chart = { chartStatus: 'success' as const, queriesResponse: null }; - const result = controlConfig!.mapStateToProps!( - explore, - createMockControlStateForConditionalFormatting(), - chart, - ); - - expect(result.columnOptions).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - value: 'untyped_col', - dataType: GenericDataType.String, - }), - ]), - ); -}); diff --git a/superset-frontend/plugins/plugin-chart-table/tsconfig.json b/superset-frontend/plugins/plugin-chart-table/tsconfig.json index 03190ec5bd4..f0bfbba676d 100644 --- a/superset-frontend/plugins/plugin-chart-table/tsconfig.json +++ b/superset-frontend/plugins/plugin-chart-table/tsconfig.json @@ -20,6 +20,7 @@ "references": [ { "path": "../../packages/superset-core" }, { "path": "../../packages/superset-ui-core" }, - { "path": "../../packages/superset-ui-chart-controls" } + { "path": "../../packages/superset-ui-chart-controls" }, + { "path": "../../packages/superset-ui-glyph-core" } ] } diff --git a/superset-frontend/plugins/plugin-chart-word-cloud/test/ColorSchemeControl.test.tsx b/superset-frontend/plugins/plugin-chart-word-cloud/test/ColorSchemeControl.test.tsx deleted file mode 100644 index 014895cf8c5..00000000000 --- a/superset-frontend/plugins/plugin-chart-word-cloud/test/ColorSchemeControl.test.tsx +++ /dev/null @@ -1,83 +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 { ColorSchemeControl } from '../src/plugin/controls'; -import { render, screen, userEvent } from 'spec/helpers/testing-library'; - -const setup = (props = {}) => { - const defaultProps = { - name: 'color_scheme', - value: '', - onChange: jest.fn(), - }; - return render(); -}; - -test('renders color scheme control', () => { - setup(); - // The Select component has an aria-label - use role to find the input specifically - expect( - screen.getByRole('combobox', { name: 'Select color scheme' }), - ).toBeInTheDocument(); -}); - -test('renders select with value', () => { - // Get a color scheme from the registry to use as a test value - const { getCategoricalSchemeRegistry } = require('@superset-ui/core'); - const registry = getCategoricalSchemeRegistry(); - const firstScheme = registry.keys()[0]; - - setup({ value: firstScheme }); - // Use role to find the input specifically - const select = screen.getByRole('combobox', { name: 'Select color scheme' }); - expect(select).toBeInTheDocument(); -}); - -test('calls onChange when value changes', async () => { - const onChange = jest.fn(); - const { getCategoricalSchemeRegistry } = require('@superset-ui/core'); - const registry = getCategoricalSchemeRegistry(); - const schemes = registry.keys(); - - if (schemes.length < 2) { - // Skip if there aren't enough schemes to test - return; - } - - const initialScheme = schemes[0]; - const newScheme = schemes[1]; - - setup({ onChange, value: initialScheme }); - - // Find the select input using role - const selectInput = screen.getByRole('combobox', { - name: 'Select color scheme', - }); - - userEvent.click(selectInput); - - // Wait for and select a different color scheme - // The scheme name should be visible in the dropdown - const newSchemeOption = await screen.findByText(newScheme, { exact: false }); - userEvent.click(newSchemeOption); - - // Verify onChange was called with the new scheme value - expect(onChange).toHaveBeenCalledWith(newScheme); - expect(onChange).toHaveBeenCalledTimes(1); -}); diff --git a/superset-frontend/plugins/plugin-chart-word-cloud/test/RotationControl.test.tsx b/superset-frontend/plugins/plugin-chart-word-cloud/test/RotationControl.test.tsx deleted file mode 100644 index b10999a56db..00000000000 --- a/superset-frontend/plugins/plugin-chart-word-cloud/test/RotationControl.test.tsx +++ /dev/null @@ -1,59 +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 { RotationControl } from '../src/plugin/controls'; -import { render, screen, userEvent } from 'spec/helpers/testing-library'; - -const setup = (props = {}) => { - const defaultProps = { - name: 'rotation', - value: 'square', - onChange: jest.fn(), - }; - return render(); -}; - -test('renders rotation control with label', () => { - setup(); - expect(screen.getByText('Word Rotation')).toBeInTheDocument(); -}); - -test('renders select with default value', () => { - setup({ value: 'flat' }); - // Check that the select is rendered (implementation depends on Select component) - expect(screen.getByRole('combobox')).toBeInTheDocument(); -}); - -test('calls onChange when value changes', async () => { - const onChange = jest.fn(); - setup({ onChange, value: 'square' }); - - // Find the select input and open the dropdown - const selectInput = screen.getByRole('combobox'); - - await userEvent.click(selectInput); - - // Wait for and select a different option - const flatOption = await screen.findByText('flat', { exact: false }); - await userEvent.click(flatOption); - - // Verify onChange was called with the string value - expect(onChange).toHaveBeenCalledWith('flat'); - expect(onChange).toHaveBeenCalledTimes(1); -}); diff --git a/superset-frontend/plugins/plugin-chart-word-cloud/test/buildQuery.test.ts b/superset-frontend/plugins/plugin-chart-word-cloud/test/buildQuery.test.ts deleted file mode 100644 index 3cb5937660b..00000000000 --- a/superset-frontend/plugins/plugin-chart-word-cloud/test/buildQuery.test.ts +++ /dev/null @@ -1,101 +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 { VizType } from '@superset-ui/core'; -import { WordCloudFormData } from '../src'; -import buildQuery from '../src/plugin/buildQuery'; - -const basicFormData: WordCloudFormData = { - datasource: '5__table', - granularity_sqla: 'ds', - series: 'foo', - viz_type: VizType.WordCloud, -}; - -describe('plugin-chart-word-cloud', () => { - describe('buildQuery', () => { - test('should build columns from series in form data', () => { - const queryContext = buildQuery(basicFormData); - const [query] = queryContext.queries; - expect(query.columns).toEqual(['foo']); - }); - - test('should not include orderby when neither sort option is enabled', () => { - const queryContext = buildQuery({ - ...basicFormData, - metric: 'count', - sort_by_metric: false, - sort_by_series: false, - row_limit: 100, - }); - const [query] = queryContext.queries; - expect(query.orderby).toBeUndefined(); - }); - - test('should order by metric DESC only when sort_by_metric is true', () => { - const queryContext = buildQuery({ - ...basicFormData, - metric: 'count', - sort_by_metric: true, - sort_by_series: false, - row_limit: 100, - }); - const [query] = queryContext.queries; - expect(query.orderby).toEqual([['count', false]]); - }); - - test('should order by series ASC only when sort_by_series is true', () => { - const queryContext = buildQuery({ - ...basicFormData, - metric: 'count', - sort_by_metric: false, - sort_by_series: true, - row_limit: 100, - }); - const [query] = queryContext.queries; - expect(query.orderby).toEqual([['foo', true]]); - }); - - test('should order by metric DESC then series ASC when both are true', () => { - const queryContext = buildQuery({ - ...basicFormData, - metric: 'count', - sort_by_metric: true, - sort_by_series: true, - row_limit: 100, - }); - const [query] = queryContext.queries; - expect(query.orderby).toEqual([ - ['count', false], - ['foo', true], - ]); - }); - - test('should order by series ASC when sort_by_series is undefined (legacy chart)', () => { - const queryContext = buildQuery({ - ...basicFormData, - metric: 'count', - sort_by_metric: false, - row_limit: 100, - }); - const [query] = queryContext.queries; - expect(query.orderby).toEqual([['foo', true]]); - }); - }); -}); diff --git a/superset-frontend/plugins/plugin-chart-word-cloud/test/controlPanel.test.ts b/superset-frontend/plugins/plugin-chart-word-cloud/test/controlPanel.test.ts deleted file mode 100644 index b792581815b..00000000000 --- a/superset-frontend/plugins/plugin-chart-word-cloud/test/controlPanel.test.ts +++ /dev/null @@ -1,69 +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 { isCustomControlItem } from '@superset-ui/chart-controls'; -import controlPanel from '../src/plugin/controlPanel'; -import React, { ReactElement } from 'react'; - -const isNameControl = ( - item: unknown, - name: string, -): item is ReactElement<{ name: string }> => - React.isValidElement<{ name: string }>(item) && item.props.name === name; - -test('control panel has rotation and color_scheme controls', () => { - const optionsSection = controlPanel.controlPanelSections.find( - (section): section is NonNullable => - Boolean(section && section.label === 'Options'), - ); - expect(optionsSection).toBeDefined(); - if (!optionsSection) { - throw new Error('Options section missing'); - } - - const rotationRow = optionsSection.controlSetRows.find(row => - row.some(item => isNameControl(item, 'rotation')), - ); - expect(rotationRow).toBeDefined(); - - const colorSchemeRow = optionsSection.controlSetRows.find(row => - row.some(item => isNameControl(item, 'color_scheme')), - ); - expect(colorSchemeRow).toBeDefined(); -}); - -test('sort_by_series defaults to true to preserve legacy ordering', () => { - const querySection = controlPanel.controlPanelSections.find( - (section): section is NonNullable => - Boolean(section && section.label === 'Query'), - ); - expect(querySection).toBeDefined(); - if (!querySection) { - throw new Error('Query section missing'); - } - - const sortBySeriesEntry = querySection.controlSetRows - .flat() - .find(item => isCustomControlItem(item) && item.name === 'sort_by_series'); - - expect(isCustomControlItem(sortBySeriesEntry)).toBe(true); - if (!isCustomControlItem(sortBySeriesEntry)) { - throw new Error('sort_by_series control missing'); - } - expect(sortBySeriesEntry.config.default).toBe(true); -}); diff --git a/superset-frontend/plugins/plugin-chart-word-cloud/tsconfig.json b/superset-frontend/plugins/plugin-chart-word-cloud/tsconfig.json index 03190ec5bd4..f0bfbba676d 100644 --- a/superset-frontend/plugins/plugin-chart-word-cloud/tsconfig.json +++ b/superset-frontend/plugins/plugin-chart-word-cloud/tsconfig.json @@ -20,6 +20,7 @@ "references": [ { "path": "../../packages/superset-core" }, { "path": "../../packages/superset-ui-core" }, - { "path": "../../packages/superset-ui-chart-controls" } + { "path": "../../packages/superset-ui-chart-controls" }, + { "path": "../../packages/superset-ui-glyph-core" } ] } diff --git a/superset-frontend/plugins/preset-chart-deckgl/package.json b/superset-frontend/plugins/preset-chart-deckgl/package.json index 02f6b4780eb..4adda7a6193 100644 --- a/superset-frontend/plugins/preset-chart-deckgl/package.json +++ b/superset-frontend/plugins/preset-chart-deckgl/package.json @@ -37,6 +37,7 @@ "@luma.gl/shadertools": "~9.2.6", "@luma.gl/webgl": "~9.2.6", "@mapbox/geojson-extent": "^1.0.1", + "@mapbox/tiny-sdf": "^2.0.7", "@math.gl/web-mercator": "^4.1.0", "@types/d3-array": "^3.2.2", "@types/geojson": "^7946.0.16", @@ -65,6 +66,7 @@ "@apache-superset/core": "*", "@superset-ui/chart-controls": "*", "@superset-ui/core": "*", + "@superset-ui/glyph-core": "*", "dayjs": "^1.11.19", "mapbox-gl": ">=1.0.0", "react": "^18.2.0", diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Geojson/Geojson.test.ts b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Geojson/Geojson.test.ts deleted file mode 100644 index 6f189dc8c58..00000000000 --- a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Geojson/Geojson.test.ts +++ /dev/null @@ -1,122 +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 { SqlaFormData } from '@superset-ui/core'; -import { - computeGeoJsonTextOptionsFromJsOutput, - computeGeoJsonTextOptionsFromFormData, - computeGeoJsonIconOptionsFromJsOutput, - computeGeoJsonIconOptionsFromFormData, -} from './Geojson'; - -jest.mock('react-map-gl/maplibre', () => ({ - __esModule: true, - Map: () => null, - useControl: () => null, -})); - -test('computeGeoJsonTextOptionsFromJsOutput returns an empty object for non-object input', () => { - expect(computeGeoJsonTextOptionsFromJsOutput(null)).toEqual({}); - expect(computeGeoJsonTextOptionsFromJsOutput(42)).toEqual({}); - expect(computeGeoJsonTextOptionsFromJsOutput([1, 2, 3])).toEqual({}); - expect(computeGeoJsonTextOptionsFromJsOutput('string')).toEqual({}); -}); - -test('computeGeoJsonTextOptionsFromJsOutput extracts valid text options from the input object', () => { - const input = { - getText: 'name', - getTextColor: [1, 2, 3, 255], - invalidOption: true, - }; - const expectedOutput = { - getText: 'name', - getTextColor: [1, 2, 3, 255], - }; - expect(computeGeoJsonTextOptionsFromJsOutput(input)).toEqual(expectedOutput); -}); - -test('computeGeoJsonTextOptionsFromFormData computes text options based on form data', () => { - const formData: SqlaFormData = { - label_property_name: 'name', - label_color: { r: 1, g: 2, b: 3, a: 1 }, - label_size: 123, - label_size_unit: 'pixels', - datasource: 'test_datasource', - viz_type: 'deck_geojson', - }; - - const expectedOutput = { - getText: expect.any(Function), - getTextColor: [1, 2, 3, 255], - getTextSize: 123, - textSizeUnits: 'pixels', - }; - - const actualOutput = computeGeoJsonTextOptionsFromFormData(formData); - expect(actualOutput).toEqual(expectedOutput); - - const sampleFeature = { properties: { name: 'Test' } }; - expect(actualOutput.getText(sampleFeature)).toBe('Test'); -}); - -test('computeGeoJsonIconOptionsFromJsOutput returns an empty object for non-object input', () => { - expect(computeGeoJsonIconOptionsFromJsOutput(null)).toEqual({}); - expect(computeGeoJsonIconOptionsFromJsOutput(42)).toEqual({}); - expect(computeGeoJsonIconOptionsFromJsOutput([1, 2, 3])).toEqual({}); - expect(computeGeoJsonIconOptionsFromJsOutput('string')).toEqual({}); -}); - -test('computeGeoJsonIconOptionsFromJsOutput extracts valid icon options from the input object', () => { - const input = { - getIcon: 'icon_name', - getIconColor: [1, 2, 3, 255], - invalidOption: false, - }; - - const expectedOutput = { - getIcon: 'icon_name', - getIconColor: [1, 2, 3, 255], - }; - - expect(computeGeoJsonIconOptionsFromJsOutput(input)).toEqual(expectedOutput); -}); - -test('computeGeoJsonIconOptionsFromFormData computes icon options based on form data', () => { - const formData: SqlaFormData = { - icon_url: 'https://example.com/icon.png', - icon_size: 123, - icon_size_unit: 'pixels', - datasource: 'test_datasource', - viz_type: 'deck_geojson', - }; - - const expectedOutput = { - getIcon: expect.any(Function), - getIconSize: 123, - iconSizeUnits: 'pixels', - }; - - const actualOutput = computeGeoJsonIconOptionsFromFormData(formData); - expect(actualOutput).toEqual(expectedOutput); - - expect(actualOutput.getIcon()).toEqual({ - url: 'https://example.com/icon.png', - height: 128, - width: 128, - }); -}); diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Path/Path.test.tsx b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Path/Path.test.tsx deleted file mode 100644 index b297c425843..00000000000 --- a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Path/Path.test.tsx +++ /dev/null @@ -1,119 +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. - */ -// eslint-disable-next-line import/no-extraneous-dependencies -import '@testing-library/jest-dom'; -import { getLayer, getPoints, getHighlightLayer } from './Path'; - -jest.mock('../../DeckGLContainer', () => ({ - DeckGLContainerStyledWrapper: ({ children }: any) => ( -
{children}
- ), -})); - -jest.mock('../../factory', () => ({ - createDeckGLComponent: jest.fn(() => () => null), - GetLayerType: {}, -})); - -const mockFormData = { - datasource: 'test_datasource', - viz_type: 'deck_path', - color_picker: { r: 0, g: 122, b: 135, a: 1 }, - line_width: 150, - line_width_unit: 'meters', - slice_id: 1, -}; - -const mockPayload = { - data: { - features: [ - { - path: [ - [-122.4, 37.8], - [-122.5, 37.9], - ], - }, - ], - }, -}; - -test('getLayer uses line_width_unit from formData', () => { - const layer = getLayer({ - formData: mockFormData, - payload: mockPayload, - onContextMenu: jest.fn(), - filterState: undefined, - setDataMask: jest.fn(), - setTooltip: jest.fn(), - emitCrossFilters: false, - }); - - expect(layer.props.widthUnits).toBe('meters'); -}); - -test('getLayer uses pixels when line_width_unit is pixels', () => { - const layer = getLayer({ - formData: { ...mockFormData, line_width_unit: 'pixels' }, - payload: mockPayload, - onContextMenu: jest.fn(), - filterState: undefined, - setDataMask: jest.fn(), - setTooltip: jest.fn(), - emitCrossFilters: false, - }); - - expect(layer.props.widthUnits).toBe('pixels'); -}); - -test('getHighlightLayer uses line_width_unit from formData', () => { - const layer = getHighlightLayer({ - formData: mockFormData, - payload: mockPayload, - filterState: { value: [] }, - onContextMenu: jest.fn(), - setDataMask: jest.fn(), - setTooltip: jest.fn(), - emitCrossFilters: false, - }); - - expect(layer.props.widthUnits).toBe('meters'); -}); - -test('getPoints extracts points from path data', () => { - const data = [ - { - path: [ - [0, 0], - [1, 1], - ], - }, - { - path: [ - [2, 2], - [3, 3], - ], - }, - ]; - - const points = getPoints(data); - - expect(points).toHaveLength(4); - expect(points[0]).toEqual([0, 0]); - expect(points[2]).toEqual([2, 2]); -}); diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Polygon/Polygon.test.tsx b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Polygon/Polygon.test.tsx deleted file mode 100644 index d107dc42771..00000000000 --- a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Polygon/Polygon.test.tsx +++ /dev/null @@ -1,355 +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. - */ -// eslint-disable-next-line import/no-extraneous-dependencies -import { render, screen } from '@testing-library/react'; -// eslint-disable-next-line import/no-extraneous-dependencies -import '@testing-library/jest-dom'; -import { supersetTheme, ThemeProvider } from '@apache-superset/core/theme'; -import DeckGLPolygon, { getPoints } from './Polygon'; -import { COLOR_SCHEME_TYPES } from '../../utilities/utils'; -import * as utils from '../../utils'; - -// Mock the utils functions -const mockGetBuckets = jest.spyOn(utils, 'getBuckets'); -const mockGetColorBreakpointsBuckets = jest.spyOn( - utils, - 'getColorBreakpointsBuckets', -); - -// Mock DeckGL container and Legend -jest.mock('../../DeckGLContainer', () => ({ - DeckGLContainerStyledWrapper: ({ children }: any) => ( -
{children}
- ), -})); - -jest.mock('../../components/Legend', () => ({ categories, position }: any) => ( -
- Legend Mock -
-)); - -const mockProps = { - formData: { - // Required QueryFormData properties - datasource: 'test_datasource', - viz_type: 'deck_polygon', - // Polygon-specific properties - metric: { label: 'population' }, - color_scheme_type: COLOR_SCHEME_TYPES.linear_palette, - legend_position: 'tr', - legend_format: '.2f', - autozoom: false, - map_style: 'mapbox://styles/mapbox/light-v9', - opacity: 80, - filled: true, - stroked: true, - extruded: false, - line_width: 1, - line_width_unit: 'pixels', - multiplier: 1, - break_points: [], - num_buckets: '5', - linear_color_scheme: 'blue_white_yellow', - }, - payload: { - data: { - features: [ - { - population: 100000, - polygon: [ - [0, 0], - [1, 0], - [1, 1], - [0, 1], - ], - }, - { - population: 200000, - polygon: [ - [2, 2], - [3, 2], - [3, 3], - [2, 3], - ], - }, - ], - mapboxApiKey: 'test-key', - }, - form_data: {}, - }, - setControlValue: jest.fn(), - viewport: { longitude: 0, latitude: 0, zoom: 1 }, - onAddFilter: jest.fn(), - width: 800, - height: 600, - onContextMenu: jest.fn(), - setDataMask: jest.fn(), - filterState: undefined, - emitCrossFilters: false, -}; - -describe('DeckGLPolygon bucket generation logic', () => { - beforeEach(() => { - jest.clearAllMocks(); - mockGetBuckets.mockReturnValue({ - '100000 - 150000': { color: [0, 100, 200], enabled: true }, - '150000 - 200000': { color: [50, 150, 250], enabled: true }, - }); - mockGetColorBreakpointsBuckets.mockReturnValue({}); - }); - - const renderWithTheme = (component: React.ReactElement) => - render({component}); - - test('should use getBuckets for linear_palette color scheme', () => { - const propsWithLinearPalette = { - ...mockProps, - formData: { - ...mockProps.formData, - color_scheme_type: COLOR_SCHEME_TYPES.linear_palette, - }, - }; - - renderWithTheme(); - - // Should call getBuckets, not getColorBreakpointsBuckets - expect(mockGetBuckets).toHaveBeenCalled(); - expect(mockGetColorBreakpointsBuckets).not.toHaveBeenCalled(); - }); - - test('should use getBuckets for fixed_color color scheme', () => { - const propsWithFixedColor = { - ...mockProps, - formData: { - ...mockProps.formData, - color_scheme_type: COLOR_SCHEME_TYPES.fixed_color, - }, - }; - - renderWithTheme(); - - // Should call getBuckets, not getColorBreakpointsBuckets - expect(mockGetBuckets).toHaveBeenCalled(); - expect(mockGetColorBreakpointsBuckets).not.toHaveBeenCalled(); - }); - - test('should use getColorBreakpointsBuckets for color_breakpoints scheme', () => { - const propsWithBreakpoints = { - ...mockProps, - formData: { - ...mockProps.formData, - color_scheme_type: COLOR_SCHEME_TYPES.color_breakpoints, - color_breakpoints: [ - { - minValue: 0, - maxValue: 100000, - color: { r: 255, g: 0, b: 0, a: 100 }, - }, - { - minValue: 100001, - maxValue: 200000, - color: { r: 0, g: 255, b: 0, a: 100 }, - }, - ], - }, - }; - - mockGetColorBreakpointsBuckets.mockReturnValue({ - '0 - 100000': { color: [255, 0, 0], enabled: true }, - '100001 - 200000': { color: [0, 255, 0], enabled: true }, - }); - - renderWithTheme(); - - // Should call getColorBreakpointsBuckets, not getBuckets - expect(mockGetColorBreakpointsBuckets).toHaveBeenCalled(); - expect(mockGetBuckets).not.toHaveBeenCalled(); - }); - - test('should use getBuckets when color_scheme_type is undefined (backward compatibility)', () => { - const propsWithUndefinedScheme = { - ...mockProps, - formData: { - ...mockProps.formData, - color_scheme_type: undefined, - }, - }; - - renderWithTheme(); - - // Should call getBuckets for backward compatibility - expect(mockGetBuckets).toHaveBeenCalled(); - expect(mockGetColorBreakpointsBuckets).not.toHaveBeenCalled(); - }); - - test('should use getBuckets for unsupported color schemes (categorical_palette)', () => { - const propsWithUnsupportedScheme = { - ...mockProps, - formData: { - ...mockProps.formData, - color_scheme_type: COLOR_SCHEME_TYPES.categorical_palette, - }, - }; - - renderWithTheme(); - - // Should fall back to getBuckets for unsupported color schemes - expect(mockGetBuckets).toHaveBeenCalled(); - expect(mockGetColorBreakpointsBuckets).not.toHaveBeenCalled(); - }); -}); - -describe('DeckGLPolygon Error Handling and Edge Cases', () => { - beforeEach(() => { - jest.clearAllMocks(); - mockGetBuckets.mockReturnValue({}); - mockGetColorBreakpointsBuckets.mockReturnValue({}); - }); - - const renderWithTheme = (component: React.ReactElement) => - render({component}); - - test('handles empty features data gracefully', () => { - const propsWithEmptyData = { - ...mockProps, - payload: { - ...mockProps.payload, - data: { - ...mockProps.payload.data, - features: [], - }, - }, - }; - - renderWithTheme(); - - // Should still call getBuckets with empty data - expect(mockGetBuckets).toHaveBeenCalled(); - expect(mockGetColorBreakpointsBuckets).not.toHaveBeenCalled(); - }); - - test('handles missing color_breakpoints for color_breakpoints scheme', () => { - const propsWithMissingBreakpoints = { - ...mockProps, - formData: { - ...mockProps.formData, - color_scheme_type: COLOR_SCHEME_TYPES.color_breakpoints, - color_breakpoints: undefined, - }, - }; - - renderWithTheme(); - - // Should call getColorBreakpointsBuckets even with undefined breakpoints - expect(mockGetColorBreakpointsBuckets).toHaveBeenCalledWith(undefined); - expect(mockGetBuckets).not.toHaveBeenCalled(); - }); - - test('handles null legend_position correctly', () => { - const propsWithNullLegendPosition = { - ...mockProps, - formData: { - ...mockProps.formData, - legend_position: null, - }, - }; - - renderWithTheme(); - - // Legend should not be rendered when position is null - expect(screen.queryByTestId('legend')).not.toBeInTheDocument(); - }); -}); - -describe('DeckGLPolygon Legend Integration', () => { - beforeEach(() => { - jest.clearAllMocks(); - mockGetBuckets.mockReturnValue({ - '100000 - 150000': { color: [0, 100, 200], enabled: true }, - '150000 - 200000': { color: [50, 150, 250], enabled: true }, - }); - }); - - const renderWithTheme = (component: React.ReactElement) => - render({component}); - - test('renders legend with non-empty categories when metric and linear_palette are defined', () => { - const { container } = renderWithTheme(); - - // Verify the component renders and calls the correct bucket function - expect(mockGetBuckets).toHaveBeenCalled(); - expect(mockGetColorBreakpointsBuckets).not.toHaveBeenCalled(); - - // Verify the legend mock was rendered with non-empty categories - const legendElement = container.querySelector('[data-testid="legend"]'); - expect(legendElement).toBeTruthy(); - const categoriesAttr = legendElement?.getAttribute('data-categories'); - const categoriesData = JSON.parse(categoriesAttr || '{}'); - expect(Object.keys(categoriesData)).toHaveLength(2); - }); - - test('does not render legend when metric is null', () => { - const propsWithoutMetric = { - ...mockProps, - formData: { - ...mockProps.formData, - metric: null, - }, - }; - - renderWithTheme(); - - // Legend should not be rendered when no metric is defined - expect(screen.queryByTestId('legend')).not.toBeInTheDocument(); - }); -}); - -describe('getPoints utility', () => { - test('extracts points from polygon data', () => { - const data = [ - { - polygon: [ - [0, 0], - [1, 0], - [1, 1], - [0, 1], - ], - }, - { - polygon: [ - [2, 2], - [3, 2], - [3, 3], - [2, 3], - ], - }, - ]; - - const points = getPoints(data); - - expect(points).toHaveLength(8); // 4 points per polygon * 2 polygons - expect(points[0]).toEqual([0, 0]); - expect(points[4]).toEqual([2, 2]); - }); -}); diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/utilities/Shared_DeckGL.tsx b/superset-frontend/plugins/preset-chart-deckgl/src/utilities/Shared_DeckGL.tsx index 490e1367be4..bb8629229af 100644 --- a/superset-frontend/plugins/preset-chart-deckgl/src/utilities/Shared_DeckGL.tsx +++ b/superset-frontend/plugins/preset-chart-deckgl/src/utilities/Shared_DeckGL.tsx @@ -713,3 +713,14 @@ export const generateDeckGLColorSchemeControls = ({ [breakpointsDefaultColor], [deckGLColorBreakpointsSelect], ]; + +// ─── Shared section definitions ────────────────────────────────────────────── + +/** + * Standard "Advanced" section shared by all DeckGL layers. + * Contains JS customization controls (columns, data mutator, tooltip, onclick). + */ +export const DECKGL_ADVANCED_SECTION = { + label: t('Advanced'), + controlSetRows: [[jsColumns], [jsDataMutator], [jsTooltip], [jsOnclickHref]], +}; diff --git a/superset-frontend/scripts/check-custom-rules.js b/superset-frontend/scripts/check-custom-rules.js index e9c6eaca924..5cb288f398c 100755 --- a/superset-frontend/scripts/check-custom-rules.js +++ b/superset-frontend/scripts/check-custom-rules.js @@ -65,10 +65,9 @@ function hasEslintDisable(path, ruleName = 'theme-colors/no-literal-colors') { if (hasDisable) return true; } - // Check if parent is a statement with leading comments - let current = path; - while (current.parent) { - current = current.parent; + // Walk up the ancestor chain using parentPath (NodePath, not raw node) + let current = path.parentPath; + while (current) { if (current.node && current.node.leadingComments) { const hasDisable = current.node.leadingComments.some( comment => @@ -78,6 +77,7 @@ function hasEslintDisable(path, ruleName = 'theme-colors/no-literal-colors') { ); if (hasDisable) return true; } + current = current.parentPath; } return false; diff --git a/superset-frontend/src/components/Chart/ChartRenderer.tsx b/superset-frontend/src/components/Chart/ChartRenderer.tsx index b5fa33c04c1..2e90be4e6b3 100644 --- a/superset-frontend/src/components/Chart/ChartRenderer.tsx +++ b/superset-frontend/src/components/Chart/ChartRenderer.tsx @@ -513,10 +513,16 @@ class ChartRenderer extends Component { ownState?.agGridFilterModel && Object.keys(ownState.agGridFilterModel).length > 0; - const currentFormDataExtended = currentFormData as JsonObject; + // Check if chart allows empty results (e.g., for drag-and-drop configuration) + const chartMetadata = getChartMetadataRegistry().get(vizType); + const allowsEmptyResults = chartMetadata?.behaviors?.includes( + Behavior.AllowsEmptyResults, + ); + const bypassNoResult = !( - currentFormDataExtended?.server_pagination && - (hasSearchText || hasAgGridFilters) + ((currentFormData as JsonObject)?.server_pagination && + (hasSearchText || hasAgGridFilters)) || + allowsEmptyResults ); return ( diff --git a/superset-frontend/src/explore/components/ControlPanelsContainer.tsx b/superset-frontend/src/explore/components/ControlPanelsContainer.tsx index fa7719bd521..a1dbacd4789 100644 --- a/superset-frontend/src/explore/components/ControlPanelsContainer.tsx +++ b/superset-frontend/src/explore/components/ControlPanelsContainer.tsx @@ -71,6 +71,8 @@ import Tabs from '@superset-ui/core/components/Tabs'; import { PluginContext } from 'src/components'; import { useConfirmModal } from 'src/hooks/useConfirmModal'; +import { type ChartArguments } from '@superset-ui/glyph-core'; +import GlyphOptionsPanel from './GlyphOptionsPanel'; import { getSectionsToRender } from 'src/explore/controlUtils'; import { ExploreActions } from 'src/explore/actions/exploreActions'; import { ChartState, ExplorePageState } from 'src/explore/types'; @@ -930,6 +932,27 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => { return ; } + // For glyph-core charts, _glyphArgs holds the raw arg definitions from defineChart(). + // We use them to render the Chart Options section natively (no ControlPanelConfig + // string expansion, no Redux controls-state for values or visibility). + const currentConfig = controlPanelRegistry.get(form_data.viz_type) as { + _glyphArgs?: unknown; + } | null; + const glyphArgs = currentConfig?._glyphArgs as ChartArguments | undefined; + + const CHART_OPTIONS_LABEL = t('Chart Options'); + // The auto-generated Chart Options section (rendered by GlyphOptionsPanel when present) + const glyphChartOptionsSection = glyphArgs + ? (customizeSections.find(s => s.label === CHART_OPTIONS_LABEL) as + | ExpandedControlPanelSectionConfig + | undefined) + : undefined; + // Sections in the Customize tab other than the Chart Options one + // (e.g. additionalSections with custom tabOverrides, Time Comparison, etc.) + const remainingCustomizeSections = glyphArgs + ? customizeSections.filter(s => s.label !== CHART_OPTIONS_LABEL) + : customizeSections; + return ( <> @@ -962,13 +985,33 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => { key: TABS_KEYS.CUSTOMIZE, label: t('Customize'), children: ( - + <> + {glyphArgs && glyphChartOptionsSection ? ( + // Glyph-core charts: hybrid rendering for Chart Options. + // Glyph args: value + visibility from formData directly. + // Additional controls (additionalControls.chartOptions): existing path. + + ) : null} + {remainingCustomizeSections.length > 0 && ( + + )} + ), }, ] diff --git a/superset-frontend/src/explore/components/ExploreChartPanel/index.tsx b/superset-frontend/src/explore/components/ExploreChartPanel/index.tsx index 697e5ef419d..c538e06a016 100644 --- a/superset-frontend/src/explore/components/ExploreChartPanel/index.tsx +++ b/superset-frontend/src/explore/components/ExploreChartPanel/index.tsx @@ -21,6 +21,7 @@ import { useDispatch, useSelector } from 'react-redux'; import Split from 'react-split'; import { t } from '@apache-superset/core/translation'; import { + Behavior, DatasourceType, ensureIsArray, isFeatureEnabled, @@ -226,16 +227,33 @@ const ExploreChartPanel = ({ const [showDatasetModal, setShowDatasetModal] = useState(false); const metaDataRegistry = getChartMetadataRegistry(); - const { useLegacyApi } = metaDataRegistry.get(vizType) ?? {}; + const chartMetadata = metaDataRegistry.get(vizType); + const { useLegacyApi } = chartMetadata ?? {}; const vizTypeNeedsDataset = useLegacyApi && datasource.type !== DatasourceType.Table; + + // Check if chart allows empty results (for drag-and-drop configuration) + const allowsEmptyResults = chartMetadata?.behaviors?.includes( + Behavior.AllowsEmptyResults, + ); + // Check if query returned no actual data rows + const hasNoDataRows = + ensureIsArray(chart.queriesResponse).length > 0 && + chart.queriesResponse?.every( + response => !response?.data || response.data.length === 0, + ); + // Suppress stale warning for AllowsEmptyResults charts with no data + // (they're in initial unconfigured state) + const isUnconfiguredEmptyChart = allowsEmptyResults && hasNoDataRows; + // added boolean column to below show boolean so that the errors aren't overlapping const showAlertBanner = !chartAlert && chartIsStale && !vizTypeNeedsDataset && chart.chartStatus !== 'failed' && - ensureIsArray(chart.queriesResponse).length > 0; + ensureIsArray(chart.queriesResponse).length > 0 && + !isUnconfiguredEmptyChart; const updateQueryContext = useCallback( async function fetchChartData() { diff --git a/superset-frontend/src/explore/components/ExploreViewContainer/index.tsx b/superset-frontend/src/explore/components/ExploreViewContainer/index.tsx index baa5b947314..d476baf5a44 100644 --- a/superset-frontend/src/explore/components/ExploreViewContainer/index.tsx +++ b/superset-frontend/src/explore/components/ExploreViewContainer/index.tsx @@ -579,15 +579,27 @@ function ExploreViewContainer(props: ExploreViewContainerProps) { } }, [isDynamicPluginLoading]); + // Track if we've already triggered initial query + const [hasTriggeredInitialQuery, setHasTriggeredInitialQuery] = + useState(false); + + // Auto-trigger query when there are no validation errors + // This effect runs on mount and when controls change (e.g., after dynamic plugin loads) useEffect(() => { + // Skip if already triggered or still loading dynamic plugin + if (hasTriggeredInitialQuery || isDynamicPluginLoading) { + return; + } + const hasError = Object.values(props.controls).some( control => control.validationErrors && control.validationErrors.length > 0, ); if (!hasError) { props.actions.triggerQuery(true, props.chart.id); + setHasTriggeredInitialQuery(true); } - }, []); + }, [props.controls, isDynamicPluginLoading, hasTriggeredInitialQuery]); const reRenderChart = useCallback( (controlsChanged?: string[]) => { diff --git a/superset-frontend/src/explore/components/GlyphOptionsPanel.tsx b/superset-frontend/src/explore/components/GlyphOptionsPanel.tsx new file mode 100644 index 00000000000..69cf5053daf --- /dev/null +++ b/superset-frontend/src/explore/components/GlyphOptionsPanel.tsx @@ -0,0 +1,223 @@ +/** + * 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. + */ + +/** + * GlyphOptionsPanel - Native React renderer for the Customize tab of glyph-core charts. + * + * Uses the fully-processed "Chart Options" section (from getSectionsToRender) as the + * authoritative list of controls and their order. For each control: + * + * - If the control name appears in `_glyphArgs`: rendered natively — value from + * formData directly, visibility from evaluateGlyphCondition(visibleWhen, formData), + * no Redux controls-state dependency. + * + * - Otherwise (additionalControls.chartOptions entries): rendered via the existing + * `renderControl()` callback, which reads values from Redux controls state and + * evaluates legacy visibility() functions. Full compatibility preserved. + * + * This hybrid strategy means every chart — even ones with many hand-crafted additional + * controls in the Customize tab (Timeseries, BigNumber, MixedTimeseries, etc.) — works + * correctly while glyph-defined args get the simplified formData-based path. + */ + +import { ReactNode } from 'react'; +import { t } from '@apache-superset/core/translation'; +import { JsonValue, QueryFormData } from '@superset-ui/core'; +import { + ControlState, + ControlType, + CustomControlItem, + isCustomControlItem, +} from '@superset-ui/chart-controls'; +import { + type ArgDef, + type ChartArguments, + evaluateGlyphCondition, + getArgVisibleWhen, + getGlyphControlConfig, + isDimensionArg, + isMetricArg, + isTemporalArg, + resolveArgClass, +} from '@superset-ui/glyph-core'; +import { Collapse } from '@superset-ui/core/components'; +import { ExploreActions } from 'src/explore/actions/exploreActions'; +import Control from './Control'; +import ControlRow from './ControlRow'; +import StashFormDataContainer from './StashFormDataContainer'; +import { ExpandedControlPanelSectionConfig } from './ControlPanelsContainer'; + +interface GlyphOptionsPanelProps { + /** Raw glyph arg definitions from defineChart() (_glyphArgs on ControlPanelConfig) */ + glyphArgs: ChartArguments; + /** + * The fully-processed "Chart Options" section from getSectionsToRender. + * This is the source of truth for control order and includes both glyph args + * AND any additionalControls.chartOptions entries. + */ + chartOptionsSection: ExpandedControlPanelSectionConfig; + formData: QueryFormData; + /** Redux controls state — used for validation errors on non-glyph controls */ + controls: Record; + actions: Pick; + /** Existing renderer for non-glyph additional controls */ + renderControl: (item: CustomControlItem) => ReactNode; + defaultExpandedKeys?: string[]; +} + +/** + * Render a single glyph arg control natively (value + visibility from formData). + */ +function GlyphArgControl({ + name, + argDef, + formData, + controls, + actions, +}: { + name: string; + argDef: ArgDef; + formData: QueryFormData; + controls: Record; + actions: Pick; +}) { + const formDataRecord = formData as Record; + const argClass = resolveArgClass(argDef); + const visibleWhen = getArgVisibleWhen(argDef); + const controlConfig = getGlyphControlConfig(argClass, name) as Record< + string, + unknown + > & { type: string }; + + const isVisible = visibleWhen + ? evaluateGlyphCondition(visibleWhen, formDataRecord) + : undefined; + + const { type, label, description, ...restConfig } = controlConfig; + const validationErrors = controls[name]?.validationErrors ?? []; + + return ( + + )} + /> + + ); +} + +/** + * Renders the Chart Options section for a glyph-core chart with hybrid strategy: + * native formData rendering for glyph args, existing renderControl for everything else. + */ +export default function GlyphOptionsPanel({ + glyphArgs, + chartOptionsSection, + formData, + controls, + actions, + renderControl, + defaultExpandedKeys, +}: GlyphOptionsPanelProps) { + // Build the set of arg names that are data args (go in the Query/Data tab, not here) + const dataArgNames = new Set( + Object.entries(glyphArgs) + .filter(([, argDef]) => { + const argClass = resolveArgClass(argDef as ArgDef); + return ( + isMetricArg(argClass) || + isDimensionArg(argClass) || + isTemporalArg(argClass) + ); + }) + .map(([name]) => name), + ); + + const rows = chartOptionsSection.controlSetRows + .map((controlSetRow, rowIndex) => { + const renderedControls = controlSetRow + .map(item => { + if (!item) return null; + + if (isCustomControlItem(item)) { + const { name } = item; + const argDef = glyphArgs[name] as ArgDef | undefined; + + if (argDef && !dataArgNames.has(name)) { + // Native glyph rendering: value from formData, visibility from formData + return ( + + ); + } + + // additionalControls.chartOptions: use existing renderControl path + return renderControl(item); + } + + return null; + }) + .filter(Boolean) as React.ReactElement[]; + + if (renderedControls.length === 0) return null; + + return ( + + ); + }) + .filter(Boolean) as ReactNode[]; + + if (rows.length === 0) return null; + + return ( + {rows}, + }, + ]} + /> + ); +} diff --git a/superset-frontend/src/explore/controlUtils/getSectionsToRender.ts b/superset-frontend/src/explore/controlUtils/getSectionsToRender.ts index 4185a631163..9918cecd696 100644 --- a/superset-frontend/src/explore/controlUtils/getSectionsToRender.ts +++ b/superset-frontend/src/explore/controlUtils/getSectionsToRender.ts @@ -36,6 +36,7 @@ const getMemoizedSectionsToRender = memoizeOne( sectionOverrides = {}, controlOverrides, controlPanelSections = [], + _glyphArgs, } = controlPanelConfig; // default control panel sections @@ -94,7 +95,22 @@ const getMemoizedSectionsToRender = memoizeOne( typeof control !== 'string' || !invalidControls.includes(control), ) - .map(item => expandControlConfig(item, controlOverrides)), + .map(item => { + // For glyph-core charts (_glyphArgs present), already-resolved + // { name, config } objects skip expandControlConfig: Control.tsx + // resolves string type names itself via controlMap, and these + // objects have no string shortcuts to look up in sharedControls. + if ( + _glyphArgs && + item && + typeof item === 'object' && + 'name' in item && + 'config' in item + ) { + return item; + } + return expandControlConfig(item, controlOverrides); + }), ) || [], }; }) diff --git a/superset-frontend/src/visualizations/presets/MainPreset.ts b/superset-frontend/src/visualizations/presets/MainPreset.ts index d1a12de0fb4..b1a9fc22022 100644 --- a/superset-frontend/src/visualizations/presets/MainPreset.ts +++ b/superset-frontend/src/visualizations/presets/MainPreset.ts @@ -41,10 +41,10 @@ import { } from '@superset-ui/legacy-preset-chart-nvd3'; import { DeckGLChartPreset } from '@superset-ui/preset-chart-deckgl'; import ScatterMapChartPlugin from '@superset-ui/plugin-chart-point-cluster-map'; -import { CartodiagramPlugin } from '@superset-ui/plugin-chart-cartodiagram'; +import { createCartodiagramPlugin } from '@superset-ui/plugin-chart-cartodiagram'; import { BigNumberChartPlugin, - BigNumberTotalChartPlugin, + BigNumberGlyphChartPlugin, EchartsPieChartPlugin, EchartsBoxPlotChartPlugin, EchartsAreaChartPlugin, @@ -84,7 +84,7 @@ import { DeckglLayerVisibilityCustomizationPlugin, } from 'src/chartCustomizations/components'; import { PivotTableChartPlugin as PivotTableChartPluginV2 } from '@superset-ui/plugin-chart-pivot-table'; -import { HandlebarsChartPlugin } from '@superset-ui/plugin-chart-handlebars'; +import HandlebarsChartPlugin from '@superset-ui/plugin-chart-handlebars'; import { ChartCustomizationPlugins, FilterPlugins } from 'src/constants'; import AgGridTableChartPlugin from '@superset-ui/plugin-chart-ag-grid-table'; import TimeTableChartPlugin from '../TimeTable'; @@ -110,7 +110,7 @@ export default class MainPreset extends Preset { presets: [new DeckGLChartPreset()], plugins: [ new BigNumberChartPlugin().configure({ key: VizType.BigNumber }), - new BigNumberTotalChartPlugin().configure({ + new BigNumberGlyphChartPlugin().configure({ key: VizType.BigNumberTotal, }), new EchartsBoxPlotChartPlugin().configure({ key: VizType.BoxPlot }), @@ -196,7 +196,7 @@ export default class MainPreset extends Preset { new EchartsSunburstChartPlugin().configure({ key: VizType.Sunburst }), new HandlebarsChartPlugin().configure({ key: VizType.Handlebars }), new EchartsBubbleChartPlugin().configure({ key: VizType.Bubble }), - new CartodiagramPlugin({ + new (createCartodiagramPlugin({ defaultLayers: [ { type: 'WMS', @@ -208,7 +208,7 @@ export default class MainPreset extends Preset { '© Map data from OpenStreetMap. Service provided by terrestris GmbH & Co. KG', }, ], - }).configure({ key: VizType.Cartodiagram }), + }))().configure({ key: VizType.Cartodiagram }), ...experimentalPlugins, ...agGridTablePlugin, ], diff --git a/superset-frontend/tsconfig.json b/superset-frontend/tsconfig.json index 6a05ebca0d6..c4c3b7dfaf4 100644 --- a/superset-frontend/tsconfig.json +++ b/superset-frontend/tsconfig.json @@ -60,7 +60,9 @@ "@superset-ui/plugin-chart-*": ["./plugins/plugin-chart-*/src"], "@superset-ui/legacy-plugin-chart-*": ["./plugins/legacy-plugin-chart-*/src"], "@superset-ui/legacy-preset-chart-*": ["./plugins/legacy-preset-chart-*/src"], - "echarts/types/src/*": ["./node_modules/echarts/types/src/*"] + "echarts/types/src/*": ["./node_modules/echarts/types/src/*"], + "@superset-ui/glyph-core": ["./packages/superset-ui-glyph-core/src"], + "@superset-ui/glyph-core/*": ["./packages/superset-ui-glyph-core/src/*"] } }, "include": [ @@ -78,6 +80,7 @@ { "path": "./packages/superset-core" }, { "path": "./packages/superset-ui-core" }, { "path": "./packages/superset-ui-chart-controls" }, + { "path": "./packages/superset-ui-glyph-core" }, { "path": "./packages/superset-ui-switchboard" }, { "path": "./plugins/legacy-plugin-chart-calendar" }, { "path": "./plugins/legacy-plugin-chart-chord" }, diff --git a/superset/charts/schemas.py b/superset/charts/schemas.py index e0cee7758c4..5e4fbe6445d 100644 --- a/superset/charts/schemas.py +++ b/superset/charts/schemas.py @@ -1038,6 +1038,15 @@ class ChartDataExtrasSchema(Schema): }, allow_none=True, ) + allow_empty_query = fields.Boolean( + metadata={ + "description": ( + "Allow queries with no metrics, columns, or groupby. " + "Used by charts that support drag-and-drop configuration." + ) + }, + load_default=False, + ) class AnnotationLayerSchema(Schema): diff --git a/superset/models/helpers.py b/superset/models/helpers.py index 7a2668a49ed..021507c03a4 100644 --- a/superset/models/helpers.py +++ b/superset/models/helpers.py @@ -1154,6 +1154,31 @@ class ExploreMixin: # pylint: disable=too-many-public-methods datasource types (Query, SqlaTable, etc.). """ qry_start_dttm = datetime.now() + + # Check if this is an empty query (for drag-and-drop configured charts) + extras = query_obj.get("extras", {}) or {} + metrics = query_obj.get("metrics") or [] + columns = query_obj.get("columns") or [] + groupby = query_obj.get("groupby") or [] + if ( + extras.get("allow_empty_query") + and not metrics + and not columns + and not groupby + ): + # Return empty result without executing any SQL + return QueryResult( + applied_template_filters=[], + applied_filter_columns=[], + rejected_filter_columns=[], + status=QueryStatus.SUCCESS, + df=pd.DataFrame(), + duration=datetime.now() - qry_start_dttm, + query="", + errors=None, + error_message=None, + ) + query_str_ext = self.get_query_str_extended(query_obj) sql = query_str_ext.sql status = QueryStatus.SUCCESS @@ -2755,7 +2780,10 @@ class ExploreMixin: # pylint: disable=too-many-public-methods "and is required by this type of chart" ) ) - if not metrics and not columns and not groupby: + # Allow charts to opt-in to empty queries (for drag-and-drop configuration) + # Note: The actual empty query handling is done in the query() method + allow_empty = extras.get("allow_empty_query", False) + if not metrics and not columns and not groupby and not allow_empty: raise QueryObjectValidationError(_("Empty query?")) metrics_exprs: list[ColumnElement] = []