diff --git a/.github/stale.yml b/.github/stale.yml index 511784c6ca..6d589b6763 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -1,7 +1,7 @@ # Configuration for probot-stale - https://github.com/probot/stale # Number of days of inactivity before an Issue or Pull Request becomes stale -daysUntilStale: 730 # two years +daysUntilStale: 731 # two years # Number of days of inactivity before an Issue or Pull Request with the stale label is closed. # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. diff --git a/.gitignore b/.gitignore index 5787067b4a..ba75419260 100644 --- a/.gitignore +++ b/.gitignore @@ -203,4 +203,6 @@ todo /components.d.ts /renderers.js /renderers.d.ts +/features.js +/features.d.ts *.tgz diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 92ba606f9d..52654df348 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,7 +13,7 @@ Contributions can be made in varied ways: - Remind the authors to provide a demo if they are reporting for a bug - Try to reproduce the problem as describe in the issues - Make pull requests to fix bugs or implement new features -- Mend or translate the documents +- Improve or translate the documents - Discuss in the [mailing list](https://echarts.apache.org/en/maillist.html) - ... @@ -21,7 +21,7 @@ Contributions can be made in varied ways: When opening new issues, please use the [echarts issue helper](https://ecomfe.github.io/echarts-issue-helper/), opening issues in any other way will cause our bot to close them automatically. -And before doing so, please search for similar questions in our [issues list](https://github.com/apache/echarts/issues?utf8=%E2%9C%93&q=is%3Aissue). If you are able to reproduce an issue found in a closed issue, please create a new issue and reference the closed one. +Additionally, before doing so, please search for similar questions in our [issues list](https://github.com/apache/echarts/issues?utf8=%E2%9C%93&q=is%3Aissue). If you are able to reproduce an issue found in a closed issue, please create a new issue and reference the closed one. Please read the [documentation](http://echarts.apache.org/option.html) carefully before asking any questions. @@ -29,9 +29,9 @@ Any questions in the form of *how can I use echarts to* or *how to use echarts x ## Release Milestone Discussion -We will start the discussion about the bugs to fix and features of each release in the [mailing list](https://echarts.apache.org/en/maillist.html). You may subscribe our [mailing list](https://echarts.apache.org/en/maillist.html) to give your valuable advice in the milestone dicussion. +We will start the discussion about the bugs to fix and features of each release in the [mailing list](https://echarts.apache.org/en/maillist.html). You may subscribe to our [mailing list](https://echarts.apache.org/en/maillist.html) to give your valuable advice in milestone dicussions. -About our release plan, we will release a mior version at the end of every month. Here is some detail. +Regarding the release plan, we will release a mior version at the end of every month. Here is some detail. 1. Assume our current stable release is 4.3.0. We will start the discussion of milestone of the release two versions ahead, which is 4.5.0 at the beginning of each month. At this time we should also kickoff the developing of the next release, which is 4.4.0. 2. Finish 4.4.0 developing at about 22th of this month and start the testing. And the 4.5.0 milestone discussion is frozen and published on the [GitHub](https://github.com/apache/echarts/milestone/14) @@ -61,9 +61,9 @@ Wiki: [How to setup the dev environment](https://github.com/apache/echarts/wiki/ + https://www.apache.org/legal/src-headers.html#3party + Licenses that are compatible with the Apache license: + BSD and MIT are compatible with the Apache license but CC_BY_SA is not (https://apache.org/legal/resolved.html#cc-sa). -+ Stack overflow: - + before intending to copy code from Stack overlow, we must check: ++ Stack Overflow: + + before intending to copy code from Stack Overlow, we must check: + https://apache.org/legal/resolved.html#stackoverflow + https://issues.apache.org/jira/browse/LEGAL-471 -+ Wikipedia: - + Wikipedia is licensed CC 4.0 BY_SA and is incompatible with the Apache license. So we should not copy code from Wikipedia. ++ Wikipedia (and most Wikimedia Foundation projects): + + Wikipedia, and most Wikimedia Foundation projects, are licensed under CC 4.0 BY_SA (and sometimes GFDL) and is incompatible with the Apache license. Therefore, we should not copy code from Wikipedia, or Wikimedia Foundation projects. diff --git a/README.md b/README.md index 5618fad9d7..e8099e323e 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ Apache ECharts is a free, powerful charting and visualization library offering a You may choose one of the following methods: -+ Download from Official Website in [中文下载页](https://echarts.apache.org/zh/download.html) -+ Download from Official Website in [English](https://echarts.apache.org/en/download.html) ++ Download from the official website in [中文下载页](https://echarts.apache.org/zh/download.html) ++ Download from the official website in [English](https://echarts.apache.org/en/download.html) + `npm install echarts --save` + CDN: [jsDelivr CDN](https://www.jsdelivr.com/package/npm/echarts?path=dist) @@ -37,7 +37,7 @@ You may choose one of the following methods: + [GitHub Issues](https://github.com/apache/echarts/issues) for bug report and feature requests + Email [dev@echarts.apache.org](mailto:dev@echarts.apache.org) for general questions -+ Subscribe [mailing list](https://echarts.apache.org/en/maillist.html) to get updated with the project ++ Subscribe to the [mailing list](https://echarts.apache.org/en/maillist.html) to get updated with the project ## Build @@ -66,7 +66,7 @@ More custom build approaches can be checked in this tutorial: [Create Custom Bui ## Contribution -If you wish to debug locally or make pull requests, please refer to [contributing](https://github.com/apache/echarts/blob/master/CONTRIBUTING.md) document. +If you wish to debug locally or make pull requests, please refer to the [contributing](https://github.com/apache/echarts/blob/master/CONTRIBUTING.md) document. ## Resources diff --git a/build/pre-publish.js b/build/pre-publish.js index 0c5e3507cf..cca4b54424 100644 --- a/build/pre-publish.js +++ b/build/pre-publish.js @@ -345,7 +345,7 @@ async function bundleDTS() { // Bundle chunks. const parts = [ - 'core', 'charts', 'components', 'renderers', 'option' + 'core', 'charts', 'components', 'renderers', 'option', 'features' ]; const inputs = {}; parts.forEach(partName => { @@ -387,7 +387,7 @@ function readTSConfig() { function generateEntries() { - ['charts', 'components', 'renderers', 'core'].forEach(entryName => { + ['charts', 'components', 'renderers', 'core', 'features'].forEach(entryName => { if (entryName !== 'option') { const jsCode = fs.readFileSync(nodePath.join(__dirname, `template/${entryName}.js`), 'utf-8'); fs.writeFileSync(nodePath.join(__dirname, `../${entryName}.js`), jsCode, 'utf-8'); diff --git a/build/template/features.d.ts b/build/template/features.d.ts new file mode 100644 index 0000000000..0e9a6fce1c --- /dev/null +++ b/build/template/features.d.ts @@ -0,0 +1,20 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT 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 * from './types/dist/features'; \ No newline at end of file diff --git a/build/template/features.js b/build/template/features.js new file mode 100644 index 0000000000..862d600af5 --- /dev/null +++ b/build/template/features.js @@ -0,0 +1,20 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT 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 * from './lib/export/features'; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f8d2f8fe65..ccd6a3e810 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1583,9 +1583,9 @@ } }, "@types/json-schema": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz", - "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==", + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", + "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", "dev": true }, "@types/node": { @@ -1649,44 +1649,192 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.13.0.tgz", - "integrity": "sha512-ygqDUm+BUPvrr0jrXqoteMqmIaZ/bixYOc3A4BRwzEPTZPi6E+n44rzNZWaB0YvtukgP+aoj0i/fyx7FkM2p1w==", + "version": "4.29.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.29.2.tgz", + "integrity": "sha512-x4EMgn4BTfVd9+Z+r+6rmWxoAzBaapt4QFqE+d8L8sUtYZYLDTK6VG/y/SMMWA5t1/BVU5Kf+20rX4PtWzUYZg==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "4.13.0", - "@typescript-eslint/scope-manager": "4.13.0", - "debug": "^4.1.1", + "@typescript-eslint/experimental-utils": "4.29.2", + "@typescript-eslint/scope-manager": "4.29.2", + "debug": "^4.3.1", "functional-red-black-tree": "^1.0.1", - "lodash": "^4.17.15", - "regexpp": "^3.0.0", - "semver": "^7.3.2", - "tsutils": "^3.17.1" + "regexpp": "^3.1.0", + "semver": "^7.3.5", + "tsutils": "^3.21.0" }, "dependencies": { + "@typescript-eslint/scope-manager": { + "version": "4.29.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.29.2.tgz", + "integrity": "sha512-mfHmvlQxmfkU8D55CkZO2sQOueTxLqGvzV+mG6S/6fIunDiD2ouwsAoiYCZYDDK73QCibYjIZmGhpvKwAB5BOA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.29.2", + "@typescript-eslint/visitor-keys": "4.29.2" + } + }, + "@typescript-eslint/types": { + "version": "4.29.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.29.2.tgz", + "integrity": "sha512-K6ApnEXId+WTGxqnda8z4LhNMa/pZmbTFkDxEBLQAbhLZL50DjeY0VIDCml/0Y3FlcbqXZrABqrcKxq+n0LwzQ==", + "dev": true + }, + "@typescript-eslint/visitor-keys": { + "version": "4.29.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.29.2.tgz", + "integrity": "sha512-bDgJLQ86oWHJoZ1ai4TZdgXzJxsea3Ee9u9wsTAvjChdj2WLcVsgWYAPeY7RQMn16tKrlQaBnpKv7KBfs4EQag==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.29.2", + "eslint-visitor-keys": "^2.0.0" + } + }, + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, "semver": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", - "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", "dev": true, "requires": { "lru-cache": "^6.0.0" } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } } } }, "@typescript-eslint/experimental-utils": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.13.0.tgz", - "integrity": "sha512-/ZsuWmqagOzNkx30VWYV3MNB/Re/CGv/7EzlqZo5RegBN8tMuPaBgNK6vPBCQA8tcYrbsrTdbx3ixMRRKEEGVw==", + "version": "4.29.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.29.2.tgz", + "integrity": "sha512-P6mn4pqObhftBBPAv4GQtEK7Yos1fz/MlpT7+YjH9fTxZcALbiiPKuSIfYP/j13CeOjfq8/fr9Thr2glM9ub7A==", "dev": true, "requires": { - "@types/json-schema": "^7.0.3", - "@typescript-eslint/scope-manager": "4.13.0", - "@typescript-eslint/types": "4.13.0", - "@typescript-eslint/typescript-estree": "4.13.0", - "eslint-scope": "^5.0.0", - "eslint-utils": "^2.0.0" + "@types/json-schema": "^7.0.7", + "@typescript-eslint/scope-manager": "4.29.2", + "@typescript-eslint/types": "4.29.2", + "@typescript-eslint/typescript-estree": "4.29.2", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + }, + "dependencies": { + "@typescript-eslint/scope-manager": { + "version": "4.29.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.29.2.tgz", + "integrity": "sha512-mfHmvlQxmfkU8D55CkZO2sQOueTxLqGvzV+mG6S/6fIunDiD2ouwsAoiYCZYDDK73QCibYjIZmGhpvKwAB5BOA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.29.2", + "@typescript-eslint/visitor-keys": "4.29.2" + } + }, + "@typescript-eslint/types": { + "version": "4.29.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.29.2.tgz", + "integrity": "sha512-K6ApnEXId+WTGxqnda8z4LhNMa/pZmbTFkDxEBLQAbhLZL50DjeY0VIDCml/0Y3FlcbqXZrABqrcKxq+n0LwzQ==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "4.29.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.29.2.tgz", + "integrity": "sha512-TJ0/hEnYxapYn9SGn3dCnETO0r+MjaxtlWZ2xU+EvytF0g4CqTpZL48SqSNn2hXsPolnewF30pdzR9a5Lj3DNg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.29.2", + "@typescript-eslint/visitor-keys": "4.29.2", + "debug": "^4.3.1", + "globby": "^11.0.3", + "is-glob": "^4.0.1", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "4.29.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.29.2.tgz", + "integrity": "sha512-bDgJLQ86oWHJoZ1ai4TZdgXzJxsea3Ee9u9wsTAvjChdj2WLcVsgWYAPeY7RQMn16tKrlQaBnpKv7KBfs4EQag==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.29.2", + "eslint-visitor-keys": "^2.0.0" + } + }, + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + } + }, + "globby": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz", + "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + } + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + } } }, "@typescript-eslint/parser": { @@ -10323,9 +10471,9 @@ } }, "tslib": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz", - "integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==" + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" }, "tslint": { "version": "5.14.0", @@ -10471,9 +10619,9 @@ } }, "typescript": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.2.tgz", - "integrity": "sha512-thGloWsGH3SOxv1SoY7QojKi0tc+8FnOmiarEGMbd/lar7QOEd3hvlx3Fp5y6FlDUGl9L+pd4n2e+oToGMmhRQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", + "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==", "dev": true }, "unbzip2-stream": { @@ -10974,11 +11122,11 @@ } }, "zrender": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.1.1.tgz", - "integrity": "sha512-oeWlmUZPQdS9f5hK4pV21tHPqA3wgQ7CkKkw7l0CCBgWlJ/FP+lRgLFtUBW6yam4JX8y9CdHJo1o587VVrbcoQ==", + "version": "npm:zrender-nightly@5.1.2-dev.20210819", + "resolved": "https://registry.npmjs.org/zrender-nightly/-/zrender-nightly-5.1.2-dev.20210819.tgz", + "integrity": "sha512-iYEp8gxfpvRvEmr6arIpMMsidWhKL9PLWuR6nD8GkR40WV8YDT8XfQgK2kn4EGB+UEHvDJITt+Ps1SupgukmWg==", "requires": { - "tslib": "2.0.3" + "tslib": "2.3.0" } } } diff --git a/package.json b/package.json index 08d679e37d..12b24afc3f 100644 --- a/package.json +++ b/package.json @@ -63,8 +63,8 @@ "lint:dist": "echo 'It might take a while. Please wait ...' && npx jshint --config .jshintrc-dist dist/echarts.js" }, "dependencies": { - "tslib": "2.0.3", - "zrender": "5.1.1" + "tslib": "2.3.0", + "zrender": "npm:zrender-nightly@^5.1.2-dev.20210720" }, "devDependencies": { "@babel/code-frame": "7.10.4", @@ -78,7 +78,7 @@ "@rollup/plugin-node-resolve": "^11.0.0", "@rollup/plugin-replace": "^2.3.4", "@types/jest": "^26.0.14", - "@typescript-eslint/eslint-plugin": "^4.9.1", + "@typescript-eslint/eslint-plugin": "^4.29.2", "@typescript-eslint/parser": "^4.9.1", "chalk": "^3.0.0", "commander": "2.11.0", @@ -106,6 +106,6 @@ "socket.io": "2.2.0", "terser": "^5.3.8", "ts-jest": "^26.4.3", - "typescript": "4.1.2" + "typescript": "4.3.5" } } diff --git a/src/animation/basicTrasition.ts b/src/animation/basicTrasition.ts new file mode 100644 index 0000000000..bc31f39c48 --- /dev/null +++ b/src/animation/basicTrasition.ts @@ -0,0 +1,337 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +// Basic transitions in the same series when shapes are the same. + +import { + AnimationOptionMixin, + AnimationDelayCallbackParam, + PayloadAnimationPart, + AnimationOption +} from '../util/types'; +import { AnimationEasing } from 'zrender/src/animation/easing'; +import Element, { ElementAnimateConfig } from 'zrender/src/Element'; +import Model from '../model/Model'; +import { + isObject, + retrieve2 +} from 'zrender/src/core/util'; +import Displayable from 'zrender/src/graphic/Displayable'; +import Group from 'zrender/src/graphic/Group'; +import { makeInner } from '../util/model'; + +// Stored properties for further transition. + +export const transitionStore = makeInner<{ + oldStyle: Displayable['style'] +}, Displayable>(); + + +type AnimateOrSetPropsOption = { + dataIndex?: number; + cb?: () => void; + during?: (percent: number) => void; + removeOpt?: AnimationOption + isFrom?: boolean; +}; + +/** + * Return null if animation is disabled. + */ +export function getAnimationConfig( + animationType: 'init' | 'update' | 'remove', + animatableModel: Model, + dataIndex: number, + // Extra opts can override the option in animatable model. + extraOpts?: Pick, + // TODO It's only for pictorial bar now. + extraDelayParams?: unknown +): Pick | null { + let animationPayload: PayloadAnimationPart; + // Check if there is global animation configuration from dataZoom/resize can override the config in option. + // If animation is enabled. Will use this animation config in payload. + // If animation is disabled. Just ignore it. + if (animatableModel && animatableModel.ecModel) { + const updatePayload = animatableModel.ecModel.getUpdatePayload(); + animationPayload = (updatePayload && updatePayload.animation) as PayloadAnimationPart; + } + const animationEnabled = animatableModel && animatableModel.isAnimationEnabled(); + + const isUpdate = animationType === 'update'; + + if (animationEnabled) { + let duration: number | Function; + let easing: AnimationEasing; + let delay: number | Function; + if (extraOpts) { + duration = retrieve2(extraOpts.duration, 200); + easing = retrieve2(extraOpts.easing, 'cubicOut'); + delay = 0; + } + else { + duration = animatableModel.getShallow( + isUpdate ? 'animationDurationUpdate' : 'animationDuration' + ); + easing = animatableModel.getShallow( + isUpdate ? 'animationEasingUpdate' : 'animationEasing' + ); + delay = animatableModel.getShallow( + isUpdate ? 'animationDelayUpdate' : 'animationDelay' + ); + } + // animation from payload has highest priority. + if (animationPayload) { + animationPayload.duration != null && (duration = animationPayload.duration); + animationPayload.easing != null && (easing = animationPayload.easing); + animationPayload.delay != null && (delay = animationPayload.delay); + } + if (typeof delay === 'function') { + delay = delay( + dataIndex as number, + extraDelayParams + ); + } + if (typeof duration === 'function') { + duration = duration(dataIndex as number); + } + const config = { + duration: duration as number || 0, + delay: delay as number, + easing + }; + + return config; + } + else { + return null; + } +} + +function animateOrSetProps( + animationType: 'init' | 'update' | 'remove', + el: Element, + props: Props, + animatableModel?: Model & { + getAnimationDelayParams?: (el: Element, dataIndex: number) => AnimationDelayCallbackParam + }, + dataIndex?: AnimateOrSetPropsOption['dataIndex'] | AnimateOrSetPropsOption['cb'] | AnimateOrSetPropsOption, + cb?: AnimateOrSetPropsOption['cb'] | AnimateOrSetPropsOption['during'], + during?: AnimateOrSetPropsOption['during'] +) { + let isFrom = false; + let removeOpt: AnimationOption; + if (typeof dataIndex === 'function') { + during = cb; + cb = dataIndex; + dataIndex = null; + } + else if (isObject(dataIndex)) { + cb = dataIndex.cb; + during = dataIndex.during; + isFrom = dataIndex.isFrom; + removeOpt = dataIndex.removeOpt; + dataIndex = dataIndex.dataIndex; + } + + const isRemove = (animationType === 'remove'); + + if (!isRemove) { + // Must stop the remove animation. + el.stopAnimation('remove'); + } + + const animationConfig = getAnimationConfig( + animationType, + animatableModel, + dataIndex as number, + isRemove ? (removeOpt || {}) : null, + (animatableModel && animatableModel.getAnimationDelayParams) + ? animatableModel.getAnimationDelayParams(el, dataIndex as number) + : null + ); + if (animationConfig && animationConfig.duration > 0) { + const duration = animationConfig.duration; + const animationDelay = animationConfig.delay; + const animationEasing = animationConfig.easing; + + const animateConfig: ElementAnimateConfig = { + duration: duration as number, + delay: animationDelay as number || 0, + easing: animationEasing, + done: cb, + force: !!cb || !!during, + // Set to final state in update/init animation. + // So the post processing based on the path shape can be done correctly. + setToFinal: !isRemove, + scope: animationType, + during: during + }; + + isFrom + ? el.animateFrom(props, animateConfig) + : el.animateTo(props, animateConfig); + } + else { + el.stopAnimation(); + // If `isFrom`, the props is the "from" props. + !isFrom && el.attr(props); + // Call during at least once. + during && during(1); + cb && (cb as AnimateOrSetPropsOption['cb'])(); + } +} + + + +/** + * Update graphic element properties with or without animation according to the + * configuration in series. + * + * Caution: this method will stop previous animation. + * So do not use this method to one element twice before + * animation starts, unless you know what you are doing. + * @example + * graphic.updateProps(el, { + * position: [100, 100] + * }, seriesModel, dataIndex, function () { console.log('Animation done!'); }); + * // Or + * graphic.updateProps(el, { + * position: [100, 100] + * }, seriesModel, function () { console.log('Animation done!'); }); + */ + function updateProps( + el: Element, + props: Props, + // TODO: TYPE AnimatableModel + animatableModel?: Model, + dataIndex?: AnimateOrSetPropsOption['dataIndex'] | AnimateOrSetPropsOption['cb'] | AnimateOrSetPropsOption, + cb?: AnimateOrSetPropsOption['cb'] | AnimateOrSetPropsOption['during'], + during?: AnimateOrSetPropsOption['during'] +) { + animateOrSetProps('update', el, props, animatableModel, dataIndex, cb, during); +} + +export {updateProps}; + +/** + * Init graphic element properties with or without animation according to the + * configuration in series. + * + * Caution: this method will stop previous animation. + * So do not use this method to one element twice before + * animation starts, unless you know what you are doing. + */ +export function initProps( + el: Element, + props: Props, + animatableModel?: Model, + dataIndex?: AnimateOrSetPropsOption['dataIndex'] | AnimateOrSetPropsOption['cb'] | AnimateOrSetPropsOption, + cb?: AnimateOrSetPropsOption['cb'] | AnimateOrSetPropsOption['during'], + during?: AnimateOrSetPropsOption['during'] +) { + animateOrSetProps('init', el, props, animatableModel, dataIndex, cb, during); +} + +/** + * If element is removed. + * It can determine if element is having remove animation. + */ + export function isElementRemoved(el: Element) { + if (!el.__zr) { + return true; + } + for (let i = 0; i < el.animators.length; i++) { + const animator = el.animators[i]; + if (animator.scope === 'remove') { + return true; + } + } + return false; +} + +/** + * Remove graphic element + */ +export function removeElement( + el: Element, + props: Props, + animatableModel?: Model, + dataIndex?: AnimateOrSetPropsOption['dataIndex'] | AnimateOrSetPropsOption['cb'] | AnimateOrSetPropsOption, + cb?: AnimateOrSetPropsOption['cb'] | AnimateOrSetPropsOption['during'], + during?: AnimateOrSetPropsOption['during'] +) { + // Don't do remove animation twice. + if (isElementRemoved(el)) { + return; + } + + animateOrSetProps('remove', el, props, animatableModel, dataIndex, cb, during); +} + +function fadeOutDisplayable( + el: Displayable, + animatableModel?: Model, + dataIndex?: number, + done?: AnimateOrSetPropsOption['cb'] +) { + el.removeTextContent(); + el.removeTextGuideLine(); + removeElement(el, { + style: { + opacity: 0 + } + }, animatableModel, dataIndex, done); +} + +export function removeElementWithFadeOut( + el: Element, + animatableModel?: Model, + dataIndex?: number +) { + function doRemove() { + el.parent && el.parent.remove(el); + } + // Hide label and labelLine first + // TODO Also use fade out animation? + if (!el.isGroup) { + fadeOutDisplayable(el as Displayable, animatableModel, dataIndex, doRemove); + } + else { + (el as Group).traverse(function (disp: Displayable) { + if (!disp.isGroup) { + // Can invoke doRemove multiple times. + fadeOutDisplayable(disp as Displayable, animatableModel, dataIndex, doRemove); + } + }); + } +} + +/** + * Save old style for style transition in universalTransition module. + * It's used when element will be reused in each render. + * For chart like map, heatmap, which will always create new element. + * We don't need to save this because universalTransition can get old style from the old element + */ +export function saveOldStyle(el: Displayable) { + transitionStore(el).oldStyle = el.style; +} + +export function getOldStyle(el: Displayable) { + return transitionStore(el).oldStyle; +} \ No newline at end of file diff --git a/src/animation/morphTransitionHelper.ts b/src/animation/morphTransitionHelper.ts new file mode 100644 index 0000000000..1b6aad2e18 --- /dev/null +++ b/src/animation/morphTransitionHelper.ts @@ -0,0 +1,265 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +import { + separateMorph, + combineMorph, + morphPath, + DividePath, + isCombineMorphing, + SeparateConfig +} from 'zrender/src/tool/morphPath'; +import { Path } from '../util/graphic'; +import SeriesModel from '../model/Series'; +import Element, { ElementAnimateConfig } from 'zrender/src/Element'; +import { defaults, isArray} from 'zrender/src/core/util'; +import { getAnimationConfig } from './basicTrasition'; +import { ECElement, UniversalTransitionOption } from '../util/types'; +import { clonePath } from 'zrender/src/tool/path'; +import Model from '../model/Model'; + + +type DescendentElements = Element[]; +type DescendentPaths = Path[]; + +function isMultiple(elements: DescendentElements | DescendentElements[]): elements is DescendentElements[] { + return isArray(elements[0]); +} + +interface MorphingBatch { + one: Path; + many: Path[]; +} + +function prepareMorphBatches(one: DescendentPaths, many: DescendentPaths[]) { + const batches: MorphingBatch[] = []; + const batchCount = one.length; + for (let i = 0; i < batchCount; i++) { + batches.push({ + one: one[i], + many: [] + }); + } + + for (let i = 0; i < many.length; i++) { + const len = many[i].length; + let k; + for (k = 0; k < len; k++) { + batches[k % batchCount].many.push(many[i][k]); + } + } + + let off = 0; + // If one has more paths than each one of many. average them. + for (let i = batchCount - 1; i >= 0; i--) { + if (!batches[i].many.length) { + const moveFrom = batches[off].many; + if (moveFrom.length <= 1) { // Not enough + // Start from the first one. + if (off) { + off = 0; + } + else { + return batches; + } + } + const len = moveFrom.length; + const mid = Math.ceil(len / 2); + batches[i].many = moveFrom.slice(mid, len); + batches[off].many = moveFrom.slice(0, mid); + + off++; + } + } + + return batches; +} + +const pathDividers: Record = { + clone(params) { + const ret: Path[] = []; + // Fitting the alpha + const approxOpacity = 1 - Math.pow(1 - params.path.style.opacity, 1 / params.count); + for (let i = 0; i < params.count; i++) { + const cloned = clonePath(params.path); + cloned.setStyle('opacity', approxOpacity); + ret.push(cloned); + } + return ret; + }, + // Use the default divider + split: null +}; + +export function applyMorphAnimation( + from: DescendentPaths | DescendentPaths[], + to: DescendentPaths | DescendentPaths[], + divideShape: UniversalTransitionOption['divideShape'], + seriesModel: SeriesModel, + dataIndex: number, + animateOtherProps: ( + fromIndividual: Path, + toIndividual: Path, + rawFrom: Path, + rawTo: Path, + animationCfg: ElementAnimateConfig + ) => void +) { + if (!from.length || !to.length) { + return; + } + + const updateAnimationCfg = getAnimationConfig('update', seriesModel, dataIndex); + if (!(updateAnimationCfg.duration > 0)) { + return; + } + const animationDelay = (seriesModel.getModel('universalTransition') as Model) + .get('delay'); + + + const animationCfg = Object.assign({ + // Need to setToFinal so the further calculation based on the style can be correct. + // Like emphasis color. + setToFinal: true + } as SeparateConfig, updateAnimationCfg); + + + let many: DescendentPaths[]; + let one: DescendentPaths; + if (isMultiple(from)) { // manyToOne + many = from; + one = to as DescendentPaths; + } + if (isMultiple(to)) { // oneToMany + many = to; + one = from as DescendentPaths; + } + + function morphOneBatch( + batch: MorphingBatch, + fromIsMany: boolean, + animateIndex: number, + animateCount: number, + forceManyOne?: boolean + ) { + const batchMany = batch.many; + const batchOne = batch.one; + if (batchMany.length === 1 && !forceManyOne) { + // Is one to one + const batchFrom: Path = fromIsMany ? batchMany[0] : batchOne; + const batchTo: Path = fromIsMany ? batchOne : batchMany[0]; + + if (isCombineMorphing(batchFrom as Path)) { + // Keep doing combine animation. + morphOneBatch({ + many: [batchFrom as Path], + one: batchTo as Path + }, true, animateIndex, animateCount, true); + } + else { + const individualAnimationCfg = animationDelay ? defaults({ + delay: animationDelay(animateIndex, animateCount) + } as ElementAnimateConfig, animationCfg) : animationCfg; + morphPath(batchFrom, batchTo, individualAnimationCfg); + animateOtherProps(batchFrom, batchTo, batchFrom, batchTo, individualAnimationCfg); + } + } + else { + const separateAnimationCfg = defaults({ + dividePath: pathDividers[divideShape], + individualDelay: animationDelay && function (idx, count, fromPath, toPath) { + return animationDelay(idx + animateIndex, animateCount); + } + } as SeparateConfig, animationCfg); + + const { + fromIndividuals, + toIndividuals + } = fromIsMany + ? combineMorph(batchMany, batchOne, separateAnimationCfg) + : separateMorph(batchOne, batchMany, separateAnimationCfg); + + const count = fromIndividuals.length; + for (let k = 0; k < count; k++) { + const individualAnimationCfg = animationDelay ? defaults({ + delay: animationDelay(k, count) + } as ElementAnimateConfig, animationCfg) : animationCfg; + animateOtherProps( + fromIndividuals[k], + toIndividuals[k], + fromIsMany ? batchMany[k] : batch.one, + fromIsMany ? batch.one : batchMany[k], + individualAnimationCfg + ); + } + } + } + + const fromIsMany = many + ? many === from + // Is one to one. If the path number not match. also needs do merge and separate morphing. + : from.length > to.length; + + const morphBatches = many + ? prepareMorphBatches(one, many) + : prepareMorphBatches( + (fromIsMany ? to : from) as DescendentPaths, + [(fromIsMany ? from : to) as DescendentPaths] + ); + let animateCount = 0; + for (let i = 0; i < morphBatches.length; i++) { + animateCount += morphBatches[i].many.length; + } + let animateIndex = 0; + for (let i = 0; i < morphBatches.length; i++) { + morphOneBatch(morphBatches[i], fromIsMany, animateIndex, animateCount); + animateIndex += morphBatches[i].many.length; + } +} + +export function getPathList( + elements: Element +): DescendentPaths; +export function getPathList( + elements: Element[] +): DescendentPaths[]; +export function getPathList( + elements: Element | Element[] +): DescendentPaths | DescendentPaths[] { + if (!elements) { + return []; + } + + if (isArray(elements)) { + const pathList = []; + for (let i = 0; i < elements.length; i++) { + pathList.push(getPathList(elements[i])); + } + return pathList as DescendentPaths[]; + } + + const pathList: DescendentPaths = []; + + elements.traverse(el => { + if ((el instanceof Path) && !(el as ECElement).disableMorphing && !el.invisible && !el.ignore) { + pathList.push(el); + } + }); + return pathList; +} \ No newline at end of file diff --git a/src/animation/universalTransition.ts b/src/animation/universalTransition.ts new file mode 100644 index 0000000000..9dc27e20c4 --- /dev/null +++ b/src/animation/universalTransition.ts @@ -0,0 +1,681 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +// Universal transitions that can animate between any shapes(series) and any properties in any amounts. + +import SeriesModel, { SERIES_UNIVERSAL_TRANSITION_PROP } from '../model/Series'; +import {createHashMap, each, map, filter, isArray} from 'zrender/src/core/util'; +import Element, { ElementAnimateConfig } from 'zrender/src/Element'; +import { applyMorphAnimation, getPathList } from './morphTransitionHelper'; +import Path from 'zrender/src/graphic/Path'; +import { EChartsExtensionInstallRegisters } from '../extension'; +import { initProps } from '../util/graphic'; +import DataDiffer from '../data/DataDiffer'; +import SeriesData from '../data/SeriesData'; +import { Dictionary, DimensionLoose, OptionDataItemObject, UniversalTransitionOption } from '../util/types'; +import { + UpdateLifecycleParams, + UpdateLifecycleTransitionItem, + UpdateLifecycleTransitionSeriesFinder +} from '../core/lifecycle'; +import { makeInner, normalizeToArray } from '../util/model'; +import { warn } from '../util/log'; +import ExtensionAPI from '../core/ExtensionAPI'; +import { getAnimationConfig, getOldStyle } from './basicTrasition'; +import Model from '../model/Model'; +import Displayable from 'zrender/src/graphic/Displayable'; + +const DATA_COUNT_THRESHOLD = 1e4; + +interface GlobalStore { oldSeries: SeriesModel[], oldData: SeriesData[] }; +const getUniversalTransitionGlobalStore = makeInner(); + +interface DiffItem { + data: SeriesData + dim: DimensionLoose + divide: UniversalTransitionOption['divideShape'] + dataIndex: number +} +interface TransitionSeries { + data: SeriesData + divide: UniversalTransitionOption['divideShape'] + dim?: DimensionLoose +} + +function getGroupIdDimension(data: SeriesData) { + const dimensions = data.dimensions; + for (let i = 0; i < dimensions.length; i++) { + const dimInfo = data.getDimensionInfo(dimensions[i]); + if (dimInfo && dimInfo.otherDims.itemGroupId === 0) { + return dimensions[i]; + } + } +} + +function flattenDataDiffItems(list: TransitionSeries[]) { + const items: DiffItem[] = []; + + each(list, seriesInfo => { + const data = seriesInfo.data; + if (data.count() > DATA_COUNT_THRESHOLD) { + if (__DEV__) { + warn('Universal transition is disabled on large data > 10k.'); + } + return; + } + const indices = data.getIndices(); + const groupDim = getGroupIdDimension(data); + for (let dataIndex = 0; dataIndex < indices.length; dataIndex++) { + items.push({ + data, + dim: seriesInfo.dim || groupDim, + divide: seriesInfo.divide, + dataIndex + }); + } + }); + + return items; +} + + +function fadeInElement(newEl: Element, newSeries: SeriesModel, newIndex: number) { + newEl.traverse(el => { + if (el instanceof Path) { + // TODO use fade in animation for target element. + initProps(el, { + style: { + opacity: 0 + } + }, newSeries, { + dataIndex: newIndex, + isFrom: true + }); + } + }); +} +function removeEl(el: Element) { + if (el.parent) { + // Bake parent transform to element. + // So it can still have proper transform to transition after it's removed. + const computedTransform = el.getComputedTransform(); + el.setLocalTransform(computedTransform); + el.parent.remove(el); + } +} +function stopAnimation(el: Element) { + el.stopAnimation(); + if (el.isGroup) { + el.traverse(child => { + child.stopAnimation(); + }); + } +} +function animateElementStyles(el: Element, dataIndex: number, seriesModel: SeriesModel) { + const animationConfig = getAnimationConfig('update', seriesModel, dataIndex); + el.traverse(child => { + if (child instanceof Displayable) { + const oldStyle = getOldStyle(child); + if (oldStyle) { + child.animateFrom({ + style: oldStyle + }, animationConfig); + } + } + }); +} + + +function isAllIdSame(oldDiffItems: DiffItem[], newDiffItems: DiffItem[]) { + const len = oldDiffItems.length; + if (len !== newDiffItems.length) { + return false; + } + for (let i = 0; i < len; i++) { + const oldItem = oldDiffItems[i]; + const newItem = newDiffItems[i]; + if (oldItem.data.getId(oldItem.dataIndex) !== newItem.data.getId(newItem.dataIndex)) { + return false; + } + } + return true; +} + +function transitionBetween( + oldList: TransitionSeries[], + newList: TransitionSeries[], + api: ExtensionAPI +) { + + const oldDiffItems = flattenDataDiffItems(oldList); + const newDiffItems = flattenDataDiffItems(newList); + + function updateMorphingPathProps( + from: Path, to: Path, + rawFrom: Path, rawTo: Path, + animationCfg: ElementAnimateConfig + ) { + if (rawFrom || from) { + to.animateFrom({ + style: (rawFrom || from).style + }, animationCfg); + } + } + + + function findKeyDim(items: DiffItem[]) { + for (let i = 0; i < items.length; i++) { + if (items[i].dim) { + return items[i].dim; + } + } + } + const oldKeyDim = findKeyDim(oldDiffItems); + const newKeyDim = findKeyDim(newDiffItems); + + let hasMorphAnimation = false; + + function createKeyGetter(isOld: boolean, onlyGetId: boolean) { + return function (diffItem: DiffItem): string { + const data = diffItem.data; + const dataIndex = diffItem.dataIndex; + // TODO if specified dim + if (onlyGetId) { + return data.getId(dataIndex); + } + + // Use group id as transition key by default. + // So we can achieve multiple to multiple animation like drilldown / up naturally. + // If group id not exits. Use id instead. If so, only one to one transition will be applied. + const dataGroupId = data.hostModel && (data.hostModel as SeriesModel).get('dataGroupId') as string; + + // If specified key dimension(itemGroupId by default). Use this same dimension from other data. + // PENDING: If only use key dimension of newData. + const keyDim = isOld + ? (oldKeyDim || newKeyDim) + : (newKeyDim || oldKeyDim); + + const dimInfo = keyDim && data.getDimensionInfo(keyDim); + const dimOrdinalMeta = dimInfo && dimInfo.ordinalMeta; + + if (dimInfo) { + // Get from encode.itemGroupId. + const key = data.get(dimInfo.name, dataIndex); + if (dimOrdinalMeta) { + return dimOrdinalMeta.categories[key as number] as string || (key + ''); + } + return key + ''; + } + + // Get groupId from raw item. { groupId: '' } + const itemVal = data.getRawDataItem(dataIndex) as OptionDataItemObject; + if (itemVal && itemVal.groupId) { + return itemVal.groupId + ''; + } + return (dataGroupId || data.getId(dataIndex)); + }; + } + + // Use id if it's very likely to be an one to one animation + // It's more robust than groupId + // TODO Check if key dimension is specified. + const useId = isAllIdSame(oldDiffItems, newDiffItems); + const isElementStillInChart: Dictionary = {}; + + if (!useId) { + // We may have different diff strategy with basicTransition if we use other dimension as key. + // If so, we can't simply check if oldEl is same with newEl. We need a map to check if oldEl is still being used in the new chart. + // We can't use the elements that already being morphed. Let it keep it's original basic transition. + for (let i = 0; i < newDiffItems.length; i++) { + const newItem = newDiffItems[i]; + const el = newItem.data.getItemGraphicEl(newItem.dataIndex); + if (el) { + isElementStillInChart[el.id] = true; + } + } + } + + function updateOneToOne(newIndex: number, oldIndex: number) { + + const oldItem = oldDiffItems[oldIndex]; + const newItem = newDiffItems[newIndex]; + + const newSeries = newItem.data.hostModel as SeriesModel; + + // TODO Mark this elements is morphed and don't morph them anymore + const oldEl = oldItem.data.getItemGraphicEl(oldItem.dataIndex); + const newEl = newItem.data.getItemGraphicEl(newItem.dataIndex); + + // Can't handle same elements. + if (oldEl === newEl) { + newEl && animateElementStyles(newEl, newItem.dataIndex, newSeries); + return; + } + + if ( + // We can't use the elements that already being morphed + (oldEl && isElementStillInChart[oldEl.id]) + ) { + return; + } + + if (newEl) { + // TODO: If keep animating the group in case + // some of the elements don't want to be morphed. + // TODO Label? + stopAnimation(newEl); + + if (oldEl) { + stopAnimation(oldEl); + + // If old element is doing leaving animation. stop it and remove it immediately. + removeEl(oldEl); + + hasMorphAnimation = true; + applyMorphAnimation( + getPathList(oldEl), + getPathList(newEl), + newItem.divide, + newSeries, + newIndex, + updateMorphingPathProps + ); + } + else { + fadeInElement(newEl, newSeries, newIndex); + } + } + // else keep oldEl leaving animation. + } + + (new DataDiffer( + oldDiffItems, + newDiffItems, + createKeyGetter(true, useId), + createKeyGetter(false, useId), + null, + 'multiple' + )) + .update(updateOneToOne) + .updateManyToOne(function (newIndex, oldIndices) { + const newItem = newDiffItems[newIndex]; + const newData = newItem.data; + const newSeries = newData.hostModel as SeriesModel; + const newEl = newData.getItemGraphicEl(newItem.dataIndex); + const oldElsList = filter( + map(oldIndices, idx => + oldDiffItems[idx].data.getItemGraphicEl(oldDiffItems[idx].dataIndex) + ), + oldEl => oldEl && oldEl !== newEl && !isElementStillInChart[oldEl.id] + ); + + if (newEl) { + stopAnimation(newEl); + if (oldElsList.length) { + // If old element is doing leaving animation. stop it and remove it immediately. + each(oldElsList, oldEl => { + stopAnimation(oldEl); + removeEl(oldEl); + }); + + hasMorphAnimation = true; + applyMorphAnimation( + getPathList(oldElsList), + getPathList(newEl), + newItem.divide, + newSeries, + newIndex, + updateMorphingPathProps + ); + + } + else { + fadeInElement(newEl, newSeries, newItem.dataIndex); + } + } + // else keep oldEl leaving animation. + }) + .updateOneToMany(function (newIndices, oldIndex) { + const oldItem = oldDiffItems[oldIndex]; + const oldEl = oldItem.data.getItemGraphicEl(oldItem.dataIndex); + + // We can't use the elements that already being morphed + if (oldEl && isElementStillInChart[oldEl.id]) { + return; + } + + const newElsList = filter( + map(newIndices, idx => + newDiffItems[idx].data.getItemGraphicEl(newDiffItems[idx].dataIndex) + ), + el => el && el !== oldEl + ); + const newSeris = newDiffItems[newIndices[0]].data.hostModel as SeriesModel; + + if (newElsList.length) { + each(newElsList, newEl => stopAnimation(newEl)); + if (oldEl) { + stopAnimation(oldEl); + // If old element is doing leaving animation. stop it and remove it immediately. + removeEl(oldEl); + + hasMorphAnimation = true; + applyMorphAnimation( + getPathList(oldEl), + getPathList(newElsList), + oldItem.divide, // Use divide on old. + newSeris, + newIndices[0], + updateMorphingPathProps + ); + } + else { + each(newElsList, newEl => fadeInElement(newEl, newSeris, newIndices[0])); + } + } + + // else keep oldEl leaving animation. + }) + .updateManyToMany(function (newIndices, oldIndices) { + // If two data are same and both have groupId. + // Normally they should be diff by id. + new DataDiffer( + oldIndices, + newIndices, + (rawIdx: number) => oldDiffItems[rawIdx].data.getId(oldDiffItems[rawIdx].dataIndex), + (rawIdx: number) => newDiffItems[rawIdx].data.getId(newDiffItems[rawIdx].dataIndex) + ).update((newIndex, oldIndex) => { + // Use the original index + updateOneToOne(newIndices[newIndex], oldIndices[oldIndex]); + }).execute(); + }) + .execute(); + + if (hasMorphAnimation) { + each(newList, ({ data }) => { + const seriesModel = data.hostModel as SeriesModel; + const view = seriesModel && api.getViewOfSeriesModel(seriesModel as SeriesModel); + const animationCfg = getAnimationConfig('update', seriesModel, 0); // use 0 index. + if (view && seriesModel.isAnimationEnabled() && animationCfg.duration > 0) { + view.group.traverse(el => { + if (el instanceof Path && !el.animators.length) { + // We can't accept there still exists element that has no animation + // if universalTransition is enabled + el.animateFrom({ + style: { + opacity: 0 + } + }, animationCfg); + } + }); + } + }); + } +} + +function getSeriesTransitionKey(series: SeriesModel) { + const seriesKey = (series.getModel('universalTransition') as Model) + .get('seriesKey'); + if (!seriesKey) { + // Use series id by default. + return series.id; + } + return seriesKey; +} + +function convertArraySeriesKeyToString(seriesKey: string[] | string) { + if (isArray(seriesKey)) { + // Order independent. + return seriesKey.sort().join(','); + } + return seriesKey; +} + +interface SeriesTransitionBatch { + oldSeries: TransitionSeries[] + newSeries: TransitionSeries[] +} + +function getDivideShapeFromData(data: SeriesData) { + if (data.hostModel) { + return ((data.hostModel as SeriesModel) + .getModel('universalTransition') as Model) + .get('divideShape'); + } +} + +function findTransitionSeriesBatches( + globalStore: GlobalStore, + params: UpdateLifecycleParams +) { + const updateBatches = createHashMap(); + + const oldDataMap = createHashMap(); + // Map that only store key in array seriesKey. + // Which is used to query the old data when transition from one to multiple series. + const oldDataMapForSplit = createHashMap<{ + key: string, + data: SeriesData + }>(); + + each(globalStore.oldSeries, (series, idx) => { + const oldData = globalStore.oldData[idx]; + const transitionKey = getSeriesTransitionKey(series); + const transitionKeyStr = convertArraySeriesKeyToString(transitionKey); + oldDataMap.set(transitionKeyStr, oldData); + + if (isArray(transitionKey)) { + // Same key can't in different array seriesKey. + each(transitionKey, key => { + oldDataMapForSplit.set(key, { + data: oldData, + key: transitionKeyStr + }); + }); + } + }); + + function checkTransitionSeriesKeyDuplicated(transitionKeyStr: string) { + if (updateBatches.get(transitionKeyStr)) { + warn(`Duplicated seriesKey in universalTransition ${transitionKeyStr}`); + } + } + each(params.updatedSeries, series => { + if (series.isUniversalTransitionEnabled() && series.isAnimationEnabled()) { + const newData = series.getData(); + const transitionKey = getSeriesTransitionKey(series); + const transitionKeyStr = convertArraySeriesKeyToString(transitionKey); + // Only transition between series with same id. + const oldData = oldDataMap.get(transitionKeyStr); + // string transition key is the best match. + if (oldData) { + if (__DEV__) { + checkTransitionSeriesKeyDuplicated(transitionKeyStr); + } + // TODO check if data is same? + updateBatches.set(transitionKeyStr, { + oldSeries: [{ + divide: getDivideShapeFromData(oldData), + data: oldData + }], + newSeries: [{ + divide: getDivideShapeFromData(newData), + data: newData + }] + }); + } + else { + // Transition from multiple series. + if (isArray(transitionKey)) { + if (__DEV__) { + checkTransitionSeriesKeyDuplicated(transitionKeyStr); + } + const oldSeries: TransitionSeries[] = []; + each(transitionKey, key => { + const oldData = oldDataMap.get(key); + if (oldData) { + oldSeries.push({ + divide: getDivideShapeFromData(oldData), + data: oldData + }); + } + }); + if (oldSeries.length) { + updateBatches.set(transitionKeyStr, { + oldSeries, + newSeries: [{ + data: newData, + divide: getDivideShapeFromData(newData) + }] + }); + } + } + else { + // Try transition to multiple series. + const oldData = oldDataMapForSplit.get(transitionKey); + if (oldData) { + let batch = updateBatches.get(oldData.key); + if (!batch) { + batch = { + oldSeries: [{ + data: oldData.data, + divide: getDivideShapeFromData(oldData.data) + }], + newSeries: [] + }; + updateBatches.set(oldData.key, batch); + } + batch.newSeries.push({ + data: newData, + divide: getDivideShapeFromData(newData) + }); + } + } + } + } + }); + + return updateBatches; +} + +function querySeries(series: SeriesModel[], finder: UpdateLifecycleTransitionSeriesFinder) { + for (let i = 0; i < series.length; i++) { + const found = finder.seriesIndex != null && finder.seriesIndex === series[i].seriesIndex + || finder.seriesId != null && finder.seriesId === series[i].id; + if (found) { + return i; + } + } +} + +function transitionSeriesFromOpt( + transitionOpt: UpdateLifecycleTransitionItem, + globalStore: GlobalStore, + params: UpdateLifecycleParams, + api: ExtensionAPI +) { + const from: TransitionSeries[] = []; + const to: TransitionSeries[] = []; + each(normalizeToArray(transitionOpt.from), finder => { + const idx = querySeries(globalStore.oldSeries, finder); + if (idx >= 0) { + from.push({ + data: globalStore.oldData[idx], + // TODO can specify divideShape in transition. + divide: getDivideShapeFromData(globalStore.oldData[idx]), + dim: finder.dimension + }); + } + }); + each(normalizeToArray(transitionOpt.to), finder => { + const idx = querySeries(params.updatedSeries, finder); + if (idx >= 0) { + const data = params.updatedSeries[idx].getData(); + to.push({ + data, + divide: getDivideShapeFromData(data), + dim: finder.dimension + }); + } + }); + if (from.length > 0 && to.length > 0) { + transitionBetween(from, to, api); + } +} + +export function installUniversalTransition(registers: EChartsExtensionInstallRegisters) { + + registers.registerUpdateLifecycle('series:beforeupdate', (ecMOdel, api, params) => { + each(normalizeToArray(params.seriesTransition), transOpt => { + each(normalizeToArray(transOpt.to), (finder) => { + const series = params.updatedSeries; + for (let i = 0; i < series.length; i++) { + if (finder.seriesIndex != null && finder.seriesIndex === series[i].seriesIndex + || finder.seriesId != null && finder.seriesId === series[i].id) { + series[i][SERIES_UNIVERSAL_TRANSITION_PROP] = true; + } + } + }); + }); + }); + registers.registerUpdateLifecycle('series:transition', (ecModel, api, params) => { + // TODO api provide an namespace that can save stuff per instance + const globalStore = getUniversalTransitionGlobalStore(api); + + // TODO multiple to multiple series. + if (globalStore.oldSeries && params.updatedSeries && params.optionChanged) { + // Use give transition config if its' give; + const transitionOpt = params.seriesTransition; + if (transitionOpt) { + each(normalizeToArray(transitionOpt), opt => { + transitionSeriesFromOpt(opt, globalStore, params, api); + }); + } + else { // Else guess from series based on transition series key. + const updateBatches = findTransitionSeriesBatches(globalStore, params); + each(updateBatches.keys(), key => { + const batch = updateBatches.get(key); + transitionBetween(batch.oldSeries, batch.newSeries, api); + }); + } + + // Reset + each(params.updatedSeries, series => { + // Reset; + if (series[SERIES_UNIVERSAL_TRANSITION_PROP]) { + series[SERIES_UNIVERSAL_TRANSITION_PROP] = false; + } + }); + } + + // Save all series of current update. Not only the updated one. + const allSeries = ecModel.getSeries(); + const savedSeries: SeriesModel[] = globalStore.oldSeries = []; + const savedData: SeriesData[] = globalStore.oldData = []; + for (let i = 0; i < allSeries.length; i++) { + const data = allSeries[i].getData(); + // Only save the data that can have transition. + // Avoid large data costing too much extra memory + if (data.count() < DATA_COUNT_THRESHOLD) { + savedSeries.push(allSeries[i]); + savedData.push(data); + } + } + }); +} \ No newline at end of file diff --git a/src/chart/bar/BarSeries.ts b/src/chart/bar/BarSeries.ts index 87713ee9dc..8c97a8b616 100644 --- a/src/chart/bar/BarSeries.ts +++ b/src/chart/bar/BarSeries.ts @@ -29,16 +29,21 @@ import { SeriesEncodeOptionMixin } from '../../util/types'; import type Cartesian2D from '../../coord/cartesian/Cartesian2D'; -import createListFromArray from '../helper/createListFromArray'; +import createSeriesData from '../helper/createSeriesData'; import type Polar from '../../coord/polar/Polar'; import { inheritDefaultOption } from '../../util/component'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import { BrushCommonSelectorsForSeries } from '../../component/brush/selector'; +export type PolarBarLabelPosition = SeriesLabelOption['position'] + | 'start' | 'insideStart' | 'middle' | 'end' | 'insideEnd'; + +export type BarSeriesLabelOption = Omit + & {position?: PolarBarLabelPosition | 'outside'}; export interface BarStateOption { itemStyle?: BarItemStyleOption - label?: SeriesLabelOption + label?: BarSeriesLabelOption } export interface BarItemStyleOption extends ItemStyleOption { @@ -84,8 +89,8 @@ class BarSeriesModel extends BaseBarSeriesModel { coordinateSystem: Cartesian2D | Polar; - getInitialData(): List { - return createListFromArray(this.getSource(), this, { + getInitialData(): SeriesData { + return createSeriesData(null, this, { useEncodeDefaulter: true, createInvertedIndices: !!this.get('realtimeSort', true) || null }); @@ -114,7 +119,7 @@ class BarSeriesModel extends BaseBarSeriesModel { return progressiveThreshold; } - brushSelector(dataIndex: number, data: List, selectors: BrushCommonSelectorsForSeries): boolean { + brushSelector(dataIndex: number, data: SeriesData, selectors: BrushCommonSelectorsForSeries): boolean { return selectors.rect(data.getItemLayout(dataIndex)); } diff --git a/src/chart/bar/BarView.ts b/src/chart/bar/BarView.ts index 470c6f6d0b..e4be916699 100644 --- a/src/chart/bar/BarView.ts +++ b/src/chart/bar/BarView.ts @@ -20,6 +20,7 @@ import Path, {PathProps} from 'zrender/src/graphic/Path'; import Group from 'zrender/src/graphic/Group'; import {extend, defaults, each, map} from 'zrender/src/core/util'; +import {BuiltinTextPosition} from 'zrender/src/core/types'; import { Rect, Sector, @@ -34,7 +35,7 @@ import {throttle} from '../../util/throttle'; import {createClipPath} from '../helper/createClipPathFromCoordSys'; import Sausage from '../../util/shape/sausage'; import ChartView from '../../view/Chart'; -import List, {DefaultDataVisual} from '../../data/List'; +import SeriesData, {DefaultDataVisual} from '../../data/SeriesData'; import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../core/ExtensionAPI'; import { @@ -46,7 +47,7 @@ import { OrdinalNumber, ParsedValue } from '../../util/types'; -import BarSeriesModel, {BarSeriesOption, BarDataItemOption} from './BarSeries'; +import BarSeriesModel, {BarSeriesOption, BarDataItemOption, PolarBarLabelPosition} from './BarSeries'; import type Axis2D from '../../coord/cartesian/Axis2D'; import type Cartesian2D from '../../coord/cartesian/Cartesian2D'; import type Polar from '../../coord/polar/Polar'; @@ -60,6 +61,8 @@ import CartesianAxisModel from '../../coord/cartesian/AxisModel'; import {LayoutRect} from '../../util/layout'; import {EventCallback} from 'zrender/src/core/Eventful'; import { warn } from '../../util/log'; +import {createSectorCalculateTextPosition, SectorTextPosition, setSectorTextRotation} from '../../label/sectorLabel'; +import { saveOldStyle } from '../../animation/basicTrasition'; const _eventPos = [0, 0]; @@ -85,7 +88,7 @@ type RealtimeSortConfig = { // Return a number, based on which the ordinal sorted. type OrderMapping = (dataIndex: number) => number; -function getClipArea(coord: CoordSysOfBar, data: List) { +function getClipArea(coord: CoordSysOfBar, data: SeriesData) { const coordSysClipArea = coord.getArea && coord.getArea(); if (isCoordinateSystemType(coord, 'cartesian2d')) { const baseAxis = coord.getBaseAxis(); @@ -112,12 +115,12 @@ class BarView extends ChartView { static type = 'bar' as const; type = BarView.type; - private _data: List; + private _data: SeriesData; private _isLargeDraw: boolean; private _isFirstFrame: boolean; // First frame after series added - private _onRendered: EventCallback; + private _onRendered: EventCallback; private _backgroundGroup: Group; @@ -241,7 +244,7 @@ class BarView extends ChartView { } // If dataZoom in filteMode: 'empty', the baseValue can be set as NaN in "axisProxy". - if (!data.hasValue(dataIndex)) { + if (!data.hasValue(dataIndex) || !isValidLayout[coord.type](layout)) { return; } @@ -316,9 +319,8 @@ class BarView extends ChartView { } let el = oldData.getItemGraphicEl(oldIndex) as BarPossiblePath; - if (!data.hasValue(newIndex)) { + if (!data.hasValue(newIndex) || !isValidLayout[coord.type](layout)) { group.remove(el); - el = null; return; } @@ -343,6 +345,9 @@ class BarView extends ChartView { roundCap ); } + else { + saveOldStyle(el); + } // Not change anything if only order changed. // Especially not change label. @@ -462,7 +467,7 @@ class BarView extends ChartView { } private _dataSort( - data: List, + data: SeriesData, baseAxis: Axis2D, orderMapping: OrderMapping ): OrdinalSortInfo { @@ -493,7 +498,7 @@ class BarView extends ChartView { } private _isOrderChangedWithinSameData( - data: List, + data: SeriesData, orderMapping: OrderMapping, baseAxis: Axis2D ): boolean { @@ -538,7 +543,7 @@ class BarView extends ChartView { } private _updateSortWithinSameData( - data: List, + data: SeriesData, orderMapping: OrderMapping, baseAxis: Axis2D, api: ExtensionAPI @@ -561,7 +566,7 @@ class BarView extends ChartView { } private _dispatchInitSort( - data: List, + data: SeriesData, realtimeSortCfg: RealtimeSortConfig, api: ExtensionAPI ) { @@ -579,12 +584,7 @@ class BarView extends ChartView { componentType: baseAxis.dim + 'Axis', isInitSort: true, axisId: baseAxis.index, - sortInfo: sortResult, - animation: { - // Update the axis label from the natural initial layout to - // sorted layout should has no animation. - duration: 0 - } + sortInfo: sortResult }); } @@ -709,7 +709,7 @@ const clip: { interface ElementCreator { ( - seriesModel: BarSeriesModel, data: List, newIndex: number, + seriesModel: BarSeriesModel, data: SeriesData, newIndex: number, layout: RectLayout | SectorLayout, isHorizontalOrRadial: boolean, animationModel: BarSeriesModel, axisModel: CartesianAxisModel | AngleAxisModel | RadiusAxisModel, @@ -755,12 +755,19 @@ const elementCreator: { const ShapeClass = (!isRadial && roundCap) ? Sausage : Sector; const sector = new ShapeClass({ - shape: defaults({clockwise: clockwise}, layout), + shape: defaults({ + clockwise: clockwise + }, layout), z2: 1 }); sector.name = 'item'; + const positionMap = createPolarPositionMapping(isRadial); + sector.calculateTextPosition = createSectorCalculateTextPosition(positionMap, { + isRoundCap: ShapeClass === Sausage + }); + // Animation if (animationModel) { const sectorShape = sector.shape; @@ -849,8 +856,30 @@ function updateRealtimeAnimation( }, axisAnimationModel, newIndex); } +function checkPropertiesNotValid>(obj: T, props: readonly (keyof T)[]) { + for (let i = 0; i < props.length; i++) { + if (!isFinite(obj[props[i]])) { + return true; + } + } + return false; +} + + +const rectPropties = ['x', 'y', 'width', 'height'] as const; +const polarPropties = ['cx', 'cy', 'r', 'startAngle', 'endAngle'] as const; +const isValidLayout: Record<'cartesian2d' | 'polar', (layout: RectLayout | SectorLayout) => boolean> = { + cartesian2d(layout: RectLayout) { + return !checkPropertiesNotValid(layout, rectPropties); + }, + + polar(layout: SectorLayout) { + return !checkPropertiesNotValid(layout, polarPropties); + } +} as const; + interface GetLayout { - (data: List, dataIndex: number, itemModel?: Model): RectLayout | SectorLayout + (data: SeriesData, dataIndex: number, itemModel?: Model): RectLayout | SectorLayout } const getLayout: { [key in 'cartesian2d' | 'polar']: GetLayout @@ -891,13 +920,31 @@ function isZeroOnPolar(layout: SectorLayout) { && layout.startAngle === layout.endAngle; } +function createPolarPositionMapping(isRadial: boolean) + : (position: PolarBarLabelPosition) => SectorTextPosition { + return ((isRadial: boolean) => { + const arcOrAngle = isRadial ? 'Arc' : 'Angle'; + return (position: PolarBarLabelPosition) => { + switch (position) { + case 'start': + case 'insideStart': + case 'end': + case 'insideEnd': + return position + arcOrAngle as SectorTextPosition; + default: + return position; + } + }; + })(isRadial); +} + function updateStyle( el: BarPossiblePath, - data: List, dataIndex: number, + data: SeriesData, dataIndex: number, itemModel: Model, layout: RectLayout | SectorLayout, seriesModel: BarSeriesModel, - isHorizontal: boolean, + isHorizontalOrRadial: boolean, isPolar: boolean ) { const style = data.getItemVisual(dataIndex, 'style'); @@ -911,34 +958,51 @@ function updateStyle( const cursorStyle = itemModel.getShallow('cursor'); cursorStyle && (el as Path).attr('cursor', cursorStyle); - if (!isPolar) { - const labelPositionOutside = isHorizontal - ? ((layout as RectLayout).height > 0 ? 'bottom' as const : 'top' as const) - : ((layout as RectLayout).width > 0 ? 'left' as const : 'right' as const); - const labelStatesModels = getLabelStatesModels(itemModel); - - setLabelStyle( - el, labelStatesModels, - { - labelFetcher: seriesModel, - labelDataIndex: dataIndex, - defaultText: getDefaultLabel(seriesModel.getData(), dataIndex), - inheritColor: style.fill as ColorString, - defaultOpacity: style.opacity, - defaultOutsidePosition: labelPositionOutside - } - ); - - const label = el.getTextContent(); - - setLabelValueAnimation( - label, - labelStatesModels, - seriesModel.getRawValue(dataIndex) as ParsedValue, - (value: number) => getDefaultInterpolatedLabel(data, value) + const labelPositionOutside = isPolar + ? (isHorizontalOrRadial + ? ((layout as SectorLayout).r >= (layout as SectorLayout).r0 ? 'endArc' as const : 'startArc' as const) + : ((layout as SectorLayout).endAngle >= (layout as SectorLayout).startAngle + ? 'endAngle' as const + : 'startAngle' as const + ) + ) + : (isHorizontalOrRadial + ? ((layout as RectLayout).height >= 0 ? 'bottom' as const : 'top' as const) + : ((layout as RectLayout).width >= 0 ? 'right' as const : 'left' as const)); + + const labelStatesModels = getLabelStatesModels(itemModel); + + setLabelStyle( + el, labelStatesModels, + { + labelFetcher: seriesModel, + labelDataIndex: dataIndex, + defaultText: getDefaultLabel(seriesModel.getData(), dataIndex), + inheritColor: style.fill as ColorString, + defaultOpacity: style.opacity, + defaultOutsidePosition: labelPositionOutside as BuiltinTextPosition + } + ); + + const label = el.getTextContent(); + if (isPolar && label) { + const position = itemModel.get(['label', 'position']); + el.textConfig.inside = position === 'middle' ? true : null; + setSectorTextRotation( + el as Sector, + position === 'outside' ? labelPositionOutside : position, + createPolarPositionMapping(isHorizontalOrRadial), + itemModel.get(['label', 'rotate']) ); } + setLabelValueAnimation( + label, + labelStatesModels, + seriesModel.getRawValue(dataIndex) as ParsedValue, + (value: number) => getDefaultInterpolatedLabel(data, value) + ); + const emphasisModel = itemModel.getModel(['emphasis']); enableHoverEmphasis(el, emphasisModel.get('focus'), emphasisModel.get('blurScope')); setStatesStylesFromModel(el, itemModel); @@ -1110,7 +1174,7 @@ function largePathFindDataIndex(largePath: LargePath, x: number, y: number) { function setLargeStyle( el: LargePath, seriesModel: BarSeriesModel, - data: List + data: SeriesData ) { const globalStyle = data.getVisual('style'); @@ -1124,7 +1188,7 @@ function setLargeStyle( function setLargeBackgroundStyle( el: LargePath, backgroundModel: Model, - data: List + data: SeriesData ) { const borderColor = backgroundModel.get('borderColor') || backgroundModel.get('color'); const itemStyle = backgroundModel.getItemStyle(); diff --git a/src/chart/bar/BaseBarSeries.ts b/src/chart/bar/BaseBarSeries.ts index c39252a69c..d4c3fa5d19 100644 --- a/src/chart/bar/BaseBarSeries.ts +++ b/src/chart/bar/BaseBarSeries.ts @@ -18,7 +18,7 @@ */ import SeriesModel from '../../model/Series'; -import createListFromArray from '../helper/createListFromArray'; +import createSeriesData from '../helper/createSeriesData'; import { SeriesOption, SeriesOnCartesianOptionMixin, @@ -28,7 +28,7 @@ import { } from '../../util/types'; import GlobalModel from '../../model/Global'; import Cartesian2D from '../../coord/cartesian/Cartesian2D'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; export interface BaseBarSeriesOption @@ -78,13 +78,13 @@ class BaseBarSeriesModel = BaseBarSeri static type = 'series.__base_bar__'; type = BaseBarSeriesModel.type; - getInitialData(option: Opts, ecModel: GlobalModel): List { - return createListFromArray(this.getSource(), this, {useEncodeDefaulter: true}); + getInitialData(option: Opts, ecModel: GlobalModel): SeriesData { + return createSeriesData(null, this, {useEncodeDefaulter: true}); } getMarkerPosition(value: ScaleDataValue[]) { const coordSys = this.coordinateSystem; - if (coordSys) { + if (coordSys && coordSys.clampData) { // PENDING if clamp ? const pt = coordSys.dataToPoint(coordSys.clampData(value)); const data = this.getData(); diff --git a/src/chart/bar/PictorialBarView.ts b/src/chart/bar/PictorialBarView.ts index 57b8c796a9..1a81fed829 100644 --- a/src/chart/bar/PictorialBarView.ts +++ b/src/chart/bar/PictorialBarView.ts @@ -22,15 +22,15 @@ import * as graphic from '../../util/graphic'; import { enableHoverEmphasis } from '../../util/states'; -import {createSymbol} from '../../util/symbol'; +import {createSymbol, normalizeSymbolOffset} from '../../util/symbol'; import {parsePercent, isNumeric} from '../../util/number'; import ChartView from '../../view/Chart'; import PictorialBarSeriesModel, {PictorialBarDataItemOption} from './PictorialBarSeries'; import ExtensionAPI from '../../core/ExtensionAPI'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import GlobalModel from '../../model/Global'; import Model from '../../model/Model'; -import { ColorString, AnimationOptionMixin } from '../../util/types'; +import { ColorString, AnimationOptionMixin, ECElement } from '../../util/types'; import type Cartesian2D from '../../coord/cartesian/Cartesian2D'; import type Displayable from 'zrender/src/graphic/Displayable'; import type Axis2D from '../../coord/cartesian/Axis2D'; @@ -41,7 +41,6 @@ import { setLabelStyle, getLabelStatesModels } from '../../label/labelStyle'; import ZRImage from 'zrender/src/graphic/Image'; import { getECData } from '../../util/innerStore'; - const BAR_BORDER_WIDTH_QUERY = ['itemStyle', 'borderWidth'] as const; // index: +isHorizontal @@ -132,7 +131,7 @@ class PictorialBarView extends ChartView { static type = 'pictorialBar'; readonly type = PictorialBarView.type; - private _data: List; + private _data: SeriesData; render( seriesModel: PictorialBarSeriesModel, @@ -240,7 +239,7 @@ class PictorialBarView extends ChartView { // Set or calculate default value about symbol, and calculate layout info. function getSymbolMeta( - data: List, + data: SeriesData, dataIndex: number, itemModel: ItemModel, opt: CreateOpts @@ -280,13 +279,7 @@ function getSymbolMeta( prepareLineWidth(itemModel, symbolMeta.symbolScale, rotation, opt, symbolMeta); const symbolSize = symbolMeta.symbolSize; - let symbolOffset = itemModel.get('symbolOffset'); - if (zrUtil.isArray(symbolOffset)) { - symbolOffset = [ - parsePercent(symbolOffset[0], symbolSize[0]), - parsePercent(symbolOffset[1], symbolSize[1]) - ]; - } + const symbolOffset = normalizeSymbolOffset(itemModel.get('symbolOffset'), symbolSize); prepareLayoutInfo( itemModel, symbolSize, layout, symbolRepeat, symbolClip, symbolOffset as number[], @@ -345,7 +338,7 @@ function convertToCoordOnAxis(axis: Axis2D, value: number) { // Support ['100%', '100%'] function prepareSymbolSize( - data: List, + data: SeriesData, dataIndex: number, layout: RectLayout, symbolRepeat: PictorialBarDataItemOption['symbolRepeat'], @@ -478,7 +471,7 @@ function prepareLayoutInfo( // Adjust calculate margin, to ensure each symbol is displayed // entirely in the given layout area. const mDiff = absBoundingLength - repeatTimes * unitLength; - symbolMarginNumeric = mDiff / 2 / (hasEndGap ? repeatTimes : repeatTimes - 1); + symbolMarginNumeric = mDiff / 2 / (hasEndGap ? repeatTimes : Math.max(repeatTimes - 1, 1)); uLenWithMargin = unitLength + symbolMarginNumeric * 2; endFix = hasEndGap ? 0 : symbolMarginNumeric * 2; @@ -689,6 +682,7 @@ function createOrUpdateBarRect( lineWidth: 0 } }); + (barRect as ECElement).disableMorphing = true; bar.add(barRect); } @@ -732,7 +726,7 @@ function createOrUpdateClip( } } -function getItemModel(data: List, dataIndex: number) { +function getItemModel(data: SeriesData, dataIndex: number) { const itemModel = data.getItemModel(dataIndex) as ItemModel; itemModel.getAnimationDelayParams = getAnimationDelayParams; itemModel.isAnimationEnabled = isAnimationEnabled; @@ -752,7 +746,7 @@ function isAnimationEnabled(this: ItemModel) { return this.parentModel.isAnimationEnabled() && !!this.getShallow('animation'); } -function createBar(data: List, opt: CreateOpts, symbolMeta: SymbolMeta, isUpdate?: boolean) { +function createBar(data: SeriesData, opt: CreateOpts, symbolMeta: SymbolMeta, isUpdate?: boolean) { // bar is the main element for each data. const bar = new graphic.Group() as PictorialBarElement; // bundle is used for location and clip. @@ -776,7 +770,6 @@ function createBar(data: List, opt: CreateOpts, symbolMeta: SymbolMeta, isUpdate bar.__pictorialShapeStr = getShapeStr(data, symbolMeta); bar.__pictorialSymbolMeta = symbolMeta; - return bar; } @@ -805,7 +798,7 @@ function updateBar(bar: PictorialBarElement, opt: CreateOpts, symbolMeta: Symbol } function removeBar( - data: List, dataIndex: number, animationModel: Model, bar: PictorialBarElement + data: SeriesData, dataIndex: number, animationModel: Model, bar: PictorialBarElement ) { // Not show text when animating const labelRect = bar.__pictorialBarRect; @@ -832,7 +825,7 @@ function removeBar( data.setItemGraphicEl(dataIndex, null); } -function getShapeStr(data: List, symbolMeta: SymbolMeta) { +function getShapeStr(data: SeriesData, symbolMeta: SymbolMeta) { return [ data.getItemVisual(symbolMeta.dataIndex, 'symbol') || 'none', !!symbolMeta.symbolRepeat, @@ -946,4 +939,4 @@ function toIntTimes(times: number) { : Math.ceil(times); } -export default PictorialBarView; \ No newline at end of file +export default PictorialBarView; diff --git a/src/chart/boxplot/BoxplotView.ts b/src/chart/boxplot/BoxplotView.ts index 4321d6aecd..748ff6ed20 100644 --- a/src/chart/boxplot/BoxplotView.ts +++ b/src/chart/boxplot/BoxplotView.ts @@ -25,14 +25,15 @@ import Path, { PathProps } from 'zrender/src/graphic/Path'; import BoxplotSeriesModel, { BoxplotDataItemOption } from './BoxplotSeries'; import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../core/ExtensionAPI'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import { BoxplotItemLayout } from './boxplotLayout'; +import { saveOldStyle } from '../../animation/basicTrasition'; class BoxplotView extends ChartView { static type = 'boxplot'; type = BoxplotView.type; - private _data: List; + private _data: SeriesData; render(seriesModel: BoxplotSeriesModel, ecModel: GlobalModel, api: ExtensionAPI) { const data = seriesModel.getData(); @@ -70,6 +71,7 @@ class BoxplotView extends ChartView { symbolEl = createNormalBox(itemLayout, data, newIdx, constDim); } else { + saveOldStyle(symbolEl); updateNormalBoxData(itemLayout, symbolEl, data, newIdx); } @@ -139,7 +141,7 @@ class BoxPath extends Path { function createNormalBox( itemLayout: BoxplotItemLayout, - data: List, + data: SeriesData, dataIndex: number, constDim: number, isInit?: boolean @@ -162,7 +164,7 @@ function createNormalBox( function updateNormalBoxData( itemLayout: BoxplotItemLayout, el: BoxPath, - data: List, + data: SeriesData, dataIndex: number, isInit?: boolean ) { diff --git a/src/chart/candlestick/CandlestickSeries.ts b/src/chart/candlestick/CandlestickSeries.ts index 68e8b8a128..d71cecd57d 100644 --- a/src/chart/candlestick/CandlestickSeries.ts +++ b/src/chart/candlestick/CandlestickSeries.ts @@ -33,7 +33,7 @@ import { SeriesEncodeOptionMixin, DefaultEmphasisFocus } from '../../util/types'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import Cartesian2D from '../../coord/cartesian/Cartesian2D'; import { BrushCommonSelectorsForSeries } from '../../component/brush/selector'; import { mixin } from 'zrender/src/core/util'; @@ -151,7 +151,7 @@ class CandlestickSeriesModel extends SeriesModel { return 'open'; } - brushSelector(dataIndex: number, data: List, selectors: BrushCommonSelectorsForSeries): boolean { + brushSelector(dataIndex: number, data: SeriesData, selectors: BrushCommonSelectorsForSeries): boolean { const itemLayout = data.getItemLayout(dataIndex); return itemLayout && selectors.rect(itemLayout.brushRect); } diff --git a/src/chart/candlestick/CandlestickView.ts b/src/chart/candlestick/CandlestickView.ts index bc3cd981b5..7e62360074 100644 --- a/src/chart/candlestick/CandlestickView.ts +++ b/src/chart/candlestick/CandlestickView.ts @@ -27,10 +27,11 @@ import CandlestickSeriesModel, { CandlestickDataItemOption } from './Candlestick import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../core/ExtensionAPI'; import { StageHandlerProgressParams } from '../../util/types'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import {CandlestickItemLayout} from './candlestickLayout'; import { CoordinateSystemClipArea } from '../../coord/CoordinateSystem'; import Model from '../../model/Model'; +import { saveOldStyle } from '../../animation/basicTrasition'; const SKIP_PROPS = ['color', 'borderColor'] as const; @@ -41,7 +42,7 @@ class CandlestickView extends ChartView { private _isLargeDraw: boolean; - private _data: List; + private _data: SeriesData; render(seriesModel: CandlestickSeriesModel, ecModel: GlobalModel, api: ExtensionAPI) { // If there is clipPath created in large mode. Remove it. @@ -137,6 +138,8 @@ class CandlestickView extends ChartView { points: itemLayout.ends } }, seriesModel, newIdx); + + saveOldStyle(el); } setBoxCommon(el, data, newIdx, isSimpleBox); @@ -270,7 +273,7 @@ function isNormalBoxClipped(clipArea: CoordinateSystemClipArea, itemLayout: Cand return clipped; } -function setBoxCommon(el: NormalBoxPath, data: List, dataIndex: number, isSimpleBox?: boolean) { +function setBoxCommon(el: NormalBoxPath, data: SeriesData, dataIndex: number, isSimpleBox?: boolean) { const itemModel = data.getItemModel(dataIndex) as Model; el.useStyle(data.getItemVisual(dataIndex, 'style')); @@ -356,7 +359,7 @@ function createLarge(seriesModel: CandlestickSeriesModel, group: graphic.Group, } } -function setLargeStyle(sign: number, el: LargeBoxPath, seriesModel: CandlestickSeriesModel, data: List) { +function setLargeStyle(sign: number, el: LargeBoxPath, seriesModel: CandlestickSeriesModel, data: SeriesData) { // TODO put in visual? const borderColor = seriesModel.get(['itemStyle', sign > 0 ? 'borderColor' : 'borderColor0']) || seriesModel.get(['itemStyle', sign > 0 ? 'color' : 'color0']); diff --git a/src/chart/candlestick/candlestickLayout.ts b/src/chart/candlestick/candlestickLayout.ts index 615c684968..e81f80b7a1 100644 --- a/src/chart/candlestick/candlestickLayout.ts +++ b/src/chart/candlestick/candlestickLayout.ts @@ -22,11 +22,12 @@ import {subPixelOptimize} from '../../util/graphic'; import createRenderPlanner from '../helper/createRenderPlanner'; import {parsePercent} from '../../util/number'; -import {retrieve2} from 'zrender/src/core/util'; -import { StageHandler, StageHandlerProgressParams } from '../../util/types'; +import {map, retrieve2} from 'zrender/src/core/util'; +import { DimensionIndex, StageHandler, StageHandlerProgressParams } from '../../util/types'; import CandlestickSeriesModel from './CandlestickSeries'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import { RectLike } from 'zrender/src/core/BoundingRect'; +import DataStore from '../../data/DataStore'; const LargeArr = typeof Float32Array !== 'undefined' ? Float32Array : Array; @@ -56,12 +57,12 @@ const candlestickLayout: StageHandler = { const cDimIdx = 0; const vDimIdx = 1; const coordDims = ['x', 'y']; - const cDim = data.mapDimension(coordDims[cDimIdx]); - const vDims = data.mapDimensionsAll(coordDims[vDimIdx]); - const openDim = vDims[0]; - const closeDim = vDims[1]; - const lowestDim = vDims[2]; - const highestDim = vDims[3]; + const cDimI = data.getDimensionIndex(data.mapDimension(coordDims[cDimIdx])); + const vDimsI = map(data.mapDimensionsAll(coordDims[vDimIdx]), data.getDimensionIndex, data); + const openDimI = vDimsI[0]; + const closeDimI = vDimsI[1]; + const lowestDimI = vDimsI[2]; + const highestDimI = vDimsI[3]; data.setLayout({ candleWidth: candleWidth, @@ -69,7 +70,7 @@ const candlestickLayout: StageHandler = { isSimpleBox: candleWidth <= 1.3 } as CandlestickLayoutMeta); - if (cDim == null || vDims.length < 4) { + if (cDimI < 0 || vDimsI.length < 4) { return; } @@ -78,15 +79,16 @@ const candlestickLayout: StageHandler = { ? largeProgress : normalProgress }; - function normalProgress(params: StageHandlerProgressParams, data: List) { + function normalProgress(params: StageHandlerProgressParams, data: SeriesData) { let dataIndex; + const store = data.getStore(); while ((dataIndex = params.next()) != null) { - const axisDimVal = data.get(cDim, dataIndex) as number; - const openVal = data.get(openDim, dataIndex) as number; - const closeVal = data.get(closeDim, dataIndex) as number; - const lowestVal = data.get(lowestDim, dataIndex) as number; - const highestVal = data.get(highestDim, dataIndex) as number; + const axisDimVal = store.get(cDimI, dataIndex) as number; + const openVal = store.get(openDimI, dataIndex) as number; + const closeVal = store.get(closeDimI, dataIndex) as number; + const lowestVal = store.get(lowestDimI, dataIndex) as number; + const highestVal = store.get(highestDimI, dataIndex) as number; const ocLow = Math.min(openVal, closeVal); const ocHigh = Math.max(openVal, closeVal); @@ -108,7 +110,7 @@ const candlestickLayout: StageHandler = { ); data.setItemLayout(dataIndex, { - sign: getSign(data, dataIndex, openVal, closeVal, closeDim), + sign: getSign(store, dataIndex, openVal, closeVal, closeDimI), initBaseline: openVal > closeVal ? ocHighPoint[vDimIdx] : ocLowPoint[vDimIdx], // open point. ends: ends, @@ -162,7 +164,7 @@ const candlestickLayout: StageHandler = { } } - function largeProgress(params: StageHandlerProgressParams, data: List) { + function largeProgress(params: StageHandlerProgressParams, data: SeriesData) { // Structure: [sign, x, yhigh, ylow, sign, x, yhigh, ylow, ...] const points = new LargeArr(params.count * 4); let offset = 0; @@ -170,13 +172,14 @@ const candlestickLayout: StageHandler = { const tmpIn: number[] = []; const tmpOut: number[] = []; let dataIndex; + const store = data.getStore(); while ((dataIndex = params.next()) != null) { - const axisDimVal = data.get(cDim, dataIndex) as number; - const openVal = data.get(openDim, dataIndex) as number; - const closeVal = data.get(closeDim, dataIndex) as number; - const lowestVal = data.get(lowestDim, dataIndex) as number; - const highestVal = data.get(highestDim, dataIndex) as number; + const axisDimVal = store.get(cDimI, dataIndex) as number; + const openVal = store.get(openDimI, dataIndex) as number; + const closeVal = store.get(closeDimI, dataIndex) as number; + const lowestVal = store.get(lowestDimI, dataIndex) as number; + const highestVal = store.get(highestDimI, dataIndex) as number; if (isNaN(axisDimVal) || isNaN(lowestVal) || isNaN(highestVal)) { points[offset++] = NaN; @@ -184,7 +187,7 @@ const candlestickLayout: StageHandler = { continue; } - points[offset++] = getSign(data, dataIndex, openVal, closeVal, closeDim); + points[offset++] = getSign(store, dataIndex, openVal, closeVal, closeDimI); tmpIn[cDimIdx] = axisDimVal; @@ -202,8 +205,10 @@ const candlestickLayout: StageHandler = { } }; -function getSign(data: List, dataIndex: number, openVal: number, closeVal: number, closeDim: string) { - let sign; +function getSign( + store: DataStore, dataIndex: number, openVal: number, closeVal: number, closeDimI: DimensionIndex +): -1 | 1 { + let sign: -1 | 1; if (openVal > closeVal) { sign = -1; } @@ -213,7 +218,7 @@ function getSign(data: List, dataIndex: number, openVal: number, closeVal: numbe else { sign = dataIndex > 0 // If close === open, compare with close of last record - ? (data.get(closeDim, dataIndex - 1) <= closeVal ? 1 : -1) + ? (store.get(closeDimI, dataIndex - 1) <= closeVal ? 1 : -1) // No record of previous, set to be positive : 1; } @@ -221,7 +226,7 @@ function getSign(data: List, dataIndex: number, openVal: number, closeVal: numbe return sign; } -function calculateCandleWidth(seriesModel: CandlestickSeriesModel, data: List) { +function calculateCandleWidth(seriesModel: CandlestickSeriesModel, data: SeriesData) { const baseAxis = seriesModel.getBaseAxis(); let extent; diff --git a/src/chart/custom/CustomSeries.ts b/src/chart/custom/CustomSeries.ts new file mode 100644 index 0000000000..2b8bb3c9b2 --- /dev/null +++ b/src/chart/custom/CustomSeries.ts @@ -0,0 +1,432 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +import Displayable from 'zrender/src/graphic/Displayable'; +import { ImageStyleProps } from 'zrender/src/graphic/Image'; +import { PathProps, PathStyleProps } from 'zrender/src/graphic/Path'; +import { ZRenderType } from 'zrender/src/zrender'; +import { BarGridLayoutOptionForCustomSeries, BarGridLayoutResult } from '../../layout/barGrid'; +import { + BlurScope, + CallbackDataParams, + Dictionary, + DimensionLoose, + ItemStyleOption, + LabelOption, + OptionDataValue, + OrdinalRawValue, + ParsedValue, + SeriesDataType, + SeriesEncodeOptionMixin, + SeriesOnCalendarOptionMixin, + SeriesOnCartesianOptionMixin, + SeriesOnGeoOptionMixin, + SeriesOnPolarOptionMixin, + SeriesOnSingleOptionMixin, + SeriesOption, + TextCommonOption, + ZRStyleProps +} from '../../util/types'; +import Element, { ElementProps } from 'zrender/src/Element'; +import SeriesData, { DefaultDataVisual } from '../../data/SeriesData'; +import GlobalModel from '../../model/Global'; +import createSeriesData from '../helper/createSeriesData'; +import { makeInner } from '../../util/model'; +import { CoordinateSystem } from '../../coord/CoordinateSystem'; +import SeriesModel from '../../model/Series'; +import { + Arc, + BezierCurve, + Circle, + CompoundPath, + Ellipse, + Line, + Polygon, + Polyline, + Rect, + Ring, + Sector +} from '../../util/graphic'; +import { TextStyleProps } from 'zrender/src/graphic/Text'; + + +export interface LooseElementProps extends ElementProps { + style?: ZRStyleProps; + shape?: Dictionary; +} + +export type CustomExtraElementInfo = Dictionary; +export const TRANSFORM_PROPS = { + x: 1, + y: 1, + scaleX: 1, + scaleY: 1, + originX: 1, + originY: 1, + rotation: 1 +} as const; +export type TransformProp = keyof typeof TRANSFORM_PROPS; + +// Also compat with ec4, where +// `visual('color') visual('borderColor')` is supported. +export const STYLE_VISUAL_TYPE = { + color: 'fill', + borderColor: 'stroke' +} as const; +export type StyleVisualProps = keyof typeof STYLE_VISUAL_TYPE; + +export const NON_STYLE_VISUAL_PROPS = { + symbol: 1, + symbolSize: 1, + symbolKeepAspect: 1, + legendIcon: 1, + visualMeta: 1, + liftZ: 1, + decal: 1 +} as const; +export type NonStyleVisualProps = keyof typeof NON_STYLE_VISUAL_PROPS; + +// Do not declare "Dictionary" in TransitionAnyOption to restrict the type check. +export type TransitionAnyOption = { + transition?: TransitionAnyProps; + enterFrom?: Dictionary; + leaveTo?: Dictionary; +}; +type TransitionAnyProps = string | string[]; +type TransitionTransformOption = { + transition?: ElementRootTransitionProp | ElementRootTransitionProp[]; + enterFrom?: Dictionary; + leaveTo?: Dictionary; +}; +type ElementRootTransitionProp = TransformProp | 'shape' | 'extra' | 'style'; +type ShapeMorphingOption = { + /** + * If do shape morphing animation when type is changed. + * Only available on path. + */ + morph?: boolean +}; + +export interface CustomBaseDuringAPI { + // Usually other props do not need to be changed in animation during. + setTransform(key: TransformProp, val: number): this + getTransform(key: TransformProp): number; + setExtra(key: string, val: unknown): this + getExtra(key: string): unknown +} +export interface CustomDuringAPI< + StyleOpt extends any = any, + ShapeOpt extends any = any +> extends CustomBaseDuringAPI { + setShape(key: T, val: ShapeOpt[T]): this; + getShape(key: T): ShapeOpt[T]; + setStyle(key: T, val: StyleOpt[T]): this + getStyle(key: T): StyleOpt[T]; +}; + + +export interface CustomBaseElementOption extends Partial>, TransitionTransformOption { + // element type, required. + type: string; + id?: string; + // For animation diff. + name?: string; + info?: CustomExtraElementInfo; + // `false` means remove the textContent. + textContent?: CustomTextOption | false; + // `false` means remove the clipPath + clipPath?: CustomBaseZRPathOption | false; + // `extra` can be set in any el option for custom prop for annimation duration. + extra?: Dictionary & TransitionAnyOption; + // updateDuringAnimation + during?(params: CustomBaseDuringAPI): void; + + focus?: 'none' | 'self' | 'series' | ArrayLike + blurScope?: BlurScope +}; + +export interface CustomDisplayableOption extends CustomBaseElementOption, Partial> { + style?: ZRStyleProps & TransitionAnyOption; + during?(params: CustomDuringAPI): void; + /** + * @deprecated + */ + // `false` means remove emphasis trigger. + styleEmphasis?: ZRStyleProps | false; + emphasis?: CustomDisplayableOptionOnState; + blur?: CustomDisplayableOptionOnState; + select?: CustomDisplayableOptionOnState; +} +export interface CustomDisplayableOptionOnState extends Partial> { + // `false` means remove emphasis trigger. + style?: (ZRStyleProps & TransitionAnyOption) | false; + + + during?(params: CustomDuringAPI): void; +} +export interface CustomGroupOption extends CustomBaseElementOption { + type: 'group'; + width?: number; + height?: number; + // @deprecated + diffChildrenByName?: boolean; + children: CustomChildElementOption[]; + $mergeChildren?: false | 'byName' | 'byIndex'; +} +export interface CustomBaseZRPathOption + extends CustomDisplayableOption, ShapeMorphingOption { + autoBatch?: boolean; + shape?: T & TransitionAnyOption; + style?: PathProps['style'] + during?(params: CustomDuringAPI): void; +} + +interface BuiltinShapes { + 'circle': Circle['shape'] + 'rect': Rect['shape'] + 'sector': Sector['shape'] + 'poygon': Polygon['shape'] + 'polyline': Polyline['shape'] + 'line': Line['shape'] + 'arc': Arc['shape'] + 'bezierCurve': BezierCurve['shape'] + 'ring': Ring['shape'] + 'ellipse': Ellipse['shape'], + 'compoundPath': CompoundPath['shape'] +} + +interface CustomSVGPathShapeOption { + // SVG Path, like 'M0,0 L0,-20 L70,-1 L70,0 Z' + pathData?: string; + // "d" is the alias of `pathData` follows the SVG convention. + d?: string; + layout?: 'center' | 'cover'; + x?: number; + y?: number; + width?: number; + height?: number; +} +export interface CustomSVGPathOption extends CustomBaseZRPathOption { + type: 'path'; +} + +interface CustomBuitinPathOption + extends CustomBaseZRPathOption { + type: T +} +type CreateCustomBuitinPathOption = T extends any + ? CustomBuitinPathOption : never; + +export type CustomPathOption = CreateCustomBuitinPathOption + | CustomSVGPathOption; + +export interface CustomImageOptionOnState extends CustomDisplayableOptionOnState { + style?: ImageStyleProps & TransitionAnyOption; +} +export interface CustomImageOption extends CustomDisplayableOption { + type: 'image'; + style?: ImageStyleProps & TransitionAnyOption; + emphasis?: CustomImageOptionOnState; + blur?: CustomImageOptionOnState; + select?: CustomImageOptionOnState; +} + +export interface CustomTextOptionOnState extends CustomDisplayableOptionOnState { + style?: TextStyleProps & TransitionAnyOption; +} +export interface CustomTextOption extends CustomDisplayableOption { + type: 'text'; + style?: TextStyleProps & TransitionAnyOption; + emphasis?: CustomTextOptionOnState; + blur?: CustomTextOptionOnState; + select?: CustomTextOptionOnState; +} + +export type CustomElementOption = CustomPathOption + | CustomImageOption + | CustomTextOption + | CustomGroupOption; + +// Can only set focus, blur on the root element. +export type CustomChildElementOption = Omit; + +export type CustomElementOptionOnState = CustomDisplayableOptionOnState + | CustomImageOptionOnState; + +export interface CustomSeriesRenderItemAPI extends + CustomSeriesRenderItemCoordinateSystemAPI { + + // Methods from ExtensionAPI. + // NOTE: Not using Pick here because we don't want to bundle ExtensionAPI into the d.ts + getWidth(): number + getHeight(): number + getZr(): ZRenderType + getDevicePixelRatio(): number + + value(dim: DimensionLoose, dataIndexInside?: number): ParsedValue; + ordinalRawValue(dim: DimensionLoose, dataIndexInside?: number): ParsedValue | OrdinalRawValue; + style(userProps?: ZRStyleProps, dataIndexInside?: number): ZRStyleProps; + styleEmphasis(userProps?: ZRStyleProps, dataIndexInside?: number): ZRStyleProps; + visual( + visualType: VT, + dataIndexInside?: number + ): VT extends NonStyleVisualProps ? DefaultDataVisual[VT] + : VT extends StyleVisualProps ? PathStyleProps[typeof STYLE_VISUAL_TYPE[VT]] + : void; + barLayout(opt: BarGridLayoutOptionForCustomSeries): BarGridLayoutResult; + currentSeriesIndices(): number[]; + font(opt: Pick): string; +} +export interface CustomSeriesRenderItemParamsCoordSys { + type: string; + // And extra params for each coordinate systems. +} +export interface CustomSeriesRenderItemCoordinateSystemAPI { + coord( + data: OptionDataValue | OptionDataValue[], + clamp?: boolean + ): number[]; + size?( + dataSize: OptionDataValue | OptionDataValue[], + dataItem: OptionDataValue | OptionDataValue[] + ): number | number[]; +} + +export type WrapEncodeDefRet = Dictionary; + +export interface CustomSeriesRenderItemParams { + context: Dictionary; + dataIndex: number; + seriesId: string; + seriesName: string; + seriesIndex: number; + coordSys: CustomSeriesRenderItemParamsCoordSys; + encode: WrapEncodeDefRet; + + dataIndexInside: number; + dataInsideLength: number; + + actionType?: string; +} +type CustomSeriesRenderItem = ( + params: CustomSeriesRenderItemParams, + api: CustomSeriesRenderItemAPI +) => CustomElementOption; + +interface CustomSeriesStateOption { + itemStyle?: ItemStyleOption; + label?: LabelOption; +} + +export interface CustomSeriesOption extends + SeriesOption, // don't support StateOption in custom series. + SeriesEncodeOptionMixin, + SeriesOnCartesianOptionMixin, + SeriesOnPolarOptionMixin, + SeriesOnSingleOptionMixin, + SeriesOnGeoOptionMixin, + SeriesOnCalendarOptionMixin { + + type?: 'custom' + + // If set as 'none', do not depends on coord sys. + coordinateSystem?: string | 'none'; + + renderItem?: CustomSeriesRenderItem; + + // Only works on polar and cartesian2d coordinate system. + clip?: boolean; +} + +export interface LegacyCustomSeriesOption extends SeriesOption, CustomSeriesStateOption {} + +export const customInnerStore = makeInner<{ + info: CustomExtraElementInfo; + customPathData: string; + customGraphicType: string; + customImagePath: CustomImageOption['style']['image']; + // customText: string; + txConZ2Set: number; + leaveToProps: ElementProps; + option: CustomElementOption; + userDuring: CustomBaseElementOption['during']; +}, Element>(); + +export default class CustomSeriesModel extends SeriesModel { + + static type = 'series.custom'; + readonly type = CustomSeriesModel.type; + + static dependencies = ['grid', 'polar', 'geo', 'singleAxis', 'calendar']; + + // preventAutoZ = true; + + currentZLevel: number; + currentZ: number; + + static defaultOption: CustomSeriesOption = { + coordinateSystem: 'cartesian2d', // Can be set as 'none' + zlevel: 0, + z: 2, + legendHoverLink: true, + + // Custom series will not clip by default. + // Some case will use custom series to draw label + // For example https://echarts.apache.org/examples/en/editor.html?c=custom-gantt-flight + clip: false + + // Cartesian coordinate system + // xAxisIndex: 0, + // yAxisIndex: 0, + + // Polar coordinate system + // polarIndex: 0, + + // Geo coordinate system + // geoIndex: 0, + }; + + optionUpdated() { + this.currentZLevel = this.get('zlevel', true); + this.currentZ = this.get('z', true); + } + + getInitialData(option: CustomSeriesOption, ecModel: GlobalModel): SeriesData { + return createSeriesData(null, this); + } + + getDataParams(dataIndex: number, dataType?: SeriesDataType, el?: Element): CallbackDataParams & { + info: CustomExtraElementInfo + } { + const params = super.getDataParams(dataIndex, dataType) as ReturnType; + el && (params.info = customInnerStore(el).info); + return params; + } +} + +export type PrepareCustomInfo = (coordSys: CoordinateSystem) => { + coordSys: CustomSeriesRenderItemParamsCoordSys; + api: CustomSeriesRenderItemCoordinateSystemAPI +}; diff --git a/src/chart/custom/CustomView.ts b/src/chart/custom/CustomView.ts new file mode 100644 index 0000000000..1ba6d86b05 --- /dev/null +++ b/src/chart/custom/CustomView.ts @@ -0,0 +1,1648 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +import { + hasOwn, assert, isString, retrieve2, retrieve3, defaults, each, + keys, bind, eqNaN, indexOf +} from 'zrender/src/core/util'; +import * as graphicUtil from '../../util/graphic'; +import { setDefaultStateProxy, enableHoverEmphasis } from '../../util/states'; +import * as labelStyleHelper from '../../label/labelStyle'; +import {getDefaultLabel} from '../helper/labelHelper'; +import {getLayoutOnAxis} from '../../layout/barGrid'; +import DataDiffer from '../../data/DataDiffer'; +import Model from '../../model/Model'; +import ChartView from '../../view/Chart'; +import {createClipPath} from '../helper/createClipPathFromCoordSys'; +import { + EventQueryItem, ECActionEvent, + DimensionLoose, + ParsedValue, + Dictionary, + Payload, + StageHandlerProgressParams, + ViewRootGroup, + ZRStyleProps, + DisplayState, + ECElement, + DisplayStateNonNormal, + OrdinalRawValue, + InnerDecalObject +} from '../../util/types'; +import Element, { ElementProps, ElementTextConfig } from 'zrender/src/Element'; +import prepareCartesian2d from '../../coord/cartesian/prepareCustom'; +import prepareGeo from '../../coord/geo/prepareCustom'; +import prepareSingleAxis from '../../coord/single/prepareCustom'; +import preparePolar from '../../coord/polar/prepareCustom'; +import prepareCalendar from '../../coord/calendar/prepareCustom'; +import SeriesData, { DefaultDataVisual } from '../../data/SeriesData'; +import GlobalModel from '../../model/Global'; +import ExtensionAPI from '../../core/ExtensionAPI'; +import Displayable, { DisplayableProps } from 'zrender/src/graphic/Displayable'; +import Axis2D from '../../coord/cartesian/Axis2D'; +import { RectLike } from 'zrender/src/core/BoundingRect'; +import { PathStyleProps } from 'zrender/src/graphic/Path'; +import { TextStyleProps } from 'zrender/src/graphic/Text'; +import { + convertToEC4StyleForCustomSerise, + isEC4CompatibleStyle, + convertFromEC4CompatibleStyle, + LegacyStyleProps, + warnDeprecated +} from '../../util/styleCompat'; +import { ItemStyleProps } from '../../model/mixin/itemStyle'; +import { warn, throwError } from '../../util/log'; +import { createOrUpdatePatternFromDecal } from '../../util/decal'; +import CustomSeriesModel, { + CustomDuringAPI, + TransformProp, + TRANSFORM_PROPS, + CustomImageOption, + CustomBaseElementOption, + CustomElementOption, + CustomElementOptionOnState, + CustomSVGPathOption, + CustomBaseZRPathOption, + CustomDisplayableOption, + CustomSeriesRenderItemAPI, + CustomSeriesRenderItemParams, + LegacyCustomSeriesOption, + CustomGroupOption, + WrapEncodeDefRet, + NonStyleVisualProps, + StyleVisualProps, + STYLE_VISUAL_TYPE, + NON_STYLE_VISUAL_PROPS, + customInnerStore, + LooseElementProps, + PrepareCustomInfo, + CustomPathOption +} from './CustomSeries'; +import { + prepareShapeOrExtraAllPropsFinal, + prepareShapeOrExtraTransitionFrom, + prepareStyleTransitionFrom, + prepareTransformAllPropsFinal, + prepareTransformTransitionFrom +} from './prepare'; +import { PatternObject } from 'zrender/src/graphic/Pattern'; + +const transformPropNamesStr = keys(TRANSFORM_PROPS).join(', '); + +const EMPHASIS = 'emphasis' as const; +const NORMAL = 'normal' as const; +const BLUR = 'blur' as const; +const SELECT = 'select' as const; +const STATES = [NORMAL, EMPHASIS, BLUR, SELECT] as const; +const PATH_ITEM_STYLE = { + normal: ['itemStyle'], + emphasis: [EMPHASIS, 'itemStyle'], + blur: [BLUR, 'itemStyle'], + select: [SELECT, 'itemStyle'] +} as const; +const PATH_LABEL = { + normal: ['label'], + emphasis: [EMPHASIS, 'label'], + blur: [BLUR, 'label'], + select: [SELECT, 'label'] +} as const; +// Use prefix to avoid index to be the same as el.name, +// which will cause weird update animation. +const GROUP_DIFF_PREFIX = 'e\0\0'; + +type AttachedTxInfo = { + isLegacy: boolean; + normal: { + cfg: ElementTextConfig; + conOpt: CustomElementOption | false; + }; + emphasis: { + cfg: ElementTextConfig; + conOpt: CustomElementOptionOnState; + }; + blur: { + cfg: ElementTextConfig; + conOpt: CustomElementOptionOnState; + }; + select: { + cfg: ElementTextConfig; + conOpt: CustomElementOptionOnState; + }; +}; +const attachedTxInfoTmp = { + normal: {}, + emphasis: {}, + blur: {}, + select: {} +} as AttachedTxInfo; + + +/** + * To reduce total package size of each coordinate systems, the modules `prepareCustom` + * of each coordinate systems are not required by each coordinate systems directly, but + * required by the module `custom`. + * + * prepareInfoForCustomSeries {Function}: optional + * @return {Object} {coordSys: {...}, api: { + * coord: function (data, clamp) {}, // return point in global. + * size: function (dataSize, dataItem) {} // return size of each axis in coordSys. + * }} + */ +const prepareCustoms: Dictionary = { + cartesian2d: prepareCartesian2d, + geo: prepareGeo, + singleAxis: prepareSingleAxis, + polar: preparePolar, + calendar: prepareCalendar +}; + + +function isPath(el: Element): el is graphicUtil.Path { + return el instanceof graphicUtil.Path; +} +function isDisplayable(el: Element) : el is Displayable { + return el instanceof Displayable; +} +function copyElement(sourceEl: Element, targetEl: Element) { + targetEl.copyTransform(sourceEl); + if (isDisplayable(targetEl) && isDisplayable(sourceEl)) { + targetEl.setStyle(sourceEl.style); + targetEl.z = sourceEl.z; + targetEl.z2 = sourceEl.z2; + targetEl.zlevel = sourceEl.zlevel; + targetEl.invisible = sourceEl.invisible; + targetEl.ignore = sourceEl.ignore; + + if (isPath(targetEl) && isPath(sourceEl)) { + targetEl.setShape(sourceEl.shape); + } + } +} +export default class CustomChartView extends ChartView { + + static type = 'custom'; + readonly type = CustomChartView.type; + + private _data: SeriesData; + + render( + customSeries: CustomSeriesModel, + ecModel: GlobalModel, + api: ExtensionAPI, + payload: Payload + ): void { + const oldData = this._data; + const data = customSeries.getData(); + const group = this.group; + const renderItem = makeRenderItem(customSeries, data, ecModel, api); + + if (!oldData) { + // Previous render is incremental render or first render. + // Needs remove the incremental rendered elements. + group.removeAll(); + } + + data.diff(oldData) + .add(function (newIdx) { + createOrUpdateItem( + api, null, newIdx, renderItem(newIdx, payload), customSeries, group, + data + ); + }) + .remove(function (oldIdx) { + doRemoveEl(oldData.getItemGraphicEl(oldIdx), customSeries, group); + }) + .update(function (newIdx, oldIdx) { + const oldEl = oldData.getItemGraphicEl(oldIdx); + + createOrUpdateItem( + api, oldEl, newIdx, renderItem(newIdx, payload), customSeries, group, + data + ); + }) + .execute(); + + // Do clipping + const clipPath = customSeries.get('clip', true) + ? createClipPath(customSeries.coordinateSystem, false, customSeries) + : null; + if (clipPath) { + group.setClipPath(clipPath); + } + else { + group.removeClipPath(); + } + + this._data = data; + } + + incrementalPrepareRender( + customSeries: CustomSeriesModel, + ecModel: GlobalModel, + api: ExtensionAPI + ): void { + this.group.removeAll(); + this._data = null; + } + + incrementalRender( + params: StageHandlerProgressParams, + customSeries: CustomSeriesModel, + ecModel: GlobalModel, + api: ExtensionAPI, + payload: Payload + ): void { + const data = customSeries.getData(); + const renderItem = makeRenderItem(customSeries, data, ecModel, api); + function setIncrementalAndHoverLayer(el: Displayable) { + if (!el.isGroup) { + el.incremental = true; + el.ensureState('emphasis').hoverLayer = true; + } + } + for (let idx = params.start; idx < params.end; idx++) { + const el = createOrUpdateItem( + null, null, idx, renderItem(idx, payload), customSeries, this.group, data + ); + el && el.traverse(setIncrementalAndHoverLayer); + } + } + + filterForExposedEvent( + eventType: string, query: EventQueryItem, targetEl: Element, packedEvent: ECActionEvent + ): boolean { + const elementName = query.element; + if (elementName == null || targetEl.name === elementName) { + return true; + } + + // Enable to give a name on a group made by `renderItem`, and listen + // events that triggerd by its descendents. + while ((targetEl = (targetEl.__hostTarget || targetEl.parent)) && targetEl !== this.group) { + if (targetEl.name === elementName) { + return true; + } + } + + return false; + } +} + + +function createEl(elOption: CustomElementOption): Element { + const graphicType = elOption.type; + let el; + + // Those graphic elements are not shapes. They should not be + // overwritten by users, so do them first. + if (graphicType === 'path') { + const shape = (elOption as CustomSVGPathOption).shape; + // Using pathRect brings convenience to users sacle svg path. + const pathRect = (shape.width != null && shape.height != null) + ? { + x: shape.x || 0, + y: shape.y || 0, + width: shape.width, + height: shape.height + } as RectLike + : null; + const pathData = getPathData(shape); + // Path is also used for icon, so layout 'center' by default. + el = graphicUtil.makePath(pathData, null, pathRect, shape.layout || 'center'); + customInnerStore(el).customPathData = pathData; + } + else if (graphicType === 'image') { + el = new graphicUtil.Image({}); + customInnerStore(el).customImagePath = (elOption as CustomImageOption).style.image; + } + else if (graphicType === 'text') { + el = new graphicUtil.Text({}); + // customInnerStore(el).customText = (elOption.style as TextStyleProps).text; + } + else if (graphicType === 'group') { + el = new graphicUtil.Group(); + } + else if (graphicType === 'compoundPath') { + throw new Error('"compoundPath" is not supported yet.'); + } + else { + const Clz = graphicUtil.getShapeClass(graphicType); + if (!Clz) { + let errMsg = ''; + if (__DEV__) { + errMsg = 'graphic type "' + graphicType + '" can not be found.'; + } + throwError(errMsg); + } + el = new Clz(); + } + + customInnerStore(el).customGraphicType = graphicType; + el.name = elOption.name; + + // Compat ec4: the default z2 lift is 1. If changing the number, + // some cases probably be broken: hierarchy layout along z, like circle packing, + // where emphasis only intending to modify color/border rather than lift z2. + (el as ECElement).z2EmphasisLift = 1; + (el as ECElement).z2SelectLift = 1; + + return el; +} + + +/** + * ---------------------------------------------------------- + * [STRATEGY_MERGE] Merge properties or erase all properties: + * + * Based on the fact that the existing zr element probably be reused, we now consider whether + * merge or erase all properties to the exsiting elements. + * That is, if a certain props is not specified in the lastest return of `renderItem`: + * + "Merge" means that do not modify the value on the existing element. + * + "Erase all" means that use a default value to the existing element. + * + * "Merge" might bring some unexpected state retaining for users and "erase all" seams to be + * more safe. "erase all" force users to specify all of the props each time, which is recommanded + * in most cases. + * But "erase all" theoretically disables the chance of performance optimization (e.g., just + * generete shape and style at the first time rather than always do that). + * So we still use "merge" rather than "erase all". If users need "erase all", they can + * simple always set all of the props each time. + * Some "object-like" config like `textConfig`, `textContent`, `style` which are not needed for + * every elment, so we replace them only when user specify them. And the that is a total replace. + * + * TODO: there is no hint of 'isFirst' to users. So the performance enhancement can not be + * performed yet. Consider the case: + * (1) setOption to "mergeChildren" with a smaller children count + * (2) Use dataZoom to make an item disappear. + * (3) User dataZoom to make the item display again. At that time, renderItem need to return the + * full option rather than partial option to recreate the element. + * + * ---------------------------------------------- + * [STRATEGY_NULL] `hasOwnProperty` or `== null`: + * + * Ditinguishing "own property" probably bring little trouble to user when make el options. + * So we trade a {xx: null} or {xx: undefined} as "not specified" if possible rather than + * "set them to null/undefined". In most cases, props can not be cleared. Some typicall + * clearable props like `style`/`textConfig`/`textContent` we enable `false` to means + * "clear". In some othere special cases that the prop is able to set as null/undefined, + * but not suitable to use `false`, `hasOwnProperty` is checked. + * + * --------------------------------------------- + * [STRATEGY_TRANSITION] The rule of transition: + * + For props on the root level of a element: + * If there is no `transition` specified, tansform props will be transitioned by default, + * which is the same as the previous setting in echarts4 and suitable for the scenario + * of dataZoom change. + * If `transition` specified, only the specified props will be transitioned. + * + For props in `shape` and `style`: + * Only props specified in `transition` will be transitioned. + * + Break: + * Since ec5, do not make transition to shape by default, because it might result in + * performance issue (especially `points` of polygon) and do not necessary in most cases. + * + * @return if `isMorphTo`, return `allPropsFinal`. + */ + +interface InnerCustomZRPathOptionStyle extends PathStyleProps { + __decalPattern: PatternObject +} + +function updateElNormal( + // Can be null/undefined + api: ExtensionAPI, + el: Element, + dataIndex: number, + elOption: CustomElementOption, + attachedTxInfo: AttachedTxInfo, + seriesModel: CustomSeriesModel, + isInit: boolean, + isTextContent: boolean +): void { + + const txCfgOpt = attachedTxInfo && attachedTxInfo.normal.cfg; + if (txCfgOpt) { + // PENDING: whether use user object directly rather than clone? + // TODO:5.0 textConfig transition animation? + el.setTextConfig(txCfgOpt); + } + + // Do some normalization on style. + const styleOpt = elOption && (elOption as CustomDisplayableOption).style; + + if (styleOpt) { + if (el.type === 'text') { + const textOptionStyle = styleOpt as TextStyleProps; + // Compatible with ec4: if `textFill` or `textStroke` exists use them. + hasOwn(textOptionStyle, 'textFill') && ( + textOptionStyle.fill = (textOptionStyle as any).textFill + ); + hasOwn(textOptionStyle, 'textStroke') && ( + textOptionStyle.stroke = (textOptionStyle as any).textStroke + ); + } + + let decalPattern; + const decalObj = isPath(el) ? (styleOpt as CustomBaseZRPathOption['style']).decal : null; + if (api && decalObj) { + (decalObj as InnerDecalObject).dirty = true; + decalPattern = createOrUpdatePatternFromDecal(decalObj, api); + } + // Always overwrite in case user specify this prop. + (styleOpt as InnerCustomZRPathOptionStyle).__decalPattern = decalPattern; + } + + // Save the meta info for further morphing. Like apply on the sub morphing elements. + const store = customInnerStore(el); + store.userDuring = elOption.during; + + const transFromProps = {} as ElementProps; + const propsToSet = {} as ElementProps; + + prepareShapeOrExtraTransitionFrom('shape', el, elOption, transFromProps, isInit); + prepareShapeOrExtraAllPropsFinal('shape', elOption, propsToSet); + prepareTransformTransitionFrom(el, elOption, transFromProps, isInit); + prepareTransformAllPropsFinal(el, elOption, propsToSet); + prepareShapeOrExtraTransitionFrom('extra', el, elOption, transFromProps, isInit); + prepareShapeOrExtraAllPropsFinal('extra', elOption, propsToSet); + prepareStyleTransitionFrom(el, elOption, styleOpt, transFromProps, isInit); + (propsToSet as DisplayableProps).style = styleOpt; + applyPropsDirectly(el, propsToSet); + applyPropsTransition(el, dataIndex, seriesModel, transFromProps, isInit); + applyMiscProps(el, elOption, isTextContent); + + styleOpt ? el.dirty() : el.markRedraw(); +} + +function applyMiscProps( + el: Element, elOption: CustomElementOption, isTextContent: boolean +) { + // Merge by default. + hasOwn(elOption, 'silent') && (el.silent = elOption.silent); + hasOwn(elOption, 'ignore') && (el.ignore = elOption.ignore); + if (isDisplayable(el)) { + hasOwn(elOption, 'invisible') && (el.invisible = (elOption as CustomDisplayableOption).invisible); + } + if (isPath(el)) { + hasOwn(elOption, 'autoBatch') && (el.autoBatch = (elOption as CustomBaseZRPathOption).autoBatch); + } + + if (!isTextContent) { + // `elOption.info` enables user to mount some info on + // elements and use them in event handlers. + // Update them only when user specified, otherwise, remain. + hasOwn(elOption, 'info') && (customInnerStore(el).info = elOption.info); + } +} + +function applyPropsDirectly( + el: Element, + // Can be null/undefined + allPropsFinal: ElementProps +) { + const elDisplayable = el.isGroup ? null : el as Displayable; + const styleOpt = (allPropsFinal as Displayable).style; + + if (elDisplayable && styleOpt) { + + // PENDING: here the input style object is used directly. + // Good for performance but bad for compatibility control. + elDisplayable.useStyle(styleOpt); + + const decalPattern = (styleOpt as InnerCustomZRPathOptionStyle).__decalPattern; + if (decalPattern) { + elDisplayable.style.decal = decalPattern; + } + + // When style object changed, how to trade the existing animation? + // It is probably complicated and not needed to cover all the cases. + // But still need consider the case: + // (1) When using init animation on `style.opacity`, and before the animation + // ended users triggers an update by mousewhel. At that time the init + // animation should better be continued rather than terminated. + // So after `useStyle` called, we should change the animation target manually + // to continue the effect of the init animation. + // (2) PENDING: If the previous animation targeted at a `val1`, and currently we need + // to update the value to `val2` and no animation declared, should be terminate + // the previous animation or just modify the target of the animation? + // Therotically That will happen not only on `style` but also on `shape` and + // `transfrom` props. But we haven't handle this case at present yet. + // (3) PENDING: Is it proper to visit `animators` and `targetName`? + const animators = elDisplayable.animators; + for (let i = 0; i < animators.length; i++) { + const animator = animators[i]; + // targetName is the "topKey". + if (animator.targetName === 'style') { + animator.changeTarget(elDisplayable.style); + } + } + } + + if (allPropsFinal) { + // Not set style here. + (allPropsFinal as DisplayableProps).style = null; + // Set el to the final state firstly. + allPropsFinal && el.attr(allPropsFinal); + (allPropsFinal as DisplayableProps).style = styleOpt; + } +} + +function applyPropsTransition( + el: Element, + dataIndex: number, + seriesModel: CustomSeriesModel, + // Can be null/undefined + transFromProps: ElementProps, + isInit: boolean +): void { + if (transFromProps) { + // NOTE: Do not use `el.updateDuringAnimation` here becuase `el.updateDuringAnimation` will + // be called mutiple time in each animation frame. For example, if both "transform" props + // and shape props and style props changed, it will generate three animator and called + // one-by-one in each animation frame. + // We use the during in `animateTo/From` params. + const userDuring = customInnerStore(el).userDuring; + // For simplicity, if during not specified, the previous during will not work any more. + const cfgDuringCall = userDuring ? bind(duringCall, { el: el, userDuring: userDuring }) : null; + const cfg = { + dataIndex: dataIndex, + isFrom: true, + during: cfgDuringCall + }; + isInit + ? graphicUtil.initProps(el, transFromProps, seriesModel, cfg) + : graphicUtil.updateProps(el, transFromProps, seriesModel, cfg); + } +} + + +// Use it to avoid it be exposed to user. +const tmpDuringScope = {} as { + el: Element; + isShapeDirty: boolean; + isStyleDirty: boolean; +}; +const customDuringAPI: CustomDuringAPI = { + // Usually other props do not need to be changed in animation during. + setTransform(key: TransformProp, val: unknown) { + if (__DEV__) { + assert(hasOwn(TRANSFORM_PROPS, key), 'Only ' + transformPropNamesStr + ' available in `setTransform`.'); + } + tmpDuringScope.el[key] = val as number; + return this; + }, + getTransform(key: TransformProp): number { + if (__DEV__) { + assert(hasOwn(TRANSFORM_PROPS, key), 'Only ' + transformPropNamesStr + ' available in `getTransform`.'); + } + return tmpDuringScope.el[key]; + }, + setShape(key: any, val: unknown) { + if (__DEV__) { + assertNotReserved(key); + } + const shape = (tmpDuringScope.el as graphicUtil.Path).shape + || ((tmpDuringScope.el as graphicUtil.Path).shape = {}); + shape[key] = val; + tmpDuringScope.isShapeDirty = true; + return this; + }, + getShape(key: any): any { + if (__DEV__) { + assertNotReserved(key); + } + const shape = (tmpDuringScope.el as graphicUtil.Path).shape; + if (shape) { + return shape[key]; + } + }, + setStyle(key: any, val: unknown) { + if (__DEV__) { + assertNotReserved(key); + } + const style = (tmpDuringScope.el as Displayable).style; + if (style) { + if (__DEV__) { + if (eqNaN(val)) { + warn('style.' + key + ' must not be assigned with NaN.'); + } + } + style[key] = val; + tmpDuringScope.isStyleDirty = true; + } + return this; + }, + getStyle(key: any): any { + if (__DEV__) { + assertNotReserved(key); + } + const style = (tmpDuringScope.el as Displayable).style; + if (style) { + return style[key]; + } + }, + setExtra(key: any, val: unknown) { + if (__DEV__) { + assertNotReserved(key); + } + const extra = (tmpDuringScope.el as LooseElementProps).extra + || ((tmpDuringScope.el as LooseElementProps).extra = {}); + extra[key] = val; + return this; + }, + getExtra(key: string): unknown { + if (__DEV__) { + assertNotReserved(key); + } + const extra = (tmpDuringScope.el as LooseElementProps).extra; + if (extra) { + return extra[key]; + } + } +}; + +function assertNotReserved(key: string) { + if (__DEV__) { + if (key === 'transition' || key === 'enterFrom' || key === 'leaveTo') { + throw new Error('key must not be "' + key + '"'); + } + } +} + +function duringCall( + this: { + el: Element; + userDuring: CustomBaseElementOption['during'] + } +): void { + // Do not provide "percent" until some requirements come. + // Because consider thies case: + // enterFrom: {x: 100, y: 30}, transition: 'x'. + // And enter duration is different from update duration. + // Thus it might be confused about the meaning of "percent" in during callback. + const scope = this; + const el = scope.el; + if (!el) { + return; + } + // If el is remove from zr by reason like legend, during still need to called, + // becuase el will be added back to zr and the prop value should not be incorrect. + + const latestUserDuring = customInnerStore(el).userDuring; + const scopeUserDuring = scope.userDuring; + // Ensured a during is only called once in each animation frame. + // If a during is called multiple times in one frame, maybe some users' calulation logic + // might be wrong (not sure whether this usage exists). + // The case of a during might be called twice can be: by default there is a animator for + // 'x', 'y' when init. Before the init animation finished, call `setOption` to start + // another animators for 'style'/'shape'/'extra'. + if (latestUserDuring !== scopeUserDuring) { + // release + scope.el = scope.userDuring = null; + return; + } + + tmpDuringScope.el = el; + tmpDuringScope.isShapeDirty = false; + tmpDuringScope.isStyleDirty = false; + + // Give no `this` to user in "during" calling. + scopeUserDuring(customDuringAPI); + + if (tmpDuringScope.isShapeDirty && (el as graphicUtil.Path).dirtyShape) { + (el as graphicUtil.Path).dirtyShape(); + } + if (tmpDuringScope.isStyleDirty && (el as Displayable).dirtyStyle) { + (el as Displayable).dirtyStyle(); + } + // markRedraw() will be called by default in during. + // FIXME `this.markRedraw();` directly ? + + // FIXME: if in future meet the case that some prop will be both modified in `during` and `state`, + // consider the issue that the prop might be incorrect when return to "normal" state. +} + +function updateElOnState( + state: DisplayStateNonNormal, + el: Element, + elStateOpt: CustomElementOptionOnState, + styleOpt: CustomElementOptionOnState['style'], + attachedTxInfo: AttachedTxInfo, + isRoot: boolean, + isTextContent: boolean +): void { + const elDisplayable = el.isGroup ? null : el as Displayable; + const txCfgOpt = attachedTxInfo && attachedTxInfo[state].cfg; + + // PENDING:5.0 support customize scale change and transition animation? + + if (elDisplayable) { + // By default support auto lift color when hover whether `emphasis` specified. + const stateObj = elDisplayable.ensureState(state); + if (styleOpt === false) { + const existingEmphasisState = elDisplayable.getState(state); + if (existingEmphasisState) { + existingEmphasisState.style = null; + } + } + else { + // style is needed to enable defaut emphasis. + stateObj.style = styleOpt || null; + } + // If `elOption.styleEmphasis` or `elOption.emphasis.style` is `false`, + // remove hover style. + // If `elOption.textConfig` or `elOption.emphasis.textConfig` is null/undefined, it does not + // make sense. So for simplicity, we do not ditinguish `hasOwnProperty` and null/undefined. + if (txCfgOpt) { + stateObj.textConfig = txCfgOpt; + } + + setDefaultStateProxy(elDisplayable); + } +} + +function updateZ( + el: Element, + elOption: CustomElementOption, + seriesModel: CustomSeriesModel +): void { + // Group not support textContent and not support z yet. + if (el.isGroup) { + return; + } + + const elDisplayable = el as Displayable; + const currentZ = seriesModel.currentZ; + const currentZLevel = seriesModel.currentZLevel; + // Always erase. + elDisplayable.z = currentZ; + elDisplayable.zlevel = currentZLevel; + // z2 must not be null/undefined, otherwise sort error may occur. + const optZ2 = (elOption as CustomDisplayableOption).z2; + optZ2 != null && (elDisplayable.z2 = optZ2 || 0); + + for (let i = 0; i < STATES.length; i++) { + updateZForEachState(elDisplayable, elOption, STATES[i]); + } +} + +function updateZForEachState( + elDisplayable: Displayable, + elOption: CustomDisplayableOption, + state: DisplayState +): void { + const isNormal = state === NORMAL; + const elStateOpt = isNormal ? elOption : retrieveStateOption( + elOption as CustomElementOption, + state as DisplayStateNonNormal + ); + const optZ2 = elStateOpt ? elStateOpt.z2 : null; + let stateObj; + if (optZ2 != null) { + // Do not `ensureState` until required. + stateObj = isNormal ? elDisplayable : elDisplayable.ensureState(state); + stateObj.z2 = optZ2 || 0; + } +} + +function makeRenderItem( + customSeries: CustomSeriesModel, + data: SeriesData, + ecModel: GlobalModel, + api: ExtensionAPI +) { + const renderItem = customSeries.get('renderItem'); + const coordSys = customSeries.coordinateSystem; + let prepareResult = {} as ReturnType; + + if (coordSys) { + if (__DEV__) { + assert(renderItem, 'series.render is required.'); + assert( + coordSys.prepareCustoms || prepareCustoms[coordSys.type], + 'This coordSys does not support custom series.' + ); + } + + // `coordSys.prepareCustoms` is used for external coord sys like bmap. + prepareResult = coordSys.prepareCustoms + ? coordSys.prepareCustoms(coordSys) + : prepareCustoms[coordSys.type](coordSys); + } + + const userAPI = defaults({ + getWidth: api.getWidth, + getHeight: api.getHeight, + getZr: api.getZr, + getDevicePixelRatio: api.getDevicePixelRatio, + value: value, + style: style, + ordinalRawValue: ordinalRawValue, + styleEmphasis: styleEmphasis, + visual: visual, + barLayout: barLayout, + currentSeriesIndices: currentSeriesIndices, + font: font + }, prepareResult.api || {}) as CustomSeriesRenderItemAPI; + + const userParams: CustomSeriesRenderItemParams = { + // The life cycle of context: current round of rendering. + // The global life cycle is probably not necessary, because + // user can store global status by themselves. + context: {}, + seriesId: customSeries.id, + seriesName: customSeries.name, + seriesIndex: customSeries.seriesIndex, + coordSys: prepareResult.coordSys, + dataInsideLength: data.count(), + encode: wrapEncodeDef(customSeries.getData()) + } as CustomSeriesRenderItemParams; + + // If someday intending to refactor them to a class, should consider do not + // break change: currently these attribute member are encapsulated in a closure + // so that do not need to force user to call these method with a scope. + + // Do not support call `api` asynchronously without dataIndexInside input. + let currDataIndexInside: number; + let currItemModel: Model; + let currItemStyleModels: Partial>> = {}; + let currLabelModels: Partial>> = {}; + + const seriesItemStyleModels = {} as Record>; + + const seriesLabelModels = {} as Record>; + + for (let i = 0; i < STATES.length; i++) { + const stateName = STATES[i]; + seriesItemStyleModels[stateName] = (customSeries as Model) + .getModel(PATH_ITEM_STYLE[stateName]); + seriesLabelModels[stateName] = (customSeries as Model) + .getModel(PATH_LABEL[stateName]); + } + + function getItemModel(dataIndexInside: number): Model { + return dataIndexInside === currDataIndexInside + ? (currItemModel || (currItemModel = data.getItemModel(dataIndexInside))) + : data.getItemModel(dataIndexInside); + } + function getItemStyleModel(dataIndexInside: number, state: DisplayState) { + return !data.hasItemOption + ? seriesItemStyleModels[state] + : dataIndexInside === currDataIndexInside + ? (currItemStyleModels[state] || ( + currItemStyleModels[state] = getItemModel(dataIndexInside).getModel(PATH_ITEM_STYLE[state]) + )) + : getItemModel(dataIndexInside).getModel(PATH_ITEM_STYLE[state]); + } + function getLabelModel(dataIndexInside: number, state: DisplayState) { + return !data.hasItemOption + ? seriesLabelModels[state] + : dataIndexInside === currDataIndexInside + ? (currLabelModels[state] || ( + currLabelModels[state] = getItemModel(dataIndexInside).getModel(PATH_LABEL[state]) + )) + : getItemModel(dataIndexInside).getModel(PATH_LABEL[state]); + } + + return function (dataIndexInside: number, payload: Payload): CustomElementOption { + currDataIndexInside = dataIndexInside; + currItemModel = null; + currItemStyleModels = {}; + currLabelModels = {}; + + return renderItem && renderItem( + defaults({ + dataIndexInside: dataIndexInside, + dataIndex: data.getRawIndex(dataIndexInside), + // Can be used for optimization when zoom or roam. + actionType: payload ? payload.type : null + } as CustomSeriesRenderItemParams, userParams), + userAPI + ); + }; + + /** + * @public + * @param dim by default 0. + * @param dataIndexInside by default `currDataIndexInside`. + */ + function value(dim?: DimensionLoose, dataIndexInside?: number): ParsedValue { + dataIndexInside == null && (dataIndexInside = currDataIndexInside); + return data.getStore().get(data.getDimensionIndex(dim || 0), dataIndexInside); + } + + /** + * @public + * @param dim by default 0. + * @param dataIndexInside by default `currDataIndexInside`. + */ + function ordinalRawValue(dim?: DimensionLoose, dataIndexInside?: number): ParsedValue | OrdinalRawValue { + dataIndexInside == null && (dataIndexInside = currDataIndexInside); + dim = dim || 0; + const dimInfo = data.getDimensionInfo(dim); + if (!dimInfo) { + const dimIndex = data.getDimensionIndex(dim); + return dimIndex >= 0 ? data.getStore().get(dimIndex, dataIndexInside) : undefined; + } + const val = data.get(dimInfo.name, dataIndexInside); + const ordinalMeta = dimInfo && dimInfo.ordinalMeta; + return ordinalMeta + ? ordinalMeta.categories[val as number] + : val; + } + + /** + * @deprecated The orgininal intention of `api.style` is enable to set itemStyle + * like other series. But it not necessary and not easy to give a strict definition + * of what it return. And since echarts5 it needs to be make compat work. So + * deprecates it since echarts5. + * + * By default, `visual` is applied to style (to support visualMap). + * `visual.color` is applied at `fill`. If user want apply visual.color on `stroke`, + * it can be implemented as: + * `api.style({stroke: api.visual('color'), fill: null})`; + * + * [Compat]: since ec5, RectText has been separated from its hosts el. + * so `api.style()` will only return the style from `itemStyle` but not handle `label` + * any more. But `series.label` config is never published in doc. + * We still compat it in `api.style()`. But not encourage to use it and will still not + * to pulish it to doc. + * @public + * @param dataIndexInside by default `currDataIndexInside`. + */ + function style(userProps?: ZRStyleProps, dataIndexInside?: number): ZRStyleProps { + if (__DEV__) { + warnDeprecated('api.style', 'Please write literal style directly instead.'); + } + + dataIndexInside == null && (dataIndexInside = currDataIndexInside); + + const style = data.getItemVisual(dataIndexInside, 'style'); + const visualColor = style && style.fill; + const opacity = style && style.opacity; + + let itemStyle = getItemStyleModel(dataIndexInside, NORMAL).getItemStyle(); + visualColor != null && (itemStyle.fill = visualColor); + opacity != null && (itemStyle.opacity = opacity); + + const opt = {inheritColor: isString(visualColor) ? visualColor : '#000'}; + const labelModel = getLabelModel(dataIndexInside, NORMAL); + // Now that the feture of "auto adjust text fill/stroke" has been migrated to zrender + // since ec5, we should set `isAttached` as `false` here and make compat in + // `convertToEC4StyleForCustomSerise`. + const textStyle = labelStyleHelper.createTextStyle(labelModel, null, opt, false, true); + textStyle.text = labelModel.getShallow('show') + ? retrieve2( + customSeries.getFormattedLabel(dataIndexInside, NORMAL), + getDefaultLabel(data, dataIndexInside) + ) + : null; + const textConfig = labelStyleHelper.createTextConfig(labelModel, opt, false); + + preFetchFromExtra(userProps, itemStyle); + itemStyle = convertToEC4StyleForCustomSerise(itemStyle, textStyle, textConfig); + + userProps && applyUserPropsAfter(itemStyle, userProps); + (itemStyle as LegacyStyleProps).legacy = true; + + return itemStyle; + } + + /** + * @deprecated The reason see `api.style()` + * @public + * @param dataIndexInside by default `currDataIndexInside`. + */ + function styleEmphasis(userProps?: ZRStyleProps, dataIndexInside?: number): ZRStyleProps { + if (__DEV__) { + warnDeprecated('api.styleEmphasis', 'Please write literal style directly instead.'); + } + + dataIndexInside == null && (dataIndexInside = currDataIndexInside); + + let itemStyle = getItemStyleModel(dataIndexInside, EMPHASIS).getItemStyle(); + const labelModel = getLabelModel(dataIndexInside, EMPHASIS); + const textStyle = labelStyleHelper.createTextStyle(labelModel, null, null, true, true); + textStyle.text = labelModel.getShallow('show') + ? retrieve3( + customSeries.getFormattedLabel(dataIndexInside, EMPHASIS), + customSeries.getFormattedLabel(dataIndexInside, NORMAL), + getDefaultLabel(data, dataIndexInside) + ) + : null; + const textConfig = labelStyleHelper.createTextConfig(labelModel, null, true); + + preFetchFromExtra(userProps, itemStyle); + itemStyle = convertToEC4StyleForCustomSerise(itemStyle, textStyle, textConfig); + + userProps && applyUserPropsAfter(itemStyle, userProps); + (itemStyle as LegacyStyleProps).legacy = true; + + return itemStyle; + } + + function applyUserPropsAfter(itemStyle: ZRStyleProps, extra: ZRStyleProps): void { + for (const key in extra) { + if (hasOwn(extra, key)) { + (itemStyle as any)[key] = (extra as any)[key]; + } + } + } + + function preFetchFromExtra(extra: ZRStyleProps, itemStyle: ItemStyleProps): void { + // A trick to retrieve those props firstly, which are used to + // apply auto inside fill/stroke in `convertToEC4StyleForCustomSerise`. + // (It's not reasonable but only for a degree of compat) + if (extra) { + (extra as any).textFill && ((itemStyle as any).textFill = (extra as any).textFill); + (extra as any).textPosition && ((itemStyle as any).textPosition = (extra as any).textPosition); + } + } + + /** + * @public + * @param dataIndexInside by default `currDataIndexInside`. + */ + function visual( + visualType: VT, + dataIndexInside?: number + ): VT extends NonStyleVisualProps ? DefaultDataVisual[VT] + : VT extends StyleVisualProps ? PathStyleProps[typeof STYLE_VISUAL_TYPE[VT]] + : never { + + dataIndexInside == null && (dataIndexInside = currDataIndexInside); + + if (hasOwn(STYLE_VISUAL_TYPE, visualType)) { + const style = data.getItemVisual(dataIndexInside, 'style'); + return style + ? style[STYLE_VISUAL_TYPE[visualType as StyleVisualProps]] as any + : null; + } + // Only support these visuals. Other visual might be inner tricky + // for performance (like `style`), do not expose to users. + if (hasOwn(NON_STYLE_VISUAL_PROPS, visualType)) { + return data.getItemVisual(dataIndexInside, visualType as NonStyleVisualProps) as any; + } + } + + /** + * @public + * @return If not support, return undefined. + */ + function barLayout( + opt: Omit[0], 'axis'> + ): ReturnType { + if (coordSys.type === 'cartesian2d') { + const baseAxis = coordSys.getBaseAxis() as Axis2D; + return getLayoutOnAxis(defaults({axis: baseAxis}, opt)); + } + } + + /** + * @public + */ + function currentSeriesIndices(): ReturnType { + return ecModel.getCurrentSeriesIndices(); + } + + /** + * @public + * @return font string + */ + function font( + opt: Parameters[0] + ): ReturnType { + return labelStyleHelper.getFont(opt, ecModel); + } +} + +function wrapEncodeDef(data: SeriesData): WrapEncodeDefRet { + const encodeDef = {} as WrapEncodeDefRet; + each(data.dimensions, function (dimName) { + const dimInfo = data.getDimensionInfo(dimName); + if (!dimInfo.isExtraCoord) { + const coordDim = dimInfo.coordDim; + const dataDims = encodeDef[coordDim] = encodeDef[coordDim] || []; + dataDims[dimInfo.coordDimIndex] = data.getDimensionIndex(dimName); + } + }); + return encodeDef; +} + +function createOrUpdateItem( + api: ExtensionAPI, + existsEl: Element, + dataIndex: number, + elOption: CustomElementOption, + seriesModel: CustomSeriesModel, + group: ViewRootGroup, + data: SeriesData +): Element { + // [Rule] + // If `renderItem` returns `null`/`undefined`/`false`, remove the previous el if existing. + // (It seems that violate the "merge" principle, but most of users probably intuitively + // regard "return;" as "show nothing element whatever", so make a exception to meet the + // most cases.) + // The rule or "merge" see [STRATEGY_MERGE]. + + // If `elOption` is `null`/`undefined`/`false` (when `renderItem` returns nothing). + if (!elOption) { + group.remove(existsEl); + return; + } + const el = doCreateOrUpdateEl(api, existsEl, dataIndex, elOption, seriesModel, group, true); + el && data.setItemGraphicEl(dataIndex, el); + + el && enableHoverEmphasis(el, elOption.focus, elOption.blurScope); + + return el; +} + +function doCreateOrUpdateEl( + api: ExtensionAPI, + existsEl: Element, + dataIndex: number, + elOption: CustomElementOption, + seriesModel: CustomSeriesModel, + group: ViewRootGroup, + isRoot: boolean +): Element { + + if (__DEV__) { + assert(elOption, 'should not have an null/undefined element setting'); + } + + let toBeReplacedIdx = -1; + const oldEl = existsEl; + if ( + existsEl && ( + doesElNeedRecreate(existsEl, elOption, seriesModel) + // || ( + // // PENDING: even in one-to-one mapping case, if el is marked as morph, + // // do not sure whether the el will be mapped to another el with different + // // hierarchy in Group tree. So always recreate el rather than reuse the el. + // morphHelper && morphHelper.isOneToOneFrom(el) + // ) + ) + ) { + // Should keep at the original index, otherwise "merge by index" will be incorrect. + toBeReplacedIdx = indexOf(group.childrenRef(), existsEl); + existsEl = null; + } + + const isInit = !existsEl; + let el = existsEl; + + if (!el) { + el = createEl(elOption); + if (oldEl) { + copyElement(oldEl, el); + } + } + else { + // FIMXE:NEXT unified clearState? + // If in some case the performance issue arised, consider + // do not clearState but update cached normal state directly. + el.clearStates(); + } + + // Need to set morph: false explictly to disable automatically morphing. + if ((elOption as CustomBaseZRPathOption).morph === false) { + (el as ECElement).disableMorphing = true; + } + else if ((el as ECElement).disableMorphing) { + (el as ECElement).disableMorphing = false; + } + + attachedTxInfoTmp.normal.cfg = attachedTxInfoTmp.normal.conOpt = + attachedTxInfoTmp.emphasis.cfg = attachedTxInfoTmp.emphasis.conOpt = + attachedTxInfoTmp.blur.cfg = attachedTxInfoTmp.blur.conOpt = + attachedTxInfoTmp.select.cfg = attachedTxInfoTmp.select.conOpt = null; + + attachedTxInfoTmp.isLegacy = false; + + doCreateOrUpdateAttachedTx( + el, dataIndex, elOption, seriesModel, isInit, attachedTxInfoTmp + ); + + doCreateOrUpdateClipPath( + el, dataIndex, elOption, seriesModel, isInit + ); + + updateElNormal( + api, + el, + dataIndex, + elOption, + attachedTxInfoTmp, + seriesModel, + isInit, + false + ); + + for (let i = 0; i < STATES.length; i++) { + const stateName = STATES[i]; + if (stateName !== NORMAL) { + const otherStateOpt = retrieveStateOption(elOption, stateName); + const otherStyleOpt = retrieveStyleOptionOnState(elOption, otherStateOpt, stateName); + updateElOnState(stateName, el, otherStateOpt, otherStyleOpt, attachedTxInfoTmp, isRoot, false); + } + } + + updateZ(el, elOption, seriesModel); + + if (elOption.type === 'group') { + mergeChildren( + api, el as graphicUtil.Group, dataIndex, elOption as CustomGroupOption, seriesModel + ); + } + + if (toBeReplacedIdx >= 0) { + group.replaceAt(el, toBeReplacedIdx); + } + else { + group.add(el); + } + + return el; +} + +// `el` must not be null/undefined. +function doesElNeedRecreate(el: Element, elOption: CustomElementOption, seriesModel: CustomSeriesModel): boolean { + const elInner = customInnerStore(el); + const elOptionType = elOption.type; + const elOptionShape = (elOption as CustomBaseZRPathOption).shape; + const elOptionStyle = (elOption as CustomDisplayableOption).style; + return ( + // Always create new if universal transition is enabled. + // Because we do transition after render. It needs to know what old element is. Replacement will loose it. + seriesModel.isUniversalTransitionEnabled() + // If `elOptionType` is `null`, follow the merge principle. + || (elOptionType != null + && elOptionType !== elInner.customGraphicType + ) + || (elOptionType === 'path' + && hasOwnPathData(elOptionShape) + && getPathData(elOptionShape) !== elInner.customPathData + ) + || (elOptionType === 'image' + && hasOwn(elOptionStyle, 'image') + && (elOptionStyle as CustomImageOption['style']).image !== elInner.customImagePath + ) + // // FIXME test and remove this restriction? + // || (elOptionType === 'text' + // && hasOwn(elOptionStyle, 'text') + // && (elOptionStyle as TextStyleProps).text !== elInner.customText + // ) + ); +} + +function doCreateOrUpdateClipPath( + el: Element, + dataIndex: number, + elOption: CustomElementOption, + seriesModel: CustomSeriesModel, + isInit: boolean +): void { + // Based on the "merge" principle, if no clipPath provided, + // do nothing. The exists clip will be totally removed only if + // `el.clipPath` is `false`. Otherwise it will be merged/replaced. + const clipPathOpt = elOption.clipPath as CustomPathOption | false; + if (clipPathOpt === false) { + if (el && el.getClipPath()) { + el.removeClipPath(); + } + } + else if (clipPathOpt) { + let clipPath = el.getClipPath(); + if (clipPath && doesElNeedRecreate( + clipPath, + clipPathOpt, + seriesModel + )) { + clipPath = null; + } + if (!clipPath) { + clipPath = createEl(clipPathOpt) as graphicUtil.Path; + if (__DEV__) { + assert( + isPath(clipPath), + 'Only any type of `path` can be used in `clipPath`, rather than ' + clipPath.type + '.' + ); + } + el.setClipPath(clipPath); + } + updateElNormal( + null, clipPath, dataIndex, clipPathOpt, null, seriesModel, isInit, false + ); + } + // If not define `clipPath` in option, do nothing unnecessary. +} + +function doCreateOrUpdateAttachedTx( + el: Element, + dataIndex: number, + elOption: CustomElementOption, + seriesModel: CustomSeriesModel, + isInit: boolean, + attachedTxInfo: AttachedTxInfo +): void { + // group do not support textContent temporarily untill necessary. + if (el.isGroup) { + return; + } + + // Normal must be called before emphasis, for `isLegacy` detection. + processTxInfo(elOption, null, attachedTxInfo); + processTxInfo(elOption, EMPHASIS, attachedTxInfo); + + // If `elOption.textConfig` or `elOption.textContent` is null/undefined, it does not make sence. + // So for simplicity, if "elOption hasOwnProperty of them but be null/undefined", we do not + // trade them as set to null to el. + // Especially: + // `elOption.textContent: false` means remove textContent. + // `elOption.textContent.emphasis.style: false` means remove the style from emphasis state. + let txConOptNormal = attachedTxInfo.normal.conOpt as CustomElementOption | false; + const txConOptEmphasis = attachedTxInfo.emphasis.conOpt as CustomElementOptionOnState; + const txConOptBlur = attachedTxInfo.blur.conOpt as CustomElementOptionOnState; + const txConOptSelect = attachedTxInfo.select.conOpt as CustomElementOptionOnState; + + if (txConOptNormal != null || txConOptEmphasis != null || txConOptSelect != null || txConOptBlur != null) { + let textContent = el.getTextContent(); + if (txConOptNormal === false) { + textContent && el.removeTextContent(); + } + else { + txConOptNormal = attachedTxInfo.normal.conOpt = txConOptNormal || {type: 'text'}; + if (!textContent) { + textContent = createEl(txConOptNormal) as graphicUtil.Text; + el.setTextContent(textContent); + } + else { + // If in some case the performance issue arised, consider + // do not clearState but update cached normal state directly. + textContent.clearStates(); + } + + updateElNormal( + null, textContent, dataIndex, txConOptNormal, null, seriesModel, isInit, true + ); + const txConStlOptNormal = txConOptNormal && (txConOptNormal as CustomDisplayableOption).style; + for (let i = 0; i < STATES.length; i++) { + const stateName = STATES[i]; + if (stateName !== NORMAL) { + const txConOptOtherState = attachedTxInfo[stateName].conOpt as CustomElementOptionOnState; + updateElOnState( + stateName, + textContent, + txConOptOtherState, + retrieveStyleOptionOnState(txConOptNormal, txConOptOtherState, stateName), + null, false, true + ); + } + } + + txConStlOptNormal ? textContent.dirty() : textContent.markRedraw(); + } + } +} + +function processTxInfo( + elOption: CustomElementOption, + state: DisplayStateNonNormal, + attachedTxInfo: AttachedTxInfo +): void { + const stateOpt = !state ? elOption : retrieveStateOption(elOption, state); + const styleOpt = !state + ? (elOption as CustomDisplayableOption).style + : retrieveStyleOptionOnState(elOption, stateOpt, EMPHASIS); + + const elType = elOption.type; + let txCfg = stateOpt ? stateOpt.textConfig : null; + const txConOptNormal = elOption.textContent; + let txConOpt: CustomElementOption | CustomElementOptionOnState = + !txConOptNormal ? null : !state ? txConOptNormal : retrieveStateOption(txConOptNormal, state); + + if (styleOpt && ( + // Because emphasis style has little info to detect legacy, + // if normal is legacy, emphasis is trade as legacy. + attachedTxInfo.isLegacy + || isEC4CompatibleStyle(styleOpt, elType, !!txCfg, !!txConOpt) + )) { + attachedTxInfo.isLegacy = true; + const convertResult = convertFromEC4CompatibleStyle(styleOpt, elType, !state); + // Explicitly specified `textConfig` and `textContent` has higher priority than + // the ones generated by legacy style. Otherwise if users use them and `api.style` + // at the same time, they not both work and hardly to known why. + if (!txCfg && convertResult.textConfig) { + txCfg = convertResult.textConfig; + } + if (!txConOpt && convertResult.textContent) { + txConOpt = convertResult.textContent; + } + } + + if (!state && txConOpt) { + const txConOptNormal = txConOpt as CustomElementOption; + // `textContent: {type: 'text'}`, the "type" is easy to be missing. So we tolerate it. + !txConOptNormal.type && (txConOptNormal.type = 'text'); + if (__DEV__) { + // Do not tolerate incorret type for forward compat. + assert( + txConOptNormal.type === 'text', + 'textContent.type must be "text"' + ); + } + } + + const info = !state ? attachedTxInfo.normal : attachedTxInfo[state]; + info.cfg = txCfg; + info.conOpt = txConOpt; +} + +function retrieveStateOption( + elOption: CustomElementOption, state: DisplayStateNonNormal +): CustomElementOptionOnState { + return !state ? elOption : elOption ? (elOption as CustomDisplayableOption)[state] : null; +} + +function retrieveStyleOptionOnState( + stateOptionNormal: CustomElementOption, + stateOption: CustomElementOptionOnState, + state: DisplayStateNonNormal +): CustomElementOptionOnState['style'] { + let style = stateOption && stateOption.style; + if (style == null && state === EMPHASIS && stateOptionNormal) { + style = (stateOptionNormal as CustomDisplayableOption).styleEmphasis; + } + return style; +} + + +// Usage: +// (1) By default, `elOption.$mergeChildren` is `'byIndex'`, which indicates that +// the existing children will not be removed, and enables the feature that +// update some of the props of some of the children simply by construct +// the returned children of `renderItem` like: +// `var children = group.children = []; children[3] = {opacity: 0.5};` +// (2) If `elOption.$mergeChildren` is `'byName'`, add/update/remove children +// by child.name. But that might be lower performance. +// (3) If `elOption.$mergeChildren` is `false`, the existing children will be +// replaced totally. +// (4) If `!elOption.children`, following the "merge" principle, nothing will happen. +// +// For implementation simpleness, do not provide a direct way to remove sinlge +// child (otherwise the total indicies of the children array have to be modified). +// User can remove a single child by set its `ignore` as `true`. +function mergeChildren( + api: ExtensionAPI, + el: graphicUtil.Group, + dataIndex: number, + elOption: CustomGroupOption, + seriesModel: CustomSeriesModel +): void { + + const newChildren = elOption.children; + const newLen = newChildren ? newChildren.length : 0; + const mergeChildren = elOption.$mergeChildren; + // `diffChildrenByName` has been deprecated. + const byName = mergeChildren === 'byName' || elOption.diffChildrenByName; + const notMerge = mergeChildren === false; + + // For better performance on roam update, only enter if necessary. + if (!newLen && !byName && !notMerge) { + return; + } + + if (byName) { + diffGroupChildren({ + api: api, + oldChildren: el.children() || [], + newChildren: newChildren as CustomElementOption[] || [], + dataIndex: dataIndex, + seriesModel: seriesModel, + group: el + }); + return; + } + + notMerge && el.removeAll(); + + // Mapping children of a group simply by index, which + // might be better performance. + let index = 0; + for (; index < newLen; index++) { + newChildren[index] && doCreateOrUpdateEl( + api, + el.childAt(index), + dataIndex, + newChildren[index] as CustomElementOption, + seriesModel, + el, + false + ); + } + for (let i = el.childCount() - 1; i >= index; i--) { + // Do not supprot leave elements that are not mentioned in the latest + // `renderItem` return. Otherwise users may not have a clear and simple + // concept that how to contorl all of the elements. + doRemoveEl(el.childAt(i), seriesModel, el); + } +} + +type DiffGroupContext = { + api: ExtensionAPI; + oldChildren: Element[]; + newChildren: CustomElementOption[]; + dataIndex: number; + seriesModel: CustomSeriesModel; + group: graphicUtil.Group; +}; +function diffGroupChildren(context: DiffGroupContext) { + (new DataDiffer( + context.oldChildren, + context.newChildren, + getKey, + getKey, + context + )) + .add(processAddUpdate) + .update(processAddUpdate) + .remove(processRemove) + .execute(); +} + +function getKey(item: Element, idx: number): string { + const name = item && item.name; + return name != null ? name : GROUP_DIFF_PREFIX + idx; +} + +function processAddUpdate( + this: DataDiffer, + newIndex: number, + oldIndex?: number +): void { + const context = this.context; + const childOption = newIndex != null ? context.newChildren[newIndex] : null; + const child = oldIndex != null ? context.oldChildren[oldIndex] : null; + + doCreateOrUpdateEl( + context.api, + child, + context.dataIndex, + childOption, + context.seriesModel, + context.group, + false + ); +} + +function processRemove(this: DataDiffer, oldIndex: number): void { + const context = this.context; + const child = context.oldChildren[oldIndex]; + doRemoveEl(child, context.seriesModel, context.group); +} + +function doRemoveEl( + el: Element, + seriesModel: CustomSeriesModel, + group: ViewRootGroup +): void { + if (el) { + const leaveToProps = customInnerStore(el).leaveToProps; + leaveToProps + ? graphicUtil.updateProps(el, leaveToProps, seriesModel, { + cb: function () { + group.remove(el); + } + }) + : group.remove(el); + } +} + +/** + * @return SVG Path data. + */ +function getPathData(shape: CustomSVGPathOption['shape']): string { + // "d" follows the SVG convention. + return shape && (shape.pathData || shape.d); +} + +function hasOwnPathData(shape: CustomSVGPathOption['shape']): boolean { + return shape && (hasOwn(shape, 'pathData') || hasOwn(shape, 'd')); +} diff --git a/src/chart/custom/install.ts b/src/chart/custom/install.ts index 50cbfac5bf..34f2b3be2c 100644 --- a/src/chart/custom/install.ts +++ b/src/chart/custom/install.ts @@ -17,2775 +17,11 @@ * under the License. */ -import { - hasOwn, assert, isString, retrieve2, retrieve3, defaults, each, - keys, isArrayLike, bind, isFunction, eqNaN, indexOf, clone -} from 'zrender/src/core/util'; -import * as graphicUtil from '../../util/graphic'; -import { setDefaultStateProxy, enableHoverEmphasis } from '../../util/states'; -import * as labelStyleHelper from '../../label/labelStyle'; -import {getDefaultLabel} from '../helper/labelHelper'; -import createListFromArray from '../helper/createListFromArray'; -import {getLayoutOnAxis, BarGridLayoutResult, BarGridLayoutOptionForCustomSeries} from '../../layout/barGrid'; -import DataDiffer, { DataDiffMode } from '../../data/DataDiffer'; -import SeriesModel from '../../model/Series'; -import Model from '../../model/Model'; -import ChartView from '../../view/Chart'; -import {createClipPath} from '../helper/createClipPathFromCoordSys'; -import { - EventQueryItem, SeriesOption, SeriesOnCartesianOptionMixin, - SeriesOnPolarOptionMixin, SeriesOnSingleOptionMixin, SeriesOnGeoOptionMixin, - SeriesOnCalendarOptionMixin, ItemStyleOption, SeriesEncodeOptionMixin, - DimensionLoose, - ParsedValue, - Dictionary, - CallbackDataParams, - Payload, - StageHandlerProgressParams, - LabelOption, - ViewRootGroup, - OptionDataValue, - ZRStyleProps, - DisplayState, - ECElement, - DisplayStateNonNormal, - BlurScope, - SeriesDataType, - OrdinalRawValue, - PayloadAnimationPart, - DecalObject, - InnerDecalObject, - TextCommonOption, - ECActionEvent -} from '../../util/types'; -import Element, { ElementProps, ElementTextConfig } from 'zrender/src/Element'; -import prepareCartesian2d from '../../coord/cartesian/prepareCustom'; -import prepareGeo from '../../coord/geo/prepareCustom'; -import prepareSingleAxis from '../../coord/single/prepareCustom'; -import preparePolar from '../../coord/polar/prepareCustom'; -import prepareCalendar from '../../coord/calendar/prepareCustom'; -import List, { DefaultDataVisual } from '../../data/List'; -import GlobalModel from '../../model/Global'; -import { makeInner, normalizeToArray } from '../../util/model'; -import ExtensionAPI from '../../core/ExtensionAPI'; -import Displayable from 'zrender/src/graphic/Displayable'; -import Axis2D from '../../coord/cartesian/Axis2D'; -import { RectLike } from 'zrender/src/core/BoundingRect'; -import { PathProps, PathStyleProps } from 'zrender/src/graphic/Path'; -import { ImageStyleProps } from 'zrender/src/graphic/Image'; -import { CoordinateSystem } from '../../coord/CoordinateSystem'; -import { TextStyleProps } from 'zrender/src/graphic/Text'; -import { - convertToEC4StyleForCustomSerise, - isEC4CompatibleStyle, - convertFromEC4CompatibleStyle, - LegacyStyleProps, - warnDeprecated -} from '../../util/styleCompat'; -import Transformable from 'zrender/src/core/Transformable'; -import { ItemStyleProps } from '../../model/mixin/itemStyle'; -import { cloneValue } from 'zrender/src/animation/Animator'; -import { warn, throwError } from '../../util/log'; -import { - combine, isInAnyMorphing, morphPath, isCombiningPath, CombineSeparateConfig, separate, CombineSeparateResult -} from 'zrender/src/tool/morphPath'; -import { AnimationEasing } from 'zrender/src/animation/easing'; -import * as matrix from 'zrender/src/core/matrix'; -import { PatternObject } from 'zrender/src/graphic/Pattern'; -import { createOrUpdatePatternFromDecal } from '../../util/decal'; -import { ZRenderType } from 'zrender/src/zrender'; import { EChartsExtensionInstallRegisters } from '../../extension'; - - -const inner = makeInner<{ - info: CustomExtraElementInfo; - customPathData: string; - customGraphicType: string; - customImagePath: CustomImageOption['style']['image']; - // customText: string; - txConZ2Set: number; - leaveToProps: ElementProps; - // Can morph: "morph" specified in option and el is Path. - canMorph: boolean; - userDuring: CustomBaseElementOption['during']; -}, Element>(); - -type CustomExtraElementInfo = Dictionary; -const TRANSFORM_PROPS = { - x: 1, - y: 1, - scaleX: 1, - scaleY: 1, - originX: 1, - originY: 1, - rotation: 1 -} as const; -type TransformProp = keyof typeof TRANSFORM_PROPS; -const transformPropNamesStr = keys(TRANSFORM_PROPS).join(', '); - -// Do not declare "Dictionary" in TransitionAnyOption to restrict the type check. -type TransitionAnyOption = { - transition?: TransitionAnyProps; - enterFrom?: Dictionary; - leaveTo?: Dictionary; -}; -type TransitionAnyProps = string | string[]; -type TransitionTransformOption = { - transition?: ElementRootTransitionProp | ElementRootTransitionProp[]; - enterFrom?: Dictionary; - leaveTo?: Dictionary; -}; -type ElementRootTransitionProp = TransformProp | 'shape' | 'extra' | 'style'; -type ShapeMorphingOption = { - /** - * If do shape morphing animation when type is changed. - * Only available on path. - */ - morph?: boolean -}; - -interface CustomBaseElementOption extends Partial>, TransitionTransformOption { - // element type, mandatory. - type: string; - id?: string; - // For animation diff. - name?: string; - info?: CustomExtraElementInfo; - // `false` means remove the textContent. - textContent?: CustomTextOption | false; - // `false` means remove the clipPath - clipPath?: CustomZRPathOption | false; - // `extra` can be set in any el option for custom prop for annimation duration. - extra?: TransitionAnyOption; - // updateDuringAnimation - during?(params: typeof customDuringAPI): void; - - focus?: 'none' | 'self' | 'series' | ArrayLike - blurScope?: BlurScope -}; -interface CustomDisplayableOption extends CustomBaseElementOption, Partial> { - style?: ZRStyleProps & TransitionAnyOption; - // `false` means remove emphasis trigger. - styleEmphasis?: ZRStyleProps | false; - emphasis?: CustomDisplayableOptionOnState; - blur?: CustomDisplayableOptionOnState; - select?: CustomDisplayableOptionOnState; -} -interface CustomDisplayableOptionOnState extends Partial> { - // `false` means remove emphasis trigger. - style?: (ZRStyleProps & TransitionAnyOption) | false; -} -interface CustomGroupOption extends CustomBaseElementOption { - type: 'group'; - width?: number; - height?: number; - // @deprecated - diffChildrenByName?: boolean; - // Can only set focus, blur on the root element. - children: Omit[]; - $mergeChildren: false | 'byName' | 'byIndex'; -} -interface CustomZRPathOption extends CustomDisplayableOption, ShapeMorphingOption { - shape?: PathProps['shape'] & TransitionAnyOption; - style?: CustomDisplayableOption['style'] & { - decal?: DecalObject; - // Only internal usage. Any user specified value will be overwritten. - __decalPattern?: PatternObject; - }; -} -interface CustomSVGPathOption extends CustomDisplayableOption, ShapeMorphingOption { - type: 'path'; - shape?: { - // SVG Path, like 'M0,0 L0,-20 L70,-1 L70,0 Z' - pathData?: string; - // "d" is the alias of `pathData` follows the SVG convention. - d?: string; - layout?: 'center' | 'cover'; - x?: number; - y?: number; - width?: number; - height?: number; - } & TransitionAnyOption; -} -interface CustomImageOption extends CustomDisplayableOption { - type: 'image'; - style?: ImageStyleProps & TransitionAnyOption; - emphasis?: CustomImageOptionOnState; - blur?: CustomImageOptionOnState; - select?: CustomImageOptionOnState; -} -interface CustomImageOptionOnState extends CustomDisplayableOptionOnState { - style?: ImageStyleProps & TransitionAnyOption; -} -interface CustomTextOption extends CustomDisplayableOption { - type: 'text'; -} -type CustomElementOption = CustomZRPathOption | CustomSVGPathOption | CustomImageOption | CustomTextOption; -type CustomElementOptionOnState = CustomDisplayableOptionOnState | CustomImageOptionOnState; - - -export interface CustomSeriesRenderItemAPI extends - CustomSeriesRenderItemCoordinateSystemAPI { - - // Methods from ExtensionAPI. - // NOTE: Not using Pick here because we don't want to bundle ExtensionAPI into the d.ts - getWidth(): number - getHeight(): number - getZr(): ZRenderType - getDevicePixelRatio(): number - - value(dim: DimensionLoose, dataIndexInside?: number): ParsedValue; - ordinalRawValue(dim: DimensionLoose, dataIndexInside?: number): ParsedValue | OrdinalRawValue; - style(userProps?: ZRStyleProps, dataIndexInside?: number): ZRStyleProps; - styleEmphasis(userProps?: ZRStyleProps, dataIndexInside?: number): ZRStyleProps; - visual( - visualType: VT, - dataIndexInside?: number - ): VT extends NonStyleVisualProps ? DefaultDataVisual[VT] - : VT extends StyleVisualProps ? PathStyleProps[typeof STYLE_VISUAL_TYPE[VT]] - : void; - barLayout(opt: BarGridLayoutOptionForCustomSeries): BarGridLayoutResult; - currentSeriesIndices(): number[]; - font(opt: Pick): string; -} -interface CustomSeriesRenderItemParamsCoordSys { - type: string; - // And extra params for each coordinate systems. -} -interface CustomSeriesRenderItemCoordinateSystemAPI { - coord( - data: OptionDataValue | OptionDataValue[], - clamp?: boolean - ): number[]; - size?( - dataSize: OptionDataValue | OptionDataValue[], - dataItem: OptionDataValue | OptionDataValue[] - ): number | number[]; -} -export interface CustomSeriesRenderItemParams { - context: Dictionary; - seriesId: string; - seriesName: string; - seriesIndex: number; - coordSys: CustomSeriesRenderItemParamsCoordSys; - dataInsideLength: number; - encode: WrapEncodeDefRet; -} -type CustomSeriesRenderItem = ( - params: CustomSeriesRenderItemParams, - api: CustomSeriesRenderItemAPI -) => CustomElementOption; - -interface CustomSeriesStateOption { - itemStyle?: ItemStyleOption; - label?: LabelOption; -} - -export interface CustomSeriesOption extends - SeriesOption, // don't support StateOption in custom series. - SeriesEncodeOptionMixin, - SeriesOnCartesianOptionMixin, - SeriesOnPolarOptionMixin, - SeriesOnSingleOptionMixin, - SeriesOnGeoOptionMixin, - SeriesOnCalendarOptionMixin { - - type?: 'custom' - - // If set as 'none', do not depends on coord sys. - coordinateSystem?: string | 'none'; - - renderItem?: CustomSeriesRenderItem; - - // Only works on polar and cartesian2d coordinate system. - clip?: boolean; -} - -interface LegacyCustomSeriesOption extends SeriesOption, CustomSeriesStateOption {} - - -interface LooseElementProps extends ElementProps { - style?: ZRStyleProps; - shape?: Dictionary; -} - -// Also compat with ec4, where -// `visual('color') visual('borderColor')` is supported. -const STYLE_VISUAL_TYPE = { - color: 'fill', - borderColor: 'stroke' -} as const; -type StyleVisualProps = keyof typeof STYLE_VISUAL_TYPE; - -const NON_STYLE_VISUAL_PROPS = { - symbol: 1, - symbolSize: 1, - symbolKeepAspect: 1, - legendIcon: 1, - visualMeta: 1, - liftZ: 1, - decal: 1 -} as const; -type NonStyleVisualProps = keyof typeof NON_STYLE_VISUAL_PROPS; - -const EMPHASIS = 'emphasis' as const; -const NORMAL = 'normal' as const; -const BLUR = 'blur' as const; -const SELECT = 'select' as const; -const STATES = [NORMAL, EMPHASIS, BLUR, SELECT] as const; -const PATH_ITEM_STYLE = { - normal: ['itemStyle'], - emphasis: [EMPHASIS, 'itemStyle'], - blur: [BLUR, 'itemStyle'], - select: [SELECT, 'itemStyle'] -} as const; -const PATH_LABEL = { - normal: ['label'], - emphasis: [EMPHASIS, 'label'], - blur: [BLUR, 'label'], - select: [SELECT, 'label'] -} as const; -// Use prefix to avoid index to be the same as el.name, -// which will cause weird update animation. -const GROUP_DIFF_PREFIX = 'e\0\0'; - -type AttachedTxInfo = { - isLegacy: boolean; - normal: { - cfg: ElementTextConfig; - conOpt: CustomElementOption | false; - }; - emphasis: { - cfg: ElementTextConfig; - conOpt: CustomElementOptionOnState; - }; - blur: { - cfg: ElementTextConfig; - conOpt: CustomElementOptionOnState; - }; - select: { - cfg: ElementTextConfig; - conOpt: CustomElementOptionOnState; - }; -}; -const attachedTxInfoTmp = { - normal: {}, - emphasis: {}, - blur: {}, - select: {} -} as AttachedTxInfo; - -const LEGACY_TRANSFORM_PROPS = { - position: ['x', 'y'], - scale: ['scaleX', 'scaleY'], - origin: ['originX', 'originY'] -} as const; -type LegacyTransformProp = keyof typeof LEGACY_TRANSFORM_PROPS; - -export type PrepareCustomInfo = (coordSys: CoordinateSystem) => { - coordSys: CustomSeriesRenderItemParamsCoordSys; - api: CustomSeriesRenderItemCoordinateSystemAPI -}; - -const tmpTransformable = new Transformable(); - -/** - * To reduce total package size of each coordinate systems, the modules `prepareCustom` - * of each coordinate systems are not required by each coordinate systems directly, but - * required by the module `custom`. - * - * prepareInfoForCustomSeries {Function}: optional - * @return {Object} {coordSys: {...}, api: { - * coord: function (data, clamp) {}, // return point in global. - * size: function (dataSize, dataItem) {} // return size of each axis in coordSys. - * }} - */ -const prepareCustoms: Dictionary = { - cartesian2d: prepareCartesian2d, - geo: prepareGeo, - singleAxis: prepareSingleAxis, - polar: preparePolar, - calendar: prepareCalendar -}; - -class CustomSeriesModel extends SeriesModel { - - static type = 'series.custom'; - readonly type = CustomSeriesModel.type; - - static dependencies = ['grid', 'polar', 'geo', 'singleAxis', 'calendar']; - - // preventAutoZ = true; - - currentZLevel: number; - currentZ: number; - - static defaultOption: CustomSeriesOption = { - coordinateSystem: 'cartesian2d', // Can be set as 'none' - zlevel: 0, - z: 2, - legendHoverLink: true, - - // Custom series will not clip by default. - // Some case will use custom series to draw label - // For example https://echarts.apache.org/examples/en/editor.html?c=custom-gantt-flight - clip: false - - // Cartesian coordinate system - // xAxisIndex: 0, - // yAxisIndex: 0, - - // Polar coordinate system - // polarIndex: 0, - - // Geo coordinate system - // geoIndex: 0, - }; - - optionUpdated() { - this.currentZLevel = this.get('zlevel', true); - this.currentZ = this.get('z', true); - } - - getInitialData(option: CustomSeriesOption, ecModel: GlobalModel): List { - return createListFromArray(this.getSource(), this); - } - - getDataParams(dataIndex: number, dataType?: SeriesDataType, el?: Element): CallbackDataParams & { - info: CustomExtraElementInfo - } { - const params = super.getDataParams(dataIndex, dataType) as ReturnType; - el && (params.info = inner(el).info); - return params; - } -} - - - -class CustomSeriesView extends ChartView { - - static type = 'custom'; - readonly type = CustomSeriesView.type; - - private _data: List; - - - render( - customSeries: CustomSeriesModel, - ecModel: GlobalModel, - api: ExtensionAPI, - payload: Payload - ): void { - const oldData = this._data; - const data = customSeries.getData(); - const group = this.group; - const renderItem = makeRenderItem(customSeries, data, ecModel, api); - - if (!oldData) { - // Previous render is incremental render or first render. - // Needs remove the incremental rendered elements. - group.removeAll(); - } - - // By default, merge mode is applied. In most cases, custom series is - // used in the scenario that data amount is not large but graphic elements - // is complicated, where merge mode is probably necessary for optimization. - // For example, reuse graphic elements and only update the transform when - // roam or data zoom according to `actionType`. - - const transOpt = customSeries.__transientTransitionOpt; - - // Enable user to disable transition animation by both set - // `from` and `to` dimension as `null`/`undefined`. - if (transOpt && (transOpt.from == null || transOpt.to == null)) { - oldData && oldData.each(function (oldIdx) { - doRemoveEl(oldData.getItemGraphicEl(oldIdx), customSeries, group); - }); - data.each(function (newIdx) { - createOrUpdateItem( - api, null, newIdx, renderItem(newIdx, payload), customSeries, group, data, null - ); - }); - } - else { - const morphPreparation = new MorphPreparation(customSeries, transOpt); - const diffMode: DataDiffMode = transOpt ? 'multiple' : 'oneToOne'; - - (new DataDiffer( - oldData ? oldData.getIndices() : [], - data.getIndices(), - createGetKey(oldData, diffMode, transOpt && transOpt.from), - createGetKey(data, diffMode, transOpt && transOpt.to), - null, - diffMode - )) - .add(function (newIdx) { - createOrUpdateItem( - api, null, newIdx, renderItem(newIdx, payload), customSeries, group, - data, null - ); - }) - .remove(function (oldIdx) { - doRemoveEl(oldData.getItemGraphicEl(oldIdx), customSeries, group); - }) - .update(function (newIdx, oldIdx) { - morphPreparation.reset('oneToOne'); - let oldEl = oldData.getItemGraphicEl(oldIdx); - morphPreparation.findAndAddFrom(oldEl); - - // PENDING: - // if may morph, currently we alway recreate the whole el. - // because if reuse some of the el in the group tree, the old el has to - // be removed from the group, and consequently we can not calculate - // the "global transition" of the old element. - // But is there performance issue? - if (morphPreparation.hasFrom()) { - removeElementDirectly(oldEl, group); - oldEl = null; - } - createOrUpdateItem( - api, oldEl, newIdx, renderItem(newIdx, payload), customSeries, group, - data, morphPreparation - ); - morphPreparation.applyMorphing(); - }) - .updateManyToOne(function (newIdx, oldIndices) { - morphPreparation.reset('manyToOne'); - for (let i = 0; i < oldIndices.length; i++) { - const oldEl = oldData.getItemGraphicEl(oldIndices[i]); - morphPreparation.findAndAddFrom(oldEl); - removeElementDirectly(oldEl, group); - } - createOrUpdateItem( - api, null, newIdx, renderItem(newIdx, payload), customSeries, group, - data, morphPreparation - ); - morphPreparation.applyMorphing(); - }) - .updateOneToMany(function (newIndices, oldIdx) { - morphPreparation.reset('oneToMany'); - const newLen = newIndices.length; - const oldEl = oldData.getItemGraphicEl(oldIdx); - morphPreparation.findAndAddFrom(oldEl); - removeElementDirectly(oldEl, group); - - for (let i = 0; i < newLen; i++) { - createOrUpdateItem( - api, null, newIndices[i], renderItem(newIndices[i], payload), customSeries, group, - data, morphPreparation - ); - } - morphPreparation.applyMorphing(); - }) - .execute(); - } - - // Do clipping - const clipPath = customSeries.get('clip', true) - ? createClipPath(customSeries.coordinateSystem, false, customSeries) - : null; - if (clipPath) { - group.setClipPath(clipPath); - } - else { - group.removeClipPath(); - } - - this._data = data; - } - - incrementalPrepareRender( - customSeries: CustomSeriesModel, - ecModel: GlobalModel, - api: ExtensionAPI - ): void { - this.group.removeAll(); - this._data = null; - } - - incrementalRender( - params: StageHandlerProgressParams, - customSeries: CustomSeriesModel, - ecModel: GlobalModel, - api: ExtensionAPI, - payload: Payload - ): void { - const data = customSeries.getData(); - const renderItem = makeRenderItem(customSeries, data, ecModel, api); - function setIncrementalAndHoverLayer(el: Displayable) { - if (!el.isGroup) { - el.incremental = true; - el.ensureState('emphasis').hoverLayer = true; - } - } - for (let idx = params.start; idx < params.end; idx++) { - const el = createOrUpdateItem( - null, null, idx, renderItem(idx, payload), customSeries, this.group, data, null - ); - el && el.traverse(setIncrementalAndHoverLayer); - } - } - - filterForExposedEvent( - eventType: string, query: EventQueryItem, targetEl: Element, packedEvent: ECActionEvent - ): boolean { - const elementName = query.element; - if (elementName == null || targetEl.name === elementName) { - return true; - } - - // Enable to give a name on a group made by `renderItem`, and listen - // events that triggerd by its descendents. - while ((targetEl = (targetEl.__hostTarget || targetEl.parent)) && targetEl !== this.group) { - if (targetEl.name === elementName) { - return true; - } - } - - return false; - } -} - - -function createGetKey( - data: List, - diffMode: DataDiffMode, - dimension: DimensionLoose -) { - if (!data) { - return; - } - - if (diffMode === 'oneToOne') { - return function (rawIdx: number, dataIndex: number) { - return data.getId(dataIndex); - }; - } - - const diffByDimName = data.getDimension(dimension); - const dimInfo = data.getDimensionInfo(diffByDimName); - - if (!dimInfo) { - let errMsg = ''; - if (__DEV__) { - errMsg = `${dimension} is not a valid dimension.`; - } - throwError(errMsg); - } - const ordinalMeta = dimInfo.ordinalMeta; - return function (rawIdx: number, dataIndex: number) { - let key = data.get(diffByDimName, dataIndex); - if (ordinalMeta) { - key = ordinalMeta.categories[key as number]; - } - return (key == null || eqNaN(key)) - ? rawIdx + '' - : '_ec_' + key; - }; -} - - -function createEl(elOption: CustomElementOption): Element { - const graphicType = elOption.type; - let el; - - // Those graphic elements are not shapes. They should not be - // overwritten by users, so do them first. - if (graphicType === 'path') { - const shape = (elOption as CustomSVGPathOption).shape; - // Using pathRect brings convenience to users sacle svg path. - const pathRect = (shape.width != null && shape.height != null) - ? { - x: shape.x || 0, - y: shape.y || 0, - width: shape.width, - height: shape.height - } as RectLike - : null; - const pathData = getPathData(shape); - // Path is also used for icon, so layout 'center' by default. - el = graphicUtil.makePath(pathData, null, pathRect, shape.layout || 'center'); - inner(el).customPathData = pathData; - } - else if (graphicType === 'image') { - el = new graphicUtil.Image({}); - inner(el).customImagePath = (elOption as CustomImageOption).style.image; - } - else if (graphicType === 'text') { - el = new graphicUtil.Text({}); - // inner(el).customText = (elOption.style as TextStyleProps).text; - } - else if (graphicType === 'group') { - el = new graphicUtil.Group(); - } - else if (graphicType === 'compoundPath') { - throw new Error('"compoundPath" is not supported yet.'); - } - else { - const Clz = graphicUtil.getShapeClass(graphicType); - if (!Clz) { - let errMsg = ''; - if (__DEV__) { - errMsg = 'graphic type "' + graphicType + '" can not be found.'; - } - throwError(errMsg); - } - el = new Clz(); - } - - inner(el).customGraphicType = graphicType; - el.name = elOption.name; - - // Compat ec4: the default z2 lift is 1. If changing the number, - // some cases probably be broken: hierarchy layout along z, like circle packing, - // where emphasis only intending to modify color/border rather than lift z2. - (el as ECElement).z2EmphasisLift = 1; - (el as ECElement).z2SelectLift = 1; - - return el; -} - -/** - * ---------------------------------------------------------- - * [STRATEGY_MERGE] Merge properties or erase all properties: - * - * Based on the fact that the existing zr element probably be reused, we now consider whether - * merge or erase all properties to the exsiting elements. - * That is, if a certain props is not specified in the lastest return of `renderItem`: - * + "Merge" means that do not modify the value on the existing element. - * + "Erase all" means that use a default value to the existing element. - * - * "Merge" might bring some unexpected state retaining for users and "erase all" seams to be - * more safe. "erase all" force users to specify all of the props each time, which is recommanded - * in most cases. - * But "erase all" theoretically disables the chance of performance optimization (e.g., just - * generete shape and style at the first time rather than always do that). - * So we still use "merge" rather than "erase all". If users need "erase all", they can - * simple always set all of the props each time. - * Some "object-like" config like `textConfig`, `textContent`, `style` which are not needed for - * every elment, so we replace them only when user specify them. And the that is a total replace. - * - * TODO: there is no hint of 'isFirst' to users. So the performance enhancement can not be - * performed yet. Consider the case: - * (1) setOption to "mergeChildren" with a smaller children count - * (2) Use dataZoom to make an item disappear. - * (3) User dataZoom to make the item display again. At that time, renderItem need to return the - * full option rather than partial option to recreate the element. - * - * ---------------------------------------------- - * [STRATEGY_NULL] `hasOwnProperty` or `== null`: - * - * Ditinguishing "own property" probably bring little trouble to user when make el options. - * So we trade a {xx: null} or {xx: undefined} as "not specified" if possible rather than - * "set them to null/undefined". In most cases, props can not be cleared. Some typicall - * clearable props like `style`/`textConfig`/`textContent` we enable `false` to means - * "clear". In some othere special cases that the prop is able to set as null/undefined, - * but not suitable to use `false`, `hasOwnProperty` is checked. - * - * --------------------------------------------- - * [STRATEGY_TRANSITION] The rule of transition: - * + For props on the root level of a element: - * If there is no `transition` specified, tansform props will be transitioned by default, - * which is the same as the previous setting in echarts4 and suitable for the scenario - * of dataZoom change. - * If `transition` specified, only the specified props will be transitioned. - * + For props in `shape` and `style`: - * Only props specified in `transition` will be transitioned. - * + Break: - * Since ec5, do not make transition to shape by default, because it might result in - * performance issue (especially `points` of polygon) and do not necessary in most cases. - * - * @return if `isMorphTo`, return `allPropsFinal`. - */ -function updateElNormal( - // Can be null/undefined - api: ExtensionAPI, - el: Element, - // Whether be a morph target. - isMorphTo: boolean, - dataIndex: number, - elOption: CustomElementOption, - styleOpt: CustomElementOption['style'], - attachedTxInfo: AttachedTxInfo, - seriesModel: CustomSeriesModel, - isInit: boolean, - isTextContent: boolean -): ElementProps { - const transFromProps = {} as ElementProps; - const allPropsFinal = {} as ElementProps; - const elDisplayable = el.isGroup ? null : el as Displayable; - - // If be "morph to", delay the `updateElNormal` when all of the els in - // this data item processed. Because at that time we can get all of the - // "morph from" and make correct separate/combine. - - !isMorphTo && prepareShapeOrExtraTransitionFrom('shape', el, null, elOption, transFromProps, isInit); - prepareShapeOrExtraAllPropsFinal('shape', elOption, allPropsFinal); - !isMorphTo && prepareShapeOrExtraTransitionFrom('extra', el, null, elOption, transFromProps, isInit); - prepareShapeOrExtraAllPropsFinal('extra', elOption, allPropsFinal); - !isMorphTo && prepareTransformTransitionFrom(el, null, elOption, transFromProps, isInit); - prepareTransformAllPropsFinal(elOption, allPropsFinal); - - const txCfgOpt = attachedTxInfo && attachedTxInfo.normal.cfg; - if (txCfgOpt) { - // PENDING: whether use user object directly rather than clone? - // TODO:5.0 textConfig transition animation? - el.setTextConfig(txCfgOpt); - } - - if (el.type === 'text' && styleOpt) { - const textOptionStyle = styleOpt as TextStyleProps; - // Compatible with ec4: if `textFill` or `textStroke` exists use them. - hasOwn(textOptionStyle, 'textFill') && ( - textOptionStyle.fill = (textOptionStyle as any).textFill - ); - hasOwn(textOptionStyle, 'textStroke') && ( - textOptionStyle.stroke = (textOptionStyle as any).textStroke - ); - } - - if (styleOpt) { - let decalPattern; - const decalObj = isPath(el) ? (styleOpt as CustomZRPathOption['style']).decal : null; - if (api && decalObj) { - (decalObj as InnerDecalObject).dirty = true; - decalPattern = createOrUpdatePatternFromDecal(decalObj, api); - } - // Always overwrite in case user specify this prop. - (styleOpt as CustomZRPathOption['style']).__decalPattern = decalPattern; - } - - !isMorphTo && prepareStyleTransitionFrom(el, null, elOption, styleOpt, transFromProps, isInit); - - if (elDisplayable) { - hasOwn(elOption, 'invisible') && (elDisplayable.invisible = elOption.invisible); - } - - // If `isMorphTo`, we should not update these props to el directly, otherwise, - // when applying morph finally, the original prop are missing for making "animation from". - if (!isMorphTo) { - applyPropsFinal(el, allPropsFinal, styleOpt); - applyTransitionFrom(el, dataIndex, elOption, seriesModel, transFromProps, isInit); - } - - // Merge by default. - hasOwn(elOption, 'silent') && (el.silent = elOption.silent); - hasOwn(elOption, 'ignore') && (el.ignore = elOption.ignore); - - if (!isTextContent) { - // `elOption.info` enables user to mount some info on - // elements and use them in event handlers. - // Update them only when user specified, otherwise, remain. - hasOwn(elOption, 'info') && (inner(el).info = elOption.info); - } - - styleOpt ? el.dirty() : el.markRedraw(); - - return isMorphTo ? allPropsFinal : null; -} - -function applyPropsFinal( - el: Element, - // Can be null/undefined - allPropsFinal: ElementProps, - styleOpt: CustomElementOption['style'] -) { - const elDisplayable = el.isGroup ? null : el as Displayable; - - if (elDisplayable && styleOpt) { - - const decalPattern = (styleOpt as CustomZRPathOption['style']).__decalPattern; - let originalDecalObj; - if (decalPattern) { - originalDecalObj = (styleOpt as CustomZRPathOption['style']).decal; - (styleOpt as any).decal = decalPattern; - } - - // PENDING: here the input style object is used directly. - // Good for performance but bad for compatibility control. - elDisplayable.useStyle(styleOpt); - - if (decalPattern) { - (styleOpt as CustomZRPathOption['style']).decal = originalDecalObj; - } - - // When style object changed, how to trade the existing animation? - // It is probably conplicated and not needed to cover all the cases. - // But still need consider the case: - // (1) When using init animation on `style.opacity`, and before the animation - // ended users triggers an update by mousewhell. At that time the init - // animation should better be continued rather than terminated. - // So after `useStyle` called, we should change the animation target manually - // to continue the effect of the init animation. - // (2) PENDING: If the previous animation targeted at a `val1`, and currently we need - // to update the value to `val2` and no animation declared, should be terminate - // the previous animation or just modify the target of the animation? - // Therotically That will happen not only on `style` but also on `shape` and - // `transfrom` props. But we haven't handle this case at present yet. - // (3) PENDING: Is it proper to visit `animators` and `targetName`? - const animators = elDisplayable.animators; - for (let i = 0; i < animators.length; i++) { - const animator = animators[i]; - // targetName is the "topKey". - if (animator.targetName === 'style') { - animator.changeTarget(elDisplayable.style); - } - } - } - - // Set el to the final state firstly. - allPropsFinal && el.attr(allPropsFinal); -} - -function applyTransitionFrom( - el: Element, - dataIndex: number, - elOption: CustomElementOption, - seriesModel: CustomSeriesModel, - // Can be null/undefined - transFromProps: ElementProps, - isInit: boolean -): void { - if (transFromProps) { - // Do not use `el.updateDuringAnimation` here becuase `el.updateDuringAnimation` will - // be called mutiple time in each animation frame. For example, if both "transform" props - // and shape props and style props changed, it will generate three animator and called - // one-by-one in each animation frame. - // We use the during in `animateTo/From` params. - const userDuring = elOption.during; - // For simplicity, if during not specified, the previous during will not work any more. - inner(el).userDuring = userDuring; - const cfgDuringCall = userDuring ? bind(duringCall, { el: el, userDuring: userDuring }) : null; - const cfg = { - dataIndex: dataIndex, - isFrom: true, - during: cfgDuringCall - }; - isInit - ? graphicUtil.initProps(el, transFromProps, seriesModel, cfg) - : graphicUtil.updateProps(el, transFromProps, seriesModel, cfg); - } -} - - -// See [STRATEGY_TRANSITION] -function prepareShapeOrExtraTransitionFrom( - mainAttr: 'shape' | 'extra', - el: Element, - morphFromEl: graphicUtil.Path, - elOption: CustomElementOption, - transFromProps: LooseElementProps, - isInit: boolean -): void { - - const attrOpt: Dictionary & TransitionAnyOption = (elOption as any)[mainAttr]; - if (!attrOpt) { - return; - } - - const elPropsInAttr = (el as LooseElementProps)[mainAttr]; - let transFromPropsInAttr: Dictionary; - - const enterFrom = attrOpt.enterFrom; - if (isInit && enterFrom) { - !transFromPropsInAttr && (transFromPropsInAttr = transFromProps[mainAttr] = {}); - const enterFromKeys = keys(enterFrom); - for (let i = 0; i < enterFromKeys.length; i++) { - // `enterFrom` props are not necessarily also declared in `shape`/`style`/..., - // for example, `opacity` can only declared in `enterFrom` but not in `style`. - const key = enterFromKeys[i]; - // Do not clone, animator will perform that clone. - transFromPropsInAttr[key] = enterFrom[key]; - } - } - - if (!isInit - && elPropsInAttr - // Just ignore shape animation in morphing. - && !(morphFromEl != null && mainAttr === 'shape') - ) { - if (attrOpt.transition) { - !transFromPropsInAttr && (transFromPropsInAttr = transFromProps[mainAttr] = {}); - const transitionKeys = normalizeToArray(attrOpt.transition); - for (let i = 0; i < transitionKeys.length; i++) { - const key = transitionKeys[i]; - const elVal = elPropsInAttr[key]; - if (__DEV__) { - checkNonStyleTansitionRefer(key, (attrOpt as any)[key], elVal); - } - // Do not clone, see `checkNonStyleTansitionRefer`. - transFromPropsInAttr[key] = elVal; - } - } - else if (indexOf(elOption.transition, mainAttr) >= 0) { - !transFromPropsInAttr && (transFromPropsInAttr = transFromProps[mainAttr] = {}); - const elPropsInAttrKeys = keys(elPropsInAttr); - for (let i = 0; i < elPropsInAttrKeys.length; i++) { - const key = elPropsInAttrKeys[i]; - const elVal = elPropsInAttr[key]; - if (isNonStyleTransitionEnabled((attrOpt as any)[key], elVal)) { - transFromPropsInAttr[key] = elVal; - } - } - } - } - - const leaveTo = attrOpt.leaveTo; - if (leaveTo) { - const leaveToProps = getOrCreateLeaveToPropsFromEl(el); - const leaveToPropsInAttr: Dictionary = leaveToProps[mainAttr] || (leaveToProps[mainAttr] = {}); - const leaveToKeys = keys(leaveTo); - for (let i = 0; i < leaveToKeys.length; i++) { - const key = leaveToKeys[i]; - leaveToPropsInAttr[key] = leaveTo[key]; - } - } -} - -function prepareShapeOrExtraAllPropsFinal( - mainAttr: 'shape' | 'extra', - elOption: CustomElementOption, - allProps: LooseElementProps -): void { - const attrOpt: Dictionary & TransitionAnyOption = (elOption as any)[mainAttr]; - if (!attrOpt) { - return; - } - const allPropsInAttr = allProps[mainAttr] = {} as Dictionary; - const keysInAttr = keys(attrOpt); - for (let i = 0; i < keysInAttr.length; i++) { - const key = keysInAttr[i]; - // To avoid share one object with different element, and - // to avoid user modify the object inexpectedly, have to clone. - allPropsInAttr[key] = cloneValue((attrOpt as any)[key]); - } -} - -// See [STRATEGY_TRANSITION]. -function prepareTransformTransitionFrom( - el: Element, - morphFromEl: graphicUtil.Path, - elOption: CustomElementOption, - transFromProps: ElementProps, - isInit: boolean -): void { - const enterFrom = elOption.enterFrom; - if (isInit && enterFrom) { - const enterFromKeys = keys(enterFrom); - for (let i = 0; i < enterFromKeys.length; i++) { - const key = enterFromKeys[i] as TransformProp; - if (__DEV__) { - checkTransformPropRefer(key, 'el.enterFrom'); - } - // Do not clone, animator will perform that clone. - transFromProps[key] = enterFrom[key] as number; - } - } - - if (!isInit) { - // If morphing, force transition all transform props. - // otherwise might have incorrect morphing animation. - if (morphFromEl) { - const fromTransformable = calcOldElLocalTransformBasedOnNewElParent(morphFromEl, el); - setTransformPropToTransitionFrom(transFromProps, 'x', fromTransformable); - setTransformPropToTransitionFrom(transFromProps, 'y', fromTransformable); - setTransformPropToTransitionFrom(transFromProps, 'scaleX', fromTransformable); - setTransformPropToTransitionFrom(transFromProps, 'scaleY', fromTransformable); - setTransformPropToTransitionFrom(transFromProps, 'originX', fromTransformable); - setTransformPropToTransitionFrom(transFromProps, 'originY', fromTransformable); - setTransformPropToTransitionFrom(transFromProps, 'rotation', fromTransformable); - } - else if (elOption.transition) { - const transitionKeys = normalizeToArray(elOption.transition); - for (let i = 0; i < transitionKeys.length; i++) { - const key = transitionKeys[i]; - if (key === 'style' || key === 'shape' || key === 'extra') { - continue; - } - const elVal = el[key]; - if (__DEV__) { - checkTransformPropRefer(key, 'el.transition'); - checkNonStyleTansitionRefer(key, elOption[key], elVal); - } - // Do not clone, see `checkNonStyleTansitionRefer`. - transFromProps[key] = elVal; - } - } - // This default transition see [STRATEGY_TRANSITION] - else { - setTransformPropToTransitionFrom(transFromProps, 'x', el); - setTransformPropToTransitionFrom(transFromProps, 'y', el); - } - } - - const leaveTo = elOption.leaveTo; - if (leaveTo) { - const leaveToProps = getOrCreateLeaveToPropsFromEl(el); - const leaveToKeys = keys(leaveTo); - for (let i = 0; i < leaveToKeys.length; i++) { - const key = leaveToKeys[i] as TransformProp; - if (__DEV__) { - checkTransformPropRefer(key, 'el.leaveTo'); - } - leaveToProps[key] = leaveTo[key] as number; - } - } -} - -function prepareTransformAllPropsFinal( - elOption: CustomElementOption, - allProps: ElementProps -): void { - setLagecyTransformProp(elOption, allProps, 'position'); - setLagecyTransformProp(elOption, allProps, 'scale'); - setLagecyTransformProp(elOption, allProps, 'origin'); - setTransformProp(elOption, allProps, 'x'); - setTransformProp(elOption, allProps, 'y'); - setTransformProp(elOption, allProps, 'scaleX'); - setTransformProp(elOption, allProps, 'scaleY'); - setTransformProp(elOption, allProps, 'originX'); - setTransformProp(elOption, allProps, 'originY'); - setTransformProp(elOption, allProps, 'rotation'); -} - -// See [STRATEGY_TRANSITION]. -function prepareStyleTransitionFrom( - el: Element, - morphFromEl: graphicUtil.Path, - elOption: CustomElementOption, - styleOpt: CustomElementOption['style'], - transFromProps: LooseElementProps, - isInit: boolean -): void { - if (!styleOpt) { - return; - } - - // At present in "many-to-one"/"one-to-many" case, to not support "many" have - // different styles and make style transitions. That might be a rare case. - const fromEl = morphFromEl || el; - - const fromElStyle = (fromEl as LooseElementProps).style as LooseElementProps['style']; - let transFromStyleProps: LooseElementProps['style']; - - const enterFrom = styleOpt.enterFrom; - if (isInit && enterFrom) { - const enterFromKeys = keys(enterFrom); - !transFromStyleProps && (transFromStyleProps = transFromProps.style = {}); - for (let i = 0; i < enterFromKeys.length; i++) { - const key = enterFromKeys[i]; - // Do not clone, animator will perform that clone. - (transFromStyleProps as any)[key] = enterFrom[key]; - } - } - - if (!isInit && fromElStyle) { - if (styleOpt.transition) { - const transitionKeys = normalizeToArray(styleOpt.transition); - !transFromStyleProps && (transFromStyleProps = transFromProps.style = {}); - for (let i = 0; i < transitionKeys.length; i++) { - const key = transitionKeys[i]; - const elVal = (fromElStyle as any)[key]; - // Do not clone, see `checkNonStyleTansitionRefer`. - (transFromStyleProps as any)[key] = elVal; - } - } - else if ( - (el as Displayable).getAnimationStyleProps - && indexOf(elOption.transition, 'style') >= 0 - ) { - const animationProps = (el as Displayable).getAnimationStyleProps(); - const animationStyleProps = animationProps ? animationProps.style : null; - if (animationStyleProps) { - !transFromStyleProps && (transFromStyleProps = transFromProps.style = {}); - const styleKeys = keys(styleOpt); - for (let i = 0; i < styleKeys.length; i++) { - const key = styleKeys[i]; - if ((animationStyleProps as Dictionary)[key]) { - const elVal = (fromElStyle as any)[key]; - (transFromStyleProps as any)[key] = elVal; - } - } - } - } - } - - const leaveTo = styleOpt.leaveTo; - if (leaveTo) { - const leaveToKeys = keys(leaveTo); - const leaveToProps = getOrCreateLeaveToPropsFromEl(el); - const leaveToStyleProps = leaveToProps.style || (leaveToProps.style = {}); - for (let i = 0; i < leaveToKeys.length; i++) { - const key = leaveToKeys[i]; - (leaveToStyleProps as any)[key] = leaveTo[key]; - } - } -} - -/** - * If make "transform"(x/y/scaleX/scaleY/orient/originX/originY) transition between - * two path elements that have different hierarchy, before we retrieve the "from" props, - * we have to calculate the local transition of the "oldPath" based on the parent of - * the "newPath". - * At present, the case only happend in "morphing". Without morphing, the transform - * transition are all between elements in the same hierarchy, where this kind of process - * is not needed. - * - * [CAVEAT]: - * This method makes sense only if: (very tricky) - * (1) "newEl" has been added to its final parent. - * (2) Local transform props of "newPath.parent" are not at their final value but already - * have been at the "from value". - * This is currently ensured by: - * (2.1) "graphicUtil.animationFrom", which will set the element to the "from value" - * immediately. - * (2.2) "morph" option is not allowed to be set on Group, so all of the groups have - * been finished their "updateElNormal" when calling this method in morphing process. - */ -function calcOldElLocalTransformBasedOnNewElParent(oldEl: Element, newEl: Element): Transformable { - if (!oldEl || oldEl === newEl || oldEl.parent === newEl.parent) { - return oldEl; - } - - // Not sure oldEl is rendered (may have "lazyUpdate"), - // so always call `getComputedTransform`. - const tmpM = tmpTransformable.transform - || (tmpTransformable.transform = matrix.identity([])); - - const oldGlobalTransform = oldEl.getComputedTransform(); - oldGlobalTransform - ? matrix.copy(tmpM, oldGlobalTransform) - : matrix.identity(tmpM); - - const newParent = newEl.parent; - if (newParent) { - newParent.getComputedTransform(); - } - - tmpTransformable.originX = oldEl.originX; - tmpTransformable.originY = oldEl.originY; - tmpTransformable.parent = newParent; - tmpTransformable.decomposeTransform(); - - return tmpTransformable; -} - -let checkNonStyleTansitionRefer: (propName: string, optVal: unknown, elVal: unknown) => void; -if (__DEV__) { - checkNonStyleTansitionRefer = function (propName: string, optVal: unknown, elVal: unknown): void { - if (!isArrayLike(optVal)) { - assert( - optVal != null && isFinite(optVal as number), - 'Prop `' + propName + '` must refer to a finite number or ArrayLike for transition.' - ); - } - else { - // Try not to copy array for performance, but if user use the same object in different - // call of `renderItem`, it will casue animation transition fail. - assert( - optVal !== elVal, - 'Prop `' + propName + '` must use different Array object each time for transition.' - ); - } - }; -} - -function isNonStyleTransitionEnabled(optVal: unknown, elVal: unknown): boolean { - // The same as `checkNonStyleTansitionRefer`. - return !isArrayLike(optVal) - ? (optVal != null && isFinite(optVal as number)) - : optVal !== elVal; -} - -let checkTransformPropRefer: (key: string, usedIn: string) => void; -if (__DEV__) { - checkTransformPropRefer = function (key: string, usedIn: string): void { - assert( - hasOwn(TRANSFORM_PROPS, key), - 'Prop `' + key + '` is not a permitted in `' + usedIn + '`. ' - + 'Only `' + keys(TRANSFORM_PROPS).join('`, `') + '` are permitted.' - ); - }; -} - -function getOrCreateLeaveToPropsFromEl(el: Element): LooseElementProps { - const innerEl = inner(el); - return innerEl.leaveToProps || (innerEl.leaveToProps = {}); -} - -// Use it to avoid it be exposed to user. -const tmpDuringScope = {} as { - el: Element; - isShapeDirty: boolean; - isStyleDirty: boolean; -}; -const customDuringAPI = { - // Usually other props do not need to be changed in animation during. - setTransform(key: TransformProp, val: unknown) { - if (__DEV__) { - assert(hasOwn(TRANSFORM_PROPS, key), 'Only ' + transformPropNamesStr + ' available in `setTransform`.'); - } - tmpDuringScope.el[key] = val as number; - return this; - }, - getTransform(key: TransformProp): unknown { - if (__DEV__) { - assert(hasOwn(TRANSFORM_PROPS, key), 'Only ' + transformPropNamesStr + ' available in `getTransform`.'); - } - return tmpDuringScope.el[key]; - }, - setShape(key: string, val: unknown) { - if (__DEV__) { - assertNotReserved(key); - } - const shape = (tmpDuringScope.el as graphicUtil.Path).shape - || ((tmpDuringScope.el as graphicUtil.Path).shape = {}); - shape[key] = val; - tmpDuringScope.isShapeDirty = true; - return this; - }, - getShape(key: string): unknown { - if (__DEV__) { - assertNotReserved(key); - } - const shape = (tmpDuringScope.el as graphicUtil.Path).shape; - if (shape) { - return shape[key]; - } - }, - setStyle(key: string, val: unknown) { - if (__DEV__) { - assertNotReserved(key); - } - const style = (tmpDuringScope.el as Displayable).style; - if (style) { - if (__DEV__) { - if (eqNaN(val)) { - warn('style.' + key + ' must not be assigned with NaN.'); - } - } - style[key] = val; - tmpDuringScope.isStyleDirty = true; - } - return this; - }, - getStyle(key: string): unknown { - if (__DEV__) { - assertNotReserved(key); - } - const style = (tmpDuringScope.el as Displayable).style; - if (style) { - return style[key]; - } - }, - setExtra(key: string, val: unknown) { - if (__DEV__) { - assertNotReserved(key); - } - const extra = (tmpDuringScope.el as LooseElementProps).extra - || ((tmpDuringScope.el as LooseElementProps).extra = {}); - extra[key] = val; - return this; - }, - getExtra(key: string): unknown { - if (__DEV__) { - assertNotReserved(key); - } - const extra = (tmpDuringScope.el as LooseElementProps).extra; - if (extra) { - return extra[key]; - } - } -}; - -function assertNotReserved(key: string) { - if (__DEV__) { - if (key === 'transition' || key === 'enterFrom' || key === 'leaveTo') { - throw new Error('key must not be "' + key + '"'); - } - } -} - -function duringCall( - this: { - el: Element; - userDuring: CustomBaseElementOption['during'] - } -): void { - // Do not provide "percent" until some requirements come. - // Because consider thies case: - // enterFrom: {x: 100, y: 30}, transition: 'x'. - // And enter duration is different from update duration. - // Thus it might be confused about the meaning of "percent" in during callback. - const scope = this; - const el = scope.el; - if (!el) { - return; - } - // If el is remove from zr by reason like legend, during still need to called, - // becuase el will be added back to zr and the prop value should not be incorrect. - - const newstUserDuring = inner(el).userDuring; - const scopeUserDuring = scope.userDuring; - // Ensured a during is only called once in each animation frame. - // If a during is called multiple times in one frame, maybe some users' calulation logic - // might be wrong (not sure whether this usage exists). - // The case of a during might be called twice can be: by default there is a animator for - // 'x', 'y' when init. Before the init animation finished, call `setOption` to start - // another animators for 'style'/'shape'/'extra'. - if (newstUserDuring !== scopeUserDuring) { - // release - scope.el = scope.userDuring = null; - return; - } - - tmpDuringScope.el = el; - tmpDuringScope.isShapeDirty = false; - tmpDuringScope.isStyleDirty = false; - - // Give no `this` to user in "during" calling. - scopeUserDuring(customDuringAPI); - - if (tmpDuringScope.isShapeDirty && (el as graphicUtil.Path).dirtyShape) { - (el as graphicUtil.Path).dirtyShape(); - } - if (tmpDuringScope.isStyleDirty && (el as Displayable).dirtyStyle) { - (el as Displayable).dirtyStyle(); - } - // markRedraw() will be called by default in during. - // FIXME `this.markRedraw();` directly ? - - // FIXME: if in future meet the case that some prop will be both modified in `during` and `state`, - // consider the issue that the prop might be incorrect when return to "normal" state. -} - -function updateElOnState( - state: DisplayStateNonNormal, - el: Element, - elStateOpt: CustomElementOptionOnState, - styleOpt: CustomElementOptionOnState['style'], - attachedTxInfo: AttachedTxInfo, - isRoot: boolean, - isTextContent: boolean -): void { - const elDisplayable = el.isGroup ? null : el as Displayable; - const txCfgOpt = attachedTxInfo && attachedTxInfo[state].cfg; - - // PENDING:5.0 support customize scale change and transition animation? - - if (elDisplayable) { - // By default support auto lift color when hover whether `emphasis` specified. - const stateObj = elDisplayable.ensureState(state); - if (styleOpt === false) { - const existingEmphasisState = elDisplayable.getState(state); - if (existingEmphasisState) { - existingEmphasisState.style = null; - } - } - else { - // style is needed to enable defaut emphasis. - stateObj.style = styleOpt || null; - } - // If `elOption.styleEmphasis` or `elOption.emphasis.style` is `false`, - // remove hover style. - // If `elOption.textConfig` or `elOption.emphasis.textConfig` is null/undefined, it does not - // make sense. So for simplicity, we do not ditinguish `hasOwnProperty` and null/undefined. - if (txCfgOpt) { - stateObj.textConfig = txCfgOpt; - } - - setDefaultStateProxy(elDisplayable); - } -} - -function updateZ( - el: Element, - elOption: CustomElementOption, - seriesModel: CustomSeriesModel, - attachedTxInfo: AttachedTxInfo -): void { - // Group not support textContent and not support z yet. - if (el.isGroup) { - return; - } - - const elDisplayable = el as Displayable; - const currentZ = seriesModel.currentZ; - const currentZLevel = seriesModel.currentZLevel; - // Always erase. - elDisplayable.z = currentZ; - elDisplayable.zlevel = currentZLevel; - // z2 must not be null/undefined, otherwise sort error may occur. - const optZ2 = elOption.z2; - optZ2 != null && (elDisplayable.z2 = optZ2 || 0); - - for (let i = 0; i < STATES.length; i++) { - updateZForEachState(elDisplayable, elOption, STATES[i]); - } -} - -function updateZForEachState( - elDisplayable: Displayable, - elOption: CustomDisplayableOption, - state: DisplayState -): void { - const isNormal = state === NORMAL; - const elStateOpt = isNormal ? elOption : retrieveStateOption(elOption, state as DisplayStateNonNormal); - const optZ2 = elStateOpt ? elStateOpt.z2 : null; - let stateObj; - if (optZ2 != null) { - // Do not `ensureState` until required. - stateObj = isNormal ? elDisplayable : elDisplayable.ensureState(state); - stateObj.z2 = optZ2 || 0; - } -} - -function setLagecyTransformProp( - elOption: CustomElementOption, - targetProps: Partial>, - legacyName: LegacyTransformProp, - fromTransformable?: Transformable // If provided, retrieve from the element. -): void { - const legacyArr = (elOption as any)[legacyName]; - const xyName = LEGACY_TRANSFORM_PROPS[legacyName]; - if (legacyArr) { - if (fromTransformable) { - targetProps[xyName[0]] = fromTransformable[xyName[0]]; - targetProps[xyName[1]] = fromTransformable[xyName[1]]; - } - else { - targetProps[xyName[0]] = legacyArr[0]; - targetProps[xyName[1]] = legacyArr[1]; - } - } -} - -function setTransformProp( - elOption: CustomElementOption, - allProps: Partial>, - name: TransformProp, - fromTransformable?: Transformable // If provided, retrieve from the element. -): void { - if (elOption[name] != null) { - allProps[name] = fromTransformable ? fromTransformable[name] : elOption[name]; - } -} - -function setTransformPropToTransitionFrom( - transitionFrom: Partial>, - name: TransformProp, - fromTransformable?: Transformable // If provided, retrieve from the element. -): void { - if (fromTransformable) { - transitionFrom[name] = fromTransformable[name]; - } -} - - -function makeRenderItem( - customSeries: CustomSeriesModel, - data: List, - ecModel: GlobalModel, - api: ExtensionAPI -) { - const renderItem = customSeries.get('renderItem'); - const coordSys = customSeries.coordinateSystem; - let prepareResult = {} as ReturnType; - - if (coordSys) { - if (__DEV__) { - assert(renderItem, 'series.render is required.'); - assert( - coordSys.prepareCustoms || prepareCustoms[coordSys.type], - 'This coordSys does not support custom series.' - ); - } - - // `coordSys.prepareCustoms` is used for external coord sys like bmap. - prepareResult = coordSys.prepareCustoms - ? coordSys.prepareCustoms(coordSys) - : prepareCustoms[coordSys.type](coordSys); - } - - const userAPI = defaults({ - getWidth: api.getWidth, - getHeight: api.getHeight, - getZr: api.getZr, - getDevicePixelRatio: api.getDevicePixelRatio, - value: value, - style: style, - ordinalRawValue: ordinalRawValue, - styleEmphasis: styleEmphasis, - visual: visual, - barLayout: barLayout, - currentSeriesIndices: currentSeriesIndices, - font: font - }, prepareResult.api || {}) as CustomSeriesRenderItemAPI; - - const userParams: CustomSeriesRenderItemParams = { - // The life cycle of context: current round of rendering. - // The global life cycle is probably not necessary, because - // user can store global status by themselves. - context: {}, - seriesId: customSeries.id, - seriesName: customSeries.name, - seriesIndex: customSeries.seriesIndex, - coordSys: prepareResult.coordSys, - dataInsideLength: data.count(), - encode: wrapEncodeDef(customSeries.getData()) - }; - - // If someday intending to refactor them to a class, should consider do not - // break change: currently these attribute member are encapsulated in a closure - // so that do not need to force user to call these method with a scope. - - // Do not support call `api` asynchronously without dataIndexInside input. - let currDataIndexInside: number; - let currItemModel: Model; - let currItemStyleModels: Partial>> = {}; - let currLabelModels: Partial>> = {}; - - const seriesItemStyleModels = {} as Record>; - - const seriesLabelModels = {} as Record>; - - for (let i = 0; i < STATES.length; i++) { - const stateName = STATES[i]; - seriesItemStyleModels[stateName] = (customSeries as Model) - .getModel(PATH_ITEM_STYLE[stateName]); - seriesLabelModels[stateName] = (customSeries as Model) - .getModel(PATH_LABEL[stateName]); - } - - function getItemModel(dataIndexInside: number): Model { - return dataIndexInside === currDataIndexInside - ? (currItemModel || (currItemModel = data.getItemModel(dataIndexInside))) - : data.getItemModel(dataIndexInside); - } - function getItemStyleModel(dataIndexInside: number, state: DisplayState) { - return !data.hasItemOption - ? seriesItemStyleModels[state] - : dataIndexInside === currDataIndexInside - ? (currItemStyleModels[state] || ( - currItemStyleModels[state] = getItemModel(dataIndexInside).getModel(PATH_ITEM_STYLE[state]) - )) - : getItemModel(dataIndexInside).getModel(PATH_ITEM_STYLE[state]); - } - function getLabelModel(dataIndexInside: number, state: DisplayState) { - return !data.hasItemOption - ? seriesLabelModels[state] - : dataIndexInside === currDataIndexInside - ? (currLabelModels[state] || ( - currLabelModels[state] = getItemModel(dataIndexInside).getModel(PATH_LABEL[state]) - )) - : getItemModel(dataIndexInside).getModel(PATH_LABEL[state]); - } - - return function (dataIndexInside: number, payload: Payload): CustomElementOption { - currDataIndexInside = dataIndexInside; - currItemModel = null; - currItemStyleModels = {}; - currLabelModels = {}; - - return renderItem && renderItem( - defaults({ - dataIndexInside: dataIndexInside, - dataIndex: data.getRawIndex(dataIndexInside), - // Can be used for optimization when zoom or roam. - actionType: payload ? payload.type : null - }, userParams), - userAPI - ); - }; - - /** - * @public - * @param dim by default 0. - * @param dataIndexInside by default `currDataIndexInside`. - */ - function value(dim?: DimensionLoose, dataIndexInside?: number): ParsedValue { - dataIndexInside == null && (dataIndexInside = currDataIndexInside); - return data.get(data.getDimension(dim || 0), dataIndexInside); - } - - /** - * @public - * @param dim by default 0. - * @param dataIndexInside by default `currDataIndexInside`. - */ - function ordinalRawValue(dim?: DimensionLoose, dataIndexInside?: number): ParsedValue | OrdinalRawValue { - dataIndexInside == null && (dataIndexInside = currDataIndexInside); - const dimInfo = data.getDimensionInfo(dim || 0); - if (!dimInfo) { - return; - } - const val = data.get(dimInfo.name, dataIndexInside); - const ordinalMeta = dimInfo && dimInfo.ordinalMeta; - return ordinalMeta - ? ordinalMeta.categories[val as number] - : val; - } - - /** - * @deprecated The orgininal intention of `api.style` is enable to set itemStyle - * like other series. But it not necessary and not easy to give a strict definition - * of what it return. And since echarts5 it needs to be make compat work. So - * deprecates it since echarts5. - * - * By default, `visual` is applied to style (to support visualMap). - * `visual.color` is applied at `fill`. If user want apply visual.color on `stroke`, - * it can be implemented as: - * `api.style({stroke: api.visual('color'), fill: null})`; - * - * [Compat]: since ec5, RectText has been separated from its hosts el. - * so `api.style()` will only return the style from `itemStyle` but not handle `label` - * any more. But `series.label` config is never published in doc. - * We still compat it in `api.style()`. But not encourage to use it and will still not - * to pulish it to doc. - * @public - * @param dataIndexInside by default `currDataIndexInside`. - */ - function style(userProps?: ZRStyleProps, dataIndexInside?: number): ZRStyleProps { - if (__DEV__) { - warnDeprecated('api.style', 'Please write literal style directly instead.'); - } - - dataIndexInside == null && (dataIndexInside = currDataIndexInside); - - const style = data.getItemVisual(dataIndexInside, 'style'); - const visualColor = style && style.fill; - const opacity = style && style.opacity; - - let itemStyle = getItemStyleModel(dataIndexInside, NORMAL).getItemStyle(); - visualColor != null && (itemStyle.fill = visualColor); - opacity != null && (itemStyle.opacity = opacity); - - const opt = {inheritColor: isString(visualColor) ? visualColor : '#000'}; - const labelModel = getLabelModel(dataIndexInside, NORMAL); - // Now that the feture of "auto adjust text fill/stroke" has been migrated to zrender - // since ec5, we should set `isAttached` as `false` here and make compat in - // `convertToEC4StyleForCustomSerise`. - const textStyle = labelStyleHelper.createTextStyle(labelModel, null, opt, false, true); - textStyle.text = labelModel.getShallow('show') - ? retrieve2( - customSeries.getFormattedLabel(dataIndexInside, NORMAL), - getDefaultLabel(data, dataIndexInside) - ) - : null; - const textConfig = labelStyleHelper.createTextConfig(labelModel, opt, false); - - preFetchFromExtra(userProps, itemStyle); - itemStyle = convertToEC4StyleForCustomSerise(itemStyle, textStyle, textConfig); - - userProps && applyUserPropsAfter(itemStyle, userProps); - (itemStyle as LegacyStyleProps).legacy = true; - - return itemStyle; - } - - /** - * @deprecated The reason see `api.style()` - * @public - * @param dataIndexInside by default `currDataIndexInside`. - */ - function styleEmphasis(userProps?: ZRStyleProps, dataIndexInside?: number): ZRStyleProps { - if (__DEV__) { - warnDeprecated('api.styleEmphasis', 'Please write literal style directly instead.'); - } - - dataIndexInside == null && (dataIndexInside = currDataIndexInside); - - let itemStyle = getItemStyleModel(dataIndexInside, EMPHASIS).getItemStyle(); - const labelModel = getLabelModel(dataIndexInside, EMPHASIS); - const textStyle = labelStyleHelper.createTextStyle(labelModel, null, null, true, true); - textStyle.text = labelModel.getShallow('show') - ? retrieve3( - customSeries.getFormattedLabel(dataIndexInside, EMPHASIS), - customSeries.getFormattedLabel(dataIndexInside, NORMAL), - getDefaultLabel(data, dataIndexInside) - ) - : null; - const textConfig = labelStyleHelper.createTextConfig(labelModel, null, true); - - preFetchFromExtra(userProps, itemStyle); - itemStyle = convertToEC4StyleForCustomSerise(itemStyle, textStyle, textConfig); - - userProps && applyUserPropsAfter(itemStyle, userProps); - (itemStyle as LegacyStyleProps).legacy = true; - - return itemStyle; - } - - function applyUserPropsAfter(itemStyle: ZRStyleProps, extra: ZRStyleProps): void { - for (const key in extra) { - if (hasOwn(extra, key)) { - (itemStyle as any)[key] = (extra as any)[key]; - } - } - } - - function preFetchFromExtra(extra: ZRStyleProps, itemStyle: ItemStyleProps): void { - // A trick to retrieve those props firstly, which are used to - // apply auto inside fill/stroke in `convertToEC4StyleForCustomSerise`. - // (It's not reasonable but only for a degree of compat) - if (extra) { - (extra as any).textFill && ((itemStyle as any).textFill = (extra as any).textFill); - (extra as any).textPosition && ((itemStyle as any).textPosition = (extra as any).textPosition); - } - } - - /** - * @public - * @param dataIndexInside by default `currDataIndexInside`. - */ - function visual( - visualType: VT, - dataIndexInside?: number - ): VT extends NonStyleVisualProps ? DefaultDataVisual[VT] - : VT extends StyleVisualProps ? PathStyleProps[typeof STYLE_VISUAL_TYPE[VT]] - : never { - - dataIndexInside == null && (dataIndexInside = currDataIndexInside); - - if (hasOwn(STYLE_VISUAL_TYPE, visualType)) { - const style = data.getItemVisual(dataIndexInside, 'style'); - return style - ? style[STYLE_VISUAL_TYPE[visualType as StyleVisualProps]] as any - : null; - } - // Only support these visuals. Other visual might be inner tricky - // for performance (like `style`), do not expose to users. - if (hasOwn(NON_STYLE_VISUAL_PROPS, visualType)) { - return data.getItemVisual(dataIndexInside, visualType as NonStyleVisualProps) as any; - } - } - - /** - * @public - * @return If not support, return undefined. - */ - function barLayout( - opt: Omit[0], 'axis'> - ): ReturnType { - if (coordSys.type === 'cartesian2d') { - const baseAxis = coordSys.getBaseAxis() as Axis2D; - return getLayoutOnAxis(defaults({axis: baseAxis}, opt)); - } - } - - /** - * @public - */ - function currentSeriesIndices(): ReturnType { - return ecModel.getCurrentSeriesIndices(); - } - - /** - * @public - * @return font string - */ - function font( - opt: Parameters[0] - ): ReturnType { - return labelStyleHelper.getFont(opt, ecModel); - } -} - -type WrapEncodeDefRet = Dictionary; - -function wrapEncodeDef(data: List): WrapEncodeDefRet { - const encodeDef = {} as WrapEncodeDefRet; - each(data.dimensions, function (dimName, dataDimIndex) { - const dimInfo = data.getDimensionInfo(dimName); - if (!dimInfo.isExtraCoord) { - const coordDim = dimInfo.coordDim; - const dataDims = encodeDef[coordDim] = encodeDef[coordDim] || []; - dataDims[dimInfo.coordDimIndex] = dataDimIndex; - } - }); - return encodeDef; -} - -function createOrUpdateItem( - api: ExtensionAPI, - el: Element, - dataIndex: number, - elOption: CustomElementOption, - seriesModel: CustomSeriesModel, - group: ViewRootGroup, - data: List, - morphPreparation: MorphPreparation -): Element { - // [Rule] - // If `renderItem` returns `null`/`undefined`/`false`, remove the previous el if existing. - // (It seems that violate the "merge" principle, but most of users probably intuitively - // regard "return;" as "show nothing element whatever", so make a exception to meet the - // most cases.) - // The rule or "merge" see [STRATEGY_MERGE]. - - // If `elOption` is `null`/`undefined`/`false` (when `renderItem` returns nothing). - if (!elOption) { - removeElementDirectly(el, group); - return; - } - el = doCreateOrUpdateEl(api, el, dataIndex, elOption, seriesModel, group, true, morphPreparation); - el && data.setItemGraphicEl(dataIndex, el); - - el && enableHoverEmphasis(el, elOption.focus, elOption.blurScope); - - return el; -} - -function doCreateOrUpdateEl( - api: ExtensionAPI, - el: Element, - dataIndex: number, - elOption: CustomElementOption, - seriesModel: CustomSeriesModel, - group: ViewRootGroup, - isRoot: boolean, - morphPreparation: MorphPreparation -): Element { - - if (__DEV__) { - assert(elOption, 'should not have an null/undefined element setting'); - } - - let toBeReplacedIdx = -1; - if ( - el && ( - doesElNeedRecreate(el, elOption) - // || ( - // // PENDING: even in one-to-one mapping case, if el is marked as morph, - // // do not sure whether the el will be mapped to another el with different - // // hierarchy in Group tree. So always recreate el rather than reuse the el. - // morphPreparation && morphPreparation.isOneToOneFrom(el) - // ) - ) - ) { - // Should keep at the original index, otherwise "merge by index" will be incorrect. - toBeReplacedIdx = group.childrenRef().indexOf(el); - el = null; - } - - const elIsNewCreated = !el; - - if (!el) { - el = createEl(elOption); - } - else { - // FIMXE:NEXT unified clearState? - // If in some case the performance issue arised, consider - // do not clearState but update cached normal state directly. - el.clearStates(); - } - - const canMorph = inner(el).canMorph = (elOption as CustomZRPathOption).morph && isPath(el); - const thisElIsMorphTo = canMorph && morphPreparation && morphPreparation.hasFrom(); - - // Use update animation when morph is enabled. - const isInit = elIsNewCreated && !thisElIsMorphTo; - - attachedTxInfoTmp.normal.cfg = attachedTxInfoTmp.normal.conOpt = - attachedTxInfoTmp.emphasis.cfg = attachedTxInfoTmp.emphasis.conOpt = - attachedTxInfoTmp.blur.cfg = attachedTxInfoTmp.blur.conOpt = - attachedTxInfoTmp.select.cfg = attachedTxInfoTmp.select.conOpt = null; - - attachedTxInfoTmp.isLegacy = false; - - doCreateOrUpdateAttachedTx( - el, dataIndex, elOption, seriesModel, isInit, attachedTxInfoTmp - ); - - doCreateOrUpdateClipPath( - el, dataIndex, elOption, seriesModel, isInit - ); - - const pendingAllPropsFinal = updateElNormal( - api, - el, - thisElIsMorphTo, - dataIndex, - elOption, - elOption.style, - attachedTxInfoTmp, - seriesModel, - isInit, - false - ); - - if (thisElIsMorphTo) { - morphPreparation.addTo(el as graphicUtil.Path, elOption, dataIndex, pendingAllPropsFinal); - } - - for (let i = 0; i < STATES.length; i++) { - const stateName = STATES[i]; - if (stateName !== NORMAL) { - const otherStateOpt = retrieveStateOption(elOption, stateName); - const otherStyleOpt = retrieveStyleOptionOnState(elOption, otherStateOpt, stateName); - updateElOnState(stateName, el, otherStateOpt, otherStyleOpt, attachedTxInfoTmp, isRoot, false); - } - } - - updateZ(el, elOption, seriesModel, attachedTxInfoTmp); - - if (elOption.type === 'group') { - mergeChildren( - api, el as graphicUtil.Group, dataIndex, elOption as CustomGroupOption, seriesModel, morphPreparation - ); - } - - if (toBeReplacedIdx >= 0) { - group.replaceAt(el, toBeReplacedIdx); - } - else { - group.add(el); - } - - return el; -} - -// `el` must not be null/undefined. -function doesElNeedRecreate(el: Element, elOption: CustomElementOption): boolean { - const elInner = inner(el); - const elOptionType = elOption.type; - const elOptionShape = (elOption as CustomZRPathOption).shape; - const elOptionStyle = elOption.style; - return ( - // If `elOptionType` is `null`, follow the merge principle. - (elOptionType != null - && elOptionType !== elInner.customGraphicType - ) - || (elOptionType === 'path' - && hasOwnPathData(elOptionShape) - && getPathData(elOptionShape) !== elInner.customPathData - ) - || (elOptionType === 'image' - && hasOwn(elOptionStyle, 'image') - && (elOptionStyle as CustomImageOption['style']).image !== elInner.customImagePath - ) - // // FIXME test and remove this restriction? - // || (elOptionType === 'text' - // && hasOwn(elOptionStyle, 'text') - // && (elOptionStyle as TextStyleProps).text !== elInner.customText - // ) - ); -} - -function doCreateOrUpdateClipPath( - el: Element, - dataIndex: number, - elOption: CustomElementOption, - seriesModel: CustomSeriesModel, - isInit: boolean -): void { - // Based on the "merge" principle, if no clipPath provided, - // do nothing. The exists clip will be totally removed only if - // `el.clipPath` is `false`. Otherwise it will be merged/replaced. - const clipPathOpt = elOption.clipPath; - if (clipPathOpt === false) { - if (el && el.getClipPath()) { - el.removeClipPath(); - } - } - else if (clipPathOpt) { - let clipPath = el.getClipPath(); - if (clipPath && doesElNeedRecreate(clipPath, clipPathOpt)) { - clipPath = null; - } - if (!clipPath) { - clipPath = createEl(clipPathOpt) as graphicUtil.Path; - if (__DEV__) { - assert( - clipPath instanceof graphicUtil.Path, - 'Only any type of `path` can be used in `clipPath`, rather than ' + clipPath.type + '.' - ); - } - el.setClipPath(clipPath); - } - updateElNormal( - null, clipPath, null, dataIndex, clipPathOpt, null, null, seriesModel, isInit, false - ); - } - // If not define `clipPath` in option, do nothing unnecessary. -} - -function doCreateOrUpdateAttachedTx( - el: Element, - dataIndex: number, - elOption: CustomElementOption, - seriesModel: CustomSeriesModel, - isInit: boolean, - attachedTxInfo: AttachedTxInfo -): void { - // group do not support textContent temporarily untill necessary. - if (el.isGroup) { - return; - } - - // Normal must be called before emphasis, for `isLegacy` detection. - processTxInfo(elOption, null, attachedTxInfo); - processTxInfo(elOption, EMPHASIS, attachedTxInfo); - - // If `elOption.textConfig` or `elOption.textContent` is null/undefined, it does not make sence. - // So for simplicity, if "elOption hasOwnProperty of them but be null/undefined", we do not - // trade them as set to null to el. - // Especially: - // `elOption.textContent: false` means remove textContent. - // `elOption.textContent.emphasis.style: false` means remove the style from emphasis state. - let txConOptNormal = attachedTxInfo.normal.conOpt as CustomElementOption | false; - const txConOptEmphasis = attachedTxInfo.emphasis.conOpt as CustomElementOptionOnState; - const txConOptBlur = attachedTxInfo.blur.conOpt as CustomElementOptionOnState; - const txConOptSelect = attachedTxInfo.select.conOpt as CustomElementOptionOnState; - - if (txConOptNormal != null || txConOptEmphasis != null || txConOptSelect != null || txConOptBlur != null) { - let textContent = el.getTextContent(); - if (txConOptNormal === false) { - textContent && el.removeTextContent(); - } - else { - txConOptNormal = attachedTxInfo.normal.conOpt = txConOptNormal || {type: 'text'}; - if (!textContent) { - textContent = createEl(txConOptNormal) as graphicUtil.Text; - el.setTextContent(textContent); - } - else { - // If in some case the performance issue arised, consider - // do not clearState but update cached normal state directly. - textContent.clearStates(); - } - const txConStlOptNormal = txConOptNormal && txConOptNormal.style; - - updateElNormal( - null, textContent, null, dataIndex, txConOptNormal, txConStlOptNormal, null, seriesModel, isInit, true - ); - for (let i = 0; i < STATES.length; i++) { - const stateName = STATES[i]; - if (stateName !== NORMAL) { - const txConOptOtherState = attachedTxInfo[stateName].conOpt as CustomElementOptionOnState; - updateElOnState( - stateName, - textContent, - txConOptOtherState, - retrieveStyleOptionOnState(txConOptNormal, txConOptOtherState, stateName), - null, false, true - ); - } - } - - txConStlOptNormal ? textContent.dirty() : textContent.markRedraw(); - } - } -} - -function processTxInfo( - elOption: CustomElementOption, - state: DisplayStateNonNormal, - attachedTxInfo: AttachedTxInfo -): void { - const stateOpt = !state ? elOption : retrieveStateOption(elOption, state); - const styleOpt = !state ? elOption.style : retrieveStyleOptionOnState(elOption, stateOpt, EMPHASIS); - - const elType = elOption.type; - let txCfg = stateOpt ? stateOpt.textConfig : null; - const txConOptNormal = elOption.textContent; - let txConOpt: CustomElementOption | CustomElementOptionOnState = - !txConOptNormal ? null : !state ? txConOptNormal : retrieveStateOption(txConOptNormal, state); - - if (styleOpt && ( - // Because emphasis style has little info to detect legacy, - // if normal is legacy, emphasis is trade as legacy. - attachedTxInfo.isLegacy - || isEC4CompatibleStyle(styleOpt, elType, !!txCfg, !!txConOpt) - )) { - attachedTxInfo.isLegacy = true; - const convertResult = convertFromEC4CompatibleStyle(styleOpt, elType, !state); - // Explicitly specified `textConfig` and `textContent` has higher priority than - // the ones generated by legacy style. Otherwise if users use them and `api.style` - // at the same time, they not both work and hardly to known why. - if (!txCfg && convertResult.textConfig) { - txCfg = convertResult.textConfig; - } - if (!txConOpt && convertResult.textContent) { - txConOpt = convertResult.textContent; - } - } - - if (!state && txConOpt) { - const txConOptNormal = txConOpt as CustomElementOption; - // `textContent: {type: 'text'}`, the "type" is easy to be missing. So we tolerate it. - !txConOptNormal.type && (txConOptNormal.type = 'text'); - if (__DEV__) { - // Do not tolerate incorret type for forward compat. - txConOptNormal.type !== 'text' && assert( - txConOptNormal.type === 'text', - 'textContent.type must be "text"' - ); - } - } - - const info = !state ? attachedTxInfo.normal : attachedTxInfo[state]; - info.cfg = txCfg; - info.conOpt = txConOpt; -} - -function retrieveStateOption( - elOption: CustomElementOption, state: DisplayStateNonNormal -): CustomElementOptionOnState { - return !state ? elOption : elOption ? elOption[state] : null; -} - -function retrieveStyleOptionOnState( - stateOptionNormal: CustomElementOption, - stateOption: CustomElementOptionOnState, - state: DisplayStateNonNormal -): CustomElementOptionOnState['style'] { - let style = stateOption && stateOption.style; - if (style == null && state === EMPHASIS && stateOptionNormal) { - style = stateOptionNormal.styleEmphasis; - } - return style; -} - - -// Usage: -// (1) By default, `elOption.$mergeChildren` is `'byIndex'`, which indicates that -// the existing children will not be removed, and enables the feature that -// update some of the props of some of the children simply by construct -// the returned children of `renderItem` like: -// `var children = group.children = []; children[3] = {opacity: 0.5};` -// (2) If `elOption.$mergeChildren` is `'byName'`, add/update/remove children -// by child.name. But that might be lower performance. -// (3) If `elOption.$mergeChildren` is `false`, the existing children will be -// replaced totally. -// (4) If `!elOption.children`, following the "merge" principle, nothing will happen. -// -// For implementation simpleness, do not provide a direct way to remove sinlge -// child (otherwise the total indicies of the children array have to be modified). -// User can remove a single child by set its `ignore` as `true`. -function mergeChildren( - api: ExtensionAPI, - el: graphicUtil.Group, - dataIndex: number, - elOption: CustomGroupOption, - seriesModel: CustomSeriesModel, - morphPreparation: MorphPreparation -): void { - - const newChildren = elOption.children; - const newLen = newChildren ? newChildren.length : 0; - const mergeChildren = elOption.$mergeChildren; - // `diffChildrenByName` has been deprecated. - const byName = mergeChildren === 'byName' || elOption.diffChildrenByName; - const notMerge = mergeChildren === false; - - // For better performance on roam update, only enter if necessary. - if (!newLen && !byName && !notMerge) { - return; - } - - if (byName) { - diffGroupChildren({ - api: api, - oldChildren: el.children() || [], - newChildren: newChildren || [], - dataIndex: dataIndex, - seriesModel: seriesModel, - group: el, - morphPreparation: morphPreparation - }); - return; - } - - notMerge && el.removeAll(); - - // Mapping children of a group simply by index, which - // might be better performance. - let index = 0; - for (; index < newLen; index++) { - newChildren[index] && doCreateOrUpdateEl( - api, - el.childAt(index), - dataIndex, - newChildren[index], - seriesModel, - el, - false, - morphPreparation - ); - } - for (let i = el.childCount() - 1; i >= index; i--) { - // Do not supprot leave elements that are not mentioned in the latest - // `renderItem` return. Otherwise users may not have a clear and simple - // concept that how to contorl all of the elements. - doRemoveEl(el.childAt(i), seriesModel, el); - } -} - -type DiffGroupContext = { - api: ExtensionAPI; - oldChildren: Element[]; - newChildren: CustomElementOption[]; - dataIndex: number; - seriesModel: CustomSeriesModel; - group: graphicUtil.Group; - morphPreparation: MorphPreparation; -}; -function diffGroupChildren(context: DiffGroupContext) { - (new DataDiffer( - context.oldChildren, - context.newChildren, - getKey, - getKey, - context - )) - .add(processAddUpdate) - .update(processAddUpdate) - .remove(processRemove) - .execute(); -} - -function getKey(item: Element, idx: number): string { - const name = item && item.name; - return name != null ? name : GROUP_DIFF_PREFIX + idx; -} - -function processAddUpdate( - this: DataDiffer, - newIndex: number, - oldIndex?: number -): void { - const context = this.context; - const childOption = newIndex != null ? context.newChildren[newIndex] : null; - const child = oldIndex != null ? context.oldChildren[oldIndex] : null; - - doCreateOrUpdateEl( - context.api, - child, - context.dataIndex, - childOption, - context.seriesModel, - context.group, - false, - context.morphPreparation - ); -} - -function processRemove(this: DataDiffer, oldIndex: number): void { - const context = this.context; - const child = context.oldChildren[oldIndex]; - doRemoveEl(child, context.seriesModel, context.group); -} - -function doRemoveEl( - el: Element, - seriesModel: CustomSeriesModel, - group: ViewRootGroup -): void { - if (el) { - const leaveToProps = inner(el).leaveToProps; - leaveToProps - ? graphicUtil.updateProps(el, leaveToProps, seriesModel, { - cb: function () { - group.remove(el); - } - }) - : group.remove(el); - } -} - -/** - * @return SVG Path data. - */ -function getPathData(shape: CustomSVGPathOption['shape']): string { - // "d" follows the SVG convention. - return shape && (shape.pathData || shape.d); -} - -function hasOwnPathData(shape: CustomSVGPathOption['shape']): boolean { - return shape && (hasOwn(shape, 'pathData') || hasOwn(shape, 'd')); -} - -function isPath(el: Element): el is graphicUtil.Path { - return el && el instanceof graphicUtil.Path; -} - -function removeElementDirectly(el: Element, group: ViewRootGroup): void { - el && group.remove(el); -} - - -type MorphPreparationType = 'oneToOne' | 'oneToMany' | 'manyToOne'; - -/** - * Any morph-potential el should added by `morphPreparation.addTo(el)`. - * And they may apply morph or not when `morphPreparation.applyMorphing()`. - * But at least, all of the "to" elements will apply all of the updates - * as `doCreateOrUpdateItem` did. - */ -class MorphPreparation { - private _type: MorphPreparationType; - private _fromList: graphicUtil.Path[] = []; - private _toList: graphicUtil.Path[] = []; - private _toElOptionList: CustomElementOption[] = []; - private _allPropsFinalList: ElementProps[] = []; - private _toDataIndices: number[] = []; - private _transOpt: SeriesModel['__transientTransitionOpt']; - private _seriesModel: CustomSeriesModel; - // Key: `toDataIndex`, not `toIdx` - private _morphConfigList: CombineSeparateConfig[] = []; - - constructor( - seriesModel: CustomSeriesModel, - transOpt: SeriesModel['__transientTransitionOpt'] - ) { - this._seriesModel = seriesModel; - this._transOpt = transOpt; - } - - hasFrom(): boolean { - return !!this._fromList.length; - } - - // isOneToOneFrom(el: Element): boolean { - // if (el && inner(el).canMorph) { - // const fromList = this._fromList; - // for (let i = 0; i < fromList.length; i++) { - // if (fromList[i] === el) { - // return true; - // } - // } - // } - // } - - findAndAddFrom(el: Element): void { - if (!el) { - return; - } - if (inner(el).canMorph) { - this._fromList.push(el as graphicUtil.Path); - } - if (el.isGroup) { - const children = (el as graphicUtil.Group).childrenRef(); - for (let i = 0; i < children.length; i++) { - this.findAndAddFrom(children[i]); - } - } - } - - addTo( - path: graphicUtil.Path, - elOption: CustomElementOption, - dataIndex: number, - allPropsFinal: ElementProps - ): void { - if (path) { - this._toList.push(path); - this._toElOptionList.push(elOption); - this._toDataIndices.push(dataIndex); - this._allPropsFinalList.push(allPropsFinal); - } - } - - applyMorphing(): void { - // [MORPHING_LOGIC_HINT] - // Pay attention to the order: - // (A) Apply `allPropsFinal` and `styleOption` to "to". - // (Then "to" becomes to the final state.) - // (B) Apply `morphPath`/`combine`/`separate`. - // (Based on the current state of "from" and the final state of "to".) - // (Then we may get "from.subList" or "to.subList".) - // (C) Copy the related props from "from" to "from.subList", from "to" to "to.subList". - // (D) Collect `transitionFromProps` for "to" and "to.subList" - // (Based on "from" or "from.subList".) - // (E) Apply `transitionFromProps` to "to" and "to.subList" - // (It might change the prop values to the first frame value.) - // Case_I: - // If (D) should be after (C), we use sequence: A - B - C - D - E - // Case_II: - // If (A) should be after (D), we use sequence: D - A - B - C - E - - // [MORPHING_LOGIC_HINT] - // zrender `morphPath`/`combine`/`separate` only manages the shape animation. - // Other props (like transfrom, style transition) will handled in echarts). - - // [MORPHING_LOGIC_HINT] - // Make sure `applyPropsFinal` always be called for "to". - - const type = this._type; - const fromList = this._fromList; - const toList = this._toList; - const toListLen = toList.length; - const fromListLen = fromList.length; - - if (!fromListLen || !toListLen) { - return; - } - - if (type === 'oneToOne') { - // In one-to-one case, we by default apply a simple rule: - // map "from" and "to" one by one. - // For this case: old_data_item_el and new_data_item_el - // has the same hierarchy of group tree but only some path type changed. - for (let toIdx = 0; toIdx < toListLen; toIdx++) { - this._oneToOneForSingleTo(toIdx, toIdx); - } - } - - else if (type === 'manyToOne') { - // A rough strategy: if there are more than one "to", we simply divide "fromList" equally. - const fromSingleSegLen = Math.max(1, Math.floor(fromListLen / toListLen)); - for ( - let toIdx = 0, fromIdxStart = 0; - toIdx < toListLen; - toIdx++, fromIdxStart += fromSingleSegLen - ) { - const fromCount = toIdx + 1 >= toListLen - ? fromListLen - fromIdxStart - : fromSingleSegLen; - this._manyToOneForSingleTo( - toIdx, fromIdxStart >= fromListLen ? null : fromIdxStart, fromCount - ); - } - } - - else if (type === 'oneToMany') { - // A rough strategy: if there are more than one "from", we simply divide "toList" equally. - const toSingleSegLen = Math.max(1, Math.floor(toListLen / fromListLen)); - for ( - let toIdxStart = 0, fromIdx = 0; - toIdxStart < toListLen; - toIdxStart += toSingleSegLen, fromIdx++ - ) { - const toCount = toIdxStart + toSingleSegLen >= toListLen - ? toListLen - toIdxStart - : toSingleSegLen; - this._oneToManyForSingleFrom( - toIdxStart, toCount, fromIdx >= fromListLen ? null : fromIdx - ); - } - } - } - - private _oneToOneForSingleTo( - // "to" must NOT be null/undefined. - toIdx: number, - // May `fromIdx >= this._fromList.length` - fromIdx: number - ): void { - const to = this._toList[toIdx]; - const toElOption = this._toElOptionList[toIdx]; - const toDataIndex = this._toDataIndices[toIdx]; - const allPropsFinal = this._allPropsFinalList[toIdx]; - const from = this._fromList[fromIdx]; - - const elAnimationConfig = this._getOrCreateMorphConfig(toDataIndex); - const morphDuration = elAnimationConfig.duration; - - if (from && isCombiningPath(from)) { - applyPropsFinal(to, allPropsFinal, toElOption.style); - - if (morphDuration) { - const combineResult = combine([from], to, elAnimationConfig, copyPropsWhenDivided); - this._processResultIndividuals(combineResult, toIdx, null); - } - // The target el will not be displayed and transition from multiple path. - // transition on the target el does not make sense. - } - else { - const morphFrom = ( - morphDuration - // from === to usually happen in scenarios where internal update like - // "dataZoom", "legendToggle" happen. If from is not in any morphing, - // we do not need to call `morphPath`. - && from - && (from !== to || isInAnyMorphing(from)) - ) ? from : null; - - // See [Case_II] above. - // In this case, there is probably `from === to`. And the `transitionFromProps` collecting - // does not depends on morphing. So we collect `transitionFromProps` first. - const transFromProps = {} as ElementProps; - prepareShapeOrExtraTransitionFrom('shape', to, morphFrom, toElOption, transFromProps, false); - prepareShapeOrExtraTransitionFrom('extra', to, morphFrom, toElOption, transFromProps, false); - prepareTransformTransitionFrom(to, morphFrom, toElOption, transFromProps, false); - prepareStyleTransitionFrom(to, morphFrom, toElOption, toElOption.style, transFromProps, false); - - applyPropsFinal(to, allPropsFinal, toElOption.style); - - if (morphFrom) { - morphPath(morphFrom, to, elAnimationConfig); - } - applyTransitionFrom(to, toDataIndex, toElOption, this._seriesModel, transFromProps, false); - } - } - - private _manyToOneForSingleTo( - // "to" must NOT be null/undefined. - toIdx: number, - // May be null. - fromIdxStart: number, - fromCount: number - ): void { - const to = this._toList[toIdx]; - const toElOption = this._toElOptionList[toIdx]; - const allPropsFinal = this._allPropsFinalList[toIdx]; - - applyPropsFinal(to, allPropsFinal, toElOption.style); - - const elAnimationConfig = this._getOrCreateMorphConfig(this._toDataIndices[toIdx]); - if (elAnimationConfig.duration && fromIdxStart != null) { - const combineFromList = []; - for (let fromIdx = fromIdxStart; fromIdx < fromCount; fromIdx++) { - combineFromList.push(this._fromList[fromIdx]); - } - const combineResult = combine(combineFromList, to, elAnimationConfig, copyPropsWhenDivided); - this._processResultIndividuals(combineResult, toIdx, null); - } - } - - private _oneToManyForSingleFrom( - // "to" must NOT be null/undefined. - toIdxStart: number, - toCount: number, - // May be null - fromIdx: number - ): void { - const from = fromIdx == null ? null : this._fromList[fromIdx]; - const toList = this._toList; - - const separateToList = []; - for (let toIdx = toIdxStart; toIdx < toCount; toIdx++) { - const to = toList[toIdx]; - applyPropsFinal(to, this._allPropsFinalList[toIdx], this._toElOptionList[toIdx].style); - separateToList.push(to); - } - - const elAnimationConfig = this._getOrCreateMorphConfig(this._toDataIndices[toIdxStart]); - if (elAnimationConfig.duration && from) { - const separateResult = separate(from, separateToList, elAnimationConfig, copyPropsWhenDivided); - this._processResultIndividuals(separateResult, toIdxStart, toCount); - } - } - - private _processResultIndividuals( - combineSeparateResult: CombineSeparateResult, - toIdxStart: number, - toCount: number - ): void { - const isSeparate = toCount != null; - - for (let i = 0; i < combineSeparateResult.count; i++) { - const fromIndividual = combineSeparateResult.fromIndividuals[i]; - const toIndividual = combineSeparateResult.toIndividuals[i]; - // Here it's a trick: - // For "combine" case, all of the `toIndividuals` map to the same `toIdx`. - // For "separate" case, the `toIndividuals` map to some certain segment of `_toList` accurately. - const toIdx = toIdxStart + (isSeparate ? i : 0); - - const toElOption = this._toElOptionList[toIdx]; - const dataIndex = this._toDataIndices[toIdx]; - - const transFromProps = {} as ElementProps; - prepareTransformTransitionFrom( - toIndividual, fromIndividual, toElOption, transFromProps, false - ); - prepareStyleTransitionFrom( - toIndividual, fromIndividual, toElOption, toElOption.style, transFromProps, false - ); - applyTransitionFrom( - toIndividual, dataIndex, toElOption, this._seriesModel, transFromProps, false - ); - } - } - - _getOrCreateMorphConfig(dataIndex: number): CombineSeparateConfig { - const morphConfigList = this._morphConfigList; - let config = morphConfigList[dataIndex]; - if (config) { - return config; - } - - let duration: number; - let easing: AnimationEasing; - let delay: number; - const seriesModel = this._seriesModel; - const transOpt = this._transOpt; - - if (seriesModel.isAnimationEnabled()) { - // PENDING: refactor? this is the same logic as `src/util/graphic.ts#animateOrSetProps`. - let animationPayload: PayloadAnimationPart; - if (seriesModel && seriesModel.ecModel) { - const updatePayload = seriesModel.ecModel.getUpdatePayload(); - animationPayload = (updatePayload && updatePayload.animation) as PayloadAnimationPart; - } - if (animationPayload) { - duration = animationPayload.duration || 0; - easing = animationPayload.easing || 'cubicOut'; - delay = animationPayload.delay || 0; - } - else { - easing = seriesModel.get('animationEasingUpdate'); - const delayOption = seriesModel.get('animationDelayUpdate'); - delay = isFunction(delayOption) ? delayOption(dataIndex) : delayOption; - const durationOption = seriesModel.get('animationDurationUpdate'); - duration = isFunction(durationOption) ? durationOption(dataIndex) : durationOption; - } - } - - config = { - duration: duration || 0, - delay: delay, - easing: easing, - dividingMethod: transOpt ? transOpt.dividingMethod : null - }; - morphConfigList[dataIndex] = config; - - return config; - } - - reset(type: MorphPreparationType): void { - // `this._morphConfigList` can be kept. It only related to `dataIndex`. - this._type = type; - this._fromList.length = - this._toList.length = - this._toElOptionList.length = - this._allPropsFinalList.length = - this._toDataIndices.length = 0; - } -} - -function copyPropsWhenDivided( - srcPath: graphicUtil.Path, - tarPath: graphicUtil.Path, - willClone: boolean -): void { - // Do not copy transform props. - // Sub paths are transfrom based on their host path. - // tarPath.x = srcPath.x; - // tarPath.y = srcPath.y; - // tarPath.scaleX = srcPath.scaleX; - // tarPath.scaleY = srcPath.scaleY; - // tarPath.originX = srcPath.originX; - // tarPath.originY = srcPath.originY; - - // If just carry the style, will not be modifed, so do not copy. - tarPath.style = willClone - ? clone(srcPath.style) - : srcPath.style; - - tarPath.zlevel = srcPath.zlevel; - tarPath.z = srcPath.z; - tarPath.z2 = srcPath.z2; -} +import CustomSeriesModel from './CustomSeries'; +import CustomChartView from './CustomView'; export function install(registers: EChartsExtensionInstallRegisters) { - registers.registerChartView(CustomSeriesView); + registers.registerChartView(CustomChartView); registers.registerSeriesModel(CustomSeriesModel); } \ No newline at end of file diff --git a/src/chart/custom/prepare.ts b/src/chart/custom/prepare.ts new file mode 100644 index 0000000000..37f8d99e30 --- /dev/null +++ b/src/chart/custom/prepare.ts @@ -0,0 +1,353 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +import Transformable from 'zrender/src/core/Transformable'; +import Element, { ElementProps } from 'zrender/src/Element'; +import { Dictionary } from '../../util/types'; +import { + CustomDisplayableOption, + CustomElementOption, + customInnerStore, + LooseElementProps, + TransformProp, + TRANSFORM_PROPS, + TransitionAnyOption +} from './CustomSeries'; +import { normalizeToArray } from '../../util/model'; +import { assert, hasOwn, indexOf, isArrayLike, keys } from 'zrender/src/core/util'; +import { cloneValue } from 'zrender/src/animation/Animator'; +import Displayable from 'zrender/src/graphic/Displayable'; + +const LEGACY_TRANSFORM_PROPS = { + position: ['x', 'y'], + scale: ['scaleX', 'scaleY'], + origin: ['originX', 'originY'] +} as const; +type LegacyTransformProp = keyof typeof LEGACY_TRANSFORM_PROPS; + +function setLegacyTransformProp( + elOption: CustomElementOption, + targetProps: Partial>, + legacyName: LegacyTransformProp +): void { + const legacyArr = (elOption as any)[legacyName]; + const xyName = LEGACY_TRANSFORM_PROPS[legacyName]; + if (legacyArr) { + targetProps[xyName[0]] = legacyArr[0]; + targetProps[xyName[1]] = legacyArr[1]; + } +} + +function setTransformProp( + elOption: CustomElementOption, + allProps: Partial>, + name: TransformProp +): void { + if (elOption[name] != null) { + allProps[name] = elOption[name]; + } +} + +function setTransformPropToTransitionFrom( + transitionFrom: Partial>, + name: TransformProp, + fromTransformable?: Transformable // If provided, retrieve from the element. +): void { + if (fromTransformable) { + transitionFrom[name] = fromTransformable[name]; + } +} + + +// See [STRATEGY_TRANSITION] +export function prepareShapeOrExtraTransitionFrom( + mainAttr: 'shape' | 'extra', + fromEl: Element, + elOption: CustomElementOption, + transFromProps: LooseElementProps, + isInit: boolean +): void { + + const attrOpt: Dictionary & TransitionAnyOption = (elOption as any)[mainAttr]; + if (!attrOpt) { + return; + } + + const elPropsInAttr = (fromEl as LooseElementProps)[mainAttr]; + let transFromPropsInAttr: Dictionary; + + const enterFrom = attrOpt.enterFrom; + if (isInit && enterFrom) { + !transFromPropsInAttr && (transFromPropsInAttr = transFromProps[mainAttr] = {}); + const enterFromKeys = keys(enterFrom); + for (let i = 0; i < enterFromKeys.length; i++) { + // `enterFrom` props are not necessarily also declared in `shape`/`style`/..., + // for example, `opacity` can only declared in `enterFrom` but not in `style`. + const key = enterFromKeys[i]; + // Do not clone, animator will perform that clone. + transFromPropsInAttr[key] = enterFrom[key]; + } + } + + if (!isInit && elPropsInAttr) { + if (attrOpt.transition) { + !transFromPropsInAttr && (transFromPropsInAttr = transFromProps[mainAttr] = {}); + const transitionKeys = normalizeToArray(attrOpt.transition); + for (let i = 0; i < transitionKeys.length; i++) { + const key = transitionKeys[i]; + const elVal = elPropsInAttr[key]; + if (__DEV__) { + checkNonStyleTansitionRefer(key, (attrOpt as any)[key], elVal); + } + // Do not clone, see `checkNonStyleTansitionRefer`. + transFromPropsInAttr[key] = elVal; + } + } + else if (indexOf(elOption.transition, mainAttr) >= 0) { + !transFromPropsInAttr && (transFromPropsInAttr = transFromProps[mainAttr] = {}); + const elPropsInAttrKeys = keys(elPropsInAttr); + for (let i = 0; i < elPropsInAttrKeys.length; i++) { + const key = elPropsInAttrKeys[i]; + const elVal = elPropsInAttr[key]; + if (isNonStyleTransitionEnabled((attrOpt as any)[key], elVal)) { + transFromPropsInAttr[key] = elVal; + } + } + } + } + + const leaveTo = attrOpt.leaveTo; + if (leaveTo) { + const leaveToProps = getOrCreateLeaveToPropsFromEl(fromEl); + const leaveToPropsInAttr: Dictionary = leaveToProps[mainAttr] || (leaveToProps[mainAttr] = {}); + const leaveToKeys = keys(leaveTo); + for (let i = 0; i < leaveToKeys.length; i++) { + const key = leaveToKeys[i]; + leaveToPropsInAttr[key] = leaveTo[key]; + } + } +} + +export function prepareShapeOrExtraAllPropsFinal( + mainAttr: 'shape' | 'extra', + elOption: CustomElementOption, + allProps: LooseElementProps +): void { + const attrOpt: Dictionary & TransitionAnyOption = (elOption as any)[mainAttr]; + if (!attrOpt) { + return; + } + const allPropsInAttr = allProps[mainAttr] = {} as Dictionary; + const keysInAttr = keys(attrOpt); + for (let i = 0; i < keysInAttr.length; i++) { + const key = keysInAttr[i]; + // To avoid share one object with different element, and + // to avoid user modify the object inexpectedly, have to clone. + allPropsInAttr[key] = cloneValue((attrOpt as any)[key]); + } +} + +// See [STRATEGY_TRANSITION]. +export function prepareTransformTransitionFrom( + el: Element, + elOption: CustomElementOption, + transFromProps: ElementProps, + isInit: boolean +): void { + const enterFrom = elOption.enterFrom; + if (isInit && enterFrom) { + const enterFromKeys = keys(enterFrom); + for (let i = 0; i < enterFromKeys.length; i++) { + const key = enterFromKeys[i] as TransformProp; + if (__DEV__) { + checkTransformPropRefer(key, 'el.enterFrom'); + } + // Do not clone, animator will perform that clone. + transFromProps[key] = enterFrom[key] as number; + } + } + + if (!isInit) { + if (elOption.transition) { + const transitionKeys = normalizeToArray(elOption.transition); + for (let i = 0; i < transitionKeys.length; i++) { + const key = transitionKeys[i]; + if (key === 'style' || key === 'shape' || key === 'extra') { + continue; + } + const elVal = el[key]; + if (__DEV__) { + checkTransformPropRefer(key, 'el.transition'); + checkNonStyleTansitionRefer(key, elOption[key], elVal); + } + // Do not clone, see `checkNonStyleTansitionRefer`. + transFromProps[key] = elVal; + } + } + // This default transition see [STRATEGY_TRANSITION] + else { + setTransformPropToTransitionFrom(transFromProps, 'x', el); + setTransformPropToTransitionFrom(transFromProps, 'y', el); + } + } + + const leaveTo = elOption.leaveTo; + if (leaveTo) { + const leaveToProps = getOrCreateLeaveToPropsFromEl(el); + const leaveToKeys = keys(leaveTo); + for (let i = 0; i < leaveToKeys.length; i++) { + const key = leaveToKeys[i] as TransformProp; + if (__DEV__) { + checkTransformPropRefer(key, 'el.leaveTo'); + } + leaveToProps[key] = leaveTo[key] as number; + } + } +} + +export function prepareTransformAllPropsFinal( + el: Element, + elOption: CustomElementOption, + allProps: ElementProps +): void { + setLegacyTransformProp(elOption, allProps, 'position'); + setLegacyTransformProp(elOption, allProps, 'scale'); + setLegacyTransformProp(elOption, allProps, 'origin'); + + setTransformProp(elOption, allProps, 'x'); + setTransformProp(elOption, allProps, 'y'); + setTransformProp(elOption, allProps, 'scaleX'); + setTransformProp(elOption, allProps, 'scaleY'); + setTransformProp(elOption, allProps, 'originX'); + setTransformProp(elOption, allProps, 'originY'); + setTransformProp(elOption, allProps, 'rotation'); +} + +// See [STRATEGY_TRANSITION]. +export function prepareStyleTransitionFrom( + fromEl: Element, + elOption: CustomElementOption, + styleOpt: CustomDisplayableOption['style'], + transFromProps: LooseElementProps, + isInit: boolean +): void { + if (!styleOpt) { + return; + } + + const fromElStyle = (fromEl as LooseElementProps).style as LooseElementProps['style']; + let transFromStyleProps: LooseElementProps['style']; + + const enterFrom = styleOpt.enterFrom; + if (isInit && enterFrom) { + const enterFromKeys = keys(enterFrom); + !transFromStyleProps && (transFromStyleProps = transFromProps.style = {}); + for (let i = 0; i < enterFromKeys.length; i++) { + const key = enterFromKeys[i]; + // Do not clone, animator will perform that clone. + (transFromStyleProps as any)[key] = enterFrom[key]; + } + } + + if (!isInit && fromElStyle) { + if (styleOpt.transition) { + const transitionKeys = normalizeToArray(styleOpt.transition); + !transFromStyleProps && (transFromStyleProps = transFromProps.style = {}); + for (let i = 0; i < transitionKeys.length; i++) { + const key = transitionKeys[i]; + const elVal = (fromElStyle as any)[key]; + // Do not clone, see `checkNonStyleTansitionRefer`. + (transFromStyleProps as any)[key] = elVal; + } + } + else if ( + (fromEl as Displayable).getAnimationStyleProps + && indexOf(elOption.transition, 'style') >= 0 + ) { + const animationProps = (fromEl as Displayable).getAnimationStyleProps(); + const animationStyleProps = animationProps ? animationProps.style : null; + if (animationStyleProps) { + !transFromStyleProps && (transFromStyleProps = transFromProps.style = {}); + const styleKeys = keys(styleOpt); + for (let i = 0; i < styleKeys.length; i++) { + const key = styleKeys[i]; + if ((animationStyleProps as Dictionary)[key]) { + const elVal = (fromElStyle as any)[key]; + (transFromStyleProps as any)[key] = elVal; + } + } + } + } + } + + const leaveTo = styleOpt.leaveTo; + if (leaveTo) { + const leaveToKeys = keys(leaveTo); + const leaveToProps = getOrCreateLeaveToPropsFromEl(fromEl); + const leaveToStyleProps = leaveToProps.style || (leaveToProps.style = {}); + for (let i = 0; i < leaveToKeys.length; i++) { + const key = leaveToKeys[i]; + (leaveToStyleProps as any)[key] = leaveTo[key]; + } + } +} + +let checkNonStyleTansitionRefer: (propName: string, optVal: unknown, elVal: unknown) => void; +if (__DEV__) { + checkNonStyleTansitionRefer = function (propName: string, optVal: unknown, elVal: unknown): void { + if (!isArrayLike(optVal)) { + assert( + optVal != null && isFinite(optVal as number), + 'Prop `' + propName + '` must refer to a finite number or ArrayLike for transition.' + ); + } + else { + // Try not to copy array for performance, but if user use the same object in different + // call of `renderItem`, it will casue animation transition fail. + assert( + optVal !== elVal, + 'Prop `' + propName + '` must use different Array object each time for transition.' + ); + } + }; +} + +function isNonStyleTransitionEnabled(optVal: unknown, elVal: unknown): boolean { + // The same as `checkNonStyleTansitionRefer`. + return !isArrayLike(optVal) + ? (optVal != null && isFinite(optVal as number)) + : optVal !== elVal; +} + +let checkTransformPropRefer: (key: string, usedIn: string) => void; +if (__DEV__) { + checkTransformPropRefer = function (key: string, usedIn: string): void { + assert( + hasOwn(TRANSFORM_PROPS, key), + 'Prop `' + key + '` is not a permitted in `' + usedIn + '`. ' + + 'Only `' + keys(TRANSFORM_PROPS).join('`, `') + '` are permitted.' + ); + }; +} + +function getOrCreateLeaveToPropsFromEl(el: Element): LooseElementProps { + const innerEl = customInnerStore(el); + return innerEl.leaveToProps || (innerEl.leaveToProps = {}); +} + diff --git a/src/chart/effectScatter/EffectScatterSeries.ts b/src/chart/effectScatter/EffectScatterSeries.ts index e6f88303c6..9632e400bf 100644 --- a/src/chart/effectScatter/EffectScatterSeries.ts +++ b/src/chart/effectScatter/EffectScatterSeries.ts @@ -17,7 +17,7 @@ * under the License. */ -import createListFromArray from '../helper/createListFromArray'; +import createSeriesData from '../helper/createSeriesData'; import SeriesModel from '../../model/Series'; import { SeriesOption, @@ -35,7 +35,7 @@ import { CallbackDataParams } from '../../util/types'; import GlobalModel from '../../model/Global'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import type { SymbolDrawItemModelOption } from '../helper/SymbolDraw'; import { BrushCommonSelectorsForSeries } from '../../component/brush/selector'; @@ -88,11 +88,11 @@ class EffectScatterSeriesModel extends SeriesModel { hasSymbolVisual = true; - getInitialData(option: EffectScatterSeriesOption, ecModel: GlobalModel): List { - return createListFromArray(this.getSource(), this, {useEncodeDefaulter: true}); + getInitialData(option: EffectScatterSeriesOption, ecModel: GlobalModel): SeriesData { + return createSeriesData(null, this, {useEncodeDefaulter: true}); } - brushSelector(dataIndex: number, data: List, selectors: BrushCommonSelectorsForSeries): boolean { + brushSelector(dataIndex: number, data: SeriesData, selectors: BrushCommonSelectorsForSeries): boolean { return selectors.point(data.getItemLayout(dataIndex)); } @@ -116,9 +116,14 @@ class EffectScatterSeriesModel extends SeriesModel { // Scale of ripple scale: 2.5, // Brush type can be fill or stroke - brushType: 'fill' + brushType: 'fill', + // Ripple number + number: 3 }, + universalTransition: { + divideShape: 'clone' + }, // Cartesian coordinate system // xAxisIndex: 0, // yAxisIndex: 0, @@ -139,4 +144,4 @@ class EffectScatterSeriesModel extends SeriesModel { }; } -export default EffectScatterSeriesModel; \ No newline at end of file +export default EffectScatterSeriesModel; diff --git a/src/chart/funnel/FunnelSeries.ts b/src/chart/funnel/FunnelSeries.ts index 96a77391ef..6abe848f0a 100644 --- a/src/chart/funnel/FunnelSeries.ts +++ b/src/chart/funnel/FunnelSeries.ts @@ -18,7 +18,7 @@ */ import * as zrUtil from 'zrender/src/core/util'; -import createListSimply from '../helper/createListSimply'; +import createSeriesDataSimply from '../helper/createSeriesDataSimply'; import {defaultEmphasis} from '../../util/model'; import {makeSeriesEncodeForNameBased} from '../../data/helper/sourceHelper'; import LegendVisualProvider from '../../visual/LegendVisualProvider'; @@ -39,7 +39,7 @@ import { SeriesEncodeOptionMixin } from '../../util/types'; import GlobalModel from '../../model/Global'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; type FunnelLabelOption = Omit & { position?: LabelOption['position'] @@ -90,8 +90,6 @@ class FunnelSeriesModel extends SeriesModel { static type = 'series.funnel' as const; type = FunnelSeriesModel.type; - useColorPaletteOnData = true; - init(option: FunnelSeriesOption) { super.init.apply(this, arguments as any); @@ -104,8 +102,8 @@ class FunnelSeriesModel extends SeriesModel { this._defaultLabelLine(option); } - getInitialData(this: FunnelSeriesModel, option: FunnelSeriesOption, ecModel: GlobalModel): List { - return createListSimply(this, { + getInitialData(this: FunnelSeriesModel, option: FunnelSeriesOption, ecModel: GlobalModel): SeriesData { + return createSeriesDataSimply(this, { coordDimensions: ['value'], encodeDefaulter: zrUtil.curry(makeSeriesEncodeForNameBased, this) }); @@ -141,6 +139,7 @@ class FunnelSeriesModel extends SeriesModel { zlevel: 0, // 一级层叠 z: 2, // 二级层叠 legendHoverLink: true, + colorBy: 'data', left: 80, top: 60, right: 80, diff --git a/src/chart/funnel/FunnelView.ts b/src/chart/funnel/FunnelView.ts index 77a1aa7a29..e379e64f34 100644 --- a/src/chart/funnel/FunnelView.ts +++ b/src/chart/funnel/FunnelView.ts @@ -23,10 +23,11 @@ import ChartView from '../../view/Chart'; import FunnelSeriesModel, {FunnelDataItemOption} from './FunnelSeries'; import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../core/ExtensionAPI'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import { ColorString } from '../../util/types'; import { setLabelLineStyle, getLabelLineStatesModels } from '../../label/labelGuideHelper'; import { setLabelStyle, getLabelStatesModels } from '../../label/labelStyle'; +import { saveOldStyle } from '../../animation/basicTrasition'; const opacityAccessPath = ['itemStyle', 'opacity'] as const; @@ -35,7 +36,7 @@ const opacityAccessPath = ['itemStyle', 'opacity'] as const; */ class FunnelPiece extends graphic.Polygon { - constructor(data: List, idx: number) { + constructor(data: SeriesData, idx: number) { super(); const polygon = this; @@ -47,7 +48,7 @@ class FunnelPiece extends graphic.Polygon { this.updateData(data, idx, true); } - updateData(data: List, idx: number, firstCreate?: boolean) { + updateData(data: SeriesData, idx: number, firstCreate?: boolean) { const polygon = this; @@ -58,7 +59,9 @@ class FunnelPiece extends graphic.Polygon { let opacity = itemModel.get(opacityAccessPath); opacity = opacity == null ? 1 : opacity; - + if (!firstCreate) { + saveOldStyle(polygon); + } // Update common style polygon.useStyle(data.getItemVisual(idx, 'style')); polygon.style.lineJoin = 'round'; @@ -92,7 +95,7 @@ class FunnelPiece extends graphic.Polygon { enableHoverEmphasis(this, emphasisModel.get('focus'), emphasisModel.get('blurScope')); } - _updateLabel(data: List, idx: number) { + _updateLabel(data: SeriesData, idx: number) { const polygon = this; const labelLine = this.getTextGuideLine(); const labelText = polygon.getTextContent(); @@ -165,7 +168,7 @@ class FunnelView extends ChartView { static type = 'funnel' as const; type = FunnelView.type; - private _data: List; + private _data: SeriesData; ignoreLabelLineUpdate = true; diff --git a/src/chart/funnel/funnelLayout.ts b/src/chart/funnel/funnelLayout.ts index 71111c3804..cb11676b2d 100644 --- a/src/chart/funnel/funnelLayout.ts +++ b/src/chart/funnel/funnelLayout.ts @@ -21,7 +21,7 @@ import * as layout from '../../util/layout'; import {parsePercent, linearMap} from '../../util/number'; import FunnelSeriesModel, { FunnelSeriesOption, FunnelDataItemOption } from './FunnelSeries'; import ExtensionAPI from '../../core/ExtensionAPI'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import GlobalModel from '../../model/Global'; function getViewRect(seriesModel: FunnelSeriesModel, api: ExtensionAPI) { @@ -33,7 +33,7 @@ function getViewRect(seriesModel: FunnelSeriesModel, api: ExtensionAPI) { ); } -function getSortedIndices(data: List, sort: FunnelSeriesOption['sort']) { +function getSortedIndices(data: SeriesData, sort: FunnelSeriesOption['sort']) { const valueDim = data.mapDimension('value'); const valueArr = data.mapArray(valueDim, function (val: number) { return val; @@ -58,7 +58,7 @@ function getSortedIndices(data: List, sort: FunnelSeriesOption['sort']) { return indices; } -function labelLayout(data: List) { +function labelLayout(data: SeriesData) { const seriesModel = data.hostModel; const orient = seriesModel.get('orient'); data.each(function (idx) { diff --git a/src/chart/gauge/GaugeSeries.ts b/src/chart/gauge/GaugeSeries.ts index 64a694070f..729af9463d 100644 --- a/src/chart/gauge/GaugeSeries.ts +++ b/src/chart/gauge/GaugeSeries.ts @@ -17,7 +17,7 @@ * under the License. */ -import createListSimply from '../helper/createListSimply'; +import createSeriesDataSimply from '../helper/createSeriesDataSimply'; import SeriesModel from '../../model/Series'; import { SeriesOption, @@ -31,7 +31,7 @@ import { SeriesEncodeOptionMixin } from '../../util/types'; import GlobalModel from '../../model/Global'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; // [percent, color] type GaugeColorStop = [number, ColorString]; @@ -43,6 +43,10 @@ interface LabelFormatter { interface PointerOption { icon?: string show?: boolean + /** + * If pointer shows above title and detail + */ + showAbove?: boolean, keepAspect?: boolean itemStyle?: ItemStyleOption /** @@ -132,7 +136,7 @@ export interface GaugeSeriesOption extends SeriesOption, Gauge show?: boolean roundCap?: boolean lineStyle?: Omit & { - color: GaugeColorStop[] + color?: GaugeColorStop[] } }, @@ -178,15 +182,15 @@ class GaugeSeriesModel extends SeriesModel { type = GaugeSeriesModel.type; visualStyleAccessPath = 'itemStyle'; - useColorPaletteOnData = true; - getInitialData(option: GaugeSeriesOption, ecModel: GlobalModel): List { - return createListSimply(this, ['value']); + getInitialData(option: GaugeSeriesOption, ecModel: GlobalModel): SeriesData { + return createSeriesDataSimply(this, ['value']); } static defaultOption: GaugeSeriesOption = { zlevel: 0, z: 2, + colorBy: 'data', // 默认全局居中 center: ['50%', '50%'], legendHoverLink: true, @@ -260,6 +264,7 @@ class GaugeSeriesModel extends SeriesModel { icon: null, offsetCenter: [0, 0], show: true, + showAbove: true, length: '60%', width: 6, keepAspect: false diff --git a/src/chart/gauge/GaugeView.ts b/src/chart/gauge/GaugeView.ts index b005b53f15..d81e10ba59 100644 --- a/src/chart/gauge/GaugeView.ts +++ b/src/chart/gauge/GaugeView.ts @@ -27,11 +27,12 @@ import GaugeSeriesModel, { GaugeDataItemOption } from './GaugeSeries'; import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../core/ExtensionAPI'; import { ColorString, ECElement } from '../../util/types'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import Sausage from '../../util/shape/sausage'; import {createSymbol} from '../../util/symbol'; import ZRImage from 'zrender/src/graphic/Image'; import {extend} from 'zrender/src/core/util'; +import {setCommonECData} from '../../util/innerStore'; type ECSymbol = ReturnType; @@ -77,7 +78,7 @@ class GaugeView extends ChartView { static type = 'gauge' as const; type = GaugeView.type; - private _data: List; + private _data: SeriesData; private _progressEls: graphic.Path[]; private _titleEls: graphic.Text[]; @@ -439,6 +440,9 @@ class GaugeView extends ChartView { } }, seriesModel); group.add(progress); + // Add data index and series index for indexing the data by element + // Useful in tooltip + setCommonECData(seriesModel.seriesIndex, data.dataType, idx, progress); progressList[idx] = progress; } }) @@ -447,15 +451,15 @@ class GaugeView extends ChartView { const previousPointer = oldData.getItemGraphicEl(oldIdx) as PointerPath; const previousRotate = previousPointer ? previousPointer.rotation : startAngle; const pointer = createPointer(newIdx, previousRotate); - pointer.rotation = previousRotate; - graphic.updateProps(pointer, { - rotation: -( - linearMap(data.get(valueDim, newIdx) as number, valueExtent, angleExtent, true) - + Math.PI / 2 - ) - }, seriesModel); - group.add(pointer); - data.setItemGraphicEl(newIdx, pointer); + pointer.rotation = previousRotate; + graphic.updateProps(pointer, { + rotation: -( + linearMap(data.get(valueDim, newIdx) as number, valueExtent, angleExtent, true) + + Math.PI / 2 + ) + }, seriesModel); + group.add(pointer); + data.setItemGraphicEl(newIdx, pointer); } if (showProgress) { @@ -471,6 +475,9 @@ class GaugeView extends ChartView { } }, seriesModel); group.add(progress); + // Add data index and series index for indexing the data by element + // Useful in tooltip + setCommonECData(seriesModel.seriesIndex, data.dataType, newIdx, progress); progressList[newIdx] = progress; } }) @@ -568,6 +575,8 @@ class GaugeView extends ChartView { const newDetailEls: graphic.Text[] = []; const hasAnimation = seriesModel.isAnimationEnabled(); + const showPointerAbove = seriesModel.get(['pointer', 'showAbove']); + data.diff(this._data) .add((idx) => { newTitleEls[idx] = new graphic.Text({ @@ -598,6 +607,7 @@ class GaugeView extends ChartView { const titleY = posInfo.cy + parsePercent(titleOffsetCenter[1], posInfo.r); const labelEl = newTitleEls[idx]; labelEl.attr({ + z2: showPointerAbove ? 0 : 2, style: createTextStyle(itemTitleModel, { x: titleX, y: titleY, @@ -623,6 +633,7 @@ class GaugeView extends ChartView { const labelEl = newDetailEls[idx]; const formatter = itemDetailModel.get('formatter'); labelEl.attr({ + z2: showPointerAbove ? 0 : 2, style: createTextStyle(itemDetailModel, { x: detailX, y: detailY, diff --git a/src/chart/graph/GraphSeries.ts b/src/chart/graph/GraphSeries.ts index 54f9f36e0e..42b833d9c9 100644 --- a/src/chart/graph/GraphSeries.ts +++ b/src/chart/graph/GraphSeries.ts @@ -17,7 +17,7 @@ * under the License. */ -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import * as zrUtil from 'zrender/src/core/util'; import {defaultEmphasis} from '../../util/model'; import Model from '../../model/Model'; @@ -230,7 +230,7 @@ class GraphSeriesModel extends SeriesModel { static readonly dependencies = ['grid', 'polar', 'geo', 'singleAxis', 'calendar']; - private _categoriesData: List; + private _categoriesData: SeriesData; private _categoriesModels: Model[]; /** @@ -272,7 +272,7 @@ class GraphSeriesModel extends SeriesModel { defaultEmphasis(option, 'edgeLabel', ['show']); } - getInitialData(option: GraphSeriesOption, ecModel: GlobalModel): List { + getInitialData(option: GraphSeriesOption, ecModel: GlobalModel): SeriesData { const edges = option.edges || option.links || []; const nodes = option.data || option.nodes || []; const self = this; @@ -287,7 +287,7 @@ class GraphSeriesModel extends SeriesModel { return graph.data; } - function beforeLink(nodeData: List, edgeData: List) { + function beforeLink(nodeData: SeriesData, edgeData: SeriesData) { // Overwrite nodeData.getItemModel to nodeData.wrapMethod('getItemModel', function (model) { const categoriesModels = self._categoriesModels; @@ -335,10 +335,10 @@ class GraphSeriesModel extends SeriesModel { } getEdgeData() { - return this.getGraph().edgeData as List; + return this.getGraph().edgeData as SeriesData; } - getCategoriesData(): List { + getCategoriesData(): SeriesData { return this._categoriesData; } @@ -380,7 +380,7 @@ class GraphSeriesModel extends SeriesModel { value: 0 }, category); }); - const categoriesData = new List(['value'], this); + const categoriesData = new SeriesData(['value'], this); categoriesData.initData(categories); this._categoriesData = categoriesData; diff --git a/src/chart/graph/GraphView.ts b/src/chart/graph/GraphView.ts index 1c8d10f921..08cc6ae46f 100644 --- a/src/chart/graph/GraphView.ts +++ b/src/chart/graph/GraphView.ts @@ -33,7 +33,7 @@ import GraphSeriesModel, { GraphNodeItemOption, GraphEdgeItemOption } from './Gr import { CoordinateSystem } from '../../coord/CoordinateSystem'; import View from '../../coord/View'; import Symbol from '../helper/Symbol'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import Line from '../helper/Line'; import { getECData } from '../../util/innerStore'; @@ -109,7 +109,7 @@ class GraphView extends ChartView { const edgeData = seriesModel.getEdgeData(); // TODO: TYPE - lineDraw.updateData(edgeData as List); + lineDraw.updateData(edgeData as SeriesData); this._updateNodeAndLinkScale(); diff --git a/src/chart/graph/circularLayoutHelper.ts b/src/chart/graph/circularLayoutHelper.ts index a81538c377..91265bea62 100644 --- a/src/chart/graph/circularLayoutHelper.ts +++ b/src/chart/graph/circularLayoutHelper.ts @@ -22,7 +22,7 @@ import * as vec2 from 'zrender/src/core/vector'; import {getSymbolSize, getNodeGlobalScale} from './graphHelper'; import GraphSeriesModel, { GraphEdgeItemOption } from './GraphSeries'; import Graph from '../../data/Graph'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import * as zrUtil from 'zrender/src/core/util'; import {getCurvenessForEdge} from '../helper/multipleGraphEdgeHelper'; @@ -105,7 +105,7 @@ interface LayoutNode { ( seriesModel: GraphSeriesModel, graph: Graph, - nodeData: List, + nodeData: SeriesData, r: number, cx: number, cy: number, diff --git a/src/chart/heatmap/HeatmapSeries.ts b/src/chart/heatmap/HeatmapSeries.ts index af831ef135..80fbb72dd4 100644 --- a/src/chart/heatmap/HeatmapSeries.ts +++ b/src/chart/heatmap/HeatmapSeries.ts @@ -18,7 +18,7 @@ */ import SeriesModel from '../../model/Series'; -import createListFromArray from '../helper/createListFromArray'; +import createSeriesData from '../helper/createSeriesData'; import CoordinateSystem from '../../core/CoordinateSystem'; import { SeriesOption, @@ -32,7 +32,7 @@ import { SeriesOnCalendarOptionMixin } from '../../util/types'; import GlobalModel from '../../model/Global'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import type Geo from '../../coord/geo/Geo'; import type Cartesian2D from '../../coord/cartesian/Cartesian2D'; import type Calendar from '../../coord/calendar/Calendar'; @@ -72,8 +72,8 @@ class HeatmapSeriesModel extends SeriesModel { // @ts-ignore coordinateSystem: Cartesian2D | Geo | Calendar; - getInitialData(option: HeatmapSeriesOption, ecModel: GlobalModel): List { - return createListFromArray(this.getSource(), this, { + getInitialData(option: HeatmapSeriesOption, ecModel: GlobalModel): SeriesData { + return createSeriesData(null, this, { generateCoord: 'value' }); } diff --git a/src/chart/helper/EffectLine.ts b/src/chart/helper/EffectLine.ts index d269faad9d..bc92620754 100644 --- a/src/chart/helper/EffectLine.ts +++ b/src/chart/helper/EffectLine.ts @@ -27,7 +27,7 @@ import * as zrUtil from 'zrender/src/core/util'; import {createSymbol} from '../../util/symbol'; import * as vec2 from 'zrender/src/core/vector'; import * as curveUtil from 'zrender/src/core/curve'; -import type List from '../../data/List'; +import type SeriesData from '../../data/SeriesData'; import { LineDrawSeriesScope, LineDrawModelOption } from './LineDraw'; import Model from '../../model/Model'; import { ColorString } from '../../util/types'; @@ -49,18 +49,18 @@ class EffectLine extends graphic.Group { private _symbolScale: number[]; - constructor(lineData: List, idx: number, seriesScope: LineDrawSeriesScope) { + constructor(lineData: SeriesData, idx: number, seriesScope: LineDrawSeriesScope) { super(); this.add(this.createLine(lineData, idx, seriesScope)); this._updateEffectSymbol(lineData, idx); } - createLine(lineData: List, idx: number, seriesScope: LineDrawSeriesScope): graphic.Group { + createLine(lineData: SeriesData, idx: number, seriesScope: LineDrawSeriesScope): graphic.Group { return new Line(lineData, idx, seriesScope); } - private _updateEffectSymbol(lineData: List, idx: number) { + private _updateEffectSymbol(lineData: SeriesData, idx: number) { const itemModel = lineData.getItemModel(idx); const effectModel = itemModel.getModel('effect'); let size = effectModel.get('symbolSize'); @@ -107,7 +107,7 @@ class EffectLine extends graphic.Group { } private _updateEffectAnimation( - lineData: List, + lineData: SeriesData, effectModel: Model, idx: number ) { @@ -189,7 +189,7 @@ class EffectLine extends graphic.Group { ]; } - updateData(lineData: List, idx: number, seriesScope: LineDrawSeriesScope) { + updateData(lineData: SeriesData, idx: number, seriesScope: LineDrawSeriesScope) { (this.childAt(0) as Line).updateData(lineData, idx, seriesScope); this._updateEffectSymbol(lineData, idx); } @@ -236,7 +236,7 @@ class EffectLine extends graphic.Group { } - updateLayout(lineData: List, idx: number) { + updateLayout(lineData: SeriesData, idx: number) { (this.childAt(0) as Line).updateLayout(lineData, idx); const effectModel = lineData.getItemModel(idx).getModel('effect'); diff --git a/src/chart/helper/EffectPolyline.ts b/src/chart/helper/EffectPolyline.ts index 80499750c6..a250cb590a 100644 --- a/src/chart/helper/EffectPolyline.ts +++ b/src/chart/helper/EffectPolyline.ts @@ -21,7 +21,7 @@ import Polyline from './Polyline'; import EffectLine, {ECSymbolOnEffectLine} from './EffectLine'; import * as vec2 from 'zrender/src/core/vector'; import { LineDrawSeriesScope } from './LineDraw'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; class EffectPolyline extends EffectLine { @@ -33,7 +33,7 @@ class EffectPolyline extends EffectLine { private _offsets: number[]; // Override - createLine(lineData: List, idx: number, seriesScope: LineDrawSeriesScope) { + createLine(lineData: SeriesData, idx: number, seriesScope: LineDrawSeriesScope) { return new Polyline(lineData, idx, seriesScope); }; diff --git a/src/chart/helper/EffectSymbol.ts b/src/chart/helper/EffectSymbol.ts index 31c6179473..d36a6ec4d8 100644 --- a/src/chart/helper/EffectSymbol.ts +++ b/src/chart/helper/EffectSymbol.ts @@ -17,19 +17,15 @@ * under the License. */ -import * as zrUtil from 'zrender/src/core/util'; -import {createSymbol} from '../../util/symbol'; +import {createSymbol, normalizeSymbolOffset, normalizeSymbolSize} from '../../util/symbol'; import {Group, Path} from '../../util/graphic'; import { enterEmphasis, leaveEmphasis, enableHoverEmphasis } from '../../util/states'; -import {parsePercent} from '../../util/number'; import SymbolClz from './Symbol'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import type { ZRColor, ECElement } from '../../util/types'; import type Displayable from 'zrender/src/graphic/Displayable'; import { SymbolDrawItemModelOption } from './SymbolDraw'; -const EFFECT_RIPPLE_NUMBER = 3; - interface RippleEffectCfg { showEffectOn?: 'emphasis' | 'render' rippleScale?: number @@ -40,14 +36,8 @@ interface RippleEffectCfg { zlevel?: number symbolType?: string color?: ZRColor - rippleEffectColor?: ZRColor -} - -function normalizeSymbolSize(symbolSize: number | number[]): number[] { - if (!zrUtil.isArray(symbolSize)) { - symbolSize = [+symbolSize, +symbolSize]; - } - return symbolSize; + rippleEffectColor?: ZRColor, + rippleNumber?: number } function updateRipplePath(rippleGroup: Group, effectCfg: RippleEffectCfg) { @@ -68,7 +58,7 @@ class EffectSymbol extends Group { private _effectCfg: RippleEffectCfg; - constructor(data: List, idx: number) { + constructor(data: SeriesData, idx: number) { super(); const symbol = new SymbolClz(data, idx); @@ -87,9 +77,10 @@ class EffectSymbol extends Group { startEffectAnimation(effectCfg: RippleEffectCfg) { const symbolType = effectCfg.symbolType; const color = effectCfg.color; + const rippleNumber = effectCfg.rippleNumber; const rippleGroup = this.childAt(1) as Group; - for (let i = 0; i < EFFECT_RIPPLE_NUMBER; i++) { + for (let i = 0; i < rippleNumber; i++) { // If width/height are set too small (e.g., set to 1) on ios10 // and macOS Sierra, a circle stroke become a rect, no matter what // the scale is set. So we set width/height as 2. See #4136. @@ -106,8 +97,7 @@ class EffectSymbol extends Group { scaleY: 0.5 }); - const delay = -i / EFFECT_RIPPLE_NUMBER * effectCfg.period + effectCfg.effectOffset; - // TODO Configurable effectCfg.period + const delay = -i / rippleNumber * effectCfg.period + effectCfg.effectOffset; ripplePath.animate('', true) .when(effectCfg.period, { scaleX: effectCfg.rippleScale / 2, @@ -136,7 +126,7 @@ class EffectSymbol extends Group { const rippleGroup = this.childAt(1) as Group; // Must reinitialize effect if following configuration changed - const DIFFICULT_PROPS = ['symbolType', 'period', 'rippleScale'] as const; + const DIFFICULT_PROPS = ['symbolType', 'period', 'rippleScale', 'rippleNumber'] as const; for (let i = 0; i < DIFFICULT_PROPS.length; i++) { const propName = DIFFICULT_PROPS[i]; if (oldEffectCfg[propName] !== effectCfg[propName]) { @@ -163,10 +153,15 @@ class EffectSymbol extends Group { leaveEmphasis(this); } + getSymbolType() { + const symbol = this.childAt(0) as SymbolClz; + return symbol && symbol.getSymbolType(); + } + /** * Update symbol properties */ - updateData(data: List, idx: number) { + updateData(data: SeriesData, idx: number) { const seriesModel = data.hostModel; (this.childAt(0) as SymbolClz).updateData(data, idx); @@ -185,13 +180,10 @@ class EffectSymbol extends Group { ripplePath.setStyle('fill', color); }); - let symbolOffset = data.getItemVisual(idx, 'symbolOffset'); + const symbolOffset = normalizeSymbolOffset(data.getItemVisual(idx, 'symbolOffset'), symbolSize); if (symbolOffset) { - if (!zrUtil.isArray(symbolOffset)) { - symbolOffset = [symbolOffset, symbolOffset]; - } - rippleGroup.x = parsePercent(symbolOffset[0], symbolSize[0]); - rippleGroup.y = parsePercent(zrUtil.retrieve2(symbolOffset[1], symbolOffset[0]) || 0, symbolSize[1]); + rippleGroup.x = symbolOffset[0]; + rippleGroup.y = symbolOffset[1]; } const symbolRotate = data.getItemVisual(idx, 'symbolRotate'); @@ -209,6 +201,7 @@ class EffectSymbol extends Group { effectCfg.symbolType = symbolType; effectCfg.color = color; effectCfg.rippleEffectColor = itemModel.get(['rippleEffect', 'color']); + effectCfg.rippleNumber = itemModel.get(['rippleEffect', 'number']); this.off('mouseover').off('mouseout').off('emphasis').off('normal'); diff --git a/src/chart/helper/LargeLineDraw.ts b/src/chart/helper/LargeLineDraw.ts index 50e4f8d0bd..fca6979fea 100644 --- a/src/chart/helper/LargeLineDraw.ts +++ b/src/chart/helper/LargeLineDraw.ts @@ -24,7 +24,7 @@ import IncrementalDisplayable from 'zrender/src/graphic/IncrementalDisplayable'; import * as lineContain from 'zrender/src/contain/line'; import * as quadraticContain from 'zrender/src/contain/quadratic'; import { PathProps } from 'zrender/src/graphic/Path'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import { StageHandlerProgressParams, LineStyleOption, ColorString } from '../../util/types'; import Model from '../../model/Model'; import { getECData } from '../../util/innerStore'; @@ -49,7 +49,7 @@ interface LargeLinesCommonOption { /** * Data which can support large lines. */ -type LargeLinesData = List & { +type LargeLinesData = SeriesData & { seriesIndex?: number }>; diff --git a/src/chart/helper/LargeSymbolDraw.ts b/src/chart/helper/LargeSymbolDraw.ts index 1840c54fb6..3f3bcbbac1 100644 --- a/src/chart/helper/LargeSymbolDraw.ts +++ b/src/chart/helper/LargeSymbolDraw.ts @@ -24,7 +24,7 @@ import * as graphic from '../../util/graphic'; import {createSymbol} from '../../util/symbol'; import IncrementalDisplayable from 'zrender/src/graphic/IncrementalDisplayable'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import { PathProps } from 'zrender/src/graphic/Path'; import PathProxy from 'zrender/src/core/PathProxy'; import SeriesModel from '../../model/Series'; @@ -182,7 +182,7 @@ class LargeSymbolDraw { /** * Update symbols draw by new data */ - updateData(data: List, opt?: UpdateOpt) { + updateData(data: SeriesData, opt?: UpdateOpt) { this.group.removeAll(); const symbolEl = new LargeSymbolPath({ rectHover: true, @@ -198,7 +198,7 @@ class LargeSymbolDraw { this._incremental = null; } - updateLayout(data: List) { + updateLayout(data: SeriesData) { if (this._incremental) { return; } @@ -214,7 +214,7 @@ class LargeSymbolDraw { }); } - incrementalPrepareUpdate(data: List) { + incrementalPrepareUpdate(data: SeriesData) { this.group.removeAll(); this._clearIncremental(); @@ -233,7 +233,7 @@ class LargeSymbolDraw { } } - incrementalUpdate(taskParams: StageHandlerProgressParams, data: List, opt: UpdateOpt) { + incrementalUpdate(taskParams: StageHandlerProgressParams, data: SeriesData, opt: UpdateOpt) { let symbolEl; if (this._incremental) { symbolEl = new LargeSymbolPath(); @@ -258,7 +258,7 @@ class LargeSymbolDraw { _setCommon( symbolEl: LargeSymbolPath, - data: List, + data: SeriesData, isIncremental: boolean, opt: UpdateOpt ) { diff --git a/src/chart/helper/Line.ts b/src/chart/helper/Line.ts index 4ec9df8db2..b806d0f7b8 100644 --- a/src/chart/helper/Line.ts +++ b/src/chart/helper/Line.ts @@ -17,19 +17,18 @@ * under the License. */ -import { isArray, each, retrieve2 } from 'zrender/src/core/util'; +import { isArray, each } from 'zrender/src/core/util'; import * as vector from 'zrender/src/core/vector'; import * as symbolUtil from '../../util/symbol'; import ECLinePath from './LinePath'; import * as graphic from '../../util/graphic'; import { enableHoverEmphasis, enterEmphasis, leaveEmphasis, SPECIAL_STATES } from '../../util/states'; import {getLabelStatesModels, setLabelStyle} from '../../label/labelStyle'; -import {round, parsePercent} from '../../util/number'; -import List from '../../data/List'; +import {round} from '../../util/number'; +import SeriesData from '../../data/SeriesData'; import { ZRTextAlign, ZRTextVerticalAlign, LineLabelOption, ColorString } from '../../util/types'; import SeriesModel from '../../model/Series'; import type { LineDrawSeriesScope, LineDrawModelOption } from './LineDraw'; - import { TextStyleProps } from 'zrender/src/graphic/Text'; import { LineDataVisual } from '../../visual/commonVisualTypes'; import Model from '../../model/Model'; @@ -42,7 +41,7 @@ type LineECSymbol = ECSymbol & { __specifiedRotation: number }; -type LineList = List; +type LineList = SeriesData; export interface LineLabel extends graphic.Text { lineLabelOriginalOpacity: number @@ -70,18 +69,13 @@ function createSymbol(name: 'fromSymbol' | 'toSymbol', lineData: LineList, idx: const symbolSize = lineData.getItemVisual(idx, name + 'Size' as 'fromSymbolSize' | 'toSymbolSize'); const symbolRotate = lineData.getItemVisual(idx, name + 'Rotate' as 'fromSymbolRotate' | 'toSymbolRotate'); - const symbolOffset = lineData.getItemVisual(idx, name + 'Offset' as 'fromSymbolOffset' | 'toSymbolOffset') || 0; + const symbolOffset = lineData.getItemVisual(idx, name + 'Offset' as 'fromSymbolOffset' | 'toSymbolOffset'); const symbolKeepAspect = lineData.getItemVisual(idx, name + 'KeepAspect' as 'fromSymbolKeepAspect' | 'toSymbolKeepAspect'); - const symbolSizeArr = isArray(symbolSize) - ? symbolSize : [symbolSize, symbolSize]; - - const symbolOffsetArr = isArray(symbolOffset) - ? symbolOffset : [symbolOffset, symbolOffset]; + const symbolSizeArr = symbolUtil.normalizeSymbolSize(symbolSize); - symbolOffsetArr[0] = parsePercent(symbolOffsetArr[0], symbolSizeArr[0]); - symbolOffsetArr[1] = parsePercent(retrieve2(symbolOffsetArr[1], symbolOffsetArr[0]), symbolSizeArr[1]); + const symbolOffsetArr = symbolUtil.normalizeSymbolOffset(symbolOffset || 0, symbolSizeArr); const symbolPath = symbolUtil.createSymbol( symbolType, @@ -139,7 +133,7 @@ class Line extends graphic.Group { private _fromSymbolType: string; private _toSymbolType: string; - constructor(lineData: List, idx: number, seriesScope?: LineDrawSeriesScope) { + constructor(lineData: SeriesData, idx: number, seriesScope?: LineDrawSeriesScope) { super(); this._createLine(lineData as LineList, idx, seriesScope); } @@ -170,7 +164,7 @@ class Line extends graphic.Group { } // TODO More strict on the List type in parameters? - updateData(lineData: List, idx: number, seriesScope: LineDrawSeriesScope) { + updateData(lineData: SeriesData, idx: number, seriesScope: LineDrawSeriesScope) { const seriesModel = lineData.hostModel; const line = this.childOfName('line') as ECLinePath; @@ -201,7 +195,7 @@ class Line extends graphic.Group { return this.childAt(0) as graphic.Line; } - _updateCommonStl(lineData: List, idx: number, seriesScope?: LineDrawSeriesScope) { + _updateCommonStl(lineData: SeriesData, idx: number, seriesScope?: LineDrawSeriesScope) { const seriesModel = lineData.hostModel as SeriesModel; const line = this.childOfName('line') as ECLinePath; @@ -313,7 +307,7 @@ class Line extends graphic.Group { leaveEmphasis(this); } - updateLayout(lineData: List, idx: number) { + updateLayout(lineData: SeriesData, idx: number) { this.setLinePoints(lineData.getItemLayout(idx)); } diff --git a/src/chart/helper/LineDraw.ts b/src/chart/helper/LineDraw.ts index 1dde5d39d4..60317bc7f1 100644 --- a/src/chart/helper/LineDraw.ts +++ b/src/chart/helper/LineDraw.ts @@ -19,7 +19,7 @@ import * as graphic from '../../util/graphic'; import LineGroup from './Line'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import { StageHandlerProgressParams, LineStyleOption, @@ -36,13 +36,13 @@ import Model from '../../model/Model'; import { getLabelStatesModels } from '../../label/labelStyle'; interface LineLike extends graphic.Group { - updateData(data: List, idx: number, scope?: LineDrawSeriesScope): void - updateLayout(data: List, idx: number): void + updateData(data: SeriesData, idx: number, scope?: LineDrawSeriesScope): void + updateLayout(data: SeriesData, idx: number): void fadeOut?(cb: () => void): void } interface LineLikeCtor { - new(data: List, idx: number, scope?: LineDrawSeriesScope): LineLike + new(data: SeriesData, idx: number, scope?: LineDrawSeriesScope): LineLike } interface LineDrawStateOption { @@ -76,7 +76,7 @@ export interface LineDrawModelOption extends LineDrawStateOption, StatesOptionMi } } -type ListForLineDraw = List>; +type ListForLineDraw = SeriesData>; export interface LineDrawSeriesScope { lineStyle?: ZRStyleProps diff --git a/src/chart/helper/Polyline.ts b/src/chart/helper/Polyline.ts index 29d0715312..a371edcfdd 100644 --- a/src/chart/helper/Polyline.ts +++ b/src/chart/helper/Polyline.ts @@ -20,15 +20,15 @@ import * as graphic from '../../util/graphic'; import { enableHoverEmphasis } from '../../util/states'; import type { LineDrawSeriesScope, LineDrawModelOption } from './LineDraw'; -import type List from '../../data/List'; +import type SeriesData from '../../data/SeriesData'; class Polyline extends graphic.Group { - constructor(lineData: List, idx: number, seriesScope: LineDrawSeriesScope) { + constructor(lineData: SeriesData, idx: number, seriesScope: LineDrawSeriesScope) { super(); this._createPolyline(lineData, idx, seriesScope); } - private _createPolyline(lineData: List, idx: number, seriesScope: LineDrawSeriesScope) { + private _createPolyline(lineData: SeriesData, idx: number, seriesScope: LineDrawSeriesScope) { // let seriesModel = lineData.hostModel; const points = lineData.getItemLayout(idx); @@ -43,7 +43,7 @@ class Polyline extends graphic.Group { this._updateCommonStl(lineData, idx, seriesScope); }; - updateData(lineData: List, idx: number, seriesScope: LineDrawSeriesScope) { + updateData(lineData: SeriesData, idx: number, seriesScope: LineDrawSeriesScope) { const seriesModel = lineData.hostModel; const line = this.childAt(0) as graphic.Polyline; @@ -57,7 +57,7 @@ class Polyline extends graphic.Group { this._updateCommonStl(lineData, idx, seriesScope); }; - _updateCommonStl(lineData: List, idx: number, seriesScope: LineDrawSeriesScope) { + _updateCommonStl(lineData: SeriesData, idx: number, seriesScope: LineDrawSeriesScope) { const line = this.childAt(0) as graphic.Polyline; const itemModel = lineData.getItemModel(idx); @@ -77,7 +77,7 @@ class Polyline extends graphic.Group { enableHoverEmphasis(this); }; - updateLayout(lineData: List, idx: number) { + updateLayout(lineData: SeriesData, idx: number) { const polyline = this.childAt(0) as graphic.Polyline; polyline.setShape('points', lineData.getItemLayout(idx)); }; diff --git a/src/chart/helper/Symbol.ts b/src/chart/helper/Symbol.ts index 15fc84c6cf..be2a5a4a2e 100644 --- a/src/chart/helper/Symbol.ts +++ b/src/chart/helper/Symbol.ts @@ -17,20 +17,20 @@ * under the License. */ -import {createSymbol} from '../../util/symbol'; +import {createSymbol, normalizeSymbolOffset, normalizeSymbolSize} from '../../util/symbol'; import * as graphic from '../../util/graphic'; import {getECData} from '../../util/innerStore'; import { enterEmphasis, leaveEmphasis, enableHoverEmphasis } from '../../util/states'; -import {parsePercent} from '../../util/number'; import {getDefaultLabel} from './labelHelper'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import { ColorString, BlurScope, AnimationOption, ZRColor } from '../../util/types'; import SeriesModel from '../../model/Series'; import { PathProps } from 'zrender/src/graphic/Path'; import { SymbolDrawSeriesScope, SymbolDrawItemModelOption } from './SymbolDraw'; -import { extend, isArray, retrieve2 } from 'zrender/src/core/util'; +import { extend } from 'zrender/src/core/util'; import { setLabelStyle, getLabelStatesModels } from '../../label/labelStyle'; import ZRImage from 'zrender/src/graphic/Image'; +import { saveOldStyle } from '../../animation/basicTrasition'; type ECSymbol = ReturnType; @@ -55,14 +55,14 @@ class Symbol extends graphic.Group { private _z2: number; - constructor(data: List, idx: number, seriesScope?: SymbolDrawSeriesScope, opts?: SymbolOpts) { + constructor(data: SeriesData, idx: number, seriesScope?: SymbolDrawSeriesScope, opts?: SymbolOpts) { super(); this.updateData(data, idx, seriesScope, opts); } _createSymbol( symbolType: string, - data: List, + data: SeriesData, idx: number, symbolSize: number[], keepAspect: boolean @@ -102,6 +102,9 @@ class Symbol extends graphic.Group { this.childAt(0).stopAnimation(null, toLastFrame); } + getSymbolType() { + return this._symbolType; + } /** * FIXME: * Caution: This method breaks the encapsulation of this module, @@ -148,7 +151,7 @@ class Symbol extends graphic.Group { /** * Update symbol properties */ - updateData(data: List, idx: number, seriesScope?: SymbolDrawSeriesScope, opts?: SymbolOpts) { + updateData(data: SeriesData, idx: number, seriesScope?: SymbolDrawSeriesScope, opts?: SymbolOpts) { this.silent = false; const symbolType = data.getItemVisual(idx, 'symbol') || 'circle'; @@ -170,6 +173,8 @@ class Symbol extends graphic.Group { }; disableAnimation ? symbolPath.attr(target) : graphic.updateProps(symbolPath, target, seriesModel, idx); + + saveOldStyle(symbolPath); } this._updateCommon(data, idx, symbolSize, seriesScope, opts); @@ -201,7 +206,7 @@ class Symbol extends graphic.Group { } _updateCommon( - data: List, + data: SeriesData, idx: number, symbolSize: number[], seriesScope?: SymbolDrawSeriesScope, @@ -255,13 +260,10 @@ class Symbol extends graphic.Group { const symbolRotate = data.getItemVisual(idx, 'symbolRotate'); symbolPath.attr('rotation', (symbolRotate || 0) * Math.PI / 180 || 0); - let symbolOffset = data.getItemVisual(idx, 'symbolOffset') || 0; + const symbolOffset = normalizeSymbolOffset(data.getItemVisual(idx, 'symbolOffset'), symbolSize); if (symbolOffset) { - if (!isArray(symbolOffset)) { - symbolOffset = [symbolOffset, symbolOffset]; - } - symbolPath.x = parsePercent(symbolOffset[0], symbolSize[0]); - symbolPath.y = parsePercent(retrieve2(symbolOffset[1], symbolOffset[0]) || 0, symbolSize[1]); + symbolPath.x = symbolOffset[0]; + symbolPath.y = symbolOffset[1]; } cursorStyle && symbolPath.attr('cursor', cursorStyle); @@ -393,11 +395,8 @@ class Symbol extends graphic.Group { ); } - static getSymbolSize(data: List, idx: number) { - const symbolSize = data.getItemVisual(idx, 'symbolSize'); - return isArray(symbolSize) - ? symbolSize.slice() - : [+symbolSize, +symbolSize]; + static getSymbolSize(data: SeriesData, idx: number) { + return normalizeSymbolSize(data.getItemVisual(idx, 'symbolSize')); } } @@ -406,5 +405,4 @@ function driftSymbol(this: ECSymbol, dx: number, dy: number) { this.parent.drift(dx, dy); } - export default Symbol; diff --git a/src/chart/helper/SymbolDraw.ts b/src/chart/helper/SymbolDraw.ts index 97c01a0829..43b1249fc9 100644 --- a/src/chart/helper/SymbolDraw.ts +++ b/src/chart/helper/SymbolDraw.ts @@ -20,7 +20,7 @@ import * as graphic from '../../util/graphic'; import SymbolClz from './Symbol'; import { isObject } from 'zrender/src/core/util'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import type Displayable from 'zrender/src/graphic/Displayable'; import { StageHandlerProgressParams, @@ -49,15 +49,15 @@ interface UpdateOpt { } interface SymbolLike extends graphic.Group { - updateData(data: List, idx: number, scope?: SymbolDrawSeriesScope, opt?: UpdateOpt): void + updateData(data: SeriesData, idx: number, scope?: SymbolDrawSeriesScope, opt?: UpdateOpt): void fadeOut?(cb: () => void): void } interface SymbolLikeCtor { - new(data: List, idx: number, scope?: SymbolDrawSeriesScope, opt?: UpdateOpt): SymbolLike + new(data: SeriesData, idx: number, scope?: SymbolDrawSeriesScope, opt?: UpdateOpt): SymbolLike } -function symbolNeedsDraw(data: List, point: number[], idx: number, opt: UpdateOpt) { +function symbolNeedsDraw(data: SeriesData, point: number[], idx: number, opt: UpdateOpt) { return point && !isNaN(point[0]) && !isNaN(point[1]) && !(opt.isIgnore && opt.isIgnore(idx)) // We do not set clipShape on group, because it will cut part of @@ -83,7 +83,12 @@ interface RippleEffectOption { brushType?: 'fill' | 'stroke' - color?: ZRColor + color?: ZRColor, + + /** + * ripple number + */ + number?: number } interface SymbolDrawStateOption { @@ -125,7 +130,7 @@ export interface SymbolDrawSeriesScope { fadeIn?: boolean } -function makeSeriesScope(data: List): SymbolDrawSeriesScope { +function makeSeriesScope(data: SeriesData): SymbolDrawSeriesScope { const seriesModel = data.hostModel as Model; const emphasisModel = seriesModel.getModel('emphasis'); return { @@ -144,7 +149,7 @@ function makeSeriesScope(data: List): SymbolDrawSeriesScope { }; } -export type ListForSymbolDraw = List>; +export type ListForSymbolDraw = SeriesData>; class SymbolDraw { group = new graphic.Group(); @@ -206,8 +211,17 @@ class SymbolDraw { group.remove(symbolEl); return; } - if (!symbolEl) { - symbolEl = new SymbolCtor(data, newIdx); + const newSymbolType = data.getItemVisual(newIdx, 'symbol') || 'circle'; + const oldSymbolType = symbolEl + && (symbolEl as SymbolClz).getSymbolType + && (symbolEl as SymbolClz).getSymbolType(); + + if (!symbolEl + // Create a new if symbol type changed. + || (oldSymbolType && oldSymbolType !== newSymbolType) + ) { + group.remove(symbolEl); + symbolEl = new SymbolCtor(data, newIdx, seriesScope, symbolUpdateOpt); symbolEl.setPosition(point); } else { diff --git a/src/chart/helper/createGraphFromNodeEdge.ts b/src/chart/helper/createGraphFromNodeEdge.ts index e2a0fca814..14eea23fd3 100644 --- a/src/chart/helper/createGraphFromNodeEdge.ts +++ b/src/chart/helper/createGraphFromNodeEdge.ts @@ -19,12 +19,12 @@ import * as zrUtil from 'zrender/src/core/util'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import Graph from '../../data/Graph'; -import linkList from '../../data/helper/linkList'; -import createDimensions from '../../data/helper/createDimensions'; +import linkSeriesData from '../../data/helper/linkSeriesData'; +import prepareSeriesDataSchema from '../../data/helper/createDimensions'; import CoordinateSystem from '../../core/CoordinateSystem'; -import createListFromArray from './createListFromArray'; +import createSeriesData from './createSeriesData'; import { OptionSourceDataOriginal, GraphEdgeItemObject, OptionDataValue, OptionDataItemObject @@ -37,7 +37,7 @@ export default function createGraphFromNodeEdge( edges: OptionSourceDataOriginal>, seriesModel: SeriesModel, directed: boolean, - beforeLink: (nodeData: List, edgeData: List) => void + beforeLink: (nodeData: SeriesData, edgeData: SeriesData) => void ): Graph { // ??? TODO // support dataset? @@ -70,7 +70,7 @@ export default function createGraphFromNodeEdge( const coordSys = seriesModel.get('coordinateSystem'); let nodeData; if (coordSys === 'cartesian2d' || coordSys === 'polar') { - nodeData = createListFromArray(nodes, seriesModel); + nodeData = createSeriesData(nodes, seriesModel); } else { const coordSysCtor = CoordinateSystem.get(coordSys); @@ -83,19 +83,20 @@ export default function createGraphFromNodeEdge( coordDimensions.concat(['value']); } - const dimensionNames = createDimensions(nodes, { - coordDimensions: coordDimensions + const { dimensions } = prepareSeriesDataSchema(nodes, { + coordDimensions: coordDimensions, + encodeDefine: seriesModel.getEncode() }); - nodeData = new List(dimensionNames, seriesModel); + nodeData = new SeriesData(dimensions, seriesModel); nodeData.initData(nodes); } - const edgeData = new List(['value'], seriesModel); + const edgeData = new SeriesData(['value'], seriesModel); edgeData.initData(validEdges, linkNameList); beforeLink && beforeLink(nodeData, edgeData); - linkList({ + linkSeriesData({ mainData: nodeData, struct: graph, structAttr: 'graph', diff --git a/src/chart/helper/createListFromArray.ts b/src/chart/helper/createSeriesData.ts similarity index 51% rename from src/chart/helper/createListFromArray.ts rename to src/chart/helper/createSeriesData.ts index a9adc2f209..704eed4c8a 100644 --- a/src/chart/helper/createListFromArray.ts +++ b/src/chart/helper/createSeriesData.ts @@ -18,37 +18,33 @@ */ import * as zrUtil from 'zrender/src/core/util'; -import List from '../../data/List'; -import createDimensions from '../../data/helper/createDimensions'; +import SeriesData from '../../data/SeriesData'; +import prepareSeriesDataSchema from '../../data/helper/createDimensions'; import {getDimensionTypeByAxis} from '../../data/helper/dimensionHelper'; import {getDataItemValue} from '../../util/model'; import CoordinateSystem from '../../core/CoordinateSystem'; import {getCoordSysInfoBySeries} from '../../model/referHelper'; -import { createSourceFromSeriesDataOption, isSourceInstance, Source } from '../../data/Source'; +import { createSourceFromSeriesDataOption, Source } from '../../data/Source'; import {enableDataStack} from '../../data/helper/dataStackHelper'; import {makeSeriesEncodeForAxisCoordSys} from '../../data/helper/sourceHelper'; import { - SOURCE_FORMAT_ORIGINAL, DimensionDefinitionLoose, DimensionDefinition, OptionSourceData, EncodeDefaulter + SOURCE_FORMAT_ORIGINAL, + DimensionDefinitionLoose, + DimensionDefinition, + OptionSourceData, + EncodeDefaulter } from '../../util/types'; import SeriesModel from '../../model/Series'; +import DataStore from '../../data/DataStore'; +import SeriesDimensionDefine from '../../data/SeriesDimensionDefine'; -function createListFromArray(source: Source | OptionSourceData, seriesModel: SeriesModel, opt?: { - generateCoord?: string - useEncodeDefaulter?: boolean | EncodeDefaulter - // By default: auto. If `true`, create inverted indices for all ordinal dimension on coordSys. - createInvertedIndices?: boolean -}): List { - opt = opt || {}; - - if (!isSourceInstance(source)) { - source = createSourceFromSeriesDataOption(source); - } - +function getCoordSysDimDefs( + seriesModel: SeriesModel, + coordSysInfo: ReturnType +) { const coordSysName = seriesModel.get('coordinateSystem'); const registeredCoordSys = CoordinateSystem.get(coordSysName); - const coordSysInfo = getCoordSysInfoBySeries(seriesModel); - let coordSysDimDefs: DimensionDefinitionLoose[]; if (coordSysInfo && coordSysInfo.coordSysDims) { @@ -60,7 +56,6 @@ function createListFromArray(source: Source | OptionSourceData, seriesModel: Ser if (axisModel) { const axisType = axisModel.get('type'); dimInfo.type = getDimensionTypeByAxis(axisType); - // dimInfo.stackable = isStackable(axisType); } return dimInfo; }); @@ -75,17 +70,14 @@ function createListFromArray(source: Source | OptionSourceData, seriesModel: Ser )) || ['x', 'y']; } - const useEncodeDefaulter = opt.useEncodeDefaulter; - const dimInfoList = createDimensions(source, { - coordDimensions: coordSysDimDefs, - generateCoord: opt.generateCoord, - encodeDefaulter: zrUtil.isFunction(useEncodeDefaulter) - ? useEncodeDefaulter - : useEncodeDefaulter - ? zrUtil.curry(makeSeriesEncodeForAxisCoordSys, coordSysDimDefs, seriesModel) - : null - }); + return coordSysDimDefs; +} +function injectOrdinalMeta( + dimInfoList: SeriesDimensionDefine[], + createInvertedIndices: boolean, + coordSysInfo: ReturnType +) { let firstCategoryDimIndex: number; let hasNameEncode: boolean; coordSysInfo && zrUtil.each(dimInfoList, function (dimInfo, dimIndex) { @@ -96,7 +88,7 @@ function createListFromArray(source: Source | OptionSourceData, seriesModel: Ser firstCategoryDimIndex = dimIndex; } dimInfo.ordinalMeta = categoryAxisModel.getOrdinalMeta(); - if (opt.createInvertedIndices) { + if (createInvertedIndices) { dimInfo.createInvertedIndices = true; } } @@ -107,26 +99,85 @@ function createListFromArray(source: Source | OptionSourceData, seriesModel: Ser if (!hasNameEncode && firstCategoryDimIndex != null) { dimInfoList[firstCategoryDimIndex].otherDims.itemName = 0; } + return firstCategoryDimIndex; +} - const stackCalculationInfo = enableDataStack(seriesModel, dimInfoList); - - const list = new List(dimInfoList, seriesModel); +/** + * Caution: there are side effects to `sourceManager` in this method. + * Should better only be called in `Series['getInitialData']`. + */ +function createSeriesData( + sourceRaw: OptionSourceData | null | undefined, + seriesModel: SeriesModel, + opt?: { + generateCoord?: string + useEncodeDefaulter?: boolean | EncodeDefaulter + // By default: auto. If `true`, create inverted indices for all ordinal dimension on coordSys. + createInvertedIndices?: boolean + } +): SeriesData { + opt = opt || {}; - list.setCalculationInfo(stackCalculationInfo); + const sourceManager = seriesModel.getSourceManager(); + let source; + let isOriginalSource = false; + if (sourceRaw) { + isOriginalSource = true; + source = createSourceFromSeriesDataOption(sourceRaw); + } + else { + source = sourceManager.getSource(); + // Is series.data. not dataset. + isOriginalSource = source.sourceFormat === SOURCE_FORMAT_ORIGINAL; + } + const coordSysInfo = getCoordSysInfoBySeries(seriesModel); + const coordSysDimDefs = getCoordSysDimDefs(seriesModel, coordSysInfo); + const useEncodeDefaulter = opt.useEncodeDefaulter; - const dimValueGetter = (firstCategoryDimIndex != null && isNeedCompleteOrdinalData(source)) - ? function (this: List, itemOpt: any, dimName: string, dataIndex: number, dimIndex: number) { - // Use dataIndex as ordinal value in categoryAxis - return dimIndex === firstCategoryDimIndex - ? dataIndex - : this.defaultDimValueGetter(itemOpt, dimName, dataIndex, dimIndex); - } + const encodeDefaulter = zrUtil.isFunction(useEncodeDefaulter) + ? useEncodeDefaulter + : useEncodeDefaulter + ? zrUtil.curry(makeSeriesEncodeForAxisCoordSys, coordSysDimDefs, seriesModel) : null; + const createDimensionOptions = { + coordDimensions: coordSysDimDefs, + generateCoord: opt.generateCoord, + encodeDefine: seriesModel.getEncode(), + encodeDefaulter: encodeDefaulter, + canOmitUnusedDimensions: !isOriginalSource + }; + const schema = prepareSeriesDataSchema(source, createDimensionOptions); + const firstCategoryDimIndex = injectOrdinalMeta( + schema.dimensions, opt.createInvertedIndices, coordSysInfo + ); + + const store = !isOriginalSource ? sourceManager.getSharedDataStore(schema) : null; + + const stackCalculationInfo = enableDataStack(seriesModel, { schema, store }); + + const data = new SeriesData(schema, seriesModel); + data.setCalculationInfo(stackCalculationInfo); + + const dimValueGetter = + firstCategoryDimIndex != null + && isNeedCompleteOrdinalData(source) + ? function (this: DataStore, itemOpt: any, dimName: string, dataIndex: number, dimIndex: number) { + // Use dataIndex as ordinal value in categoryAxis + return dimIndex === firstCategoryDimIndex + ? dataIndex + : this.defaultDimValueGetter(itemOpt, dimName, dataIndex, dimIndex); + } + : null; - list.hasItemOption = false; - list.initData(source, null, dimValueGetter); + data.hasItemOption = false; + data.initData( + // Try to reuse the data store in sourceManager if using dataset. + isOriginalSource ? source : store, + null, + dimValueGetter + ); - return list; + return data; } function isNeedCompleteOrdinalData(source: Source) { @@ -137,12 +188,12 @@ function isNeedCompleteOrdinalData(source: Source) { } } -function firstDataNotNull(data: ArrayLike) { +function firstDataNotNull(arr: ArrayLike) { let i = 0; - while (i < data.length && data[i] == null) { + while (i < arr.length && arr[i] == null) { i++; } - return data[i]; + return arr[i]; } -export default createListFromArray; +export default createSeriesData; diff --git a/src/chart/helper/createListSimply.ts b/src/chart/helper/createSeriesDataSimply.ts similarity index 67% rename from src/chart/helper/createListSimply.ts rename to src/chart/helper/createSeriesDataSimply.ts index b38d3cdcf5..d99c246ecf 100644 --- a/src/chart/helper/createListSimply.ts +++ b/src/chart/helper/createSeriesDataSimply.ts @@ -17,8 +17,8 @@ * under the License. */ -import createDimensions, {CreateDimensionsParams} from '../../data/helper/createDimensions'; -import List from '../../data/List'; +import prepareSeriesDataSchema, {PrepareSeriesDataSchemaParams} from '../../data/helper/createDimensions'; +import SeriesData from '../../data/SeriesData'; import {extend, isArray} from 'zrender/src/core/util'; import SeriesModel from '../../model/Series'; @@ -32,18 +32,22 @@ import SeriesModel from '../../model/Series'; * dimensionsCount: 5 * }); */ -export default function createListSimply( +export default function createSeriesDataSimply( seriesModel: SeriesModel, - opt: CreateDimensionsParams | CreateDimensionsParams['coordDimensions'], + opt: PrepareSeriesDataSchemaParams | PrepareSeriesDataSchemaParams['coordDimensions'], nameList?: string[] -): List { - opt = isArray(opt) && {coordDimensions: opt} || extend({}, opt); +): SeriesData { + opt = isArray(opt) && { + coordDimensions: opt + } || extend({ + encodeDefine: seriesModel.getEncode() + }, opt); const source = seriesModel.getSource(); - const dimensionsInfo = createDimensions(source, opt as CreateDimensionsParams); + const { dimensions } = prepareSeriesDataSchema(source, opt as PrepareSeriesDataSchemaParams); - const list = new List(dimensionsInfo, seriesModel); + const list = new SeriesData(dimensions, seriesModel); list.initData(source, nameList); return list; diff --git a/src/chart/helper/labelHelper.ts b/src/chart/helper/labelHelper.ts index 58a0b0d859..e8d791685f 100644 --- a/src/chart/helper/labelHelper.ts +++ b/src/chart/helper/labelHelper.ts @@ -19,7 +19,7 @@ import {retrieveRawValue} from '../../data/helper/dataProvider'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import { InterpolatableValue } from '../../util/types'; import { isArray } from 'zrender/src/core/util'; @@ -27,7 +27,7 @@ import { isArray } from 'zrender/src/core/util'; * @return label string. Not null/undefined */ export function getDefaultLabel( - data: List, + data: SeriesData, dataIndex: number ): string { const labelDims = data.mapDimensionsAll('defaultedLabel'); @@ -48,7 +48,7 @@ export function getDefaultLabel( } export function getDefaultInterpolatedLabel( - data: List, + data: SeriesData, interpolatedValue: InterpolatableValue ): string { const labelDims = data.mapDimensionsAll('defaultedLabel'); @@ -58,9 +58,9 @@ export function getDefaultInterpolatedLabel( const vals = []; for (let i = 0; i < labelDims.length; i++) { - const dimInfo = data.getDimensionInfo(labelDims[i]); - if (dimInfo) { - vals.push(interpolatedValue[dimInfo.index]); + const dimIndex = data.getDimensionIndex(labelDims[i]); + if (dimIndex >= 0) { + vals.push(interpolatedValue[dimIndex]); } } return vals.join(' '); diff --git a/src/chart/helper/pieHelper.ts b/src/chart/helper/pieHelper.ts index aea5b5b165..0e579c8d84 100644 --- a/src/chart/helper/pieHelper.ts +++ b/src/chart/helper/pieHelper.ts @@ -24,11 +24,12 @@ import { parsePercent } from 'zrender/src/contain/text'; export function getSectorCornerRadius( model: Model<{ borderRadius?: string | number | (string | number)[] }>, - shape: Pick + shape: Pick, + zeroIfNull?: boolean ) { let cornerRadius = model.get('borderRadius'); if (cornerRadius == null) { - return null; + return zeroIfNull ? {innerCornerRadius: 0, cornerRadius: 0} : null; } if (!isArray(cornerRadius)) { cornerRadius = [cornerRadius, cornerRadius]; diff --git a/src/chart/helper/whiskerBoxCommon.ts b/src/chart/helper/whiskerBoxCommon.ts index 679360dae4..177105283e 100644 --- a/src/chart/helper/whiskerBoxCommon.ts +++ b/src/chart/helper/whiskerBoxCommon.ts @@ -17,7 +17,7 @@ * under the License. */ -import createListSimply from '../helper/createListSimply'; +import createSeriesDataSimply from './createSeriesDataSimply'; import * as zrUtil from 'zrender/src/core/util'; import {getDimensionTypeByAxis} from '../../data/helper/dimensionHelper'; import {makeSeriesEncodeForAxisCoordSys} from '../../data/helper/sourceHelper'; @@ -25,7 +25,7 @@ import type { SeriesOption, SeriesOnCartesianOptionMixin, LayoutOrient } from '. import type GlobalModel from '../../model/Global'; import type SeriesModel from '../../model/Series'; import type CartesianAxisModel from '../../coord/cartesian/AxisModel'; -import type List from '../../data/List'; +import type SeriesData from '../../data/SeriesData'; import type Axis2D from '../../coord/cartesian/Axis2D'; import { CoordDimensionDefinition } from '../../data/helper/createDimensions'; @@ -55,7 +55,7 @@ class WhiskerBoxCommonMixin { /** * @override */ - getInitialData(option: Opts, ecModel: GlobalModel): List { + getInitialData(option: Opts, ecModel: GlobalModel): SeriesData { // When both types of xAxis and yAxis are 'value', layout is // needed to be specified by user. Otherwise, layout can be // judged by which axis is category. @@ -94,18 +94,21 @@ class WhiskerBoxCommonMixin { const otherAxisType = axisModels[1 - baseAxisDimIndex].get('type'); const data = option.data as WhiskerBoxCommonData; - // ??? FIXME make a stage to perform data transfrom. - // MUST create a new data, consider setOption({}) again. + // Clone a new data for next setOption({}) usage. + // Avoid modifying current data will affect further update. if (data && addOrdinal) { const newOptionData: WhiskerBoxCommonData = []; zrUtil.each(data, function (item, index) { let newItem; if (zrUtil.isArray(item)) { newItem = item.slice(); + // Modify current using data. item.unshift(index); } else if (zrUtil.isArray(item.value)) { - newItem = item.value.slice(); + newItem = zrUtil.extend({}, item); + newItem.value = newItem.value.slice(); + // Modify current using data. item.value.unshift(index); } else { @@ -132,7 +135,7 @@ class WhiskerBoxCommonMixin { dimsDef: defaultValueDimensions.slice() }]; - return createListSimply( + return createSeriesDataSimply( this, { coordDimensions: coordDimensions, diff --git a/src/chart/line/LineSeries.ts b/src/chart/line/LineSeries.ts index 7d44f23298..a836132e4d 100644 --- a/src/chart/line/LineSeries.ts +++ b/src/chart/line/LineSeries.ts @@ -17,7 +17,7 @@ * under the License. */ -import createListFromArray from '../helper/createListFromArray'; +import createSeriesData from '../helper/createSeriesData'; import SeriesModel from '../../model/Series'; import { SeriesOnCartesianOptionMixin, @@ -36,7 +36,7 @@ import { CallbackDataParams, DefaultEmphasisFocus } from '../../util/types'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import type Cartesian2D from '../../coord/cartesian/Cartesian2D'; import type Polar from '../../coord/polar/Polar'; import {createSymbol, ECSymbol} from '../../util/symbol'; @@ -55,6 +55,7 @@ interface ExtraStateOption { export interface LineStateOption { itemStyle?: ItemStyleOption label?: SeriesLabelOption + endLabel?: LineEndLabelOption } export interface LineDataItemOption extends SymbolOptionMixin, @@ -129,14 +130,14 @@ class LineSeriesModel extends SeriesModel { hasSymbolVisual = true; - getInitialData(option: LineSeriesOption): List { + getInitialData(option: LineSeriesOption): SeriesData { if (__DEV__) { const coordSys = option.coordinateSystem; if (coordSys !== 'polar' && coordSys !== 'cartesian2d') { throw new Error('Line not support coordinateSystem besides cartesian and polar'); } } - return createListFromArray(this.getSource(), this, { + return createSeriesData(null, this, { useEncodeDefaulter: true }); } @@ -207,7 +208,11 @@ class LineSeriesModel extends SeriesModel { // Disable progressive progressive: 0, - hoverLayerThreshold: Infinity + hoverLayerThreshold: Infinity, + + universalTransition: { + divideShape: 'clone' + } }; getLegendIcon(opt: LegendIconParams): ECSymbol | Group { diff --git a/src/chart/line/LineView.ts b/src/chart/line/LineView.ts index d14424b855..9cabed1302 100644 --- a/src/chart/line/LineView.ts +++ b/src/chart/line/LineView.ts @@ -35,7 +35,7 @@ import type ExtensionAPI from '../../core/ExtensionAPI'; // TODO import Cartesian2D from '../../coord/cartesian/Cartesian2D'; import Polar from '../../coord/polar/Polar'; -import type List from '../../data/List'; +import type SeriesData from '../../data/SeriesData'; import type { Payload, Dictionary, @@ -48,7 +48,7 @@ import type { import type OrdinalScale from '../../scale/Ordinal'; import type Axis2D from '../../coord/cartesian/Axis2D'; import { CoordinateSystemClipArea, isCoordinateSystemType } from '../../coord/CoordinateSystem'; -import { setStatesStylesFromModel, setStatesFlag, enableHoverEmphasis } from '../../util/states'; +import { setStatesStylesFromModel, setStatesFlag, enableHoverEmphasis, SPECIAL_STATES } from '../../util/states'; import Model from '../../model/Model'; import {setLabelStyle, getLabelStatesModels, labelInner} from '../../label/labelStyle'; import {getDefaultLabel, getDefaultInterpolatedLabel} from '../helper/labelHelper'; @@ -59,11 +59,16 @@ import { convertToColorString } from '../../util/format'; type PolarArea = ReturnType; type Cartesian2DArea = ReturnType; - interface SymbolExtended extends SymbolClz { __temp: boolean } +interface ColorStop { + offset: number + coord?: number + color: ColorString +} + function isPointsSame(points1: ArrayLike, points2: ArrayLike) { if (points1.length !== points2.length) { return; @@ -121,7 +126,7 @@ function getSmooth(smooth: number | boolean) { function getStackedOnPoints( coordSys: Cartesian2D | Polar, - data: List, + data: SeriesData, dataCoordInfo: ReturnType ) { if (!dataCoordInfo.valueDim) { @@ -187,7 +192,7 @@ function turnPointsIntoStep( } function getVisualGradient( - data: List, + data: SeriesData, coordSys: Cartesian2D | Polar ) { const visualMetaList = data.getVisual('visualMeta'); @@ -207,9 +212,7 @@ function getVisualGradient( let visualMeta; for (let i = visualMetaList.length - 1; i >= 0; i--) { - const dimIndex = visualMetaList[i].dimension; - const dimName = data.dimensions[dimIndex]; - const dimInfo = data.getDimensionInfo(dimName); + const dimInfo = data.getDimensionInfo(visualMetaList[i].dimension); coordDim = (dimInfo && dimInfo.coordDim) as 'x' | 'y'; // Can only be x or y if (coordDim === 'x' || coordDim === 'y') { @@ -233,17 +236,17 @@ function getVisualGradient( // LinearGradient to render `outerColors`. const axis = coordSys.getAxis(coordDim); + const axisScaleExtent = axis.scale.getExtent(); - interface ColorStop { - offset: number - coord?: number - color: ColorString - } // dataToCoord mapping may not be linear, but must be monotonic. const colorStops: ColorStop[] = zrUtil.map(visualMeta.stops, function (stop) { + let coord = axis.toGlobalCoord(axis.dataToCoord(stop.value)); + // normalize the infinite value + isNaN(coord) || isFinite(coord) + || (coord = axis.toGlobalCoord(axis.dataToCoord(axisScaleExtent[+(coord < 0)]))); return { offset: 0, - coord: axis.toGlobalCoord(axis.dataToCoord(stop.value, true)), + coord, color: stop.color }; }); @@ -290,7 +293,7 @@ function getVisualGradient( function getIsIgnoreFunc( seriesModel: LineSeriesModel, - data: List, + data: SeriesData, coordSys: Cartesian2D ) { const showAllSymbol = seriesModel.get('showAllSymbol'); @@ -332,7 +335,7 @@ function getIsIgnoreFunc( function canShowAllSymbolForCategory( categoryAxis: Axis2D, - data: List + data: SeriesData ) { // In mose cases, line is monotonous on category axis, and the label size // is close with each other. So we check the symbol size and some of the @@ -412,6 +415,20 @@ function getIndexRange(points: ArrayLike, xOrY: number, dim: 'x' | 'y') }; } +function anyStateShowEndLabel( + seriesModel: LineSeriesModel +) { + if (seriesModel.get(['endLabel', 'show'])) { + return true; + } + for (let i = 0; i < SPECIAL_STATES.length; i++) { + if (seriesModel.get([SPECIAL_STATES[i], 'endLabel', 'show'])) { + return true; + } + } + return false; +} + interface EndLabelAnimationRecord { lastFrameIndex: number @@ -427,13 +444,12 @@ function createLineClipPath( ) { if (isCoordinateSystemType(coordSys, 'cartesian2d')) { const endLabelModel = seriesModel.getModel('endLabel'); - const showEndLabel = endLabelModel.get('show'); const valueAnimation = endLabelModel.get('valueAnimation'); const data = seriesModel.getData(); const labelAnimationRecord: EndLabelAnimationRecord = { lastFrameIndex: 0 }; - const during = showEndLabel + const during = anyStateShowEndLabel(seriesModel) ? (percent: number, clipRect: graphic.Rect) => { lineView._endLabelOnDuring( percent, @@ -531,7 +547,7 @@ class LineView extends ChartView { _clipShapeForSymbol: CoordinateSystemClipArea; - _data: List; + _data: SeriesData; init() { const lineGroup = new graphic.Group(); @@ -949,7 +965,7 @@ class LineView extends ChartView { } _initSymbolLabelAnimation( - data: List, + data: SeriesData, coordSys: Polar | Cartesian2D, clipShape: PolarArea | Cartesian2DArea ) { @@ -1029,6 +1045,7 @@ class LineView extends ChartView { scaleY: 1 }, { duration: 200, + setToFinal: true, delay: delay }); @@ -1055,7 +1072,7 @@ class LineView extends ChartView { ) { const endLabelModel = seriesModel.getModel('endLabel'); - if (endLabelModel.get('show')) { + if (anyStateShowEndLabel(seriesModel)) { const data = seriesModel.getData(); const polyline = this._polyline; let endLabel = this._endLabel; @@ -1099,7 +1116,7 @@ class LineView extends ChartView { _endLabelOnDuring( percent: number, clipRect: graphic.Rect, - data: List, + data: SeriesData, animationRecord: EndLabelAnimationRecord, valueAnimation: boolean, endLabelModel: Model, @@ -1186,7 +1203,7 @@ class LineView extends ChartView { */ // FIXME Two value axis _doUpdateAnimation( - data: List, + data: SeriesData, stackedOnPoints: ArrayLike, coordSys: Cartesian2D | Polar, api: ExtensionAPI, diff --git a/src/chart/line/helper.ts b/src/chart/line/helper.ts index 200824f2cb..e58d2e70e0 100644 --- a/src/chart/line/helper.ts +++ b/src/chart/line/helper.ts @@ -21,7 +21,7 @@ import {isDimensionStacked} from '../../data/helper/dataStackHelper'; import {map} from 'zrender/src/core/util'; import type Polar from '../../coord/polar/Polar'; import type Cartesian2D from '../../coord/cartesian/Cartesian2D'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import Axis from '../../coord/Axis'; import type { LineSeriesOption } from './LineSeries'; @@ -39,7 +39,7 @@ interface CoordInfo { export function prepareDataCoordInfo( coordSys: Cartesian2D | Polar, - data: List, + data: SeriesData, valueOrigin?: LineSeriesOption['areaStyle']['origin'] ): CoordInfo { const baseAxis = coordSys.getBaseAxis(); @@ -109,7 +109,7 @@ function getValueStart(valueAxis: Axis, valueOrigin: LineSeriesOption['areaStyle export function getStackedOnPoint( dataCoordInfo: CoordInfo, coordSys: Cartesian2D | Polar, - data: List, + data: SeriesData, idx: number ) { let value = NaN; diff --git a/src/chart/line/lineAnimationDiff.ts b/src/chart/line/lineAnimationDiff.ts index b491665435..60a93a1b1c 100644 --- a/src/chart/line/lineAnimationDiff.ts +++ b/src/chart/line/lineAnimationDiff.ts @@ -18,7 +18,7 @@ */ import {prepareDataCoordInfo, getStackedOnPoint} from './helper'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import type Cartesian2D from '../../coord/cartesian/Cartesian2D'; import type Polar from '../../coord/polar/Polar'; import { LineSeriesOption } from './LineSeries'; @@ -30,7 +30,7 @@ interface DiffItem { idx1?: number } -function diffData(oldData: List, newData: List) { +function diffData(oldData: SeriesData, newData: SeriesData) { const diffResult: DiffItem[] = []; newData.diff(oldData) @@ -49,7 +49,7 @@ function diffData(oldData: List, newData: List) { } export default function lineAnimationDiff( - oldData: List, newData: List, + oldData: SeriesData, newData: SeriesData, oldStackedOnPoints: ArrayLike, newStackedOnPoints: ArrayLike, oldCoordSys: Cartesian2D | Polar, newCoordSys: Cartesian2D | Polar, oldValueOrigin: LineSeriesOption['areaStyle']['origin'], diff --git a/src/chart/lines/LinesSeries.ts b/src/chart/lines/LinesSeries.ts index 98cb2dd5e4..65355e062f 100644 --- a/src/chart/lines/LinesSeries.ts +++ b/src/chart/lines/LinesSeries.ts @@ -20,7 +20,7 @@ /* global Uint32Array, Float64Array, Float32Array */ import SeriesModel from '../../model/Series'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import { concatArray, mergeAll, map } from 'zrender/src/core/util'; import CoordinateSystem from '../../core/CoordinateSystem'; import { @@ -101,6 +101,7 @@ export interface LinesDataItemOption extends LinesStateOption, StatesOptionMixin } export interface LinesSeriesOption extends SeriesOption, LinesStateOption, + SeriesOnCartesianOptionMixin, SeriesOnGeoOptionMixin, SeriesOnPolarOptionMixin, SeriesOnCalendarOptionMixin, SeriesLargeOptionMixin { @@ -297,7 +298,7 @@ class LinesSeriesModel extends SeriesModel { } } - const lineData = new List(['value'], this); + const lineData = new SeriesData(['value'], this); lineData.hasItemOption = false; lineData.initData(option.data, [], function (dataItem, dimName, dataIndex, dimIndex) { diff --git a/src/chart/lines/LinesView.ts b/src/chart/lines/LinesView.ts index 72453c16fd..6c4479da44 100644 --- a/src/chart/lines/LinesView.ts +++ b/src/chart/lines/LinesView.ts @@ -31,7 +31,7 @@ import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../core/ExtensionAPI'; import CanvasPainter from 'zrender/src/canvas/Painter'; import { StageHandlerProgressParams, StageHandlerProgressExecutor } from '../../util/types'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import type Polar from '../../coord/polar/Polar'; import type Cartesian2D from '../../coord/cartesian/Cartesian2D'; @@ -90,7 +90,7 @@ class LinesView extends ChartView { } } - lineDraw.updateData(data as List); + lineDraw.updateData(data as SeriesData); const clipPath = seriesModel.get('clip', true) && createClipPath( (seriesModel.coordinateSystem as Polar | Cartesian2D), false, seriesModel @@ -156,7 +156,7 @@ class LinesView extends ChartView { } } - _updateLineDraw(data: List, seriesModel: LinesSeriesModel) { + _updateLineDraw(data: SeriesData, seriesModel: LinesSeriesModel) { let lineDraw = this._lineDraw; const hasEffect = this._showEffect(seriesModel); const isPolyline = !!seriesModel.get('polyline'); @@ -186,7 +186,6 @@ class LinesView extends ChartView { this._hasEffet = hasEffect; this._isPolyline = isPolyline; this._isLargeDraw = isLargeDraw; - this.group.removeAll(); } this.group.add(lineDraw.group); @@ -214,6 +213,10 @@ class LinesView extends ChartView { this._clearLayer(api); } + dispose(ecModel: GlobalModel, api: ExtensionAPI) { + this.remove(ecModel, api); + } + } -export default LinesView; \ No newline at end of file +export default LinesView; diff --git a/src/chart/lines/linesVisual.ts b/src/chart/lines/linesVisual.ts index 1e4c931b2b..0e18b691e5 100644 --- a/src/chart/lines/linesVisual.ts +++ b/src/chart/lines/linesVisual.ts @@ -18,7 +18,7 @@ */ import { StageHandler } from '../../util/types'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import LinesSeriesModel, { LinesDataItemOption } from './LinesSeries'; import Model from '../../model/Model'; import { LineDataVisual } from '../../visual/commonVisualTypes'; @@ -37,7 +37,7 @@ const linesVisual: StageHandler = { reset(seriesModel: LinesSeriesModel) { const symbolType = normalize(seriesModel.get('symbol')); const symbolSize = normalize(seriesModel.get('symbolSize')); - const data = seriesModel.getData() as List; + const data = seriesModel.getData() as SeriesData; data.setVisual('fromSymbol', symbolType && symbolType[0]); data.setVisual('toSymbol', symbolType && symbolType[1]); @@ -45,7 +45,7 @@ const linesVisual: StageHandler = { data.setVisual('toSymbolSize', symbolSize && symbolSize[1]); function dataEach( - data: List, + data: SeriesData, idx: number ): void { const itemModel = data.getItemModel(idx) as Model; diff --git a/src/chart/map/MapSeries.ts b/src/chart/map/MapSeries.ts index e26247f6e2..2081886e31 100644 --- a/src/chart/map/MapSeries.ts +++ b/src/chart/map/MapSeries.ts @@ -19,7 +19,7 @@ import * as zrUtil from 'zrender/src/core/util'; -import createListSimply from '../helper/createListSimply'; +import createSeriesDataSimply from '../helper/createSeriesDataSimply'; import SeriesModel from '../../model/Series'; import geoSourceManager from '../../coord/geo/geoSourceManager'; import {makeSeriesEncodeForNameBased} from '../../data/helper/sourceHelper'; @@ -36,7 +36,7 @@ import { } from '../../util/types'; import { Dictionary } from 'zrender/src/core/types'; import GeoModel, { GeoCommonOptionMixin, GeoItemStyleOption } from '../../coord/geo/GeoModel'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import Model from '../../model/Model'; import Geo from '../../coord/geo/Geo'; import { createTooltipMarkup } from '../../component/tooltip/tooltipMarkup'; @@ -82,7 +82,7 @@ export interface MapSeriesOption extends // @deprecated. Only for echarts2 backward compat. geoCoord?: Dictionary; - data?: OptionDataValueNumeric[] | OptionDataValueNumeric[][] | MapDataItemOption[] + data?: (OptionDataValueNumeric | OptionDataValueNumeric[] | MapDataItemOption)[] nameProperty?: string; @@ -101,7 +101,7 @@ class MapSeries extends SeriesModel { // ----------------- // Injected outside - originalData: List; + originalData: SeriesData; mainSeries: MapSeries; // Only first map series of same mapType will drawMap. needsDrawMap: boolean = false; @@ -109,8 +109,8 @@ class MapSeries extends SeriesModel { seriesGroup: MapSeries[] = []; - getInitialData(this: MapSeries, option: MapSeriesOption): List { - const data = createListSimply(this, { + getInitialData(this: MapSeries, option: MapSeriesOption): SeriesData { + const data = createSeriesDataSimply(this, { coordDimensions: ['value'], encodeDefaulter: zrUtil.curry(makeSeriesEncodeForNameBased, this) }); @@ -341,4 +341,4 @@ class MapSeries extends SeriesModel { } -export default MapSeries; \ No newline at end of file +export default MapSeries; diff --git a/src/chart/map/mapDataStatistic.ts b/src/chart/map/mapDataStatistic.ts index 0df4040f29..504f5484ff 100644 --- a/src/chart/map/mapDataStatistic.ts +++ b/src/chart/map/mapDataStatistic.ts @@ -19,12 +19,12 @@ import * as zrUtil from 'zrender/src/core/util'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import MapSeries, { MapValueCalculationType } from './MapSeries'; import GlobalModel from '../../model/Global'; // FIXME 公用? -function dataStatistics(datas: List[], statisticType: MapValueCalculationType): List { +function dataStatistics(datas: SeriesData[], statisticType: MapValueCalculationType): SeriesData { const dataNameMap = {} as {[mapKey: string]: number[]}; zrUtil.each(datas, function (data) { diff --git a/src/chart/map/mapSymbolLayout.ts b/src/chart/map/mapSymbolLayout.ts index 9771eb53a4..bfc3130512 100644 --- a/src/chart/map/mapSymbolLayout.ts +++ b/src/chart/map/mapSymbolLayout.ts @@ -22,7 +22,6 @@ import * as zrUtil from 'zrender/src/core/util'; import GlobalModel from '../../model/Global'; import MapSeries from './MapSeries'; import { Dictionary } from '../../util/types'; -import { GeoJSONRegion } from '../../coord/geo/Region'; export default function mapSymbolLayout(ecModel: GlobalModel) { diff --git a/src/chart/parallel/ParallelSeries.ts b/src/chart/parallel/ParallelSeries.ts index aaca025b05..c75ea4e8fd 100644 --- a/src/chart/parallel/ParallelSeries.ts +++ b/src/chart/parallel/ParallelSeries.ts @@ -20,7 +20,7 @@ import {each, bind} from 'zrender/src/core/util'; import SeriesModel from '../../model/Series'; -import createListFromArray from '../helper/createListFromArray'; +import createSeriesData from '../helper/createSeriesData'; import { SeriesOption, SeriesEncodeOptionMixin, @@ -35,7 +35,7 @@ import { OptionEncode } from '../../util/types'; import GlobalModel from '../../model/Global'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import { ParallelActiveState, ParallelAxisOption } from '../../coord/parallel/AxisModel'; import Parallel from '../../coord/parallel/Parallel'; import ParallelModel from '../../coord/parallel/ParallelModel'; @@ -91,8 +91,8 @@ class ParallelSeriesModel extends SeriesModel { coordinateSystem: Parallel; - getInitialData(this: ParallelSeriesModel, option: ParallelSeriesOption, ecModel: GlobalModel): List { - return createListFromArray(this.getSource(), this, { + getInitialData(this: ParallelSeriesModel, option: ParallelSeriesOption, ecModel: GlobalModel): SeriesData { + return createSeriesData(null, this, { useEncodeDefaulter: bind(makeDefaultEncode, null, this) }); } diff --git a/src/chart/parallel/ParallelView.ts b/src/chart/parallel/ParallelView.ts index 8449497c79..1704b09c90 100644 --- a/src/chart/parallel/ParallelView.ts +++ b/src/chart/parallel/ParallelView.ts @@ -20,7 +20,7 @@ import * as graphic from '../../util/graphic'; import { setStatesStylesFromModel, enableHoverEmphasis } from '../../util/states'; import ChartView from '../../view/Chart'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import ParallelSeriesModel, { ParallelSeriesDataItemOption } from './ParallelSeries'; import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../core/ExtensionAPI'; @@ -29,6 +29,7 @@ import Parallel from '../../coord/parallel/Parallel'; import { OptionAxisType } from '../../coord/axisCommonTypes'; import { numericToNumber } from '../../util/number'; import { eqNaN } from 'zrender/src/core/util'; +import { saveOldStyle } from '../../animation/basicTrasition'; const DEFAULT_SMOOTH = 0.3; @@ -41,7 +42,7 @@ class ParallelView extends ChartView { private _dataGroup = new graphic.Group(); - private _data: List; + private _data: SeriesData; private _initialized = false; @@ -84,6 +85,8 @@ class ParallelView extends ChartView { graphic.updateProps(line, {shape: {points: points}}, seriesModel, newDataIndex); + saveOldStyle(line); + updateElCommon(line, data, newDataIndex, seriesScope); } @@ -157,7 +160,7 @@ function createGridClipShape(coordSys: Parallel, seriesModel: ParallelSeriesMode return rectEl; } -function createLinePoints(data: List, dataIndex: number, dimensions: string[], coordSys: Parallel) { +function createLinePoints(data: SeriesData, dataIndex: number, dimensions: string[], coordSys: Parallel) { const points = []; for (let i = 0; i < dimensions.length; i++) { const dimName = dimensions[i]; @@ -169,7 +172,7 @@ function createLinePoints(data: List, dataIndex: number, dimensions: string[], c return points; } -function addEl(data: List, dataGroup: graphic.Group, dataIndex: number, dimensions: string[], coordSys: Parallel) { +function addEl(data: SeriesData, dataGroup: graphic.Group, dataIndex: number, dimensions: string[], coordSys: Parallel) { const points = createLinePoints(data, dataIndex, dimensions, coordSys); const line = new graphic.Polyline({ shape: {points: points}, @@ -192,7 +195,7 @@ function makeSeriesScope(seriesModel: ParallelSeriesModel): ParallelDrawSeriesSc function updateElCommon( el: graphic.Polyline, - data: List, + data: SeriesData, dataIndex: number, seriesScope: ParallelDrawSeriesScope ) { diff --git a/src/chart/pie/PieSeries.ts b/src/chart/pie/PieSeries.ts index fa0706ec4c..b68726285e 100644 --- a/src/chart/pie/PieSeries.ts +++ b/src/chart/pie/PieSeries.ts @@ -17,11 +17,11 @@ * under the License. */ -import createListSimply from '../helper/createListSimply'; +import createSeriesDataSimply from '../helper/createSeriesDataSimply'; import * as zrUtil from 'zrender/src/core/util'; import * as modelUtil from '../../util/model'; -import {getPercentWithPrecision} from '../../util/number'; -import {makeSeriesEncodeForNameBased} from '../../data/helper/sourceHelper'; +import { getPercentWithPrecision } from '../../util/number'; +import { makeSeriesEncodeForNameBased } from '../../data/helper/sourceHelper'; import LegendVisualProvider from '../../visual/LegendVisualProvider'; import SeriesModel from '../../model/Series'; import { @@ -38,7 +38,7 @@ import { SeriesLabelOption, DefaultEmphasisFocus } from '../../util/types'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; interface PieItemStyleOption extends ItemStyleOption { // can be 10 @@ -59,7 +59,7 @@ export interface PieStateOption { labelLine?: PieLabelLineOption } interface PieLabelOption extends Omit { - rotate?: number + rotate?: number | boolean | 'radial' | 'tangential' alignTo?: 'none' | 'labelLine' | 'edge' edgeDistance?: string | number /** @@ -119,15 +119,16 @@ export interface PieSeriesOption extends animationType?: 'expansion' | 'scale' animationTypeUpdate?: 'transition' | 'expansion' - data?: OptionDataValueNumeric[] | OptionDataValueNumeric[][] | PieDataItemOption[] + showEmptyCircle?: boolean; + emptyCircleStyle?: PieItemStyleOption; + + data?: (OptionDataValueNumeric | OptionDataValueNumeric[] | PieDataItemOption)[] } class PieSeriesModel extends SeriesModel { static type = 'series.pie' as const; - useColorPaletteOnData = true; - /** * @overwrite */ @@ -153,8 +154,8 @@ class PieSeriesModel extends SeriesModel { /** * @overwrite */ - getInitialData(this: PieSeriesModel): List { - return createListSimply(this, { + getInitialData(this: PieSeriesModel): SeriesData { + return createSeriesDataSimply(this, { coordDimensions: ['value'], encodeDefaulter: zrUtil.curry(makeSeriesEncodeForNameBased, this) }); @@ -200,7 +201,7 @@ class PieSeriesModel extends SeriesModel { zlevel: 0, z: 2, legendHoverLink: true, - + colorBy: 'data', // 默认全局居中 center: ['50%', '50%'], radius: [0, '75%'], @@ -274,7 +275,14 @@ class PieSeriesModel extends SeriesModel { } }, itemStyle: { - borderWidth: 1 + borderWidth: 1, + borderJoin: 'round' + }, + + showEmptyCircle: true, + emptyCircleStyle: { + color: 'lightgray', + opacity: 1 }, labelLayout: { diff --git a/src/chart/pie/PieView.ts b/src/chart/pie/PieView.ts index e7e575f568..66c07e8fc1 100644 --- a/src/chart/pie/PieView.ts +++ b/src/chart/pie/PieView.ts @@ -26,19 +26,21 @@ import ChartView from '../../view/Chart'; import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../core/ExtensionAPI'; import { Payload, ColorString } from '../../util/types'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import PieSeriesModel, {PieDataItemOption} from './PieSeries'; import labelLayout from './labelLayout'; import { setLabelLineStyle, getLabelLineStatesModels } from '../../label/labelGuideHelper'; import { setLabelStyle, getLabelStatesModels } from '../../label/labelStyle'; import { getSectorCornerRadius } from '../helper/pieHelper'; +import {saveOldStyle} from '../../animation/basicTrasition'; +import { getBasicPieLayout } from './pieLayout'; /** * Piece of pie including Sector, Label, LabelLine */ class PiePiece extends graphic.Sector { - constructor(data: List, idx: number, startAngle: number) { + constructor(data: SeriesData, idx: number, startAngle: number) { super(); this.z2 = 2; @@ -50,15 +52,17 @@ class PiePiece extends graphic.Sector { this.updateData(data, idx, startAngle, true); } - updateData(data: List, idx: number, startAngle?: number, firstCreate?: boolean): void { + updateData(data: SeriesData, idx: number, startAngle?: number, firstCreate?: boolean): void { const sector = this; const seriesModel = data.hostModel as PieSeriesModel; const itemModel = data.getItemModel(idx); const emphasisModel = itemModel.getModel('emphasis'); const layout = data.getItemLayout(idx) as graphic.Sector['shape']; + // cornerRadius & innerCornerRadius doesn't exist in the item layout. Use `0` if null value is specified. + // see `setItemLayout` in `pieLayout.ts`. const sectorShape = extend( - getSectorCornerRadius(itemModel.getModel('itemStyle'), layout) || {}, + getSectorCornerRadius(itemModel.getModel('itemStyle'), layout, true), layout ); @@ -103,6 +107,7 @@ class PiePiece extends graphic.Sector { } } else { + saveOldStyle(sector); // Transition animation from the old shape graphic.updateProps(sector, { shape: sectorShape @@ -110,6 +115,7 @@ class PiePiece extends graphic.Sector { } sector.useStyle(data.getItemVisual(idx, 'style')); + setStatesStylesFromModel(sector, itemModel); const midAngle = (layout.startAngle + layout.endAngle) / 2; @@ -152,7 +158,7 @@ class PiePiece extends graphic.Sector { enableHoverEmphasis(this, emphasisModel.get('focus'), emphasisModel.get('blurScope')); } - private _updateLabel(seriesModel: PieSeriesModel, data: List, idx: number): void { + private _updateLabel(seriesModel: PieSeriesModel, data: SeriesData, idx: number): void { const sector = this; const itemModel = data.getItemModel(idx); const labelLineModel = itemModel.getModel('labelLine'); @@ -217,7 +223,8 @@ class PieView extends ChartView { ignoreLabelLineUpdate = true; private _sectorGroup: graphic.Group; - private _data: List; + private _data: SeriesData; + private _emptyCircleSector: graphic.Sector; init(): void { const sectorGroup = new graphic.Group(); @@ -242,6 +249,20 @@ class PieView extends ChartView { } } + // remove empty-circle if it exists + if (this._emptyCircleSector) { + group.remove(this._emptyCircleSector); + } + // when all data are filtered, show lightgray empty circle + if (data.count() === 0 && seriesModel.get('showEmptyCircle')) { + const sector = new graphic.Sector({ + shape: getBasicPieLayout(seriesModel, api) + }); + sector.useStyle(seriesModel.getModel('emptyCircleStyle').getItemStyle()); + this._emptyCircleSector = sector; + group.add(sector); + } + data.diff(oldData) .add(function (idx) { const piePiece = new PiePiece(data, idx, startAngle); diff --git a/src/chart/pie/install.ts b/src/chart/pie/install.ts index 79027b224f..82f157f437 100644 --- a/src/chart/pie/install.ts +++ b/src/chart/pie/install.ts @@ -25,6 +25,7 @@ import dataFilter from '../../processor/dataFilter'; import { curry } from 'zrender/src/core/util'; import PieView from './PieView'; import PieSeriesModel from './PieSeries'; +import negativeDataFilter from '../../processor/negativeDataFilter'; export function install(registers: EChartsExtensionInstallRegisters) { registers.registerChartView(PieView); @@ -34,4 +35,5 @@ export function install(registers: EChartsExtensionInstallRegisters) { registers.registerLayout(curry(pieLayout, 'pie')); registers.registerProcessor(dataFilter('pie')); + registers.registerProcessor(negativeDataFilter('pie')); } \ No newline at end of file diff --git a/src/chart/pie/labelLayout.ts b/src/chart/pie/labelLayout.ts index 3a6a75e5f9..14be831543 100644 --- a/src/chart/pie/labelLayout.ts +++ b/src/chart/pie/labelLayout.ts @@ -355,10 +355,26 @@ export default function pieLabelLayout( if (typeof rotate === 'number') { labelRotate = rotate * (Math.PI / 180); } + else if (labelPosition === 'center') { + labelRotate = 0; + } else { - labelRotate = rotate - ? (nx < 0 ? -midAngle + Math.PI : -midAngle) - : 0; + const radialAngle = nx < 0 ? -midAngle + Math.PI : -midAngle; + if (rotate === 'radial' || rotate === true) { + labelRotate = radialAngle; + } + else if (rotate === 'tangential' + && labelPosition !== 'outside' + && labelPosition !== 'outer' + ) { + labelRotate = radialAngle + Math.PI / 2; + if (labelRotate > Math.PI / 2) { + labelRotate -= Math.PI; + } + } + else { + labelRotate = 0; + } } hasLabelRotate = !!labelRotate; diff --git a/src/chart/pie/pieLayout.ts b/src/chart/pie/pieLayout.ts index f515233d64..0598a779aa 100644 --- a/src/chart/pie/pieLayout.ts +++ b/src/chart/pie/pieLayout.ts @@ -17,12 +17,13 @@ * under the License. */ -import {parsePercent, linearMap} from '../../util/number'; +import { parsePercent, linearMap } from '../../util/number'; import * as layout from '../../util/layout'; import * as zrUtil from 'zrender/src/core/util'; import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../core/ExtensionAPI'; import PieSeriesModel from './PieSeries'; +import { SectorShape } from 'zrender/src/graphic/shape/Sector'; const PI2 = Math.PI * 2; const RADIAN = Math.PI / 180; @@ -36,6 +37,34 @@ function getViewRect(seriesModel: PieSeriesModel, api: ExtensionAPI) { ); } +export function getBasicPieLayout(seriesModel: PieSeriesModel, api: ExtensionAPI): + Pick { + const viewRect = getViewRect(seriesModel, api); + + let center = seriesModel.get('center'); + let radius = seriesModel.get('radius'); + + if (!zrUtil.isArray(radius)) { + radius = [0, radius]; + } + if (!zrUtil.isArray(center)) { + center = [center, center]; + } + const width = parsePercent(viewRect.width, api.getWidth()); + const height = parsePercent(viewRect.height, api.getHeight()); + const size = Math.min(width, height); + const cx = parsePercent(center[0], width) + viewRect.x; + const cy = parsePercent(center[1], height) + viewRect.y; + const r0 = parsePercent(radius[0], size / 2); + const r = parsePercent(radius[1], size / 2); + return { + cx, + cy, + r0, + r + }; +} + export default function pieLayout( seriesType: 'pie', ecModel: GlobalModel, @@ -46,23 +75,7 @@ export default function pieLayout( const valueDim = data.mapDimension('value'); const viewRect = getViewRect(seriesModel, api); - let center = seriesModel.get('center'); - let radius = seriesModel.get('radius'); - - if (!zrUtil.isArray(radius)) { - radius = [0, radius]; - } - if (!zrUtil.isArray(center)) { - center = [center, center]; - } - - const width = parsePercent(viewRect.width, api.getWidth()); - const height = parsePercent(viewRect.height, api.getHeight()); - const size = Math.min(width, height); - const cx = parsePercent(center[0], width) + viewRect.x; - const cy = parsePercent(center[1], height) + viewRect.y; - const r0 = parsePercent(radius[0], size / 2); - const r = parsePercent(radius[1], size / 2); + const { cx, cy, r, r0 } = getBasicPieLayout(seriesModel, api); const startAngle = -seriesModel.get('startAngle') * RADIAN; diff --git a/src/chart/radar/RadarSeries.ts b/src/chart/radar/RadarSeries.ts index f768195986..ffc8ea47d1 100644 --- a/src/chart/radar/RadarSeries.ts +++ b/src/chart/radar/RadarSeries.ts @@ -18,7 +18,7 @@ */ import SeriesModel from '../../model/Series'; -import createListSimply from '../helper/createListSimply'; +import createSeriesDataSimply from '../helper/createSeriesDataSimply'; import * as zrUtil from 'zrender/src/core/util'; import LegendVisualProvider from '../../visual/LegendVisualProvider'; import { @@ -35,7 +35,7 @@ import { CallbackDataParams } from '../../util/types'; import GlobalModel from '../../model/Global'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import Radar from '../../coord/radar/Radar'; import { createTooltipMarkup, retrieveVisualColorForTooltipMarker @@ -74,8 +74,6 @@ class RadarSeriesModel extends SeriesModel { coordinateSystem: Radar; - useColorPaletteOnData = true; - hasSymbolVisual = true; // Overwrite @@ -90,8 +88,8 @@ class RadarSeriesModel extends SeriesModel { } - getInitialData(option: RadarSeriesOption, ecModel: GlobalModel): List { - return createListSimply(this, { + getInitialData(option: RadarSeriesOption, ecModel: GlobalModel): SeriesData { + return createSeriesDataSimply(this, { generateCoord: 'indicator_', generateCoordCount: Infinity }); @@ -147,12 +145,14 @@ class RadarSeriesModel extends SeriesModel { static defaultOption: RadarSeriesOption = { zlevel: 0, z: 2, + colorBy: 'data', coordinateSystem: 'radar', legendHoverLink: true, radarIndex: 0, lineStyle: { width: 2, - type: 'solid' + type: 'solid', + join: 'round' }, label: { position: 'top' diff --git a/src/chart/radar/RadarView.ts b/src/chart/radar/RadarView.ts index 6060593b76..f38d1df866 100644 --- a/src/chart/radar/RadarView.ts +++ b/src/chart/radar/RadarView.ts @@ -24,19 +24,13 @@ import * as symbolUtil from '../../util/symbol'; import ChartView from '../../view/Chart'; import RadarSeriesModel, { RadarSeriesDataItemOption } from './RadarSeries'; import ExtensionAPI from '../../core/ExtensionAPI'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import { ColorString } from '../../util/types'; import GlobalModel from '../../model/Global'; import { VectorArray } from 'zrender/src/core/vector'; import { setLabelStyle, getLabelStatesModels } from '../../label/labelStyle'; import ZRImage from 'zrender/src/graphic/Image'; - -function normalizeSymbolSize(symbolSize: number | number[]) { - if (!zrUtil.isArray(symbolSize)) { - symbolSize = [+symbolSize, +symbolSize]; - } - return symbolSize; -} +import { saveOldStyle } from '../../animation/basicTrasition'; type RadarSymbol = ReturnType & { __dimIdx: number @@ -46,7 +40,7 @@ class RadarView extends ChartView { static type = 'radar'; type = RadarView.type; - private _data: List; + private _data: SeriesData; render(seriesModel: RadarSeriesModel, ecModel: GlobalModel, api: ExtensionAPI) { const polar = seriesModel.coordinateSystem; @@ -55,12 +49,12 @@ class RadarView extends ChartView { const data = seriesModel.getData(); const oldData = this._data; - function createSymbol(data: List, idx: number) { + function createSymbol(data: SeriesData, idx: number) { const symbolType = data.getItemVisual(idx, 'symbol') as string || 'circle'; if (symbolType === 'none') { return; } - const symbolSize = normalizeSymbolSize( + const symbolSize = symbolUtil.normalizeSymbolSize( data.getItemVisual(idx, 'symbolSize') ); const symbolPath = symbolUtil.createSymbol( @@ -83,7 +77,7 @@ class RadarView extends ChartView { oldPoints: VectorArray[], newPoints: VectorArray[], symbolGroup: graphic.Group, - data: List, + data: SeriesData, idx: number, isInit?: boolean ) { @@ -170,6 +164,9 @@ class RadarView extends ChartView { false ); + saveOldStyle(polygon); + saveOldStyle(polyline); + graphic.updateProps(polyline, target, seriesModel); graphic.updateProps(polygon, target, seriesModel); @@ -241,11 +238,12 @@ class RadarView extends ChartView { else { symbolPath.useStyle(itemStyle); symbolPath.setColor(color); + symbolPath.style.strokeNoScale = true; } const pathEmphasisState = symbolPath.ensureState('emphasis'); pathEmphasisState.style = zrUtil.clone(itemHoverStyle); - let defaultText = data.get(data.dimensions[symbolPath.__dimIdx], idx); + let defaultText = data.getStore().get(data.getDimensionIndex(symbolPath.__dimIdx), idx); (defaultText == null || isNaN(defaultText as number)) && (defaultText = ''); setLabelStyle( @@ -273,4 +271,4 @@ class RadarView extends ChartView { } } -export default RadarView; \ No newline at end of file +export default RadarView; diff --git a/src/chart/sankey/SankeySeries.ts b/src/chart/sankey/SankeySeries.ts index 64825cb1f6..6131f56e1a 100644 --- a/src/chart/sankey/SankeySeries.ts +++ b/src/chart/sankey/SankeySeries.ts @@ -36,7 +36,7 @@ import { DefaultEmphasisFocus } from '../../util/types'; import GlobalModel from '../../model/Global'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import { LayoutRect } from '../../util/layout'; import { createTooltipMarkup } from '../../component/tooltip/tooltipMarkup'; @@ -148,9 +148,6 @@ class SankeySeriesModel extends SeriesModel { /** * Init a graph data structure from data in option series - * - * @param {Object} option the object used to config echarts view - * @return {module:echarts/data/List} storage initial data */ getInitialData(option: SankeySeriesOption, ecModel: GlobalModel) { const links = option.edges || option.links; @@ -173,7 +170,7 @@ class SankeySeriesModel extends SeriesModel { const graph = createGraphFromNodeEdge(nodes, links, this, true, beforeLink); return graph.data; } - function beforeLink(nodeData: List, edgeData: List) { + function beforeLink(nodeData: SeriesData, edgeData: SeriesData) { nodeData.wrapMethod('getItemModel', function (model: Model, idx: number) { const seriesModel = model.parentModel as SankeySeriesModel; const layout = seriesModel.getData().getItemLayout(idx); @@ -204,7 +201,8 @@ class SankeySeriesModel extends SeriesModel { } setNodePosition(dataIndex: number, localPosition: number[]) { - const dataItem = this.option.data[dataIndex]; + const nodes = this.option.data || this.option.nodes; + const dataItem = nodes[dataIndex]; dataItem.localX = localPosition[0]; dataItem.localY = localPosition[1]; } diff --git a/src/chart/sankey/SankeyView.ts b/src/chart/sankey/SankeyView.ts index 480aeb5909..863f1c30c1 100644 --- a/src/chart/sankey/SankeyView.ts +++ b/src/chart/sankey/SankeyView.ts @@ -25,7 +25,7 @@ import SankeySeriesModel, { SankeyEdgeItemOption, SankeyNodeItemOption } from '. import ChartView from '../../view/Chart'; import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../core/ExtensionAPI'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import { RectLike } from 'zrender/src/core/BoundingRect'; import { setLabelStyle, getLabelStatesModels } from '../../label/labelStyle'; import { getECData } from '../../util/innerStore'; @@ -107,7 +107,7 @@ class SankeyView extends ChartView { private _focusAdjacencyDisabled = false; - private _data: List; + private _data: SeriesData; render(seriesModel: SankeySeriesModel, ecModel: GlobalModel, api: ExtensionAPI) { const sankeyView = this; @@ -207,7 +207,7 @@ class SankeyView extends ChartView { const sourceColor = edge.node1.getVisual('color'); const targetColor = edge.node2.getVisual('color'); if (typeof sourceColor === 'string' && typeof targetColor === 'string') { - curve.style.fill = new graphic.LinearGradient(0, 0, 1, 0, [{ + curve.style.fill = new graphic.LinearGradient(0, 0, +(orient === 'horizontal'), +(orient === 'vertical'), [{ color: sourceColor, offset: 0 }, { diff --git a/src/chart/scatter/ScatterSeries.ts b/src/chart/scatter/ScatterSeries.ts index 92efb41f47..40c4c73256 100644 --- a/src/chart/scatter/ScatterSeries.ts +++ b/src/chart/scatter/ScatterSeries.ts @@ -17,7 +17,7 @@ * under the License. */ -import createListFromArray from '../helper/createListFromArray'; +import createSeriesData from '../helper/createSeriesData'; import SeriesModel from '../../model/Series'; import { SeriesOption, @@ -39,7 +39,7 @@ import { DefaultEmphasisFocus } from '../../util/types'; import GlobalModel from '../../model/Global'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import { BrushCommonSelectorsForSeries } from '../../component/brush/selector'; interface ScatterStateOption { @@ -84,8 +84,8 @@ class ScatterSeriesModel extends SeriesModel { hasSymbolVisual = true; - getInitialData(option: ScatterSeriesOption, ecModel: GlobalModel): List { - return createListFromArray(this.getSource(), this, { + getInitialData(option: ScatterSeriesOption, ecModel: GlobalModel): SeriesData { + return createSeriesData(null, this, { useEncodeDefaulter: true }); } @@ -109,7 +109,7 @@ class ScatterSeriesModel extends SeriesModel { return progressiveThreshold; } - brushSelector(dataIndex: number, data: List, selectors: BrushCommonSelectorsForSeries): boolean { + brushSelector(dataIndex: number, data: SeriesData, selectors: BrushCommonSelectorsForSeries): boolean { return selectors.point(data.getItemLayout(dataIndex)); } @@ -144,6 +144,10 @@ class ScatterSeriesModel extends SeriesModel { itemStyle: { borderColor: '#212121' } + }, + + universalTransition: { + divideShape: 'clone' } // progressive: null }; diff --git a/src/chart/scatter/ScatterView.ts b/src/chart/scatter/ScatterView.ts index 4a7fbb0b4d..1bf6e3ca32 100644 --- a/src/chart/scatter/ScatterView.ts +++ b/src/chart/scatter/ScatterView.ts @@ -25,7 +25,7 @@ import ChartView from '../../view/Chart'; import ScatterSeriesModel from './ScatterSeries'; import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../core/ExtensionAPI'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import { TaskProgressParams } from '../../core/task'; import type { StageHandlerProgressExecutor } from '../../util/types'; @@ -99,7 +99,7 @@ class ScatterView extends ChartView { return seriesModel.get('clip', true) ? clipArea : null; } - _updateSymbolDraw(data: List, seriesModel: ScatterSeriesModel) { + _updateSymbolDraw(data: SeriesData, seriesModel: ScatterSeriesModel) { let symbolDraw = this._symbolDraw; const pipelineContext = seriesModel.pipelineContext; const isLargeDraw = pipelineContext.large; diff --git a/src/chart/sunburst/SunburstPiece.ts b/src/chart/sunburst/SunburstPiece.ts index 7f0dbe1620..b5cfd99684 100644 --- a/src/chart/sunburst/SunburstPiece.ts +++ b/src/chart/sunburst/SunburstPiece.ts @@ -31,6 +31,7 @@ import { getECData } from '../../util/innerStore'; import { getSectorCornerRadius } from '../helper/pieHelper'; import {createOrUpdatePatternFromDecal} from '../../util/decal'; import ExtensionAPI from '../../core/ExtensionAPI'; +import { saveOldStyle } from '../../animation/basicTrasition'; const DEFAULT_SECTOR_Z = 2; const DEFAULT_TEXT_Z = 4; @@ -98,7 +99,7 @@ class SunburstPiece extends graphic.Sector { normalStyle.decal = createOrUpdatePatternFromDecal(decal, api); } - const cornerRadius = getSectorCornerRadius(itemModel.getModel('itemStyle'), sectorShape); + const cornerRadius = getSectorCornerRadius(itemModel.getModel('itemStyle'), sectorShape, true); zrUtil.extend(sectorShape, cornerRadius); zrUtil.each(SPECIAL_STATES, function (stateName) { @@ -132,6 +133,8 @@ class SunburstPiece extends graphic.Sector { graphic.updateProps(sector, { shape: sectorShape }, seriesModel); + + saveOldStyle(sector); } sector.useStyle(normalStyle); @@ -278,4 +281,4 @@ class SunburstPiece extends graphic.Sector { } -export default SunburstPiece; \ No newline at end of file +export default SunburstPiece; diff --git a/src/chart/sunburst/SunburstSeries.ts b/src/chart/sunburst/SunburstSeries.ts index a1e38bf748..f53b7c3150 100644 --- a/src/chart/sunburst/SunburstSeries.ts +++ b/src/chart/sunburst/SunburstSeries.ts @@ -30,10 +30,11 @@ import { CallbackDataParams, StatesOptionMixin, OptionDataItemObject, - DefaultEmphasisFocus + DefaultEmphasisFocus, + SunburstColorByMixin } from '../../util/types'; import GlobalModel from '../../model/Global'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import Model from '../../model/Model'; import enableAriaDecalForTree from '../helper/enableAriaDecalForTree'; @@ -105,6 +106,7 @@ interface SortParam { } export interface SunburstSeriesOption extends SeriesOption, SunburstStateOption, + SunburstColorByMixin, CircleLayoutOptionMixin { type?: 'sunburst' @@ -164,7 +166,7 @@ class SunburstSeriesModel extends SeriesModel { // to choose mappings approach among old shapes and new shapes. const tree = Tree.createTree(root, this, beforeLink); - function beforeLink(nodeData: List) { + function beforeLink(nodeData: SeriesData) { nodeData.wrapMethod('getItemModel', function (model, idx) { const node = tree.getNodeByDataIndex(idx); const levelModel = levelModels[node.depth]; @@ -255,8 +257,6 @@ class SunburstSeriesModel extends SeriesModel { data: [], - levels: [], - /** * Sort order. * diff --git a/src/chart/sunburst/sunburstVisual.ts b/src/chart/sunburst/sunburstVisual.ts index de68e7a5e0..f43a961637 100644 --- a/src/chart/sunburst/sunburstVisual.ts +++ b/src/chart/sunburst/sunburstVisual.ts @@ -17,12 +17,18 @@ * under the License. */ +import { lift } from 'zrender/src/tool/color'; +import { extend, map } from 'zrender/src/core/util'; import GlobalModel from '../../model/Global'; import SunburstSeriesModel, { SunburstSeriesNodeItemOption } from './SunburstSeries'; -import { extend } from 'zrender/src/core/util'; -import { Dictionary, ColorString } from '../../util/types'; +import { Dictionary, ColorString, ZRColor } from '../../util/types'; +import { setItemVisualFromData } from '../../visual/helper'; import { TreeNode } from '../../data/Tree'; -import { lift } from 'zrender/src/tool/color'; + +type ParentColorMap = { + node: TreeNode, + parentColor: ZRColor +}; export default function sunburstVisual(ecModel: GlobalModel) { diff --git a/src/chart/themeRiver/ThemeRiverSeries.ts b/src/chart/themeRiver/ThemeRiverSeries.ts index 6eb4f721e5..4cd6c57fa2 100644 --- a/src/chart/themeRiver/ThemeRiverSeries.ts +++ b/src/chart/themeRiver/ThemeRiverSeries.ts @@ -18,9 +18,9 @@ */ import SeriesModel from '../../model/Series'; -import createDimensions from '../../data/helper/createDimensions'; +import prepareSeriesDataSchema from '../../data/helper/createDimensions'; import {getDimensionTypeByAxis} from '../../data/helper/dimensionHelper'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import * as zrUtil from 'zrender/src/core/util'; import {groupData, SINGLE_REFERRING} from '../../util/model'; import LegendVisualProvider from '../../visual/LegendVisualProvider'; @@ -81,8 +81,6 @@ class ThemeRiverSeriesModel extends SeriesModel { coordinateSystem: Single; - useColorPaletteOnData = true; - /** * @override */ @@ -154,7 +152,7 @@ class ThemeRiverSeriesModel extends SeriesModel { * @param option the initial option that user gived * @param ecModel the model object for themeRiver option */ - getInitialData(option: ThemeRiverSeriesOption, ecModel: GlobalModel): List { + getInitialData(option: ThemeRiverSeriesOption, ecModel: GlobalModel): SeriesData { const singleAxisModel = this.getReferringComponents('singleAxis', SINGLE_REFERRING).models[0]; @@ -179,7 +177,7 @@ class ThemeRiverSeriesModel extends SeriesModel { } } - const dimensionsInfo = createDimensions(data, { + const { dimensions } = prepareSeriesDataSchema(data, { coordDimensions: ['single'], dimensionsDefine: [ { @@ -202,7 +200,7 @@ class ThemeRiverSeriesModel extends SeriesModel { } }); - const list = new List(dimensionsInfo, this); + const list = new SeriesData(dimensions, this); list.initData(data); return list; @@ -293,6 +291,7 @@ class ThemeRiverSeriesModel extends SeriesModel { zlevel: 0, z: 2, + colorBy: 'data', coordinateSystem: 'singleAxis', // gap in axis's orthogonal orientation diff --git a/src/chart/themeRiver/ThemeRiverView.ts b/src/chart/themeRiver/ThemeRiverView.ts index c62e7897cd..ba0f149cbf 100644 --- a/src/chart/themeRiver/ThemeRiverView.ts +++ b/src/chart/themeRiver/ThemeRiverView.ts @@ -29,6 +29,7 @@ import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../core/ExtensionAPI'; import { RectLike } from 'zrender/src/core/BoundingRect'; import { ColorString } from '../../util/types'; +import { saveOldStyle } from '../../animation/basicTrasition'; type LayerSeries = ReturnType; @@ -134,6 +135,8 @@ class ThemeRiverView extends ChartView { stackedOnPoints: points1 } }, seriesModel); + + saveOldStyle(polygon); } setLabelStyle(polygon, getLabelStatesModels(seriesModel), { diff --git a/src/chart/themeRiver/themeRiverLayout.ts b/src/chart/themeRiver/themeRiverLayout.ts index 6de3e658d1..ec51217113 100644 --- a/src/chart/themeRiver/themeRiverLayout.ts +++ b/src/chart/themeRiver/themeRiverLayout.ts @@ -23,7 +23,7 @@ import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../core/ExtensionAPI'; import ThemeRiverSeriesModel, { ThemeRiverSeriesOption } from './ThemeRiverSeries'; import { RectLike } from 'zrender/src/core/BoundingRect'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; export interface ThemeRiverLayoutInfo { rect: RectLike @@ -75,7 +75,7 @@ export default function themeRiverLayout(ecModel: GlobalModel, api: ExtensionAPI * @param seriesModel the model object of themeRiver series * @param height value used to compute every series height */ -function doThemeRiverLayout(data: List, seriesModel: ThemeRiverSeriesModel, height: number) { +function doThemeRiverLayout(data: SeriesData, seriesModel: ThemeRiverSeriesModel, height: number) { if (!data.count()) { return; } diff --git a/src/chart/tree/TreeSeries.ts b/src/chart/tree/TreeSeries.ts index 2c197e444f..118ffb2fad 100644 --- a/src/chart/tree/TreeSeries.ts +++ b/src/chart/tree/TreeSeries.ts @@ -33,7 +33,7 @@ import { CallbackDataParams, DefaultEmphasisFocus } from '../../util/types'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import View from '../../coord/View'; import { LayoutRect } from '../../util/layout'; import Model from '../../model/Model'; @@ -141,10 +141,8 @@ class TreeSeriesModel extends SeriesModel { /** * Init a tree data structure from data in option series - * @param option the object used to config echarts view - * @return storage initial data */ - getInitialData(option: TreeSeriesOption): List { + getInitialData(option: TreeSeriesOption): SeriesData { //create an virtual root const root: TreeSeriesNodeItemOption = { @@ -157,7 +155,7 @@ class TreeSeriesModel extends SeriesModel { const tree = Tree.createTree(root, this, beforeLink); - function beforeLink(nodeData: List) { + function beforeLink(nodeData: SeriesData) { nodeData.wrapMethod('getItemModel', function (model, idx) { const node = tree.getNodeByDataIndex(idx); if (!(node && node.children.length && node.isExpand)) { diff --git a/src/chart/tree/TreeView.ts b/src/chart/tree/TreeView.ts index bcc8be8ee7..6f9b44aee7 100644 --- a/src/chart/tree/TreeView.ts +++ b/src/chart/tree/TreeView.ts @@ -34,9 +34,9 @@ import Path, { PathProps, PathStyleProps } from 'zrender/src/graphic/Path'; import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../core/ExtensionAPI'; import { TreeNode } from '../../data/Tree'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import { setStatesStylesFromModel, setStatesFlag, setDefaultStateProxy, HOVER_STATE_BLUR } from '../../util/states'; -import { ECElement } from '../../util/types'; +import { AnimationOption, ECElement } from '../../util/types'; type TreeSymbol = SymbolClz & { __edge: graphic.BezierCurve | TreePath @@ -134,7 +134,7 @@ class TreeView extends ChartView { private _controller: RoamController; private _controllerHost: RoamControllerHost; - private _data: List; + private _data: SeriesData; private _nodeScaleRatio: number; private _min: number[]; @@ -358,7 +358,7 @@ class TreeView extends ChartView { } -function symbolNeedsDraw(data: List, dataIndex: number) { +function symbolNeedsDraw(data: SeriesData, dataIndex: number) { const layout = data.getItemLayout(dataIndex); return layout @@ -367,7 +367,7 @@ function symbolNeedsDraw(data: List, dataIndex: number) { function updateNode( - data: List, + data: SeriesData, dataIndex: number, symbolEl: TreeSymbol, group: graphic.Group, @@ -595,44 +595,21 @@ function drawEdge( } } -function removeNode( - data: List, - dataIndex: number, - symbolEl: TreeSymbol, +function removeNodeEdge( + node: TreeNode, + data: SeriesData, group: graphic.Group, - seriesModel: TreeSeriesModel + seriesModel: TreeSeriesModel, + removeAnimationOpt: AnimationOption ) { - const node = data.tree.getNodeByDataIndex(dataIndex); const virtualRoot = data.tree.root; + const { source, sourceLayout } = getSourceNode(virtualRoot, node); - let source = node.parentNode === virtualRoot ? node : node.parentNode || node; - // let edgeShape = seriesScope.edgeShape; - let sourceLayout; - while (sourceLayout = source.getLayout(), sourceLayout == null) { - source = source.parentNode === virtualRoot ? source : source.parentNode || source; - } - - // Use same duration and easing with update to have more consistent animation. - const removeAnimationOpt = { - duration: seriesModel.get('animationDurationUpdate') as number, - easing: seriesModel.get('animationEasingUpdate') - }; + const symbolEl: TreeSymbol = data.getItemGraphicEl(node.dataIndex) as TreeSymbol; - graphic.removeElement(symbolEl, { - x: sourceLayout.x + 1, - y: sourceLayout.y + 1 - }, seriesModel, { - cb() { - group.remove(symbolEl); - data.setItemGraphicEl(dataIndex, null); - }, - removeOpt: removeAnimationOpt - }); - - symbolEl.fadeOut(null, { - fadeLabel: true, - animation: removeAnimationOpt - }); + if (!symbolEl) { + return; + } const sourceSymbolEl = data.getItemGraphicEl(source.dataIndex) as TreeSymbol; const sourceEdge = sourceSymbolEl.__edge; @@ -688,6 +665,60 @@ function removeNode( } } +function getSourceNode(virtualRoot: TreeNode, node: TreeNode): { source: TreeNode, sourceLayout: TreeNodeLayout } { + let source = node.parentNode === virtualRoot ? node : node.parentNode || node; + let sourceLayout; + while (sourceLayout = source.getLayout(), sourceLayout == null) { + source = source.parentNode === virtualRoot ? source : source.parentNode || source; + } + return { + source, + sourceLayout + }; +} + +function removeNode( + data: SeriesData, + dataIndex: number, + symbolEl: TreeSymbol, + group: graphic.Group, + seriesModel: TreeSeriesModel +) { + const node = data.tree.getNodeByDataIndex(dataIndex); + const virtualRoot = data.tree.root; + + const { sourceLayout } = getSourceNode(virtualRoot, node); + + // Use same duration and easing with update to have more consistent animation. + const removeAnimationOpt = { + duration: seriesModel.get('animationDurationUpdate') as number, + easing: seriesModel.get('animationEasingUpdate') + }; + + graphic.removeElement(symbolEl, { + x: sourceLayout.x + 1, + y: sourceLayout.y + 1 + }, seriesModel, { + cb() { + group.remove(symbolEl); + data.setItemGraphicEl(dataIndex, null); + }, + removeOpt: removeAnimationOpt + }); + + symbolEl.fadeOut(null, { + fadeLabel: true, + animation: removeAnimationOpt + }); + + // remove edge as parent node + node.children.forEach(childNode => { + removeNodeEdge(childNode, data, group, seriesModel, removeAnimationOpt); + }); + // remove edge as child node + removeNodeEdge(node, data, group, seriesModel, removeAnimationOpt); +} + function getEdgeShape( layoutOpt: TreeSeriesOption['layout'], orient: TreeSeriesOption['orient'], diff --git a/src/chart/treemap/TreemapSeries.ts b/src/chart/treemap/TreemapSeries.ts index fab2fc3257..287540e9c4 100644 --- a/src/chart/treemap/TreemapSeries.ts +++ b/src/chart/treemap/TreemapSeries.ts @@ -36,11 +36,12 @@ import { DecalObject, SeriesLabelOption, DefaultEmphasisFocus, - AriaOptionMixin + AriaOptionMixin, + ColorBy } from '../../util/types'; import GlobalModel from '../../model/Global'; import { LayoutRect } from '../../util/layout'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import { normalizeToArray } from '../../util/model'; import { createTooltipMarkup } from '../../component/tooltip/tooltipMarkup'; import enableAriaDecalForTree from '../helper/enableAriaDecalForTree'; @@ -76,7 +77,12 @@ interface TreePathInfo { } interface TreemapSeriesCallbackDataParams extends CallbackDataParams { + /** + * @deprecated + */ treePathInfo?: TreePathInfo[] + + treeAncestors?: TreePathInfo[] } interface ExtraStateOption { @@ -97,6 +103,9 @@ export interface TreemapSeriesVisualOption { */ visualDimension?: number | string + /** + * @deprecated Use colorBy instead + */ colorMappingBy?: 'value' | 'index' | 'id' visualMin?: number @@ -140,7 +149,8 @@ export interface TreemapSeriesNodeItemOption extends TreemapSeriesVisualOption, } export interface TreemapSeriesOption - extends SeriesOption, TreemapStateOption, + extends SeriesOption, + TreemapStateOption, BoxLayoutOptionMixin, RoamOptionMixin, TreemapSeriesVisualOption { @@ -190,7 +200,7 @@ export interface TreemapSeriesOption show?: boolean height?: number - emptyItemWidth: number // With of empty width + emptyItemWidth?: number // With of empty width itemStyle?: BreadcrumbItemStyleOption emphasis?: { @@ -367,7 +377,7 @@ class TreemapSeriesModel extends SeriesModel { // to choose mappings approach among old shapes and new shapes. const tree = Tree.createTree(root, this, beforeLink); - function beforeLink(nodeData: List) { + function beforeLink(nodeData: SeriesData) { nodeData.wrapMethod('getItemModel', function (model, idx) { const node = tree.getNodeByDataIndex(idx); const levelModel = node ? levelModels[node.depth] : null; @@ -412,7 +422,9 @@ class TreemapSeriesModel extends SeriesModel { const params = super.getDataParams.apply(this, arguments as any) as TreemapSeriesCallbackDataParams; const node = this.getData().tree.getNodeByDataIndex(dataIndex); - params.treePathInfo = wrapTreePathInfo(node, this); + params.treeAncestors = wrapTreePathInfo(node, this); + // compatitable the previous code. + params.treePathInfo = params.treeAncestors; return params; } @@ -575,4 +587,4 @@ function setDefault(levels: TreemapSeriesLevelOption[], ecModel: GlobalModel) { return levels; } -export default TreemapSeriesModel; \ No newline at end of file +export default TreemapSeriesModel; diff --git a/src/chart/treemap/TreemapView.ts b/src/chart/treemap/TreemapView.ts index 1947317496..b9e2e9c72b 100644 --- a/src/chart/treemap/TreemapView.ts +++ b/src/chart/treemap/TreemapView.ts @@ -202,9 +202,11 @@ class TreemapView extends ChartView { : null; const containerGroup = this._giveContainerGroup(layoutInfo); + const hasAnimation = seriesModel.get('animation'); const renderResult = this._doRender(containerGroup, seriesModel, reRoot); ( + hasAnimation && !isInit && ( !payloadType || payloadType === 'treemapZoomToNode' @@ -357,10 +359,6 @@ class TreemapView extends ChartView { seriesModel: TreemapSeriesModel, reRoot: ReRoot ) { - if (!seriesModel.get('animation')) { - return; - } - const durationOption = seriesModel.get('animationDurationUpdate'); const easingOption = seriesModel.get('animationEasing'); // TODO: do not support function until necessary. @@ -830,6 +828,8 @@ function renderNode( const content = giveGraphic('content', Rect, depth, Z2_CONTENT); content && renderContent(group, content); + (bg as ECElement).disableMorphing = true; + if (bg && isHighDownDispatcher(bg)) { setAsHighDownDispatcher(bg, false); } @@ -980,6 +980,9 @@ function renderNode( ); const textEl = rectEl.getTextContent(); + if (!textEl) { + return; + } const textStyle = textEl.style; const textPadding = normalizeCssArray(textStyle.padding || 0); @@ -1104,4 +1107,4 @@ function calculateZ2(depth: number, z2InLevel: number) { return depth * Z2_BASE + z2InLevel; } -export default TreemapView; \ No newline at end of file +export default TreemapView; diff --git a/src/component/axis/AxisBuilder.ts b/src/component/axis/AxisBuilder.ts index 9dfaa99f92..c262931039 100644 --- a/src/component/axis/AxisBuilder.ts +++ b/src/component/axis/AxisBuilder.ts @@ -23,7 +23,7 @@ import {getECData} from '../../util/innerStore'; import {createTextStyle} from '../../label/labelStyle'; import Model from '../../model/Model'; import {isRadianAroundZero, remRadian} from '../../util/number'; -import {createSymbol} from '../../util/symbol'; +import {createSymbol, normalizeSymbolOffset} from '../../util/symbol'; import * as matrixUtil from 'zrender/src/core/matrix'; import {applyTransform as v2ApplyTransform} from 'zrender/src/core/vector'; import {shouldShowAllLabels} from '../../coord/axisHelper'; @@ -34,7 +34,6 @@ import Element from 'zrender/src/Element'; import { PathStyleProps } from 'zrender/src/graphic/Path'; import OrdinalScale from '../../scale/Ordinal'; - const PI = Math.PI; type AxisIndexKey = 'xAxisIndex' | 'yAxisIndex' | 'radiusAxisIndex' @@ -283,14 +282,10 @@ const builders: Record<'axisLine' | 'axisTickLabel' | 'axisName', AxisElementsBu group.add(line); let arrows = axisModel.get(['axisLine', 'symbol']); - let arrowSize = axisModel.get(['axisLine', 'symbolSize']); - - let arrowOffset = axisModel.get(['axisLine', 'symbolOffset']) || 0; - if (typeof arrowOffset === 'number') { - arrowOffset = [arrowOffset, arrowOffset]; - } if (arrows != null) { + let arrowSize = axisModel.get(['axisLine', 'symbolSize']); + if (typeof arrows === 'string') { // Use the same arrow for start and end point arrows = [arrows, arrows]; @@ -302,6 +297,8 @@ const builders: Record<'axisLine' | 'axisTickLabel' | 'axisName', AxisElementsBu arrowSize = [arrowSize, arrowSize]; } + const arrowOffset = normalizeSymbolOffset(axisModel.get(['axisLine', 'symbolOffset']) || 0, arrowSize); + const symbolWidth = arrowSize[0]; const symbolHeight = arrowSize[1]; @@ -823,4 +820,4 @@ function buildAxisLabel( } -export default AxisBuilder; \ No newline at end of file +export default AxisBuilder; diff --git a/src/component/axis/CartesianAxisView.ts b/src/component/axis/CartesianAxisView.ts index 60bb844380..20005749f8 100644 --- a/src/component/axis/CartesianAxisView.ts +++ b/src/component/axis/CartesianAxisView.ts @@ -90,7 +90,14 @@ class CartesianAxisView extends AxisView { } }, this); - graphic.groupTransition(oldAxisGroup, this._axisGroup, axisModel); + // THIS is a special case for bar racing chart. + // Update the axis label from the natural initial layout to + // sorted layout should has no animation. + const isInitialSortFromBarRacing = payload && payload.type === 'changeAxisOrder' && payload.isInitSort; + + if (!isInitialSortFromBarRacing) { + graphic.groupTransition(oldAxisGroup, this._axisGroup, axisModel); + } super.render(axisModel, ecModel, api, payload); } diff --git a/src/component/dataZoom/AxisProxy.ts b/src/component/dataZoom/AxisProxy.ts index 2d7b0f8a52..e7be785fca 100644 --- a/src/component/dataZoom/AxisProxy.ts +++ b/src/component/dataZoom/AxisProxy.ts @@ -245,16 +245,6 @@ class AxisProxy { // Culculate data window and data extent, and record them. this._dataExtent = calculateDataExtent(this, this._dimName, targetSeries); - // this.hasSeriesStacked = false; - // each(targetSeries, function (series) { - // let data = series.getData(); - // let dataDim = data.mapDimension(this._dimName); - // let stackedDimension = data.getCalculationInfo('stackedDimension'); - // if (stackedDimension && stackedDimension === dataDim) { - // this.hasSeriesStacked = true; - // } - // }, this); - // `calculateDataWindow` uses min/maxSpan. this._updateMinMaxSpan(); @@ -311,12 +301,14 @@ class AxisProxy { } if (filterMode === 'weakFilter') { + const store = seriesData.getStore(); + const dataDimIndices = zrUtil.map(dataDims, dim => seriesData.getDimensionIndex(dim), seriesData); seriesData.filterSelf(function (dataIndex) { let leftOut; let rightOut; let hasValue; for (let i = 0; i < dataDims.length; i++) { - const value = seriesData.get(dataDims[i], dataIndex) as number; + const value = store.get(dataDimIndices[i], dataIndex) as number; const thisHasValue = !isNaN(value); const thisLeftOut = value < valueWindow[0]; const thisRightOut = value > valueWindow[1]; diff --git a/src/component/dataZoom/SliderZoomView.ts b/src/component/dataZoom/SliderZoomView.ts index 83039ec495..b677224785 100644 --- a/src/component/dataZoom/SliderZoomView.ts +++ b/src/component/dataZoom/SliderZoomView.ts @@ -57,7 +57,8 @@ const SHOW_DATA_SHADOW_SERIES_TYPE = ['line', 'bar', 'candlestick', 'scatter']; const REALTIME_ANIMATION_CONFIG = { easing: 'cubicOut', - duration: 100 + duration: 100, + delay: 0 } as const; // const NORMAL_ANIMATION_CONFIG = { diff --git a/src/component/geo/GeoView.ts b/src/component/geo/GeoView.ts index 0db6d36ec0..4d967878eb 100644 --- a/src/component/geo/GeoView.ts +++ b/src/component/geo/GeoView.ts @@ -42,30 +42,28 @@ class GeoView extends ComponentView { focusBlurEnabled = true; init(ecModel: GlobalModel, api: ExtensionAPI) { - const mapDraw = new MapDraw(api); - this._mapDraw = mapDraw; - - this.group.add(mapDraw.group); - this._api = api; } render( geoModel: GeoModel, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload ): void { - const mapDraw = this._mapDraw; - if (geoModel.get('show')) { - mapDraw.draw(geoModel, ecModel, api, this, payload); - } - else { - this._mapDraw.group.removeAll(); + this._model = geoModel; + + if (!geoModel.get('show')) { + this._mapDraw && this._mapDraw.remove(); + this._mapDraw = null; + return; } + if (!this._mapDraw) { + this._mapDraw = new MapDraw(api); + } + const mapDraw = this._mapDraw; + mapDraw.draw(geoModel, ecModel, api, this, payload); mapDraw.group.on('click', this._handleRegionClick, this); mapDraw.group.silent = geoModel.get('silent'); - - this._model = geoModel; - + this.group.add(mapDraw.group); this.updateSelectStatus(geoModel, ecModel, api); } diff --git a/src/component/helper/BrushController.ts b/src/component/helper/BrushController.ts index 9320ece4ae..647f17c9e6 100644 --- a/src/component/helper/BrushController.ts +++ b/src/component/helper/BrushController.ts @@ -186,7 +186,9 @@ export interface BrushControllerEvents { * removeOnClick: boolean * } */ -class BrushController extends Eventful { +class BrushController extends Eventful<{ + [key in keyof BrushControllerEvents]: (params: BrushControllerEvents[key]) => void | undefined +}> { readonly group: graphic.Group; diff --git a/src/component/helper/MapDraw.ts b/src/component/helper/MapDraw.ts index 31ff42d239..56bf2c54bc 100644 --- a/src/component/helper/MapDraw.ts +++ b/src/component/helper/MapDraw.ts @@ -46,7 +46,7 @@ import { ViewCoordSysTransformInfoPart } from '../../coord/View'; import { GeoSVGGraphicRecord, GeoSVGResource } from '../../coord/geo/GeoSVGResource'; import Displayable from 'zrender/src/graphic/Displayable'; import Element from 'zrender/src/Element'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import { GeoJSONRegion } from '../../coord/geo/Region'; import { SVGNodeTagLower } from 'zrender/src/tool/parseSVG'; import { makeInner } from '../../util/model'; @@ -62,7 +62,7 @@ interface ViewBuildContext { api: ExtensionAPI; geo: Geo; mapOrGeoModel: GeoModel | MapSeries; - data: List; + data: SeriesData; isVisualEncodedByVisualMap: boolean; isGeo: boolean; transformInfoRaw: ViewCoordSysTransformInfoPart; diff --git a/src/component/helper/RoamController.ts b/src/component/helper/RoamController.ts index 4b12a6a10e..edff61052d 100644 --- a/src/component/helper/RoamController.ts +++ b/src/component/helper/RoamController.ts @@ -43,7 +43,7 @@ type RoamEventType = keyof RoamEventParams; type RoamBehavior = 'zoomOnMouseWheel' | 'moveOnMouseMove' | 'moveOnMouseWheel'; -export type RoamEventParams = { +export interface RoamEventParams { 'zoom': { scale: number originX: number @@ -79,7 +79,9 @@ export interface RoamControllerHost { } } -class RoamController extends Eventful { +class RoamController extends Eventful<{ + [key in keyof RoamEventParams]: (params: RoamEventParams[key]) => void | undefined +}> { pointerChecker: (e: ZRElementEvent, x: number, y: number) => boolean; @@ -309,7 +311,8 @@ function trigger( // Also provide behavior checker for event listener, for some case that // multiple components share one listener. contollerEvent.isAvailableBehavior = bind(isAvailableBehavior, null, behaviorToCheck, e); - controller.trigger(eventName, contollerEvent); + // TODO should not have type issue. + (controller as any).trigger(eventName, contollerEvent); } // settings: { diff --git a/src/component/legend/LegendModel.ts b/src/component/legend/LegendModel.ts index c03f1002d5..9026471423 100644 --- a/src/component/legend/LegendModel.ts +++ b/src/component/legend/LegendModel.ts @@ -46,13 +46,13 @@ const getDefaultSelectorOptions = function (ecModel: GlobalModel, type: string): if (type === 'all') { return { type: 'all', - title: ecModel.getLocale(['legend', 'selector', 'all']) + title: ecModel.getLocaleModel().get(['legend', 'selector', 'all']) }; } else if (type === 'inverse') { return { type: 'inverse', - title: ecModel.getLocale(['legend', 'selector', 'inverse']) + title: ecModel.getLocaleModel().get(['legend', 'selector', 'inverse']) }; } }; diff --git a/src/component/marker/MarkAreaView.ts b/src/component/marker/MarkAreaView.ts index bb20679489..e23403cbfe 100644 --- a/src/component/marker/MarkAreaView.ts +++ b/src/component/marker/MarkAreaView.ts @@ -20,19 +20,19 @@ // TODO Optimize on polar import * as colorUtil from 'zrender/src/tool/color'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import * as numberUtil from '../../util/number'; import * as graphic from '../../util/graphic'; import { enableHoverEmphasis, setStatesStylesFromModel } from '../../util/states'; import * as markerHelper from './markerHelper'; import MarkerView from './MarkerView'; -import { retrieve, mergeAll, map, defaults, curry, filter, HashMap } from 'zrender/src/core/util'; +import { retrieve, mergeAll, map, curry, filter, HashMap, extend } from 'zrender/src/core/util'; import { ScaleDataValue, ParsedValue, ZRColor } from '../../util/types'; import { CoordinateSystem, isCoordinateSystemType } from '../../coord/CoordinateSystem'; import MarkAreaModel, { MarkArea2DDataItemOption } from './MarkAreaModel'; import SeriesModel from '../../model/Series'; import Cartesian2D from '../../coord/cartesian/Cartesian2D'; -import DataDimensionInfo from '../../data/DataDimensionInfo'; +import SeriesDimensionDefine from '../../data/SeriesDimensionDefine'; import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../core/ExtensionAPI'; import MarkerModel from './MarkerModel'; @@ -47,7 +47,7 @@ interface MarkAreaDrawGroup { } const inner = makeInner<{ - data: List + data: SeriesData }, MarkAreaDrawGroup>(); // Merge two ends option into one. @@ -137,7 +137,7 @@ function markAreaFilter(coordSys: CoordinateSystem, item: MarkAreaMergedItemOpti // dims can be ['x0', 'y0'], ['x1', 'y1'], ['x0', 'y1'], ['x1', 'y0'] function getSingleMarkerEndPoint( - data: List, + data: SeriesData, idx: number, dims: typeof dimPermutations[number], seriesModel: SeriesModel, @@ -362,8 +362,8 @@ function createList( maModel: MarkAreaModel ) { - let coordDimsInfos: DataDimensionInfo[]; - let areaData: List; + let coordDimsInfos: SeriesDimensionDefine[]; + let areaData: SeriesData; const dims = ['x0', 'y0', 'x1', 'y1']; if (coordSys) { coordDimsInfos = map(coordSys && coordSys.dimensions, function (coordDim) { @@ -372,11 +372,13 @@ function createList( data.mapDimension(coordDim) ) || {}; // In map series data don't have lng and lat dimension. Fallback to same with coordSys - return defaults({ - name: coordDim - }, info); + return extend(extend({}, info), { + name: coordDim, + // DON'T use ordinalMeta to parse and collect ordinal. + ordinalMeta: null + }); }); - areaData = new List(map(dims, function (dim, idx) { + areaData = new SeriesData(map(dims, function (dim, idx) { return { name: dim, type: coordDimsInfos[idx % 2].type @@ -388,7 +390,7 @@ function createList( name: 'value', type: 'float' }]; - areaData = new List(coordDimsInfos, maModel); + areaData = new SeriesData(coordDimsInfos, maModel); } let optData = map(maModel.get('data'), curry( diff --git a/src/component/marker/MarkLineView.ts b/src/component/marker/MarkLineView.ts index a1fc0e9e7f..212ce68ced 100644 --- a/src/component/marker/MarkLineView.ts +++ b/src/component/marker/MarkLineView.ts @@ -17,7 +17,7 @@ * under the License. */ -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import * as numberUtil from '../../util/number'; import * as markerHelper from './markerHelper'; import LineDraw from '../../chart/helper/LineDraw'; @@ -57,9 +57,9 @@ type MarkLineMergedItemOption = MarkLine2DDataItemOption[number]; const inner = makeInner<{ // from data - from: List + from: SeriesData // to data - to: List + to: SeriesData }, MarkLineModel>(); const markLineTransform = function ( @@ -196,7 +196,7 @@ function markLineFilter( } function updateSingleMarkerEndLayout( - data: List, + data: SeriesData, idx: number, isFrom: boolean, seriesModel: SeriesModel, @@ -311,17 +311,22 @@ class MarkLineView extends MarkerView { const fromData = mlData.from; const toData = mlData.to; - const lineData = mlData.line as List; + const lineData = mlData.line as SeriesData; inner(mlModel).from = fromData; inner(mlModel).to = toData; // Line data for tooltip and formatter mlModel.setData(lineData); + // TODO + // Functionally, `symbolSize` & `symbolOffset` can also be 2D array now. + // But the related logic and type definition are not finished yet. + // Finish it if required let symbolType = mlModel.get('symbol'); let symbolSize = mlModel.get('symbolSize'); let symbolRotate = mlModel.get('symbolRotate'); let symbolOffset = mlModel.get('symbolOffset'); + // TODO: support callback function like markPoint if (!isArray(symbolType)) { symbolType = [symbolType, symbolType]; } @@ -383,7 +388,7 @@ class MarkLineView extends MarkerView { }); function updateDataVisualAndLayout( - data: List, + data: SeriesData, idx: number, isFrom: boolean ) { @@ -402,13 +407,14 @@ class MarkLineView extends MarkerView { symbolKeepAspect: itemModel.get('symbolKeepAspect'), // `0` should be considered as a valid value, so use `retrieve2` instead of `||` symbolOffset: retrieve2( - itemModel.get('symbolOffset'), + itemModel.get('symbolOffset', true), (symbolOffset as (string | number)[])[isFrom ? 0 : 1] ), symbolRotate: retrieve2( itemModel.get('symbolRotate', true), (symbolRotate as number[])[isFrom ? 0 : 1] ), + // TODO: when 2d array is supported, it should ignore parent symbolSize: retrieve2( itemModel.get('symbolSize'), (symbolSize as number[])[isFrom ? 0 : 1] @@ -436,7 +442,11 @@ function createList(coordSys: CoordinateSystem, seriesModel: SeriesModel, mlMode seriesModel.getData().mapDimension(coordDim) ) || {}; // In map series data don't have lng and lat dimension. Fallback to same with coordSys - return defaults({name: coordDim}, info); + return extend(extend({}, info), { + name: coordDim, + // DON'T use ordinalMeta to parse and collect ordinal. + ordinalMeta: null + }); }); } else { @@ -446,10 +456,10 @@ function createList(coordSys: CoordinateSystem, seriesModel: SeriesModel, mlMode }]; } - const fromData = new List(coordDimsInfos, mlModel); - const toData = new List(coordDimsInfos, mlModel); + const fromData = new SeriesData(coordDimsInfos, mlModel); + const toData = new SeriesData(coordDimsInfos, mlModel); // No dimensions - const lineData = new List([], mlModel); + const lineData = new SeriesData([], mlModel); let optData = map(mlModel.get('data'), curry( markLineTransform, seriesModel, coordSys, mlModel diff --git a/src/component/marker/MarkPointView.ts b/src/component/marker/MarkPointView.ts index 82f1d69864..f0693da1fe 100644 --- a/src/component/marker/MarkPointView.ts +++ b/src/component/marker/MarkPointView.ts @@ -20,7 +20,7 @@ import SymbolDraw from '../../chart/helper/SymbolDraw'; import * as numberUtil from '../../util/number'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import * as markerHelper from './markerHelper'; import MarkerView from './MarkerView'; import { CoordinateSystem } from '../../coord/CoordinateSystem'; @@ -29,13 +29,13 @@ import MarkPointModel, {MarkPointDataItemOption} from './MarkPointModel'; import GlobalModel from '../../model/Global'; import MarkerModel from './MarkerModel'; import ExtensionAPI from '../../core/ExtensionAPI'; -import { HashMap, isFunction, map, defaults, filter, curry } from 'zrender/src/core/util'; +import { HashMap, isFunction, map, defaults, filter, curry, extend } from 'zrender/src/core/util'; import { getECData } from '../../util/innerStore'; import { getVisualFromData } from '../../visual/helper'; import { ZRColor } from '../../util/types'; function updateMarkerLayout( - mpData: List, + mpData: SeriesData, seriesModel: SeriesModel, api: ExtensionAPI ) { @@ -59,7 +59,6 @@ function updateMarkerLayout( const x = mpData.get(coordSys.dimensions[0], idx); const y = mpData.get(coordSys.dimensions[1], idx); point = coordSys.dataToPoint([x, y]); - } // Use x, y if has any @@ -108,7 +107,7 @@ class MarkPointView extends MarkerView { const symbolDraw = symbolDrawMap.get(seriesId) || symbolDrawMap.set(seriesId, new SymbolDraw()); - const mpData = createList(coordSys, seriesModel, mpModel); + const mpData = createData(coordSys, seriesModel, mpModel); // FIXME mpModel.setData(mpData); @@ -120,8 +119,11 @@ class MarkPointView extends MarkerView { let symbol = itemModel.getShallow('symbol'); let symbolSize = itemModel.getShallow('symbolSize'); let symbolRotate = itemModel.getShallow('symbolRotate'); + let symbolOffset = itemModel.getShallow('symbolOffset'); + const symbolKeepAspect = itemModel.getShallow('symbolKeepAspect'); - if (isFunction(symbol) || isFunction(symbolSize) || isFunction(symbolRotate)) { + // TODO: refactor needed: single data item should not support callback function + if (isFunction(symbol) || isFunction(symbolSize) || isFunction(symbolRotate) || isFunction(symbolOffset)) { const rawIdx = mpModel.getRawValue(idx); const dataParams = mpModel.getDataParams(idx); if (isFunction(symbol)) { @@ -134,6 +136,9 @@ class MarkPointView extends MarkerView { if (isFunction(symbolRotate)) { symbolRotate = symbolRotate(rawIdx, dataParams); } + if (isFunction(symbolOffset)) { + symbolOffset = symbolOffset(rawIdx, dataParams); + } } const style = itemModel.getModel('itemStyle').getItemStyle(); @@ -146,6 +151,8 @@ class MarkPointView extends MarkerView { symbol: symbol, symbolSize: symbolSize, symbolRotate: symbolRotate, + symbolOffset: symbolOffset, + symbolKeepAspect: symbolKeepAspect, style }); }); @@ -168,7 +175,7 @@ class MarkPointView extends MarkerView { } } -function createList( +function createData( coordSys: CoordinateSystem, seriesModel: SeriesModel, mpModel: MarkPointModel @@ -180,7 +187,11 @@ function createList( seriesModel.getData().mapDimension(coordDim) ) || {}; // In map series data don't have lng and lat dimension. Fallback to same with coordSys - return defaults({name: coordDim}, info); + return extend(extend({}, info), { + name: coordDim, + // DON'T use ordinalMeta to parse and collect ordinal. + ordinalMeta: null + }); }); } else { @@ -190,7 +201,7 @@ function createList( }]; } - const mpData = new List(coordDimsInfos, mpModel); + const mpData = new SeriesData(coordDimsInfos, mpModel); let dataOpt = map(mpModel.get('data'), curry( markerHelper.dataTransform, seriesModel )); @@ -209,4 +220,4 @@ function createList( return mpData; } -export default MarkPointView; \ No newline at end of file +export default MarkPointView; diff --git a/src/component/marker/MarkerModel.ts b/src/component/marker/MarkerModel.ts index 341bf5b347..6a68558cab 100644 --- a/src/component/marker/MarkerModel.ts +++ b/src/component/marker/MarkerModel.ts @@ -32,7 +32,7 @@ import { } from '../../util/types'; import Model from '../../model/Model'; import GlobalModel from '../../model/Global'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import { makeInner, defaultEmphasis } from '../../util/model'; import { createTooltipMarkup } from '../tooltip/tooltipMarkup'; @@ -109,7 +109,7 @@ abstract class MarkerModel extends Com __hostSeries: SeriesModel; - private _data: List; + private _data: SeriesData; /** * @overrite @@ -217,11 +217,11 @@ abstract class MarkerModel extends Com }); } - getData(): List { - return this._data as List; + getData(): SeriesData { + return this._data as SeriesData; } - setData(data: List) { + setData(data: SeriesData) { this._data = data; } diff --git a/src/component/marker/markerHelper.ts b/src/component/marker/markerHelper.ts index 4ae4b7573b..0987473a59 100644 --- a/src/component/marker/markerHelper.ts +++ b/src/component/marker/markerHelper.ts @@ -20,18 +20,18 @@ import * as numberUtil from '../../util/number'; import {isDimensionStacked} from '../../data/helper/dataStackHelper'; import SeriesModel from '../../model/Series'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import { MarkerStatisticType, MarkerPositionOption } from './MarkerModel'; import { indexOf, curry, clone, isArray } from 'zrender/src/core/util'; import Axis from '../../coord/Axis'; import { CoordinateSystem } from '../../coord/CoordinateSystem'; -import { ScaleDataValue, ParsedValue } from '../../util/types'; +import { ScaleDataValue, ParsedValue, DimensionLoose, DimensionName } from '../../util/types'; interface MarkerAxisInfo { - valueDataDim: string + valueDataDim: DimensionName valueAxis: Axis baseAxis: Axis - baseDataDim: string + baseDataDim: DimensionName } function hasXOrY(item: MarkerPositionOption) { @@ -42,33 +42,9 @@ function hasXAndY(item: MarkerPositionOption) { return !isNaN(parseFloat(item.x as string)) && !isNaN(parseFloat(item.y as string)); } -// Make it simple, do not visit all stacked value to count precision. -// function getPrecision(data, valueAxisDim, dataIndex) { -// let precision = -1; -// let stackedDim = data.mapDimension(valueAxisDim); -// do { -// precision = Math.max( -// numberUtil.getPrecision(data.get(stackedDim, dataIndex)), -// precision -// ); -// let stackedOnSeries = data.getCalculationInfo('stackedOnSeries'); -// if (stackedOnSeries) { -// let byValue = data.get(data.getCalculationInfo('stackedByDimension'), dataIndex); -// data = stackedOnSeries.getData(); -// dataIndex = data.indexOf(data.getCalculationInfo('stackedByDimension'), byValue); -// stackedDim = data.getCalculationInfo('stackedDimension'); -// } -// else { -// data = null; -// } -// } while (data); - -// return precision; -// } - function markerTypeCalculatorWithExtent( markerType: MarkerStatisticType, - data: List, + data: SeriesData, otherDataDim: string, targetDataDim: string, otherCoordIndex: number, @@ -109,10 +85,6 @@ const markerTypeCalculator = { * Transform markPoint data item to format used in List by do the following * 1. Calculate statistic like `max`, `min`, `average` * 2. Convert `item.xAxis`, `item.yAxis` to `item.coord` array - * @param {module:echarts/model/Series} seriesModel - * @param {module:echarts/coord/*} [coordSys] - * @param {Object} item - * @return {Object} */ export function dataTransform( seriesModel: SeriesModel, @@ -171,7 +143,7 @@ export function dataTransform( export function getAxisInfo( item: MarkerPositionOption, - data: List, + data: SeriesData, coordSys: CoordinateSystem, seriesModel: SeriesModel ) { @@ -194,16 +166,9 @@ export function getAxisInfo( return ret; } -function dataDimToCoordDim(seriesModel: SeriesModel, dataDim: string) { - const data = seriesModel.getData(); - const dimensions = data.dimensions; - dataDim = data.getDimension(dataDim); - for (let i = 0; i < dimensions.length; i++) { - const dimItem = data.getDimensionInfo(dimensions[i]); - if (dimItem.name === dataDim) { - return dimItem.coordDim; - } - } +function dataDimToCoordDim(seriesModel: SeriesModel, dataDim: DimensionLoose): DimensionName { + const dimItem = seriesModel.getData().getDimensionInfo(dataDim); + return dimItem && dimItem.coordDim; } /** @@ -236,7 +201,7 @@ export function dimValueGetter( } export function numCalculate( - data: List, + data: SeriesData, valueDataDim: string, type: MarkerStatisticType ) { diff --git a/src/component/timeline/SliderTimelineModel.ts b/src/component/timeline/SliderTimelineModel.ts index 618e49c4cf..ec20bc948b 100644 --- a/src/component/timeline/SliderTimelineModel.ts +++ b/src/component/timeline/SliderTimelineModel.ts @@ -20,7 +20,7 @@ import TimelineModel, { TimelineOption } from './TimelineModel'; import { DataFormatMixin } from '../../model/mixin/dataFormat'; import { mixin } from 'zrender/src/core/util'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import { inheritDefaultOption } from '../../util/component'; export interface SliderTimelineOption extends TimelineOption { @@ -149,7 +149,7 @@ class SliderTimelineModel extends TimelineModel { } interface SliderTimelineModel extends DataFormatMixin { - getData(): List + getData(): SeriesData } mixin(SliderTimelineModel, DataFormatMixin.prototype); diff --git a/src/component/timeline/SliderTimelineView.ts b/src/component/timeline/SliderTimelineView.ts index 163e335ade..b49bc95620 100644 --- a/src/component/timeline/SliderTimelineView.ts +++ b/src/component/timeline/SliderTimelineView.ts @@ -24,7 +24,7 @@ import { createTextStyle } from '../../label/labelStyle'; import * as layout from '../../util/layout'; import TimelineView from './TimelineView'; import TimelineAxis from './TimelineAxis'; -import {createSymbol} from '../../util/symbol'; +import {createSymbol, normalizeSymbolOffset, normalizeSymbolSize} from '../../util/symbol'; import * as numberUtil from '../../util/number'; import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../core/ExtensionAPI'; @@ -823,20 +823,15 @@ function giveSymbol( z2: 100 }, opt, true); - let symbolSize = hostModel.get('symbolSize'); - symbolSize = symbolSize instanceof Array - ? symbolSize.slice() - : [+symbolSize, +symbolSize]; + const symbolSize = normalizeSymbolSize(hostModel.get('symbolSize')); opt.scaleX = symbolSize[0] / 2; opt.scaleY = symbolSize[1] / 2; - const symbolOffset = hostModel.get('symbolOffset'); + const symbolOffset = normalizeSymbolOffset(hostModel.get('symbolOffset'), symbolSize); if (symbolOffset) { - opt.x = opt.x || 0; - opt.y = opt.y || 0; - opt.x += numberUtil.parsePercent(symbolOffset[0], symbolSize[0]); - opt.y += numberUtil.parsePercent(symbolOffset[1], symbolSize[1]); + opt.x = (opt.x || 0) + symbolOffset[0]; + opt.y = (opt.y || 0) + symbolOffset[1]; } const symbolRotate = hostModel.get('symbolRotate'); @@ -895,4 +890,4 @@ function pointerMoveTo( } } -export default SliderTimelineView; \ No newline at end of file +export default SliderTimelineView; diff --git a/src/component/timeline/TimelineModel.ts b/src/component/timeline/TimelineModel.ts index 619f468f3a..ecfd963f3b 100644 --- a/src/component/timeline/TimelineModel.ts +++ b/src/component/timeline/TimelineModel.ts @@ -18,7 +18,7 @@ */ import ComponentModel from '../../model/Component'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import { ComponentOption, BoxLayoutOptionMixin, @@ -171,7 +171,7 @@ class TimelineModel extends ComponentModel { layoutMode = 'box'; - private _data: List; + private _data: SeriesData; private _names: string[]; @@ -275,7 +275,7 @@ class TimelineModel extends ComponentModel { value: 'number' })[axisType] || 'number'; - const data = this._data = new List([{ + const data = this._data = new SeriesData([{ name: 'value', type: dimType }], this); diff --git a/src/component/timeline/timelineAction.ts b/src/component/timeline/timelineAction.ts index c4ef6869d7..202bc18b47 100644 --- a/src/component/timeline/timelineAction.ts +++ b/src/component/timeline/timelineAction.ts @@ -22,6 +22,7 @@ import TimelineModel from './TimelineModel'; import { defaults } from 'zrender/src/core/util'; import { Payload } from '../../util/types'; import { EChartsExtensionInstallRegisters } from '../../extension'; +import ExtensionAPI from '../../core/ExtensionAPI'; export interface TimelineChangePayload extends Payload { type: 'timelineChange' @@ -38,14 +39,25 @@ export function installTimelineAction(registers: EChartsExtensionInstallRegister {type: 'timelineChange', event: 'timelineChanged', update: 'prepareAndUpdate'}, - function (payload: TimelineChangePayload, ecModel: GlobalModel) { + function (payload: TimelineChangePayload, ecModel: GlobalModel, api: ExtensionAPI) { const timelineModel = ecModel.getComponent('timeline') as TimelineModel; if (timelineModel && payload.currentIndex != null) { timelineModel.setCurrentIndex(payload.currentIndex); - if (!timelineModel.get('loop', true) && timelineModel.isIndexMax()) { + if ( + !timelineModel.get('loop', true) + && timelineModel.isIndexMax() + && timelineModel.getPlayState() + ) { timelineModel.setPlayState(false); + + // The timeline has played to the end, trigger event + api.dispatchAction({ + type: 'timelinePlayChange', + playState: false, + from: payload.from + }); } } diff --git a/src/component/toolbox/feature/Brush.ts b/src/component/toolbox/feature/Brush.ts index c8c3197050..7c4b7f2fab 100644 --- a/src/component/toolbox/feature/Brush.ts +++ b/src/component/toolbox/feature/Brush.ts @@ -143,7 +143,7 @@ class BrushFeature extends ToolboxFeature { /* eslint-enable */ }, // `rect`, `polygon`, `lineX`, `lineY`, `keep`, `clear` - title: ecModel.getLocale(['toolbox', 'brush', 'title']) + title: ecModel.getLocaleModel().get(['toolbox', 'brush', 'title']) }; return defaultOption; diff --git a/src/component/toolbox/feature/DataView.ts b/src/component/toolbox/feature/DataView.ts index cf10b17244..6256705e18 100644 --- a/src/component/toolbox/feature/DataView.ts +++ b/src/component/toolbox/feature/DataView.ts @@ -17,6 +17,8 @@ * under the License. */ +/* global document */ + import * as echarts from '../../../core/echarts'; import * as zrUtil from 'zrender/src/core/util'; import GlobalModel from '../../../model/Global'; @@ -449,8 +451,8 @@ class DataView extends ToolboxFeature { // eslint-disable-next-line icon: 'M17.5,17.3H33 M17.5,17.3H33 M45.4,29.5h-28 M11.5,2v56H51V14.8L38.4,2H11.5z M38.4,2.2v12.7H51 M45.4,41.7h-28', - title: ecModel.getLocale(['toolbox', 'dataView', 'title']), - lang: ecModel.getLocale(['toolbox', 'dataView', 'lang']), + title: ecModel.getLocaleModel().get(['toolbox', 'dataView', 'title']), + lang: ecModel.getLocaleModel().get(['toolbox', 'dataView', 'lang']), backgroundColor: '#fff', textColor: '#000', textareaColor: '#fff', diff --git a/src/component/toolbox/feature/DataZoom.ts b/src/component/toolbox/feature/DataZoom.ts index d87b8f8075..09366771b4 100644 --- a/src/component/toolbox/feature/DataZoom.ts +++ b/src/component/toolbox/feature/DataZoom.ts @@ -210,7 +210,7 @@ class DataZoomFeature extends ToolboxFeature { back: 'M22,1.4L9.9,13.5l12.3,12.3 M10.3,13.5H54.9v44.6 H10.3v-26' }, // `zoom`, `back` - title: ecModel.getLocale(['toolbox', 'dataZoom', 'title']), + title: ecModel.getLocaleModel().get(['toolbox', 'dataZoom', 'title']), brushStyle: { borderWidth: 0, color: 'rgba(210,219,238,0.2)' diff --git a/src/component/toolbox/feature/MagicType.ts b/src/component/toolbox/feature/MagicType.ts index eb21efb4fe..e13d6db6cd 100644 --- a/src/component/toolbox/feature/MagicType.ts +++ b/src/component/toolbox/feature/MagicType.ts @@ -87,7 +87,7 @@ class MagicType extends ToolboxFeature { stack: 'M8.2,38.4l-8.4,4.1l30.6,15.3L60,42.5l-8.1-4.1l-21.5,11L8.2,38.4z M51.9,30l-8.1,4.2l-13.4,6.9l-13.9-6.9L8.2,30l-8.4,4.2l8.4,4.2l22.2,11l21.5-11l8.1-4.2L51.9,30z M51.9,21.7l-8.1,4.2L35.7,30l-5.3,2.8L24.9,30l-8.4-4.1l-8.3-4.2l-8.4,4.2L8.2,30l8.3,4.2l13.9,6.9l13.4-6.9l8.1-4.2l8.1-4.1L51.9,21.7zM30.4,2.2L-0.2,17.5l8.4,4.1l8.3,4.2l8.4,4.2l5.5,2.7l5.3-2.7l8.1-4.2l8.1-4.2l8.1-4.1L30.4,2.2z' // jshint ignore:line }, // `line`, `bar`, `stack`, `tiled` - title: ecModel.getLocale(['toolbox', 'magicType', 'title']), + title: ecModel.getLocaleModel().get(['toolbox', 'magicType', 'title']), option: {}, seriesIndex: {} }; diff --git a/src/component/toolbox/feature/Restore.ts b/src/component/toolbox/feature/Restore.ts index e7ce593bdb..6930287c86 100644 --- a/src/component/toolbox/feature/Restore.ts +++ b/src/component/toolbox/feature/Restore.ts @@ -44,7 +44,7 @@ class RestoreOption extends ToolboxFeature { show: true, // eslint-disable-next-line icon: 'M3.8,33.4 M47,18.9h9.8V8.7 M56.3,20.1 C52.1,9,40.5,0.6,26.8,2.1C12.6,3.7,1.6,16.2,2.1,30.6 M13,41.1H3.1v10.2 M3.7,39.9c4.2,11.1,15.8,19.5,29.5,18 c14.2-1.6,25.2-14.1,24.7-28.5', - title: ecModel.getLocale(['toolbox', 'restore', 'title']) + title: ecModel.getLocaleModel().get(['toolbox', 'restore', 'title']) }; return defaultOption; diff --git a/src/component/toolbox/feature/SaveAsImage.ts b/src/component/toolbox/feature/SaveAsImage.ts index 6da25bad1c..c59bb860b4 100644 --- a/src/component/toolbox/feature/SaveAsImage.ts +++ b/src/component/toolbox/feature/SaveAsImage.ts @@ -17,7 +17,7 @@ * under the License. */ -/* global Uint8Array */ +/* global Uint8Array, document */ import env from 'zrender/src/core/env'; import { ToolboxFeature, ToolboxFeatureOption } from '../featureManager'; @@ -127,7 +127,7 @@ class SaveAsImage extends ToolboxFeature { const defaultOption: ToolboxSaveAsImageFeatureOption = { show: true, icon: 'M4.7,22.9L29.3,45.5L54.7,23.4M4.6,43.6L4.6,58L53.8,58L53.8,43.6M29.2,45.1L29.2,0', - title: ecModel.getLocale(['toolbox', 'saveAsImage', 'title']), + title: ecModel.getLocaleModel().get(['toolbox', 'saveAsImage', 'title']), type: 'png', // Default use option.backgroundColor // backgroundColor: '#fff', @@ -136,7 +136,7 @@ class SaveAsImage extends ToolboxFeature { excludeComponents: ['toolbox'], // use current pixel ratio of device by default // pixelRatio: 1, - lang: ecModel.getLocale(['toolbox', 'saveAsImage', 'lang']) + lang: ecModel.getLocaleModel().get(['toolbox', 'saveAsImage', 'lang']) }; return defaultOption; diff --git a/src/component/tooltip/TooltipHTMLContent.ts b/src/component/tooltip/TooltipHTMLContent.ts index 7d70c1b36d..d6e85c17bd 100644 --- a/src/component/tooltip/TooltipHTMLContent.ts +++ b/src/component/tooltip/TooltipHTMLContent.ts @@ -28,7 +28,7 @@ import type { ZRenderType } from 'zrender/src/zrender'; import type { TooltipOption } from './TooltipModel'; import Model from '../../model/Model'; import type { ZRRawEvent } from 'zrender/src/core/types'; -import type { ColorString, ZRColor } from '../../util/types'; +import type { ZRColor } from '../../util/types'; import type CanvasPainter from 'zrender/src/canvas/Painter'; import type SVGPainter from 'zrender/src/svg/Painter'; import { @@ -60,7 +60,7 @@ function mirrorPos(pos: string): string { } function assembleArrow( - backgroundColor: ColorString, + tooltipModel: Model, borderColor: ZRColor, arrowPosition: TooltipOption['position'] ) { @@ -68,28 +68,39 @@ function assembleArrow( return ''; } + const backgroundColor = tooltipModel.get('backgroundColor'); + const borderWidth = tooltipModel.get('borderWidth'); + borderColor = convertToColorString(borderColor); const arrowPos = mirrorPos(arrowPosition); - let positionStyle = `${arrowPos}:-6px;`; + const arrowSize = Math.max(Math.round(borderWidth) * 1.5, 6); + let positionStyle = ''; let transformStyle = CSS_TRANSFORM_VENDOR + ':'; + let rotateDeg; if (indexOf(['left', 'right'], arrowPos) > -1) { positionStyle += 'top:50%'; - transformStyle += `translateY(-50%) rotate(${arrowPos === 'left' ? -225 : -45}deg)`; + transformStyle += `translateY(-50%) rotate(${rotateDeg = arrowPos === 'left' ? -225 : -45}deg)`; } else { positionStyle += 'left:50%'; - transformStyle += `translateX(-50%) rotate(${arrowPos === 'top' ? 225 : 45}deg)`; + transformStyle += `translateX(-50%) rotate(${rotateDeg = arrowPos === 'top' ? 225 : 45}deg)`; } - - const borderStyle = `${borderColor} solid 1px;`; + const rotateRadian = rotateDeg * Math.PI / 180; + const arrowWH = arrowSize + borderWidth; + const rotatedWH = arrowWH * Math.abs(Math.cos(rotateRadian)) + arrowWH * Math.abs(Math.sin(rotateRadian)); + const arrowOffset = Math.round(((rotatedWH - Math.SQRT2 * borderWidth) / 2 + + Math.SQRT2 * borderWidth - (rotatedWH - arrowWH) / 2) * 100) / 100; + positionStyle += `;${arrowPos}:-${arrowOffset}px`; + + const borderStyle = `${borderColor} solid ${borderWidth}px;`; const styleCss = [ - 'position:absolute;width:10px;height:10px;', + `position:absolute;width:${arrowSize}px;height:${arrowSize}px;`, `${positionStyle};${transformStyle};`, `border-bottom:${borderStyle}`, `border-right:${borderStyle}`, - `background-color:${backgroundColor};`, - 'box-shadow:8px 8px 16px -3px #000;' + `background-color:${backgroundColor};` ]; + return `
`; } @@ -388,7 +399,7 @@ class TooltipHTMLContent { // stop, "unfocusAdjacency". Here `pointer-events: none` is used to solve // it. Although it is not supported by IE8~IE10, fortunately it is a rare // scenario. - + `;pointer-event:${this._enterable ? 'auto' : 'none'}`; + + `;pointer-events:${this._enterable ? 'auto' : 'none'}`; } this._show = true; @@ -397,24 +408,26 @@ class TooltipHTMLContent { } setContent( - content: string | HTMLElement[], + content: string | HTMLElement | HTMLElement[], markers: unknown, tooltipModel: Model, borderColor?: ZRColor, arrowPosition?: TooltipOption['position'] ) { + const el = this.el; + if (content == null) { + el.innerHTML = ''; return; } - const el = this.el; - + let arrow = ''; if (isString(arrowPosition) && tooltipModel.get('trigger') === 'item' && !shouldTooltipConfine(tooltipModel)) { - content += assembleArrow(tooltipModel.get('backgroundColor'), borderColor, arrowPosition); + arrow = assembleArrow(tooltipModel, borderColor, arrowPosition); } if (isString(content)) { - el.innerHTML = content; + el.innerHTML = content + arrow; } else if (content) { // Clear previous @@ -427,6 +440,14 @@ class TooltipHTMLContent { el.appendChild(content[i]); } } + // no arrow if empty + if (arrow && el.childNodes.length) { + // no need to create a new parent element, but it's not supported by IE 10 and older. + // const arrowEl = document.createRange().createContextualFragment(arrow); + const arrowEl = document.createElement('div'); + arrowEl.innerHTML = arrow; + el.appendChild(arrowEl); + } } } @@ -436,7 +457,7 @@ class TooltipHTMLContent { getSize() { const el = this.el; - return [el.clientWidth, el.clientHeight]; + return [el.offsetWidth, el.offsetHeight]; } moveTo(zrX: number, zrY: number) { diff --git a/src/component/tooltip/TooltipRichContent.ts b/src/component/tooltip/TooltipRichContent.ts index 94c2e6defb..59cb944723 100644 --- a/src/component/tooltip/TooltipRichContent.ts +++ b/src/component/tooltip/TooltipRichContent.ts @@ -71,7 +71,7 @@ class TooltipRichContent { * Set tooltip content */ setContent( - content: string | HTMLElement[], + content: string | HTMLElement | HTMLElement[], markupStyleCreator: TooltipMarkupStyleCreator, tooltipModel: Model, borderColor: ZRColor, diff --git a/src/component/tooltip/TooltipView.ts b/src/component/tooltip/TooltipView.ts index 0aa7f7cfa8..c2688f62c7 100644 --- a/src/component/tooltip/TooltipView.ts +++ b/src/component/tooltip/TooltipView.ts @@ -47,7 +47,7 @@ import { } from '../../util/types'; import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../core/ExtensionAPI'; -import TooltipModel, {TooltipOption} from './TooltipModel'; +import TooltipModel, { TooltipOption } from './TooltipModel'; import Element from 'zrender/src/Element'; import { AxisBaseModel } from '../../coord/AxisBaseModel'; // import { isDimensionStacked } from '../../data/helper/dataStackHelper'; @@ -63,7 +63,7 @@ const each = zrUtil.each; const parsePercent = numberUtil.parsePercent; const proxyRect = new graphic.Rect({ - shape: {x: -1, y: -1, width: 2, height: 2} + shape: { x: -1, y: -1, width: 2, height: 2 } }); interface DataIndex { @@ -165,6 +165,7 @@ class TooltipView extends ComponentView { private _showTimout: number; private _lastDataByCoordSys: DataByCoordSys[]; + private _cbParamsList: TooltipCallbackDataParams[]; init(ecModel: GlobalModel, api: ExtensionAPI) { if (env.node) { @@ -260,7 +261,7 @@ class TooltipView extends ComponentView { y: self._lastY, dataByCoordSys: self._lastDataByCoordSys }); - }); + }) as any; } } @@ -554,6 +555,11 @@ class TooltipView extends ComponentView { const series = ecModel.getSeriesByIndex(idxItem.seriesIndex); const dataIndex = idxItem.dataIndexInside; const cbParams = series.getDataParams(dataIndex) as TooltipCallbackDataParams; + // Can't find data. + if (cbParams.dataIndex < 0) { + return; + } + cbParams.axisDim = axisItem.axisDim; cbParams.axisIndex = axisItem.axisIndex; cbParams.axisType = axisItem.axisType; @@ -599,7 +605,7 @@ class TooltipView extends ComponentView { const allMarkupText = markupTextArrLegacy.join(blockBreak); this._showOrMove(singleTooltipModel, function (this: TooltipView) { - if (this._updateContentNotChangedOnAxis(dataByCoordSys)) { + if (this._updateContentNotChangedOnAxis(dataByCoordSys, cbParamsList)) { this._updatePosition( singleTooltipModel, positionExpr, @@ -784,7 +790,7 @@ class TooltipView extends ComponentView { const formatter = tooltipModel.get('formatter'); positionExpr = positionExpr || tooltipModel.get('position'); - let html: string | HTMLElement[] = defaultHtml; + let html: string | HTMLElement | HTMLElement[] = defaultHtml; const nearPoint = this._getNearestPoint( [x, y], params, @@ -793,27 +799,32 @@ class TooltipView extends ComponentView { ); const nearPointColor = nearPoint.color; - if (formatter && zrUtil.isString(formatter)) { - const useUTC = tooltipModel.ecModel.get('useUTC'); - const params0 = zrUtil.isArray(params) ? params[0] : params; - const isTimeAxis = params0 && params0.axisType && params0.axisType.indexOf('time') >= 0; - html = formatter; - if (isTimeAxis) { - html = timeFormat(params0.axisValue, html, useUTC); - } - html = formatUtil.formatTpl(html, params, true); - } - else if (zrUtil.isFunction(formatter)) { - const callback = bind(function (cbTicket: string, html: string | HTMLElement[]) { - if (cbTicket === this._ticket) { - tooltipContent.setContent(html, markupStyleCreator, tooltipModel, nearPointColor, positionExpr); - this._updatePosition( - tooltipModel, positionExpr, x, y, tooltipContent, params, el - ); + if (formatter) { + if (zrUtil.isString(formatter)) { + const useUTC = tooltipModel.ecModel.get('useUTC'); + const params0 = zrUtil.isArray(params) ? params[0] : params; + const isTimeAxis = params0 && params0.axisType && params0.axisType.indexOf('time') >= 0; + html = formatter; + if (isTimeAxis) { + html = timeFormat(params0.axisValue, html, useUTC); } - }, this); - this._ticket = asyncTicket; - html = formatter(params, asyncTicket, callback); + html = formatUtil.formatTpl(html, params, true); + } + else if (zrUtil.isFunction(formatter)) { + const callback = bind(function (cbTicket: string, html: string | HTMLElement | HTMLElement[]) { + if (cbTicket === this._ticket) { + tooltipContent.setContent(html, markupStyleCreator, tooltipModel, nearPointColor, positionExpr); + this._updatePosition( + tooltipModel, positionExpr, x, y, tooltipContent, params, el + ); + } + }, this); + this._ticket = asyncTicket; + html = formatter(params, asyncTicket, callback); + } + else { + html = formatter; + } } tooltipContent.setContent(html, markupStyleCreator, tooltipModel, nearPointColor, positionExpr); @@ -882,7 +893,7 @@ class TooltipView extends ComponentView { boxLayoutPosition.width = contentSize[0]; boxLayoutPosition.height = contentSize[1]; const layoutRect = layoutUtil.getLayoutRect( - boxLayoutPosition, {width: viewWidth, height: viewHeight} + boxLayoutPosition, { width: viewWidth, height: viewHeight } ); x = layoutRect.x; y = layoutRect.y; @@ -894,7 +905,7 @@ class TooltipView extends ComponentView { // Specify tooltip position by string 'top' 'bottom' 'left' 'right' around graphic element else if (zrUtil.isString(positionExpr) && el) { const pos = calcTooltipPosition( - positionExpr, rect, contentSize + positionExpr, rect, contentSize, tooltipModel.get('borderWidth'), ); x = pos[0]; y = pos[1]; @@ -923,18 +934,22 @@ class TooltipView extends ComponentView { // FIXME // Should we remove this but leave this to user? - private _updateContentNotChangedOnAxis(dataByCoordSys: DataByCoordSys[]) { + private _updateContentNotChangedOnAxis( + dataByCoordSys: DataByCoordSys[], + cbParamsList: TooltipCallbackDataParams[] + ) { const lastCoordSys = this._lastDataByCoordSys; + const lastCbParamsList = this._cbParamsList; let contentNotChanged = !!lastCoordSys && lastCoordSys.length === dataByCoordSys.length; - contentNotChanged && each(lastCoordSys, function (lastItemCoordSys, indexCoordSys) { + contentNotChanged && each(lastCoordSys, (lastItemCoordSys, indexCoordSys) => { const lastDataByAxis = lastItemCoordSys.dataByAxis || [] as DataByAxis[]; const thisItemCoordSys = dataByCoordSys[indexCoordSys] || {} as DataByCoordSys; const thisDataByAxis = thisItemCoordSys.dataByAxis || [] as DataByAxis[]; contentNotChanged = contentNotChanged && lastDataByAxis.length === thisDataByAxis.length; - contentNotChanged && each(lastDataByAxis, function (lastItem, indexAxis) { + contentNotChanged && each(lastDataByAxis, (lastItem, indexAxis) => { const thisItem = thisDataByAxis[indexAxis] || {} as DataByAxis; const lastIndices = lastItem.seriesDataIndices || [] as DataIndex[]; const newIndices = thisItem.seriesDataIndices || [] as DataIndex[]; @@ -945,16 +960,27 @@ class TooltipView extends ComponentView { && lastItem.axisId === thisItem.axisId && lastIndices.length === newIndices.length; - contentNotChanged && each(lastIndices, function (lastIdxItem, j) { + contentNotChanged && each(lastIndices, (lastIdxItem, j) => { const newIdxItem = newIndices[j]; contentNotChanged = contentNotChanged && lastIdxItem.seriesIndex === newIdxItem.seriesIndex && lastIdxItem.dataIndex === newIdxItem.dataIndex; }); + + // check is cbParams data value changed + lastCbParamsList && zrUtil.each(lastItem.seriesDataIndices, (idxItem) => { + const seriesIdx = idxItem.seriesIndex; + const cbParams = cbParamsList[seriesIdx]; + const lastCbParams = lastCbParamsList[seriesIdx]; + if (cbParams && lastCbParams && lastCbParams.data !== cbParams.data) { + contentNotChanged = false; + } + }); }); }); this._lastDataByCoordSys = dataByCoordSys; + this._cbParamsList = cbParamsList; return !!contentNotChanged; } @@ -1089,12 +1115,12 @@ function confineTooltipPosition( function calcTooltipPosition( position: TooltipOption['position'], rect: ZRRectLike, - contentSize: number[] + contentSize: number[], + borderWidth: number ): [number, number] { const domWidth = contentSize[0]; const domHeight = contentSize[1]; - const gap = 10; - const offset = 5; + const offset = Math.max(Math.ceil(Math.sqrt(2 * borderWidth * borderWidth)), 5); let x = 0; let y = 0; const rectWidth = rect.width; @@ -1106,18 +1132,18 @@ function calcTooltipPosition( break; case 'top': x = rect.x + rectWidth / 2 - domWidth / 2; - y = rect.y - domHeight - gap; + y = rect.y - domHeight - offset; break; case 'bottom': x = rect.x + rectWidth / 2 - domWidth / 2; - y = rect.y + rectHeight + gap; + y = rect.y + rectHeight + offset; break; case 'left': - x = rect.x - domWidth - gap - offset; + x = rect.x - domWidth - offset; y = rect.y + rectHeight / 2 - domHeight / 2; break; case 'right': - x = rect.x + rectWidth + gap + offset; + x = rect.x + rectWidth + offset; y = rect.y + rectHeight / 2 - domHeight / 2; } return [x, y]; diff --git a/src/component/visualMap/ContinuousModel.ts b/src/component/visualMap/ContinuousModel.ts index c43e2dd905..ef62a7ca55 100644 --- a/src/component/visualMap/ContinuousModel.ts +++ b/src/component/visualMap/ContinuousModel.ts @@ -203,7 +203,7 @@ class ContinuousModel extends VisualMapModel { const dataIndices: number[] = []; const data = seriesModel.getData(); - data.each(this.getDataDimension(data), function (value, dataIndex) { + data.each(this.getDataDimensionIndex(data), function (value, dataIndex) { range[0] <= value && value <= range[1] && dataIndices.push(dataIndex); }, this); diff --git a/src/component/visualMap/ContinuousView.ts b/src/component/visualMap/ContinuousView.ts index e15a74cd6b..4b9e959487 100644 --- a/src/component/visualMap/ContinuousView.ts +++ b/src/component/visualMap/ContinuousView.ts @@ -832,7 +832,7 @@ class ContinuousView extends VisualMapView { } const data = dataModel.getData(ecData.dataType); - const value = data.get(visualMapModel.getDataDimension(data), ecData.dataIndex) as number; + const value = data.getStore().get(visualMapModel.getDataDimensionIndex(data), ecData.dataIndex) as number; if (!isNaN(value)) { this._showIndicator(value, value); diff --git a/src/component/visualMap/PiecewiseModel.ts b/src/component/visualMap/PiecewiseModel.ts index 520c7094ae..3800d2b72d 100644 --- a/src/component/visualMap/PiecewiseModel.ts +++ b/src/component/visualMap/PiecewiseModel.ts @@ -316,7 +316,7 @@ class PiecewiseModel extends VisualMapModel { const dataIndices: number[] = []; const data = seriesModel.getData(); - data.each(this.getDataDimension(data), function (value: number, dataIndex: number) { + data.each(this.getDataDimensionIndex(data), function (value: number, dataIndex: number) { // Should always base on model pieceList, because it is order sensitive. const pIdx = VisualMapping.findPieceIndex(value, pieceList); pIdx === pieceIndex && dataIndices.push(dataIndex); diff --git a/src/component/visualMap/VisualMapModel.ts b/src/component/visualMap/VisualMapModel.ts index 2ef3c33542..8534b29708 100644 --- a/src/component/visualMap/VisualMapModel.ts +++ b/src/component/visualMap/VisualMapModel.ts @@ -32,13 +32,14 @@ import { ZRColor, BorderOptionMixin, OptionDataValue, - BuiltinVisualProperty + BuiltinVisualProperty, + DimensionIndex } from '../../util/types'; import ComponentModel from '../../model/Component'; import Model from '../../model/Model'; import GlobalModel from '../../model/Global'; import SeriesModel from '../../model/Series'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; const mapVisual = VisualMapping.mapVisual; const eachVisual = VisualMapping.eachVisual; @@ -156,7 +157,7 @@ export interface VisualMeta { stops: { value: number, color: ColorString}[] outerColors: ColorString[] - dimension?: number + dimension?: DimensionIndex } class VisualMapModel extends ComponentModel { @@ -377,25 +378,41 @@ class VisualMapModel extends Com } /** + * PENDING: + * delete this method if no outer usage. + * * Return Concrete dimention. If return null/undefined, no dimension used. */ - getDataDimension(list: List) { + // getDataDimension(data: SeriesData) { + // const optDim = this.option.dimension; + + // if (optDim != null) { + // return data.getDimension(optDim); + // } + + // const dimNames = data.dimensions; + // for (let i = dimNames.length - 1; i >= 0; i--) { + // const dimName = dimNames[i]; + // const dimInfo = data.getDimensionInfo(dimName); + // if (!dimInfo.isCalculationCoord) { + // return dimName; + // } + // } + // } + + getDataDimensionIndex(data: SeriesData): DimensionIndex { const optDim = this.option.dimension; - const listDimensions = list.dimensions; - if (optDim == null && !listDimensions.length) { - return; - } if (optDim != null) { - return list.getDimension(optDim); + return data.getDimensionIndex(optDim); } - const dimNames = list.dimensions; + const dimNames = data.dimensions; for (let i = dimNames.length - 1; i >= 0; i--) { const dimName = dimNames[i]; - const dimInfo = list.getDimensionInfo(dimName); + const dimInfo = data.getDimensionInfo(dimName); if (!dimInfo.isCalculationCoord) { - return dimName; + return dimInfo.storeDimIndex; } } } diff --git a/src/component/visualMap/visualEncoding.ts b/src/component/visualMap/visualEncoding.ts index f2a008bbbb..6f090b3694 100644 --- a/src/component/visualMap/visualEncoding.ts +++ b/src/component/visualMap/visualEncoding.ts @@ -42,7 +42,7 @@ export const visualMapEncodingHandlers: StageHandler[] = [ visualMapModel.stateList, visualMapModel.targetVisuals, zrUtil.bind(visualMapModel.getValueState, visualMapModel), - visualMapModel.getDataDimension(seriesModel.getData()) + visualMapModel.getDataDimensionIndex(seriesModel.getData()) )); }); @@ -65,11 +65,10 @@ export const visualMapEncodingHandlers: StageHandler[] = [ outerColors: [] } as VisualMeta; - const concreteDim = visualMapModel.getDataDimension(data); - const dimInfo = data.getDimensionInfo(concreteDim); - if (dimInfo != null) { + const dimIdx = visualMapModel.getDataDimensionIndex(data); + if (dimIdx >= 0) { // visualMeta.dimension should be dimension index, but not concrete dimension. - visualMeta.dimension = dimInfo.index; + visualMeta.dimension = dimIdx; visualMetaList.push(visualMeta); } } diff --git a/src/coord/CoordinateSystem.ts b/src/coord/CoordinateSystem.ts index cea258a1ce..0ebe41e916 100644 --- a/src/coord/CoordinateSystem.ts +++ b/src/coord/CoordinateSystem.ts @@ -26,7 +26,7 @@ import { BoundingRect } from '../util/graphic'; import { MatrixArray } from 'zrender/src/core/matrix'; import ComponentModel from '../model/Component'; import { RectLike } from 'zrender/src/core/BoundingRect'; -import type { PrepareCustomInfo } from '../chart/custom/install'; +import type { PrepareCustomInfo } from '../chart/custom/CustomSeries'; export interface CoordinateSystemCreator { diff --git a/src/coord/axisCommonTypes.ts b/src/coord/axisCommonTypes.ts index 3ba456835f..9d9bf1117a 100644 --- a/src/coord/axisCommonTypes.ts +++ b/src/coord/axisCommonTypes.ts @@ -133,7 +133,7 @@ interface AxisLineOption { // The arrow at both ends the the axis. symbol?: string | [string, string], symbolSize?: number[], - symbolOffset?: number[], + symbolOffset?: string | number | (string | number)[], lineStyle?: LineStyleOption, } @@ -219,4 +219,4 @@ interface SplitAreaOption { interval?: 'auto' | number | ((index:number, value: string) => boolean) // colors will display in turn areaStyle?: AreaStyleOption -} \ No newline at end of file +} diff --git a/src/coord/axisHelper.ts b/src/coord/axisHelper.ts index 3bc6860442..d058db3ad1 100644 --- a/src/coord/axisHelper.ts +++ b/src/coord/axisHelper.ts @@ -35,7 +35,7 @@ import LogScale from '../scale/Log'; import Axis from './Axis'; import { AxisBaseOption, TimeAxisLabelFormatterOption } from './axisCommonTypes'; import type CartesianAxisModel from './cartesian/AxisModel'; -import List from '../data/List'; +import SeriesData from '../data/SeriesData'; import { getStackedDimension } from '../data/helper/dataStackHelper'; import { Dictionary, DimensionName, ScaleTick, TimeScaleTick } from '../util/types'; import { ensureScaleRawExtentInfo } from './scaleRawExtentInfo'; @@ -361,7 +361,7 @@ export function shouldShowAllLabels(axis: Axis): boolean { && getOptionCategoryInterval(axis.getLabelModel()) === 0; } -export function getDataDimensionsOnAxis(data: List, axisDim: string): DimensionName[] { +export function getDataDimensionsOnAxis(data: SeriesData, axisDim: string): DimensionName[] { // Remove duplicated dat dimensions caused by `getStackedDimension`. const dataDimMap = {} as Dictionary; // Currently `mapDimensionsAll` will contain stack result dimension ('__\0ecstackresult'). @@ -379,7 +379,7 @@ export function getDataDimensionsOnAxis(data: List, axisDim: string): DimensionN return zrUtil.keys(dataDimMap); } -export function unionAxisExtentFromData(dataExtent: number[], data: List, axisDim: string): void { +export function unionAxisExtentFromData(dataExtent: number[], data: SeriesData, axisDim: string): void { if (data) { zrUtil.each(getDataDimensionsOnAxis(data, axisDim), function (dim) { const seriesExtent = data.getApproximateExtent(dim); diff --git a/src/coord/axisModelCreator.ts b/src/coord/axisModelCreator.ts index e525f76f26..5d275ee459 100644 --- a/src/coord/axisModelCreator.ts +++ b/src/coord/axisModelCreator.ts @@ -69,9 +69,6 @@ export default function axisModelCreator< private __ordinalMeta: OrdinalMeta; - constructor(...args: any[]) { - super(...args); - } mergeDefaultAndTheme(option: AxisOptionT, ecModel: GlobalModel): void { const layoutMode = fetchLayoutMode(this); diff --git a/src/coord/cartesian/Grid.ts b/src/coord/cartesian/Grid.ts index 32c616dbf5..6eef23eac1 100644 --- a/src/coord/cartesian/Grid.ts +++ b/src/coord/cartesian/Grid.ts @@ -44,7 +44,7 @@ import ExtensionAPI from '../../core/ExtensionAPI'; import { Dictionary } from 'zrender/src/core/types'; import {CoordinateSystemMaster} from '../CoordinateSystem'; import { ScaleDataValue } from '../../util/types'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; import OrdinalScale from '../../scale/Ordinal'; import { isCartesian2DSeries, findAxisModels } from './cartesianAxisHelper'; @@ -450,7 +450,7 @@ class Grid implements CoordinateSystemMaster { } }, this); - function unionExtent(data: List, axis: Axis2D): void { + function unionExtent(data: SeriesData, axis: Axis2D): void { each(getDataDimensionsOnAxis(data, axis.dim), function (dim) { axis.scale.unionExtentFromData(data, dim); }); diff --git a/src/coord/geo/Geo.ts b/src/coord/geo/Geo.ts index aa15eba1e1..a3896c1eca 100644 --- a/src/coord/geo/Geo.ts +++ b/src/coord/geo/Geo.ts @@ -44,10 +44,12 @@ const GEO_DEFAULT_PARAMS: { } } as const; +export const geo2DDimensions = ['lng', 'lat']; + class Geo extends View { - dimensions = ['lng', 'lat']; + dimensions = geo2DDimensions; type = 'geo'; diff --git a/src/coord/geo/geoCreator.ts b/src/coord/geo/geoCreator.ts index 618d098fed..daef2835c3 100644 --- a/src/coord/geo/geoCreator.ts +++ b/src/coord/geo/geoCreator.ts @@ -18,7 +18,7 @@ */ import * as zrUtil from 'zrender/src/core/util'; -import Geo from './Geo'; +import Geo, { geo2DDimensions } from './Geo'; import * as layout from '../../util/layout'; import * as numberUtil from '../../util/number'; import geoSourceManager from './geoSourceManager'; @@ -130,7 +130,7 @@ function setGeoCoords(geo: Geo, model: MapSeries) { class GeoCreator implements CoordinateSystemCreator { // For deciding which dimensions to use when creating list data - dimensions = Geo.prototype.dimensions; + dimensions = geo2DDimensions; create(ecModel: GlobalModel, api: ExtensionAPI): Geo[] { const geoList = [] as Geo[]; diff --git a/src/coord/geo/geoTypes.ts b/src/coord/geo/geoTypes.ts index ce734dc068..e2cd1d80d2 100644 --- a/src/coord/geo/geoTypes.ts +++ b/src/coord/geo/geoTypes.ts @@ -23,8 +23,8 @@ import { Group } from '../../util/graphic'; import { Region } from './Region'; -export type GeoSVGSourceInput = 'string' | Document | SVGElement; -export type GeoJSONSourceInput = 'string' | GeoJSON | GeoJSONCompressed; +export type GeoSVGSourceInput = string | Document | SVGElement; +export type GeoJSONSourceInput = string | GeoJSON | GeoJSONCompressed; export interface NameMap { [regionName: string]: string diff --git a/src/coord/parallel/Parallel.ts b/src/coord/parallel/Parallel.ts index 6080b85f2b..e22cf9a8d7 100644 --- a/src/coord/parallel/Parallel.ts +++ b/src/coord/parallel/Parallel.ts @@ -37,7 +37,7 @@ import ExtensionAPI from '../../core/ExtensionAPI'; import { Dictionary, DimensionName, ScaleDataValue } from '../../util/types'; import { CoordinateSystem, CoordinateSystemMaster } from '../CoordinateSystem'; import ParallelAxisModel, { ParallelActiveState } from './AxisModel'; -import List from '../../data/List'; +import SeriesData from '../../data/SeriesData'; const each = zrUtil.each; const mathMin = Math.min; @@ -357,7 +357,7 @@ class Parallel implements CoordinateSystemMaster, CoordinateSystem { * @param end the next dataIndex of the last dataIndex will be travel. */ eachActiveState( - data: List, + data: SeriesData, callback: (activeState: ParallelActiveState, dataIndex: number) => void, start?: number, end?: number diff --git a/src/coord/polar/Polar.ts b/src/coord/polar/Polar.ts index 4e53b13e72..912f6c7777 100644 --- a/src/coord/polar/Polar.ts +++ b/src/coord/polar/Polar.ts @@ -26,6 +26,8 @@ import { ParsedModelFinder, ParsedModelFinderKnown } from '../../util/model'; import { ScaleDataValue } from '../../util/types'; import ExtensionAPI from '../../core/ExtensionAPI'; +export const polarDimensions = ['radius', 'angle']; + interface Polar { update(ecModel: GlobalModel, api: ExtensionAPI): void } @@ -33,7 +35,7 @@ class Polar implements CoordinateSystem, CoordinateSystemMaster { readonly name: string; - readonly dimensions = ['radius', 'angle']; + readonly dimensions = polarDimensions; readonly type = 'polar'; diff --git a/src/coord/polar/polarCreator.ts b/src/coord/polar/polarCreator.ts index 2b00a2df09..69feeca425 100644 --- a/src/coord/polar/polarCreator.ts +++ b/src/coord/polar/polarCreator.ts @@ -20,7 +20,7 @@ // TODO Axis scale import * as zrUtil from 'zrender/src/core/util'; -import Polar from './Polar'; +import Polar, { polarDimensions } from './Polar'; import {parsePercent} from '../../util/number'; import { createScaleByModel, @@ -132,7 +132,7 @@ function setAxis(axis: RadiusAxis | AngleAxis, axisModel: PolarAxisModel) { const polarCreator = { - dimensions: Polar.prototype.dimensions, + dimensions: polarDimensions, create: function (ecModel: GlobalModel, api: ExtensionAPI) { const polarList: Polar[] = []; diff --git a/src/coord/single/Single.ts b/src/coord/single/Single.ts index cee845b8c9..5ad403169e 100644 --- a/src/coord/single/Single.ts +++ b/src/coord/single/Single.ts @@ -33,6 +33,7 @@ import SingleAxisModel from './AxisModel'; import { ParsedModelFinder, ParsedModelFinderKnown } from '../../util/model'; import { ScaleDataValue } from '../../util/types'; +export const singleDimensions = ['single']; /** * Create a single coordinates system. */ @@ -44,7 +45,7 @@ class Single implements CoordinateSystem, CoordinateSystemMaster { /** * Add it just for draw tooltip. */ - readonly dimensions = ['single']; + readonly dimensions = singleDimensions; name: string; diff --git a/src/coord/single/singleCreator.ts b/src/coord/single/singleCreator.ts index 537395038d..1c2c5849a0 100644 --- a/src/coord/single/singleCreator.ts +++ b/src/coord/single/singleCreator.ts @@ -21,7 +21,7 @@ * Single coordinate system creator. */ -import Single from './Single'; +import Single, { singleDimensions } from './Single'; import GlobalModel from '../../model/Global'; import ExtensionAPI from '../../core/ExtensionAPI'; import SingleAxisModel from './AxisModel'; @@ -62,7 +62,7 @@ function create(ecModel: GlobalModel, api: ExtensionAPI) { const singleCreator = { create: create, - dimensions: Single.prototype.dimensions + dimensions: singleDimensions }; export default singleCreator; \ No newline at end of file diff --git a/src/core/Scheduler.ts b/src/core/Scheduler.ts index 2081e4a094..4d67411a4c 100644 --- a/src/core/Scheduler.ts +++ b/src/core/Scheduler.ts @@ -33,7 +33,7 @@ import { import { EChartsType } from './echarts'; import SeriesModel from '../model/Series'; import ChartView from '../view/Chart'; -import List from '../data/List'; +import SeriesData from '../data/SeriesData'; export type GeneralTask = Task; export type SeriesTask = Task; @@ -76,7 +76,7 @@ type PerformStageTaskOpt = { export interface SeriesTaskContext extends TaskContext { model?: SeriesModel; - data?: List; + data?: SeriesData; view?: ChartView; ecModel?: GlobalModel; api?: ExtensionAPI; diff --git a/src/core/echarts.ts b/src/core/echarts.ts index c4b0252ad3..fc6d428b04 100644 --- a/src/core/echarts.ts +++ b/src/core/echarts.ts @@ -17,11 +17,30 @@ * under the License. */ import * as zrender from 'zrender/src/zrender'; -import * as zrUtil from 'zrender/src/core/util'; +import { + assert, + each, + isFunction, + isObject, + indexOf, + bind, + clone, + setAsPrimitive, + createCanvas, + extend, + HashMap, + createHashMap, + map, + defaults, + isDom, + isArray, + $override, + noop +} from 'zrender/src/core/util'; import * as colorTool from 'zrender/src/tool/color'; import env from 'zrender/src/core/env'; import timsort from 'zrender/src/core/timsort'; -import Eventful from 'zrender/src/core/Eventful'; +import Eventful, { EventCallbackSingleParam } from 'zrender/src/core/Eventful'; import Element, { ElementEvent } from 'zrender/src/Element'; import GlobalModel, {QueryConditionKindA, GlobalModelSetOptionOpts} from '../model/Global'; import ExtensionAPI from './ExtensionAPI'; @@ -86,7 +105,6 @@ import { ComponentSubType, ColorString, SelectChangedPayload, - DimensionLoose, ScaleDataValue, ZRElementEventName, ECElementEvent, @@ -96,8 +114,7 @@ import Displayable from 'zrender/src/graphic/Displayable'; import IncrementalDisplayable from 'zrender/src/graphic/IncrementalDisplayable'; import { seriesSymbolTask, dataSymbolTask } from '../visual/symbol'; import { getVisualFromData, getItemVisualFromData } from '../visual/helper'; -import LabelManager from '../label/LabelManager'; -import { deprecateLog, throwError } from '../util/log'; +import { deprecateLog } from '../util/log'; import { handleLegacySelectEvents } from '../legacy/dataSelectAction'; import { registerExternalTransform } from '../data/helper/transform'; @@ -106,21 +123,20 @@ import { createLocaleObject, SYSTEM_LANG, LocaleOption } from './locale'; import type {EChartsOption} from '../export/option'; import { findEventDispatcher } from '../util/event'; import decal from '../visual/decal'; -import type {MorphDividingMethod} from 'zrender/src/tool/morphPath'; import CanvasPainter from 'zrender/src/canvas/Painter'; import SVGPainter from 'zrender/src/svg/Painter'; import geoSourceManager from '../coord/geo/geoSourceManager'; +import lifecycle, { + LifecycleEvents, + UpdateLifecycleTransitionItem, + UpdateLifecycleParams, + UpdateLifecycleTransitionOpt +} from './lifecycle'; declare let global: any; type ModelFinder = modelUtil.ModelFinder; -const assert = zrUtil.assert; -const each = zrUtil.each; -const isFunction = zrUtil.isFunction; -const isObject = zrUtil.isObject; -const indexOf = zrUtil.indexOf; - const hasWindow = typeof window !== 'undefined'; export const version = '5.1.2'; @@ -183,7 +199,7 @@ export const PRIORITY = { // This flag is used to carry out this rule. // All events will be triggered out side main process (i.e. when !this[IN_MAIN_PROCESS]). const IN_MAIN_PROCESS_KEY = '__flagInMainProcess' as const; -const OPTION_UPDATED_KEY = '__optionUpdated' as const; +const PENDING_UPDATE = '__pendingUpdate' as const; const STATUS_NEEDS_UPDATE_KEY = '__needsUpdateStatus' as const; const ACTION_REG = /^[a-zA-Z0-9_]+$/; @@ -196,6 +212,9 @@ type ConnectStatus = | typeof CONNECT_STATUS_UPDATING | typeof CONNECT_STATUS_UPDATED; +export type SetOptionTransitionOpt = UpdateLifecycleTransitionOpt; +export type SetOptionTransitionOptItem = UpdateLifecycleTransitionItem; + export interface SetOptionOpts { notMerge?: boolean; lazyUpdate?: boolean; @@ -206,14 +225,6 @@ export interface SetOptionOpts { transition?: SetOptionTransitionOpt }; -export interface SetOptionTransitionOptItem { - // If `from` not given, it means that do not make series transition mandatorily. - // There might be transition mapping dy default. Sometimes we do not need them, - // which might bring about misleading. - from?: SetOptionTransitionOptFinder; - to: SetOptionTransitionOptFinder; - dividingMethod: MorphDividingMethod; -}; export interface ResizeOpts { width?: number | 'auto', // Can be 'auto' (the same as null/undefined) @@ -222,11 +233,6 @@ export interface ResizeOpts { silent?: boolean // by default false. }; -interface SetOptionTransitionOptFinder extends modelUtil.ModelFinderObject { - dimension: DimensionLoose; -} -type SetOptionTransitionOpt = SetOptionTransitionOptItem | SetOptionTransitionOptItem[]; - interface PostIniter { (chart: EChartsType): void } @@ -266,7 +272,7 @@ let prepareView: (ecIns: ECharts, isComponent: boolean) => void; let updateDirectly: ( ecIns: ECharts, method: string, payload: Payload, mainType: ComponentMainType, subType?: ComponentSubType ) => void; -type UpdateMethod = (this: ECharts, payload?: Payload) => void; +type UpdateMethod = (this: ECharts, payload?: Payload, renderParams?: UpdateLifecycleParams) => void; let updateMethods: { prepareAndUpdate: UpdateMethod, update: UpdateMethod, @@ -288,36 +294,41 @@ let triggerUpdatedEvent: (this: ECharts, silent: boolean) => void; let bindRenderedEvent: (zr: zrender.ZRenderType, ecIns: ECharts) => void; let bindMouseEvent: (zr: zrender.ZRenderType, ecIns: ECharts) => void; let clearColorPalette: (ecModel: GlobalModel) => void; -let render: (ecIns: ECharts, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload) => void; +let render: ( + ecIns: ECharts, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload, updateParams: UpdateLifecycleParams +) => void; let renderComponents: ( - ecIns: ECharts, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload, dirtyList?: ComponentView[] + ecIns: ECharts, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload, + updateParams: UpdateLifecycleParams, dirtyList?: ComponentView[] ) => void; let renderSeries: ( - ecIns: ECharts, - ecModel: GlobalModel, - api: ExtensionAPI, - payload: Payload | 'remain', + ecIns: ECharts, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload | 'remain', + updateParams: UpdateLifecycleParams, dirtyMap?: {[uid: string]: any} ) => void; -let performPostUpdateFuncs: (ecModel: GlobalModel, api: ExtensionAPI) => void; let createExtensionAPI: (ecIns: ECharts) => ExtensionAPI; let enableConnect: (ecIns: ECharts) => void; -let setTransitionOpt: ( - chart: ECharts, - transitionOpt: SetOptionTransitionOpt -) => void; let markStatusToUpdate: (ecIns: ECharts) => void; let applyChangedStates: (ecIns: ECharts) => void; +type RenderedEventParam = { elapsedTime: number }; type ECEventDefinition = { - [key in ZRElementEventName]: ECElementEvent + [key in ZRElementEventName]: EventCallbackSingleParam } & { - rendered: { elapsedTime: number } - finished: undefined + rendered: EventCallbackSingleParam + finished: () => void | boolean } & { // TODO: Use ECActionEvent - [key: string]: any + [key: string]: (...args: unknown[]) => void | boolean +}; +type EChartsInitOpts = { + locale?: string | LocaleOption, + renderer?: RendererType, + devicePixelRatio?: number, + useDirtyRect?: boolean, + width?: number, + height?: number }; class ECharts extends Eventful { @@ -371,10 +382,11 @@ class ECharts extends Eventful { private _loadingFX: LoadingEffect; - private _labelManager: LabelManager; - - private [OPTION_UPDATED_KEY]: boolean | {silent: boolean}; + private [PENDING_UPDATE]: { + silent: boolean + updateParams: UpdateLifecycleParams + }; private [IN_MAIN_PROCESS_KEY]: boolean; private [CONNECT_STATUS_KEY]: ConnectStatus; private [STATUS_NEEDS_UPDATE_KEY]: boolean; @@ -383,14 +395,7 @@ class ECharts extends Eventful { dom: HTMLElement, // Theme name or themeOption. theme?: string | ThemeOption, - opts?: { - locale?: string | LocaleOption, - renderer?: RendererType, - devicePixelRatio?: number, - useDirtyRect?: boolean, - width?: number, - height?: number - } + opts?: EChartsInitOpts ) { super(new ECEventProcessor()); @@ -428,9 +433,9 @@ class ECharts extends Eventful { }); // Expect 60 fps. - this._throttledZrFlush = throttle(zrUtil.bind(zr.flush, zr), 17); + this._throttledZrFlush = throttle(bind(zr.flush, zr), 17); - theme = zrUtil.clone(theme); + theme = clone(theme); theme && backwardCompat(theme as ECUnitOption, true); this._theme = theme; @@ -452,13 +457,11 @@ class ECharts extends Eventful { this._messageCenter = new MessageCenter(); - this._labelManager = new LabelManager(); - // Init mouse events this._initEvents(); // In case some people write `window.onresize = chart.resize` - this.resize = zrUtil.bind(this.resize, this); + this.resize = bind(this.resize, this); zr.animation.on('frame', this._onframe, this); @@ -467,7 +470,7 @@ class ECharts extends Eventful { bindMouseEvent(zr, this); // ECharts instance can be used as value. - zrUtil.setAsPrimitive(this); + setAsPrimitive(this); } private _onframe(): void { @@ -480,13 +483,13 @@ class ECharts extends Eventful { const scheduler = this._scheduler; // Lazy update - if (this[OPTION_UPDATED_KEY]) { - const silent = (this[OPTION_UPDATED_KEY] as any).silent; + if (this[PENDING_UPDATE]) { + const silent = (this[PENDING_UPDATE] as any).silent; this[IN_MAIN_PROCESS_KEY] = true; prepare(this); - updateMethods.update.call(this); + updateMethods.update.call(this, null, this[PENDING_UPDATE].updateParams); // At present, in each frame, zrender performs: // (1) animation step forward. @@ -498,7 +501,7 @@ class ECharts extends Eventful { this[IN_MAIN_PROCESS_KEY] = false; - this[OPTION_UPDATED_KEY] = false; + this[PENDING_UPDATE] = null; flushPendingActions.call(this, silent); @@ -530,7 +533,7 @@ class ECharts extends Eventful { // console.log('--- ec frame visual ---', remainTime); scheduler.performVisualTasks(ecModel); - renderSeries(this, this._model, api, 'remain'); + renderSeries(this, this._model, api, 'remain', {}); remainTime -= (+new Date() - startTime); } @@ -608,10 +611,16 @@ class ECharts extends Eventful { this._model.setOption(option as ECBasicOption, { replaceMerge }, optionPreprocessorFuncs); - setTransitionOpt(this, transitionOpt); + const updateParams = { + seriesTransition: transitionOpt, + optionChanged: true + } as UpdateLifecycleParams; if (lazyUpdate) { - this[OPTION_UPDATED_KEY] = {silent: silent}; + this[PENDING_UPDATE] = { + silent: silent, + updateParams: updateParams + }; this[IN_MAIN_PROCESS_KEY] = false; // `setOption(option, {lazyMode: true})` may be called when zrender has been slept. @@ -621,13 +630,13 @@ class ECharts extends Eventful { else { prepare(this); - updateMethods.update.call(this); + updateMethods.update.call(this, null, updateParams); // Ensure zr refresh sychronously, and then pixel in canvas can be // fetched after `setOption`. this._zr.flush(); - this[OPTION_UPDATED_KEY] = false; + this[PENDING_UPDATE] = null; this[IN_MAIN_PROCESS_KEY] = false; flushPendingActions.call(this, silent); @@ -693,7 +702,7 @@ class ECharts extends Eventful { const zr = this._zr; const list = zr.storage.getDisplayList(); // Stop animations - zrUtil.each(list, function (el: Element) { + each(list, function (el: Element) { el.stopAnimation(null, true); }); @@ -773,11 +782,11 @@ class ECharts extends Eventful { const canvasList: {dom: HTMLCanvasElement | string, left: number, top: number}[] = []; const dpr = (opts && opts.pixelRatio) || this.getDevicePixelRatio(); - zrUtil.each(instances, function (chart, id) { + each(instances, function (chart, id) { if (chart.group === groupId) { const canvas = isSvg ? (chart.getZr().painter as SVGPainter).getSvgDom().innerHTML - : chart.getRenderedCanvas(zrUtil.clone(opts)); + : chart.getRenderedCanvas(clone(opts)); const boundingRect = chart.getDom().getBoundingClientRect(); left = mathMin(boundingRect.left, left); top = mathMin(boundingRect.top, top); @@ -797,7 +806,7 @@ class ECharts extends Eventful { bottom *= dpr; const width = right - left; const height = bottom - top; - const targetCanvas = zrUtil.createCanvas(); + const targetCanvas = createCanvas(); const zr = zrender.init(targetCanvas, { renderer: isSvg ? 'svg' : 'canvas' }); @@ -895,8 +904,8 @@ class ECharts extends Eventful { const findResult = modelUtil.parseFinder(ecModel, finder); - zrUtil.each(findResult, function (models, key) { - key.indexOf('Models') >= 0 && zrUtil.each(models as ComponentModel[], function (model) { + each(findResult, function (models, key) { + key.indexOf('Models') >= 0 && each(models as ComponentModel[], function (model) { const coordSys = (model as CoordinateSystemHostModel).coordinateSystem; if (coordSys && coordSys.containPoint) { result = result || !!coordSys.containPoint(value); @@ -1007,7 +1016,7 @@ class ECharts extends Eventful { } // If element has custom eventData of components else if (ecData.eventData) { - params = zrUtil.extend({}, ecData.eventData) as ECElementEvent; + params = extend({}, ecData.eventData) as ECElementEvent; return true; } }, true); @@ -1075,7 +1084,7 @@ class ECharts extends Eventful { each(eventActionMap, (actionType, eventType) => { this._messageCenter.on(eventType, function (event: Payload) { - this.trigger(eventType, event); + (this as any).trigger(eventType, event); }, this); }); @@ -1085,7 +1094,7 @@ class ECharts extends Eventful { ['selectchanged'], (eventType) => { this._messageCenter.on(eventType, function (event: Payload) { - this.trigger(eventType, event); + (this as any).trigger(eventType, event); }, this); } ); @@ -1114,20 +1123,37 @@ class ECharts extends Eventful { modelUtil.setAttribute(this.getDom(), DOM_ATTRIBUTE_KEY, ''); - const api = this._api; - const ecModel = this._model; + const chart = this; + const api = chart._api; + const ecModel = chart._model; - each(this._componentsViews, function (component) { + each(chart._componentsViews, function (component) { component.dispose(ecModel, api); }); - each(this._chartsViews, function (chart) { + each(chart._chartsViews, function (chart) { chart.dispose(ecModel, api); }); // Dispose after all views disposed - this._zr.dispose(); - - delete instances[this.id]; + chart._zr.dispose(); + + // Set properties to null. + // To reduce the memory cost in case the top code still holds this instance unexpectedly. + chart._dom = + chart._model = + chart._chartsMap = + chart._componentsMap = + chart._chartsViews = + chart._componentsViews = + chart._scheduler = + chart._api = + chart._zr = + chart._throttledZrFlush = + chart._theme = + chart._coordSysMgr = + chart._messageCenter = null; + + delete instances[chart.id]; } /** @@ -1160,12 +1186,12 @@ class ECharts extends Eventful { // There is some real cases that: // chart.setOption(option, { lazyUpdate: true }); // chart.resize(); - if (this[OPTION_UPDATED_KEY]) { + if (this[PENDING_UPDATE]) { if (silent == null) { - silent = (this[OPTION_UPDATED_KEY] as any).silent; + silent = (this[PENDING_UPDATE] as any).silent; } needPrepare = true; - this[OPTION_UPDATED_KEY] = false; + this[PENDING_UPDATE] = null; } this[IN_MAIN_PROCESS_KEY] = true; @@ -1174,7 +1200,7 @@ class ECharts extends Eventful { updateMethods.update.call(this, { type: 'resize', - animation: zrUtil.extend({ + animation: extend({ // Disable animation duration: 0 }, opts && opts.animation) @@ -1234,7 +1260,7 @@ class ECharts extends Eventful { } makeActionFromEvent(eventObj: ECActionEvent): Payload { - const payload = zrUtil.extend({}, eventObj) as Payload; + const payload = extend({}, eventObj) as Payload; payload.type = eventActionMap[eventObj.type]; return payload; } @@ -1301,10 +1327,11 @@ class ECharts extends Eventful { } updateLabelLayout() { - const labelManager = this._labelManager; - labelManager.updateLayoutConfig(this._api); - labelManager.layout(this._api); - labelManager.processLabelsOverall(); + lifecycle.trigger('series:layoutlabels', this._model, this._api, { + // Not adding series labels. + // TODO + updatedSeries: [] + }); } appendData(params: { @@ -1473,9 +1500,9 @@ class ECharts extends Eventful { subType && (condition.subType = subType); // subType may be '' by parseClassType; const excludeSeriesId = payload.excludeSeriesId; - let excludeSeriesIdMap: zrUtil.HashMap; + let excludeSeriesIdMap: HashMap; if (excludeSeriesId != null) { - excludeSeriesIdMap = zrUtil.createHashMap(); + excludeSeriesIdMap = createHashMap(); each(modelUtil.normalizeToArray(excludeSeriesId), id => { const modelId = modelUtil.convertOptionIdName(id, null); if (modelId != null) { @@ -1490,46 +1517,54 @@ class ECharts extends Eventful { // If dispatchAction before setOption, do nothing. ecModel && ecModel.eachComponent(condition, function (model) { - if (!excludeSeriesIdMap || excludeSeriesIdMap.get(model.id) == null) { - if (isHighDownPayload(payload)) { - if (model instanceof SeriesModel) { - if (payload.type === HIGHLIGHT_ACTION_TYPE && !payload.notBlur) { - blurSeriesFromHighlightPayload(model, payload, ecIns._api); - } - } - else { - const { focusSelf, dispatchers } = findComponentHighDownDispatchers( - model.mainType, model.componentIndex, payload.name, ecIns._api - ); - if (payload.type === HIGHLIGHT_ACTION_TYPE && focusSelf && !payload.notBlur) { - blurComponent(model.mainType, model.componentIndex, ecIns._api); - } - // PENDING: - // Whether to put this "enter emphasis" code in `ComponentView`, - // which will be the same as `ChartView` but might be not necessary - // and will be far from this logic. - if (dispatchers) { - each(dispatchers, dispatcher => { - payload.type === HIGHLIGHT_ACTION_TYPE - ? enterEmphasis(dispatcher) - : leaveEmphasis(dispatcher); - }); - } + const isExcluded = excludeSeriesIdMap && excludeSeriesIdMap.get(model.id) !== null; + if (isExcluded) { + return; + }; + if (isHighDownPayload(payload)) { + if (model instanceof SeriesModel) { + if (payload.type === HIGHLIGHT_ACTION_TYPE && !payload.notBlur) { + blurSeriesFromHighlightPayload(model, payload, ecIns._api); } } - else if (isSelectChangePayload(payload)) { - // TODO geo - if (model instanceof SeriesModel) { - toggleSelectionFromPayload(model, payload, ecIns._api); - updateSeriesElementSelection(model); - markStatusToUpdate(ecIns); + else { + const { focusSelf, dispatchers } = findComponentHighDownDispatchers( + model.mainType, model.componentIndex, payload.name, ecIns._api + ); + if (payload.type === HIGHLIGHT_ACTION_TYPE && focusSelf && !payload.notBlur) { + blurComponent(model.mainType, model.componentIndex, ecIns._api); + } + // PENDING: + // Whether to put this "enter emphasis" code in `ComponentView`, + // which will be the same as `ChartView` but might be not necessary + // and will be far from this logic. + if (dispatchers) { + each(dispatchers, dispatcher => { + payload.type === HIGHLIGHT_ACTION_TYPE + ? enterEmphasis(dispatcher) + : leaveEmphasis(dispatcher); + }); } } - - callView(ecIns[ - mainType === 'series' ? '_chartsMap' : '_componentsMap' - ][model.__viewId]); } + else if (isSelectChangePayload(payload)) { + // TODO geo + if (model instanceof SeriesModel) { + toggleSelectionFromPayload(model, payload, ecIns._api); + updateSeriesElementSelection(model); + markStatusToUpdate(ecIns); + } + } + }, ecIns); + + ecModel && ecModel.eachComponent(condition, function (model) { + const isExcluded = excludeSeriesIdMap && excludeSeriesIdMap.get(model.id) !== null; + if (isExcluded) { + return; + }; + callView(ecIns[ + mainType === 'series' ? '_chartsMap' : '_componentsMap' + ][model.__viewId]); }, ecIns); function callView(view: ComponentView | ChartView) { @@ -1543,12 +1578,15 @@ class ECharts extends Eventful { prepareAndUpdate(this: ECharts, payload: Payload): void { prepare(this); - updateMethods.update.call(this, payload); + updateMethods.update.call(this, payload, { + // Needs to mark option changed if newOption is given. + // It's from MagicType. + // TODO If use a separate flag optionChanged in payload? + optionChanged: payload.newOption != null + }); }, - update(this: ECharts, payload: Payload): void { - // console.profile && console.profile('update'); - + update(this: ECharts, payload: Payload, updateParams: UpdateLifecycleParams): void { const ecModel = this._model; const api = this._api; const zr = this._zr; @@ -1590,7 +1628,7 @@ class ECharts extends Eventful { clearColorPalette(ecModel); scheduler.performVisualTasks(ecModel, payload); - render(this, ecModel, api, payload); + render(this, ecModel, api, payload, updateParams); // Set background let backgroundColor = ecModel.get('backgroundColor') || 'transparent'; @@ -1613,9 +1651,7 @@ class ECharts extends Eventful { } } - performPostUpdateFuncs(ecModel, api); - - // console.profile && console.profileEnd('update'); + lifecycle.trigger('afterupdate', ecModel, api); }, updateTransform(this: ECharts, payload: Payload): void { @@ -1649,7 +1685,7 @@ class ECharts extends Eventful { } }); - const seriesDirtyMap = zrUtil.createHashMap(); + const seriesDirtyMap = createHashMap(); ecModel.eachSeries((seriesModel) => { const chartView = this._chartsMap[seriesModel.__viewId]; if (chartView.updateTransform) { @@ -1670,9 +1706,9 @@ class ECharts extends Eventful { // Currently, not call render of components. Geo render cost a lot. // renderComponents(ecIns, ecModel, api, payload, componentDirtyList); - renderSeries(this, ecModel, api, payload, seriesDirtyMap); + renderSeries(this, ecModel, api, payload, {}, seriesDirtyMap); - performPostUpdateFuncs(ecModel, this._api); + lifecycle.trigger('afterupdate', ecModel, api); }, updateView(this: ECharts, payload: Payload): void { @@ -1692,9 +1728,9 @@ class ECharts extends Eventful { // Keep pipe to the exist pipeline because it depends on the render task of the full pipeline. this._scheduler.performVisualTasks(ecModel, payload, {setDirty: true}); - render(this, this._model, this._api, payload); + render(this, ecModel, this._api, payload, {}); - performPostUpdateFuncs(ecModel, this._api); + lifecycle.trigger('afterupdate', ecModel, this._api); }, updateVisual(this: ECharts, payload: Payload): void { @@ -1735,7 +1771,7 @@ class ECharts extends Eventful { chartView.updateVisual(seriesModel, ecModel, this._api, payload); }); - performPostUpdateFuncs(ecModel, this._api); + lifecycle.trigger('afterupdate', ecModel, this._api); }, updateLayout(this: ECharts, payload: Payload): void { @@ -1801,8 +1837,8 @@ class ECharts extends Eventful { // Batch action if (payload.batch) { batched = true; - payloads = zrUtil.map(payload.batch, function (item) { - item = zrUtil.defaults(zrUtil.extend({}, item), payload); + payloads = map(payload.batch, function (item) { + item = defaults(extend({}, item), payload); item.batch = null; return item as Payload; }); @@ -1818,7 +1854,7 @@ class ECharts extends Eventful { // Action can specify the event by return it. eventObj = actionWrap.action(batchItem, this._model, this._api) as ECActionEvent; // Emit event outside - eventObj = eventObj || zrUtil.extend({} as ECActionEvent, batchItem); + eventObj = eventObj || extend({} as ECActionEvent, batchItem); // Convert type to eventType eventObj.type = actionInfo.event || eventObj.type; eventObjBatch.push(eventObj); @@ -1843,10 +1879,10 @@ class ECharts extends Eventful { if (updateMethod !== 'none' && !isHighDown && !isSelectChange && !cptType) { // Still dirty - if (this[OPTION_UPDATED_KEY]) { + if (this[PENDING_UPDATE]) { prepare(this); updateMethods.update.call(this, payload); - this[OPTION_UPDATED_KEY] = false; + this[PENDING_UPDATE] = null; } else { updateMethods[updateMethod as keyof typeof updateMethods].call(this, payload); @@ -1910,7 +1946,7 @@ class ECharts extends Eventful { * (5) no delayed setOption needs to be processed. */ bindRenderedEvent = function (zr: zrender.ZRenderType, ecIns: ECharts): void { - zr.on('rendered', function (params: ECEventDefinition['rendered']) { + zr.on('rendered', function (params: RenderedEventParam) { ecIns.trigger('rendered', params); @@ -1923,7 +1959,7 @@ class ECharts extends Eventful { // and this checking is called on frame, we also check // animation finished for robustness. zr.animation.isFinished() - && !ecIns[OPTION_UPDATED_KEY] + && !ecIns[PENDING_UPDATE] && !ecIns._scheduler.unfinished && !ecIns._pendingActions.length ) { @@ -1973,15 +2009,18 @@ class ECharts extends Eventful { }); }; - render = function (ecIns: ECharts, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload): void { + render = ( + ecIns: ECharts, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload, + updateParams: UpdateLifecycleParams + ) => { - renderComponents(ecIns, ecModel, api, payload); + renderComponents(ecIns, ecModel, api, payload, updateParams); each(ecIns._chartsViews, function (chart: ChartView) { chart.__alive = false; }); - renderSeries(ecIns, ecModel, api, payload); + renderSeries(ecIns, ecModel, api, payload, updateParams); // Remove groups of unrendered charts each(ecIns._chartsViews, function (chart: ChartView) { @@ -1991,9 +2030,10 @@ class ECharts extends Eventful { }); }; - renderComponents = function ( - ecIns: ECharts, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload, dirtyList?: ComponentView[] - ): void { + renderComponents = ( + ecIns: ECharts, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload, + updateParams: UpdateLifecycleParams, dirtyList?: ComponentView[] + ) => { each(dirtyList || ecIns._componentsViews, function (componentView: ComponentView) { const componentModel = componentView.__model; clearStates(componentModel, componentView); @@ -2010,18 +2050,23 @@ class ECharts extends Eventful { /** * Render each chart and component */ - renderSeries = function ( + renderSeries = ( ecIns: ECharts, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload | 'remain', + updateParams: UpdateLifecycleParams, dirtyMap?: {[uid: string]: any} - ): void { + ) => { // Render all charts const scheduler = ecIns._scheduler; - const labelManager = ecIns._labelManager; - labelManager.clearLabels(); + updateParams = extend(updateParams || {}, { + updatedSeries: ecModel.getSeries() + }); + + // TODO progressive? + lifecycle.trigger('series:beforeupdate', ecModel, api, updateParams); let unfinished: boolean = false; ecModel.eachSeries(function (seriesModel) { @@ -2041,8 +2086,6 @@ class ECharts extends Eventful { unfinished = true; } - seriesModel.__transientTransitionOpt = null; - chartView.group.silent = !!seriesModel.get('silent'); // Should not call markRedraw on group, because it will disable zrender // increamental render (alway render from the __startIndex each frame) @@ -2051,16 +2094,14 @@ class ECharts extends Eventful { updateBlend(seriesModel, chartView); updateSeriesElementSelection(seriesModel); - - // Add labels. - labelManager.addLabelsOfSeries(chartView); }); scheduler.unfinished = unfinished || scheduler.unfinished; - labelManager.updateLayoutConfig(api); - labelManager.layout(api); - labelManager.processLabelsOverall(); + lifecycle.trigger('series:layoutlabels', ecModel, api, updateParams); + + // transition after label is layouted. + lifecycle.trigger('series:transition', ecModel, api, updateParams); ecModel.eachSeries(function (seriesModel) { const chartView = ecIns._chartsMap[seriesModel.__viewId]; @@ -2072,15 +2113,10 @@ class ECharts extends Eventful { updateStates(seriesModel, chartView); }); - // If use hover layer updateHoverLayerStatus(ecIns, ecModel); - }; - performPostUpdateFuncs = function (ecModel: GlobalModel, api: ExtensionAPI): void { - each(postUpdateFuncs, function (func) { - func(ecModel, api); - }); + lifecycle.trigger('series:afterupdate', ecModel, api, updateParams); }; markStatusToUpdate = function (ecIns: ECharts): void { @@ -2406,59 +2442,6 @@ class ECharts extends Eventful { }); }); }; - - setTransitionOpt = function ( - chart: ECharts, - transitionOpt: SetOptionTransitionOpt - ): void { - const ecModel = chart._model; - - zrUtil.each(modelUtil.normalizeToArray(transitionOpt), transOpt => { - let errMsg; - const fromOpt = transOpt.from; - const toOpt = transOpt.to; - - if (toOpt == null) { - if (__DEV__) { - errMsg = '`transition.to` must be specified.'; - } - throwError(errMsg); - } - - const finderOpt = { - includeMainTypes: ['series'], - enableAll: false, - enableNone: false - }; - const fromResult = fromOpt ? modelUtil.parseFinder(ecModel, fromOpt, finderOpt) : null; - const toResult = modelUtil.parseFinder(ecModel, toOpt, finderOpt) as modelUtil.ParsedModelFinderKnown; - const toSeries = toResult.seriesModel; - - if (toSeries == null) { - errMsg = ''; - if (__DEV__) { - errMsg = '`transition` is only supported on series.'; - } - } - if (fromResult && fromResult.seriesModel !== toSeries) { - errMsg = ''; - if (__DEV__) { - errMsg = '`transition.from` and `transition.to` must be specified to the same series.'; - } - } - if (errMsg != null) { - throwError(errMsg); - } - - // Just a temp solution: mount them on series. - toSeries.__transientTransitionOpt = { - from: fromOpt ? fromOpt.dimension : null, - to: toOpt.dimension, - dividingMethod: transOpt.dividingMethod - }; - }); - }; - })(); } @@ -2534,10 +2517,6 @@ const dataProcessorFuncs: StageHandlerInternal[] = []; const optionPreprocessorFuncs: OptionPreprocessor[] = []; -const postInitFuncs: PostIniter[] = []; - -const postUpdateFuncs: PostUpdater[] = []; - const visualFuncs: StageHandlerInternal[] = []; const themeStorage: {[themeName: string]: ThemeOption} = {}; @@ -2559,17 +2538,13 @@ const DOM_ATTRIBUTE_KEY = '_echarts_instance_'; * Can be 'auto' (the same as null/undefined) * @param opts.height Use clientHeight of the input `dom` by default. * Can be 'auto' (the same as null/undefined) + * @param opts.locale Specify the locale. + * @param opts.useDirtyRect Enable dirty rectangle rendering or not. */ export function init( dom: HTMLElement, theme?: string | object, - opts?: { - renderer?: RendererType, - devicePixelRatio?: number, - width?: number, - height?: number, - locale?: string | LocaleOption - } + opts?: EChartsInitOpts ): EChartsType { if (__DEV__) { if (!dom) { @@ -2586,7 +2561,7 @@ export function init( } if (__DEV__) { - if (zrUtil.isDom(dom) + if (isDom(dom) && dom.nodeName.toUpperCase() !== 'CANVAS' && ( (!dom.clientWidth && (!opts || opts.width == null)) @@ -2608,9 +2583,7 @@ export function init( enableConnect(chart); - each(postInitFuncs, (postInitFunc) => { - postInitFunc(chart); - }); + lifecycle.trigger('afterinit', chart); return chart; } @@ -2634,7 +2607,7 @@ export function init( */ export function connect(groupId: string | EChartsType[]): string { // Is array of charts - if (zrUtil.isArray(groupId)) { + if (isArray(groupId)) { const charts = groupId; groupId = null; // If any chart has group @@ -2717,9 +2690,7 @@ export function registerProcessor( * @param {Function} postInitFunc */ export function registerPostInit(postInitFunc: PostIniter): void { - if (indexOf(postInitFuncs, postInitFunc) < 0) { - postInitFunc && postInitFuncs.push(postInitFunc); - } + registerUpdateLifecycle('afterinit', postInitFunc); } /** @@ -2727,9 +2698,13 @@ export function registerPostInit(postInitFunc: PostIniter): void { * @param {Function} postUpdateFunc */ export function registerPostUpdate(postUpdateFunc: PostUpdater): void { - if (indexOf(postUpdateFuncs, postUpdateFunc) < 0) { - postUpdateFunc && postUpdateFuncs.push(postUpdateFunc); - } + registerUpdateLifecycle('afterupdate', postUpdateFunc); +} + +export function registerUpdateLifecycle( + name: T, cb: (...args: LifecycleEvents[T]) => void +): void { + (lifecycle as any).on(name, cb); } /** @@ -2894,7 +2869,7 @@ export function registerLoading( * }); */ export function setCanvasCreator(creator: () => HTMLCanvasElement): void { - zrUtil.$override('createCanvas', creator); + $override('createCanvas', creator); } /** @@ -2951,31 +2926,31 @@ registerAction({ type: HIGHLIGHT_ACTION_TYPE, event: HIGHLIGHT_ACTION_TYPE, update: HIGHLIGHT_ACTION_TYPE -}, zrUtil.noop); +}, noop); registerAction({ type: DOWNPLAY_ACTION_TYPE, event: DOWNPLAY_ACTION_TYPE, update: DOWNPLAY_ACTION_TYPE -}, zrUtil.noop); +}, noop); registerAction({ type: SELECT_ACTION_TYPE, event: SELECT_ACTION_TYPE, update: SELECT_ACTION_TYPE -}, zrUtil.noop); +}, noop); registerAction({ type: UNSELECT_ACTION_TYPE, event: UNSELECT_ACTION_TYPE, update: UNSELECT_ACTION_TYPE -}, zrUtil.noop); +}, noop); registerAction({ type: TOGGLE_SELECT_ACTION_TYPE, event: TOGGLE_SELECT_ACTION_TYPE, update: TOGGLE_SELECT_ACTION_TYPE -}, zrUtil.noop); +}, noop); // Default theme registerTheme('light', lightTheme); diff --git a/src/core/lifecycle.ts b/src/core/lifecycle.ts new file mode 100644 index 0000000000..933aca567f --- /dev/null +++ b/src/core/lifecycle.ts @@ -0,0 +1,73 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +import Eventful, { EventCallback } from 'zrender/src/core/Eventful'; +import SeriesModel from '../model/Series'; +import GlobalModel from '../model/Global'; +import { EChartsType } from './echarts'; +import ExtensionAPI from './ExtensionAPI'; +import { ModelFinderIdQuery, ModelFinderIndexQuery } from '../util/model'; +import { DimensionLoose } from '../util/types'; + +export interface UpdateLifecycleTransitionSeriesFinder { + seriesIndex?: ModelFinderIndexQuery, + seriesId?: ModelFinderIdQuery + dimension: DimensionLoose; +} + +export interface UpdateLifecycleTransitionItem { + // If `from` not given, it means that do not make series transition mandatorily. + // There might be transition mapping dy default. Sometimes we do not need them, + // which might bring about misleading. + from?: UpdateLifecycleTransitionSeriesFinder | UpdateLifecycleTransitionSeriesFinder[]; + to: UpdateLifecycleTransitionSeriesFinder | UpdateLifecycleTransitionSeriesFinder[]; +}; + +export type UpdateLifecycleTransitionOpt = UpdateLifecycleTransitionItem | UpdateLifecycleTransitionItem[]; + +export interface UpdateLifecycleParams { + updatedSeries?: SeriesModel[] + + /** + * If this update is from setOption and option is changed. + */ + optionChanged?: boolean + + // Specify series to transition in this setOption. + seriesTransition?: UpdateLifecycleTransitionOpt +} +interface LifecycleEvents { + 'afterinit': [EChartsType], + 'series:beforeupdate': [GlobalModel, ExtensionAPI, UpdateLifecycleParams], + 'series:layoutlabels': [GlobalModel, ExtensionAPI, UpdateLifecycleParams], + 'series:transition': [GlobalModel, ExtensionAPI, UpdateLifecycleParams], + 'series:afterupdate': [GlobalModel, ExtensionAPI, UpdateLifecycleParams] + // 'series:beforeeachupdate': [GlobalModel, ExtensionAPI, SeriesModel] + // 'series:aftereachupdate': [GlobalModel, ExtensionAPI, SeriesModel] + 'afterupdate': [GlobalModel, ExtensionAPI] +} + +const lifecycle = new Eventful<{ + [key in keyof LifecycleEvents]: EventCallback +}>(); + + +export default lifecycle; + +export {LifecycleEvents}; \ No newline at end of file diff --git a/src/core/task.ts b/src/core/task.ts index 97bcaeb70c..08d7e23ff6 100644 --- a/src/core/task.ts +++ b/src/core/task.ts @@ -21,12 +21,12 @@ import {assert, isArray} from 'zrender/src/core/util'; import SeriesModel from '../model/Series'; import { Pipeline } from './Scheduler'; import { Payload } from '../util/types'; -import List from '../data/List'; +import SeriesData from '../data/SeriesData'; export interface TaskContext { - outputData?: List; - data?: List; + outputData?: SeriesData; + data?: SeriesData; payload?: Payload; model?: SeriesModel; }; diff --git a/src/data/DataDiffer.ts b/src/data/DataDiffer.ts index dc48fd2ad5..aed27d8eb8 100644 --- a/src/data/DataDiffer.ts +++ b/src/data/DataDiffer.ts @@ -28,6 +28,7 @@ type DiffCallbackUpdate = (newIndex: number, oldIndex: number) => void; type DiffCallbackRemove = (oldIndex: number) => void; type DiffCallbackUpdateManyToOne = (newIndex: number, oldIndex: number[]) => void; type DiffCallbackUpdateOneToMany = (newIndex: number[], oldIndex: number) => void; +type DiffCallbackUpdateManyToMany = (newIndex: number[], oldIndex: number[]) => void; /** * The value of `DataIndexMap` can only be: @@ -61,6 +62,7 @@ class DataDiffer { private _update: DiffCallbackUpdate; private _updateManyToOne: DiffCallbackUpdateManyToOne; private _updateOneToMany: DiffCallbackUpdateOneToMany; + private _updateManyToMany: DiffCallbackUpdateManyToMany; private _remove: DiffCallbackRemove; private _diffModeMultiple: boolean; @@ -121,6 +123,13 @@ class DataDiffer { this._updateOneToMany = func; return this; } + /** + * Callback function when update a data and only work in `cbMode: 'byKey'`. + */ + updateManyToMany(func: DiffCallbackUpdateManyToMany): this { + this._updateManyToMany = func; + return this; + } /** * Callback function when remove a data @@ -226,6 +235,10 @@ class DataDiffer { this._update && this._update(newIdxMapVal as number, oldIdxMapVal as number); newDataIndexMap[oldKey] = null; } + else if (oldIdxMapValLen > 1 && newIdxMapValLen > 1) { + this._updateManyToMany && this._updateManyToMany(newIdxMapVal as number[], oldIdxMapVal as number[]); + newDataIndexMap[oldKey] = null; + } else if (oldIdxMapValLen > 1) { for (let i = 0; i < oldIdxMapValLen; i++) { this._remove && this._remove((oldIdxMapVal as number[])[i]); diff --git a/src/data/DataStore.ts b/src/data/DataStore.ts new file mode 100644 index 0000000000..b48cb3e4ad --- /dev/null +++ b/src/data/DataStore.ts @@ -0,0 +1,1297 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +import { assert, clone, createHashMap, isFunction, keys, map, reduce } from 'zrender/src/core/util'; +import { + DimensionIndex, + DimensionName, + OptionDataItem, + ParsedValue, + ParsedValueNumeric +} from '../util/types'; +import { DataProvider } from './helper/dataProvider'; +import { parseDataValue } from './helper/dataValueHelper'; +import OrdinalMeta from './OrdinalMeta'; +import { shouldRetrieveDataByName, Source } from './Source'; + +const UNDEFINED = 'undefined'; +/* global Float64Array, Int32Array, Uint32Array, Uint16Array */ + +// Caution: MUST not use `new CtorUint32Array(arr, 0, len)`, because the Ctor of array is +// different from the Ctor of typed array. +export const CtorUint32Array = typeof Uint32Array === UNDEFINED ? Array : Uint32Array; +export const CtorUint16Array = typeof Uint16Array === UNDEFINED ? Array : Uint16Array; +export const CtorInt32Array = typeof Int32Array === UNDEFINED ? Array : Int32Array; +export const CtorFloat64Array = typeof Float64Array === UNDEFINED ? Array : Float64Array; +/** + * Multi dimensional data store + */ +const dataCtors = { + 'float': CtorFloat64Array, + 'int': CtorInt32Array, + // Ordinal data type can be string or int + 'ordinal': Array, + 'number': Array, + 'time': CtorFloat64Array +} as const; + +export type DataStoreDimensionType = keyof typeof dataCtors; + +type DataTypedArray = Uint32Array | Int32Array | Uint16Array | Float64Array; +type DataTypedArrayConstructor = typeof Uint32Array | typeof Int32Array | typeof Uint16Array | typeof Float64Array; +type DataArrayLikeConstructor = typeof Array | DataTypedArrayConstructor; + + +type DataValueChunk = ArrayLike; + +// If Ctx not specified, use List as Ctx +type EachCb0 = (idx: number) => void; +type EachCb1 = (x: ParsedValue, idx: number) => void; +type EachCb2 = (x: ParsedValue, y: ParsedValue, idx: number) => void; +type EachCb = (...args: any) => void; +type FilterCb0 = (idx: number) => boolean; +type FilterCb1 = (x: ParsedValue, idx: number) => boolean; +type FilterCb = (...args: any) => boolean; +// type MapArrayCb = (...args: any) => any; +type MapCb = (...args: any) => ParsedValue | ParsedValue[]; + +export type DimValueGetter = ( + this: DataStore, + dataItem: any, + property: string, + dataIndex: number, + dimIndex: DimensionIndex +) => ParsedValue; + +export interface DataStoreDimensionDefine { + /** + * Default to be float. + */ + type?: DataStoreDimensionType; + + /** + * Only used in SOURCE_FORMAT_OBJECT_ROWS and SOURCE_FORMAT_KEYED_COLUMNS to retrieve value + * by "object property". + * For example, in `[{bb: 124, aa: 543}, ...]`, "aa" and "bb" is "object property". + * + * Deliberately name it as "property" rather than "name" to prevent it from been used in + * SOURCE_FORMAT_ARRAY_ROWS, becuase if it comes from series, it probably + * can not be shared by different series. + */ + property?: string; + + /** + * When using category axis. + * Category strings will be collected and stored in ordinalMeta.categories. + * And store will store the index of categories. + */ + ordinalMeta?: OrdinalMeta, + + /** + * Offset for ordinal parsing and collect + */ + ordinalOffset?: number +} + +let defaultDimValueGetters: {[sourceFormat: string]: DimValueGetter}; + +function getIndicesCtor(rawCount: number): DataArrayLikeConstructor { + // The possible max value in this._indicies is always this._rawCount despite of filtering. + return rawCount > 65535 ? CtorUint32Array : CtorUint16Array; +}; +function getInitialExtent(): [number, number] { + return [Infinity, -Infinity]; +}; +function cloneChunk(originalChunk: DataValueChunk): DataValueChunk { + const Ctor = originalChunk.constructor; + // Only shallow clone is enough when Array. + return Ctor === Array + ? (originalChunk as Array).slice() + : new (Ctor as DataTypedArrayConstructor)(originalChunk as DataTypedArray); +} + +function prepareStore( + store: DataValueChunk[], + dimIdx: number, + dimType: DataStoreDimensionType, + end: number, + append?: boolean +): void { + const DataCtor = dataCtors[dimType || 'float']; + + if (append) { + const oldStore = store[dimIdx]; + const oldLen = oldStore && oldStore.length; + if (!(oldLen === end)) { + const newStore = new DataCtor(end); + // The cost of the copy is probably inconsiderable + // within the initial chunkSize. + for (let j = 0; j < oldLen; j++) { + newStore[j] = oldStore[j]; + } + store[dimIdx] = newStore; + } + } + else { + store[dimIdx] = new DataCtor(end); + } +}; + +/** + * Basically, DataStore API keep immutable. + */ +class DataStore { + private _chunks: DataValueChunk[] = []; + + private _provider: DataProvider; + + // It will not be calculated util needed. + private _rawExtent: [number, number][] = []; + + private _extent: [number, number][] = []; + + // Indices stores the indices of data subset after filtered. + // This data subset will be used in chart. + private _indices: ArrayLike; + + private _count: number = 0; + private _rawCount: number = 0; + + private _dimensions: DataStoreDimensionDefine[]; + private _dimValueGetter: DimValueGetter; + + private _calcDimNameToIdx = createHashMap(); + + defaultDimValueGetter: DimValueGetter; + + /** + * Initialize from data + */ + initData( + provider: DataProvider, + inputDimensions: DataStoreDimensionDefine[], + dimValueGetter?: DimValueGetter + ): void { + if (__DEV__) { + assert( + isFunction(provider.getItem) && isFunction(provider.count), + 'Inavlid data provider.' + ); + } + + this._provider = provider; + + // Clear + this._chunks = []; + this._indices = null; + this.getRawIndex = this._getRawIdxIdentity; + + const source = provider.getSource(); + const defaultGetter = this.defaultDimValueGetter = + defaultDimValueGetters[source.sourceFormat]; + // Default dim value getter + this._dimValueGetter = dimValueGetter || defaultGetter; + + // Reset raw extent. + this._rawExtent = []; + const willRetrieveDataByName = shouldRetrieveDataByName(source); + this._dimensions = map(inputDimensions, dim => { + if (__DEV__) { + if (willRetrieveDataByName) { + assert(dim.property != null); + } + } + return { + // Only pick these two props. Not leak other properties like orderMeta. + type: dim.type, + property: dim.property + }; + }); + + this._initDataFromProvider(0, provider.count()); + } + + getProvider(): DataProvider { + return this._provider; + } + + /** + * Caution: even when a `source` instance owned by a series, the created data store + * may still be shared by different sereis (the source hash does not use all `source` + * props, see `sourceManager`). In this case, the `source` props that are not used in + * hash (like `source.dimensionDefine`) probably only belongs to a certain series and + * thus should not be fetch here. + */ + getSource(): Source { + return this._provider.getSource(); + } + + /** + * @caution Only used in dataStack. + */ + ensureCalculationDimension(dimName: DimensionName, type: DataStoreDimensionType): DimensionIndex { + const calcDimNameToIdx = this._calcDimNameToIdx; + const dimensions = this._dimensions; + + let calcDimIdx = calcDimNameToIdx.get(dimName); + if (calcDimIdx != null) { + if (dimensions[calcDimIdx].type === type) { + return calcDimIdx; + } + } + else { + calcDimIdx = dimensions.length; + } + + dimensions[calcDimIdx] = { type: type }; + calcDimNameToIdx.set(dimName, calcDimIdx); + + this._chunks[calcDimIdx] = new dataCtors[type || 'float'](this._rawCount); + this._rawExtent[calcDimIdx] = getInitialExtent(); + + return calcDimIdx; + } + + collectOrdinalMeta( + dimIdx: number, + ordinalMeta: OrdinalMeta + ): void { + const chunk = this._chunks[dimIdx]; + const dim = this._dimensions[dimIdx]; + const rawExtents = this._rawExtent; + + const offset = dim.ordinalOffset || 0; + const len = chunk.length; + + if (offset === 0) { + // We need to reset the rawExtent if collect is from start. + // Because this dimension may be guessed as number and calcuating a wrong extent. + rawExtents[dimIdx] = getInitialExtent(); + } + + const dimRawExtent = rawExtents[dimIdx]; + + // Parse from previous data offset. len may be changed after appendData + for (let i = offset; i < len; i++) { + const val = (chunk as any)[i] = ordinalMeta.parseAndCollect(chunk[i]); + dimRawExtent[0] = Math.min(val, dimRawExtent[0]); + dimRawExtent[1] = Math.max(val, dimRawExtent[1]); + } + + dim.ordinalMeta = ordinalMeta; + dim.ordinalOffset = len; + dim.type = 'ordinal'; // Force to be ordinal + } + + getOrdinalMeta(dimIdx: number): OrdinalMeta { + const dimInfo = this._dimensions[dimIdx]; + const ordinalMeta = dimInfo.ordinalMeta; + return ordinalMeta; + } + + getDimensionProperty(dimIndex: DimensionIndex): DataStoreDimensionDefine['property'] { + const item = this._dimensions[dimIndex]; + return item && item.property; + } + + /** + * Caution: Can be only called on raw data (before `this._indices` created). + */ + appendData(data: ArrayLike): number[] { + if (__DEV__) { + assert(!this._indices, 'appendData can only be called on raw data.'); + } + + const provider = this._provider; + const start = this.count(); + provider.appendData(data); + let end = provider.count(); + if (!provider.persistent) { + end += start; + } + + if (start < end) { + this._initDataFromProvider(start, end, true); + } + + return [start, end]; + } + + appendValues(values: any[][], minFillLen?: number): { start: number; end: number } { + const chunks = this._chunks; + const dimensions = this._dimensions; + const dimLen = dimensions.length; + const rawExtent = this._rawExtent; + + const start = this.count(); + const end = start + Math.max(values.length, minFillLen || 0); + + for (let i = 0; i < dimLen; i++) { + const dim = dimensions[i]; + prepareStore(chunks, i, dim.type, end, true); + } + + const emptyDataItem: number[] = []; + for (let idx = start; idx < end; idx++) { + const sourceIdx = idx - start; + // Store the data by dimensions + for (let dimIdx = 0; dimIdx < dimLen; dimIdx++) { + const dim = dimensions[dimIdx]; + const val = defaultDimValueGetters.arrayRows.call( + this, values[sourceIdx] || emptyDataItem, dim.property, sourceIdx, dimIdx + ) as ParsedValueNumeric; + (chunks[dimIdx] as any)[idx] = val; + + const dimRawExtent = rawExtent[dimIdx]; + val < dimRawExtent[0] && (dimRawExtent[0] = val); + val > dimRawExtent[1] && (dimRawExtent[1] = val); + } + } + + this._rawCount = this._count = end; + + return {start, end}; + } + + private _initDataFromProvider( + start: number, + end: number, + append?: boolean + ): void { + const provider = this._provider; + const chunks = this._chunks; + const dimensions = this._dimensions; + const dimLen = dimensions.length; + const rawExtent = this._rawExtent; + const dimNames = map(dimensions, dim => dim.property); + + for (let i = 0; i < dimLen; i++) { + const dim = dimensions[i]; + if (!rawExtent[i]) { + rawExtent[i] = getInitialExtent(); + } + prepareStore(chunks, i, dim.type, end, append); + } + + if (provider.fillStorage) { + provider.fillStorage(start, end, chunks, rawExtent); + } + else { + let dataItem = [] as OptionDataItem; + for (let idx = start; idx < end; idx++) { + // NOTICE: Try not to write things into dataItem + dataItem = provider.getItem(idx, dataItem); + // Each data item is value + // [1, 2] + // 2 + // Bar chart, line chart which uses category axis + // only gives the 'y' value. 'x' value is the indices of category + // Use a tempValue to normalize the value to be a (x, y) value + + // Store the data by dimensions + for (let dimIdx = 0; dimIdx < dimLen; dimIdx++) { + const dimStorage = chunks[dimIdx]; + // PENDING NULL is empty or zero + const val = this._dimValueGetter( + dataItem, dimNames[dimIdx], idx, dimIdx + ) as ParsedValueNumeric; + (dimStorage as ParsedValue[])[idx] = val; + + const dimRawExtent = rawExtent[dimIdx]; + val < dimRawExtent[0] && (dimRawExtent[0] = val); + val > dimRawExtent[1] && (dimRawExtent[1] = val); + } + } + } + + if (!provider.persistent && provider.clean) { + // Clean unused data if data source is typed array. + provider.clean(); + } + + this._rawCount = this._count = end; + // Reset data extent + this._extent = []; + } + + count(): number { + return this._count; + } + + /** + * Get value. Return NaN if idx is out of range. + */ + get(dim: DimensionIndex, idx: number): ParsedValue { + if (!(idx >= 0 && idx < this._count)) { + return NaN; + } + const dimStore = this._chunks[dim]; + return dimStore ? dimStore[this.getRawIndex(idx)] : NaN; + } + + getValues(idx: number): ParsedValue[]; + getValues(dimensions: readonly DimensionIndex[], idx?: number): ParsedValue[] + getValues(dimensions: readonly DimensionIndex[] | number, idx?: number): ParsedValue[] { + const values = []; + let dimArr: DimensionIndex[] = []; + if (idx == null) { + idx = dimensions as number; + // TODO get all from store? + dimensions = []; + // All dimensions + for (let i = 0; i < this._dimensions.length; i++) { + dimArr.push(i); + } + } + else { + dimArr = dimensions as DimensionIndex[]; + } + + for (let i = 0, len = dimArr.length; i < len; i++) { + values.push(this.get(dimArr[i], idx)); + } + + return values; + } + + /** + * @param dim concrete dim + */ + getByRawIndex(dim: DimensionIndex, rawIdx: number): ParsedValue { + if (!(rawIdx >= 0 && rawIdx < this._rawCount)) { + return NaN; + } + const dimStore = this._chunks[dim]; + return dimStore ? dimStore[rawIdx] : NaN; + } + + /** + * Get sum of data in one dimension + */ + getSum(dim: DimensionIndex): number { + const dimData = this._chunks[dim]; + let sum = 0; + if (dimData) { + for (let i = 0, len = this.count(); i < len; i++) { + const value = this.get(dim, i) as number; + if (!isNaN(value)) { + sum += value; + } + } + } + return sum; + } + + /** + * Get median of data in one dimension + */ + getMedian(dim: DimensionIndex): number { + const dimDataArray: ParsedValue[] = []; + // map all data of one dimension + this.each([dim], function (val) { + if (!isNaN(val as number)) { + dimDataArray.push(val); + } + }); + + // TODO + // Use quick select? + const sortedDimDataArray = dimDataArray.sort(function (a: number, b: number) { + return a - b; + }) as number[]; + const len = this.count(); + // calculate median + return len === 0 + ? 0 + : len % 2 === 1 + ? sortedDimDataArray[(len - 1) / 2] + : (sortedDimDataArray[len / 2] + sortedDimDataArray[len / 2 - 1]) / 2; + } + + /** + * Retreive the index with given raw data index + */ + indexOfRawIndex(rawIndex: number): number { + if (rawIndex >= this._rawCount || rawIndex < 0) { + return -1; + } + + if (!this._indices) { + return rawIndex; + } + + // Indices are ascending + const indices = this._indices; + + // If rawIndex === dataIndex + const rawDataIndex = indices[rawIndex]; + if (rawDataIndex != null && rawDataIndex < this._count && rawDataIndex === rawIndex) { + return rawIndex; + } + + let left = 0; + let right = this._count - 1; + while (left <= right) { + const mid = (left + right) / 2 | 0; + if (indices[mid] < rawIndex) { + left = mid + 1; + } + else if (indices[mid] > rawIndex) { + right = mid - 1; + } + else { + return mid; + } + } + return -1; + } + + + /** + * Retreive the index of nearest value + * @param dim + * @param value + * @param [maxDistance=Infinity] + * @return If and only if multiple indices has + * the same value, they are put to the result. + */ + indicesOfNearest( + dim: DimensionIndex, value: number, maxDistance?: number + ): number[] { + const chunks = this._chunks; + const dimData = chunks[dim]; + const nearestIndices: number[] = []; + + if (!dimData) { + return nearestIndices; + } + + if (maxDistance == null) { + maxDistance = Infinity; + } + + let minDist = Infinity; + let minDiff = -1; + let nearestIndicesLen = 0; + + // Check the test case of `test/ut/spec/data/SeriesData.js`. + for (let i = 0, len = this.count(); i < len; i++) { + const dataIndex = this.getRawIndex(i); + const diff = value - (dimData[dataIndex] as number); + const dist = Math.abs(diff); + if (dist <= maxDistance) { + // When the `value` is at the middle of `this.get(dim, i)` and `this.get(dim, i+1)`, + // we'd better not push both of them to `nearestIndices`, otherwise it is easy to + // get more than one item in `nearestIndices` (more specifically, in `tooltip`). + // So we chose the one that `diff >= 0` in this csae. + // But if `this.get(dim, i)` and `this.get(dim, j)` get the same value, both of them + // should be push to `nearestIndices`. + if (dist < minDist + || (dist === minDist && diff >= 0 && minDiff < 0) + ) { + minDist = dist; + minDiff = diff; + nearestIndicesLen = 0; + } + if (diff === minDiff) { + nearestIndices[nearestIndicesLen++] = i; + } + } + } + nearestIndices.length = nearestIndicesLen; + + return nearestIndices; + } + + getIndices(): ArrayLike { + let newIndices; + + const indices = this._indices; + if (indices) { + const Ctor = indices.constructor as DataArrayLikeConstructor; + const thisCount = this._count; + // `new Array(a, b, c)` is different from `new Uint32Array(a, b, c)`. + if (Ctor === Array) { + newIndices = new Ctor(thisCount); + for (let i = 0; i < thisCount; i++) { + newIndices[i] = indices[i]; + } + } + else { + newIndices = new (Ctor as DataTypedArrayConstructor)( + (indices as DataTypedArray).buffer, 0, thisCount + ); + } + } + else { + const Ctor = getIndicesCtor(this._rawCount); + newIndices = new Ctor(this.count()); + for (let i = 0; i < newIndices.length; i++) { + newIndices[i] = i; + } + } + + return newIndices; + } + + /** + * Data filter. + */ + filter( + dims: DimensionIndex[], + cb: FilterCb + ): DataStore { + if (!this._count) { + return this; + } + + const newStore = this.clone(); + + const count = newStore.count(); + const Ctor = getIndicesCtor(newStore._rawCount); + const newIndices = new Ctor(count); + const value = []; + const dimSize = dims.length; + + let offset = 0; + const dim0 = dims[0]; + const chunks = newStore._chunks; + + for (let i = 0; i < count; i++) { + let keep; + const rawIdx = newStore.getRawIndex(i); + // Simple optimization + if (dimSize === 0) { + keep = (cb as FilterCb0)(i); + } + else if (dimSize === 1) { + const val = chunks[dim0][rawIdx]; + keep = (cb as FilterCb1)(val, i); + } + else { + let k = 0; + for (; k < dimSize; k++) { + value[k] = chunks[dims[k]][rawIdx]; + } + value[k] = i; + keep = (cb as FilterCb).apply(null, value); + } + if (keep) { + newIndices[offset++] = rawIdx; + } + } + + // Set indices after filtered. + if (offset < count) { + newStore._indices = newIndices; + } + newStore._count = offset; + // Reset data extent + newStore._extent = []; + + newStore._updateGetRawIdx(); + + return newStore; + } + + /** + * Select data in range. (For optimization of filter) + * (Manually inline code, support 5 million data filtering in data zoom.) + */ + selectRange(range: {[dimIdx: number]: [number, number]}): DataStore { + const newStore = this.clone(); + + const len = newStore._count; + + if (!len) { + return this; + } + + const dims = keys(range); + const dimSize = dims.length; + if (!dimSize) { + return this; + } + + const originalCount = newStore.count(); + const Ctor = getIndicesCtor(newStore._rawCount); + const newIndices = new Ctor(originalCount); + + let offset = 0; + const dim0 = dims[0]; + + const min = range[dim0][0]; + const max = range[dim0][1]; + const storeArr = newStore._chunks; + + let quickFinished = false; + if (!newStore._indices) { + // Extreme optimization for common case. About 2x faster in chrome. + let idx = 0; + if (dimSize === 1) { + const dimStorage = storeArr[dims[0]]; + for (let i = 0; i < len; i++) { + const val = dimStorage[i]; + // NaN will not be filtered. Consider the case, in line chart, empty + // value indicates the line should be broken. But for the case like + // scatter plot, a data item with empty value will not be rendered, + // but the axis extent may be effected if some other dim of the data + // item has value. Fortunately it is not a significant negative effect. + if ( + (val >= min && val <= max) || isNaN(val as any) + ) { + newIndices[offset++] = idx; + } + idx++; + } + quickFinished = true; + } + else if (dimSize === 2) { + const dimStorage = storeArr[dims[0]]; + const dimStorage2 = storeArr[dims[1]]; + const min2 = range[dims[1]][0]; + const max2 = range[dims[1]][1]; + for (let i = 0; i < len; i++) { + const val = dimStorage[i]; + const val2 = dimStorage2[i]; + // Do not filter NaN, see comment above. + if (( + (val >= min && val <= max) || isNaN(val as any) + ) + && ( + (val2 >= min2 && val2 <= max2) || isNaN(val2 as any) + ) + ) { + newIndices[offset++] = idx; + } + idx++; + } + quickFinished = true; + } + } + if (!quickFinished) { + if (dimSize === 1) { + for (let i = 0; i < originalCount; i++) { + const rawIndex = newStore.getRawIndex(i); + const val = storeArr[dims[0]][rawIndex]; + // Do not filter NaN, see comment above. + if ( + (val >= min && val <= max) || isNaN(val as any) + ) { + newIndices[offset++] = rawIndex; + } + } + } + else { + for (let i = 0; i < originalCount; i++) { + let keep = true; + const rawIndex = newStore.getRawIndex(i); + for (let k = 0; k < dimSize; k++) { + const dimk = dims[k]; + const val = storeArr[dimk][rawIndex]; + // Do not filter NaN, see comment above. + if (val < range[dimk][0] || val > range[dimk][1]) { + keep = false; + } + } + if (keep) { + newIndices[offset++] = newStore.getRawIndex(i); + } + } + } + } + + // Set indices after filtered. + if (offset < originalCount) { + newStore._indices = newIndices; + } + newStore._count = offset; + // Reset data extent + newStore._extent = []; + + newStore._updateGetRawIdx(); + + return newStore; + } + + // /** + // * Data mapping to a plain array + // */ + // mapArray(dims: DimensionIndex[], cb: MapArrayCb): any[] { + // const result: any[] = []; + // this.each(dims, function () { + // result.push(cb && (cb as MapArrayCb).apply(null, arguments)); + // }); + // return result; + // } + + /** + * Data mapping to a new List with given dimensions + */ + map(dims: DimensionIndex[], cb: MapCb): DataStore { + // TODO only clone picked chunks. + const target = this.clone(dims); + this._updateDims(target, dims, cb); + return target; + } + + /** + * @caution Danger!! Only used in dataStack. + */ + modify(dims: DimensionIndex[], cb: MapCb) { + this._updateDims(this, dims, cb); + } + + private _updateDims( + target: DataStore, + dims: DimensionIndex[], + cb: MapCb + ) { + const targetChunks = target._chunks; + + const tmpRetValue = []; + const dimSize = dims.length; + const dataCount = target.count(); + const values = []; + const rawExtent = target._rawExtent; + + for (let i = 0; i < dims.length; i++) { + rawExtent[dims[i]] = getInitialExtent(); + } + + for (let dataIndex = 0; dataIndex < dataCount; dataIndex++) { + const rawIndex = target.getRawIndex(dataIndex); + + for (let k = 0; k < dimSize; k++) { + values[k] = targetChunks[dims[k]][rawIndex]; + } + values[dimSize] = dataIndex; + + let retValue = cb && cb.apply(null, values); + if (retValue != null) { + // a number or string (in oridinal dimension)? + if (typeof retValue !== 'object') { + tmpRetValue[0] = retValue; + retValue = tmpRetValue; + } + + for (let i = 0; i < retValue.length; i++) { + const dim = dims[i]; + const val = retValue[i]; + const rawExtentOnDim = rawExtent[dim]; + + const dimStore = targetChunks[dim]; + if (dimStore) { + (dimStore as ParsedValue[])[rawIndex] = val; + } + + if (val < rawExtentOnDim[0]) { + rawExtentOnDim[0] = val as number; + } + if (val > rawExtentOnDim[1]) { + rawExtentOnDim[1] = val as number; + } + } + } + } + } + + /** + * Large data down sampling using largest-triangle-three-buckets + * @param {string} valueDimension + * @param {number} targetCount + */ + lttbDownSample( + valueDimension: DimensionIndex, + rate: number + ): DataStore { + const target = this.clone([valueDimension], true); + const targetStorage = target._chunks; + const dimStore = targetStorage[valueDimension]; + const len = this.count(); + + let sampledIndex = 0; + + const frameSize = Math.floor(1 / rate); + + let currentRawIndex = this.getRawIndex(0); + let maxArea; + let area; + let nextRawIndex; + + const newIndices = new (getIndicesCtor(this._rawCount))(Math.ceil(len / frameSize) + 2); + + // First frame use the first data. + newIndices[sampledIndex++] = currentRawIndex; + for (let i = 1; i < len - 1; i += frameSize) { + const nextFrameStart = Math.min(i + frameSize, len - 1); + const nextFrameEnd = Math.min(i + frameSize * 2, len); + + const avgX = (nextFrameEnd + nextFrameStart) / 2; + let avgY = 0; + + for (let idx = nextFrameStart; idx < nextFrameEnd; idx++) { + const rawIndex = this.getRawIndex(idx); + const y = dimStore[rawIndex] as number; + if (isNaN(y)) { + continue; + } + avgY += y as number; + } + avgY /= (nextFrameEnd - nextFrameStart); + + const frameStart = i; + const frameEnd = Math.min(i + frameSize, len); + + const pointAX = i - 1; + const pointAY = dimStore[currentRawIndex] as number; + + maxArea = -1; + + nextRawIndex = frameStart; + // Find a point from current frame that construct a triangel with largest area with previous selected point + // And the average of next frame. + for (let idx = frameStart; idx < frameEnd; idx++) { + const rawIndex = this.getRawIndex(idx); + const y = dimStore[rawIndex] as number; + if (isNaN(y)) { + continue; + } + // Calculate triangle area over three buckets + area = Math.abs((pointAX - avgX) * (y - pointAY) + - (pointAX - idx) * (avgY - pointAY) + ); + if (area > maxArea) { + maxArea = area; + nextRawIndex = rawIndex; // Next a is this b + } + } + + newIndices[sampledIndex++] = nextRawIndex; + + currentRawIndex = nextRawIndex; // This a is the next a (chosen b) + } + + // First frame use the last data. + newIndices[sampledIndex++] = this.getRawIndex(len - 1); + target._count = sampledIndex; + target._indices = newIndices; + + target.getRawIndex = this._getRawIdx; + return target; + } + + + /** + * Large data down sampling on given dimension + * @param sampleIndex Sample index for name and id + */ + downSample( + dimension: DimensionIndex, + rate: number, + sampleValue: (frameValues: ArrayLike) => ParsedValueNumeric, + sampleIndex: (frameValues: ArrayLike, value: ParsedValueNumeric) => number + ): DataStore { + const target = this.clone([dimension], true); + const targetStorage = target._chunks; + + const frameValues = []; + let frameSize = Math.floor(1 / rate); + + const dimStore = targetStorage[dimension]; + const len = this.count(); + const rawExtentOnDim = target._rawExtent[dimension] = getInitialExtent(); + + const newIndices = new (getIndicesCtor(this._rawCount))(Math.ceil(len / frameSize)); + + let offset = 0; + for (let i = 0; i < len; i += frameSize) { + // Last frame + if (frameSize > len - i) { + frameSize = len - i; + frameValues.length = frameSize; + } + for (let k = 0; k < frameSize; k++) { + const dataIdx = this.getRawIndex(i + k); + frameValues[k] = dimStore[dataIdx]; + } + const value = sampleValue(frameValues); + const sampleFrameIdx = this.getRawIndex( + Math.min(i + sampleIndex(frameValues, value) || 0, len - 1) + ); + // Only write value on the filtered data + (dimStore as number[])[sampleFrameIdx] = value; + + if (value < rawExtentOnDim[0]) { + rawExtentOnDim[0] = value; + } + if (value > rawExtentOnDim[1]) { + rawExtentOnDim[1] = value; + } + + newIndices[offset++] = sampleFrameIdx; + } + + target._count = offset; + target._indices = newIndices; + + target._updateGetRawIdx(); + + return target; + } + + /** + * Data iteration + * @param ctx default this + * @example + * list.each('x', function (x, idx) {}); + * list.each(['x', 'y'], function (x, y, idx) {}); + * list.each(function (idx) {}) + */ + each(dims: DimensionIndex[], cb: EachCb): void { + if (!this._count) { + return; + } + const dimSize = dims.length; + const chunks = this._chunks; + + for (let i = 0, len = this.count(); i < len; i++) { + const rawIdx = this.getRawIndex(i); + // Simple optimization + switch (dimSize) { + case 0: + (cb as EachCb0)(i); + break; + case 1: + (cb as EachCb1)(chunks[dims[0]][rawIdx], i); + break; + case 2: + (cb as EachCb2)( + chunks[dims[0]][rawIdx], chunks[dims[1]][rawIdx], i + ); + break; + default: + let k = 0; + const value = []; + for (; k < dimSize; k++) { + value[k] = chunks[dims[k]][rawIdx]; + } + // Index + value[k] = i; + (cb as EachCb).apply(null, value); + } + } + } + + /** + * Get extent of data in one dimension + */ + getDataExtent(dim: DimensionIndex): [number, number] { + // Make sure use concrete dim as cache name. + const dimData = this._chunks[dim]; + const initialExtent = getInitialExtent(); + + if (!dimData) { + return initialExtent; + } + + // Make more strict checkings to ensure hitting cache. + const currEnd = this.count(); + + // Consider the most cases when using data zoom, `getDataExtent` + // happened before filtering. We cache raw extent, which is not + // necessary to be cleared and recalculated when restore data. + const useRaw = !this._indices; + let dimExtent: [number, number]; + + if (useRaw) { + return this._rawExtent[dim].slice() as [number, number]; + } + dimExtent = this._extent[dim]; + if (dimExtent) { + return dimExtent.slice() as [number, number]; + } + dimExtent = initialExtent; + + let min = dimExtent[0]; + let max = dimExtent[1]; + + for (let i = 0; i < currEnd; i++) { + const rawIdx = this.getRawIndex(i); + const value = dimData[rawIdx] as ParsedValueNumeric; + value < min && (min = value); + value > max && (max = value); + } + + dimExtent = [min, max]; + + this._extent[dim] = dimExtent; + + return dimExtent; + } + + /** + * Get raw data index. + * Do not initialize. + * Default `getRawIndex`. And it can be changed. + */ + getRawIndex: (idx: number) => number; + + /** + * Get raw data item + */ + getRawDataItem(idx: number): OptionDataItem { + const rawIdx = this.getRawIndex(idx); + if (!this._provider.persistent) { + const val = []; + const chunks = this._chunks; + for (let i = 0; i < chunks.length; i++) { + val.push(chunks[i][rawIdx]); + } + return val; + } + else { + return this._provider.getItem(rawIdx); + } + } + + /** + * Clone shallow. + * + * @param clonedDims Determine which dims to clone. Will share the data if not specified. + */ + clone(clonedDims?: DimensionIndex[], ignoreIndices?: boolean): DataStore { + const target = new DataStore(); + const chunks = this._chunks; + const clonedDimsMap = clonedDims && reduce(clonedDims, (obj, dimIdx) => { + obj[dimIdx] = true; + return obj; + }, {} as Record); + + if (clonedDimsMap) { + for (let i = 0; i < chunks.length; i++) { + // Not clone if dim is not picked. + target._chunks[i] = !clonedDimsMap[i] ? chunks[i] : cloneChunk(chunks[i]); + } + } + else { + target._chunks = chunks; + } + this._copyCommonProps(target); + + if (!ignoreIndices) { + target._indices = this._cloneIndices(); + } + target._updateGetRawIdx(); + return target; + } + + private _copyCommonProps(target: DataStore): void { + target._count = this._count; + target._rawCount = this._rawCount; + target._provider = this._provider; + target._dimensions = this._dimensions; + + target._extent = clone(this._extent); + target._rawExtent = clone(this._rawExtent); + } + + private _cloneIndices(): DataStore['_indices'] { + if (this._indices) { + const Ctor = this._indices.constructor as DataArrayLikeConstructor; + let indices; + if (Ctor === Array) { + const thisCount = this._indices.length; + indices = new Ctor(thisCount); + for (let i = 0; i < thisCount; i++) { + indices[i] = this._indices[i]; + } + } + else { + indices = new (Ctor as DataTypedArrayConstructor)(this._indices); + } + return indices; + } + return null; + } + + private _getRawIdxIdentity(idx: number): number { + return idx; + } + private _getRawIdx(idx: number): number { + if (idx < this._count && idx >= 0) { + return this._indices[idx]; + } + return -1; + } + + private _updateGetRawIdx(): void { + this.getRawIndex = this._indices ? this._getRawIdx : this._getRawIdxIdentity; + } + + private static internalField = (function () { + + function getDimValueSimply( + this: DataStore, dataItem: any, property: string, dataIndex: number, dimIndex: number + ): ParsedValue { + return parseDataValue(dataItem[dimIndex], this._dimensions[dimIndex]); + } + + defaultDimValueGetters = { + + arrayRows: getDimValueSimply, + + objectRows( + this: DataStore, dataItem: any, property: string, dataIndex: number, dimIndex: number + ): ParsedValue { + return parseDataValue(dataItem[property], this._dimensions[dimIndex]); + }, + + keyedColumns: getDimValueSimply, + + original( + this: DataStore, dataItem: any, property: string, dataIndex: number, dimIndex: number + ): ParsedValue { + // Performance sensitive, do not use modelUtil.getDataItemValue. + // If dataItem is an plain object with no value field, the let `value` + // will be assigned with the object, but it will be tread correctly + // in the `convertValue`. + const value = dataItem && (dataItem.value == null ? dataItem : dataItem.value); + + return parseDataValue( + (value instanceof Array) + ? value[dimIndex] + // If value is a single number or something else not array. + : value, + this._dimensions[dimIndex] + ); + }, + + typedArray: function ( + this: DataStore, dataItem: any, property: string, dataIndex: number, dimIndex: number + ): ParsedValue { + return dataItem[dimIndex]; + } + + }; + + })(); +} + +export default DataStore; \ No newline at end of file diff --git a/src/data/Graph.ts b/src/data/Graph.ts index 75ec38f654..4605ff1593 100644 --- a/src/data/Graph.ts +++ b/src/data/Graph.ts @@ -19,7 +19,7 @@ import * as zrUtil from 'zrender/src/core/util'; import { Dictionary } from 'zrender/src/core/types'; -import List from './List'; +import SeriesData from './SeriesData'; import Model from '../model/Model'; import Element from 'zrender/src/Element'; import { DimensionLoose, ParsedValue } from '../util/types'; @@ -36,9 +36,9 @@ class Graph { readonly edges: GraphEdge[] = []; - data: List; + data: SeriesData; - edgeData: List; + edgeData: SeriesData; /** * Whether directed graph. @@ -458,7 +458,7 @@ function createGraphDataProxyMixin( */ getValue(this: Host, dimension?: DimensionLoose): ParsedValue { const data = this[hostName][dataName]; - return data.get(data.getDimension(dimension || 'value'), this.dataIndex); + return data.getStore().get(data.getDimensionIndex(dimension || 'value'), this.dataIndex); }, // TODO: TYPE stricter type. setVisual(this: Host, key: string | Dictionary, value?: any) { diff --git a/src/data/List.ts b/src/data/List.ts deleted file mode 100644 index 0ad2792fa4..0000000000 --- a/src/data/List.ts +++ /dev/null @@ -1,2278 +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. -*/ - -/* global Float64Array, Int32Array, Uint32Array, Uint16Array */ - -/** - * List for data storage - */ - -import * as zrUtil from 'zrender/src/core/util'; -import {PathStyleProps} from 'zrender/src/graphic/Path'; -import Model from '../model/Model'; -import DataDiffer from './DataDiffer'; -import {DefaultDataProvider, DataProvider} from './helper/dataProvider'; -import {summarizeDimensions, DimensionSummary} from './helper/dimensionHelper'; -import DataDimensionInfo from './DataDimensionInfo'; -import {ArrayLike, Dictionary, FunctionPropertyNames} from 'zrender/src/core/types'; -import Element from 'zrender/src/Element'; -import { - DimensionIndex, DimensionName, DimensionLoose, OptionDataItem, - ParsedValue, ParsedValueNumeric, OrdinalNumber, DimensionUserOuput, - ModelOption, SeriesDataType, OptionSourceData, SOURCE_FORMAT_TYPED_ARRAY, SOURCE_FORMAT_ORIGINAL, - DecalObject -} from '../util/types'; -import {isDataItemOption, convertOptionIdName} from '../util/model'; -import { getECData } from '../util/innerStore'; -import type Graph from './Graph'; -import type Tree from './Tree'; -import type { VisualMeta } from '../component/visualMap/VisualMapModel'; -import { parseDataValue } from './helper/dataValueHelper'; -import {isSourceInstance, Source} from './Source'; -import OrdinalMeta from './OrdinalMeta'; -import { LineStyleProps } from '../model/mixin/lineStyle'; - -const mathFloor = Math.floor; -const isObject = zrUtil.isObject; -const map = zrUtil.map; - -const UNDEFINED = 'undefined'; -const INDEX_NOT_FOUND = -1; - -// Use prefix to avoid index to be the same as otherIdList[idx], -// which will cause weird udpate animation. -const ID_PREFIX = 'e\0\0'; - -const dataCtors = { - 'float': typeof Float64Array === UNDEFINED - ? Array : Float64Array, - 'int': typeof Int32Array === UNDEFINED - ? Array : Int32Array, - // Ordinal data type can be string or int - 'ordinal': Array, - 'number': Array, - 'time': Array -}; - -export type ListDimensionType = keyof typeof dataCtors; - -// Caution: MUST not use `new CtorUint32Array(arr, 0, len)`, because the Ctor of array is -// different from the Ctor of typed array. -const CtorUint32Array = typeof Uint32Array === UNDEFINED ? Array : Uint32Array; -const CtorInt32Array = typeof Int32Array === UNDEFINED ? Array : Int32Array; -const CtorUint16Array = typeof Uint16Array === UNDEFINED ? Array : Uint16Array; - -type DataTypedArray = Uint32Array | Int32Array | Uint16Array | Float64Array; -type DataTypedArrayConstructor = typeof Uint32Array | typeof Int32Array | typeof Uint16Array | typeof Float64Array; -type DataArrayLikeConstructor = typeof Array | DataTypedArrayConstructor; - - -type DimValueGetter = ( - this: List, - dataItem: any, - dimName: DimensionName, - dataIndex: number, - dimIndex: DimensionIndex -) => ParsedValue; - -type DataValueChunk = ArrayLike; -type DataStorage = {[dimName: string]: DataValueChunk}; -type NameRepeatCount = {[name: string]: number}; - - -type ItrParamDims = DimensionLoose | Array; -// If Ctx not specified, use List as Ctx -type CtxOrList = unknown extends Ctx ? List : Ctx; -type EachCb0 = (this: CtxOrList, idx: number) => void; -type EachCb1 = (this: CtxOrList, x: ParsedValue, idx: number) => void; -type EachCb2 = (this: CtxOrList, x: ParsedValue, y: ParsedValue, idx: number) => void; -type EachCb = (this: CtxOrList, ...args: any) => void; -type FilterCb0 = (this: CtxOrList, idx: number) => boolean; -type FilterCb1 = (this: CtxOrList, x: ParsedValue, idx: number) => boolean; -type FilterCb2 = (this: CtxOrList, x: ParsedValue, y: ParsedValue, idx: number) => boolean; -type FilterCb = (this: CtxOrList, ...args: any) => boolean; -type MapArrayCb0 = (this: CtxOrList, idx: number) => any; -type MapArrayCb1 = (this: CtxOrList, x: ParsedValue, idx: number) => any; -type MapArrayCb2 = (this: CtxOrList, x: ParsedValue, y: ParsedValue, idx: number) => any; -type MapArrayCb = (this: CtxOrList, ...args: any) => any; -type MapCb1 = (this: CtxOrList, x: ParsedValue, idx: number) => ParsedValue | ParsedValue[]; -type MapCb2 = (this: CtxOrList, x: ParsedValue, y: ParsedValue, idx: number) => - ParsedValue | ParsedValue[]; -type MapCb = (this: CtxOrList, ...args: any) => ParsedValue | ParsedValue[]; - - -const TRANSFERABLE_PROPERTIES = [ - 'hasItemOption', '_nameList', '_idList', '_invertedIndicesMap', - '_rawData', '_dimValueGetter', - '_count', '_rawCount', '_nameDimIdx', '_idDimIdx', '_nameRepeatCount' -]; -const CLONE_PROPERTIES = [ - '_extent', '_approximateExtent', '_rawExtent' -]; - -export interface DefaultDataVisual { - style: PathStyleProps - // Draw type determined which prop should be set with encoded color. - // It's only available on the global visual. Use getVisual('drawType') to access it. - // It will be set in visual/style.ts module in the first priority. - drawType: 'fill' | 'stroke' - - symbol?: string - symbolSize?: number | number[] - symbolRotate?: number - symbolKeepAspect?: boolean - symbolOffset?: string | number | (string | number)[] - - liftZ?: number - // For legend. - legendIcon?: string - legendLineStyle?: LineStyleProps - - // visualMap will inject visualMeta data - visualMeta?: VisualMeta[] - - // If color is encoded from palette - colorFromPalette?: boolean - - decal?: DecalObject -} - -export interface DataCalculationInfo { - stackedDimension: string; - stackedByDimension: string; - isStackedByIndex: boolean; - stackedOverDimension: string; - stackResultDimension: string; - stackedOnSeries?: SERIES_MODEL; -} - -// ----------------------------- -// Internal method declarations: -// ----------------------------- -let defaultDimValueGetters: {[sourceFormat: string]: DimValueGetter}; -let prepareInvertedIndex: (list: List) => void; -let getIndicesCtor: (list: List) => DataArrayLikeConstructor; -let prepareStorage: ( - storage: DataStorage, dimInfo: DataDimensionInfo, end: number, append?: boolean -) => void; -let getRawIndexWithoutIndices: (this: List, idx: number) => number; -let getRawIndexWithIndices: (this: List, idx: number) => number; -let getId: (list: List, rawIndex: number) => string; -let getIdNameFromStore: (list: List, dimIdx: number, ordinalMeta: OrdinalMeta, rawIndex: number) => string; -let makeIdFromName: (list: List, idx: number) => void; -let normalizeDimensions: (dimensions: ItrParamDims) => Array; -let validateDimensions: (list: List, dims: DimensionName[]) => void; -let cloneListForMapAndSample: (original: List, excludeDimensions: DimensionName[]) => List; -let getInitialExtent: () => [number, number]; -let setItemDataAndSeriesIndex: (this: Element, child: Element) => void; -let transferProperties: (target: List, source: List) => void; - - -class List< - HostModel extends Model = Model, - Visual extends DefaultDataVisual = DefaultDataVisual -> { - - readonly type = 'list'; - - readonly dimensions: string[]; - - // Infomation of each data dimension, like data type. - private _dimensionInfos: {[dimName: string]: DataDimensionInfo}; - - readonly hostModel: HostModel; - - /** - * @readonly - */ - dataType: SeriesDataType; - - /** - * @readonly - * Host graph if List is used to store graph nodes / edges. - */ - graph?: Graph; - - /** - * @readonly - * Host tree if List is used to store tree ndoes. - */ - tree?: Tree; - - // Indices stores the indices of data subset after filtered. - // This data subset will be used in chart. - private _indices: ArrayLike; - - private _count: number = 0; - private _rawCount: number = 0; - private _storage: DataStorage = {}; - // We have an extra array store here. It's faster to be acessed than KV structured `_storage`. - // We profile the code `storage[dim]` and it seems to be KeyedLoadIC_Megamorphic instead of fast property access. - // Not sure why this happens. But using an extra array seems leads to faster `initData` - // See https://github.com/apache/incubator-echarts/pull/13314 for more explanation. - private _storageArr: DataValueChunk[] = []; - private _nameList: string[] = []; - private _idList: string[] = []; - - // Models of data option is stored sparse for optimizing memory cost - // Never used yet (not used yet). - // private _optionModels: Model[] = []; - - // Global visual properties after visual coding - private _visual: Dictionary = {}; - - // Globel layout properties. - private _layout: Dictionary = {}; - - // Item visual properties after visual coding - private _itemVisuals: Dictionary[] = []; - - // Item layout properties after layout - private _itemLayouts: any[] = []; - - // Graphic elemnents - private _graphicEls: Element[] = []; - - private _rawData: DataProvider; - - // Raw extent will not be cloned, but only transfered. - // It will not be calculated util needed. - private _rawExtent: {[dimName: string]: [number, number]} = {}; - - private _extent: {[dimName: string]: [number, number]} = {}; - - // key: dim, value: extent - private _approximateExtent: {[dimName: string]: [number, number]} = {}; - - private _dimensionsSummary: DimensionSummary; - - private _invertedIndicesMap: {[dimName: string]: ArrayLike}; - - private _calculationInfo: DataCalculationInfo = {} as DataCalculationInfo; - - // User output info of this data. - // DO NOT use it in other places! - // When preparing user params for user callbacks, we have - // to clone these inner data structures to prevent users - // from modifying them to effect built-in logic. And for - // performance consideration we make this `userOutput` to - // avoid clone them too many times. - readonly userOutput: DimensionUserOuput; - - // Having detected that there is data item is non primitive type - // (in type `OptionDataItemObject`). - // Like `data: [ { value: xx, itemStyle: {...} }, ...]` - // At present it only happen in `SOURCE_FORMAT_ORIGINAL`. - hasItemOption: boolean = true; - - // @readonly - defaultDimValueGetter: DimValueGetter; - private _dimValueGetter: DimValueGetter; - private _dimValueGetterArrayRows: DimValueGetter; - - // id or name is used on dynamic data, mapping old and new items. - // When generating id from name, avoid repeat. - private _nameRepeatCount: NameRepeatCount; - private _nameDimIdx: number; - private _nameOrdinalMeta: OrdinalMeta; - private _idDimIdx: number; - private _idOrdinalMeta: OrdinalMeta; - private _dontMakeIdFromName: boolean; - - private __wrappedMethods: string[]; - - // Methods that create a new list based on this list should be listed here. - // Notice that those method should `RETURN` the new list. - TRANSFERABLE_METHODS = ['cloneShallow', 'downSample', 'lttbDownSample', 'map'] as const; - // Methods that change indices of this list should be listed here. - CHANGABLE_METHODS = ['filterSelf', 'selectRange'] as const; - DOWNSAMPLE_METHODS = ['downSample', 'lttbDownSample'] as const; - - /** - * @param dimensions - * For example, ['someDimName', {name: 'someDimName', type: 'someDimType'}, ...]. - * Dimensions should be concrete names like x, y, z, lng, lat, angle, radius - */ - constructor(dimensions: Array, hostModel: HostModel) { - dimensions = dimensions || ['x', 'y']; - - const dimensionInfos: Dictionary = {}; - const dimensionNames = []; - const invertedIndicesMap: Dictionary = {}; - - for (let i = 0; i < dimensions.length; i++) { - // Use the original dimensions[i], where other flag props may exists. - const dimInfoInput = dimensions[i]; - - const dimensionInfo: DataDimensionInfo = - zrUtil.isString(dimInfoInput) - ? new DataDimensionInfo({name: dimInfoInput}) - : !(dimInfoInput instanceof DataDimensionInfo) - ? new DataDimensionInfo(dimInfoInput) - : dimInfoInput; - - const dimensionName = dimensionInfo.name; - dimensionInfo.type = dimensionInfo.type || 'float'; - if (!dimensionInfo.coordDim) { - dimensionInfo.coordDim = dimensionName; - dimensionInfo.coordDimIndex = 0; - } - - const otherDims = dimensionInfo.otherDims = dimensionInfo.otherDims || {}; - dimensionNames.push(dimensionName); - dimensionInfos[dimensionName] = dimensionInfo; - - dimensionInfo.index = i; - - if (dimensionInfo.createInvertedIndices) { - invertedIndicesMap[dimensionName] = []; - } - if (otherDims.itemName === 0) { - this._nameDimIdx = i; - this._nameOrdinalMeta = dimensionInfo.ordinalMeta; - } - if (otherDims.itemId === 0) { - this._idDimIdx = i; - this._idOrdinalMeta = dimensionInfo.ordinalMeta; - } - } - - this.dimensions = dimensionNames; - this._dimensionInfos = dimensionInfos; - this.hostModel = hostModel; - - // Cache summary info for fast visit. See "dimensionHelper". - this._dimensionsSummary = summarizeDimensions(this); - - this._invertedIndicesMap = invertedIndicesMap; - - this.userOutput = this._dimensionsSummary.userOutput; - } - - /** - * The meanings of the input parameter `dim`: - * - * + If dim is a number (e.g., `1`), it means the index of the dimension. - * For example, `getDimension(0)` will return 'x' or 'lng' or 'radius'. - * + If dim is a number-like string (e.g., `"1"`): - * + If there is the same concrete dim name defined in `this.dimensions`, it means that concrete name. - * + If not, it will be converted to a number, which means the index of the dimension. - * (why? because of the backward compatbility. We have been tolerating number-like string in - * dimension setting, although now it seems that it is not a good idea.) - * For example, `visualMap[i].dimension: "1"` is the same meaning as `visualMap[i].dimension: 1`, - * if no dimension name is defined as `"1"`. - * + If dim is a not-number-like string, it means the concrete dim name. - * For example, it can be be default name `"x"`, `"y"`, `"z"`, `"lng"`, `"lat"`, `"angle"`, `"radius"`, - * or customized in `dimensions` property of option like `"age"`. - * - * Get dimension name - * @param dim See above. - * @return Concrete dim name. - */ - getDimension(dim: DimensionLoose): DimensionName { - if (typeof dim === 'number' - // If being a number-like string but not being defined a dimension name. - || (!isNaN(dim as any) && !this._dimensionInfos.hasOwnProperty(dim)) - ) { - dim = this.dimensions[dim as DimensionIndex]; - } - return dim as DimensionName; - } - - /** - * Get type and calculation info of particular dimension - * @param dim - * Dimension can be concrete names like x, y, z, lng, lat, angle, radius - * Or a ordinal number. For example getDimensionInfo(0) will return 'x' or 'lng' or 'radius' - */ - getDimensionInfo(dim: DimensionLoose): DataDimensionInfo { - // Do not clone, because there may be categories in dimInfo. - return this._dimensionInfos[this.getDimension(dim)]; - } - - /** - * concrete dimension name list on coord. - */ - getDimensionsOnCoord(): DimensionName[] { - return this._dimensionsSummary.dataDimsOnCoord.slice(); - } - - /** - * @param coordDim - * @param idx A coordDim may map to more than one data dim. - * If not specified, return the first dim not extra. - * @return concrete data dim. If not found, return null/undefined - */ - mapDimension(coordDim: DimensionName): DimensionName; - mapDimension(coordDim: DimensionName, idx: number): DimensionName; - mapDimension(coordDim: DimensionName, idx?: number): DimensionName { - const dimensionsSummary = this._dimensionsSummary; - - if (idx == null) { - return dimensionsSummary.encodeFirstDimNotExtra[coordDim] as any; - } - - const dims = dimensionsSummary.encode[coordDim]; - return dims ? dims[idx as number] as any : null; - } - - mapDimensionsAll(coordDim: DimensionName): DimensionName[] { - const dimensionsSummary = this._dimensionsSummary; - const dims = dimensionsSummary.encode[coordDim]; - return (dims || []).slice(); - } - - /** - * Initialize from data - * @param data source or data or data provider. - * @param nameList The name of a datum is used on data diff and - * default label/tooltip. - * A name can be specified in encode.itemName, - * or dataItem.name (only for series option data), - * or provided in nameList from outside. - */ - initData( - data: Source | OptionSourceData | DataProvider, - nameList?: string[], - dimValueGetter?: DimValueGetter - ): void { - - const notProvider = isSourceInstance(data) || zrUtil.isArrayLike(data); - const provider: DataProvider = notProvider - ? new DefaultDataProvider(data as Source | OptionSourceData, this.dimensions.length) - : data as DataProvider; - - if (__DEV__) { - zrUtil.assert( - notProvider || ( - zrUtil.isFunction(provider.getItem) - && zrUtil.isFunction(provider.count) - ), - 'Inavlid data provider.' - ); - } - - this._rawData = provider; - const sourceFormat = provider.getSource().sourceFormat; - - // Clear - this._storage = {}; - this._indices = null; - this._dontMakeIdFromName = - this._idDimIdx != null - || sourceFormat === SOURCE_FORMAT_TYPED_ARRAY // Cosndier performance. - || !!provider.fillStorage; - - this._nameList = (nameList || []).slice(); - this._idList = []; - - this._nameRepeatCount = {}; - - if (!dimValueGetter) { - this.hasItemOption = false; - } - - this.defaultDimValueGetter = defaultDimValueGetters[sourceFormat]; - // Default dim value getter - this._dimValueGetter = dimValueGetter = dimValueGetter - || this.defaultDimValueGetter; - this._dimValueGetterArrayRows = defaultDimValueGetters.arrayRows; - - // Reset raw extent. - this._rawExtent = {}; - - this._initDataFromProvider(0, provider.count()); - - // If data has no item option. - if (provider.pure) { - this.hasItemOption = false; - } - } - - getProvider(): DataProvider { - return this._rawData; - } - - /** - * Caution: Can be only called on raw data (before `this._indices` created). - */ - appendData(data: ArrayLike): void { - if (__DEV__) { - zrUtil.assert(!this._indices, 'appendData can only be called on raw data.'); - } - - const rawData = this._rawData; - const start = this.count(); - rawData.appendData(data); - let end = rawData.count(); - if (!rawData.persistent) { - end += start; - } - this._initDataFromProvider(start, end, true); - } - - /** - * Caution: Can be only called on raw data (before `this._indices` created). - * This method does not modify `rawData` (`dataProvider`), but only - * add values to storage. - * - * The final count will be increased by `Math.max(values.length, names.length)`. - * - * @param values That is the SourceType: 'arrayRows', like - * [ - * [12, 33, 44], - * [NaN, 43, 1], - * ['-', 'asdf', 0] - * ] - * Each item is exaclty cooresponding to a dimension. - */ - appendValues(values: any[][], names?: string[]): void { - const storage = this._storage; - const dimensions = this.dimensions; - const dimLen = dimensions.length; - const rawExtent = this._rawExtent; - - const start = this.count(); - const end = start + Math.max(values.length, names ? names.length : 0); - - for (let i = 0; i < dimLen; i++) { - const dim = dimensions[i]; - if (!rawExtent[dim]) { - rawExtent[dim] = getInitialExtent(); - } - prepareStorage(storage, this._dimensionInfos[dim], end, true); - } - - const rawExtentArr = map(dimensions, (dim) => { - return rawExtent[dim]; - }); - - const storageArr = this._storageArr = map(dimensions, (dim) => { - return storage[dim]; - }); - - const emptyDataItem: number[] = []; - for (let idx = start; idx < end; idx++) { - const sourceIdx = idx - start; - // Store the data by dimensions - for (let dimIdx = 0; dimIdx < dimLen; dimIdx++) { - const dim = dimensions[dimIdx]; - const val = this._dimValueGetterArrayRows( - values[sourceIdx] || emptyDataItem, dim, sourceIdx, dimIdx - ) as ParsedValueNumeric; - storageArr[dimIdx][idx] = val; - - const dimRawExtent = rawExtentArr[dimIdx]; - val < dimRawExtent[0] && (dimRawExtent[0] = val); - val > dimRawExtent[1] && (dimRawExtent[1] = val); - } - - if (names) { - this._nameList[idx] = names[sourceIdx]; - if (!this._dontMakeIdFromName) { - makeIdFromName(this, idx); - } - } - } - - this._rawCount = this._count = end; - - // Reset data extent - this._extent = {}; - - prepareInvertedIndex(this); - } - - private _initDataFromProvider(start: number, end: number, append?: boolean): void { - if (start >= end) { - return; - } - - const rawData = this._rawData; - const storage = this._storage; - const dimensions = this.dimensions; - const dimLen = dimensions.length; - const dimensionInfoMap = this._dimensionInfos; - const nameList = this._nameList; - const idList = this._idList; - const rawExtent = this._rawExtent; - const sourceFormat = rawData.getSource().sourceFormat; - const isFormatOriginal = sourceFormat === SOURCE_FORMAT_ORIGINAL; - - for (let i = 0; i < dimLen; i++) { - const dim = dimensions[i]; - if (!rawExtent[dim]) { - rawExtent[dim] = getInitialExtent(); - } - prepareStorage(storage, dimensionInfoMap[dim], end, append); - } - - const storageArr = this._storageArr = map(dimensions, (dim) => { - return storage[dim]; - }); - - const rawExtentArr = map(dimensions, (dim) => { - return rawExtent[dim]; - }); - - if (rawData.fillStorage) { - rawData.fillStorage(start, end, storageArr, rawExtentArr); - } - else { - let dataItem = [] as OptionDataItem; - for (let idx = start; idx < end; idx++) { - // NOTICE: Try not to write things into dataItem - dataItem = rawData.getItem(idx, dataItem); - // Each data item is value - // [1, 2] - // 2 - // Bar chart, line chart which uses category axis - // only gives the 'y' value. 'x' value is the indices of category - // Use a tempValue to normalize the value to be a (x, y) value - - // Store the data by dimensions - for (let dimIdx = 0; dimIdx < dimLen; dimIdx++) { - const dim = dimensions[dimIdx]; - const dimStorage = storageArr[dimIdx]; - // PENDING NULL is empty or zero - const val = this._dimValueGetter(dataItem, dim, idx, dimIdx) as ParsedValueNumeric; - dimStorage[idx] = val; - - const dimRawExtent = rawExtentArr[dimIdx]; - val < dimRawExtent[0] && (dimRawExtent[0] = val); - val > dimRawExtent[1] && (dimRawExtent[1] = val); - } - - // If dataItem is {name: ...} or {id: ...}, it has highest priority. - // This kind of ids and names are always stored `_nameList` and `_idList`. - if (isFormatOriginal && !rawData.pure && dataItem) { - const itemName = (dataItem as any).name; - if (nameList[idx] == null && itemName != null) { - nameList[idx] = convertOptionIdName(itemName, null); - } - const itemId = (dataItem as any).id; - if (idList[idx] == null && itemId != null) { - idList[idx] = convertOptionIdName(itemId, null); - } - } - - if (!this._dontMakeIdFromName) { - makeIdFromName(this, idx); - } - } - } - - if (!rawData.persistent && rawData.clean) { - // Clean unused data if data source is typed array. - rawData.clean(); - } - - this._rawCount = this._count = end; - - // Reset data extent - this._extent = {}; - - prepareInvertedIndex(this); - } - - count(): number { - return this._count; - } - - getIndices(): ArrayLike { - let newIndices; - - const indices = this._indices; - if (indices) { - const Ctor = indices.constructor as DataArrayLikeConstructor; - const thisCount = this._count; - // `new Array(a, b, c)` is different from `new Uint32Array(a, b, c)`. - if (Ctor === Array) { - newIndices = new Ctor(thisCount); - for (let i = 0; i < thisCount; i++) { - newIndices[i] = indices[i]; - } - } - else { - newIndices = new (Ctor as DataTypedArrayConstructor)( - (indices as DataTypedArray).buffer, 0, thisCount - ); - } - } - else { - const Ctor = getIndicesCtor(this); - newIndices = new Ctor(this.count()); - for (let i = 0; i < newIndices.length; i++) { - newIndices[i] = i; - } - } - - return newIndices; - } - - // Get data by index of dimension. - // Because in v8 access array by number variable is faster than access object by string variable - // Not sure why but the optimization just works. - getByDimIdx(dimIdx: number, idx: number): ParsedValue { - if (!(idx >= 0 && idx < this._count)) { - return NaN; - } - const dimStore = this._storageArr[dimIdx]; - return dimStore ? dimStore[this.getRawIndex(idx)] : NaN; - } - - /** - * Get value. Return NaN if idx is out of range. - * @param dim Dim must be concrete name. - */ - get(dim: DimensionName, idx: number): ParsedValue { - if (!(idx >= 0 && idx < this._count)) { - return NaN; - } - const dimStore = this._storage[dim]; - return dimStore ? dimStore[this.getRawIndex(idx)] : NaN; - } - - /** - * @param dim concrete dim - */ - getByRawIndex(dim: DimensionName, rawIdx: number): ParsedValue { - if (!(rawIdx >= 0 && rawIdx < this._rawCount)) { - return NaN; - } - const dimStore = this._storage[dim]; - return dimStore ? dimStore[rawIdx] : NaN; - } - - /** - * Get value for multi dimensions. - * @param dimensions If ignored, using all dimensions. - */ - getValues(idx: number): ParsedValue[]; - getValues(dimensions: readonly DimensionName[], idx: number): ParsedValue[]; - getValues(dimensions: readonly DimensionName[] | number, idx?: number): ParsedValue[] { - const values = []; - - if (!zrUtil.isArray(dimensions)) { - // stack = idx; - idx = dimensions as number; - dimensions = this.dimensions; - } - - for (let i = 0, len = dimensions.length; i < len; i++) { - values.push(this.get(dimensions[i], idx /*, stack */)); - } - - return values; - } - - /** - * If value is NaN. Inlcuding '-' - * Only check the coord dimensions. - */ - hasValue(idx: number): boolean { - const dataDimsOnCoord = this._dimensionsSummary.dataDimsOnCoord; - for (let i = 0, len = dataDimsOnCoord.length; i < len; i++) { - // Ordinal type originally can be string or number. - // But when an ordinal type is used on coord, it can - // not be string but only number. So we can also use isNaN. - if (isNaN(this.get(dataDimsOnCoord[i], idx) as any)) { - return false; - } - } - return true; - } - - /** - * Get extent of data in one dimension - */ - getDataExtent(dim: DimensionLoose): [number, number] { - // Make sure use concrete dim as cache name. - dim = this.getDimension(dim); - const dimData = this._storage[dim]; - const initialExtent = getInitialExtent(); - - // stack = !!((stack || false) && this.getCalculationInfo(dim)); - - if (!dimData) { - return initialExtent; - } - - // Make more strict checkings to ensure hitting cache. - const currEnd = this.count(); - // let cacheName = [dim, !!stack].join('_'); - // let cacheName = dim; - - // Consider the most cases when using data zoom, `getDataExtent` - // happened before filtering. We cache raw extent, which is not - // necessary to be cleared and recalculated when restore data. - const useRaw = !this._indices; // && !stack; - let dimExtent: [number, number]; - - if (useRaw) { - return this._rawExtent[dim].slice() as [number, number]; - } - dimExtent = this._extent[dim]; - if (dimExtent) { - return dimExtent.slice() as [number, number]; - } - dimExtent = initialExtent; - - let min = dimExtent[0]; - let max = dimExtent[1]; - - for (let i = 0; i < currEnd; i++) { - const rawIdx = this.getRawIndex(i); - const value = dimData[rawIdx] as ParsedValueNumeric; - value < min && (min = value); - value > max && (max = value); - } - - dimExtent = [min, max]; - - this._extent[dim] = dimExtent; - - return dimExtent; - } - - /** - * PENDING: In fact currently this function is only used to short-circuit - * the calling of `scale.unionExtentFromData` when data have been filtered by modules - * like "dataZoom". `scale.unionExtentFromData` is used to calculate data extent for series on - * an axis, but if a "axis related data filter module" is used, the extent of the axis have - * been fixed and no need to calling `scale.unionExtentFromData` actually. - * But if we add "custom data filter" in future, which is not "axis related", this method may - * be still needed. - * - * Optimize for the scenario that data is filtered by a given extent. - * Consider that if data amount is more than hundreds of thousand, - * extent calculation will cost more than 10ms and the cache will - * be erased because of the filtering. - */ - getApproximateExtent(dim: DimensionLoose): [number, number] { - dim = this.getDimension(dim); - return this._approximateExtent[dim] || this.getDataExtent(dim); - } - - /** - * Calculate extent on a filtered data might be time consuming. - * Approximate extent is only used for: calculte extent of filtered data outside. - */ - setApproximateExtent(extent: [number, number], dim: DimensionLoose): void { - dim = this.getDimension(dim); - this._approximateExtent[dim] = extent.slice() as [number, number]; - } - - getCalculationInfo>( - key: CALC_INFO_KEY - ): DataCalculationInfo[CALC_INFO_KEY] { - return this._calculationInfo[key]; - } - - /** - * @param key or k-v object - */ - setCalculationInfo( - key: DataCalculationInfo - ): void; - setCalculationInfo>( - key: CALC_INFO_KEY, - value: DataCalculationInfo[CALC_INFO_KEY] - ): void; - setCalculationInfo( - key: (keyof DataCalculationInfo) | DataCalculationInfo, - value?: DataCalculationInfo[keyof DataCalculationInfo] - ): void { - isObject(key) - ? zrUtil.extend(this._calculationInfo, key as object) - : ((this._calculationInfo as any)[key] = value); - } - - /** - * Get sum of data in one dimension - */ - getSum(dim: DimensionName): number { - const dimData = this._storage[dim]; - let sum = 0; - if (dimData) { - for (let i = 0, len = this.count(); i < len; i++) { - const value = this.get(dim, i) as number; - if (!isNaN(value)) { - sum += value; - } - } - } - return sum; - } - - /** - * Get median of data in one dimension - */ - getMedian(dim: DimensionLoose): number { - const dimDataArray: ParsedValue[] = []; - // map all data of one dimension - this.each(dim, function (val) { - if (!isNaN(val as number)) { - dimDataArray.push(val); - } - }); - - // TODO - // Use quick select? - const sortedDimDataArray = dimDataArray.sort(function (a: number, b: number) { - return a - b; - }) as number[]; - const len = this.count(); - // calculate median - return len === 0 - ? 0 - : len % 2 === 1 - ? sortedDimDataArray[(len - 1) / 2] - : (sortedDimDataArray[len / 2] + sortedDimDataArray[len / 2 - 1]) / 2; - } - - // /** - // * Retreive the index with given value - // * @param {string} dim Concrete dimension. - // * @param {number} value - // * @return {number} - // */ - // Currently incorrect: should return dataIndex but not rawIndex. - // Do not fix it until this method is to be used somewhere. - // FIXME Precision of float value - // indexOf(dim, value) { - // let storage = this._storage; - // let dimData = storage[dim]; - // let chunkSize = this._chunkSize; - // if (dimData) { - // for (let i = 0, len = this.count(); i < len; i++) { - // let chunkIndex = mathFloor(i / chunkSize); - // let chunkOffset = i % chunkSize; - // if (dimData[chunkIndex][chunkOffset] === value) { - // return i; - // } - // } - // } - // return -1; - // } - - /** - * Only support the dimension which inverted index created. - * Do not support other cases until required. - * @param dim concrete dim - * @param value ordinal index - * @return rawIndex - */ - rawIndexOf(dim: DimensionName, value: OrdinalNumber): number { - const invertedIndices = dim && this._invertedIndicesMap[dim]; - if (__DEV__) { - if (!invertedIndices) { - throw new Error('Do not supported yet'); - } - } - const rawIndex = invertedIndices[value]; - if (rawIndex == null || isNaN(rawIndex)) { - return INDEX_NOT_FOUND; - } - return rawIndex; - } - - /** - * Retreive the index with given name - */ - indexOfName(name: string): number { - for (let i = 0, len = this.count(); i < len; i++) { - if (this.getName(i) === name) { - return i; - } - } - - return -1; - } - - /** - * Retreive the index with given raw data index - */ - indexOfRawIndex(rawIndex: number): number { - if (rawIndex >= this._rawCount || rawIndex < 0) { - return -1; - } - - if (!this._indices) { - return rawIndex; - } - - // Indices are ascending - const indices = this._indices; - - // If rawIndex === dataIndex - const rawDataIndex = indices[rawIndex]; - if (rawDataIndex != null && rawDataIndex < this._count && rawDataIndex === rawIndex) { - return rawIndex; - } - - let left = 0; - let right = this._count - 1; - while (left <= right) { - const mid = (left + right) / 2 | 0; - if (indices[mid] < rawIndex) { - left = mid + 1; - } - else if (indices[mid] > rawIndex) { - right = mid - 1; - } - else { - return mid; - } - } - return -1; - } - - /** - * Retreive the index of nearest value - * @param dim - * @param value - * @param [maxDistance=Infinity] - * @return If and only if multiple indices has - * the same value, they are put to the result. - */ - indicesOfNearest( - dim: DimensionName, value: number, maxDistance?: number - ): number[] { - const storage = this._storage; - const dimData = storage[dim]; - const nearestIndices: number[] = []; - - if (!dimData) { - return nearestIndices; - } - - if (maxDistance == null) { - maxDistance = Infinity; - } - - let minDist = Infinity; - let minDiff = -1; - let nearestIndicesLen = 0; - - - // Check the test case of `test/ut/spec/data/List.js`. - for (let i = 0, len = this.count(); i < len; i++) { - const dataIndex = this.getRawIndex(i); - const diff = value - (dimData[dataIndex] as number); - const dist = Math.abs(diff); - if (dist <= maxDistance) { - // When the `value` is at the middle of `this.get(dim, i)` and `this.get(dim, i+1)`, - // we'd better not push both of them to `nearestIndices`, otherwise it is easy to - // get more than one item in `nearestIndices` (more specifically, in `tooltip`). - // So we chose the one that `diff >= 0` in this csae. - // But if `this.get(dim, i)` and `this.get(dim, j)` get the same value, both of them - // should be push to `nearestIndices`. - if (dist < minDist - || (dist === minDist && diff >= 0 && minDiff < 0) - ) { - minDist = dist; - minDiff = diff; - nearestIndicesLen = 0; - } - if (diff === minDiff) { - nearestIndices[nearestIndicesLen++] = i; - } - } - } - nearestIndices.length = nearestIndicesLen; - - return nearestIndices; - } - - /** - * Get raw data index. - * Do not initialize. - * Default `getRawIndex`. And it can be changed. - */ - getRawIndex: (idx: number) => number = getRawIndexWithoutIndices; - - /** - * Get raw data item - */ - getRawDataItem(idx: number): OptionDataItem { - if (!this._rawData.persistent) { - const val = []; - for (let i = 0; i < this.dimensions.length; i++) { - const dim = this.dimensions[i]; - val.push(this.get(dim, idx)); - } - return val; - } - else { - return this._rawData.getItem(this.getRawIndex(idx)); - } - } - - /** - * @return Never be null/undefined. `number` will be converted to string. Becuase: - * In most cases, name is used in display, where returning a string is more convenient. - * In other cases, name is used in query (see `indexOfName`), where we can keep the - * rule that name `2` equals to name `'2'`. - */ - getName(idx: number): string { - const rawIndex = this.getRawIndex(idx); - let name = this._nameList[rawIndex]; - if (name == null && this._nameDimIdx != null) { - name = getIdNameFromStore(this, this._nameDimIdx, this._nameOrdinalMeta, rawIndex); - } - if (name == null) { - name = ''; - } - return name; - } - - /** - * @return Never null/undefined. `number` will be converted to string. Becuase: - * In all cases having encountered at present, id is used in making diff comparison, which - * are usually based on hash map. We can keep the rule that the internal id are always string - * (treat `2` is the same as `'2'`) to make the related logic simple. - */ - getId(idx: number): string { - return getId(this, this.getRawIndex(idx)); - } - - /** - * Data iteration - * @param ctx default this - * @example - * list.each('x', function (x, idx) {}); - * list.each(['x', 'y'], function (x, y, idx) {}); - * list.each(function (idx) {}) - */ - each(cb: EachCb0, ctx?: Ctx, ctxCompat?: Ctx): void; - each(dims: DimensionLoose, cb: EachCb1, ctx?: Ctx, ctxCompat?: Ctx): void; - each(dims: [DimensionLoose], cb: EachCb1, ctx?: Ctx, ctxCompat?: Ctx): void; - each(dims: [DimensionLoose, DimensionLoose], cb: EachCb2, ctx?: Ctx, ctxCompat?: Ctx): void; - each(dims: ItrParamDims, cb: EachCb, ctx?: Ctx, ctxCompat?: Ctx): void; - each( - dims: ItrParamDims | EachCb, - cb: EachCb | Ctx, - ctx?: Ctx, - ctxCompat?: Ctx - ): void { - 'use strict'; - - if (!this._count) { - return; - } - - if (typeof dims === 'function') { - ctxCompat = ctx; - ctx = cb as Ctx; - cb = dims; - dims = []; - } - - // ctxCompat just for compat echarts3 - const fCtx = (ctx || ctxCompat || this) as CtxOrList; - - const dimNames = map(normalizeDimensions(dims), this.getDimension, this); - - if (__DEV__) { - validateDimensions(this, dimNames); - } - - const dimSize = dimNames.length; - const dimIndices = map(dimNames, (dimName) => { - return this._dimensionInfos[dimName].index; - }); - const storageArr = this._storageArr; - - for (let i = 0, len = this.count(); i < len; i++) { - const rawIdx = this.getRawIndex(i); - // Simple optimization - switch (dimSize) { - case 0: - (cb as EachCb0).call(fCtx, i); - break; - case 1: - (cb as EachCb1).call(fCtx, storageArr[dimIndices[0]][rawIdx], i); - break; - case 2: - (cb as EachCb2).call( - fCtx, storageArr[dimIndices[0]][rawIdx], storageArr[dimIndices[1]][rawIdx], i - ); - break; - default: - let k = 0; - const value = []; - for (; k < dimSize; k++) { - value[k] = storageArr[dimIndices[k]][rawIdx]; - } - // Index - value[k] = i; - (cb as EachCb).apply(fCtx, value); - } - } - } - - /** - * Data filter - */ - filterSelf(cb: FilterCb0, ctx?: Ctx, ctxCompat?: Ctx): this; - filterSelf(dims: DimensionLoose, cb: FilterCb1, ctx?: Ctx, ctxCompat?: Ctx): this; - filterSelf(dims: [DimensionLoose], cb: FilterCb1, ctx?: Ctx, ctxCompat?: Ctx): this; - filterSelf(dims: [DimensionLoose, DimensionLoose], cb: FilterCb2, ctx?: Ctx, ctxCompat?: Ctx): this; - filterSelf(dims: ItrParamDims, cb: FilterCb, ctx?: Ctx, ctxCompat?: Ctx): this; - filterSelf( - dims: ItrParamDims | FilterCb, - cb: FilterCb | Ctx, - ctx?: Ctx, - ctxCompat?: Ctx - ): List { - 'use strict'; - - if (!this._count) { - return; - } - - if (typeof dims === 'function') { - ctxCompat = ctx; - ctx = cb as Ctx; - cb = dims; - dims = []; - } - - // ctxCompat just for compat echarts3 - const fCtx = (ctx || ctxCompat || this) as CtxOrList; - - const dimNames = map( - normalizeDimensions(dims), this.getDimension, this - ); - - if (__DEV__) { - validateDimensions(this, dimNames); - } - - - const count = this.count(); - const Ctor = getIndicesCtor(this); - const newIndices = new Ctor(count); - const value = []; - const dimSize = dimNames.length; - - let offset = 0; - const dimIndices = map(dimNames, (dimName) => { - return this._dimensionInfos[dimName].index; - }); - const dim0 = dimIndices[0]; - const storageArr = this._storageArr; - - for (let i = 0; i < count; i++) { - let keep; - const rawIdx = this.getRawIndex(i); - // Simple optimization - if (dimSize === 0) { - keep = (cb as FilterCb0).call(fCtx, i); - } - else if (dimSize === 1) { - const val = storageArr[dim0][rawIdx]; - keep = (cb as FilterCb1).call(fCtx, val, i); - } - else { - let k = 0; - for (; k < dimSize; k++) { - value[k] = storageArr[dimIndices[k]][rawIdx]; - } - value[k] = i; - keep = (cb as FilterCb).apply(fCtx, value); - } - if (keep) { - newIndices[offset++] = rawIdx; - } - } - - // Set indices after filtered. - if (offset < count) { - this._indices = newIndices; - } - this._count = offset; - // Reset data extent - this._extent = {}; - - this.getRawIndex = this._indices ? getRawIndexWithIndices : getRawIndexWithoutIndices; - - return this; - } - - /** - * Select data in range. (For optimization of filter) - * (Manually inline code, support 5 million data filtering in data zoom.) - */ - selectRange(range: {[dimName: string]: [number, number]}): List { - 'use strict'; - - const len = this._count; - - if (!len) { - return; - } - - const dimensions = []; - for (const dim in range) { - if (range.hasOwnProperty(dim)) { - dimensions.push(dim); - } - } - - if (__DEV__) { - validateDimensions(this, dimensions); - } - - const dimSize = dimensions.length; - if (!dimSize) { - return; - } - - const originalCount = this.count(); - const Ctor = getIndicesCtor(this); - const newIndices = new Ctor(originalCount); - - let offset = 0; - const dim0 = dimensions[0]; - const dimIndices = map(dimensions, (dimName) => { - return this._dimensionInfos[dimName].index; - }); - - const min = range[dim0][0]; - const max = range[dim0][1]; - const storageArr = this._storageArr; - - let quickFinished = false; - if (!this._indices) { - // Extreme optimization for common case. About 2x faster in chrome. - let idx = 0; - if (dimSize === 1) { - const dimStorage = storageArr[dimIndices[0]]; - for (let i = 0; i < len; i++) { - const val = dimStorage[i]; - // NaN will not be filtered. Consider the case, in line chart, empty - // value indicates the line should be broken. But for the case like - // scatter plot, a data item with empty value will not be rendered, - // but the axis extent may be effected if some other dim of the data - // item has value. Fortunately it is not a significant negative effect. - if ( - (val >= min && val <= max) || isNaN(val as any) - ) { - newIndices[offset++] = idx; - } - idx++; - } - quickFinished = true; - } - else if (dimSize === 2) { - const dimStorage = storageArr[dimIndices[0]]; - const dimStorage2 = storageArr[dimIndices[1]]; - const min2 = range[dimensions[1]][0]; - const max2 = range[dimensions[1]][1]; - for (let i = 0; i < len; i++) { - const val = dimStorage[i]; - const val2 = dimStorage2[i]; - // Do not filter NaN, see comment above. - if (( - (val >= min && val <= max) || isNaN(val as any) - ) - && ( - (val2 >= min2 && val2 <= max2) || isNaN(val2 as any) - ) - ) { - newIndices[offset++] = idx; - } - idx++; - } - quickFinished = true; - } - } - if (!quickFinished) { - if (dimSize === 1) { - for (let i = 0; i < originalCount; i++) { - const rawIndex = this.getRawIndex(i); - const val = storageArr[dimIndices[0]][rawIndex]; - // Do not filter NaN, see comment above. - if ( - (val >= min && val <= max) || isNaN(val as any) - ) { - newIndices[offset++] = rawIndex; - } - } - } - else { - for (let i = 0; i < originalCount; i++) { - let keep = true; - const rawIndex = this.getRawIndex(i); - for (let k = 0; k < dimSize; k++) { - const dimk = dimensions[k]; - const val = storageArr[dimIndices[k]][rawIndex]; - // Do not filter NaN, see comment above. - if (val < range[dimk][0] || val > range[dimk][1]) { - keep = false; - } - } - if (keep) { - newIndices[offset++] = this.getRawIndex(i); - } - } - } - } - - // Set indices after filtered. - if (offset < originalCount) { - this._indices = newIndices; - } - this._count = offset; - // Reset data extent - this._extent = {}; - - this.getRawIndex = this._indices ? getRawIndexWithIndices : getRawIndexWithoutIndices; - - return this; - } - - /** - * Data mapping to a plain array - */ - mapArray>(cb: Cb, ctx?: Ctx, ctxCompat?: Ctx): ReturnType[]; - /* eslint-disable */ - mapArray>(dims: DimensionLoose, cb: Cb, ctx?: Ctx, ctxCompat?: Ctx): ReturnType[]; - mapArray>(dims: [DimensionLoose], cb: Cb, ctx?: Ctx, ctxCompat?: Ctx): ReturnType[]; - mapArray>(dims: [DimensionLoose, DimensionLoose], cb: Cb, ctx?: Ctx, ctxCompat?: Ctx): ReturnType[]; - mapArray>(dims: ItrParamDims, cb: Cb, ctx?: Ctx, ctxCompat?: Ctx): ReturnType[]; - /* eslint-enable */ - mapArray( - dims: ItrParamDims | MapArrayCb, - cb: MapArrayCb | Ctx, - ctx?: Ctx, - ctxCompat?: Ctx - ): any[] { - 'use strict'; - - if (typeof dims === 'function') { - ctxCompat = ctx; - ctx = cb as Ctx; - cb = dims; - dims = []; - } - - // ctxCompat just for compat echarts3 - ctx = (ctx || ctxCompat || this) as Ctx; - - const result: any[] = []; - this.each(dims, function () { - result.push(cb && (cb as MapArrayCb).apply(this, arguments)); - }, ctx); - return result; - } - - /** - * Data mapping to a new List with given dimensions - */ - map(dims: DimensionLoose, cb: MapCb1, ctx?: Ctx, ctxCompat?: Ctx): List; - map(dims: [DimensionLoose], cb: MapCb1, ctx?: Ctx, ctxCompat?: Ctx): List; - map(dims: [DimensionLoose, DimensionLoose], cb: MapCb2, ctx?: Ctx, ctxCompat?: Ctx): List; - map( - dims: ItrParamDims, - cb: MapCb, - ctx?: Ctx, - ctxCompat?: Ctx - ): List { - 'use strict'; - - // ctxCompat just for compat echarts3 - const fCtx = (ctx || ctxCompat || this) as CtxOrList; - - const dimNames = map( - normalizeDimensions(dims), this.getDimension, this - ); - - if (__DEV__) { - validateDimensions(this, dimNames); - } - - const list = cloneListForMapAndSample(this, dimNames); - const storage = list._storage; - - // Following properties are all immutable. - // So we can reference to the same value - list._indices = this._indices; - list.getRawIndex = list._indices ? getRawIndexWithIndices : getRawIndexWithoutIndices; - - const tmpRetValue = []; - const dimSize = dimNames.length; - const dataCount = this.count(); - const values = []; - const rawExtent = list._rawExtent; - - for (let dataIndex = 0; dataIndex < dataCount; dataIndex++) { - for (let dimIndex = 0; dimIndex < dimSize; dimIndex++) { - values[dimIndex] = this.get(dimNames[dimIndex], dataIndex); - } - values[dimSize] = dataIndex; - - let retValue = cb && cb.apply(fCtx, values); - if (retValue != null) { - // a number or string (in oridinal dimension)? - if (typeof retValue !== 'object') { - tmpRetValue[0] = retValue; - retValue = tmpRetValue; - } - - const rawIndex = this.getRawIndex(dataIndex); - - for (let i = 0; i < retValue.length; i++) { - const dim = dimNames[i]; - const val = retValue[i]; - const rawExtentOnDim = rawExtent[dim]; - - const dimStore = storage[dim]; - if (dimStore) { - dimStore[rawIndex] = val; - } - - if (val < rawExtentOnDim[0]) { - rawExtentOnDim[0] = val as number; - } - if (val > rawExtentOnDim[1]) { - rawExtentOnDim[1] = val as number; - } - } - } - } - - return list; - } - - /** - * Large data down sampling on given dimension - * @param sampleIndex Sample index for name and id - */ - downSample( - dimension: DimensionName, - rate: number, - sampleValue: (frameValues: ArrayLike) => ParsedValueNumeric, - sampleIndex: (frameValues: ArrayLike, value: ParsedValueNumeric) => number - ): List { - const list = cloneListForMapAndSample(this, [dimension]); - const targetStorage = list._storage; - - const frameValues = []; - let frameSize = mathFloor(1 / rate); - - const dimStore = targetStorage[dimension]; - const len = this.count(); - const rawExtentOnDim = list._rawExtent[dimension]; - - const newIndices = new (getIndicesCtor(this))(len); - - let offset = 0; - for (let i = 0; i < len; i += frameSize) { - // Last frame - if (frameSize > len - i) { - frameSize = len - i; - frameValues.length = frameSize; - } - for (let k = 0; k < frameSize; k++) { - const dataIdx = this.getRawIndex(i + k); - frameValues[k] = dimStore[dataIdx]; - } - const value = sampleValue(frameValues); - const sampleFrameIdx = this.getRawIndex( - Math.min(i + sampleIndex(frameValues, value) || 0, len - 1) - ); - // Only write value on the filtered data - dimStore[sampleFrameIdx] = value; - - if (value < rawExtentOnDim[0]) { - rawExtentOnDim[0] = value; - } - if (value > rawExtentOnDim[1]) { - rawExtentOnDim[1] = value; - } - - newIndices[offset++] = sampleFrameIdx; - } - - list._count = offset; - list._indices = newIndices; - - list.getRawIndex = getRawIndexWithIndices; - - return list as List; - } - - /** - * Large data down sampling using largest-triangle-three-buckets - * @param {string} valueDimension - * @param {number} targetCount - */ - lttbDownSample( - valueDimension: DimensionName, - rate: number - ) { - const list = cloneListForMapAndSample(this, []); - const targetStorage = list._storage; - const dimStore = targetStorage[valueDimension]; - const len = this.count(); - const newIndices = new (getIndicesCtor(this))(len); - - let sampledIndex = 0; - - const frameSize = mathFloor(1 / rate); - - let currentRawIndex = this.getRawIndex(0); - let maxArea; - let area; - let nextRawIndex; - - // First frame use the first data. - newIndices[sampledIndex++] = currentRawIndex; - for (let i = 1; i < len - 1; i += frameSize) { - const nextFrameStart = Math.min(i + frameSize, len - 1); - const nextFrameEnd = Math.min(i + frameSize * 2, len); - - const avgX = (nextFrameEnd + nextFrameStart) / 2; - let avgY = 0; - - for (let idx = nextFrameStart; idx < nextFrameEnd; idx++) { - const rawIndex = this.getRawIndex(idx); - const y = dimStore[rawIndex] as number; - if (isNaN(y)) { - continue; - } - avgY += y as number; - } - avgY /= (nextFrameEnd - nextFrameStart); - - const frameStart = i; - const frameEnd = Math.min(i + frameSize, len); - - const pointAX = i - 1; - const pointAY = dimStore[currentRawIndex] as number; - - maxArea = -1; - - nextRawIndex = frameStart; - // Find a point from current frame that construct a triangel with largest area with previous selected point - // And the average of next frame. - for (let idx = frameStart; idx < frameEnd; idx++) { - const rawIndex = this.getRawIndex(idx); - const y = dimStore[rawIndex] as number; - if (isNaN(y)) { - continue; - } - // Calculate triangle area over three buckets - area = Math.abs((pointAX - avgX) * (y - pointAY) - - (pointAX - idx) * (avgY - pointAY) - ); - if (area > maxArea) { - maxArea = area; - nextRawIndex = rawIndex; // Next a is this b - } - } - - newIndices[sampledIndex++] = nextRawIndex; - - currentRawIndex = nextRawIndex; // This a is the next a (chosen b) - } - - // First frame use the last data. - newIndices[sampledIndex++] = this.getRawIndex(len - 1); - list._count = sampledIndex; - list._indices = newIndices; - - list.getRawIndex = getRawIndexWithIndices; - return list; - } - - - /** - * Get model of one data item. - */ - // TODO: Type of data item - getItemModel(idx: number): Model - > { - const hostModel = this.hostModel; - const dataItem = this.getRawDataItem(idx) as ModelOption; - return new Model(dataItem, hostModel, hostModel && hostModel.ecModel); - } - - /** - * Create a data differ - */ - diff(otherList: List): DataDiffer { - const thisList = this; - - return new DataDiffer( - otherList ? otherList.getIndices() : [], - this.getIndices(), - function (idx: number) { - return getId(otherList, idx); - }, - function (idx: number) { - return getId(thisList, idx); - } - ); - } - - /** - * Get visual property. - */ - getVisual(key: K): Visual[K] { - const visual = this._visual as Visual; - return visual && visual[key]; - } - - /** - * Set visual property - * - * @example - * setVisual('color', color); - * setVisual({ - * 'color': color - * }); - */ - setVisual(key: K, val: Visual[K]): void; - setVisual(kvObj: Partial): void; - setVisual(kvObj: string | Partial, val?: any): void { - this._visual = this._visual || {}; - if (isObject(kvObj)) { - zrUtil.extend(this._visual, kvObj); - } - else { - this._visual[kvObj as string] = val; - } - } - - /** - * Get visual property of single data item - */ - // eslint-disable-next-line - getItemVisual(idx: number, key: K): Visual[K] { - const itemVisual = this._itemVisuals[idx] as Visual; - const val = itemVisual && itemVisual[key]; - if (val == null) { - // Use global visual property - return this.getVisual(key); - } - return val; - } - - /** - * If exists visual property of single data item - */ - hasItemVisual() { - return this._itemVisuals.length > 0; - } - - /** - * Make sure itemVisual property is unique - */ - // TODO: use key to save visual to reduce memory. - ensureUniqueItemVisual(idx: number, key: K): Visual[K] { - const itemVisuals = this._itemVisuals; - let itemVisual = itemVisuals[idx] as Visual; - if (!itemVisual) { - itemVisual = itemVisuals[idx] = {} as Visual; - } - let val = itemVisual[key]; - if (val == null) { - val = this.getVisual(key); - - // TODO Performance? - if (zrUtil.isArray(val)) { - val = val.slice() as unknown as Visual[K]; - } - else if (isObject(val)) { - val = zrUtil.extend({}, val); - } - - itemVisual[key] = val; - } - return val; - } - /** - * Set visual property of single data item - * - * @param {number} idx - * @param {string|Object} key - * @param {*} [value] - * - * @example - * setItemVisual(0, 'color', color); - * setItemVisual(0, { - * 'color': color - * }); - */ - // eslint-disable-next-line - setItemVisual(idx: number, key: K, value: Visual[K]): void; - setItemVisual(idx: number, kvObject: Partial): void; - // eslint-disable-next-line - setItemVisual(idx: number, key: K | Partial, value?: Visual[K]): void { - const itemVisual = this._itemVisuals[idx] || {}; - this._itemVisuals[idx] = itemVisual; - - if (isObject(key)) { - zrUtil.extend(itemVisual, key); - } - else { - itemVisual[key as string] = value; - } - } - - /** - * Clear itemVisuals and list visual. - */ - clearAllVisual(): void { - this._visual = {}; - this._itemVisuals = []; - } - - /** - * Set layout property. - */ - setLayout(key: string, val: any): void; - setLayout(kvObj: Dictionary): void; - setLayout(key: string | Dictionary, val?: any): void { - if (isObject(key)) { - for (const name in key) { - if (key.hasOwnProperty(name)) { - this.setLayout(name, key[name]); - } - } - return; - } - this._layout[key] = val; - } - - /** - * Get layout property. - */ - getLayout(key: string): any { - return this._layout[key]; - } - - /** - * Get layout of single data item - */ - getItemLayout(idx: number): any { - return this._itemLayouts[idx]; - } - - /** - * Set layout of single data item - */ - setItemLayout( - idx: number, - layout: (M extends true ? Dictionary : any), - merge?: M - ): void { - this._itemLayouts[idx] = merge - ? zrUtil.extend(this._itemLayouts[idx] || {}, layout) - : layout; - } - - /** - * Clear all layout of single data item - */ - clearItemLayouts(): void { - this._itemLayouts.length = 0; - } - - /** - * Set graphic element relative to data. It can be set as null - */ - setItemGraphicEl(idx: number, el: Element): void { - const hostModel = this.hostModel; - - if (el) { - const ecData = getECData(el); - // Add data index and series index for indexing the data by element - // Useful in tooltip - ecData.dataIndex = idx; - ecData.dataType = this.dataType; - ecData.seriesIndex = hostModel && (hostModel as any).seriesIndex; - - // TODO: not store dataIndex on children. - if (el.type === 'group') { - el.traverse(setItemDataAndSeriesIndex, el); - } - } - - this._graphicEls[idx] = el; - } - - getItemGraphicEl(idx: number): Element { - return this._graphicEls[idx]; - } - - eachItemGraphicEl( - cb: (this: Ctx, el: Element, idx: number) => void, - context?: Ctx - ): void { - zrUtil.each(this._graphicEls, function (el, idx) { - if (el) { - cb && cb.call(context, el, idx); - } - }); - } - - /** - * Shallow clone a new list except visual and layout properties, and graph elements. - * New list only change the indices. - */ - cloneShallow(list?: List): List { - if (!list) { - const dimensionInfoList = map(this.dimensions, this.getDimensionInfo, this); - list = new List(dimensionInfoList, this.hostModel); - } - - // FIXME - list._storage = this._storage; - list._storageArr = this._storageArr; - - transferProperties(list, this); - - // Clone will not change the data extent and indices - if (this._indices) { - const Ctor = this._indices.constructor as DataArrayLikeConstructor; - if (Ctor === Array) { - const thisCount = this._indices.length; - list._indices = new Ctor(thisCount); - for (let i = 0; i < thisCount; i++) { - list._indices[i] = this._indices[i]; - } - } - else { - list._indices = new (Ctor as DataTypedArrayConstructor)(this._indices); - } - } - else { - list._indices = null; - } - list.getRawIndex = list._indices ? getRawIndexWithIndices : getRawIndexWithoutIndices; - - return list; - } - - /** - * Wrap some method to add more feature - */ - wrapMethod( - methodName: FunctionPropertyNames, - injectFunction: (...args: any) => any - ): void { - const originalMethod = this[methodName]; - if (typeof originalMethod !== 'function') { - return; - } - this.__wrappedMethods = this.__wrappedMethods || []; - this.__wrappedMethods.push(methodName); - this[methodName] = function () { - const res = (originalMethod as any).apply(this, arguments); - return injectFunction.apply(this, [res].concat(zrUtil.slice(arguments))); - }; - } - - - // ---------------------------------------------------------- - // A work around for internal method visiting private member. - // ---------------------------------------------------------- - private static internalField = (function () { - - defaultDimValueGetters = { - - arrayRows: getDimValueSimply, - - objectRows: function ( - this: List, dataItem: Dictionary, dimName: string, dataIndex: number, dimIndex: number - ): ParsedValue { - return parseDataValue(dataItem[dimName], this._dimensionInfos[dimName]); - }, - - keyedColumns: getDimValueSimply, - - original: function ( - this: List, dataItem: any, dimName: string, dataIndex: number, dimIndex: number - ): ParsedValue { - // Performance sensitive, do not use modelUtil.getDataItemValue. - // If dataItem is an plain object with no value field, the let `value` - // will be assigned with the object, but it will be tread correctly - // in the `convertValue`. - const value = dataItem && (dataItem.value == null ? dataItem : dataItem.value); - - // If any dataItem is like { value: 10 } - if (!this._rawData.pure && isDataItemOption(dataItem)) { - this.hasItemOption = true; - } - return parseDataValue( - (value instanceof Array) - ? value[dimIndex] - // If value is a single number or something else not array. - : value, - this._dimensionInfos[dimName] - ); - }, - - typedArray: function ( - this: List, dataItem: any, dimName: string, dataIndex: number, dimIndex: number - ): ParsedValue { - return dataItem[dimIndex]; - } - - }; - - function getDimValueSimply( - this: List, dataItem: any, dimName: string, dataIndex: number, dimIndex: number - ): ParsedValue { - return parseDataValue(dataItem[dimIndex], this._dimensionInfos[dimName]); - } - - prepareInvertedIndex = function (list: List): void { - const invertedIndicesMap = list._invertedIndicesMap; - zrUtil.each(invertedIndicesMap, function (invertedIndices, dim) { - const dimInfo = list._dimensionInfos[dim]; - - // Currently, only dimensions that has ordinalMeta can create inverted indices. - const ordinalMeta = dimInfo.ordinalMeta; - if (ordinalMeta) { - invertedIndices = invertedIndicesMap[dim] = new CtorInt32Array( - ordinalMeta.categories.length - ); - // The default value of TypedArray is 0. To avoid miss - // mapping to 0, we should set it as INDEX_NOT_FOUND. - for (let i = 0; i < invertedIndices.length; i++) { - invertedIndices[i] = INDEX_NOT_FOUND; - } - for (let i = 0; i < list._count; i++) { - // Only support the case that all values are distinct. - invertedIndices[list.get(dim, i) as number] = i; - } - } - }); - }; - - getIdNameFromStore = function ( - list: List, dimIdx: number, ordinalMeta: OrdinalMeta, rawIndex: number - ): string { - let val; - const chunk = list._storageArr[dimIdx]; - if (chunk) { - val = chunk[rawIndex]; - if (ordinalMeta && ordinalMeta.categories.length) { - val = ordinalMeta.categories[val as OrdinalNumber]; - } - } - return convertOptionIdName(val, null); - }; - - getIndicesCtor = function (list: List): DataArrayLikeConstructor { - // The possible max value in this._indicies is always this._rawCount despite of filtering. - return list._rawCount > 65535 ? CtorUint32Array : CtorUint16Array; - }; - - prepareStorage = function ( - storage: DataStorage, - dimInfo: DataDimensionInfo, - end: number, - append?: boolean - ): void { - const DataCtor = dataCtors[dimInfo.type]; - const dim = dimInfo.name; - - if (append) { - const oldStore = storage[dim]; - const oldLen = oldStore && oldStore.length; - if (!(oldLen === end)) { - const newStore = new DataCtor(end); - // The cost of the copy is probably inconsiderable - // within the initial chunkSize. - for (let j = 0; j < oldLen; j++) { - newStore[j] = oldStore[j]; - } - storage[dim] = newStore; - } - } - else { - storage[dim] = new DataCtor(end); - } - }; - - getRawIndexWithoutIndices = function (this: List, idx: number): number { - return idx; - }; - - getRawIndexWithIndices = function (this: List, idx: number): number { - if (idx < this._count && idx >= 0) { - return this._indices[idx]; - } - return -1; - }; - - /** - * @see the comment of `List['getId']`. - */ - getId = function (list: List, rawIndex: number): string { - let id = list._idList[rawIndex]; - if (id == null && list._idDimIdx != null) { - id = getIdNameFromStore(list, list._idDimIdx, list._idOrdinalMeta, rawIndex); - } - if (id == null) { - id = ID_PREFIX + rawIndex; - } - return id; - }; - - normalizeDimensions = function ( - dimensions: ItrParamDims - ): Array { - if (!zrUtil.isArray(dimensions)) { - dimensions = dimensions != null ? [dimensions] : []; - } - return dimensions; - }; - - validateDimensions = function (list: List, dims: DimensionName[]): void { - for (let i = 0; i < dims.length; i++) { - // stroage may be empty when no data, so use - // dimensionInfos to check. - if (!list._dimensionInfos[dims[i]]) { - console.error('Unkown dimension ' + dims[i]); - } - } - }; - - // Data in excludeDimensions is copied, otherwise transfered. - cloneListForMapAndSample = function ( - original: List, excludeDimensions: DimensionName[] - ): List { - const allDimensions = original.dimensions; - const list = new List( - map(allDimensions, original.getDimensionInfo, original), - original.hostModel - ); - // FIXME If needs stackedOn, value may already been stacked - transferProperties(list, original); - - const storage = list._storage = {} as DataStorage; - const originalStorage = original._storage; - const storageArr: DataValueChunk[] = list._storageArr = []; - - // Init storage - for (let i = 0; i < allDimensions.length; i++) { - const dim = allDimensions[i]; - if (originalStorage[dim]) { - // Notice that we do not reset invertedIndicesMap here, becuase - // there is no scenario of mapping or sampling ordinal dimension. - if (zrUtil.indexOf(excludeDimensions, dim) >= 0) { - storage[dim] = cloneChunk(originalStorage[dim]); - list._rawExtent[dim] = getInitialExtent(); - list._extent[dim] = null; - } - else { - // Direct reference for other dimensions - storage[dim] = originalStorage[dim]; - } - storageArr.push(storage[dim]); - } - } - return list; - }; - - function cloneChunk(originalChunk: DataValueChunk): DataValueChunk { - const Ctor = originalChunk.constructor; - // Only shallow clone is enough when Array. - return Ctor === Array - ? (originalChunk as Array).slice() - : new (Ctor as DataTypedArrayConstructor)(originalChunk as DataTypedArray); - } - - getInitialExtent = function (): [number, number] { - return [Infinity, -Infinity]; - }; - - setItemDataAndSeriesIndex = function (this: Element, child: Element): void { - const childECData = getECData(child); - const thisECData = getECData(this); - childECData.seriesIndex = thisECData.seriesIndex; - childECData.dataIndex = thisECData.dataIndex; - childECData.dataType = thisECData.dataType; - }; - - transferProperties = function (target: List, source: List): void { - zrUtil.each( - TRANSFERABLE_PROPERTIES.concat(source.__wrappedMethods || []), - function (propName) { - if (source.hasOwnProperty(propName)) { - (target as any)[propName] = (source as any)[propName]; - } - } - ); - - target.__wrappedMethods = source.__wrappedMethods; - - zrUtil.each(CLONE_PROPERTIES, function (propName) { - (target as any)[propName] = zrUtil.clone((source as any)[propName]); - }); - - target._calculationInfo = zrUtil.extend({}, source._calculationInfo); - }; - - makeIdFromName = function (list: List, idx: number): void { - const nameList = list._nameList; - const idList = list._idList; - const nameDimIdx = list._nameDimIdx; - const idDimIdx = list._idDimIdx; - - let name = nameList[idx]; - let id = idList[idx]; - - if (name == null && nameDimIdx != null) { - nameList[idx] = name = getIdNameFromStore(list, nameDimIdx, list._nameOrdinalMeta, idx); - } - if (id == null && idDimIdx != null) { - idList[idx] = id = getIdNameFromStore(list, idDimIdx, list._idOrdinalMeta, idx); - } - if (id == null && name != null) { - const nameRepeatCount = list._nameRepeatCount; - const nmCnt = nameRepeatCount[name] = (nameRepeatCount[name] || 0) + 1; - id = name; - if (nmCnt > 1) { - id += '__ec__' + nmCnt; - } - idList[idx] = id; - } - }; - - })(); - -} - -interface List { - getLinkedData(dataType?: SeriesDataType): List; - getLinkedDataAll(): { data: List, type?: SeriesDataType }[]; -} - -export default List; diff --git a/src/data/OrdinalMeta.ts b/src/data/OrdinalMeta.ts index f2fd790c80..d56273678e 100644 --- a/src/data/OrdinalMeta.ts +++ b/src/data/OrdinalMeta.ts @@ -21,6 +21,7 @@ import {createHashMap, isObject, map, HashMap} from 'zrender/src/core/util'; import Model from '../model/Model'; import { OrdinalNumber, OrdinalRawValue } from '../util/types'; +let uidBase = 0; class OrdinalMeta { @@ -32,6 +33,8 @@ class OrdinalMeta { private _map: HashMap; + readonly uid: number; + constructor(opt: { categories?: OrdinalRawValue[], @@ -41,6 +44,7 @@ class OrdinalMeta { this.categories = opt.categories || []; this._needCollect = opt.needCollect; this._deduplication = opt.deduplication; + this.uid = ++uidBase; } static createByAxisModel(axisModel: Model): OrdinalMeta { diff --git a/src/data/SeriesData.ts b/src/data/SeriesData.ts new file mode 100644 index 0000000000..99b3c04dc2 --- /dev/null +++ b/src/data/SeriesData.ts @@ -0,0 +1,1508 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +/* global Int32Array */ + + +import * as zrUtil from 'zrender/src/core/util'; +import {PathStyleProps} from 'zrender/src/graphic/Path'; +import Model from '../model/Model'; +import DataDiffer from './DataDiffer'; +import {DataProvider, DefaultDataProvider} from './helper/dataProvider'; +import {summarizeDimensions, DimensionSummary} from './helper/dimensionHelper'; +import SeriesDimensionDefine from './SeriesDimensionDefine'; +import {ArrayLike, Dictionary, FunctionPropertyNames} from 'zrender/src/core/types'; +import Element from 'zrender/src/Element'; +import { + DimensionIndex, DimensionName, DimensionLoose, OptionDataItem, + ParsedValue, ParsedValueNumeric, + ModelOption, SeriesDataType, OptionSourceData, SOURCE_FORMAT_TYPED_ARRAY, SOURCE_FORMAT_ORIGINAL, + DecalObject, + OrdinalNumber, + OrdinalRawValue +} from '../util/types'; +import {convertOptionIdName, isDataItemOption} from '../util/model'; +import { setCommonECData } from '../util/innerStore'; +import type Graph from './Graph'; +import type Tree from './Tree'; +import type { VisualMeta } from '../component/visualMap/VisualMapModel'; +import {isSourceInstance, Source} from './Source'; +import { LineStyleProps } from '../model/mixin/lineStyle'; +import DataStore, { DataStoreDimensionDefine, DimValueGetter } from './DataStore'; +import { isSeriesDataSchema, SeriesDataSchema } from './helper/SeriesDataSchema'; + +const isObject = zrUtil.isObject; +const map = zrUtil.map; + +const CtorInt32Array = typeof Int32Array === 'undefined' ? Array : Int32Array; + +// Use prefix to avoid index to be the same as otherIdList[idx], +// which will cause weird udpate animation. +const ID_PREFIX = 'e\0\0'; + +const INDEX_NOT_FOUND = -1; + +type NameRepeatCount = {[name: string]: number}; +type ItrParamDims = DimensionLoose | Array; +// If Ctx not specified, use List as Ctx +type CtxOrList = unknown extends Ctx ? SeriesData : Ctx; +type EachCb0 = (this: CtxOrList, idx: number) => void; +type EachCb1 = (this: CtxOrList, x: ParsedValue, idx: number) => void; +type EachCb2 = (this: CtxOrList, x: ParsedValue, y: ParsedValue, idx: number) => void; +type EachCb = (this: CtxOrList, ...args: any) => void; +type FilterCb0 = (this: CtxOrList, idx: number) => boolean; +type FilterCb1 = (this: CtxOrList, x: ParsedValue, idx: number) => boolean; +type FilterCb2 = (this: CtxOrList, x: ParsedValue, y: ParsedValue, idx: number) => boolean; +type FilterCb = (this: CtxOrList, ...args: any) => boolean; +type MapArrayCb0 = (this: CtxOrList, idx: number) => any; +type MapArrayCb1 = (this: CtxOrList, x: ParsedValue, idx: number) => any; +type MapArrayCb2 = (this: CtxOrList, x: ParsedValue, y: ParsedValue, idx: number) => any; +type MapArrayCb = (this: CtxOrList, ...args: any) => any; +type MapCb1 = (this: CtxOrList, x: ParsedValue, idx: number) => ParsedValue | ParsedValue[]; +type MapCb2 = (this: CtxOrList, x: ParsedValue, y: ParsedValue, idx: number) => + ParsedValue | ParsedValue[]; +type MapCb = (this: CtxOrList, ...args: any) => ParsedValue | ParsedValue[]; + +type SeriesDimensionDefineLoose = string | object | SeriesDimensionDefine; + +// `SeriesDimensionLoose` and `SeriesDimensionName` is the dimension that is used by coordinate +// system or declared in `series.encode`, which will be saved in `SeriesData`. Other dimension +// might not be saved in `SeriesData` for performance consideration. See `createDimension` for +// more details. +type SeriesDimensionLoose = DimensionLoose; +type SeriesDimensionName = DimensionName; +// type SeriesDimensionIndex = DimensionIndex; + + +const TRANSFERABLE_PROPERTIES = [ + 'hasItemOption', '_nameList', '_idList', '_invertedIndicesMap', + '_dimSummary', 'userOutput', + '_rawData', '_dimValueGetter', + '_nameDimIdx', '_idDimIdx', '_nameRepeatCount' +]; + +const CLONE_PROPERTIES = [ + '_approximateExtent' +]; + +export interface DefaultDataVisual { + style: PathStyleProps + // Draw type determined which prop should be set with encoded color. + // It's only available on the global visual. Use getVisual('drawType') to access it. + // It will be set in visual/style.ts module in the first priority. + drawType: 'fill' | 'stroke' + + symbol?: string + symbolSize?: number | number[] + symbolRotate?: number + symbolKeepAspect?: boolean + symbolOffset?: string | number | (string | number)[] + + liftZ?: number + // For legend. + legendIcon?: string + legendLineStyle?: LineStyleProps + + // visualMap will inject visualMeta data + visualMeta?: VisualMeta[] + + // If color is encoded from palette + colorFromPalette?: boolean + + decal?: DecalObject +} + +export interface DataCalculationInfo { + stackedDimension: DimensionName; + stackedByDimension: DimensionName; + isStackedByIndex: boolean; + stackedOverDimension: DimensionName; + stackResultDimension: DimensionName; + stackedOnSeries?: SERIES_MODEL; +} + +// ----------------------------- +// Internal method declarations: +// ----------------------------- +let prepareInvertedIndex: (data: SeriesData) => void; +let getId: (data: SeriesData, rawIndex: number) => string; +let getIdNameFromStore: (data: SeriesData, dimIdx: number, dataIdx: number) => string; +let normalizeDimensions: (dimensions: ItrParamDims) => Array; +let transferProperties: (target: SeriesData, source: SeriesData) => void; +let cloneListForMapAndSample: (original: SeriesData) => SeriesData; +let makeIdFromName: (data: SeriesData, idx: number) => void; + +class SeriesData< + HostModel extends Model = Model, + Visual extends DefaultDataVisual = DefaultDataVisual +> { + + readonly type = 'list'; + + /** + * Name of dimensions list of SeriesData. + * + * @caution Carefully use the index of this array. + * Becuase when DataStore is an extra high dimension(>30) dataset. We will only pick + * the used dimensions from DataStore to avoid performance issue. + */ + readonly dimensions: SeriesDimensionName[]; + + // Infomation of each data dimension, like data type. + private _dimInfos: Record; + + private _dimOmitted = false; + private _schema?: SeriesDataSchema; + /** + * @pending + * Actually we do not really need to convert dimensionIndex to dimensionName + * and do not need `_dimIdxToName` if we do everything internally based on dimension + * index rather than dimension name. + */ + private _dimIdxToName?: zrUtil.HashMap; + + readonly hostModel: HostModel; + + /** + * @readonly + */ + dataType: SeriesDataType; + + /** + * @readonly + * Host graph if List is used to store graph nodes / edges. + */ + graph?: Graph; + + /** + * @readonly + * Host tree if List is used to store tree ndoes. + */ + tree?: Tree; + + private _store: DataStore; + + private _nameList: string[] = []; + private _idList: string[] = []; + + // Models of data option is stored sparse for optimizing memory cost + // Never used yet (not used yet). + // private _optionModels: Model[] = []; + + // Global visual properties after visual coding + private _visual: Dictionary = {}; + + // Globel layout properties. + private _layout: Dictionary = {}; + + // Item visual properties after visual coding + private _itemVisuals: Dictionary[] = []; + + // Item layout properties after layout + private _itemLayouts: any[] = []; + + // Graphic elemnents + private _graphicEls: Element[] = []; + + // key: dim, value: extent + private _approximateExtent: Record = {}; + + private _dimSummary: DimensionSummary; + + private _invertedIndicesMap: Record>; + + private _calculationInfo: DataCalculationInfo = {} as DataCalculationInfo; + + // User output info of this data. + // DO NOT use it in other places! + // When preparing user params for user callbacks, we have + // to clone these inner data structures to prevent users + // from modifying them to effect built-in logic. And for + // performance consideration we make this `userOutput` to + // avoid clone them too many times. + userOutput: DimensionSummary['userOutput']; + + // Having detected that there is data item is non primitive type + // (in type `OptionDataItemObject`). + // Like `data: [ { value: xx, itemStyle: {...} }, ...]` + // At present it only happen in `SOURCE_FORMAT_ORIGINAL`. + hasItemOption: boolean = false; + + // id or name is used on dynamic data, mapping old and new items. + // When generating id from name, avoid repeat. + private _nameRepeatCount: NameRepeatCount; + private _nameDimIdx: number; + private _idDimIdx: number; + + private __wrappedMethods: string[]; + + // Methods that create a new list based on this list should be listed here. + // Notice that those method should `RETURN` the new list. + TRANSFERABLE_METHODS = ['cloneShallow', 'downSample', 'lttbDownSample', 'map'] as const; + // Methods that change indices of this list should be listed here. + CHANGABLE_METHODS = ['filterSelf', 'selectRange'] as const; + DOWNSAMPLE_METHODS = ['downSample', 'lttbDownSample'] as const; + + /** + * @param dimensionsInput.dimensions + * For example, ['someDimName', {name: 'someDimName', type: 'someDimType'}, ...]. + * Dimensions should be concrete names like x, y, z, lng, lat, angle, radius + */ + constructor( + dimensionsInput: SeriesDataSchema | SeriesDimensionDefineLoose[], + hostModel: HostModel + ) { + let dimensions: SeriesDimensionDefineLoose[]; + let assignStoreDimIdx = false; + if (isSeriesDataSchema(dimensionsInput)) { + dimensions = dimensionsInput.dimensions; + this._dimOmitted = dimensionsInput.isDimensionOmitted(); + this._schema = dimensionsInput; + } + else { + assignStoreDimIdx = true; + dimensions = dimensionsInput as SeriesDimensionDefineLoose[]; + } + + dimensions = dimensions || ['x', 'y']; + + const dimensionInfos: Dictionary = {}; + const dimensionNames = []; + const invertedIndicesMap: Dictionary = {}; + let needsHasOwn = false; + const emptyObj = {}; + + for (let i = 0; i < dimensions.length; i++) { + // Use the original dimensions[i], where other flag props may exists. + const dimInfoInput = dimensions[i]; + + const dimensionInfo: SeriesDimensionDefine = + zrUtil.isString(dimInfoInput) + ? new SeriesDimensionDefine({name: dimInfoInput}) + : !(dimInfoInput instanceof SeriesDimensionDefine) + ? new SeriesDimensionDefine(dimInfoInput) + : dimInfoInput; + + const dimensionName = dimensionInfo.name; + dimensionInfo.type = dimensionInfo.type || 'float'; + if (!dimensionInfo.coordDim) { + dimensionInfo.coordDim = dimensionName; + dimensionInfo.coordDimIndex = 0; + } + + const otherDims = dimensionInfo.otherDims = dimensionInfo.otherDims || {}; + dimensionNames.push(dimensionName); + dimensionInfos[dimensionName] = dimensionInfo; + if ((emptyObj as any)[dimensionName] != null) { + needsHasOwn = true; + } + + if (dimensionInfo.createInvertedIndices) { + invertedIndicesMap[dimensionName] = []; + } + if (otherDims.itemName === 0) { + this._nameDimIdx = i; + } + if (otherDims.itemId === 0) { + this._idDimIdx = i; + } + + if (__DEV__) { + zrUtil.assert(assignStoreDimIdx || dimensionInfo.storeDimIndex >= 0); + } + if (assignStoreDimIdx) { + dimensionInfo.storeDimIndex = i; + } + } + + this.dimensions = dimensionNames; + this._dimInfos = dimensionInfos; + this._initGetDimensionInfo(needsHasOwn); + + this.hostModel = hostModel; + + this._invertedIndicesMap = invertedIndicesMap; + + if (this._dimOmitted) { + const dimIdxToName = this._dimIdxToName = zrUtil.createHashMap(); + zrUtil.each(dimensionNames, dimName => { + dimIdxToName.set(dimensionInfos[dimName].storeDimIndex, dimName); + }); + } + } + + /** + * + * Get concrete dimension name by dimension name or dimension index. + * If input a dimension name, do not validate whether the dimension name exits. + * + * @caution + * @param dim Must make sure the dimension is `SeriesDimensionLoose`. + * Because only those dimensions will have auto-generated dimension names if not + * have a user-specified name, and other dimensions will get a return of null/undefined. + * + * @notice Becuause of this reason, should better use `getDimensionIndex` instead, for examples: + * ```js + * const val = data.getStore().get(data.getDimensionIndex(dim), dataIdx); + * ``` + * + * @return Concrete dim name. + */ + getDimension(dim: SeriesDimensionLoose): DimensionName { + let dimIdx = this._recognizeDimIndex(dim); + if (dimIdx == null) { + return dim as DimensionName; + } + dimIdx = dim as DimensionIndex; + + if (!this._dimOmitted) { + return this.dimensions[dimIdx]; + } + + // Retrieve from series dimension definition becuase it probably contains + // generated dimension name (like 'x', 'y'). + const dimName = this._dimIdxToName.get(dimIdx); + if (dimName != null) { + return dimName; + } + + const sourceDimDef = this._schema.getSourceDimension(dimIdx); + if (sourceDimDef) { + return sourceDimDef.name; + } + } + + /** + * Get dimension index in data store. Return -1 if not found. + * Can be used to index value from getRawValue. + */ + getDimensionIndex(dim: DimensionLoose): DimensionIndex { + const dimIdx = this._recognizeDimIndex(dim); + if (dimIdx != null) { + return dimIdx; + } + + if (dim == null) { + return -1; + } + + const dimInfo = this._getDimInfo(dim as DimensionName); + return dimInfo + ? dimInfo.storeDimIndex + : this._dimOmitted + ? this._schema.getSourceDimensionIndex(dim as DimensionName) + : -1; + } + + /** + * The meanings of the input parameter `dim`: + * + * + If dim is a number (e.g., `1`), it means the index of the dimension. + * For example, `getDimension(0)` will return 'x' or 'lng' or 'radius'. + * + If dim is a number-like string (e.g., `"1"`): + * + If there is the same concrete dim name defined in `series.dimensions` or `dataset.dimensions`, + * it means that concrete name. + * + If not, it will be converted to a number, which means the index of the dimension. + * (why? because of the backward compatbility. We have been tolerating number-like string in + * dimension setting, although now it seems that it is not a good idea.) + * For example, `visualMap[i].dimension: "1"` is the same meaning as `visualMap[i].dimension: 1`, + * if no dimension name is defined as `"1"`. + * + If dim is a not-number-like string, it means the concrete dim name. + * For example, it can be be default name `"x"`, `"y"`, `"z"`, `"lng"`, `"lat"`, `"angle"`, `"radius"`, + * or customized in `dimensions` property of option like `"age"`. + * + * @return recogonized `DimensionIndex`. Otherwise return null/undefined (means that dim is `DimensionName`). + */ + private _recognizeDimIndex(dim: DimensionLoose): DimensionIndex { + if (typeof dim === 'number' + // If being a number-like string but not being defined as a dimension name. + || ( + dim != null + && !isNaN(dim as any) + && !this._getDimInfo(dim) + && (!this._dimOmitted || this._schema.getSourceDimensionIndex(dim) < 0) + ) + ) { + return +dim; + } + } + + private _getStoreDimIndex(dim: DimensionLoose): DimensionIndex { + const dimIdx = this.getDimensionIndex(dim); + if (__DEV__) { + if (dimIdx == null) { + throw new Error('Unkown dimension ' + dim); + } + } + return dimIdx; + } + + /** + * Get type and calculation info of particular dimension + * @param dim + * Dimension can be concrete names like x, y, z, lng, lat, angle, radius + * Or a ordinal number. For example getDimensionInfo(0) will return 'x' or 'lng' or 'radius' + */ + getDimensionInfo(dim: SeriesDimensionLoose): SeriesDimensionDefine { + // Do not clone, because there may be categories in dimInfo. + return this._getDimInfo(this.getDimension(dim)); + } + + /** + * If `dimName` if from outside of `SeriesData`, + * use this method other than visit `this._dimInfos` directly. + */ + private _getDimInfo: (dimName: SeriesDimensionName) => SeriesDimensionDefine; + + private _initGetDimensionInfo(needsHasOwn: boolean): void { + const dimensionInfos = this._dimInfos; + this._getDimInfo = needsHasOwn + ? dimName => (dimensionInfos.hasOwnProperty(dimName) ? dimensionInfos[dimName] : undefined) + : dimName => dimensionInfos[dimName]; + } + + /** + * concrete dimension name list on coord. + */ + getDimensionsOnCoord(): SeriesDimensionName[] { + return this._dimSummary.dataDimsOnCoord.slice(); + } + + /** + * @param coordDim + * @param idx A coordDim may map to more than one data dim. + * If not specified, return the first dim not extra. + * @return concrete data dim. If not found, return null/undefined + */ + mapDimension(coordDim: SeriesDimensionName): SeriesDimensionName; + mapDimension(coordDim: SeriesDimensionName, idx: number): SeriesDimensionName; + mapDimension(coordDim: SeriesDimensionName, idx?: number): SeriesDimensionName { + const dimensionsSummary = this._dimSummary; + + if (idx == null) { + return dimensionsSummary.encodeFirstDimNotExtra[coordDim] as any; + } + + const dims = dimensionsSummary.encode[coordDim]; + return dims ? dims[idx as number] as any : null; + } + + mapDimensionsAll(coordDim: SeriesDimensionName): SeriesDimensionName[] { + const dimensionsSummary = this._dimSummary; + const dims = dimensionsSummary.encode[coordDim]; + return (dims || []).slice(); + } + + getStore() { + return this._store; + } + + /** + * Initialize from data + * @param data source or data or data store. + * @param nameList The name of a datum is used on data diff and + * default label/tooltip. + * A name can be specified in encode.itemName, + * or dataItem.name (only for series option data), + * or provided in nameList from outside. + */ + initData( + data: Source | OptionSourceData | DataStore | DataProvider, + nameList?: string[], + dimValueGetter?: DimValueGetter + ): void { + let store: DataStore; + if (data instanceof DataStore) { + store = data; + } + + if (!store) { + const dimensions = this.dimensions; + const provider = (isSourceInstance(data) || zrUtil.isArrayLike(data)) + ? new DefaultDataProvider(data as Source | OptionSourceData, dimensions.length) + : data as DataProvider; + store = new DataStore(); + const dimensionInfos: DataStoreDimensionDefine[] = map(dimensions, dimName => ({ + type: this._dimInfos[dimName].type, + property: dimName + })); + store.initData(provider, dimensionInfos, dimValueGetter); + } + + this._store = store; + + // Reset + this._nameList = (nameList || []).slice(); + this._idList = []; + this._nameRepeatCount = {}; + + this._doInit(0, store.count()); + + // Cache summary info for fast visit. See "dimensionHelper". + // Needs to be initialized after store is prepared. + this._dimSummary = summarizeDimensions(this, this._schema); + this.userOutput = this._dimSummary.userOutput; + } + + /** + * Caution: Can be only called on raw data (before `this._indices` created). + */ + appendData(data: ArrayLike): void { + const range = this._store.appendData(data); + this._doInit(range[0], range[1]); + } + /** + * Caution: Can be only called on raw data (before `this._indices` created). + * This method does not modify `rawData` (`dataProvider`), but only + * add values to store. + * + * The final count will be increased by `Math.max(values.length, names.length)`. + * + * @param values That is the SourceType: 'arrayRows', like + * [ + * [12, 33, 44], + * [NaN, 43, 1], + * ['-', 'asdf', 0] + * ] + * Each item is exaclty cooresponding to a dimension. + */ + appendValues(values: any[][], names?: string[]): void { + const {start, end} = this._store.appendValues(values, names.length); + const shouldMakeIdFromName = this._shouldMakeIdFromName(); + + this._updateOrdinalMeta(); + + if (names) { + for (let idx = start; idx < end; idx++) { + const sourceIdx = idx - start; + this._nameList[idx] = names[sourceIdx]; + if (shouldMakeIdFromName) { + makeIdFromName(this, idx); + } + } + } + } + + private _updateOrdinalMeta(): void { + const store = this._store; + const dimensions = this.dimensions; + for (let i = 0; i < dimensions.length; i++) { + const dimInfo = this._dimInfos[dimensions[i]]; + if (dimInfo.ordinalMeta) { + store.collectOrdinalMeta(dimInfo.storeDimIndex, dimInfo.ordinalMeta); + } + } + } + + private _shouldMakeIdFromName(): boolean { + const provider = this._store.getProvider(); + return this._idDimIdx == null + && provider.getSource().sourceFormat !== SOURCE_FORMAT_TYPED_ARRAY + && !provider.fillStorage; + } + + private _doInit(start: number, end: number): void { + if (start >= end) { + return; + } + + const store = this._store; + const provider = store.getProvider(); + + this._updateOrdinalMeta(); + + const nameList = this._nameList; + const idList = this._idList; + const sourceFormat = provider.getSource().sourceFormat; + const isFormatOriginal = sourceFormat === SOURCE_FORMAT_ORIGINAL; + + // Each data item is value + // [1, 2] + // 2 + // Bar chart, line chart which uses category axis + // only gives the 'y' value. 'x' value is the indices of category + // Use a tempValue to normalize the value to be a (x, y) value + // If dataItem is {name: ...} or {id: ...}, it has highest priority. + // This kind of ids and names are always stored `_nameList` and `_idList`. + if (isFormatOriginal && !provider.pure) { + const sharedDataItem = [] as OptionDataItem; + for (let idx = start; idx < end; idx++) { + // NOTICE: Try not to write things into dataItem + const dataItem = provider.getItem(idx, sharedDataItem); + if (!this.hasItemOption && isDataItemOption(dataItem)) { + this.hasItemOption = true; + } + if (dataItem) { + const itemName = (dataItem as any).name; + if (nameList[idx] == null && itemName != null) { + nameList[idx] = convertOptionIdName(itemName, null); + } + const itemId = (dataItem as any).id; + if (idList[idx] == null && itemId != null) { + idList[idx] = convertOptionIdName(itemId, null); + } + } + } + } + + if (this._shouldMakeIdFromName()) { + for (let idx = start; idx < end; idx++) { + makeIdFromName(this, idx); + } + } + + prepareInvertedIndex(this); + } + + /** + * PENDING: In fact currently this function is only used to short-circuit + * the calling of `scale.unionExtentFromData` when data have been filtered by modules + * like "dataZoom". `scale.unionExtentFromData` is used to calculate data extent for series on + * an axis, but if a "axis related data filter module" is used, the extent of the axis have + * been fixed and no need to calling `scale.unionExtentFromData` actually. + * But if we add "custom data filter" in future, which is not "axis related", this method may + * be still needed. + * + * Optimize for the scenario that data is filtered by a given extent. + * Consider that if data amount is more than hundreds of thousand, + * extent calculation will cost more than 10ms and the cache will + * be erased because of the filtering. + */ + getApproximateExtent(dim: SeriesDimensionLoose): [number, number] { + return this._approximateExtent[dim] || this._store.getDataExtent(this._getStoreDimIndex(dim)); + } + + /** + * Calculate extent on a filtered data might be time consuming. + * Approximate extent is only used for: calculte extent of filtered data outside. + */ + setApproximateExtent(extent: [number, number], dim: SeriesDimensionLoose): void { + dim = this.getDimension(dim); + this._approximateExtent[dim] = extent.slice() as [number, number]; + } + + getCalculationInfo>( + key: CALC_INFO_KEY + ): DataCalculationInfo[CALC_INFO_KEY] { + return this._calculationInfo[key]; + } + + /** + * @param key or k-v object + */ + setCalculationInfo( + key: DataCalculationInfo + ): void; + setCalculationInfo>( + key: CALC_INFO_KEY, + value: DataCalculationInfo[CALC_INFO_KEY] + ): void; + setCalculationInfo( + key: (keyof DataCalculationInfo) | DataCalculationInfo, + value?: DataCalculationInfo[keyof DataCalculationInfo] + ): void { + isObject(key) + ? zrUtil.extend(this._calculationInfo, key as object) + : ((this._calculationInfo as any)[key] = value); + } + + /** + * @return Never be null/undefined. `number` will be converted to string. Becuase: + * In most cases, name is used in display, where returning a string is more convenient. + * In other cases, name is used in query (see `indexOfName`), where we can keep the + * rule that name `2` equals to name `'2'`. + */ + getName(idx: number): string { + const rawIndex = this.getRawIndex(idx); + let name = this._nameList[rawIndex]; + if (name == null && this._nameDimIdx != null) { + name = getIdNameFromStore(this, this._nameDimIdx, rawIndex); + } + if (name == null) { + name = ''; + } + return name; + } + + private _getCategory(dimIdx: number, idx: number): OrdinalRawValue { + const ordinal = this._store.get(dimIdx, idx); + const ordinalMeta = this._store.getOrdinalMeta(dimIdx); + if (ordinalMeta) { + return ordinalMeta.categories[ordinal as OrdinalNumber]; + } + return ordinal; + } + + /** + * @return Never null/undefined. `number` will be converted to string. Becuase: + * In all cases having encountered at present, id is used in making diff comparison, which + * are usually based on hash map. We can keep the rule that the internal id are always string + * (treat `2` is the same as `'2'`) to make the related logic simple. + */ + getId(idx: number): string { + return getId(this, this.getRawIndex(idx)); + } + + count(): number { + return this._store.count(); + } + + /** + * Get value. Return NaN if idx is out of range. + * + * @notice Should better to use `data.getStore().get(dimIndex, dataIdx)` instead. + */ + get(dim: SeriesDimensionName, idx: number): ParsedValue { + const store = this._store; + const dimInfo = this._dimInfos[dim]; + if (dimInfo) { + return store.get(dimInfo.storeDimIndex, idx); + } + } + + /** + * @notice Should better to use `data.getStore().getByRawIndex(dimIndex, dataIdx)` instead. + */ + getByRawIndex(dim: SeriesDimensionName, rawIdx: number): ParsedValue { + const store = this._store; + const dimInfo = this._dimInfos[dim]; + if (dimInfo) { + return store.getByRawIndex(dimInfo.storeDimIndex, rawIdx); + } + } + + getIndices() { + return this._store.getIndices(); + } + + getDataExtent(dim: DimensionLoose): [number, number] { + return this._store.getDataExtent(this._getStoreDimIndex(dim)); + } + + getSum(dim: DimensionLoose): number { + return this._store.getSum(this._getStoreDimIndex(dim)); + } + + getMedian(dim: DimensionLoose): number { + return this._store.getMedian(this._getStoreDimIndex(dim)); + } + /** + * Get value for multi dimensions. + * @param dimensions If ignored, using all dimensions. + */ + getValues(idx: number): ParsedValue[]; + getValues(dimensions: readonly DimensionName[], idx: number): ParsedValue[]; + getValues(dimensions: readonly DimensionName[] | number, idx?: number): ParsedValue[] { + const store = this._store; + return zrUtil.isArray(dimensions) + ? store.getValues(map(dimensions, dim => this._getStoreDimIndex(dim)), idx) + : store.getValues(dimensions as number); + } + + /** + * If value is NaN. Inlcuding '-' + * Only check the coord dimensions. + */ + hasValue(idx: number): boolean { + const dataDimIndicesOnCoord = this._dimSummary.dataDimIndicesOnCoord; + for (let i = 0, len = dataDimIndicesOnCoord.length; i < len; i++) { + // Ordinal type originally can be string or number. + // But when an ordinal type is used on coord, it can + // not be string but only number. So we can also use isNaN. + if (isNaN(this._store.get(dataDimIndicesOnCoord[i], idx) as any)) { + return false; + } + } + return true; + } + + /** + * Retreive the index with given name + */ + indexOfName(name: string): number { + for (let i = 0, len = this._store.count(); i < len; i++) { + if (this.getName(i) === name) { + return i; + } + } + return -1; + } + + getRawIndex(idx: number): number { + return this._store.getRawIndex(idx); + } + + indexOfRawIndex(rawIndex: number): number { + return this._store.indexOfRawIndex(rawIndex); + } + + /** + * Only support the dimension which inverted index created. + * Do not support other cases until required. + * @param dim concrete dim + * @param value ordinal index + * @return rawIndex + */ + rawIndexOf(dim: SeriesDimensionName, value: OrdinalNumber): number { + const invertedIndices = dim && this._invertedIndicesMap[dim]; + if (__DEV__) { + if (!invertedIndices) { + throw new Error('Do not supported yet'); + } + } + const rawIndex = invertedIndices[value]; + if (rawIndex == null || isNaN(rawIndex)) { + return INDEX_NOT_FOUND; + } + return rawIndex; + } + + /** + * Retreive the index of nearest value + * @param dim + * @param value + * @param [maxDistance=Infinity] + * @return If and only if multiple indices has + * the same value, they are put to the result. + */ + indicesOfNearest(dim: DimensionLoose, value: number, maxDistance?: number): number[] { + return this._store.indicesOfNearest( + this._getStoreDimIndex(dim), + value, maxDistance + ); + } + /** + * Data iteration + * @param ctx default this + * @example + * list.each('x', function (x, idx) {}); + * list.each(['x', 'y'], function (x, y, idx) {}); + * list.each(function (idx) {}) + */ + each(cb: EachCb0, ctx?: Ctx, ctxCompat?: Ctx): void; + each(dims: DimensionLoose, cb: EachCb1, ctx?: Ctx): void; + each(dims: [DimensionLoose], cb: EachCb1, ctx?: Ctx): void; + each(dims: [DimensionLoose, DimensionLoose], cb: EachCb2, ctx?: Ctx): void; + each(dims: ItrParamDims, cb: EachCb, ctx?: Ctx): void; + each( + dims: ItrParamDims | EachCb, + cb: EachCb | Ctx, + ctx?: Ctx + ): void { + 'use strict'; + + if (typeof dims === 'function') { + ctx = cb as Ctx; + cb = dims; + dims = []; + } + + // ctxCompat just for compat echarts3 + const fCtx = (ctx || this) as CtxOrList; + + const dimIndices = map(normalizeDimensions(dims), this._getStoreDimIndex, this); + + this._store.each(dimIndices, (fCtx + ? zrUtil.bind(cb as any, fCtx as any) + : cb) as any + ); + } + /** + * Data filter + */ + filterSelf(cb: FilterCb0, ctx?: Ctx, ctxCompat?: Ctx): this; + filterSelf(dims: DimensionLoose, cb: FilterCb1, ctx?: Ctx): this; + filterSelf(dims: [DimensionLoose], cb: FilterCb1, ctx?: Ctx): this; + filterSelf(dims: [DimensionLoose, DimensionLoose], cb: FilterCb2, ctx?: Ctx): this; + filterSelf(dims: ItrParamDims, cb: FilterCb, ctx?: Ctx): this; + filterSelf( + dims: ItrParamDims | FilterCb, + cb: FilterCb | Ctx, + ctx?: Ctx + ): SeriesData { + 'use strict'; + + if (typeof dims === 'function') { + ctx = cb as Ctx; + cb = dims; + dims = []; + } + + // ctxCompat just for compat echarts3 + const fCtx = (ctx || this) as CtxOrList; + + const dimIndices = map(normalizeDimensions(dims), this._getStoreDimIndex, this); + + this._store = this._store.filter(dimIndices, (fCtx + ? zrUtil.bind(cb as any, fCtx as any) + : cb) as any + ); + + return this; + } + + /** + * Select data in range. (For optimization of filter) + * (Manually inline code, support 5 million data filtering in data zoom.) + */ + selectRange(range: Record): SeriesData { + 'use strict'; + + const innerRange: Record = {}; + const dims = zrUtil.keys(range); + const dimIndices: number[] = []; + zrUtil.each(dims, (dim) => { + const dimIdx = this._getStoreDimIndex(dim); + innerRange[dimIdx] = range[dim]; + dimIndices.push(dimIdx); + }); + + this._store = this._store.selectRange(innerRange); + return this; + } + + /** + * Data mapping to a plain array + */ + mapArray>(cb: Cb, ctx?: Ctx, ctxCompat?: Ctx): ReturnType[]; + /* eslint-disable max-len */ + mapArray>(dims: DimensionLoose, cb: Cb, ctx?: Ctx, ctxCompat?: Ctx): ReturnType[]; + mapArray>(dims: [DimensionLoose], cb: Cb, ctx?: Ctx, ctxCompat?: Ctx): ReturnType[]; + mapArray>(dims: [DimensionLoose, DimensionLoose], cb: Cb, ctx?: Ctx, ctxCompat?: Ctx): ReturnType[]; + mapArray>(dims: ItrParamDims, cb: Cb, ctx?: Ctx, ctxCompat?: Ctx): ReturnType[]; + /* eslint-enable max-len */ + mapArray( + dims: ItrParamDims | MapArrayCb, + cb: MapArrayCb | Ctx, + ctx?: Ctx + ): unknown[] { + 'use strict'; + + if (typeof dims === 'function') { + ctx = cb as Ctx; + cb = dims; + dims = []; + } + + // ctxCompat just for compat echarts3 + ctx = (ctx || this) as Ctx; + + const result: unknown[] = []; + this.each(dims, function () { + result.push(cb && (cb as MapArrayCb).apply(this, arguments)); + }, ctx); + return result; + } + + /** + * Data mapping to a new List with given dimensions + */ + map(dims: DimensionLoose, cb: MapCb1, ctx?: Ctx, ctxCompat?: Ctx): SeriesData; + map(dims: [DimensionLoose], cb: MapCb1, ctx?: Ctx, ctxCompat?: Ctx): SeriesData; + // eslint-disable-next-line max-len + map(dims: [DimensionLoose, DimensionLoose], cb: MapCb2, ctx?: Ctx, ctxCompat?: Ctx): SeriesData; + map( + dims: ItrParamDims, + cb: MapCb, + ctx?: Ctx, + ctxCompat?: Ctx + ): SeriesData { + 'use strict'; + + // ctxCompat just for compat echarts3 + const fCtx = (ctx || ctxCompat || this) as CtxOrList; + + const dimIndices = map( + normalizeDimensions(dims), this._getStoreDimIndex, this + ); + + const list = cloneListForMapAndSample(this); + list._store = this._store.map( + dimIndices, + fCtx ? zrUtil.bind(cb, fCtx) : cb + ); + return list; + } + + /** + * !!Danger: used on stack dimension only. + */ + modify(dims: DimensionLoose, cb: MapCb1, ctx?: Ctx, ctxCompat?: Ctx): void; + modify(dims: [DimensionLoose], cb: MapCb1, ctx?: Ctx, ctxCompat?: Ctx): void; + modify(dims: [DimensionLoose, DimensionLoose], cb: MapCb2, ctx?: Ctx, ctxCompat?: Ctx): void; + modify( + dims: ItrParamDims, + cb: MapCb, + ctx?: Ctx, + ctxCompat?: Ctx + ): void { + // ctxCompat just for compat echarts3 + const fCtx = (ctx || ctxCompat || this) as CtxOrList; + + if (__DEV__) { + zrUtil.each(normalizeDimensions(dims), dim => { + const dimInfo = this.getDimensionInfo(dim); + if (!dimInfo.isCalculationCoord) { + console.error('Danger: only stack dimension can be modified'); + } + }); + } + + const dimIndices = map( + normalizeDimensions(dims), this._getStoreDimIndex, this + ); + + // If do shallow clone here, if there are too many stacked series, + // it still cost lots of memory, becuase `_store.dimensions` are not shared. + // We should consider there probably be shallow clone happen in each sereis + // in consequent filter/map. + this._store.modify( + dimIndices, + fCtx ? zrUtil.bind(cb, fCtx) : cb + ); + } + + /** + * Large data down sampling on given dimension + * @param sampleIndex Sample index for name and id + */ + downSample( + dimension: DimensionLoose, + rate: number, + sampleValue: (frameValues: ArrayLike) => ParsedValueNumeric, + sampleIndex: (frameValues: ArrayLike, value: ParsedValueNumeric) => number + ): SeriesData { + const list = cloneListForMapAndSample(this); + list._store = this._store.downSample( + this._getStoreDimIndex(dimension), + rate, + sampleValue, + sampleIndex + ); + return list as SeriesData; + } + + /** + * Large data down sampling using largest-triangle-three-buckets + * @param {string} valueDimension + * @param {number} targetCount + */ + lttbDownSample( + valueDimension: DimensionLoose, + rate: number + ): SeriesData { + const list = cloneListForMapAndSample(this); + list._store = this._store.lttbDownSample( + this._getStoreDimIndex(valueDimension), + rate + ); + return list as SeriesData; + } + + getRawDataItem(idx: number) { + return this._store.getRawDataItem(idx); + } + + /** + * Get model of one data item. + */ + // TODO: Type of data item + getItemModel(idx: number): Model + > { + const hostModel = this.hostModel; + const dataItem = this.getRawDataItem(idx) as ModelOption; + return new Model(dataItem, hostModel, hostModel && hostModel.ecModel); + } + + /** + * Create a data differ + */ + diff(otherList: SeriesData): DataDiffer { + const thisList = this; + + return new DataDiffer( + otherList ? otherList.getStore().getIndices() : [], + this.getStore().getIndices(), + function (idx: number) { + return getId(otherList, idx); + }, + function (idx: number) { + return getId(thisList, idx); + } + ); + } + + /** + * Get visual property. + */ + getVisual(key: K): Visual[K] { + const visual = this._visual as Visual; + return visual && visual[key]; + } + + /** + * Set visual property + * + * @example + * setVisual('color', color); + * setVisual({ + * 'color': color + * }); + */ + setVisual(key: K, val: Visual[K]): void; + setVisual(kvObj: Partial): void; + setVisual(kvObj: string | Partial, val?: any): void { + this._visual = this._visual || {}; + if (isObject(kvObj)) { + zrUtil.extend(this._visual, kvObj); + } + else { + this._visual[kvObj as string] = val; + } + } + + /** + * Get visual property of single data item + */ + // eslint-disable-next-line + getItemVisual(idx: number, key: K): Visual[K] { + const itemVisual = this._itemVisuals[idx] as Visual; + const val = itemVisual && itemVisual[key]; + if (val == null) { + // Use global visual property + return this.getVisual(key); + } + return val; + } + + /** + * If exists visual property of single data item + */ + hasItemVisual() { + return this._itemVisuals.length > 0; + } + + /** + * Make sure itemVisual property is unique + */ + // TODO: use key to save visual to reduce memory. + ensureUniqueItemVisual(idx: number, key: K): Visual[K] { + const itemVisuals = this._itemVisuals; + let itemVisual = itemVisuals[idx] as Visual; + if (!itemVisual) { + itemVisual = itemVisuals[idx] = {} as Visual; + } + let val = itemVisual[key]; + if (val == null) { + val = this.getVisual(key); + + // TODO Performance? + if (zrUtil.isArray(val)) { + val = val.slice() as unknown as Visual[K]; + } + else if (isObject(val)) { + val = zrUtil.extend({}, val); + } + + itemVisual[key] = val; + } + return val; + } + /** + * Set visual property of single data item + * + * @param {number} idx + * @param {string|Object} key + * @param {*} [value] + * + * @example + * setItemVisual(0, 'color', color); + * setItemVisual(0, { + * 'color': color + * }); + */ + // eslint-disable-next-line + setItemVisual(idx: number, key: K, value: Visual[K]): void; + setItemVisual(idx: number, kvObject: Partial): void; + // eslint-disable-next-line + setItemVisual(idx: number, key: K | Partial, value?: Visual[K]): void { + const itemVisual = this._itemVisuals[idx] || {}; + this._itemVisuals[idx] = itemVisual; + + if (isObject(key)) { + zrUtil.extend(itemVisual, key); + } + else { + itemVisual[key as string] = value; + } + } + + /** + * Clear itemVisuals and list visual. + */ + clearAllVisual(): void { + this._visual = {}; + this._itemVisuals = []; + } + + /** + * Set layout property. + */ + setLayout(key: string, val: any): void; + setLayout(kvObj: Dictionary): void; + setLayout(key: string | Dictionary, val?: any): void { + if (isObject(key)) { + for (const name in key) { + if (key.hasOwnProperty(name)) { + this.setLayout(name, key[name]); + } + } + return; + } + this._layout[key] = val; + } + + /** + * Get layout property. + */ + getLayout(key: string): any { + return this._layout[key]; + } + + /** + * Get layout of single data item + */ + getItemLayout(idx: number): any { + return this._itemLayouts[idx]; + } + + /** + * Set layout of single data item + */ + setItemLayout( + idx: number, + layout: (M extends true ? Dictionary : any), + merge?: M + ): void { + this._itemLayouts[idx] = merge + ? zrUtil.extend(this._itemLayouts[idx] || {}, layout) + : layout; + } + + /** + * Clear all layout of single data item + */ + clearItemLayouts(): void { + this._itemLayouts.length = 0; + } + + /** + * Set graphic element relative to data. It can be set as null + */ + setItemGraphicEl(idx: number, el: Element): void { + const seriesIndex = this.hostModel && (this.hostModel as any).seriesIndex; + + setCommonECData(seriesIndex, this.dataType, idx, el); + + this._graphicEls[idx] = el; + } + + getItemGraphicEl(idx: number): Element { + return this._graphicEls[idx]; + } + + eachItemGraphicEl( + cb: (this: Ctx, el: Element, idx: number) => void, + context?: Ctx + ): void { + zrUtil.each(this._graphicEls, function (el, idx) { + if (el) { + cb && cb.call(context, el, idx); + } + }); + } + + /** + * Shallow clone a new list except visual and layout properties, and graph elements. + * New list only change the indices. + */ + cloneShallow(list?: SeriesData): SeriesData { + if (!list) { + list = new SeriesData( + this._schema + ? this._schema + : map(this.dimensions, this._getDimInfo, this), + this.hostModel + ); + } + + transferProperties(list, this); + list._store = this._store; + + return list; + } + + /** + * Wrap some method to add more feature + */ + wrapMethod( + methodName: FunctionPropertyNames, + injectFunction: (...args: any) => any + ): void { + const originalMethod = this[methodName]; + if (typeof originalMethod !== 'function') { + return; + } + this.__wrappedMethods = this.__wrappedMethods || []; + this.__wrappedMethods.push(methodName); + this[methodName] = function () { + const res = (originalMethod as any).apply(this, arguments); + return injectFunction.apply(this, [res].concat(zrUtil.slice(arguments))); + }; + } + + + // ---------------------------------------------------------- + // A work around for internal method visiting private member. + // ---------------------------------------------------------- + private static internalField = (function () { + + prepareInvertedIndex = function (data: SeriesData): void { + const invertedIndicesMap = data._invertedIndicesMap; + zrUtil.each(invertedIndicesMap, function (invertedIndices, dim) { + const dimInfo = data._dimInfos[dim]; + // Currently, only dimensions that has ordinalMeta can create inverted indices. + const ordinalMeta = dimInfo.ordinalMeta; + const store = data._store; + if (ordinalMeta) { + invertedIndices = invertedIndicesMap[dim] = new CtorInt32Array( + ordinalMeta.categories.length + ); + // The default value of TypedArray is 0. To avoid miss + // mapping to 0, we should set it as INDEX_NOT_FOUND. + for (let i = 0; i < invertedIndices.length; i++) { + invertedIndices[i] = INDEX_NOT_FOUND; + } + for (let i = 0; i < store.count(); i++) { + // Only support the case that all values are distinct. + invertedIndices[store.get(dimInfo.storeDimIndex, i) as number] = i; + } + } + }); + }; + + getIdNameFromStore = function ( + data: SeriesData, dimIdx: number, idx: number + ): string { + return convertOptionIdName(data._getCategory(dimIdx, idx), null); + }; + + /** + * @see the comment of `List['getId']`. + */ + getId = function (data: SeriesData, rawIndex: number): string { + let id = data._idList[rawIndex]; + if (id == null && data._idDimIdx != null) { + id = getIdNameFromStore(data, data._idDimIdx, rawIndex); + } + if (id == null) { + id = ID_PREFIX + rawIndex; + } + return id; + }; + + normalizeDimensions = function ( + dimensions: ItrParamDims + ): Array { + if (!zrUtil.isArray(dimensions)) { + dimensions = dimensions != null ? [dimensions] : []; + } + return dimensions; + }; + + /** + * Data in excludeDimensions is copied, otherwise transfered. + */ + cloneListForMapAndSample = function (original: SeriesData): SeriesData { + const list = new SeriesData( + original._schema + ? original._schema + : map(original.dimensions, original._getDimInfo, original), + original.hostModel + ); + // FIXME If needs stackedOn, value may already been stacked + transferProperties(list, original); + return list; + }; + + transferProperties = function (target: SeriesData, source: SeriesData): void { + zrUtil.each( + TRANSFERABLE_PROPERTIES.concat(source.__wrappedMethods || []), + function (propName) { + if (source.hasOwnProperty(propName)) { + (target as any)[propName] = (source as any)[propName]; + } + } + ); + + target.__wrappedMethods = source.__wrappedMethods; + + zrUtil.each(CLONE_PROPERTIES, function (propName) { + (target as any)[propName] = zrUtil.clone((source as any)[propName]); + }); + + target._calculationInfo = zrUtil.extend({}, source._calculationInfo); + }; + makeIdFromName = function (data: SeriesData, idx: number): void { + const nameList = data._nameList; + const idList = data._idList; + const nameDimIdx = data._nameDimIdx; + const idDimIdx = data._idDimIdx; + + let name = nameList[idx]; + let id = idList[idx]; + + if (name == null && nameDimIdx != null) { + nameList[idx] = name = getIdNameFromStore(data, nameDimIdx, idx); + } + if (id == null && idDimIdx != null) { + idList[idx] = id = getIdNameFromStore(data, idDimIdx, idx); + } + if (id == null && name != null) { + const nameRepeatCount = data._nameRepeatCount; + const nmCnt = nameRepeatCount[name] = (nameRepeatCount[name] || 0) + 1; + id = name; + if (nmCnt > 1) { + id += '__ec__' + nmCnt; + } + idList[idx] = id; + } + }; + })(); + +} + +interface SeriesData { + getLinkedData(dataType?: SeriesDataType): SeriesData; + getLinkedDataAll(): { data: SeriesData, type?: SeriesDataType }[]; +} + +export default SeriesData; diff --git a/src/data/DataDimensionInfo.ts b/src/data/SeriesDimensionDefine.ts similarity index 80% rename from src/data/DataDimensionInfo.ts rename to src/data/SeriesDimensionDefine.ts index e2f3e02cf0..3d4b628f21 100644 --- a/src/data/DataDimensionInfo.ts +++ b/src/data/SeriesDimensionDefine.ts @@ -21,11 +21,10 @@ import * as zrUtil from 'zrender/src/core/util'; import OrdinalMeta from './OrdinalMeta'; import { DataVisualDimensions, DimensionType } from '../util/types'; -class DataDimensionInfo { +class SeriesDimensionDefine { /** * Dimension type. The enumerable values are the key of - * `dataCtors` of `data/List`. * Optional. */ type?: DimensionType; @@ -46,6 +45,17 @@ class DataDimensionInfo { // See Series.ts#formatArrayValue tooltip?: boolean; + /** + * This dimension maps to the the dimension in dataStore by `storeDimIndex`. + * Notice the facts: + * 1. When there are too many dimensions in data store, seriesData only save the + * used store dimensions. + * 2. We use dimensionIndex but not name to reference store dimension + * becuause the dataset dimension definition might has no name specified by users, + * or names in sereis dimension definition might be different from dataset. + */ + storeDimIndex?: number; + /** * Which coordSys dimension this dimension mapped to. * A `coordDim` can be a "coordSysDim" that the coordSys required @@ -61,21 +71,14 @@ class DataDimensionInfo { * Mandatory. */ coordDimIndex?: number; - - /** - * This index of this dimension info in `data/List#_dimensionInfos`. - * Mandatory after added to `data/List`. - */ - index?: number; - /** * The format of `otherDims` is: * ```js * { - * tooltip: number optional, - * label: number optional, - * itemName: number optional, - * seriesName: number optional, + * tooltip?: number + * label?: number + * itemName?: number + * seriesName?: number * } * ``` * @@ -94,7 +97,7 @@ class DataDimensionInfo { * this.otherDims = { * // `3` is at the index `0` of the `encode.tooltip` * tooltip: 0, - * // `3` is at the index `1` of the `encode.tooltip` + * // `3` is at the index `1` of the `encode.label` * label: 1 * }; * ``` @@ -126,7 +129,7 @@ class DataDimensionInfo { /** * @param opt All of the fields will be shallow copied. */ - constructor(opt?: object | DataDimensionInfo) { + constructor(opt?: object | SeriesDimensionDefine) { if (opt != null) { zrUtil.extend(this, opt); } @@ -134,4 +137,4 @@ class DataDimensionInfo { }; -export default DataDimensionInfo; +export default SeriesDimensionDefine; diff --git a/src/data/Source.ts b/src/data/Source.ts index bedebb14f7..a530a50811 100644 --- a/src/data/Source.ts +++ b/src/data/Source.ts @@ -32,7 +32,6 @@ import { DimensionName, OptionSourceHeader, DimensionDefinitionLoose, - OptionEncode, SOURCE_FORMAT_ARRAY_ROWS, SOURCE_FORMAT_OBJECT_ROWS, Dictionary, @@ -45,6 +44,7 @@ import { } from '../util/types'; import { DatasetOption } from '../component/dataset/install'; import { getDataItemValue } from '../util/model'; +import { BE_ORDINAL, guessOrdinal } from './helper/sourceHelper'; /** * [sourceFormat] @@ -119,13 +119,6 @@ class SourceImpl { */ readonly dimensionsDefine: DimensionDefinition[]; - /** - * encode definition in option. - * can be null/undefined. - * Might be specified outside. - */ - readonly encodeDefine: HashMap; - /** * Only make sense in `SOURCE_FORMAT_ARRAY_ROWS`. * That is the same as `sourceHeader: number`, @@ -146,8 +139,6 @@ class SourceImpl { */ readonly metaRawOption: SourceMetaRawOption; - // readonly frozen: boolean; - constructor(fields: { data: OptionSourceData, @@ -176,42 +167,22 @@ class SourceImpl { // Visit config this.seriesLayoutBy = fields.seriesLayoutBy || SERIES_LAYOUT_BY_COLUMN; this.startIndex = fields.startIndex || 0; - this.dimensionsDefine = fields.dimensionsDefine; this.dimensionsDetectedCount = fields.dimensionsDetectedCount; - this.encodeDefine = fields.encodeDefine; this.metaRawOption = fields.metaRawOption; - } - // There is performance issue in some browser like Safari, - // an also slower than clone in Chrome. - // So DO NOT use `Object.freeze`. - /** - * When expose the source to thrid-party transform, it probably better to - * freeze to make sure immutability. - * If a third-party transform modify the raw upstream data structure, it might bring about - * "uncertain effect" when using multiple transforms with different combinations. - * - * [Caveat] - * `OptionManager.ts` have perform `clone` in `setOption`. - * The original user input object should better not be frozen in case they - * make other usages. - */ - // freeze() { - // assert(sourceFormatCanBeExposed(this)); - // const data = this.data as OptionSourceDataArrayRows; - // if (this.frozen || !data || !isFunction(Object.freeze)) { - // return; - // } - // // @ts-ignore - // this.frozen = true; - // // PENDING: - // // There is a flaw that there might be non-primitive values like `Date`. - // // Is it worth handling that? - // for (let i = 0; i < data.length; i++) { - // Object.freeze(data[i]); - // } - // Object.freeze(data); - // } + const dimensionsDefine = this.dimensionsDefine = fields.dimensionsDefine; + + if (dimensionsDefine) { + for (let i = 0; i < dimensionsDefine.length; i++) { + const dim = dimensionsDefine[i]; + if (dim.type == null) { + if (guessOrdinal(this, i) === BE_ORDINAL.Must) { + dim.type = 'ordinal'; + } + } + } + } + } } @@ -219,12 +190,15 @@ export function isSourceInstance(val: unknown): val is Source { return val instanceof SourceImpl; } +/** + * Create a source from option. + * NOTE: Created source is immutable. Don't change any properties in it. + */ export function createSource( sourceData: OptionSourceData, thisMetaRawOption: SourceMetaRawOption, // can be null. If not provided, auto detect it from `sourceData`. - sourceFormat: SourceFormat, - encodeDefine: OptionEncode // can be null + sourceFormat: SourceFormat ): Source { sourceFormat = sourceFormat || detectSourceFormat(sourceData); const seriesLayoutBy = thisMetaRawOption.seriesLayoutBy; @@ -243,7 +217,6 @@ export function createSource( dimensionsDefine: determined.dimensionsDefine, startIndex: determined.startIndex, dimensionsDetectedCount: determined.dimensionsDetectedCount, - encodeDefine: makeEncodeDefine(encodeDefine), metaRawOption: clone(thisMetaRawOption) }); @@ -273,20 +246,10 @@ export function cloneSourceShallow(source: Source): Source { seriesLayoutBy: source.seriesLayoutBy, dimensionsDefine: clone(source.dimensionsDefine), startIndex: source.startIndex, - dimensionsDetectedCount: source.dimensionsDetectedCount, - encodeDefine: makeEncodeDefine(source.encodeDefine) + dimensionsDetectedCount: source.dimensionsDetectedCount }); } -function makeEncodeDefine( - encodeDefine: OptionEncode | HashMap -): HashMap { - // null means user not specify `series.encode`. - return encodeDefine - ? createHashMap(encodeDefine) - : null; -} - /** * Note: An empty array will be detected as `SOURCE_FORMAT_ARRAY_ROWS`. */ @@ -516,3 +479,8 @@ function arrayRowsTravelFirst( } } } + +export function shouldRetrieveDataByName(source: Source): boolean { + const sourceFormat = source.sourceFormat; + return sourceFormat === SOURCE_FORMAT_OBJECT_ROWS || sourceFormat === SOURCE_FORMAT_KEYED_COLUMNS; +} diff --git a/src/data/Tree.ts b/src/data/Tree.ts index 684fcb83e7..0389f6a9bf 100644 --- a/src/data/Tree.ts +++ b/src/data/Tree.ts @@ -23,9 +23,9 @@ import * as zrUtil from 'zrender/src/core/util'; import Model from '../model/Model'; -import linkList from './helper/linkList'; -import List from './List'; -import createDimensions from './helper/createDimensions'; +import linkSeriesData from './helper/linkSeriesData'; +import SeriesData from './SeriesData'; +import prepareSeriesDataSchema from './helper/createDimensions'; import { DimensionLoose, ParsedValue, OptionDataValue, OptionDataItemObject @@ -210,7 +210,7 @@ export class TreeNode { getValue(dimension?: DimensionLoose): ParsedValue { const data = this.hostTree.data; - return data.get(data.getDimension(dimension || 'value'), this.dataIndex); + return data.getStore().get(data.getDimensionIndex(dimension || 'value'), this.dataIndex); } setLayout(layout: any, merge?: boolean) { @@ -273,6 +273,22 @@ export class TreeNode { return this.hostTree.data.getId(this.dataIndex); } + /** + * index in parent's children + */ + getChildIndex(): number { + if (this.parentNode) { + const children = this.parentNode.children; + for (let i = 0; i < children.length; ++i) { + if (children[i] === this) { + return i; + } + } + return -1; + } + return -1; + } + /** * if this is an ancestor of another node * @@ -307,7 +323,7 @@ class Tree { root: TreeNode; - data: List; + data: SeriesData; hostModel: HostModel; @@ -399,7 +415,7 @@ class Tree { static createTree( dataRoot: T, hostModel: HostModel, - beforeLink?: (data: List) => void + beforeLink?: (data: SeriesData) => void ) { const tree = new Tree(hostModel); @@ -431,17 +447,17 @@ class Tree { tree.root.updateDepthAndHeight(0); - const dimensionsInfo = createDimensions(listData, { + const { dimensions } = prepareSeriesDataSchema(listData, { coordDimensions: ['value'], dimensionsCount: dimMax }); - const list = new List(dimensionsInfo, hostModel); + const list = new SeriesData(dimensions, hostModel); list.initData(listData); beforeLink && beforeLink(list); - linkList({ + linkSeriesData({ mainData: list, struct: tree, structAttr: 'tree' diff --git a/src/data/helper/SeriesDataSchema.ts b/src/data/helper/SeriesDataSchema.ts new file mode 100644 index 0000000000..67e9f1fc9a --- /dev/null +++ b/src/data/helper/SeriesDataSchema.ts @@ -0,0 +1,267 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +import { createHashMap, HashMap, isObject, retrieve2 } from 'zrender/src/core/util'; +import { makeInner } from '../../util/model'; +import { + DimensionDefinition, DimensionDefinitionLoose, DimensionIndex, DimensionName, DimensionType +} from '../../util/types'; +import { DataStoreDimensionDefine } from '../DataStore'; +import OrdinalMeta from '../OrdinalMeta'; +import SeriesDimensionDefine from '../SeriesDimensionDefine'; +import { shouldRetrieveDataByName, Source } from '../Source'; + +const inner = makeInner<{ + dimNameMap: HashMap; +}, Source>(); + +const dimTypeShort = { + float: 'f', int: 'i', ordinal: 'o', number: 'n', time: 't' +} as const; + +/** + * Represents the dimension requirement of a series. + * + * NOTICE: + * When there are too many dimensions in dataset and many series, only the used dimensions + * (i.e., used by coord sys and declared in `series.encode`) are add to `dimensionDefineList`. + * But users may query data by other unused dimension names. + * In this case, users can only query data if and only if they have defined dimension names + * via ec option, so we provide `getDimensionIndexFromSource`, which only query them from + * `source` dimensions. + */ +export class SeriesDataSchema { + + /** + * When there are too many dimensions, `dimensionDefineList` might only contain + * used dimensions. + * + * CAUTION: + * Should have been sorted by `storeDimIndex` asc. + * + * PENDING: + * The item can still be modified outsite. + * But MUST NOT add/remove item of this array. + */ + readonly dimensions: SeriesDimensionDefine[]; + + readonly source: Source; + + private _fullDimCount: number; + private _dimNameMap: ReturnType['dimNameMap']; + private _dimOmitted: boolean; + + constructor(opt: { + source: Source, + dimensions: SeriesDimensionDefine[], + fullDimensionCount: number, + dimensionOmitted: boolean + }) { + this.dimensions = opt.dimensions; + this._dimOmitted = opt.dimensionOmitted; + this.source = opt.source; + this._fullDimCount = opt.fullDimensionCount; + + this._updateDimOmitted(opt.dimensionOmitted); + } + + isDimensionOmitted(): boolean { + return this._dimOmitted; + } + + private _updateDimOmitted(dimensionOmitted: boolean): void { + this._dimOmitted = dimensionOmitted; + if (!dimensionOmitted) { + return; + } + if (!this._dimNameMap) { + this._dimNameMap = ensureSourceDimNameMap(this.source); + } + } + + /** + * @caution Can only be used when `dimensionOmitted: true`. + * + * Get index by user defined dimension name (i.e., not internal generate name). + * That is, get index from `dimensionsDefine`. + * If no `dimensionsDefine`, or no name get, return -1. + */ + getSourceDimensionIndex(dimName: DimensionName): DimensionIndex { + return retrieve2(this._dimNameMap.get(dimName), -1); + } + + /** + * @caution Can only be used when `dimensionOmitted: true`. + * + * Notice: may return `null`/`undefined` if user not specify dimension names. + */ + getSourceDimension(dimIndex: DimensionIndex): DimensionDefinition { + const dimensionsDefine = this.source.dimensionsDefine; + if (dimensionsDefine) { + return dimensionsDefine[dimIndex]; + } + } + + makeStoreSchema(): { + dimensions: DataStoreDimensionDefine[]; + hash: string + } { + const dimCount = this._fullDimCount; + const willRetrieveDataByName = shouldRetrieveDataByName(this.source); + const makeHashStrict = !shouldOmitUnusedDimensions(dimCount); + + // If source don't have dimensions or series don't omit unsed dimensions. + // Generate from seriesDimList directly + let dimHash = ''; + const dims: DataStoreDimensionDefine[] = []; + + for (let fullDimIdx = 0, seriesDimIdx = 0; fullDimIdx < dimCount; fullDimIdx++) { + let property: string; + let type: DimensionType; + let ordinalMeta: OrdinalMeta; + + const seriesDimDef = this.dimensions[seriesDimIdx]; + // The list has been sorted by `storeDimIndex` asc. + if (seriesDimDef && seriesDimDef.storeDimIndex === fullDimIdx) { + property = willRetrieveDataByName ? seriesDimDef.name : null; + type = seriesDimDef.type; + ordinalMeta = seriesDimDef.ordinalMeta; + + seriesDimIdx++; + } + else { + const sourceDimDef = this.getSourceDimension(fullDimIdx); + if (sourceDimDef) { + property = willRetrieveDataByName ? sourceDimDef.name : null; + type = sourceDimDef.type; + } + } + + dims.push({ property, type, ordinalMeta }); + + // If retrieving data by index, + // use to determine whether data can be shared. + // (Becuase in this case there might be no dimension name defined in dataset, but indices always exists). + // (indices are always 0, 1, 2, ..., so we can ignore them to shorten the hash). + // Otherwise if retrieving data by property name (like `data: [{aa: 123, bb: 765}, ...]`), + // use in hash. + if (willRetrieveDataByName + && property != null + // For data stack, we have make sure each series has its own dim on this store. + // So we do not add property to hash to make sure they can share this store. + && (!seriesDimDef || !seriesDimDef.isCalculationCoord) + ) { + dimHash += (makeHashStrict + // Use escape character '`' in case that property name contains '$'. + ? property.replace(/\`/g, '`1').replace(/\$/g, '`2') + // For better performance, when there are large dimensions, tolerant this defects that hardly meet. + : property + ); + } + dimHash += '$'; + dimHash += dimTypeShort[type] || 'f'; + + if (ordinalMeta) { + dimHash += ordinalMeta.uid; + } + + dimHash += '$'; + } + + // Source from endpoint(usually series) will be read differently + // when seriesLayoutBy or startIndex(which is affected by sourceHeader) are different. + // So we use this three props as key. + const source = this.source; + const hash = [ + source.seriesLayoutBy, + source.startIndex, + dimHash + ].join('$$'); + + return { + dimensions: dims, + hash: hash + }; + } + + makeOutputDimensionNames(): DimensionName[] { + const result = [] as DimensionName[]; + + for (let fullDimIdx = 0, seriesDimIdx = 0; fullDimIdx < this._fullDimCount; fullDimIdx++) { + let name: DimensionName; + const seriesDimDef = this.dimensions[seriesDimIdx]; + // The list has been sorted by `storeDimIndex` asc. + if (seriesDimDef && seriesDimDef.storeDimIndex === fullDimIdx) { + if (!seriesDimDef.isCalculationCoord) { + name = seriesDimDef.name; + } + seriesDimIdx++; + } + else { + const sourceDimDef = this.getSourceDimension(fullDimIdx); + if (sourceDimDef) { + name = sourceDimDef.name; + } + } + result.push(name); + } + + return result; + } + + appendCalculationDimension(dimDef: SeriesDimensionDefine): void { + this.dimensions.push(dimDef); + dimDef.isCalculationCoord = true; + this._fullDimCount++; + // If append dimension on a data store, consider the store + // might be shared by different series, series dimensions not + // really map to store dimensions. + this._updateDimOmitted(true); + } +} + +export function isSeriesDataSchema( + schema: any +): schema is SeriesDataSchema { + return schema instanceof SeriesDataSchema; +} + + +export function createDimNameMap(dimsDef: DimensionDefinitionLoose[]): HashMap { + const dataDimNameMap = createHashMap(); + for (let i = 0; i < (dimsDef || []).length; i++) { + const dimDefItemRaw = dimsDef[i]; + const userDimName = isObject(dimDefItemRaw) ? dimDefItemRaw.name : dimDefItemRaw; + if (userDimName != null && dataDimNameMap.get(userDimName) == null) { + dataDimNameMap.set(userDimName, i); + } + } + return dataDimNameMap; +} + +export function ensureSourceDimNameMap(source: Source): HashMap { + const innerSource = inner(source); + return innerSource.dimNameMap || ( + innerSource.dimNameMap = createDimNameMap(source.dimensionsDefine) + ); +} + +export function shouldOmitUnusedDimensions(dimCount: number): boolean { + return dimCount > 30; +} diff --git a/src/data/helper/completeDimensions.ts b/src/data/helper/completeDimensions.ts deleted file mode 100644 index 972dbc5930..0000000000 --- a/src/data/helper/completeDimensions.ts +++ /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. -*/ - -/** - * @deprecated - * Use `echarts/data/helper/createDimensions` instead. - */ - -import {createHashMap, each, isString, defaults, extend, isObject, clone, HashMap} from 'zrender/src/core/util'; -import {normalizeToArray} from '../../util/model'; -import {guessOrdinal, BE_ORDINAL} from './sourceHelper'; -import { createSourceFromSeriesDataOption, isSourceInstance, Source } from '../Source'; -import { - VISUAL_DIMENSIONS, DimensionDefinitionLoose, OptionSourceData, - EncodeDefaulter, OptionEncodeValue, OptionEncode, DimensionName, DimensionIndex, DataVisualDimensions -} from '../../util/types'; -import DataDimensionInfo from '../DataDimensionInfo'; -import List from '../List'; -import { CoordDimensionDefinition, CoordDimensionDefinitionLoose } from './createDimensions'; - -/** - * @see {module:echarts/test/ut/spec/data/completeDimensions} - * - * This method builds the relationship between: - * + "what the coord sys or series requires (see `sysDims`)", - * + "what the user defines (in `encode` and `dimensions`, see `opt.dimsDef` and `opt.encodeDef`)" - * + "what the data source provids (see `source`)". - * - * Some guess strategy will be adapted if user does not define something. - * If no 'value' dimension specified, the first no-named dimension will be - * named as 'value'. - * - * @param {Array.} sysDims Necessary dimensions, like ['x', 'y'], which - * provides not only dim template, but also default order. - * properties: 'name', 'type', 'displayName'. - * `name` of each item provides default coord name. - * [{dimsDef: [string|Object, ...]}, ...] dimsDef of sysDim item provides default dim name, and - * provide dims count that the sysDim required. - * [{ordinalMeta}] can be specified. - * @param {module:echarts/data/Source|Array|Object} source or data (for compatibal with pervious) - * @param {Object} [opt] - * @param {Array.} [opt.dimsDef] option.series.dimensions User defined dimensions - * For example: ['asdf', {name, type}, ...]. - * @param {Object|HashMap} [opt.encodeDef] option.series.encode {x: 2, y: [3, 1], tooltip: [1, 2], label: 3} - * @param {Function} [opt.encodeDefaulter] Called if no `opt.encodeDef` exists. - * If not specified, auto find the next available data dim. - * param source {module:data/Source} - * param dimCount {number} - * return {Object} encode Never be `null/undefined`. - * @param {string} [opt.generateCoord] Generate coord dim with the given name. - * If not specified, extra dim names will be: - * 'value', 'value0', 'value1', ... - * @param {number} [opt.generateCoordCount] By default, the generated dim name is `generateCoord`. - * If `generateCoordCount` specified, the generated dim names will be: - * `generateCoord` + 0, `generateCoord` + 1, ... - * can be Infinity, indicate that use all of the remain columns. - * @param {number} [opt.dimCount] If not specified, guess by the first data item. - * @return {Array.} - */ -function completeDimensions( - sysDims: CoordDimensionDefinitionLoose[], - source: Source | List | OptionSourceData, - opt: { - dimsDef?: DimensionDefinitionLoose[]; - encodeDef?: HashMap | OptionEncode; - dimCount?: number; - encodeDefaulter?: EncodeDefaulter; - generateCoord?: string; - generateCoordCount?: number; - } -): DataDimensionInfo[] { - if (!isSourceInstance(source)) { - source = createSourceFromSeriesDataOption(source as OptionSourceData); - } - - opt = opt || {}; - sysDims = (sysDims || []).slice(); - const dimsDef = (opt.dimsDef || []).slice(); - const dataDimNameMap = createHashMap(); - const coordDimNameMap = createHashMap(); - // let valueCandidate; - const result: DataDimensionInfo[] = []; - - const dimCount = getDimCount(source, sysDims, dimsDef, opt.dimCount); - - // Apply user defined dims (`name` and `type`) and init result. - for (let i = 0; i < dimCount; i++) { - const dimDefItemRaw = dimsDef[i]; - const dimDefItem = dimsDef[i] = extend( - {}, isObject(dimDefItemRaw) ? dimDefItemRaw : { name: dimDefItemRaw } - ); - const userDimName = dimDefItem.name; - const resultItem = result[i] = new DataDimensionInfo(); - // Name will be applied later for avoiding duplication. - if (userDimName != null && dataDimNameMap.get(userDimName) == null) { - // Only if `series.dimensions` is defined in option - // displayName, will be set, and dimension will be diplayed vertically in - // tooltip by default. - resultItem.name = resultItem.displayName = userDimName; - dataDimNameMap.set(userDimName, i); - } - dimDefItem.type != null && (resultItem.type = dimDefItem.type); - dimDefItem.displayName != null && (resultItem.displayName = dimDefItem.displayName); - } - - let encodeDef = opt.encodeDef; - if (!encodeDef && opt.encodeDefaulter) { - encodeDef = opt.encodeDefaulter(source, dimCount); - } - const encodeDefMap = createHashMap(encodeDef as any); - - // Set `coordDim` and `coordDimIndex` by `encodeDefMap` and normalize `encodeDefMap`. - encodeDefMap.each(function (dataDimsRaw, coordDim) { - const dataDims = normalizeToArray(dataDimsRaw as []).slice(); - - // Note: It is allowed that `dataDims.length` is `0`, e.g., options is - // `{encode: {x: -1, y: 1}}`. Should not filter anything in - // this case. - if (dataDims.length === 1 && !isString(dataDims[0]) && dataDims[0] < 0) { - encodeDefMap.set(coordDim, false); - return; - } - - const validDataDims = encodeDefMap.set(coordDim, []) as DimensionIndex[]; - each(dataDims, function (resultDimIdxOrName, idx) { - // The input resultDimIdx can be dim name or index. - const resultDimIdx = isString(resultDimIdxOrName) - ? dataDimNameMap.get(resultDimIdxOrName) - : resultDimIdxOrName; - if (resultDimIdx != null && resultDimIdx < dimCount) { - validDataDims[idx] = resultDimIdx; - applyDim(result[resultDimIdx], coordDim, idx); - } - }); - }); - - // Apply templetes and default order from `sysDims`. - let availDimIdx = 0; - each(sysDims, function (sysDimItemRaw) { - let coordDim: DimensionName; - let sysDimItemDimsDef: CoordDimensionDefinition['dimsDef']; - let sysDimItemOtherDims: CoordDimensionDefinition['otherDims']; - let sysDimItem: CoordDimensionDefinition; - if (isString(sysDimItemRaw)) { - coordDim = sysDimItemRaw; - sysDimItem = {} as CoordDimensionDefinition; - } - else { - sysDimItem = sysDimItemRaw; - coordDim = sysDimItem.name; - const ordinalMeta = sysDimItem.ordinalMeta; - sysDimItem.ordinalMeta = null; - sysDimItem = clone(sysDimItem); - sysDimItem.ordinalMeta = ordinalMeta; - // `coordDimIndex` should not be set directly. - sysDimItemDimsDef = sysDimItem.dimsDef; - sysDimItemOtherDims = sysDimItem.otherDims; - sysDimItem.name = sysDimItem.coordDim = sysDimItem.coordDimIndex = - sysDimItem.dimsDef = sysDimItem.otherDims = null; - } - - let dataDims = encodeDefMap.get(coordDim); - - // negative resultDimIdx means no need to mapping. - if (dataDims === false) { - return; - } - - dataDims = normalizeToArray(dataDims); - - // dimensions provides default dim sequences. - if (!dataDims.length) { - for (let i = 0; i < (sysDimItemDimsDef && sysDimItemDimsDef.length || 1); i++) { - while (availDimIdx < result.length && result[availDimIdx].coordDim != null) { - availDimIdx++; - } - availDimIdx < result.length && dataDims.push(availDimIdx++); - } - } - - // Apply templates. - each(dataDims, function (resultDimIdx, coordDimIndex) { - const resultItem = result[resultDimIdx]; - applyDim(defaults(resultItem, sysDimItem), coordDim, coordDimIndex); - if (resultItem.name == null && sysDimItemDimsDef) { - let sysDimItemDimsDefItem = sysDimItemDimsDef[coordDimIndex]; - !isObject(sysDimItemDimsDefItem) && (sysDimItemDimsDefItem = {name: sysDimItemDimsDefItem}); - resultItem.name = resultItem.displayName = sysDimItemDimsDefItem.name; - resultItem.defaultTooltip = sysDimItemDimsDefItem.defaultTooltip; - } - // FIXME refactor, currently only used in case: {otherDims: {tooltip: false}} - sysDimItemOtherDims && defaults(resultItem.otherDims, sysDimItemOtherDims); - }); - }); - - function applyDim(resultItem: DataDimensionInfo, coordDim: DimensionName, coordDimIndex: DimensionIndex) { - if (VISUAL_DIMENSIONS.get(coordDim as keyof DataVisualDimensions) != null) { - resultItem.otherDims[coordDim as keyof DataVisualDimensions] = coordDimIndex; - } - else { - resultItem.coordDim = coordDim; - resultItem.coordDimIndex = coordDimIndex; - coordDimNameMap.set(coordDim, true); - } - } - - // Make sure the first extra dim is 'value'. - const generateCoord = opt.generateCoord; - let generateCoordCount = opt.generateCoordCount; - const fromZero = generateCoordCount != null; - generateCoordCount = generateCoord ? (generateCoordCount || 1) : 0; - const extra = generateCoord || 'value'; - - // Set dim `name` and other `coordDim` and other props. - for (let resultDimIdx = 0; resultDimIdx < dimCount; resultDimIdx++) { - const resultItem = result[resultDimIdx] = result[resultDimIdx] || new DataDimensionInfo(); - const coordDim = resultItem.coordDim; - - if (coordDim == null) { - resultItem.coordDim = genName( - extra, coordDimNameMap, fromZero - ); - resultItem.coordDimIndex = 0; - if (!generateCoord || generateCoordCount <= 0) { - resultItem.isExtraCoord = true; - } - generateCoordCount--; - } - - resultItem.name == null && (resultItem.name = genName( - resultItem.coordDim, dataDimNameMap, false - )); - - if (resultItem.type == null - && ( - guessOrdinal(source, resultDimIdx) === BE_ORDINAL.Must - // Consider the case: - // { - // dataset: {source: [ - // ['2001', 123], - // ['2002', 456], - // ... - // ['The others', 987], - // ]}, - // series: {type: 'pie'} - // } - // The first colum should better be treated as a "ordinal" although it - // might not able to be detected as an "ordinal" by `guessOrdinal`. - || (resultItem.isExtraCoord - && (resultItem.otherDims.itemName != null - || resultItem.otherDims.seriesName != null - ) - ) - ) - ) { - resultItem.type = 'ordinal'; - } - } - - return result; -} - -// ??? TODO -// Originally detect dimCount by data[0]. Should we -// optimize it to only by sysDims and dimensions and encode. -// So only necessary dims will be initialized. -// But -// (1) custom series should be considered. where other dims -// may be visited. -// (2) sometimes user need to calcualte bubble size or use visualMap -// on other dimensions besides coordSys needed. -// So, dims that is not used by system, should be shared in storage? -function getDimCount( - source: Source, - sysDims: CoordDimensionDefinitionLoose[], - dimsDef: DimensionDefinitionLoose[], - optDimCount: number -): number { - // Note that the result dimCount should not small than columns count - // of data, otherwise `dataDimNameMap` checking will be incorrect. - let dimCount = Math.max( - source.dimensionsDetectedCount || 1, - sysDims.length, - dimsDef.length, - optDimCount || 0 - ); - each(sysDims, function (sysDimItem) { - let sysDimItemDimsDef; - if (isObject(sysDimItem) && (sysDimItemDimsDef = sysDimItem.dimsDef)) { - dimCount = Math.max(dimCount, sysDimItemDimsDef.length); - } - }); - return dimCount; -} - -function genName( - name: DimensionName, - map: HashMap, - fromZero: boolean -): DimensionName { - if (fromZero || map.get(name) != null) { - let i = 0; - while (map.get(name + i) != null) { - i++; - } - name += i; - } - map.set(name, true); - return name; -} - -export default completeDimensions; diff --git a/src/data/helper/createDimensions.ts b/src/data/helper/createDimensions.ts index 102104c5e0..cd44440aa7 100644 --- a/src/data/helper/createDimensions.ts +++ b/src/data/helper/createDimensions.ts @@ -17,20 +17,28 @@ * under the License. */ -/** - * Substitute `completeDimensions`. - * `completeDimensions` is to be deprecated. - */ -import completeDimensions from './completeDimensions'; import { DimensionDefinitionLoose, OptionEncode, OptionEncodeValue, - EncodeDefaulter, OptionSourceData, DimensionName, DimensionDefinition, DataVisualDimensions, DimensionIndex + EncodeDefaulter, + OptionSourceData, + DimensionName, + DimensionDefinition, + DataVisualDimensions, + DimensionIndex, + VISUAL_DIMENSIONS } from '../../util/types'; -import List from '../List'; -import DataDimensionInfo from '../DataDimensionInfo'; -import { HashMap } from 'zrender/src/core/util'; +import SeriesDimensionDefine from '../SeriesDimensionDefine'; +import { + createHashMap, defaults, each, extend, HashMap, isObject, isString +} from 'zrender/src/core/util'; import OrdinalMeta from '../OrdinalMeta'; -import { Source } from '../Source'; +import { createSourceFromSeriesDataOption, isSourceInstance, Source } from '../Source'; +import { CtorInt32Array } from '../DataStore'; +import { normalizeToArray } from '../../util/model'; +import { BE_ORDINAL, guessOrdinal } from './sourceHelper'; +import { + createDimNameMap, ensureSourceDimNameMap, SeriesDataSchema, shouldOmitUnusedDimensions +} from './SeriesDataSchema'; export interface CoordDimensionDefinition extends DimensionDefinition { @@ -42,35 +50,363 @@ export interface CoordDimensionDefinition extends DimensionDefinition { } export type CoordDimensionDefinitionLoose = CoordDimensionDefinition['name'] | CoordDimensionDefinition; -export type CreateDimensionsParams = { +export type PrepareSeriesDataSchemaParams = { coordDimensions?: CoordDimensionDefinitionLoose[], + /** + * Will use `source.dimensionsDefine` if not given. + */ dimensionsDefine?: DimensionDefinitionLoose[], + /** + * Will use `source.encodeDefine` if not given. + */ encodeDefine?: HashMap | OptionEncode, dimensionsCount?: number, + /** + * Make default encode if user not specified. + */ encodeDefaulter?: EncodeDefaulter, generateCoord?: string, - generateCoordCount?: number + generateCoordCount?: number, + + /** + * If be able to omit unused dimension + * Used to improve the performance on high dimension data. + */ + canOmitUnusedDimensions?: boolean }; /** - * @param opt.coordDimensions - * @param opt.dimensionsDefine By default `source.dimensionsDefine` Overwrite source define. - * @param opt.encodeDefine By default `source.encodeDefine` Overwrite source define. - * @param opt.encodeDefaulter Make default encode if user not specified. + * For outside usage compat (like echarts-gl are using it). + */ +export function createDimensions( + source: Source | OptionSourceData, + opt?: PrepareSeriesDataSchemaParams +): SeriesDimensionDefine[] { + return prepareSeriesDataSchema(source, opt).dimensions; +} + +/** + * This method builds the relationship between: + * + "what the coord sys or series requires (see `coordDimensions`)", + * + "what the user defines (in `encode` and `dimensions`, see `opt.dimensionsDefine` and `opt.encodeDefine`)" + * + "what the data source provids (see `source`)". + * + * Some guess strategy will be adapted if user does not define something. + * If no 'value' dimension specified, the first no-named dimension will be + * named as 'value'. + * + * @return The results are always sorted by `storeDimIndex` asc. */ -export default function createDimensions( +export default function prepareSeriesDataSchema( // TODO: TYPE completeDimensions type - source: Source | List | OptionSourceData, - opt?: CreateDimensionsParams -): DataDimensionInfo[] { + source: Source | OptionSourceData, + opt?: PrepareSeriesDataSchemaParams +): SeriesDataSchema { + if (!isSourceInstance(source)) { + source = createSourceFromSeriesDataOption(source as OptionSourceData); + } + opt = opt || {}; - return completeDimensions(opt.coordDimensions || [], source, { - // FIXME:TS detect whether source then call `.dimensionsDefine` and `.encodeDefine`? - dimsDef: opt.dimensionsDefine || (source as Source).dimensionsDefine, - encodeDef: opt.encodeDefine || (source as Source).encodeDefine, - dimCount: opt.dimensionsCount, - encodeDefaulter: opt.encodeDefaulter, - generateCoord: opt.generateCoord, - generateCoordCount: opt.generateCoordCount + + const sysDims = opt.coordDimensions || []; + const dimsDef = opt.dimensionsDefine || source.dimensionsDefine || []; + const coordDimNameMap = createHashMap(); + const resultList: SeriesDimensionDefine[] = []; + const dimCount = getDimCount(source, sysDims, dimsDef, opt.dimensionsCount); + + // Try to ignore unsed dimensions if sharing a high dimension datastore + // 30 is an experience value. + const omitUnusedDimensions = opt.canOmitUnusedDimensions && shouldOmitUnusedDimensions(dimCount); + + const isUsingSourceDimensionsDef = dimsDef === source.dimensionsDefine; + const dataDimNameMap = isUsingSourceDimensionsDef + ? ensureSourceDimNameMap(source) : createDimNameMap(dimsDef); + + let encodeDef = opt.encodeDefine; + if (!encodeDef && opt.encodeDefaulter) { + encodeDef = opt.encodeDefaulter(source, dimCount); + } + const encodeDefMap = createHashMap(encodeDef as any); + + const indicesMap = new CtorInt32Array(dimCount); + for (let i = 0; i < indicesMap.length; i++) { + indicesMap[i] = -1; + } + + function getResultItem(dimIdx: number) { + const idx = indicesMap[dimIdx]; + if (idx < 0) { + const dimDefItemRaw = dimsDef[dimIdx]; + const dimDefItem = isObject(dimDefItemRaw) ? dimDefItemRaw : { name: dimDefItemRaw }; + const resultItem = new SeriesDimensionDefine(); + const userDimName = dimDefItem.name; + if (userDimName != null && dataDimNameMap.get(userDimName) != null) { + // Only if `series.dimensions` is defined in option + // displayName, will be set, and dimension will be diplayed vertically in + // tooltip by default. + resultItem.name = resultItem.displayName = userDimName; + } + dimDefItem.type != null && (resultItem.type = dimDefItem.type); + dimDefItem.displayName != null && (resultItem.displayName = dimDefItem.displayName); + const newIdx = resultList.length; + indicesMap[dimIdx] = newIdx; + resultItem.storeDimIndex = dimIdx; + resultList.push(resultItem); + return resultItem; + } + return resultList[idx]; + } + + if (!omitUnusedDimensions) { + for (let i = 0; i < dimCount; i++) { + getResultItem(i); + } + } + + // Set `coordDim` and `coordDimIndex` by `encodeDefMap` and normalize `encodeDefMap`. + encodeDefMap.each(function (dataDimsRaw, coordDim) { + const dataDims = normalizeToArray(dataDimsRaw as []).slice(); + + // Note: It is allowed that `dataDims.length` is `0`, e.g., options is + // `{encode: {x: -1, y: 1}}`. Should not filter anything in + // this case. + if (dataDims.length === 1 && !isString(dataDims[0]) && dataDims[0] < 0) { + encodeDefMap.set(coordDim, false); + return; + } + + const validDataDims = encodeDefMap.set(coordDim, []) as DimensionIndex[]; + each(dataDims, function (resultDimIdxOrName, idx) { + // The input resultDimIdx can be dim name or index. + const resultDimIdx = isString(resultDimIdxOrName) + ? dataDimNameMap.get(resultDimIdxOrName) + : resultDimIdxOrName; + if (resultDimIdx != null && resultDimIdx < dimCount) { + validDataDims[idx] = resultDimIdx; + applyDim(getResultItem(resultDimIdx), coordDim, idx); + } + }); }); + + // Apply templetes and default order from `sysDims`. + let availDimIdx = 0; + each(sysDims, function (sysDimItemRaw) { + let coordDim: DimensionName; + let sysDimItemDimsDef: CoordDimensionDefinition['dimsDef']; + let sysDimItemOtherDims: CoordDimensionDefinition['otherDims']; + let sysDimItem: CoordDimensionDefinition; + if (isString(sysDimItemRaw)) { + coordDim = sysDimItemRaw; + sysDimItem = {} as CoordDimensionDefinition; + } + else { + sysDimItem = sysDimItemRaw; + coordDim = sysDimItem.name; + const ordinalMeta = sysDimItem.ordinalMeta; + sysDimItem.ordinalMeta = null; + sysDimItem = extend({}, sysDimItem); + sysDimItem.ordinalMeta = ordinalMeta; + // `coordDimIndex` should not be set directly. + sysDimItemDimsDef = sysDimItem.dimsDef; + sysDimItemOtherDims = sysDimItem.otherDims; + sysDimItem.name = sysDimItem.coordDim = sysDimItem.coordDimIndex = + sysDimItem.dimsDef = sysDimItem.otherDims = null; + } + + let dataDims = encodeDefMap.get(coordDim); + + // negative resultDimIdx means no need to mapping. + if (dataDims === false) { + return; + } + + dataDims = normalizeToArray(dataDims); + + // dimensions provides default dim sequences. + if (!dataDims.length) { + for (let i = 0; i < (sysDimItemDimsDef && sysDimItemDimsDef.length || 1); i++) { + while (availDimIdx < dimCount && getResultItem(availDimIdx).coordDim != null) { + availDimIdx++; + } + availDimIdx < dimCount && dataDims.push(availDimIdx++); + } + } + + // Apply templates. + each(dataDims, function (resultDimIdx, coordDimIndex) { + const resultItem = getResultItem(resultDimIdx); + // Coordinate system has a higher priority on dim type than source. + if (isUsingSourceDimensionsDef && sysDimItem.type != null) { + resultItem.type = sysDimItem.type; + } + applyDim(defaults(resultItem, sysDimItem), coordDim, coordDimIndex); + if (resultItem.name == null && sysDimItemDimsDef) { + let sysDimItemDimsDefItem = sysDimItemDimsDef[coordDimIndex]; + !isObject(sysDimItemDimsDefItem) && (sysDimItemDimsDefItem = { + name: sysDimItemDimsDefItem + }); + resultItem.name = resultItem.displayName = sysDimItemDimsDefItem.name; + resultItem.defaultTooltip = sysDimItemDimsDefItem.defaultTooltip; + } + // FIXME refactor, currently only used in case: {otherDims: {tooltip: false}} + sysDimItemOtherDims && defaults(resultItem.otherDims, sysDimItemOtherDims); + }); + }); + + function applyDim(resultItem: SeriesDimensionDefine, coordDim: DimensionName, coordDimIndex: DimensionIndex) { + if (VISUAL_DIMENSIONS.get(coordDim as keyof DataVisualDimensions) != null) { + resultItem.otherDims[coordDim as keyof DataVisualDimensions] = coordDimIndex; + } + else { + resultItem.coordDim = coordDim; + resultItem.coordDimIndex = coordDimIndex; + coordDimNameMap.set(coordDim, true); + } + } + + // Make sure the first extra dim is 'value'. + const generateCoord = opt.generateCoord; + let generateCoordCount = opt.generateCoordCount; + const fromZero = generateCoordCount != null; + generateCoordCount = generateCoord ? (generateCoordCount || 1) : 0; + const extra = generateCoord || 'value'; + + function ifNoNameFillWithCoordName(resultItem: SeriesDimensionDefine): void { + if (resultItem.name == null) { + // Duplication will be removed in the next step. + resultItem.name = resultItem.coordDim; + } + } + + // Set dim `name` and other `coordDim` and other props. + if (!omitUnusedDimensions) { + for (let resultDimIdx = 0; resultDimIdx < dimCount; resultDimIdx++) { + const resultItem = getResultItem(resultDimIdx); + const coordDim = resultItem.coordDim; + + if (coordDim == null) { + // TODO no need to generate coordDim for isExtraCoord? + resultItem.coordDim = genCoordDimName( + extra, coordDimNameMap, fromZero + ); + + resultItem.coordDimIndex = 0; + // Series specified generateCoord is using out. + if (!generateCoord || generateCoordCount <= 0) { + resultItem.isExtraCoord = true; + } + generateCoordCount--; + } + + ifNoNameFillWithCoordName(resultItem); + + if (resultItem.type == null + && ( + guessOrdinal(source, resultDimIdx) === BE_ORDINAL.Must + // Consider the case: + // { + // dataset: {source: [ + // ['2001', 123], + // ['2002', 456], + // ... + // ['The others', 987], + // ]}, + // series: {type: 'pie'} + // } + // The first colum should better be treated as a "ordinal" although it + // might not able to be detected as an "ordinal" by `guessOrdinal`. + || (resultItem.isExtraCoord + && (resultItem.otherDims.itemName != null + || resultItem.otherDims.seriesName != null + ) + ) + ) + ) { + resultItem.type = 'ordinal'; + } + } + } + else { + each(resultList, resultItem => { + // PENDING: guessOrdinal or let user specify type: 'ordinal' manually? + ifNoNameFillWithCoordName(resultItem); + }); + // Sort dimensions: there are some rule that use the last dim as label, + // and for some latter travel process easier. + resultList.sort((item0, item1) => item0.storeDimIndex - item1.storeDimIndex); + } + + removeDuplication(resultList); + + return new SeriesDataSchema({ + source, + dimensions: resultList, + fullDimensionCount: dimCount, + dimensionOmitted: omitUnusedDimensions + }); +} + +function removeDuplication(result: SeriesDimensionDefine[]) { + const duplicationMap = createHashMap(); + for (let i = 0; i < result.length; i++) { + const dim = result[i]; + const dimOriginalName = dim.name; + let count = duplicationMap.get(dimOriginalName) || 0; + if (count > 0) { + // Starts from 0. + dim.name = dimOriginalName + (count - 1); + } + count++; + duplicationMap.set(dimOriginalName, count); + } +} + +// ??? TODO +// Originally detect dimCount by data[0]. Should we +// optimize it to only by sysDims and dimensions and encode. +// So only necessary dims will be initialized. +// But +// (1) custom series should be considered. where other dims +// may be visited. +// (2) sometimes user need to calcualte bubble size or use visualMap +// on other dimensions besides coordSys needed. +// So, dims that is not used by system, should be shared in data store? +function getDimCount( + source: Source, + sysDims: CoordDimensionDefinitionLoose[], + dimsDef: DimensionDefinitionLoose[], + optDimCount?: number +): number { + // Note that the result dimCount should not small than columns count + // of data, otherwise `dataDimNameMap` checking will be incorrect. + let dimCount = Math.max( + source.dimensionsDetectedCount || 1, + sysDims.length, + dimsDef.length, + optDimCount || 0 + ); + each(sysDims, function (sysDimItem) { + let sysDimItemDimsDef; + if (isObject(sysDimItem) && (sysDimItemDimsDef = sysDimItem.dimsDef)) { + dimCount = Math.max(dimCount, sysDimItemDimsDef.length); + } + }); + return dimCount; +} + +function genCoordDimName( + name: DimensionName, + map: HashMap, + fromZero: boolean +) { + const mapData = map.data; + if (fromZero || mapData.hasOwnProperty(name)) { + let i = 0; + while (mapData.hasOwnProperty(name + i)) { + i++; + } + name += i; + } + map.set(name, true); + return name; } diff --git a/src/data/helper/dataProvider.ts b/src/data/helper/dataProvider.ts index 6135a9d44c..37247edb2c 100644 --- a/src/data/helper/dataProvider.ts +++ b/src/data/helper/dataProvider.ts @@ -34,9 +34,9 @@ import { SERIES_LAYOUT_BY_COLUMN, SERIES_LAYOUT_BY_ROW, DimensionName, DimensionIndex, OptionSourceData, - DimensionIndexLoose, OptionDataItem, OptionDataValue, SourceFormat, SeriesLayoutBy, ParsedValue + OptionDataItem, OptionDataValue, SourceFormat, SeriesLayoutBy, ParsedValue, DimensionLoose, NullUndefined } from '../../util/types'; -import List from '../List'; +import SeriesData from '../SeriesData'; export interface DataProvider { /** @@ -136,7 +136,7 @@ export class DefaultDataProvider implements DataProvider { return 0; } - getItem(idx: number, out?: ArrayLike): OptionDataItem { + getItem(idx: number, out?: ArrayLike): OptionDataItem { return; } @@ -291,8 +291,11 @@ type RawSourceItemGetter = ( rawData: OptionSourceData, startIndex: number, dimsDef: { name?: DimensionName }[], - idx: number -) => OptionDataItem; + idx: number, + // Only used in SOURCE_FORMAT_ARRAY_ROWS + '_' + SERIES_LAYOUT_BY_ROW and SOURCE_FORMAT_KEYED_COLUMNS + // to avoid create a new [] if `out` is provided. + out?: ArrayLike +) => OptionDataItem | ArrayLike; const getItemSimply: RawSourceItemGetter = function ( rawData, startIndex, dimsDef, idx @@ -303,26 +306,26 @@ const getItemSimply: RawSourceItemGetter = function ( const rawSourceItemGetterMap: Dictionary = { [SOURCE_FORMAT_ARRAY_ROWS + '_' + SERIES_LAYOUT_BY_COLUMN]: function ( rawData, startIndex, dimsDef, idx - ): OptionDataValue[] { + ) { return (rawData as OptionDataValue[][])[idx + startIndex]; }, [SOURCE_FORMAT_ARRAY_ROWS + '_' + SERIES_LAYOUT_BY_ROW]: function ( - rawData, startIndex, dimsDef, idx - ): OptionDataValue[] { + rawData, startIndex, dimsDef, idx, out + ) { idx += startIndex; - const item = []; + const item = out || []; const data = rawData as OptionDataValue[][]; for (let i = 0; i < data.length; i++) { const row = data[i]; - item.push(row ? row[idx] : null); + item[i] = row ? row[idx] : null; } return item; }, [SOURCE_FORMAT_OBJECT_ROWS]: getItemSimply, [SOURCE_FORMAT_KEYED_COLUMNS]: function ( - rawData, startIndex, dimsDef, idx - ): OptionDataValue[] { - const item = []; + rawData, startIndex, dimsDef, idx, out + ) { + const item = out || []; for (let i = 0; i < dimsDef.length; i++) { const dimName = dimsDef[i].name; if (__DEV__) { @@ -331,7 +334,7 @@ const rawSourceItemGetterMap: Dictionary = { } } const col = (rawData as Dictionary)[dimName]; - item.push(col ? col[idx] : null); + item[i] = col ? col[idx] : null; } return item; }, @@ -343,7 +346,7 @@ export function getRawSourceItemGetter( ): RawSourceItemGetter { const method = rawSourceItemGetterMap[getMethodMapKey(sourceFormat, seriesLayoutBy)]; if (__DEV__) { - assert(method, 'Do not suppport get item on "' + sourceFormat + '", "' + seriesLayoutBy + '".'); + assert(method, 'Do not support get item on "' + sourceFormat + '", "' + seriesLayoutBy + '".'); } return method; } @@ -402,42 +405,37 @@ export function getRawSourceDataCounter( } - -// TODO -// merge it to dataProvider? type RawSourceValueGetter = ( dataItem: OptionDataItem, dimIndex: DimensionIndex, - dimName: DimensionName - // If dimIndex is null/undefined, return OptionDataItem. - // Otherwise, return OptionDataValue. -) => OptionDataValue | OptionDataItem; + property: DimensionName +) => OptionDataValue; const getRawValueSimply = function ( - dataItem: ArrayLike, dimIndex: number, dimName: string -): OptionDataValue | ArrayLike { - return dimIndex != null ? dataItem[dimIndex] : dataItem; + dataItem: ArrayLike, dimIndex: number, property: string +): OptionDataValue { + return dataItem[dimIndex]; }; -const rawSourceValueGetterMap: {[sourceFormat: string]: RawSourceValueGetter} = { +const rawSourceValueGetterMap: Partial> = { [SOURCE_FORMAT_ARRAY_ROWS]: getRawValueSimply, [SOURCE_FORMAT_OBJECT_ROWS]: function ( - dataItem: Dictionary, dimIndex: number, dimName: string - ): OptionDataValue | Dictionary { - return dimIndex != null ? dataItem[dimName] : dataItem; + dataItem: Dictionary, dimIndex: number, property: string + ): OptionDataValue { + return dataItem[property]; }, [SOURCE_FORMAT_KEYED_COLUMNS]: getRawValueSimply, [SOURCE_FORMAT_ORIGINAL]: function ( - dataItem: OptionDataItem, dimIndex: number, dimName: string - ): OptionDataValue | OptionDataItem { + dataItem: OptionDataItem, dimIndex: number, property: string + ): OptionDataValue { // FIXME: In some case (markpoint in geo (geo-map.html)), // dataItem is {coord: [...]} const value = getDataItemValue(dataItem); - return (dimIndex == null || !(value instanceof Array)) + return !(value instanceof Array) ? value : value[dimIndex]; }, @@ -469,9 +467,10 @@ function getMethodMapKey(sourceFormat: SourceFormat, seriesLayoutBy: SeriesLayou // value may be 0.91000000001, which have brings trouble to display. // TODO: consider how to treat null/undefined/NaN when display? export function retrieveRawValue( - data: List, dataIndex: number, dim?: DimensionName | DimensionIndexLoose + data: SeriesData, dataIndex: number, // If dimIndex is null/undefined, return OptionDataItem. // Otherwise, return OptionDataValue. + dim?: DimensionLoose | NullUndefined ): OptionDataValue | OptionDataItem { if (!data) { return; @@ -484,17 +483,22 @@ export function retrieveRawValue( return; } - const sourceFormat = data.getProvider().getSource().sourceFormat; - let dimName; - let dimIndex; + const store = data.getStore(); + const sourceFormat = store.getSource().sourceFormat; - const dimInfo = data.getDimensionInfo(dim); - if (dimInfo) { - dimName = dimInfo.name; - dimIndex = dimInfo.index; - } + if (dim != null) { + const dimIndex = data.getDimensionIndex(dim); + const property = store.getDimensionProperty(dimIndex); - return getRawSourceValueGetter(sourceFormat)(dataItem, dimIndex, dimName); + return getRawSourceValueGetter(sourceFormat)(dataItem, dimIndex, property); + } + else { + let result = dataItem; + if (sourceFormat === SOURCE_FORMAT_ORIGINAL) { + result = getDataItemValue(dataItem); + } + return result; + } } @@ -510,12 +514,12 @@ export function retrieveRawValue( * @param dataIndex * @param attr like 'selected' */ -export function retrieveRawAttr(data: List, dataIndex: number, attr: string): any { +export function retrieveRawAttr(data: SeriesData, dataIndex: number, attr: string): any { if (!data) { return; } - const sourceFormat = data.getProvider().getSource().sourceFormat; + const sourceFormat = data.getStore().getSource().sourceFormat; if (sourceFormat !== SOURCE_FORMAT_ORIGINAL && sourceFormat !== SOURCE_FORMAT_OBJECT_ROWS diff --git a/src/data/helper/dataStackHelper.ts b/src/data/helper/dataStackHelper.ts index cef669caf3..deb3e3e918 100644 --- a/src/data/helper/dataStackHelper.ts +++ b/src/data/helper/dataStackHelper.ts @@ -18,11 +18,21 @@ */ import {each, isString} from 'zrender/src/core/util'; -import DataDimensionInfo from '../DataDimensionInfo'; +import SeriesDimensionDefine from '../SeriesDimensionDefine'; import SeriesModel from '../../model/Series'; -import List, { DataCalculationInfo } from '../List'; +import SeriesData, { DataCalculationInfo } from '../SeriesData'; import type { SeriesOption, SeriesStackOptionMixin, DimensionName } from '../../util/types'; - +import { isSeriesDataSchema, SeriesDataSchema } from './SeriesDataSchema'; +import DataStore from '../DataStore'; + +type EnableDataStackDimensionsInput = { + schema: SeriesDataSchema; + // If given, stack dimension will be ensured on this store. + // Otherwise, stack dimesnion will be appended at the tail, and should not + // be used on a shared store, but should create a brand new stroage later. + store?: DataStore; +}; +type EnableDataStackDimensionsInputLegacy = (SeriesDimensionDefine | string)[]; /** * Note that it is too complicated to support 3d stack by value @@ -30,8 +40,8 @@ import type { SeriesOption, SeriesStackOptionMixin, DimensionName } from '../../ * we just support that stacked by index. * * @param seriesModel - * @param dimensionInfoList The same as the input of . - * The input dimensionInfoList will be modified. + * @param dimensionsInput The same as the input of . + * The input will be modified. * @param opt * @param opt.stackedCoordDimension Specify a coord dimension if needed. * @param opt.byIndex=false @@ -46,8 +56,9 @@ import type { SeriesOption, SeriesStackOptionMixin, DimensionName } from '../../ */ export function enableDataStack( seriesModel: SeriesModel, - dimensionInfoList: (DataDimensionInfo | string)[], + dimensionsInput: EnableDataStackDimensionsInput | EnableDataStackDimensionsInputLegacy, opt?: { + // Backward compat stackedCoordDimension?: string byIndex?: boolean } @@ -63,18 +74,31 @@ export function enableDataStack( let byIndex = opt.byIndex; const stackedCoordDimension = opt.stackedCoordDimension; + let dimensionDefineList: EnableDataStackDimensionsInputLegacy; + let schema: SeriesDataSchema; + let store: DataStore; + + if (isLegacyDimensionsInput(dimensionsInput)) { + dimensionDefineList = dimensionsInput; + } + else { + schema = dimensionsInput.schema; + dimensionDefineList = schema.dimensions; + store = dimensionsInput.store; + } + // Compatibal: when `stack` is set as '', do not stack. const mayStack = !!(seriesModel && seriesModel.get('stack')); - let stackedByDimInfo: DataDimensionInfo; - let stackedDimInfo: DataDimensionInfo; + let stackedByDimInfo: SeriesDimensionDefine; + let stackedDimInfo: SeriesDimensionDefine; let stackResultDimension: string; let stackedOverDimension: string; - each(dimensionInfoList, function (dimensionInfo, index) { + each(dimensionDefineList, function (dimensionInfo, index) { if (isString(dimensionInfo)) { - dimensionInfoList[index] = dimensionInfo = { + dimensionDefineList[index] = dimensionInfo = { name: dimensionInfo as string - } as DataDimensionInfo; + } as SeriesDimensionDefine; } if (mayStack && !dimensionInfo.isExtraCoord) { @@ -104,8 +128,10 @@ export function enableDataStack( // might not be a good way. if (stackedDimInfo) { // Use a weird name that not duplicated with other names. - stackResultDimension = '__\0ecstackresult'; - stackedOverDimension = '__\0ecstackedover'; + // Also need to use seriesModel.id as postfix because different + // series may share same data store. The stack dimension needs to be distinguished. + stackResultDimension = '__\0ecstackresult_' + seriesModel.id; + stackedOverDimension = '__\0ecstackedover_' + seriesModel.id; // Create inverted index to fast query index by value. if (stackedByDimInfo) { @@ -116,33 +142,49 @@ export function enableDataStack( const stackedDimType = stackedDimInfo.type; let stackedDimCoordIndex = 0; - each(dimensionInfoList, function (dimensionInfo: DataDimensionInfo) { + each(dimensionDefineList, function (dimensionInfo: SeriesDimensionDefine) { if (dimensionInfo.coordDim === stackedDimCoordDim) { stackedDimCoordIndex++; } }); - dimensionInfoList.push({ + const stackedOverDimensionDefine: SeriesDimensionDefine = { name: stackResultDimension, coordDim: stackedDimCoordDim, coordDimIndex: stackedDimCoordIndex, type: stackedDimType, isExtraCoord: true, - isCalculationCoord: true - }); + isCalculationCoord: true, + storeDimIndex: dimensionDefineList.length + }; - stackedDimCoordIndex++; - - dimensionInfoList.push({ + const stackResultDimensionDefine: SeriesDimensionDefine = { name: stackedOverDimension, // This dimension contains stack base (generally, 0), so do not set it as // `stackedDimCoordDim` to avoid extent calculation, consider log scale. coordDim: stackedOverDimension, - coordDimIndex: stackedDimCoordIndex, + coordDimIndex: stackedDimCoordIndex + 1, type: stackedDimType, isExtraCoord: true, - isCalculationCoord: true - }); + isCalculationCoord: true, + storeDimIndex: dimensionDefineList.length + 1 + }; + + if (schema) { + if (store) { + stackedOverDimensionDefine.storeDimIndex = + store.ensureCalculationDimension(stackedOverDimension, stackedDimType); + stackResultDimensionDefine.storeDimIndex = + store.ensureCalculationDimension(stackResultDimension, stackedDimType); + } + + schema.appendCalculationDimension(stackedOverDimensionDefine); + schema.appendCalculationDimension(stackResultDimensionDefine); + } + else { + dimensionDefineList.push(stackedOverDimensionDefine); + dimensionDefineList.push(stackResultDimensionDefine); + } } return { @@ -154,18 +196,19 @@ export function enableDataStack( }; } -export function isDimensionStacked(data: List, stackedDim: string /*, stackedByDim*/): boolean { +function isLegacyDimensionsInput( + dimensionsInput: Parameters[1] +): dimensionsInput is EnableDataStackDimensionsInputLegacy { + return !isSeriesDataSchema((dimensionsInput as EnableDataStackDimensionsInput).schema); +} + +export function isDimensionStacked(data: SeriesData, stackedDim: string): boolean { // Each single series only maps to one pair of axis. So we do not need to // check stackByDim, whatever stacked by a dimension or stacked by index. return !!stackedDim && stackedDim === data.getCalculationInfo('stackedDimension'); - // && ( - // stackedByDim != null - // ? stackedByDim === data.getCalculationInfo('stackedByDimension') - // : data.getCalculationInfo('isStackedByIndex') - // ); } -export function getStackedDimension(data: List, targetDim: string): DimensionName { +export function getStackedDimension(data: SeriesData, targetDim: string): DimensionName { return isDimensionStacked(data, targetDim) ? data.getCalculationInfo('stackResultDimension') : targetDim; diff --git a/src/data/helper/dataValueHelper.ts b/src/data/helper/dataValueHelper.ts index 98e36c86d4..1c10798c76 100644 --- a/src/data/helper/dataValueHelper.ts +++ b/src/data/helper/dataValueHelper.ts @@ -18,7 +18,6 @@ */ import { ParsedValue, DimensionType } from '../../util/types'; -import OrdinalMeta from '../OrdinalMeta'; import { parseDate, numericToNumber } from '../../util/number'; import { createHashMap, trim, hasOwn } from 'zrender/src/core/util'; import { throwError } from '../../util/log'; @@ -40,18 +39,14 @@ export function parseDataValue( // will be parsed to NaN if do not set `type` as 'ordinal'. It has been // the logic in `List.ts` for long time. Follow the same way if you need // to get same result as List did from a raw value. - type?: DimensionType, - ordinalMeta?: OrdinalMeta + type?: DimensionType } ): ParsedValue { // Performance sensitive. const dimType = opt && opt.type; if (dimType === 'ordinal') { // If given value is a category string - const ordinalMeta = opt && opt.ordinalMeta; - return ordinalMeta - ? ordinalMeta.parseAndCollect(value) - : value; + return value; } if (dimType === 'time' diff --git a/src/data/helper/dimensionHelper.ts b/src/data/helper/dimensionHelper.ts index b38062dbf3..e4d9899ca0 100644 --- a/src/data/helper/dimensionHelper.ts +++ b/src/data/helper/dimensionHelper.ts @@ -18,11 +18,13 @@ */ -import {each, createHashMap, assert} from 'zrender/src/core/util'; -import List, { ListDimensionType } from '../List'; +import {each, createHashMap, assert, map} from 'zrender/src/core/util'; +import SeriesData from '../SeriesData'; import { - DimensionName, VISUAL_DIMENSIONS, DimensionType, DimensionUserOuput, DimensionUserOuputEncode, DimensionIndex + DimensionName, VISUAL_DIMENSIONS, DimensionType, DimensionIndex } from '../../util/types'; +import { DataStoreDimensionType } from '../DataStore'; +import { SeriesDataSchema } from './SeriesDataSchema'; export type DimensionSummaryEncode = { defaultedLabel: DimensionName[], @@ -37,21 +39,68 @@ export type DimensionSummary = { userOutput: DimensionUserOuput, // All of the data dim names that mapped by coordDim. dataDimsOnCoord: DimensionName[], + dataDimIndicesOnCoord: DimensionIndex[], encodeFirstDimNotExtra: {[coordDim: string]: DimensionName}, }; -export function summarizeDimensions(data: List): DimensionSummary { +export type DimensionUserOuputEncode = { + // index: coordDimIndex, value: dataDimIndex + [coordOrVisualDimName: string]: DimensionIndex[] +}; + +class DimensionUserOuput { + private _encode: DimensionUserOuputEncode; + private _cachedDimNames: DimensionName[]; + private _schema?: SeriesDataSchema; + + constructor( + encode: DimensionUserOuputEncode, + dimRequest?: SeriesDataSchema + ) { + this._encode = encode; + this._schema = dimRequest; + } + + get(): { + fullDimensions: DimensionName[]; + encode: DimensionUserOuputEncode; + } { + return { + // Do not generate full dimension name until fist used. + fullDimensions: this._getFullDimensionNames(), + encode: this._encode + }; + } + + /** + * Get all data store dimension names. + * Theoretically a series data store is defined both by series and used dataset (if any). + * If some dimensions are omitted for performance reason in `this.dimensions`, + * the dimension name may not be auto-generated if user does not specify a dimension name. + * In this case, the dimension name is `null`/`undefined`. + */ + private _getFullDimensionNames(): DimensionName[] { + if (!this._cachedDimNames) { + this._cachedDimNames = this._schema + ? this._schema.makeOutputDimensionNames() + : []; + } + return this._cachedDimNames; + } +}; + + +export function summarizeDimensions( + data: SeriesData, + schema?: SeriesDataSchema +): DimensionSummary { const summary: DimensionSummary = {} as DimensionSummary; const encode = summary.encode = {} as DimensionSummaryEncode; const notExtraCoordDimMap = createHashMap<1, DimensionName>(); let defaultedLabel = [] as DimensionName[]; let defaultedTooltip = [] as DimensionName[]; - // See the comment of `List.js#userOutput`. - const userOutput = summary.userOutput = { - dimensionNames: data.dimensions.slice(), - encode: {} - }; + const userOutputEncode = {} as DimensionUserOuputEncode; each(data.dimensions, function (dimName) { const dimItem = data.getDimensionInfo(dimName); @@ -78,7 +127,8 @@ export function summarizeDimensions(data: List): DimensionSummary { // User output encode do not contain generated coords. // And it only has index. User can use index to retrieve value from the raw item array. - getOrCreateEncodeArr(userOutput.encode, coordDim)[coordDimIndex] = dimItem.index; + getOrCreateEncodeArr(userOutputEncode, coordDim)[coordDimIndex] = + data.getDimensionIndex(dimItem.name); } if (dimItem.defaultTooltip) { defaultedTooltip.push(dimName); @@ -107,6 +157,9 @@ export function summarizeDimensions(data: List): DimensionSummary { }); summary.dataDimsOnCoord = dataDimsOnCoord; + summary.dataDimIndicesOnCoord = map( + dataDimsOnCoord, dimName => data.getDimensionInfo(dimName).storeDimIndex + ); summary.encodeFirstDimNotExtra = encodeFirstDimNotExtra; const encodeLabel = encode.label; @@ -127,6 +180,8 @@ export function summarizeDimensions(data: List): DimensionSummary { encode.defaultedLabel = defaultedLabel; encode.defaultedTooltip = defaultedTooltip; + summary.userOutput = new DimensionUserOuput(userOutputEncode, schema); + return summary; } @@ -140,7 +195,7 @@ function getOrCreateEncodeArr( } // FIXME:TS should be type `AxisType` -export function getDimensionTypeByAxis(axisType: string): ListDimensionType { +export function getDimensionTypeByAxis(axisType: string): DataStoreDimensionType { return axisType === 'category' ? 'ordinal' : axisType === 'time' diff --git a/src/data/helper/linkList.ts b/src/data/helper/linkList.ts index a7cecc484c..61a926bd8b 100644 --- a/src/data/helper/linkList.ts +++ b/src/data/helper/linkList.ts @@ -17,170 +17,6 @@ * under the License. */ - -/** - * Link lists and struct (graph or tree) - */ - -import { curry, each, assert, extend, map, keys } from 'zrender/src/core/util'; -import List from '../List'; -import { makeInner } from '../../util/model'; -import { SeriesDataType } from '../../util/types'; - -// That is: { dataType: data }, -// like: { node: nodeList, edge: edgeList }. -// Should contain mainData. -type Datas = { [key in SeriesDataType]?: List }; -type StructReferDataAttr = 'data' | 'edgeData'; -type StructAttr = 'tree' | 'graph'; - -const inner = makeInner<{ - datas: Datas; - mainData: List; -}, List>(); - - -// Caution: -// In most case, either list or its shallow clones (see list.cloneShallow) -// is active in echarts process. So considering heap memory consumption, -// we do not clone tree or graph, but share them among list and its shallow clones. -// But in some rare case, we have to keep old list (like do animation in chart). So -// please take care that both the old list and the new list share the same tree/graph. - -type LinkListOpt = { - mainData: List; - // For example, instance of Graph or Tree. - struct: { - update: () => void; - } & { - [key in StructReferDataAttr]?: List - }; - // Will designate: `mainData[structAttr] = struct;` - structAttr: StructAttr; - datas?: Datas; - // { dataType: attr }, - // Will designate: `struct[datasAttr[dataType]] = list;` - datasAttr?: { [key in SeriesDataType]?: StructReferDataAttr }; -}; - -function linkList(opt: LinkListOpt): void { - const mainData = opt.mainData; - let datas = opt.datas; - - if (!datas) { - datas = { main: mainData }; - opt.datasAttr = { main: 'data' }; - } - opt.datas = opt.mainData = null; - - linkAll(mainData, datas, opt); - - // Porxy data original methods. - each(datas, function (data: List) { - each(mainData.TRANSFERABLE_METHODS, function (methodName) { - data.wrapMethod(methodName, curry(transferInjection, opt)); - }); - }); - - // Beyond transfer, additional features should be added to `cloneShallow`. - mainData.wrapMethod('cloneShallow', curry(cloneShallowInjection, opt)); - - // Only mainData trigger change, because struct.update may trigger - // another changable methods, which may bring about dead lock. - each(mainData.CHANGABLE_METHODS, function (methodName) { - mainData.wrapMethod(methodName, curry(changeInjection, opt)); - }); - - // Make sure datas contains mainData. - assert(datas[mainData.dataType] === mainData); -} - -function transferInjection(this: List, opt: LinkListOpt, res: List): unknown { - if (isMainData(this)) { - // Transfer datas to new main data. - const datas = extend({}, inner(this).datas); - datas[this.dataType] = res; - linkAll(res, datas, opt); - } - else { - // Modify the reference in main data to point newData. - linkSingle(res, this.dataType, inner(this).mainData, opt); - } - return res; -} - -function changeInjection(opt: LinkListOpt, res: unknown): unknown { - opt.struct && opt.struct.update(); - return res; -} - -function cloneShallowInjection(opt: LinkListOpt, res: List): List { - // cloneShallow, which brings about some fragilities, may be inappropriate - // to be exposed as an API. So for implementation simplicity we can make - // the restriction that cloneShallow of not-mainData should not be invoked - // outside, but only be invoked here. - each(inner(res).datas, function (data: List, dataType) { - data !== res && linkSingle(data.cloneShallow(), dataType, res, opt); - }); - return res; -} - -/** - * Supplement method to List. - * - * @public - * @param [dataType] If not specified, return mainData. - */ -function getLinkedData(this: List, dataType?: SeriesDataType): List { - const mainData = inner(this).mainData; - return (dataType == null || mainData == null) - ? mainData - : inner(mainData).datas[dataType]; -} - -/** - * Get list of all linked data - */ -function getLinkedDataAll(this: List): { - data: List, - type?: SeriesDataType -}[] { - const mainData = inner(this).mainData; - return (mainData == null) - ? [{ data: mainData }] - : map(keys(inner(mainData).datas), function (type) { - return { - type, - data: inner(mainData).datas[type] - }; - }); -} - -function isMainData(data: List): boolean { - return inner(data).mainData === data; -} - -function linkAll(mainData: List, datas: Datas, opt: LinkListOpt): void { - inner(mainData).datas = {}; - each(datas, function (data: List, dataType) { - linkSingle(data, dataType, mainData, opt); - }); -} - -function linkSingle(data: List, dataType: SeriesDataType, mainData: List, opt: LinkListOpt): void { - inner(mainData).datas[dataType] = data; - inner(data).mainData = mainData; - - data.dataType = dataType; - - if (opt.struct) { - data[opt.structAttr] = opt.struct as any; - opt.struct[opt.datasAttr[dataType]] = data; - } - - // Supplement method. - data.getLinkedData = getLinkedData; - data.getLinkedDataAll = getLinkedDataAll; -} - -export default linkList; +// TODO: this module is only for compatibility with echarts-gl +import linkSeriesData from './linkSeriesData'; +export default linkSeriesData; \ No newline at end of file diff --git a/src/data/helper/linkSeriesData.ts b/src/data/helper/linkSeriesData.ts new file mode 100644 index 0000000000..2b767cf427 --- /dev/null +++ b/src/data/helper/linkSeriesData.ts @@ -0,0 +1,186 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + + +/** + * Link lists and struct (graph or tree) + */ + +import { curry, each, assert, extend, map, keys } from 'zrender/src/core/util'; +import SeriesData from '../SeriesData'; +import { makeInner } from '../../util/model'; +import { SeriesDataType } from '../../util/types'; + +// That is: { dataType: data }, +// like: { node: nodeList, edge: edgeList }. +// Should contain mainData. +type Datas = { [key in SeriesDataType]?: SeriesData }; +type StructReferDataAttr = 'data' | 'edgeData'; +type StructAttr = 'tree' | 'graph'; + +const inner = makeInner<{ + datas: Datas; + mainData: SeriesData; +}, SeriesData>(); + + +// Caution: +// In most case, either seriesData or its shallow clones (see seriesData.cloneShallow) +// is active in echarts process. So considering heap memory consumption, +// we do not clone tree or graph, but share them among seriesData and its shallow clones. +// But in some rare case, we have to keep old seriesData (like do animation in chart). So +// please take care that both the old seriesData and the new seriesData share the same tree/graph. + +type LinkSeriesDataOpt = { + mainData: SeriesData; + // For example, instance of Graph or Tree. + struct: { + update: () => void; + } & { + [key in StructReferDataAttr]?: SeriesData + }; + // Will designate: `mainData[structAttr] = struct;` + structAttr: StructAttr; + datas?: Datas; + // { dataType: attr }, + // Will designate: `struct[datasAttr[dataType]] = list;` + datasAttr?: { [key in SeriesDataType]?: StructReferDataAttr }; +}; + +function linkSeriesData(opt: LinkSeriesDataOpt): void { + const mainData = opt.mainData; + let datas = opt.datas; + + if (!datas) { + datas = { main: mainData }; + opt.datasAttr = { main: 'data' }; + } + opt.datas = opt.mainData = null; + + linkAll(mainData, datas, opt); + + // Porxy data original methods. + each(datas, function (data: SeriesData) { + each(mainData.TRANSFERABLE_METHODS, function (methodName) { + data.wrapMethod(methodName, curry(transferInjection, opt)); + }); + }); + + // Beyond transfer, additional features should be added to `cloneShallow`. + mainData.wrapMethod('cloneShallow', curry(cloneShallowInjection, opt)); + + // Only mainData trigger change, because struct.update may trigger + // another changable methods, which may bring about dead lock. + each(mainData.CHANGABLE_METHODS, function (methodName) { + mainData.wrapMethod(methodName, curry(changeInjection, opt)); + }); + + // Make sure datas contains mainData. + assert(datas[mainData.dataType] === mainData); +} + +function transferInjection(this: SeriesData, opt: LinkSeriesDataOpt, res: SeriesData): unknown { + if (isMainData(this)) { + // Transfer datas to new main data. + const datas = extend({}, inner(this).datas); + datas[this.dataType] = res; + linkAll(res, datas, opt); + } + else { + // Modify the reference in main data to point newData. + linkSingle(res, this.dataType, inner(this).mainData, opt); + } + return res; +} + +function changeInjection(opt: LinkSeriesDataOpt, res: unknown): unknown { + opt.struct && opt.struct.update(); + return res; +} + +function cloneShallowInjection(opt: LinkSeriesDataOpt, res: SeriesData): SeriesData { + // cloneShallow, which brings about some fragilities, may be inappropriate + // to be exposed as an API. So for implementation simplicity we can make + // the restriction that cloneShallow of not-mainData should not be invoked + // outside, but only be invoked here. + each(inner(res).datas, function (data: SeriesData, dataType) { + data !== res && linkSingle(data.cloneShallow(), dataType, res, opt); + }); + return res; +} + +/** + * Supplement method to List. + * + * @public + * @param [dataType] If not specified, return mainData. + */ +function getLinkedData(this: SeriesData, dataType?: SeriesDataType): SeriesData { + const mainData = inner(this).mainData; + return (dataType == null || mainData == null) + ? mainData + : inner(mainData).datas[dataType]; +} + +/** + * Get list of all linked data + */ +function getLinkedDataAll(this: SeriesData): { + data: SeriesData, + type?: SeriesDataType +}[] { + const mainData = inner(this).mainData; + return (mainData == null) + ? [{ data: mainData }] + : map(keys(inner(mainData).datas), function (type) { + return { + type, + data: inner(mainData).datas[type] + }; + }); +} + +function isMainData(data: SeriesData): boolean { + return inner(data).mainData === data; +} + +function linkAll(mainData: SeriesData, datas: Datas, opt: LinkSeriesDataOpt): void { + inner(mainData).datas = {}; + each(datas, function (data: SeriesData, dataType) { + linkSingle(data, dataType, mainData, opt); + }); +} + +function linkSingle(data: SeriesData, dataType: SeriesDataType, mainData: SeriesData, opt: LinkSeriesDataOpt): void { + inner(mainData).datas[dataType] = data; + inner(data).mainData = mainData; + + data.dataType = dataType; + + if (opt.struct) { + data[opt.structAttr] = opt.struct as any; + opt.struct[opt.datasAttr[dataType]] = data; + } + + // Supplement method. + data.getLinkedData = getLinkedData; + data.getLinkedDataAll = getLinkedDataAll; +} + +export default linkSeriesData; diff --git a/src/data/helper/sourceManager.ts b/src/data/helper/sourceManager.ts index bf3f1ece88..019b317f4e 100644 --- a/src/data/helper/sourceManager.ts +++ b/src/data/helper/sourceManager.ts @@ -19,18 +19,25 @@ import { DatasetModel } from '../../component/dataset/install'; import SeriesModel from '../../model/Series'; -import { setAsPrimitive, map, isTypedArray, assert, each, retrieve2 } from 'zrender/src/core/util'; +import { + setAsPrimitive, map, isTypedArray, assert, each, retrieve2 +} from 'zrender/src/core/util'; import { SourceMetaRawOption, Source, createSource, cloneSourceShallow } from '../Source'; import { SeriesEncodableModel, OptionSourceData, SOURCE_FORMAT_TYPED_ARRAY, SOURCE_FORMAT_ORIGINAL, - SourceFormat, SeriesLayoutBy, OptionSourceHeader, DimensionDefinitionLoose + SourceFormat, SeriesLayoutBy, OptionSourceHeader, + DimensionDefinitionLoose, Dictionary } from '../../util/types'; import { querySeriesUpstreamDatasetModel, queryDatasetUpstreamDatasetModels } from './sourceHelper'; import { applyDataTransform } from './transform'; +import DataStore, { DataStoreDimensionDefine } from '../DataStore'; +import { DefaultDataProvider } from './dataProvider'; +import { SeriesDataSchema } from './SeriesDataSchema'; +type DataStoreMap = Dictionary; /** * [REQUIREMENT_MEMO]: @@ -131,11 +138,15 @@ export class SourceManager { // Cached source. Do not repeat calculating if not dirty. private _sourceList: Source[] = []; + private _storeList: DataStoreMap[] = []; + // version sign of each upstream source manager. private _upstreamSignList: string[] = []; private _versionSignBase = 0; + private _dirty = true; + constructor(sourceHost: DatasetModel | SeriesModel) { this._sourceHost = sourceHost; } @@ -145,6 +156,8 @@ export class SourceManager { */ dirty() { this._setLocalSource([], []); + this._storeList = []; + this._dirty = true; } private _setLocalSource( @@ -175,11 +188,13 @@ export class SourceManager { // cache the result source to prevent from repeating transform. if (this._isDirty()) { this._createSource(); + this._dirty = false; } } private _createSource(): void { this._setLocalSource([], []); + const sourceHost = this._sourceHost; const upSourceMgrList = this._getUpstreamSourceManagers(); @@ -211,30 +226,25 @@ export class SourceManager { } // See [REQUIREMENT_MEMO], merge settings on series and parent dataset if it is root. - const newMetaRawOption = this._getSourceMetaRawOption(); - const upMetaRawOption = upSource ? upSource.metaRawOption : null; - const seriesLayoutBy = retrieve2( - newMetaRawOption.seriesLayoutBy, - upMetaRawOption ? upMetaRawOption.seriesLayoutBy : null - ); - const sourceHeader = retrieve2( - newMetaRawOption.sourceHeader, - upMetaRawOption ? upMetaRawOption.sourceHeader : null - ); + const newMetaRawOption = this._getSourceMetaRawOption() || {} as SourceMetaRawOption; + const upMetaRawOption = upSource && upSource.metaRawOption || {} as SourceMetaRawOption; + const seriesLayoutBy = retrieve2(newMetaRawOption.seriesLayoutBy, upMetaRawOption.seriesLayoutBy) || null; + const sourceHeader = retrieve2(newMetaRawOption.sourceHeader, upMetaRawOption.sourceHeader) || null; // Note here we should not use `upSource.dimensionsDefine`. Consider the case: // `upSource.dimensionsDefine` is detected by `seriesLayoutBy: 'column'`, // but series need `seriesLayoutBy: 'row'`. - const dimensions = retrieve2( - newMetaRawOption.dimensions, - upMetaRawOption ? upMetaRawOption.dimensions : null - ); - - resultSourceList = [createSource( + const dimensions = retrieve2(newMetaRawOption.dimensions, upMetaRawOption.dimensions); + + // We share source with dataset as much as possible + // to avoid extra memroy cost of high dimensional data. + const needsCreateSource = seriesLayoutBy !== upMetaRawOption.seriesLayoutBy + || !!sourceHeader !== !!upMetaRawOption.sourceHeader + || dimensions; + resultSourceList = needsCreateSource ? [createSource( data, { seriesLayoutBy, sourceHeader, dimensions }, - sourceFormat, - seriesModel.get('encode', true) - )]; + sourceFormat + )] : []; } else { const datasetModel = sourceHost as DatasetModel; @@ -251,8 +261,6 @@ export class SourceManager { resultSourceList = [createSource( sourceData, this._getSourceMetaRawOption(), - null, - // Note: dataset option does not have `encode`. null )]; upstreamSignList = []; @@ -322,8 +330,7 @@ export class SourceManager { } private _isDirty(): boolean { - const sourceList = this._sourceList; - if (!sourceList.length) { + if (this._dirty) { return true; } @@ -346,8 +353,73 @@ export class SourceManager { * @param sourceIndex By defualt 0, means "main source". * Most cases there is only one source. */ - getSource(sourceIndex?: number) { - return this._sourceList[sourceIndex || 0]; + getSource(sourceIndex?: number): Source { + sourceIndex = sourceIndex || 0; + const source = this._sourceList[sourceIndex]; + if (!source) { + // Series may share source instance with dataset. + const upSourceMgrList = this._getUpstreamSourceManagers(); + return upSourceMgrList[0] + && upSourceMgrList[0].getSource(sourceIndex); + } + return source; + } + + /** + * + * Get a data store which can be shared across series. + * Only available for series. + * + * @param seriesDimRequest Dimensions that are generated in series. + * Should have been sorted by `storeDimIndex` asc. + */ + getSharedDataStore(seriesDimRequest: SeriesDataSchema): DataStore { + if (__DEV__) { + assert(isSeries(this._sourceHost), 'Can only call getDataStore on series source manager.'); + } + const schema = seriesDimRequest.makeStoreSchema(); + return this._innerGetDataStore( + schema.dimensions, seriesDimRequest.source, schema.hash + ); + } + + private _innerGetDataStore( + storeDims: DataStoreDimensionDefine[], + seriesSource: Source, + sourceReadKey: string + ): DataStore | undefined { + // TODO Can use other sourceIndex? + const sourceIndex = 0; + + const storeList = this._storeList; + + let cachedStoreMap = storeList[sourceIndex]; + + if (!cachedStoreMap) { + cachedStoreMap = storeList[sourceIndex] = {}; + } + + let cachedStore = cachedStoreMap[sourceReadKey]; + if (!cachedStore) { + const upSourceMgr = this._getUpstreamSourceManagers()[0]; + + if (isSeries(this._sourceHost) && upSourceMgr) { + cachedStore = upSourceMgr._innerGetDataStore( + storeDims, seriesSource, sourceReadKey + ); + } + else { + cachedStore = new DataStore(); + // Always create store from source of series. + cachedStore.initData( + new DefaultDataProvider(seriesSource, storeDims.length), + storeDims + ); + } + cachedStoreMap[sourceReadKey] = cachedStore; + } + + return cachedStore; } /** diff --git a/src/data/helper/transform.ts b/src/data/helper/transform.ts index 43625cd917..78c1a70754 100644 --- a/src/data/helper/transform.ts +++ b/src/data/helper/transform.ts @@ -533,7 +533,6 @@ function applySingleDataTransform( return createSource( result.data, resultMetaRawOption, - null, null ); }); diff --git a/src/echarts.all.ts b/src/echarts.all.ts index e66f8b9aeb..090d86bd42 100644 --- a/src/echarts.all.ts +++ b/src/echarts.all.ts @@ -87,6 +87,11 @@ import { TransformComponent } from './export/components'; +import { + UniversalTransition, + LabelLayout +} from './export/features'; + // ----------------- // Render engines @@ -332,4 +337,20 @@ use(AriaComponent); // }); use(TransformComponent); -use(DatasetComponent); \ No newline at end of file +use(DatasetComponent); + +// universal transition +// chart.setOption({ +// series: { +// universalTransition: { enabled: true } +// } +// }) +use(UniversalTransition); + +// label layout +// chart.setOption({ +// series: { +// labelLayout: { hideOverlap: true } +// } +// }) +use(LabelLayout); \ No newline at end of file diff --git a/src/echarts.ts b/src/echarts.ts index e998b0f36b..2c4df0d8e7 100644 --- a/src/echarts.ts +++ b/src/echarts.ts @@ -27,7 +27,7 @@ import {install as DatasetComponent} from './component/dataset/install'; // Default to have canvas renderer and dataset for compitatble reason. use([CanvasRenderer, DatasetComponent]); -// Compatitable with the following code +// TODO: Compatitable with the following code // import echarts from 'echarts/lib/echarts' export default { init() { @@ -38,4 +38,9 @@ export default { // @ts-ignore return init.apply(null, arguments); } -}; \ No newline at end of file +}; + +// Import label layout by default. +// TODO remove +import {installLabelLayout} from './label/installLabelLayout'; +use(installLabelLayout); \ No newline at end of file diff --git a/src/export/api.ts b/src/export/api.ts index 1d84814ada..d7f7cf0e33 100644 --- a/src/export/api.ts +++ b/src/export/api.ts @@ -24,6 +24,7 @@ import ComponentView, { ComponentViewConstructor } from '../view/Component'; import SeriesModel, { SeriesModelConstructor } from '../model/Series'; import ChartView, { ChartViewConstructor } from '../view/Chart'; +import SeriesData from '../data/SeriesData'; // Provide utilities API in echarts. It will be in echarts namespace. // Like echarts.util, echarts.graphic @@ -53,7 +54,8 @@ export * as util from './api/util'; export {default as env} from 'zrender/src/core/env'; //////////////// Export for Exension Usage //////////////// -export {default as List} from '../data/List'; +// export {SeriesData}; +export {SeriesData as List}; // TODO: Compatitable with exists echarts-gl code export {default as Model} from '../model/Model'; export {default as Axis} from '../coord/Axis'; diff --git a/src/export/api/helper.ts b/src/export/api/helper.ts index 07d6cef2c7..3d63a7b140 100644 --- a/src/export/api/helper.ts +++ b/src/export/api/helper.ts @@ -22,7 +22,7 @@ */ import * as zrUtil from 'zrender/src/core/util'; -import createListFromArray from '../../chart/helper/createListFromArray'; +import createSeriesData from '../../chart/helper/createSeriesData'; // import createGraphFromNodeEdge from './chart/helper/createGraphFromNodeEdge'; import * as axisHelper from '../../coord/axisHelper'; import {AxisModelCommonMixin} from '../../coord/axisModelCommonMixin'; @@ -43,7 +43,7 @@ import { DisplayState, TextCommonOption } from '../../util/types'; * Create a muti dimension List structure from seriesModel. */ export function createList(seriesModel: SeriesModel) { - return createListFromArray(seriesModel.getSource(), seriesModel); + return createSeriesData(null, seriesModel); } // export function createGraph(seriesModel) { @@ -54,7 +54,7 @@ export function createList(seriesModel: SeriesModel) { export {getLayoutRect}; -export {default as createDimensions} from '../../data/helper/createDimensions'; +export {createDimensions} from '../../data/helper/createDimensions'; export const dataStack = { isDimensionStacked: isDimensionStacked, diff --git a/src/export/core.ts b/src/export/core.ts index 8cfb253e89..c1702f2154 100644 --- a/src/export/core.ts +++ b/src/export/core.ts @@ -21,6 +21,13 @@ export * from '../core/echarts'; export * from './api'; +import { use } from '../extension'; + +// Import label layout by default. +// TODO remove +import {installLabelLayout} from '../label/installLabelLayout'; +use(installLabelLayout); + // Export necessary types export {ZRColor as Color, Payload} from '../util/types'; diff --git a/src/export/features.ts b/src/export/features.ts new file mode 100644 index 0000000000..12dd1d24f0 --- /dev/null +++ b/src/export/features.ts @@ -0,0 +1,23 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +// Module that exports complex but fancy features. + +export {installUniversalTransition as UniversalTransition} from '../animation/universalTransition'; +export {installLabelLayout as LabelLayout} from '../label/installLabelLayout'; \ No newline at end of file diff --git a/src/export/option.ts b/src/export/option.ts index 3ba1bb9ffb..7ed1b4b35e 100644 --- a/src/export/option.ts +++ b/src/export/option.ts @@ -86,7 +86,7 @@ import type {HeatmapSeriesOption as HeatmapSeriesOptionInner} from '../chart/hea import type {PictorialBarSeriesOption as PictorialBarSeriesOptionInner} from '../chart/bar/PictorialBarSeries'; import type {ThemeRiverSeriesOption as ThemeRiverSeriesOptionInner} from '../chart/themeRiver/ThemeRiverSeries'; import type {SunburstSeriesOption as SunburstSeriesOptionInner} from '../chart/sunburst/SunburstSeries'; -import type {CustomSeriesOption as CustomSeriesOptionInner} from '../chart/custom/install'; +import type {CustomSeriesOption as CustomSeriesOptionInner} from '../chart/custom/CustomSeries'; import type { GraphicComponentLooseOption as GraphicComponentOption } from '../component/graphic/install'; import type { DatasetOption as DatasetComponentOption } from '../component/dataset/install'; diff --git a/src/extension.ts b/src/extension.ts index 2f223aaafe..f781f51073 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -29,6 +29,7 @@ import { registerTransform, registerLoading, registerMap, + registerUpdateLifecycle, PRIORITY } from './core/echarts'; import ComponentView from './view/Component'; @@ -47,6 +48,7 @@ const extensionRegisters = { registerProcessor, registerPostInit, registerPostUpdate, + registerUpdateLifecycle, registerAction, registerCoordinateSystem, registerLayout, diff --git a/src/label/installLabelLayout.ts b/src/label/installLabelLayout.ts new file mode 100644 index 0000000000..0db95e0ae2 --- /dev/null +++ b/src/label/installLabelLayout.ts @@ -0,0 +1,27 @@ +import { EChartsExtensionInstallRegisters } from '../extension'; +import { makeInner } from '../util/model'; +import LabelManager from './LabelManager'; +import ExtensionAPI from '../core/ExtensionAPI'; + +const getLabelManager = makeInner<{ labelManager: LabelManager }, ExtensionAPI>(); +export function installLabelLayout(registers: EChartsExtensionInstallRegisters) { + registers.registerUpdateLifecycle('series:beforeupdate', (ecModel, api, params) => { + // TODO api provide an namespace that can save stuff per instance + let labelManager = getLabelManager(api).labelManager; + if (!labelManager) { + labelManager = getLabelManager(api).labelManager = new LabelManager(); + } + labelManager.clearLabels(); + }); + + registers.registerUpdateLifecycle('series:layoutlabels', (ecModel, api, params) => { + const labelManager = getLabelManager(api).labelManager; + + params.updatedSeries.forEach(series => { + labelManager.addLabelsOfSeries(api.getViewOfSeriesModel(series)); + }); + labelManager.updateLayoutConfig(api); + labelManager.layout(api); + labelManager.processLabelsOverall(); + }); +} \ No newline at end of file diff --git a/src/label/labelStyle.ts b/src/label/labelStyle.ts index 0012983733..606aa948f1 100644 --- a/src/label/labelStyle.ts +++ b/src/label/labelStyle.ts @@ -38,7 +38,7 @@ import { isFunction, retrieve2, extend, keys, trim } from 'zrender/src/core/util import { SPECIAL_STATES, DISPLAY_STATES } from '../util/states'; import { deprecateReplaceLog } from '../util/log'; import { makeInner, interpolateRawValues } from '../util/model'; -import List from '../data/List'; +import SeriesData from '../data/SeriesData'; import { initProps, updateProps } from '../util/graphic'; import { getECData } from '../util/innerStore'; @@ -674,7 +674,6 @@ export function setLabelValueAnimation( const obj = labelInner(label); obj.prevValue = obj.value; obj.value = value; - const normalLabelModel = labelStatesModels.normal; obj.valueAnimation = normalLabelModel.get('valueAnimation'); @@ -689,7 +688,7 @@ export function setLabelValueAnimation( export function animateLabelValue( textEl: ZRText, dataIndex: number, - data: List, + data: SeriesData, animatableModel: Model, labelFetcher: SetLabelStyleOpt['labelFetcher'] ) { @@ -711,6 +710,7 @@ export function animateLabelValue( targetValue, percent ); + labelInnerStore.interpolatedValue = percent === 1 ? null : interpolated; const labelText = getLabelText({ @@ -724,7 +724,7 @@ export function animateLabelValue( setLabelText(textEl, labelText); } - (currValue == null + (labelInnerStore.prevValue == null ? initProps : updateProps )(textEl, {}, animatableModel, dataIndex, null, during); diff --git a/src/label/sectorLabel.ts b/src/label/sectorLabel.ts new file mode 100644 index 0000000000..6ed74d2b3a --- /dev/null +++ b/src/label/sectorLabel.ts @@ -0,0 +1,249 @@ +import {calculateTextPosition, TextPositionCalculationResult} from 'zrender/src/contain/text'; +import { RectLike } from 'zrender/src/core/BoundingRect'; +import {BuiltinTextPosition, TextAlign, TextVerticalAlign} from 'zrender/src/core/types'; +import {isArray} from 'zrender/src/core/util'; +import {ElementCalculateTextPosition, ElementTextConfig} from 'zrender/src/Element'; +import { Sector } from '../util/graphic'; + +export type SectorTextPosition = BuiltinTextPosition + | 'startAngle' | 'insideStartAngle' + | 'endAngle' | 'insideEndAngle' + | 'middle' + | 'startArc' | 'insideStartArc' + | 'endArc' | 'insideEndArc' + | (number | string)[]; + +export type SectorLike = { + cx: number + cy: number + r0: number + r: number + startAngle: number + endAngle: number + clockwise: boolean +}; + +export function createSectorCalculateTextPosition( + positionMapping: (seriesLabelPosition: T) => SectorTextPosition, + opts?: { + /** + * If has round cap on two ends. If so, label should have an extra offset + */ + isRoundCap?: boolean + } +): ElementCalculateTextPosition { + + opts = opts || {}; + const isRoundCap = opts.isRoundCap; + + return function ( + this: Sector, + out: TextPositionCalculationResult, + opts: { + position?: SectorTextPosition + distance?: number + global?: boolean + }, + boundingRect: RectLike + ) { + const textPosition = opts.position; + + if (!textPosition || textPosition instanceof Array) { + return calculateTextPosition( + out, + opts as ElementTextConfig, + boundingRect + ); + } + + const mappedSectorPosition = positionMapping(textPosition as T); + const distance = opts.distance != null ? opts.distance : 5; + const sector = this.shape; + const cx = sector.cx; + const cy = sector.cy; + const r = sector.r; + const r0 = sector.r0; + const middleR = (r + r0) / 2; + const startAngle = sector.startAngle; + const endAngle = sector.endAngle; + const middleAngle = (startAngle + endAngle) / 2; + const extraDist = isRoundCap ? Math.abs(r - r0) / 2 : 0; + + const mathCos = Math.cos; + const mathSin = Math.sin; + + // base position: top-left + let x = cx + r * mathCos(startAngle); + let y = cy + r * mathSin(startAngle); + + let textAlign: TextAlign = 'left'; + let textVerticalAlign: TextVerticalAlign = 'top'; + + switch (mappedSectorPosition) { + case 'startArc': + x = cx + (r0 - distance) * mathCos(middleAngle); + y = cy + (r0 - distance) * mathSin(middleAngle); + textAlign = 'center'; + textVerticalAlign = 'top'; + break; + + case 'insideStartArc': + x = cx + (r0 + distance) * mathCos(middleAngle); + y = cy + (r0 + distance) * mathSin(middleAngle); + textAlign = 'center'; + textVerticalAlign = 'bottom'; + break; + + case 'startAngle': + x = cx + middleR * mathCos(startAngle) + + adjustAngleDistanceX(startAngle, distance + extraDist, false); + y = cy + middleR * mathSin(startAngle) + + adjustAngleDistanceY(startAngle, distance + extraDist, false); + textAlign = 'right'; + textVerticalAlign = 'middle'; + break; + + case 'insideStartAngle': + x = cx + middleR * mathCos(startAngle) + + adjustAngleDistanceX(startAngle, -distance + extraDist, false); + y = cy + middleR * mathSin(startAngle) + + adjustAngleDistanceY(startAngle, -distance + extraDist, false); + textAlign = 'left'; + textVerticalAlign = 'middle'; + break; + + case 'middle': + x = cx + middleR * mathCos(middleAngle); + y = cy + middleR * mathSin(middleAngle); + textAlign = 'center'; + textVerticalAlign = 'middle'; + break; + + case 'endArc': + x = cx + (r + distance) * mathCos(middleAngle); + y = cy + (r + distance) * mathSin(middleAngle); + textAlign = 'center'; + textVerticalAlign = 'bottom'; + break; + + case 'insideEndArc': + x = cx + (r - distance) * mathCos(middleAngle); + y = cy + (r - distance) * mathSin(middleAngle); + textAlign = 'center'; + textVerticalAlign = 'top'; + break; + + case 'endAngle': + x = cx + middleR * mathCos(endAngle) + + adjustAngleDistanceX(endAngle, distance + extraDist, true); + y = cy + middleR * mathSin(endAngle) + + adjustAngleDistanceY(endAngle, distance + extraDist, true); + textAlign = 'left'; + textVerticalAlign = 'middle'; + break; + + case 'insideEndAngle': + x = cx + middleR * mathCos(endAngle) + + adjustAngleDistanceX(endAngle, -distance + extraDist, true); + y = cy + middleR * mathSin(endAngle) + + adjustAngleDistanceY(endAngle, -distance + extraDist, true); + textAlign = 'right'; + textVerticalAlign = 'middle'; + break; + + default: + return calculateTextPosition( + out, + opts as ElementTextConfig, + boundingRect + ); + } + + out = out || {} as TextPositionCalculationResult; + out.x = x; + out.y = y; + out.align = textAlign; + out.verticalAlign = textVerticalAlign; + + return out; + }; +} + +export function setSectorTextRotation( + sector: Sector, + textPosition: T, + positionMapping: (seriesLabelPosition: T) => SectorTextPosition, + rotateType: number | 'auto' +) { + if (typeof rotateType === 'number') { + // user-set rotation + sector.setTextConfig({ + rotation: rotateType + }); + return; + } + else if (isArray(textPosition)) { + // user-set position, use 0 as auto rotation + sector.setTextConfig({ + rotation: 0 + }); + return; + } + + const shape = sector.shape; + const startAngle = shape.clockwise ? shape.startAngle : shape.endAngle; + const endAngle = shape.clockwise ? shape.endAngle : shape.startAngle; + const middleAngle = (startAngle + endAngle) / 2; + + let anchorAngle; + const mappedSectorPosition = positionMapping(textPosition); + switch (mappedSectorPosition) { + case 'startArc': + case 'insideStartArc': + case 'middle': + case 'insideEndArc': + case 'endArc': + anchorAngle = middleAngle; + break; + + case 'startAngle': + case 'insideStartAngle': + anchorAngle = startAngle; + break; + + case 'endAngle': + case 'insideEndAngle': + anchorAngle = endAngle; + break; + + default: + sector.setTextConfig({ + rotation: 0 + }); + return; + } + + let rotate = Math.PI * 1.5 - anchorAngle; + /** + * TODO: labels with rotate > Math.PI / 2 should be rotate another + * half round flipped to increase readability. However, only middle + * position supports this for now, because in other positions, the + * anchor point is not at the center of the text, so the positions + * after rotating is not as expected. + */ + if (mappedSectorPosition === 'middle' && rotate > Math.PI / 2 && rotate < Math.PI * 1.5) { + rotate -= Math.PI; + } + + sector.setTextConfig({ + rotation: rotate + }); +} + +function adjustAngleDistanceX(angle: number, distance: number, isEnd: boolean) { + return distance * Math.sin(angle) * (isEnd ? -1 : 1); +} + +function adjustAngleDistanceY(angle: number, distance: number, isEnd: boolean) { + return distance * Math.cos(angle) * (isEnd ? 1 : -1); +} diff --git a/src/layout/barGrid.ts b/src/layout/barGrid.ts index edf64007aa..4a7c90887f 100644 --- a/src/layout/barGrid.ts +++ b/src/layout/barGrid.ts @@ -158,9 +158,10 @@ function getValueAxesMinGaps(barSeries: BarSeriesModel[]) { const data = seriesModel.getData(); const key = baseAxis.dim + '_' + baseAxis.index; - const dim = data.mapDimension(baseAxis.dim); - for (let i = 0, cnt = data.count(); i < cnt; ++i) { - const value = data.get(dim, i) as number; + const dimIdx = data.getDimensionIndex(data.mapDimension(baseAxis.dim)); + const store = data.getStore(); + for (let i = 0, cnt = store.count(); i < cnt; ++i) { + const value = store.get(dimIdx, i) as number; if (!axisValues[key]) { // No previous data for the axis axisValues[key] = [value]; @@ -472,14 +473,17 @@ export function layout(seriesType: string, ecModel: GlobalModel) { const valueDim = data.mapDimension(valueAxis.dim); const baseDim = data.mapDimension(baseAxis.dim); - const stacked = isDimensionStacked(data, valueDim /*, baseDim*/); + const stacked = isDimensionStacked(data, valueDim); const isValueAxisH = valueAxis.isHorizontal(); const valueAxisStart = getValueAxisStart(baseAxis, valueAxis, stacked); - for (let idx = 0, len = data.count(); idx < len; idx++) { - const value = data.get(valueDim, idx); - const baseValue = data.get(baseDim, idx) as number; + const store = data.getStore(); + const valueDimIdx = data.getDimensionIndex(valueDim); + const baseDimIdx = data.getDimensionIndex(baseDim); + for (let idx = 0, len = store.count(); idx < len; idx++) { + const value = store.get(valueDimIdx, idx); + const baseValue = store.get(baseDimIdx, idx) as number; const sign = value >= 0 ? 'p' : 'n' as 'p' | 'n'; let baseCoord = valueAxisStart; @@ -563,8 +567,8 @@ export const largeLayout: StageHandler = { const coordLayout = cartesian.master.getRect(); const baseAxis = cartesian.getBaseAxis(); const valueAxis = cartesian.getOtherAxis(baseAxis); - const valueDim = data.mapDimension(valueAxis.dim); - const baseDim = data.mapDimension(baseAxis.dim); + const valueDimI = data.getDimensionIndex(data.mapDimension(valueAxis.dim)); + const baseDimI = data.getDimensionIndex(data.mapDimension(baseAxis.dim)); const valueAxisHorizontal = valueAxis.isHorizontal(); const valueDimIdx = valueAxisHorizontal ? 0 : 1; @@ -586,10 +590,11 @@ export const largeLayout: StageHandler = { const valuePair = []; let pointsOffset = 0; let idxOffset = 0; + const store = data.getStore(); while ((dataIndex = params.next()) != null) { - valuePair[valueDimIdx] = data.get(valueDim, dataIndex); - valuePair[1 - valueDimIdx] = data.get(baseDim, dataIndex); + valuePair[valueDimIdx] = store.get(valueDimI, dataIndex); + valuePair[1 - valueDimIdx] = store.get(baseDimI, dataIndex); coord = cartesian.dataToPoint(valuePair, null); // Data index might not be in order, depends on `progressiveChunkMode`. diff --git a/src/layout/points.ts b/src/layout/points.ts index 8946852ef1..bde24f1535 100644 --- a/src/layout/points.ts +++ b/src/layout/points.ts @@ -56,11 +56,9 @@ export default function pointsLayout(seriesType: string, forceStoreInTypedArray? dims[1] = stackResultDim; } - const dimInfo0 = data.getDimensionInfo(dims[0]); - const dimInfo1 = data.getDimensionInfo(dims[1]); - - const dimIdx0 = dimInfo0 && dimInfo0.index; - const dimIdx1 = dimInfo1 && dimInfo1.index; + const store = data.getStore(); + const dimIdx0 = data.getDimensionIndex(dims[0]); + const dimIdx1 = data.getDimensionIndex(dims[1]); return dimLen && { progress(params, data) { @@ -74,13 +72,13 @@ export default function pointsLayout(seriesType: string, forceStoreInTypedArray? let point; if (dimLen === 1) { - const x = data.getByDimIdx(dimIdx0, i) as ParsedValueNumeric; + const x = store.get(dimIdx0, i) as ParsedValueNumeric; // NOTE: Make sure the second parameter is null to use default strategy. point = coordSys.dataToPoint(x, null, tmpOut); } else { - tmpIn[0] = data.getByDimIdx(dimIdx0, i) as ParsedValueNumeric; - tmpIn[1] = data.getByDimIdx(dimIdx1, i) as ParsedValueNumeric; + tmpIn[0] = store.get(dimIdx0, i) as ParsedValueNumeric; + tmpIn[1] = store.get(dimIdx1, i) as ParsedValueNumeric; // Let coordinate system to handle the NaN data. point = coordSys.dataToPoint(tmpIn, null, tmpOut); } diff --git a/src/model/Global.ts b/src/model/Global.ts index 0a06d5bcd8..312a067c09 100644 --- a/src/model/Global.ts +++ b/src/model/Global.ts @@ -530,11 +530,6 @@ echarts.use([${seriesImportName}]);`); return this._locale; } - getLocale(localePosition: Parameters['get']>[0]): any { - const locale = this.getLocaleModel(); - return locale.get(localePosition as any); - } - setUpdatePayload(payload: Payload) { this._payload = payload; } @@ -758,7 +753,7 @@ echarts.use([${seriesImportName}]);`); */ getSeries(): SeriesModel[] { return filter( - this._componentsMap.get('series').slice() as SeriesModel[], + this._componentsMap.get('series') as SeriesModel[], oneSeries => !!oneSeries ); } diff --git a/src/model/Series.ts b/src/model/Series.ts index 5a54ad4e66..a11bc5c6de 100644 --- a/src/model/Series.ts +++ b/src/model/Series.ts @@ -19,12 +19,17 @@ import * as zrUtil from 'zrender/src/core/util'; import env from 'zrender/src/core/env'; -import type {MorphDividingMethod} from 'zrender/src/tool/morphPath'; import * as modelUtil from '../util/model'; import { DataHost, DimensionName, StageHandlerProgressParams, SeriesOption, ZRColor, BoxLayoutOptionMixin, - ScaleDataValue, Dictionary, OptionDataItemObject, SeriesDataType, DimensionLoose + ScaleDataValue, + Dictionary, + OptionDataItemObject, + SeriesDataType, + SeriesEncodeOptionMixin, + OptionEncodeValue, + ColorBy } from '../util/types'; import ComponentModel, { ComponentModelConstructor } from './Component'; import {PaletteMixin} from './mixin/palette'; @@ -41,7 +46,7 @@ import { CoordinateSystem } from '../coord/CoordinateSystem'; import { ExtendableConstructor, mountExtend, Constructor } from '../util/clazz'; import { PipelineContext, SeriesTaskContext, GeneralTask, OverallTask, SeriesTask } from '../core/Scheduler'; import LegendVisualProvider from '../visual/LegendVisualProvider'; -import List from '../data/List'; +import SeriesData from '../data/SeriesData'; import Axis from '../coord/Axis'; import type { BrushCommonSelectorsForSeries, BrushSelectableArea } from '../component/brush/selector'; import makeStyleMapper from './mixin/makeStyleMapper'; @@ -53,15 +58,17 @@ import {Group} from '../util/graphic'; import {LegendIconParams} from '../component/legend/LegendModel'; const inner = modelUtil.makeInner<{ - data: List - dataBeforeProcessed: List + data: SeriesData + dataBeforeProcessed: SeriesData sourceManager: SourceManager }, SeriesModel>(); -function getSelectionKey(data: List, dataIndex: number): string { +function getSelectionKey(data: SeriesData, dataIndex: number): string { return data.getName(dataIndex) || data.getId(dataIndex); } +export const SERIES_UNIVERSAL_TRANSITION_PROP = '__universalTransitionEnabled'; + interface SeriesModel { /** * Convinient for override in extended class. @@ -104,7 +111,7 @@ interface SeriesModel { */ brushSelector( dataIndex: number, - data: List, + data: SeriesData, selectors: BrushCommonSelectorsForSeries, area: BrushSelectableArea ): boolean; @@ -141,18 +148,6 @@ class SeriesModel extends ComponentMode // Injected outside pipelineContext: PipelineContext; - // only avalible in `render()` caused by `setOption`. - __transientTransitionOpt: { - // [MEMO] Currently only support single "from". If intending to - // support multiple "from", if not hard to implement "merge morph", - // but correspondingly not easy to implement "split morph". - - // Both from and to can be null/undefined, which meams no transform mapping. - from: DimensionLoose; - to: DimensionLoose; - dividingMethod: MorphDividingMethod; - }; - // --------------------------------------- // Props to tell visual/style.ts about how to do visual encoding. // --------------------------------------- @@ -168,8 +163,6 @@ class SeriesModel extends ComponentMode // If ignore style on data. It's only for global visual/style.ts // Enabled when series it self will handle it. ignoreStyleOnData: boolean; - // If use palette on each data. - useColorPaletteOnData: boolean; // If do symbol visual encoding hasSymbolVisual: boolean; // Default symbol type. @@ -177,6 +170,10 @@ class SeriesModel extends ComponentMode // Symbol provide to legend. legendIcon: string; + // It will be set temporary when cross series transition setting is from setOption. + // TODO if deprecate further? + [SERIES_UNIVERSAL_TRANSITION_PROP]: boolean; + // --------------------------------------- // Props about data selection // --------------------------------------- @@ -188,7 +185,6 @@ class SeriesModel extends ComponentMode const proto = SeriesModel.prototype; proto.type = 'series.__base__'; proto.seriesIndex = 0; - proto.useColorPaletteOnData = false; proto.ignoreStyleOnData = false; proto.hasSymbolVisual = false; proto.defaultSymbol = 'circle'; @@ -227,7 +223,7 @@ class SeriesModel extends ComponentMode // dataBeforeProcessed by cloneShallow), cloneShallow will // cause data.graph.data !== data when using // module:echarts/data/Graph or module:echarts/data/Tree. - // See module:echarts/data/helper/linkList + // See module:echarts/data/helper/linkSeriesData // Theoretically, it is unreasonable to call `seriesModel.getData()` in the model // init or merge stage, because the data can be restored. So we do not `restoreData` @@ -321,7 +317,7 @@ class SeriesModel extends ComponentMode * Init a data structure from data related option in series * Must be overriden. */ - getInitialData(option: Opt, ecModel: GlobalModel): List { + getInitialData(option: Opt, ecModel: GlobalModel): SeriesData { return; } @@ -342,23 +338,23 @@ class SeriesModel extends ComponentMode * data in the stream procedure. So we fetch data from upstream * each time `task.perform` called. */ - getData(dataType?: SeriesDataType): List { + getData(dataType?: SeriesDataType): SeriesData { const task = getCurrentTask(this); if (task) { const data = task.context.data; - return (dataType == null ? data : data.getLinkedData(dataType)) as List; + return (dataType == null ? data : data.getLinkedData(dataType)) as SeriesData; } else { // When series is not alive (that may happen when click toolbox // restore or setOption with not merge mode), series data may // be still need to judge animation or something when graphic // elements want to know whether fade out. - return inner(this).data as List; + return inner(this).data as SeriesData; } } getAllData(): ({ - data: List, + data: SeriesData, type?: SeriesDataType })[] { const mainData = this.getData(); @@ -367,7 +363,7 @@ class SeriesModel extends ComponentMode : [{ data: mainData }]; } - setData(data: List): void { + setData(data: SeriesData): void { const task = getCurrentTask(this); if (task) { const context = task.context; @@ -392,17 +388,37 @@ class SeriesModel extends ComponentMode inner(this).data = data; } + getEncode() { + const encode = (this as Model).get('encode', true); + if (encode) { + return zrUtil.createHashMap(encode); + } + } + + getSourceManager(): SourceManager { + return inner(this).sourceManager; + } + getSource(): Source { - return inner(this).sourceManager.getSource(); + return this.getSourceManager().getSource(); } /** * Get data before processed */ - getRawData(): List { + getRawData(): SeriesData { return inner(this).dataBeforeProcessed; } + getColorBy(): ColorBy { + const colorBy = this.get('colorBy'); + return colorBy || 'series'; + } + + isColorBySeries(): boolean { + return this.getColorBy() === 'series'; + } + /** * Get base axis if has coordinate system and has axis. * By default use coordSys.getBaseAxis(); @@ -543,7 +559,26 @@ class SeriesModel extends ComponentMode return selectedMap[nameOrId] || false; } - private _innerSelect(data: List, innerDataIndices: number[]) { + isUniversalTransitionEnabled(): boolean { + if (this[SERIES_UNIVERSAL_TRANSITION_PROP]) { + return true; + } + + const universalTransitionOpt = this.option.universalTransition; + // Quick reject + if (!universalTransitionOpt) { + return false; + } + + if (universalTransitionOpt === true) { + return true; + } + + // Can be simply 'universalTransition: true' + return universalTransitionOpt && universalTransitionOpt.enabled; + } + + private _innerSelect(data: SeriesData, innerDataIndices: number[]) { const selectedMode = this.option.selectedMode; const len = innerDataIndices.length; if (!selectedMode || !len) { @@ -572,7 +607,7 @@ class SeriesModel extends ComponentMode } } - private _initSelectedMapFromData(data: List) { + private _initSelectedMapFromData(data: SeriesData) { // Ignore select info in data if selectedMap exists. // NOTE It's only for legacy usage. edge data is not supported. if (this.option.selectedMap) { @@ -663,13 +698,13 @@ function dataTaskProgress(param: StageHandlerProgressParams, context: SeriesTask } // TODO refactor -function wrapData(data: List, seriesModel: SeriesModel): void { - zrUtil.each([...data.CHANGABLE_METHODS, ...data.DOWNSAMPLE_METHODS], function (methodName) { +function wrapData(data: SeriesData, seriesModel: SeriesModel): void { + zrUtil.each(zrUtil.concatArray(data.CHANGABLE_METHODS, data.DOWNSAMPLE_METHODS), function (methodName) { data.wrapMethod(methodName as any, zrUtil.curry(onDataChange, seriesModel)); }); } -function onDataChange(this: List, seriesModel: SeriesModel, newList: List): List { +function onDataChange(this: SeriesData, seriesModel: SeriesModel, newList: SeriesData): SeriesData { const task = getCurrentTask(seriesModel); if (task) { // Consider case: filter, selectRange diff --git a/src/model/globalDefault.ts b/src/model/globalDefault.ts index 838491a5fe..304a550121 100644 --- a/src/model/globalDefault.ts +++ b/src/model/globalDefault.ts @@ -32,18 +32,9 @@ export default { darkMode: 'auto', // backgroundColor: 'rgba(0,0,0,0)', - // https://dribbble.com/shots/1065960-Infographic-Pie-chart-visualization - // color: ['#5793f3', '#d14a61', '#fd9c35', '#675bba', '#fec42c', '#dd4444', '#d4df5a', '#cd4870'], - // Light colors: - // color: ['#bcd3bb', '#e88f70', '#edc1a5', '#9dc5c8', '#e1e8c8', '#7b7c68', '#e5b5b5', '#f0b489', '#928ea8', '#bda29a'], - // color: ['#cc5664', '#9bd6ec', '#ea946e', '#8acaaa', '#f1ec64', '#ee8686', '#a48dc1', '#5da6bc', '#b9dcae'], - // Dark colors: - // color: [ - // '#c23531', '#2f4554', '#61a0a8', '#d48265', '#91c7ae', '#749f83', - // '#ca8622', '#bda29a', '#6e7074', '#546570', '#c4ccd3' - // ], + colorBy: 'series', + color: [ - // '#51689b', '#ce5c5c', '#fbc357', '#8fbf8f', '#659d84', '#fb8e6a', '#c77288', '#786090', '#91c4c5', '#6890ba' '#5470c6', '#91cc75', '#fac858', diff --git a/src/model/mixin/dataFormat.ts b/src/model/mixin/dataFormat.ts index 4e364537a9..d634a7b8f7 100644 --- a/src/model/mixin/dataFormat.ts +++ b/src/model/mixin/dataFormat.ts @@ -35,7 +35,7 @@ import { } from '../../util/types'; import GlobalModel from '../Global'; import { TooltipMarkupBlockFragment } from '../../component/tooltip/tooltipMarkup'; -import { makePrintable } from '../../util/log'; +import { error, makePrintable } from '../../util/log'; const DIMENSION_LABEL_REG = /\{@(.+?)\}/g; @@ -70,7 +70,7 @@ export class DataFormatMixin { const borderColor = style && style.stroke as ColorString; const mainType = this.mainType; const isSeries = mainType === 'series'; - const userOutput = data.userOutput; + const userOutput = data.userOutput && data.userOutput.get(); return { componentType: mainType, @@ -87,7 +87,7 @@ export class DataFormatMixin { value: rawValue, color: color, borderColor: borderColor, - dimensionNames: userOutput ? userOutput.dimensionNames : null, + dimensionNames: userOutput ? userOutput.fullDimensions : null, encode: userOutput ? userOutput.encode : null, // Param name list for mapping `a`, `b`, `c`, `d`, `e` @@ -149,16 +149,23 @@ export class DataFormatMixin { // Do not support '}' in dim name util have to. return str.replace(DIMENSION_LABEL_REG, function (origin, dimStr: string) { const len = dimStr.length; - const dimLoose: DimensionLoose = (dimStr.charAt(0) === '[' && dimStr.charAt(len - 1) === ']') - ? +dimStr.slice(1, len - 1) // Also support: '[]' => 0 - : dimStr; + + let dimLoose: DimensionLoose = dimStr; + if (dimLoose.charAt(0) === '[' && dimLoose.charAt(len - 1) === ']') { + dimLoose = +dimLoose.slice(1, len - 1); // Also support: '[]' => 0 + if (__DEV__) { + if (isNaN(dimLoose)) { + error(`Invalide label formatter: @${dimStr}, only support @[0], @[1], @[2], ...`); + } + } + } let val = retrieveRawValue(data, dataIndex, dimLoose) as OptionDataValue; if (extendParams && zrUtil.isArray(extendParams.interpolatedValue)) { - const dimInfo = data.getDimensionInfo(dimLoose); - if (dimInfo) { - val = extendParams.interpolatedValue[dimInfo.index]; + const dimIndex = data.getDimensionIndex(dimLoose); + if (dimIndex >= 0) { + val = extendParams.interpolatedValue[dimIndex]; } } diff --git a/src/processor/dataFilter.ts b/src/processor/dataFilter.ts index 08393ebd77..d6d704c2f1 100644 --- a/src/processor/dataFilter.ts +++ b/src/processor/dataFilter.ts @@ -1,5 +1,3 @@ -import { StageHandler } from '../util/types'; - /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file @@ -19,6 +17,8 @@ import { StageHandler } from '../util/types'; * under the License. */ +import { StageHandler } from '../util/types'; + export default function dataFilter(seriesType: string): StageHandler { return { seriesType: seriesType, diff --git a/src/processor/dataStack.ts b/src/processor/dataStack.ts index e9559cbad2..42f7779ba6 100644 --- a/src/processor/dataStack.ts +++ b/src/processor/dataStack.ts @@ -20,19 +20,22 @@ import {createHashMap, each} from 'zrender/src/core/util'; import GlobalModel from '../model/Global'; import SeriesModel from '../model/Series'; -import { SeriesOption, SeriesStackOptionMixin, DimensionName } from '../util/types'; -import List from '../data/List'; +import { SeriesOption, SeriesStackOptionMixin } from '../util/types'; +import SeriesData, { DataCalculationInfo } from '../data/SeriesData'; import { addSafe } from '../util/number'; -interface StackInfo { - stackedDimension: DimensionName - isStackedByIndex: boolean - stackedByDimension: DimensionName - stackResultDimension: DimensionName - stackedOverDimension: DimensionName - data: List +type StackInfo = Pick< + DataCalculationInfo, + 'stackedDimension' + | 'isStackedByIndex' + | 'stackedByDimension' + | 'stackResultDimension' + | 'stackedOverDimension' +> & { + data: SeriesData seriesModel: SeriesModel -} +}; + // (1) [Caution]: the logic is correct based on the premises: // data processing stage is blocked in stream. // See @@ -87,7 +90,7 @@ function calculateStack(stackInfoList: StackInfo[]) { // Should not write on raw data, because stack series model list changes // depending on legend selection. - const newData = targetData.map(dims, function (v0, v1, dataIndex) { + targetData.modify(dims, function (v0, v1, dataIndex) { let sum = targetData.get(targetStackInfo.stackedDimension, dataIndex) as number; // Consider `connectNulls` of line area, if value is NaN, stackedOver @@ -141,9 +144,5 @@ function calculateStack(stackInfoList: StackInfo[]) { return resultVal; }); - - (targetData.hostModel as SeriesModel).setData(newData); - // Update for consequent calculation - targetStackInfo.data = newData; }); } diff --git a/src/processor/negativeDataFilter.ts b/src/processor/negativeDataFilter.ts new file mode 100644 index 0000000000..f0316a18ad --- /dev/null +++ b/src/processor/negativeDataFilter.ts @@ -0,0 +1,38 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +import { StageHandler } from '../util/types'; + +export default function negativeDataFilter(seriesType: string): StageHandler { + return { + seriesType: seriesType, + reset: function (seriesModel, ecModel) { + const data = seriesModel.getData(); + data.filterSelf(function (idx) { + // handle negative value condition + const valueDim = data.mapDimension('value'); + const curValue = data.get(valueDim, idx); + if (typeof curValue === 'number' && !isNaN(curValue) && curValue < 0) { + return false; + } + return true; + }); + } + }; +} diff --git a/src/scale/Log.ts b/src/scale/Log.ts index 11a1c84fa7..f8c3258b49 100644 --- a/src/scale/Log.ts +++ b/src/scale/Log.ts @@ -24,7 +24,7 @@ import * as scaleHelper from './helper'; // Use some method of IntervalScale import IntervalScale from './Interval'; -import List from '../data/List'; +import SeriesData from '../data/SeriesData'; import { DimensionName, ScaleTick } from '../util/types'; const scaleProto = Scale.prototype; @@ -118,7 +118,7 @@ class LogScale extends Scale { scaleProto.unionExtent.call(this, extent); } - unionExtentFromData(data: List, dim: DimensionName): void { + unionExtentFromData(data: SeriesData, dim: DimensionName): void { // TODO // filter value that <= 0 this.unionExtent(data.getApproximateExtent(dim)); diff --git a/src/scale/Ordinal.ts b/src/scale/Ordinal.ts index 76f515a0c5..7f23c2d4cc 100644 --- a/src/scale/Ordinal.ts +++ b/src/scale/Ordinal.ts @@ -26,7 +26,7 @@ import Scale from './Scale'; import OrdinalMeta from '../data/OrdinalMeta'; -import List from '../data/List'; +import SeriesData from '../data/SeriesData'; import * as scaleHelper from './helper'; import { OrdinalRawValue, @@ -263,7 +263,7 @@ class OrdinalScale extends Scale { return this._extent[1] - this._extent[0] + 1; } - unionExtentFromData(data: List, dim: DimensionLoose) { + unionExtentFromData(data: SeriesData, dim: DimensionLoose) { this.unionExtent(data.getApproximateExtent(dim)); } diff --git a/src/scale/Scale.ts b/src/scale/Scale.ts index 701013c5d9..afae3f46b7 100644 --- a/src/scale/Scale.ts +++ b/src/scale/Scale.ts @@ -20,7 +20,7 @@ import * as clazzUtil from '../util/clazz'; import { Dictionary } from 'zrender/src/core/types'; -import List from '../data/List'; +import SeriesData from '../data/SeriesData'; import { DimensionName, ScaleDataValue, @@ -91,7 +91,7 @@ abstract class Scale = Dictionary> /** * Set extent from data */ - unionExtentFromData(data: List, dim: DimensionName | DimensionLoose): void { + unionExtentFromData(data: SeriesData, dim: DimensionName | DimensionLoose): void { this.unionExtent(data.getApproximateExtent(dim)); } diff --git a/src/util/graphic.ts b/src/util/graphic.ts index 5823c056fe..fb40300c40 100644 --- a/src/util/graphic.ts +++ b/src/util/graphic.ts @@ -49,11 +49,8 @@ import Element from 'zrender/src/Element'; import Model from '../model/Model'; import { AnimationOptionMixin, - AnimationDelayCallbackParam, ZRRectLike, ZRStyleProps, - PayloadAnimationPart, - AnimationOption, CommonTooltipOption, ComponentItemTooltipLabelFormatterParams } from './types'; @@ -62,18 +59,29 @@ import { isArrayLike, map, defaults, - isObject, - retrieve2, isString, keys, each, hasOwn } from 'zrender/src/core/util'; -import { AnimationEasing } from 'zrender/src/animation/easing'; import { getECData } from './innerStore'; import ComponentModel from '../model/Component'; +import { + updateProps, + initProps, + removeElement, + removeElementWithFadeOut, + isElementRemoved +} from '../animation/basicTrasition'; + +/** + * @deprecated export for compatitable reason + */ +export {updateProps, initProps, removeElement, removeElementWithFadeOut, isElementRemoved}; + + const mathMax = Math.max; const mathMin = Math.min; @@ -297,262 +305,6 @@ export function subPixelOptimizeRect(param: { export const subPixelOptimize = subPixelOptimizeUtil.subPixelOptimize; -type AnimateOrSetPropsOption = { - dataIndex?: number; - cb?: () => void; - during?: (percent: number) => void; - removeOpt?: AnimationOption - isFrom?: boolean; -}; - -function animateOrSetProps( - animationType: 'init' | 'update' | 'remove', - el: Element, - props: Props, - animatableModel?: Model & { - getAnimationDelayParams?: (el: Element, dataIndex: number) => AnimationDelayCallbackParam - }, - dataIndex?: AnimateOrSetPropsOption['dataIndex'] | AnimateOrSetPropsOption['cb'] | AnimateOrSetPropsOption, - cb?: AnimateOrSetPropsOption['cb'] | AnimateOrSetPropsOption['during'], - during?: AnimateOrSetPropsOption['during'] -) { - let isFrom = false; - let removeOpt: AnimationOption; - if (typeof dataIndex === 'function') { - during = cb; - cb = dataIndex; - dataIndex = null; - } - else if (isObject(dataIndex)) { - cb = dataIndex.cb; - during = dataIndex.during; - isFrom = dataIndex.isFrom; - removeOpt = dataIndex.removeOpt; - dataIndex = dataIndex.dataIndex; - } - const isUpdate = animationType === 'update'; - const isRemove = animationType === 'remove'; - - let animationPayload: PayloadAnimationPart; - // Check if there is global animation configuration from dataZoom/resize can override the config in option. - // If animation is enabled. Will use this animation config in payload. - // If animation is disabled. Just ignore it. - if (animatableModel && animatableModel.ecModel) { - const updatePayload = animatableModel.ecModel.getUpdatePayload(); - animationPayload = (updatePayload && updatePayload.animation) as PayloadAnimationPart; - } - const animationEnabled = animatableModel && animatableModel.isAnimationEnabled(); - - if (!isRemove) { - // Must stop the remove animation. - el.stopAnimation('remove'); - } - - if (animationEnabled) { - let duration: number | Function; - let animationEasing: AnimationEasing; - let animationDelay: number | Function; - if (animationPayload) { - duration = animationPayload.duration || 0; - animationEasing = animationPayload.easing || 'cubicOut'; - animationDelay = animationPayload.delay || 0; - } - else if (isRemove) { - removeOpt = removeOpt || {}; - duration = retrieve2(removeOpt.duration, 200); - animationEasing = retrieve2(removeOpt.easing, 'cubicOut'); - animationDelay = 0; - } - else { - duration = animatableModel.getShallow( - isUpdate ? 'animationDurationUpdate' : 'animationDuration' - ); - animationEasing = animatableModel.getShallow( - isUpdate ? 'animationEasingUpdate' : 'animationEasing' - ); - animationDelay = animatableModel.getShallow( - isUpdate ? 'animationDelayUpdate' : 'animationDelay' - ); - } - if (typeof animationDelay === 'function') { - animationDelay = animationDelay( - dataIndex as number, - animatableModel.getAnimationDelayParams - ? animatableModel.getAnimationDelayParams(el, dataIndex as number) - : null - ); - } - if (typeof duration === 'function') { - duration = duration(dataIndex as number); - } - - duration > 0 - ? ( - isFrom - ? el.animateFrom(props, { - duration: duration as number, - delay: animationDelay as number || 0, - easing: animationEasing, - done: cb, - force: !!cb || !!during, - scope: animationType, - during: during - }) - : el.animateTo(props, { - duration: duration as number, - delay: animationDelay as number || 0, - easing: animationEasing, - done: cb, - force: !!cb || !!during, - setToFinal: true, - scope: animationType, - during: during - }) - ) - // FIXME: - // If `duration` is 0, only the animation on props - // can be stoped, other animation should be continued? - // But at present using duration 0 in `animateTo`, `animateFrom` - // might cause unexpected behavior. - : ( - el.stopAnimation(), - // If `isFrom`, the props is the "from" props. - !isFrom && el.attr(props), - cb && (cb as AnimateOrSetPropsOption['cb'])() - ); - } - else { - el.stopAnimation(); - !isFrom && el.attr(props); - // Call during once. - during && during(1); - cb && (cb as AnimateOrSetPropsOption['cb'])(); - } -} - -/** - * Update graphic element properties with or without animation according to the - * configuration in series. - * - * Caution: this method will stop previous animation. - * So do not use this method to one element twice before - * animation starts, unless you know what you are doing. - * @example - * graphic.updateProps(el, { - * position: [100, 100] - * }, seriesModel, dataIndex, function () { console.log('Animation done!'); }); - * // Or - * graphic.updateProps(el, { - * position: [100, 100] - * }, seriesModel, function () { console.log('Animation done!'); }); - */ -function updateProps( - el: Element, - props: Props, - // TODO: TYPE AnimatableModel - animatableModel?: Model, - dataIndex?: AnimateOrSetPropsOption['dataIndex'] | AnimateOrSetPropsOption['cb'] | AnimateOrSetPropsOption, - cb?: AnimateOrSetPropsOption['cb'] | AnimateOrSetPropsOption['during'], - during?: AnimateOrSetPropsOption['during'] -) { - animateOrSetProps('update', el, props, animatableModel, dataIndex, cb, during); -} - -export {updateProps}; - -/** - * Init graphic element properties with or without animation according to the - * configuration in series. - * - * Caution: this method will stop previous animation. - * So do not use this method to one element twice before - * animation starts, unless you know what you are doing. - */ -export function initProps( - el: Element, - props: Props, - animatableModel?: Model, - dataIndex?: AnimateOrSetPropsOption['dataIndex'] | AnimateOrSetPropsOption['cb'] | AnimateOrSetPropsOption, - cb?: AnimateOrSetPropsOption['cb'] | AnimateOrSetPropsOption['during'], - during?: AnimateOrSetPropsOption['during'] -) { - animateOrSetProps('init', el, props, animatableModel, dataIndex, cb, during); -} - -/** - * Remove graphic element - */ -export function removeElement( - el: Element, - props: Props, - animatableModel?: Model, - dataIndex?: AnimateOrSetPropsOption['dataIndex'] | AnimateOrSetPropsOption['cb'] | AnimateOrSetPropsOption, - cb?: AnimateOrSetPropsOption['cb'] | AnimateOrSetPropsOption['during'], - during?: AnimateOrSetPropsOption['during'] -) { - // Don't do remove animation twice. - if (isElementRemoved(el)) { - return; - } - - animateOrSetProps('remove', el, props, animatableModel, dataIndex, cb, during); -} - -function fadeOutDisplayable( - el: Displayable, - animatableModel?: Model, - dataIndex?: number, - done?: AnimateOrSetPropsOption['cb'] -) { - el.removeTextContent(); - el.removeTextGuideLine(); - removeElement(el, { - style: { - opacity: 0 - } - }, animatableModel, dataIndex, done); -} - -export function removeElementWithFadeOut( - el: Element, - animatableModel?: Model, - dataIndex?: number -) { - function doRemove() { - el.parent && el.parent.remove(el); - } - // Hide label and labelLine first - // TODO Also use fade out animation? - if (!el.isGroup) { - fadeOutDisplayable(el as Displayable, animatableModel, dataIndex, doRemove); - } - else { - (el as Group).traverse(function (disp: Displayable) { - if (!disp.isGroup) { - // Can invoke doRemove multiple times. - fadeOutDisplayable(disp as Displayable, animatableModel, dataIndex, doRemove); - } - }); - } -} - -/** - * If element is removed. - * It can determine if element is having remove animation. - */ -export function isElementRemoved(el: Element) { - if (!el.__zr) { - return true; - } - for (let i = 0; i < el.animators.length; i++) { - const animator = el.animators[i]; - if (animator.scope === 'remove') { - return true; - } - } - return false; -} - /** * Get transform matrix of target (param target), * in coordinate of its ancestor (param ancestor) diff --git a/src/util/innerStore.ts b/src/util/innerStore.ts index 9fa4355046..dbaaecf040 100644 --- a/src/util/innerStore.ts +++ b/src/util/innerStore.ts @@ -51,4 +51,26 @@ export interface ECData { option: ComponentItemTooltipOption; }; } + export const getECData = makeInner(); + +export const setCommonECData = (seriesIndex: number, dataType: SeriesDataType, dataIdx: number, el: Element) => { + if (el) { + const ecData = getECData(el); + // Add data index and series index for indexing the data by element + // Useful in tooltip + ecData.dataIndex = dataIdx; + ecData.dataType = dataType; + ecData.seriesIndex = seriesIndex; + + // TODO: not store dataIndex on children. + if (el.type === 'group') { + el.traverse(function (child: Element): void { + const childECData = getECData(child); + childECData.seriesIndex = seriesIndex; + childECData.dataIndex = dataIdx; + childECData.dataType = dataType; + }); + } + } +}; diff --git a/src/util/model.ts b/src/util/model.ts index b640ec6017..9f78c1583e 100644 --- a/src/util/model.ts +++ b/src/util/model.ts @@ -32,7 +32,7 @@ import { import env from 'zrender/src/core/env'; import GlobalModel from '../model/Global'; import ComponentModel, {ComponentModelConstructor} from '../model/Component'; -import List from '../data/List'; +import SeriesData from '../data/SeriesData'; import { ComponentOption, ComponentMainType, @@ -686,7 +686,7 @@ export function compressBatches( * each of which can be Array or primary type. * @return dataIndex If not found, return undefined/null. */ -export function queryDataIndex(data: List, payload: Payload & { +export function queryDataIndex(data: SeriesData, payload: Payload & { dataIndexInside?: number | number[] dataIndex?: number | number[] name?: string | string[] @@ -1032,7 +1032,7 @@ export function groupData( * Other cases do not supported. */ export function interpolateRawValues( - data: List, + data: SeriesData, precision: number | 'auto', sourceValue: InterpolatableValue, targetValue: InterpolatableValue, @@ -1070,7 +1070,7 @@ export function interpolateRawValues( for (let i = 0; i < length; ++i) { const info = data.getDimensionInfo(i); // Don't interpolate ordinal dims - if (info.type === 'ordinal') { + if (info && info.type === 'ordinal') { // In init, there is no `sourceValue`, but should better not to get undefined result. interpolated[i] = (percent < 1 && leftArr ? leftArr : rightArr)[i] as number; } diff --git a/src/util/number.ts b/src/util/number.ts index b916053280..2ec02f3f8d 100644 --- a/src/util/number.ts +++ b/src/util/number.ts @@ -359,7 +359,7 @@ export function parseDate(value: unknown): Date { +match[4] || 0, +(match[5] || 0), +match[6] || 0, - +match[7] || 0 + match[7] ? +match[7].substring(0, 3) : 0 ); } // Timezoneoffset of Javascript Date has considered DST (Daylight Saving Time, @@ -381,7 +381,7 @@ export function parseDate(value: unknown): Date { hour, +(match[5] || 0), +match[6] || 0, - +match[7] || 0 + match[7] ? +match[7].substring(0, 3) : 0 )); } } diff --git a/src/util/quickSelect.ts b/src/util/quickSelect.ts index abb382270f..e66fac51ba 100644 --- a/src/util/quickSelect.ts +++ b/src/util/quickSelect.ts @@ -67,7 +67,6 @@ function select(arr: T[], left: number, right: number, nth: number, compareFu /** * @example - * let quickSelect = require('echarts/core/quickSelect'); * let arr = [5, 2, 1, 4, 3] * quickSelect(arr, 3); * quickSelect(arr, 0, 3, 1, function (a, b) {return a - b}); diff --git a/src/util/states.ts b/src/util/states.ts index f977fbd9b3..ac5e01c890 100644 --- a/src/util/states.ts +++ b/src/util/states.ts @@ -41,7 +41,7 @@ import { import { extend, indexOf, isArrayLike, isObject, keys, isArray, each } from 'zrender/src/core/util'; import { getECData } from './innerStore'; import * as colorTool from 'zrender/src/tool/color'; -import List from '../data/List'; +import SeriesData from '../data/SeriesData'; import SeriesModel from '../model/Series'; import { CoordinateSystemMaster, CoordinateSystem } from '../coord/CoordinateSystem'; import { queryDataIndex, makeInner } from './model'; @@ -236,9 +236,17 @@ function createEmphasisDefaultState( const fromStroke = hasSelect ? (store.selectStroke || store.normalStroke) : store.normalStroke; if (hasFillOrStroke(fromFill) || hasFillOrStroke(fromStroke)) { state = state || {}; - // Apply default color lift let emphasisStyle = state.style || {}; - if (!hasFillOrStroke(emphasisStyle.fill) && hasFillOrStroke(fromFill)) { + + // inherit case + if (emphasisStyle.fill === 'inherit') { + cloned = true; + state = extend({}, state); + emphasisStyle = extend({}, emphasisStyle); + emphasisStyle.fill = fromFill; + } + // Apply default color lift + else if (!hasFillOrStroke(emphasisStyle.fill) && hasFillOrStroke(fromFill)) { cloned = true; // Not modify the original value. state = extend({}, state); @@ -414,7 +422,7 @@ export function blurSeries( const ecModel = api.getModel(); blurScope = blurScope || 'coordinateSystem'; - function leaveBlurOfIndices(data: List, dataIndices: ArrayLike) { + function leaveBlurOfIndices(data: SeriesData, dataIndices: ArrayLike) { for (let i = 0; i < dataIndices.length; i++) { const itemEl = data.getItemGraphicEl(dataIndices[i]); itemEl && leaveBlur(itemEl); diff --git a/src/util/styleCompat.ts b/src/util/styleCompat.ts index bcc817bd1b..d70a6017c9 100644 --- a/src/util/styleCompat.ts +++ b/src/util/styleCompat.ts @@ -85,6 +85,10 @@ export function convertFromEC4CompatibleStyle(hostStyle: ZRStyleProps, elType: s hasOwn(srcStyle, 'rich') && (textContentStyle.rich = srcStyle.rich); hasOwn(srcStyle, 'textFill') && (textContentStyle.fill = srcStyle.textFill); hasOwn(srcStyle, 'textStroke') && (textContentStyle.stroke = srcStyle.textStroke); + hasOwn(srcStyle, 'fontFamily') && (textContentStyle.fontFamily = srcStyle.fontFamily); + hasOwn(srcStyle, 'fontSize') && (textContentStyle.fontSize = srcStyle.fontSize); + hasOwn(srcStyle, 'fontStyle') && (textContentStyle.fontStyle = srcStyle.fontStyle); + hasOwn(srcStyle, 'fontWeight') && (textContentStyle.fontWeight = srcStyle.fontWeight); textContent = { type: 'text', diff --git a/src/util/symbol.ts b/src/util/symbol.ts index 5e1c00ffb3..602ea48e5c 100644 --- a/src/util/symbol.ts +++ b/src/util/symbol.ts @@ -19,12 +19,13 @@ // Symbol factory -import * as zrUtil from 'zrender/src/core/util'; +import { each, isArray, retrieve2 } from 'zrender/src/core/util'; import * as graphic from './graphic'; import BoundingRect from 'zrender/src/core/BoundingRect'; -import {calculateTextPosition} from 'zrender/src/contain/text'; +import { calculateTextPosition } from 'zrender/src/contain/text'; import { Dictionary } from 'zrender/src/core/types'; -import { ZRColor } from './types'; +import { SymbolOptionMixin, ZRColor } from './types'; +import { parsePercent } from './number'; export type ECSymbol = graphic.Path & { __isEmptyBrush?: boolean @@ -263,7 +264,7 @@ const symbolShapeMakers: Dictionary = { }; export const symbolBuildProxies: Dictionary = {}; -zrUtil.each(symbolCtors, function (Ctor, name) { +each(symbolCtors, function (Ctor, name) { symbolBuildProxies[name] = new Ctor(); }); @@ -384,3 +385,26 @@ export function createSymbol( return symbolPath as ECSymbol; } + +export function normalizeSymbolSize(symbolSize: number | number[]): [number, number] { + if (!isArray(symbolSize)) { + symbolSize = [+symbolSize, +symbolSize]; + } + return [symbolSize[0] || 0, symbolSize[1] || 0]; +} + +export function normalizeSymbolOffset( + symbolOffset: SymbolOptionMixin['symbolOffset'], + symbolSize: number[] +): [number, number] { + if (symbolOffset == null) { + return; + } + if (!isArray(symbolOffset)) { + symbolOffset = [symbolOffset, symbolOffset]; + } + return [ + parsePercent(symbolOffset[0], symbolSize[0]) || 0, + parsePercent(retrieve2(symbolOffset[1], symbolOffset[0]), symbolSize[1]) || 0 + ]; +} diff --git a/src/util/time.ts b/src/util/time.ts index d71a8daff9..72878a6858 100644 --- a/src/util/time.ts +++ b/src/util/time.ts @@ -37,8 +37,8 @@ export const defaultLeveledFormatter = { hour: '{HH}:{mm}', minute: '{HH}:{mm}', second: '{HH}:{mm}:{ss}', - millisecond: '{hh}:{mm}:{ss} {SSS}', - none: '{yyyy}-{MM}-{dd} {hh}:{mm}:{ss} {SSS}' + millisecond: '{HH}:{mm}:{ss} {SSS}', + none: '{yyyy}-{MM}-{dd} {HH}:{mm}:{ss} {SSS}' }; const fullDayFormatter = '{yyyy}-{MM}-{dd}'; @@ -318,7 +318,7 @@ export function secondsGetterName(isUTC: boolean) { } export function millisecondsGetterName(isUTC: boolean) { - return isUTC ? 'getUTCSeconds' : 'getSeconds'; + return isUTC ? 'getUTCMilliseconds' : 'getMilliseconds'; } export function fullYearSetterName(isUTC: boolean) { @@ -346,5 +346,5 @@ export function secondsSetterName(isUTC: boolean) { } export function millisecondsSetterName(isUTC: boolean) { - return isUTC ? 'setUTCSeconds' : 'setSeconds'; + return isUTC ? 'setUTCMilliseconds' : 'setMilliseconds'; } diff --git a/src/util/types.ts b/src/util/types.ts index 1d11e52a92..ab1a6b1b2b 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -33,7 +33,7 @@ import ExtensionAPI from '../core/ExtensionAPI'; import SeriesModel from '../model/Series'; import { createHashMap, HashMap } from 'zrender/src/core/util'; import { TaskPlanCallbackReturn, TaskProgressParams } from '../core/task'; -import List, {ListDimensionType} from '../data/List'; +import SeriesData from '../data/SeriesData'; import { Dictionary, ElementEventName, ImageLike, TextAlign, TextVerticalAlign } from 'zrender/src/core/types'; import { PatternObject } from 'zrender/src/graphic/Pattern'; import { TooltipMarker } from './format'; @@ -47,6 +47,8 @@ import { ImageStyleProps } from 'zrender/src/graphic/Image'; import ZRText, { TextStyleProps } from 'zrender/src/graphic/Text'; import { Source } from '../data/Source'; import Model from '../model/Model'; +import { DataStoreDimensionType } from '../data/DataStore'; +import { DimensionUserOuputEncode } from '../data/helper/dimensionHelper'; @@ -57,6 +59,7 @@ import Model from '../model/Model'; export {Dictionary}; export type RendererType = 'canvas' | 'svg'; +export type NullUndefined = null | undefined; export type LayoutOrient = 'vertical' | 'horizontal'; export type HorizontalAlign = 'left' | 'center' | 'right'; @@ -121,10 +124,14 @@ export interface ECElement extends Element { * Force disable overall layout */ disableLabelLayout?: boolean + /** + * Force disable morphing + */ + disableMorphing?: boolean } export interface DataHost { - getData(dataType?: SeriesDataType): List; + getData(dataType?: SeriesDataType): SeriesData; } export interface DataModel extends Model, DataHost, DataFormatMixin {} @@ -296,8 +303,8 @@ export interface StageHandlerInternal extends StageHandler { export type StageHandlerProgressParams = TaskProgressParams; export interface StageHandlerProgressExecutor { - dataEach?: (data: List, idx: number) => void; - progress?: (params: StageHandlerProgressParams, data: List) => void; + dataEach?: (data: SeriesData, idx: number) => void; + progress?: (params: StageHandlerProgressParams, data: SeriesData) => void; } export type StageHandlerPlanReturn = TaskPlanCallbackReturn; export interface StageHandlerPlan { @@ -351,11 +358,11 @@ export type OrdinalSortInfo = { /** * `OptionDataValue` is the primitive value in `series.data` or `dataset.source`. * `OptionDataValue` are parsed (see `src/data/helper/dataValueHelper.parseDataValue`) - * into `ParsedValue` and stored into `data/List` storage. + * into `ParsedValue` and stored into `data/SeriesData` storage. * Note: * (1) The term "parse" does not mean `src/scale/Scale['parse']`. * (2) If a category dimension is not mapped to any axis, its raw value will NOT be - * parsed to `OrdinalNumber` but keep the original `OrdinalRawValue` in `src/data/List` storage. + * parsed to `OrdinalNumber` but keep the original `OrdinalRawValue` in `src/data/SeriesData` storage. */ export type ParsedValue = ParsedValueNumeric | OrdinalRawValue; export type ParsedValueNumeric = number | OrdinalNumber; @@ -410,10 +417,10 @@ export type DimensionIndex = number; export type DimensionIndexLoose = DimensionIndex | string; export type DimensionName = string; export type DimensionLoose = DimensionName | DimensionIndexLoose; -export type DimensionType = ListDimensionType; +export type DimensionType = DataStoreDimensionType; export const VISUAL_DIMENSIONS = createHashMap([ - 'tooltip', 'label', 'itemName', 'itemId', 'seriesName' + 'tooltip', 'label', 'itemName', 'itemId', 'itemGroupId', 'seriesName' ]); // The key is VISUAL_DIMENSIONS export interface DataVisualDimensions { @@ -424,11 +431,12 @@ export interface DataVisualDimensions { label?: DimensionIndex; itemName?: DimensionIndex; itemId?: DimensionIndex; + itemGroupId?: DimensionIndex; seriesName?: DimensionIndex; } export type DimensionDefinition = { - type?: ListDimensionType, + type?: DataStoreDimensionType, name?: DimensionName, displayName?: string }; @@ -597,6 +605,7 @@ export type OptionDataItem = export type OptionDataItemObject = { id?: OptionId; name?: OptionName; + groupId?: OptionId; value?: T[] | T; selected?: boolean; }; @@ -641,6 +650,11 @@ export interface OptionEncodeVisualDimensions { itemId?: OptionEncodeValue; seriesName?: OptionEncodeValue; // Notice: `value` is coordDim, not nonCoordDim. + + // Group id is used for linking the aggregate relationship between two set of data. + // Which is useful in prepresenting the transition key of drilldown/up animation. + // Or hover linking. + itemGroupId?: OptionEncodeValue; } export interface OptionEncode extends OptionEncodeVisualDimensions { [coordDim: string]: OptionEncodeValue | undefined @@ -679,16 +693,6 @@ export interface CallbackDataParams { $vars: string[]; } export type InterpolatableValue = ParsedValue | ParsedValue[]; -export type DimensionUserOuputEncode = { - [coordOrVisualDimName: string]: - // index: coordDimIndex, value: dataDimIndex - DimensionIndex[] -}; -export type DimensionUserOuput = { - // The same as `data.dimensions` - dimensionNames: DimensionName[] - encode: DimensionUserOuputEncode -}; export type DecalDashArrayX = number | (number | number[])[]; export type DecalDashArrayY = number | number[]; @@ -841,6 +845,12 @@ export interface BorderOptionMixin { borderMiterLimit?: number } +export type ColorBy = 'series' | 'data'; + +export interface SunburstColorByMixin { + colorBy?: ColorBy +} + export type AnimationDelayCallbackParam = { count: number index: number @@ -922,7 +932,7 @@ export interface RoamOptionMixin { export type SymbolSizeCallback = (rawValue: any, params: T) => number | number[]; export type SymbolCallback = (rawValue: any, params: T) => string; export type SymbolRotateCallback = (rawValue: any, params: T) => number; -export type SymbolOffsetCallback = (rawValue: any, params: T) => (string | number)[]; +export type SymbolOffsetCallback = (rawValue: any, params: T) => string | number | (string | number)[]; /** * Mixin of option set to control the element symbol. * Include type of symbol, and size of symbol. @@ -941,7 +951,7 @@ export interface SymbolOptionMixin { symbolKeepAspect?: boolean - symbolOffset?: (string | number)[] | (unknown extends T ? never : SymbolOffsetCallback) + symbolOffset?: string | number | (string | number)[] | (unknown extends T ? never : SymbolOffsetCallback) } /** @@ -1219,13 +1229,15 @@ export interface TooltipFormatterCallback { * For sync callback * params will be an array on axis trigger. */ - (params: T, asyncTicket: string): string | HTMLElement[] + (params: T, asyncTicket: string): string | HTMLElement | HTMLElement[] /** * For async callback. * Returned html string will be a placeholder when callback is not invoked. */ - (params: T, asyncTicket: string, callback: (cbTicket: string, htmlOrDomNodes: string | HTMLElement[]) => void) - : string | HTMLElement[] + ( + params: T, asyncTicket: string, + callback: (cbTicket: string, htmlOrDomNodes: string | HTMLElement | HTMLElement[]) => void + ) : string | HTMLElement | HTMLElement[] } type TooltipBuiltinPosition = 'inside' | 'top' | 'left' | 'right' | 'bottom'; @@ -1476,7 +1488,6 @@ export interface ComponentOption { z?: number; zlevel?: number; - // FIXME:TS more } export type BlurScope = 'coordinateSystem' | 'series' | 'global'; @@ -1537,6 +1548,30 @@ export interface StatesOptionMixin number + /** + * How to divide the shape in combine and split animation. + */ + divideShape?: 'clone' | 'split' + /** + * Series will have transition between if they have same seriesKey. + * Usually it is a string. It can also be an array, + * which means it can be transition from or to multiple series with each key in this array item. + * + * Note: + * If two series have both array seriesKey. They will be compared after concated to a string(which is order independent) + * Transition between string key has higher priority. + * + * Default to use series id. + */ + seriesKey?: string | string[] +} + export interface SeriesOption< StateOption=any, ExtraStateOpts extends ExtraStateOptsBase = DefaultExtraStateOpts> extends ComponentOption, @@ -1555,9 +1590,18 @@ export interface SeriesOption< */ cursor?: string + /** + * groupId of data. can be used for doing drilldown / up animation + * It will be ignored if: + * - groupId is specified in each data + * - encode.itemGroupId is given. + */ + dataGroupId?: OptionId // Needs to be override data?: unknown + colorBy?: ColorBy + legendHoverLink?: boolean /** @@ -1572,7 +1616,6 @@ export interface SeriesOption< coordinateSystem?: string hoverLayerThreshold?: number - // FIXME:TS more /** * When dataset is used, seriesLayoutBy specifies whether the column or the row of dataset is mapped to the series @@ -1593,6 +1636,14 @@ export interface SeriesOption< */ stateAnimation?: AnimationOption + /** + * If enabled universal transition cross series. + * @example + * universalTransition: true + * universalTransition: { enabled: true } + */ + universalTransition?: boolean | UniversalTransitionOption + /** * Map of selected data * key is name or index of data. @@ -1610,11 +1661,8 @@ export interface SeriesOnCartesianOptionMixin { } export interface SeriesOnPolarOptionMixin { - radiusAxisIndex?: number - angleAxisIndex?: number - - radiusAxisId?: string - angleAxisId?: string + polarIndex?: number + polarId?: string; } export interface SeriesOnSingleOptionMixin { diff --git a/src/view/Chart.ts b/src/view/Chart.ts index eb9db4a2d1..8f32838f3f 100644 --- a/src/view/Chart.ts +++ b/src/view/Chart.ts @@ -34,7 +34,7 @@ import { StageHandlerPlanReturn, DisplayState, StageHandlerProgressParams, ECElementEvent } from '../util/types'; import { SeriesTaskContext, SeriesTask } from '../core/Scheduler'; -import List from '../data/List'; +import SeriesData from '../data/SeriesData'; const inner = modelUtil.makeInner<{ updateMethod: keyof ChartView @@ -206,7 +206,7 @@ function elSetState(el: Element, state: DisplayState, highlightDigit: number) { } } -function toggleHighlight(data: List, payload: Payload, state: DisplayState) { +function toggleHighlight(data: SeriesData, payload: Payload, state: DisplayState) { const dataIndex = modelUtil.queryDataIndex(data, payload); const highlightDigit = (payload && payload.highlightKey != null) diff --git a/src/visual/LegendVisualProvider.ts b/src/visual/LegendVisualProvider.ts index c62297eadd..c4ff79a3f8 100644 --- a/src/visual/LegendVisualProvider.ts +++ b/src/visual/LegendVisualProvider.ts @@ -1,4 +1,4 @@ -import List from '../data/List'; +import SeriesData from '../data/SeriesData'; /* * Licensed to the Apache Software Foundation (ASF) under one @@ -26,14 +26,14 @@ import List from '../data/List'; */ class LegendVisualProvider { - private _getDataWithEncodedVisual: () => List; - private _getRawData: () => List; + private _getDataWithEncodedVisual: () => SeriesData; + private _getRawData: () => SeriesData; constructor( // Function to get data after filtered. It stores all the encoding info - getDataWithEncodedVisual: () => List, + getDataWithEncodedVisual: () => SeriesData, // Function to get raw data before filtered. - getRawData: () => List + getRawData: () => SeriesData ) { this._getDataWithEncodedVisual = getDataWithEncodedVisual; this._getRawData = getRawData; diff --git a/src/visual/aria.ts b/src/visual/aria.ts index 62b2eca2d4..9d2afa691f 100644 --- a/src/visual/aria.ts +++ b/src/visual/aria.ts @@ -67,8 +67,8 @@ export default function ariaVisual(ecModel: GlobalModel, api: ExtensionAPI) { // Each type of series use one scope. // Pie and funnel are using diferrent scopes const paletteScopeGroupByType = zrUtil.createHashMap(); - ecModel.eachSeries(function (seriesModel) { - if (!seriesModel.useColorPaletteOnData) { + ecModel.eachSeries((seriesModel: SeriesModel) => { + if (seriesModel.isColorBySeries()) { return; } let decalScope = paletteScopeGroupByType.get(seriesModel.type); @@ -79,7 +79,7 @@ export default function ariaVisual(ecModel: GlobalModel, api: ExtensionAPI) { inner(seriesModel).scope = decalScope; }); - ecModel.eachRawSeries(seriesModel => { + ecModel.eachRawSeries((seriesModel: SeriesModel) => { if (ecModel.isSeriesFiltered(seriesModel)) { return; } @@ -91,7 +91,7 @@ export default function ariaVisual(ecModel: GlobalModel, api: ExtensionAPI) { const data = seriesModel.getData(); - if (seriesModel.useColorPaletteOnData) { + if (!seriesModel.isColorBySeries()) { const dataAll = seriesModel.getRawData(); const idxMap: Dictionary = {}; const decalScope = inner(seriesModel).scope; diff --git a/src/visual/commonVisualTypes.ts b/src/visual/commonVisualTypes.ts index e279a4cfd2..7b812141ad 100644 --- a/src/visual/commonVisualTypes.ts +++ b/src/visual/commonVisualTypes.ts @@ -17,7 +17,7 @@ * under the License. */ -import { DefaultDataVisual } from '../data/List'; +import { DefaultDataVisual } from '../data/SeriesData'; export interface LineDataVisual extends DefaultDataVisual { fromSymbol: string diff --git a/src/visual/helper.ts b/src/visual/helper.ts index e419a9a524..5c60f6f99d 100644 --- a/src/visual/helper.ts +++ b/src/visual/helper.ts @@ -24,10 +24,10 @@ * In the List module storage: * 'style', 'symbol', 'symbolSize'... */ -import List from '../data/List'; +import SeriesData from '../data/SeriesData'; -export function getItemVisualFromData(data: List, dataIndex: number, key: string) { +export function getItemVisualFromData(data: SeriesData, dataIndex: number, key: string) { switch (key) { case 'color': const style = data.getItemVisual(dataIndex, 'style'); @@ -45,7 +45,7 @@ export function getItemVisualFromData(data: List, dataIndex: number, key: string } } -export function getVisualFromData(data: List, key: string) { +export function getVisualFromData(data: SeriesData, key: string) { switch (key) { case 'color': const style = data.getVisual('style'); @@ -63,7 +63,7 @@ export function getVisualFromData(data: List, key: string) { } } -export function setItemVisualFromData(data: List, dataIndex: number, key: string, value: any) { +export function setItemVisualFromData(data: SeriesData, dataIndex: number, key: string, value: any) { switch (key) { case 'color': // Make sure not sharing style object. diff --git a/src/visual/style.ts b/src/visual/style.ts index 7d4a0c117f..8683a21484 100644 --- a/src/visual/style.ts +++ b/src/visual/style.ts @@ -18,7 +18,8 @@ */ import { isFunction, extend, createHashMap } from 'zrender/src/core/util'; -import { StageHandler, CallbackDataParams, ZRColor, Dictionary, InnerDecalObject } from '../util/types'; +import { StageHandler, CallbackDataParams, ZRColor, Dictionary, InnerDecalObject, SeriesOption } + from '../util/types'; import makeStyleMapper from '../model/mixin/makeStyleMapper'; import { ITEM_STYLE_KEY_MAP } from '../model/mixin/itemStyle'; import { LINE_STYLE_KEY_MAP } from '../model/mixin/lineStyle'; @@ -179,21 +180,23 @@ const dataColorPaletteTask: StageHandler = { // Each type of series use one scope. // Pie and funnel are using diferrent scopes const paletteScopeGroupByType = createHashMap(); - ecModel.eachSeries(function (seriesModel) { - if (!seriesModel.useColorPaletteOnData) { + ecModel.eachSeries((seriesModel: SeriesModel) => { + const colorBy = seriesModel.getColorBy(); + if (seriesModel.isColorBySeries()) { return; } - let colorScope = paletteScopeGroupByType.get(seriesModel.type); + const key = seriesModel.type + '-' + colorBy; + let colorScope = paletteScopeGroupByType.get(key); if (!colorScope) { colorScope = {}; - paletteScopeGroupByType.set(seriesModel.type, colorScope); + paletteScopeGroupByType.set(key, colorScope); } inner(seriesModel).scope = colorScope; }); - ecModel.eachSeries(function (seriesModel) { - if (!seriesModel.useColorPaletteOnData || ecModel.isSeriesFiltered(seriesModel)) { + ecModel.eachSeries((seriesModel: SeriesModel) => { + if (seriesModel.isColorBySeries() || ecModel.isSeriesFiltered(seriesModel)) { return; } diff --git a/src/visual/symbol.ts b/src/visual/symbol.ts index ae9f625c38..019d536177 100644 --- a/src/visual/symbol.ts +++ b/src/visual/symbol.ts @@ -28,7 +28,7 @@ import { SymbolRotateCallback, SymbolOffsetCallback } from '../util/types'; -import List from '../data/List'; +import SeriesData from '../data/SeriesData'; import SeriesModel from '../model/Series'; import GlobalModel from '../model/Global'; @@ -83,7 +83,7 @@ const seriesSymbolTask: StageHandler = { symbolSize: seriesSymbolSize as number | number[], symbolKeepAspect: keepAspect, symbolRotate: seriesSymbolRotate as number, - symbolOffset: seriesSymbolOffset as (string | number)[] + symbolOffset: seriesSymbolOffset as string | number | (string | number)[] }); // Only visible series has each data be visual encoded @@ -91,7 +91,7 @@ const seriesSymbolTask: StageHandler = { return; } - function dataEach(data: List, idx: number) { + function dataEach(data: SeriesData, idx: number) { const rawValue = seriesModel.getRawValue(idx); const params = seriesModel.getDataParams(idx); hasSymbolTypeCallback && data.setItemVisual( @@ -133,7 +133,7 @@ const dataSymbolTask: StageHandler = { const data = seriesModel.getData(); - function dataEach(data: List, idx: number) { + function dataEach(data: SeriesData, idx: number) { const itemModel = data.getItemModel(idx); const itemSymbolType = itemModel.getShallow('symbol', true); const itemSymbolSize = itemModel.getShallow('symbolSize', true); diff --git a/src/visual/visualSolution.ts b/src/visual/visualSolution.ts index accd72dd0f..33fffb5ee8 100644 --- a/src/visual/visualSolution.ts +++ b/src/visual/visualSolution.ts @@ -28,9 +28,10 @@ import { BuiltinVisualProperty, ParsedValue, DimensionLoose, - StageHandlerProgressExecutor + StageHandlerProgressExecutor, + DimensionIndex } from '../util/types'; -import List from '../data/List'; +import SeriesData from '../data/SeriesData'; import { getItemVisualFromData, setItemVisualFromData } from './helper'; const each = zrUtil.each; @@ -135,7 +136,7 @@ export function replaceVisualOption( export function applyVisual( stateList: readonly VisualState[], visualMappings: VisualMappingCollection, - data: List, + data: SeriesData, getValueState: (this: Scope, valueOrIndex: ParsedValue | number) => VisualState, scope?: Scope, dimension?: DimensionLoose @@ -209,9 +210,9 @@ export function incrementalApplyVisual( return { progress: function progress(params, data) { - let dimName: string; + let dimIndex: DimensionIndex; if (dim != null) { - dimName = data.getDimension(dim); + dimIndex = data.getDimensionIndex(dim); } function getVisual(key: string) { @@ -223,6 +224,7 @@ export function incrementalApplyVisual( } let dataIndex: number; + const store = data.getStore(); while ((dataIndex = params.next()) != null) { const rawDataItem = data.getRawDataItem(dataIndex); @@ -233,7 +235,7 @@ export function incrementalApplyVisual( } const value = dim != null - ? data.get(dimName, dataIndex) + ? store.get(dimIndex, dataIndex) : dataIndex; const valueState = getValueState(value); diff --git a/test/bar-polar-label.html b/test/bar-polar-label.html new file mode 100644 index 0000000000..0f6a1c5bd6 --- /dev/null +++ b/test/bar-polar-label.html @@ -0,0 +1,134 @@ + + + + + + + + + + + + +

Polar bar series label positions

+
+
+
+ + + diff --git a/test/bar-race.html b/test/bar-race.html index 12cb5202bf..3a50764569 100644 --- a/test/bar-race.html +++ b/test/bar-race.html @@ -26,6 +26,7 @@ + @@ -39,22 +40,12 @@ color: #eee; font-size: 14px; } - .chart { - height: 600px; - } -
- - - - - - -
- - -
+
+
+
+
@@ -171,8 +162,6 @@ 'echarts' ], function (echarts) { - var chart = echarts.init(document.getElementById('main2'), null, { - }); var option = { xAxis: { max: 'dataMax' @@ -230,8 +219,6 @@ animationEasingUpdate: 'linear' }; - chart.setOption(option); - setTimeout(() => { var data = option.series[0].data; data[1].value += 100; @@ -244,16 +231,28 @@ chart.setOption(option); }, 3400); + const buttons = []; + const texts = ['A++', 'B++', 'C++', 'D++']; for (var x = 0; x < 4; ++x) { (function (x) { - var btn = document.getElementById('btn-' + (x + 1)); - btn.onclick = function () { - var data = option.series[0].data; - data[x].value += Math.round(100 * Math.random()); - chart.setOption(option); - }; + buttons.push({ + text: texts[x], + onclick() { + var data = option.series[0].data; + data[x].value += Math.round(100 * Math.random()); + chart.setOption(option); + } + }); })(x); } + + var chart = testHelper.create(echarts, 'main2', { + title: [ + 'Update Data Dynamically' + ], + option: option, + buttons + }); } ); @@ -264,8 +263,7 @@ require(['echarts'], function (echarts) { - var chart = echarts.init(document.getElementById('main3'), null, { - }); + var option = { title: { text: 'When yAxis max is larger than yAxis data length, it should not get error' @@ -284,10 +282,64 @@ }] }; - chart.setOption(option); + var chart = testHelper.create(echarts, 'main3', { + title: [ + 'When yAxis max is larger than yAxis data length, it should not get error' + ], + option: option + }); } ); + + diff --git a/test/bar-race2.html b/test/bar-race2.html index bbd0a9ce2f..037571acc7 100644 --- a/test/bar-race2.html +++ b/test/bar-race2.html @@ -330,6 +330,7 @@ realtimeSort: true, label: { show: true, + valueAnimation: true, position: 'top' }, data: [ @@ -392,6 +393,7 @@ realtimeSort: true, label: { show: true, + valueAnimation: true, position: 'top' }, data: data @@ -470,6 +472,7 @@ realtimeSort: true, label: { show: true, + valueAnimation: true, position: 'top' }, data: data diff --git a/test/bar-stack.html b/test/bar-stack.html index 4c47b61ac3..51ce177f2f 100644 --- a/test/bar-stack.html +++ b/test/bar-stack.html @@ -35,78 +35,10 @@ -
- - - - - +
+
+
@@ -510,5 +442,289 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/candlestick-case.html b/test/candlestick-case.html new file mode 100644 index 0000000000..2e5a33f747 --- /dev/null +++ b/test/candlestick-case.html @@ -0,0 +1,369 @@ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + diff --git a/test/circle-packing-with-d3.compat.html b/test/circle-packing-with-d3.compat.html index 3c4418b514..c4a300669b 100644 --- a/test/circle-packing-with-d3.compat.html +++ b/test/circle-packing-with-d3.compat.html @@ -131,18 +131,6 @@ } var option = { - xAxis: { - axisLine: {show: false}, - axisTick: {show: false}, - axisLabel: {show: false}, - splitLine: {show: false} - }, - yAxis: { - axisLine: {show: false}, - axisTick: {show: false}, - axisLabel: {show: false}, - splitLine: {show: false} - }, tooltip: {}, visualMap: { show: false, @@ -156,6 +144,7 @@ series: { type: 'custom', renderItem: renderItem, + coordinateSystem: null, encode: { tooltip: 0, itemName: 2 diff --git a/test/circle-packing-with-d3.html b/test/circle-packing-with-d3.html index 55082daedb..519a3edf12 100644 --- a/test/circle-packing-with-d3.html +++ b/test/circle-packing-with-d3.html @@ -78,7 +78,7 @@ function renderItem(params, api) { var context = params.context; - if (!context.layout) { + if (!context.nodes) { d3.pack() .size([api.getWidth() - 2, api.getHeight() - 2]) .padding(3)(root); @@ -156,18 +156,6 @@ } var option = { - xAxis: { - axisLine: {show: false}, - axisTick: {show: false}, - axisLabel: {show: false}, - splitLine: {show: false} - }, - yAxis: { - axisLine: {show: false}, - axisTick: {show: false}, - axisLabel: {show: false}, - splitLine: {show: false} - }, tooltip: {}, visualMap: { show: false, @@ -181,6 +169,7 @@ series: { type: 'custom', renderItem: renderItem, + coordinateSystem: null, encode: { tooltip: 0, itemName: 2 @@ -191,13 +180,6 @@ chart.setOption(option); - // testHelper.printElements(chart, { - // attr: ['z', 'z2', 'style.text', 'style.fill', 'style.stroke'], - // filter: function (el) { - // return el.style && el.style.text; - // } - // }); - }); diff --git a/test/colorBy.html b/test/colorBy.html new file mode 100644 index 0000000000..4e7431894c --- /dev/null +++ b/test/colorBy.html @@ -0,0 +1,233 @@ + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + +
+ + +
+ + +
+ + + + + + + + + + + + + + + + + + diff --git a/test/custom-hexbin.html b/test/custom-hexbin.html index 1ed29dcfb8..91a3ee7f63 100644 --- a/test/custom-hexbin.html +++ b/test/custom-hexbin.html @@ -177,7 +177,6 @@

Hexagonal Binning

type: 'group', children: [{ type, - morph: true, shape: type === 'polygon' ? { points: points } : { @@ -193,7 +192,6 @@

Hexagonal Binning

} }, { type, - morph: true, shape: type === 'polygon' ? { points: pointsBG } : { @@ -321,6 +319,9 @@

Hexagonal Binning

myChart.setOption({ series: [{ type: 'custom', + universalTransition: { + enabled: true + }, renderItem: createItemRenderer('polygon') }] }); @@ -331,6 +332,9 @@

Hexagonal Binning

myChart.setOption({ series: [{ type: 'custom', + universalTransition: { + enabled: true + }, renderItem: createItemRenderer('circle') }] }); diff --git a/test/custom-shape-morphing.html b/test/custom-shape-morphing.html index c5a98a0deb..38df70e6e9 100644 --- a/test/custom-shape-morphing.html +++ b/test/custom-shape-morphing.html @@ -24,7 +24,6 @@ - @@ -34,8 +33,14 @@ height: 100%; margin: 0; } + #next { + position: absolute; + left: 10px; + top: 10px; + }
+ diff --git a/test/custom-shape-morphing2.html b/test/custom-shape-morphing2.html index 6089feeaed..8b5d2bcdff 100644 --- a/test/custom-shape-morphing2.html +++ b/test/custom-shape-morphing2.html @@ -249,6 +249,9 @@ coordinateSystem: 'cartesian2d', animationDurationUpdate: ANIMATION_DURATION_UPDATE, datasetId: datasetId, + universalTransition: { + enabled: true + }, encode: { itemName: 'ID', x: 'STE', @@ -276,7 +279,6 @@ var symbolPath = getFromPalette(zTagVal, SYMBOL_PATHS); return { type: 'circle', - morph: true, shape: { cx: pos[0], cy: pos[1], @@ -301,7 +303,6 @@ var symbolPath = getFromPalette(zTagVal, SYMBOL_PATHS); return { type: 'path', - morph: true, x: pos[0], y: pos[1], shape: { @@ -332,7 +333,6 @@ y: pos[1], children: [{ type: 'polygon', - morph: true, shape: { points: [ [-40, -2], @@ -348,7 +348,6 @@ transition: ['shape', 'style'] }, { type: 'rect', - morph: true, shape: { x: -20, y: 0, @@ -392,9 +391,13 @@ coordinateSystem: 'cartesian2d', animationDurationUpdate: ANIMATION_DURATION_UPDATE, datasetId: datasetId, + universalTransition: { + enabled: true + }, encode: { x: 'M_TAG', y: 'ATA', + itemGroupId: 'M_TAG', tooltip: ['M_TAG', 'ATA'] }, renderItem: function (params, api) { @@ -406,7 +409,6 @@ var width = size[0] * 0.4; return { type: 'rect', - morph: true, shape: { x: tarPos[0] - width / 2, y: tarPos[1], @@ -442,6 +444,9 @@ value: 'ATA', tooltip: 'ATA' }, + universalTransition: { + enabled: true + }, renderItem: function (params, api) { var context = params.context; if (!context.layout) { @@ -467,7 +472,6 @@ var height = chart.getHeight(); return { type: 'sector', - morph: true, shape: { cx: width / 2, cy: height / 2, @@ -519,9 +523,13 @@ coordinateSystem: 'cartesian2d', animationDurationUpdate: ANIMATION_DURATION_UPDATE, datasetId: datasetId, + universalTransition: { + enabled: true + }, encode: { x: 'CLUSTER_CENTER_STE', y: 'CLUSTER_CENTER_ATA', + itemGroupId: 'CLUSTER_IDX', tooltip: ['CLUSTER_CENTER_STE', 'CLUSTER_CENTER_ATA'] }, renderItem: function (params, api) { @@ -542,7 +550,6 @@ var radius = count / context.totalCount * 100 + 10; return { type: 'circle', - morph: true, shape: { cx: pos[0], cy: pos[1], @@ -565,53 +572,20 @@ - var optionList = []; + var options = {}; var buttons = []; + echarts.util.each(optionCreators, function (creator, key) { var optionWrap = creator(); - optionList.push({ - key: key, - dataMetaKey: optionWrap.datasetId, - option: optionWrap.option - }); + options[key] = Object.assign({}, baseOption, optionWrap.option); buttons.push({ text: key, onclick: function () { - player.go(key); + chart.setOption(options[key], true); } }); }); - - var player = ecSimpleOptionPlayer.create({ - chart: function () { - return chart; - }, - seriesIndex: 0, - replaceMerge: ['xAxis', 'yAxis'], - dataMeta: { - raw: { - dimensions: RAW_DATA_DIMENSIONS, - uniqueDimension: 'ID' - }, - mTagSum: { - dimensions: M_TAG_SUM_DIMENSIONS, - uniqueDimension: 'M_TAG' - }, - rawClusters: { - dimensions: RAW_CLUSTER_DIMENSIONS, - uniqueDimension: 'ID', - dividingMethod: 'duplicate' - }, - rawClusterCenters: { - dimensions: RAW_CLUSTER_CENTERS_DIMENSIONS, - uniqueDimension: 'CLUSTER_IDX' - } - }, - optionList: optionList - }); - - var chart = testHelper.create(echarts, 'main0', { title: [ 'Test: buttons, should morph animation combine/separate.', @@ -619,7 +593,7 @@ 'Test: click buttons **twice**, should no blink.', 'Test: use dataZoom, the "update animation" should exist' ], - option: baseOption, + option: options['Scatter_singleSVG_ATA_STE'], lazyUpdate: true, height: 600, buttons: buttons.concat([{ @@ -641,8 +615,6 @@ }]) }); - player.go('Scatter_singleSVG_ATA_STE'); - }); diff --git a/test/custom-shape-morphing3.html b/test/custom-shape-morphing3.html index 1345fa4f4f..a5765f0fdf 100644 --- a/test/custom-shape-morphing3.html +++ b/test/custom-shape-morphing3.html @@ -165,7 +165,6 @@ children: [{ type: 'rect', transition: ['shape', 'style'], - morph: true, shape: { x: basePos[0], y: basePos[1], @@ -218,7 +217,6 @@ children: [{ type: 'rect', transition: ['shape', 'style'], - morph: true, shape: { x: basePos[0], y: basePos[1], @@ -270,7 +268,6 @@ children: [{ type: 'circle', transition: ['shape', 'style'], - morph: true, shape: { cx: pos[0], cy: pos[1], @@ -323,7 +320,6 @@ children: [{ type: 'rect', transition: ['shape', 'style'], - morph: true, shape: { x: basePos[0], y: valPos[1] - height / 2, @@ -394,8 +390,7 @@ title: [ 'Test: buttons, should morph animation merge/split.', 'Test: click buttons **before animation finished**, should no blink.', - 'Test: click buttons **twice**, should no blink.', - 'Test: use dataZoom, update animation should exist' + 'Test: click buttons **twice**, should no blink.' ], option: baseOption, lazyUpdate: true, diff --git a/test/dataset-case.html b/test/dataset-case.html new file mode 100644 index 0000000000..577401433e --- /dev/null +++ b/test/dataset-case.html @@ -0,0 +1,746 @@ + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/dataset-performance.html b/test/dataset-performance.html new file mode 100644 index 0000000000..25e12f41eb --- /dev/null +++ b/test/dataset-performance.html @@ -0,0 +1,220 @@ + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + diff --git a/test/effectScatter.html b/test/effectScatter.html index 30ab909d85..bce8254dde 100644 --- a/test/effectScatter.html +++ b/test/effectScatter.html @@ -547,12 +547,17 @@ }); setInterval(function () { + var rippleEffectCount = ~~(Math.random() * 9) + 1; + console.log('rippleEffectCount', rippleEffectCount); myChart.setOption({ series: [{ name: 'Top 5', data: convertData(data.sort(function (a, b) { return b.value - a.value; - }).slice(0, Math.round(6 * Math.random()))) + }).slice(0, Math.round(6 * Math.random()))), + rippleEffect: { + count: rippleEffectCount + } }] }); }, 2000); @@ -561,4 +566,4 @@ - \ No newline at end of file + diff --git a/test/emphasis-inherit.html b/test/emphasis-inherit.html new file mode 100644 index 0000000000..0e7190c928 --- /dev/null +++ b/test/emphasis-inherit.html @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + diff --git a/test/geo-update.html b/test/geo-update.html index 6c57fe3c38..a68827dfbd 100644 --- a/test/geo-update.html +++ b/test/geo-update.html @@ -38,6 +38,7 @@
+
@@ -375,6 +376,54 @@ }); + diff --git a/test/geoScatter.html b/test/geoScatter.html index 52d8b524d6..b619b5cd13 100644 --- a/test/geoScatter.html +++ b/test/geoScatter.html @@ -271,6 +271,7 @@ data: ['scatter', 'scatter2'] }, geo: [{ + show: false, map: 'china', roam: true, left: 100, @@ -296,20 +297,6 @@ } } }] - }, { - map: 'china', - roam: true, - selectedMode: 'multiple', - left: null, - right: 100, - width: 300, - itemStyle: { - repeat: 'repeat', - areaColor: { - image: pattern, - repeat: 'repeat' - } - } }], tooltip: { trigger: 'axis', @@ -320,6 +307,14 @@ series: [] }); + setTimeout(function () { + chart.setOption({ + geo: { + show: true + } + }) + }, 2000) + chart.on('geoselectchanged', function (param) { console.log(param); }); diff --git a/test/hoverFocus.html b/test/hoverFocus.html index dbd9b8e28c..9fa288bb0e 100644 --- a/test/hoverFocus.html +++ b/test/hoverFocus.html @@ -82,7 +82,7 @@

Tests for focus and blurScope

allChartsOptions.forEach(function (chartOption) { chartOption.series.forEach(function (series) { series.emphasis = series.emphasis || {}; - series.emphasis.focus = seriesFocusType[series.type] || 'series'; + series.emphasis.focus = series.emphasis.focus || seriesFocusType[series.type] || 'series'; if (series.renderItem) { const oldRenderItem = series.renderItem; @@ -123,12 +123,254 @@

Tests for focus and blurScope

document.querySelector('#main').appendChild(dom); const chart = echarts.init(dom); - chart.setOption(chartOption); - charts.push(chart); }); + // testcases about dispatchAction: 'highlight' + (function addDispatchActionHighlightChart(){ + // Basic options for all charts + var polarData = []; + for (var i = 0; i <= 100; i++) { + var theta = (i / 100) * 360; + var r = 5 * (1 + Math.sin((theta / 180) * Math.PI)); + polarData.push([r, theta]); + } + + var seriesBlurEmphasis = { + focus: 'self', + blurScope: "series" + }; + var globalBlurEmphasis = { + focus: "series", + blurScope: "global", + }; + var coordinateSystemBlurEmphasis = { + focus: 'series', + blurScope: "coordinateSystem", + }; + + const options = [ + ////////////// Hightlight multilple series(blurScope = 'series') ////////////// + { + id: "highlightMultipleSeries(blurScope='series', focus='self')", + name: "Dispatch action to hightlight multiple series (with blurScope='series', focus='self')", + legend: { + top: 30, + data: [ + "Direct", + "Mail Ad", + "Affiliate Ad", + "Video Ad", + "Search Engine", + ], + }, + xAxis: { + type: "category", + data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"], + }, + yAxis: { + type: "value", + }, + series: [ + { + name: "Direct", + type: "line", + label: { + show: true, + }, + emphasis: seriesBlurEmphasis, + data: [320, 302, 301, 334, 390, 330, 320], + }, + { + name: "Mail Ad", + type: "line", + label: { + show: true, + }, + emphasis: seriesBlurEmphasis, + data: [120, 132, 101, 134, 90, 230, 210], + }, + { + name: "Affiliate Ad", + type: "line", + label: { + show: true, + }, + emphasis: seriesBlurEmphasis, + data: [220, 182, 191, 234, 290, 330, 310], + }, + { + name: "Video Ad", + type: "line", + label: { + show: true, + }, + emphasis: seriesBlurEmphasis, + data: [150, 212, 201, 154, 190, 330, 410], + }, + ], + }, + ////////////// Hightlight multilple series(blurScope = 'global') ////////////// + { + id: "highlightMultipleSeries(blurScope='global', focus='series')", + name: "Dispatch action to hightlight multiple series (with blurScope='global', focus='series')", + legend: { + top: 30, + data: [ + "Direct", + "Mail Ad", + "Affiliate Ad", + "Video Ad", + "Search Engine", + ], + }, + xAxis: { + type: "category", + data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"], + }, + yAxis: { + type: "value", + }, + series: [ + { + name: "Direct", + type: "line", + label: { + show: true, + }, + data: [320, 302, 301, 334, 390, 330, 320], + emphasis: globalBlurEmphasis, + }, + { + name: "Mail Ad", + type: "line", + label: { + show: true, + }, + data: [120, 132, 101, 134, 90, 230, 210], + emphasis: globalBlurEmphasis, + }, + { + name: "Affiliate Ad", + type: "line", + label: { + show: true, + }, + data: [220, 182, 191, 234, 290, 330, 310], + emphasis: globalBlurEmphasis, + }, + { + name: "Video Ad", + type: "line", + label: { + show: true, + }, + data: [150, 212, 201, 154, 190, 330, 410], + emphasis: globalBlurEmphasis, + }, + ], + }, + ////////////// Hightlight multilple series(blurScope = 'coordinateSystem') ////////////// + { + id: "highlightMultipleSeries(blurScope='coordinateSystem', focus='series')", + name: "Dispatch action to hightlight multiple series (with blurScope='coordinateSystem', focus='series')", + legend: { + top: 30, + data: [ + "polar", + "Direct", + "Mail Ad", + "Affiliate Ad", + "Video Ad", + "Search Engine", + ], + }, + xAxis: { + type: "category", + data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"], + }, + yAxis: { + type: "value", + }, + polar: {}, + + angleAxis: { + type: 'value', + startAngle: 0 + }, + radiusAxis: { + }, + series: [ + { + coordinateSystem: 'polar', + name: 'polar', + type: 'line', + data: polarData, + emphasis: coordinateSystemBlurEmphasis, + }, + { + name: "Direct", + type: "line", + label: { + show: true, + }, + data: [320, 302, 301, 334, 390, 330, 320], + emphasis: coordinateSystemBlurEmphasis, + }, + { + name: "Mail Ad", + type: "line", + label: { + show: true, + }, + data: [120, 132, 101, 134, 90, 230, 210], + emphasis: coordinateSystemBlurEmphasis, + }, + { + name: "Affiliate Ad", + type: "line", + label: { + show: true, + }, + data: [220, 182, 191, 234, 290, 330, 310], + emphasis: coordinateSystemBlurEmphasis, + }, + { + name: "Video Ad", + type: "line", + label: { + show: true, + }, + data: [150, 212, 201, 154, 190, 330, 410], + emphasis: coordinateSystemBlurEmphasis, + }, + ], + },] + for(const chartOption of options){ + const dom = document.createElement('div'); + dom.className = 'chart'; + document.querySelector('#main').appendChild(dom); + const chart = testHelper.create(echarts, dom, { + title: chartOption.name, + option: chartOption, + buttons: [ + { + text: 'Dispatch highlight action({seriesIndex: [1, 2], dataIndex: [0,1,2]})', + onclick: function () { + chart.dispatchAction({ + type: "highlight", + seriesIndex: [1, 2], + dataIndex: [0,1,2], + }); + } + }, + ], + }); + charts.push(chart); + } + })(); + const blurOpts = { opacity: 0.1, grayColor: false diff --git a/test/hoverFocus2.html b/test/hoverFocus2.html index 4cd8910798..3f739a2295 100644 --- a/test/hoverFocus2.html +++ b/test/hoverFocus2.html @@ -397,6 +397,14 @@ type: 'downplay' }); } + }, { + text: 'Highlight all', + onclick: function () { + chart.dispatchAction({ + type: 'highlight', + seriesIndex: [0, 1], + }); + } }); var chart = testHelper.create(echarts, 'main2', { diff --git a/test/lazyUpdate.html b/test/lazyUpdate.html index 7b9b0044a5..ff5daac1a4 100644 --- a/test/lazyUpdate.html +++ b/test/lazyUpdate.html @@ -166,7 +166,7 @@ console.time('update lazy'); update(true); console.timeEnd('update lazy'); - }, 1000); + }, 200); }); diff --git a/test/lib/reset.css b/test/lib/reset.css index fd34ca634e..6a2ddce4ed 100644 --- a/test/lib/reset.css +++ b/test/lib/reset.css @@ -82,6 +82,7 @@ pre.test-print-object { font-family: Menlo, Monaco, 'Courier New', monospace; } .test-chart { + position: relative; height: 400px; } @@ -123,6 +124,13 @@ td.test-data-table-key { height: 500px; } +.record-video { + position: absolute; + right: 10px; + top: 5px; + z-index: 1000; +} + .control-frame-btn-panel { position: fixed; top: 10px; diff --git a/test/lib/testHelper.js b/test/lib/testHelper.js index be1d49113f..7d4908475c 100644 --- a/test/lib/testHelper.js +++ b/test/lib/testHelper.js @@ -57,6 +57,7 @@ * @param {Array.|Object} [opt.button] {text: ..., onClick: ...}, or an array of them. * @param {Array.|Object} [opt.buttons] {text: ..., onClick: ...}, or an array of them. * @param {boolean} [opt.recordCanvas] 'test/lib/canteen.js' is required. + * @param {boolean} [opt.recordVideo] */ testHelper.create = function (echarts, domOrId, opt) { var dom = getDom(domOrId); @@ -72,6 +73,7 @@ var dataTableContainer = document.createElement('div'); var infoContainer = document.createElement('div'); var recordCanvasContainer = document.createElement('div'); + var recordVideoContainer = document.createElement('div'); title.setAttribute('title', dom.getAttribute('id')); @@ -83,6 +85,7 @@ dataTableContainer.className = 'test-data-table'; infoContainer.className = 'test-info'; recordCanvasContainer.className = 'record-canvas'; + recordVideoContainer.className = 'record-video'; if (opt.info) { dom.className += ' test-chart-block-has-right'; @@ -90,6 +93,7 @@ } left.appendChild(recordCanvasContainer); + left.appendChild(recordVideoContainer); left.appendChild(buttonsContainer); left.appendChild(dataTableContainer); left.appendChild(chartContainer); @@ -153,6 +157,10 @@ initRecordCanvas(opt, chart, recordCanvasContainer); + if (opt.recordVideo) { + initRecordVideo(chart, recordVideoContainer); + } + chart.__testHelper = { updateInfo: updateInfo }; @@ -214,6 +222,23 @@ } } + function initRecordVideo(chart, recordVideoContainer) { + var button = document.createElement('button'); + button.innerHTML = 'Start Recording'; + recordVideoContainer.appendChild(button); + var recorder = new VideoRecorder(chart); + + var isRecording = false; + + + button.onclick = function () { + isRecording ? recorder.stop() : recorder.start(); + button.innerHTML = `${isRecording ? 'Start' : 'Stop'} Recording`; + + isRecording = !isRecording; + } + } + /** * @param {ECharts} echarts * @param {HTMLElement|string} domOrId @@ -287,6 +312,10 @@ * @param checkFn {Function} param: a function `assert`. */ testHelper.printAssert = function (chartOrDomId, checkerFn) { + if (!chartOrDomId) { + return; + } + var hostDOMEl; var chart; if (typeof chartOrDomId === 'string') { @@ -994,10 +1023,72 @@ function isObject(value) { // Avoid a V8 JIT bug in Chrome 19-20. // See https://code.google.com/p/v8/issues/detail?id=2291 for more details. - const type = typeof value; + var type = typeof value; return type === 'function' || (!!value && type === 'object'); } + function VideoRecorder(chart) { + this.start = startRecording; + this.stop = stopRecording; + + var recorder = null; + + var oldRefreshImmediately = chart.getZr().refreshImmediately; + + function startRecording() { + // Normal resolution or high resolution? + var compositeCanvas = document.createElement('canvas'); + var width = chart.getWidth(); + var height = chart.getHeight(); + compositeCanvas.width = width; + compositeCanvas.height = height; + var compositeCtx = compositeCanvas.getContext('2d'); + + chart.getZr().refreshImmediately = function () { + var ret = oldRefreshImmediately.apply(this, arguments); + var canvasList = chart.getDom().querySelectorAll('canvas'); + compositeCtx.fillStyle = '#fff'; + compositeCtx.fillRect(0, 0, width, height); + for (var i = 0; i < canvasList.length; i++) { + compositeCtx.drawImage(canvasList[i], 0, 0, width, height); + } + return ret; + } + + var stream = compositeCanvas.captureStream(25); + recorder = new MediaRecorder(stream, { mimeType: 'video/webm' }); + + var videoData = []; + recorder.ondataavailable = function (event) { + if (event.data && event.data.size) { + videoData.push(event.data); + } + }; + + recorder.onstop = function () { + var url = URL.createObjectURL(new Blob(videoData, { type: 'video/webm' })); + + var a = document.createElement('a'); + a.href = url; + a.download = 'recording.webm'; + a.click(); + + setTimeout(function () { + window.URL.revokeObjectURL(url); + }, 100); + }; + + recorder.start(); + } + + function stopRecording() { + if (recorder) { + chart.getZr().refreshImmediately = oldRefreshImmediately; + recorder.stop(); + } + } + } + context.testHelper = testHelper; })(window); \ No newline at end of file diff --git a/test/line-case.html b/test/line-case.html new file mode 100644 index 0000000000..066715e43e --- /dev/null +++ b/test/line-case.html @@ -0,0 +1,460 @@ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + diff --git a/test/line-endLabel.html b/test/line-endLabel.html index cefa176aa8..860fec0841 100644 --- a/test/line-endLabel.html +++ b/test/line-endLabel.html @@ -36,7 +36,7 @@ height: 1000px; margin-bottom: 30px; } - #main2 { + #main2, #main3 { width: 100%; height: 300px; margin-bottom: 30px; @@ -128,6 +128,8 @@
+
+ + + diff --git a/test/linear-gradient.html b/test/linear-gradient.html index 5d06ee210b..5ec82008ac 100644 --- a/test/linear-gradient.html +++ b/test/linear-gradient.html @@ -39,8 +39,7 @@
- - +
@@ -87,9 +86,9 @@ emphasis: { focus: 'series' }, - lineStyle: { - color: '#FEC171' - }, + // lineStyle: { + // color: '#FEC171' + // }, data: [15, 200, 3000, 50000, 1200000], markLine: { data: [{ @@ -110,35 +109,41 @@ emphasis: { focus: 'series' }, - lineStyle: { - color: '#409EFF' - }, + // lineStyle: { + // color: '#409EFF' + // }, data: [1200000, 50000, 3000, 200, 15], markLine: { data: [{ - name: `Y 轴值为 150 的水平线`, - yAxis: 150 + name: `X 轴值为 4-2 的水平线`, + xAxis: '4-2' }], lineStyle: { - color: '#30B08F' + color: 'red' } } } ], visualMap: { - show: false, + // show: false, pieces: [{ - gt: 0, + gte: 0, lte: 150, color: '#30B08F' }, { gt: 150, color: '#C03639' }], - left: '55%', - top: -2, - orient: 'horizontal' - } + outOfRange: { + color: 'gray' + }, + right: '10%', + bottom: '10%', + orient: 'vertical' + }, + dataZoom: [{ + type: 'inside' + }] }; testHelper.create(echarts, 'main0', { @@ -223,6 +228,144 @@ + + diff --git a/test/lines-bus.html b/test/lines-bus.html index ac6989dc53..08dce490d5 100644 --- a/test/lines-bus.html +++ b/test/lines-bus.html @@ -39,9 +39,15 @@ left: 10px; top: 10px; } + #clear2 { + position: absolute; + left: 10px; + top: 40px; + }
- + + - \ No newline at end of file + diff --git a/test/pictorial-zero-value.html b/test/pictorial-zero-value.html new file mode 100644 index 0000000000..49935bb2bb --- /dev/null +++ b/test/pictorial-zero-value.html @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + +
+
+ + + + diff --git a/test/pie-label-rotate.html b/test/pie-label-rotate.html new file mode 100644 index 0000000000..bc53f28cdd --- /dev/null +++ b/test/pie-label-rotate.html @@ -0,0 +1,107 @@ + + + + + + + + + + + + +

Pie series label rotate

+
+ + + diff --git a/test/pie-negative-value.html b/test/pie-negative-value.html new file mode 100644 index 0000000000..44361fe114 --- /dev/null +++ b/test/pie-negative-value.html @@ -0,0 +1,201 @@ + + + + + + + + + + + + +
+
+
+ + + diff --git a/test/radar.html b/test/radar.html index ffb8ae6534..64e4e31bf7 100644 --- a/test/radar.html +++ b/test/radar.html @@ -76,6 +76,10 @@ show: true } }, + itemStyle: { + borderWidth: 3, + borderColor: '#fff' + }, // areaStyle: {normal: {}}, data : [ { diff --git a/test/radar4.html b/test/radar4.html index 1063e9d35e..c9f4752d1a 100644 --- a/test/radar4.html +++ b/test/radar4.html @@ -100,6 +100,38 @@ ], center : ['75%', 210], radius : 150 + }, + { + indicator: [{ + name: 'a', + max: 13 + }, + { + name: 'b', + max: 1, + min: 0 + }, + { + name: 'c', + max: 16000 + }, + { + name: 'd', + max: 30000 + }, + { + name: 'e', + max: 38000 + }, + { + name: 'f', + max: 52000 + }, + { + name: 'g', + max: 25000 + }], + radius : 150 } ], series : [ @@ -191,6 +223,22 @@ } } ] + }, + { + name: 'join', + polarIndex: 2, + type: 'radar', + data: [{ + value: [ + 3, + 1, + 1, + 53, + 66, + 18, + 0.0121 + ] + }] } ] }); diff --git a/test/runTest/actions/__meta__.json b/test/runTest/actions/__meta__.json index dbbff05fcd..28341e7bd1 100644 --- a/test/runTest/actions/__meta__.json +++ b/test/runTest/actions/__meta__.json @@ -25,7 +25,8 @@ "bar-polar-multi-series-radial": 1, "bar-polar-null-data-radial": 1, "bar-polar-stack": 1, - "bar-stack": 1, + "bar-race2": 2, + "bar-stack": 3, "bar-start": 1, "bar-width": 3, "bar2": 3, @@ -40,6 +41,7 @@ "calendar-heatmap": 1, "calendar-month": 1, "candlestick": 2, + "candlestick-case": 1, "candlestick-empty": 1, "candlestick-large": 4, "candlestick-large2": 1, @@ -56,8 +58,10 @@ "custom-children-remove": 1, "custom-hexbin": 2, "custom-large": 1, + "custom-shape-morphing": 1, "custom-text-content": 6, "dataSelect": 7, + "dataset-case": 6, "dataZoom-action": 4, "dataZoom-axes": 4, "dataZoom-axis-type": 3, @@ -77,12 +81,14 @@ "drag-out": 3, "dynamic-splitNumber": 1, "ec-event": 4, + "emphasis-inherit": 1, "funnel": 2, "gauge-simple": 2, "geo-map": 3, "geo-map-features": 3, "geo-svg": 8, "geo-svg-demo": 6, + "geo-update": 1, "geoScatter": 1, "getOption": 1, "graph": 2, @@ -92,7 +98,7 @@ "heatmap": 1, "heatmap-map": 1, "homepage3": 1, - "hoverFocus": 9, + "hoverFocus": 12, "hoverFocus2": 3, "hoverStyle": 14, "hoverStyle2": 1, @@ -108,6 +114,7 @@ "line-crash": 1, "line-endLabel": 1, "line-extraneous": 2, + "lines-bus": 1, "map": 3, "map-contour": 2, "map-default": 1, @@ -173,5 +180,8 @@ "treemap-obama": 2, "treemap-option": 1, "treemap-option2": 2, + "universalTransition": 6, + "universalTransition2": 3, + "universalTransition3": 2, "visualMap-categories": 1 } \ No newline at end of file diff --git a/test/runTest/actions/bar-race2.json b/test/runTest/actions/bar-race2.json new file mode 100644 index 0000000000..b0ce47c46f --- /dev/null +++ b/test/runTest/actions/bar-race2.json @@ -0,0 +1 @@ +[{"name":"Action 1","ops":[{"type":"mousedown","time":866,"x":43,"y":142},{"type":"mouseup","time":963,"x":43,"y":142},{"time":964,"delay":400,"type":"screenshot-auto"},{"type":"screenshot","time":3902}],"scrollY":407,"scrollX":0,"timestamp":1624253557305},{"name":"Action 2","ops":[{"type":"mousedown","time":533,"x":25,"y":124},{"type":"mouseup","time":647,"x":25,"y":124},{"time":648,"delay":400,"type":"screenshot-auto"},{"type":"screenshot","time":4369}],"scrollY":889,"scrollX":0,"timestamp":1624253606591}] \ No newline at end of file diff --git a/test/runTest/actions/bar-stack.json b/test/runTest/actions/bar-stack.json index 575ec17a80..9ee7d3da98 100644 --- a/test/runTest/actions/bar-stack.json +++ b/test/runTest/actions/bar-stack.json @@ -1 +1 @@ -[{"name":"Action 1","ops":[{"type":"mousedown","time":307,"x":645,"y":74},{"type":"mouseup","time":381,"x":645,"y":74},{"time":382,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":476,"x":645,"y":75},{"type":"mousemove","time":684,"x":641,"y":93},{"type":"mousedown","time":734,"x":641,"y":93},{"type":"mouseup","time":818,"x":641,"y":93},{"time":819,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":884,"x":642,"y":97},{"type":"mousemove","time":1084,"x":643,"y":117},{"type":"mousedown","time":1184,"x":643,"y":117},{"type":"mouseup","time":1283,"x":643,"y":117},{"time":1284,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":1341,"x":643,"y":117},{"type":"mousemove","time":1542,"x":645,"y":137},{"type":"mousemove","time":1751,"x":645,"y":140},{"type":"mousedown","time":1851,"x":645,"y":140},{"type":"mouseup","time":1926,"x":645,"y":140},{"time":1927,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":1952,"x":645,"y":140},{"type":"mousemove","time":2152,"x":646,"y":169},{"type":"mousedown","time":2324,"x":646,"y":169},{"type":"mouseup","time":2418,"x":646,"y":169},{"time":2419,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":2475,"x":647,"y":170},{"type":"mousemove","time":2675,"x":647,"y":186},{"type":"mousedown","time":2826,"x":647,"y":187},{"type":"mouseup","time":2895,"x":647,"y":187},{"time":2896,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":2935,"x":647,"y":190},{"type":"mousemove","time":3138,"x":644,"y":217},{"type":"mousedown","time":3368,"x":644,"y":217},{"type":"mouseup","time":3456,"x":644,"y":217},{"time":3457,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":3509,"x":644,"y":218},{"type":"mousemove","time":3709,"x":647,"y":232},{"type":"mousedown","time":3772,"x":647,"y":232},{"type":"mouseup","time":3850,"x":647,"y":232},{"time":3851,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":3876,"x":647,"y":233},{"type":"mousemove","time":4077,"x":644,"y":263},{"type":"mousedown","time":4192,"x":644,"y":263},{"type":"mouseup","time":4274,"x":644,"y":263},{"time":4275,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":4297,"x":644,"y":264},{"type":"mousemove","time":4504,"x":643,"y":286},{"type":"mousedown","time":4744,"x":643,"y":286},{"type":"mousemove","time":4791,"x":643,"y":286},{"type":"mouseup","time":4840,"x":643,"y":286},{"time":4841,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":4991,"x":642,"y":304},{"type":"mousemove","time":5191,"x":642,"y":313},{"type":"mousedown","time":5239,"x":642,"y":313},{"type":"mouseup","time":5476,"x":642,"y":313},{"time":5477,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":5492,"x":642,"y":325},{"type":"mousemove","time":5692,"x":646,"y":329},{"type":"mousemove","time":5892,"x":649,"y":314},{"type":"mousemove","time":6092,"x":643,"y":325},{"type":"mousemove","time":6292,"x":643,"y":329},{"type":"mousedown","time":6310,"x":643,"y":329},{"type":"mouseup","time":6359,"x":643,"y":329},{"time":6360,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":6493,"x":641,"y":344},{"type":"mousemove","time":6694,"x":640,"y":352},{"type":"mousedown","time":6810,"x":640,"y":352},{"type":"mousemove","time":6879,"x":640,"y":352},{"type":"mouseup","time":6888,"x":640,"y":352},{"time":6889,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":7081,"x":640,"y":353},{"type":"mousedown","time":7694,"x":640,"y":353},{"type":"mouseup","time":7792,"x":640,"y":353},{"time":7793,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":7806,"x":640,"y":353},{"type":"mousemove","time":8010,"x":641,"y":339},{"type":"mousemove","time":8210,"x":642,"y":327},{"type":"mousedown","time":8459,"x":642,"y":327},{"type":"mouseup","time":8535,"x":642,"y":327},{"time":8536,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":8595,"x":642,"y":327},{"type":"mousemove","time":8795,"x":646,"y":301},{"type":"mousemove","time":8995,"x":646,"y":248},{"type":"mousemove","time":9201,"x":621,"y":84},{"type":"mousemove","time":9412,"x":640,"y":76},{"type":"mousedown","time":9595,"x":643,"y":74},{"type":"mousemove","time":9613,"x":643,"y":74},{"type":"mouseup","time":9668,"x":643,"y":74},{"time":9669,"delay":400,"type":"screenshot-auto"}],"scrollY":0,"scrollX":0,"timestamp":1568018408672}] \ No newline at end of file +[{"name":"Action 1","ops":[{"type":"mousedown","time":307,"x":645,"y":74},{"type":"mouseup","time":381,"x":645,"y":74},{"time":382,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":476,"x":645,"y":75},{"type":"mousemove","time":684,"x":641,"y":93},{"type":"mousedown","time":734,"x":641,"y":93},{"type":"mouseup","time":818,"x":641,"y":93},{"time":819,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":884,"x":642,"y":97},{"type":"mousemove","time":1084,"x":643,"y":117},{"type":"mousedown","time":1184,"x":643,"y":117},{"type":"mouseup","time":1283,"x":643,"y":117},{"time":1284,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":1341,"x":643,"y":117},{"type":"mousemove","time":1542,"x":645,"y":137},{"type":"mousemove","time":1751,"x":645,"y":140},{"type":"mousedown","time":1851,"x":645,"y":140},{"type":"mouseup","time":1926,"x":645,"y":140},{"time":1927,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":1952,"x":645,"y":140},{"type":"mousemove","time":2152,"x":646,"y":169},{"type":"mousedown","time":2324,"x":646,"y":169},{"type":"mouseup","time":2418,"x":646,"y":169},{"time":2419,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":2475,"x":647,"y":170},{"type":"mousemove","time":2675,"x":647,"y":186},{"type":"mousedown","time":2826,"x":647,"y":187},{"type":"mouseup","time":2895,"x":647,"y":187},{"time":2896,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":2935,"x":647,"y":190},{"type":"mousemove","time":3138,"x":644,"y":217},{"type":"mousedown","time":3368,"x":644,"y":217},{"type":"mouseup","time":3456,"x":644,"y":217},{"time":3457,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":3509,"x":644,"y":218},{"type":"mousemove","time":3709,"x":647,"y":232},{"type":"mousedown","time":3772,"x":647,"y":232},{"type":"mouseup","time":3850,"x":647,"y":232},{"time":3851,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":3876,"x":647,"y":233},{"type":"mousemove","time":4077,"x":644,"y":263},{"type":"mousedown","time":4192,"x":644,"y":263},{"type":"mouseup","time":4274,"x":644,"y":263},{"time":4275,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":4297,"x":644,"y":264},{"type":"mousemove","time":4504,"x":643,"y":286},{"type":"mousedown","time":4744,"x":643,"y":286},{"type":"mousemove","time":4791,"x":643,"y":286},{"type":"mouseup","time":4840,"x":643,"y":286},{"time":4841,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":4991,"x":642,"y":304},{"type":"mousemove","time":5191,"x":642,"y":313},{"type":"mousedown","time":5239,"x":642,"y":313},{"type":"mouseup","time":5476,"x":642,"y":313},{"time":5477,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":5492,"x":642,"y":325},{"type":"mousemove","time":5692,"x":646,"y":329},{"type":"mousemove","time":5892,"x":649,"y":314},{"type":"mousemove","time":6092,"x":643,"y":325},{"type":"mousemove","time":6292,"x":643,"y":329},{"type":"mousedown","time":6310,"x":643,"y":329},{"type":"mouseup","time":6359,"x":643,"y":329},{"time":6360,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":6493,"x":641,"y":344},{"type":"mousemove","time":6694,"x":640,"y":352},{"type":"mousedown","time":6810,"x":640,"y":352},{"type":"mousemove","time":6879,"x":640,"y":352},{"type":"mouseup","time":6888,"x":640,"y":352},{"time":6889,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":7081,"x":640,"y":353},{"type":"mousedown","time":7694,"x":640,"y":353},{"type":"mouseup","time":7792,"x":640,"y":353},{"time":7793,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":7806,"x":640,"y":353},{"type":"mousemove","time":8010,"x":641,"y":339},{"type":"mousemove","time":8210,"x":642,"y":327},{"type":"mousedown","time":8459,"x":642,"y":327},{"type":"mouseup","time":8535,"x":642,"y":327},{"time":8536,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":8595,"x":642,"y":327},{"type":"mousemove","time":8795,"x":646,"y":301},{"type":"mousemove","time":8995,"x":646,"y":248},{"type":"mousemove","time":9201,"x":621,"y":84},{"type":"mousemove","time":9412,"x":640,"y":76},{"type":"mousedown","time":9595,"x":643,"y":74},{"type":"mousemove","time":9613,"x":643,"y":74},{"type":"mouseup","time":9668,"x":643,"y":74},{"time":9669,"delay":400,"type":"screenshot-auto"}],"scrollY":0,"scrollX":0,"timestamp":1568018408672},{"name":"Action 2","ops":[{"type":"mousemove","time":176,"x":607,"y":352},{"type":"mousemove","time":377,"x":611,"y":239},{"type":"mousemove","time":585,"x":304,"y":122},{"type":"mousemove","time":743,"x":299,"y":121},{"type":"mousemove","time":943,"x":101,"y":125},{"type":"mousemove","time":1149,"x":70,"y":138},{"type":"mousedown","time":1194,"x":67,"y":138},{"type":"mouseup","time":1333,"x":67,"y":138},{"time":1334,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":1371,"x":67,"y":138},{"type":"mousemove","time":1559,"x":68,"y":138},{"type":"mousemove","time":1759,"x":97,"y":140},{"type":"mousedown","time":1900,"x":112,"y":140},{"type":"mousemove","time":1959,"x":112,"y":140},{"type":"mouseup","time":2032,"x":112,"y":140},{"time":2033,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":2125,"x":111,"y":140},{"type":"mousemove","time":2325,"x":65,"y":140},{"type":"mousemove","time":2525,"x":51,"y":140},{"type":"mousemove","time":2734,"x":51,"y":140},{"type":"mousemove","time":2759,"x":51,"y":140},{"type":"mousemove","time":2960,"x":303,"y":158},{"type":"mousemove","time":3172,"x":347,"y":181},{"type":"mousedown","time":3318,"x":355,"y":183},{"type":"mousemove","time":3384,"x":355,"y":182},{"type":"mouseup","time":3450,"x":355,"y":182},{"time":3451,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":3692,"x":355,"y":182},{"type":"mousemove","time":3892,"x":390,"y":173},{"type":"mousedown","time":3951,"x":391,"y":174},{"type":"mouseup","time":4100,"x":391,"y":174},{"time":4101,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":4124,"x":391,"y":174},{"type":"mousemove","time":4527,"x":393,"y":174},{"type":"mousemove","time":4726,"x":422,"y":162},{"type":"mousemove","time":4928,"x":409,"y":175},{"type":"mousemove","time":5130,"x":355,"y":175},{"type":"mousedown","time":5197,"x":353,"y":174},{"type":"mouseup","time":5329,"x":353,"y":174},{"time":5330,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":5357,"x":353,"y":174},{"type":"mousemove","time":5457,"x":353,"y":174},{"type":"mousemove","time":5665,"x":435,"y":170},{"type":"mousedown","time":5880,"x":458,"y":169},{"type":"mousemove","time":5899,"x":458,"y":169},{"type":"mouseup","time":6004,"x":458,"y":169},{"time":6005,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":6121,"x":457,"y":169},{"type":"mousemove","time":6321,"x":430,"y":169},{"type":"mousedown","time":6497,"x":402,"y":168},{"type":"mousemove","time":6532,"x":401,"y":168},{"type":"mouseup","time":6613,"x":401,"y":168},{"time":6614,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":6737,"x":393,"y":168},{"type":"mousemove","time":6937,"x":366,"y":168},{"type":"mousemove","time":7138,"x":419,"y":168},{"type":"mousemove","time":7347,"x":443,"y":168},{"type":"mousedown","time":7369,"x":444,"y":168},{"type":"mouseup","time":7487,"x":444,"y":168},{"time":7488,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":7563,"x":444,"y":168},{"type":"mousemove","time":7604,"x":444,"y":169},{"type":"mousemove","time":7804,"x":415,"y":169},{"type":"mousemove","time":8004,"x":405,"y":168},{"type":"mousemove","time":8206,"x":366,"y":174},{"type":"mousemove","time":8406,"x":359,"y":175},{"type":"mousemove","time":8607,"x":345,"y":176},{"type":"mousemove","time":8813,"x":343,"y":176},{"type":"mousemove","time":9037,"x":343,"y":175},{"type":"mousemove","time":9337,"x":343,"y":174},{"type":"mousemove","time":9547,"x":343,"y":171},{"type":"mousemove","time":12239,"x":343,"y":171}],"scrollY":493,"scrollX":0,"timestamp":1629615854239},{"name":"Action 3","ops":[{"type":"mousemove","time":120,"x":110,"y":154},{"type":"mousemove","time":325,"x":71,"y":167},{"type":"mousemove","time":533,"x":47,"y":198},{"type":"mousemove","time":733,"x":48,"y":196},{"type":"mousemove","time":933,"x":49,"y":193},{"type":"mousedown","time":1090,"x":49,"y":193},{"type":"mouseup","time":1223,"x":49,"y":193},{"time":1224,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":1400,"x":50,"y":193},{"type":"mousemove","time":1609,"x":115,"y":194},{"type":"mousedown","time":1779,"x":125,"y":199},{"type":"mousemove","time":1823,"x":125,"y":199},{"type":"mouseup","time":1941,"x":125,"y":199},{"time":1942,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":2149,"x":125,"y":199},{"type":"mousemove","time":2349,"x":101,"y":201},{"type":"mousemove","time":2549,"x":75,"y":200},{"type":"mousemove","time":2750,"x":66,"y":197},{"type":"mousemove","time":2959,"x":63,"y":195},{"type":"mousedown","time":3792,"x":63,"y":195},{"type":"mouseup","time":3880,"x":63,"y":195},{"time":3881,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":3999,"x":65,"y":195},{"type":"mousemove","time":4199,"x":268,"y":230},{"type":"mousemove","time":4399,"x":341,"y":245},{"type":"mousemove","time":4599,"x":327,"y":223},{"type":"mousedown","time":4761,"x":355,"y":217},{"type":"mousemove","time":4810,"x":355,"y":217},{"type":"mouseup","time":4843,"x":355,"y":217},{"time":4844,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":5083,"x":359,"y":217},{"type":"mousedown","time":5276,"x":403,"y":219},{"type":"mousemove","time":5298,"x":403,"y":219},{"type":"mouseup","time":5361,"x":403,"y":219},{"time":5362,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":5499,"x":430,"y":219},{"type":"mousedown","time":5659,"x":449,"y":220},{"type":"mousemove","time":5699,"x":449,"y":220},{"type":"mouseup","time":5796,"x":449,"y":220},{"time":5797,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":5883,"x":446,"y":220},{"type":"mousemove","time":6083,"x":412,"y":220},{"type":"mousemove","time":6283,"x":410,"y":221},{"type":"mousedown","time":6311,"x":410,"y":221},{"type":"mouseup","time":6460,"x":410,"y":221},{"time":6461,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":6570,"x":412,"y":221},{"type":"mousemove","time":6776,"x":458,"y":221},{"type":"mousedown","time":6977,"x":449,"y":221},{"type":"mousemove","time":6986,"x":448,"y":221},{"type":"mouseup","time":7065,"x":448,"y":221},{"time":7066,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":7202,"x":429,"y":221},{"type":"mousemove","time":7414,"x":357,"y":219},{"type":"mousedown","time":7498,"x":344,"y":221},{"type":"mouseup","time":7593,"x":344,"y":221},{"time":7594,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":7633,"x":344,"y":221},{"type":"mousemove","time":7824,"x":337,"y":221},{"type":"mousemove","time":8026,"x":123,"y":181},{"type":"mousemove","time":8243,"x":122,"y":179},{"type":"mousemove","time":8457,"x":118,"y":195},{"type":"mousedown","time":8523,"x":118,"y":195},{"type":"mouseup","time":8663,"x":118,"y":195},{"time":8664,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":8696,"x":118,"y":195},{"type":"mousemove","time":8822,"x":117,"y":195},{"type":"mousemove","time":9024,"x":78,"y":197},{"type":"mousemove","time":9230,"x":57,"y":199},{"type":"mousemove","time":9447,"x":56,"y":199},{"type":"mousemove","time":9872,"x":56,"y":199}],"scrollY":845,"scrollX":0,"timestamp":1629615870890}] \ No newline at end of file diff --git a/test/runTest/actions/candlestick-case.json b/test/runTest/actions/candlestick-case.json new file mode 100644 index 0000000000..bc02692b43 --- /dev/null +++ b/test/runTest/actions/candlestick-case.json @@ -0,0 +1 @@ +[{"name":"Action 1","ops":[{"type":"mousedown","time":300,"x":50,"y":77},{"type":"mouseup","time":423,"x":50,"y":77},{"time":424,"delay":400,"type":"screenshot-auto"}],"scrollY":0,"scrollX":0,"timestamp":1626405373145}] \ No newline at end of file diff --git a/test/runTest/actions/custom-shape-morphing.json b/test/runTest/actions/custom-shape-morphing.json new file mode 100644 index 0000000000..a0e816f4fc --- /dev/null +++ b/test/runTest/actions/custom-shape-morphing.json @@ -0,0 +1 @@ +[{"name":"Action 1","ops":[{"type":"mousemove","time":197,"x":22,"y":25},{"type":"mousedown","time":503,"x":22,"y":25},{"type":"mouseup","time":550,"x":22,"y":25},{"time":551,"delay":400,"type":"screenshot-auto"},{"type":"mousedown","time":2853,"x":22,"y":25},{"type":"mouseup","time":2919,"x":22,"y":25},{"time":2920,"delay":400,"type":"screenshot-auto"},{"type":"mousedown","time":5467,"x":22,"y":25},{"type":"mouseup","time":5543,"x":22,"y":25},{"time":5544,"delay":400,"type":"screenshot-auto"},{"type":"mousedown","time":7968,"x":22,"y":25},{"type":"mouseup","time":8059,"x":22,"y":25},{"time":8060,"delay":400,"type":"screenshot-auto"}],"scrollY":0,"scrollX":0,"timestamp":1623062310496}] \ No newline at end of file diff --git a/test/runTest/actions/dataset-case.json b/test/runTest/actions/dataset-case.json new file mode 100644 index 0000000000..23e8b4027f --- /dev/null +++ b/test/runTest/actions/dataset-case.json @@ -0,0 +1 @@ +[{"name":"Action 1","ops":[{"type":"mousemove","time":420,"x":111,"y":360},{"type":"mousemove","time":628,"x":77,"y":398},{"type":"mousemove","time":836,"x":66,"y":390},{"type":"mousedown","time":868,"x":66,"y":390},{"type":"mouseup","time":1012,"x":66,"y":390},{"time":1013,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":1037,"x":66,"y":390},{"type":"mousemove","time":1120,"x":66,"y":392},{"type":"mousemove","time":1328,"x":66,"y":412},{"type":"mousedown","time":1380,"x":66,"y":413},{"type":"mouseup","time":1500,"x":66,"y":413},{"time":1501,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":1544,"x":66,"y":413},{"type":"mousemove","time":2003,"x":66,"y":417},{"type":"mousemove","time":2204,"x":61,"y":459},{"type":"mousemove","time":2412,"x":60,"y":471},{"type":"mousedown","time":2628,"x":59,"y":468},{"type":"mousemove","time":2644,"x":59,"y":468},{"type":"mouseup","time":2695,"x":59,"y":468},{"time":2696,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":2936,"x":59,"y":467},{"type":"mousemove","time":3136,"x":61,"y":439},{"type":"mousedown","time":3201,"x":61,"y":438},{"type":"mouseup","time":3312,"x":61,"y":438},{"time":3313,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":3346,"x":61,"y":438},{"type":"mousemove","time":3403,"x":61,"y":439},{"type":"mousemove","time":3610,"x":61,"y":493},{"type":"mousedown","time":3728,"x":61,"y":495},{"type":"mousemove","time":3829,"x":61,"y":495},{"type":"mouseup","time":3845,"x":61,"y":495},{"time":3846,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":4086,"x":61,"y":495},{"type":"mousedown","time":4228,"x":62,"y":489},{"type":"mousemove","time":4293,"x":62,"y":489},{"type":"mouseup","time":4331,"x":62,"y":489},{"time":4332,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":4535,"x":62,"y":491},{"type":"mousedown","time":4711,"x":62,"y":505},{"type":"mousemove","time":4736,"x":62,"y":505},{"type":"mouseup","time":4827,"x":62,"y":505},{"time":4828,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":5137,"x":62,"y":505},{"type":"mousemove","time":5344,"x":71,"y":488},{"type":"mousedown","time":5486,"x":72,"y":484},{"type":"mousemove","time":5560,"x":72,"y":484},{"type":"mouseup","time":5613,"x":72,"y":484},{"time":5614,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":5785,"x":72,"y":483},{"type":"mousemove","time":5995,"x":72,"y":463},{"type":"mousedown","time":6003,"x":72,"y":463},{"type":"mouseup","time":6111,"x":72,"y":463},{"time":6112,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":6220,"x":72,"y":462},{"type":"mousemove","time":6428,"x":72,"y":443},{"type":"mousedown","time":6553,"x":72,"y":438},{"type":"mousemove","time":6645,"x":72,"y":438},{"type":"mouseup","time":6693,"x":72,"y":438},{"time":6694,"delay":400,"type":"screenshot-auto"}],"scrollY":0,"scrollX":0,"timestamp":1629397351306},{"name":"Action 2","ops":[{"type":"mousedown","time":500,"x":99,"y":302},{"type":"mouseup","time":667,"x":99,"y":302},{"time":668,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":827,"x":99,"y":303},{"type":"mousemove","time":1035,"x":97,"y":325},{"type":"mousedown","time":1188,"x":96,"y":328},{"type":"mousemove","time":1316,"x":96,"y":328},{"type":"mouseup","time":1323,"x":96,"y":328},{"time":1324,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":1409,"x":96,"y":328},{"type":"mousemove","time":1609,"x":96,"y":297},{"type":"mousedown","time":1802,"x":96,"y":282},{"type":"mousemove","time":1818,"x":96,"y":282},{"type":"mouseup","time":1919,"x":96,"y":282},{"time":1920,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":1975,"x":96,"y":282},{"type":"mousemove","time":2176,"x":96,"y":340},{"type":"mousemove","time":2376,"x":95,"y":350},{"type":"mousedown","time":2502,"x":95,"y":352},{"type":"mousemove","time":2586,"x":95,"y":352},{"type":"mouseup","time":2602,"x":95,"y":352},{"time":2603,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":2843,"x":95,"y":352},{"type":"mousemove","time":3043,"x":82,"y":392},{"type":"mousedown","time":3085,"x":82,"y":392},{"type":"mouseup","time":3185,"x":82,"y":392},{"time":3186,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":3243,"x":82,"y":392},{"type":"mousemove","time":3409,"x":82,"y":391},{"type":"mousedown","time":3552,"x":84,"y":383},{"type":"mousemove","time":3610,"x":84,"y":383},{"type":"mouseup","time":3655,"x":84,"y":383},{"time":3656,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":3859,"x":84,"y":383},{"type":"mousemove","time":4059,"x":83,"y":401},{"type":"mousedown","time":4152,"x":83,"y":402},{"type":"mouseup","time":4256,"x":83,"y":402},{"time":4257,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":4278,"x":83,"y":402},{"type":"mousemove","time":4576,"x":83,"y":400},{"type":"mousemove","time":4783,"x":86,"y":389},{"type":"mousedown","time":4859,"x":86,"y":389},{"type":"mouseup","time":4966,"x":86,"y":389},{"time":4967,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":5178,"x":86,"y":388},{"type":"mousedown","time":5384,"x":86,"y":377},{"type":"mousemove","time":5400,"x":86,"y":377},{"type":"mouseup","time":5491,"x":86,"y":377},{"time":5492,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":5776,"x":86,"y":373},{"type":"mousemove","time":5976,"x":89,"y":358},{"type":"mousedown","time":6005,"x":89,"y":357},{"type":"mouseup","time":6118,"x":89,"y":357},{"time":6119,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":6187,"x":89,"y":357},{"type":"mousemove","time":6260,"x":89,"y":356},{"type":"mousemove","time":6460,"x":89,"y":336},{"type":"mousedown","time":6520,"x":89,"y":334},{"type":"mouseup","time":6653,"x":89,"y":334},{"time":6654,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":6681,"x":89,"y":334},{"type":"mousemove","time":6810,"x":89,"y":334},{"type":"mousemove","time":7010,"x":89,"y":306},{"type":"mousedown","time":7104,"x":89,"y":305},{"type":"mousemove","time":7219,"x":89,"y":305},{"type":"mouseup","time":7227,"x":89,"y":305},{"time":7228,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":7459,"x":89,"y":304},{"type":"mousemove","time":7669,"x":91,"y":283},{"type":"mousedown","time":7690,"x":91,"y":283},{"type":"mouseup","time":7809,"x":91,"y":283},{"time":7810,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":7862,"x":91,"y":285},{"type":"mousemove","time":8062,"x":89,"y":383},{"type":"mousemove","time":8270,"x":83,"y":418},{"type":"mousemove","time":8476,"x":86,"y":405},{"type":"mousedown","time":8518,"x":86,"y":405},{"type":"mouseup","time":8634,"x":86,"y":405},{"time":8635,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":8678,"x":86,"y":405},{"type":"mousemove","time":8859,"x":87,"y":405},{"type":"mousemove","time":9060,"x":162,"y":341},{"type":"mousemove","time":9270,"x":162,"y":341}],"scrollY":642,"scrollX":0,"timestamp":1629397363631},{"name":"Action 3","ops":[{"type":"mousemove","time":246,"x":109,"y":267},{"type":"mousedown","time":428,"x":108,"y":260},{"type":"mousemove","time":452,"x":108,"y":260},{"type":"mouseup","time":555,"x":108,"y":260},{"time":556,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":645,"x":108,"y":262},{"type":"mousemove","time":853,"x":108,"y":279},{"type":"mousedown","time":871,"x":108,"y":279},{"type":"mouseup","time":973,"x":108,"y":279},{"time":974,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":1070,"x":108,"y":279},{"type":"mousemove","time":1231,"x":108,"y":281},{"type":"mousemove","time":1441,"x":92,"y":333},{"type":"mousedown","time":1473,"x":92,"y":333},{"type":"mouseup","time":1622,"x":92,"y":333},{"time":1623,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":1745,"x":92,"y":333},{"type":"mousemove","time":1945,"x":94,"y":312},{"type":"mousedown","time":1989,"x":94,"y":311},{"type":"mouseup","time":2095,"x":94,"y":311},{"time":2096,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":2154,"x":94,"y":311},{"type":"mousemove","time":2329,"x":94,"y":311},{"type":"mousemove","time":2539,"x":100,"y":278},{"type":"mousedown","time":2577,"x":100,"y":276},{"type":"mouseup","time":2721,"x":100,"y":276},{"time":2722,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":2754,"x":100,"y":276},{"type":"mousemove","time":2896,"x":100,"y":277},{"type":"mousedown","time":3022,"x":100,"y":279},{"type":"mousemove","time":3105,"x":100,"y":279},{"type":"mouseup","time":3138,"x":100,"y":279},{"time":3139,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":3379,"x":100,"y":280},{"type":"mousemove","time":3579,"x":88,"y":340},{"type":"mousemove","time":3779,"x":85,"y":352},{"type":"mousedown","time":3907,"x":83,"y":361},{"type":"mousemove","time":3988,"x":83,"y":361},{"type":"mouseup","time":4021,"x":83,"y":361},{"time":4022,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":4162,"x":83,"y":363},{"type":"mousemove","time":4362,"x":83,"y":388},{"type":"mousedown","time":4421,"x":83,"y":388},{"type":"mouseup","time":4587,"x":83,"y":388},{"time":4588,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":4796,"x":83,"y":387},{"type":"mousemove","time":5006,"x":94,"y":350},{"type":"mousedown","time":5095,"x":97,"y":342},{"type":"mousemove","time":5221,"x":97,"y":342},{"type":"mouseup","time":5289,"x":97,"y":342},{"time":5290,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":5394,"x":97,"y":343},{"type":"mousemove","time":5599,"x":97,"y":348},{"type":"mousedown","time":5624,"x":97,"y":348},{"type":"mouseup","time":5756,"x":97,"y":348},{"time":5757,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":5929,"x":97,"y":349},{"type":"mousemove","time":6131,"x":97,"y":354},{"type":"mousedown","time":6159,"x":97,"y":354},{"type":"mouseup","time":6258,"x":97,"y":354},{"time":6259,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":6340,"x":97,"y":354},{"type":"mousemove","time":6497,"x":97,"y":357},{"type":"mousemove","time":6707,"x":86,"y":381},{"type":"mousedown","time":6772,"x":86,"y":382},{"type":"mouseup","time":6873,"x":86,"y":382},{"time":6874,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":6924,"x":86,"y":382},{"type":"mousemove","time":7081,"x":86,"y":379},{"type":"mousemove","time":7289,"x":94,"y":320},{"type":"mousemove","time":7496,"x":97,"y":313},{"type":"mousedown","time":7517,"x":97,"y":313},{"type":"mouseup","time":7670,"x":97,"y":313},{"time":7671,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":7879,"x":97,"y":312},{"type":"mousemove","time":8078,"x":102,"y":287},{"type":"mousedown","time":8157,"x":102,"y":286},{"type":"mousemove","time":8279,"x":102,"y":286},{"type":"mouseup","time":8309,"x":102,"y":286},{"time":8310,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":8397,"x":102,"y":285},{"type":"mousemove","time":8609,"x":102,"y":268},{"type":"mousedown","time":8662,"x":102,"y":266},{"type":"mouseup","time":8792,"x":102,"y":266},{"time":8793,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":8825,"x":102,"y":266}],"scrollY":1197,"scrollX":0,"timestamp":1629397376601},{"name":"Action 4","ops":[{"type":"mousemove","time":549,"x":116,"y":368},{"type":"mousemove","time":749,"x":40,"y":337},{"type":"mousemove","time":959,"x":18,"y":334},{"type":"mousemove","time":1171,"x":28,"y":336},{"type":"mousemove","time":1374,"x":33,"y":376},{"type":"mousemove","time":1582,"x":33,"y":378},{"type":"mousemove","time":1793,"x":34,"y":328},{"type":"mousemove","time":1994,"x":37,"y":295},{"type":"mousemove","time":2201,"x":179,"y":306},{"type":"mousemove","time":2411,"x":277,"y":333},{"type":"mousemove","time":2611,"x":278,"y":333},{"type":"mousemove","time":2811,"x":286,"y":333},{"type":"mousemove","time":3011,"x":288,"y":402},{"type":"mousemove","time":3211,"x":290,"y":416},{"type":"mousemove","time":3420,"x":290,"y":419},{"type":"mousemove","time":3478,"x":290,"y":419},{"type":"mousemove","time":3692,"x":570,"y":342},{"type":"mousemove","time":3894,"x":583,"y":361},{"type":"mousemove","time":4094,"x":581,"y":368},{"type":"mousemove","time":4294,"x":579,"y":231},{"type":"mousemove","time":4494,"x":576,"y":226},{"type":"mousemove","time":4694,"x":187,"y":264},{"type":"mousemove","time":4894,"x":109,"y":232},{"type":"mousemove","time":5103,"x":47,"y":259},{"type":"mousemove","time":5311,"x":34,"y":274},{"type":"mousemove","time":5524,"x":30,"y":274},{"type":"mousemove","time":5737,"x":30,"y":273},{"type":"mousedown","time":5888,"x":30,"y":273},{"type":"mousemove","time":5897,"x":30,"y":277},{"type":"mousemove","time":6097,"x":28,"y":314},{"type":"mousemove","time":6312,"x":30,"y":328},{"type":"mouseup","time":6523,"x":30,"y":326},{"time":6524,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":6534,"x":32,"y":322},{"type":"mousemove","time":6738,"x":36,"y":272},{"type":"mousemove","time":6955,"x":35,"y":270},{"type":"mousemove","time":7188,"x":34,"y":268},{"type":"mousedown","time":7390,"x":34,"y":268},{"type":"mousemove","time":7399,"x":34,"y":270},{"type":"mousemove","time":7601,"x":32,"y":312},{"type":"mousemove","time":7806,"x":33,"y":334},{"type":"mousemove","time":8012,"x":34,"y":340},{"type":"mouseup","time":8190,"x":34,"y":340},{"time":8191,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":8200,"x":33,"y":343},{"type":"mousemove","time":8407,"x":27,"y":375},{"type":"mousemove","time":8621,"x":27,"y":376},{"type":"mousedown","time":8670,"x":27,"y":376},{"type":"mousemove","time":8682,"x":27,"y":374},{"type":"mousemove","time":8898,"x":28,"y":344},{"type":"mousemove","time":9110,"x":31,"y":316},{"type":"mousemove","time":9312,"x":34,"y":286},{"type":"mousemove","time":9527,"x":34,"y":284},{"type":"mouseup","time":9704,"x":34,"y":284},{"time":9705,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":9713,"x":34,"y":290},{"type":"mousemove","time":9922,"x":31,"y":356},{"type":"mousemove","time":10193,"x":32,"y":343},{"type":"mousemove","time":10405,"x":32,"y":337},{"type":"mousedown","time":10724,"x":32,"y":337},{"type":"mousemove","time":10734,"x":32,"y":339},{"type":"mousemove","time":10951,"x":32,"y":437},{"type":"mousemove","time":11172,"x":32,"y":451},{"type":"mouseup","time":11245,"x":32,"y":451},{"time":11246,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":11255,"x":34,"y":447},{"type":"mousemove","time":11455,"x":68,"y":373},{"type":"mousedown","time":11538,"x":68,"y":371},{"type":"mouseup","time":11636,"x":68,"y":371},{"time":11637,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":11672,"x":68,"y":371}],"scrollY":1549,"scrollX":0,"timestamp":1629397393090},{"name":"Action 5","ops":[{"type":"mousemove","time":204,"x":43,"y":319},{"type":"mousemove","time":404,"x":30,"y":318},{"type":"mousemove","time":604,"x":28,"y":316},{"type":"mousemove","time":814,"x":30,"y":315},{"type":"mousedown","time":1047,"x":30,"y":315},{"type":"mousemove","time":1056,"x":30,"y":316},{"type":"mousemove","time":1260,"x":33,"y":342},{"type":"mousemove","time":1470,"x":36,"y":364},{"type":"mousemove","time":1681,"x":36,"y":369},{"type":"mouseup","time":1731,"x":36,"y":369},{"time":1732,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":1887,"x":34,"y":421},{"type":"mousemove","time":2087,"x":33,"y":423},{"type":"mousedown","time":2197,"x":33,"y":423},{"type":"mousemove","time":2209,"x":33,"y":418},{"type":"mousemove","time":2421,"x":30,"y":378},{"type":"mousemove","time":2636,"x":32,"y":322},{"type":"mousemove","time":2848,"x":34,"y":311},{"type":"mouseup","time":2885,"x":34,"y":311},{"time":2886,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":3055,"x":24,"y":392},{"type":"mousemove","time":3264,"x":23,"y":407},{"type":"mousemove","time":3471,"x":23,"y":402},{"type":"mousemove","time":3671,"x":24,"y":399},{"type":"mousemove","time":3879,"x":24,"y":399},{"type":"mousedown","time":3985,"x":24,"y":399},{"type":"mousemove","time":3995,"x":24,"y":402},{"type":"mousemove","time":4202,"x":28,"y":493},{"type":"mousemove","time":4418,"x":28,"y":496},{"type":"mouseup","time":4463,"x":28,"y":496},{"time":4464,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":4475,"x":37,"y":486},{"type":"mousemove","time":4682,"x":101,"y":418}],"scrollY":1991,"scrollX":0,"timestamp":1629397408600},{"name":"Action 6","ops":[{"type":"mousemove","time":532,"x":127,"y":195},{"type":"mousemove","time":732,"x":129,"y":188},{"type":"mousemove","time":932,"x":130,"y":180},{"type":"mousemove","time":1132,"x":130,"y":180},{"type":"mousedown","time":1477,"x":130,"y":180},{"type":"mouseup","time":1614,"x":130,"y":180},{"time":1615,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":2849,"x":130,"y":180},{"type":"mousemove","time":3049,"x":275,"y":178},{"type":"mousemove","time":3249,"x":302,"y":181},{"type":"mousedown","time":3376,"x":303,"y":181},{"type":"mousemove","time":3459,"x":303,"y":181},{"type":"mouseup","time":3544,"x":303,"y":181},{"time":3545,"delay":400,"type":"screenshot-auto"},{"type":"mousedown","time":4749,"x":303,"y":181},{"type":"mouseup","time":4927,"x":303,"y":181},{"time":4928,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":5250,"x":302,"y":181},{"type":"mousemove","time":5450,"x":237,"y":192},{"type":"mousemove","time":5650,"x":221,"y":184},{"type":"mousedown","time":5725,"x":221,"y":183},{"type":"mousemove","time":5858,"x":221,"y":183},{"type":"mouseup","time":5878,"x":221,"y":183},{"time":5879,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":6282,"x":228,"y":183},{"type":"mousemove","time":6482,"x":348,"y":214},{"type":"mousedown","time":6664,"x":355,"y":216},{"type":"mousemove","time":6696,"x":355,"y":216},{"type":"mouseup","time":6760,"x":355,"y":216},{"time":6761,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":7199,"x":355,"y":216},{"type":"mousemove","time":7400,"x":374,"y":211},{"type":"mousemove","time":7610,"x":377,"y":211},{"type":"mousedown","time":8180,"x":377,"y":211},{"type":"mouseup","time":8293,"x":377,"y":211},{"time":8294,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":8466,"x":383,"y":211},{"type":"mousemove","time":8666,"x":424,"y":211},{"type":"mousedown","time":8732,"x":425,"y":211},{"type":"mouseup","time":8860,"x":425,"y":211},{"time":8861,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":8887,"x":425,"y":211},{"type":"mousemove","time":9166,"x":424,"y":211},{"type":"mousemove","time":9366,"x":364,"y":215},{"type":"mousedown","time":9377,"x":363,"y":215},{"type":"mouseup","time":9511,"x":363,"y":215},{"time":9512,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":9580,"x":363,"y":215},{"type":"mousemove","time":9616,"x":364,"y":215},{"type":"mousemove","time":9816,"x":424,"y":215},{"type":"mousemove","time":10025,"x":434,"y":215},{"type":"mousedown","time":10048,"x":434,"y":215},{"type":"mouseup","time":10178,"x":434,"y":215},{"time":10179,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":10282,"x":434,"y":215},{"type":"mousemove","time":10483,"x":394,"y":215},{"type":"mousedown","time":10612,"x":374,"y":214},{"type":"mousemove","time":10693,"x":374,"y":214},{"type":"mouseup","time":10745,"x":374,"y":214},{"time":10746,"delay":400,"type":"screenshot-auto"}],"scrollY":2475,"scrollX":0,"timestamp":1629397417622}] \ No newline at end of file diff --git a/test/runTest/actions/emphasis-inherit.json b/test/runTest/actions/emphasis-inherit.json new file mode 100644 index 0000000000..af47bb7160 --- /dev/null +++ b/test/runTest/actions/emphasis-inherit.json @@ -0,0 +1 @@ +[{"name":"Action 1","ops":[{"type":"mousemove","time":1280,"x":532,"y":589},{"type":"mousemove","time":1482,"x":513,"y":439},{"type":"mousemove","time":1681,"x":512,"y":437},{"type":"mousemove","time":1899,"x":463,"y":381},{"type":"mousemove","time":2106,"x":453,"y":354},{"type":"mousemove","time":2321,"x":452,"y":351},{"type":"mousedown","time":2772,"x":452,"y":351},{"type":"mouseup","time":2921,"x":452,"y":351},{"time":2922,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":3897,"x":452,"y":351},{"type":"mousemove","time":4097,"x":501,"y":252},{"type":"mousemove","time":4306,"x":503,"y":249},{"type":"mousedown","time":4688,"x":503,"y":249},{"type":"mouseup","time":4837,"x":503,"y":249},{"time":4838,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":5314,"x":495,"y":243},{"type":"mousemove","time":5516,"x":360,"y":140},{"type":"mousemove","time":5721,"x":358,"y":139},{"type":"mousedown","time":6093,"x":358,"y":139},{"type":"mouseup","time":6245,"x":358,"y":139},{"time":6246,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":6765,"x":355,"y":140},{"type":"mousemove","time":6972,"x":245,"y":249},{"type":"mousemove","time":7180,"x":253,"y":240},{"type":"mousemove","time":7380,"x":270,"y":222},{"type":"mousemove","time":7589,"x":270,"y":222},{"type":"mousemove","time":7664,"x":271,"y":222},{"type":"mousemove","time":7874,"x":794,"y":191},{"type":"mousemove","time":8931,"x":783,"y":406},{"type":"mousemove","time":9138,"x":609,"y":511},{"type":"mousemove","time":9349,"x":559,"y":538}],"scrollY":0,"scrollX":0,"timestamp":1623912564572}] \ No newline at end of file diff --git a/test/runTest/actions/geo-update.json b/test/runTest/actions/geo-update.json new file mode 100644 index 0000000000..310d357a9b --- /dev/null +++ b/test/runTest/actions/geo-update.json @@ -0,0 +1 @@ +[{"name":"Action 1","ops":[{"type":"mousedown","time":122,"x":81,"y":281},{"type":"mouseup","time":272,"x":81,"y":281},{"time":273,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":358,"x":82,"y":280},{"type":"mousemove","time":558,"x":107,"y":277},{"type":"mousedown","time":746,"x":123,"y":278},{"type":"mousemove","time":761,"x":123,"y":278},{"type":"mouseup","time":845,"x":123,"y":278},{"time":846,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":973,"x":109,"y":276},{"type":"mousemove","time":1173,"x":28,"y":281},{"type":"mousemove","time":1378,"x":17,"y":280},{"type":"mousedown","time":1620,"x":17,"y":280},{"type":"mouseup","time":1713,"x":17,"y":280},{"time":1714,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":1807,"x":17,"y":280},{"type":"mousemove","time":2007,"x":111,"y":274},{"type":"mousemove","time":2208,"x":136,"y":273},{"type":"mousedown","time":2525,"x":136,"y":273},{"type":"mouseup","time":2600,"x":136,"y":273},{"time":2601,"delay":400,"type":"screenshot-auto"}],"scrollY":175,"scrollX":0,"timestamp":1626342039818}] \ No newline at end of file diff --git a/test/runTest/actions/hoverFocus.json b/test/runTest/actions/hoverFocus.json index d9e7feb72d..07149c1e1d 100644 --- a/test/runTest/actions/hoverFocus.json +++ b/test/runTest/actions/hoverFocus.json @@ -1 +1 @@ -[{"name":"Action 1","ops":[{"type":"mousemove","time":370,"x":409,"y":156},{"type":"mousemove","time":579,"x":448,"y":261},{"type":"mousemove","time":796,"x":464,"y":300},{"type":"screenshot","time":1752},{"type":"mousemove","time":1970,"x":464,"y":304},{"type":"mousemove","time":2171,"x":471,"y":404},{"type":"mousemove","time":2382,"x":472,"y":407},{"type":"mousemove","time":2622,"x":472,"y":407},{"type":"mousemove","time":3021,"x":471,"y":407},{"type":"screenshot","time":3164},{"type":"mousemove","time":3230,"x":471,"y":407},{"type":"mousemove","time":3756,"x":471,"y":407},{"type":"mousemove","time":3956,"x":442,"y":206},{"type":"mousemove","time":4156,"x":421,"y":99},{"type":"mousemove","time":4363,"x":416,"y":86},{"type":"screenshot","time":5215},{"type":"mousemove","time":5407,"x":426,"y":86},{"type":"mousemove","time":5618,"x":585,"y":87},{"type":"mousemove","time":5836,"x":596,"y":85},{"type":"screenshot","time":6451}],"scrollY":0,"scrollX":0,"timestamp":1603943939135},{"name":"Action 2","ops":[{"type":"mousemove","time":282,"x":619,"y":92},{"type":"mousemove","time":482,"x":644,"y":50},{"type":"mousemove","time":687,"x":639,"y":46},{"type":"mousedown","time":827,"x":639,"y":46},{"type":"mouseup","time":904,"x":639,"y":46},{"time":905,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":1535,"x":638,"y":46},{"type":"mousemove","time":1738,"x":636,"y":48},{"type":"mousemove","time":1950,"x":581,"y":92},{"type":"mousemove","time":2169,"x":579,"y":95},{"type":"screenshot","time":2831}],"scrollY":0,"scrollX":0,"timestamp":1603943994365},{"name":"Action 3","ops":[{"type":"mousemove","time":150,"x":662,"y":372},{"type":"mousemove","time":368,"x":485,"y":340},{"type":"mousemove","time":516,"x":485,"y":338},{"type":"mousemove","time":717,"x":485,"y":336},{"type":"screenshot","time":1241},{"type":"mousemove","time":1902,"x":494,"y":351},{"type":"mousemove","time":2110,"x":548,"y":402},{"type":"mousemove","time":2837,"x":548,"y":401},{"type":"mousemove","time":3043,"x":673,"y":195},{"type":"mousemove","time":3255,"x":622,"y":48},{"type":"mousemove","time":3460,"x":617,"y":22},{"type":"mousemove","time":3678,"x":630,"y":35},{"type":"mousemove","time":3887,"x":630,"y":35},{"type":"mousedown","time":3954,"x":631,"y":35},{"type":"mouseup","time":4061,"x":631,"y":35},{"time":4062,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":4627,"x":634,"y":37},{"type":"mousemove","time":4840,"x":634,"y":38},{"type":"mousemove","time":5044,"x":635,"y":46},{"type":"mousemove","time":5244,"x":576,"y":244},{"type":"mousemove","time":5452,"x":570,"y":258},{"type":"screenshot","time":6912},{"type":"mousemove","time":7248,"x":571,"y":260},{"type":"mousemove","time":7448,"x":641,"y":368},{"type":"screenshot","time":7939},{"type":"mousemove","time":8917,"x":642,"y":367}],"scrollY":740,"scrollX":0,"timestamp":1603944053030},{"name":"Action 4","ops":[{"type":"mousemove","time":353,"x":523,"y":357},{"type":"mousemove","time":553,"x":493,"y":345},{"type":"screenshot","time":1126},{"type":"mousemove","time":1153,"x":490,"y":345},{"type":"mousemove","time":1354,"x":279,"y":335},{"type":"mousemove","time":1554,"x":267,"y":337},{"type":"mousemove","time":1754,"x":285,"y":342},{"type":"mousemove","time":1955,"x":287,"y":343},{"type":"screenshot","time":2368},{"type":"mousemove","time":2438,"x":284,"y":345},{"type":"mousemove","time":2638,"x":254,"y":353},{"type":"mousemove","time":2847,"x":234,"y":353},{"type":"screenshot","time":3403},{"type":"mousemove","time":3556,"x":242,"y":344},{"type":"mousemove","time":3756,"x":300,"y":240},{"type":"mousemove","time":3957,"x":310,"y":224},{"type":"mousemove","time":4157,"x":320,"y":212}],"scrollY":720,"scrollX":0,"timestamp":1603944084664},{"name":"Action 5","ops":[{"type":"mousemove","time":453,"x":47,"y":201},{"type":"mousemove","time":654,"x":48,"y":198},{"type":"mousemove","time":1738,"x":48,"y":198},{"type":"mousemove","time":1938,"x":50,"y":169},{"type":"mousemove","time":2151,"x":50,"y":167},{"type":"mousemove","time":2389,"x":50,"y":168},{"type":"mousemove","time":2589,"x":49,"y":169},{"type":"screenshot","time":3181},{"type":"mousemove","time":3307,"x":49,"y":168},{"type":"mousemove","time":3515,"x":46,"y":155},{"type":"mousemove","time":3607,"x":46,"y":154},{"type":"mousemove","time":3807,"x":50,"y":153},{"type":"mousedown","time":3919,"x":50,"y":153},{"type":"mousemove","time":4020,"x":50,"y":153},{"type":"mouseup","time":4577,"x":50,"y":153},{"time":4578,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":4618,"x":50,"y":156},{"type":"mousemove","time":4824,"x":131,"y":204},{"type":"mousemove","time":5040,"x":150,"y":215},{"type":"mousemove","time":5249,"x":150,"y":216}],"scrollY":1225,"scrollX":0,"timestamp":1603944095448},{"name":"Action 6","ops":[{"type":"mousemove","time":137,"x":602,"y":347},{"type":"mousemove","time":346,"x":504,"y":293},{"type":"mousemove","time":564,"x":467,"y":279},{"type":"mousemove","time":1396,"x":462,"y":281},{"type":"mousemove","time":1600,"x":399,"y":293},{"type":"mousemove","time":1815,"x":376,"y":295}],"scrollY":3777,"scrollX":0,"timestamp":1603944120020},{"name":"Action 7","ops":[{"type":"mousemove","time":104,"x":404,"y":311},{"type":"mousemove","time":305,"x":594,"y":265},{"type":"mousemove","time":515,"x":676,"y":260},{"type":"mousemove","time":722,"x":705,"y":249},{"type":"mousemove","time":1856,"x":705,"y":252},{"type":"mousemove","time":2056,"x":703,"y":271},{"type":"mousemove","time":2407,"x":703,"y":271},{"type":"mousemove","time":2608,"x":702,"y":275},{"type":"mousemove","time":2821,"x":702,"y":275},{"type":"screenshot","time":3210},{"type":"mousemove","time":3548,"x":702,"y":276},{"type":"mousemove","time":3749,"x":701,"y":278}],"scrollY":3266,"scrollX":0,"timestamp":1603944127517},{"name":"Action 8","ops":[{"type":"mousemove","time":143,"x":27,"y":280},{"type":"mousemove","time":352,"x":165,"y":88},{"type":"mousemove","time":588,"x":170,"y":82},{"type":"mousemove","time":805,"x":171,"y":81},{"type":"mousemove","time":1159,"x":172,"y":80},{"type":"mousemove","time":1360,"x":178,"y":95},{"type":"mousemove","time":1573,"x":180,"y":102},{"type":"mousemove","time":2395,"x":180,"y":103},{"type":"mousemove","time":2597,"x":249,"y":290},{"type":"mousemove","time":2801,"x":253,"y":308},{"type":"mousemove","time":3002,"x":253,"y":308},{"type":"mousemove","time":3036,"x":253,"y":308},{"type":"mousemove","time":3920,"x":253,"y":308},{"type":"mousemove","time":4120,"x":488,"y":278},{"type":"mousemove","time":4320,"x":529,"y":266},{"type":"mousemove","time":4520,"x":533,"y":254},{"type":"mousemove","time":4721,"x":533,"y":251}],"scrollY":4251,"scrollX":0,"timestamp":1603944137897},{"name":"Action 9","ops":[{"type":"mousemove","time":238,"x":454,"y":359},{"type":"mousemove","time":448,"x":344,"y":284},{"type":"mousemove","time":1356,"x":345,"y":291},{"type":"mousemove","time":1556,"x":353,"y":442},{"type":"mousemove","time":1789,"x":353,"y":442}],"scrollY":5780,"scrollX":0,"timestamp":1603944148069}] \ No newline at end of file +[{"name":"Action 1","ops":[{"type":"mousemove","time":370,"x":409,"y":156},{"type":"mousemove","time":579,"x":448,"y":261},{"type":"mousemove","time":796,"x":464,"y":300},{"type":"screenshot","time":1752},{"type":"mousemove","time":1970,"x":464,"y":304},{"type":"mousemove","time":2171,"x":471,"y":404},{"type":"mousemove","time":2382,"x":472,"y":407},{"type":"mousemove","time":2622,"x":472,"y":407},{"type":"mousemove","time":3021,"x":471,"y":407},{"type":"screenshot","time":3164},{"type":"mousemove","time":3230,"x":471,"y":407},{"type":"mousemove","time":3756,"x":471,"y":407},{"type":"mousemove","time":3956,"x":442,"y":206},{"type":"mousemove","time":4156,"x":421,"y":99},{"type":"mousemove","time":4363,"x":416,"y":86},{"type":"screenshot","time":5215},{"type":"mousemove","time":5407,"x":426,"y":86},{"type":"mousemove","time":5618,"x":585,"y":87},{"type":"mousemove","time":5836,"x":596,"y":85},{"type":"screenshot","time":6451}],"scrollY":0,"scrollX":0,"timestamp":1603943939135},{"name":"Action 2","ops":[{"type":"mousemove","time":282,"x":619,"y":92},{"type":"mousemove","time":482,"x":644,"y":50},{"type":"mousemove","time":687,"x":639,"y":46},{"type":"mousedown","time":827,"x":639,"y":46},{"type":"mouseup","time":904,"x":639,"y":46},{"time":905,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":1535,"x":638,"y":46},{"type":"mousemove","time":1738,"x":636,"y":48},{"type":"mousemove","time":1950,"x":581,"y":92},{"type":"mousemove","time":2169,"x":579,"y":95},{"type":"screenshot","time":2831}],"scrollY":0,"scrollX":0,"timestamp":1603943994365},{"name":"Action 3","ops":[{"type":"mousemove","time":150,"x":662,"y":372},{"type":"mousemove","time":368,"x":485,"y":340},{"type":"mousemove","time":516,"x":485,"y":338},{"type":"mousemove","time":717,"x":485,"y":336},{"type":"screenshot","time":1241},{"type":"mousemove","time":1902,"x":494,"y":351},{"type":"mousemove","time":2110,"x":548,"y":402},{"type":"mousemove","time":2837,"x":548,"y":401},{"type":"mousemove","time":3043,"x":673,"y":195},{"type":"mousemove","time":3255,"x":622,"y":48},{"type":"mousemove","time":3460,"x":617,"y":22},{"type":"mousemove","time":3678,"x":630,"y":35},{"type":"mousemove","time":3887,"x":630,"y":35},{"type":"mousedown","time":3954,"x":631,"y":35},{"type":"mouseup","time":4061,"x":631,"y":35},{"time":4062,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":4627,"x":634,"y":37},{"type":"mousemove","time":4840,"x":634,"y":38},{"type":"mousemove","time":5044,"x":635,"y":46},{"type":"mousemove","time":5244,"x":576,"y":244},{"type":"mousemove","time":5452,"x":570,"y":258},{"type":"screenshot","time":6912},{"type":"mousemove","time":7248,"x":571,"y":260},{"type":"mousemove","time":7448,"x":641,"y":368},{"type":"screenshot","time":7939},{"type":"mousemove","time":8917,"x":642,"y":367}],"scrollY":740,"scrollX":0,"timestamp":1603944053030},{"name":"Action 4","ops":[{"type":"mousemove","time":353,"x":523,"y":357},{"type":"mousemove","time":553,"x":493,"y":345},{"type":"screenshot","time":1126},{"type":"mousemove","time":1153,"x":490,"y":345},{"type":"mousemove","time":1354,"x":279,"y":335},{"type":"mousemove","time":1554,"x":267,"y":337},{"type":"mousemove","time":1754,"x":285,"y":342},{"type":"mousemove","time":1955,"x":287,"y":343},{"type":"screenshot","time":2368},{"type":"mousemove","time":2438,"x":284,"y":345},{"type":"mousemove","time":2638,"x":254,"y":353},{"type":"mousemove","time":2847,"x":234,"y":353},{"type":"screenshot","time":3403},{"type":"mousemove","time":3556,"x":242,"y":344},{"type":"mousemove","time":3756,"x":300,"y":240},{"type":"mousemove","time":3957,"x":310,"y":224},{"type":"mousemove","time":4157,"x":320,"y":212}],"scrollY":720,"scrollX":0,"timestamp":1603944084664},{"name":"Action 5","ops":[{"type":"mousemove","time":453,"x":47,"y":201},{"type":"mousemove","time":654,"x":48,"y":198},{"type":"mousemove","time":1738,"x":48,"y":198},{"type":"mousemove","time":1938,"x":50,"y":169},{"type":"mousemove","time":2151,"x":50,"y":167},{"type":"mousemove","time":2389,"x":50,"y":168},{"type":"mousemove","time":2589,"x":49,"y":169},{"type":"screenshot","time":3181},{"type":"mousemove","time":3307,"x":49,"y":168},{"type":"mousemove","time":3515,"x":46,"y":155},{"type":"mousemove","time":3607,"x":46,"y":154},{"type":"mousemove","time":3807,"x":50,"y":153},{"type":"mousedown","time":3919,"x":50,"y":153},{"type":"mousemove","time":4020,"x":50,"y":153},{"type":"mouseup","time":4577,"x":50,"y":153},{"time":4578,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":4618,"x":50,"y":156},{"type":"mousemove","time":4824,"x":131,"y":204},{"type":"mousemove","time":5040,"x":150,"y":215},{"type":"mousemove","time":5249,"x":150,"y":216}],"scrollY":1225,"scrollX":0,"timestamp":1603944095448},{"name":"Action 6","ops":[{"type":"mousemove","time":137,"x":602,"y":347},{"type":"mousemove","time":346,"x":504,"y":293},{"type":"mousemove","time":564,"x":467,"y":279},{"type":"mousemove","time":1396,"x":462,"y":281},{"type":"mousemove","time":1600,"x":399,"y":293},{"type":"mousemove","time":1815,"x":376,"y":295}],"scrollY":3777,"scrollX":0,"timestamp":1603944120020},{"name":"Action 7","ops":[{"type":"mousemove","time":104,"x":404,"y":311},{"type":"mousemove","time":305,"x":594,"y":265},{"type":"mousemove","time":515,"x":676,"y":260},{"type":"mousemove","time":722,"x":705,"y":249},{"type":"mousemove","time":1856,"x":705,"y":252},{"type":"mousemove","time":2056,"x":703,"y":271},{"type":"mousemove","time":2407,"x":703,"y":271},{"type":"mousemove","time":2608,"x":702,"y":275},{"type":"mousemove","time":2821,"x":702,"y":275},{"type":"screenshot","time":3210},{"type":"mousemove","time":3548,"x":702,"y":276},{"type":"mousemove","time":3749,"x":701,"y":278}],"scrollY":3266,"scrollX":0,"timestamp":1603944127517},{"name":"Action 8","ops":[{"type":"mousemove","time":143,"x":27,"y":280},{"type":"mousemove","time":352,"x":165,"y":88},{"type":"mousemove","time":588,"x":170,"y":82},{"type":"mousemove","time":805,"x":171,"y":81},{"type":"mousemove","time":1159,"x":172,"y":80},{"type":"mousemove","time":1360,"x":178,"y":95},{"type":"mousemove","time":1573,"x":180,"y":102},{"type":"mousemove","time":2395,"x":180,"y":103},{"type":"mousemove","time":2597,"x":249,"y":290},{"type":"mousemove","time":2801,"x":253,"y":308},{"type":"mousemove","time":3002,"x":253,"y":308},{"type":"mousemove","time":3036,"x":253,"y":308},{"type":"mousemove","time":3920,"x":253,"y":308},{"type":"mousemove","time":4120,"x":488,"y":278},{"type":"mousemove","time":4320,"x":529,"y":266},{"type":"mousemove","time":4520,"x":533,"y":254},{"type":"mousemove","time":4721,"x":533,"y":251}],"scrollY":4251,"scrollX":0,"timestamp":1603944137897},{"name":"Action 9","ops":[{"type":"mousemove","time":238,"x":454,"y":359},{"type":"mousemove","time":448,"x":344,"y":284},{"type":"mousemove","time":1356,"x":345,"y":291},{"type":"mousemove","time":1556,"x":353,"y":442},{"type":"mousemove","time":1789,"x":353,"y":442}],"scrollY":5780,"scrollX":0,"timestamp":1603944148069},{"name":"Action 10","ops":[{"type":"mousemove","time":206,"x":572,"y":157},{"type":"mousemove","time":280,"x":561,"y":142},{"type":"mousemove","time":481,"x":559,"y":132},{"type":"mousedown","time":663,"x":558,"y":128},{"type":"mousemove","time":681,"x":558,"y":128},{"type":"mouseup","time":780,"x":558,"y":128},{"time":781,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":865,"x":558,"y":128},{"type":"mousemove","time":1065,"x":569,"y":140},{"type":"mousemove","time":1266,"x":597,"y":165},{"type":"mousemove","time":1476,"x":597,"y":166}],"scrollY":6331,"scrollX":0,"timestamp":1625215232660},{"name":"Action 11","ops":[{"type":"mousemove","time":242,"x":677,"y":162},{"type":"mousemove","time":443,"x":644,"y":154},{"type":"mousemove","time":643,"x":599,"y":144},{"type":"mousemove","time":844,"x":583,"y":140},{"type":"mousemove","time":1053,"x":578,"y":139},{"type":"mousemove","time":1128,"x":577,"y":138},{"type":"mousedown","time":1226,"x":575,"y":137},{"type":"mousemove","time":1337,"x":575,"y":137},{"type":"mouseup","time":1348,"x":575,"y":137},{"time":1349,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":1577,"x":576,"y":137},{"type":"mousemove","time":1777,"x":621,"y":158},{"type":"mousemove","time":1990,"x":635,"y":164},{"type":"mousemove","time":2228,"x":636,"y":165}],"scrollY":6821,"scrollX":0,"timestamp":1625215245384},{"name":"Action 12","ops":[{"type":"mousemove","time":84,"x":578,"y":183},{"type":"mousemove","time":210,"x":573,"y":182},{"type":"mousemove","time":418,"x":563,"y":182},{"type":"mousedown","time":557,"x":563,"y":182},{"type":"mouseup","time":686,"x":563,"y":182},{"time":687,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":880,"x":563,"y":182},{"type":"mousemove","time":1087,"x":637,"y":189},{"type":"mousemove","time":1296,"x":660,"y":201}],"scrollY":7277,"scrollX":0,"timestamp":1625215256451}] \ No newline at end of file diff --git a/test/runTest/actions/lines-bus.json b/test/runTest/actions/lines-bus.json new file mode 100644 index 0000000000..52bb26d74c --- /dev/null +++ b/test/runTest/actions/lines-bus.json @@ -0,0 +1 @@ +[{"name":"Action 1","ops":[{"type":"mousemove","time":355,"x":281,"y":183},{"type":"mousemove","time":579,"x":97,"y":93},{"type":"mousemove","time":781,"x":62,"y":57},{"type":"mousemove","time":1000,"x":62,"y":53},{"type":"mousedown","time":1249,"x":62,"y":53},{"type":"mouseup","time":1349,"x":62,"y":53},{"time":1350,"delay":400,"type":"screenshot-auto"},{"type":"mousemove","time":1630,"x":66,"y":53},{"type":"mousemove","time":1834,"x":260,"y":184},{"type":"mousedown","time":1935,"x":260,"y":184},{"type":"mouseup","time":2017,"x":260,"y":184},{"time":2018,"delay":400,"type":"screenshot-auto"}],"scrollY":0,"scrollX":0,"timestamp":1622903652167}] \ No newline at end of file diff --git a/test/runTest/actions/universalTransition.json b/test/runTest/actions/universalTransition.json new file mode 100644 index 0000000000..af7029a956 --- /dev/null +++ b/test/runTest/actions/universalTransition.json @@ -0,0 +1 @@ +[{"name":"Action 1","ops":[{"type":"mousedown","time":610,"x":14,"y":77},{"type":"mouseup","time":737,"x":14,"y":77},{"time":738,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":1116,"x":14,"y":77},{"type":"mousemove","time":1316,"x":53,"y":76},{"type":"mousemove","time":1521,"x":70,"y":76},{"type":"mousedown","time":2141,"x":70,"y":76},{"type":"mouseup","time":2207,"x":70,"y":76},{"time":2208,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":3750,"x":71,"y":76},{"type":"mousemove","time":3950,"x":141,"y":80},{"type":"mousemove","time":4152,"x":145,"y":80},{"type":"mousedown","time":4370,"x":145,"y":80},{"type":"mousemove","time":4416,"x":145,"y":80},{"type":"mouseup","time":4446,"x":145,"y":80},{"time":4447,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":6000,"x":146,"y":80},{"type":"mousemove","time":6207,"x":191,"y":82},{"type":"mousemove","time":6472,"x":192,"y":82},{"type":"mousedown","time":6629,"x":192,"y":82},{"type":"mouseup","time":6707,"x":192,"y":82},{"time":6708,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":7549,"x":192,"y":82},{"type":"mousemove","time":7750,"x":191,"y":83},{"type":"mousemove","time":8167,"x":192,"y":83},{"type":"mousemove","time":8370,"x":232,"y":81},{"type":"mousedown","time":9014,"x":232,"y":81},{"type":"mouseup","time":9092,"x":232,"y":81},{"time":9093,"delay":100,"type":"screenshot-auto"}],"scrollY":0,"scrollX":0,"timestamp":1624199268008},{"name":"Action 2","ops":[{"type":"mousedown","time":313,"x":136,"y":86},{"type":"mouseup","time":402,"x":136,"y":86},{"time":403,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":676,"x":137,"y":85},{"type":"mousemove","time":876,"x":220,"y":81},{"type":"mousemove","time":1081,"x":227,"y":80},{"type":"mousedown","time":1988,"x":227,"y":80},{"type":"mouseup","time":2083,"x":227,"y":80},{"time":2084,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":2376,"x":227,"y":79},{"type":"mousemove","time":2576,"x":272,"y":80},{"type":"mousemove","time":2780,"x":326,"y":80},{"type":"mousemove","time":2997,"x":338,"y":80},{"type":"mousedown","time":3654,"x":338,"y":80},{"type":"mouseup","time":3748,"x":338,"y":80},{"time":3749,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":4094,"x":339,"y":80},{"type":"mousemove","time":4297,"x":394,"y":81},{"type":"mousemove","time":4510,"x":459,"y":83},{"type":"mousemove","time":4715,"x":464,"y":83},{"type":"mousedown","time":5448,"x":464,"y":83},{"type":"mouseup","time":5565,"x":464,"y":83},{"time":5566,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":6843,"x":464,"y":84},{"type":"mousemove","time":7043,"x":467,"y":275},{"type":"mousemove","time":7243,"x":480,"y":351},{"type":"mousemove","time":7443,"x":472,"y":420},{"type":"mousemove","time":7650,"x":471,"y":420},{"type":"mousedown","time":7797,"x":471,"y":420},{"type":"mousemove","time":7909,"x":471,"y":420},{"type":"mousemove","time":8117,"x":402,"y":424},{"type":"mousemove","time":8326,"x":368,"y":426},{"type":"mousemove","time":8532,"x":354,"y":428},{"type":"mousemove","time":8742,"x":360,"y":428},{"type":"mousemove","time":8942,"x":375,"y":427},{"type":"mousemove","time":9143,"x":380,"y":427},{"type":"mousemove","time":9343,"x":396,"y":427},{"type":"mousemove","time":9543,"x":410,"y":427},{"type":"mousemove","time":9750,"x":415,"y":429},{"type":"mousemove","time":9977,"x":417,"y":429},{"type":"mouseup","time":10184,"x":417,"y":429},{"time":10185,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":10259,"x":416,"y":429},{"type":"mousemove","time":10459,"x":408,"y":431},{"type":"mousemove","time":10659,"x":376,"y":432},{"type":"mousemove","time":10866,"x":372,"y":431},{"type":"mousedown","time":11157,"x":372,"y":431},{"type":"mousemove","time":11343,"x":372,"y":431},{"type":"mousemove","time":11543,"x":419,"y":430},{"type":"mousemove","time":11757,"x":432,"y":429},{"type":"mousemove","time":11967,"x":445,"y":428},{"type":"mouseup","time":12266,"x":445,"y":428},{"time":12267,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":12393,"x":445,"y":428},{"type":"mousemove","time":12593,"x":278,"y":254},{"type":"mousemove","time":12793,"x":117,"y":127},{"type":"mousemove","time":12993,"x":68,"y":86},{"type":"mousemove","time":13197,"x":43,"y":86},{"type":"mousedown","time":13633,"x":43,"y":86},{"type":"mouseup","time":13714,"x":43,"y":86},{"time":13715,"delay":100,"type":"screenshot-auto"}],"scrollY":492.0244140625,"scrollX":0,"timestamp":1624199496137},{"name":"Action 3","ops":[{"type":"mousedown","time":969,"x":102,"y":102},{"type":"mouseup","time":1070,"x":102,"y":102},{"time":1071,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":1707,"x":102,"y":102},{"type":"mousemove","time":1912,"x":73,"y":101},{"type":"mousemove","time":2124,"x":63,"y":99},{"type":"mousemove","time":2328,"x":43,"y":96},{"type":"mousemove","time":2608,"x":43,"y":96},{"type":"mousedown","time":3041,"x":43,"y":96},{"type":"mouseup","time":3128,"x":43,"y":96},{"time":3129,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":4174,"x":43,"y":96},{"type":"mousemove","time":4377,"x":98,"y":95},{"type":"mousedown","time":4595,"x":102,"y":94},{"type":"mousemove","time":4600,"x":102,"y":94},{"type":"mouseup","time":4681,"x":102,"y":94},{"time":4682,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":4806,"x":89,"y":97},{"type":"mousemove","time":5006,"x":68,"y":98},{"type":"mousedown","time":5165,"x":62,"y":98},{"type":"mousemove","time":5213,"x":62,"y":98},{"type":"mouseup","time":5247,"x":62,"y":98},{"time":5248,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":5724,"x":62,"y":98}],"scrollY":979.994384765625,"scrollX":0,"timestamp":1624199518756},{"name":"Action 4","ops":[{"type":"mousedown","time":479,"x":96,"y":36},{"type":"mouseup","time":627,"x":96,"y":36},{"time":628,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":1788,"x":97,"y":36},{"type":"mousemove","time":1990,"x":198,"y":45},{"type":"mousemove","time":2211,"x":198,"y":45},{"type":"mousedown","time":2335,"x":198,"y":45},{"type":"mouseup","time":2414,"x":198,"y":45},{"time":2415,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":3254,"x":198,"y":45},{"type":"mousemove","time":3454,"x":300,"y":69},{"type":"mousemove","time":3654,"x":341,"y":68},{"type":"mousemove","time":3859,"x":365,"y":71},{"type":"mousedown","time":4069,"x":365,"y":71},{"type":"mouseup","time":4158,"x":365,"y":71},{"time":4159,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":4988,"x":364,"y":71},{"type":"mousemove","time":5192,"x":303,"y":64},{"type":"mousemove","time":5404,"x":104,"y":55},{"type":"mousemove","time":5604,"x":0,"y":44},{"type":"mousemove","time":5788,"x":8,"y":45},{"type":"mousemove","time":5988,"x":49,"y":46},{"type":"mousemove","time":6191,"x":34,"y":49},{"type":"mousedown","time":6310,"x":33,"y":49},{"type":"mouseup","time":6396,"x":33,"y":49},{"time":6397,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":6458,"x":33,"y":49},{"type":"mousemove","time":7355,"x":33,"y":49},{"type":"mousemove","time":7557,"x":229,"y":67},{"type":"mousemove","time":7771,"x":368,"y":87},{"type":"mousemove","time":7971,"x":373,"y":81},{"type":"mousedown","time":8108,"x":373,"y":80},{"type":"mousemove","time":8175,"x":373,"y":80},{"type":"mouseup","time":8196,"x":373,"y":80},{"time":8197,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":9287,"x":373,"y":80},{"type":"mousemove","time":9488,"x":242,"y":62},{"type":"mousemove","time":9693,"x":113,"y":37},{"type":"mousedown","time":9851,"x":113,"y":37},{"type":"mouseup","time":9943,"x":113,"y":37},{"time":9944,"delay":100,"type":"screenshot-auto"}],"scrollY":1531.4329833984375,"scrollX":0,"timestamp":1624199536593},{"name":"Action 5","ops":[{"type":"mousedown","time":578,"x":23,"y":36},{"type":"mouseup","time":660,"x":23,"y":36},{"time":661,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":939,"x":23,"y":35},{"type":"mousemove","time":1148,"x":62,"y":32},{"type":"mousemove","time":1364,"x":63,"y":32},{"type":"mousemove","time":1540,"x":63,"y":32},{"type":"mousemove","time":1740,"x":65,"y":31},{"type":"mousemove","time":2092,"x":65,"y":31},{"type":"mousedown","time":2097,"x":65,"y":31},{"type":"mouseup","time":2210,"x":65,"y":31},{"time":2211,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":3106,"x":65,"y":31},{"type":"mousemove","time":3311,"x":11,"y":31},{"type":"mousedown","time":3650,"x":11,"y":31},{"type":"mouseup","time":3728,"x":11,"y":31},{"time":3729,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":3823,"x":12,"y":31},{"type":"mousemove","time":4023,"x":50,"y":29},{"type":"mousemove","time":4224,"x":56,"y":29},{"type":"mousedown","time":4320,"x":58,"y":29},{"type":"mouseup","time":4436,"x":58,"y":29},{"time":4437,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":4442,"x":58,"y":29},{"type":"mousemove","time":5840,"x":57,"y":29},{"type":"mousemove","time":6040,"x":44,"y":32},{"type":"mousemove","time":6240,"x":41,"y":32},{"type":"mousemove","time":6445,"x":40,"y":32},{"type":"mousedown","time":6552,"x":40,"y":32},{"type":"mouseup","time":6664,"x":40,"y":32},{"time":6665,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":7074,"x":40,"y":33},{"type":"mousemove","time":7286,"x":46,"y":327},{"type":"mousemove","time":7490,"x":32,"y":427},{"type":"mousemove","time":7700,"x":14,"y":475},{"type":"mousemove","time":7924,"x":15,"y":477},{"type":"mousemove","time":8128,"x":16,"y":477},{"type":"mousemove","time":8381,"x":17,"y":481},{"type":"mousedown","time":8556,"x":17,"y":481},{"type":"mousemove","time":8673,"x":17,"y":482},{"type":"mousemove","time":8884,"x":7,"y":544},{"type":"mousemove","time":9089,"x":11,"y":560},{"type":"mousemove","time":9298,"x":11,"y":560},{"type":"mousemove","time":9440,"x":11,"y":561},{"type":"mousemove","time":9640,"x":12,"y":566},{"type":"mouseup","time":9781,"x":12,"y":566},{"time":9782,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":9807,"x":12,"y":566},{"type":"mousemove","time":10014,"x":12,"y":566},{"type":"mousemove","time":10123,"x":12,"y":566},{"type":"mousemove","time":10323,"x":10,"y":582},{"type":"mousemove","time":10533,"x":12,"y":586},{"type":"mousedown","time":10765,"x":12,"y":586},{"type":"mousemove","time":10812,"x":12,"y":586},{"type":"mousemove","time":10890,"x":12,"y":586},{"type":"mousemove","time":11106,"x":14,"y":568},{"type":"mousemove","time":11306,"x":12,"y":525},{"type":"mouseup","time":11514,"x":12,"y":525},{"time":11515,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":12457,"x":12,"y":525},{"type":"mousemove","time":12657,"x":84,"y":178},{"type":"mousemove","time":12862,"x":127,"y":10},{"type":"mousemove","time":13161,"x":71,"y":22},{"type":"mousedown","time":13347,"x":71,"y":24},{"type":"mousemove","time":13394,"x":71,"y":24},{"type":"mouseup","time":13448,"x":71,"y":24},{"time":13449,"delay":100,"type":"screenshot-auto"}],"scrollY":2261.93359375,"scrollX":0,"timestamp":1624199554291},{"name":"Action 6","ops":[{"type":"mousedown","time":410,"x":42,"y":182},{"type":"mouseup","time":497,"x":42,"y":182},{"time":498,"delay":100,"type":"screenshot-auto"},{"type":"mousedown","time":1733,"x":42,"y":182},{"type":"mouseup","time":1846,"x":42,"y":182},{"time":1847,"delay":100,"type":"screenshot-auto"},{"type":"mousedown","time":2043,"x":42,"y":182},{"type":"mouseup","time":2095,"x":42,"y":182},{"time":2096,"delay":100,"type":"screenshot-auto"}],"scrollY":2812,"scrollX":0,"timestamp":1624535432676}] \ No newline at end of file diff --git a/test/runTest/actions/universalTransition2.json b/test/runTest/actions/universalTransition2.json new file mode 100644 index 0000000000..9e746cd2ef --- /dev/null +++ b/test/runTest/actions/universalTransition2.json @@ -0,0 +1 @@ +[{"name":"Action 1","ops":[{"type":"mousedown","time":442,"x":233,"y":291},{"type":"mouseup","time":541,"x":233,"y":291},{"time":542,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":796,"x":232,"y":291},{"type":"mousemove","time":1001,"x":113,"y":115},{"type":"mousemove","time":1212,"x":99,"y":92},{"type":"mousemove","time":1412,"x":93,"y":92},{"type":"mousemove","time":1615,"x":87,"y":91},{"type":"mousedown","time":1824,"x":83,"y":90},{"type":"mousemove","time":1834,"x":83,"y":90},{"type":"mouseup","time":1924,"x":83,"y":90},{"time":1925,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":2746,"x":85,"y":91},{"type":"mousemove","time":2946,"x":617,"y":252},{"type":"mousemove","time":3146,"x":620,"y":263},{"type":"mousedown","time":3376,"x":620,"y":263},{"type":"mouseup","time":3489,"x":620,"y":263},{"time":3490,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":4479,"x":611,"y":253},{"type":"mousemove","time":4679,"x":56,"y":69},{"type":"mousemove","time":4879,"x":55,"y":72},{"type":"mousemove","time":5079,"x":67,"y":84},{"type":"mousemove","time":5284,"x":67,"y":84},{"type":"mousedown","time":5469,"x":67,"y":84},{"type":"mouseup","time":5568,"x":67,"y":84},{"time":5569,"delay":100,"type":"screenshot-auto"}],"scrollY":0,"scrollX":0,"timestamp":1625573395168},{"name":"Action 2","ops":[{"type":"mousedown","time":515,"x":79,"y":82},{"type":"mouseup","time":628,"x":79,"y":82},{"time":629,"delay":100,"type":"screenshot-auto"},{"type":"mousedown","time":2160,"x":79,"y":82},{"type":"mouseup","time":2263,"x":79,"y":82},{"time":2264,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":3626,"x":78,"y":82},{"type":"mousemove","time":3828,"x":56,"y":87},{"type":"mousemove","time":4041,"x":39,"y":88},{"type":"mousedown","time":4228,"x":21,"y":87},{"type":"mousemove","time":4241,"x":21,"y":87},{"type":"mouseup","time":4363,"x":21,"y":87},{"time":4364,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":4458,"x":21,"y":87},{"type":"mousemove","time":5091,"x":21,"y":87},{"type":"mousemove","time":5392,"x":21,"y":87},{"type":"mousemove","time":5594,"x":49,"y":87},{"type":"mousedown","time":5814,"x":64,"y":85},{"type":"mousemove","time":5828,"x":64,"y":85},{"type":"mouseup","time":5905,"x":64,"y":85},{"time":5906,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":6041,"x":64,"y":85},{"type":"mousemove","time":6241,"x":51,"y":85},{"type":"mousemove","time":6441,"x":35,"y":87},{"type":"mousemove","time":6641,"x":22,"y":88},{"type":"mousedown","time":6671,"x":20,"y":88},{"type":"mouseup","time":6748,"x":19,"y":88},{"time":6749,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":6842,"x":19,"y":88},{"type":"mousemove","time":7058,"x":19,"y":88},{"type":"mousemove","time":7258,"x":23,"y":88},{"type":"mousemove","time":7458,"x":29,"y":90},{"type":"mousemove","time":7662,"x":31,"y":91},{"type":"mousemove","time":7878,"x":32,"y":91},{"type":"mousemove","time":8112,"x":73,"y":107}],"scrollY":466.8011474609375,"scrollX":0,"timestamp":1624199608724},{"name":"Action 3","ops":[{"type":"mousedown","time":572,"x":85,"y":204},{"type":"mouseup","time":639,"x":85,"y":204},{"time":640,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":2041,"x":85,"y":204},{"type":"mousemove","time":2241,"x":66,"y":207},{"type":"mousemove","time":2445,"x":44,"y":210},{"type":"mousedown","time":2663,"x":44,"y":210},{"type":"mouseup","time":2746,"x":44,"y":210},{"time":2747,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":3858,"x":44,"y":210},{"type":"mousemove","time":4058,"x":67,"y":208},{"type":"mousemove","time":4258,"x":75,"y":208},{"type":"mousedown","time":4331,"x":77,"y":208},{"type":"mouseup","time":4420,"x":77,"y":208},{"time":4421,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":4464,"x":77,"y":208},{"type":"mousemove","time":4857,"x":76,"y":208},{"type":"mousemove","time":5741,"x":76,"y":208},{"type":"mousemove","time":5941,"x":43,"y":209},{"type":"mousemove","time":6146,"x":27,"y":210},{"type":"mousedown","time":6367,"x":27,"y":210},{"type":"mouseup","time":6457,"x":27,"y":210},{"time":6458,"delay":100,"type":"screenshot-auto"}],"scrollY":844.4351806640625,"scrollX":0,"timestamp":1625573412957}] \ No newline at end of file diff --git a/test/runTest/actions/universalTransition3.json b/test/runTest/actions/universalTransition3.json new file mode 100644 index 0000000000..35192e8324 --- /dev/null +++ b/test/runTest/actions/universalTransition3.json @@ -0,0 +1 @@ +[{"name":"Action 1","ops":[{"type":"mousemove","time":502,"x":120,"y":93},{"type":"mousemove","time":703,"x":122,"y":85},{"type":"mousemove","time":918,"x":122,"y":84},{"type":"mousedown","time":955,"x":122,"y":84},{"type":"mouseup","time":1040,"x":122,"y":84},{"time":1041,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":2351,"x":122,"y":84},{"type":"mousemove","time":2551,"x":163,"y":79},{"type":"mousemove","time":2753,"x":267,"y":75},{"type":"mousemove","time":2960,"x":270,"y":75},{"type":"mousedown","time":3239,"x":270,"y":75},{"type":"mouseup","time":3341,"x":270,"y":75},{"time":3342,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":4585,"x":269,"y":75},{"type":"mousemove","time":4785,"x":192,"y":73},{"type":"mousemove","time":4988,"x":189,"y":74},{"type":"mousedown","time":5024,"x":189,"y":74},{"type":"mouseup","time":5124,"x":189,"y":74},{"time":5125,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":6635,"x":219,"y":74},{"type":"mousemove","time":6838,"x":326,"y":80},{"type":"mousemove","time":7051,"x":310,"y":80},{"type":"mousedown","time":7149,"x":310,"y":80},{"type":"mouseup","time":7273,"x":310,"y":80},{"time":7274,"delay":100,"type":"screenshot-auto"}],"scrollY":0,"scrollX":0,"timestamp":1624199657698},{"name":"Action 2","ops":[{"type":"mousedown","time":725,"x":448,"y":299},{"type":"mouseup","time":815,"x":448,"y":299},{"time":816,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":1618,"x":448,"y":300},{"type":"mousemove","time":1820,"x":446,"y":303},{"type":"mousemove","time":2068,"x":446,"y":303},{"type":"mousemove","time":2268,"x":329,"y":158},{"type":"mousemove","time":2469,"x":287,"y":108},{"type":"mousemove","time":2673,"x":203,"y":81},{"type":"mousedown","time":2806,"x":203,"y":81},{"type":"mouseup","time":2922,"x":203,"y":81},{"time":2923,"delay":100,"type":"screenshot-auto"},{"type":"mousemove","time":3635,"x":203,"y":81},{"type":"mousemove","time":3842,"x":194,"y":78}],"scrollY":0,"scrollX":0,"timestamp":1624199674798}] \ No newline at end of file diff --git a/test/runTest/blacklist.js b/test/runTest/blacklist.js index ba638ed9fb..3bedb0cf4c 100644 --- a/test/runTest/blacklist.js +++ b/test/runTest/blacklist.js @@ -39,7 +39,9 @@ module.exports.blacklist = [ // This case will have timeout 'visualMap-performance1.html', - 'lines-stream-not-large.html' + 'lines-stream-not-large.html', + + 'dataset-performance.html' ]; diff --git a/test/runTest/runtime/main.js b/test/runTest/runtime/main.js index 712e77f1ea..633911d327 100644 --- a/test/runTest/runtime/main.js +++ b/test/runTest/runtime/main.js @@ -112,7 +112,6 @@ window.__VRT_RUN_ACTIONS__ = async function (actions, restoredActionIndex, resto } - window.addEventListener('DOMContentLoaded', () => { let style = document.createElement('style'); // Disable all css animation since it will cause screenshot inconsistent. diff --git a/test/sankey-series-nodes.html b/test/sankey-series-nodes.html new file mode 100644 index 0000000000..7d40be3c11 --- /dev/null +++ b/test/sankey-series-nodes.html @@ -0,0 +1,79 @@ + + + + + + + + + + + + + +
+ + + \ No newline at end of file diff --git a/test/sankey.html b/test/sankey.html index 74091be3d2..1536192b22 100644 --- a/test/sankey.html +++ b/test/sankey.html @@ -25,43 +25,92 @@ + + + + -
+ + +
+
+ + + + + + + + + + + \ No newline at end of file diff --git a/test/scatter.html b/test/scatter.html index 3fd4fbbc9e..19d6eb30dd 100644 --- a/test/scatter.html +++ b/test/scatter.html @@ -76,10 +76,9 @@ } }, tooltip: { - trigger: 'axis', - axisPointer: { - type: 'cross' - } + trigger: 'item', + position: 'top', + borderWidth: 4 }, xAxis: { type: 'value', @@ -158,4 +157,4 @@ - \ No newline at end of file + diff --git a/test/timeline-event.html b/test/timeline-event.html new file mode 100644 index 0000000000..621afb4840 --- /dev/null +++ b/test/timeline-event.html @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + diff --git a/test/tooltip-domnode.html b/test/tooltip-domnode.html index 50686c3275..dc53d02e69 100644 --- a/test/tooltip-domnode.html +++ b/test/tooltip-domnode.html @@ -33,6 +33,9 @@
+
+
+
+ + + + + diff --git a/test/tree-basic.html b/test/tree-basic.html index 07bd6978e6..a9ea9ff1d3 100644 --- a/test/tree-basic.html +++ b/test/tree-basic.html @@ -114,13 +114,13 @@ }); setTimeout(function() { - var newData = echarts.util.clone(data); - newData.children.splice(0, 1); + // replace root node + var newData = [data.children[1]]; chart.setOption({ series: [{ type: 'tree', id: '3', - data: [newData] + data: newData }] }, false); }, 1000); diff --git a/test/treemap-setOption-twice-noAnimation.html b/test/treemap-setOption-twice-noAnimation.html new file mode 100644 index 0000000000..273cb53b17 --- /dev/null +++ b/test/treemap-setOption-twice-noAnimation.html @@ -0,0 +1,121 @@ + + + + + + + + + + + + + +
+ + + + diff --git a/test/treemap-simple.html b/test/treemap-simple.html index b61679c4c1..c71b8122ca 100644 --- a/test/treemap-simple.html +++ b/test/treemap-simple.html @@ -1,4 +1,3 @@ - - - - - - - - -
- + + + - var chart = echarts.init(document.getElementById('main'), null, { + - }); + +
+
+ - + var chart1 = echarts.init(document.getElementById('main2')); + option = { + series: [{ + label: { + show: false + }, + emphasis: { + label: { + show: true, + formatter() { + return 11111; + } + } + }, + type: 'treemap', + data: [{ + name: 'nodeA', // First tree + value: 10, + children: [{ + name: 'nodeAa', // First leaf of first tree + value: 4 + }, { + name: 'nodeAb', // Second leaf of first tree + value: 6 + }] + }] + }] + }; + + chart1.setOption(option); + }); + + + + \ No newline at end of file diff --git a/test/types/event.ts b/test/types/event.ts index af3511f8f4..6980abecd2 100644 --- a/test/types/event.ts +++ b/test/types/event.ts @@ -34,9 +34,16 @@ chart.setOption(option); // Mouse event. chart.on('click', function (params) { console.log(params.name); + this.off('click'); }); // Rendered event. chart.on('rendered', function (params) { console.log(params.elapsedTime); + this.off('rendered'); +}); + +chart.getZr().on('click', function (params) { + console.log(params.offsetX); + this.off('click'); }); \ No newline at end of file diff --git a/test/universalTransition.html b/test/universalTransition.html new file mode 100644 index 0000000000..0599a26274 --- /dev/null +++ b/test/universalTransition.html @@ -0,0 +1,912 @@ + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + diff --git a/test/universalTransition2.html b/test/universalTransition2.html new file mode 100644 index 0000000000..ef3a998a13 --- /dev/null +++ b/test/universalTransition2.html @@ -0,0 +1,498 @@ + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + + + + + + + + + + + + diff --git a/test/universalTransition3.html b/test/universalTransition3.html new file mode 100644 index 0000000000..fc0b226f6f --- /dev/null +++ b/test/universalTransition3.html @@ -0,0 +1,432 @@ + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + diff --git a/test/ut/jest.config.js b/test/ut/jest.config.js index 9946de2a14..5ca8031953 100644 --- a/test/ut/jest.config.js +++ b/test/ut/jest.config.js @@ -17,6 +17,9 @@ * under the License. */ +const { pathsToModuleNameMapper } = require('ts-jest/utils'); +const { compilerOptions } = require('./tsconfig') + module.exports = { preset: 'ts-jest', testEnvironment: 'jsdom', @@ -44,5 +47,8 @@ module.exports = { '**/spec/model/*.test.ts', '**/spec/scale/*.test.ts', '**/spec/util/*.test.ts' - ] + ], + moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { + prefix: '/' + }) }; diff --git a/test/ut/spec/data/List.test.ts b/test/ut/spec/data/List.test.ts deleted file mode 100644 index c136afc31b..0000000000 --- a/test/ut/spec/data/List.test.ts +++ /dev/null @@ -1,557 +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. -*/ - -/* global Float32Array */ - -import List from '../../../../src/data/List'; -import Model from '../../../../src/model/Model'; -import { createSourceFromSeriesDataOption, Source, createSource } from '../../../../src/data/Source'; -import { OptionDataItemObject, OptionDataValue, SOURCE_FORMAT_ARRAY_ROWS } from '../../../../src/util/types'; -import DataDimensionInfo from '../../../../src/data/DataDimensionInfo'; -import OrdinalMeta from '../../../../src/data/OrdinalMeta'; - - -const ID_PREFIX = 'e\0\0'; -const NAME_REPEAT_PREFIX = '__ec__'; - - -describe('List', function () { - - describe('Data Manipulation', function () { - - it('initData 1d', function () { - const list = new List(['x', 'y'], new Model()); - list.initData([10, 20, 30]); - expect(list.get('x', 0)).toEqual(10); - expect(list.get('x', 1)).toEqual(20); - expect(list.get('x', 2)).toEqual(30); - expect(list.get('y', 1)).toEqual(20); - }); - - it('initData 2d', function () { - const list = new List(['x', 'y'], new Model()); - list.initData([[10, 15], [20, 25], [30, 35]]); - expect(list.get('x', 1)).toEqual(20); - expect(list.get('y', 1)).toEqual(25); - }); - - it('initData 2d yx', function () { - const list = new List(['y', 'x'], new Model()); - list.initData([[10, 15], [20, 25], [30, 35]]); - expect(list.get('x', 1)).toEqual(25); - expect(list.get('y', 1)).toEqual(20); - }); - - it('Data with option 1d', function () { - const list = new List(['x', 'y'], new Model()); - list.initData([ - 1, - { - value: 2, - somProp: 'foo' - } as OptionDataItemObject - ]); - expect(list.getItemModel(1).get('somProp' as any)).toEqual('foo'); - expect(list.getItemModel(0).get('somProp' as any)).toBeNull(); - }); - - it('Empty data', function () { - const list = new List(['x', 'y'], new Model()); - list.initData([1, '-']); - expect(list.get('y', 1)).toBeNaN(); - }); - - it('getRawValue', function () { - const list1 = new List(['x', 'y'], new Model()); - // here construct a new list2 because if we only use one list - // to call initData() twice, list._chunkCount will be accumulated - // to 1 instead of 0. - const list2 = new List(['x', 'y'], new Model()); - - list1.initData([1, 2, 3]); - expect(list1.getItemModel(1).option).toEqual(2); - - list2.initData([[10, 15], [20, 25], [30, 35]]); - expect(list2.getItemModel(1).option).toEqual([20, 25]); - }); - - it('indexOfRawIndex', function () { - const list = new List(['x'], new Model()); - list.initData([]); - expect(list.indexOfRawIndex(1)).toEqual(-1); - - const list1 = new List(['x'], new Model()); - list1.initData([0]); - expect(list1.indexOfRawIndex(0)).toEqual(0); - expect(list1.indexOfRawIndex(1)).toEqual(-1); - - const list2 = new List(['x'], new Model()); - list2.initData([0, 1, 2, 3]); - expect(list2.indexOfRawIndex(1)).toEqual(1); - expect(list2.indexOfRawIndex(2)).toEqual(2); - expect(list2.indexOfRawIndex(5)).toEqual(-1); - - const list3 = new List(['x'], new Model()); - list3.initData([0, 1, 2, 3, 4]); - expect(list3.indexOfRawIndex(2)).toEqual(2); - expect(list3.indexOfRawIndex(3)).toEqual(3); - expect(list3.indexOfRawIndex(5)).toEqual(-1); - - list3.filterSelf(function (idx) { - return idx >= 2; - }); - expect(list3.indexOfRawIndex(2)).toEqual(0); - }); - - it('getDataExtent', function () { - const list = new List(['x', 'y'], new Model()); - list.initData([1, 2, 3]); - expect(list.getDataExtent('x')).toEqual([1, 3]); - expect(list.getDataExtent('y')).toEqual([1, 3]); - }); - - it('Data types', function () { - const list = new List([{ - name: 'x', - type: 'int' - }, { - name: 'y', - type: 'float' - }], new Model()); - list.initData([[1.1, 1.1]]); - expect(list.get('x', 0)).toEqual(1); - expect(list.get('y', 0)).toBeCloseTo(1.1, 5); - }); - - it('map', function () { - const list = new List(['x', 'y'], new Model()); - list.initData([[10, 15], [20, 25], [30, 35]]); - expect(list.map(['x', 'y'], function (x: number, y: number) { - return [x + 2, y + 2]; - }).mapArray('x', function (x) { - return x; - })).toEqual([12, 22, 32]); - }); - - it('mapArray', function () { - const list = new List(['x', 'y'], new Model()); - list.initData([[10, 15], [20, 25], [30, 35]]); - expect(list.mapArray(['x', 'y'], function (x, y) { - return [x, y]; - })).toEqual([[10, 15], [20, 25], [30, 35]]); - }); - - it('filterSelf', function () { - const list = new List(['x', 'y'], new Model()); - list.initData([[10, 15], [20, 25], [30, 35]]); - expect(list.filterSelf(['x', 'y'], function (x, y) { - return x < 30 && x > 10; - }).mapArray('x', function (x) { - return x; - })).toEqual([20]); - }); - - it('dataProvider', function () { - const list = new List(['x', 'y'], new Model()); - const typedArray = new Float32Array([10, 10, 20, 20]); - const source = createSourceFromSeriesDataOption(typedArray); - list.initData({ - count: function (): number { - return typedArray.length / 2; - }, - getItem: function (idx: number): number[] { - return [typedArray[idx * 2], typedArray[idx * 2 + 1]]; - }, - getSource: function (): Source { - return source; - } - }); - expect(list.mapArray(['x', 'y'], function (x, y) { - return [x, y]; - })).toEqual([[10, 10], [20, 20]]); - expect(list.getRawDataItem(0)).toEqual([10, 10]); - expect(list.getItemModel(0).option).toEqual([10, 10]); - }); - }); - - describe('Data read', function () { - it('indicesOfNearest', function () { - const list = new List(['value'], new Model()); - // ---- index: 0 1 2 3 4 5 6 7 - list.initData([10, 20, 30, 35, 40, 40, 35, 50]); - - expect(list.indicesOfNearest('value', 24.5)).toEqual([1]); - expect(list.indicesOfNearest('value', 25)).toEqual([1]); - expect(list.indicesOfNearest('value', 25.5)).toEqual([2]); - expect(list.indicesOfNearest('value', 25.5)).toEqual([2]); - expect(list.indicesOfNearest('value', 41)).toEqual([4, 5]); - expect(list.indicesOfNearest('value', 39)).toEqual([4, 5]); - expect(list.indicesOfNearest('value', 41)).toEqual([4, 5]); - expect(list.indicesOfNearest('value', 36)).toEqual([3, 6]); - - expect(list.indicesOfNearest('value', 50.6, 0.5)).toEqual([]); - expect(list.indicesOfNearest('value', 50.5, 0.5)).toEqual([7]); - }); - }); - - describe('id_and_name', function () { - - function makeOneByOneChecker(list: List) { - let getIdDataIndex = 0; - let getNameDataIndex = 0; - - return { - idEqualsTo: function (expectedId: string): void { - expect(list.getId(getIdDataIndex)).toEqual(expectedId); - getIdDataIndex++; - }, - nameEqualsTo: function (expectedName: string): void { - expect(list.getName(getNameDataIndex)).toEqual(expectedName); - getNameDataIndex++; - }, - currGetIdDataIndex: function (): number { - return getIdDataIndex; - }, - currGetNameDataIndex: function (): number { - return getNameDataIndex; - } - }; - } - - describe('only_name_declared', function () { - - function doChecks(list: List) { - const oneByOne = makeOneByOneChecker(list); - - oneByOne.idEqualsTo('a'); - oneByOne.idEqualsTo('b'); - oneByOne.idEqualsTo(`b${NAME_REPEAT_PREFIX}2`); - oneByOne.idEqualsTo('c'); - oneByOne.idEqualsTo(`${ID_PREFIX}4`); - oneByOne.idEqualsTo(`c${NAME_REPEAT_PREFIX}2`); - oneByOne.idEqualsTo('d'); - oneByOne.idEqualsTo(`c${NAME_REPEAT_PREFIX}3`); - - oneByOne.nameEqualsTo('a'); - oneByOne.nameEqualsTo('b'); - oneByOne.nameEqualsTo('b'); - oneByOne.nameEqualsTo('c'); - oneByOne.nameEqualsTo(''); - oneByOne.nameEqualsTo('c'); - oneByOne.nameEqualsTo('d'); - oneByOne.nameEqualsTo('c'); - } - - it('sourceFormatOriginal', function () { - const list = new List(['x', 'y'], new Model()); - list.initData([ - { value: 10, name: 'a' }, - { value: 20, name: 'b' }, - { value: 30, name: 'b' }, - { value: 40, name: 'c' }, - { value: 50 }, // name not declared - { value: 60, name: 'c' }, - { value: 70, name: 'd' }, - { value: 80, name: 'c' } - ]); - - doChecks(list); - }); - - it('sourceFormatArrayRows', function () { - const list = new List( - [ - 'x', - { name: 'q', type: 'ordinal', otherDims: { itemName: 0 } } - ], - new Model() - ); - const source = createSource( - [ - [ 10, 'a' ], - [ 20, 'b' ], - [ 30, 'b' ], - [ 40, 'c' ], - [ 50, null ], - [ 60, 'c' ], - [ 70, 'd' ], - [ 80, 'c' ] - ], - { - seriesLayoutBy: 'column', - sourceHeader: 0, - dimensions: null - }, - SOURCE_FORMAT_ARRAY_ROWS, - { - itemName: 1 - } - ); - list.initData(source); - - doChecks(list); - }); - }); - - - describe('id_name_declared_sourceFormat_original', function () { - - it('sourceFormatOriginal', function () { - const list = new List(['x'], new Model()); - const oneByOne = makeOneByOneChecker(list); - - list.initData([ - { value: 0, id: 'myId_10' }, - { value: 10, id: 555 }, // numeric id. - { value: 20, id: '666%' }, - { value: 30, id: 'myId_good', name: 'b' }, - { value: 40, name: 'b' }, - { value: 50, id: null }, - { value: 60, id: undefined }, - { value: 70, id: NaN }, - { value: 80, id: '' }, - { value: 90, name: 'b' }, - { value: 100 }, - { value: 110, id: 'myId_better' }, - { value: 120, id: 'myId_better' } // duplicated id. - ]); - - oneByOne.idEqualsTo('myId_10'); - oneByOne.idEqualsTo('555'); - oneByOne.idEqualsTo('666%'); - oneByOne.idEqualsTo('myId_good'); - oneByOne.idEqualsTo('b'); - oneByOne.idEqualsTo(`${ID_PREFIX}${oneByOne.currGetIdDataIndex()}`); - oneByOne.idEqualsTo(`${ID_PREFIX}${oneByOne.currGetIdDataIndex()}`); - oneByOne.idEqualsTo('NaN'); - oneByOne.idEqualsTo(''); - oneByOne.idEqualsTo(`b${NAME_REPEAT_PREFIX}2`); - oneByOne.idEqualsTo(`${ID_PREFIX}${oneByOne.currGetIdDataIndex()}`); - oneByOne.idEqualsTo('myId_better'); - oneByOne.idEqualsTo('myId_better'); - - oneByOne.nameEqualsTo(''); - oneByOne.nameEqualsTo(''); - oneByOne.nameEqualsTo(''); - oneByOne.nameEqualsTo('b'); - oneByOne.nameEqualsTo('b'); - oneByOne.nameEqualsTo(''); - oneByOne.nameEqualsTo(''); - oneByOne.nameEqualsTo(''); - oneByOne.nameEqualsTo(''); - oneByOne.nameEqualsTo('b'); - oneByOne.nameEqualsTo(''); - oneByOne.nameEqualsTo(''); - oneByOne.nameEqualsTo(''); - - list.appendData([ - { value: 200, id: 'myId_best' }, - { value: 210, id: 999 }, // numeric id. - { value: 220, id: '777px' }, - { value: 230, name: 'b' }, - { value: 240 } - ]); - - oneByOne.idEqualsTo('myId_best'); - oneByOne.idEqualsTo('999'); - oneByOne.idEqualsTo('777px'); - oneByOne.idEqualsTo(`b${NAME_REPEAT_PREFIX}3`); - oneByOne.idEqualsTo(`${ID_PREFIX}${oneByOne.currGetIdDataIndex()}`); - - oneByOne.nameEqualsTo(''); - oneByOne.nameEqualsTo(''); - oneByOne.nameEqualsTo(''); - oneByOne.nameEqualsTo('b'); - oneByOne.nameEqualsTo(''); - - list.appendValues( - [ - [300], - [310], - [320] - ], - [ - 'b', - 'c', - null - ] - ); - - oneByOne.idEqualsTo(`b${NAME_REPEAT_PREFIX}4`); - oneByOne.idEqualsTo('c'); - oneByOne.idEqualsTo(`${ID_PREFIX}${oneByOne.currGetIdDataIndex()}`); - - oneByOne.nameEqualsTo('b'); - oneByOne.nameEqualsTo('c'); - oneByOne.nameEqualsTo(''); - }); - - }); - - describe('id_name_declared_sourceFormat_arrayRows', function () { - - function makeChecker(list: List) { - const oneByOne = makeOneByOneChecker(list); - return { - checkAfterInitData() { - oneByOne.idEqualsTo('myId_10'); - oneByOne.idEqualsTo('555'); - oneByOne.idEqualsTo('666%'); - oneByOne.idEqualsTo('myId_good'); - oneByOne.idEqualsTo(`${ID_PREFIX}${oneByOne.currGetIdDataIndex()}`); - oneByOne.idEqualsTo(`${ID_PREFIX}${oneByOne.currGetIdDataIndex()}`); - oneByOne.idEqualsTo(`${ID_PREFIX}${oneByOne.currGetIdDataIndex()}`); - oneByOne.idEqualsTo('NaN'); - oneByOne.idEqualsTo(''); - oneByOne.idEqualsTo(`${ID_PREFIX}${oneByOne.currGetIdDataIndex()}`); - oneByOne.idEqualsTo(`${ID_PREFIX}${oneByOne.currGetIdDataIndex()}`); - oneByOne.idEqualsTo('myId_better'); - oneByOne.idEqualsTo('myId_better'); - - oneByOne.nameEqualsTo(''); - oneByOne.nameEqualsTo(''); - oneByOne.nameEqualsTo(''); - oneByOne.nameEqualsTo('b'); - oneByOne.nameEqualsTo('b'); - oneByOne.nameEqualsTo(''); - oneByOne.nameEqualsTo(''); - oneByOne.nameEqualsTo(''); - oneByOne.nameEqualsTo(''); - oneByOne.nameEqualsTo('b'); - oneByOne.nameEqualsTo(''); - oneByOne.nameEqualsTo(''); - oneByOne.nameEqualsTo(''); - }, - checkAfterAppendData() { - oneByOne.idEqualsTo('myId_best'); - oneByOne.idEqualsTo('999'); - oneByOne.idEqualsTo('777px'); - oneByOne.idEqualsTo(`${ID_PREFIX}${oneByOne.currGetIdDataIndex()}`); - oneByOne.idEqualsTo(`${ID_PREFIX}${oneByOne.currGetIdDataIndex()}`); - - oneByOne.nameEqualsTo(''); - oneByOne.nameEqualsTo(''); - oneByOne.nameEqualsTo(''); - oneByOne.nameEqualsTo('b'); - oneByOne.nameEqualsTo(''); - }, - checkAfterAppendValues() { - oneByOne.idEqualsTo(`${ID_PREFIX}${oneByOne.currGetIdDataIndex()}`); - oneByOne.idEqualsTo(`${ID_PREFIX}${oneByOne.currGetIdDataIndex()}`); - oneByOne.idEqualsTo(`${ID_PREFIX}${oneByOne.currGetIdDataIndex()}`); - - oneByOne.nameEqualsTo('b'); - oneByOne.nameEqualsTo('c'); - oneByOne.nameEqualsTo(''); - } - }; - } - - it('no_ordinalMeta', function () { - testArrayRowsInSource([ - { name: 'x', type: 'number' }, - { name: 'p', type: 'ordinal', otherDims: { itemId: 0 } }, - { name: 'q', type: 'ordinal', otherDims: { itemName: 0 } } - ]); - }); - - it('has_ordinalMeta', function () { - const ordinalMetaP = new OrdinalMeta({ - categories: [], - needCollect: true, - deduplication: true - }); - const ordinalMetaQ = new OrdinalMeta({ - categories: [], - needCollect: true, - deduplication: true - }); - testArrayRowsInSource([ - { name: 'x', type: 'number' }, - { name: 'p', type: 'ordinal', otherDims: { itemId: 0 }, ordinalMeta: ordinalMetaP }, - { name: 'q', type: 'ordinal', otherDims: { itemName: 0 }, ordinalMeta: ordinalMetaQ } - ]); - }); - - function testArrayRowsInSource(dimensionsInfo: DataDimensionInfo[]): void { - const list = new List(dimensionsInfo, new Model()); - const checker = makeChecker(list); - - const source = createSource( - [ - [0, 'myId_10', null], - [10, 555, null], // numeric id. - [20, '666%', null], - [30, 'myId_good', 'b'], - [40, null, 'b'], - [50, null, null], - [60, undefined, null], - [70, NaN, null], - [80, '', null], - [90, null, 'b'], - [100, null, null], - [110, 'myId_better', null], - [120, 'myId_better', null] // duplicated id. - ], - { - seriesLayoutBy: 'column', - sourceHeader: 0, - dimensions: null - }, - SOURCE_FORMAT_ARRAY_ROWS, - { - itemId: 1, - itemName: 2 - } - ); - list.initData(source); - - checker.checkAfterInitData(); - - list.appendData([ - [ 200, 'myId_best', null ], - [ 210, 999, null ], // numeric id. - [ 220, '777px', null], - [ 230, null, 'b' ], - [ 240, null, null ] - ]); - - checker.checkAfterAppendData(); - - list.appendValues( - [ - [300], - [310], - [320] - ], - [ - 'b', - 'c', - null - ] - ); - - checker.checkAfterAppendValues(); - } - - }); - - }); -}); diff --git a/test/ut/spec/data/SeriesData.test.ts b/test/ut/spec/data/SeriesData.test.ts new file mode 100644 index 0000000000..564772b310 --- /dev/null +++ b/test/ut/spec/data/SeriesData.test.ts @@ -0,0 +1,585 @@ + +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +/* global Float32Array */ + +import SeriesData from '@/src/data/SeriesData'; +import Model from '@/src/model/Model'; +import { createSourceFromSeriesDataOption, Source, createSource } from '@/src/data/Source'; +import { OptionDataItemObject, + OptionDataValue, + SOURCE_FORMAT_ARRAY_ROWS, + SOURCE_FORMAT_ORIGINAL } from '@/src/util/types'; +import SeriesDimensionDefine from '@/src/data/SeriesDimensionDefine'; +import OrdinalMeta from '@/src/data/OrdinalMeta'; +import DataStore from '@/src/data/DataStore'; +import { DefaultDataProvider } from '@/src/data/helper/dataProvider'; +import { SeriesDataSchema } from '@/src/data/helper/SeriesDataSchema'; + + +const ID_PREFIX = 'e\0\0'; +const NAME_REPEAT_PREFIX = '__ec__'; + + +describe('SeriesData', function () { + + describe('Data Manipulation', function () { + + it('initData 1d', function () { + const data = new SeriesData(['x', 'y'], new Model()); + data.initData([10, 20, 30]); + expect(data.get('x', 0)).toEqual(10); + expect(data.get('x', 1)).toEqual(20); + expect(data.get('x', 2)).toEqual(30); + expect(data.get('y', 1)).toEqual(20); + }); + + it('initData 2d', function () { + const data = new SeriesData(['x', 'y'], new Model()); + data.initData([[10, 15], [20, 25], [30, 35]]); + expect(data.get('x', 1)).toEqual(20); + expect(data.get('y', 1)).toEqual(25); + }); + + it('initData 2d yx', function () { + const data = new SeriesData(['y', 'x'], new Model()); + data.initData([[10, 15], [20, 25], [30, 35]]); + expect(data.get('x', 1)).toEqual(25); + expect(data.get('y', 1)).toEqual(20); + }); + + it('Data with option 1d', function () { + const data = new SeriesData(['x', 'y'], new Model()); + data.initData([ + 1, + { + value: 2, + somProp: 'foo' + } as OptionDataItemObject + ]); + expect(data.getItemModel(1).get('somProp' as any)).toEqual('foo'); + expect(data.getItemModel(0).get('somProp' as any)).toBeNull(); + }); + + it('Empty data', function () { + const data = new SeriesData(['x', 'y'], new Model()); + data.initData([1, '-']); + expect(data.get('y', 1)).toBeNaN(); + }); + + it('getRawValue', function () { + const data1 = new SeriesData(['x', 'y'], new Model()); + // here construct a new data2 because if we only use one data + // to call initData() twice, data._chunkCount will be accumulated + // to 1 instead of 0. + const data2 = new SeriesData(['x', 'y'], new Model()); + + data1.initData([1, 2, 3]); + expect(data1.getItemModel(1).option).toEqual(2); + + data2.initData([[10, 15], [20, 25], [30, 35]]); + expect(data2.getItemModel(1).option).toEqual([20, 25]); + }); + + it('indexOfRawIndex', function () { + const data = new SeriesData(['x'], new Model()); + data.initData([]); + expect(data.indexOfRawIndex(1)).toEqual(-1); + + const data1 = new SeriesData(['x'], new Model()); + data1.initData([0]); + expect(data1.indexOfRawIndex(0)).toEqual(0); + expect(data1.indexOfRawIndex(1)).toEqual(-1); + + const data2 = new SeriesData(['x'], new Model()); + data2.initData([0, 1, 2, 3]); + expect(data2.indexOfRawIndex(1)).toEqual(1); + expect(data2.indexOfRawIndex(2)).toEqual(2); + expect(data2.indexOfRawIndex(5)).toEqual(-1); + + const data3 = new SeriesData(['x'], new Model()); + data3.initData([0, 1, 2, 3, 4]); + expect(data3.indexOfRawIndex(2)).toEqual(2); + expect(data3.indexOfRawIndex(3)).toEqual(3); + expect(data3.indexOfRawIndex(5)).toEqual(-1); + + data3.filterSelf(function (idx) { + return idx >= 2; + }); + expect(data3.indexOfRawIndex(2)).toEqual(0); + }); + + it('getDataExtent', function () { + const data = new SeriesData(['x', 'y'], new Model()); + data.initData([1, 2, 3]); + expect(data.getDataExtent('x')).toEqual([1, 3]); + expect(data.getDataExtent('y')).toEqual([1, 3]); + }); + + it('Data types', function () { + const data = new SeriesData([{ + name: 'x', + type: 'int' + }, { + name: 'y', + type: 'float' + }], new Model()); + data.initData([[1.1, 1.1]]); + expect(data.get('x', 0)).toEqual(1); + expect(data.get('y', 0)).toBeCloseTo(1.1, 5); + }); + + it('map', function () { + const data = new SeriesData(['x', 'y'], new Model()); + data.initData([[10, 15], [20, 25], [30, 35]]); + expect(data.map(['x', 'y'], function (x: number, y: number) { + return [x + 2, y + 2]; + }).mapArray('x', function (x) { + return x; + })).toEqual([12, 22, 32]); + }); + + it('mapArray', function () { + const data = new SeriesData(['x', 'y'], new Model()); + data.initData([[10, 15], [20, 25], [30, 35]]); + expect(data.mapArray(['x', 'y'], function (x, y) { + return [x, y]; + })).toEqual([[10, 15], [20, 25], [30, 35]]); + }); + + it('filterSelf', function () { + const data = new SeriesData(['x', 'y'], new Model()); + data.initData([[10, 15], [20, 25], [30, 35]]); + expect(data.filterSelf(['x', 'y'], function (x, y) { + return x < 30 && x > 10; + }).mapArray('x', function (x) { + return x; + })).toEqual([20]); + }); + + it('dataProvider', function () { + const data = new SeriesData(['x', 'y'], new Model()); + const typedArray = new Float32Array([10, 10, 20, 20]); + const source = createSourceFromSeriesDataOption(typedArray); + data.initData({ + count: function (): number { + return typedArray.length / 2; + }, + getItem: function (idx: number): number[] { + return [typedArray[idx * 2], typedArray[idx * 2 + 1]]; + }, + getSource: function (): Source { + return source; + } + }); + expect(data.mapArray(['x', 'y'], function (x, y) { + return [x, y]; + })).toEqual([[10, 10], [20, 20]]); + expect(data.getRawDataItem(0)).toEqual([10, 10]); + expect(data.getItemModel(0).option).toEqual([10, 10]); + }); + }); + + describe('Data store', function () { + it('should guess ordinal correctly', function () { + const source = createSource([['A', 15], ['B', 25], ['C', 35]], { + dimensions: ['A', 'B'], + seriesLayoutBy: null, + sourceHeader: false + }, SOURCE_FORMAT_ORIGINAL); + expect(source.dimensionsDefine[0].type).toEqual('ordinal'); + }); + + function createStore() { + const provider = new DefaultDataProvider([['A', 15], ['B', 25], ['C', 35]]); + const store = new DataStore(); + store.initData(provider, [{type: 'ordinal'}, {type: 'float'}]); + return store; + } + + + it('SeriesData can still get other dims value from store when only part of dims are given.', function () { + const source = createSource( + [['A', 15, 20, 'cat'], ['B', 25, 30, 'mouse'], ['C', 35, 40, 'dog']], + { + dimensions: null, + seriesLayoutBy: null, + sourceHeader: false + }, + SOURCE_FORMAT_ARRAY_ROWS + ); + const store = new DataStore(); + store.initData(new DefaultDataProvider(source), [ + {type: 'ordinal'}, {type: 'float'}, {type: 'float'}, {type: 'ordinal'} + ]); + const schema = new SeriesDataSchema({ + source: source, + dimensions: [ + { type: 'float', name: 'dim1', storeDimIndex: 1 }, + { type: 'ordinal', name: 'dim3', storeDimIndex: 3 } + ], + fullDimensionCount: 2, + dimensionOmitted: true + }); + const data = new SeriesData(schema, null); + data.initData(store); + // Store should be the same. + expect(data.getStore()).toBe(store); + // Get self dim + expect(data.get('dim1', 0)).toEqual(15); + expect(data.get('dim1', 1)).toEqual(25); + // Get other dim + expect(data.getStore().get(0, 0)).toEqual('A'); + expect(data.getStore().get(0, 1)).toEqual('B'); + expect(data.getStore().get(2, 0)).toEqual(20); + expect(data.getStore().get(2, 1)).toEqual(30); + // Get all + expect(data.getValues(['dim3', 'dim1'], 0)).toEqual(['cat', 15]); + expect(data.getValues(1)).toEqual(['B', 25, 30, 'mouse']); + }); + + it('SeriesData#cloneShallow should share store', function () { + const store = createStore(); + const dims = [{ type: 'float', name: 'dim2' }]; + const data = new SeriesData(dims, null); + data.initData(store); + const data2 = data.cloneShallow(); + expect(data2.getStore()).toBe(data.getStore()); + }); + }); + + describe('Data read', function () { + it('indicesOfNearest', function () { + const data = new SeriesData(['value'], new Model()); + // ---- index: 0 1 2 3 4 5 6 7 + data.initData([10, 20, 30, 35, 40, 40, 35, 50]); + + expect(data.indicesOfNearest('value', 24.5)).toEqual([1]); + expect(data.indicesOfNearest('value', 25)).toEqual([1]); + expect(data.indicesOfNearest('value', 25.5)).toEqual([2]); + expect(data.indicesOfNearest('value', 25.5)).toEqual([2]); + expect(data.indicesOfNearest('value', 41)).toEqual([4, 5]); + expect(data.indicesOfNearest('value', 39)).toEqual([4, 5]); + expect(data.indicesOfNearest('value', 41)).toEqual([4, 5]); + expect(data.indicesOfNearest('value', 36)).toEqual([3, 6]); + + expect(data.indicesOfNearest('value', 50.6, 0.5)).toEqual([]); + expect(data.indicesOfNearest('value', 50.5, 0.5)).toEqual([7]); + }); + }); + + describe('id_and_name', function () { + + function makeOneByOneChecker(list: SeriesData) { + let getIdDataIndex = 0; + let getNameDataIndex = 0; + + return { + nextIdEqualsTo: function (expectedId: string): void { + expect(list.getId(getIdDataIndex)).toEqual(expectedId); + getIdDataIndex++; + }, + nextNameEqualsTo: function (expectedName: string): void { + expect(list.getName(getNameDataIndex)).toEqual(expectedName); + getNameDataIndex++; + }, + currGetIdDataIndex: function (): number { + return getIdDataIndex; + }, + currGetNameDataIndex: function (): number { + return getNameDataIndex; + } + }; + } + + describe('only_name_declared', function () { + + function doChecks(list: SeriesData) { + const oneByOne = makeOneByOneChecker(list); + + oneByOne.nextIdEqualsTo('a'); + oneByOne.nextIdEqualsTo('b'); + oneByOne.nextIdEqualsTo(`b${NAME_REPEAT_PREFIX}2`); + oneByOne.nextIdEqualsTo('c'); + oneByOne.nextIdEqualsTo(`${ID_PREFIX}4`); + oneByOne.nextIdEqualsTo(`c${NAME_REPEAT_PREFIX}2`); + oneByOne.nextIdEqualsTo('d'); + oneByOne.nextIdEqualsTo(`c${NAME_REPEAT_PREFIX}3`); + + oneByOne.nextNameEqualsTo('a'); + oneByOne.nextNameEqualsTo('b'); + oneByOne.nextNameEqualsTo('b'); + oneByOne.nextNameEqualsTo('c'); + oneByOne.nextNameEqualsTo(''); + oneByOne.nextNameEqualsTo('c'); + oneByOne.nextNameEqualsTo('d'); + oneByOne.nextNameEqualsTo('c'); + } + + it('sourceFormatOriginal', function () { + const list = new SeriesData(['x', 'y'], new Model()); + list.initData([ + { value: 10, name: 'a' }, + { value: 20, name: 'b' }, + { value: 30, name: 'b' }, + { value: 40, name: 'c' }, + { value: 50 }, // name not declared + { value: 60, name: 'c' }, + { value: 70, name: 'd' }, + { value: 80, name: 'c' } + ]); + + doChecks(list); + }); + + it('sourceFormatArrayRows', function () { + const list = new SeriesData( + [ + 'x', + { name: 'q', type: 'ordinal', otherDims: { itemName: 0 } } + ], + new Model() + ); + const source = createSource( + [ + [ 10, 'a' ], + [ 20, 'b' ], + [ 30, 'b' ], + [ 40, 'c' ], + [ 50, null ], + [ 60, 'c' ], + [ 70, 'd' ], + [ 80, 'c' ] + ], + { + seriesLayoutBy: 'column', + sourceHeader: 0, + dimensions: null + }, + SOURCE_FORMAT_ARRAY_ROWS + ); + list.initData(source); + + doChecks(list); + }); + }); + + + describe('id_name_declared_sourceFormat_original', function () { + + it('sourceFormatOriginal', function () { + const list = new SeriesData(['x'], new Model()); + const oneByOne = makeOneByOneChecker(list); + + list.initData([ + { value: 0, id: 'myId_10' }, + { value: 10, id: 555 }, // numeric id. + { value: 20, id: '666%' }, + { value: 30, id: 'myId_good', name: 'b' }, + { value: 40, name: 'b' }, + { value: 50, id: null }, + { value: 60, id: undefined }, + { value: 70, id: NaN }, + { value: 80, id: '' }, + { value: 90, name: 'b' }, + { value: 100 }, + { value: 110, id: 'myId_better' }, + { value: 120, id: 'myId_better' } // duplicated id. + ]); + + oneByOne.nextIdEqualsTo('myId_10'); + oneByOne.nextIdEqualsTo('555'); + oneByOne.nextIdEqualsTo('666%'); + oneByOne.nextIdEqualsTo('myId_good'); + oneByOne.nextIdEqualsTo('b'); + oneByOne.nextIdEqualsTo(`${ID_PREFIX}${oneByOne.currGetIdDataIndex()}`); + oneByOne.nextIdEqualsTo(`${ID_PREFIX}${oneByOne.currGetIdDataIndex()}`); + oneByOne.nextIdEqualsTo('NaN'); + oneByOne.nextIdEqualsTo(''); + oneByOne.nextIdEqualsTo(`b${NAME_REPEAT_PREFIX}2`); + oneByOne.nextIdEqualsTo(`${ID_PREFIX}${oneByOne.currGetIdDataIndex()}`); + oneByOne.nextIdEqualsTo('myId_better'); + oneByOne.nextIdEqualsTo('myId_better'); + + oneByOne.nextNameEqualsTo(''); + oneByOne.nextNameEqualsTo(''); + oneByOne.nextNameEqualsTo(''); + oneByOne.nextNameEqualsTo('b'); + oneByOne.nextNameEqualsTo('b'); + oneByOne.nextNameEqualsTo(''); + oneByOne.nextNameEqualsTo(''); + oneByOne.nextNameEqualsTo(''); + oneByOne.nextNameEqualsTo(''); + oneByOne.nextNameEqualsTo('b'); + oneByOne.nextNameEqualsTo(''); + oneByOne.nextNameEqualsTo(''); + oneByOne.nextNameEqualsTo(''); + + list.appendData([ + { value: 200, id: 'myId_best' }, + { value: 210, id: 999 }, // numeric id. + { value: 220, id: '777px' }, + { value: 230, name: 'b' }, + { value: 240 } + ]); + + oneByOne.nextIdEqualsTo('myId_best'); + oneByOne.nextIdEqualsTo('999'); + oneByOne.nextIdEqualsTo('777px'); + oneByOne.nextIdEqualsTo(`b${NAME_REPEAT_PREFIX}3`); + oneByOne.nextIdEqualsTo(`${ID_PREFIX}${oneByOne.currGetIdDataIndex()}`); + + oneByOne.nextNameEqualsTo(''); + oneByOne.nextNameEqualsTo(''); + oneByOne.nextNameEqualsTo(''); + oneByOne.nextNameEqualsTo('b'); + oneByOne.nextNameEqualsTo(''); + + list.appendValues([], ['b', 'c', null]); + + oneByOne.nextIdEqualsTo(`b${NAME_REPEAT_PREFIX}4`); + oneByOne.nextIdEqualsTo('c'); + oneByOne.nextIdEqualsTo(`${ID_PREFIX}${oneByOne.currGetIdDataIndex()}`); + + oneByOne.nextNameEqualsTo('b'); + oneByOne.nextNameEqualsTo('c'); + oneByOne.nextNameEqualsTo(''); + }); + + }); + + describe('id_name_declared_sourceFormat_arrayRows', function () { + + it('no_ordinalMeta', function () { + testArrayRowsInSource([ + { name: 'x', type: 'number' }, + { name: 'p', type: 'ordinal', otherDims: { itemId: 0 } }, + { name: 'q', type: 'ordinal', otherDims: { itemName: 0 } } + ]); + }); + + it('has_ordinalMeta', function () { + const ordinalMetaP = new OrdinalMeta({ + categories: [], + needCollect: true, + deduplication: true + }); + const ordinalMetaQ = new OrdinalMeta({ + categories: [], + needCollect: true, + deduplication: true + }); + testArrayRowsInSource([ + { name: 'x', type: 'number' }, + { name: 'p', type: 'ordinal', otherDims: { itemId: 0 }, ordinalMeta: ordinalMetaP }, + { name: 'q', type: 'ordinal', otherDims: { itemName: 0 }, ordinalMeta: ordinalMetaQ } + ]); + }); + + function testArrayRowsInSource(dimensionsInfo: SeriesDimensionDefine[]): void { + const list = new SeriesData(dimensionsInfo, new Model()); + const oneByOne = makeOneByOneChecker(list); + + const source = createSource( + [ + [0, 'myId_10', null], + [10, 555, null], // numeric id. + [20, '666%', null], + [30, 'myId_good', 'b'], + [40, null, 'b'], + [50, null, null], + [60, undefined, null], + [70, NaN, null], + [80, '', null], + [90, null, 'b'], + [100, null, null], + [110, 'myId_better', null], + [120, 'myId_better', null] // duplicated id. + ], + { + seriesLayoutBy: 'column', + sourceHeader: 0, + dimensions: null + }, + SOURCE_FORMAT_ARRAY_ROWS + ); + list.initData(source); + oneByOne.nextIdEqualsTo('myId_10'); + oneByOne.nextIdEqualsTo('555'); + oneByOne.nextIdEqualsTo('666%'); + oneByOne.nextIdEqualsTo('myId_good'); + oneByOne.nextIdEqualsTo(`${ID_PREFIX}${oneByOne.currGetIdDataIndex()}`); + oneByOne.nextIdEqualsTo(`${ID_PREFIX}${oneByOne.currGetIdDataIndex()}`); + oneByOne.nextIdEqualsTo(`${ID_PREFIX}${oneByOne.currGetIdDataIndex()}`); + oneByOne.nextIdEqualsTo('NaN'); + oneByOne.nextIdEqualsTo(''); + oneByOne.nextIdEqualsTo(`${ID_PREFIX}${oneByOne.currGetIdDataIndex()}`); + oneByOne.nextIdEqualsTo(`${ID_PREFIX}${oneByOne.currGetIdDataIndex()}`); + oneByOne.nextIdEqualsTo('myId_better'); + oneByOne.nextIdEqualsTo('myId_better'); + + oneByOne.nextNameEqualsTo(''); + oneByOne.nextNameEqualsTo(''); + oneByOne.nextNameEqualsTo(''); + oneByOne.nextNameEqualsTo('b'); + oneByOne.nextNameEqualsTo('b'); + oneByOne.nextNameEqualsTo(''); + oneByOne.nextNameEqualsTo(''); + oneByOne.nextNameEqualsTo(''); + oneByOne.nextNameEqualsTo(''); + oneByOne.nextNameEqualsTo('b'); + oneByOne.nextNameEqualsTo(''); + oneByOne.nextNameEqualsTo(''); + oneByOne.nextNameEqualsTo(''); + + list.appendData([ + [ 200, 'myId_best', null ], + [ 210, 999, null ], // numeric id. + [ 220, '777px', null], + [ 230, null, 'b' ], + [ 240, null, null ] + ]); + + oneByOne.nextIdEqualsTo('myId_best'); + oneByOne.nextIdEqualsTo('999'); + oneByOne.nextIdEqualsTo('777px'); + oneByOne.nextIdEqualsTo(`${ID_PREFIX}${oneByOne.currGetIdDataIndex()}`); + oneByOne.nextIdEqualsTo(`${ID_PREFIX}${oneByOne.currGetIdDataIndex()}`); + + oneByOne.nextNameEqualsTo(''); + oneByOne.nextNameEqualsTo(''); + oneByOne.nextNameEqualsTo(''); + oneByOne.nextNameEqualsTo('b'); + oneByOne.nextNameEqualsTo(''); + + list.appendValues([], ['b', 'c', null]); + + oneByOne.nextIdEqualsTo(`${ID_PREFIX}${oneByOne.currGetIdDataIndex()}`); + oneByOne.nextIdEqualsTo(`${ID_PREFIX}${oneByOne.currGetIdDataIndex()}`); + oneByOne.nextIdEqualsTo(`${ID_PREFIX}${oneByOne.currGetIdDataIndex()}`); + + oneByOne.nextNameEqualsTo('b'); + oneByOne.nextNameEqualsTo('c'); + oneByOne.nextNameEqualsTo(''); + } + + }); + }); +}); diff --git a/test/ut/spec/data/completeDimensions.test.ts b/test/ut/spec/data/createDimensions.test.ts similarity index 63% rename from test/ut/spec/data/completeDimensions.test.ts rename to test/ut/spec/data/createDimensions.test.ts index 24efc4b382..5bb809badd 100644 --- a/test/ut/spec/data/completeDimensions.test.ts +++ b/test/ut/spec/data/createDimensions.test.ts @@ -18,29 +18,27 @@ */ -import completeDimensions from '../../../../src/data/helper/completeDimensions'; -import { createSource } from '../../../../src/data/Source'; -import { SOURCE_FORMAT_ARRAY_ROWS, SERIES_LAYOUT_BY_COLUMN } from '../../../../src/util/types'; - -type ParametersOfCompleteDimensions = Parameters; - -describe('completeDimensions', function () { - - function doCompleteDimensions( - sysDims: ParametersOfCompleteDimensions[0], - data: ParametersOfCompleteDimensions[1], - opt: ParametersOfCompleteDimensions[2] - ) { - const result = completeDimensions(sysDims, data, opt); - if (result) { - for (let i = 0; i < result.length; i++) { - const item = result[i]; - if (item && item.hasOwnProperty('dimsDef') && (item as any).dimsDef == null) { - delete (item as any).dimsDef; - } +import SeriesDimensionDefine from '@/src/data/SeriesDimensionDefine'; +import createDimensions from '@/src/data/helper/createDimensions'; +import { createSource } from '@/src/data/Source'; +import { SOURCE_FORMAT_ARRAY_ROWS, SERIES_LAYOUT_BY_COLUMN } from '@/src/util/types'; + +type ParametersOfCreateDimensions = Parameters; + +describe('createDimensions', function () { + + function doCreateDimensions( + source: ParametersOfCreateDimensions[0], + opt: ParametersOfCreateDimensions[1] + ): SeriesDimensionDefine[] { + const result = createDimensions(source, opt); + for (let i = 0; i < result.dimensions.length; i++) { + const item = result.dimensions[i]; + if (item && item.hasOwnProperty('dimsDef') && (item as any).dimsDef == null) { + delete (item as any).dimsDef; } } - return result; + return result.dimensions; } it('namesMoreThanDimCount', function () { @@ -77,12 +75,12 @@ describe('completeDimensions', function () { sourceHeader: 0, dimensions: void 0 }, - SOURCE_FORMAT_ARRAY_ROWS, - null + SOURCE_FORMAT_ARRAY_ROWS ); const opt = { - 'dimsDef': [ + 'coordDimensions': sysDims, + 'dimensionsDefine': [ { 'name': 'date', 'displayName': 'date' @@ -128,7 +126,7 @@ describe('completeDimensions', function () { 'displayName': 'sma9' } ], - 'encodeDef': { + 'encodeDefine': { 'x': 'date', 'y': [ 'haOpen', @@ -143,10 +141,10 @@ describe('completeDimensions', function () { 'close' ] }, - 'dimCount': 5 + 'dimensionsCount': 5 }; - const result: unknown = [ + const result: SeriesDimensionDefine[] = [ { 'otherDims': { 'tooltip': false, @@ -157,7 +155,7 @@ describe('completeDimensions', function () { 'coordDim': 'x', 'coordDimIndex': 0, 'type': 'ordinal', - 'ordinalMeta': undefined + 'storeDimIndex': 0 }, { 'otherDims': { @@ -167,7 +165,8 @@ describe('completeDimensions', function () { 'name': 'open', 'coordDim': 'value', 'coordDimIndex': 0, - 'isExtraCoord': true + 'isExtraCoord': true, + 'storeDimIndex': 1 }, { 'otherDims': { @@ -177,7 +176,8 @@ describe('completeDimensions', function () { 'name': 'high', 'coordDim': 'value0', 'coordDimIndex': 0, - 'isExtraCoord': true + 'isExtraCoord': true, + 'storeDimIndex': 2 }, { 'otherDims': { @@ -187,7 +187,8 @@ describe('completeDimensions', function () { 'name': 'low', 'coordDim': 'value1', 'coordDimIndex': 0, - 'isExtraCoord': true + 'isExtraCoord': true, + 'storeDimIndex': 3 }, { 'otherDims': { @@ -197,7 +198,8 @@ describe('completeDimensions', function () { 'name': 'close', 'coordDim': 'value2', 'coordDimIndex': 0, - 'isExtraCoord': true + 'isExtraCoord': true, + 'storeDimIndex': 4 }, { 'otherDims': {}, @@ -205,7 +207,8 @@ describe('completeDimensions', function () { 'name': 'volume', 'coordDim': 'value3', 'coordDimIndex': 0, - 'isExtraCoord': true + 'isExtraCoord': true, + 'storeDimIndex': 5 }, { 'otherDims': {}, @@ -214,7 +217,7 @@ describe('completeDimensions', function () { 'coordDim': 'y', 'coordDimIndex': 0, 'type': 'float', - 'ordinalMeta': undefined + 'storeDimIndex': 6 }, { 'otherDims': {}, @@ -223,7 +226,7 @@ describe('completeDimensions', function () { 'coordDim': 'y', 'coordDimIndex': 3, 'type': 'float', - 'ordinalMeta': undefined + 'storeDimIndex': 7 }, { 'otherDims': {}, @@ -232,7 +235,7 @@ describe('completeDimensions', function () { 'coordDim': 'y', 'coordDimIndex': 2, 'type': 'float', - 'ordinalMeta': undefined + 'storeDimIndex': 8 }, { 'otherDims': {}, @@ -241,7 +244,7 @@ describe('completeDimensions', function () { 'coordDim': 'y', 'coordDimIndex': 1, 'type': 'float', - 'ordinalMeta': undefined + 'storeDimIndex': 9 }, { 'otherDims': {}, @@ -249,135 +252,153 @@ describe('completeDimensions', function () { 'name': 'sma9', 'coordDim': 'value4', 'coordDimIndex': 0, - 'isExtraCoord': true + 'isExtraCoord': true, + 'storeDimIndex': 10 } ]; - expect(doCompleteDimensions(sysDims, source, opt)).toEqual(result); + expect(doCreateDimensions(source, opt)).toEqual(result.map(a => new SeriesDimensionDefine(a))); }); it('differentData', function () { function doTest( - sysDims: ParametersOfCompleteDimensions[0], - data: ParametersOfCompleteDimensions[1], - opt: ParametersOfCompleteDimensions[2], - result: unknown + source: ParametersOfCreateDimensions[0], + opt: ParametersOfCreateDimensions[1], + result: SeriesDimensionDefine[] ) { - expect(doCompleteDimensions(sysDims, data, opt)).toEqual(result); + expect(doCreateDimensions(source, opt)).toEqual(result.map(a => new SeriesDimensionDefine(a))); } // test dimcount - doTest(['x', 'y'], [], null, [ + doTest([], { coordDimensions: ['x', 'y']}, [ { 'otherDims': {}, 'coordDim': 'x', 'coordDimIndex': 0, - 'name': 'x' + 'name': 'x', + 'storeDimIndex': 0 }, { 'otherDims': {}, 'coordDim': 'y', 'coordDimIndex': 0, - 'name': 'y' + 'name': 'y', + 'storeDimIndex': 1 } ]); - doTest(['x', 'y'], [12], null, [ + doTest([12], { coordDimensions: ['x', 'y']}, [ { 'otherDims': {}, 'coordDim': 'x', 'coordDimIndex': 0, - 'name': 'x' + 'name': 'x', + 'storeDimIndex': 0 }, { 'otherDims': {}, 'coordDim': 'y', 'coordDimIndex': 0, - 'name': 'y' + 'name': 'y', + 'storeDimIndex': 1 } ]); - doTest(['x', 'y'], [12, 4], null, [ + doTest([12, 4], { coordDimensions: ['x', 'y']}, [ { 'otherDims': {}, 'coordDim': 'x', 'coordDimIndex': 0, - 'name': 'x' + 'name': 'x', + 'storeDimIndex': 0 }, { 'otherDims': {}, 'coordDim': 'y', 'coordDimIndex': 0, - 'name': 'y' + 'name': 'y', + 'storeDimIndex': 1 } ]); - doTest(['x'], [[32, 55]], null, [ + doTest([[32, 55]], { coordDimensions: ['x']}, [ { 'otherDims': {}, 'coordDim': 'x', 'coordDimIndex': 0, - 'name': 'x' + 'name': 'x', + 'storeDimIndex': 0 } ]); - doTest(['x', 'y', 'z'], [[32, 55]], null, [ + doTest([[32, 55]], { coordDimensions: ['x', 'y', 'z']}, [ { 'otherDims': {}, 'coordDim': 'x', 'coordDimIndex': 0, - 'name': 'x' + 'name': 'x', + 'storeDimIndex': 0 }, { 'otherDims': {}, 'coordDim': 'y', 'coordDimIndex': 0, - 'name': 'y' + 'name': 'y', + 'storeDimIndex': 1 }, { 'otherDims': {}, 'coordDim': 'z', 'coordDimIndex': 0, - 'name': 'z' + 'name': 'z', + 'storeDimIndex': 2 } ]); - doTest(['x'], [[32, 55], [99, 11]], null, [ + doTest([[32, 55], [99, 11]], { coordDimensions: ['x']}, [ { 'otherDims': {}, 'coordDim': 'x', 'coordDimIndex': 0, - 'name': 'x' + 'name': 'x', + 'storeDimIndex': 0 } ]); - doTest(['x', 'y'], [[32, 55], [99, 11]], {dimCount: 4}, [ + doTest([[32, 55], [99, 11]], { + dimensionsCount: 4, + coordDimensions: ['x', 'y'] + }, [ { 'otherDims': {}, 'coordDim': 'x', 'coordDimIndex': 0, - 'name': 'x' + 'name': 'x', + 'storeDimIndex': 0 }, { 'otherDims': {}, 'coordDim': 'y', 'coordDimIndex': 0, - 'name': 'y' + 'name': 'y', + 'storeDimIndex': 1 }, { 'otherDims': {}, 'coordDim': 'value', 'coordDimIndex': 0, 'isExtraCoord': true, - 'name': 'value' + 'name': 'value', + 'storeDimIndex': 2 }, { 'otherDims': {}, 'coordDim': 'value0', 'coordDimIndex': 0, 'isExtraCoord': true, - 'name': 'value0' + 'name': 'value0', + 'storeDimIndex': 3 } ]); }); @@ -391,12 +412,11 @@ describe('completeDimensions', function () { it('differentSysDims', function () { function doTest( - sysDims: ParametersOfCompleteDimensions[0], - data: ParametersOfCompleteDimensions[1], - opt: ParametersOfCompleteDimensions[2], - result: unknown + source: ParametersOfCreateDimensions[0], + opt: ParametersOfCreateDimensions[1], + result: SeriesDimensionDefine[] ) { - expect(doCompleteDimensions(sysDims, data, opt)).toEqual(result); + expect(doCreateDimensions(source, opt)).toEqual(result.map(a => new SeriesDimensionDefine(a))); } const data = [ @@ -405,41 +425,43 @@ describe('completeDimensions', function () { ]; doTest( - ['x', 'y'], data, null, + data, { coordDimensions: ['x', 'y'] }, [ { 'otherDims': {}, 'coordDim': 'x', 'coordDimIndex': 0, 'name': 'x', - 'type': 'ordinal' + 'type': 'ordinal', + 'storeDimIndex': 0 }, { 'otherDims': {}, 'coordDim': 'y', 'coordDimIndex': 0, - 'name': 'y' + 'name': 'y', + 'storeDimIndex': 1 } ] ); doTest( - ['value'], data, null, + data, { coordDimensions: ['value'] }, [ { 'otherDims': {}, 'coordDim': 'value', 'coordDimIndex': 0, 'name': 'value', - 'type': 'ordinal' + 'type': 'ordinal', + 'storeDimIndex': 0 } ] ); doTest( - [{name: 'time', type: 'time' as const}, 'value'], data, - null, + { coordDimensions: [{name: 'time', type: 'time' as const}, 'value'] }, [ { 'otherDims': {}, @@ -447,28 +469,32 @@ describe('completeDimensions', function () { 'type': 'time', 'coordDimIndex': 0, 'ordinalMeta': undefined, - 'coordDim': 'time' + 'coordDim': 'time', + 'storeDimIndex': 0 }, { 'otherDims': {}, 'coordDim': 'value', 'coordDimIndex': 0, - 'name': 'value' + 'name': 'value', + 'storeDimIndex': 1 } ] ); doTest( - [{ - name: 'y', - otherDims: { - tooltip: false - }, - dimsDef: ['base'] - }, { - name: 'x', - dimsDef: ['open', 'close'] - }], data, {}, + data, { + coordDimensions: [{ + name: 'y', + otherDims: { + tooltip: false + }, + dimsDef: ['base'] + }, { + name: 'x', + dimsDef: ['open', 'close'] + }] + }, [ { 'otherDims': { @@ -480,7 +506,8 @@ describe('completeDimensions', function () { 'coordDim': 'y', 'type': 'ordinal', 'displayName': 'base', - 'ordinalMeta': undefined + 'ordinalMeta': undefined, + 'storeDimIndex': 0 }, { 'otherDims': {}, @@ -489,24 +516,26 @@ describe('completeDimensions', function () { 'defaultTooltip': undefined, 'coordDimIndex': 0, 'coordDim': 'x', - 'displayName': 'open' + 'displayName': 'open', + 'storeDimIndex': 1 } ] ); doTest( - [{ - name: 'y', - otherDims: { - tooltip: false - }, - dimsDef: ['base'] - }, { - name: 'x', - dimsDef: ['open', 'close'] - }], data, { - dimsDef: ['基础', '打开', '关闭'], - encodeDef: { + data, { + dimensionsDefine: ['基础', '打开', '关闭'], + coordDimensions: [{ + name: 'y', + otherDims: { + tooltip: false + }, + dimsDef: ['base'] + }, { + name: 'x', + dimsDef: ['open', 'close'] + }], + encodeDefine: { tooltip: [1, 2, 0] } }, @@ -520,7 +549,8 @@ describe('completeDimensions', function () { 'ordinalMeta': undefined, 'coordDimIndex': 0, 'coordDim': 'y', - 'type': 'ordinal' + 'type': 'ordinal', + 'storeDimIndex': 0 }, { 'otherDims': { @@ -530,7 +560,8 @@ describe('completeDimensions', function () { 'displayName': '打开', 'coordDimIndex': 0, 'ordinalMeta': undefined, - 'coordDim': 'x' + 'coordDim': 'x', + 'storeDimIndex': 1 }, { 'otherDims': { @@ -540,24 +571,26 @@ describe('completeDimensions', function () { 'displayName': '关闭', 'ordinalMeta': undefined, 'coordDimIndex': 1, - 'coordDim': 'x' + 'coordDim': 'x', + 'storeDimIndex': 2 } ] ); doTest( - [{ - name: 'y', - otherDims: { - tooltip: false - }, - dimsDef: ['base'] - }, { - name: 'x', - dimsDef: ['open', 'close'] - }], data, { - dimsDef: ['基础', null, '关闭'], - encodeDef: { + data, { + coordDimensions: [{ + name: 'y', + otherDims: { + tooltip: false + }, + dimsDef: ['base'] + }, { + name: 'x', + dimsDef: ['open', 'close'] + }], + dimensionsDefine: ['基础', null, '关闭'], + encodeDefine: { x: [0, 4] } }, @@ -569,7 +602,8 @@ describe('completeDimensions', function () { 'coordDimIndex': 0, 'coordDim': 'x', 'ordinalMeta': undefined, - 'type': 'ordinal' + 'type': 'ordinal', + 'storeDimIndex': 0 }, { 'otherDims': { @@ -580,7 +614,8 @@ describe('completeDimensions', function () { 'ordinalMeta': undefined, 'defaultTooltip': undefined, 'coordDimIndex': 0, - 'coordDim': 'y' + 'coordDim': 'y', + 'storeDimIndex': 1 }, { 'otherDims': {}, @@ -588,7 +623,8 @@ describe('completeDimensions', function () { 'displayName': '关闭', 'coordDimIndex': 0, 'isExtraCoord': true, - 'coordDim': 'value' + 'coordDim': 'value', + 'storeDimIndex': 2 } ] ); @@ -605,18 +641,20 @@ describe('completeDimensions', function () { it('dimsDef', function () { function doTest( - sysDims: ParametersOfCompleteDimensions[0], - data: ParametersOfCompleteDimensions[1], - opt: ParametersOfCompleteDimensions[2], - result: unknown + source: ParametersOfCreateDimensions[0], + opt: ParametersOfCreateDimensions[1], + result: SeriesDimensionDefine[] ) { - expect(doCompleteDimensions(sysDims, data, opt)).toEqual(result); + expect(doCreateDimensions(source, opt)).toEqual(result.map(a => new SeriesDimensionDefine(a))); } const data = [['iw', 332, 4434, 323, 59], ['vrr', 44, 11, 144, 55]]; doTest( - ['x', 'y', 'value'], data, - {dimsDef: ['挨克思', null, '歪溜']}, + data, + { + dimensionsDefine: ['挨克思', null, '歪溜'], + coordDimensions: ['x', 'y', 'value'] + }, [ { 'otherDims': {}, @@ -624,27 +662,33 @@ describe('completeDimensions', function () { 'name': '挨克思', 'type': 'ordinal', 'coordDim': 'x', - 'coordDimIndex': 0 + 'coordDimIndex': 0, + 'storeDimIndex': 0 }, { 'otherDims': {}, 'coordDim': 'y', 'coordDimIndex': 0, - 'name': 'y' + 'name': 'y', + 'storeDimIndex': 1 }, { 'otherDims': {}, 'displayName': '歪溜', 'name': '歪溜', 'coordDim': 'value', - 'coordDimIndex': 0 + 'coordDimIndex': 0, + 'storeDimIndex': 2 } ] ); doTest( - ['x', 'y', 'value'], data, - {dimsDef: ['挨克思', null, {type: 'ordinal' as const}]}, // no name but only type + data, + { + dimensionsDefine: ['挨克思', null, {type: 'ordinal' as const}], + coordDimensions: ['x', 'y', 'value'] + }, // no name but only type [ { 'otherDims': {}, @@ -652,27 +696,33 @@ describe('completeDimensions', function () { 'name': '挨克思', 'type': 'ordinal', 'coordDim': 'x', - 'coordDimIndex': 0 + 'coordDimIndex': 0, + 'storeDimIndex': 0 }, { 'otherDims': {}, 'coordDim': 'y', 'coordDimIndex': 0, - 'name': 'y' + 'name': 'y', + 'storeDimIndex': 1 }, { 'otherDims': {}, 'name': 'value', 'coordDim': 'value', 'type': 'ordinal', - 'coordDimIndex': 0 + 'coordDimIndex': 0, + 'storeDimIndex': 2 } ] ); doTest( - [{name: 'time', type: 'time' as const}, 'value'], data, - {dimsDef: [{name: '泰亩', type: 'ordinal'}, {name: '歪溜', type: 'float'}]}, + data, + { + dimensionsDefine: [{name: '泰亩', type: 'ordinal'}, {name: '歪溜', type: 'float'}], + coordDimensions: [{name: 'time', type: 'time' as const}, 'value'] + }, [ { 'otherDims': {}, @@ -681,7 +731,8 @@ describe('completeDimensions', function () { 'type': 'ordinal', 'ordinalMeta': undefined, 'coordDimIndex': 0, - 'coordDim': 'time' + 'coordDim': 'time', + 'storeDimIndex': 0 }, { 'otherDims': {}, @@ -689,7 +740,38 @@ describe('completeDimensions', function () { 'name': '歪溜', 'type': 'float', 'coordDim': 'value', - 'coordDimIndex': 0 + 'coordDimIndex': 0, + 'storeDimIndex': 1 + } + ] + ); + + // Duplicate name + doTest( + data, + { + dimensionsDefine: [{name: '泰亩', type: 'ordinal'}, {name: '泰亩', type: 'float'}], + coordDimensions: [{name: 'time', type: 'time' as const}, 'value'] + }, + [ + { + 'otherDims': {}, + 'displayName': '泰亩', + 'name': '泰亩', + 'type': 'ordinal', + 'ordinalMeta': undefined, + 'coordDimIndex': 0, + 'coordDim': 'time', + 'storeDimIndex': 0 + }, + { + 'otherDims': {}, + 'displayName': '泰亩', + 'name': '泰亩0', + 'type': 'float', + 'coordDim': 'value', + 'coordDimIndex': 0, + 'storeDimIndex': 1 } ] ); @@ -705,20 +787,19 @@ describe('completeDimensions', function () { it('encodeDef', function () { function doTest( - sysDims: ParametersOfCompleteDimensions[0], - data: ParametersOfCompleteDimensions[1], - opt: ParametersOfCompleteDimensions[2], - result: unknown + source: ParametersOfCreateDimensions[0], + opt: ParametersOfCreateDimensions[1], + result: SeriesDimensionDefine[] ) { - expect(doCompleteDimensions(sysDims, data, opt)).toEqual(result); + expect(doCreateDimensions(source, opt)).toEqual(result.map(a => new SeriesDimensionDefine(a))); } const data = [['iw', 332, 4434, 323, 'd8', 59], ['vrr', 44, 11, 144, '-', 55]]; doTest( - null, data, + data, { - encodeDef: { + encodeDefine: { x: 2, y: [1, 4], tooltip: 2, @@ -732,16 +813,17 @@ describe('completeDimensions', function () { 'coordDimIndex': 0, 'name': 'value', 'isExtraCoord': true, - 'type': 'ordinal' + 'type': 'ordinal', + 'storeDimIndex': 0 } ] ); doTest( - null, data, + data, { - dimsDef: ['挨克思', null, '歪溜'], - encodeDef: { + dimensionsDefine: ['挨克思', null, '歪溜'], + encodeDefine: { x: 2, y: [1, 4], tooltip: 2, @@ -756,13 +838,15 @@ describe('completeDimensions', function () { 'type': 'ordinal', 'coordDim': 'value', 'coordDimIndex': 0, - 'isExtraCoord': true + 'isExtraCoord': true, + 'storeDimIndex': 0 }, { 'otherDims': {}, 'coordDim': 'y', 'coordDimIndex': 0, - 'name': 'y' + 'name': 'y', + 'storeDimIndex': 1 }, { 'otherDims': { @@ -771,16 +855,18 @@ describe('completeDimensions', function () { 'displayName': '歪溜', 'name': '歪溜', 'coordDim': 'x', - 'coordDimIndex': 0 + 'coordDimIndex': 0, + 'storeDimIndex': 2 } ] ); doTest( - ['x', {name: 'y', type: 'time' as const}, 'z'], data, + data, { - dimsDef: ['挨克思', null, '歪溜'], - encodeDef: { + dimensionsDefine: ['挨克思', null, '歪溜'], + coordDimensions: ['x', {name: 'y', type: 'time' as const}, 'z'], + encodeDefine: { x: 2, y: [1, 4], tooltip: 2, @@ -794,7 +880,8 @@ describe('completeDimensions', function () { 'name': '挨克思', 'type': 'ordinal', 'coordDim': 'z', - 'coordDimIndex': 0 + 'coordDimIndex': 0, + 'storeDimIndex': 0 }, { 'otherDims': {}, @@ -802,7 +889,8 @@ describe('completeDimensions', function () { 'coordDimIndex': 0, 'name': 'y', 'type': 'time', - 'ordinalMeta': undefined + 'ordinalMeta': undefined, + 'storeDimIndex': 1 }, { 'otherDims': { @@ -811,17 +899,19 @@ describe('completeDimensions', function () { 'displayName': '歪溜', 'name': '歪溜', 'coordDim': 'x', - 'coordDimIndex': 0 + 'coordDimIndex': 0, + 'storeDimIndex': 2 } ] ); doTest( - [{name: 'time', type: 'time' as const}, 'value'], data, + data, { // dimsDef type 'ordinal' has higher priority then sysDims type 'time'. - dimsDef: [{name: '泰亩', type: 'ordinal'}, {name: '歪溜', type: 'float'}], - encodeDef: { + dimensionsDefine: [{name: '泰亩', type: 'ordinal'}, {name: '歪溜', type: 'float'}], + coordDimensions: [{name: 'time', type: 'time' as const}, 'value'], + encodeDefine: { tooltip: 2 } }, @@ -833,7 +923,8 @@ describe('completeDimensions', function () { 'type': 'ordinal', 'ordinalMeta': undefined, 'coordDimIndex': 0, - 'coordDim': 'time' + 'coordDim': 'time', + 'storeDimIndex': 0 }, { 'otherDims': {}, @@ -841,17 +932,19 @@ describe('completeDimensions', function () { 'name': '歪溜', 'type': 'float', 'coordDim': 'value', - 'coordDimIndex': 0 + 'coordDimIndex': 0, + 'storeDimIndex': 1 } ] ); doTest( - [{name: 'time', type: 'time' as const}, 'value'], data, + data, { // dimsDef type 'ordinal' has higher priority then sysDims type 'time'. - dimsDef: [{name: '泰亩', type: 'ordinal'}, {name: '歪溜', type: 'float'}], - encodeDef: { + dimensionsDefine: [{name: '泰亩', type: 'ordinal'}, {name: '歪溜', type: 'float'}], + coordDimensions: [{name: 'time', type: 'time' as const}, 'value'], + encodeDefine: { tooltip: 2 } }, @@ -863,7 +956,8 @@ describe('completeDimensions', function () { 'type': 'ordinal', 'ordinalMeta': undefined, 'coordDimIndex': 0, - 'coordDim': 'time' + 'coordDim': 'time', + 'storeDimIndex': 0 }, { 'otherDims': {}, @@ -871,7 +965,8 @@ describe('completeDimensions', function () { 'name': '歪溜', 'type': 'float', 'coordDim': 'value', - 'coordDimIndex': 0 + 'coordDimIndex': 0, + 'storeDimIndex': 1 } ] ); diff --git a/test/ut/spec/data/dataTransform.test.ts b/test/ut/spec/data/dataTransform.test.ts index 316064fb30..04cc20eae3 100644 --- a/test/ut/spec/data/dataTransform.test.ts +++ b/test/ut/spec/data/dataTransform.test.ts @@ -17,10 +17,10 @@ * under the License. */ -import { EChartsType } from '../../../../src/echarts'; +import { EChartsType } from '@/src/echarts'; import { createChart, removeChart, getECModel } from '../../core/utHelper'; -import { EChartsOption } from '../../../../src/export/option'; -import { retrieveRawValue } from '../../../../src/data/helper/dataProvider'; +import { EChartsOption } from '@/src/export/option'; +import { retrieveRawValue } from '@/src/data/helper/dataProvider'; describe('dataTransform', function () { diff --git a/test/ut/spec/data/dataValueHelper.test.ts b/test/ut/spec/data/dataValueHelper.test.ts index 729395cb0c..a1d971a4ed 100644 --- a/test/ut/spec/data/dataValueHelper.test.ts +++ b/test/ut/spec/data/dataValueHelper.test.ts @@ -18,7 +18,7 @@ */ -import * as dataValueHelper from '../../../../src/data/helper/dataValueHelper'; +import * as dataValueHelper from '@/src/data/helper/dataValueHelper'; const NO_SUCH_CASE = 'NO_SUCH_CASE'; diff --git a/test/ut/spec/model/Global.test.ts b/test/ut/spec/model/Global.test.ts index 632f7f1cef..a115b99a41 100755 --- a/test/ut/spec/model/Global.test.ts +++ b/test/ut/spec/model/Global.test.ts @@ -18,13 +18,13 @@ * under the License. */ -import { EChartsType } from '../../../../src/echarts'; +import { EChartsType } from '@/src/echarts'; import { createChart, getECModel } from '../../core/utHelper'; -import { ComponentMainType, ParsedValue } from '../../../../src/util/types'; -import SeriesModel from '../../../../src/model/Series'; -import ComponentModel from '../../../../src/model/Component'; -import ChartView from '../../../../src/view/Chart'; -import { EChartsOption } from '../../../../src/export/option'; +import { ComponentMainType, ParsedValue } from '@/src/util/types'; +import SeriesModel from '@/src/model/Series'; +import ComponentModel from '@/src/model/Component'; +import ChartView from '@/src/view/Chart'; +import { EChartsOption } from '@/src/export/option'; type OriginModelView = { model: SeriesModel; diff --git a/test/ut/spec/model/componentDependency.test.ts b/test/ut/spec/model/componentDependency.test.ts index 31e2f6b55f..b86cf90981 100755 --- a/test/ut/spec/model/componentDependency.test.ts +++ b/test/ut/spec/model/componentDependency.test.ts @@ -18,8 +18,8 @@ * under the License. */ -import ComponentModel, { ComponentModelConstructor } from '../../../../src/model/Component'; -import { ComponentMainType } from '../../../../src/util/types'; +import ComponentModel, { ComponentModelConstructor } from '@/src/model/Component'; +import { ComponentMainType } from '@/src/util/types'; const componentModelConstructor = ComponentModel as ComponentModelConstructor; diff --git a/test/ut/spec/model/componentMissing.test.ts b/test/ut/spec/model/componentMissing.test.ts index 67e19322b3..bfbe58a810 100644 --- a/test/ut/spec/model/componentMissing.test.ts +++ b/test/ut/spec/model/componentMissing.test.ts @@ -18,18 +18,18 @@ * under the License. */ -import { init, use, EChartsType } from '../../../../src/export/core'; +import { init, use, EChartsType } from '@/src/export/core'; import { PieChart -} from '../../../../src/export/charts'; +} from '@/src/export/charts'; import { TitleComponent -} from '../../../../src/export/components'; +} from '@/src/export/components'; import { CanvasRenderer -} from '../../../../src/export/renderers'; +} from '@/src/export/renderers'; use([PieChart, TitleComponent, CanvasRenderer]); -import { EChartsOption } from '../../../../src/export/option'; +import { EChartsOption } from '@/src/export/option'; function createChart(theme?: object): EChartsType { diff --git a/test/ut/spec/model/timelineMediaOptions.test.ts b/test/ut/spec/model/timelineMediaOptions.test.ts index d0bf40245f..0b2d436443 100755 --- a/test/ut/spec/model/timelineMediaOptions.test.ts +++ b/test/ut/spec/model/timelineMediaOptions.test.ts @@ -18,13 +18,13 @@ * under the License. */ -import { EChartsType } from '../../../../src/echarts'; -import SeriesModel from '../../../../src/model/Series'; -import { ParsedValue } from '../../../../src/util/types'; -import { LegendOption } from '../../../../src/component/legend/LegendModel'; -import TimelineModel from '../../../../src/component/timeline/TimelineModel'; +import { EChartsType } from '@/src/echarts'; +import SeriesModel from '@/src/model/Series'; +import { ParsedValue } from '@/src/util/types'; +import { LegendOption } from '@/src/component/legend/LegendModel'; +import TimelineModel from '@/src/component/timeline/TimelineModel'; import { createChart, getECModel } from '../../core/utHelper'; -import { EChartsOption } from '../../../../src/export/option'; +import { EChartsOption } from '@/src/export/option'; describe('timelineMediaOptions', function () { diff --git a/test/ut/spec/scale/interval.test.ts b/test/ut/spec/scale/interval.test.ts index aec5999e0d..67965d7792 100755 --- a/test/ut/spec/scale/interval.test.ts +++ b/test/ut/spec/scale/interval.test.ts @@ -19,11 +19,11 @@ */ import { createChart, getECModel } from '../../core/utHelper'; -import { EChartsType } from '../../../../src/echarts'; -import CartesianAxisModel from '../../../../src/coord/cartesian/AxisModel'; -import IntervalScale from '../../../../src/scale/Interval'; -import { intervalScaleNiceTicks } from '../../../../src/scale/helper'; -import { getPrecisionSafe } from '../../../../src/util/number'; +import { EChartsType } from '@/src/echarts'; +import CartesianAxisModel from '@/src/coord/cartesian/AxisModel'; +import IntervalScale from '@/src/scale/Interval'; +import { intervalScaleNiceTicks } from '@/src/scale/helper'; +import { getPrecisionSafe } from '@/src/util/number'; describe('scale_interval', function () { diff --git a/test/ut/spec/series/custom.test.ts b/test/ut/spec/series/custom.test.ts index 0cc7874877..ec0af5769d 100644 --- a/test/ut/spec/series/custom.test.ts +++ b/test/ut/spec/series/custom.test.ts @@ -17,10 +17,10 @@ * under the License. */ -import { EChartsType } from '../../../../src/echarts'; +import { EChartsType } from '@/src/echarts'; import { createChart } from '../../core/utHelper'; -import { ZRColor } from '../../../../src/util/types'; -import { CustomSeriesRenderItemAPI, CustomSeriesRenderItemParams } from '../../../../src/chart/custom/install'; +import { ZRColor } from '@/src/util/types'; +import { CustomSeriesRenderItemAPI, CustomSeriesRenderItemParams } from '@/src/chart/custom/CustomSeries'; describe('custom_series', function () { diff --git a/test/ut/spec/util/graphic.test.ts b/test/ut/spec/util/graphic.test.ts index 5a3b1962d1..f6856ffe02 100755 --- a/test/ut/spec/util/graphic.test.ts +++ b/test/ut/spec/util/graphic.test.ts @@ -21,7 +21,7 @@ import { subPixelOptimize, subPixelOptimizeLine, subPixelOptimizeRect } from 'zrender/src/graphic/helper/subPixelOptimize'; -import { lineLineIntersect } from '../../../../src/util/graphic'; +import { lineLineIntersect } from '@/src/util/graphic'; describe('util/graphic', function () { diff --git a/test/ut/spec/util/layout.test.ts b/test/ut/spec/util/layout.test.ts index 4c43ceb17a..7006229685 100644 --- a/test/ut/spec/util/layout.test.ts +++ b/test/ut/spec/util/layout.test.ts @@ -19,8 +19,8 @@ */ // import { Dictionary } from 'zrender/src/core/types'; -import { mergeLayoutParam } from '../../../../src/util/layout'; -import { BoxLayoutOptionMixin } from '../../../../src/util/types'; +import { mergeLayoutParam } from '@/src/util/layout'; +import { BoxLayoutOptionMixin } from '@/src/util/types'; describe('util/number', function () { diff --git a/test/ut/spec/util/model.test.ts b/test/ut/spec/util/model.test.ts index bde88823b0..40f7890324 100755 --- a/test/ut/spec/util/model.test.ts +++ b/test/ut/spec/util/model.test.ts @@ -18,7 +18,7 @@ * under the License. */ -import { compressBatches } from '../../../../src/util/model'; +import { compressBatches } from '@/src/util/model'; describe('util/model', function () { diff --git a/test/ut/spec/util/number.test.ts b/test/ut/spec/util/number.test.ts index ae96c38db3..52d872130c 100755 --- a/test/ut/spec/util/number.test.ts +++ b/test/ut/spec/util/number.test.ts @@ -22,7 +22,7 @@ import { linearMap, parseDate, reformIntervals, getPrecisionSafe, getPrecision, getPercentWithPrecision, quantityExponent, quantity, nice, isNumeric, numericToNumber, addSafe -} from '../../../../src/util/number'; +} from '@/src/util/number'; describe('util/number', function () { @@ -271,6 +271,7 @@ describe('util/number', function () { expect(+parseDate('2012-03-04T05:06:07.123-0700')).toEqual(1330862767123); expect(+parseDate('2012-03-04T05:06:07.123-07:00')).toEqual(1330862767123); expect(+parseDate('2012-03-04T5:6:7.123-07:00')).toEqual(1330862767123); + expect(+parseDate('2012-03-04T05:06:07.123000Z')).toEqual(+new Date('2012-03-04T05:06:07.123Z')); // Other string expect(+parseDate('2012')).toEqual(+new Date('2012-01-01T00:00:00')); diff --git a/test/ut/tsconfig.json b/test/ut/tsconfig.json index 0cdbc3d770..63885f0a1e 100644 --- a/test/ut/tsconfig.json +++ b/test/ut/tsconfig.json @@ -6,7 +6,12 @@ "noImplicitThis": true, "strictBindCallApply": true, - "esModuleInterop": true + "esModuleInterop": true, + + "baseUrl": "./", + "paths": { + "@/*": ["../../*"] + } }, "include": [ "**/*.ts"