diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index 14a0fcaa81..916dadda53 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -49,7 +49,7 @@ eg. - + ### 🔗 Reproduce Link diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md index bc72b546fd..518d3e947a 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -8,6 +8,39 @@ labels: 🙏feature request +### 🏷 Version + + + + + + +| Package | Version | +| -------------- | ------- | +| @antv/s2 | | +| @antv/s2-react | | +| @antv/s2-vue | | + +### Sheet Type + + + + +- [ ] PivotSheet +- [ ] TableSheet +- [ ] GridAnalysisSheet +- [ ] StrategySheet +- [ ] EditableSheet + ### 🖋 Description diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml index 6e3945cfa1..1f7d825c29 100644 --- a/.github/workflows/auto-release.yml +++ b/.github/workflows/auto-release.yml @@ -16,7 +16,7 @@ jobs: node-version: [20] # semantic-release 需要 >= 16 的 Node.js 环境 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # Python 3.11 和 node-gyp 有兼容问题, 导致无法安装依赖 # https://github.com/slint-ui/slint/commit/a9c48e33502fdebc36c5aa2f4f516c2218424679#diff-944291df2c9c06359d37cc8833d182d705c9e8c3108e7cfe132d61a06e9133dd @@ -29,7 +29,7 @@ jobs: version: 8 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' @@ -44,7 +44,7 @@ jobs: { "msgtype": "link", "link": { - "title": "🚀 开始自动发布 🚀", + "title": "🚑 开始自动发布 (${{ github.head_ref || github.ref_name }}) 🚑", "text": "🔗 请点击链接查看详情", "messageUrl": "https://github.com/antvis/S2/actions/workflows/auto-release.yml", "picUrl": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*gt5-RZDjt3IAAAAAAAAAAAAADmJ7AQ/original" @@ -77,7 +77,7 @@ jobs: { "msgtype": "link", "link": { - "title": "🚨 自动发布失败 🚨", + "title": "🚨 自动发布失败 (${{ github.head_ref || github.ref_name }})", "text": "🔗 请点击链接查看具体原因, 及时修复, 尝试点击右上角 [Re-run all jobs] 重试, 或手动发布 🚑", "messageUrl": "https://github.com/antvis/S2/actions/workflows/auto-release.yml", "picUrl": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*PRSkSqsE_vYAAAAAAAAAAAAADmJ7AQ/original" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 4a4f797a5a..ee18ab6b2b 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -9,17 +9,21 @@ # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # -name: "CodeQL" +name: 🏨 CodeQL on: push: - branches: [ master ] + branches: [ master, alpha, beta, next ] pull_request: # The branches below must be a subset of the branches above - branches: [ master ] + branches: [ master, alpha, beta, next ] schedule: - cron: '38 0 * * 2' +concurrency: + group: ${{github.workflow}}-${{github.event_name}}-${{github.ref}} + cancel-in-progress: true + jobs: analyze: name: Analyze diff --git a/.github/workflows/compressed-size.yml b/.github/workflows/compressed-size.yml index 9092245ce9..c25c7f9a23 100644 --- a/.github/workflows/compressed-size.yml +++ b/.github/workflows/compressed-size.yml @@ -4,6 +4,10 @@ on: pull_request: types: [opened, synchronize] +concurrency: + group: ${{github.workflow}}-${{github.event_name}}-${{github.ref}} + cancel-in-progress: true + jobs: compressed-size: @@ -11,7 +15,7 @@ jobs: steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # Python 3.11 和 node-gyp 有兼容问题, 导致无法安装依赖 # https://github.com/slint-ui/slint/commit/a9c48e33502fdebc36c5aa2f4f516c2218424679#diff-944291df2c9c06359d37cc8833d182d705c9e8c3108e7cfe132d61a06e9133dd diff --git a/.github/workflows/disscustion.yml b/.github/workflows/disscustion.yml index 4e91b508d2..f863c2fd91 100644 --- a/.github/workflows/disscustion.yml +++ b/.github/workflows/disscustion.yml @@ -1,5 +1,5 @@ -name: Discussions +name: 💬 Discussions on: discussion: @@ -21,6 +21,6 @@ jobs: "title": "📢 用户: ${{ github.event.discussion.user.login }} 创建了讨论:(${{ github.event.discussion.title }})", "text": "👀 点击查看", "messageUrl": "${{ github.event.discussion.html_url }}", - "picUrl": "https://gw.alipayobjects.com/zos/antfincdn/ISzgBCtgR/2c5c4aaa-4f40-46f7-8f6b-427fa9ff07bb.png" + "picUrl": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*1PTTQLk3j5AAAAAAAAAAAAAADmJ7AQ/original" } } diff --git a/.github/workflows/issue-labeled.yml b/.github/workflows/issue-labeled.yml index 4eb12f78cc..31f9629bdf 100644 --- a/.github/workflows/issue-labeled.yml +++ b/.github/workflows/issue-labeled.yml @@ -105,7 +105,7 @@ jobs: body: | 你好 @${{ github.event.issue.user.login }},经过我们的反复讨论, 你的需求现已被采纳, 我们会排期开发, 但人力资源有限, 短期内无法支持, 请关注后续发布日志。当然, 如果能贡献 PR 帮助我们改进, 不胜感激! - Hello, @${{ github.vent.issue.user.login }}, your feature request has been accepted after our repeated discussion. We will schedule the development. However, it could not be supported in the short term since limited time, please pay attention to the follow-up release logs. Of course, looking forward for your PR! + Hello, @${{ github.event.issue.user.login }}, your feature request has been accepted after our repeated discussion. We will schedule the development. However, it could not be supported in the short term since limited time, please pay attention to the follow-up release logs. Of course, looking forward for your PR! - name: Rejected if: github.event.label.name == '❌ won''t support' @@ -118,3 +118,25 @@ jobs: 你好 @${{ github.event.issue.user.login }},经过我们的反复讨论, 你的需求过于定制化,不适合直接添加到 S2 中, S2 作为开源框架,只会进行通用能力的增强和自定义接口的开放。你可以通过 S2 提供的自定义能力自行实现,感谢你的理解。 Hello, @${{ github.event.issue.user.login }}, After our repeated discussions, your needs are too customized and not suitable for implementing directly to S2. As an open source framework, S2 will only enhance general capabilities and open custom interfaces. You can implement it yourself through the customization capabilities provided by S2, thank you for your understanding. + + - name: Supported or fixed in next + if: github.event.label.name == '✨ supported or fixed in next' + uses: actions-cool/issues-helper@main + with: + actions: 'create-comment' + token: ${{ secrets.GITHUB_TOKEN }} + issue-number: ${{ github.event.issue.number }} + body: | + 你好 @${{ github.event.issue.user.login }},该功能或缺陷已经在 `2.0 next 版本` 中支持或修复,`next` 版本目前处于内测中, 感谢你的支持与理解。 + + 如有任何 `2.0 版本` 问题,请前往[讨论区](https://github.com/antvis/S2/discussions/1933),正式版预计年底发布 (文档施工中 🚧), 抢先试用: + + ```bash + yarn add @antv/s2@next + yarn add @antv/s2-react@next + yarn add @antv/s2-vue@next + ``` + + Hello, @${{ github.event.issue.user.login }}, This feature or flaw has been supported or fixed in `2.0 next version`, `next` version is currently in private beta, thank you for your support and understanding. + + Any `2.0` version issues, please go to [discussion](https://github.com/antvis/S2/discussions/1933), which released the official version is expected to the end (document 🚧) during construction, the first trial: diff --git a/.github/workflows/issue-opend.yml b/.github/workflows/issue-opend.yml index 1cfb08c64c..ec973f8af4 100644 --- a/.github/workflows/issue-opend.yml +++ b/.github/workflows/issue-opend.yml @@ -20,9 +20,9 @@ jobs: Hello, @${{ github.event.issue.user.login }}, please edit your issue title. a concise issue title will save everyone time. please do not leave the title as the body or empty. - # 如果是 bug 的 issue, 但是基本的版本号,表格类型, 描述都没有, 直接关闭, 不多BB. - - name: check bug report issue body - if: contains(github.event.issue.title, '🐛') == true && contains(github.event.issue.body, 'Version') == false && contains(github.event.issue.body, 'Sheet Type') == false && contains(github.event.issue.body, 'Description') == false + # 如果 issue 的提交者无视模版, 连基本的版本号,表格类型, 描述都没有, 直接自动关闭, 不多BB. + - name: check issue body + if: contains(github.event.issue.body, 'Version') == false && contains(github.event.issue.body, 'Sheet Type') == false && contains(github.event.issue.body, 'Description') == false uses: actions-cool/issues-helper@main with: actions: 'create-comment,add-labels,close-issue' diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 2f2d4322d0..c43309deac 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,4 +1,4 @@ -name: lint +name: 👨‍🔬 lint on: [pull_request] @@ -6,7 +6,6 @@ concurrency: group: ${{github.workflow}}-${{github.event_name}}-${{github.ref}} cancel-in-progress: true - jobs: lint: runs-on: macos-latest @@ -16,7 +15,7 @@ jobs: node-version: [20] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # Python 3.11 和 node-gyp 有兼容问题, 导致无法安装依赖 # https://github.com/slint-ui/slint/commit/a9c48e33502fdebc36c5aa2f4f516c2218424679#diff-944291df2c9c06359d37cc8833d182d705c9e8c3108e7cfe132d61a06e9133dd @@ -29,7 +28,7 @@ jobs: version: 8 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' @@ -43,8 +42,8 @@ jobs: - name: Build run: pnpm build - - name: Bundle size - run: pnpm bundle:size + - name: Bundle size limit + run: pnpm build:size-limit env: CI: true BUNDLESIZE_GITHUB_TOKEN: ${{ secrets.BUNDLESIZE_GITHUB_TOKEN }} diff --git a/.github/workflows/pr-auto-assign-reviewer.yml b/.github/workflows/pr-auto-assign-reviewer.yml index 9f74e609f9..cc1f6cec4e 100644 --- a/.github/workflows/pr-auto-assign-reviewer.yml +++ b/.github/workflows/pr-auto-assign-reviewer.yml @@ -21,5 +21,5 @@ jobs: with: token: ${{ secrets.GITHUB_TOKEN }} pr-emoji: '+1, rocket' - reviewers: 'serializedowen,lcx-seima,lijinke666,wjgogogo,stone-lyl,GaoFuhong' + reviewers: 'lijinke666,wjgogogo,wuhaiyang' review-creator: false diff --git a/.github/workflows/prerelease-notify.yml b/.github/workflows/prerelease-notify.yml index 12d063a8d8..84126f1d97 100644 --- a/.github/workflows/prerelease-notify.yml +++ b/.github/workflows/prerelease-notify.yml @@ -23,7 +23,7 @@ jobs: ${{ secrets.DING_TALK_ACCESS_TOKEN }} ${{ secrets.DING_TALK_GROUP_TOKEN }} notify_title: '🎉 {release_tag} 发布 🎉' - notify_body: '## { title }
![preview](https://gw.alipayobjects.com/zos/antfincdn/ISzgBCtgR/2c5c4aaa-4f40-46f7-8f6b-427fa9ff07bb.png)
{ body }
' + notify_body: '## { title }
![preview](https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*1PTTQLk3j5AAAAAAAAAAAAAADmJ7AQ/original)
{ body }
' notify_footer: '> 该版本为测试版, 请谨慎使用, 前往 [**AntV/S2 Releases**]({ release_url }) 查看完整更新日志.' at_all: false enable_prerelease: true diff --git a/.github/workflows/release-notify.yml b/.github/workflows/release-notify.yml index 36452302a0..b4d892176f 100644 --- a/.github/workflows/release-notify.yml +++ b/.github/workflows/release-notify.yml @@ -18,8 +18,8 @@ jobs: ${{ secrets.DING_TALK_ACCESS_TOKEN }} ${{ secrets.DING_TALK_GROUP_TOKEN }} ${{ secrets.DING_TALK_PUBLIC_TOKEN }} - notify_title: '🎉 S2 新版本发布啦 🎉' - notify_body: '## { title }
![preview](https://gw.alipayobjects.com/zos/antfincdn/ISzgBCtgR/2c5c4aaa-4f40-46f7-8f6b-427fa9ff07bb.png)
看看有哪些更新吧
' + notify_title: '🎉 AntV/S2 新版本发布啦 🎉' + notify_body: '## { title }
![preview](https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*1PTTQLk3j5AAAAAAAAAAAAAADmJ7AQ/original)
看看有哪些更新吧
' notify_footer: '> 前往 [**AntV/S2 Releases**](https://github.com/antvis/S2/releases/latest) 查看完整更新日志.' at_all: false enable_prerelease: false diff --git a/.github/workflows/release-success.yml b/.github/workflows/release-success.yml index 164f782554..6fefaea70c 100644 --- a/.github/workflows/release-success.yml +++ b/.github/workflows/release-success.yml @@ -16,8 +16,19 @@ jobs: defaults: run: working-directory: s2-site + + strategy: + matrix: + node-version: [18] + steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'yarn' - name: Git bootstrap run: | @@ -56,7 +67,7 @@ jobs: { "msgtype": "link", "link": { - "title": "📢 开始自动部署旧版官网(https://s2.antv.vision) 📢 ", + "title": "📢 开始自动部署旧版官网 (https://s2.antv.vision) 📢 ", "text": "🔗 请点击链接查看详情", "messageUrl": "https://github.com/antvis/S2/actions/workflows/release-success.yml", "picUrl": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*gt5-RZDjt3IAAAAAAAAAAAAADmJ7AQ/original" @@ -68,7 +79,7 @@ jobs: with: node-version: 16 cache: 'yarn' - + # 安装官网依赖 - name: Install Dependencies run: yarn diff --git a/.github/workflows/site-build-notify.yml b/.github/workflows/site-build-notify.yml index 9022cbc5f6..d34d32097c 100644 --- a/.github/workflows/site-build-notify.yml +++ b/.github/workflows/site-build-notify.yml @@ -16,10 +16,10 @@ jobs: { "msgtype": "link", "link": { - "title": "✅ 旧官网(https://s2.antv.vision) 部署成功", - "text": "点击访问 https://s2.antv.vision/", + "title": "✅ 旧官网 (https://s2.antv.vision) 部署成功", + "text": "点击访问", "messageUrl": "https://s2.antv.vision/", - "picUrl": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*Eel8Rp5jlAkAAAAAAAAAAAAADmJ7AQ/original" + "picUrl": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*1PTTQLk3j5AAAAAAAAAAAAAADmJ7AQ/original" } } diff --git a/.github/workflows/sync-notify.yml b/.github/workflows/sync-notify.yml index afde29a503..227d218ae5 100644 --- a/.github/workflows/sync-notify.yml +++ b/.github/workflows/sync-notify.yml @@ -23,7 +23,7 @@ jobs: "title": "✅ 同步 changelog 成功", "text": "📢 请发布值班合并该 PR 后, 访问官网查看是否有异常", "messageUrl": "https://s2.antv.antgroup.com", - "picUrl": "https://gw.alipayobjects.com/zos/antfincdn/ISzgBCtgR/2c5c4aaa-4f40-46f7-8f6b-427fa9ff07bb.png" + "picUrl": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*1PTTQLk3j5AAAAAAAAAAAAAADmJ7AQ/original" } } diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3fb24312f1..7a6b58f018 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -name: test +name: 💯 test on: [pull_request] @@ -18,7 +18,7 @@ jobs: node-version: [20] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # Python 3.11 和 node-gyp 有兼容问题, 导致无法安装依赖 # https://github.com/slint-ui/slint/commit/a9c48e33502fdebc36c5aa2f4f516c2218424679#diff-944291df2c9c06359d37cc8833d182d705c9e8c3108e7cfe132d61a06e9133dd @@ -31,7 +31,7 @@ jobs: version: 8 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' @@ -44,9 +44,10 @@ jobs: pnpm test:ci-coverage - name: Upload test coverage - uses: codecov/codecov-action@v2 + uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false verbose: true - name: Workflow failed alert diff --git a/.releaserc.base.js b/.releaserc.base.js index 29d4c0ef0e..544d514383 100644 --- a/.releaserc.base.js +++ b/.releaserc.base.js @@ -1,11 +1,13 @@ +const path = require('path'); + module.exports = { + extends: 'semantic-release-monorepo', branches: [ 'latest', { name: 'beta', channel: 'beta', prerelease: true }, { name: 'alpha', channel: 'alpha', prerelease: true }, { name: 'next', channel: 'next', prerelease: true }, ], - extends: 'semantic-release-monorepo', plugins: [ [ '@semantic-release/commit-analyzer', @@ -26,11 +28,19 @@ module.exports = { '@semantic-release/npm', [ '@semantic-release/git', - { + { message: 'chore(release): 🤖 ${nextRelease.gitTag} [skip ci]', - }, + }, ], '@semantic-release/github', + [ + '@semantic-release/exec', + { + prepareCmd: + `node ${path.resolve(__dirname, './scripts/add-version.js')} ` + + '${nextRelease.gitTag}', + }, + ], ], preset: 'angular', }; diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b7b5f275ed..5f81433362 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,7 +38,7 @@ 2. 安装依赖:`pnpm install` 3. 提交你的改动,commit 请遵守 [AngularJS Git Commit Message Conventions](https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit#heading=h.uyo6cb12dt6w) 4. 如果你的改动是修复 bug, 还可以在提交信息后面加上 `close #issue 号`, 这样可以在 pr 合并后,可以自动关闭对应的 issue, 比如 `fix: render bug close #123` -5. 确保加上了对应的单元测试和文档 (如有必要) +5. 确保加上了对应的单元测试和文档 (如果有 `Snapshot` UI 快照 (.snap 文件)更新, 可以运行 `yarn core:test -- -u` 和 `yarn react:test -- -u` 自动更新, 并一起提交上来, 请勿手动编辑) 6. 所有 Lint 和 Test 检查通过后,并且 review 通过,我们会合并你的 pr. ![preview](https://gw.alipayobjects.com/zos/antfincdn/ssOxFrycD/86339514-5f9a-4101-8690-e47c97cd8af5.png) diff --git a/LICENSE b/LICENSE index 6fc25ee611..a63e9f8b5b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2021 AntV +Copyright (c) 2021-present AntV Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.en-US.md b/README.en-US.md index 1b93f73601..e2902b21bc 100644 --- a/README.en-US.md +++ b/README.en-US.md @@ -68,9 +68,9 @@ demo components and expansion capabilities, it allows developers to use it quick ## 📦 Installation ```bash -$ npm install @antv/s2 -# yarn add @antv/s2 -# pnpm install @antv/s2 +$ npm install @antv/s2 --save +# yarn add @antv/s2 --save +# pnpm install @antv/s2 --save ``` ## 🔨 Getting Started @@ -229,9 +229,6 @@ pnpm site:start S2 - - S2 -

## 👬 Contributors diff --git a/README.md b/README.md index 5b32f0d71a..1210bbc8bf 100644 --- a/README.md +++ b/README.md @@ -64,9 +64,9 @@ S2 是 AntV 在多维交叉分析表格领域的解决方案,完全基于数 ## 📦 安装 ```bash -$ npm install @antv/s2 -# yarn add @antv/s2 -# pnpm install @antv/s2 +$ npm install @antv/s2 --save +# yarn add @antv/s2 --save +# pnpm install @antv/s2 --save ``` ## 🔨 使用 @@ -223,9 +223,6 @@ pnpm site:start DingTalk - - qq -

## 👬 Contributors diff --git a/package.json b/package.json index 8fcdb05722..0b20ab3a44 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,7 @@ "@rollup/plugin-typescript": "^11.1.5", "@rushstack/eslint-patch": "^1.5.1", "@semantic-release/changelog": "^6.0.3", + "@semantic-release/exec": "^6.0.3", "@semantic-release/git": "^10.0.1", "@swc/core": "^1.3.95", "@swc/jest": "^0.2.29", @@ -164,7 +165,8 @@ "typescript": "^5.2.2", "vite": "^4.5.0", "vite-plugin-imp": "^2.4.0", - "vue-jest": "^5.0.0-alpha.10" + "vue-jest": "^5.0.0-alpha.10", + "size-limit": "^11.0.0" }, "license": "MIT", "repository": { diff --git a/packages/s2-core/CHANGELOG.md b/packages/s2-core/CHANGELOG.md index ee669635bd..d2bec1e76c 100644 --- a/packages/s2-core/CHANGELOG.md +++ b/packages/s2-core/CHANGELOG.md @@ -1,22 +1,19 @@ # [@antv/s2-v2.0.0-next.10](https://github.com/antvis/S2/compare/@antv/s2-v2.0.0-next.9...@antv/s2-v2.0.0-next.10) (2023-12-12) - ### Features * 支持在单元格内渲染 G2 图表 ([#2437](https://github.com/antvis/S2/issues/2437)) ([497f941](https://github.com/antvis/S2/commit/497f9414b89fce01b60db9b6c2eb4292ffe69c1d)) # [@antv/s2-v2.0.0-next.9](https://github.com/antvis/S2/compare/@antv/s2-v2.0.0-next.8...@antv/s2-v2.0.0-next.9) (2023-11-22) - ### Features * headerActionIcons 支持细粒度配置 & 修复异步渲染导致无法获取实例的问题 ([#2301](https://github.com/antvis/S2/issues/2301)) ([b2d6f1f](https://github.com/antvis/S2/commit/b2d6f1fb04d3fa73129669fc7d2dec84943252db)) * **layout:** 单元格支持渲染多行文本 ([#2383](https://github.com/antvis/S2/issues/2383)) ([e3b919a](https://github.com/antvis/S2/commit/e3b919a4f37d600a0f516944edf4eed8b2c0174d)) * 支持 antd v5 ([#2413](https://github.com/antvis/S2/issues/2413)) ([299c7bf](https://github.com/antvis/S2/commit/299c7bfe2e86838153273c92dd6d2b72917cfdea)) -* 支持 React 18 (兼容 React 16/17) ([#2373](https://github.com/antvis/S2/issues/2373)) ([25ce9b0](https://github.com/antvis/S2/commit/25ce9b0ccc3e609d8add09b3209f6f981dc1dc4e)) +* 支持 React 18 (兼容 React 16/17) ([#2373](https://github.com/antvis/S2/issues/2373)) ([25ce9b0](https://github.com/antvis/S2/commit/25ce9b0ccc3e609d8add09b3209f6f981dc1dc4e)) * 支持自定义 G 5.0 插件和配置 ([#2423](https://github.com/antvis/S2/issues/2423)) ([cc6c47f](https://github.com/antvis/S2/commit/cc6c47fd0927125bbc378fe6914becfcbe1b0acd)) - ### BREAKING CHANGES * 移除 devicePixelRatio 和 supportsCSSTransform @@ -75,13 +72,13 @@ ### Features * 使用 requestIdleCallback 处理数据大量导出的情况 ([#2272](https://github.com/antvis/S2/issues/2272)) ([42a5551](https://github.com/antvis/S2/commit/42a55516dd369d9ab5579b52fbc9900b0ad81858)) -* 同步复制支持自定义transformer ([#2201](https://github.com/antvis/S2/issues/2201)) ([9003767](https://github.com/antvis/S2/commit/9003767d584248b9d122f299326fd14753961883)) +* 同步复制支持自定义 transformer ([#2201](https://github.com/antvis/S2/issues/2201)) ([9003767](https://github.com/antvis/S2/commit/9003767d584248b9d122f299326fd14753961883)) * 增加暗黑主题 ([#2130](https://github.com/antvis/S2/issues/2130)) ([51dbdcf](https://github.com/antvis/S2/commit/51dbdcf564b387a3fd1809a71016f3a91eebde38)) * 完善复制和导出在格式化后,总计、小计对应数值没有格式化的问题 ([#2237](https://github.com/antvis/S2/issues/2237)) ([abc0dbb](https://github.com/antvis/S2/commit/abc0dbb1544d9a4ef133e6a2c7d2d09ac8f35b48)) * 文本和图标的条件格式支持主题配置 ([#2267](https://github.com/antvis/S2/issues/2267)) ([c332c68](https://github.com/antvis/S2/commit/c332c687dfb7be1d07b79b44934f78c1947cc466)) * 条件格式 mapping 增加第三个参数获取单元格实例 ([#2242](https://github.com/antvis/S2/issues/2242)) ([aae427d](https://github.com/antvis/S2/commit/aae427dfe6a87cae577ce2449fd6058d358971f9)) * 行列头兼容 condition icon 和 action icons ([#2161](https://github.com/antvis/S2/issues/2161)) ([1df4286](https://github.com/antvis/S2/commit/1df42860f6a12d3cb182ba7633c4984a04e62890)) -* 适配g5.0异步渲染 ([#2251](https://github.com/antvis/S2/issues/2251)) ([069d03d](https://github.com/antvis/S2/commit/069d03d299429c2ffab3e20d56ecd6bb30119ffd)) +* 适配 g5.0 异步渲染 ([#2251](https://github.com/antvis/S2/issues/2251)) ([069d03d](https://github.com/antvis/S2/commit/069d03d299429c2ffab3e20d56ecd6bb30119ffd)) # [@antv/s2-v2.0.0-next.7](https://github.com/antvis/S2/compare/@antv/s2-v2.0.0-next.6...@antv/s2-v2.0.0-next.7) (2023-04-28) @@ -95,25 +92,25 @@ * **tooltip:** 修复特定配置下点击 tooltip 内容后 tooltip 关闭 close [#2170](https://github.com/antvis/S2/issues/2170) ([#2172](https://github.com/antvis/S2/issues/2172)) ([6219e57](https://github.com/antvis/S2/commit/6219e579364cfb7ac3a8b3db4ae01c5672d7f2d4)) * 修复 cornerText 配置对树状模式的适配 ([#2167](https://github.com/antvis/S2/issues/2167)) ([e9efcea](https://github.com/antvis/S2/commit/e9efcea944f5d0793d4a1250362e6b6f6b492c52)) * 修复总计小计 linkField 样式问题 ([#2169](https://github.com/antvis/S2/issues/2169)) ([4450278](https://github.com/antvis/S2/commit/4450278d82888c117e5bd9d31874b88ecdb33d99)) -* 修改DataCell类 drawLinkFieldShape 方法名为 drawLinkFieldShapeOwn ([d5e14b2](https://github.com/antvis/S2/commit/d5e14b25abba5bfaf74dddb17d9f5b44c74bc29b)) +* 修改 DataCell 类 drawLinkFieldShape 方法名为 drawLinkFieldShapeOwn ([d5e14b2](https://github.com/antvis/S2/commit/d5e14b25abba5bfaf74dddb17d9f5b44c74bc29b)) * 多指标行头总计节点宽度计算错误 ([#2165](https://github.com/antvis/S2/issues/2165)) ([08ef330](https://github.com/antvis/S2/commit/08ef330a02a1fbf11f49090f4fd7f5d2b0cc1093)) -* 微应用环境识别mouseEvent失效 ([bddbe34](https://github.com/antvis/S2/commit/bddbe34104355ac0087bc9f72377889a8f444d7a)), closes [#2162](https://github.com/antvis/S2/issues/2162) +* 微应用环境识别 mouseEvent 失效 ([bddbe34](https://github.com/antvis/S2/commit/bddbe34104355ac0087bc9f72377889a8f444d7a)), closes [#2162](https://github.com/antvis/S2/issues/2162) * 统一风格、删除冗余代码 ([7b4ef0e](https://github.com/antvis/S2/commit/7b4ef0edf72e059b427c54e6ea881c4c8e347aed)) * 行头过宽且不冻结时滚动条渲染错误 ([#2173](https://github.com/antvis/S2/issues/2173)) ([ab79ea0](https://github.com/antvis/S2/commit/ab79ea0664046bc6479a717d7b3b0ee7efe05b31)) -* 避免s2实例被污染 ([8c44a85](https://github.com/antvis/S2/commit/8c44a85a678eadaab3fb2a66b5b02a123f74c9bb)) +* 避免 s2 实例被污染 ([8c44a85](https://github.com/antvis/S2/commit/8c44a85a678eadaab3fb2a66b5b02a123f74c9bb)) ### Features -* icon支持更新name与fill ([#2138](https://github.com/antvis/S2/issues/2138)) ([d000aea](https://github.com/antvis/S2/commit/d000aeac332676cfa15d9986ec7f4be948c565d0)) +* icon 支持更新 name 与 fill ([#2138](https://github.com/antvis/S2/issues/2138)) ([d000aea](https://github.com/antvis/S2/commit/d000aeac332676cfa15d9986ec7f4be948c565d0)) * **interaction:** 点击角头后支持选中所对应那一列的行头 close [#2073](https://github.com/antvis/S2/issues/2073) ([#2081](https://github.com/antvis/S2/issues/2081)) ([ad2b5d8](https://github.com/antvis/S2/commit/ad2b5d87edf4c529d7c9a5e1348e893e14547ef3)) * **interaction:** 行头支持滚动刷选 ([#2087](https://github.com/antvis/S2/issues/2087)) ([65c3f3b](https://github.com/antvis/S2/commit/65c3f3b6a37709c0fa684b0f5717d3b349251e48)) -* 修改文档、添加用例演示、修改方法名drawLinkFieldShapLogic -> drawLinkField ([7f2bd69](https://github.com/antvis/S2/commit/7f2bd690bd703b8e4d678c03b9fc79db30848ca3)) +* 修改文档、添加用例演示、修改方法名 drawLinkFieldShapLogic -> drawLinkField ([7f2bd69](https://github.com/antvis/S2/commit/7f2bd690bd703b8e4d678c03b9fc79db30848ca3)) * 在 shape 中添加文本的原始值 ([#2109](https://github.com/antvis/S2/issues/2109)) ([4d81e72](https://github.com/antvis/S2/commit/4d81e72440d797fd7a06179794c342f009fc39c3)) -* 增加dataCell 下划线测试用例及demo ([a5efe17](https://github.com/antvis/S2/commit/a5efe17bda06cc8eba633cbea9c56ceb8b8c703e)) -* 增加自定义merged-cell ([534cc15](https://github.com/antvis/S2/commit/534cc15da9f766f95be3c622e65e45d8796ff020)) -* 复制支持自定义transformer ([#2090](https://github.com/antvis/S2/issues/2090)) ([250eecd](https://github.com/antvis/S2/commit/250eecd32ed4f48b95ed7c4e480fa3c75d4bb5d7)) +* 增加 dataCell 下划线测试用例及 demo ([a5efe17](https://github.com/antvis/S2/commit/a5efe17bda06cc8eba633cbea9c56ceb8b8c703e)) +* 增加自定义 merged-cell ([534cc15](https://github.com/antvis/S2/commit/534cc15da9f766f95be3c622e65e45d8796ff020)) +* 复制支持自定义 transformer ([#2090](https://github.com/antvis/S2/issues/2090)) ([250eecd](https://github.com/antvis/S2/commit/250eecd32ed4f48b95ed7c4e480fa3c75d4bb5d7)) * 提取跳转链接下划线 公共逻辑 到 BaseCell 类 ([34dbbb3](https://github.com/antvis/S2/commit/34dbbb3bdf028cb96508dcead724d9ac9bcc1ab9)) -* 数据单元格DataCell类中增加链接跳转渲染 ([bb5a964](https://github.com/antvis/S2/commit/bb5a964787a80843515b4d552adb3fdb59393e3d)) +* 数据单元格 DataCell 类中增加链接跳转渲染 ([bb5a964](https://github.com/antvis/S2/commit/bb5a964787a80843515b4d552adb3fdb59393e3d)) # [@antv/s2-v2.0.0-next.6](https://github.com/antvis/S2/compare/@antv/s2-v2.0.0-next.5...@antv/s2-v2.0.0-next.6) (2023-04-23) @@ -300,7 +297,6 @@ ### Bug Fixes -* 下钻后 meta.childField 不正确 ([#1788](https://github.com/antvis/S2/issues/1788)) ([1c61dd4](https://github.com/antvis/S2/commit/1c61dd4081c9d3fed6f276b0546865914040b07a)) * 重构绘制盒模型,修复边框偏移问题 ([#1854](https://github.com/antvis/S2/issues/1854)) ([f7e0858](https://github.com/antvis/S2/commit/f7e0858a937ea557532a7fff948e9af3b6a1fdff)) # [@antv/s2-v1.35.0](https://github.com/antvis/S2/compare/@antv/s2-v1.34.1...@antv/s2-v1.35.0) (2022-11-21) @@ -315,11 +311,11 @@ # [@antv/s2-v1.35.0](https://github.com/antvis/S2/compare/@antv/s2-v1.34.1...@antv/s2-v1.35.0) (2022-11-21) -### Features +======= -* 明细表支持多级表头 ([#1921](https://github.com/antvis/S2/issues/1921)) ([47cdbdc](https://github.com/antvis/S2/commit/47cdbdccafbd7f19a05550a483a42aac11a93778)), closes [#1687](https://github.com/antvis/S2/issues/1687) [#1801](https://github.com/antvis/S2/issues/1801) +# [@antv/s2-v1.35.1](https://github.com/antvis/S2/compare/@antv/s2-v1.35.0...@antv/s2-v1.35.1) (2022-11-28) -# [@antv/s2-v1.34.1](https://github.com/antvis/S2/compare/@antv/s2-v1.34.0...@antv/s2-v1.34.1) (2022-11-18) +### Bug Fixes ### Bug Fixes @@ -487,11 +483,6 @@ * 复制支持 html 格式 ([#1647](https://github.com/antvis/S2/issues/1647)) ([3ea6349](https://github.com/antvis/S2/commit/3ea634970a162d869cf12dad7aa754bebafd30f3)) * 支持 resize 最右侧 column ([#1611](https://github.com/antvis/S2/issues/1611)) ([f63bfa2](https://github.com/antvis/S2/commit/f63bfa2a0e95c8c42c064d0e2e56ce9550ac50c6)) -# [@antv/s2-v1.24.0](https://github.com/antvis/S2/compare/@antv/s2-v1.23.0...@antv/s2-v1.24.0) (2022-07-22) - -### Bug Fixes - -* **layout:** 修复 Firefox 浏览器部分 icon 渲染失败 close [#1571](https://github.com/antvis/S2/issues/1571) ([#1599](https://github.com/antvis/S2/issues/1599)) ([6b76c4e](https://github.com/antvis/S2/commit/6b76c4e2c80b88eeb63d7adfc6b48da7d0b3ea4c)) * **strategysheet:** 修复单元格宽度拖拽变小后子弹图宽度计算错误 ([#1584](https://github.com/antvis/S2/issues/1584)) ([99b8593](https://github.com/antvis/S2/commit/99b859392c7151d5700bf1c505a02f795b9a3f80)) * **strategysheet:** 修复子弹图进度小于 1% 时显示错误的问题 ([#1563](https://github.com/antvis/S2/issues/1563)) ([936ca6a](https://github.com/antvis/S2/commit/936ca6a3a7bf40ddc0ff1a0271c3a5ffb1091dcf)) * **strategysheet:** 修复子弹图颜色显示错误 & 百分比精度问题 ([#1588](https://github.com/antvis/S2/issues/1588)) ([c4bb48c](https://github.com/antvis/S2/commit/c4bb48cbe128b47e3574af903142934fd7452846)) diff --git a/packages/s2-core/README.md b/packages/s2-core/README.md index 80a489c909..c04f9fec8c 100644 --- a/packages/s2-core/README.md +++ b/packages/s2-core/README.md @@ -143,7 +143,7 @@ const s2DataConfig = { ```ts const s2Options = { width: 600, - height: 600, + height: 600 } ``` @@ -219,9 +219,6 @@ pnpm site:start DingTalk - - qq -

## 👬 Contributors diff --git a/packages/s2-core/__tests__/bugs/__snapshots__/issue-2359-spec.ts.snap b/packages/s2-core/__tests__/bugs/__snapshots__/issue-2359-spec.ts.snap new file mode 100644 index 0000000000..076a151e3f --- /dev/null +++ b/packages/s2-core/__tests__/bugs/__snapshots__/issue-2359-spec.ts.snap @@ -0,0 +1,67 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Table Sheet Custom Multiple Values Tests should use current cell text theme 1`] = ` +Array [ + Array [ + Object { + "fill": "#000000", + "fontSize": 12, + }, + ], + Array [ + Object { + "fill": "red", + "fontSize": 20, + }, + Object { + "fill": "red", + "fontSize": 20, + }, + Object { + "fill": "red", + "fontSize": 20, + }, + ], +] +`; + +exports[`Table Sheet Custom Multiple Values Tests should use current cell text theme 2`] = ` +Array [ + Array [ + Object { + "fill": "#000000", + "fontSize": 12, + }, + ], + Array [ + Object { + "fill": "#000000", + "fontSize": 12, + }, + ], + Array [ + Object { + "fill": "#000000", + "fontSize": 12, + }, + ], + Array [ + Object { + "fill": "green", + "fontSize": 30, + }, + ], + Array [ + Object { + "fill": "green", + "fontSize": 30, + }, + ], + Array [ + Object { + "fill": "green", + "fontSize": 30, + }, + ], +] +`; diff --git a/packages/s2-core/__tests__/bugs/__snapshots__/issue-565-spec.ts.snap b/packages/s2-core/__tests__/bugs/__snapshots__/issue-565-spec.ts.snap new file mode 100644 index 0000000000..72fd2fd2d2 --- /dev/null +++ b/packages/s2-core/__tests__/bugs/__snapshots__/issue-565-spec.ts.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Export data in pivot tree mode should export correct col header in pivot tree mode 1`] = ` +" col0 col0-0 col0-0 + col1 col1-0 col1-1 + col2 col2-0 col2-0 +row0 row1 row2 number number +row0 +row0 row1-0 +row0 row1-0 row2-0 3 +row0 row1-1 +row0 row1-1 row2-0 2 4" +`; diff --git a/packages/s2-core/__tests__/bugs/issue-1191-spec.ts b/packages/s2-core/__tests__/bugs/issue-1191-spec.ts index f533ac3630..696f9e0ca1 100644 --- a/packages/s2-core/__tests__/bugs/issue-1191-spec.ts +++ b/packages/s2-core/__tests__/bugs/issue-1191-spec.ts @@ -49,7 +49,7 @@ const dataCfg: S2DataConfig = { { field: 'price', name: '价格', - formatter: (v) => `price:${v}`, + formatter: (v) => `price: ${v}`, }, ], }; @@ -74,21 +74,21 @@ describe('Link Field Tests', () => { test('province row cell should use link field style', () => { // 浙江省对应 cell - const province = s2.facet.rowHeader?.children[0]; + const province = s2.facet + .getRowCells() + .find((cell) => cell.getMeta().value === '浙江'); - // @ts-ignore - expect(province.textShape.attr('fill')).toEqual('red'); - // @ts-ignore - expect(province.linkFieldShape).toBeDefined(); + expect(province?.getTextShape().attr('fill')).toEqual('red'); + expect(province?.getLinkFieldShape()).toBeDefined(); }); test('city row cell should not use link field style', () => { // 义乌对应 cell - const city = s2.facet.rowHeader?.children[1]; + const city = s2.facet + .getRowCells() + .find((cell) => cell.getMeta().value === '义乌'); - // @ts-ignore - expect(city.textShape.attr('fill')).not.toEqual('red'); - // @ts-ignore - expect(city.linkFieldShape).not.toBeDefined(); + expect(city?.getTextShape().attr('fill')).not.toEqual('red'); + expect(city?.getLinkFieldShape()).not.toBeDefined(); }); }); diff --git a/packages/s2-core/__tests__/bugs/issue-1201-spec.ts b/packages/s2-core/__tests__/bugs/issue-1201-spec.ts index f04d0d57d5..2af1b5b5bb 100644 --- a/packages/s2-core/__tests__/bugs/issue-1201-spec.ts +++ b/packages/s2-core/__tests__/bugs/issue-1201-spec.ts @@ -4,11 +4,12 @@ * https://github.com/antvis/S2/issues/1201 * fillOpacity */ -import { getContainer } from '../util/helpers'; +import type { S2Options } from '../../src'; import * as mockDataConfig from '../data/data-issue-292.json'; +import { getContainer } from '../util/helpers'; import { PivotSheet } from '@/sheet-type'; -const s2Options = { +const s2Options: S2Options = { width: 800, height: 600, }; @@ -17,22 +18,20 @@ describe('background color opacity test', () => { test('should set background color opacity correctly', async () => { const s2 = new PivotSheet(getContainer(), mockDataConfig, s2Options); - s2.setThemeCfg({ - theme: { - cornerCell: { - cell: { - backgroundColorOpacity: 0.1, - }, + s2.setTheme({ + cornerCell: { + cell: { + backgroundColorOpacity: 0.1, }, - rowCell: { - cell: { - backgroundColorOpacity: 0.2, - }, + }, + rowCell: { + cell: { + backgroundColorOpacity: 0.2, }, - colCell: { - cell: { - backgroundColorOpacity: 0.3, - }, + }, + colCell: { + cell: { + backgroundColorOpacity: 0.3, }, }, }); @@ -40,21 +39,18 @@ describe('background color opacity test', () => { await s2.render(); // corner cell - const cornerCell = s2.facet.cornerHeader.children[0]; + const cornerCell = s2.facet.getCornerCells()[0]; - // @ts-ignore - expect(cornerCell.backgroundShape.attr('fillOpacity')).toEqual(0.1); + expect(cornerCell.getBackgroundShape().style.fillOpacity).toEqual(0.1); // row cell - const rowCell = s2.facet.rowHeader!.children[0]; + const rowCell = s2.facet.getRowCells()[0]; - // @ts-ignore - expect(rowCell.backgroundShape.attr('fillOpacity')).toEqual(0.2); + expect(rowCell.getBackgroundShape().style.fillOpacity).toEqual(0.2); // col cell - const colCell = s2.facet.columnHeader.children[0].children[0]; + const colCell = s2.facet.getColCells()[0]; - // @ts-ignore - expect(colCell.backgroundShape.attr('fillOpacity')).toEqual(0.3); + expect(colCell.getBackgroundShape().style.fillOpacity).toEqual(0.3); }); }); diff --git a/packages/s2-core/__tests__/bugs/issue-1561-spec.ts b/packages/s2-core/__tests__/bugs/issue-1561-spec.ts index dadd3db4a6..058eeb32c2 100644 --- a/packages/s2-core/__tests__/bugs/issue-1561-spec.ts +++ b/packages/s2-core/__tests__/bugs/issue-1561-spec.ts @@ -23,22 +23,23 @@ describe('Grid Border Tests', () => { const panelScrollGroup = s2.facet.panelGroup.children[0]; const gridGroup = (panelScrollGroup as any).gridGroup as Group; - const originalLeftBorderBbox = (gridGroup.children[0] as Group).getBBox(); + const originalLeftBorderBBox = (gridGroup.children[0] as Group).getBBox(); s2.facet.updateScrollOffset({ offsetX: { value: 100, animate: false } }); s2.facet.updateScrollOffset({ offsetX: { value: 200, animate: false } }); s2.facet.updateScrollOffset({ offsetX: { value: 300, animate: false } }); s2.facet.updateScrollOffset({ offsetX: { value: 0, animate: false } }); - const newLeftBorderBbox = (gridGroup.children[0] as Group).getBBox(); + + const newLeftBorderBBbox = (gridGroup.children[0] as Group).getBBox(); const widthRatio = - newLeftBorderBbox.right - - newLeftBorderBbox.left - - (originalLeftBorderBbox.right - originalLeftBorderBbox.left); + newLeftBorderBBbox.right - + newLeftBorderBBbox.left - + (originalLeftBorderBBox.right - originalLeftBorderBBox.left); const heightRatio = - newLeftBorderBbox.bottom - - newLeftBorderBbox.top - - (originalLeftBorderBbox.bottom - originalLeftBorderBbox.top); + newLeftBorderBBbox.bottom - + newLeftBorderBBbox.top - + (originalLeftBorderBBox.bottom - originalLeftBorderBBox.top); // g绘制时,会将坐标1变成0.5,来达到真正绘制1px的效果,因此宽高不一定完全相同,会有1px的差值 expect(widthRatio).toBeLessThanOrEqual(1); diff --git a/packages/s2-core/__tests__/bugs/issue-1587-spec.ts b/packages/s2-core/__tests__/bugs/issue-1587-spec.ts index bb5ff56da7..c39d5c3903 100644 --- a/packages/s2-core/__tests__/bugs/issue-1587-spec.ts +++ b/packages/s2-core/__tests__/bugs/issue-1587-spec.ts @@ -11,9 +11,8 @@ import { PivotSheet } from '@/sheet-type'; const s2Options: S2Options = { width: 800, height: 600, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error pagination: { + current: 1, pageSize: 2, }, }; diff --git a/packages/s2-core/__tests__/bugs/issue-1624-spec.ts b/packages/s2-core/__tests__/bugs/issue-1624-spec.ts new file mode 100644 index 0000000000..9ce404a029 --- /dev/null +++ b/packages/s2-core/__tests__/bugs/issue-1624-spec.ts @@ -0,0 +1,51 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/** + * @description spec for issue #1624 + * https://github.com/antvis/S2/issues/1624 + */ + +import * as mockDataConfig from '../data/simple-data.json'; +import { getContainer, sleep } from '../util/helpers'; +import { S2Event, type S2Options } from '@/index'; +import { PivotSheet } from '@/sheet-type'; + +const s2Options: S2Options = { + width: 800, + height: 600, + hdAdapter: false, +}; + +describe('Data Cell Border Tests', () => { + const borderWidth = 4; + + test('should draw correct data cell border when hover focus', async () => { + const s2 = new PivotSheet(getContainer(), mockDataConfig, s2Options); + + s2.setTheme({ + dataCell: { + cell: { + verticalBorderWidth: borderWidth, + horizontalBorderWidth: borderWidth, + }, + }, + }); + await s2.render(); + + const dataCell = s2.facet.getDataCells()[0]; + + s2.emit(S2Event.DATA_CELL_HOVER, { + target: dataCell, + } as any); + + await sleep(40); + + const meta = dataCell.getBBoxByType(); + const borderBBox = dataCell + .getStateShapes() + .get('interactiveBorderShape')! + .getBBox(); + + expect(meta.width).toBeGreaterThanOrEqual(borderBBox.width + borderWidth); + expect(meta.height).toBeGreaterThanOrEqual(borderBBox.height + borderWidth); + }); +}); diff --git a/packages/s2-core/__tests__/bugs/issue-1715-spec.ts b/packages/s2-core/__tests__/bugs/issue-1715-spec.ts index 682d3f62d6..4eaf9d183b 100644 --- a/packages/s2-core/__tests__/bugs/issue-1715-spec.ts +++ b/packages/s2-core/__tests__/bugs/issue-1715-spec.ts @@ -4,6 +4,7 @@ * https://github.com/antvis/S2/issues/1715 * https://github.com/antvis/S2/issues/2049 */ + import { getContainer } from '../util/helpers'; import * as mockDataConfig from '../data/mock-dataset.json'; import type { S2DataConfig, S2Options, SpreadSheet } from '../../src'; @@ -81,9 +82,10 @@ describe('Multi Values GrandTotal Height Test', () => { const grandTotalsNode = s2.facet .getColNodes() - .find((node) => node.isGrandTotals)!; + .find((node) => node.isGrandTotals && node.isTotalRoot); - expect(s2.facet.getLayoutResult().colsHierarchy.height).toBe(60); - expect(grandTotalsNode.height).toEqual(30); + // 有多个 Value 时不允许隐藏度量列 + expect(s2.facet.getLayoutResult().colsHierarchy.height).toBe(90); + expect(grandTotalsNode!.height).toEqual(60); }); }); diff --git a/packages/s2-core/__tests__/bugs/issue-1781-spec.ts b/packages/s2-core/__tests__/bugs/issue-1781-spec.ts index 30dee7a17b..c8a357c5b3 100644 --- a/packages/s2-core/__tests__/bugs/issue-1781-spec.ts +++ b/packages/s2-core/__tests__/bugs/issue-1781-spec.ts @@ -4,12 +4,13 @@ * https://github.com/antvis/S2/issues/1781 */ +import type { FederatedPointerEvent } from '@antv/g'; +import * as mockDataConfig from '../data/simple-table-data.json'; import { createFederatedMouseEvent, getContainer, sleep, } from '../util/helpers'; -import * as mockDataConfig from '../data/simple-table-data.json'; import { OriginEventType, S2Event, @@ -46,27 +47,29 @@ describe('Hover Focus Tests', () => { await sleep(3000); // 浙江省份信息 - const provinceCell = s2.facet.panelScrollGroup.getChildByIndex(7); + const provinceCell = s2.facet.getDataCells()[7]; // 义乌城市信息 - const cityCell = s2.facet.panelScrollGroup.getChildByIndex(10); + const cityCell = s2.facet.getDataCells()[10]; const event = createFederatedMouseEvent(s2, OriginEventType.POINTER_MOVE); event.target = provinceCell; - // @ts-ignore - s2.emit(S2Event.DATA_CELL_HOVER, event); + s2.emit(S2Event.DATA_CELL_HOVER, event as FederatedPointerEvent); expect( - // @ts-ignore - provinceCell.stateShapes + provinceCell + .getStateShapes() .get('interactiveBorderShape') ?.attr('visibility'), ).toEqual('visible'); + expect( - // @ts-ignore - cityCell.stateShapes.get('interactiveBorderShape')?.attr('visibility'), + cityCell + .getStateShapes() + .get('interactiveBorderShape') + ?.attr('visibility'), ).toEqual('hidden'); }); }); diff --git a/packages/s2-core/__tests__/bugs/issue-2164-spec.ts b/packages/s2-core/__tests__/bugs/issue-2164-spec.ts index 394da2dc35..a9dde4113f 100644 --- a/packages/s2-core/__tests__/bugs/issue-2164-spec.ts +++ b/packages/s2-core/__tests__/bugs/issue-2164-spec.ts @@ -6,14 +6,14 @@ import { getContainer } from '../util/helpers'; import * as mockDataConfig from '../data/simple-data.json'; -import type { S2Options } from '@/index'; +import { LayoutWidthType, type S2Options } from '@/index'; import { PivotSheet } from '@/sheet-type'; const s2Options: S2Options = { width: 400, height: 400, style: { - layoutWidthType: 'compact', + layoutWidthType: LayoutWidthType.Compact, }, totals: { row: { @@ -32,7 +32,8 @@ describe('Grand Total Row Node Tests', () => { ...mockDataConfig, fields: { ...mockDataConfig.fields, - valueInCols: false, // 指标放行头 + // 指标放行头 + valueInCols: false, }, }, s2Options, diff --git a/packages/s2-core/__tests__/bugs/issue-2195-spec.ts b/packages/s2-core/__tests__/bugs/issue-2195-spec.ts new file mode 100644 index 0000000000..ed8c1e1d7f --- /dev/null +++ b/packages/s2-core/__tests__/bugs/issue-2195-spec.ts @@ -0,0 +1,74 @@ +/** + * 字段代码中有方括号,无法拖拽调整该字段所在列的宽度 + * 描述应当调整为:字段代码中有方括号,无法使用 `xxxByField` 配置调整该字段的布局 + * @description spec for issue #2195 + * https://github.com/antvis/S2/issues/2195 + */ + +import { getContainer } from '../util/helpers'; +import type { S2DataConfig, S2Options } from '@/index'; +import { PivotSheet } from '@/sheet-type'; + +const modifiedMockDataConfig: S2DataConfig = { + fields: { + rows: ['province', '[city]'], + columns: ['type'], + values: ['price', 'cost'], + valueInCols: true, + }, + data: [ + { + province: '浙江', + '[city]': '义乌', + type: '笔', + price: 1, + cost: 2, + }, + { + province: '浙江', + '[city]': '义乌', + type: '笔', + price: 1, + cost: 2, + }, + { + province: '浙江', + '[city]': '杭州', + type: '笔', + price: 1, + cost: 2, + }, + ], +}; + +const s2Options: S2Options = { + width: 400, + height: 400, + style: { + rowCell: { + widthByField: { + province: 300, + '[city]': 123, + }, + }, + }, +}; + +describe('Field surrounded by square brackets Tests', () => { + test('should render correctly when use field surrounded by square brackets', async () => { + const s2 = new PivotSheet( + getContainer(), + modifiedMockDataConfig, + s2Options, + ); + + await s2.render(); + + s2.facet + .getLayoutResult() + .rowNodes.filter((node) => node.field === '[city]') + .forEach((node) => { + expect(node.width).toEqual(123); + }); + }); +}); diff --git a/packages/s2-core/__tests__/bugs/issue-2199-spec.ts b/packages/s2-core/__tests__/bugs/issue-2199-spec.ts new file mode 100644 index 0000000000..fa82bb1813 --- /dev/null +++ b/packages/s2-core/__tests__/bugs/issue-2199-spec.ts @@ -0,0 +1,41 @@ +/** + * @description spec for issue #2199 + * https://github.com/antvis/S2/issues/2199 + * 明细表: 当有冻结列 + 列分组的情况下, 会出现列头文本不居中现象 + */ +import { getContainer } from 'tests/util/helpers'; +import dataCfg from '../data/data-issue-2199.json'; +import { TableSheet } from '@/sheet-type'; +import type { S2Options } from '@/common/interface'; + +const s2Options: S2Options = { + width: 300, + height: 480, + showSeriesNumber: true, + frozen: { + colCount: 1, + }, +}; + +describe('ColCell Text Center Tests', () => { + test('should draw text centered in cell', async () => { + const s2 = new TableSheet(getContainer(), dataCfg, s2Options); + + await s2.render(); + + s2.facet.updateScrollOffset({ + offsetX: { + value: 500, + animate: false, + }, + }); + + const node = s2.facet.getColNodes(0).slice(-1)?.[0]; + const cell = node?.belongsCell; + const { width: nodeWidth, x: nodeX } = node; + const { width: textWidth, x: textXActual } = cell!.getBBoxByType(); + const textXCalc = nodeX + (nodeWidth - textWidth) / 2; + + expect(textXCalc).toBeCloseTo(textXActual); + }); +}); diff --git a/packages/s2-core/__tests__/bugs/issue-2322-spec.ts b/packages/s2-core/__tests__/bugs/issue-2322-spec.ts new file mode 100644 index 0000000000..3b5a8874dd --- /dev/null +++ b/packages/s2-core/__tests__/bugs/issue-2322-spec.ts @@ -0,0 +1,50 @@ +/** + * @description spec for issue #2322 + * https://github.com/antvis/S2/issues/2322 + * 明细表: 多列筛选后清空其中一列筛选,导致其他筛选也清空 + */ +import { getContainer, sleep } from 'tests/util/helpers'; +import dataCfg from '../data/data-issue-2322.json'; +import { S2Event } from '@/common'; +import type { S2Options } from '@/common/interface'; +import { TableSheet } from '@/sheet-type'; + +const s2Options: S2Options = { + width: 800, + height: 480, + showSeriesNumber: true, + frozen: { + colCount: 1, + }, +}; + +describe('Table Sheet Filter Test', () => { + test('should filter correctly when part of the filter is cleaned', async () => { + const s2 = new TableSheet(getContainer(), dataCfg, s2Options); + + await s2.render(); + + // 为两个不同的列设定过滤 + s2.emit(S2Event.RANGE_FILTER, { + filterKey: 'province', + filteredValues: ['吉林'], + }); + s2.emit(S2Event.RANGE_FILTER, { + filterKey: 'city', + filteredValues: ['杭州'], + }); + + // 删除一列过滤 + s2.emit(S2Event.RANGE_FILTER, { + filterKey: 'province', + filteredValues: [], + }); + + await sleep(200); + + // 应过滤掉 city = 杭州 的值,共4行 + expect(s2.dataSet.getDisplayDataSet()).toHaveLength( + s2.dataSet.originData.length - 4, + ); + }); +}); diff --git a/packages/s2-core/__tests__/bugs/issue-2340-spec.ts b/packages/s2-core/__tests__/bugs/issue-2340-spec.ts new file mode 100644 index 0000000000..33f194d379 --- /dev/null +++ b/packages/s2-core/__tests__/bugs/issue-2340-spec.ts @@ -0,0 +1,80 @@ +/** + * @description spec for issue #2340 + * https://github.com/antvis/S2/issues/2340 + */ +import { + CellType, + InteractionStateName, + getCellMeta, + type S2CellType, + type S2Options, +} from '../../src'; +import { createPivotSheet, sleep } from '../util/helpers'; + +const s2Options: S2Options = { + width: 800, + height: 600, + style: { + dataCell: { + width: 200, + height: 200, + }, + }, +}; + +describe('Header Brush Selection Tests', () => { + test.each([CellType.COL_CELL, CellType.ROW_CELL])( + 'should not trigger data cell selected when header selected and scroll out of viewport', + async (cellType) => { + const s2 = createPivotSheet(s2Options, { useSimpleData: false }); + + await s2.render(); + + const isRow = cellType === CellType.ROW_CELL; + const targetCells = isRow + ? s2.facet.getRowCells() + : s2.facet.getColCells(); + + const cells = [ + targetCells.find((cell) => { + const meta = cell.getMeta(); + + return meta.isLeaf; + }), + ] as S2CellType[]; + + s2.interaction.changeState({ + cells: cells.map(getCellMeta), + stateName: InteractionStateName.BRUSH_SELECTED, + }); + + await sleep(500); + + const offsetKey = isRow ? 'offsetY' : 'offsetX'; + + // 将圈选的单元格滑出可视范围 + s2.facet.updateScrollOffset({ + [offsetKey]: { value: 300 }, + }); + + await sleep(500); + + // 还原 + s2.facet.updateScrollOffset({ + [offsetKey]: { value: 0 }, + }); + + expect(s2.interaction.getActiveCells()).toHaveLength(1); + expect(s2.interaction.getCurrentStateName()).toEqual( + InteractionStateName.BRUSH_SELECTED, + ); + + // 交互过的不应该有 dataCell (未触发过列头多选) + s2.interaction.getInteractedCells().forEach((cell) => { + expect(cell.cellType).toEqual( + isRow ? CellType.ROW_CELL : CellType.COL_CELL, + ); + }); + }, + ); +}); diff --git a/packages/s2-core/__tests__/bugs/issue-2359-spec.ts b/packages/s2-core/__tests__/bugs/issue-2359-spec.ts new file mode 100644 index 0000000000..2d4ad61d5e --- /dev/null +++ b/packages/s2-core/__tests__/bugs/issue-2359-spec.ts @@ -0,0 +1,66 @@ +/** + * @description spec for issue #2359 + * https://github.com/antvis/S2/issues/2359 + * 明细表: 自定义列头误用 dataCell 样式 + */ +import { pick } from 'lodash'; +import { createTableSheet } from 'tests/util/helpers'; +import { TableColCell, drawObjectText } from '../../src'; +import type { S2CellType, S2Options } from '@/common/interface'; + +class TestColCell extends TableColCell { + drawTextShape() { + drawObjectText(this, { + values: [['A', 'B', 'C']], + }); + } +} + +const s2Options: S2Options = { + width: 300, + height: 480, + showSeriesNumber: true, + colCell: (...args) => new TestColCell(...args), +}; + +describe('Table Sheet Custom Multiple Values Tests', () => { + test('should use current cell text theme', async () => { + const s2 = createTableSheet(s2Options); + + s2.setTheme({ + colCell: { + measureText: { + fontSize: 12, + }, + bolderText: { + fontSize: 14, + }, + text: { + fontSize: 20, + fill: 'red', + }, + }, + dataCell: { + text: { + fontSize: 30, + fill: 'green', + }, + }, + }); + await s2.render(); + + const mapTheme = (cell: S2CellType) => { + return cell + .getTextShapes() + .map((shape) => pick(shape.attributes, ['fill', 'fontSize'])); + }; + + const colCellTexts = s2.facet.getColCells().map(mapTheme); + + const dataCellTexts = s2.facet.getDataCells().map(mapTheme); + + expect(colCellTexts).toMatchSnapshot(); + + expect(dataCellTexts).toMatchSnapshot(); + }); +}); diff --git a/packages/s2-core/__tests__/bugs/issue-2385-spec.ts b/packages/s2-core/__tests__/bugs/issue-2385-spec.ts new file mode 100644 index 0000000000..592524fa35 --- /dev/null +++ b/packages/s2-core/__tests__/bugs/issue-2385-spec.ts @@ -0,0 +1,62 @@ +/** + * @description spec for issue #2385 + * https://github.com/antvis/S2/issues/2385 + */ +import { LayoutWidthType, type S2Options } from '../../src'; +import * as mockDataConfig from '../data/data-issue-2385.json'; +import { getContainer } from '../util/helpers'; +import { PivotSheet, TableSheet } from '@/sheet-type'; + +const s2Options: S2Options = { + width: 800, + height: 600, + style: { + dataCell: { + width: 200, + }, + layoutWidthType: LayoutWidthType.Compact, + }, +}; + +describe('Compare Layout Tests', () => { + test('should get max col width for pivot sheet', async () => { + const s2 = new PivotSheet(getContainer(), mockDataConfig, s2Options); + + s2.setTheme({ + dataCell: { + text: { + fontSize: 20, + }, + }, + }); + await s2.render(); + + const colLeafNodes = s2.facet.getColLeafNodes(); + + expect(Math.floor(colLeafNodes[0].width)).toBeCloseTo(189); + expect(Math.floor(colLeafNodes[1].width)).toEqual(90); + }); + + test('should get max col width for table sheet', async () => { + const s2 = new TableSheet(getContainer(), mockDataConfig, s2Options); + + s2.setDataCfg({ + fields: { + columns: ['price'], + }, + }); + s2.setTheme({ + dataCell: { + text: { + fontSize: 20, + }, + }, + }); + + await s2.render(); + + const colLeafNodes = s2.facet.getColLeafNodes(); + + expect(Math.floor(colLeafNodes[0].width)).toBeCloseTo(182); + }); +}); diff --git a/packages/s2-core/__tests__/bugs/issue-2501-spec.ts b/packages/s2-core/__tests__/bugs/issue-2501-spec.ts new file mode 100644 index 0000000000..143c76f1ad --- /dev/null +++ b/packages/s2-core/__tests__/bugs/issue-2501-spec.ts @@ -0,0 +1,63 @@ +/** + * @description spec for issue #2501 + * https://github.com/antvis/S2/issues/2501 + */ + +import type { TableFacet } from '../../src/facet'; +import * as mockDataConfig from '../data/simple-table-data.json'; +import { getContainer } from '../util/helpers'; +import type { SpreadSheet, S2DataConfig, S2Options } from '@/index'; +import { TableSheet } from '@/sheet-type'; + +const s2DataConfig: S2DataConfig = { + ...mockDataConfig, +}; + +const s2Options: S2Options = { + width: 800, + height: 400, + style: { + rowCell: { + heightByField: { + '0': 100, + '1': 150, + }, + }, + }, +}; + +describe('Table Sheet Row Offsets Tests', () => { + let s2: SpreadSheet; + + beforeEach(() => { + s2 = new TableSheet(getContainer(), s2DataConfig, s2Options); + + s2.render(); + }); + + test('should get correctly row offset data', () => { + expect((s2.facet as TableFacet).rowOffsets).toMatchInlineSnapshot(` + Array [ + 0, + 100, + 250, + 280, + ] + `); + }); + + test('should get correctly data cell offset for heightByField', () => { + const { getCellOffsetY, getTotalHeight } = s2.facet.getViewCellHeights(); + + expect(getCellOffsetY(0)).toEqual(0); + expect(getCellOffsetY(1)).toEqual(100); + expect(getCellOffsetY(2)).toEqual(250); + expect(getTotalHeight()).toEqual(280); + }); + + test('should get correctly row layout for heightByField', () => { + const { getTotalLength } = s2.facet.getViewCellHeights(); + + expect(getTotalLength()).toEqual(3); + }); +}); diff --git a/packages/s2-core/__tests__/bugs/issue-446-spec.ts b/packages/s2-core/__tests__/bugs/issue-446-spec.ts index 385d726105..5d8f283250 100644 --- a/packages/s2-core/__tests__/bugs/issue-446-spec.ts +++ b/packages/s2-core/__tests__/bugs/issue-446-spec.ts @@ -7,7 +7,7 @@ import { getContainer } from '../util/helpers'; import * as mockDataConfig from '../data/data-issue-446.json'; import { TableSheet } from '@/sheet-type'; -import { copyData } from '@/utils'; +import { asyncGetAllPlainData } from '@/utils'; const s2Options = { width: 800, @@ -20,7 +20,7 @@ describe('export', () => { const s2 = new TableSheet(getContainer(), mockDataConfig, s2Options); await s2.render(); - const data = copyData({ + const data = await asyncGetAllPlainData({ sheetInstance: s2, split: '\t', formatOptions: true, @@ -43,7 +43,7 @@ describe('export', () => { }); await s2.render(); - const data = copyData({ + const data = await asyncGetAllPlainData({ sheetInstance: s2, split: '\t', }); diff --git a/packages/s2-core/__tests__/bugs/issue-565-spec.ts b/packages/s2-core/__tests__/bugs/issue-565-spec.ts index d86f5100eb..2705af69a9 100644 --- a/packages/s2-core/__tests__/bugs/issue-565-spec.ts +++ b/packages/s2-core/__tests__/bugs/issue-565-spec.ts @@ -6,13 +6,14 @@ */ import * as mockDataConfig from 'tests/data/data-issue-565.json'; import { getContainer } from 'tests/util/helpers'; +import type { S2Options } from '../../src'; import { PivotSheet } from '@/sheet-type'; -import { copyData } from '@/utils'; +import { asyncGetAllPlainData } from '@/utils'; -const s2Options = { +const s2Options: S2Options = { width: 800, height: 600, - hierarchyType: 'tree' as const, + hierarchyType: 'tree', }; describe('Export data in pivot tree mode', () => { @@ -20,7 +21,8 @@ describe('Export data in pivot tree mode', () => { const s2 = new PivotSheet(getContainer(), mockDataConfig, s2Options); await s2.render(); - const data = copyData({ + + const data = await asyncGetAllPlainData({ sheetInstance: s2, split: '\t', }); @@ -32,16 +34,6 @@ describe('Export data in pivot tree mode', () => { expect(rows[1].split('\t')[0]).toEqual(''); expect(rows[7].split('\t')[0]).toEqual('row0'); expect(rows[8].split('\t')[0]).toEqual('row0'); - expect(data).toMatchInlineSnapshot(` - " col0 col0-0 col0-0 - col1 col1-0 col1-1 - col2 col2-0 col2-0 - row0 row1 row2 number number - row0 - row0 row1-0 - row0 row1-0 row2-0 3 - row0 row1-1 - row0 row1-1 row2-0 2 4" - `); + expect(data).toMatchSnapshot(); }); }); diff --git a/packages/s2-core/__tests__/bugs/issue-725-spec.ts b/packages/s2-core/__tests__/bugs/issue-725-spec.ts index 6d222d4230..ad503c135b 100644 --- a/packages/s2-core/__tests__/bugs/issue-725-spec.ts +++ b/packages/s2-core/__tests__/bugs/issue-725-spec.ts @@ -3,15 +3,13 @@ * https://github.com/antvis/S2/issues/725 * Wrong multi measure render * Wrong group sort - * */ import * as mockDataConfig from 'tests/data/data-issue-725.json'; import { assembleDataCfg } from '../util'; import type { S2DataConfig } from '@/common/interface'; -import { PivotSheet } from '@/sheet-type'; import { PivotDataSet } from '@/data-set'; -import { getDimensionsWithoutPathPre } from '@/utils/dataset/pivot-data-set'; +import { PivotSheet } from '@/sheet-type'; jest.mock('@/sheet-type'); @@ -39,18 +37,17 @@ describe('Group Sort When Have Same Child Measure', () => { }); test('should get correct group sort', () => { - expect( - getDimensionsWithoutPathPre(dataSet.getDimensionValues('type')), - ).toEqual(['办公用品', '家具产品', '家具产品', '办公用品']); - expect( - getDimensionsWithoutPathPre( - dataSet.getDimensionValues('type', { city: '白山' }), - ), - ).toEqual(['办公用品', '家具产品']); - expect( - getDimensionsWithoutPathPre( - dataSet.getDimensionValues('type', { city: '抚顺' }), - ), - ).toEqual(['家具产品', '办公用品']); + expect(dataSet.getDimensionValues('type')).toEqual([ + '办公用品', + '家具产品', + ]); + expect(dataSet.getDimensionValues('type', { city: '白山' })).toEqual([ + '办公用品', + '家具产品', + ]); + expect(dataSet.getDimensionValues('type', { city: '抚顺' })).toEqual([ + '家具产品', + '办公用品', + ]); }); }); diff --git a/packages/s2-core/__tests__/data/data-custom-trees.ts b/packages/s2-core/__tests__/data/data-custom-trees.ts new file mode 100644 index 0000000000..7e5a8832a2 --- /dev/null +++ b/packages/s2-core/__tests__/data/data-custom-trees.ts @@ -0,0 +1,22 @@ +export const dataCustomTrees = [ + { + type: '家具', + sub_type: '桌子', + 'measure-a': 1, + 'measure-b': 2, + 'measure-c': 3, + 'measure-d': 4, + 'measure-e': 5, + 'measure-f': 6, + }, + { + type: '家具', + sub_type: '椅子', + 'measure-a': 11, + 'measure-b': 22, + 'measure-c': 33, + 'measure-d': 44, + 'measure-e': 55, + 'measure-f': 66, + }, +]; diff --git a/packages/s2-core/__tests__/data/data-issue-2199.json b/packages/s2-core/__tests__/data/data-issue-2199.json new file mode 100644 index 0000000000..bb93e0401f --- /dev/null +++ b/packages/s2-core/__tests__/data/data-issue-2199.json @@ -0,0 +1,126 @@ +{ + "fields": { + "columns": [ + { + "field": "area", + "title": "位置", + "children": [ + { "field": "province", "title": "省份" }, + { "field": "city", "title": "城市" } + ] + }, + { + "field": "type", + "title": "商品类别" + }, + { + "field": "money", + "title": "金额", + "children": [ + { + "field": "price", + "title": "价格" + } + ] + } + ] + }, + "data": [ + { + "province": "浙江", + "city": "杭州", + "type": "笔", + "price": 1 + }, + { + "province": "浙江", + "city": "杭州", + "type": "纸张", + "price": 2 + }, + { + "province": "浙江", + "city": "舟山", + "type": "笔", + "price": 17 + }, + { + "province": "浙江", + "city": "舟山", + "type": "纸张", + "price": 6 + }, + { + "province": "吉林", + "city": "长春", + "type": "笔", + "price": 8 + }, + { + "province": "吉林", + "city": "白山", + "type": "笔", + "price": 12 + }, + { + "province": "吉林", + "city": "长春", + "type": "纸张", + "price": 3 + }, + { + "province": "吉林", + "city": "白山", + "type": "纸张", + "price": 25 + }, + { + "province": "浙江", + "city": "杭州", + "type": "笔", + "price": 20 + }, + { + "province": "浙江", + "city": "杭州", + "type": "纸张", + "price": 10 + }, + { + "province": "浙江", + "city": "舟山", + "type": "笔", + "price": 15 + }, + { + "province": "浙江", + "city": "舟山", + "type": "纸张", + "price": 2 + }, + { + "province": "吉林", + "city": "长春", + "type": "笔", + "price": 15 + }, + { + "province": "吉林", + "city": "白山", + "type": "笔", + "price": 30 + }, + { + "province": "吉林", + "city": "长春", + "type": "纸张", + "price": 40 + }, + { + "province": "吉林", + "city": "白山", + "type": "纸张", + "price": 50 + } + ] +} diff --git a/packages/s2-core/__tests__/data/data-issue-2322.json b/packages/s2-core/__tests__/data/data-issue-2322.json new file mode 100644 index 0000000000..9dde6f3fc4 --- /dev/null +++ b/packages/s2-core/__tests__/data/data-issue-2322.json @@ -0,0 +1,132 @@ +{ + "fields": { + "columns": [ + { + "field": "area", + "title": "区域", + "children": [ + { + "field": "province", + "title": "省份" + }, + { + "field": "city", + "title": "城市" + } + ] + }, + { + "field": "type", + "title": "商品类别" + }, + { + "field": "money", + "title": "金额", + "children": [ + { + "field": "price", + "title": "价格" + } + ] + } + ] + }, + "data": [ + { + "province": "浙江", + "city": "杭州", + "type": "笔", + "price": 1 + }, + { + "province": "浙江", + "city": "杭州", + "type": "纸张", + "price": 2 + }, + { + "province": "浙江", + "city": "舟山", + "type": "笔", + "price": 17 + }, + { + "province": "浙江", + "city": "舟山", + "type": "纸张", + "price": 6 + }, + { + "province": "吉林", + "city": "长春", + "type": "笔", + "price": 8 + }, + { + "province": "吉林", + "city": "白山", + "type": "笔", + "price": 12 + }, + { + "province": "吉林", + "city": "长春", + "type": "纸张", + "price": 3 + }, + { + "province": "吉林", + "city": "白山", + "type": "纸张", + "price": 25 + }, + { + "province": "浙江", + "city": "杭州", + "type": "笔", + "price": 20 + }, + { + "province": "浙江", + "city": "杭州", + "type": "纸张", + "price": 10 + }, + { + "province": "浙江", + "city": "舟山", + "type": "笔", + "price": 15 + }, + { + "province": "浙江", + "city": "舟山", + "type": "纸张", + "price": 2 + }, + { + "province": "吉林", + "city": "长春", + "type": "笔", + "price": 15 + }, + { + "province": "吉林", + "city": "白山", + "type": "笔", + "price": 30 + }, + { + "province": "吉林", + "city": "长春", + "type": "纸张", + "price": 40 + }, + { + "province": "吉林", + "city": "白山", + "type": "纸张", + "price": 50 + } + ] +} diff --git a/packages/s2-core/__tests__/data/data-issue-2385.json b/packages/s2-core/__tests__/data/data-issue-2385.json new file mode 100644 index 0000000000..d6f20caac5 --- /dev/null +++ b/packages/s2-core/__tests__/data/data-issue-2385.json @@ -0,0 +1,138 @@ +{ + "data": [ + { + "province": "浙江", + "city": "杭州", + "type": "笔", + "price": "11111111111111111", + "cost": "33.333" + }, + { + "province": "浙江", + "city": "杭州", + "type": "纸张", + "price": "2", + "cost": "1.5" + }, + { + "province": "浙江", + "city": "舟山", + "type": "笔", + "price": "2", + "cost": "1.5" + }, + { + "province": "浙江", + "city": "舟山", + "type": "纸张", + "price": "666.333", + "cost": "0.2" + }, + { + "province": "吉林", + "city": "长春", + "type": "笔", + "price": "3", + "cost": "2" + }, + { + "province": "吉林", + "city": "长春", + "type": "纸张", + "price": "2", + "cost": "1" + }, + { + "province": "吉林", + "city": "白山", + "type": "笔", + "price": "4", + "cost": "3" + }, + { + "province": "吉林", + "city": "白山", + "type": "纸张", + "price": "1", + "cost": "33.333" + }, + { + "price": "15.5", + "cost": "10.2" + }, + { + "province": "浙江", + "price": "5.5", + "cost": "3.7" + }, + { + "province": "浙江", + "city": "杭州", + "price": "3", + "cost": "2" + }, + { + "province": "浙江", + "city": "舟山", + "price": "2.5", + "cost": "1.7" + }, + { + "province": "吉林", + "price": "10", + "cost": "6.5" + }, + { + "province": "吉林", + "city": "长春", + "price": "5", + "cost": "3" + }, + { + "province": "吉林", + "city": "白山", + "price": "5", + "cost": "3.5" + }, + { + "type": "笔", + "price": "10", + "cost": "7" + }, + { + "type": "笔", + "province": "浙江", + "price": "3", + "cost": "2" + }, + { + "type": "笔", + "province": "吉林", + "price": "7", + "cost": "5" + }, + { + "type": "纸张", + "price": "5.5", + "cost": "3.2" + }, + { + "type": "纸张", + "province": "浙江", + "price": "2.5", + "cost": "1.7" + }, + { + "type": "纸张", + "province": "吉林", + "price": "3", + "cost": "1.5" + } + ], + "fields": { + "rows": ["province", "city"], + "columns": ["type"], + "values": ["price"], + "valueInCols": true + } +} diff --git a/packages/s2-core/__tests__/data/data-issue-292.json b/packages/s2-core/__tests__/data/data-issue-292.json index 8a387e8391..98ccfbf083 100644 --- a/packages/s2-core/__tests__/data/data-issue-292.json +++ b/packages/s2-core/__tests__/data/data-issue-292.json @@ -10,12 +10,7 @@ "province": "浙江", "city": "义乌", "type": "笔", - "cost": "2" - }, - { - "province": "浙江", - "city": "义乌", - "type": "笔", + "cost": "2", "price": "8" }, { diff --git a/packages/s2-core/__tests__/data/data-issue-725.json b/packages/s2-core/__tests__/data/data-issue-725.json index ad4d2a36dc..5510e53fd0 100644 --- a/packages/s2-core/__tests__/data/data-issue-725.json +++ b/packages/s2-core/__tests__/data/data-issue-725.json @@ -19,11 +19,6 @@ } ], "data": [ - { - "city": "白山", - "type": "办公用品", - "price": "" - }, { "city": "白山", "type": "办公用品", diff --git a/packages/s2-core/__tests__/data/sort-advanced.ts b/packages/s2-core/__tests__/data/sort-advanced.ts index bd067d3b47..e6068afe62 100644 --- a/packages/s2-core/__tests__/data/sort-advanced.ts +++ b/packages/s2-core/__tests__/data/sort-advanced.ts @@ -183,4 +183,5 @@ export const sortData = { price: '3', }, ], + totalData: [], }; diff --git a/packages/s2-core/__tests__/data/total-group-data.ts b/packages/s2-core/__tests__/data/total-group-data.ts new file mode 100644 index 0000000000..ec1c1a7e3c --- /dev/null +++ b/packages/s2-core/__tests__/data/total-group-data.ts @@ -0,0 +1,201 @@ +import { Aggregation, type S2DataConfig, type S2Options } from '@/common'; + +export const s2Options: S2Options = { + width: 800, + height: 600, + // 配置行小计总计显示,且按维度分组(列小计总计同理) + totals: { + row: { + showGrandTotals: true, + showSubTotals: true, + reverseGrandTotalsLayout: true, + reverseSubTotalsLayout: true, + subTotalsDimensions: ['province'], + calcGrandTotals: { + aggregation: Aggregation.SUM, + }, + calcSubTotals: { + aggregation: Aggregation.SUM, + }, + grandTotalsGroupDimensions: ['type'], + subTotalsGroupDimensions: ['type'], + }, + col: { + showGrandTotals: true, + showSubTotals: true, + reverseGrandTotalsLayout: true, + reverseSubTotalsLayout: true, + calcGrandTotals: { + aggregation: Aggregation.SUM, + }, + calcSubTotals: { + aggregation: Aggregation.SUM, + }, + grandTotalsGroupDimensions: ['sub_type'], + }, + }, +}; + +export const dataCfg: S2DataConfig = { + fields: { + rows: ['province', 'city', 'type'], + columns: ['sub_type'], + values: ['price', 'cost'], + }, + meta: [ + { + field: 'province', + name: '省份', + }, + { + field: 'city', + name: '城市', + }, + { + field: 'type', + name: '商品类别', + }, + { + field: 'sub_type', + name: '商品子类别', + }, + { + field: 'price', + name: '价格', + }, + { + field: 'cost', + name: '成本', + }, + ], + data: [ + { + price: 100, + cost: 100, + province: '浙江省', + city: '杭州市', + type: '家具', + sub_type: '桌子', + }, + { + price: 100, + cost: 100, + province: '浙江省', + city: '杭州市', + type: '家具', + sub_type: '沙发', + }, + { + price: 100, + cost: 100, + province: '浙江省', + city: '杭州市', + type: '办公用品', + sub_type: '笔', + }, + + { + price: 200, + cost: 200, + province: '浙江省', + city: '舟山市', + type: '家具', + sub_type: '桌子', + }, + + { + price: 200, + cost: 200, + province: '浙江省', + city: '舟山市', + type: '家具', + sub_type: '沙发', + }, + + { + price: 200, + cost: 200, + province: '浙江省', + city: '舟山市', + type: '办公用品', + sub_type: '笔', + }, + + { + price: 200, + cost: 200, + province: '浙江省', + city: '舟山市', + type: '办公用品', + sub_type: '纸张', + }, + { + price: 300, + cost: 300, + province: '四川省', + city: '成都市', + type: '家具', + sub_type: '桌子', + }, + { + price: 400, + cost: 400, + province: '四川省', + city: '绵阳市', + type: '家具', + sub_type: '桌子', + }, + + { + price: 300, + cost: 300, + province: '四川省', + city: '成都市', + type: '家具', + sub_type: '沙发', + }, + { + price: 400, + cost: 400, + province: '四川省', + city: '绵阳市', + type: '家具', + sub_type: '沙发', + }, + + { + price: 300, + cost: 300, + province: '四川省', + city: '成都市', + type: '办公用品', + sub_type: '笔', + }, + { + price: 400, + cost: 400, + province: '四川省', + city: '绵阳市', + type: '办公用品', + sub_type: '笔', + }, + + { + price: 300, + cost: 300, + province: '四川省', + city: '成都市', + type: '办公用品', + sub_type: '纸张', + }, + { + price: 400, + cost: 400, + province: '四川省', + city: '绵阳市', + type: '办公用品', + sub_type: '纸张', + }, + ], + totalData: [], +}; diff --git a/packages/s2-core/__tests__/spreadsheet/__snapshots__/corner-spec.ts.snap b/packages/s2-core/__tests__/spreadsheet/__snapshots__/corner-spec.ts.snap index e906a49ea1..8bb717bbfc 100644 --- a/packages/s2-core/__tests__/spreadsheet/__snapshots__/corner-spec.ts.snap +++ b/packages/s2-core/__tests__/spreadsheet/__snapshots__/corner-spec.ts.snap @@ -11,7 +11,7 @@ Array [ "field": "province", "height": 30, "hierarchy": undefined, - "id": "", + "id": "province", "inCollapseNode": undefined, "isCollapsed": undefined, "isGrandTotals": undefined, @@ -19,6 +19,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "level": undefined, "padding": 0, @@ -28,7 +29,7 @@ Array [ "seriesNumberWidth": undefined, "spreadsheet": Anything, "value": "province", - "width": 99, + "width": 99.33, "x": 0, "y": 0, }, @@ -41,7 +42,7 @@ Array [ "field": "city", "height": 30, "hierarchy": undefined, - "id": "", + "id": "city", "inCollapseNode": undefined, "isCollapsed": undefined, "isGrandTotals": undefined, @@ -49,6 +50,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "level": undefined, "padding": 0, @@ -58,8 +60,8 @@ Array [ "seriesNumberWidth": undefined, "spreadsheet": Anything, "value": "city", - "width": 99, - "x": 99, + "width": 99.33, + "x": 99.33, "y": 0, }, ] @@ -84,6 +86,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "level": undefined, "padding": 0, @@ -106,7 +109,7 @@ Array [ "field": "province", "height": 30, "hierarchy": undefined, - "id": "", + "id": "province", "inCollapseNode": undefined, "isCollapsed": undefined, "isGrandTotals": undefined, @@ -114,6 +117,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "level": undefined, "padding": 0, @@ -136,7 +140,7 @@ Array [ "field": "city", "height": 30, "hierarchy": undefined, - "id": "", + "id": "city", "inCollapseNode": undefined, "isCollapsed": undefined, "isGrandTotals": undefined, @@ -144,6 +148,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "level": undefined, "padding": 0, @@ -171,7 +176,7 @@ Array [ "field": "", "height": 30, "hierarchy": undefined, - "id": "", + "id": "province/city/数值", "inCollapseNode": undefined, "isCollapsed": undefined, "isGrandTotals": undefined, @@ -179,6 +184,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "level": undefined, "padding": 0, @@ -188,7 +194,7 @@ Array [ "seriesNumberWidth": 0, "spreadsheet": Anything, "value": "province/city/数值", - "width": 145, + "width": 145.36, "x": 0, "y": 0, }, @@ -214,6 +220,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "level": undefined, "padding": 0, @@ -236,7 +243,7 @@ Array [ "field": "", "height": 30, "hierarchy": undefined, - "id": "", + "id": "province/city/数值", "inCollapseNode": undefined, "isCollapsed": undefined, "isGrandTotals": undefined, @@ -244,6 +251,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "level": undefined, "padding": 0, @@ -253,7 +261,7 @@ Array [ "seriesNumberWidth": 80, "spreadsheet": Anything, "value": "province/city/数值", - "width": 145, + "width": 145.36, "x": 80, "y": 0, }, @@ -271,7 +279,7 @@ Array [ "field": "province", "height": 30, "hierarchy": undefined, - "id": "", + "id": "province", "inCollapseNode": undefined, "isCollapsed": undefined, "isGrandTotals": undefined, @@ -279,6 +287,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "level": undefined, "padding": 0, @@ -301,7 +310,7 @@ Array [ "field": "city", "height": 30, "hierarchy": undefined, - "id": "", + "id": "city", "inCollapseNode": undefined, "isCollapsed": undefined, "isGrandTotals": undefined, @@ -309,6 +318,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "level": undefined, "padding": 0, @@ -344,6 +354,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "level": undefined, "padding": 0, @@ -366,7 +377,7 @@ Array [ "field": "province", "height": 30, "hierarchy": undefined, - "id": "", + "id": "province", "inCollapseNode": undefined, "isCollapsed": undefined, "isGrandTotals": undefined, @@ -374,6 +385,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "level": undefined, "padding": 0, @@ -396,7 +408,7 @@ Array [ "field": "city", "height": 30, "hierarchy": undefined, - "id": "", + "id": "city", "inCollapseNode": undefined, "isCollapsed": undefined, "isGrandTotals": undefined, @@ -404,6 +416,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "level": undefined, "padding": 0, @@ -431,7 +444,7 @@ Array [ "field": "", "height": 30, "hierarchy": undefined, - "id": "", + "id": "province/city", "inCollapseNode": undefined, "isCollapsed": undefined, "isGrandTotals": undefined, @@ -439,6 +452,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "level": undefined, "padding": 0, @@ -474,6 +488,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "level": undefined, "padding": 0, @@ -496,7 +511,7 @@ Array [ "field": "", "height": 30, "hierarchy": undefined, - "id": "", + "id": "province/city", "inCollapseNode": undefined, "isCollapsed": undefined, "isGrandTotals": undefined, @@ -504,6 +519,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "level": undefined, "padding": 0, @@ -531,7 +547,7 @@ Array [ "field": "province", "height": 30, "hierarchy": undefined, - "id": "", + "id": "province", "inCollapseNode": undefined, "isCollapsed": undefined, "isGrandTotals": undefined, @@ -539,6 +555,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "level": undefined, "padding": 0, @@ -548,7 +565,7 @@ Array [ "seriesNumberWidth": undefined, "spreadsheet": Anything, "value": "province", - "width": 99, + "width": 99.33, "x": 0, "y": 0, }, @@ -561,7 +578,7 @@ Array [ "field": "city", "height": 30, "hierarchy": undefined, - "id": "", + "id": "city", "inCollapseNode": undefined, "isCollapsed": undefined, "isGrandTotals": undefined, @@ -569,6 +586,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "level": undefined, "padding": 0, @@ -578,8 +596,8 @@ Array [ "seriesNumberWidth": undefined, "spreadsheet": Anything, "value": "city", - "width": 99, - "x": 99, + "width": 99.33, + "x": 99.33, "y": 0, }, ] @@ -604,6 +622,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "level": undefined, "padding": 0, @@ -626,7 +645,7 @@ Array [ "field": "province", "height": 30, "hierarchy": undefined, - "id": "", + "id": "province", "inCollapseNode": undefined, "isCollapsed": undefined, "isGrandTotals": undefined, @@ -634,6 +653,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "level": undefined, "padding": 0, @@ -656,7 +676,7 @@ Array [ "field": "city", "height": 30, "hierarchy": undefined, - "id": "", + "id": "city", "inCollapseNode": undefined, "isCollapsed": undefined, "isGrandTotals": undefined, @@ -664,6 +684,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "level": undefined, "padding": 0, @@ -691,7 +712,7 @@ Array [ "field": "", "height": 30, "hierarchy": undefined, - "id": "", + "id": "province/city", "inCollapseNode": undefined, "isCollapsed": undefined, "isGrandTotals": undefined, @@ -699,6 +720,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "level": undefined, "padding": 0, @@ -734,6 +756,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "level": undefined, "padding": 0, @@ -756,7 +779,7 @@ Array [ "field": "", "height": 30, "hierarchy": undefined, - "id": "", + "id": "province/city", "inCollapseNode": undefined, "isCollapsed": undefined, "isGrandTotals": undefined, @@ -764,6 +787,7 @@ Array [ "isPivotMode": true, "isSubTotals": undefined, "isTotalMeasure": undefined, + "isTotalRoot": undefined, "isTotals": undefined, "level": undefined, "padding": 0, diff --git a/packages/s2-core/__tests__/spreadsheet/__snapshots__/custom-cell-style-spec.ts.snap b/packages/s2-core/__tests__/spreadsheet/__snapshots__/custom-cell-style-spec.ts.snap index 2baf34699e..14478ee83c 100644 --- a/packages/s2-core/__tests__/spreadsheet/__snapshots__/custom-cell-style-spec.ts.snap +++ b/packages/s2-core/__tests__/spreadsheet/__snapshots__/custom-cell-style-spec.ts.snap @@ -45,17 +45,17 @@ Array [ Object { "height": 30, "id": "root[&]笔", - "width": 300, + "width": 299, }, Object { "height": 30, "id": "root[&]笔[&]price", - "width": 150, + "width": 149.5, }, Object { "height": 30, "id": "root[&]笔[&]cost", - "width": 150, + "width": 149.5, }, ] `; @@ -65,17 +65,17 @@ Array [ Object { "height": 60, "id": "root[&]浙江", - "width": 149, + "width": 149.5, }, Object { "height": 30, "id": "root[&]浙江[&]义乌", - "width": 149, + "width": 149.5, }, Object { "height": 30, "id": "root[&]浙江[&]杭州", - "width": 149, + "width": 149.5, }, ] `; @@ -140,7 +140,7 @@ Array [ Object { "height": 66, "id": "root[&]浙江", - "width": 149, + "width": 149.5, }, Object { "height": 33, @@ -160,17 +160,17 @@ Array [ Object { "height": 330, "id": "root[&]浙江", - "width": 149, + "width": 149.5, }, Object { "height": 30, "id": "root[&]浙江[&]义乌", - "width": 149, + "width": 149.5, }, Object { "height": 300, "id": "root[&]浙江[&]杭州", - "width": 149, + "width": 149.5, }, ] `; @@ -180,17 +180,17 @@ Array [ Object { "height": 60, "id": "root[&]浙江", - "width": 149, + "width": 149.5, }, Object { "height": 30, "id": "root[&]浙江[&]义乌", - "width": 149, + "width": 149.5, }, Object { "height": 30, "id": "root[&]浙江[&]杭州", - "width": 149, + "width": 149.5, }, ] `; @@ -200,17 +200,17 @@ Array [ Object { "height": 30, "id": "root[&]笔", - "width": 300, + "width": 299, }, Object { "height": 30, "id": "root[&]笔[&]price", - "width": 150, + "width": 149.5, }, Object { "height": 30, "id": "root[&]笔[&]cost", - "width": 150, + "width": 149.5, }, ] `; @@ -220,22 +220,22 @@ Array [ Object { "height": 30, "id": "root[&]浙江[&]义乌-root[&]笔[&]price", - "width": 150, + "width": 149.5, }, Object { "height": 30, "id": "root[&]浙江[&]杭州-root[&]笔[&]price", - "width": 150, + "width": 149.5, }, Object { "height": 30, "id": "root[&]浙江[&]义乌-root[&]笔[&]cost", - "width": 150, + "width": 149.5, }, Object { "height": 30, "id": "root[&]浙江[&]杭州-root[&]笔[&]cost", - "width": 150, + "width": 149.5, }, ] `; @@ -247,12 +247,12 @@ Array [ Object { "height": 30, "id": "root[&]类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "root[&]子类别", - "width": 299, + "width": 299.5, }, ] `; @@ -262,132 +262,132 @@ Array [ Object { "height": 30, "id": "0-root[&]类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "1-root[&]类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "2-root[&]类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "3-root[&]类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "4-root[&]类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "5-root[&]类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "6-root[&]类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "7-root[&]类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "8-root[&]类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "9-root[&]类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "10-root[&]类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "11-root[&]类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "12-root[&]类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "0-root[&]子类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "1-root[&]子类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "2-root[&]子类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "3-root[&]子类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "4-root[&]子类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "5-root[&]子类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "6-root[&]子类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "7-root[&]子类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "8-root[&]子类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "9-root[&]子类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "10-root[&]子类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "11-root[&]子类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "12-root[&]子类别", - "width": 299, + "width": 299.5, }, ] `; @@ -427,7 +427,7 @@ Array [ Object { "height": 60, "id": "root[&]类型", - "width": 119, + "width": 119.8, }, Object { "height": 30, @@ -482,72 +482,72 @@ Array [ Object { "height": 60, "id": "0-root[&]类别", - "width": 299, + "width": 299.5, }, Object { "height": 60, "id": "1-root[&]类别", - "width": 299, + "width": 299.5, }, Object { "height": 60, "id": "2-root[&]类别", - "width": 299, + "width": 299.5, }, Object { "height": 60, "id": "3-root[&]类别", - "width": 299, + "width": 299.5, }, Object { "height": 60, "id": "4-root[&]类别", - "width": 299, + "width": 299.5, }, Object { "height": 60, "id": "5-root[&]类别", - "width": 299, + "width": 299.5, }, Object { "height": 60, "id": "6-root[&]类别", - "width": 299, + "width": 299.5, }, Object { "height": 60, "id": "0-root[&]子类别", - "width": 299, + "width": 299.5, }, Object { "height": 60, "id": "1-root[&]子类别", - "width": 299, + "width": 299.5, }, Object { "height": 60, "id": "2-root[&]子类别", - "width": 299, + "width": 299.5, }, Object { "height": 60, "id": "3-root[&]子类别", - "width": 299, + "width": 299.5, }, Object { "height": 60, "id": "4-root[&]子类别", - "width": 299, + "width": 299.5, }, Object { "height": 60, "id": "5-root[&]子类别", - "width": 299, + "width": 299.5, }, Object { "height": 60, "id": "6-root[&]子类别", - "width": 299, + "width": 299.5, }, ] `; @@ -557,102 +557,102 @@ Array [ Object { "height": 40, "id": "0-root[&]类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "1-root[&]类别", - "width": 299, + "width": 299.5, }, Object { "height": 100, "id": "2-root[&]类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "3-root[&]类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "4-root[&]类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "5-root[&]类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "6-root[&]类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "7-root[&]类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "8-root[&]类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "9-root[&]类别", - "width": 299, + "width": 299.5, }, Object { "height": 40, "id": "0-root[&]子类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "1-root[&]子类别", - "width": 299, + "width": 299.5, }, Object { "height": 100, "id": "2-root[&]子类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "3-root[&]子类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "4-root[&]子类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "5-root[&]子类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "6-root[&]子类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "7-root[&]子类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "8-root[&]子类别", - "width": 299, + "width": 299.5, }, Object { "height": 30, "id": "9-root[&]子类别", - "width": 299, + "width": 299.5, }, ] `; diff --git a/packages/s2-core/__tests__/spreadsheet/__snapshots__/custom-grid-spec.ts.snap b/packages/s2-core/__tests__/spreadsheet/__snapshots__/custom-grid-spec.ts.snap index 4e93984e3d..63872b5a1c 100644 --- a/packages/s2-core/__tests__/spreadsheet/__snapshots__/custom-grid-spec.ts.snap +++ b/packages/s2-core/__tests__/spreadsheet/__snapshots__/custom-grid-spec.ts.snap @@ -253,7 +253,7 @@ Array [ "field": "a-1", "height": 30, "value": "自定义节点 a-1", - "width": 300, + "width": 299.01, "x": 0, "y": 0, }, @@ -262,7 +262,7 @@ Array [ "field": "a-1-1", "height": 30, "value": "自定义节点 a-1-1", - "width": 200, + "width": 199.34, "x": 0, "y": 30, }, @@ -271,7 +271,7 @@ Array [ "field": "measure-1", "height": 30, "value": "指标1", - "width": 100, + "width": 99.67, "x": 0, "y": 60, }, @@ -280,8 +280,8 @@ Array [ "field": "measure-2", "height": 30, "value": "指标2", - "width": 100, - "x": 100, + "width": 99.67, + "x": 99.67, "y": 60, }, Object { @@ -289,8 +289,8 @@ Array [ "field": "a-1-2", "height": 60, "value": "自定义节点 a-1-2", - "width": 100, - "x": 200, + "width": 99.67, + "x": 199.34, "y": 30, }, Object { @@ -298,8 +298,8 @@ Array [ "field": "a-2", "height": 90, "value": "自定义节点 a-2", - "width": 100, - "x": 300, + "width": 99.67, + "x": 299.01, "y": 0, }, ] @@ -309,19 +309,19 @@ exports[`SpreadSheet Custom Grid Tests Custom Row Grid Tests should calc correct Array [ Object { "field": "measure-1", - "width": 119, + "width": 119.6, }, Object { "field": "measure-2", - "width": 119, + "width": 119.6, }, Object { "field": "a-1-2", - "width": 159, + "width": 159.6, }, Object { "field": "a-2", - "width": 278, + "width": 279.2, }, ] `; @@ -330,11 +330,11 @@ exports[`SpreadSheet Custom Grid Tests Custom Row Grid Tests should calc correct Array [ Object { "field": "sub_type", - "width": 160, + "width": 159.4, }, Object { "field": "sub_type", - "width": 160, + "width": 159.4, }, ] `; @@ -435,37 +435,37 @@ Array [ "description": "a-1 描述", "height": 90, "value": "自定义节点 a-1", - "width": 119, + "width": 119.6, }, Object { "description": "a-1-1 描述", "height": 60, "value": "自定义节点 a-1-1", - "width": 119, + "width": 119.6, }, Object { "description": "指标1描述", "height": 30, "value": "指标1", - "width": 119, + "width": 119.6, }, Object { "description": "指标2描述", "height": 30, "value": "指标2", - "width": 119, + "width": 119.6, }, Object { "description": "a-1-2 描述", "height": 30, "value": "自定义节点 a-1-2", - "width": 238, + "width": 239.2, }, Object { "description": "a-2 描述", "height": 30, "value": "自定义节点 a-2", - "width": 357, + "width": 358.79999999999995, }, ] `; diff --git a/packages/s2-core/__tests__/spreadsheet/__snapshots__/custom-table-col-spec.ts.snap b/packages/s2-core/__tests__/spreadsheet/__snapshots__/custom-table-col-spec.ts.snap index e607dd4387..e7310e8255 100644 --- a/packages/s2-core/__tests__/spreadsheet/__snapshots__/custom-table-col-spec.ts.snap +++ b/packages/s2-core/__tests__/spreadsheet/__snapshots__/custom-table-col-spec.ts.snap @@ -169,43 +169,43 @@ Array [ "description": undefined, "height": 30, "value": "地区", - "width": 238, + "width": 239.6, }, Object { "description": undefined, "height": 30, "value": "省份", - "width": 119, + "width": 119.8, }, Object { "description": undefined, "height": 30, "value": "城市", - "width": 119, + "width": 119.8, }, Object { "description": undefined, "height": 60, "value": "类型", - "width": 119, + "width": 119.8, }, Object { "description": undefined, "height": 30, "value": "金额", - "width": 238, + "width": 239.6, }, Object { "description": "价格描述", "height": 30, "value": "价格", - "width": 119, + "width": 119.8, }, Object { "description": undefined, "height": 30, "value": "数量", - "width": 119, + "width": 119.8, }, ] `; @@ -216,37 +216,37 @@ Array [ "description": "a-1 描述", "height": 30, "value": "自定义节点 a-1", - "width": 447, + "width": 449.25, }, Object { "description": "a-1-1 描述", "height": 30, "value": "自定义节点 a-1-1", - "width": 298, + "width": 299.5, }, Object { "description": "指标1描述", "height": 30, "value": "指标1", - "width": 149, + "width": 149.75, }, Object { "description": "指标2描述", "height": 30, "value": "指标2", - "width": 149, + "width": 149.75, }, Object { "description": "a-1-2 描述", "height": 60, "value": "自定义节点 a-1-2", - "width": 149, + "width": 149.75, }, Object { "description": "a-2 描述", "height": 90, "value": "自定义节点 a-2", - "width": 149, + "width": 149.75, }, ] `; diff --git a/packages/s2-core/__tests__/spreadsheet/__snapshots__/hidden-columns-spec.ts.snap b/packages/s2-core/__tests__/spreadsheet/__snapshots__/hidden-columns-spec.ts.snap new file mode 100644 index 0000000000..90c2f63ff0 --- /dev/null +++ b/packages/s2-core/__tests__/spreadsheet/__snapshots__/hidden-columns-spec.ts.snap @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SpreadSheet Hidden Columns Tests PivotSheet Multiple Values Tests should render correct row corner after hide measure node 1`] = ` +Array [ + Object { + "height": 30, + "width": 119.6, + "x": 0, + "y": 60, + }, + Object { + "height": 30, + "width": 119.6, + "x": 119.6, + "y": 60, + }, + Object { + "height": 30, + "width": 239.2, + "x": 0, + "y": 0, + }, + Object { + "height": 30, + "width": 239.2, + "x": 0, + "y": 30, + }, +] +`; diff --git a/packages/s2-core/__tests__/spreadsheet/__snapshots__/multi-line-text-spec.ts.snap b/packages/s2-core/__tests__/spreadsheet/__snapshots__/multi-line-text-spec.ts.snap index 5b04f4dac4..f9206280f8 100644 --- a/packages/s2-core/__tests__/spreadsheet/__snapshots__/multi-line-text-spec.ts.snap +++ b/packages/s2-core/__tests__/spreadsheet/__snapshots__/multi-line-text-spec.ts.snap @@ -2428,12 +2428,12 @@ Array [ "width": 96, }, Object { - "actualText": "城我是省略号", + "actualText": "城市城@@@", "actualTextHeight": 16, - "actualTextWidth": 73, + "actualTextWidth": 72, "height": 30, "multiLineActualTexts": Array [ - "城我是省略号", + "城市城@@@", ], "originalText": "城市城市城市城市城市城市城市城市城市城市城市城市", "width": 96, @@ -2977,7 +2977,7 @@ Array [ "省份", ], "originalText": "省份", - "width": 103, + "width": 103.8, }, Object { "actualText": "城市城市城市城市城市城市城市城市城市城市...", @@ -2990,7 +2990,7 @@ Array [ "城市城市城市...", ], "originalText": "城市城市城市城市城市城市城市城市城市城市城市城市", - "width": 103, + "width": 103.8, }, Object { "actualText": "类别类别类别类别类别类别类别类别类别类别", @@ -3003,7 +3003,7 @@ Array [ "类别类别类别", ], "originalText": "类别类别类别类别类别类别类别类别类别类别", - "width": 103, + "width": 103.8, }, Object { "actualText": "子类别", @@ -3014,7 +3014,7 @@ Array [ "子类别", ], "originalText": "子类别", - "width": 103, + "width": 103.8, }, Object { "actualText": "数量数量数量数量数量数量数量数量数量数量...", @@ -3027,7 +3027,7 @@ Array [ "数量数量数量...", ], "originalText": "数量数量数量数量数量数量数量数量数量数量数量", - "width": 103, + "width": 103.8, }, ] `; @@ -3177,7 +3177,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省浙江省浙江省浙江省浙江省浙江省浙江...", @@ -3190,7 +3190,7 @@ Array [ "省浙江省浙江...", ], "originalText": "浙江省浙江省浙江省浙江省浙江省浙江省浙江省浙江省浙江省浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -3201,7 +3201,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -3212,7 +3212,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -3223,7 +3223,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -3234,7 +3234,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -3245,7 +3245,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -3256,7 +3256,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -3267,7 +3267,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -3278,7 +3278,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -3289,7 +3289,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -3300,7 +3300,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "绍兴市", @@ -3311,7 +3311,7 @@ Array [ "绍兴市", ], "originalText": "绍兴市", - "width": 103, + "width": 103.8, }, Object { "actualText": "杭州市杭州市杭州市杭州市杭州市杭州市杭州...", @@ -3324,7 +3324,7 @@ Array [ "市杭州市杭州...", ], "originalText": "杭州市杭州市杭州市杭州市杭州市杭州市杭州市杭州市杭州市杭州市", - "width": 103, + "width": 103.8, }, Object { "actualText": "绍兴市", @@ -3335,7 +3335,7 @@ Array [ "绍兴市", ], "originalText": "绍兴市", - "width": 103, + "width": 103.8, }, Object { "actualText": "宁波市", @@ -3346,7 +3346,7 @@ Array [ "宁波市", ], "originalText": "宁波市", - "width": 103, + "width": 103.8, }, Object { "actualText": "舟山市", @@ -3357,7 +3357,7 @@ Array [ "舟山市", ], "originalText": "舟山市", - "width": 103, + "width": 103.8, }, Object { "actualText": "杭州市", @@ -3368,7 +3368,7 @@ Array [ "杭州市", ], "originalText": "杭州市", - "width": 103, + "width": 103.8, }, Object { "actualText": "绍兴市", @@ -3379,7 +3379,7 @@ Array [ "绍兴市", ], "originalText": "绍兴市", - "width": 103, + "width": 103.8, }, Object { "actualText": "宁波市", @@ -3390,7 +3390,7 @@ Array [ "宁波市", ], "originalText": "宁波市", - "width": 103, + "width": 103.8, }, Object { "actualText": "舟山市", @@ -3401,7 +3401,7 @@ Array [ "舟山市", ], "originalText": "舟山市", - "width": 103, + "width": 103.8, }, Object { "actualText": "杭州市", @@ -3412,7 +3412,7 @@ Array [ "杭州市", ], "originalText": "杭州市", - "width": 103, + "width": 103.8, }, Object { "actualText": "绍兴市", @@ -3423,7 +3423,7 @@ Array [ "绍兴市", ], "originalText": "绍兴市", - "width": 103, + "width": 103.8, }, Object { "actualText": "宁波市", @@ -3434,7 +3434,7 @@ Array [ "宁波市", ], "originalText": "宁波市", - "width": 103, + "width": 103.8, }, Object { "actualText": "家具", @@ -3445,7 +3445,7 @@ Array [ "家具", ], "originalText": "家具", - "width": 103, + "width": 103.8, }, Object { "actualText": "家具家具家具家具家具家具家具家具家具家具...", @@ -3458,7 +3458,7 @@ Array [ "家具家具家具...", ], "originalText": "家具家具家具家具家具家具家具家具家具家具家具家具家具家具", - "width": 103, + "width": 103.8, }, Object { "actualText": "家具", @@ -3469,7 +3469,7 @@ Array [ "家具", ], "originalText": "家具", - "width": 103, + "width": 103.8, }, Object { "actualText": "家具", @@ -3480,7 +3480,7 @@ Array [ "家具", ], "originalText": "家具", - "width": 103, + "width": 103.8, }, Object { "actualText": "家具", @@ -3491,7 +3491,7 @@ Array [ "家具", ], "originalText": "家具", - "width": 103, + "width": 103.8, }, Object { "actualText": "家具", @@ -3502,7 +3502,7 @@ Array [ "家具", ], "originalText": "家具", - "width": 103, + "width": 103.8, }, Object { "actualText": "家具", @@ -3513,7 +3513,7 @@ Array [ "家具", ], "originalText": "家具", - "width": 103, + "width": 103.8, }, Object { "actualText": "家具", @@ -3524,7 +3524,7 @@ Array [ "家具", ], "originalText": "家具", - "width": 103, + "width": 103.8, }, Object { "actualText": "家具", @@ -3535,7 +3535,7 @@ Array [ "家具", ], "originalText": "家具", - "width": 103, + "width": 103.8, }, Object { "actualText": "办公用品", @@ -3546,7 +3546,7 @@ Array [ "办公用品", ], "originalText": "办公用品", - "width": 103, + "width": 103.8, }, Object { "actualText": "办公用品", @@ -3557,7 +3557,7 @@ Array [ "办公用品", ], "originalText": "办公用品", - "width": 103, + "width": 103.8, }, Object { "actualText": "办公用品", @@ -3568,7 +3568,7 @@ Array [ "办公用品", ], "originalText": "办公用品", - "width": 103, + "width": 103.8, }, Object { "actualText": "桌子", @@ -3579,7 +3579,7 @@ Array [ "桌子", ], "originalText": "桌子", - "width": 103, + "width": 103.8, }, Object { "actualText": "桌子桌子桌子桌子桌子桌子桌子桌子桌子桌子...", @@ -3592,7 +3592,7 @@ Array [ "桌子桌子桌子...", ], "originalText": "桌子桌子桌子桌子桌子桌子桌子桌子桌子桌子桌子桌子桌子桌子桌子", - "width": 103, + "width": 103.8, }, Object { "actualText": "桌子", @@ -3603,7 +3603,7 @@ Array [ "桌子", ], "originalText": "桌子", - "width": 103, + "width": 103.8, }, Object { "actualText": "桌子", @@ -3614,7 +3614,7 @@ Array [ "桌子", ], "originalText": "桌子", - "width": 103, + "width": 103.8, }, Object { "actualText": "桌子", @@ -3625,7 +3625,7 @@ Array [ "桌子", ], "originalText": "桌子", - "width": 103, + "width": 103.8, }, Object { "actualText": "沙发", @@ -3636,7 +3636,7 @@ Array [ "沙发", ], "originalText": "沙发", - "width": 103, + "width": 103.8, }, Object { "actualText": "沙发", @@ -3647,7 +3647,7 @@ Array [ "沙发", ], "originalText": "沙发", - "width": 103, + "width": 103.8, }, Object { "actualText": "沙发", @@ -3658,7 +3658,7 @@ Array [ "沙发", ], "originalText": "沙发", - "width": 103, + "width": 103.8, }, Object { "actualText": "沙发", @@ -3669,7 +3669,7 @@ Array [ "沙发", ], "originalText": "沙发", - "width": 103, + "width": 103.8, }, Object { "actualText": "笔", @@ -3680,7 +3680,7 @@ Array [ "笔", ], "originalText": "笔", - "width": 103, + "width": 103.8, }, Object { "actualText": "笔", @@ -3691,7 +3691,7 @@ Array [ "笔", ], "originalText": "笔", - "width": 103, + "width": 103.8, }, Object { "actualText": "笔", @@ -3702,7 +3702,7 @@ Array [ "笔", ], "originalText": "笔", - "width": 103, + "width": 103.8, }, Object { "actualText": "236723672361111", @@ -3710,11 +3710,11 @@ Array [ "actualTextWidth": 100, "height": 30, "multiLineActualTexts": Array [ - "236723672361", - "111", + "2367236723611", + "11", ], "originalText": "236723672361111", - "width": 103, + "width": 103.8, }, Object { "actualText": "7789778977897789778977897789", @@ -3722,12 +3722,12 @@ Array [ "actualTextWidth": 189, "height": 30, "multiLineActualTexts": Array [ - "778977897789", - "778977897789", - "7789", + "7789778977897", + "7897789778977", + "89", ], "originalText": "7789778977897789778977897789", - "width": 103, + "width": 103.8, }, Object { "actualText": "236723672361111", @@ -3735,11 +3735,11 @@ Array [ "actualTextWidth": 100, "height": 30, "multiLineActualTexts": Array [ - "236723672361", - "111", + "2367236723611", + "11", ], "originalText": "236723672361111", - "width": 103, + "width": 103.8, }, Object { "actualText": "3877", @@ -3750,7 +3750,7 @@ Array [ "3877", ], "originalText": "3877", - "width": 103, + "width": 103.8, }, Object { "actualText": "4342", @@ -3761,7 +3761,7 @@ Array [ "4342", ], "originalText": "4342", - "width": 103, + "width": 103.8, }, Object { "actualText": "5343", @@ -3772,7 +3772,7 @@ Array [ "5343", ], "originalText": "5343", - "width": 103, + "width": 103.8, }, Object { "actualText": "632", @@ -3783,7 +3783,7 @@ Array [ "632", ], "originalText": "632", - "width": 103, + "width": 103.8, }, Object { "actualText": "7234", @@ -3794,7 +3794,7 @@ Array [ "7234", ], "originalText": "7234", - "width": 103, + "width": 103.8, }, Object { "actualText": "834", @@ -3805,7 +3805,7 @@ Array [ "834", ], "originalText": "834", - "width": 103, + "width": 103.8, }, Object { "actualText": "945", @@ -3816,7 +3816,7 @@ Array [ "945", ], "originalText": "945", - "width": 103, + "width": 103.8, }, Object { "actualText": "1304", @@ -3827,7 +3827,7 @@ Array [ "1304", ], "originalText": "1304", - "width": 103, + "width": 103.8, }, Object { "actualText": "1145", @@ -3838,7 +3838,7 @@ Array [ "1145", ], "originalText": "1145", - "width": 103, + "width": 103.8, }, ] `; @@ -4018,7 +4018,7 @@ Array [ "省份", ], "originalText": "省份", - "width": 103, + "width": 103.8, }, Object { "actualText": "城市城市城市城市城市城市城...", @@ -4030,7 +4030,7 @@ Array [ "市城市城市城...", ], "originalText": "城市城市城市城市城市城市城市城市城市城市城市城市", - "width": 103, + "width": 103.8, }, Object { "actualText": "类别类别类别类别类别类别类...", @@ -4042,7 +4042,7 @@ Array [ "别类别类别类...", ], "originalText": "类别类别类别类别类别类别类别类别类别类别", - "width": 103, + "width": 103.8, }, Object { "actualText": "子类别", @@ -4053,7 +4053,7 @@ Array [ "子类别", ], "originalText": "子类别", - "width": 103, + "width": 103.8, }, Object { "actualText": "数量数量数量数量数量数量数...", @@ -4065,7 +4065,7 @@ Array [ "量数量数量数...", ], "originalText": "数量数量数量数量数量数量数量数量数量数量数量", - "width": 103, + "width": 103.8, }, ] `; @@ -4215,7 +4215,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省浙江省浙江省浙江省浙...", @@ -4227,7 +4227,7 @@ Array [ "江省浙江省浙...", ], "originalText": "浙江省浙江省浙江省浙江省浙江省浙江省浙江省浙江省浙江省浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -4238,7 +4238,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -4249,7 +4249,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -4260,7 +4260,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -4271,7 +4271,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -4282,7 +4282,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -4293,7 +4293,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -4304,7 +4304,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -4315,7 +4315,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -4326,7 +4326,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -4337,7 +4337,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "绍兴市", @@ -4348,7 +4348,7 @@ Array [ "绍兴市", ], "originalText": "绍兴市", - "width": 103, + "width": 103.8, }, Object { "actualText": "杭州市杭州市杭州市杭州市杭...", @@ -4360,7 +4360,7 @@ Array [ "州市杭州市杭...", ], "originalText": "杭州市杭州市杭州市杭州市杭州市杭州市杭州市杭州市杭州市杭州市", - "width": 103, + "width": 103.8, }, Object { "actualText": "绍兴市", @@ -4371,7 +4371,7 @@ Array [ "绍兴市", ], "originalText": "绍兴市", - "width": 103, + "width": 103.8, }, Object { "actualText": "宁波市", @@ -4382,7 +4382,7 @@ Array [ "宁波市", ], "originalText": "宁波市", - "width": 103, + "width": 103.8, }, Object { "actualText": "舟山市", @@ -4393,7 +4393,7 @@ Array [ "舟山市", ], "originalText": "舟山市", - "width": 103, + "width": 103.8, }, Object { "actualText": "杭州市", @@ -4404,7 +4404,7 @@ Array [ "杭州市", ], "originalText": "杭州市", - "width": 103, + "width": 103.8, }, Object { "actualText": "绍兴市", @@ -4415,7 +4415,7 @@ Array [ "绍兴市", ], "originalText": "绍兴市", - "width": 103, + "width": 103.8, }, Object { "actualText": "宁波市", @@ -4426,7 +4426,7 @@ Array [ "宁波市", ], "originalText": "宁波市", - "width": 103, + "width": 103.8, }, Object { "actualText": "舟山市", @@ -4437,7 +4437,7 @@ Array [ "舟山市", ], "originalText": "舟山市", - "width": 103, + "width": 103.8, }, Object { "actualText": "杭州市", @@ -4448,7 +4448,7 @@ Array [ "杭州市", ], "originalText": "杭州市", - "width": 103, + "width": 103.8, }, Object { "actualText": "绍兴市", @@ -4459,7 +4459,7 @@ Array [ "绍兴市", ], "originalText": "绍兴市", - "width": 103, + "width": 103.8, }, Object { "actualText": "宁波市", @@ -4470,7 +4470,7 @@ Array [ "宁波市", ], "originalText": "宁波市", - "width": 103, + "width": 103.8, }, Object { "actualText": "家具", @@ -4481,7 +4481,7 @@ Array [ "家具", ], "originalText": "家具", - "width": 103, + "width": 103.8, }, Object { "actualText": "家具家具家具家具家具家具家...", @@ -4493,7 +4493,7 @@ Array [ "具家具家具家...", ], "originalText": "家具家具家具家具家具家具家具家具家具家具家具家具家具家具", - "width": 103, + "width": 103.8, }, Object { "actualText": "家具", @@ -4504,7 +4504,7 @@ Array [ "家具", ], "originalText": "家具", - "width": 103, + "width": 103.8, }, Object { "actualText": "家具", @@ -4515,7 +4515,7 @@ Array [ "家具", ], "originalText": "家具", - "width": 103, + "width": 103.8, }, Object { "actualText": "家具", @@ -4526,7 +4526,7 @@ Array [ "家具", ], "originalText": "家具", - "width": 103, + "width": 103.8, }, Object { "actualText": "家具", @@ -4537,7 +4537,7 @@ Array [ "家具", ], "originalText": "家具", - "width": 103, + "width": 103.8, }, Object { "actualText": "家具", @@ -4548,7 +4548,7 @@ Array [ "家具", ], "originalText": "家具", - "width": 103, + "width": 103.8, }, Object { "actualText": "家具", @@ -4559,7 +4559,7 @@ Array [ "家具", ], "originalText": "家具", - "width": 103, + "width": 103.8, }, Object { "actualText": "家具", @@ -4570,7 +4570,7 @@ Array [ "家具", ], "originalText": "家具", - "width": 103, + "width": 103.8, }, Object { "actualText": "办公用品", @@ -4581,7 +4581,7 @@ Array [ "办公用品", ], "originalText": "办公用品", - "width": 103, + "width": 103.8, }, Object { "actualText": "办公用品", @@ -4592,7 +4592,7 @@ Array [ "办公用品", ], "originalText": "办公用品", - "width": 103, + "width": 103.8, }, Object { "actualText": "办公用品", @@ -4603,7 +4603,7 @@ Array [ "办公用品", ], "originalText": "办公用品", - "width": 103, + "width": 103.8, }, Object { "actualText": "桌子", @@ -4614,7 +4614,7 @@ Array [ "桌子", ], "originalText": "桌子", - "width": 103, + "width": 103.8, }, Object { "actualText": "桌子桌子桌子桌子桌子桌子桌...", @@ -4626,7 +4626,7 @@ Array [ "子桌子桌子桌...", ], "originalText": "桌子桌子桌子桌子桌子桌子桌子桌子桌子桌子桌子桌子桌子桌子桌子", - "width": 103, + "width": 103.8, }, Object { "actualText": "桌子", @@ -4637,7 +4637,7 @@ Array [ "桌子", ], "originalText": "桌子", - "width": 103, + "width": 103.8, }, Object { "actualText": "桌子", @@ -4648,7 +4648,7 @@ Array [ "桌子", ], "originalText": "桌子", - "width": 103, + "width": 103.8, }, Object { "actualText": "桌子", @@ -4659,7 +4659,7 @@ Array [ "桌子", ], "originalText": "桌子", - "width": 103, + "width": 103.8, }, Object { "actualText": "沙发", @@ -4670,7 +4670,7 @@ Array [ "沙发", ], "originalText": "沙发", - "width": 103, + "width": 103.8, }, Object { "actualText": "沙发", @@ -4681,7 +4681,7 @@ Array [ "沙发", ], "originalText": "沙发", - "width": 103, + "width": 103.8, }, Object { "actualText": "沙发", @@ -4692,7 +4692,7 @@ Array [ "沙发", ], "originalText": "沙发", - "width": 103, + "width": 103.8, }, Object { "actualText": "沙发", @@ -4703,7 +4703,7 @@ Array [ "沙发", ], "originalText": "沙发", - "width": 103, + "width": 103.8, }, Object { "actualText": "笔", @@ -4714,7 +4714,7 @@ Array [ "笔", ], "originalText": "笔", - "width": 103, + "width": 103.8, }, Object { "actualText": "笔", @@ -4725,7 +4725,7 @@ Array [ "笔", ], "originalText": "笔", - "width": 103, + "width": 103.8, }, Object { "actualText": "笔", @@ -4736,7 +4736,7 @@ Array [ "笔", ], "originalText": "笔", - "width": 103, + "width": 103.8, }, Object { "actualText": "236723672361111", @@ -4744,23 +4744,23 @@ Array [ "actualTextWidth": 100, "height": 30, "multiLineActualTexts": Array [ - "236723672361", - "111", + "2367236723611", + "11", ], "originalText": "236723672361111", - "width": 103, + "width": 103.8, }, Object { - "actualText": "77897789778977897789778...", + "actualText": "778977897789778977897789...", "actualTextHeight": 30, - "actualTextWidth": 165, + "actualTextWidth": 172, "height": 30, "multiLineActualTexts": Array [ - "778977897789", - "77897789778...", + "7789778977897", + "78977897789...", ], "originalText": "7789778977897789778977897789", - "width": 103, + "width": 103.8, }, Object { "actualText": "236723672361111", @@ -4768,11 +4768,11 @@ Array [ "actualTextWidth": 100, "height": 30, "multiLineActualTexts": Array [ - "236723672361", - "111", + "2367236723611", + "11", ], "originalText": "236723672361111", - "width": 103, + "width": 103.8, }, Object { "actualText": "3877", @@ -4783,7 +4783,7 @@ Array [ "3877", ], "originalText": "3877", - "width": 103, + "width": 103.8, }, Object { "actualText": "4342", @@ -4794,7 +4794,7 @@ Array [ "4342", ], "originalText": "4342", - "width": 103, + "width": 103.8, }, Object { "actualText": "5343", @@ -4805,7 +4805,7 @@ Array [ "5343", ], "originalText": "5343", - "width": 103, + "width": 103.8, }, Object { "actualText": "632", @@ -4816,7 +4816,7 @@ Array [ "632", ], "originalText": "632", - "width": 103, + "width": 103.8, }, Object { "actualText": "7234", @@ -4827,7 +4827,7 @@ Array [ "7234", ], "originalText": "7234", - "width": 103, + "width": 103.8, }, Object { "actualText": "834", @@ -4838,7 +4838,7 @@ Array [ "834", ], "originalText": "834", - "width": 103, + "width": 103.8, }, Object { "actualText": "945", @@ -4849,7 +4849,7 @@ Array [ "945", ], "originalText": "945", - "width": 103, + "width": 103.8, }, Object { "actualText": "1304", @@ -4860,7 +4860,7 @@ Array [ "1304", ], "originalText": "1304", - "width": 103, + "width": 103.8, }, Object { "actualText": "1145", @@ -4871,7 +4871,7 @@ Array [ "1145", ], "originalText": "1145", - "width": 103, + "width": 103.8, }, ] `; @@ -5062,7 +5062,7 @@ Array [ "省份", ], "originalText": "省份", - "width": 103, + "width": 103.8, }, Object { "actualText": "城市城市城市...", @@ -5073,7 +5073,7 @@ Array [ "城市城市城市...", ], "originalText": "城市城市城市城市城市城市城市城市城市城市城市城市", - "width": 103, + "width": 103.8, }, Object { "actualText": "类别类别类别...", @@ -5084,7 +5084,7 @@ Array [ "类别类别类别...", ], "originalText": "类别类别类别类别类别类别类别类别类别类别", - "width": 103, + "width": 103.8, }, Object { "actualText": "子类别", @@ -5095,7 +5095,7 @@ Array [ "子类别", ], "originalText": "子类别", - "width": 103, + "width": 103.8, }, Object { "actualText": "数量数量数量...", @@ -5106,7 +5106,7 @@ Array [ "数量数量数量...", ], "originalText": "数量数量数量数量数量数量数量数量数量数量数量", - "width": 103, + "width": 103.8, }, ] `; @@ -5267,7 +5267,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省浙江省...", @@ -5278,7 +5278,7 @@ Array [ "浙江省浙江省...", ], "originalText": "浙江省浙江省浙江省浙江省浙江省浙江省浙江省浙江省浙江省浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -5289,7 +5289,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -5300,7 +5300,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -5311,7 +5311,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -5322,7 +5322,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -5333,7 +5333,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -5344,7 +5344,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -5355,7 +5355,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -5366,7 +5366,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -5377,7 +5377,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -5388,7 +5388,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -5399,7 +5399,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "绍兴市", @@ -5410,7 +5410,7 @@ Array [ "绍兴市", ], "originalText": "绍兴市", - "width": 103, + "width": 103.8, }, Object { "actualText": "杭州市杭州市...", @@ -5421,7 +5421,7 @@ Array [ "杭州市杭州市...", ], "originalText": "杭州市杭州市杭州市杭州市杭州市杭州市杭州市杭州市杭州市杭州市", - "width": 103, + "width": 103.8, }, Object { "actualText": "绍兴市", @@ -5432,7 +5432,7 @@ Array [ "绍兴市", ], "originalText": "绍兴市", - "width": 103, + "width": 103.8, }, Object { "actualText": "宁波市", @@ -5443,7 +5443,7 @@ Array [ "宁波市", ], "originalText": "宁波市", - "width": 103, + "width": 103.8, }, Object { "actualText": "舟山市", @@ -5454,7 +5454,7 @@ Array [ "舟山市", ], "originalText": "舟山市", - "width": 103, + "width": 103.8, }, Object { "actualText": "杭州市", @@ -5465,7 +5465,7 @@ Array [ "杭州市", ], "originalText": "杭州市", - "width": 103, + "width": 103.8, }, Object { "actualText": "绍兴市", @@ -5476,7 +5476,7 @@ Array [ "绍兴市", ], "originalText": "绍兴市", - "width": 103, + "width": 103.8, }, Object { "actualText": "宁波市", @@ -5487,7 +5487,7 @@ Array [ "宁波市", ], "originalText": "宁波市", - "width": 103, + "width": 103.8, }, Object { "actualText": "舟山市", @@ -5498,7 +5498,7 @@ Array [ "舟山市", ], "originalText": "舟山市", - "width": 103, + "width": 103.8, }, Object { "actualText": "杭州市", @@ -5509,7 +5509,7 @@ Array [ "杭州市", ], "originalText": "杭州市", - "width": 103, + "width": 103.8, }, Object { "actualText": "绍兴市", @@ -5520,7 +5520,7 @@ Array [ "绍兴市", ], "originalText": "绍兴市", - "width": 103, + "width": 103.8, }, Object { "actualText": "宁波市", @@ -5531,7 +5531,7 @@ Array [ "宁波市", ], "originalText": "宁波市", - "width": 103, + "width": 103.8, }, Object { "actualText": "舟山市", @@ -5542,7 +5542,7 @@ Array [ "舟山市", ], "originalText": "舟山市", - "width": 103, + "width": 103.8, }, Object { "actualText": "家具", @@ -5553,7 +5553,7 @@ Array [ "家具", ], "originalText": "家具", - "width": 103, + "width": 103.8, }, Object { "actualText": "家具家具家具...", @@ -5564,7 +5564,7 @@ Array [ "家具家具家具...", ], "originalText": "家具家具家具家具家具家具家具家具家具家具家具家具家具家具", - "width": 103, + "width": 103.8, }, Object { "actualText": "家具", @@ -5575,7 +5575,7 @@ Array [ "家具", ], "originalText": "家具", - "width": 103, + "width": 103.8, }, Object { "actualText": "家具", @@ -5586,7 +5586,7 @@ Array [ "家具", ], "originalText": "家具", - "width": 103, + "width": 103.8, }, Object { "actualText": "家具", @@ -5597,7 +5597,7 @@ Array [ "家具", ], "originalText": "家具", - "width": 103, + "width": 103.8, }, Object { "actualText": "家具", @@ -5608,7 +5608,7 @@ Array [ "家具", ], "originalText": "家具", - "width": 103, + "width": 103.8, }, Object { "actualText": "家具", @@ -5619,7 +5619,7 @@ Array [ "家具", ], "originalText": "家具", - "width": 103, + "width": 103.8, }, Object { "actualText": "家具", @@ -5630,7 +5630,7 @@ Array [ "家具", ], "originalText": "家具", - "width": 103, + "width": 103.8, }, Object { "actualText": "家具", @@ -5641,7 +5641,7 @@ Array [ "家具", ], "originalText": "家具", - "width": 103, + "width": 103.8, }, Object { "actualText": "办公用品", @@ -5652,7 +5652,7 @@ Array [ "办公用品", ], "originalText": "办公用品", - "width": 103, + "width": 103.8, }, Object { "actualText": "办公用品", @@ -5663,7 +5663,7 @@ Array [ "办公用品", ], "originalText": "办公用品", - "width": 103, + "width": 103.8, }, Object { "actualText": "办公用品", @@ -5674,7 +5674,7 @@ Array [ "办公用品", ], "originalText": "办公用品", - "width": 103, + "width": 103.8, }, Object { "actualText": "办公用品", @@ -5685,7 +5685,7 @@ Array [ "办公用品", ], "originalText": "办公用品", - "width": 103, + "width": 103.8, }, Object { "actualText": "桌子", @@ -5696,7 +5696,7 @@ Array [ "桌子", ], "originalText": "桌子", - "width": 103, + "width": 103.8, }, Object { "actualText": "桌子桌子桌子...", @@ -5707,7 +5707,7 @@ Array [ "桌子桌子桌子...", ], "originalText": "桌子桌子桌子桌子桌子桌子桌子桌子桌子桌子桌子桌子桌子桌子桌子", - "width": 103, + "width": 103.8, }, Object { "actualText": "桌子", @@ -5718,7 +5718,7 @@ Array [ "桌子", ], "originalText": "桌子", - "width": 103, + "width": 103.8, }, Object { "actualText": "桌子", @@ -5729,7 +5729,7 @@ Array [ "桌子", ], "originalText": "桌子", - "width": 103, + "width": 103.8, }, Object { "actualText": "桌子", @@ -5740,7 +5740,7 @@ Array [ "桌子", ], "originalText": "桌子", - "width": 103, + "width": 103.8, }, Object { "actualText": "沙发", @@ -5751,7 +5751,7 @@ Array [ "沙发", ], "originalText": "沙发", - "width": 103, + "width": 103.8, }, Object { "actualText": "沙发", @@ -5762,7 +5762,7 @@ Array [ "沙发", ], "originalText": "沙发", - "width": 103, + "width": 103.8, }, Object { "actualText": "沙发", @@ -5773,7 +5773,7 @@ Array [ "沙发", ], "originalText": "沙发", - "width": 103, + "width": 103.8, }, Object { "actualText": "沙发", @@ -5784,7 +5784,7 @@ Array [ "沙发", ], "originalText": "沙发", - "width": 103, + "width": 103.8, }, Object { "actualText": "笔", @@ -5795,7 +5795,7 @@ Array [ "笔", ], "originalText": "笔", - "width": 103, + "width": 103.8, }, Object { "actualText": "笔", @@ -5806,7 +5806,7 @@ Array [ "笔", ], "originalText": "笔", - "width": 103, + "width": 103.8, }, Object { "actualText": "笔", @@ -5817,7 +5817,7 @@ Array [ "笔", ], "originalText": "笔", - "width": 103, + "width": 103.8, }, Object { "actualText": "笔", @@ -5828,7 +5828,7 @@ Array [ "笔", ], "originalText": "笔", - "width": 103, + "width": 103.8, }, Object { "actualText": "23672367236...", @@ -5839,7 +5839,7 @@ Array [ "23672367236...", ], "originalText": "236723672361111", - "width": 103, + "width": 103.8, }, Object { "actualText": "77897789778...", @@ -5850,7 +5850,7 @@ Array [ "77897789778...", ], "originalText": "7789778977897789778977897789", - "width": 103, + "width": 103.8, }, Object { "actualText": "23672367236...", @@ -5861,7 +5861,7 @@ Array [ "23672367236...", ], "originalText": "236723672361111", - "width": 103, + "width": 103.8, }, Object { "actualText": "3877", @@ -5872,7 +5872,7 @@ Array [ "3877", ], "originalText": "3877", - "width": 103, + "width": 103.8, }, Object { "actualText": "4342", @@ -5883,7 +5883,7 @@ Array [ "4342", ], "originalText": "4342", - "width": 103, + "width": 103.8, }, Object { "actualText": "5343", @@ -5894,7 +5894,7 @@ Array [ "5343", ], "originalText": "5343", - "width": 103, + "width": 103.8, }, Object { "actualText": "632", @@ -5905,7 +5905,7 @@ Array [ "632", ], "originalText": "632", - "width": 103, + "width": 103.8, }, Object { "actualText": "7234", @@ -5916,7 +5916,7 @@ Array [ "7234", ], "originalText": "7234", - "width": 103, + "width": 103.8, }, Object { "actualText": "834", @@ -5927,7 +5927,7 @@ Array [ "834", ], "originalText": "834", - "width": 103, + "width": 103.8, }, Object { "actualText": "945", @@ -5938,7 +5938,7 @@ Array [ "945", ], "originalText": "945", - "width": 103, + "width": 103.8, }, Object { "actualText": "1304", @@ -5949,7 +5949,7 @@ Array [ "1304", ], "originalText": "1304", - "width": 103, + "width": 103.8, }, Object { "actualText": "1145", @@ -5960,7 +5960,7 @@ Array [ "1145", ], "originalText": "1145", - "width": 103, + "width": 103.8, }, Object { "actualText": "1432", @@ -5971,7 +5971,7 @@ Array [ "1432", ], "originalText": "1432", - "width": 103, + "width": 103.8, }, ] `; @@ -6217,7 +6217,7 @@ Array [ "省份", ], "originalText": "省份", - "width": 103, + "width": 103.8, }, Object { "actualText": "城市城市城市城市城市城市城...", @@ -6229,7 +6229,7 @@ Array [ "市城市城市城...", ], "originalText": "城市城市城市城市城市城市城市城市城市城市城市城市", - "width": 103, + "width": 103.8, }, Object { "actualText": "类别类别类别类别类别类别类...", @@ -6241,7 +6241,7 @@ Array [ "别类别类别类...", ], "originalText": "类别类别类别类别类别类别类别类别类别类别", - "width": 103, + "width": 103.8, }, Object { "actualText": "子类别", @@ -6252,7 +6252,7 @@ Array [ "子类别", ], "originalText": "子类别", - "width": 103, + "width": 103.8, }, Object { "actualText": "数量数量数量数量数量数量数...", @@ -6264,7 +6264,7 @@ Array [ "量数量数量数...", ], "originalText": "数量数量数量数量数量数量数量数量数量数量数量", - "width": 103, + "width": 103.8, }, ] `; @@ -6480,7 +6480,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省浙江省浙江省浙江省浙...", @@ -6492,7 +6492,7 @@ Array [ "江省浙江省浙...", ], "originalText": "浙江省浙江省浙江省浙江省浙江省浙江省浙江省浙江省浙江省浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -6503,7 +6503,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -6514,7 +6514,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -6525,7 +6525,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -6536,7 +6536,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -6547,7 +6547,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -6558,7 +6558,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -6569,7 +6569,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -6580,7 +6580,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -6591,7 +6591,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -6602,7 +6602,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -6613,7 +6613,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -6624,7 +6624,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -6635,7 +6635,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -6646,7 +6646,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "浙江省", @@ -6657,7 +6657,7 @@ Array [ "浙江省", ], "originalText": "浙江省", - "width": 103, + "width": 103.8, }, Object { "actualText": "四川省", @@ -6668,7 +6668,7 @@ Array [ "四川省", ], "originalText": "四川省", - "width": 103, + "width": 103.8, }, Object { "actualText": "绍兴市", @@ -6679,7 +6679,7 @@ Array [ "绍兴市", ], "originalText": "绍兴市", - "width": 103, + "width": 103.8, }, Object { "actualText": "杭州市杭州市杭州市杭州市杭...", @@ -6691,7 +6691,7 @@ Array [ "州市杭州市杭...", ], "originalText": "杭州市杭州市杭州市杭州市杭州市杭州市杭州市杭州市杭州市杭州市", - "width": 103, + "width": 103.8, }, Object { "actualText": "绍兴市", @@ -6702,7 +6702,7 @@ Array [ "绍兴市", ], "originalText": "绍兴市", - "width": 103, + "width": 103.8, }, Object { "actualText": "宁波市", @@ -6713,7 +6713,7 @@ Array [ "宁波市", ], "originalText": "宁波市", - "width": 103, + "width": 103.8, }, Object { "actualText": "舟山市", @@ -6724,7 +6724,7 @@ Array [ "舟山市", ], "originalText": "舟山市", - "width": 103, + "width": 103.8, }, Object { "actualText": "杭州市", @@ -6735,7 +6735,7 @@ Array [ "杭州市", ], "originalText": "杭州市", - "width": 103, + "width": 103.8, }, Object { "actualText": "绍兴市", @@ -6746,7 +6746,7 @@ Array [ "绍兴市", ], "originalText": "绍兴市", - "width": 103, + "width": 103.8, }, Object { "actualText": "宁波市", @@ -6757,7 +6757,7 @@ Array [ "宁波市", ], "originalText": "宁波市", - "width": 103, + "width": 103.8, }, Object { "actualText": "舟山市", @@ -6768,7 +6768,7 @@ Array [ "舟山市", ], "originalText": "舟山市", - "width": 103, + "width": 103.8, }, Object { "actualText": "杭州市", @@ -6779,7 +6779,7 @@ Array [ "杭州市", ], "originalText": "杭州市", - "width": 103, + "width": 103.8, }, Object { "actualText": "绍兴市", @@ -6790,7 +6790,7 @@ Array [ "绍兴市", ], "originalText": "绍兴市", - "width": 103, + "width": 103.8, }, Object { "actualText": "宁波市", @@ -6801,7 +6801,7 @@ Array [ "宁波市", ], "originalText": "宁波市", - "width": 103, + "width": 103.8, }, Object { "actualText": "舟山市", @@ -6812,7 +6812,7 @@ Array [ "舟山市", ], "originalText": "舟山市", - "width": 103, + "width": 103.8, }, Object { "actualText": "杭州市", @@ -6823,7 +6823,7 @@ Array [ "杭州市", ], "originalText": "杭州市", - "width": 103, + "width": 103.8, }, Object { "actualText": "绍兴市", @@ -6834,7 +6834,7 @@ Array [ "绍兴市", ], "originalText": "绍兴市", - "width": 103, + "width": 103.8, }, Object { "actualText": "宁波市", @@ -6845,7 +6845,7 @@ Array [ "宁波市", ], "originalText": "宁波市", - "width": 103, + "width": 103.8, }, Object { "actualText": "舟山市", @@ -6856,7 +6856,7 @@ Array [ "舟山市", ], "originalText": "舟山市", - "width": 103, + "width": 103.8, }, Object { "actualText": "成都市", @@ -6867,7 +6867,7 @@ Array [ "成都市", ], "originalText": "成都市", - "width": 103, + "width": 103.8, }, Object { "actualText": "家具", @@ -6878,7 +6878,7 @@ Array [ "家具", ], "originalText": "家具", - "width": 103, + "width": 103.8, }, Object { "actualText": "家具家具家具家具家具家具家...", @@ -6890,7 +6890,7 @@ Array [ "具家具家具家...", ], "originalText": "家具家具家具家具家具家具家具家具家具家具家具家具家具家具", - "width": 103, + "width": 103.8, }, Object { "actualText": "家具", @@ -6901,7 +6901,7 @@ Array [ "家具", ], "originalText": "家具", - "width": 103, + "width": 103.8, }, Object { "actualText": "家具", @@ -6912,7 +6912,7 @@ Array [ "家具", ], "originalText": "家具", - "width": 103, + "width": 103.8, }, Object { "actualText": "家具", @@ -6923,7 +6923,7 @@ Array [ "家具", ], "originalText": "家具", - "width": 103, + "width": 103.8, }, Object { "actualText": "家具", @@ -6934,7 +6934,7 @@ Array [ "家具", ], "originalText": "家具", - "width": 103, + "width": 103.8, }, Object { "actualText": "家具", @@ -6945,7 +6945,7 @@ Array [ "家具", ], "originalText": "家具", - "width": 103, + "width": 103.8, }, Object { "actualText": "家具", @@ -6956,7 +6956,7 @@ Array [ "家具", ], "originalText": "家具", - "width": 103, + "width": 103.8, }, Object { "actualText": "家具", @@ -6967,7 +6967,7 @@ Array [ "家具", ], "originalText": "家具", - "width": 103, + "width": 103.8, }, Object { "actualText": "办公用品", @@ -6978,7 +6978,7 @@ Array [ "办公用品", ], "originalText": "办公用品", - "width": 103, + "width": 103.8, }, Object { "actualText": "办公用品", @@ -6989,7 +6989,7 @@ Array [ "办公用品", ], "originalText": "办公用品", - "width": 103, + "width": 103.8, }, Object { "actualText": "办公用品", @@ -7000,7 +7000,7 @@ Array [ "办公用品", ], "originalText": "办公用品", - "width": 103, + "width": 103.8, }, Object { "actualText": "办公用品", @@ -7011,7 +7011,7 @@ Array [ "办公用品", ], "originalText": "办公用品", - "width": 103, + "width": 103.8, }, Object { "actualText": "办公用品", @@ -7022,7 +7022,7 @@ Array [ "办公用品", ], "originalText": "办公用品", - "width": 103, + "width": 103.8, }, Object { "actualText": "办公用品", @@ -7033,7 +7033,7 @@ Array [ "办公用品", ], "originalText": "办公用品", - "width": 103, + "width": 103.8, }, Object { "actualText": "办公用品", @@ -7044,7 +7044,7 @@ Array [ "办公用品", ], "originalText": "办公用品", - "width": 103, + "width": 103.8, }, Object { "actualText": "办公用品", @@ -7055,7 +7055,7 @@ Array [ "办公用品", ], "originalText": "办公用品", - "width": 103, + "width": 103.8, }, Object { "actualText": "家具", @@ -7066,7 +7066,7 @@ Array [ "家具", ], "originalText": "家具", - "width": 103, + "width": 103.8, }, Object { "actualText": "桌子", @@ -7077,7 +7077,7 @@ Array [ "桌子", ], "originalText": "桌子", - "width": 103, + "width": 103.8, }, Object { "actualText": "桌子桌子桌子桌子桌子桌子桌...", @@ -7089,7 +7089,7 @@ Array [ "子桌子桌子桌...", ], "originalText": "桌子桌子桌子桌子桌子桌子桌子桌子桌子桌子桌子桌子桌子桌子桌子", - "width": 103, + "width": 103.8, }, Object { "actualText": "桌子", @@ -7100,7 +7100,7 @@ Array [ "桌子", ], "originalText": "桌子", - "width": 103, + "width": 103.8, }, Object { "actualText": "桌子", @@ -7111,7 +7111,7 @@ Array [ "桌子", ], "originalText": "桌子", - "width": 103, + "width": 103.8, }, Object { "actualText": "桌子", @@ -7122,7 +7122,7 @@ Array [ "桌子", ], "originalText": "桌子", - "width": 103, + "width": 103.8, }, Object { "actualText": "沙发", @@ -7133,7 +7133,7 @@ Array [ "沙发", ], "originalText": "沙发", - "width": 103, + "width": 103.8, }, Object { "actualText": "沙发", @@ -7144,7 +7144,7 @@ Array [ "沙发", ], "originalText": "沙发", - "width": 103, + "width": 103.8, }, Object { "actualText": "沙发", @@ -7155,7 +7155,7 @@ Array [ "沙发", ], "originalText": "沙发", - "width": 103, + "width": 103.8, }, Object { "actualText": "沙发", @@ -7166,7 +7166,7 @@ Array [ "沙发", ], "originalText": "沙发", - "width": 103, + "width": 103.8, }, Object { "actualText": "笔", @@ -7177,7 +7177,7 @@ Array [ "笔", ], "originalText": "笔", - "width": 103, + "width": 103.8, }, Object { "actualText": "笔", @@ -7188,7 +7188,7 @@ Array [ "笔", ], "originalText": "笔", - "width": 103, + "width": 103.8, }, Object { "actualText": "笔", @@ -7199,7 +7199,7 @@ Array [ "笔", ], "originalText": "笔", - "width": 103, + "width": 103.8, }, Object { "actualText": "笔", @@ -7210,7 +7210,7 @@ Array [ "笔", ], "originalText": "笔", - "width": 103, + "width": 103.8, }, Object { "actualText": "纸张", @@ -7221,7 +7221,7 @@ Array [ "纸张", ], "originalText": "纸张", - "width": 103, + "width": 103.8, }, Object { "actualText": "纸张", @@ -7232,7 +7232,7 @@ Array [ "纸张", ], "originalText": "纸张", - "width": 103, + "width": 103.8, }, Object { "actualText": "纸张", @@ -7243,7 +7243,7 @@ Array [ "纸张", ], "originalText": "纸张", - "width": 103, + "width": 103.8, }, Object { "actualText": "纸张", @@ -7254,7 +7254,7 @@ Array [ "纸张", ], "originalText": "纸张", - "width": 103, + "width": 103.8, }, Object { "actualText": "桌子", @@ -7265,7 +7265,7 @@ Array [ "桌子", ], "originalText": "桌子", - "width": 103, + "width": 103.8, }, Object { "actualText": "236723672361111", @@ -7273,23 +7273,23 @@ Array [ "actualTextWidth": 100, "height": 20, "multiLineActualTexts": Array [ - "236723672361", - "111", + "2367236723611", + "11", ], "originalText": "236723672361111", - "width": 103, + "width": 103.8, }, Object { - "actualText": "77897789778977897789778...", + "actualText": "778977897789778977897789...", "actualTextHeight": 30, - "actualTextWidth": 165, + "actualTextWidth": 172, "height": 20, "multiLineActualTexts": Array [ - "778977897789", - "77897789778...", + "7789778977897", + "78977897789...", ], "originalText": "7789778977897789778977897789", - "width": 103, + "width": 103.8, }, Object { "actualText": "236723672361111", @@ -7297,11 +7297,11 @@ Array [ "actualTextWidth": 100, "height": 20, "multiLineActualTexts": Array [ - "236723672361", - "111", + "2367236723611", + "11", ], "originalText": "236723672361111", - "width": 103, + "width": 103.8, }, Object { "actualText": "3877", @@ -7312,7 +7312,7 @@ Array [ "3877", ], "originalText": "3877", - "width": 103, + "width": 103.8, }, Object { "actualText": "4342", @@ -7323,7 +7323,7 @@ Array [ "4342", ], "originalText": "4342", - "width": 103, + "width": 103.8, }, Object { "actualText": "5343", @@ -7334,7 +7334,7 @@ Array [ "5343", ], "originalText": "5343", - "width": 103, + "width": 103.8, }, Object { "actualText": "632", @@ -7345,7 +7345,7 @@ Array [ "632", ], "originalText": "632", - "width": 103, + "width": 103.8, }, Object { "actualText": "7234", @@ -7356,7 +7356,7 @@ Array [ "7234", ], "originalText": "7234", - "width": 103, + "width": 103.8, }, Object { "actualText": "834", @@ -7367,7 +7367,7 @@ Array [ "834", ], "originalText": "834", - "width": 103, + "width": 103.8, }, Object { "actualText": "945", @@ -7378,7 +7378,7 @@ Array [ "945", ], "originalText": "945", - "width": 103, + "width": 103.8, }, Object { "actualText": "1304", @@ -7389,7 +7389,7 @@ Array [ "1304", ], "originalText": "1304", - "width": 103, + "width": 103.8, }, Object { "actualText": "1145", @@ -7400,7 +7400,7 @@ Array [ "1145", ], "originalText": "1145", - "width": 103, + "width": 103.8, }, Object { "actualText": "1432", @@ -7411,7 +7411,7 @@ Array [ "1432", ], "originalText": "1432", - "width": 103, + "width": 103.8, }, Object { "actualText": "1343", @@ -7422,7 +7422,7 @@ Array [ "1343", ], "originalText": "1343", - "width": 103, + "width": 103.8, }, Object { "actualText": "1354", @@ -7433,7 +7433,7 @@ Array [ "1354", ], "originalText": "1354", - "width": 103, + "width": 103.8, }, Object { "actualText": "1523", @@ -7444,7 +7444,7 @@ Array [ "1523", ], "originalText": "1523", - "width": 103, + "width": 103.8, }, Object { "actualText": "1634", @@ -7455,7 +7455,7 @@ Array [ "1634", ], "originalText": "1634", - "width": 103, + "width": 103.8, }, Object { "actualText": "1723", @@ -7466,7 +7466,7 @@ Array [ "1723", ], "originalText": "1723", - "width": 103, + "width": 103.8, }, ] `; @@ -7493,7 +7493,7 @@ Array [ "省份", ], "originalText": "省份", - "width": 103, + "width": 103.8, }, Object { "actualText": "城市城市城市城市城市城市城...", @@ -7505,7 +7505,7 @@ Array [ "市城市城市城...", ], "originalText": "城市城市城市城市城市城市城市城市城市城市城市城市", - "width": 103, + "width": 103.8, }, Object { "actualText": "类别类别类别类别类别类别类...", @@ -7517,7 +7517,7 @@ Array [ "别类别类别类...", ], "originalText": "类别类别类别类别类别类别类别类别类别类别", - "width": 103, + "width": 103.8, }, Object { "actualText": "子类别", @@ -7528,7 +7528,7 @@ Array [ "子类别", ], "originalText": "子类别", - "width": 103, + "width": 103.8, }, Object { "actualText": "数量数量数量数量数量数量数...", @@ -7540,7 +7540,7 @@ Array [ "量数量数量数...", ], "originalText": "数量数量数量数量数量数量数量数量数量数量数量", - "width": 103, + "width": 103.8, }, ] `; diff --git a/packages/s2-core/__tests__/spreadsheet/__snapshots__/spread-sheet-resize-spec.ts.snap b/packages/s2-core/__tests__/spreadsheet/__snapshots__/spread-sheet-resize-spec.ts.snap new file mode 100644 index 0000000000..fc3bf7f405 --- /dev/null +++ b/packages/s2-core/__tests__/spreadsheet/__snapshots__/spread-sheet-resize-spec.ts.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SpreadSheet Resize Active Tests should render correctly layout when tree row width is invalid number 1`] = ` +Array [ + Object { + "height": 30, + "id": "root[&]浙江", + "width": 120, + }, + Object { + "height": 30, + "id": "root[&]浙江[&]义乌", + "width": 120, + }, + Object { + "height": 30, + "id": "root[&]浙江[&]杭州", + "width": 120, + }, +] +`; diff --git a/packages/s2-core/__tests__/spreadsheet/__snapshots__/theme-spec.ts.snap b/packages/s2-core/__tests__/spreadsheet/__snapshots__/theme-spec.ts.snap index 309bd54147..81c158e752 100644 --- a/packages/s2-core/__tests__/spreadsheet/__snapshots__/theme-spec.ts.snap +++ b/packages/s2-core/__tests__/spreadsheet/__snapshots__/theme-spec.ts.snap @@ -22,6 +22,7 @@ Object { "cell": Object { "backgroundColor": "#3471F9", "backgroundColorOpacity": 1, + "borderDash": Array [], "horizontalBorderColor": "#5286FA", "horizontalBorderColorOpacity": 1, "horizontalBorderWidth": 1, @@ -106,7 +107,7 @@ Object { "fontWeight": 700, "maxLines": 1, "opacity": 1, - "textAlign": "left", + "textAlign": "right", "textBaseline": "middle", "textOverflow": "ellipsis", "wordWrap": true, @@ -114,6 +115,7 @@ Object { "cell": Object { "backgroundColor": "#3471F9", "backgroundColorOpacity": 1, + "borderDash": Array [], "horizontalBorderColor": "#5286FA", "horizontalBorderColorOpacity": 1, "horizontalBorderWidth": 1, @@ -142,7 +144,7 @@ Object { "fontWeight": 700, "maxLines": 1, "opacity": 1, - "textAlign": "right", + "textAlign": "left", "textBaseline": "middle", "textOverflow": "ellipsis", "wordWrap": true, @@ -448,6 +450,7 @@ Object { "cell": Object { "backgroundColor": "#F5F8FF", "backgroundColorOpacity": 1, + "borderDash": Array [], "horizontalBorderColor": "#E1EAFE", "horizontalBorderColorOpacity": 1, "horizontalBorderWidth": 1, @@ -549,6 +552,7 @@ Object { "trackColor": "rgba(0,0,0,0.01)", }, "splitLine": Object { + "borderDash": Array [], "horizontalBorderColor": "#3471F9", "horizontalBorderColorOpacity": 0.2, "horizontalBorderWidth": 2, @@ -587,6 +591,7 @@ Object { "cell": Object { "backgroundColor": "#133aad", "backgroundColorOpacity": 1, + "borderDash": Array [], "horizontalBorderColor": "#0647b1", "horizontalBorderColorOpacity": 1, "horizontalBorderWidth": 1, @@ -671,7 +676,7 @@ Object { "fontWeight": 700, "maxLines": 1, "opacity": 1, - "textAlign": "left", + "textAlign": "right", "textBaseline": "middle", "textOverflow": "ellipsis", "wordWrap": true, @@ -679,6 +684,7 @@ Object { "cell": Object { "backgroundColor": "#133aad", "backgroundColorOpacity": 1, + "borderDash": Array [], "horizontalBorderColor": "#0647b1", "horizontalBorderColorOpacity": 1, "horizontalBorderWidth": 1, @@ -707,7 +713,7 @@ Object { "fontWeight": 700, "maxLines": 1, "opacity": 1, - "textAlign": "right", + "textAlign": "left", "textBaseline": "middle", "textOverflow": "ellipsis", "wordWrap": true, @@ -1013,6 +1019,7 @@ Object { "cell": Object { "backgroundColor": "#151a27", "backgroundColorOpacity": 1, + "borderDash": Array [], "horizontalBorderColor": "#1e2436", "horizontalBorderColorOpacity": 1, "horizontalBorderWidth": 1, @@ -1114,6 +1121,7 @@ Object { "trackColor": "rgba(0,0,0,0.01)", }, "splitLine": Object { + "borderDash": Array [], "horizontalBorderColor": "#7899ff", "horizontalBorderColorOpacity": 0.2, "horizontalBorderWidth": 2, @@ -1152,6 +1160,7 @@ Object { "cell": Object { "backgroundColor": "#E0E9FD", "backgroundColorOpacity": 1, + "borderDash": Array [], "horizontalBorderColor": "#CCDBFC", "horizontalBorderColorOpacity": 1, "horizontalBorderWidth": 1, @@ -1236,7 +1245,7 @@ Object { "fontWeight": 700, "maxLines": 1, "opacity": 1, - "textAlign": "left", + "textAlign": "right", "textBaseline": "middle", "textOverflow": "ellipsis", "wordWrap": true, @@ -1244,6 +1253,7 @@ Object { "cell": Object { "backgroundColor": "#E0E9FD", "backgroundColorOpacity": 1, + "borderDash": Array [], "horizontalBorderColor": "#CCDBFC", "horizontalBorderColorOpacity": 1, "horizontalBorderWidth": 1, @@ -1272,7 +1282,7 @@ Object { "fontWeight": 700, "maxLines": 1, "opacity": 1, - "textAlign": "right", + "textAlign": "left", "textBaseline": "middle", "textOverflow": "ellipsis", "wordWrap": true, @@ -1578,6 +1588,7 @@ Object { "cell": Object { "backgroundColor": "#F5F8FE", "backgroundColorOpacity": 1, + "borderDash": Array [], "horizontalBorderColor": "#E0E9FD", "horizontalBorderColorOpacity": 1, "horizontalBorderWidth": 1, @@ -1679,6 +1690,7 @@ Object { "trackColor": "rgba(0,0,0,0.01)", }, "splitLine": Object { + "borderDash": Array [], "horizontalBorderColor": "#326EF4", "horizontalBorderColorOpacity": 0.2, "horizontalBorderWidth": 2, @@ -1695,7 +1707,7 @@ Object { } `; -exports[`SpreadSheet Theme Tests Theme Default Value Tests should get default theme 2`] = ` +exports[`SpreadSheet Theme Tests Theme Default Value Tests should get gray theme 1`] = ` Object { "background": Object { "color": "#FFFFFF", @@ -1715,9 +1727,10 @@ Object { "wordWrap": true, }, "cell": Object { - "backgroundColor": "#E0E9FD", + "backgroundColor": "#F0F2F4", "backgroundColorOpacity": 1, - "horizontalBorderColor": "#CCDBFC", + "borderDash": Array [], + "horizontalBorderColor": "#E7E9ED", "horizontalBorderColorOpacity": 1, "horizontalBorderWidth": 1, "interactionState": Object { @@ -1726,7 +1739,7 @@ Object { "backgroundOpacity": 1, }, "hover": Object { - "backgroundColor": "#CCDBFC", + "backgroundColor": "#E7E9ED", "backgroundOpacity": 0.6, }, "prepareSelect": Object { @@ -1739,7 +1752,7 @@ Object { "backgroundOpacity": 1, }, "selected": Object { - "backgroundColor": "#CCDBFC", + "backgroundColor": "#E7E9ED", "backgroundOpacity": 0.6, }, "unselected": Object { @@ -1754,7 +1767,7 @@ Object { "right": 8, "top": 4, }, - "verticalBorderColor": "#CCDBFC", + "verticalBorderColor": "#E7E9ED", "verticalBorderColorOpacity": 1, "verticalBorderWidth": 1, }, @@ -1801,15 +1814,16 @@ Object { "fontWeight": 700, "maxLines": 1, "opacity": 1, - "textAlign": "left", + "textAlign": "right", "textBaseline": "middle", "textOverflow": "ellipsis", "wordWrap": true, }, "cell": Object { - "backgroundColor": "#E0E9FD", + "backgroundColor": "#F0F2F4", "backgroundColorOpacity": 1, - "horizontalBorderColor": "#CCDBFC", + "borderDash": Array [], + "horizontalBorderColor": "#E7E9ED", "horizontalBorderColorOpacity": 1, "horizontalBorderWidth": 1, "padding": Object { @@ -1818,7 +1832,7 @@ Object { "right": 8, "top": 4, }, - "verticalBorderColor": "#CCDBFC", + "verticalBorderColor": "#E7E9ED", "verticalBorderColorOpacity": 1, "verticalBorderWidth": 1, }, @@ -1837,7 +1851,7 @@ Object { "fontWeight": 700, "maxLines": 1, "opacity": 1, - "textAlign": "right", + "textAlign": "left", "textBaseline": "middle", "textOverflow": "ellipsis", "wordWrap": true, @@ -1849,7 +1863,7 @@ Object { "fontFamily": "Roboto, PingFangSC, Microsoft YaHei, Arial, sans-serif", "fontSize": 12, "fontWeight": 700, - "linkTextFill": "#326EF4", + "linkTextFill": "#565C64", "maxLines": 1, "opacity": 1, "textAlign": "right", @@ -1860,8 +1874,8 @@ Object { "cell": Object { "backgroundColor": "#FFFFFF", "backgroundColorOpacity": 1, - "crossBackgroundColor": "#F5F8FE", - "horizontalBorderColor": "#E0E9FD", + "crossBackgroundColor": "#FAFBFB", + "horizontalBorderColor": "#F0F2F4", "horizontalBorderColorOpacity": 1, "horizontalBorderWidth": 1, "interactionState": Object { @@ -1870,11 +1884,11 @@ Object { "backgroundOpacity": 1, }, "hover": Object { - "backgroundColor": "#E0E9FD", + "backgroundColor": "#F0F2F4", "backgroundOpacity": 0.6, }, "hoverFocus": Object { - "backgroundColor": "#E0E9FD", + "backgroundColor": "#F0F2F4", "backgroundOpacity": 0.6, "borderColor": "#000000", "borderOpacity": 1, @@ -1890,7 +1904,7 @@ Object { "backgroundOpacity": 1, }, "selected": Object { - "backgroundColor": "#E0E9FD", + "backgroundColor": "#F0F2F4", "backgroundOpacity": 0.6, }, "unselected": Object { @@ -1905,7 +1919,7 @@ Object { "right": 8, "top": 8, }, - "verticalBorderColor": "#E0E9FD", + "verticalBorderColor": "#F0F2F4", "verticalBorderColorOpacity": 1, "verticalBorderWidth": 1, }, @@ -1919,7 +1933,7 @@ Object { }, "miniChart": Object { "bar": Object { - "fill": "#326EF4", + "fill": "#565C64", "intervalPadding": 4, "opacity": 1, }, @@ -1943,17 +1957,17 @@ Object { }, }, "interval": Object { - "fill": "#326EF4", + "fill": "#9DA7B6", "height": 12, }, "line": Object { "linkLine": Object { - "fill": "#326EF4", + "fill": "#565C64", "opacity": 0.6, "size": 1.5, }, "point": Object { - "fill": "#326EF4", + "fill": "#565C64", "opacity": 1, "size": 2.2, }, @@ -1964,7 +1978,7 @@ Object { "fontFamily": "Roboto, PingFangSC, Microsoft YaHei, Arial, sans-serif", "fontSize": 12, "fontWeight": "normal", - "linkTextFill": "#326EF4", + "linkTextFill": "#565C64", "maxLines": 1, "opacity": 1, "textAlign": "right", @@ -1979,7 +1993,7 @@ Object { "fontFamily": "Roboto, PingFangSC, Microsoft YaHei, Arial, sans-serif", "fontSize": 12, "fontWeight": 700, - "linkTextFill": "#326EF4", + "linkTextFill": "#565C64", "maxLines": 1, "opacity": 1, "textAlign": "right", @@ -1990,8 +2004,8 @@ Object { "cell": Object { "backgroundColor": "#FFFFFF", "backgroundColorOpacity": 1, - "crossBackgroundColor": "#F5F8FE", - "horizontalBorderColor": "#E0E9FD", + "crossBackgroundColor": "#FAFBFB", + "horizontalBorderColor": "#F0F2F4", "horizontalBorderColorOpacity": 1, "horizontalBorderWidth": 1, "interactionState": Object { @@ -2000,11 +2014,11 @@ Object { "backgroundOpacity": 1, }, "hover": Object { - "backgroundColor": "#E0E9FD", + "backgroundColor": "#F0F2F4", "backgroundOpacity": 0.6, }, "hoverFocus": Object { - "backgroundColor": "#E0E9FD", + "backgroundColor": "#F0F2F4", "backgroundOpacity": 0.6, "borderColor": "#000000", "borderOpacity": 1, @@ -2020,7 +2034,7 @@ Object { "backgroundOpacity": 1, }, "selected": Object { - "backgroundColor": "#E0E9FD", + "backgroundColor": "#F0F2F4", "backgroundOpacity": 0.6, }, "unselected": Object { @@ -2035,7 +2049,7 @@ Object { "right": 8, "top": 8, }, - "verticalBorderColor": "#E0E9FD", + "verticalBorderColor": "#F0F2F4", "verticalBorderColorOpacity": 1, "verticalBorderWidth": 1, }, @@ -2049,7 +2063,7 @@ Object { }, "miniChart": Object { "bar": Object { - "fill": "#326EF4", + "fill": "#565C64", "intervalPadding": 4, "opacity": 1, }, @@ -2073,17 +2087,17 @@ Object { }, }, "interval": Object { - "fill": "#326EF4", + "fill": "#9DA7B6", "height": 12, }, "line": Object { "linkLine": Object { - "fill": "#326EF4", + "fill": "#565C64", "opacity": 0.6, "size": 1.5, }, "point": Object { - "fill": "#326EF4", + "fill": "#565C64", "opacity": 1, "size": 2.2, }, @@ -2094,7 +2108,7 @@ Object { "fontFamily": "Roboto, PingFangSC, Microsoft YaHei, Arial, sans-serif", "fontSize": 12, "fontWeight": "normal", - "linkTextFill": "#326EF4", + "linkTextFill": "#565C64", "maxLines": 1, "opacity": 1, "textAlign": "right", @@ -2104,13 +2118,13 @@ Object { }, }, "prepareSelectMask": Object { - "backgroundColor": "#234DAB", + "backgroundColor": "#6E757F", "backgroundOpacity": 0.3, }, "resizeArea": Object { - "background": "#326EF4", + "background": "#9DA7B6", "backgroundOpacity": 0, - "guideLineColor": "#326EF4", + "guideLineColor": "#9DA7B6", "guideLineDash": Array [ 3, 3, @@ -2118,7 +2132,7 @@ Object { "guideLineDisableColor": "rgba(0,0,0,0.25)", "interactionState": Object { "hover": Object { - "backgroundColor": "#326EF4", + "backgroundColor": "#9DA7B6", "backgroundOpacity": 1, }, }, @@ -2132,7 +2146,7 @@ Object { "fontFamily": "Roboto, PingFangSC, Microsoft YaHei, Arial, sans-serif", "fontSize": 12, "fontWeight": 700, - "linkTextFill": "#326EF4", + "linkTextFill": "#565C64", "maxLines": 1, "opacity": 1, "textAlign": "left", @@ -2141,9 +2155,10 @@ Object { "wordWrap": true, }, "cell": Object { - "backgroundColor": "#F5F8FE", + "backgroundColor": "#FAFBFB", "backgroundColorOpacity": 1, - "horizontalBorderColor": "#E0E9FD", + "borderDash": Array [], + "horizontalBorderColor": "#F0F2F4", "horizontalBorderColorOpacity": 1, "horizontalBorderWidth": 1, "interactionState": Object { @@ -2152,7 +2167,7 @@ Object { "backgroundOpacity": 1, }, "hover": Object { - "backgroundColor": "#E0E9FD", + "backgroundColor": "#F0F2F4", "backgroundOpacity": 0.6, }, "prepareSelect": Object { @@ -2165,7 +2180,7 @@ Object { "backgroundOpacity": 1, }, "selected": Object { - "backgroundColor": "#E0E9FD", + "backgroundColor": "#F0F2F4", "backgroundOpacity": 0.6, }, "unselected": Object { @@ -2180,7 +2195,7 @@ Object { "right": 8, "top": 4, }, - "verticalBorderColor": "#E0E9FD", + "verticalBorderColor": "#F0F2F4", "verticalBorderColorOpacity": 1, "verticalBorderWidth": 1, }, @@ -2197,7 +2212,7 @@ Object { "fontFamily": "Roboto, PingFangSC, Microsoft YaHei, Arial, sans-serif", "fontSize": 12, "fontWeight": "normal", - "linkTextFill": "#326EF4", + "linkTextFill": "#565C64", "maxLines": 1, "opacity": 1, "textAlign": "left", @@ -2211,7 +2226,7 @@ Object { "fontFamily": "Roboto, PingFangSC, Microsoft YaHei, Arial, sans-serif", "fontSize": 12, "fontWeight": "normal", - "linkTextFill": "#326EF4", + "linkTextFill": "#565C64", "maxLines": 1, "opacity": 1, "textAlign": "center", @@ -2224,7 +2239,7 @@ Object { "fontFamily": "Roboto, PingFangSC, Microsoft YaHei, Arial, sans-serif", "fontSize": 12, "fontWeight": "normal", - "linkTextFill": "#326EF4", + "linkTextFill": "#565C64", "maxLines": 1, "opacity": 1, "textAlign": "left", @@ -2244,7 +2259,8 @@ Object { "trackColor": "rgba(0,0,0,0.01)", }, "splitLine": Object { - "horizontalBorderColor": "#326EF4", + "borderDash": Array [], + "horizontalBorderColor": "#BAC1CC", "horizontalBorderColorOpacity": 0.2, "horizontalBorderWidth": 2, "shadowColors": Object { @@ -2253,14 +2269,14 @@ Object { }, "shadowWidth": 8, "showShadow": true, - "verticalBorderColor": "#326EF4", + "verticalBorderColor": "#BAC1CC", "verticalBorderColorOpacity": 0.25, "verticalBorderWidth": 2, }, } `; -exports[`SpreadSheet Theme Tests Theme Default Value Tests should get gray theme 1`] = ` +exports[`SpreadSheet Theme Tests Theme Default Value Tests should get pivot sheet default theme 1`] = ` Object { "background": Object { "color": "#FFFFFF", @@ -2280,9 +2296,10 @@ Object { "wordWrap": true, }, "cell": Object { - "backgroundColor": "#F0F2F4", + "backgroundColor": "#E0E9FD", "backgroundColorOpacity": 1, - "horizontalBorderColor": "#E7E9ED", + "borderDash": Array [], + "horizontalBorderColor": "#CCDBFC", "horizontalBorderColorOpacity": 1, "horizontalBorderWidth": 1, "interactionState": Object { @@ -2291,7 +2308,7 @@ Object { "backgroundOpacity": 1, }, "hover": Object { - "backgroundColor": "#E7E9ED", + "backgroundColor": "#CCDBFC", "backgroundOpacity": 0.6, }, "prepareSelect": Object { @@ -2304,7 +2321,7 @@ Object { "backgroundOpacity": 1, }, "selected": Object { - "backgroundColor": "#E7E9ED", + "backgroundColor": "#CCDBFC", "backgroundOpacity": 0.6, }, "unselected": Object { @@ -2319,7 +2336,7 @@ Object { "right": 8, "top": 4, }, - "verticalBorderColor": "#E7E9ED", + "verticalBorderColor": "#CCDBFC", "verticalBorderColorOpacity": 1, "verticalBorderWidth": 1, }, @@ -2366,15 +2383,16 @@ Object { "fontWeight": 700, "maxLines": 1, "opacity": 1, - "textAlign": "left", + "textAlign": "right", "textBaseline": "middle", "textOverflow": "ellipsis", "wordWrap": true, }, "cell": Object { - "backgroundColor": "#F0F2F4", + "backgroundColor": "#E0E9FD", "backgroundColorOpacity": 1, - "horizontalBorderColor": "#E7E9ED", + "borderDash": Array [], + "horizontalBorderColor": "#CCDBFC", "horizontalBorderColorOpacity": 1, "horizontalBorderWidth": 1, "padding": Object { @@ -2383,7 +2401,7 @@ Object { "right": 8, "top": 4, }, - "verticalBorderColor": "#E7E9ED", + "verticalBorderColor": "#CCDBFC", "verticalBorderColorOpacity": 1, "verticalBorderWidth": 1, }, @@ -2402,7 +2420,7 @@ Object { "fontWeight": 700, "maxLines": 1, "opacity": 1, - "textAlign": "right", + "textAlign": "left", "textBaseline": "middle", "textOverflow": "ellipsis", "wordWrap": true, @@ -2414,7 +2432,7 @@ Object { "fontFamily": "Roboto, PingFangSC, Microsoft YaHei, Arial, sans-serif", "fontSize": 12, "fontWeight": 700, - "linkTextFill": "#565C64", + "linkTextFill": "#326EF4", "maxLines": 1, "opacity": 1, "textAlign": "right", @@ -2425,8 +2443,8 @@ Object { "cell": Object { "backgroundColor": "#FFFFFF", "backgroundColorOpacity": 1, - "crossBackgroundColor": "#FAFBFB", - "horizontalBorderColor": "#F0F2F4", + "crossBackgroundColor": "#F5F8FE", + "horizontalBorderColor": "#E0E9FD", "horizontalBorderColorOpacity": 1, "horizontalBorderWidth": 1, "interactionState": Object { @@ -2435,11 +2453,11 @@ Object { "backgroundOpacity": 1, }, "hover": Object { - "backgroundColor": "#F0F2F4", + "backgroundColor": "#E0E9FD", "backgroundOpacity": 0.6, }, "hoverFocus": Object { - "backgroundColor": "#F0F2F4", + "backgroundColor": "#E0E9FD", "backgroundOpacity": 0.6, "borderColor": "#000000", "borderOpacity": 1, @@ -2455,7 +2473,7 @@ Object { "backgroundOpacity": 1, }, "selected": Object { - "backgroundColor": "#F0F2F4", + "backgroundColor": "#E0E9FD", "backgroundOpacity": 0.6, }, "unselected": Object { @@ -2470,7 +2488,7 @@ Object { "right": 8, "top": 8, }, - "verticalBorderColor": "#F0F2F4", + "verticalBorderColor": "#E0E9FD", "verticalBorderColorOpacity": 1, "verticalBorderWidth": 1, }, @@ -2484,7 +2502,7 @@ Object { }, "miniChart": Object { "bar": Object { - "fill": "#565C64", + "fill": "#326EF4", "intervalPadding": 4, "opacity": 1, }, @@ -2508,17 +2526,17 @@ Object { }, }, "interval": Object { - "fill": "#9DA7B6", + "fill": "#326EF4", "height": 12, }, "line": Object { "linkLine": Object { - "fill": "#565C64", + "fill": "#326EF4", "opacity": 0.6, "size": 1.5, }, "point": Object { - "fill": "#565C64", + "fill": "#326EF4", "opacity": 1, "size": 2.2, }, @@ -2529,7 +2547,7 @@ Object { "fontFamily": "Roboto, PingFangSC, Microsoft YaHei, Arial, sans-serif", "fontSize": 12, "fontWeight": "normal", - "linkTextFill": "#565C64", + "linkTextFill": "#326EF4", "maxLines": 1, "opacity": 1, "textAlign": "right", @@ -2544,7 +2562,7 @@ Object { "fontFamily": "Roboto, PingFangSC, Microsoft YaHei, Arial, sans-serif", "fontSize": 12, "fontWeight": 700, - "linkTextFill": "#565C64", + "linkTextFill": "#326EF4", "maxLines": 1, "opacity": 1, "textAlign": "right", @@ -2555,8 +2573,8 @@ Object { "cell": Object { "backgroundColor": "#FFFFFF", "backgroundColorOpacity": 1, - "crossBackgroundColor": "#FAFBFB", - "horizontalBorderColor": "#F0F2F4", + "crossBackgroundColor": "#F5F8FE", + "horizontalBorderColor": "#E0E9FD", "horizontalBorderColorOpacity": 1, "horizontalBorderWidth": 1, "interactionState": Object { @@ -2565,11 +2583,11 @@ Object { "backgroundOpacity": 1, }, "hover": Object { - "backgroundColor": "#F0F2F4", + "backgroundColor": "#E0E9FD", "backgroundOpacity": 0.6, }, "hoverFocus": Object { - "backgroundColor": "#F0F2F4", + "backgroundColor": "#E0E9FD", "backgroundOpacity": 0.6, "borderColor": "#000000", "borderOpacity": 1, @@ -2585,7 +2603,7 @@ Object { "backgroundOpacity": 1, }, "selected": Object { - "backgroundColor": "#F0F2F4", + "backgroundColor": "#E0E9FD", "backgroundOpacity": 0.6, }, "unselected": Object { @@ -2600,7 +2618,7 @@ Object { "right": 8, "top": 8, }, - "verticalBorderColor": "#F0F2F4", + "verticalBorderColor": "#E0E9FD", "verticalBorderColorOpacity": 1, "verticalBorderWidth": 1, }, @@ -2614,7 +2632,7 @@ Object { }, "miniChart": Object { "bar": Object { - "fill": "#565C64", + "fill": "#326EF4", "intervalPadding": 4, "opacity": 1, }, @@ -2638,17 +2656,17 @@ Object { }, }, "interval": Object { - "fill": "#9DA7B6", + "fill": "#326EF4", "height": 12, }, "line": Object { "linkLine": Object { - "fill": "#565C64", + "fill": "#326EF4", "opacity": 0.6, "size": 1.5, }, "point": Object { - "fill": "#565C64", + "fill": "#326EF4", "opacity": 1, "size": 2.2, }, @@ -2659,7 +2677,7 @@ Object { "fontFamily": "Roboto, PingFangSC, Microsoft YaHei, Arial, sans-serif", "fontSize": 12, "fontWeight": "normal", - "linkTextFill": "#565C64", + "linkTextFill": "#326EF4", "maxLines": 1, "opacity": 1, "textAlign": "right", @@ -2669,13 +2687,13 @@ Object { }, }, "prepareSelectMask": Object { - "backgroundColor": "#6E757F", + "backgroundColor": "#234DAB", "backgroundOpacity": 0.3, }, "resizeArea": Object { - "background": "#9DA7B6", + "background": "#326EF4", "backgroundOpacity": 0, - "guideLineColor": "#9DA7B6", + "guideLineColor": "#326EF4", "guideLineDash": Array [ 3, 3, @@ -2683,7 +2701,7 @@ Object { "guideLineDisableColor": "rgba(0,0,0,0.25)", "interactionState": Object { "hover": Object { - "backgroundColor": "#9DA7B6", + "backgroundColor": "#326EF4", "backgroundOpacity": 1, }, }, @@ -2697,7 +2715,7 @@ Object { "fontFamily": "Roboto, PingFangSC, Microsoft YaHei, Arial, sans-serif", "fontSize": 12, "fontWeight": 700, - "linkTextFill": "#565C64", + "linkTextFill": "#326EF4", "maxLines": 1, "opacity": 1, "textAlign": "left", @@ -2706,9 +2724,10 @@ Object { "wordWrap": true, }, "cell": Object { - "backgroundColor": "#FAFBFB", + "backgroundColor": "#F5F8FE", "backgroundColorOpacity": 1, - "horizontalBorderColor": "#F0F2F4", + "borderDash": Array [], + "horizontalBorderColor": "#E0E9FD", "horizontalBorderColorOpacity": 1, "horizontalBorderWidth": 1, "interactionState": Object { @@ -2717,7 +2736,7 @@ Object { "backgroundOpacity": 1, }, "hover": Object { - "backgroundColor": "#F0F2F4", + "backgroundColor": "#E0E9FD", "backgroundOpacity": 0.6, }, "prepareSelect": Object { @@ -2730,7 +2749,7 @@ Object { "backgroundOpacity": 1, }, "selected": Object { - "backgroundColor": "#F0F2F4", + "backgroundColor": "#E0E9FD", "backgroundOpacity": 0.6, }, "unselected": Object { @@ -2745,7 +2764,7 @@ Object { "right": 8, "top": 4, }, - "verticalBorderColor": "#F0F2F4", + "verticalBorderColor": "#E0E9FD", "verticalBorderColorOpacity": 1, "verticalBorderWidth": 1, }, @@ -2762,7 +2781,7 @@ Object { "fontFamily": "Roboto, PingFangSC, Microsoft YaHei, Arial, sans-serif", "fontSize": 12, "fontWeight": "normal", - "linkTextFill": "#565C64", + "linkTextFill": "#326EF4", "maxLines": 1, "opacity": 1, "textAlign": "left", @@ -2776,7 +2795,7 @@ Object { "fontFamily": "Roboto, PingFangSC, Microsoft YaHei, Arial, sans-serif", "fontSize": 12, "fontWeight": "normal", - "linkTextFill": "#565C64", + "linkTextFill": "#326EF4", "maxLines": 1, "opacity": 1, "textAlign": "center", @@ -2789,7 +2808,7 @@ Object { "fontFamily": "Roboto, PingFangSC, Microsoft YaHei, Arial, sans-serif", "fontSize": 12, "fontWeight": "normal", - "linkTextFill": "#565C64", + "linkTextFill": "#326EF4", "maxLines": 1, "opacity": 1, "textAlign": "left", @@ -2809,7 +2828,8 @@ Object { "trackColor": "rgba(0,0,0,0.01)", }, "splitLine": Object { - "horizontalBorderColor": "#BAC1CC", + "borderDash": Array [], + "horizontalBorderColor": "#326EF4", "horizontalBorderColorOpacity": 0.2, "horizontalBorderWidth": 2, "shadowColors": Object { @@ -2818,7 +2838,576 @@ Object { }, "shadowWidth": 8, "showShadow": true, - "verticalBorderColor": "#BAC1CC", + "verticalBorderColor": "#326EF4", + "verticalBorderColorOpacity": 0.25, + "verticalBorderWidth": 2, + }, +} +`; + +exports[`SpreadSheet Theme Tests Theme Default Value Tests should get table sheet theme 1`] = ` +Object { + "background": Object { + "color": "#FFFFFF", + "opacity": 1, + }, + "colCell": Object { + "bolderText": Object { + "fill": "#000000", + "fontFamily": "Roboto, PingFangSC, Microsoft YaHei, Arial, sans-serif", + "fontSize": 12, + "fontWeight": 700, + "maxLines": 1, + "opacity": 1, + "textAlign": "center", + "textBaseline": "middle", + "textOverflow": "ellipsis", + "wordWrap": true, + }, + "cell": Object { + "backgroundColor": "#E0E9FD", + "backgroundColorOpacity": 1, + "borderDash": Array [], + "horizontalBorderColor": "#CCDBFC", + "horizontalBorderColorOpacity": 1, + "horizontalBorderWidth": 1, + "interactionState": Object { + "highlight": Object { + "backgroundColor": "#87B5FF", + "backgroundOpacity": 1, + }, + "hover": Object { + "backgroundColor": "#CCDBFC", + "backgroundOpacity": 0.6, + }, + "prepareSelect": Object { + "borderColor": "#000000", + "borderOpacity": 1, + "borderWidth": 1, + }, + "searchResult": Object { + "backgroundColor": "#F0F7FF", + "backgroundOpacity": 1, + }, + "selected": Object { + "backgroundColor": "#CCDBFC", + "backgroundOpacity": 0.6, + }, + "unselected": Object { + "backgroundOpacity": 0.3, + "opacity": 0.3, + "textOpacity": 0.3, + }, + }, + "padding": Object { + "bottom": 4, + "left": 8, + "right": 8, + "top": 4, + }, + "verticalBorderColor": "#CCDBFC", + "verticalBorderColorOpacity": 1, + "verticalBorderWidth": 1, + }, + "icon": Object { + "fill": "#000000", + "margin": Object { + "bottom": 6, + "left": 4, + "right": 4, + "top": 6, + }, + "size": 10, + }, + "measureText": Object { + "fill": "#000000", + "fontFamily": "Roboto, PingFangSC, Microsoft YaHei, Arial, sans-serif", + "fontSize": 12, + "fontWeight": "normal", + "maxLines": 1, + "opacity": 1, + "textAlign": "right", + "textBaseline": "middle", + "textOverflow": "ellipsis", + "wordWrap": true, + }, + "text": Object { + "fill": "#000000", + "fontFamily": "Roboto, PingFangSC, Microsoft YaHei, Arial, sans-serif", + "fontSize": 12, + "fontWeight": "normal", + "maxLines": 1, + "opacity": 1, + "textAlign": "center", + "textBaseline": "middle", + "textOverflow": "ellipsis", + "wordWrap": true, + }, + }, + "cornerCell": Object { + "bolderText": Object { + "fill": "#000000", + "fontFamily": "Roboto, PingFangSC, Microsoft YaHei, Arial, sans-serif", + "fontSize": 12, + "fontWeight": 700, + "maxLines": 1, + "opacity": 1, + "textAlign": "center", + "textBaseline": "middle", + "textOverflow": "ellipsis", + "wordWrap": true, + }, + "cell": Object { + "backgroundColor": "#E0E9FD", + "backgroundColorOpacity": 1, + "borderDash": Array [], + "horizontalBorderColor": "#CCDBFC", + "horizontalBorderColorOpacity": 1, + "horizontalBorderWidth": 1, + "padding": Object { + "bottom": 4, + "left": 8, + "right": 8, + "top": 4, + }, + "verticalBorderColor": "#CCDBFC", + "verticalBorderColorOpacity": 1, + "verticalBorderWidth": 1, + }, + "icon": Object { + "fill": "#000000", + "margin": Object { + "left": 4, + "right": 4, + }, + "size": 10, + }, + "text": Object { + "fill": "#000000", + "fontFamily": "Roboto, PingFangSC, Microsoft YaHei, Arial, sans-serif", + "fontSize": 12, + "fontWeight": 700, + "maxLines": 1, + "opacity": 1, + "textAlign": "center", + "textBaseline": "middle", + "textOverflow": "ellipsis", + "wordWrap": true, + }, + }, + "dataCell": Object { + "bolderText": Object { + "fill": "#000000", + "fontFamily": "Roboto, PingFangSC, Microsoft YaHei, Arial, sans-serif", + "fontSize": 12, + "fontWeight": 700, + "linkTextFill": "#326EF4", + "maxLines": 1, + "opacity": 1, + "textAlign": "right", + "textBaseline": "middle", + "textOverflow": "ellipsis", + "wordWrap": true, + }, + "cell": Object { + "backgroundColor": "#FFFFFF", + "backgroundColorOpacity": 1, + "crossBackgroundColor": "#F5F8FE", + "horizontalBorderColor": "#E0E9FD", + "horizontalBorderColorOpacity": 1, + "horizontalBorderWidth": 1, + "interactionState": Object { + "highlight": Object { + "backgroundColor": "#87B5FF", + "backgroundOpacity": 1, + }, + "hover": Object { + "backgroundColor": "#E0E9FD", + "backgroundOpacity": 0.6, + }, + "hoverFocus": Object { + "backgroundColor": "#E0E9FD", + "backgroundOpacity": 0.6, + "borderColor": "#000000", + "borderOpacity": 1, + "borderWidth": 1, + }, + "prepareSelect": Object { + "borderColor": "#000000", + "borderOpacity": 1, + "borderWidth": 1, + }, + "searchResult": Object { + "backgroundColor": "#F0F7FF", + "backgroundOpacity": 1, + }, + "selected": Object { + "backgroundColor": "#E0E9FD", + "backgroundOpacity": 0.6, + }, + "unselected": Object { + "backgroundOpacity": 0.3, + "opacity": 0.3, + "textOpacity": 0.3, + }, + }, + "padding": Object { + "bottom": 8, + "left": 8, + "right": 8, + "top": 8, + }, + "verticalBorderColor": "#E0E9FD", + "verticalBorderColorOpacity": 1, + "verticalBorderWidth": 1, + }, + "icon": Object { + "fill": "#000000", + "margin": Object { + "left": 4, + "right": 4, + }, + "size": 10, + }, + "miniChart": Object { + "bar": Object { + "fill": "#326EF4", + "intervalPadding": 4, + "opacity": 1, + }, + "bullet": Object { + "backgroundColor": "#E9E9E9", + "comparativeMeasure": Object { + "fill": "#000000", + "height": 12, + "opacity": 0.25, + "width": 1, + }, + "progressBar": Object { + "height": 10, + "innerHeight": 6, + "widthPercent": 0.6, + }, + "rangeColors": Object { + "bad": "#FF4D4F", + "good": "#29A294", + "satisfactory": "#FAAD14", + }, + }, + "interval": Object { + "fill": "#326EF4", + "height": 12, + }, + "line": Object { + "linkLine": Object { + "fill": "#326EF4", + "opacity": 0.6, + "size": 1.5, + }, + "point": Object { + "fill": "#326EF4", + "opacity": 1, + "size": 2.2, + }, + }, + }, + "text": Object { + "fill": "#000000", + "fontFamily": "Roboto, PingFangSC, Microsoft YaHei, Arial, sans-serif", + "fontSize": 12, + "fontWeight": "normal", + "linkTextFill": "#326EF4", + "maxLines": 1, + "opacity": 1, + "textAlign": "right", + "textBaseline": "middle", + "textOverflow": "ellipsis", + "wordWrap": true, + }, + }, + "mergedCell": Object { + "bolderText": Object { + "fill": "#000000", + "fontFamily": "Roboto, PingFangSC, Microsoft YaHei, Arial, sans-serif", + "fontSize": 12, + "fontWeight": 700, + "linkTextFill": "#326EF4", + "maxLines": 1, + "opacity": 1, + "textAlign": "right", + "textBaseline": "middle", + "textOverflow": "ellipsis", + "wordWrap": true, + }, + "cell": Object { + "backgroundColor": "#FFFFFF", + "backgroundColorOpacity": 1, + "crossBackgroundColor": "#F5F8FE", + "horizontalBorderColor": "#E0E9FD", + "horizontalBorderColorOpacity": 1, + "horizontalBorderWidth": 1, + "interactionState": Object { + "highlight": Object { + "backgroundColor": "#87B5FF", + "backgroundOpacity": 1, + }, + "hover": Object { + "backgroundColor": "#E0E9FD", + "backgroundOpacity": 0.6, + }, + "hoverFocus": Object { + "backgroundColor": "#E0E9FD", + "backgroundOpacity": 0.6, + "borderColor": "#000000", + "borderOpacity": 1, + "borderWidth": 1, + }, + "prepareSelect": Object { + "borderColor": "#000000", + "borderOpacity": 1, + "borderWidth": 1, + }, + "searchResult": Object { + "backgroundColor": "#F0F7FF", + "backgroundOpacity": 1, + }, + "selected": Object { + "backgroundColor": "#E0E9FD", + "backgroundOpacity": 0.6, + }, + "unselected": Object { + "backgroundOpacity": 0.3, + "opacity": 0.3, + "textOpacity": 0.3, + }, + }, + "padding": Object { + "bottom": 8, + "left": 8, + "right": 8, + "top": 8, + }, + "verticalBorderColor": "#E0E9FD", + "verticalBorderColorOpacity": 1, + "verticalBorderWidth": 1, + }, + "icon": Object { + "fill": "#000000", + "margin": Object { + "left": 4, + "right": 4, + }, + "size": 10, + }, + "miniChart": Object { + "bar": Object { + "fill": "#326EF4", + "intervalPadding": 4, + "opacity": 1, + }, + "bullet": Object { + "backgroundColor": "#E9E9E9", + "comparativeMeasure": Object { + "fill": "#000000", + "height": 12, + "opacity": 0.25, + "width": 1, + }, + "progressBar": Object { + "height": 10, + "innerHeight": 6, + "widthPercent": 0.6, + }, + "rangeColors": Object { + "bad": "#FF4D4F", + "good": "#29A294", + "satisfactory": "#FAAD14", + }, + }, + "interval": Object { + "fill": "#326EF4", + "height": 12, + }, + "line": Object { + "linkLine": Object { + "fill": "#326EF4", + "opacity": 0.6, + "size": 1.5, + }, + "point": Object { + "fill": "#326EF4", + "opacity": 1, + "size": 2.2, + }, + }, + }, + "text": Object { + "fill": "#000000", + "fontFamily": "Roboto, PingFangSC, Microsoft YaHei, Arial, sans-serif", + "fontSize": 12, + "fontWeight": "normal", + "linkTextFill": "#326EF4", + "maxLines": 1, + "opacity": 1, + "textAlign": "right", + "textBaseline": "middle", + "textOverflow": "ellipsis", + "wordWrap": true, + }, + }, + "prepareSelectMask": Object { + "backgroundColor": "#234DAB", + "backgroundOpacity": 0.3, + }, + "resizeArea": Object { + "background": "#326EF4", + "backgroundOpacity": 0, + "guideLineColor": "#326EF4", + "guideLineDash": Array [ + 3, + 3, + ], + "guideLineDisableColor": "rgba(0,0,0,0.25)", + "interactionState": Object { + "hover": Object { + "backgroundColor": "#326EF4", + "backgroundOpacity": 1, + }, + }, + "minCellHeight": 40, + "minCellWidth": 42, + "size": 3, + }, + "rowCell": Object { + "bolderText": Object { + "fill": "#000000", + "fontFamily": "Roboto, PingFangSC, Microsoft YaHei, Arial, sans-serif", + "fontSize": 12, + "fontWeight": 700, + "linkTextFill": "#326EF4", + "maxLines": 1, + "opacity": 1, + "textAlign": "center", + "textBaseline": "middle", + "textOverflow": "ellipsis", + "wordWrap": true, + }, + "cell": Object { + "backgroundColor": "#F5F8FE", + "backgroundColorOpacity": 1, + "borderDash": Array [], + "horizontalBorderColor": "#E0E9FD", + "horizontalBorderColorOpacity": 1, + "horizontalBorderWidth": 1, + "interactionState": Object { + "highlight": Object { + "backgroundColor": "#87B5FF", + "backgroundOpacity": 1, + }, + "hover": Object { + "backgroundColor": "#E0E9FD", + "backgroundOpacity": 0.6, + }, + "prepareSelect": Object { + "borderColor": "#000000", + "borderOpacity": 1, + "borderWidth": 1, + }, + "searchResult": Object { + "backgroundColor": "#F0F7FF", + "backgroundOpacity": 1, + }, + "selected": Object { + "backgroundColor": "#E0E9FD", + "backgroundOpacity": 0.6, + }, + "unselected": Object { + "backgroundOpacity": 0.3, + "opacity": 0.3, + "textOpacity": 0.3, + }, + }, + "padding": Object { + "bottom": 4, + "left": 8, + "right": 8, + "top": 4, + }, + "verticalBorderColor": "#E0E9FD", + "verticalBorderColorOpacity": 1, + "verticalBorderWidth": 1, + }, + "icon": Object { + "fill": "#000000", + "margin": Object { + "left": 4, + "right": 4, + }, + "size": 10, + }, + "measureText": Object { + "fill": "#000000", + "fontFamily": "Roboto, PingFangSC, Microsoft YaHei, Arial, sans-serif", + "fontSize": 12, + "fontWeight": "normal", + "linkTextFill": "#326EF4", + "maxLines": 1, + "opacity": 1, + "textAlign": "center", + "textBaseline": "middle", + "textOverflow": "ellipsis", + "wordWrap": true, + }, + "seriesNumberWidth": 80, + "seriesText": Object { + "fill": "#000000", + "fontFamily": "Roboto, PingFangSC, Microsoft YaHei, Arial, sans-serif", + "fontSize": 12, + "fontWeight": "normal", + "linkTextFill": "#326EF4", + "maxLines": 1, + "opacity": 1, + "textAlign": "center", + "textBaseline": "middle", + "textOverflow": "ellipsis", + "wordWrap": true, + }, + "text": Object { + "fill": "#000000", + "fontFamily": "Roboto, PingFangSC, Microsoft YaHei, Arial, sans-serif", + "fontSize": 12, + "fontWeight": "normal", + "linkTextFill": "#326EF4", + "maxLines": 1, + "opacity": 1, + "textAlign": "center", + "textBaseline": "middle", + "textOverflow": "ellipsis", + "wordWrap": true, + }, + }, + "scrollBar": Object { + "hoverSize": 8, + "lineCap": "round", + "size": 6, + "thumbColor": "rgba(0,0,0,0.15)", + "thumbHorizontalMinSize": 32, + "thumbHoverColor": "rgba(0,0,0,0.25)", + "thumbVerticalMinSize": 32, + "trackColor": "rgba(0,0,0,0.01)", + }, + "splitLine": Object { + "borderDash": Array [], + "horizontalBorderColor": "#326EF4", + "horizontalBorderColorOpacity": 0.2, + "horizontalBorderWidth": 2, + "shadowColors": Object { + "left": "rgba(0,0,0,0.1)", + "right": "rgba(0,0,0,0)", + }, + "shadowWidth": 8, + "showShadow": true, + "verticalBorderColor": "#326EF4", "verticalBorderColorOpacity": 0.25, "verticalBorderWidth": 2, }, diff --git a/packages/s2-core/__tests__/spreadsheet/corner-spec.ts b/packages/s2-core/__tests__/spreadsheet/corner-spec.ts index 0336cbc086..4acec56dde 100644 --- a/packages/s2-core/__tests__/spreadsheet/corner-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/corner-spec.ts @@ -49,6 +49,7 @@ describe('PivotSheet Corner Tests', () => { fields: { ...simpleDataConfig.fields, columns: [], + values: ['price'], }, }); s2.setOptions({ @@ -96,6 +97,15 @@ describe('PivotSheet Corner Tests', () => { }, }, }); + + s2.setDataCfg({ + ...simpleDataConfig, + fields: { + ...simpleDataConfig.fields, + values: ['price'], + }, + }); + await s2.render(); const cornerNodes = s2.facet.getCornerNodes(); @@ -315,11 +325,9 @@ describe('PivotSheet Corner Tests', () => { expect(cornerNode.value).toEqual(cornerText); - const cell = s2.facet.cornerHeader.children[0]; + const cornerCell = s2.facet.getCornerCells()[0]; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(cell.actualText).toEqual(cornerText); + expect(cornerCell.getActualText()).toEqual(cornerText); }); test('should get custom corner extra text when hierarchy type is tree', async () => { @@ -343,10 +351,8 @@ describe('PivotSheet Corner Tests', () => { expect(cornerNode.value).toEqual(cornerExtraFieldText); - const cell = s2.facet.cornerHeader.children[2]; + const cornerCell = s2.facet.getCornerCells()[2]; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(cell.actualText).toEqual(cornerExtraFieldText); + expect(cornerCell.getActualText()).toEqual(cornerExtraFieldText); }); }); diff --git a/packages/s2-core/__tests__/spreadsheet/custom-cell-style-spec.ts b/packages/s2-core/__tests__/spreadsheet/custom-cell-style-spec.ts index e8a04a4caf..fad262d910 100644 --- a/packages/s2-core/__tests__/spreadsheet/custom-cell-style-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/custom-cell-style-spec.ts @@ -4,12 +4,13 @@ import { getContainer, } from 'tests/util/helpers'; import * as dataConfig from 'tests/data/mock-dataset.json'; +import * as simpleDataConfig from 'tests/data/simple-data.json'; import { customColSimpleColumns } from '../data/custom-table-col-fields'; import { EXTRA_FIELD, type S2DataConfig } from '@/common'; import type { ViewMeta } from '@/common/interface/basic'; import type { Node } from '@/facet/layout/node'; import type { S2Options } from '@/common/interface'; -import { TableSheet, type SpreadSheet } from '@/sheet-type'; +import { TableSheet, type SpreadSheet, PivotSheet } from '@/sheet-type'; describe('SpreadSheet Custom Cell Style Tests', () => { let s2: SpreadSheet; @@ -37,7 +38,7 @@ describe('SpreadSheet Custom Cell Style Tests', () => { }); afterEach(() => { - s2.destroy(); + // s2.destroy(); }); test('should render default cell style', () => { @@ -386,17 +387,27 @@ describe('SpreadSheet Custom Cell Style Tests', () => { }); test('should get custom col cell style if measure column hidden', async () => { - const sheet = createPivotSheet({ - ...s2Options, - style: { - colCell: { - hideValue: true, - widthByField: { - 'root[&]笔': 100, + const sheet = new PivotSheet( + getContainer(), + { + ...simpleDataConfig, + fields: { + ...simpleDataConfig.fields, + values: ['price'], + }, + }, + { + ...s2Options, + style: { + colCell: { + hideValue: true, + widthByField: { + 'root[&]笔': 100, + }, }, }, }, - }); + ); await sheet.render(); diff --git a/packages/s2-core/__tests__/spreadsheet/custom-grid-spec.ts b/packages/s2-core/__tests__/spreadsheet/custom-grid-spec.ts index 5c4a40292f..05c7d307a8 100644 --- a/packages/s2-core/__tests__/spreadsheet/custom-grid-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/custom-grid-spec.ts @@ -3,7 +3,6 @@ import { CustomGridData } from 'tests/data/data-custom-grid'; import { getContainer } from 'tests/util/helpers'; import { pick } from 'lodash'; import { waitForRender } from 'tests/util'; -import type { HeaderCell } from '../../src/cell/header-cell'; import { KEY_GROUP_COL_RESIZE_AREA } from '../../src/common/constant'; import { CustomGridPivotDataSet } from '../../src/data-set/custom-grid-pivot-data-set'; import { @@ -186,9 +185,7 @@ describe('SpreadSheet Custom Grid Tests', () => { ); test('should render custom format corner text', () => { - const cornerCellLabels = ( - s2.facet.cornerHeader.children as HeaderCell[] - ).map((cell: HeaderCell) => { + const cornerCellLabels = s2.facet.getCornerCells().map((cell) => { const value = cell.getActualText(); const meta = cell.getMeta(); @@ -363,9 +360,7 @@ describe('SpreadSheet Custom Grid Tests', () => { ); test('should render custom format corner text', () => { - const cornerCellLabels = ( - s2.facet.cornerHeader.children as HeaderCell[] - ).map((cell: HeaderCell) => { + const cornerCellLabels = s2.facet.getCornerCells().map((cell) => { const value = cell.getActualText(); const meta = cell.getMeta(); diff --git a/packages/s2-core/__tests__/spreadsheet/custom-tree-spec.ts b/packages/s2-core/__tests__/spreadsheet/custom-tree-spec.ts index 8c5ecdd755..854a168503 100644 --- a/packages/s2-core/__tests__/spreadsheet/custom-tree-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/custom-tree-spec.ts @@ -27,11 +27,9 @@ const s2Options: S2Options = { describe('SpreadSheet Custom Tree Tests', () => { let s2: SpreadSheet; - const getCornerCellLabels = () => - (s2.facet as any) - .getCornerHeader() - .getChildren() - .map((cell: HeaderCell) => cell.getActualText()); + const getCornerCellLabels = () => { + return s2.facet.getCornerCells().map((cell) => cell.getActualText()); + }; const mapRowNodes = (spreadsheet: SpreadSheet) => spreadsheet.facet.getRowLeafNodes().map((node) => { @@ -202,10 +200,7 @@ describe('SpreadSheet Custom Tree Tests', () => { await s2.render(); - const cornerCellLabels = (s2.facet as any) - .getCornerHeader() - .getChildren() - .map((cell: HeaderCell) => cell.getActualText()); + const cornerCellLabels = getCornerCellLabels(); expect(cornerCellLabels).toEqual(['测试', '类型']); }); diff --git a/packages/s2-core/__tests__/spreadsheet/empty-dataset-spec.ts b/packages/s2-core/__tests__/spreadsheet/empty-dataset-spec.ts new file mode 100644 index 0000000000..9f70f1426c --- /dev/null +++ b/packages/s2-core/__tests__/spreadsheet/empty-dataset-spec.ts @@ -0,0 +1,69 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +import { getContainer } from 'tests/util/helpers'; +import type { S2DataConfig } from '../../src'; +import { PivotSheet, TableSheet } from '@/sheet-type'; +import type { S2Options } from '@/common/interface/s2Options'; + +const s2Options: S2Options = { + width: 400, + height: 400, + hierarchyType: 'grid', +}; + +describe('Empty Dataset Structure Tests', () => { + test('should generate placeholder for pivot mode with single dimension', async () => { + const container = getContainer(); + + const s2DataCfg: S2DataConfig = { + fields: { + rows: ['province', 'city'], + columns: ['type'], + values: ['price'], + valueInCols: true, + }, + data: [], + }; + const s2 = new PivotSheet(container, s2DataCfg, s2Options); + + await s2.render(); + + // @ts-ignore + expect(s2.facet.panelScrollGroupIndexes).toEqual([0, 0, 0, 0]); + }); + + test('should generate placeholder for pivot mode with two dimensions', async () => { + const container = getContainer(); + + const s2DataCfg: S2DataConfig = { + fields: { + rows: ['province', 'city'], + columns: ['type'], + values: ['price', 'cost'], + valueInCols: true, + }, + data: [], + }; + + const s2 = new PivotSheet(container, s2DataCfg, s2Options); + + await s2.render(); + // @ts-ignore + expect(s2.facet.panelScrollGroupIndexes).toEqual([0, 1, 0, 0]); + }); + + test(`shouldn't generate placeholder for table mode`, async () => { + const container = getContainer(); + + const s2DataCfg: S2DataConfig = { + fields: { + columns: ['province', 'city', 'type', 'price', 'cost'], + }, + data: [], + }; + const s2 = new TableSheet(container, s2DataCfg, s2Options); + + await s2.render(); + // @ts-ignore + expect(s2.facet.panelScrollGroupIndexes).toEqual([]); + }); +}); diff --git a/packages/s2-core/__tests__/spreadsheet/empty-string-values-spec.ts b/packages/s2-core/__tests__/spreadsheet/empty-string-values-spec.ts new file mode 100644 index 0000000000..ea9933c33e --- /dev/null +++ b/packages/s2-core/__tests__/spreadsheet/empty-string-values-spec.ts @@ -0,0 +1,124 @@ +import { getContainer } from 'tests/util/helpers'; +import { LayoutWidthType, type S2DataConfig, type S2Options } from '@/common'; +import { PivotSheet, SpreadSheet } from '@/sheet-type'; + +const s2Options: S2Options = { + debug: true, + width: 600, + height: 400, + hierarchyType: 'grid', + style: { + layoutWidthType: LayoutWidthType.Adaptive, + dataCell: { + height: 30, + }, + }, +}; + +const testDataCfg: S2DataConfig = { + meta: [ + { + field: 'first', + name: '一级维度', + }, + { + field: 'second', + name: '二级维度', + }, + { + field: 'number', + name: '数值', + }, + ], + fields: { + rows: ['first', 'second'], + columns: [], + values: ['number'], + valueInCols: true, + }, + data: [ + { + first: '', + second: '维值1', + number: 100, + }, + { + first: '', + second: '维值2', + number: 200, + }, + { + first: null, + second: '维值3', + number: 300, + }, + { + first: null, + second: '维值4', + number: 400, + }, + { + first: '非空维度', + second: '维值5', + number: 500, + }, + { + first: '非空维度', + second: '维值6', + number: 600, + }, + ], +}; + +describe('Empty String Values Tests', () => { + let s2: SpreadSheet; + + beforeEach(() => { + s2 = new PivotSheet(getContainer(), testDataCfg, s2Options); + s2.render(); + }); + + test('should get correctly first dimension values', () => { + const values = s2.dataSet.getDimensionValues('first'); + + expect(values).toEqual(['', 'null', '非空维度']); + }); + + test('should get correctly second dimension values', () => { + const values = s2.dataSet.getDimensionValues('second'); + + expect(values).toEqual([ + '维值1', + '维值2', + '维值3', + '维值4', + '维值5', + '维值6', + ]); + }); + + test('should get correctly second dimension values by specific query', () => { + let values = s2.dataSet.getDimensionValues('second', { first: '' }); + + expect(values).toEqual(['维值1', '维值2']); + + values = s2.dataSet.getDimensionValues('second', { first: 'null' }); + expect(values).toEqual(['维值3', '维值4']); + + values = s2.dataSet.getDimensionValues('second', { first: '非空维度' }); + expect(values).toEqual(['维值5', '维值6']); + }); + + test('should get correctly layout result', () => { + const nodes = s2.facet.getLayoutResult().rowLeafNodes; + + expect(nodes.map((node) => node.id)).toEqual([ + 'root[&][&]维值1', + 'root[&][&]维值2', + 'root[&]null[&]维值3', + 'root[&]null[&]维值4', + 'root[&]非空维度[&]维值5', + 'root[&]非空维度[&]维值6', + ]); + }); +}); diff --git a/packages/s2-core/__tests__/spreadsheet/header-action-icons-spec.ts b/packages/s2-core/__tests__/spreadsheet/header-action-icons-spec.ts index 993e89ef0d..4a9390d360 100644 --- a/packages/s2-core/__tests__/spreadsheet/header-action-icons-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/header-action-icons-spec.ts @@ -183,21 +183,21 @@ describe('HeaderActionIcons Tests', () => { ], Array [ Object { - "x": 158, + "x": 158.5, "y": 9.5, }, Object { - "x": 200, + "x": 200.5, "y": 9.5, }, ], Array [ Object { - "x": 158, + "x": 158.5, "y": 39.5, }, Object { - "x": 200, + "x": 200.5, "y": 39.5, }, ], @@ -281,13 +281,13 @@ describe('HeaderActionIcons Tests', () => { }, Object { "cfg": Object { - "x": 186, + "x": 186.5, "y": 9.5, }, }, Object { "cfg": Object { - "x": 186, + "x": 186.5, "y": 39.5, }, }, diff --git a/packages/s2-core/__tests__/spreadsheet/hidden-columns-spec.ts b/packages/s2-core/__tests__/spreadsheet/hidden-columns-spec.ts index ac6c7760e1..19cc8de525 100644 --- a/packages/s2-core/__tests__/spreadsheet/hidden-columns-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/hidden-columns-spec.ts @@ -3,11 +3,11 @@ import * as mockDataConfig from 'tests/data/mock-dataset.json'; import * as mockPivotDataConfig from 'tests/data/simple-data.json'; import * as mockTableDataConfig from 'tests/data/simple-table-data.json'; import { waitForRender } from 'tests/util'; -import { getContainer } from 'tests/util/helpers'; +import { createPivotSheet, getContainer } from 'tests/util/helpers'; import { customColGridSimpleFields } from '../data/custom-grid-simple-fields'; import { customColMultipleColumns } from '../data/custom-table-col-fields'; import { PivotSheet, TableSheet } from '@/sheet-type'; -import type { HiddenColumnsInfo, S2Options } from '@/common'; +import type { HiddenColumnsInfo, S2DataConfig, S2Options } from '@/common'; const s2Options: S2Options = { width: 400, @@ -213,7 +213,7 @@ describe('SpreadSheet Hidden Columns Tests', () => { let pivotSheet: PivotSheet; - const pivotDataCfg = { + const pivotDataCfg: S2DataConfig = { ...mockPivotDataConfig, fields: { rows: ['province'], @@ -229,7 +229,7 @@ describe('SpreadSheet Hidden Columns Tests', () => { }); afterEach(() => { - pivotSheet.destroy(); + // pivotSheet.destroy(); }); test('should get init column node', () => { @@ -297,7 +297,7 @@ describe('SpreadSheet Hidden Columns Tests', () => { ); }); - test('should not rerender after hidden empty column fields if disable force render', () => { + test('should not rerender after hidden empty column fields if disable force render', async () => { const defaultHiddenColumnsDetail = [ null, ] as unknown as HiddenColumnsInfo[]; @@ -308,7 +308,7 @@ describe('SpreadSheet Hidden Columns Tests', () => { .spyOn(pivotSheet, 'render') .mockImplementationOnce(async () => {}); - pivotSheet.interaction.hideColumns([], false); + await pivotSheet.interaction.hideColumns([], false); const hiddenColumnsDetail = pivotSheet.store.get('hiddenColumnsDetail'); @@ -316,7 +316,7 @@ describe('SpreadSheet Hidden Columns Tests', () => { expect(hiddenColumnsDetail).toEqual(defaultHiddenColumnsDetail); }); - test('should rerender after hidden empty column fields if enable force render', () => { + test('should rerender after hidden empty column fields if enable force render', async () => { const defaultHiddenColumnsDetail = [ null, ] as unknown as HiddenColumnsInfo[]; @@ -327,7 +327,7 @@ describe('SpreadSheet Hidden Columns Tests', () => { .spyOn(pivotSheet, 'render') .mockImplementationOnce(async () => {}); - pivotSheet.interaction.hideColumns([], true); + await pivotSheet.interaction.hideColumns([], true); const hiddenColumnsDetail = pivotSheet.store.get('hiddenColumnsDetail'); @@ -402,13 +402,8 @@ describe('SpreadSheet Hidden Columns Tests', () => { .getColNodes() .find((node) => node.isGrandTotals)!; - const rootNode = pivotSheet.facet - .getColNodes() - .find((node) => node.id === 'root[&]笔')!; - - const parentNode = pivotSheet.facet - .getColNodes() - .find((node) => node.id === 'root[&]笔[&]义乌')!; + const rootNode = pivotSheet.facet.getColNodeById('root[&]笔')!; + const parentNode = pivotSheet.facet.getColNodeById('root[&]笔[&]义乌')!; const hiddenColumnsInfo = pivotSheet.store.get('hiddenColumnsDetail')[0]; @@ -420,6 +415,67 @@ describe('SpreadSheet Hidden Columns Tests', () => { expect(parentNode.hiddenChildNodeInfo).toEqual(hiddenColumnsInfo); }); + // https://github.com/antvis/S2/issues/2355 + test('should render correctly x and width after hide columns when there is only one value for the higher-level dimension.', async () => { + const nodeId = 'root[&]笔[&]义乌[&]price'; + + pivotSheet.setOptions({ + style: { + colCell: { + width: 100, + }, + }, + }); + const data = pivotSheet.dataCfg.data.map((i) => ({ ...i, cost: 0 })); + + pivotSheet.setDataCfg({ + data, + fields: { + values: ['cost', 'price'], + }, + }); + await pivotSheet.render(); + + await pivotSheet.interaction.hideColumns([nodeId]); + + const rootNode = pivotSheet.facet.getColNodeById('root[&]笔'); + + expect(rootNode!.width).toEqual(300); + expect(rootNode!.x).toEqual(0); + }); + + // https://github.com/antvis/S2/issues/2194 + test('should render correctly when always hidden last column', async () => { + const sheet = createPivotSheet( + { + interaction: { + hiddenColumnFields: [], + }, + }, + { useSimpleData: false }, + ); + + await sheet.render(); + + // 模拟一列一列的手动隐藏最后一列 + const colIds = [ + 'root[&]办公用品[&]纸张[&]数量', + 'root[&]办公用品[&]笔[&]数量', + 'root[&]家具[&]沙发[&]数量', + ]; + + await Promise.all( + colIds.map(async (field) => { + await sheet.interaction.hideColumns([field]); + }), + ); + + const leafNodes = sheet.facet.getColLeafNodes(); + + expect(leafNodes).toHaveLength(1); + expect(leafNodes[0].id).toEqual('root[&]家具[&]桌子[&]数量'); + }); + test('should hide columns for multiple columns', async () => { const hiddenColumns = [ 'root[&]自定义节点 a-1[&]自定义节点 a-1-1[&]指标1', @@ -512,9 +568,7 @@ describe('SpreadSheet Hidden Columns Tests', () => { await sheet.interaction.hideColumns([id]); }); - const totalsSiblingNode = sheet.facet - .getColNodes() - .find((node) => node.id === 'root[&]家具')!; + const totalsSiblingNode = sheet.facet.getColNodeById('root[&]家具')!; expect(totalsSiblingNode.x).toEqual(x); expect(totalsSiblingNode.width).toEqual(width); @@ -526,8 +580,8 @@ describe('SpreadSheet Hidden Columns Tests', () => { test('should hide measure node', async () => { const nodeIds = [ - 'root[&]家具[&]桌子[&]number', - 'root[&]办公用品[&]笔[&]number', + 'root[&]家具[&]桌子[&]数量', + 'root[&]办公用品[&]笔[&]数量', ]; await waitForRender(sheet, async () => { @@ -545,8 +599,8 @@ describe('SpreadSheet Hidden Columns Tests', () => { const nodeIds = [ 'root[&]总计', 'root[&]家具[&]小计', - 'root[&]家具[&]桌子[&]number', - 'root[&]办公用品[&]笔[&]number', + 'root[&]家具[&]桌子[&]数量', + 'root[&]办公用品[&]笔[&]数量', ]; await waitForRender(sheet, async () => { @@ -564,39 +618,12 @@ describe('SpreadSheet Hidden Columns Tests', () => { expect(colsHierarchy.sampleNodeForLastLevel?.height).toStrictEqual(30); expect(colsHierarchy.sampleNodeForLastLevel?.y).toStrictEqual(60); expect(colsHierarchy.height).toStrictEqual(90); - expect(colCornerNodesMeta).toMatchInlineSnapshot(` - Array [ - Object { - "height": 30, - "width": 119, - "x": 0, - "y": 60, - }, - Object { - "height": 30, - "width": 119, - "x": 119, - "y": 60, - }, - Object { - "height": 30, - "width": 238, - "x": 0, - "y": 0, - }, - Object { - "height": 30, - "width": 238, - "x": 0, - "y": 30, - }, - ] - `); + expect(colCornerNodesMeta).toMatchSnapshot(); }); // https://github.com/antvis/S2/issues/1721 - test('should hide grand totals node1', async () => { - const nodeId = 'root[&]总计[&]sub_type'; + test('should hide grand totals node', async () => { + const nodeId = 'root[&]总计[&]子类别'; sheet.setDataCfg({ ...mockDataConfig, @@ -607,16 +634,37 @@ describe('SpreadSheet Hidden Columns Tests', () => { valueInCols: true, }, }); + await sheet.render(); - await waitForRender(sheet, async () => { - await sheet.interaction.hideColumns([nodeId]); - }); + await sheet.interaction.hideColumns([nodeId]); const leafNodes = sheet.facet.getColLeafNodes(); expect(leafNodes.some((node) => node.id === nodeId)).toBeFalsy(); expect(leafNodes).toHaveLength(5); }); + + test.each(['grid', 'tree'] as S2Options['hierarchyType'][])( + 'hiding the column totals should not hide the row totals for %s mode', + async (hierarchyType) => { + sheet.setOptions({ hierarchyType }); + await sheet.render(); + + const nodeId = 'root[&]总计'; + const preRowNodes = sheet.facet.getRowNodes(); + const preColumnNodes = sheet.facet.getColNodes(); + + await waitForRender(sheet, async () => { + await sheet.interaction.hideColumns([nodeId]); + }); + + expect(sheet.facet.getRowNodes()[0].id).toBe(nodeId); + expect(sheet.facet.getRowNodes().length).toBe(preRowNodes.length); + expect(sheet.facet.getColNodes().length).toBe( + preColumnNodes.length - 1, + ); + }, + ); }); }); }); diff --git a/packages/s2-core/__tests__/spreadsheet/interaction-brush-selection-scroll-spec.ts b/packages/s2-core/__tests__/spreadsheet/interaction-brush-selection-scroll-spec.ts index a1a8739ac9..a579f830b5 100644 --- a/packages/s2-core/__tests__/spreadsheet/interaction-brush-selection-scroll-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/interaction-brush-selection-scroll-spec.ts @@ -15,6 +15,7 @@ import { TableSheet, SpreadSheet, type S2CellType, + LayoutWidthType, } from '@/index'; const data = getMockData( @@ -55,7 +56,7 @@ const options: S2Options = { showSeriesNumber: true, placeholder: '', style: { - layoutWidthType: 'compact', + layoutWidthType: LayoutWidthType.Compact, dataCell: { height: 32, }, diff --git a/packages/s2-core/__tests__/spreadsheet/miss-dimension-values-spec.ts b/packages/s2-core/__tests__/spreadsheet/miss-dimension-values-spec.ts new file mode 100644 index 0000000000..7fd84b7ba9 --- /dev/null +++ b/packages/s2-core/__tests__/spreadsheet/miss-dimension-values-spec.ts @@ -0,0 +1,269 @@ +import { getContainer } from 'tests/util/helpers'; +import { + EMPTY_FIELD_VALUE, + LayoutWidthType, + ORIGIN_FIELD, + type S2DataConfig, + type S2Options, +} from '@/common'; +import { PivotSheet, SpreadSheet } from '@/sheet-type'; + +const s2Options: S2Options = { + debug: true, + width: 600, + height: 400, + hierarchyType: 'grid', + totals: { + row: { + showGrandTotals: false, + showSubTotals: { + always: false, + }, + reverseGrandTotalsLayout: false, + reverseSubTotalsLayout: false, + subTotalsDimensions: ['first', 'second'], + }, + col: { + showGrandTotals: false, + showSubTotals: false, + reverseGrandTotalsLayout: false, + reverseSubTotalsLayout: false, + subTotalsDimensions: [], + }, + }, + style: { + layoutWidthType: LayoutWidthType.Adaptive, + dataCell: { + height: 30, + }, + }, + showDefaultHeaderActionIcon: false, +}; + +const testDataCfg: S2DataConfig = { + meta: [ + { + field: 'first', + name: '一级维度', + }, + { + field: 'second', + name: '二级维度', + }, + { + field: 'third', + name: '三级维度', + }, + { + field: 'number', + name: '数值', + }, + ], + fields: { + rows: ['first', 'second', 'third'], + columns: [], + values: ['number'], + valueInCols: true, + }, + data: [ + { + first: '总计', + number: 1732771, + }, + { + first: '维值-1', + second: '维值-2', + third: '维度-3', + number: 172245, + }, + { + first: '维值-1', + second: '维值-2', + third: '维度-3', + number: 12222, + }, + { + first: '维值-1', + second: '维值-3', + third: '维值-3', + number: 11111, + }, + { + first: '维值-1', + second: '维值-3', + third: '维度-3', + number: 11111, + }, + { + first: '维值-1', + number: 456, + }, + { + first: '测试-1', + second: '测试-2', + third: '维度-3', + number: 12, + }, + { + first: '测试-1', + second: '测试-2', + third: '维度-3', + number: 4444567, + }, + { + first: '测试-1', + second: '测试-3', + number: 111233, + }, + { + first: '测试-1', + second: '测试-3', + number: 785222, + }, + { + first: '测试-1', + second: '测试-4', + third: '维度-3', + number: 6455644, + }, + { + first: '测试-1', + second: '测试-4', + number: 289898, + }, + { + first: '测试-1', + second: '测试-5', + number: 2222, + }, + { + first: '测试-1', + second: '测试-5', + third: '维度-3', + number: 1111, + }, + { + first: '测试-1', + number: 125555, + }, + { + first: '测试-6', + second: '测试-x', + number: 409090, + }, + { + first: '测试-6', + second: '测试-x', + number: 111111, + }, + { + first: '测试-6', + second: '测试-7', + number: 5555, + }, + { + first: '测试-6', + second: '测试-7', + number: 67878, + }, + { + first: '测试-6', + second: '测试-8', + number: 53445.464, + }, + { + first: '测试-6', + second: '测试-8', + number: 456.464, + }, + { + first: '测试-6', + number: 123.416, + }, + ], +}; + +describe('Miss Dimension Values Tests', () => { + let s2: SpreadSheet; + + beforeEach(async () => { + s2 = new PivotSheet(getContainer(), testDataCfg, s2Options); + await s2.render(); + }); + + test('should get correctly empty dimension values', () => { + const emptyDimensionValueNode = s2.facet.getRowNodes()[0].children[0]; + + expect(emptyDimensionValueNode.value).toEqual(EMPTY_FIELD_VALUE); + expect(emptyDimensionValueNode.id).toEqual( + `root[&]总计[&]${EMPTY_FIELD_VALUE}`, + ); + expect(emptyDimensionValueNode.belongsCell!.getActualText()).toEqual('-'); + }); + + test('should get correctly empty dimension values and use custom placeholder text', async () => { + const placeholder = '*'; + + s2.setOptions({ + placeholder, + }); + + await s2.render(false); + + const emptyDimensionValueNode = s2.facet.getRowNodes()[0].children[0]; + + expect(emptyDimensionValueNode.belongsCell!.getActualText()).toEqual( + placeholder, + ); + }); + + test('should generate correct query for empty node', () => { + const emptyDimensionValueNode1 = s2.facet.getRowNodes()[0]; + + expect(emptyDimensionValueNode1.query).toEqual({ + first: '总计', + }); + + const emptyDimensionValueNode2 = s2.facet.getRowNodes()[1]; + + expect(emptyDimensionValueNode2.query).toEqual({ + first: '总计', + }); + }); + + test('should get correctly dimension data and ignore empty dimension value', () => { + const emptyDimensionValueNode = s2.facet.getRowNodes()[0].children[0]; + + const data = s2.dataSet.getCellMultiData({ + query: emptyDimensionValueNode.query!, + }); + const dimensionValues = s2.dataSet.getDimensionValues( + emptyDimensionValueNode.field, + ); + const emptyDimensionDataCell = s2.facet.getDataCells()[0]; + + expect(emptyDimensionValueNode.query).toEqual( + emptyDimensionValueNode.parent!.query, + ); + expect(emptyDimensionDataCell.getMeta().fieldValue).toEqual(1732771); + expect(data[0][ORIGIN_FIELD]).toMatchInlineSnapshot(` + Object { + "first": "总计", + "number": 1732771, + } + `); + expect(dimensionValues).toMatchInlineSnapshot(` + Array [ + "维值-2", + "维值-3", + "测试-2", + "测试-3", + "测试-4", + "测试-5", + "测试-x", + "测试-7", + "测试-8", + ] + `); + }); +}); diff --git a/packages/s2-core/__tests__/spreadsheet/row-link-spec.ts b/packages/s2-core/__tests__/spreadsheet/row-link-spec.ts index 41d7d61a62..18dd45ec8e 100644 --- a/packages/s2-core/__tests__/spreadsheet/row-link-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/row-link-spec.ts @@ -1,7 +1,7 @@ import { getContainer } from 'tests/util/helpers'; import { noop } from 'lodash'; import { PivotSheet } from '@/sheet-type'; -import { S2Event, type S2Options } from '@/common'; +import { S2Event, type S2DataConfig, type S2Options } from '@/common'; const s2Options: S2Options = { width: 400, @@ -19,7 +19,7 @@ const s2Options: S2Options = { }, }; -const s2DataCfg = { +const s2DataCfg: S2DataConfig = { fields: { rows: ['province', 'city'], columns: ['type'], @@ -32,14 +32,7 @@ const s2DataCfg = { city: '义乌1', type: '笔', price: 1, - cost: 9, - }, - { - province: '浙江', - city: '义乌1', - type: '笔', - price: 10, - cost: 99, + cost: 2, }, { province: '浙江', @@ -76,7 +69,7 @@ describe('Row Text Link Tests', () => { let s2: PivotSheet; const linkFieldJump = jest.fn(); - beforeAll(async () => { + beforeEach(async () => { container = getContainer(); s2 = new PivotSheet(container, s2DataCfg, s2Options); await s2.render(); @@ -122,15 +115,15 @@ describe('Row Text Link Tests', () => { }, } as any); - expect(linkFieldJump).toHaveBeenCalledWith({ + expect(linkFieldJump).toHaveBeenLastCalledWith({ field: 'city', cellData: rowNode, record: { province: '浙江', city: '义乌1', type: '笔', - price: 10, - cost: 99, + price: 1, + cost: 2, rowIndex: 1, }, }); diff --git a/packages/s2-core/__tests__/spreadsheet/scroll-spec.ts b/packages/s2-core/__tests__/spreadsheet/scroll-spec.ts index 34a09bb472..33439bc201 100644 --- a/packages/s2-core/__tests__/spreadsheet/scroll-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/scroll-spec.ts @@ -1,7 +1,8 @@ /* eslint-disable jest/no-conditional-expect */ import * as mockDataConfig from 'tests/data/simple-data.json'; import { createMockCellInfo, getContainer, sleep } from 'tests/util/helpers'; -import { ScrollType } from '../../src/ui/scrollbar'; +import { get } from 'lodash'; +import { ScrollBar, ScrollType } from '../../src/ui/scrollbar'; import type { CellScrollPosition } from './../../src/common/interface/scroll'; import { PivotSheet, SpreadSheet } from '@/sheet-type'; import type { @@ -12,6 +13,7 @@ import type { import { InteractionStateName, InterceptType, + LayoutWidthType, OriginEventType, S2Event, ScrollbarPositionType, @@ -381,6 +383,62 @@ describe('Scroll Tests', () => { }, ); + // https://github.com/antvis/S2/issues/2222 + test.each([ + { + type: 'horizontal', + offset: { + scrollX: 20, + scrollY: 0, + }, + }, + { + type: 'vertical', + offset: { + scrollX: 0, + scrollY: 20, + }, + }, + ])( + 'should trigger hover cells when hover cells after scroll by %o', + async ({ offset }) => { + s2.setOptions({ + interaction: { + hoverAfterScroll: true, + }, + }); + + s2.facet.cornerBBox.maxY = -9999; + s2.facet.panelBBox.minX = -9999; + s2.facet.panelBBox.minY = -9999; + + const bbox = s2.getCanvasElement().getBoundingClientRect(); + const mousemoveEvent = new MouseEvent(OriginEventType.POINTER_MOVE, { + clientX: bbox.left + 100, + clientY: bbox.top + 100, + }); + + canvas.dispatchEvent(mousemoveEvent); + + s2.container.emit = jest.fn(); + + const wheelEvent = new WheelEvent('wheel', { + deltaX: offset.scrollX, + deltaY: offset.scrollY, + }); + + canvas.dispatchEvent(wheelEvent); + + // wait requestAnimationFrame and debounce + await sleep(1000); + + expect(s2.container.emit).toHaveBeenCalledWith( + OriginEventType.POINTER_MOVE, + expect.any(Object), + ); + }, + ); + test('should not trigger scroll event on passive renders', async () => { const sheet = new PivotSheet(getContainer(), mockDataConfig, { ...s2Options, @@ -403,7 +461,7 @@ describe('Scroll Tests', () => { scrollbarPosition: ScrollbarPositionType.CONTENT, }, style: { - layoutWidthType: 'compact', + layoutWidthType: LayoutWidthType.Compact, }, }); s2.changeSheetSize(100, 1000); // 横向滚动条 @@ -414,7 +472,8 @@ describe('Scroll Tests', () => { s2.changeSheetSize(1000, 150); // 纵向滚动条 await s2.render(false); - expect(s2.facet.vScrollBar.getBBox().x).toBe(195); + + expect(Math.floor(s2.facet.vScrollBar.getBBox().x)).toEqual(195); s2.setOptions({ interaction: { @@ -455,11 +514,11 @@ describe('Scroll Tests', () => { const scrollBar = s2.facet[name]; - const positon = scrollBar['getCoordinatesWithBBoxExtraPadding'](); + const position = scrollBar['getCoordinatesWithBBoxExtraPadding'](); expect( Math.round(scrollBar.thumbShape.getBBox()[key] as number), - ).toStrictEqual(Math.round(positon.end - positon.start)); + ).toStrictEqual(Math.round(position.end - position.start)); }, ); @@ -492,7 +551,7 @@ describe('Scroll Tests', () => { rowHeader: true, }, style: { - layoutWidthType: 'compact', + layoutWidthType: LayoutWidthType.Compact, rowCell: { width: 200, }, @@ -639,13 +698,13 @@ describe('Scroll Tests', () => { ).toBeFalsy(); }); - test('should scroll horizontally when shift key is held', async () => { + test('should scroll horizontally when shift key is held on Windows', async () => { s2.setOptions({ frozen: { rowHeader: true, }, style: { - layoutWidthType: 'compact', + layoutWidthType: LayoutWidthType.Compact, rowCell: { width: 200, }, @@ -676,11 +735,39 @@ describe('Scroll Tests', () => { shiftKey: true, }); + Object.defineProperty(window.navigator, 'userAgent', { + value: 'Windows', + configurable: true, + writable: true, + }); + canvas.dispatchEvent(wheelEvent); + await sleep(200); + expect(onScroll).toHaveBeenCalled(); + }); - await sleep(1000); + test('should not scroll horizontally when shift key is held on macOS', async () => { + const onScroll = jest.fn((...args) => { + expect(args[0].rowHeaderScrollX).toBeGreaterThan(0); + expect(args[0].scrollX).toBe(0); + expect(args[0].scrollY).toBe(0); + }); - expect(onScroll).toHaveBeenCalled(); + const wheelEvent = new WheelEvent('wheel', { + deltaX: 0, + deltaY: 20, + shiftKey: true, + }); + + Object.defineProperty(window.navigator, 'userAgent', { + value: 'Mac OS', + configurable: true, + writable: true, + }); + + canvas.dispatchEvent(wheelEvent); + await sleep(200); + expect(onScroll).not.toHaveBeenCalled(); }); it('should not change init body overscrollBehavior style when render and destroyed', async () => { @@ -766,4 +853,92 @@ describe('Scroll Tests', () => { expect(errorSpy).not.toHaveBeenCalled(); }); + + // https://github.com/antvis/S2/issues/2316 + test('should not throw infinite error if spreadsheet is unmounted during scrolling', async () => { + s2.setOptions({ + style: { + rowCell: { + width: 200, + }, + dataCell: { + width: 30, + }, + }, + }); + + await s2.render(false); + + const errorSpy = jest + .spyOn(console, 'error') + .mockImplementationOnce(() => {}); + + // 滚动时 unmount 表格实例 + s2.facet.scrollWithAnimation( + { + offsetX: { + value: 10, + animate: true, + }, + offsetY: { + value: 10, + animate: true, + }, + }, + 200, + ); + s2.destroy(); + + await sleep(500); + + expect(errorSpy).toHaveBeenCalledTimes(0); + }); + + // https://github.com/antvis/S2/issues/2376 + test.each(['hScrollBar', 'hRowScrollBar'])( + 'should not reset interaction state after %s scrollbar thumb or track clicked', + (scrollbarName) => { + const isMatchElementSpy = jest + .spyOn(s2.interaction.eventController, 'isMatchElement') + .mockImplementation(() => true); + + const reset = jest.fn(); + + const scrollbar = get(s2.facet, scrollbarName) as ScrollBar; + const colCell = s2.facet.getColLeafCells()[0]!; + + s2.on(S2Event.GLOBAL_RESET, reset); + s2.interaction.selectHeaderCell({ + cell: colCell, + }); + + expect(s2.interaction.isSelectedState()).toBeTruthy(); + + const { maxX, maxY } = s2.facet?.panelBBox || {}; + const { x, y } = canvas.getBoundingClientRect() || {}; + + // 滚动条 + window.dispatchEvent( + new MouseEvent('click', { + clientX: x + scrollbar.position.x, + // 在滚动条内点击 + clientY: y + scrollbar.position.y + scrollbar!.theme!.size! - 2, + } as MouseEventInit), + ); + + // 滑动轨道 + window.dispatchEvent( + new MouseEvent('click', { + // 右下角滑道点击 + clientX: x + maxX - 2, + clientY: y + maxY + 2, + } as MouseEventInit), + ); + + expect(s2.interaction.isSelectedState()).toBeTruthy(); + expect(reset).not.toHaveBeenCalled(); + + isMatchElementSpy.mockClear(); + }, + ); }); diff --git a/packages/s2-core/__tests__/spreadsheet/sort-by-order-spec.ts b/packages/s2-core/__tests__/spreadsheet/sort-by-order-spec.ts index 33a238ffdc..ab07de4ac3 100644 --- a/packages/s2-core/__tests__/spreadsheet/sort-by-order-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/sort-by-order-spec.ts @@ -73,12 +73,12 @@ describe('Manual Sort Tests', () => { }, { sortFieldId: 'type2', - sortBy: ['整体访问', '小程序访问', '支付宝访问'], + sortBy: ['支付宝访问', '整体访问', '小程序访问'], }, ], }; - beforeAll(async () => { + beforeEach(async () => { const container = getContainer(); s2 = new PivotSheet(container, mockDataCfg, s2Options); @@ -94,12 +94,12 @@ describe('Manual Sort Tests', () => { s2.dataSet.getDimensionValues('type2', { type1: '整体访问', }), - ).toEqual(['整体访问', '小程序访问', '支付宝访问']); + ).toEqual(['支付宝访问', '整体访问', '小程序访问']); expect( s2.dataSet.getDimensionValues('type2', { type1: '小程序访问', }), - ).toEqual(['小程序访问', '整体访问', '支付宝访问']); + ).toEqual(['支付宝访问', '整体访问', '小程序访问']); expect( s2.dataSet.getDimensionValues('type2', { type1: '支付宝访问', diff --git a/packages/s2-core/__tests__/spreadsheet/spread-sheet-facet-layout-api-spec.ts b/packages/s2-core/__tests__/spreadsheet/spread-sheet-facet-layout-api-spec.ts index d711058a39..da32d09082 100644 --- a/packages/s2-core/__tests__/spreadsheet/spread-sheet-facet-layout-api-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/spread-sheet-facet-layout-api-spec.ts @@ -35,17 +35,17 @@ describe('Facet Layout API Tests', () => { Array [ Object { "field": "province", - "id": "", + "id": "province", "value": "province", }, Object { "field": "city", - "id": "", + "id": "city", "value": "city", }, Object { "field": "type", - "id": "", + "id": "type", "value": "type", }, Object { @@ -107,17 +107,17 @@ describe('Facet Layout API Tests', () => { }, Object { "field": "province", - "id": "", + "id": "province", "value": "province", }, Object { "field": "city", - "id": "", + "id": "city", "value": "city", }, Object { "field": "type", - "id": "", + "id": "type", "value": "type", }, Object { @@ -164,17 +164,17 @@ describe('Facet Layout API Tests', () => { Array [ Object { "field": "province", - "id": "", + "id": "province", "value": "province", }, Object { "field": "city", - "id": "", + "id": "city", "value": "city", }, Object { "field": "type", - "id": "", + "id": "type", "value": "type", }, Object { @@ -226,17 +226,17 @@ describe('Facet Layout API Tests', () => { Array [ Object { "field": "province", - "id": "", + "id": "province", "value": "province", }, Object { "field": "city", - "id": "", + "id": "city", "value": "city", }, Object { "field": "type", - "id": "", + "id": "type", "value": "type", }, Object { @@ -314,7 +314,7 @@ describe('Facet Layout API Tests', () => { Array [ Object { "field": "type", - "id": "", + "id": "type", "value": "type", }, Object { @@ -353,17 +353,17 @@ describe('Facet Layout API Tests', () => { Array [ Object { "field": "province", - "id": "", + "id": "province", "value": "province", }, Object { "field": "city", - "id": "", + "id": "city", "value": "city", }, Object { "field": "type", - "id": "", + "id": "type", "value": "type", }, ] @@ -385,17 +385,17 @@ describe('Facet Layout API Tests', () => { }, Object { "field": "province", - "id": "", + "id": "province", "value": "province", }, Object { "field": "city", - "id": "", + "id": "city", "value": "city", }, Object { "field": "type", - "id": "", + "id": "type", "value": "type", }, ] @@ -407,17 +407,17 @@ describe('Facet Layout API Tests', () => { Array [ Object { "field": "province", - "id": "", + "id": "province", "value": "province", }, Object { "field": "city", - "id": "", + "id": "city", "value": "city", }, Object { "field": "type", - "id": "", + "id": "type", "value": "type", }, ] @@ -439,17 +439,17 @@ describe('Facet Layout API Tests', () => { }, Object { "field": "province", - "id": "", + "id": "province", "value": "province", }, Object { "field": "city", - "id": "", + "id": "city", "value": "city", }, Object { "field": "type", - "id": "", + "id": "type", "value": "type", }, ] diff --git a/packages/s2-core/__tests__/spreadsheet/spread-sheet-resize-spec.ts b/packages/s2-core/__tests__/spreadsheet/spread-sheet-resize-spec.ts index 198c0513c3..4c2abe2f06 100644 --- a/packages/s2-core/__tests__/spreadsheet/spread-sheet-resize-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/spread-sheet-resize-spec.ts @@ -1,15 +1,15 @@ import * as mockDataConfig from 'tests/data/simple-data.json'; import { getContainer } from 'tests/util/helpers'; import type { Group } from '@antv/g'; -import { PivotSheet } from '@/sheet-type'; import { - KEY_GROUP_COL_RESIZE_AREA, - KEY_GROUP_CORNER_RESIZE_AREA, - KEY_GROUP_ROW_RESIZE_AREA, type S2Options, -} from '@/common'; + PivotSheet, + KEY_GROUP_ROW_RESIZE_AREA, + KEY_GROUP_CORNER_RESIZE_AREA, + KEY_GROUP_COL_RESIZE_AREA, +} from '../../src'; -async function renderSheet(options: S2Options) { +async function renderSheet(options?: S2Options) { const s2 = new PivotSheet(getContainer(), mockDataConfig, { height: 150, ...options, @@ -22,6 +22,7 @@ async function renderSheet(options: S2Options) { }, }, }); + await s2.render(); return s2; @@ -191,4 +192,31 @@ describe('SpreadSheet Resize Active Tests', () => { expect(group.getElementById(KEY_GROUP_ROW_RESIZE_AREA)).toBeNull(); expect(group.getElementById(KEY_GROUP_CORNER_RESIZE_AREA)).toBeNull(); }); + + test('should render correctly layout when tree row width is invalid number', async () => { + const s2 = await renderSheet(); + + s2.setOptions({ + hierarchyType: 'tree', + style: { + rowCell: { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + width: '@', + }, + }, + }); + + await s2.render(false); + + const nodes = s2.facet.getRowNodes().map((node) => { + return { + id: node.id, + width: node.width, + height: node.height, + }; + }); + + expect(nodes).toMatchSnapshot(); + }); }); diff --git a/packages/s2-core/__tests__/spreadsheet/spread-sheet-series-number-spec.ts b/packages/s2-core/__tests__/spreadsheet/spread-sheet-series-number-spec.ts index 6944013201..0573dc8232 100644 --- a/packages/s2-core/__tests__/spreadsheet/spread-sheet-series-number-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/spread-sheet-series-number-spec.ts @@ -1,8 +1,6 @@ import * as mockDataConfig from 'tests/data/simple-data.json'; import { getContainer } from 'tests/util/helpers'; -import { PivotSheet } from '@/sheet-type'; -import type { S2Options } from '@/common'; -import type { SeriesNumberCell } from '@/cell'; +import { type S2Options, PivotSheet } from '../../src'; const s2Options: S2Options = { width: 400, @@ -23,14 +21,10 @@ describe('SpreadSheet Series Number Tests', () => { await s2.render(); - const seriesNumberHeader = s2.facet.seriesNumberHeader; + const seriesNumberCell = s2.facet.getSeriesNumberCells(); - expect(seriesNumberHeader?.children).toHaveLength(1); - - const seriesNum1 = seriesNumberHeader?.children[0] as SeriesNumberCell; - - // @ts-ignore - expect(seriesNum1.meta.height).toEqual(60); + expect(seriesNumberCell).toHaveLength(1); + expect(seriesNumberCell[0].getMeta().height).toEqual(60); }); test("series number should contain root parent and it's all children in tree mode", async () => { @@ -41,13 +35,9 @@ describe('SpreadSheet Series Number Tests', () => { await s2.render(); - const seriesNumberHeader = s2.facet.seriesNumberHeader; - - expect(seriesNumberHeader?.children).toHaveLength(1); - - const seriesNum1 = seriesNumberHeader?.children[0] as SeriesNumberCell; + const seriesNumberCell = s2.facet.getSeriesNumberCells(); - // @ts-ignore - expect(seriesNum1.meta.height).toEqual(90); + expect(seriesNumberCell).toHaveLength(1); + expect(seriesNumberCell[0].getMeta().height).toEqual(90); }); }); diff --git a/packages/s2-core/__tests__/spreadsheet/spread-sheet-totals-spec.ts b/packages/s2-core/__tests__/spreadsheet/spread-sheet-totals-spec.ts index eeb3b48ac8..666aff715c 100644 --- a/packages/s2-core/__tests__/spreadsheet/spread-sheet-totals-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/spread-sheet-totals-spec.ts @@ -1,8 +1,7 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import { get, merge } from 'lodash'; +import { merge } from 'lodash'; import { assembleDataCfg, assembleOptions, TOTALS_OPTIONS } from 'tests/util'; import { getContainer } from 'tests/util/helpers'; -import { DataCell } from '@/cell'; import type { RawData, S2DataConfig, S2Options } from '@/common'; import type { Node } from '@/facet/layout/node'; import { PivotSheet } from '@/sheet-type'; @@ -147,30 +146,22 @@ describe('Spreadsheet Totals Tests', () => { }); await spreadsheet.render(); - const grandTotal = spreadsheet.facet.panelScrollGroup.children.find( - (child) => - child instanceof DataCell && get(child, 'meta.rowId') === 'root[&]总计', - ) as DataCell; + const grandTotal = spreadsheet.facet + .getDataCells() + .find((cell) => cell.getMeta().rowId === 'root[&]总计')!; - // @ts-ignore - expect(grandTotal.textShape.attr('text')).toEqual('26193'); + expect(grandTotal.getTextShape().attr('text')).toEqual('26193'); - const rowSubtotal1 = spreadsheet.facet.panelScrollGroup.children.find( - (child) => - child instanceof DataCell && - get(child, 'meta.rowId') === 'root[&]浙江省', - ) as DataCell; + const rowSubtotal1 = spreadsheet.facet + .getDataCells() + .find((cell) => cell.getMeta().rowId === 'root[&]浙江省')!; - // @ts-ignore - expect(rowSubtotal1.textShape).toBeUndefined(); + expect(rowSubtotal1.getTextShape()).toBeUndefined(); - const rowSubtotal2 = spreadsheet.facet.panelScrollGroup.children.find( - (child) => - child instanceof DataCell && - get(child, 'meta.rowId') === 'root[&]浙江省', - ) as DataCell; + const rowSubtotal2 = spreadsheet.facet + .getDataCells() + .find((cell) => cell.getMeta().rowId === 'root[&]四川省')!; - // @ts-ignore - expect(rowSubtotal2.textShape).toBeUndefined(); + expect(rowSubtotal2.getTextShape()).toBeUndefined(); }); }); diff --git a/packages/s2-core/__tests__/spreadsheet/spread-sheet-tree-mode-spec.ts b/packages/s2-core/__tests__/spreadsheet/spread-sheet-tree-mode-spec.ts index ae94d02794..2861bae844 100644 --- a/packages/s2-core/__tests__/spreadsheet/spread-sheet-tree-mode-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/spread-sheet-tree-mode-spec.ts @@ -1,5 +1,6 @@ import * as mockDataConfig from 'tests/data/simple-data.json'; import { createPivotSheet, getContainer } from 'tests/util/helpers'; +import { PivotSheet } from '../../src'; import type { S2DataConfig, S2Options } from '@/common'; const s2Options: S2Options = { @@ -11,11 +12,11 @@ const s2Options: S2Options = { describe('SpreadSheet Tree Mode Tests', () => { let container: HTMLElement; - beforeAll(() => { + beforeEach(() => { container = getContainer(); }); - afterAll(() => { + afterEach(() => { container?.remove(); }); @@ -51,5 +52,43 @@ describe('SpreadSheet Tree Mode Tests', () => { rowsHierarchyWidth, ); }); + + // https://github.com/antvis/S2/issues/2389 + test('the corner should only have one line with action icon', async () => { + // 行头维度更改为较长的 name + const newDataCfg: S2DataConfig = { + ...mockDataConfig, + meta: [ + { + field: 'province', + name: '省1234567890份', + }, + { + field: 'city', + name: '城1234567890市', + }, + ], + }; + + // 添加 icon + const newS2Options: S2Options = { + ...s2Options, + headerActionIcons: [ + { + icons: ['SortDownSelected'], + belongsCell: 'cornerCell', + }, + ], + }; + const s2 = new PivotSheet(container, newDataCfg, newS2Options); + + await s2.render(); + + // 检查文本是否只有一行 + const cornerCell = s2.facet.getCornerCells()[0]; + + expect(cornerCell.getTextShapes()).toHaveLength(1); + expect(cornerCell.isMultiLineText()).toBeFalsy(); + }); }); }); diff --git a/packages/s2-core/__tests__/spreadsheet/table-sheet-spec.ts b/packages/s2-core/__tests__/spreadsheet/table-sheet-spec.ts index c9ae16ea22..19c04800dc 100644 --- a/packages/s2-core/__tests__/spreadsheet/table-sheet-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/table-sheet-spec.ts @@ -1,20 +1,19 @@ -import { last } from 'lodash'; import { getContainer, getMockData, sleep } from 'tests/util/helpers'; import { - ColCell, DeviceType, ResizeType, TableSheet, type RawData, type S2DataConfig, type S2Options, + LayoutWidthType, } from '@/index'; const data = getMockData( '../../../s2-react/__tests__/data/tableau-supermarket.csv', ) as RawData[]; -const columns = [ +const columns: string[] = [ 'order_id', 'order_date', 'ship_date', @@ -75,7 +74,7 @@ const options: S2Options = { showSeriesNumber: true, placeholder: '', style: { - layoutWidthType: 'compact', + layoutWidthType: LayoutWidthType.Compact, dataCell: { height: 32, }, @@ -193,12 +192,11 @@ describe('TableSheet normal spec', () => { await s2.render(); - const getLastColCell = () => - last(s2.facet.getColNodes())!.belongsCell as ColCell; - const preColWidth = getLastColCell().getMeta().width; - await sleep(30); + let columnNodes = s2.facet.getColNodes(); + + const startCellWidth = columnNodes[columnNodes.length - 1].width; const { x, width, top } = s2.getCanvasElement().getBoundingClientRect(); s2.getCanvasElement().dispatchEvent( @@ -232,9 +230,10 @@ describe('TableSheet normal spec', () => { await sleep(300); - const currentColWidth = getLastColCell().getMeta().width; + columnNodes = s2.facet.getColNodes(); + const endCellWidth = columnNodes[columnNodes.length - 1].width; - expect(currentColWidth).toBeGreaterThanOrEqual(resizeLength + preColWidth); + expect(Math.floor(endCellWidth - startCellWidth)).toBe(140); }); test('should render link shape', async () => { diff --git a/packages/s2-core/__tests__/spreadsheet/theme-spec.ts b/packages/s2-core/__tests__/spreadsheet/theme-spec.ts index 950807e4d7..2a9e180e62 100644 --- a/packages/s2-core/__tests__/spreadsheet/theme-spec.ts +++ b/packages/s2-core/__tests__/spreadsheet/theme-spec.ts @@ -1,7 +1,5 @@ /* eslint-disable jest/expect-expect */ -import { Text, type Group } from '@antv/g'; -import { createPivotSheet } from 'tests/util/helpers'; -import type { RowCell } from '@/cell'; +import { createPivotSheet, createTableSheet } from 'tests/util/helpers'; import { CellType, EXTRA_COLUMN_FIELD, @@ -62,11 +60,17 @@ describe('SpreadSheet Theme Tests', () => { expect(s2.getThemeName()).toEqual('dark'); }); - test('should get default theme', () => { + test('should get pivot sheet default theme', () => { expect(s2.theme).toMatchSnapshot(); expect(s2.theme).toEqual(s2.getTheme()); }); + test('should get table sheet theme', () => { + const tableSheet = createTableSheet(null); + + expect(tableSheet.theme).toMatchSnapshot(); + }); + test.each(['dark', 'gray', 'colorful', 'default'] as ThemeName[])( 'should get %s theme', (name) => { @@ -219,7 +223,7 @@ describe('SpreadSheet Theme Tests', () => { s2.setThemeCfg(getRowCellThemeCfg(align)); await s2.render(); - const rowCell = s2.facet.rowHeader!.children[0] as RowCell; + const rowCell = s2.facet.getRowCells()[0]; const rowCellWidth = rowCell.getMeta().width; const actionIcon = rowCell.getActionIcons()[0]; @@ -463,9 +467,6 @@ describe('SpreadSheet Theme Tests', () => { }); describe('Series Cell Tests', () => { - const getTextShape = (group: Group) => - group.children.find((child) => child instanceof Text) as Text; - test.each(['top', 'middle', 'bottom'] as TextBaseline[])( 'should render %s text align for column nodes', async (textBaseline) => { @@ -488,14 +489,19 @@ describe('SpreadSheet Theme Tests', () => { await s2.render(); - const rowCell = s2.facet.rowHeader!.children[0] as Group; // 浙江省 - const textOfRowCell = getTextShape(rowCell); + // 浙江省 + const rowCell = s2.facet.getRowCells()[0]; + const rowCellTextShape = rowCell.getTextShape(); - const seriesCell = s2.facet.seriesNumberHeader!.children[0] as Group; // 序号1 - const textOfSeriesCell = getTextShape(seriesCell); + // 序号1 + const seriesCell = s2.facet.getSeriesNumberCells()[0]; + const seriesCellTextShape = seriesCell.getTextShape(); - expect(textOfRowCell?.attr('textBaseline')).toEqual(textBaseline); - expect(textOfSeriesCell?.attr('textBaseline')).toEqual(textBaseline); + expect(rowCellTextShape?.attr('textBaseline')).toEqual(textBaseline); + expect(seriesCellTextShape?.attr('textBaseline')).toEqual(textBaseline); + expect(rowCellTextShape.attr('y')).toEqual( + seriesCellTextShape.attr('y'), + ); }, ); }); diff --git a/packages/s2-core/__tests__/spreadsheet/total-group-spec.ts b/packages/s2-core/__tests__/spreadsheet/total-group-spec.ts new file mode 100644 index 0000000000..4505813244 --- /dev/null +++ b/packages/s2-core/__tests__/spreadsheet/total-group-spec.ts @@ -0,0 +1,332 @@ +import { getContainer } from 'tests/util/helpers'; +import { map } from 'lodash'; +import { s2Options, dataCfg } from '../data/total-group-data'; +import { CellData } from '../../src'; +import type { PivotFacet } from '../../src/facet'; +import { PivotSheet } from '@/sheet-type'; +import { type S2Options, ORIGIN_FIELD } from '@/common'; + +describe('Total Group Dimension Test', () => { + let container: HTMLDivElement; + + let s2: PivotSheet; + + beforeEach(() => { + container = getContainer(); + }); + + afterEach(() => { + // s2?.destroy(); + }); + + test(`should get correct layout with row total group dimension 'type'`, async () => { + s2 = new PivotSheet(container, dataCfg, s2Options as S2Options); + await s2.render(); + + const facet = s2.facet as PivotFacet; + const { rowLeafNodes } = facet.getLayoutResult(); + + expect(map(rowLeafNodes, 'id')).toMatchInlineSnapshot(` + Array [ + "root[&]总计[&]家具", + "root[&]总计[&]办公用品", + "root[&]浙江省[&]小计[&]家具", + "root[&]浙江省[&]小计[&]办公用品", + "root[&]浙江省[&]杭州市[&]家具", + "root[&]浙江省[&]杭州市[&]办公用品", + "root[&]浙江省[&]舟山市[&]家具", + "root[&]浙江省[&]舟山市[&]办公用品", + "root[&]四川省[&]小计[&]家具", + "root[&]四川省[&]小计[&]办公用品", + "root[&]四川省[&]成都市[&]家具", + "root[&]四川省[&]成都市[&]办公用品", + "root[&]四川省[&]绵阳市[&]家具", + "root[&]四川省[&]绵阳市[&]办公用品", + ] + `); + + expect((facet.getCellMeta(0, 0)!.data as CellData)[ORIGIN_FIELD]).toEqual({ + type: '家具', + price: 2000, + }); + expect((facet.getCellMeta(1, 1)!.data as CellData)[ORIGIN_FIELD]).toEqual({ + type: '办公用品', + cost: 1900, + }); + }); + + test(`should get correct layout with row total group dimension 'city'`, async () => { + const newS2Options = { + ...s2Options, + totals: { + ...s2Options.totals, + row: { + ...s2Options.totals!.row!, + grandTotalsGroupDimensions: ['city'], + }, + }, + }; + + s2 = new PivotSheet(container, dataCfg, newS2Options as S2Options); + await s2.render(); + + const facet = s2.facet; + const { rowLeafNodes } = facet.getLayoutResult(); + + expect(map(rowLeafNodes, 'id')).toMatchInlineSnapshot(` + Array [ + "root[&]总计[&]杭州市", + "root[&]总计[&]舟山市", + "root[&]总计[&]成都市", + "root[&]总计[&]绵阳市", + "root[&]浙江省[&]小计[&]家具", + "root[&]浙江省[&]小计[&]办公用品", + "root[&]浙江省[&]杭州市[&]家具", + "root[&]浙江省[&]杭州市[&]办公用品", + "root[&]浙江省[&]舟山市[&]家具", + "root[&]浙江省[&]舟山市[&]办公用品", + "root[&]四川省[&]小计[&]家具", + "root[&]四川省[&]小计[&]办公用品", + "root[&]四川省[&]成都市[&]家具", + "root[&]四川省[&]成都市[&]办公用品", + "root[&]四川省[&]绵阳市[&]家具", + "root[&]四川省[&]绵阳市[&]办公用品", + ] + `); + + expect((facet.getCellMeta(0, 0)!.data as CellData)[ORIGIN_FIELD]).toEqual({ + city: '杭州市', + price: 300, + }); + + expect((facet.getCellMeta(1, 0)!.data as CellData)[ORIGIN_FIELD]).toEqual({ + city: '舟山市', + price: 800, + }); + + expect((facet.getCellMeta(2, 0)!.data as CellData)[ORIGIN_FIELD]).toEqual({ + city: '成都市', + price: 1200, + }); + + expect((facet.getCellMeta(3, 0)!.data as CellData)[ORIGIN_FIELD]).toEqual({ + city: '绵阳市', + price: 1600, + }); + }); + + test(`should get correct layout with row sub group dimension 'type'`, async () => { + const newS2Options = { + ...s2Options, + totals: { + ...s2Options.totals, + row: { + ...s2Options.totals!.row, + // 总计分组下,city 城市维度会出现分组 + grandTotalsGroupDimensions: ['city'], + subTotalsGroupDimensions: ['type'], + }, + }, + }; + + const newDataCfg = { + ...dataCfg, + fields: { + ...dataCfg.fields, + rows: ['province', 'city', 'type'], + columns: ['sub_type'], + values: ['price', 'cost'], + }, + }; + + s2 = new PivotSheet(container, newDataCfg, newS2Options as S2Options); + await s2.render(); + + const facet = s2.facet; + const { rowLeafNodes } = facet.getLayoutResult(); + + expect(rowLeafNodes[4].id).toEqual('root[&]浙江省[&]小计[&]家具'); + expect(rowLeafNodes[5].id).toEqual('root[&]浙江省[&]小计[&]办公用品'); + + expect((facet.getCellMeta(4, 0)!.data as CellData)[ORIGIN_FIELD]).toEqual({ + province: '浙江省', + price: 600, + type: '家具', + }); + + expect((facet.getCellMeta(5, 0)!.data as CellData)[ORIGIN_FIELD]).toEqual({ + province: '浙江省', + type: '办公用品', + price: 500, + }); + }); + + test(`should get correct layout with col total group dimension 'type'`, async () => { + const newS2Options = { + ...s2Options, + totals: { + ...s2Options.totals, + col: { + ...s2Options.totals!.col, + grandTotalsGroupDimensions: ['type'], + }, + }, + }; + + const newDataCfg = { + ...dataCfg, + fields: { + ...dataCfg.fields, + rows: ['province'], + columns: ['city', 'type', 'sub_type'], + values: ['price', 'cost'], + }, + }; + + s2 = new PivotSheet(container, newDataCfg, newS2Options as S2Options); + await s2.render(); + + const facet = s2.facet; + + expect((facet.getCellMeta(0, 0)!.data as CellData)[ORIGIN_FIELD]).toEqual({ + type: '家具', + price: 2000, + }); + + expect((facet.getCellMeta(0, 2)!.data as CellData)[ORIGIN_FIELD]).toEqual({ + type: '办公用品', + price: 1900, + }); + }); + + test(`should get correct layout with col total group dimension 'sub_type'`, async () => { + const newS2Options = { + ...s2Options, + totals: { + ...s2Options.totals, + col: { + ...s2Options.totals!.col, + grandTotalsGroupDimensions: ['sub_type'], + }, + }, + }; + + const newDataCfg = { + ...dataCfg, + fields: { + ...dataCfg.fields, + rows: ['province'], + columns: ['city', 'type', 'sub_type'], + values: ['price', 'cost'], + }, + }; + + s2 = new PivotSheet(container, newDataCfg, newS2Options as S2Options); + await s2.render(); + + const facet = s2.facet; + + expect((facet.getCellMeta(0, 0)!.data as CellData)[ORIGIN_FIELD]).toEqual({ + sub_type: '桌子', + price: 1000, + }); + + expect((facet.getCellMeta(0, 2)!.data as CellData)[ORIGIN_FIELD]).toEqual({ + sub_type: '沙发', + price: 1000, + }); + + expect((facet.getCellMeta(0, 4)!.data as CellData)[ORIGIN_FIELD]).toEqual({ + sub_type: '笔', + price: 1000, + }); + + expect((facet.getCellMeta(0, 6)!.data as CellData)[ORIGIN_FIELD]).toEqual({ + sub_type: '纸张', + price: 900, + }); + }); + + test(`should get correct layout with col sub total group dimension 'sub_type'`, async () => { + const newS2Options = { + ...s2Options, + totals: { + ...s2Options.totals, + col: { + ...s2Options.totals!.col, + grandTotalsGroupDimensions: [], + subTotalsDimensions: ['city'], + subTotalsGroupDimensions: ['sub_type'], + }, + }, + }; + + const newDataCfg = { + ...dataCfg, + fields: { + ...dataCfg.fields, + rows: ['province'], + columns: ['city', 'type', 'sub_type'], + values: ['price', 'cost'], + }, + }; + + s2 = new PivotSheet(container, newDataCfg, newS2Options as S2Options); + await s2.render(); + + const facet = s2.facet; + + expect((facet.getCellMeta(0, 2)!.data as CellData)[ORIGIN_FIELD]).toEqual({ + city: '杭州市', + sub_type: '桌子', + price: 100, + }); + + expect((facet.getCellMeta(0, 4)!.data as CellData)[ORIGIN_FIELD]).toEqual({ + city: '杭州市', + sub_type: '沙发', + price: 100, + }); + + expect((facet.getCellMeta(0, 6)!.data as CellData)[ORIGIN_FIELD]).toEqual({ + city: '杭州市', + sub_type: '笔', + price: 100, + }); + }); + + test(`should get correct layout with giving total data`, async () => { + const newDataCfg = { + ...dataCfg, + data: dataCfg.data.concat([ + { + type: '家具', + price: 6666, + cost: 6666, + }, + { + type: '办公用品', + price: 9999, + cost: 9999, + }, + ]), + }; + + s2 = new PivotSheet(container, newDataCfg, s2Options as S2Options); + await s2.render(); + + const facet = s2.facet; + + expect((facet.getCellMeta(0, 0)!.data as CellData)[ORIGIN_FIELD]).toEqual({ + type: '家具', + price: 6666, + cost: 6666, + }); + + expect((facet.getCellMeta(1, 0)!.data as CellData)[ORIGIN_FIELD]).toEqual({ + type: '办公用品', + price: 9999, + cost: 9999, + }); + }); +}); diff --git a/packages/s2-core/__tests__/unit/cell/col-cell-spec.ts b/packages/s2-core/__tests__/unit/cell/col-cell-spec.ts index 9331a781d0..66be515b2d 100644 --- a/packages/s2-core/__tests__/unit/cell/col-cell-spec.ts +++ b/packages/s2-core/__tests__/unit/cell/col-cell-spec.ts @@ -1,13 +1,13 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import { get, set } from 'lodash'; +import { set } from 'lodash'; import { createFakeSpreadSheet, createPivotSheet } from 'tests/util/helpers'; import type { ColHeaderConfig } from '../../../src/facet/header'; import { getContainer } from './../../util/helpers'; -import type { Node } from '@/facet/layout/node'; -import { PivotDataSet } from '@/data-set'; -import { SpreadSheet, PivotSheet } from '@/sheet-type'; -import { EXTRA_FIELD, type Formatter, type TextAlign } from '@/common'; import { ColCell } from '@/cell'; +import { EXTRA_FIELD, type Formatter, type TextAlign } from '@/common'; +import { PivotDataSet } from '@/data-set'; +import type { Node } from '@/facet/layout/node'; +import { PivotSheet, SpreadSheet } from '@/sheet-type'; const MockPivotSheet = PivotSheet as unknown as jest.Mock; const MockPivotDataSet = PivotDataSet as unknown as jest.Mock; @@ -101,9 +101,7 @@ describe('Col Cell Tests', () => { const colCell = new ColCell(node, s2, headerConfig); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(colCell.textShape.attr('text')).toEqual('test'); + expect(colCell.getTextShape().attr('text')).toEqual('test'); }); }); @@ -125,7 +123,7 @@ describe('Col Cell Tests', () => { test('should draw right condition text shape', async () => { await s2.render(); - const colCell = s2.facet.columnHeader.children[0].children[1] as ColCell; + const colCell = s2.facet.getColCells()[1]; expect(colCell.getTextShape().parsedStyle.fill).toBeColor('#5083F5'); }); @@ -150,10 +148,13 @@ describe('Col Cell Tests', () => { }); await s2.render(); - const colCell = s2.facet.columnHeader.children[0].children[0]; + const colCell = s2.facet.getColCells()[0]; - expect(get(colCell, 'conditionIconShape.cfg.name')).toEqual('CellUp'); - expect(get(colCell, 'conditionIconShape.cfg.fill')).toEqual('red'); + // @ts-ignore + expect(colCell.rightIconPosition).toEqual({ + x: 152, + y: 10.5, + }); }); test('should draw right condition background shape', async () => { @@ -171,10 +172,11 @@ describe('Col Cell Tests', () => { ], }, }); + await s2.render(); - const colCell = s2.facet.columnHeader.children[0].children[1]; + const colCell = s2.facet.getColCells()[1]; - expect(get(colCell, 'backgroundShape.parsedStyle.fill')).toBeColor( + expect(colCell.getBackgroundShape().parsedStyle.fill).toBeColor( '#F7B46F', ); }); @@ -198,7 +200,7 @@ describe('Col Cell Tests', () => { }); await s2.render(); - const colCell = s2.facet.columnHeader.children[0].children[0] as ColCell; + const colCell = s2.facet.getColCells()[0]; const { fill, fontSize, fontWeight } = colCell.getTextShape().attributes; expect(fill).toEqual('red'); diff --git a/packages/s2-core/__tests__/unit/cell/corner-cell-spec.ts b/packages/s2-core/__tests__/unit/cell/corner-cell-spec.ts index 76455c2c5e..1cd114bdcd 100644 --- a/packages/s2-core/__tests__/unit/cell/corner-cell-spec.ts +++ b/packages/s2-core/__tests__/unit/cell/corner-cell-spec.ts @@ -26,7 +26,6 @@ describe('Corner Cell Tests', () => { }); const drawTextShapeSpy = jest - // @ts-ignore .spyOn(cornerCell, 'drawTextShape') .mockImplementationOnce(() => true); diff --git a/packages/s2-core/__tests__/unit/cell/custom-tree-corner-cell-spec.ts b/packages/s2-core/__tests__/unit/cell/custom-tree-corner-cell-spec.ts index 5a4da7c8b1..68d3041791 100644 --- a/packages/s2-core/__tests__/unit/cell/custom-tree-corner-cell-spec.ts +++ b/packages/s2-core/__tests__/unit/cell/custom-tree-corner-cell-spec.ts @@ -4,10 +4,9 @@ import { customTreeNodes } from 'tests/data/custom-tree-nodes'; import { CustomTreeData } from 'tests/data/data-custom-tree'; import { getContainer } from 'tests/util/helpers'; -import { get } from 'lodash'; import type { S2DataConfig } from '@/common/interface'; import { PivotSheet, SpreadSheet } from '@/sheet-type'; -import { CornerCell, type S2Options } from '@/index'; +import { type S2Options } from '@/index'; describe('test for corner text', () => { const values: string[] = [ @@ -44,21 +43,19 @@ describe('test for corner text', () => { }); test('get correct default corner text when the corner label is empty', () => { - const cornerCells = mockSheet.facet.cornerHeader.children; + const cornerCells = mockSheet.facet.getCornerCells(); - expect(get(cornerCells[0], 'actualText')).toEqual('自定义节点A/指标E/数值'); - expect(get(cornerCells[1], 'actualText')).toEqual('type'); + expect(cornerCells[0].getActualText()).toEqual('自定义节点A/指标E/数值'); + expect(cornerCells[1].getActualText()).toEqual('type'); }); test('get correct default corner text when set the cornerText.', async () => { mockSheet.setOptions({ ...options, cornerText: 'test' }); await mockSheet.render(); - const cornerCells = mockSheet.facet.cornerHeader.children.filter( - (v) => v instanceof CornerCell, - ); + const cornerCells = mockSheet.facet.getCornerCells(); - expect(get(cornerCells[0], 'actualText')).toEqual('test'); - expect(get(cornerCells[1], 'actualText')).toEqual('type'); + expect(cornerCells[0].getActualText()).toEqual('test'); + expect(cornerCells[1].getActualText()).toEqual('type'); }); }); diff --git a/packages/s2-core/__tests__/unit/cell/data-cell-spec.ts b/packages/s2-core/__tests__/unit/cell/data-cell-spec.ts index 096ccf74bf..6ce23c12d3 100644 --- a/packages/s2-core/__tests__/unit/cell/data-cell-spec.ts +++ b/packages/s2-core/__tests__/unit/cell/data-cell-spec.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ import type { Rect } from '@antv/g'; -import { find, get } from 'lodash'; +import { find, get, keys } from 'lodash'; import { createPivotSheet, createTableSheet } from 'tests/util/helpers'; import { DataCell } from '@/cell'; import type { TextAlign } from '@/common'; @@ -13,11 +13,11 @@ import { type S2CellType, type ViewMeta, } from '@/common'; -import { EXTRA_FIELD, VALUE_FIELD } from '@/common/constant/basic'; import { DEFAULT_FONT_COLOR, REVERSE_FONT_COLOR, } from '@/common/constant/condition'; +import { EXTRA_FIELD, VALUE_FIELD } from '@/common/constant/field'; import { PivotDataSet } from '@/data-set'; import type { PivotFacet } from '@/facet'; import { PivotSheet, SpreadSheet } from '@/sheet-type'; @@ -28,11 +28,11 @@ const MockPivotDataSet = PivotDataSet as unknown as jest.Mock; const findDataCell = ( s2: SpreadSheet, valueField: 'price' | 'cost' | 'number', -) => - s2.facet.panelGroup.children[0].find( - (item) => - item instanceof DataCell && item.getMeta().valueField === valueField, - ); +) => { + return s2.facet + .getDataCells() + .find((item) => item.getMeta().valueField === valueField); +}; describe('Data Cell Tests', () => { const meta = { @@ -57,7 +57,7 @@ describe('Data Cell Tests', () => { }); test.each([ - ['left', 311], + ['left', 312], ['center', 375], ['right', 438], ] as const)( @@ -75,14 +75,13 @@ describe('Data Cell Tests', () => { }, }, }); + await s2.render(); - const panelBBoxInstance = s2.facet.panelGroup.children[0]; - const dataCell = panelBBoxInstance.children.find( - (item) => item instanceof DataCell, - ) as DataCell; - const { left: minX, right: maxX } = - dataCell['linkFieldShape'].getBBox(); + const dataCell = s2.facet.getDataCells()[0]; + const { left: minX, right: maxX } = dataCell + .getLinkFieldShape() + .getBBox(); // 宽度相当 const linkLength = maxX - minX; @@ -100,7 +99,7 @@ describe('Data Cell Tests', () => { }); describe('Data Cell Formatter & Method Tests', () => { - beforeEach(() => { + beforeEach(async () => { const container = document.createElement('div'); s2 = new MockPivotSheet(container); @@ -110,7 +109,11 @@ describe('Data Cell Tests', () => { s2.facet = { getRowLeafNodes: () => [], + getRowLeafNodeByIndex: jest.fn(), + getCells: () => [], } as unknown as PivotFacet; + + await s2.render(); }); test('should pass complete data into formatter', () => { @@ -207,7 +210,7 @@ describe('Data Cell Tests', () => { fill: 'red', }); - beforeEach(() => { + beforeEach(async () => { const container = document.createElement('div'); s2 = new MockPivotSheet(container); @@ -217,7 +220,11 @@ describe('Data Cell Tests', () => { s2.facet = { getRowLeafNodes: () => [], + getRowLeafNodeByIndex: jest.fn(), + getCells: () => [], } as unknown as PivotFacet; + + await s2.render(); }); test("shouldn't init when width or height is not positive", () => { @@ -543,11 +550,12 @@ describe('Data Cell Tests', () => { field: 'cost', mapping(value, dataInfo) { const originData = s2.dataSet.originData; - const resultData = find(originData, dataInfo); + const resultData = find(originData, (item) => + keys(item).every((key) => item[key] === dataInfo[key]), + ); - expect(resultData).toEqual(dataInfo); - // @ts-ignore - expect(value).toEqual(resultData.cost); + expect(value).toEqual(resultData?.['cost']); + expect(value).toEqual(dataInfo['cost']); return { fill: '#fffae6', diff --git a/packages/s2-core/__tests__/unit/cell/header-cell-spec.ts b/packages/s2-core/__tests__/unit/cell/header-cell-spec.ts index a86817ee4f..08e98afa71 100644 --- a/packages/s2-core/__tests__/unit/cell/header-cell-spec.ts +++ b/packages/s2-core/__tests__/unit/cell/header-cell-spec.ts @@ -49,6 +49,70 @@ describe('header cell formatter test', () => { expect(rowCell.getFieldValue()).toBe('杭州1'); }); + test('should not format pivot col and row total cell', () => { + const colNode = new Node({ + id: `root[&]总计`, + field: '', + value: '总计', + parent: root, + isTotalRoot: true, + }); + const rowNode = new Node({ + id: `root[&]杭州[&]小计`, + field: '', + value: '小计', + parent: root, + isTotalRoot: true, + }); + + const formatter: Formatter = (value) => { + return `${value}1`; + }; + + jest.spyOn(s2.dataSet, 'getFieldFormatter').mockReturnValue(formatter); + + const colCell = new ColCell(colNode, s2); + const rowCell = new RowCell(rowNode, s2); + + expect(colCell.getFieldValue()).toBe('总计'); + expect(rowCell.getFieldValue()).toBe('小计'); + }); + + test('should not format pivot row grand total cell in tree mode', () => { + const rowGrandTotalNode = new Node({ + id: `root[&]总计`, + field: '', + value: '总计', + parent: root, + isTotals: true, + isGrandTotals: true, + isTotalRoot: true, + }); + + const rowSubTotalNode = new Node({ + id: `root[&]杭州`, + field: '', + value: '杭州', + parent: root, + isTotals: true, + isSubTotals: true, + isGrandTotals: false, + }); + + const formatter: Formatter = (value) => { + return `${value}1`; + }; + + jest.spyOn(s2.dataSet, 'getFieldFormatter').mockReturnValue(formatter); + jest.spyOn(s2, 'isHierarchyTreeType').mockReturnValue(true); + + const grandTotalCell = new ColCell(rowGrandTotalNode, s2); + const subTotalCell = new RowCell(rowSubTotalNode, s2); + + expect(grandTotalCell.getFieldValue()).toBe('总计'); + expect(subTotalCell.getFieldValue()).toBe('杭州1'); + }); + test('pivot corner cell not formatter', () => { const formatter: Formatter = (value) => `${value}1`; diff --git a/packages/s2-core/__tests__/unit/cell/row-cell-spec.ts b/packages/s2-core/__tests__/unit/cell/row-cell-spec.ts index c53885a731..e60a3c0a09 100644 --- a/packages/s2-core/__tests__/unit/cell/row-cell-spec.ts +++ b/packages/s2-core/__tests__/unit/cell/row-cell-spec.ts @@ -1,8 +1,6 @@ -import { get } from 'lodash'; import { createPivotSheet } from 'tests/util/helpers'; -import type { RowCell } from '@antv/s2'; -import type { SpreadSheet } from '@/sheet-type'; import type { TextAlign } from '@/common'; +import type { SpreadSheet } from '@/sheet-type'; describe('Row Cell Tests', () => { describe('Link Shape Tests', () => { @@ -15,8 +13,8 @@ describe('Row Cell Tests', () => { test.each([ ['left', 21], - ['center', 75], - ['right', 129], + ['center', 75.25], + ['right', 129.5], ] as [TextAlign, number][])( 'should align link shape with text by %o', async (textAlign, textCenterX) => { @@ -34,7 +32,7 @@ describe('Row Cell Tests', () => { }); await s2.render(); - const provinceCell = s2.facet.rowHeader!.children[0] as RowCell; + const provinceCell = s2.facet.getRowCells()[0]; const { left: minX, right: maxX } = provinceCell .getLinkFieldShape() .getBBox(); @@ -72,9 +70,9 @@ describe('Row Cell Tests', () => { test('should draw right condition text shape', async () => { await s2.render(); - const rowCell = s2.facet.rowHeader!.children[1] as RowCell; + const rowCell = s2.facet.getRowCells()[1]; - expect(rowCell.getTextShape().parsedStyle.fill).toBeColor('#5083F5'); + expect(rowCell.getTextShape().style.fill).toEqual('#5083F5'); }); test('should draw right condition icon shape', async () => { @@ -93,11 +91,12 @@ describe('Row Cell Tests', () => { ], }, }); + await s2.render(); - const rowCell = s2.facet.rowHeader!.children[1]; + const rowCell = s2.facet.getRowCells()[1]; - expect(get(rowCell, 'conditionIconShape.cfg.name')).toEqual('CellUp'); - expect(get(rowCell, 'conditionIconShape.cfg.fill')).toEqual('red'); + // @ts-ignore + expect(rowCell.rightIconPosition).toEqual({ x: 186.5, y: 9.5 }); }); test('should draw right condition background shape', async () => { @@ -115,12 +114,11 @@ describe('Row Cell Tests', () => { ], }, }); + await s2.render(); - const rowCell = s2.facet.rowHeader!.children[0]; + const rowCell = s2.facet.getRowCells()[0]; - expect(get(rowCell, 'backgroundShape.parsedStyle.fill')).toBeColor( - '#F7B46F', - ); + expect(rowCell.getBackgroundShape().style.fill).toEqual('#F7B46F'); }); test('should render text by text theme', async () => { @@ -140,9 +138,10 @@ describe('Row Cell Tests', () => { ], }, }); + await s2.render(); - const rowCell = s2.facet.rowHeader!.children[1] as RowCell; + const rowCell = s2.facet.getRowCells()[1]; const { fill, fontSize, fontWeight } = rowCell.getTextShape().attributes; expect(fill).toEqual('red'); @@ -150,4 +149,44 @@ describe('Row Cell Tests', () => { expect(fontWeight).toEqual(800); }); }); + + describe('Cross Background Color Tests', () => { + const s2 = createPivotSheet({ + width: 800, + height: 600, + }); + + const crossColor = '#FFFFFF'; + const defaultColor = '#F5F8FF'; + const cellColorConfig = { + crossBackgroundColor: crossColor, + backgroundColor: defaultColor, + }; + + s2.setTheme({ + rowCell: { + cell: cellColorConfig, + }, + dataCell: { + cell: cellColorConfig, + }, + }); + + test('should draw right condition background shape', async () => { + await s2.render(); + + const rowCell0 = s2.facet.getRowCells()[0]; + const rowCell1 = s2.facet.getRowCells()[1]; + const rowCell2 = s2.facet.getRowCells()[2]; + + expect(rowCell0.getActualText()).toEqual('浙江'); + expect(rowCell0.getBackgroundShape().style.fill).toEqual(defaultColor); + + expect(rowCell1.getActualText()).toEqual('义乌'); + expect(rowCell1.getBackgroundShape().style.fill).toEqual(crossColor); + + expect(rowCell2.getActualText()).toEqual('杭州'); + expect(rowCell2.getBackgroundShape().style.fill).toEqual(defaultColor); + }); + }); }); diff --git a/packages/s2-core/__tests__/unit/common/i18n/index-spec.ts b/packages/s2-core/__tests__/unit/common/i18n/index-spec.ts index bad4ef936d..f4849f4766 100644 --- a/packages/s2-core/__tests__/unit/common/i18n/index-spec.ts +++ b/packages/s2-core/__tests__/unit/common/i18n/index-spec.ts @@ -19,7 +19,7 @@ describe('I18n Test', () => { setLang('en_US'); expect(i18n('小计')).toEqual('Total'); expect(i18n('总计')).toEqual('Total'); - expect(i18n('总和')).toEqual('SUM'); + expect(i18n('总和')).toEqual('(SUM)'); expect(i18n('项')).toEqual('items'); expect(i18n('已选择')).toEqual('selected'); expect(i18n('序号')).toEqual('Index'); @@ -27,13 +27,14 @@ describe('I18n Test', () => { expect(i18n('数值')).toEqual('Measure'); expect(i18n('共计')).toEqual('Total'); expect(i18n('条')).toEqual(''); + expect(i18n(',')).toEqual(', '); }); test('should show Chinese text when set lang to zh', () => { setLang('zh_CN'); expect(i18n('小计')).toEqual('小计'); expect(i18n('总计')).toEqual('总计'); - expect(i18n('总和')).toEqual('总和'); + expect(i18n('总和')).toEqual('(总和)'); expect(i18n('项')).toEqual('项'); expect(i18n('已选择')).toEqual('已选择'); expect(i18n('序号')).toEqual('序号'); @@ -41,5 +42,6 @@ describe('I18n Test', () => { expect(i18n('数值')).toEqual('数值'); expect(i18n('共计')).toEqual('共计'); expect(i18n('条')).toEqual('条'); + expect(i18n(',')).toEqual(','); }); }); diff --git a/packages/s2-core/__tests__/unit/data-process/pivot-spec.tsx b/packages/s2-core/__tests__/unit/data-process/pivot-spec.tsx index c07d3637b5..c7d5d0493c 100644 --- a/packages/s2-core/__tests__/unit/data-process/pivot-spec.tsx +++ b/packages/s2-core/__tests__/unit/data-process/pivot-spec.tsx @@ -7,6 +7,7 @@ import { assembleDataCfg, assembleOptions } from '../../util'; import { getContainer } from '../../util/helpers'; import { data } from '../../data/mock-dataset.json'; import type { ViewMeta } from '../../../src/common'; +import type { CellData } from '../../../src'; import { VALUE_FIELD } from '@/common/constant'; import type { PivotDataSet } from '@/data-set/pivot-data-set'; import { PivotSheet, SpreadSheet } from '@/sheet-type'; @@ -49,48 +50,59 @@ describe('Pivot Table Core Data Process', () => { }); test('should get correct indexes data', () => { + const prefix = 'province[&]city[&]type[&]sub_type'; + const ds = s2.dataSet as PivotDataSet; const indexesData = ds.indexesData; - expect(flattenDeep(indexesData).filter(Boolean)).toHaveLength( + expect(flattenDeep(indexesData[prefix]).filter(Boolean)).toHaveLength( data.length, ); - expect(get(indexesData, '1.1.1.1')).toEqual({ + // 左上角 + expect(get(indexesData, [prefix, 1, 1, 1, 1, 1])).toEqual({ province: '浙江省', city: '杭州市', type: '家具', sub_type: '桌子', number: 7789, - }); // 左上角 - expect(get(indexesData, '1.1.2.2')).toEqual({ + }); + + // 右上角 + expect(get(indexesData, [prefix, 1, 1, 2, 2, 1])).toEqual({ province: '浙江省', city: '杭州市', type: '办公用品', sub_type: '纸张', number: 1343, - }); // 右上角 - expect(get(indexesData, '2.4.1.1')).toEqual({ + }); + + // 左下角 + expect(get(indexesData, [prefix, 2, 4, 1, 1, 1])).toEqual({ province: '四川省', city: '乐山市', type: '家具', sub_type: '桌子', number: 2330, - }); // 左下角 - expect(get(indexesData, '2.4.2.2')).toEqual({ + }); + + // 右下角 + expect(get(indexesData, [prefix, 2, 4, 2, 2, 1])).toEqual({ province: '四川省', city: '乐山市', type: '办公用品', sub_type: '纸张', number: 352, - }); // 右下角 - expect(get(indexesData, '1.4.2.1')).toEqual({ + }); + + // 中间 + expect(get(indexesData, [prefix, 1, 4, 2, 1, 1])).toEqual({ province: '浙江省', city: '舟山市', type: '办公用品', sub_type: '笔', number: 1432, - }); // 中间 + }); }); }); @@ -140,6 +152,7 @@ describe('Pivot Table Core Data Process', () => { '南充市', '乐山市', ]); + // 父子关系正确 const leavesNodes = rowsHierarchy.getLeaves(); const firstLeafNode = leavesNodes[0]; @@ -166,26 +179,26 @@ describe('Pivot Table Core Data Process', () => { // 节点正确 expect(colsHierarchy.getIndexNodes()).toHaveLength(4); - expect(colsHierarchy.getNodes()).toHaveLength(10); // 价格在列头 家具[&]桌子[&]number + expect(colsHierarchy.getNodes()).toHaveLength(10); // 价格在列头 家具[&]桌子[&]数量 // 叶子节点正确 expect(colsHierarchy.getLeaves().map((node) => node.value)).toEqual([ - 'number', - 'number', - 'number', - 'number', + '数量', + '数量', + '数量', + '数量', ]); // 层级正确 expect(colsHierarchy.getNodes().map((node) => node.value)).toEqual([ '家具', '桌子', - 'number', + '数量', '沙发', - 'number', + '数量', '办公用品', '笔', - 'number', + '数量', '纸张', - 'number', + '数量', ]); expect(colsHierarchy.getNodes(0).map((node) => node.value)).toEqual([ '家具', @@ -198,16 +211,17 @@ describe('Pivot Table Core Data Process', () => { '纸张', ]); expect(colsHierarchy.getNodes(2).map((node) => node.value)).toEqual([ - 'number', - 'number', - 'number', - 'number', + '数量', + '数量', + '数量', + '数量', ]); + // 父子关系正确 const leavesNodes = colsHierarchy.getLeaves(); const firstLeafNode = leavesNodes[0]; - expect(firstLeafNode.value).toEqual('number'); + expect(firstLeafNode.value).toEqual('数量'); expect(firstLeafNode.parent!.value).toEqual('桌子'); expect(firstLeafNode.parent!.parent?.value).toEqual('家具'); expect( @@ -215,7 +229,7 @@ describe('Pivot Table Core Data Process', () => { ).toEqual(['桌子', '沙发']); const lastLeafNode = leavesNodes[leavesNodes.length - 1]; - expect(lastLeafNode.value).toEqual('number'); + expect(lastLeafNode.value).toEqual('数量'); expect(lastLeafNode.parent!.value).toEqual('纸张'); expect(lastLeafNode.parent!.parent?.value).toEqual('办公用品'); expect( @@ -228,25 +242,25 @@ describe('Pivot Table Core Data Process', () => { test('should calc correct row & cell width', () => { const { rowLeafNodes, colLeafNodes } = s2.facet.getLayoutResult(); - expect(rowLeafNodes[0].width).toEqual(99); - expect(colLeafNodes[0].width).toEqual(100); + expect(rowLeafNodes[0].width).toEqual(99.66); + expect(colLeafNodes[0].width).toEqual(99.67); }); test('should calc correct row node size and coordinate', () => { const { dataCell } = s2.options.style!; const { rowsHierarchy, rowLeafNodes } = s2.facet.getLayoutResult(); // all sample width. - expect(rowsHierarchy.sampleNodesForAllLevels[0]?.width).toEqual(99); - expect(rowsHierarchy.sampleNodesForAllLevels[1]?.width).toEqual(99); + expect(rowsHierarchy.sampleNodesForAllLevels[0]?.width).toEqual(99.66); + expect(rowsHierarchy.sampleNodesForAllLevels[1]?.width).toEqual(99.66); // all width expect(uniq(rowsHierarchy.getNodes().map((node) => node.width))).toEqual([ - 99, + 99.66, ]); // leaf node rowLeafNodes.forEach((node, index) => { expect(node.height).toEqual(dataCell?.height!); expect(node.y).toEqual(node.height * index); - expect(node.x).toEqual(99); + expect(node.x).toEqual(99.66); }); // level = 0 const provinceNodes = rowsHierarchy.getNodes(0); @@ -284,7 +298,7 @@ describe('Pivot Table Core Data Process', () => { colLeafNodes.forEach((node, index) => { const width = Math.floor(node.width); - expect(width).toEqual(100); + expect(width).toEqual(99); expect(node.x).toEqual(node.width * index); expect(node.y).toEqual(node.level * (colCell!.height as number)); }); @@ -316,7 +330,8 @@ describe('Pivot Table Core Data Process', () => { describe('4、Calculate data cell info', () => { test('should get correct data value', () => { const { getCellMeta } = s2.facet; - const getData = (meta: ViewMeta | null) => meta?.data?.[VALUE_FIELD]; + const getData = (meta: ViewMeta | null) => + (meta?.data as CellData)?.[VALUE_FIELD]; // 左上角 expect(getData(getCellMeta(0, 0))).toBe(7789); diff --git a/packages/s2-core/__tests__/unit/data-set/__snapshots__/pivot-data-set-total-spec.ts.snap b/packages/s2-core/__tests__/unit/data-set/__snapshots__/pivot-data-set-total-spec.ts.snap new file mode 100644 index 0000000000..fe05152534 --- /dev/null +++ b/packages/s2-core/__tests__/unit/data-set/__snapshots__/pivot-data-set-total-spec.ts.snap @@ -0,0 +1,133 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Pivot Dataset Total Test test for total with dimension group get correct MultiData when query need to be processed 1`] = ` +Array [ + CellData { + "extraField": "number", + "raw": Object { + "city": "杭州市", + "number": 7789, + "province": "浙江省", + "sub_type": "桌子", + "type": "家具", + }, + }, + CellData { + "extraField": "number", + "raw": Object { + "city": "绍兴市", + "number": 2367, + "province": "浙江省", + "sub_type": "桌子", + "type": "家具", + }, + }, + CellData { + "extraField": "number", + "raw": Object { + "city": "宁波市", + "number": 3877, + "province": "浙江省", + "sub_type": "桌子", + "type": "家具", + }, + }, + CellData { + "extraField": "number", + "raw": Object { + "city": "舟山市", + "number": 4342, + "province": "浙江省", + "sub_type": "桌子", + "type": "家具", + }, + }, +] +`; + +exports[`Pivot Dataset Total Test test for total with dimension group get correct MultiData when query need to be processed 2`] = `Array []`; + +exports[`Pivot Dataset Total Test test for total with dimension group get correct MultiData when query need to be processed 3`] = ` +Array [ + CellData { + "extraField": "number", + "raw": Object { + "city": "杭州市", + "number": 7789, + "province": "浙江省", + "sub_type": "桌子", + "type": "家具", + }, + }, + CellData { + "extraField": "number", + "raw": Object { + "city": "绍兴市", + "number": 2367, + "province": "浙江省", + "sub_type": "桌子", + "type": "家具", + }, + }, + CellData { + "extraField": "number", + "raw": Object { + "city": "宁波市", + "number": 3877, + "province": "浙江省", + "sub_type": "桌子", + "type": "家具", + }, + }, + CellData { + "extraField": "number", + "raw": Object { + "city": "舟山市", + "number": 4342, + "province": "浙江省", + "sub_type": "桌子", + "type": "家具", + }, + }, + CellData { + "extraField": "number", + "raw": Object { + "city": "成都市", + "number": 1723, + "province": "四川省", + "sub_type": "桌子", + "type": "家具", + }, + }, + CellData { + "extraField": "number", + "raw": Object { + "city": "绵阳市", + "number": 1822, + "province": "四川省", + "sub_type": "桌子", + "type": "家具", + }, + }, + CellData { + "extraField": "number", + "raw": Object { + "city": "南充市", + "number": 1943, + "province": "四川省", + "sub_type": "桌子", + "type": "家具", + }, + }, + CellData { + "extraField": "number", + "raw": Object { + "city": "乐山市", + "number": 2330, + "province": "四川省", + "sub_type": "桌子", + "type": "家具", + }, + }, +] +`; diff --git a/packages/s2-core/__tests__/unit/data-set/__snapshots__/table-data-set-spec.ts.snap b/packages/s2-core/__tests__/unit/data-set/__snapshots__/table-data-set-spec.ts.snap new file mode 100644 index 0000000000..13feac731e --- /dev/null +++ b/packages/s2-core/__tests__/unit/data-set/__snapshots__/table-data-set-spec.ts.snap @@ -0,0 +1,1191 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Table Mode Dataset Test test for query data #getMultiData by empty query 1`] = ` +Array [ + Object { + "city": "杭州市", + "number": 7789, + "province": "浙江省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "city": "绍兴市", + "number": 2367, + "province": "浙江省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "city": "宁波市", + "number": 3877, + "province": "浙江省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "city": "舟山市", + "number": 4342, + "province": "浙江省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "city": "杭州市", + "number": 5343, + "province": "浙江省", + "sub_type": "沙发", + "type": "家具", + }, + Object { + "city": "绍兴市", + "number": 632, + "province": "浙江省", + "sub_type": "沙发", + "type": "家具", + }, + Object { + "city": "宁波市", + "number": 7234, + "province": "浙江省", + "sub_type": "沙发", + "type": "家具", + }, + Object { + "city": "舟山市", + "number": 834, + "province": "浙江省", + "sub_type": "沙发", + "type": "家具", + }, + Object { + "city": "杭州市", + "number": 945, + "province": "浙江省", + "sub_type": "笔", + "type": "办公用品", + }, + Object { + "city": "绍兴市", + "number": 1304, + "province": "浙江省", + "sub_type": "笔", + "type": "办公用品", + }, + Object { + "city": "宁波市", + "number": 1145, + "province": "浙江省", + "sub_type": "笔", + "type": "办公用品", + }, + Object { + "city": "舟山市", + "number": 1432, + "province": "浙江省", + "sub_type": "笔", + "type": "办公用品", + }, + Object { + "city": "杭州市", + "number": 1343, + "province": "浙江省", + "sub_type": "纸张", + "type": "办公用品", + }, + Object { + "city": "绍兴市", + "number": 1354, + "province": "浙江省", + "sub_type": "纸张", + "type": "办公用品", + }, + Object { + "city": "宁波市", + "number": 1523, + "province": "浙江省", + "sub_type": "纸张", + "type": "办公用品", + }, + Object { + "city": "舟山市", + "number": 1634, + "province": "浙江省", + "sub_type": "纸张", + "type": "办公用品", + }, + Object { + "city": "成都市", + "number": 1723, + "province": "四川省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "city": "绵阳市", + "number": 1822, + "province": "四川省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "city": "南充市", + "number": 1943, + "province": "四川省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "city": "乐山市", + "number": 2330, + "province": "四川省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "city": "成都市", + "number": 2451, + "province": "四川省", + "sub_type": "沙发", + "type": "家具", + }, + Object { + "city": "绵阳市", + "number": 2244, + "province": "四川省", + "sub_type": "沙发", + "type": "家具", + }, + Object { + "city": "南充市", + "number": 2333, + "province": "四川省", + "sub_type": "沙发", + "type": "家具", + }, + Object { + "city": "乐山市", + "number": 2445, + "province": "四川省", + "sub_type": "沙发", + "type": "家具", + }, + Object { + "city": "成都市", + "number": 2335, + "province": "四川省", + "sub_type": "笔", + "type": "办公用品", + }, + Object { + "city": "绵阳市", + "number": 245, + "province": "四川省", + "sub_type": "笔", + "type": "办公用品", + }, + Object { + "city": "南充市", + "number": 2457, + "province": "四川省", + "sub_type": "笔", + "type": "办公用品", + }, + Object { + "city": "乐山市", + "number": 2458, + "province": "四川省", + "sub_type": "笔", + "type": "办公用品", + }, + Object { + "city": "成都市", + "number": 4004, + "province": "四川省", + "sub_type": "纸张", + "type": "办公用品", + }, + Object { + "city": "绵阳市", + "number": 3077, + "province": "四川省", + "sub_type": "纸张", + "type": "办公用品", + }, + Object { + "city": "南充市", + "number": 3551, + "province": "四川省", + "sub_type": "纸张", + "type": "办公用品", + }, + Object { + "city": "乐山市", + "number": 352, + "province": "四川省", + "sub_type": "纸张", + "type": "办公用品", + }, + Object { + "number": 26193, + "sub_type": "桌子", + "type": "家具", + }, + Object { + "number": 49709, + "type": "家具", + }, + Object { + "number": 23516, + "sub_type": "沙发", + "type": "家具", + }, + Object { + "number": 29159, + "type": "办公用品", + }, + Object { + "number": 12321, + "sub_type": "笔", + "type": "办公用品", + }, + Object { + "number": 16838, + "sub_type": "纸张", + "type": "办公用品", + }, + Object { + "number": 18375, + "province": "浙江省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "number": 14043, + "province": "浙江省", + "sub_type": "沙发", + "type": "家具", + }, + Object { + "number": 4826, + "province": "浙江省", + "sub_type": "笔", + "type": "办公用品", + }, + Object { + "number": 5854, + "province": "浙江省", + "sub_type": "纸张", + "type": "办公用品", + }, + Object { + "number": 7818, + "province": "四川省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "number": 9473, + "province": "四川省", + "sub_type": "沙发", + "type": "家具", + }, + Object { + "number": 7495, + "province": "四川省", + "sub_type": "笔", + "type": "办公用品", + }, + Object { + "number": 10984, + "province": "四川省", + "sub_type": "纸张", + "type": "办公用品", + }, + Object { + "city": "杭州市", + "number": 13132, + "province": "浙江省", + "type": "家具", + }, + Object { + "city": "杭州市", + "number": 2288, + "province": "浙江省", + "type": "办公用品", + }, + Object { + "city": "杭州市", + "number": 15420, + "province": "浙江省", + }, + Object { + "city": "绍兴市", + "number": 2999, + "province": "浙江省", + "type": "家具", + }, + Object { + "city": "绍兴市", + "number": 2658, + "province": "浙江省", + "type": "办公用品", + }, + Object { + "city": "绍兴市", + "number": 5657, + "province": "浙江省", + }, + Object { + "city": "宁波市", + "number": 11111, + "province": "浙江省", + "type": "家具", + }, + Object { + "city": "宁波市", + "number": 2668, + "province": "浙江省", + "type": "办公用品", + }, + Object { + "city": "宁波市", + "number": 13779, + "province": "浙江省", + }, + Object { + "city": "舟山市", + "number": 5176, + "province": "浙江省", + "type": "家具", + }, + Object { + "city": "舟山市", + "number": 3066, + "province": "浙江省", + "type": "办公用品", + }, + Object { + "city": "舟山市", + "number": 8242, + "province": "浙江省", + }, + Object { + "city": "成都市", + "number": 4174, + "province": "四川省", + "type": "家具", + }, + Object { + "city": "成都市", + "number": 6339, + "province": "四川省", + "type": "办公用品", + }, + Object { + "city": "成都市", + "number": 10513, + "province": "四川省", + }, + Object { + "city": "绵阳市", + "number": 4066, + "province": "四川省", + "type": "家具", + }, + Object { + "city": "绵阳市", + "number": 3322, + "province": "四川省", + "type": "办公用品", + }, + Object { + "city": "绵阳市", + "number": 7388, + "province": "四川省", + }, + Object { + "city": "南充市", + "number": 4276, + "province": "四川省", + "type": "家具", + }, + Object { + "city": "南充市", + "number": 6008, + "province": "四川省", + "type": "办公用品", + }, + Object { + "city": "南充市", + "number": 10284, + "province": "四川省", + }, + Object { + "city": "乐山市", + "number": 4775, + "province": "四川省", + "type": "家具", + }, + Object { + "city": "乐山市", + "number": 2810, + "province": "四川省", + "type": "办公用品", + }, + Object { + "city": "乐山市", + "number": 7585, + "province": "四川省", + }, + Object { + "number": 32418, + "province": "浙江省", + "type": "家具", + }, + Object { + "number": 10680, + "province": "浙江省", + "type": "办公用品", + }, + Object { + "number": 43098, + "province": "浙江省", + }, + Object { + "number": 17291, + "province": "四川省", + "type": "家具", + }, + Object { + "number": 18479, + "province": "四川省", + "type": "办公用品", + }, + Object { + "number": 35770, + "province": "四川省", + }, + Object { + "number": 78868, + }, +] +`; + +exports[`Table Mode Dataset Test test for query data #getMultiData by field query 1`] = ` +Array [ + "杭州市", + "绍兴市", + "宁波市", + "舟山市", + "杭州市", + "绍兴市", + "宁波市", + "舟山市", + "杭州市", + "绍兴市", + "宁波市", + "舟山市", + "杭州市", + "绍兴市", + "宁波市", + "舟山市", + "成都市", + "绵阳市", + "南充市", + "乐山市", + "成都市", + "绵阳市", + "南充市", + "乐山市", + "成都市", + "绵阳市", + "南充市", + "乐山市", + "成都市", + "绵阳市", + "南充市", + "乐山市", + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + "杭州市", + "杭州市", + "杭州市", + "绍兴市", + "绍兴市", + "绍兴市", + "宁波市", + "宁波市", + "宁波市", + "舟山市", + "舟山市", + "舟山市", + "成都市", + "成都市", + "成都市", + "绵阳市", + "绵阳市", + "绵阳市", + "南充市", + "南充市", + "南充市", + "乐山市", + "乐山市", + "乐山市", + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, +] +`; + +exports[`Table Mode Dataset Test test for query data #getMultiData by field query 2`] = ` +Array [ + 7789, + 2367, + 3877, + 4342, + 5343, + 632, + 7234, + 834, + 945, + 1304, + 1145, + 1432, + 1343, + 1354, + 1523, + 1634, + 1723, + 1822, + 1943, + 2330, + 2451, + 2244, + 2333, + 2445, + 2335, + 245, + 2457, + 2458, + 4004, + 3077, + 3551, + 352, + 26193, + 49709, + 23516, + 29159, + 12321, + 16838, + 18375, + 14043, + 4826, + 5854, + 7818, + 9473, + 7495, + 10984, + 13132, + 2288, + 15420, + 2999, + 2658, + 5657, + 11111, + 2668, + 13779, + 5176, + 3066, + 8242, + 4174, + 6339, + 10513, + 4066, + 3322, + 7388, + 4276, + 6008, + 10284, + 4775, + 2810, + 7585, + 32418, + 10680, + 43098, + 17291, + 18479, + 35770, + 78868, +] +`; + +exports[`Table Mode Dataset Test test for query data #getMultiData by field query 3`] = ` +Array [ + "桌子", + "桌子", + "桌子", + "桌子", + "沙发", + "沙发", + "沙发", + "沙发", + "笔", + "笔", + "笔", + "笔", + "纸张", + "纸张", + "纸张", + "纸张", + "桌子", + "桌子", + "桌子", + "桌子", + "沙发", + "沙发", + "沙发", + "沙发", + "笔", + "笔", + "笔", + "笔", + "纸张", + "纸张", + "纸张", + "纸张", + "桌子", + undefined, + "沙发", + undefined, + "笔", + "纸张", + "桌子", + "沙发", + "笔", + "纸张", + "桌子", + "沙发", + "笔", + "纸张", + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, +] +`; + +exports[`Table Mode Dataset Test test for query data #getMultiData by rowIndex query 2`] = ` +Array [ + Object { + "city": "杭州市", + "number": 7789, + "province": "浙江省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "city": "绍兴市", + "number": 2367, + "province": "浙江省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "city": "宁波市", + "number": 3877, + "province": "浙江省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "city": "舟山市", + "number": 4342, + "province": "浙江省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "city": "杭州市", + "number": 5343, + "province": "浙江省", + "sub_type": "沙发", + "type": "家具", + }, + Object { + "city": "绍兴市", + "number": 632, + "province": "浙江省", + "sub_type": "沙发", + "type": "家具", + }, + Object { + "city": "宁波市", + "number": 7234, + "province": "浙江省", + "sub_type": "沙发", + "type": "家具", + }, + Object { + "city": "舟山市", + "number": 834, + "province": "浙江省", + "sub_type": "沙发", + "type": "家具", + }, + Object { + "city": "杭州市", + "number": 945, + "province": "浙江省", + "sub_type": "笔", + "type": "办公用品", + }, + Object { + "city": "绍兴市", + "number": 1304, + "province": "浙江省", + "sub_type": "笔", + "type": "办公用品", + }, + Object { + "city": "宁波市", + "number": 1145, + "province": "浙江省", + "sub_type": "笔", + "type": "办公用品", + }, + Object { + "city": "舟山市", + "number": 1432, + "province": "浙江省", + "sub_type": "笔", + "type": "办公用品", + }, + Object { + "city": "杭州市", + "number": 1343, + "province": "浙江省", + "sub_type": "纸张", + "type": "办公用品", + }, + Object { + "city": "绍兴市", + "number": 1354, + "province": "浙江省", + "sub_type": "纸张", + "type": "办公用品", + }, + Object { + "city": "宁波市", + "number": 1523, + "province": "浙江省", + "sub_type": "纸张", + "type": "办公用品", + }, + Object { + "city": "舟山市", + "number": 1634, + "province": "浙江省", + "sub_type": "纸张", + "type": "办公用品", + }, + Object { + "city": "成都市", + "number": 1723, + "province": "四川省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "city": "绵阳市", + "number": 1822, + "province": "四川省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "city": "南充市", + "number": 1943, + "province": "四川省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "city": "乐山市", + "number": 2330, + "province": "四川省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "city": "成都市", + "number": 2451, + "province": "四川省", + "sub_type": "沙发", + "type": "家具", + }, + Object { + "city": "绵阳市", + "number": 2244, + "province": "四川省", + "sub_type": "沙发", + "type": "家具", + }, + Object { + "city": "南充市", + "number": 2333, + "province": "四川省", + "sub_type": "沙发", + "type": "家具", + }, + Object { + "city": "乐山市", + "number": 2445, + "province": "四川省", + "sub_type": "沙发", + "type": "家具", + }, + Object { + "city": "成都市", + "number": 2335, + "province": "四川省", + "sub_type": "笔", + "type": "办公用品", + }, + Object { + "city": "绵阳市", + "number": 245, + "province": "四川省", + "sub_type": "笔", + "type": "办公用品", + }, + Object { + "city": "南充市", + "number": 2457, + "province": "四川省", + "sub_type": "笔", + "type": "办公用品", + }, + Object { + "city": "乐山市", + "number": 2458, + "province": "四川省", + "sub_type": "笔", + "type": "办公用品", + }, + Object { + "city": "成都市", + "number": 4004, + "province": "四川省", + "sub_type": "纸张", + "type": "办公用品", + }, + Object { + "city": "绵阳市", + "number": 3077, + "province": "四川省", + "sub_type": "纸张", + "type": "办公用品", + }, + Object { + "city": "南充市", + "number": 3551, + "province": "四川省", + "sub_type": "纸张", + "type": "办公用品", + }, + Object { + "city": "乐山市", + "number": 352, + "province": "四川省", + "sub_type": "纸张", + "type": "办公用品", + }, + Object { + "number": 26193, + "sub_type": "桌子", + "type": "家具", + }, + Object { + "number": 49709, + "type": "家具", + }, + Object { + "number": 23516, + "sub_type": "沙发", + "type": "家具", + }, + Object { + "number": 29159, + "type": "办公用品", + }, + Object { + "number": 12321, + "sub_type": "笔", + "type": "办公用品", + }, + Object { + "number": 16838, + "sub_type": "纸张", + "type": "办公用品", + }, + Object { + "number": 18375, + "province": "浙江省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "number": 14043, + "province": "浙江省", + "sub_type": "沙发", + "type": "家具", + }, + Object { + "number": 4826, + "province": "浙江省", + "sub_type": "笔", + "type": "办公用品", + }, + Object { + "number": 5854, + "province": "浙江省", + "sub_type": "纸张", + "type": "办公用品", + }, + Object { + "number": 7818, + "province": "四川省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "number": 9473, + "province": "四川省", + "sub_type": "沙发", + "type": "家具", + }, + Object { + "number": 7495, + "province": "四川省", + "sub_type": "笔", + "type": "办公用品", + }, + Object { + "number": 10984, + "province": "四川省", + "sub_type": "纸张", + "type": "办公用品", + }, + Object { + "city": "杭州市", + "number": 13132, + "province": "浙江省", + "type": "家具", + }, + Object { + "city": "杭州市", + "number": 2288, + "province": "浙江省", + "type": "办公用品", + }, + Object { + "city": "杭州市", + "number": 15420, + "province": "浙江省", + }, + Object { + "city": "绍兴市", + "number": 2999, + "province": "浙江省", + "type": "家具", + }, + Object { + "city": "绍兴市", + "number": 2658, + "province": "浙江省", + "type": "办公用品", + }, + Object { + "city": "绍兴市", + "number": 5657, + "province": "浙江省", + }, + Object { + "city": "宁波市", + "number": 11111, + "province": "浙江省", + "type": "家具", + }, + Object { + "city": "宁波市", + "number": 2668, + "province": "浙江省", + "type": "办公用品", + }, + Object { + "city": "宁波市", + "number": 13779, + "province": "浙江省", + }, + Object { + "city": "舟山市", + "number": 5176, + "province": "浙江省", + "type": "家具", + }, + Object { + "city": "舟山市", + "number": 3066, + "province": "浙江省", + "type": "办公用品", + }, + Object { + "city": "舟山市", + "number": 8242, + "province": "浙江省", + }, + Object { + "city": "成都市", + "number": 4174, + "province": "四川省", + "type": "家具", + }, + Object { + "city": "成都市", + "number": 6339, + "province": "四川省", + "type": "办公用品", + }, + Object { + "city": "成都市", + "number": 10513, + "province": "四川省", + }, + Object { + "city": "绵阳市", + "number": 4066, + "province": "四川省", + "type": "家具", + }, + Object { + "city": "绵阳市", + "number": 3322, + "province": "四川省", + "type": "办公用品", + }, + Object { + "city": "绵阳市", + "number": 7388, + "province": "四川省", + }, + Object { + "city": "南充市", + "number": 4276, + "province": "四川省", + "type": "家具", + }, + Object { + "city": "南充市", + "number": 6008, + "province": "四川省", + "type": "办公用品", + }, + Object { + "city": "南充市", + "number": 10284, + "province": "四川省", + }, + Object { + "city": "乐山市", + "number": 4775, + "province": "四川省", + "type": "家具", + }, + Object { + "city": "乐山市", + "number": 2810, + "province": "四川省", + "type": "办公用品", + }, + Object { + "city": "乐山市", + "number": 7585, + "province": "四川省", + }, + Object { + "number": 32418, + "province": "浙江省", + "type": "家具", + }, + Object { + "number": 10680, + "province": "浙江省", + "type": "办公用品", + }, + Object { + "number": 43098, + "province": "浙江省", + }, + Object { + "number": 17291, + "province": "四川省", + "type": "家具", + }, + Object { + "number": 18479, + "province": "四川省", + "type": "办公用品", + }, + Object { + "number": 35770, + "province": "四川省", + }, + Object { + "number": 78868, + }, +] +`; diff --git a/packages/s2-core/__tests__/unit/data-set/custom-tree-data-set-spec.ts b/packages/s2-core/__tests__/unit/data-set/custom-tree-data-set-spec.ts index d4fc7554d9..34b2cc9a8c 100644 --- a/packages/s2-core/__tests__/unit/data-set/custom-tree-data-set-spec.ts +++ b/packages/s2-core/__tests__/unit/data-set/custom-tree-data-set-spec.ts @@ -5,7 +5,7 @@ import { get } from 'lodash'; import { customTreeNodes } from 'tests/data/custom-tree-nodes'; import { CustomTreeData } from 'tests/data/data-custom-tree'; import type { S2DataConfig } from '@/common/interface'; -import { EXTRA_FIELD } from '@/common/constant'; +import { EXTRA_FIELD, ORIGIN_FIELD } from '@/common/constant'; import { PivotSheet } from '@/sheet-type'; import { CustomTreePivotDataSet } from '@/data-set/custom-tree-pivot-data-set'; @@ -50,7 +50,7 @@ describe('Custom Tree Dataset Test', () => { test('should get empty row pivot meta', () => { const rowPivotMeta = dataSet.rowPivotMeta; - expect([...rowPivotMeta.keys()]).toEqual([]); + expect([...rowPivotMeta.keys()]).toEqual(values); }); test('should get correct col pivot meta', () => { @@ -66,9 +66,10 @@ describe('Custom Tree Dataset Test', () => { }); test('should get correct indexesData', () => { + const prefix = 'type[&]sub_type'; const indexesData = dataSet.indexesData; - expect(get(indexesData, '1.1')).toEqual({ + expect(get(indexesData, [prefix, 1, 1, 1])).toEqual({ type: '家具', sub_type: '桌子', 'measure-a': 1, @@ -78,7 +79,7 @@ describe('Custom Tree Dataset Test', () => { 'measure-e': 5, 'measure-f': 6, }); - expect(get(indexesData, '1.2')).toEqual({ + expect(get(indexesData, [prefix, 1, 1, 2])).toEqual({ type: '家具', sub_type: '椅子', 'measure-a': 11, @@ -94,27 +95,23 @@ describe('Custom Tree Dataset Test', () => { describe('test for query data', () => { test('getCellData function', () => { expect( - dataSet - .getCellData({ - query: { - type: '家具', - sub_type: '桌子', - [EXTRA_FIELD]: 'measure-a', - }, - })! - .getOrigin(), + dataSet.getCellData({ + query: { + type: '家具', + sub_type: '桌子', + [EXTRA_FIELD]: 'measure-a', + }, + })?.[ORIGIN_FIELD], ).toContainEntries([['measure-a', 1]]); expect( - dataSet - .getCellData({ - query: { - type: '家具', - sub_type: '椅子', - [EXTRA_FIELD]: 'measure-e', - }, - })! - .getOrigin(), + dataSet.getCellData({ + query: { + type: '家具', + sub_type: '椅子', + [EXTRA_FIELD]: 'measure-e', + }, + })?.[ORIGIN_FIELD], ).toContainEntries([['measure-e', 55]]); }); }); diff --git a/packages/s2-core/__tests__/unit/data-set/pivot-data-set-row-value-spec.ts b/packages/s2-core/__tests__/unit/data-set/pivot-data-set-row-value-spec.ts index 0c08e25cd5..40ba2a4ccf 100644 --- a/packages/s2-core/__tests__/unit/data-set/pivot-data-set-row-value-spec.ts +++ b/packages/s2-core/__tests__/unit/data-set/pivot-data-set-row-value-spec.ts @@ -4,7 +4,7 @@ import { get, keys } from 'lodash'; import { data } from 'tests/data/mock-dataset.json'; import { assembleDataCfg } from '../../util'; -import { EXTRA_FIELD } from '@/common/constant'; +import { EXTRA_FIELD, VALUE_FIELD } from '@/common/constant'; import type { S2DataConfig } from '@/common/interface'; import { PivotSheet } from '@/sheet-type'; import { PivotDataSet } from '@/data-set/pivot-data-set'; @@ -95,23 +95,24 @@ describe('Pivot Mode Test When Value In Row', () => { }); test('should get correct indexesData', () => { + const prefix = 'province[&]city[&]type[&]sub_type'; const indexesData = dataSet.indexesData; - expect(get(indexesData, '1.1.1.1')).toEqual({ + expect(get(indexesData, [prefix, 1, 1, 1, 1, 1])).toEqual({ province: '浙江省', city: '杭州市', type: '家具', sub_type: '桌子', number: 7789, }); - expect(get(indexesData, '1.2.2.1')).toEqual({ + expect(get(indexesData, [prefix, 1, 2, 1, 1, 2])).toEqual({ province: '浙江省', city: '绍兴市', - type: '办公用品', - sub_type: '笔', - number: 1304, + type: '家具', + sub_type: '沙发', + number: 632, }); - expect(get(indexesData, '2.1.1.2')).toEqual({ + expect(get(indexesData, [prefix, 2, 1, 1, 1, 2])).toEqual({ province: '四川省', city: '成都市', type: '家具', @@ -126,6 +127,7 @@ describe('Pivot Mode Test When Value In Row', () => { expect([...keys(sortedDimensionValues)]).toEqual([ 'province', 'city', + EXTRA_FIELD, 'type', 'sub_type', ]); @@ -133,8 +135,98 @@ describe('Pivot Mode Test When Value In Row', () => { getDimensionsWithoutPathPre(sortedDimensionValues['province']), ).toEqual(['浙江省', '四川省']); expect( - getDimensionsWithoutPathPre(sortedDimensionValues['city']), - ).toEqual([ + getDimensionsWithoutPathPre(sortedDimensionValues['sub_type']), + ).toEqual(['桌子', '沙发', '笔', '纸张']); + }); + }); + + describe('test for query data', () => { + test('getCellData function', () => { + expect( + dataSet.getCellData({ + query: { + province: '浙江省', + city: '杭州市', + type: '家具', + sub_type: '桌子', + [EXTRA_FIELD]: 'number', + }, + })?.[VALUE_FIELD], + ).toEqual(7789); + + expect( + dataSet.getCellData({ + query: { + province: '四川省', + city: '乐山市', + type: '办公用品', + sub_type: '纸张', + [EXTRA_FIELD]: 'number', + }, + })?.[VALUE_FIELD], + ).toEqual(352); + }); + + test('getMultiData function', () => { + const specialQuery = { + province: '浙江省', + city: '杭州市', + type: '家具', + sub_type: '桌子', + [EXTRA_FIELD]: 'number', + }; + + expect(dataSet.getCellMultiData({ query: specialQuery })).toHaveLength(1); + expect( + dataSet.getCellMultiData({ query: specialQuery })[0]?.[VALUE_FIELD], + ).toEqual(7789); + + expect( + dataSet.getCellMultiData({ + query: { + province: '浙江省', + type: '家具', + sub_type: '桌子', + [EXTRA_FIELD]: 'number', + }, + }), + ).toHaveLength(4); + + expect( + dataSet.getCellMultiData({ + query: { + type: '家具', + sub_type: '桌子', + [EXTRA_FIELD]: 'number', + }, + }), + ).toHaveLength(8); + + expect( + dataSet.getCellMultiData({ + query: { + type: '家具', + [EXTRA_FIELD]: 'number', + }, + }), + ).toHaveLength(16); + + expect( + dataSet.getCellMultiData({ + query: { + [EXTRA_FIELD]: 'number', + }, + }), + ).toHaveLength(32); + }); + + test('getDimensionValues function', () => { + // without query + expect(dataSet.getDimensionValues('province')).toEqual([ + '浙江省', + '四川省', + ]); + expect(dataSet.getDimensionValues('city')).toEqual([ '杭州市', '绍兴市', '宁波市', @@ -144,6 +236,19 @@ describe('Pivot Mode Test When Value In Row', () => { '南充市', '乐山市', ]); + expect(dataSet.getDimensionValues('type')).toEqual(['家具', '办公用品']); + expect(dataSet.getDimensionValues('sub_type')).toEqual([ + '桌子', + '沙发', + '笔', + '纸张', + ]); + + expect(dataSet.getDimensionValues('empty')).toEqual([]); + + const sortedDimensionValues = dataSet.sortedDimensionValues; + + // with query expect( getDimensionsWithoutPathPre(sortedDimensionValues['type']), ).toEqual(['家具', '办公用品']); diff --git a/packages/s2-core/__tests__/unit/data-set/pivot-data-set-spec.ts b/packages/s2-core/__tests__/unit/data-set/pivot-data-set-spec.ts index 929f98ca26..0e24c8451e 100644 --- a/packages/s2-core/__tests__/unit/data-set/pivot-data-set-spec.ts +++ b/packages/s2-core/__tests__/unit/data-set/pivot-data-set-spec.ts @@ -14,7 +14,7 @@ import type { SortMethod, CustomHeaderField, } from '@/common/interface'; -import { EXTRA_FIELD, TOTAL_VALUE } from '@/common/constant'; +import { EXTRA_FIELD, ORIGIN_FIELD, TOTAL_VALUE } from '@/common/constant'; import type { S2DataConfig } from '@/common/interface'; import { PivotSheet } from '@/sheet-type'; import { PivotDataSet } from '@/data-set/pivot-data-set'; @@ -105,22 +105,23 @@ describe('Pivot Dataset Test', () => { test('should get correct indexesData', () => { const indexesData = dataSet.indexesData; + const prefix = 'province[&]city[&]type[&]sub_type'; - expect(get(indexesData, '1.1.1.1')).toEqual({ + expect(get(indexesData, [prefix, 1, 1, 1, 1, 1])).toEqual({ province: '浙江省', city: '杭州市', type: '家具', sub_type: '桌子', number: 7789, }); - expect(get(indexesData, '1.2.2.1')).toEqual({ + expect(get(indexesData, [prefix, 1, 2, 2, 1, 1])).toEqual({ province: '浙江省', city: '绍兴市', type: '办公用品', sub_type: '笔', number: 1304, }); - expect(get(indexesData, '2.1.1.2')).toEqual({ + expect(get(indexesData, [prefix, 2, 1, 1, 2, 1])).toEqual({ province: '四川省', city: '成都市', type: '家具', @@ -137,6 +138,7 @@ describe('Pivot Dataset Test', () => { 'city', 'type', 'sub_type', + EXTRA_FIELD, ]); expect( getDimensionsWithoutPathPre(sortedDimensionValues['province']), @@ -160,6 +162,10 @@ describe('Pivot Dataset Test', () => { getDimensionsWithoutPathPre(sortedDimensionValues['sub_type']), ).toEqual(['桌子', '沙发', '笔', '纸张']); }); + + test('should get correctly empty dataset result', () => { + expect(dataSet.isEmpty()).toBeFalsy(); + }); }); describe('test for query data', () => { @@ -175,7 +181,7 @@ describe('Pivot Dataset Test', () => { isTotals: true, }); - expect(cell1!.getOrigin()).toContainEntries([['number', 7789]]); + expect(cell1?.[ORIGIN_FIELD]).toContainEntries([['number', 7789]]); const cell2 = dataSet.getCellData({ query: { @@ -188,7 +194,7 @@ describe('Pivot Dataset Test', () => { isTotals: true, }); - expect(cell2!.getOrigin()).toContainEntries([['number', 352]]); + expect(cell2?.[ORIGIN_FIELD]).toContainEntries([['number', 352]]); }); describe('getCellMultiData function', () => { @@ -208,89 +214,9 @@ describe('Pivot Dataset Test', () => { 1, ); expect( - dataSet.getCellMultiData({ query: specialQuery })[0].getOrigin(), + dataSet.getCellMultiData({ query: specialQuery })[0]?.[ORIGIN_FIELD], ).toContainEntries([['number', 7789]]); }); - - test('should get all detail data when child dimension is not specified', () => { - expect( - dataSet.getCellMultiData({ - query: { - province: '浙江省', - type: '家具', - sub_type: '桌子', - [EXTRA_FIELD]: 'number', - }, - totals: { - row: { totalDimensions: false }, - column: { totalDimensions: false }, - }, - }), - ).toHaveLength(4); - - expect( - dataSet.getCellMultiData({ - query: { - type: '家具', - sub_type: '桌子', - [EXTRA_FIELD]: 'number', - }, - totals: { - row: { totalDimensions: false }, - column: { totalDimensions: false }, - }, - }), - ).toHaveLength(8); - - expect( - dataSet.getCellMultiData({ - query: { - type: '家具', - [EXTRA_FIELD]: 'number', - }, - totals: { - row: { totalDimensions: false }, - column: { totalDimensions: false }, - }, - }), - ).toHaveLength(16); - - expect( - dataSet.getCellMultiData({ - query: { - [EXTRA_FIELD]: 'number', - }, - totals: { - row: { totalDimensions: false }, - column: { totalDimensions: false }, - }, - }), - ).toHaveLength(32); - }); - - test('should only query grand total data', () => { - expect( - dataSet.getCellMultiData({ - query: { [EXTRA_FIELD]: 'number' }, - totals: { - row: { grandTotalOnly: true }, - column: { grandTotalOnly: true }, - }, - }), - ).toHaveLength(1); - }); - - test('should query all grand total and sub total data in columns for all cities', () => { - expect( - dataSet.getCellMultiData({ - query: { [EXTRA_FIELD]: 'number' }, - totals: { - row: { totalDimensions: false }, - column: { grandTotalOnly: false, subTotalOnly: true }, - }, - }), - ).toHaveLength(24); - }); }); test('getDimensionValues function', () => { diff --git a/packages/s2-core/__tests__/unit/data-set/pivot-data-set-total-spec.ts b/packages/s2-core/__tests__/unit/data-set/pivot-data-set-total-spec.ts index f6261ede2a..cd8f4a4f03 100644 --- a/packages/s2-core/__tests__/unit/data-set/pivot-data-set-total-spec.ts +++ b/packages/s2-core/__tests__/unit/data-set/pivot-data-set-total-spec.ts @@ -2,19 +2,25 @@ * pivot mode base data-set test. */ import { get, keys } from 'lodash'; -import * as multiDataCfg from 'tests/data/simple-data.json'; import * as mockData from 'tests/data/mock-dataset.json'; -import { assembleDataCfg, TOTALS_OPTIONS } from '../../util'; +import * as multiDataCfg from 'tests/data/simple-data.json'; import type { Query } from '../../../src/data-set/interface'; -import { EXTRA_FIELD, TOTAL_VALUE, VALUE_FIELD } from '@/common/constant'; +import { TOTALS_OPTIONS, assembleDataCfg } from '../../util'; +import { + EXTRA_FIELD, + ORIGIN_FIELD, + QueryDataType, + TOTAL_VALUE, + VALUE_FIELD, +} from '@/common/constant'; import { - type S2DataConfig, Aggregation, type CalcTotals, + type S2DataConfig, } from '@/common/interface'; -import { PivotSheet } from '@/sheet-type'; -import { PivotDataSet } from '@/data-set/pivot-data-set'; import { Store } from '@/common/store'; +import { PivotDataSet } from '@/data-set/pivot-data-set'; +import { PivotSheet } from '@/sheet-type'; import { getDimensionsWithoutPathPre } from '@/utils/dataset/pivot-data-set'; jest.mock('@/sheet-type'); @@ -78,6 +84,12 @@ describe('Pivot Dataset Total Test', () => { TOTAL_VALUE, ]); + expect([...colPivotMeta.get('家具')!.children.keys()]).toEqual([ + '桌子', + '沙发', + TOTAL_VALUE, + ]); + expect([...colPivotMeta.get('办公用品')!.children.keys()]).toEqual([ '笔', '纸张', @@ -88,24 +100,32 @@ describe('Pivot Dataset Total Test', () => { test('should get correct indexesData', () => { const indexesData = dataSet.indexesData; - expect(get(indexesData, '1.1.0.0')).toEqual({ + expect( + get(indexesData, ['province[&]city[&]type[&]sub_type', 1, 1, 0, 0, 1]), + ).toEqual({ province: '浙江省', city: '杭州市', number: 15420, }); - expect(get(indexesData, '1.1.2.0')).toEqual({ + expect( + get(indexesData, ['province[&]city[&]type[&]sub_type', 1, 1, 2, 0, 1]), + ).toEqual({ province: '浙江省', city: '杭州市', type: '办公用品', number: 2288, }); - expect(get(indexesData, '2.0.2.0')).toEqual({ + expect( + get(indexesData, ['province[&]city[&]type[&]sub_type', 2, 0, 2, 0, 1]), + ).toEqual({ province: '四川省', type: '办公用品', number: 18479, }); - expect(get(indexesData, '0.0.0.0')).toEqual({ + expect( + get(indexesData, ['province[&]city[&]type[&]sub_type', 0, 0, 0, 0, 1]), + ).toEqual({ number: 78868, }); }); @@ -118,10 +138,12 @@ describe('Pivot Dataset Total Test', () => { 'city', 'type', 'sub_type', + EXTRA_FIELD, ]); expect( getDimensionsWithoutPathPre(sortedDimensionValues['province']), ).toEqual(['浙江省', '四川省', TOTAL_VALUE]); + expect( getDimensionsWithoutPathPre(sortedDimensionValues['city']), ).toEqual([ @@ -140,6 +162,9 @@ describe('Pivot Dataset Total Test', () => { expect( getDimensionsWithoutPathPre(sortedDimensionValues['type']), ).toEqual(['家具', '办公用品', TOTAL_VALUE]); + expect( + getDimensionsWithoutPathPre(sortedDimensionValues['type']), + ).toEqual(['家具', '办公用品', TOTAL_VALUE]); expect( getDimensionsWithoutPathPre(sortedDimensionValues['sub_type']), ).toEqual([ @@ -151,80 +176,79 @@ describe('Pivot Dataset Total Test', () => { TOTAL_VALUE, TOTAL_VALUE, ]); + expect( + getDimensionsWithoutPathPre(sortedDimensionValues[EXTRA_FIELD]), + ).toEqual([ + 'number', + 'number', + 'number', + 'number', + 'number', + 'number', + 'number', + ]); }); }); describe('test for query data', () => { test('getCellData function', () => { expect( - dataSet - .getCellData({ - query: { - province: '浙江省', - type: '家具', - sub_type: '桌子', - [EXTRA_FIELD]: 'number', - }, - })! - .getOrigin(), + dataSet.getCellData({ + query: { + province: '浙江省', + type: '家具', + sub_type: '桌子', + [EXTRA_FIELD]: 'number', + }, + })?.[ORIGIN_FIELD], ).toContainEntries([['number', 18375]]); expect( - dataSet - .getCellData({ - query: { - type: '家具', - sub_type: '桌子', - [EXTRA_FIELD]: 'number', - }, - })! - .getOrigin(), + dataSet.getCellData({ + query: { + type: '家具', + sub_type: '桌子', + [EXTRA_FIELD]: 'number', + }, + })?.[ORIGIN_FIELD], ).toContainEntries([['number', 26193]]); expect( - dataSet - .getCellData({ - query: { - province: '浙江省', - city: '杭州市', - type: '家具', - [EXTRA_FIELD]: 'number', - }, - })! - .getOrigin(), + dataSet.getCellData({ + query: { + province: '浙江省', + city: '杭州市', + type: '家具', + [EXTRA_FIELD]: 'number', + }, + })?.[ORIGIN_FIELD], ).toContainEntries([['number', 13132]]); expect( - dataSet - .getCellData({ - query: { - province: '浙江省', - city: '杭州市', - [EXTRA_FIELD]: 'number', - }, - })! - .getOrigin(), + dataSet.getCellData({ + query: { + province: '浙江省', + city: '杭州市', + [EXTRA_FIELD]: 'number', + }, + })?.[ORIGIN_FIELD], ).toContainEntries([['number', 15420]]); expect( - dataSet - .getCellData({ - query: { - type: '家具', - [EXTRA_FIELD]: 'number', - }, - })! - .getOrigin(), + dataSet.getCellData({ + query: { + type: '家具', + [EXTRA_FIELD]: 'number', + }, + })?.[ORIGIN_FIELD], ).toContainEntries([['number', 49709]]); expect( - dataSet - .getCellData({ - query: { - [EXTRA_FIELD]: 'number', - }, - })! - .getOrigin(), + dataSet.getCellData({ + query: { + [EXTRA_FIELD]: 'number', + }, + })?.[ORIGIN_FIELD], ).toContainEntries([['number', 78868]]); }); @@ -268,80 +292,68 @@ describe('Pivot Dataset Total Test', () => { }); test('should get correct total cell data when calculated by aggregation', () => { expect( - dataSet - .getCellData({ - query: { - province: '浙江省', - type: '家具', - sub_type: '桌子', - [EXTRA_FIELD]: 'number', - }, - isTotals: true, - })! - .getOrigin(), + dataSet.getCellData({ + query: { + province: '浙江省', + type: '家具', + sub_type: '桌子', + [EXTRA_FIELD]: 'number', + }, + isTotals: true, + })?.[ORIGIN_FIELD], ).toContainEntries([['number', 18375]]); expect( - dataSet - .getCellData({ - query: { - type: '家具', - sub_type: '桌子', - [EXTRA_FIELD]: 'number', - }, - isTotals: true, - })! - .getOrigin(), + dataSet.getCellData({ + query: { + type: '家具', + sub_type: '桌子', + [EXTRA_FIELD]: 'number', + }, + isTotals: true, + })?.[ORIGIN_FIELD], ).toContainEntries([['number', 26193]]); expect( - dataSet - .getCellData({ - query: { - province: '浙江省', - city: '杭州市', - type: '家具', - [EXTRA_FIELD]: 'number', - }, - isTotals: true, - })! - .getOrigin(), + dataSet.getCellData({ + query: { + province: '浙江省', + city: '杭州市', + type: '家具', + [EXTRA_FIELD]: 'number', + }, + isTotals: true, + })?.[ORIGIN_FIELD], ).toContainEntries([['number', 13132]]); expect( - dataSet - .getCellData({ - query: { - province: '浙江省', - city: '杭州市', - [EXTRA_FIELD]: 'number', - }, - isTotals: true, - })! - .getOrigin(), + dataSet.getCellData({ + query: { + province: '浙江省', + city: '杭州市', + [EXTRA_FIELD]: 'number', + }, + isTotals: true, + })?.[ORIGIN_FIELD], ).toContainEntries([['number', 15420]]); expect( - dataSet - .getCellData({ - query: { - type: '家具', - [EXTRA_FIELD]: 'number', - }, - isTotals: true, - })! - .getOrigin(), + dataSet.getCellData({ + query: { + type: '家具', + [EXTRA_FIELD]: 'number', + }, + isTotals: true, + })?.[ORIGIN_FIELD], ).toContainEntries([['number', 49709]]); expect( - dataSet - .getCellData({ - query: { - [EXTRA_FIELD]: 'number', - }, - isTotals: true, - })! - .getOrigin(), + dataSet.getCellData({ + query: { + [EXTRA_FIELD]: 'number', + }, + isTotals: true, + })?.[ORIGIN_FIELD], ).toContainEntries([['number', 78868]]); }); @@ -389,28 +401,24 @@ describe('Pivot Dataset Total Test', () => { }); test('should get correct total cell data when calculated by aggregation and multi values', () => { expect( - dataSet - .getCellData({ - query: { - province: '浙江', - type: '笔', - [EXTRA_FIELD]: 'price', - }, - isTotals: true, - })! - .getOrigin(), + dataSet.getCellData({ + query: { + province: '浙江', + type: '笔', + [EXTRA_FIELD]: 'price', + }, + isTotals: true, + })?.[ORIGIN_FIELD], ).toContainEntries([['price', 2]]); expect( - dataSet - .getCellData({ - query: { - province: '浙江', - type: '笔', - [EXTRA_FIELD]: 'cost', - }, - isTotals: true, - })! - .getOrigin(), + dataSet.getCellData({ + query: { + province: '浙江', + type: '笔', + [EXTRA_FIELD]: 'cost', + }, + isTotals: true, + })?.[ORIGIN_FIELD], ).toContainEntries([['cost', 4]]); }); }); @@ -473,82 +481,126 @@ describe('Pivot Dataset Total Test', () => { dataSet = new PivotDataSet(mockSheet); dataSet.setDataCfg(dataCfg); }); + + test('should get correct total cell data when totals calculated by calcFunc and Existential dimension grouping', () => { + const totalStatus = { + isRowTotal: true, + isColTotal: true, + isRowSubTotal: true, + isColSubTotal: true, + }; + + expect( + dataSet.getCellData({ + query: { + province: '浙江省', + sub_type: '桌子', + [EXTRA_FIELD]: 'number', + }, + isTotals: true, + totalStatus, + })?.[VALUE_FIELD], + ).toEqual(18375); + + expect( + dataSet.getCellData({ + query: { + province: '浙江省', + [EXTRA_FIELD]: 'number', + }, + totalStatus, + isTotals: true, + })?.[VALUE_FIELD], + ).toEqual(43098); + + expect( + dataSet.getCellData({ + query: { + sub_type: '桌子', + [EXTRA_FIELD]: 'number', + }, + totalStatus, + isTotals: true, + })?.[VALUE_FIELD], + ).toEqual(26193); + + expect( + dataSet.getCellData({ + query: { + province: '浙江省', + type: '家具', + [EXTRA_FIELD]: 'number', + }, + isTotals: true, + totalStatus, + })?.[VALUE_FIELD], + ).toEqual(32418); + }); + test('should get correct total cell data when totals calculated by calcFunc', () => { expect( - dataSet - .getCellData({ - query: { - province: '浙江省', - type: '家具', - sub_type: '桌子', - [EXTRA_FIELD]: 'number', - }, - isTotals: true, - })! - .getOrigin(), + dataSet.getCellData({ + query: { + province: '浙江省', + type: '家具', + sub_type: '桌子', + [EXTRA_FIELD]: 'number', + }, + isTotals: true, + })?.[ORIGIN_FIELD], ).toContainEntries([['number', 18375]]); expect( - dataSet - .getCellData({ - query: { - type: '家具', - sub_type: '桌子', - [EXTRA_FIELD]: 'number', - }, - isTotals: true, - })! - .getOrigin(), + dataSet.getCellData({ + query: { + type: '家具', + sub_type: '桌子', + [EXTRA_FIELD]: 'number', + }, + isTotals: true, + })?.[ORIGIN_FIELD], ).toContainEntries([['number', 52386]]); expect( - dataSet - .getCellData({ - query: { - province: '浙江省', - city: '杭州市', - type: '家具', - [EXTRA_FIELD]: 'number', - }, - isTotals: true, - })! - .getOrigin(), + dataSet.getCellData({ + query: { + province: '浙江省', + city: '杭州市', + type: '家具', + [EXTRA_FIELD]: 'number', + }, + isTotals: true, + })?.[ORIGIN_FIELD], ).toContainEntries([['number', 26264]]); expect( - dataSet - .getCellData({ - query: { - province: '浙江省', - city: '杭州市', - [EXTRA_FIELD]: 'number', - }, - isTotals: true, - })! - .getOrigin(), + dataSet.getCellData({ + query: { + province: '浙江省', + city: '杭州市', + [EXTRA_FIELD]: 'number', + }, + isTotals: true, + })?.[ORIGIN_FIELD], ).toContainEntries([['number', 15420]]); expect( - dataSet - .getCellData({ - query: { - type: '家具', - [EXTRA_FIELD]: 'number', - }, - isTotals: true, - })! - .getOrigin(), + dataSet.getCellData({ + query: { + type: '家具', + [EXTRA_FIELD]: 'number', + }, + isTotals: true, + })?.[ORIGIN_FIELD], ).toContainEntries([['number', 99418]]); expect( - dataSet - .getCellData({ - query: { - [EXTRA_FIELD]: 'number', - }, - isTotals: true, - })! - .getOrigin(), + dataSet.getCellData({ + query: { + [EXTRA_FIELD]: 'number', + }, + isTotals: true, + })?.[ORIGIN_FIELD], ).toContainEntries([['number', 78868]]); }); }); @@ -564,7 +616,7 @@ describe('Pivot Dataset Total Test', () => { expect(dataSet.getCellMultiData({ query: specialQuery })).toHaveLength(1); expect( - dataSet.getCellMultiData({ query: specialQuery })[0].getOrigin(), + dataSet.getCellMultiData({ query: specialQuery })[0]?.[ORIGIN_FIELD], ).toContainEntries([['number', 7789]]); expect( dataSet.getCellMultiData({ @@ -721,4 +773,53 @@ describe('Pivot Dataset Total Test', () => { expect(isColSubTotal4).toBeTrue(); }); }); + + describe('test for total with dimension group', () => { + beforeEach(() => { + MockPivotSheet.mockClear(); + const mockSheet = new MockPivotSheet(); + + mockSheet.store = new Store(); + mockSheet.isValueInCols = () => true; + dataSet = new PivotDataSet(mockSheet); + + dataCfg = assembleDataCfg({ + meta: [], + fields: { + rows: ['province', 'city', 'type', 'sub_type'], + columns: [], + }, + }); + dataSet.setDataCfg(dataCfg); + }); + + test('get correct MultiData when query need to be processed', () => { + expect( + dataSet.getCellMultiData({ + query: { + province: '浙江省', + sub_type: '桌子', + }, + queryType: QueryDataType.DetailOnly, + }), + ).toMatchSnapshot(); + expect( + dataSet.getCellMultiData({ + query: { + province: '浙江省', + sub_type: '杭州市', + }, + queryType: QueryDataType.DetailOnly, + }), + ).toMatchSnapshot(); + expect( + dataSet.getCellMultiData({ + query: { + sub_type: '桌子', + }, + queryType: QueryDataType.DetailOnly, + }), + ).toMatchSnapshot(); + }); + }); }); diff --git a/packages/s2-core/__tests__/unit/data-set/table-data-set-spec.ts b/packages/s2-core/__tests__/unit/data-set/table-data-set-spec.ts index d3f6ab1f30..a96e5d399c 100644 --- a/packages/s2-core/__tests__/unit/data-set/table-data-set-spec.ts +++ b/packages/s2-core/__tests__/unit/data-set/table-data-set-spec.ts @@ -3,7 +3,7 @@ */ import { assembleDataCfg } from 'tests/util'; import type { S2DataConfig } from '@/common/interface'; -import { TableSheet } from '@/sheet-type'; +import { SpreadSheet, TableSheet } from '@/sheet-type'; import { TableDataSet } from '@/data-set/table-data-set'; jest.mock('@/sheet-type'); @@ -11,7 +11,9 @@ jest.mock('@/facet/layout/node'); const MockTableSheet = TableSheet as any as jest.Mock; describe('Table Mode Dataset Test', () => { + let s2: SpreadSheet; let dataSet: TableDataSet; + const mockNumberFormatter = jest.fn().mockReturnValue('number'); const mockSubTypeFormatter = jest.fn().mockReturnValue('sub_type'); const mockTypeFormatter = jest.fn().mockReturnValue('type'); @@ -59,12 +61,14 @@ describe('Table Mode Dataset Test', () => { beforeEach(() => { MockTableSheet.mockClear(); - dataSet = new TableDataSet(new MockTableSheet()); + + s2 = new MockTableSheet(); + dataSet = new TableDataSet(s2); dataSet.setDataCfg(dataCfg); }); - afterAll(() => {}); + afterEach(() => {}); describe('meta config test', () => { test.each` @@ -100,10 +104,14 @@ describe('Table Mode Dataset Test', () => { test('should get correct meta data', () => { expect(dataSet.meta).toEqual(expect.objectContaining([])); }); + + test('should get correctly empty dataset result', () => { + expect(dataSet.isEmpty()).toBeFalsy(); + }); }); describe('test for query data', () => { - test('getCellData function', () => { + test('#getCellData', () => { expect( dataSet.getCellData({ query: { rowIndex: 0 }, @@ -120,7 +128,7 @@ describe('Table Mode Dataset Test', () => { dataSet.getCellData({ query: { rowIndex: 0, - col: 'city', + field: 'city', }, }), ).toEqual('杭州市'); @@ -129,7 +137,7 @@ describe('Table Mode Dataset Test', () => { dataSet.getCellData({ query: { rowIndex: 2, - col: 'number', + field: 'number', }, }), ).toEqual(3877); @@ -138,10 +146,97 @@ describe('Table Mode Dataset Test', () => { dataSet.getCellData({ query: { rowIndex: 5, - col: 'sub_type', + field: 'sub_type', }, }), ).toEqual('沙发'); }); + + test('#getMultiData by empty query', () => { + expect(dataSet.getCellMultiData({})).toMatchSnapshot(); + }); + + test('#getMultiData by rowIndex query', () => { + expect( + dataSet.getCellMultiData({ + query: { + rowIndex: 0, + }, + }), + ).toMatchInlineSnapshot(` + Array [ + Object { + "city": "杭州市", + "number": 7789, + "province": "浙江省", + "sub_type": "桌子", + "type": "家具", + }, + ] + `); + + expect( + dataSet.getCellMultiData({ + query: { + rowIndex: -1, + }, + }), + ).toMatchSnapshot(); + }); + + test('#getMultiData by field query', () => { + expect( + dataSet.getCellMultiData({ + query: { + field: 'city', + }, + }), + ).toMatchSnapshot(); + + expect( + dataSet.getCellMultiData({ + query: { + field: 'number', + }, + }), + ).toMatchSnapshot(); + + expect( + dataSet.getCellMultiData({ + query: { + field: 'sub_type', + }, + }), + ).toMatchSnapshot(); + }); + + test('#getMultiData by field and rowIndex query', () => { + expect( + dataSet.getCellMultiData({ + query: { + field: 'city', + rowIndex: 0, + }, + }), + ).toEqual(['杭州市']); + + expect( + dataSet.getCellMultiData({ + query: { + field: 'number', + rowIndex: 2, + }, + }), + ).toEqual([3877]); + + expect( + dataSet.getCellMultiData({ + query: { + field: 'sub_type', + rowIndex: 3, + }, + }), + ).toEqual(['桌子']); + }); }); }); diff --git a/packages/s2-core/__tests__/unit/dataset/__snapshots__/table-dataset-spec.ts.snap b/packages/s2-core/__tests__/unit/dataset/__snapshots__/table-dataset-spec.ts.snap new file mode 100644 index 0000000000..b37ddaca28 --- /dev/null +++ b/packages/s2-core/__tests__/unit/dataset/__snapshots__/table-dataset-spec.ts.snap @@ -0,0 +1,459 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Table Mode Dataset Test test for sort and filter should asc sort by number field 1`] = ` +Array [ + Object { + "city": "绵阳市", + "number": 245, + "province": "四川省", + "sub_type": "笔", + "type": "办公用品", + }, + Object { + "city": "乐山市", + "number": 352, + "province": "四川省", + "sub_type": "纸张", + "type": "办公用品", + }, + Object { + "city": "绍兴市", + "number": 632, + "province": "浙江省", + "sub_type": "沙发", + "type": "家具", + }, + Object { + "city": "舟山市", + "number": 834, + "province": "浙江省", + "sub_type": "沙发", + "type": "家具", + }, + Object { + "city": "杭州市", + "number": 945, + "province": "浙江省", + "sub_type": "笔", + "type": "办公用品", + }, + Object { + "city": "宁波市", + "number": 1145, + "province": "浙江省", + "sub_type": "笔", + "type": "办公用品", + }, + Object { + "city": "绍兴市", + "number": 1304, + "province": "浙江省", + "sub_type": "笔", + "type": "办公用品", + }, + Object { + "city": "杭州市", + "number": 1343, + "province": "浙江省", + "sub_type": "纸张", + "type": "办公用品", + }, + Object { + "city": "绍兴市", + "number": 1354, + "province": "浙江省", + "sub_type": "纸张", + "type": "办公用品", + }, + Object { + "city": "舟山市", + "number": 1432, + "province": "浙江省", + "sub_type": "笔", + "type": "办公用品", + }, + Object { + "city": "宁波市", + "number": 1523, + "province": "浙江省", + "sub_type": "纸张", + "type": "办公用品", + }, + Object { + "city": "舟山市", + "number": 1634, + "province": "浙江省", + "sub_type": "纸张", + "type": "办公用品", + }, + Object { + "city": "成都市", + "number": 1723, + "province": "四川省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "city": "绵阳市", + "number": 1822, + "province": "四川省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "city": "南充市", + "number": 1943, + "province": "四川省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "city": "绵阳市", + "number": 2244, + "province": "四川省", + "sub_type": "沙发", + "type": "家具", + }, + Object { + "city": "乐山市", + "number": 2330, + "province": "四川省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "city": "南充市", + "number": 2333, + "province": "四川省", + "sub_type": "沙发", + "type": "家具", + }, + Object { + "city": "成都市", + "number": 2335, + "province": "四川省", + "sub_type": "笔", + "type": "办公用品", + }, + Object { + "city": "绍兴市", + "number": 2367, + "province": "浙江省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "city": "乐山市", + "number": 2445, + "province": "四川省", + "sub_type": "沙发", + "type": "家具", + }, + Object { + "city": "成都市", + "number": 2451, + "province": "四川省", + "sub_type": "沙发", + "type": "家具", + }, + Object { + "city": "南充市", + "number": 2457, + "province": "四川省", + "sub_type": "笔", + "type": "办公用品", + }, + Object { + "city": "乐山市", + "number": 2458, + "province": "四川省", + "sub_type": "笔", + "type": "办公用品", + }, + Object { + "city": "绵阳市", + "number": 3077, + "province": "四川省", + "sub_type": "纸张", + "type": "办公用品", + }, + Object { + "city": "南充市", + "number": 3551, + "province": "四川省", + "sub_type": "纸张", + "type": "办公用品", + }, + Object { + "city": "宁波市", + "number": 3877, + "province": "浙江省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "city": "成都市", + "number": 4004, + "province": "四川省", + "sub_type": "纸张", + "type": "办公用品", + }, + Object { + "city": "舟山市", + "number": 4342, + "province": "浙江省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "city": "杭州市", + "number": 5343, + "province": "浙江省", + "sub_type": "沙发", + "type": "家具", + }, + Object { + "city": "宁波市", + "number": 7234, + "province": "浙江省", + "sub_type": "沙发", + "type": "家具", + }, + Object { + "city": "杭州市", + "number": 7789, + "province": "浙江省", + "sub_type": "桌子", + "type": "家具", + }, +] +`; + +exports[`Table Mode Dataset Test test for sort and filter should desc sort by number field 1`] = ` +Array [ + Object { + "city": "杭州市", + "number": 7789, + "province": "浙江省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "city": "宁波市", + "number": 7234, + "province": "浙江省", + "sub_type": "沙发", + "type": "家具", + }, + Object { + "city": "杭州市", + "number": 5343, + "province": "浙江省", + "sub_type": "沙发", + "type": "家具", + }, + Object { + "city": "舟山市", + "number": 4342, + "province": "浙江省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "city": "成都市", + "number": 4004, + "province": "四川省", + "sub_type": "纸张", + "type": "办公用品", + }, + Object { + "city": "宁波市", + "number": 3877, + "province": "浙江省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "city": "南充市", + "number": 3551, + "province": "四川省", + "sub_type": "纸张", + "type": "办公用品", + }, + Object { + "city": "绵阳市", + "number": 3077, + "province": "四川省", + "sub_type": "纸张", + "type": "办公用品", + }, + Object { + "city": "乐山市", + "number": 2458, + "province": "四川省", + "sub_type": "笔", + "type": "办公用品", + }, + Object { + "city": "南充市", + "number": 2457, + "province": "四川省", + "sub_type": "笔", + "type": "办公用品", + }, + Object { + "city": "成都市", + "number": 2451, + "province": "四川省", + "sub_type": "沙发", + "type": "家具", + }, + Object { + "city": "乐山市", + "number": 2445, + "province": "四川省", + "sub_type": "沙发", + "type": "家具", + }, + Object { + "city": "绍兴市", + "number": 2367, + "province": "浙江省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "city": "成都市", + "number": 2335, + "province": "四川省", + "sub_type": "笔", + "type": "办公用品", + }, + Object { + "city": "南充市", + "number": 2333, + "province": "四川省", + "sub_type": "沙发", + "type": "家具", + }, + Object { + "city": "乐山市", + "number": 2330, + "province": "四川省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "city": "绵阳市", + "number": 2244, + "province": "四川省", + "sub_type": "沙发", + "type": "家具", + }, + Object { + "city": "南充市", + "number": 1943, + "province": "四川省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "city": "绵阳市", + "number": 1822, + "province": "四川省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "city": "成都市", + "number": 1723, + "province": "四川省", + "sub_type": "桌子", + "type": "家具", + }, + Object { + "city": "舟山市", + "number": 1634, + "province": "浙江省", + "sub_type": "纸张", + "type": "办公用品", + }, + Object { + "city": "宁波市", + "number": 1523, + "province": "浙江省", + "sub_type": "纸张", + "type": "办公用品", + }, + Object { + "city": "舟山市", + "number": 1432, + "province": "浙江省", + "sub_type": "笔", + "type": "办公用品", + }, + Object { + "city": "绍兴市", + "number": 1354, + "province": "浙江省", + "sub_type": "纸张", + "type": "办公用品", + }, + Object { + "city": "杭州市", + "number": 1343, + "province": "浙江省", + "sub_type": "纸张", + "type": "办公用品", + }, + Object { + "city": "绍兴市", + "number": 1304, + "province": "浙江省", + "sub_type": "笔", + "type": "办公用品", + }, + Object { + "city": "宁波市", + "number": 1145, + "province": "浙江省", + "sub_type": "笔", + "type": "办公用品", + }, + Object { + "city": "杭州市", + "number": 945, + "province": "浙江省", + "sub_type": "笔", + "type": "办公用品", + }, + Object { + "city": "舟山市", + "number": 834, + "province": "浙江省", + "sub_type": "沙发", + "type": "家具", + }, + Object { + "city": "绍兴市", + "number": 632, + "province": "浙江省", + "sub_type": "沙发", + "type": "家具", + }, + Object { + "city": "乐山市", + "number": 352, + "province": "四川省", + "sub_type": "纸张", + "type": "办公用品", + }, + Object { + "city": "绵阳市", + "number": 245, + "province": "四川省", + "sub_type": "笔", + "type": "办公用品", + }, +] +`; diff --git a/packages/s2-core/__tests__/unit/dataset/table-dataset-spec.ts b/packages/s2-core/__tests__/unit/dataset/table-dataset-spec.ts index 8089f50f50..07cc181f83 100644 --- a/packages/s2-core/__tests__/unit/dataset/table-dataset-spec.ts +++ b/packages/s2-core/__tests__/unit/dataset/table-dataset-spec.ts @@ -1,12 +1,12 @@ /** * table mode data-set test. */ -import { orderBy, uniq } from 'lodash'; +import { first, last, orderBy, uniq } from 'lodash'; import { data } from 'tests/data/mock-dataset.json'; import { assembleDataCfg } from '../../util'; import type { S2DataConfig, SortParam } from '@/common/interface'; -import { TableSheet } from '@/sheet-type'; import { TableDataSet } from '@/data-set/table-data-set'; +import { TableSheet } from '@/sheet-type'; jest.mock('@/sheet-type'); jest.mock('@/facet/layout/node'); @@ -15,6 +15,7 @@ const MockTableSheet = TableSheet as any as jest.Mock; describe('Table Mode Dataset Test', () => { let dataSet: TableDataSet; + const dataCfg: S2DataConfig = { ...assembleDataCfg({}), meta: [], @@ -36,6 +37,10 @@ describe('Table Mode Dataset Test', () => { dataSet.setDataCfg(dataCfg); }); + afterEach(() => { + MockTableSheet.mockClear(); + }); + describe('test base dataset structure', () => { test('should get correct field data', () => { expect(dataSet.fields.rows).toEqual(undefined); @@ -60,7 +65,7 @@ describe('Table Mode Dataset Test', () => { dataSet.getCellData({ query: { rowIndex: 0, - col: 'city', + field: 'city', }, }), ).toEqual('杭州市'); @@ -69,7 +74,7 @@ describe('Table Mode Dataset Test', () => { dataSet.getCellData({ query: { rowIndex: 2, - col: 'number', + field: 'number', }, }), ).toEqual(3877); @@ -78,7 +83,7 @@ describe('Table Mode Dataset Test', () => { dataSet.getCellData({ query: { rowIndex: 5, - col: 'sub_type', + field: 'sub_type', }, }), ).toEqual('沙发'); @@ -97,7 +102,7 @@ describe('Table Mode Dataset Test', () => { expect( emptyDataSet.getCellData({ query: { - col: 'sub_type', + field: 'sub_type', rowIndex: 0, }, }), @@ -120,11 +125,12 @@ describe('Table Mode Dataset Test', () => { dataSet.getCellData({ query: { rowIndex: 0, - col: 'city', + field: 'city', }, }), ).toEqual('成都市'); }); + it('should getCellData with customFilter', () => { dataSet.setDataCfg({ ...dataCfg, @@ -139,7 +145,7 @@ describe('Table Mode Dataset Test', () => { dataSet.getCellData({ query: { rowIndex: 0, - col: 'city', + field: 'city', }, }), ).toEqual('杭州市'); @@ -161,7 +167,7 @@ describe('Table Mode Dataset Test', () => { dataSet.getCellData({ query: { rowIndex: 0, - col: 'city', + field: 'city', }, }), ).toEqual('成都市'); @@ -181,7 +187,7 @@ describe('Table Mode Dataset Test', () => { dataSet.getCellData({ query: { rowIndex: 0, - col: 'number', + field: 'number', }, }), ).toEqual(245); @@ -292,5 +298,118 @@ describe('Table Mode Dataset Test', () => { dataSet.handleDimensionValuesSort(); expect([...result, ...rest]).toStrictEqual(dataSet.getDisplayDataSet()); }); + + it('should asc sort by number field', () => { + const sortFieldId = 'number'; + + dataSet.sortParams = [ + { + sortFieldId, + sortMethod: 'asc', + }, + ]; + + dataSet.handleDimensionValuesSort(); + + expect(dataSet.getDisplayDataSet()).toHaveLength(32); + expect(dataSet.getDisplayDataSet()).toMatchSnapshot(); + }); + + it('should desc sort by number field', () => { + const sortFieldId = 'number'; + + dataSet.sortParams = [ + { + sortFieldId, + sortMethod: 'desc', + }, + ]; + + dataSet.handleDimensionValuesSort(); + + expect(dataSet.getDisplayDataSet()).toHaveLength(32); + expect(dataSet.getDisplayDataSet()).toMatchSnapshot(); + }); + + // https://github.com/antvis/S2/issues/2388 + it('should frozen correctly desc sorted data', () => { + const sortFieldId = 'number'; + + Object.defineProperty(dataSet.spreadsheet, 'options', { + value: { + frozenRowCount: 1, + frozenTrailingRowCount: 1, + }, + }); + + dataSet.sortParams = [ + { + sortFieldId, + sortMethod: 'desc', + }, + ]; + + dataSet.handleDimensionValuesSort(); + + expect(dataSet.getDisplayDataSet()).toHaveLength(32); + expect(first(dataSet.getDisplayDataSet())).toMatchInlineSnapshot(` + Object { + "city": "杭州市", + "number": 7789, + "province": "浙江省", + "sub_type": "桌子", + "type": "家具", + } + `); + expect(last(dataSet.getDisplayDataSet())).toMatchInlineSnapshot(` + Object { + "city": "绵阳市", + "number": 245, + "province": "四川省", + "sub_type": "笔", + "type": "办公用品", + } + `); + }); + + it('should frozen correctly asc sorted data', () => { + const sortFieldId = 'number'; + + Object.defineProperty(dataSet.spreadsheet, 'options', { + value: { + frozenRowCount: 1, + frozenTrailingRowCount: 1, + }, + }); + + dataSet.sortParams = [ + { + sortFieldId, + sortMethod: 'asc', + }, + ]; + + dataSet.handleDimensionValuesSort(); + + expect(dataSet.getDisplayDataSet()).toHaveLength(32); + expect(first(dataSet.getDisplayDataSet())).toMatchInlineSnapshot(` + Object { + "city": "绵阳市", + "number": 245, + "province": "四川省", + "sub_type": "笔", + "type": "办公用品", + } + `); + expect(last(dataSet.getDisplayDataSet())).toMatchInlineSnapshot(` + Object { + "city": "杭州市", + "number": 7789, + "province": "浙江省", + "sub_type": "桌子", + "type": "家具", + } + `); + }); }); }); diff --git a/packages/s2-core/__tests__/unit/facet/__snapshots__/table-facet-spec.ts.snap b/packages/s2-core/__tests__/unit/facet/__snapshots__/table-facet-spec.ts.snap new file mode 100644 index 0000000000..df5e1dc0ba --- /dev/null +++ b/packages/s2-core/__tests__/unit/facet/__snapshots__/table-facet-spec.ts.snap @@ -0,0 +1,145 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Table Mode Facet Test With Adaptive Layout should get correct col layout col hierarchy coordinate with adaptive layout 1`] = ` +Array [ + Object { + "height": 30, + "width": 119.8, + "x": 0, + "y": 0, + }, + Object { + "height": 30, + "width": 119.8, + "x": 119.8, + "y": 0, + }, + Object { + "height": 30, + "width": 119.8, + "x": 239.6, + "y": 0, + }, + Object { + "height": 30, + "width": 119.8, + "x": 359.4, + "y": 0, + }, + Object { + "height": 30, + "width": 119.8, + "x": 479.2, + "y": 0, + }, +] +`; + +exports[`Table Mode Facet Test With Adaptive Layout should get correct col layout with seriesNumber col hierarchy coordinate with adaptive layout with seriesNumber 1`] = ` +Array [ + Object { + "height": 30, + "width": 103.8, + "x": 80, + "y": 0, + }, + Object { + "height": 30, + "width": 103.8, + "x": 183.8, + "y": 0, + }, + Object { + "height": 30, + "width": 103.8, + "x": 287.6, + "y": 0, + }, + Object { + "height": 30, + "width": 103.8, + "x": 391.40000000000003, + "y": 0, + }, + Object { + "height": 30, + "width": 103.8, + "x": 495.20000000000005, + "y": 0, + }, +] +`; + +exports[`Table Mode Facet With Column Grouping Frozen Test should get correct frozenInfo 1`] = ` +Object { + "frozenCol": Object { + "range": Array [ + 0, + 0, + ], + "width": 199.66, + "x": 0, + }, + "frozenRow": Object { + "height": 60, + "range": Array [ + 0, + 1, + ], + "y": 0, + }, + "frozenTrailingCol": Object { + "range": Array [ + 2, + 2, + ], + "width": 199.66, + "x": 399.34000000000003, + }, + "frozenTrailingRow": Object { + "height": 60, + "range": Array [ + 30, + 31, + ], + "y": 502, + }, +} +`; + +exports[`Table Mode Facet With Frozen Test should get correct frozenInfo 1`] = ` +Object { + "frozenCol": Object { + "range": Array [ + 0, + 1, + ], + "width": 239.6, + "x": 0, + }, + "frozenRow": Object { + "height": 60, + "range": Array [ + 0, + 1, + ], + "y": 0, + }, + "frozenTrailingCol": Object { + "range": Array [ + 3, + 4, + ], + "width": 239.60000000000002, + "x": 359.4, + }, + "frozenTrailingRow": Object { + "height": 60, + "range": Array [ + 30, + 31, + ], + "y": 502, + }, +} +`; diff --git a/packages/s2-core/__tests__/unit/facet/bbox/cornerBBox-spec.ts b/packages/s2-core/__tests__/unit/facet/bbox/corner-bbox-spec.ts similarity index 98% rename from packages/s2-core/__tests__/unit/facet/bbox/cornerBBox-spec.ts rename to packages/s2-core/__tests__/unit/facet/bbox/corner-bbox-spec.ts index 6381e67460..229ef9cde7 100644 --- a/packages/s2-core/__tests__/unit/facet/bbox/cornerBBox-spec.ts +++ b/packages/s2-core/__tests__/unit/facet/bbox/corner-bbox-spec.ts @@ -1,5 +1,5 @@ import type { BaseFacet } from '@/facet/base-facet'; -import { CornerBBox } from '@/facet/bbox/cornerBBox'; +import { CornerBBox } from '@/facet/bbox/corner-bbox'; describe('cornerBBox test', () => { let mockFacet: BaseFacet; diff --git a/packages/s2-core/__tests__/unit/facet/bbox/panelBBox-spec.ts b/packages/s2-core/__tests__/unit/facet/bbox/panel-bbox-spec.ts similarity index 98% rename from packages/s2-core/__tests__/unit/facet/bbox/panelBBox-spec.ts rename to packages/s2-core/__tests__/unit/facet/bbox/panel-bbox-spec.ts index f758ee9ed0..e6635f0a66 100644 --- a/packages/s2-core/__tests__/unit/facet/bbox/panelBBox-spec.ts +++ b/packages/s2-core/__tests__/unit/facet/bbox/panel-bbox-spec.ts @@ -1,6 +1,6 @@ import type { S2Options, ThemeCfg } from '@/common'; import type { BaseFacet } from '@/facet/base-facet'; -import { PanelBBox } from '@/facet/bbox/panelBBox'; +import { PanelBBox } from '@/facet/bbox/panel-bbox'; describe('PanelBBox Tests', () => { const layoutResult = { diff --git a/packages/s2-core/__tests__/unit/facet/header/frozen-row-spec.ts b/packages/s2-core/__tests__/unit/facet/header/frozen-row-spec.ts new file mode 100644 index 0000000000..43e038b1b0 --- /dev/null +++ b/packages/s2-core/__tests__/unit/facet/header/frozen-row-spec.ts @@ -0,0 +1,51 @@ +import { createPivotSheet } from 'tests/util/helpers'; +import { get } from 'lodash'; +import type { HierarchyType, RowHeader } from '../../../../src'; +import type { FrozenFacet } from '../../../../src/facet/frozen-facet'; +import { DEFAULT_OPTIONS, FrozenGroupType } from '@/common'; +import { RowCell } from '@/cell'; + +const s2 = createPivotSheet( + { + ...DEFAULT_OPTIONS, + frozen: { + firstRow: true, + }, + totals: { row: { showGrandTotals: true, reverseGrandTotalsLayout: true } }, + showSeriesNumber: false, + }, + { useSimpleData: false }, +); + +describe('Pivot Frozen Row Header Test', () => { + test.each(['grid', 'tree'] as HierarchyType[])( + 'frozen row header group api', + async (hierarchyType) => { + s2.setOptions({ hierarchyType }); + await s2.render(); + + const rowHeader = s2.facet.rowHeader as RowHeader; + + expect(rowHeader).toBeTruthy(); + expect(rowHeader.frozenRowGroup).toBeTruthy(); + expect(rowHeader.scrollGroup).toBeTruthy(); + + expect(rowHeader.frozenRowGroup.children).toHaveLength(1); + const frozenRowCell = rowHeader.frozenRowGroup.children[0]; + + expect(frozenRowCell instanceof RowCell).toBeTrue(); + expect(get(frozenRowCell, 'meta.height')).toEqual(30); + + expect(rowHeader.scrollGroup.getChildren()).toHaveLength(10); + const scrollCell = rowHeader.scrollGroup.getChildren()[0]; + + expect(scrollCell instanceof RowCell).toBeTrue(); + expect(get(frozenRowCell, 'meta.height')).toEqual(30); + + expect( + (s2.facet as FrozenFacet).frozenGroupInfo[FrozenGroupType.FROZEN_ROW] + .height, + ).toBe(30); + }, + ); +}); diff --git a/packages/s2-core/__tests__/unit/facet/layout/col-node-width-spec.ts b/packages/s2-core/__tests__/unit/facet/layout/col-node-width-spec.ts index 271b8c12de..b6d1e7fc98 100644 --- a/packages/s2-core/__tests__/unit/facet/layout/col-node-width-spec.ts +++ b/packages/s2-core/__tests__/unit/facet/layout/col-node-width-spec.ts @@ -2,7 +2,7 @@ import * as mockDataConfig from 'tests/data/simple-data.json'; import * as mockTableDataConfig from 'tests/data/simple-table-data.json'; import { getContainer } from 'tests/util/helpers'; import { PivotSheet, TableSheet } from '@/sheet-type'; -import type { S2DataConfig, S2Options } from '@/common'; +import { LayoutWidthType, type S2DataConfig, type S2Options } from '@/common'; const s2options: S2Options = { width: 800, @@ -35,7 +35,7 @@ describe('Col width Test', () => { }); test('get correct width in layoutWidthType adaptive mode', () => { - expect(s2.facet.getColLeafNodes()[0].width).toBe(200); + expect(s2.facet.getColLeafNodes()[0].width).toBe(199.5); }); test('get correct width in layoutWidthType adaptive mode when enable series number', async () => { @@ -44,7 +44,7 @@ describe('Col width Test', () => { }); await s2.render(); - expect(s2.facet.getColLeafNodes()[0].width).toBe(180); + expect(s2.facet.getColLeafNodes()[0].width).toBe(179.5); }); test('get correct width in layoutWidthType adaptive tree mode', async () => { @@ -69,7 +69,7 @@ describe('Col width Test', () => { test('get correct width in layoutWidthType compact mode', async () => { s2.setOptions({ style: { - layoutWidthType: 'compact', + layoutWidthType: LayoutWidthType.Compact, }, }); await s2.render(); @@ -92,7 +92,7 @@ describe('Col width Test', () => { }); s2.setOptions({ style: { - layoutWidthType: 'compact', + layoutWidthType: LayoutWidthType.Compact, }, }); await s2.render(); @@ -126,7 +126,7 @@ describe('Col width Test', () => { test('get correct width in layoutWidthType compact mode', async () => { s2.setOptions({ style: { - layoutWidthType: 'compact', + layoutWidthType: LayoutWidthType.Compact, }, }); await s2.render(); diff --git a/packages/s2-core/__tests__/unit/facet/layout/row-node-width-spec.ts b/packages/s2-core/__tests__/unit/facet/layout/row-node-width-spec.ts index dc208af2d9..a2c2141155 100644 --- a/packages/s2-core/__tests__/unit/facet/layout/row-node-width-spec.ts +++ b/packages/s2-core/__tests__/unit/facet/layout/row-node-width-spec.ts @@ -1,7 +1,7 @@ import * as mockDataConfig from 'tests/data/data-issue-372.json'; import { getContainer } from 'tests/util/helpers'; import { PivotSheet } from '@/sheet-type'; -import type { S2Options } from '@/common'; +import { LayoutWidthType, type S2Options } from '@/common'; const s2options: S2Options = { width: 800, @@ -25,7 +25,7 @@ describe('Row width Test in grid mode', () => { test('get the correct custom width of row nodes when the layoutWidthType equals colAdaptive', async () => { s2.setOptions({ style: { - layoutWidthType: 'compact', + layoutWidthType: LayoutWidthType.Compact, rowCell: { width: 50 }, }, }); @@ -39,7 +39,7 @@ describe('Row width Test in grid mode', () => { test('get the correct custom width of row nodes when the layoutWidthType equals compact', async () => { s2.setOptions({ style: { - layoutWidthType: 'compact', + layoutWidthType: LayoutWidthType.Compact, rowCell: { width: 20 }, }, }); diff --git a/packages/s2-core/__tests__/unit/facet/pivot-facet-spec.ts b/packages/s2-core/__tests__/unit/facet/pivot-facet-spec.ts index 83d37f218b..e24c1868fe 100644 --- a/packages/s2-core/__tests__/unit/facet/pivot-facet-spec.ts +++ b/packages/s2-core/__tests__/unit/facet/pivot-facet-spec.ts @@ -2,24 +2,29 @@ * pivot mode pivot test. */ import { Canvas, Group, Rect, type CanvasConfig } from '@antv/g'; -import { assembleDataCfg, assembleOptions } from 'tests/util'; -import { size, find } from 'lodash'; import { Renderer } from '@antv/g-canvas'; +import { find, size } from 'lodash'; +import { assembleDataCfg, assembleOptions } from 'tests/util'; +import { FrozenGroupType } from '../../../src'; +import { createFakeSpreadSheet } from '../../util/helpers'; import { getMockPivotMeta } from './util'; -import { Node } from '@/facet/layout/node'; -import { DEFAULT_TREE_ROW_CELL_WIDTH } from '@/common/constant/options'; -import type { PanelScrollGroup } from '@/group/panel-scroll-group'; -import { SpreadSheet } from '@/sheet-type'; -import { PivotDataSet } from '@/data-set/pivot-data-set'; -import { PivotFacet } from '@/facet/pivot-facet'; import { CornerCell, DataCell } from '@/cell'; +import { + DEFAULT_OPTIONS, + DEFAULT_STYLE, + DEFAULT_TREE_ROW_CELL_WIDTH, +} from '@/common/constant/options'; +import type { ViewMeta } from '@/common/interface/basic'; import { Store } from '@/common/store'; -import { getTheme } from '@/theme'; -import { DEFAULT_OPTIONS, DEFAULT_STYLE } from '@/common/constant/options'; +import type { CellData } from '@/data-set/cell-data'; +import { PivotDataSet } from '@/data-set/pivot-data-set'; import { ColHeader, CornerHeader, Frame, RowHeader } from '@/facet/header'; -import type { ViewMeta } from '@/common/interface/basic'; +import { Node } from '@/facet/layout/node'; +import { PivotFacet } from '@/facet/pivot-facet'; +import type { PanelScrollGroup } from '@/group/panel-scroll-group'; import { RootInteraction } from '@/interaction/root'; -import type { CellData } from '@/data-set/cell-data'; +import { SpreadSheet } from '@/sheet-type'; +import { getTheme } from '@/theme'; jest.mock('@/interaction/root'); @@ -49,7 +54,9 @@ jest.mock('@/sheet-type', () => { return { dataCfg: assembleDataCfg(), - options: assembleOptions(), + options: assembleOptions({ + dataCell: (viewMeta) => new DataCell(viewMeta, viewMeta.spreadsheet), + }), container, theme: getTheme({}), store: new Store(), @@ -70,11 +77,20 @@ jest.mock('@/sheet-type', () => { layoutResult: { rowLeafNodes: [], }, + getLayoutResult: () => ({ rowLeafNodes: [], colLeafNodes: [] }), getRowLeafNodes: () => [], getRowNodes: () => [], getColNodes: () => [], getHiddenColumnsInfo: jest.fn(), getCellMeta: jest.fn(), + getRowLeafNodeByIndex: () => [], + frozenGroupInfo: { + [FrozenGroupType.FROZEN_ROW]: {}, + [FrozenGroupType.FROZEN_COL]: {}, + [FrozenGroupType.FROZEN_TRAILING_ROW]: {}, + [FrozenGroupType.FROZEN_TRAILING_COL]: {}, + }, + cornerBBox: {}, }, getCanvasElement: () => container.getContextService().getDomElement() as HTMLCanvasElement, @@ -103,12 +119,17 @@ jest.mock('@/data-set/pivot-data-set', () => { sortedDimensionValues, moreThanOneValue: jest.fn(), getField: jest.fn(), + transformIndexesData: actualPivotDataSet.prototype.transformIndexesData, + getExistValuesByDataItem: + actualPivotDataSet.prototype.getExistValuesByDataItem, getFieldFormatter: actualDataSet.prototype.getFieldFormatter, getFieldMeta: (field: string, meta: ViewMeta) => find(meta, { field }), getFieldName: actualPivotDataSet.prototype.getFieldName, getCellData: actualPivotDataSet.prototype.getCellData, getCellMultiData: jest.fn(), getDimensionValues: actualPivotDataSet.prototype.getDimensionValues, + getFieldsAndPivotMetaByField: + actualPivotDataSet.prototype.getFieldsAndPivotMetaByField, }; }), }; @@ -130,13 +151,16 @@ describe('Pivot Mode Facet Test', () => { s2.options = assembleOptions({ dataCell: (viewMeta) => new DataCell(viewMeta, s2), }); + const facet = new PivotFacet(s2); - const facet: PivotFacet = new PivotFacet(s2); - - beforeAll(async () => { + beforeEach(async () => { await s2.container.ready; }); + afterEach(() => { + facet.destroy(); + }); + describe('should get correct hierarchy', () => { const { dataCell, colCell } = s2.options.style!; const { rowsHierarchy, colsHierarchy, colLeafNodes } = @@ -155,9 +179,9 @@ describe('Pivot Mode Facet Test', () => { expect(rowsHierarchy.getNodes(0)).toHaveLength(2); rowsHierarchy.getLeaves().forEach((node, index) => { - expect(node.width).toBe(99); + expect(Math.floor(node.width)).toBeCloseTo(99); expect(node.height).toBe(dataCell!.height!); - expect(node.x).toBe(99 * node.level); + expect(Math.floor(node.x)).toBeCloseTo(99 * node.level); expect(node.y).toBe(node.height * index); }); @@ -181,9 +205,9 @@ describe('Pivot Mode Facet Test', () => { expect(colsHierarchy.getNodes(0)).toHaveLength(2); colsHierarchy.getLeaves().forEach((node, index) => { - expect(node.width).toBe(width); + expect(Math.ceil(node.width)).toBeCloseTo(width); expect(node.height).toBe(colCell!.height); - expect(node.x).toBe(width * index); + expect(Math.ceil(node.x)).toBe(width * index); expect(node.y).toBe(node.height * node.level); }); @@ -219,22 +243,18 @@ describe('Pivot Mode Facet Test', () => { }); describe('should get correct result when tree mode', () => { - s2.isHierarchyTreeType = jest.fn().mockReturnValue(true); - s2.options = assembleOptions({ - hierarchyType: 'tree', - }); - // 小于 DEFAULT_TREE_ROW_WIDTH - const spy = jest.spyOn(s2, 'measureTextWidth').mockReturnValue(30); - - s2.dataSet = new MockPivotDataSet(s2); - const treeFacet = new PivotFacet(s2); - const { rowsHierarchy } = treeFacet.getLayoutResult(); + test('row hierarchy when tree mode', () => { + s2.isHierarchyTreeType = jest.fn().mockReturnValue(true); + s2.options = assembleOptions({ + hierarchyType: 'tree', + }); + // 小于 DEFAULT_TREE_ROW_WIDTH + const spy = jest.spyOn(s2, 'measureTextWidth').mockReturnValue(30); - afterAll(() => { - spy.mockRestore(); - }); + s2.dataSet = new MockPivotDataSet(s2); + const treeFacet = new PivotFacet(s2); + const { rowsHierarchy } = treeFacet.getLayoutResult(); - test('row hierarchy when tree mode', () => { const { dataCell, rowCell } = s2.options.style!; expect(rowsHierarchy.getLeaves()).toHaveLength(8); @@ -248,31 +268,33 @@ describe('Pivot Mode Facet Test', () => { expect(node.x).toBe(0); expect(node.y).toBe(node.height * index); }); + + spy.mockRestore(); }); }); describe('should get correct layer after render', () => { - beforeAll(() => { + beforeEach(() => { facet.render(); }); - afterAll(() => { + afterEach(() => { facet.destroy(); }); test('get header after render', () => { const { rowHeader, cornerHeader, columnHeader, centerFrame } = facet; - expect(rowHeader instanceof RowHeader).toBeTrue(); - expect(rowHeader!.children).toHaveLength(10); + expect(rowHeader).toBeInstanceOf(RowHeader); + expect(rowHeader!.children[0].children).toHaveLength(10); expect(rowHeader!.parsedStyle.visibility).not.toEqual('hidden'); - expect(cornerHeader instanceof CornerHeader).toBeTrue(); - expect(cornerHeader.children.length).toBe(3); + expect(cornerHeader).toBeInstanceOf(CornerHeader); + expect(cornerHeader.children).toHaveLength(2); expect(cornerHeader.parsedStyle.visibility).not.toEqual('hidden'); - expect(columnHeader instanceof ColHeader).toBeTrue(); - expect(centerFrame instanceof Frame).toBeTrue(); + expect(columnHeader).toBeInstanceOf(ColHeader); + expect(centerFrame).toBeInstanceOf(Frame); }); test('get background after render', () => { @@ -287,24 +309,13 @@ describe('Pivot Mode Facet Test', () => { }); describe('should get correct result when enable series number', () => { - const mockDataSet = new MockPivotDataSet(s2); + test('render correct corner header', () => { + const s2 = createFakeSpreadSheet(); - s2.options = assembleOptions({ - showSeriesNumber: true, - dataCell: (fct) => new DataCell(fct, s2), - }); - s2.dataSet = mockDataSet; - const seriesNumberFacet = new PivotFacet(s2); + s2.dataSet = new MockPivotDataSet(s2); + const seriesNumberFacet = new PivotFacet(s2); - beforeAll(() => { seriesNumberFacet.render(); - }); - - afterAll(() => { - seriesNumberFacet.destroy(); - }); - - test('render correct corner header', () => { const { cornerHeader } = seriesNumberFacet; expect(cornerHeader instanceof CornerHeader).toBeTrue(); @@ -320,7 +331,7 @@ describe('Pivot Mode Facet Test', () => { }); it.each(['updateScrollOffset', 'scrollWithAnimation', 'scrollImmediately'])( - 'should not throw "Cannot read property \'value\' of undefined" error if called with single offset config', + 'should not throw "Cannot read property \'value\' of undefined" error if called with single offset config by %s', (method) => { const onlyOffsetYFn = () => { // @ts-ignore diff --git a/packages/s2-core/__tests__/unit/facet/table-facet-spec.ts b/packages/s2-core/__tests__/unit/facet/table-facet-spec.ts index 7a8f38966f..c5b621d547 100644 --- a/packages/s2-core/__tests__/unit/facet/table-facet-spec.ts +++ b/packages/s2-core/__tests__/unit/facet/table-facet-spec.ts @@ -4,15 +4,17 @@ import { Canvas, Group, type CanvasConfig } from '@antv/g'; import { Renderer } from '@antv/g-canvas'; import { assembleDataCfg, assembleOptions } from 'tests/util'; +import { pick } from 'lodash'; import { data } from '../../data/mock-dataset.json'; -import { ROOT_NODE_ID } from '@/common/constant'; +import { createFakeSpreadSheet } from '../../util/helpers'; +import { LayoutWidthType, ROOT_NODE_ID } from '@/common/constant'; import { Store } from '@/common/store'; import { TableDataSet } from '@/data-set/table-data-set'; import { TableFacet } from '@/facet/table-facet'; import { getFrozenLeafNodesCount } from '@/facet/utils'; import { - customMerge, Node, + customMerge, type HiddenColumnsInfo, type S2DataConfig, type S2Options, @@ -20,6 +22,10 @@ import { import { SpreadSheet } from '@/sheet-type'; import { getTheme } from '@/theme'; +const actualDataSet = jest.requireActual( + '@/data-set/base-data-set', +).BaseDataSet; + jest.mock('@/sheet-type', () => { return { SpreadSheet: jest.fn().mockImplementation(() => { @@ -54,6 +60,9 @@ jest.mock('@/sheet-type', () => { getHiddenColumnsInfo: jest.fn(), getColNodeHeight: jest.fn(), }, + dataSet: { + isEmpty: jest.fn(), + }, isHierarchyTreeType: jest.fn(), getCanvasElement: () => container.getContextService().getDomElement() as HTMLCanvasElement, @@ -89,8 +98,10 @@ jest.mock('@/data-set/table-data-set', () => { getFieldName: jest.fn(), getDimensionValues: jest.fn(), getDisplayDataSet: jest.fn(() => data), - getFieldFormatter: jest.fn(), getCellData: () => 1, + getFieldMeta: jest.fn(), + getFieldFormatter: actualDataSet.prototype.getFieldFormatter, + isEmpty: jest.fn(), }; }), }; @@ -118,6 +129,8 @@ const createMockTableFacet = ( ), }); s2.dataSet = new MockTableDataSet(s2); + // @ts-ignore + s2.dataSet.getField = jest.fn(); s2.dataSet.fields = s2.dataCfg.fields; const facet = new TableFacet(s2); @@ -156,38 +169,73 @@ describe('Table Mode Facet Test', () => { expect(facet.getColLeafNodes()[0].value).toEqual(seriesNumberText); }); + + describe('should get none layer when dataCfg.fields is empty', () => { + const spreadsheet = createFakeSpreadSheet({ + s2DataConfig: { + fields: { + rows: [], + columns: [], + values: [], + }, + }, + }); + const mockDataSet = new MockTableDataSet(spreadsheet); + + spreadsheet.dataSet = mockDataSet; + + const newFacet: TableFacet = new TableFacet(spreadsheet); + + beforeEach(() => { + newFacet.render(); + }); + + afterEach(() => { + newFacet.destroy(); + }); + + test('can not get header after render in table sheet', () => { + const { rowHeader, cornerHeader, columnHeader, centerFrame } = newFacet; + + expect(rowHeader).toBeFalsy(); + expect(cornerHeader).toBeFalsy(); + expect(columnHeader).toBeFalsy(); + expect(centerFrame).toBeFalsy(); + }); + + test('can not get series number after render in table sheet', () => { + const { backgroundGroup } = newFacet; + const rect = backgroundGroup.children[0]; + + expect(rect).toBeFalsy(); + }); + }); }); describe('Table Mode Facet Test With Adaptive Layout', () => { describe('should get correct col layout', () => { - const { facet, s2 } = createMockTableFacet({ - showSeriesNumber: false, - }); - const { colCell } = s2.options.style!; - test('col hierarchy coordinate with adaptive layout', () => { - const adaptiveWith = 119; - - facet.getColLeafNodes().forEach((node, index) => { - expect(node.y).toBe(0); - expect(node.x).toBe(index * adaptiveWith); - expect(Math.round(node.width)).toBe(adaptiveWith); - expect(node.height).toBe(colCell!.height); + const { facet } = createMockTableFacet({ + showSeriesNumber: false, }); + + expect( + facet + .getColLeafNodes() + .map((node) => pick(node, ['x', 'y', 'width', 'height'])), + ).toMatchSnapshot(); }); }); describe('should get correct col layout with seriesNumber', () => { - const { facet, s2 } = createMockTableFacet({ - showSeriesNumber: true, - }); - const { colCell } = s2.options.style!; - test('col hierarchy coordinate with adaptive layout with seriesNumber', () => { + const { facet, s2 } = createMockTableFacet({ + showSeriesNumber: true, + }); + const { colCell } = s2.options.style!; const colLeafNodes = facet.getColLeafNodes(); const seriesNumberWidth = facet.getSeriesNumberWidth(); - const adaptiveWith = 103; const seriesNumberNode = colLeafNodes[0]; @@ -196,12 +244,11 @@ describe('Table Mode Facet Test With Adaptive Layout', () => { expect(seriesNumberNode.width).toBe(seriesNumberWidth); expect(seriesNumberNode.height).toBe(colCell!.height); - colLeafNodes.slice(1).forEach((node, index) => { - expect(node.y).toBe(0); - expect(node.x).toBe(index * adaptiveWith + seriesNumberWidth); - expect(node.width).toBe(adaptiveWith); - expect(node.height).toBe(colCell!.height); - }); + expect( + colLeafNodes + .slice(1) + .map((node) => pick(node, ['x', 'y', 'width', 'height'])), + ).toMatchSnapshot(); }); }); }); @@ -233,7 +280,7 @@ describe('Table Mode Facet Test With Compact Layout', () => { }, undefined, (spreadsheet) => { - spreadsheet.getLayoutWidthType = () => 'compact'; + spreadsheet.getLayoutWidthType = () => LayoutWidthType.Compact; spreadsheet.measureTextWidth = mockMeasureFunc as unknown as SpreadSheet['measureTextWidth']; spreadsheet.measureTextWidthRoughly = mockMeasureFunc; @@ -280,7 +327,7 @@ describe('Table Mode Facet Test With Compact Layout', () => { }, undefined, (spreadsheet) => { - spreadsheet.getLayoutWidthType = () => 'compact'; + spreadsheet.getLayoutWidthType = () => LayoutWidthType.Compact; spreadsheet.measureTextWidth = mockMeasureFunc as unknown as SpreadSheet['measureTextWidth']; spreadsheet.measureTextWidthRoughly = mockMeasureFunc; @@ -318,38 +365,7 @@ describe('Table Mode Facet With Frozen Test', () => { test('should get correct frozenInfo', () => { facet.calculateFrozenGroupInfo(); - expect(facet.frozenGroupInfo).toMatchInlineSnapshot(` - Object { - "frozenCol": Object { - "range": Array [ - 0, - 1, - ], - "width": 238, - }, - "frozenRow": Object { - "height": 60, - "range": Array [ - 0, - 1, - ], - }, - "frozenTrailingCol": Object { - "range": Array [ - 3, - 4, - ], - "width": 238, - }, - "frozenTrailingRow": Object { - "height": 60, - "range": Array [ - 30, - 31, - ], - }, - } - `); + expect(facet.frozenGroupInfo).toMatchSnapshot(); }); test('should get correct xy indexes with frozen', () => { @@ -378,8 +394,8 @@ describe('Table Mode Facet With Frozen Test', () => { .getColLeafNodes() .slice(-colCount) .reverse() - .map((node) => node.x), - ).toEqual([481, 362]); + .map((node) => Math.floor(node.x)), + ).toEqual([479, 359]); }); test('should get correct cell layout with frozenTrailingCol', () => { @@ -390,8 +406,8 @@ describe('Table Mode Facet With Frozen Test', () => { .getColLeafNodes() .slice(-trailingColCount!) .reverse() - .map((node) => node.x), - ).toEqual([481, 362]); + .map((node) => Math.floor(node.x)), + ).toEqual([479, 359]); }); test('should get correct cell layout with frozenTrailingRow', () => { @@ -643,38 +659,7 @@ describe('Table Mode Facet With Column Grouping Frozen Test', () => { test('should get correct frozenInfo', () => { facet.calculateFrozenGroupInfo(); - expect(facet.frozenGroupInfo).toMatchInlineSnapshot(` - Object { - "frozenCol": Object { - "range": Array [ - 0, - 0, - ], - "width": 199, - }, - "frozenRow": Object { - "height": 60, - "range": Array [ - 0, - 1, - ], - }, - "frozenTrailingCol": Object { - "range": Array [ - 2, - 2, - ], - "width": 199, - }, - "frozenTrailingRow": Object { - "height": 60, - "range": Array [ - 30, - 31, - ], - }, - } - `); + expect(facet.frozenGroupInfo).toMatchSnapshot(); }); test('should get correct col layout with frozen col', () => { @@ -704,12 +689,8 @@ describe('Table Mode Facet With Column Grouping Frozen Test', () => { .getColLeafNodes() .slice(-trailingColCount) .reverse() - .map((node) => node.x), - ).toMatchInlineSnapshot(` - Array [ - 401, - ] - `); + .map((node) => Math.floor(node.x)), + ).toEqual([399]); }); test('should get correct cell layout with frozenTrailingRow', () => { @@ -722,12 +703,7 @@ describe('Table Mode Facet With Column Grouping Frozen Test', () => { .slice(-trailingRowCount!) .reverse() .map((_, idx) => getCellMeta(displayData.length - 1 - idx, 1)!.y), - ).toMatchInlineSnapshot(` - Array [ - 532, - 502, - ] - `); + ).toEqual([532, 502]); }); test('should get correct viewCellHeights result', () => { diff --git a/packages/s2-core/__tests__/unit/facet/util.ts b/packages/s2-core/__tests__/unit/facet/util.ts index 8df105b473..45aed17f8c 100644 --- a/packages/s2-core/__tests__/unit/facet/util.ts +++ b/packages/s2-core/__tests__/unit/facet/util.ts @@ -1,6 +1,8 @@ import { assembleDataCfg } from 'tests/util'; -import type { RawData } from '../../../src/common'; -import { transformIndexesData } from '@/utils/dataset/pivot-data-set'; +import { + getExistValues, + transformIndexesData, +} from '@/utils/dataset/pivot-data-set'; /** * 获取 Mock 数据 @@ -9,17 +11,20 @@ export function getMockPivotMeta() { const sortedDimensionValues = {}; const rawRowPivotMeta = new Map(); const rawColPivotMeta = new Map(); - const rawIndexesData: RawData[][] | RawData[] = []; + const { fields, data } = assembleDataCfg(); + const rawIndexesData = {}; return transformIndexesData({ - rows: fields.rows, - columns: fields.columns, - values: fields.values, - originData: data, + rows: fields.rows as string[], + columns: fields.columns as string[], + values: fields.values as string[], + data, indexesData: rawIndexesData, sortedDimensionValues, rowPivotMeta: rawRowPivotMeta, colPivotMeta: rawColPivotMeta, + valueInCols: true, + getExistValuesByDataItem: getExistValues, }); } diff --git a/packages/s2-core/__tests__/unit/interaction/base-interaction/click/data-cell-click-spec.ts b/packages/s2-core/__tests__/unit/interaction/base-interaction/click/data-cell-click-spec.ts index e8fdcc89f3..a8823d2063 100644 --- a/packages/s2-core/__tests__/unit/interaction/base-interaction/click/data-cell-click-spec.ts +++ b/packages/s2-core/__tests__/unit/interaction/base-interaction/click/data-cell-click-spec.ts @@ -3,8 +3,6 @@ import { createMockCellInfo, sleep, } from 'tests/util/helpers'; -import type { GEvent } from '@/index'; -import type { SpreadSheet } from '@/sheet-type'; import { HOVER_FOCUS_DURATION, InteractionName, @@ -12,7 +10,10 @@ import { InterceptType, S2Event, } from '@/common/constant'; +import type { InteractionCellHighlightOptions } from '@/common/interface'; import { CustomRect } from '@/engine'; +import type { GEvent } from '@/index'; +import type { SpreadSheet } from '@/sheet-type'; jest.mock('@/interaction/event-controller'); @@ -84,9 +85,30 @@ describe('Interaction Data Cell Click Tests', () => { s2.emit(S2Event.DATA_CELL_CLICK, { stopPropagation() {}, } as unknown as GEvent); + expect(selected).toHaveBeenCalledWith([mockCellInfo.mockCell]); }); + // https://github.com/antvis/S2/issues/2447 + test('should emit cell selected event when cell unselected', () => { + jest + .spyOn(s2.interaction, 'isSelectedCell') + .mockImplementationOnce(() => true); + + const selected = jest.fn(); + + s2.on(S2Event.GLOBAL_SELECTED, selected); + + s2.emit(S2Event.DATA_CELL_CLICK, { + stopPropagation() {}, + originalEvent: { + detail: 1, + }, + } as unknown as GEvent); + + expect(selected).toHaveBeenCalledWith([]); + }); + test('should emit link field jump event when link field text click and not show tooltip', () => { const linkFieldJump = jest.fn(); @@ -172,4 +194,66 @@ describe('Interaction Data Cell Click Tests', () => { expect(s2.interaction.isHoverFocusState()).toBeFalsy(); expect(clearHoverTimerSpy).toHaveBeenCalledTimes(2); }); + + test('should highlight the column header cell and row header cell when data cell clicked', () => { + const headerCellId0 = 'header-0'; + const headerCellId1 = 'header-1'; + const columnNode: Node[] = [ + { + belongsCell: { + getMeta: () => ({ + id: headerCellId0, + colIndex: -1, + rowIndex: -1, + }), + } as any, + id: headerCellId0, + } as unknown as Node, + { + belongsCell: { + getMeta: () => ({ + id: headerCellId1, + colIndex: -1, + rowIndex: -1, + }), + } as any, + id: headerCellId1, + } as unknown as Node, + ]; + + s2.facet.getColNodes = jest.fn(() => columnNode) as any; + s2.facet.getRowNodes = jest.fn(() => []); + + const firstDataCellInfo = createMockCellInfo( + `${headerCellId0}[&]first-data-cell`, + ); + + s2.getCell = () => firstDataCellInfo.mockCell as any; + + s2.setOptions({ + interaction: { + selectedCellHighlight: { + colHeader: true, + rowHeader: true, + } as InteractionCellHighlightOptions, + }, + }); + + s2.interaction.updateCells = jest.fn(); + s2.facet.getColCells = jest.fn(); + s2.facet.getRowCells = jest.fn(); + + s2.emit(S2Event.DATA_CELL_CLICK, { + stopPropagation() {}, + } as unknown as GEvent); + + expect(s2.interaction.getState()).toEqual({ + cells: [firstDataCellInfo.mockCellMeta], + stateName: InteractionStateName.SELECTED, + onUpdateCells: expect.any(Function), + }); + expect(s2.interaction.updateCells).toHaveBeenCalled(); + expect(s2.facet.getColCells).toHaveBeenCalled(); + expect(s2.facet.getRowCells).toHaveBeenCalled(); + }); }); diff --git a/packages/s2-core/__tests__/unit/interaction/base-interaction/click/row-column-click-spec.ts b/packages/s2-core/__tests__/unit/interaction/base-interaction/click/row-column-click-spec.ts index b911a822af..52bcd2b273 100644 --- a/packages/s2-core/__tests__/unit/interaction/base-interaction/click/row-column-click-spec.ts +++ b/packages/s2-core/__tests__/unit/interaction/base-interaction/click/row-column-click-spec.ts @@ -9,7 +9,12 @@ import type { ViewMeta, } from '@/common/interface'; import type { SpreadSheet } from '@/sheet-type'; -import { InteractionStateName, S2Event } from '@/common/constant'; +import { + InteractionKeyboardKey, + InteractionStateName, + InterceptType, + S2Event, +} from '@/common/constant'; import type { Node } from '@/facet/layout/node'; import { CustomRect } from '@/engine'; @@ -85,6 +90,42 @@ describe('Interaction Row & Column Cell Click Tests', () => { expect(rowColumnClick.bindEvents).toBeDefined(); }); + test.each([InteractionKeyboardKey.META, InteractionKeyboardKey.CONTROL])( + 'should add click intercept when %s keydown', + (key) => { + s2.emit(S2Event.GLOBAL_KEYBOARD_DOWN, { + key, + } as KeyboardEvent); + + expect(s2.interaction.hasIntercepts([InterceptType.CLICK])).toBeTruthy(); + }, + ); + + test.each([InteractionKeyboardKey.META, InteractionKeyboardKey.CONTROL])( + 'should remove click intercept when %s keyup', + (key) => { + s2.interaction.addIntercepts([InterceptType.CLICK]); + s2.emit(S2Event.GLOBAL_KEYBOARD_UP, { + key, + } as KeyboardEvent); + + expect(s2.interaction.hasIntercepts([InterceptType.CLICK])).toBeFalsy(); + }, + ); + + test.each([InteractionKeyboardKey.META, InteractionKeyboardKey.CONTROL])( + 'should remove click intercept when %s released', + () => { + Object.defineProperty(rowColumnClick, 'isMultiSelection', { + value: true, + }); + s2.interaction.addIntercepts([InterceptType.CLICK]); + s2.emit(S2Event.GLOBAL_MOUSE_MOVE, {} as MouseEvent); + + expect(s2.interaction.hasIntercepts([InterceptType.CLICK])).toBeFalsy(); + }, + ); + // https://github.com/antvis/S2/issues/1243 test.each([S2Event.ROW_CELL_CLICK, S2Event.COL_CELL_CLICK])( 'should selected cell when %s cell clicked', @@ -108,6 +149,7 @@ describe('Interaction Row & Column Cell Click Tests', () => { }); expect(s2.showTooltipWithInfo).toHaveBeenCalled(); expect(selected).toHaveBeenCalled(); + expect(s2.interaction.hasIntercepts([InterceptType.HOVER])).toBeTrue(); isSelectedCellSpy.mockRestore(); }, @@ -139,6 +181,7 @@ describe('Interaction Row & Column Cell Click Tests', () => { expect(s2.interaction.getState().cells).toEqual([]); expect(s2.showTooltipWithInfo).toHaveBeenCalled(); expect(selected).toHaveBeenCalled(); + expect(s2.interaction.hasIntercepts([InterceptType.HOVER])).toBeFalse(); getInteractedCellsSpy.mockRestore(); }, diff --git a/packages/s2-core/__tests__/unit/interaction/base-interaction/hover-spec.ts b/packages/s2-core/__tests__/unit/interaction/base-interaction/hover-spec.ts index 42bfa5ec4d..53a5c36dc8 100644 --- a/packages/s2-core/__tests__/unit/interaction/base-interaction/hover-spec.ts +++ b/packages/s2-core/__tests__/unit/interaction/base-interaction/hover-spec.ts @@ -32,6 +32,7 @@ describe('Interaction Hover Tests', () => { const mockTooltipParams = [ [{ value: undefined, valueField: undefined }], { + enableFormat: true, hideSummary: true, isTotals: undefined, onlyShowCellText: true, @@ -77,7 +78,7 @@ describe('Interaction Hover Tests', () => { mockCellUpdate.mockReset(); }); - afterAll(() => { + afterEach(() => { mockCellUpdate.mockRestore(); }); @@ -86,6 +87,15 @@ describe('Interaction Hover Tests', () => { }); test('should trigger data cell hover', async () => { + const interactionGetHoverHighlightSpy = jest + .spyOn(s2.interaction, 'getHoverHighlight') + .mockImplementationOnce(() => ({ + rowHeader: true, + colHeader: true, + currentRow: true, + currentCol: true, + })); + s2.emit(S2Event.DATA_CELL_HOVER, { target: {} } as GEvent); expect(s2.interaction.getState()).toEqual({ cells: [mockCellMeta], @@ -99,6 +109,37 @@ describe('Interaction Hover Tests', () => { stateName: InteractionStateName.HOVER_FOCUS, }); expect(s2.showTooltipWithInfo).toHaveBeenCalled(); + expect(interactionGetHoverHighlightSpy).toHaveBeenCalled(); + }); + + test('should trigger data cell hover depend on separate config', async () => { + s2.facet.getColCells = jest.fn(); + s2.facet.getRowCells = jest.fn(); + + s2.setOptions({ + interaction: { + hoverHighlight: { + colHeader: true, + rowHeader: false, + }, + }, + }); + + s2.emit(S2Event.DATA_CELL_HOVER, { target: {} } as GEvent); + expect(s2.interaction.getState()).toEqual({ + cells: [mockCellMeta], + stateName: InteractionStateName.HOVER, + }); + + await sleep(1000); + + expect(s2.interaction.getState()).toEqual({ + cells: [mockCellMeta], + stateName: InteractionStateName.HOVER_FOCUS, + }); + + expect(s2.facet.getColCells).toHaveBeenCalled(); + expect(s2.facet.getRowCells).not.toHaveBeenCalled(); }); test('should not trigger data cell hover when hover cell not change', () => { @@ -283,6 +324,7 @@ describe('Interaction Hover Tests', () => { await sleep(HOVER_FOCUS_DURATION + 200); expect(s2.showTooltipWithInfo).toHaveBeenCalled(); + expect(s2.hideTooltip).toHaveBeenCalled(); }, ); diff --git a/packages/s2-core/__tests__/unit/interaction/brush-selection/base-brush-selection-spec.ts b/packages/s2-core/__tests__/unit/interaction/brush-selection/base-brush-selection-spec.ts index f1b5b92ee5..2ca03ad8d0 100644 --- a/packages/s2-core/__tests__/unit/interaction/brush-selection/base-brush-selection-spec.ts +++ b/packages/s2-core/__tests__/unit/interaction/brush-selection/base-brush-selection-spec.ts @@ -59,7 +59,15 @@ describe('Interaction Base Cell Brush Selection Tests', () => { s2 = new PivotSheet(getContainer(), null as unknown as S2DataConfig, null); await s2.render(); + mockRootInteraction = new MockRootInteraction(s2); + mockRootInteraction.getBrushSelection = () => { + return { + dataCell: true, + rowCell: true, + colCell: true, + }; + }; s2.getCell = jest.fn(() => startBrushDataCell) as any; s2.facet.foregroundGroup = new Group(); s2.showTooltipWithInfo = jest.fn(); diff --git a/packages/s2-core/__tests__/unit/interaction/brush-selection/col-brush-selection-spec.ts b/packages/s2-core/__tests__/unit/interaction/brush-selection/col-brush-selection-spec.ts index fae5b8fdfd..99b88feadd 100644 --- a/packages/s2-core/__tests__/unit/interaction/brush-selection/col-brush-selection-spec.ts +++ b/packages/s2-core/__tests__/unit/interaction/brush-selection/col-brush-selection-spec.ts @@ -68,11 +68,19 @@ describe('Interaction Col Cell Brush Selection Tests', () => { }, }, }); + await s2.render(); + s2.showTooltipWithInfo = jest.fn(); mockRootInteraction = new MockRootInteraction(s2); s2.getCell = jest.fn(() => startBrushColCell) as any; + mockRootInteraction.getBrushSelection = () => { + return { + dataCell: true, + rowCell: true, + colCell: true, + }; + }; s2.interaction = mockRootInteraction; - await s2.render(); brushSelectionInstance = new ColCellBrushSelection(s2); brushSelectionInstance.brushSelectionStage = @@ -203,4 +211,30 @@ describe('Interaction Col Cell Brush Selection Tests', () => { expect(selectedFn).toHaveBeenCalledTimes(1); expect(brushSelectionFn).toHaveBeenCalledTimes(1); }); + + test('should not emit brush secletion event', () => { + mockRootInteraction.getBrushSelection = () => ({ + dataCell: true, + rowCell: true, + colCell: false, + }); + + const brushSelectionFn = jest.fn(); + + s2.on(S2Event.COL_CELL_BRUSH_SELECTION, brushSelectionFn); + + // ================== mouse down ================== + emitEvent(S2Event.COL_CELL_MOUSE_DOWN, { x: 200, y: 0 }); + + // ================== mouse move ================== + emitEvent(S2Event.COL_CELL_MOUSE_MOVE, { + clientX: 600, + clientY: 90, + }); + + // ================== mouse up ================== + emitEvent(S2Event.GLOBAL_MOUSE_UP, {}); + // emit event + expect(brushSelectionFn).toHaveBeenCalledTimes(0); + }); }); diff --git a/packages/s2-core/__tests__/unit/interaction/brush-selection/data-brush-selection-spec.ts b/packages/s2-core/__tests__/unit/interaction/brush-selection/data-brush-selection-spec.ts index 4aed4c4dd2..e088960cf2 100644 --- a/packages/s2-core/__tests__/unit/interaction/brush-selection/data-brush-selection-spec.ts +++ b/packages/s2-core/__tests__/unit/interaction/brush-selection/data-brush-selection-spec.ts @@ -115,6 +115,8 @@ describe('Interaction Data Cell Brush Selection Tests', () => { null as unknown as S2DataConfig, null as unknown as S2Options, ); + await s2.render(); + mockRootInteraction = new MockRootInteraction(s2); s2.getCell = jest.fn(() => startBrushDataCell) as any; s2.showTooltipWithInfo = jest.fn(); @@ -126,8 +128,14 @@ describe('Interaction Data Cell Brush Selection Tests', () => { currentCol: false, }; }; + mockRootInteraction.getBrushSelection = () => { + return { + dataCell: true, + rowCell: true, + colCell: true, + }; + }; s2.interaction = mockRootInteraction; - await s2.render(); s2.facet.getDataCells = () => panelGroupAllDataCells; s2.facet.getLayoutResult = () => ({ @@ -462,15 +470,23 @@ describe('Interaction Data Cell Brush Selection Tests', () => { (facet as TableFacet).frozenGroupInfo = { [FrozenGroupType.FROZEN_COL]: { width: 100, + x: 0, + range: [], }, [FrozenGroupType.FROZEN_TRAILING_COL]: { width: 100, + x: 0, + range: [], }, [FrozenGroupType.FROZEN_ROW]: { height: 0, + y: 0, + range: [], }, [FrozenGroupType.FROZEN_TRAILING_ROW]: { height: 0, + y: 0, + range: [], }, }; @@ -488,16 +504,24 @@ describe('Interaction Data Cell Brush Selection Tests', () => { (facet as TableFacet).frozenGroupInfo = { [FrozenGroupType.FROZEN_COL]: { + x: 0, width: 0, + range: [], }, [FrozenGroupType.FROZEN_TRAILING_COL]: { + x: 0, width: 0, + range: [], }, [FrozenGroupType.FROZEN_ROW]: { + y: 0, + range: [], height: 100, }, [FrozenGroupType.FROZEN_TRAILING_ROW]: { height: 100, + y: 0, + range: [], }, }; expect(getScrollOffsetForRow(7, ScrollDirection.SCROLL_UP, s2)).toBe(600); @@ -519,15 +543,23 @@ describe('Interaction Data Cell Brush Selection Tests', () => { (s2.facet as TableFacet).frozenGroupInfo = { [FrozenGroupType.FROZEN_COL]: { + width: 0, + x: 0, range: [0, 1], }, [FrozenGroupType.FROZEN_TRAILING_COL]: { + width: 0, + x: 0, range: [8, 9], }, [FrozenGroupType.FROZEN_ROW]: { + y: 0, + height: 0, range: [0, 1], }, [FrozenGroupType.FROZEN_TRAILING_ROW]: { + y: 0, + height: 0, range: [8, 9], }, }; @@ -541,4 +573,30 @@ describe('Interaction Data Cell Brush Selection Tests', () => { expect(validateXIndex(8)).toBe(null); expect(validateXIndex(7)).toBe(7); }); + + test('should not emit brush selection event', () => { + mockRootInteraction.getBrushSelection = () => ({ + dataCell: false, + rowCell: true, + colCell: true, + }); + + const brushSelectionFn = jest.fn(); + + s2.on(S2Event.DATA_CELL_BRUSH_SELECTION, brushSelectionFn); + + // ================== mouse down ================== + emitEvent(S2Event.DATA_CELL_MOUSE_DOWN, { x: 10, y: 20 }); + + // ================== mouse move ================== + emitGlobalEvent(S2Event.GLOBAL_MOUSE_MOVE, { + clientX: 100, + clientY: 200, + }); + + // ================== mouse up ================== + emitEvent(S2Event.GLOBAL_MOUSE_UP, {}); + // emit event + expect(brushSelectionFn).toHaveBeenCalledTimes(0); + }); }); diff --git a/packages/s2-core/__tests__/unit/interaction/brush-selection/row-brush-selection-spec.ts b/packages/s2-core/__tests__/unit/interaction/brush-selection/row-brush-selection-spec.ts index 797ba740a1..d36a16a44b 100644 --- a/packages/s2-core/__tests__/unit/interaction/brush-selection/row-brush-selection-spec.ts +++ b/packages/s2-core/__tests__/unit/interaction/brush-selection/row-brush-selection-spec.ts @@ -68,11 +68,19 @@ describe('Interaction Row Cell Brush Selection Tests', () => { }, }); + await s2.render(); + s2.showTooltipWithInfo = jest.fn(); s2.getCell = jest.fn(() => startBrushRowCell) as any; mockRootInteraction = new MockRootInteraction(s2); + mockRootInteraction.getBrushSelection = () => { + return { + dataCell: true, + rowCell: true, + colCell: true, + }; + }; s2.interaction = mockRootInteraction; - await s2.render(); brushSelectionInstance = new RowCellBrushSelection(s2); brushSelectionInstance.brushSelectionStage = @@ -250,4 +258,28 @@ describe('Interaction Row Cell Brush Selection Tests', () => { // get brush range selected cells expect(brushSelectionInstance.brushRangeCells.length).toEqual(8); }); + + test('should not emit brush secletion event', () => { + mockRootInteraction.getBrushSelection = () => ({ + dataCell: true, + rowCell: false, + colCell: true, + }); + + const brushSelectionFn = jest.fn(); + + s2.on(S2Event.ROW_CELL_BRUSH_SELECTION, brushSelectionFn); + + // ================== mouse down ================== + emitEvent(S2Event.ROW_CELL_MOUSE_DOWN, { x: 10, y: 90 }); + + s2.getCell = jest.fn(() => endBrushRowCell) as any; + // ================== mouse move ================== + emitEvent(S2Event.GLOBAL_MOUSE_MOVE, { clientX: 180, clientY: 400 }); + + // ================== mouse up ================== + emitEvent(S2Event.GLOBAL_MOUSE_UP, {}); + // emit event + expect(brushSelectionFn).toHaveBeenCalledTimes(0); + }); }); diff --git a/packages/s2-core/__tests__/unit/interaction/data-cell-multi-selection-spec.ts b/packages/s2-core/__tests__/unit/interaction/data-cell-multi-selection-spec.ts index b546e9ca74..31ce8676bf 100644 --- a/packages/s2-core/__tests__/unit/interaction/data-cell-multi-selection-spec.ts +++ b/packages/s2-core/__tests__/unit/interaction/data-cell-multi-selection-spec.ts @@ -40,7 +40,7 @@ describe('Interaction Data Cell Multi Selection Tests', () => { }); test.each([InteractionKeyboardKey.META, InteractionKeyboardKey.CONTROL])( - 'should add click intercept when keydown', + 'should add click intercept when %s keydown', (key) => { s2.emit(S2Event.GLOBAL_KEYBOARD_DOWN, { key, @@ -51,8 +51,9 @@ describe('Interaction Data Cell Multi Selection Tests', () => { ); test.each([InteractionKeyboardKey.META, InteractionKeyboardKey.CONTROL])( - 'should remove click intercept when keyup', + 'should remove click intercept when %s keyup', (key) => { + s2.interaction.addIntercepts([InterceptType.CLICK]); s2.emit(S2Event.GLOBAL_KEYBOARD_UP, { key, } as KeyboardEvent); @@ -61,6 +62,19 @@ describe('Interaction Data Cell Multi Selection Tests', () => { }, ); + test.each([InteractionKeyboardKey.META, InteractionKeyboardKey.CONTROL])( + 'should remove click intercept when %s released', + () => { + Object.defineProperty(dataCellMultiSelection, 'isMultiSelection', { + value: true, + }); + s2.interaction.addIntercepts([InterceptType.CLICK]); + s2.emit(S2Event.GLOBAL_MOUSE_MOVE, {} as MouseEvent); + + expect(s2.interaction.hasIntercepts([InterceptType.CLICK])).toBeFalsy(); + }, + ); + test.each([InteractionKeyboardKey.META, InteractionKeyboardKey.CONTROL])( 'should select multiple data cell', (key) => { diff --git a/packages/s2-core/__tests__/unit/interaction/event-controller-spec.ts b/packages/s2-core/__tests__/unit/interaction/event-controller-spec.ts index ba67172c53..0eb3b690ac 100644 --- a/packages/s2-core/__tests__/unit/interaction/event-controller-spec.ts +++ b/packages/s2-core/__tests__/unit/interaction/event-controller-spec.ts @@ -125,12 +125,11 @@ describe('Interaction Event Controller Tests', () => { renderer: new Renderer() as unknown as CanvasConfig['renderer'], }); spreadsheet.facet = { + ...spreadsheet.facet, panelBBox: { maxX: s2Options.width, maxY: s2Options.height, } as BBox, - getDataCells: jest.fn(), - getCells: jest.fn(), } as unknown as BaseFacet; spreadsheet.interaction = new RootInteraction( spreadsheet as unknown as SpreadSheet, @@ -549,14 +548,18 @@ describe('Interaction Event Controller Tests', () => { test('should reset if current mouse inside the canvas container, but outside the panel facet', () => { spreadsheet.facet = { + ...spreadsheet.facet, panelBBox: { maxX: 100, maxY: 100, } as BBox, } as BaseFacet; + + const selected = jest.fn(); const reset = jest.fn(); spreadsheet.on(S2Event.GLOBAL_RESET, reset); + spreadsheet.on(S2Event.GLOBAL_SELECTED, selected); const pointInCanvas = spreadsheet.container.viewport2Client({ x: 120, @@ -571,25 +574,31 @@ describe('Interaction Event Controller Tests', () => { } as MouseEventInit), ); + expect(selected).toHaveBeenCalledWith([]); expect(reset).toHaveBeenCalled(); expect(spreadsheet.interaction.reset).toHaveBeenCalled(); }); test('should reset if press ecs', () => { spreadsheet.facet = { + ...spreadsheet.facet, panelBBox: { maxX: 100, maxY: 100, } as BBox, } as BaseFacet; + + const selected = jest.fn(); const reset = jest.fn(); spreadsheet.on(S2Event.GLOBAL_RESET, reset); + spreadsheet.on(S2Event.GLOBAL_SELECTED, selected); window.dispatchEvent( new KeyboardEvent('keydown', { key: InteractionKeyboardKey.ESC }), ); + expect(selected).toHaveBeenCalledWith([]); expect(reset).toHaveBeenCalled(); expect(spreadsheet.interaction.reset).toHaveBeenCalled(); }); @@ -664,6 +673,7 @@ describe('Interaction Event Controller Tests', () => { test('should disable reset if autoResetSheetStyle set to false', () => { spreadsheet.facet = { + ...spreadsheet.facet, panelBBox: { maxX: 100, maxY: 100, diff --git a/packages/s2-core/__tests__/unit/interaction/range-selection-spec.ts b/packages/s2-core/__tests__/unit/interaction/range-selection-spec.ts index 89ad7f4935..20da43616f 100644 --- a/packages/s2-core/__tests__/unit/interaction/range-selection-spec.ts +++ b/packages/s2-core/__tests__/unit/interaction/range-selection-spec.ts @@ -10,10 +10,12 @@ import { S2Event, } from '@/common/constant'; import { RangeSelection } from '@/interaction/range-selection'; +import { getCellMeta } from '@/utils'; jest.mock('@/utils/tooltip'); jest.mock('@/interaction/event-controller'); jest.mock('@/interaction/base-interaction/click/data-cell-click'); +jest.mock('@/interaction/base-interaction/click/row-column-click'); describe('Interaction Range Selection Tests', () => { let rangeSelection: RangeSelection; @@ -47,6 +49,7 @@ describe('Interaction Range Selection Tests', () => { }); test('should remove click intercept when shift keyup', () => { + s2.interaction.addIntercepts([InterceptType.CLICK]); s2.emit(S2Event.GLOBAL_KEYBOARD_UP, { key: InteractionKeyboardKey.SHIFT, } as KeyboardEvent); @@ -54,6 +57,16 @@ describe('Interaction Range Selection Tests', () => { expect(s2.interaction.hasIntercepts([InterceptType.CLICK])).toBeFalsy(); }); + test('should remove click intercept when shift released', () => { + Object.defineProperty(rangeSelection, 'isRangeSelection', { + value: true, + }); + s2.interaction.addIntercepts([InterceptType.CLICK]); + s2.emit(S2Event.GLOBAL_MOUSE_MOVE, {} as MouseEvent); + + expect(s2.interaction.hasIntercepts([InterceptType.CLICK])).toBeFalsy(); + }); + test('should set last clicked cell', () => { s2.interaction.changeState({ cells: [], @@ -70,6 +83,28 @@ describe('Interaction Range Selection Tests', () => { expect(s2.store.get('lastClickedCell')).toEqual(mockCell00.mockCell); }); + test('should remove hover intercepts when col cell unselected', () => { + const mockCell00 = createMockCellInfo('3-3', { rowIndex: 3, colIndex: 3 }); + + s2.getCell = () => mockCell00.mockCell as any; + + s2.interaction.addIntercepts([InterceptType.HOVER]); + + // 有选中时,不应清理 hover 拦截 + s2.interaction.getCells = () => [getCellMeta(mockCell00.mockCell)]; + s2.emit(S2Event.COL_CELL_CLICK, { + stopPropagation() {}, + } as unknown as GEvent); + expect(s2.interaction.hasIntercepts([InterceptType.HOVER])).toBeTrue(); + + // 无选中时,应清理拦截 + s2.interaction.getCells = () => []; + s2.emit(S2Event.COL_CELL_CLICK, { + stopPropagation() {}, + } as unknown as GEvent); + expect(s2.interaction.hasIntercepts([InterceptType.HOVER])).toBeFalse(); + }); + // should use data cell click interaction for single cell select test('should not hide tooltip and change single data cell state', () => { s2.store.set('lastClickedCell', null); diff --git a/packages/s2-core/__tests__/unit/interaction/root-spec.ts b/packages/s2-core/__tests__/unit/interaction/root-spec.ts index e35b3123e0..683b673589 100644 --- a/packages/s2-core/__tests__/unit/interaction/root-spec.ts +++ b/packages/s2-core/__tests__/unit/interaction/root-spec.ts @@ -1,13 +1,15 @@ import type { Canvas } from '@antv/g'; -import { createMockCellInfo, sleep } from 'tests/util/helpers'; import { get } from 'lodash'; +import { createMockCellInfo, sleep } from 'tests/util/helpers'; import type { PivotFacet } from '../../../src/facet'; import { Store } from '@/common/store'; import { BaseEvent, CellType, + ColCellBrushSelection, CornerCellClick, DataCell, + DataCellBrushSelection, DataCellClick, DataCellMultiSelection, GuiIcon, @@ -17,17 +19,15 @@ import { InterceptType, MergedCell, MergedCellClick, + Node, RangeSelection, + RowCellBrushSelection, RowColumnClick, RowColumnResize, RowTextClick, - type S2Options, SelectedCellMove, SpreadSheet, - Node, - DataCellBrushSelection, - ColCellBrushSelection, - RowCellBrushSelection, + type S2Options, } from '@/index'; import { RootInteraction } from '@/interaction/root'; import { mergeCell, unmergeCell } from '@/utils/interaction/merge-cell'; @@ -68,7 +68,7 @@ describe('RootInteraction Tests', () => { }, }) as unknown as DataCell; - beforeAll(() => { + beforeEach(() => { MockSpreadSheet.mockClear(); panelGroupAllDataCells = Array.from({ length: 10 }).map( (_, idx) => getMockCell(idx), @@ -100,6 +100,10 @@ describe('RootInteraction Tests', () => { mockSpreadSheetInstance.interaction = rootInteraction; }); + afterEach(() => { + rootInteraction.destroy(); + }); + test('should get default interaction state', () => { expect(rootInteraction.getState()).toEqual({ cells: [], @@ -174,9 +178,9 @@ describe('RootInteraction Tests', () => { // https://github.com/antvis/S2/issues/1243 test('should multi selected header cells', () => { - const isEqualStateNameSpy = jest + jest .spyOn(rootInteraction, 'isEqualStateName') - .mockImplementation(() => false); + .mockImplementationOnce(() => false); const mockCellA = createMockCellInfo('test-A').mockCell; const mockCellB = createMockCellInfo('test-B').mockCell; @@ -187,10 +191,7 @@ describe('RootInteraction Tests', () => { isMultiSelection: true, }); - expect(rootInteraction.getState().cells).toEqual([ - getCellMeta(mockCell), - getCellMeta(mockCellA), - ]); + expect(rootInteraction.getState().cells).toEqual([getCellMeta(mockCellA)]); // 选中 cellB rootInteraction.selectHeaderCell({ @@ -199,7 +200,6 @@ describe('RootInteraction Tests', () => { }); expect(rootInteraction.getState().cells).toEqual([ - getCellMeta(mockCell), getCellMeta(mockCellA), getCellMeta(mockCellB), ]); @@ -211,12 +211,7 @@ describe('RootInteraction Tests', () => { }); // 取消选中 - expect(rootInteraction.getState().cells).toEqual([ - getCellMeta(mockCell), - getCellMeta(mockCellA), - ]); - - isEqualStateNameSpy.mockRestore(); + expect(rootInteraction.getState().cells).toEqual([getCellMeta(mockCellA)]); }); test('should call merge cells', () => { @@ -515,7 +510,9 @@ describe('RootInteraction Tests', () => { InterceptType.CLICK, ]), ).toBeTruthy(); + rootInteraction.removeIntercepts([InterceptType.CLICK]); + expect(rootInteraction.hasIntercepts([InterceptType.CLICK])).toBeFalsy(); expect(rootInteraction.hasIntercepts([InterceptType.HOVER])).toBeTruthy(); }); @@ -671,16 +668,20 @@ describe('RootInteraction Tests', () => { }); }); - test('should reset interaction when visibilitychange', () => { - rootInteraction = new RootInteraction(mockSpreadSheetInstance); - mockSpreadSheetInstance.interaction = rootInteraction; - rootInteraction.interactions.forEach((interaction) => { - interaction.reset = jest.fn(); - }); + // eslint-disable-next-line jest/no-disabled-tests + test.skip('should reset interaction when visibilitychange', () => { + const resetSpyList = [...rootInteraction.interactions.values()].map( + (interaction) => { + return jest + .spyOn(interaction, 'reset') + .mockImplementationOnce(() => {}); + }, + ); + window.dispatchEvent(new Event('visibilitychange')); - rootInteraction.interactions.forEach((interaction) => { - expect(interaction.reset).toHaveBeenCalled(); + resetSpyList.forEach((resetSpy) => { + expect(resetSpy).toHaveBeenCalledTimes(1); }); }); }); diff --git a/packages/s2-core/__tests__/unit/interaction/row-column-resize-spec.ts b/packages/s2-core/__tests__/unit/interaction/row-column-resize-spec.ts index 26b133a534..8953d35524 100644 --- a/packages/s2-core/__tests__/unit/interaction/row-column-resize-spec.ts +++ b/packages/s2-core/__tests__/unit/interaction/row-column-resize-spec.ts @@ -226,6 +226,8 @@ describe('Interaction Row Column Resize Tests', () => { { offsetX: 10, offsetY: 20, + clientX: 10, + clientY: 20, }, resizeInfo, ); @@ -233,6 +235,7 @@ describe('Interaction Row Column Resize Tests', () => { expect(s2.store.get('resized')).toBeFalsy(); expect(rowColumnResizeInstance.resizeStartPosition).toStrictEqual({ offsetX: 10, + clientX: 10, }); expect(getStartGuideLine().attr('path')).toStrictEqual([ ['M', 3.5, 2], @@ -356,12 +359,15 @@ describe('Interaction Row Column Resize Tests', () => { { offsetX: 10, offsetY: 20, + clientX: 10, + clientY: 20, }, resizeInfo, ); expect(rowColumnResizeInstance.resizeStartPosition).toStrictEqual({ offsetY: 20, + clientY: 20, }); expect(getStartGuideLine().attr('path')).toStrictEqual([ ['M', 2, 3.5], @@ -750,6 +756,8 @@ describe('Interaction Row Column Resize Tests', () => { { offsetX: 10, offsetY: 20, + clientX: 10, + clientY: 20, }, resizeInfo, ); @@ -759,6 +767,8 @@ describe('Interaction Row Column Resize Tests', () => { { offsetX: 20, offsetY: 20, + clientX: 20, + clientY: 20, }, resizeInfo, ); diff --git a/packages/s2-core/__tests__/unit/sheet-type/pivot-sheet-spec.ts b/packages/s2-core/__tests__/unit/sheet-type/pivot-sheet-spec.ts index 35f4efca83..1a62ea9e86 100644 --- a/packages/s2-core/__tests__/unit/sheet-type/pivot-sheet-spec.ts +++ b/packages/s2-core/__tests__/unit/sheet-type/pivot-sheet-spec.ts @@ -1,6 +1,6 @@ // eslint-disable-next-line max-classes-per-file import { Canvas, CanvasEvent } from '@antv/g'; -import { cloneDeep, get, last } from 'lodash'; +import { cloneDeep, last } from 'lodash'; import dataCfg from 'tests/data/simple-data.json'; import { waitForRender } from 'tests/util'; import { createPivotSheet, getContainer, sleep } from 'tests/util/helpers'; @@ -33,7 +33,6 @@ import { type S2Options, type TooltipShowOptions, } from '@/common'; -import type { CornerCell } from '@/cell/corner-cell'; jest.mock('@/utils/hide-columns'); @@ -67,14 +66,15 @@ describe('PivotSheet Tests', () => { let container: HTMLDivElement; - beforeAll(async () => { + beforeEach(async () => { setLang('zh_CN'); + container = getContainer(); s2 = new PivotSheet(container, dataCfg, s2Options); await s2.render(); }); - afterAll(() => { + afterEach(() => { container?.remove(); s2?.destroy(); }); @@ -130,7 +130,7 @@ describe('PivotSheet Tests', () => { s2.tooltip.destroy(); // remove container - expect(s2.tooltip.container).toBe(null); + expect(s2.tooltip.container).toBeFalsy(); // reset position expect(s2.tooltip.position).toEqual({ x: 0, @@ -478,6 +478,53 @@ describe('PivotSheet Tests', () => { expect(s2.options.showSeriesNumber).toBeTruthy(); }); + test('should init new tooltip', () => { + const tooltipDestroySpy = jest + .spyOn(s2.tooltip, 'destroy') + .mockImplementationOnce(() => {}); + + class CustomTooltip extends BaseTooltip {} + + s2.setOptions({ + tooltip: { + render: (spreadsheet) => new CustomTooltip(spreadsheet), + }, + }); + + expect(tooltipDestroySpy).toHaveBeenCalled(); + expect(s2.tooltip).toBeInstanceOf(CustomTooltip); + }); + + test('should refresh brush selection info', () => { + s2.setOptions({ + interaction: { + brushSelection: true, + }, + }); + + expect(s2.interaction.getBrushSelection()).toStrictEqual({ + dataCell: true, + rowCell: true, + colCell: true, + }); + + s2.setOptions({ + interaction: { + brushSelection: { + dataCell: true, + rowCell: false, + colCell: false, + }, + }, + }); + + expect(s2.interaction.getBrushSelection()).toStrictEqual({ + dataCell: true, + rowCell: false, + colCell: false, + }); + }); + test('should render sheet', async () => { const facetRenderSpy = jest .spyOn(s2, 'buildFacet' as any) @@ -610,7 +657,7 @@ describe('PivotSheet Tests', () => { expect(s2.facet.foregroundGroup.children).toHaveLength(9); // panel scroll group - expect(s2.facet.panelGroup.children).toHaveLength(1); + expect(s2.facet.panelGroup.children).toHaveLength(7); expect( s2.facet.panelGroup.getElementsByName(KEY_GROUP_PANEL_SCROLL), ).toHaveLength(1); @@ -726,7 +773,7 @@ describe('PivotSheet Tests', () => { const clearDrillDownDataSpy = jest .spyOn(s2.dataSet, 'clearDrillDownData' as any) - .mockImplementation(() => {}); + .mockImplementation(() => true); s2.clearDrillDownData(); @@ -742,6 +789,22 @@ describe('PivotSheet Tests', () => { renderSpy.mockRestore(); }); + test(`shouldn't rerender without drill down data`, () => { + const renderSpy = jest + .spyOn(s2, 'render') + .mockImplementationOnce(() => Promise.resolve()); + + const clearDrillDownDataSpy = jest + .spyOn(s2.dataSet, 'clearDrillDownData' as any) + .mockImplementation(() => false); + + s2.clearDrillDownData(); + + expect(clearDrillDownDataSpy).toHaveBeenCalledTimes(1); + // rerender + expect(renderSpy).toHaveBeenCalledTimes(0); + }); + test('should get extra field text', async () => { const pivotSheet = new PivotSheet( container, @@ -754,11 +817,9 @@ describe('PivotSheet Tests', () => { ); await pivotSheet.render(); - const extraField = last( - pivotSheet.facet.cornerHeader.children, - ) as CornerCell; + const extraField = last(pivotSheet.facet.getCornerCells()); - expect(get(extraField, 'actualText')).toEqual('数值'); + expect(extraField?.getActualText()).toEqual('数值'); }); // https://github.com/antvis/S2/issues/1212 @@ -780,11 +841,9 @@ describe('PivotSheet Tests', () => { await pivotSheet.render(); - const extraField = last( - pivotSheet.facet.cornerHeader.children, - ) as CornerCell; + const extraField = last(pivotSheet.facet.getCornerCells()); - expect(get(extraField, 'actualText')).toEqual(cornerExtraFieldText); + expect(extraField?.getActualText()).toEqual(cornerExtraFieldText); }); describe('Tree Collapse Tests', () => { @@ -994,7 +1053,7 @@ describe('PivotSheet Tests', () => { { query: undefined, sortByMeasure: nodeMeta.value, - sortFieldId: 'field', + sortFieldId: 'city', sortMethod: 'asc', }, ]); @@ -1010,7 +1069,7 @@ describe('PivotSheet Tests', () => { { query: undefined, sortByMeasure: nodeMeta.value, - sortFieldId: 'field', + sortFieldId: 'city', sortMethod: 'desc', }, ]); @@ -1042,7 +1101,7 @@ describe('PivotSheet Tests', () => { { query: { $$extra$$: 'price', type: '笔' }, sortByMeasure: 'price', - sortFieldId: 'field', + sortFieldId: 'city', sortMethod: 'asc', }, ]); @@ -1063,7 +1122,11 @@ describe('PivotSheet Tests', () => { s2.store.set('test', 111); // restore mock... - (s2.tooltip.show as jest.Mock).mockRestore(); + const tooltipShowSpy = jest + .spyOn(s2.tooltip, 'show') + .mockImplementationOnce(() => {}); + + tooltipShowSpy.mockRestore(); s2.showTooltip({ position: { x: 10, diff --git a/packages/s2-core/__tests__/unit/sheet-type/table-sheet-spec.ts b/packages/s2-core/__tests__/unit/sheet-type/table-sheet-spec.ts index ed8e336f0b..f3ca32b9dc 100644 --- a/packages/s2-core/__tests__/unit/sheet-type/table-sheet-spec.ts +++ b/packages/s2-core/__tests__/unit/sheet-type/table-sheet-spec.ts @@ -23,16 +23,16 @@ describe('TableSheet Tests', () => { let container: HTMLDivElement; - beforeAll(async () => { + beforeEach(async () => { container = getContainer(); s2 = new TableSheet(container, dataCfg, s2Options); await s2.render(); s2.store.set('sortMethodMap', null); }); - afterAll(() => { - container?.remove(); - s2?.destroy(); + afterEach(() => { + // container?.remove(); + // s2?.destroy(); }); describe('TableSheet Sort Tests', () => { @@ -95,15 +95,10 @@ describe('TableSheet Tests', () => { s2.groupSortByMethod('desc', node); expect(s2.store.get('sortMethodMap')).toEqual({ - city: 'asc', cost: 'desc', }); expect(s2.getMenuDefaultSelectedKeys(node.id)).toEqual(['desc']); expect(s2.dataCfg.sortParams).toEqual([ - { - sortFieldId: 'city', - sortMethod: 'asc', - }, { sortFieldId: 'cost', sortMethod: 'desc', @@ -267,4 +262,8 @@ describe('TableSheet Tests', () => { expect(sheet.facet).toBeInstanceOf(TableFacet); expect(mockRender).toHaveBeenCalledTimes(1); }); + + test('should get content height', () => { + expect(s2.getContentHeight()).toEqual(120); + }); }); diff --git a/packages/s2-core/__tests__/unit/utils/__snapshots__/sort-action-spec.tsx.snap b/packages/s2-core/__tests__/unit/utils/__snapshots__/sort-action-spec.tsx.snap new file mode 100644 index 0000000000..831f4bd7ae --- /dev/null +++ b/packages/s2-core/__tests__/unit/utils/__snapshots__/sort-action-spec.tsx.snap @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`total group dimension sort test should sort by col total with group 1`] = ` +Array [ + CellData { + "extraField": "price", + "raw": Object { + "city": "杭州", + "price": "666", + "type": "笔", + }, + }, + CellData { + "extraField": "price", + "raw": Object { + "city": "杭州", + "price": "999", + "type": "纸张", + }, + }, +] +`; diff --git a/packages/s2-core/__tests__/unit/utils/data-set-operate-spec.tsx b/packages/s2-core/__tests__/unit/utils/data-set-operate-spec.tsx index 5fd2938768..70efb9d846 100644 --- a/packages/s2-core/__tests__/unit/utils/data-set-operate-spec.tsx +++ b/packages/s2-core/__tests__/unit/utils/data-set-operate-spec.tsx @@ -1,11 +1,11 @@ import { set } from 'lodash'; +import { flattenIndexesData } from '../../../src/utils/dataset/pivot-data-set'; +import { QueryDataType, type FlattingIndexesData } from '../../../src'; +import { Aggregation } from '@/common/interface'; import { - getListBySorted, getAggregationAndCalcFuncByQuery, - flattenIndexesData, + getListBySorted, } from '@/utils/data-set-operate'; -import { Aggregation, type FlattingIndexesData } from '@/common/interface'; -import { DataSelectType } from '@/common/constant/total'; describe('Data Set Operate Test', () => { const data: FlattingIndexesData = []; @@ -27,18 +27,12 @@ describe('Data Set Operate Test', () => { }); test('flatten out all data with all select type', () => { - expect(flattenIndexesData(data, DataSelectType.All)).toBeArrayOfSize(6); - }); - - test('flatten out total data with total only type', () => { - expect( - flattenIndexesData(data, DataSelectType.TotalOnly), - ).toBeArrayOfSize(2); + expect(flattenIndexesData(data, QueryDataType.All)).toBeArrayOfSize(6); }); test('flatten out detail data with detail only type', () => { expect( - flattenIndexesData(data, DataSelectType.DetailOnly), + flattenIndexesData(data, QueryDataType.DetailOnly), ).toBeArrayOfSize(4); }); }); diff --git a/packages/s2-core/__tests__/unit/utils/dataset/pivot-data-set-spec.ts b/packages/s2-core/__tests__/unit/utils/dataset/pivot-data-set-spec.ts index 029c730360..418a6753cf 100644 --- a/packages/s2-core/__tests__/unit/utils/dataset/pivot-data-set-spec.ts +++ b/packages/s2-core/__tests__/unit/utils/dataset/pivot-data-set-spec.ts @@ -1,212 +1,643 @@ -import { assembleDataCfg } from 'tests/util'; -import { get } from 'lodash'; -import { data } from 'tests/data/mock-dataset.json'; +import { MULTI_VALUE } from '@/common/constant/field'; +import type { PivotMeta, SortedDimensionValues } from '@/data-set/interface'; import { - deleteMetaById, - transformIndexesData, - transformDimensionsValues, - getDataPath, - getDimensionsWithoutPathPre, - getDimensionsWithParentPath, + existDimensionTotalGroup, + flattenDimensionValues, } from '@/utils/dataset/pivot-data-set'; -import type { S2DataConfig } from '@/common/interface'; -import { CellData } from '@/data-set/cell-data'; -describe('PivotDataSet util test', () => { - const dataCfg: S2DataConfig = assembleDataCfg({ - data, - meta: [], - }); +describe('pivot-data-set utils test', () => { + let fields: string[]; + let sortedDimensionValues: SortedDimensionValues; + let pivotMeta: PivotMeta; + + beforeEach(() => { + fields = ['province', 'city', 'type', 'subType']; - test('for deleteMetaById function', () => { - const childrenMeta = { - level: 0, - children: new Map(), - childField: 'country', + sortedDimensionValues = { + province: ['浙江省', '四川省', '$$total$$'], + city: [ + '$$total$$[&]$$total$$', + '四川省[&]$$total$$', + '四川省[&]成[&]都[&]市', + '四川省[&]绵阳市', + '浙江省[&]$$total$$', + '浙江省[&]杭州市', + '浙江省[&]舟山市', + ], + type: [ + '$$total$$[&]$$total$$[&]$$total$$', + '四川省[&]$$total$$[&]$$total$$', + '四川省[&]成[&]都[&]市[&]$$total$$', + '四川省[&]成[&]都[&]市[&]家具', + '四川省[&]成[&]都[&]市[&]办公用品', + '四川省[&]绵阳市[&]$$total$$', + '四川省[&]绵阳市[&]家具', + '四川省[&]绵阳市[&]办公用品', + '浙江省[&]$$total$$[&]$$total$$', + '浙江省[&]杭州市[&]$$total$$', + '浙江省[&]杭州市[&]家具', + '浙江省[&]杭州市[&]办公用品', + '浙江省[&]舟山市[&]$$total$$', + '浙江省[&]舟山市[&]家具', + '浙江省[&]舟山市[&]办公用品', + ], + + subType: [ + '$$total$$[&]$$total$$[&]$$total$$[&]$$total$$', + '四川省[&]$$total$$[&]$$total$$[&]$$total$$', + '四川省[&]成[&]都[&]市[&]$$total$$[&]$$total$$', + '四川省[&]成[&]都[&]市[&]家具[&]$$total$$', + '四川省[&]成[&]都[&]市[&]家具[&]桌子', + '四川省[&]成[&]都[&]市[&]家具[&]沙发', + '四川省[&]成[&]都[&]市[&]办公用品[&]$$total$$', + '四川省[&]成[&]都[&]市[&]办公用品[&]笔', + '四川省[&]成[&]都[&]市[&]办公用品[&]纸张', + '四川省[&]绵阳市[&]$$total$$[&]$$total$$', + '四川省[&]绵阳市[&]家具[&]$$total$$', + '四川省[&]绵阳市[&]家具[&]桌子', + '四川省[&]绵阳市[&]家具[&]沙发', + '四川省[&]绵阳市[&]办公用品[&]$$total$$', + '四川省[&]绵阳市[&]办公用品[&]笔', + '四川省[&]绵阳市[&]办公用品[&]纸张', + '浙江省[&]$$total$$[&]$$total$$[&]$$total$$', + '浙江省[&]杭州市[&]$$total$$[&]$$total$$', + '浙江省[&]杭州市[&]家具[&]$$total$$', + '浙江省[&]杭州市[&]家具[&]桌子', + '浙江省[&]杭州市[&]家具[&]沙发', + '浙江省[&]杭州市[&]办公用品[&]$$total$$', + '浙江省[&]杭州市[&]办公用品[&]笔', + '浙江省[&]杭州市[&]办公用品[&]纸张', + '浙江省[&]舟山市[&]$$total$$[&]$$total$$', + '浙江省[&]舟山市[&]家具[&]$$total$$', + '浙江省[&]舟山市[&]家具[&]桌子', + '浙江省[&]舟山市[&]家具[&]沙发', + '浙江省[&]舟山市[&]办公用品[&]$$total$$', + '浙江省[&]舟山市[&]办公用品[&]笔', + '浙江省[&]舟山市[&]办公用品[&]纸张', + ], }; - const meta = new Map().set('浙江省', { - level: 0, - children: new Map().set('杭州市', childrenMeta), - childField: 'city', - }); - deleteMetaById(meta, 'root[&]浙江省'); - const result = meta.get('浙江省'); + pivotMeta = new Map([ + [ + '四川省', + { + childField: 'city', + level: 1, + id: '四川省', + dimensions: ['四川省'], + children: new Map([ + [ + '成[&]都[&]市', + { + childField: 'type', + level: 1, + id: '四川省[&]成[&]都[&]市', + dimensions: ['四川省', '成[&]都[&]市'], + children: new Map([ + [ + '家具', + { + childField: 'subType', + level: 1, + id: '四川省[&]成[&]都[&]市[&]家具', + dimensions: ['四川省', '成[&]都[&]市', '家具'], + children: new Map([ + [ + '桌子', + { + childFiled: null, + id: '四川省[&]成[&]都[&]市[&]家具[&]桌子', + dimensions: [ + '四川省', + '成[&]都[&]市', + '家具', + '桌子', + ], - expect(result.childField).toBeUndefined(); - expect(result.children).toBeEmpty(); + level: 1, + children: new Map(), + }, + ], + [ + '沙发', + { + childFiled: null, + level: 2, + id: '四川省[&]成[&]都[&]市[&]家具[&]沙发', + dimensions: [ + '四川省', + '成[&]都[&]市', + '家具', + '沙发', + ], + children: new Map(), + }, + ], + ]), + }, + ], + [ + '办公用品', + { + childField: 'subType', + level: 2, + id: '四川省[&]成[&]都[&]市[&]办公用品', + dimensions: ['四川省', '成[&]都[&]市', '办公用品'], + children: new Map([ + [ + '笔', + { + childFiled: null, + level: 1, + id: '四川省[&]成[&]都[&]市[&]办公用品[&]笔', + dimensions: [ + '四川省', + '成[&]都[&]市', + '办公用品', + '笔', + ], + children: new Map(), + }, + ], + [ + '纸张', + { + childFiled: null, + level: 2, + id: '四川省[&]成[&]都[&]市[&]办公用品[&]纸张', + dimensions: [ + '四川省', + '成[&]都[&]市', + '办公用品', + '纸张', + ], + children: new Map(), + }, + ], + ]), + }, + ], + ]), + }, + ], + [ + '绵阳市', + { + childField: 'type', + level: 2, + id: '四川省[&]绵阳市', + dimensions: ['四川省', '绵阳市'], + children: new Map([ + [ + '家具', + { + childField: 'subType', + level: 1, + id: '四川省[&]绵阳市[&]家具', + dimensions: ['四川省', '绵阳市', '家具'], + children: new Map([ + [ + '桌子', + { + childFiled: null, + level: 1, + id: '四川省[&]绵阳市[&]家具[&]桌子', + dimensions: ['四川省', '绵阳市', '家具', '桌子'], + children: new Map(), + }, + ], + [ + '沙发', + { + childFiled: null, + level: 2, + id: '四川省[&]绵阳市[&]家具[&]沙发', + dimensions: ['四川省', '绵阳市', '家具', '沙发'], + children: new Map(), + }, + ], + ]), + }, + ], + [ + '办公用品', + { + childField: 'subType', + level: 2, + id: '四川省[&]绵阳市[&]办公用品', + dimensions: ['四川省', '绵阳市', '办公用品'], + children: new Map([ + [ + '笔', + { + childFiled: null, + level: 1, + id: '四川省[&]绵阳市[&]办公用品[&]笔', + dimensions: ['四川省', '绵阳市', '办公用品', '笔'], + children: new Map(), + }, + ], + [ + '纸张', + { + childFiled: null, + level: 2, + id: '四川省[&]绵阳市[&]办公用品[&]纸张', + dimensions: [ + '四川省', + '绵阳市', + '办公用品', + '纸张', + ], + children: new Map(), + }, + ], + ]), + }, + ], + ]), + }, + ], + ]), + }, + ], + [ + '浙江省', + { + childField: 'city', + level: 2, + id: '浙江省', + dimensions: ['浙江省'], + children: new Map([ + [ + '杭州市', + { + childField: 'type', + level: 1, + id: '浙江省[&]杭州市', + dimensions: ['浙江省', '杭州市'], + children: new Map([ + [ + '家具', + { + childField: 'subType', + level: 1, + id: '浙江省[&]杭州市[&]家具', + dimensions: ['浙江省', '杭州市', '家具'], + children: new Map([ + [ + '桌子', + { + childFiled: null, + level: 1, + id: '浙江省[&]杭州市[&]家具[&]桌子', + dimensions: ['浙江省', '杭州市', '家具', '桌子'], + children: new Map(), + }, + ], + [ + '沙发', + { + childFiled: null, + level: 2, + id: '浙江省[&]杭州市[&]家具[&]沙发', + dimensions: ['浙江省', '杭州市', '家具', '沙发'], + children: new Map(), + }, + ], + ]), + }, + ], + [ + '办公用品', + { + childField: 'subType', + level: 2, + id: '浙江省[&]杭州市[&]办公用品', + dimensions: ['浙江省', '杭州市'], + children: new Map([ + [ + '笔', + { + childFiled: null, + level: 1, + id: '浙江省[&]杭州市[&]办公用品[&]笔', + dimensions: ['浙江省', '杭州市', '办公用品', '笔'], + children: new Map(), + }, + ], + [ + '纸张', + { + childFiled: null, + level: 2, + id: '浙江省[&]杭州市[&]办公用品[&]纸张', + dimensions: [ + '浙江省', + '杭州市', + '办公用品', + '纸张', + ], + children: new Map(), + }, + ], + ]), + }, + ], + ]), + }, + ], + [ + '舟山市', + { + childField: 'type', + level: 2, + id: '浙江省[&]舟山市', + dimensions: ['浙江省', '舟山市'], + children: new Map([ + [ + '家具', + { + childField: 'subType', + level: 1, + id: '浙江省[&]舟山市[&]家具', + dimensions: ['浙江省', '舟山市', '家具'], + children: new Map([ + [ + '桌子', + { + childFiled: null, + level: 1, + id: '浙江省[&]舟山市[&]家具[&]桌子', + dimensions: ['浙江省', '舟山市', '家具', '桌子'], + children: new Map(), + }, + ], + [ + '沙发', + { + childFiled: null, + level: 2, + id: '浙江省[&]舟山市[&]家具[&]沙发', + dimensions: ['浙江省', '舟山市', '家具', '沙发'], + children: new Map(), + }, + ], + ]), + }, + ], + [ + '办公用品', + { + childField: 'subType', + level: 2, + id: '浙江省[&]舟山市[&]办公用品', + dimensions: ['浙江省', '舟山市', '办公用品'], + children: new Map([ + [ + '笔', + { + childFiled: null, + level: 1, + id: '浙江省[&]舟山市[&]办公用品[&]笔', + dimensions: ['浙江省', '舟山市', '办公用品', '笔'], + children: new Map(), + }, + ], + [ + '纸张', + { + childFiled: null, + level: 2, + id: '浙江省[&]舟山市[&]办公用品[&]纸张', + dimensions: [ + '浙江省', + '舟山市', + '办公用品', + '纸张', + ], + children: new Map(), + }, + ], + ]), + }, + ], + ]), + }, + ], + ]), + }, + ], + ]) as unknown as PivotMeta; }); - test('for transformIndexesData function', () => { - const { rows, columns, values } = dataCfg.fields; - const sortedDimensionValues = {}; - const rowPivotMeta = new Map(); - const colPivotMeta = new Map(); - const result = transformIndexesData({ - rows, - values, - columns: columns as string[], - originData: dataCfg.data, - indexesData: [], - sortedDimensionValues, - rowPivotMeta, - colPivotMeta, - }); - - expect(result.indexesData).toHaveLength(3); - expect(result.paths).toHaveLength(32); - expect(get(result.indexesData, result.paths[0])).toEqual({ - city: '杭州市', - number: 7789, - province: '浙江省', - sub_type: '桌子', - type: '家具', - }); - expect(result.colPivotMeta?.has('家具')).toBeTrue(); - expect(result.rowPivotMeta?.has('浙江省')).toBeTrue(); + test(`should return false if doesn't exist total group`, () => { expect( - getDimensionsWithoutPathPre(result.sortedDimensionValues['province']), - ).toEqual(['浙江省', '四川省']); - }); + existDimensionTotalGroup(['家具', '纸张', MULTI_VALUE, MULTI_VALUE]), + ).toBeFalse(); - test('for transformDimensionsValues function', () => { - const rows = ['province', 'city']; - const data = { - city: '杭州市', - number: 7789, - province: '浙江省', - sub_type: '桌子', - type: '家具', - }; - const result = transformDimensionsValues(data, rows); + expect( + existDimensionTotalGroup([ + MULTI_VALUE, + MULTI_VALUE, + MULTI_VALUE, + MULTI_VALUE, + ]), + ).toBeFalse(); - expect(result).toEqual(['浙江省', '杭州市']); + expect( + existDimensionTotalGroup(['四川省', '成[&]都[&]市', '办公用品', '纸张']), + ).toBeFalse(); }); - test('for return type of transformDimensionsValues function', () => { - const rows = ['row0', 'row1']; - const data = { - row0: 0, - number: 7789, - row1: 1, - sub_type: '桌子', - type: '家具', - }; - const result = transformDimensionsValues(data, rows); + test('should return true if exist total group', () => { + expect( + existDimensionTotalGroup(['四川省', MULTI_VALUE, '家具', MULTI_VALUE]), + ).toBeTrue(); - expect(result).toEqual(['0', '1']); - }); + expect( + existDimensionTotalGroup([MULTI_VALUE, MULTI_VALUE, '家具', MULTI_VALUE]), + ).toBeTrue(); - test('for getDataPath function', () => { - const rowDimensionValues = ['浙江省', '杭州市']; - const colDimensionValues = ['家具', '桌子']; - const rows = ['province', 'city']; - const columns = ['type', 'sub_type']; - const values = ['value']; - const rowPivotMeta = new Map(); - const colPivotMeta = new Map(); - - const result = getDataPath({ - rowDimensionValues, - colDimensionValues, - rowPivotMeta, - colPivotMeta, - shouldCreateOrUpdate: true, - rows, - columns, - values, - }); - - expect(result).toEqual([1, 1, 1, 1]); + expect( + existDimensionTotalGroup([MULTI_VALUE, MULTI_VALUE, MULTI_VALUE, '纸张']), + ).toBeTrue(); }); - test('for getDataPath function when not createIfNotExist and without rows or columns', () => { - const rowDimensionValues = ['浙江省', '杭州市']; - const colDimensionValues = ['家具', '桌子']; - const rowPivotMeta = new Map(); - const colPivotMeta = new Map(); - - getDataPath({ - rowDimensionValues, - colDimensionValues, - rowPivotMeta, - colPivotMeta, - }); - expect(rowPivotMeta.size).toEqual(0); - expect(colPivotMeta.size).toEqual(0); - }); + test(`should return flatten dimension values if doesn't exist total group`, () => { + expect( + flattenDimensionValues({ + fields, + pivotMeta, + sortedDimensionValues, + dimensionValues: ['四川省', '成[&]都[&]市', '办公用品', '纸张'], + }), + ).toEqual([['四川省', '成[&]都[&]市', '办公用品', '纸张']]); - test('for getDataPath function when createIfNotExist and without rows or columns', () => { - const rowDimensionValues = ['浙江省', '杭州市']; - const colDimensionValues = ['家具', '桌子']; - const rowPivotMeta = new Map(); - const colPivotMeta = new Map(); - - getDataPath({ - rowDimensionValues, - colDimensionValues, - rowPivotMeta, - colPivotMeta, - shouldCreateOrUpdate: true, - }); - expect(rowPivotMeta.get(rowDimensionValues[0]).childField).toBeUndefined(); - expect(colPivotMeta.get(colDimensionValues[0]).childField).toBeUndefined(); + expect( + flattenDimensionValues({ + fields, + pivotMeta, + sortedDimensionValues, + dimensionValues: ['四川省', '成[&]都[&]市', MULTI_VALUE, MULTI_VALUE], + }), + ).toEqual([['四川省', '成[&]都[&]市', MULTI_VALUE, MULTI_VALUE]]); }); - test('for getDataPath function when createIfNotExist and with rows or columns', () => { - const rowDimensionValues = ['浙江省', '杭州市']; - const colDimensionValues = ['家具', '桌子']; - const rows = ['province', 'city']; - const columns = ['type', 'sub_type']; - const rowPivotMeta = new Map(); - const colPivotMeta = new Map(); - - getDataPath({ - rowDimensionValues, - colDimensionValues, - rowPivotMeta, - colPivotMeta, - shouldCreateOrUpdate: true, - rows, - columns, - }); - expect(rowPivotMeta.get(rowDimensionValues[0]).childField).toEqual('city'); - expect(colPivotMeta.get(colDimensionValues[0]).childField).toEqual( - 'sub_type', - ); - }); + test(`should return flatten dimension values if exist total group`, () => { + expect( + flattenDimensionValues({ + fields, + pivotMeta, + sortedDimensionValues, + dimensionValues: [MULTI_VALUE, '成[&]都[&]市', MULTI_VALUE, '纸张'], + }), + ).toMatchInlineSnapshot(` + Array [ + Array [ + "四川省", + "成[&]都[&]市", + "办公用品", + "纸张", + ], + ] + `); - test('for getDimensionsWithoutPathPre function', () => { - const dimensions = ['芜湖市[&]家具[&]椅子', '芜湖市[&]家具', '芜湖市']; + expect( + flattenDimensionValues({ + fields, + pivotMeta, + sortedDimensionValues, + dimensionValues: [ + MULTI_VALUE, + '成[&]都[&]市', + MULTI_VALUE, + MULTI_VALUE, + ], + }), + ).toMatchInlineSnapshot(` + Array [ + Array [ + "四川省", + "成[&]都[&]市", + "家具", + "桌子", + ], + Array [ + "四川省", + "成[&]都[&]市", + "家具", + "沙发", + ], + Array [ + "四川省", + "成[&]都[&]市", + "办公用品", + "笔", + ], + Array [ + "四川省", + "成[&]都[&]市", + "办公用品", + "纸张", + ], + ] + `); - expect(getDimensionsWithoutPathPre(dimensions)).toEqual([ - '椅子', - '家具', - '芜湖市', - ]); - }); + expect( + flattenDimensionValues({ + fields, + pivotMeta, + sortedDimensionValues, + dimensionValues: [MULTI_VALUE, MULTI_VALUE, '办公用品', MULTI_VALUE], + }), + ).toMatchInlineSnapshot(` + Array [ + Array [ + "四川省", + "成[&]都[&]市", + "办公用品", + "笔", + ], + Array [ + "四川省", + "成[&]都[&]市", + "办公用品", + "纸张", + ], + Array [ + "四川省", + "绵阳市", + "办公用品", + "笔", + ], + Array [ + "四川省", + "绵阳市", + "办公用品", + "纸张", + ], + Array [ + "浙江省", + "杭州市", + "办公用品", + "笔", + ], + Array [ + "浙江省", + "杭州市", + "办公用品", + "纸张", + ], + Array [ + "浙江省", + "舟山市", + "办公用品", + "笔", + ], + Array [ + "浙江省", + "舟山市", + "办公用品", + "纸张", + ], + ] + `); - test('for getDimensionsWithParentPath function', () => { - const field = 'city'; - const defaultDimensions = ['province', 'city']; - const dimensions = [ - new CellData( - { - province: '辽宁省', - city: '芜湖市', - category: '家具', - subCategory: '椅子', - price: '', - }, - 'price', - ), - ]; - const result = getDimensionsWithParentPath( - field, - defaultDimensions, - dimensions, - ); - - expect(result).toEqual(['辽宁省[&]芜湖市']); + expect( + flattenDimensionValues({ + fields, + pivotMeta, + sortedDimensionValues, + dimensionValues: ['四川省', MULTI_VALUE, '办公用品', MULTI_VALUE], + }), + ).toMatchInlineSnapshot(` + Array [ + Array [ + "四川省", + "成[&]都[&]市", + "办公用品", + "笔", + ], + Array [ + "四川省", + "成[&]都[&]市", + "办公用品", + "纸张", + ], + Array [ + "四川省", + "绵阳市", + "办公用品", + "笔", + ], + Array [ + "四川省", + "绵阳市", + "办公用品", + "纸张", + ], + ] + `); }); }); diff --git a/packages/s2-core/__tests__/unit/utils/export/__snapshots__/copy-spec.ts.snap b/packages/s2-core/__tests__/unit/utils/export/__snapshots__/copy-spec.ts.snap new file mode 100644 index 0000000000..c7c3058c62 --- /dev/null +++ b/packages/s2-core/__tests__/unit/utils/export/__snapshots__/copy-spec.ts.snap @@ -0,0 +1,162 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`List Table Core Data Process should copy all data 1`] = ` +"1 浙江省 杭州市 家具 ### 问题摘要 +- **会话地址**: 7789 +2 浙江省 绍兴市 家具 桌子 2367 +3 浙江省 宁波市 家具 桌子 3877 +4 浙江省 舟山市 家具 桌子 4342 +5 浙江省 杭州市 家具 沙发 5343 +6 浙江省 绍兴市 家具 沙发 632 +7 浙江省 宁波市 家具 沙发 7234 +8 浙江省 舟山市 家具 沙发 834 +9 浙江省 杭州市 办公用品 笔 945 +10 浙江省 绍兴市 办公用品 笔 1304 +11 浙江省 宁波市 办公用品 笔 1145 +12 浙江省 舟山市 办公用品 笔 1432 +13 浙江省 杭州市 办公用品 纸张 1343 +14 浙江省 绍兴市 办公用品 纸张 1354 +15 浙江省 宁波市 办公用品 纸张 1523 +16 浙江省 舟山市 办公用品 纸张 1634 +17 四川省 成都市 家具 桌子 1723 +18 四川省 绵阳市 家具 桌子 1822 +19 四川省 南充市 家具 桌子 1943 +20 四川省 乐山市 家具 桌子 2330 +21 四川省 成都市 家具 沙发 2451 +22 四川省 绵阳市 家具 沙发 2244 +23 四川省 南充市 家具 沙发 2333 +24 四川省 乐山市 家具 沙发 2445 +25 四川省 成都市 办公用品 笔 2335 +26 四川省 绵阳市 办公用品 笔 245 +27 四川省 南充市 办公用品 笔 2457 +28 四川省 乐山市 办公用品 笔 2458 +29 四川省 成都市 办公用品 纸张 4004 +30 四川省 绵阳市 办公用品 纸张 3077 +31 四川省 南充市 办公用品 纸张 3551 +32 四川省 乐山市 办公用品 纸张 352" +`; + +exports[`List Table Core Data Process should copy correct data when selected diagonal cells 1`] = ` +"浙江省 + + + + + + + + 宁波市" +`; + +exports[`Pivot Table getBrushHeaderCopyable should copy all col data in grid mode for custom field meta 1`] = ` +Array [ + Object { + "content": "家具 家具 办公用品 办公用品 +桌子 沙发 笔 纸张 +数量 数量 数量 数量", + "type": "text/plain", + }, + Object { + "content": "
家具家具办公用品办公用品
桌子沙发纸张
数量数量数量数量
", + "type": "text/html", + }, +] +`; + +exports[`Pivot Table getBrushHeaderCopyable should copy all original row data in grid mode if contains text ellipses 1`] = ` +Array [ + Object { + "content": "浙江省 杭州市 +浙江省 绍兴市 +浙江省 宁波市 +浙江省 舟山市 +四川省 成都市 +四川省 绵阳市 +四川省 南充市 +四川省 乐山市", + "type": "text/plain", + }, + Object { + "content": "
浙江省杭州市
浙江省绍兴市
浙江省宁波市
浙江省舟山市
四川省成都市
四川省绵阳市
四川省南充市
四川省乐山市
", + "type": "text/html", + }, +] +`; + +exports[`Pivot Table getBrushHeaderCopyable should copy all row data in grid mode with formatter 1`] = ` +"浙江省 杭州市 数值 +浙江省 绍兴市 数值 +浙江省 宁波市 数值 +浙江省 舟山市 数值 +四川省 成都市 数值 +四川省 绵阳市 数值 +四川省 南充市 数值 +四川省 乐山市 数值" +`; + +exports[`Pivot Table getBrushHeaderCopyable should copy col total data in grid mode 1`] = ` +Array [ + Object { + "content": "家具 家具 家具 办公用品 办公用品 +桌子 沙发 小计 笔 纸张 +number number 小计 number number", + "type": "text/plain", + }, + Object { + "content": "
家具家具家具办公用品办公用品
桌子沙发小计纸张
numbernumber小计numbernumber
", + "type": "text/html", + }, +] +`; + +exports[`Tree Table Core Data Process should copy normal data with header for custom field formatter if enable copyWithFormat 1`] = ` +Array [ + Object { + "content": " 家具 家具 家具 办公用品 办公用品 + 桌子 沙发 小计 笔 纸张 + 数量 数量 数量 数量 +浙江省 18375-@ 14043-@ 32418-@ 4826-@ 5854-@ +浙江省 杭州市 7789-@ 5343-@ 13132-@ 945-@ 1343-@ +浙江省 绍兴市 2367-@ 632-@ 2999-@ 1304-@ 1354-@ +浙江省 宁波市 3877-@ 7234-@ 11111-@ 1145-@ 1523-@ +浙江省 舟山市 4342-@ 834-@ 5176-@ 1432-@ 1634-@ +四川省 7818-@ 9473-@ 17291-@ 7495-@ 10984-@ +四川省 成都市 1723-@ 2451-@ 4174-@ 2335-@ 4004-@ +四川省 绵阳市 1822-@ 2244-@ 4066-@ 245-@ 3077-@ +四川省 南充市 1943-@ 2333-@ 4276-@ 2457-@ 3551-@ +四川省 乐山市 2330-@ 2445-@ 4775-@ 2458-@ 352-@ +总计 26193-@ 23516-@ 49709-@ 12321-@ 16838-@", + "type": "text/plain", + }, + Object { + "content": "
家具家具家具办公用品办公用品
桌子沙发小计纸张
数量数量数量数量
浙江省18375-@14043-@32418-@4826-@5854-@
浙江省杭州市7789-@5343-@13132-@945-@1343-@
浙江省绍兴市2367-@632-@2999-@1304-@1354-@
浙江省宁波市3877-@7234-@11111-@1145-@1523-@
浙江省舟山市4342-@834-@5176-@1432-@1634-@
四川省7818-@9473-@17291-@7495-@10984-@
四川省成都市1723-@2451-@4174-@2335-@4004-@
四川省绵阳市1822-@2244-@4066-@245-@3077-@
四川省南充市1943-@2333-@4276-@2457-@3551-@
四川省乐山市2330-@2445-@4775-@2458-@352-@
总计26193-@23516-@49709-@12321-@16838-@
", + "type": "text/html", + }, +] +`; + +exports[`Tree Table Core Data Process should copy normal data with header for custom field name 1`] = ` +Array [ + Object { + "content": " 家具 家具 家具 办公用品 办公用品 + 桌子 沙发 小计 笔 纸张 + 数量 数量 数量 数量 +浙江省 18375 14043 32418 4826 5854 +浙江省 杭州市 7789 5343 13132 945 1343 +浙江省 绍兴市 2367 632 2999 1304 1354 +浙江省 宁波市 3877 7234 11111 1145 1523 +浙江省 舟山市 4342 834 5176 1432 1634 +四川省 7818 9473 17291 7495 10984 +四川省 成都市 1723 2451 4174 2335 4004 +四川省 绵阳市 1822 2244 4066 245 3077 +四川省 南充市 1943 2333 4276 2457 3551 +四川省 乐山市 2330 2445 4775 2458 352 +总计 26193 23516 49709 12321 16838", + "type": "text/plain", + }, + Object { + "content": "
家具家具家具办公用品办公用品
桌子沙发小计纸张
数量数量数量数量
浙江省18375140433241848265854
浙江省杭州市77895343131329451343
浙江省绍兴市2367632299913041354
浙江省宁波市387772341111111451523
浙江省舟山市4342834517614321634
四川省7818947317291749510984
四川省成都市17232451417423354004
四川省绵阳市1822224440662453077
四川省南充市19432333427624573551
四川省乐山市2330244547752458352
总计2619323516497091232116838
", + "type": "text/html", + }, +] +`; diff --git a/packages/s2-core/__tests__/unit/utils/export/__snapshots__/export-pivot-spec.ts.snap b/packages/s2-core/__tests__/unit/utils/export/__snapshots__/export-pivot-spec.ts.snap new file mode 100644 index 0000000000..7bbda655b9 --- /dev/null +++ b/packages/s2-core/__tests__/unit/utils/export/__snapshots__/export-pivot-spec.ts.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PivotSheet Export Test should export correct data when data is incomplete 1`] = ` +" province 浙江省 浙江省 浙江省 浙江省 四川省 四川省 四川省 四川省 + city 杭州市 绍兴市 宁波市 舟山市 成都市 绵阳市 南充市 乐山市 +type sub_type number number number number number number number number +家具 +家具 桌子 2367 3877 4342 1723 1822 1943 2330 +家具 沙发 632 7234 834 2451 2244 2333 2445 +办公用品 +办公用品 笔 +办公用品 纸张 1354 1523 1634 4004 3077 3551 352" +`; diff --git a/packages/s2-core/__tests__/unit/utils/export/__snapshots__/export-spec.ts.snap b/packages/s2-core/__tests__/unit/utils/export/__snapshots__/export-spec.ts.snap new file mode 100644 index 0000000000..d4c258e782 --- /dev/null +++ b/packages/s2-core/__tests__/unit/utils/export/__snapshots__/export-spec.ts.snap @@ -0,0 +1,360 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PivotSheet Export Test should export correct $$extra$$ field name 1`] = ` +Array [ + "type", + "sub_type", + "数值", + "数值", + "数值", + "数值", + "数值", + "数值", + "数值", + "数值 +", +] +`; + +exports[`TableSheet Export Test should export correct data with no series number 1`] = ` +Array [ + "province city type sub_type number +", + "浙江省 杭州市 家具 桌子 7789 +", + "浙江省 绍兴市 家具 桌子 2367 +", + "浙江省 宁波市 家具 桌子 3877 +", + "浙江省 舟山市 家具 桌子 4342 +", + "浙江省 杭州市 家具 沙发 5343 +", + "浙江省 绍兴市 家具 沙发 632 +", + "浙江省 宁波市 家具 沙发 7234 +", + "浙江省 舟山市 家具 沙发 834 +", + "浙江省 杭州市 办公用品 笔 945 +", + "浙江省 绍兴市 办公用品 笔 1304 +", + "浙江省 宁波市 办公用品 笔 1145 +", + "浙江省 舟山市 办公用品 笔 1432 +", + "浙江省 杭州市 办公用品 纸张 1343 +", + "浙江省 绍兴市 办公用品 纸张 1354 +", + "浙江省 宁波市 办公用品 纸张 1523 +", + "浙江省 舟山市 办公用品 纸张 1634 +", + "四川省 成都市 家具 桌子 1723 +", + "四川省 绵阳市 家具 桌子 1822 +", + "四川省 南充市 家具 桌子 1943 +", + "四川省 乐山市 家具 桌子 2330 +", + "四川省 成都市 家具 沙发 2451 +", + "四川省 绵阳市 家具 沙发 2244 +", + "四川省 南充市 家具 沙发 2333 +", + "四川省 乐山市 家具 沙发 2445 +", + "四川省 成都市 办公用品 笔 2335 +", + "四川省 绵阳市 办公用品 笔 245 +", + "四川省 南充市 办公用品 笔 2457 +", + "四川省 乐山市 办公用品 笔 2458 +", + "四川省 成都市 办公用品 纸张 4004 +", + "四川省 绵阳市 办公用品 纸张 3077 +", + "四川省 南充市 办公用品 纸张 3551 +", + "四川省 乐山市 办公用品 纸张 352 +", + " 家具 桌子 26193 +", + " 家具 49709 +", + " 家具 沙发 23516 +", + " 办公用品 29159 +", + " 办公用品 笔 12321 +", + " 办公用品 纸张 16838 +", + "浙江省 家具 桌子 18375 +", + "浙江省 家具 沙发 14043 +", + "浙江省 办公用品 笔 4826 +", + "浙江省 办公用品 纸张 5854 +", + "四川省 家具 桌子 7818 +", + "四川省 家具 沙发 9473 +", + "四川省 办公用品 笔 7495 +", + "四川省 办公用品 纸张 10984 +", + "浙江省 杭州市 家具 13132 +", + "浙江省 杭州市 办公用品 2288 +", + "浙江省 杭州市 15420 +", + "浙江省 绍兴市 家具 2999 +", + "浙江省 绍兴市 办公用品 2658 +", + "浙江省 绍兴市 5657 +", + "浙江省 宁波市 家具 11111 +", + "浙江省 宁波市 办公用品 2668 +", + "浙江省 宁波市 13779 +", + "浙江省 舟山市 家具 5176 +", + "浙江省 舟山市 办公用品 3066 +", + "浙江省 舟山市 8242 +", + "四川省 成都市 家具 4174 +", + "四川省 成都市 办公用品 6339 +", + "四川省 成都市 10513 +", + "四川省 绵阳市 家具 4066 +", + "四川省 绵阳市 办公用品 3322 +", + "四川省 绵阳市 7388 +", + "四川省 南充市 家具 4276 +", + "四川省 南充市 办公用品 6008 +", + "四川省 南充市 10284 +", + "四川省 乐山市 家具 4775 +", + "四川省 乐山市 办公用品 2810 +", + "四川省 乐山市 7585 +", + "浙江省 家具 32418 +", + "浙江省 办公用品 10680 +", + "浙江省 43098 +", + "四川省 家具 17291 +", + "四川省 办公用品 18479 +", + "四川省 35770 +", + " 78868", +] +`; + +exports[`TableSheet Export Test should export correct data with no series number 2`] = ` +Array [ + "province", + "city", + "type", + "sub_type", + "number +", +] +`; + +exports[`TableSheet Export Test should export correct data with series number 1`] = ` +Array [ + "序号 province city 产品类型 sub_type number +", + "1 浙江省 杭州市 家具 桌子 7789 +", + "2 浙江省 绍兴市 家具 桌子 2367 +", + "3 浙江省 宁波市 家具 桌子 3877 +", + "4 浙江省 舟山市 家具 桌子 4342 +", + "5 浙江省 杭州市 家具 沙发 5343 +", + "6 浙江省 绍兴市 家具 沙发 632 +", + "7 浙江省 宁波市 家具 沙发 7234 +", + "8 浙江省 舟山市 家具 沙发 834 +", + "9 浙江省 杭州市 办公用品 笔 945 +", + "10 浙江省 绍兴市 办公用品 笔 1304 +", + "11 浙江省 宁波市 办公用品 笔 1145 +", + "12 浙江省 舟山市 办公用品 笔 1432 +", + "13 浙江省 杭州市 办公用品 纸张 1343 +", + "14 浙江省 绍兴市 办公用品 纸张 1354 +", + "15 浙江省 宁波市 办公用品 纸张 1523 +", + "16 浙江省 舟山市 办公用品 纸张 1634 +", + "17 四川省 成都市 家具 桌子 1723 +", + "18 四川省 绵阳市 家具 桌子 1822 +", + "19 四川省 南充市 家具 桌子 1943 +", + "20 四川省 乐山市 家具 桌子 2330 +", + "21 四川省 成都市 家具 沙发 2451 +", + "22 四川省 绵阳市 家具 沙发 2244 +", + "23 四川省 南充市 家具 沙发 2333 +", + "24 四川省 乐山市 家具 沙发 2445 +", + "25 四川省 成都市 办公用品 笔 2335 +", + "26 四川省 绵阳市 办公用品 笔 245 +", + "27 四川省 南充市 办公用品 笔 2457 +", + "28 四川省 乐山市 办公用品 笔 2458 +", + "29 四川省 成都市 办公用品 纸张 4004 +", + "30 四川省 绵阳市 办公用品 纸张 3077 +", + "31 四川省 南充市 办公用品 纸张 3551 +", + "32 四川省 乐山市 办公用品 纸张 352 +", + "33 家具 桌子 26193 +", + "34 家具 49709 +", + "35 家具 沙发 23516 +", + "36 办公用品 29159 +", + "37 办公用品 笔 12321 +", + "38 办公用品 纸张 16838 +", + "39 浙江省 家具 桌子 18375 +", + "40 浙江省 家具 沙发 14043 +", + "41 浙江省 办公用品 笔 4826 +", + "42 浙江省 办公用品 纸张 5854 +", + "43 四川省 家具 桌子 7818 +", + "44 四川省 家具 沙发 9473 +", + "45 四川省 办公用品 笔 7495 +", + "46 四川省 办公用品 纸张 10984 +", + "47 浙江省 杭州市 家具 13132 +", + "48 浙江省 杭州市 办公用品 2288 +", + "49 浙江省 杭州市 15420 +", + "50 浙江省 绍兴市 家具 2999 +", + "51 浙江省 绍兴市 办公用品 2658 +", + "52 浙江省 绍兴市 5657 +", + "53 浙江省 宁波市 家具 11111 +", + "54 浙江省 宁波市 办公用品 2668 +", + "55 浙江省 宁波市 13779 +", + "56 浙江省 舟山市 家具 5176 +", + "57 浙江省 舟山市 办公用品 3066 +", + "58 浙江省 舟山市 8242 +", + "59 四川省 成都市 家具 4174 +", + "60 四川省 成都市 办公用品 6339 +", + "61 四川省 成都市 10513 +", + "62 四川省 绵阳市 家具 4066 +", + "63 四川省 绵阳市 办公用品 3322 +", + "64 四川省 绵阳市 7388 +", + "65 四川省 南充市 家具 4276 +", + "66 四川省 南充市 办公用品 6008 +", + "67 四川省 南充市 10284 +", + "68 四川省 乐山市 家具 4775 +", + "69 四川省 乐山市 办公用品 2810 +", + "70 四川省 乐山市 7585 +", + "71 浙江省 家具 32418 +", + "72 浙江省 办公用品 10680 +", + "73 浙江省 43098 +", + "74 四川省 家具 17291 +", + "75 四川省 办公用品 18479 +", + "76 四川省 35770 +", + "77 78868", +] +`; + +exports[`TableSheet Export Test should export correct data with series number 2`] = ` +Array [ + "序号", + "province", + "city", + "产品类型", + "sub_type", + "number +", +] +`; diff --git a/packages/s2-core/__tests__/unit/utils/export/copy-spec.ts b/packages/s2-core/__tests__/unit/utils/export/copy-spec.ts index 4687639bf2..086fa5ec1f 100644 --- a/packages/s2-core/__tests__/unit/utils/export/copy-spec.ts +++ b/packages/s2-core/__tests__/unit/utils/export/copy-spec.ts @@ -1,24 +1,24 @@ import { map } from 'lodash'; import { data as originalData, totalData } from 'tests/data/mock-dataset.json'; import { + TOTALS_OPTIONS, assembleDataCfg, assembleOptions, - TOTALS_OPTIONS, waitForRender, } from 'tests/util'; import { getContainer } from 'tests/util/helpers'; -import type { Meta, S2DataConfig } from '@/common/interface'; -import { Aggregation } from '@/common/interface'; -import { TableDataCell, TableSeriesNumberCell } from '@/cell'; +import type { S2DataConfig } from '../../../../src/common'; +import { TableSeriesNumberCell } from '@/cell'; import { NewLine, NewTab, S2Event } from '@/common/constant'; import { - CellType, InteractionStateName, SortMethodType, } from '@/common/constant/interaction'; -import { PivotSheet, TableSheet } from '@/sheet-type'; +import type { Meta } from '@/common/interface'; +import { Aggregation } from '@/common/interface'; +import { PivotSheet, SpreadSheet, TableSheet } from '@/sheet-type'; import { getSelectedData } from '@/utils/export/copy'; -import { CopyMIMEType } from '@/utils/export/interface'; +import { CopyMIMEType } from '@/common/interface/export'; import { convertString } from '@/utils/export/method'; import { getCellMeta } from '@/utils/interaction/select-event'; @@ -32,7 +32,7 @@ const testData = originalData.map((item, i) => { return { ...item }; }); -const getCopyPlainContent = (sheet: TableSheet | PivotSheet): string => { +const getCopyPlainContent = (sheet: SpreadSheet): string => { const data = getSelectedData(sheet); return data[0].content; @@ -53,8 +53,14 @@ describe('List Table Core Data Process', () => { }), assembleOptions({ showSeriesNumber: true, + interaction: { + selectedCellHighlight: { + currentRow: true, + }, + }, }), ); + await s2.render(); }); @@ -74,7 +80,7 @@ describe('List Table Core Data Process', () => { it('should copy normal data', () => { const cell = s2.facet .getDataCells() - .find((cell) => !(cell instanceof TableSeriesNumberCell))!; + .find((cell) => cell.getMeta().valueField === 'province')!; s2.interaction.changeState({ cells: [getCellMeta(cell)], @@ -94,14 +100,13 @@ describe('List Table Core Data Process', () => { }); it('should copy row data', () => { - const cell = s2.facet - .getCells() - .filter((cell) => cell instanceof TableSeriesNumberCell)[3]; + const cell = s2.facet.getSeriesNumberCells()[3]; s2.interaction.changeState({ cells: [getCellMeta(cell)], stateName: InteractionStateName.SELECTED, }); + expect(getCopyPlainContent(s2)).toMatchInlineSnapshot( `"1 浙江省 舟山市 家具 桌子 4342"`, ); @@ -113,6 +118,7 @@ describe('List Table Core Data Process', () => { stateName: InteractionStateName.ALL_SELECTED, }); + expect(getCopyPlainContent(s2)).toMatchSnapshot(); expect(getCopyPlainContent(s2).split(NewLine).length).toBe(33); expect(getCopyPlainContent(s2).split(NewLine)[2]).toMatchInlineSnapshot( `"2 浙江省 绍兴市 家具 桌子 2367"`, @@ -130,12 +136,13 @@ describe('List Table Core Data Process', () => { const cell = s2.facet .getDataCells() - .find((cell) => !(cell instanceof TableSeriesNumberCell))!; + .find((cell) => cell.getMeta().valueField === 'province')!; s2.interaction.changeState({ cells: [getCellMeta(cell)], stateName: InteractionStateName.SELECTED, }); + expect(getCopyPlainContent(s2)).toEqual('province\r\n浙江省'); }); @@ -159,53 +166,46 @@ describe('List Table Core Data Process', () => { await sheet.render(); const cell = s2.facet - .getCells() - .filter( - (cell) => - cell.cellType === CellType.DATA_CELL && - !(cell instanceof TableSeriesNumberCell), - )[0]; + .getDataCells() + .find((cell) => cell.getMeta().valueField === 'province')!; sheet.interaction.changeState({ cells: [getCellMeta(cell)], stateName: InteractionStateName.SELECTED, }); + expect(getCopyPlainContent(sheet)).toEqual('浙江省元'); }); // https://github.com/antvis/S2/issues/1770 it('should copy format data with row header selected', async () => { - const sheet = new TableSheet( - getContainer(), - assembleDataCfg({ - meta: [{ field: 'province', formatter: (v) => `${v}_formatted` }], - fields: { - columns: ['province', 'city', 'type', 'sub_type', 'number'], - }, - }), - assembleOptions({ - interaction: { - enableCopy: true, - copyWithFormat: true, - }, - showSeriesNumber: false, - }), - ); + s2.setDataCfg({ + meta: [{ field: 'province', formatter: (v) => `${v}_formatted` }], + fields: { + columns: ['province', 'city', 'type', 'sub_type', 'number'], + }, + }); - await sheet.render(); + s2.setOptions({ + interaction: { + enableCopy: true, + copyWithFormat: true, + }, + showSeriesNumber: false, + }); - const cell = sheet.facet - .getCells() - .filter((cell) => cell instanceof TableDataCell)[0]; + await s2.render(); - sheet.interaction.changeState({ - cells: [getCellMeta(cell)], + const dataCell = s2.facet.getDataCells()[0]; + + s2.interaction.changeState({ + cells: [getCellMeta(dataCell)], stateName: InteractionStateName.SELECTED, }); - const data = getCopyPlainContent(sheet); + const data = getCopyPlainContent(s2); - expect(data).toBe('浙江省_formatted'); + expect(data).toEqual('浙江省_formatted'); }); // https://github.com/antvis/S2/issues/1770 @@ -231,7 +231,7 @@ describe('List Table Core Data Process', () => { const cell = sheet.facet.getColCells()[1]; - await sheet.interaction.changeState({ + sheet.interaction.changeState({ cells: [getCellMeta(cell)], stateName: InteractionStateName.SELECTED, }); @@ -253,17 +253,7 @@ describe('List Table Core Data Process', () => { const dataContent = getCopyPlainContent(s2); - expect(dataContent).toMatchInlineSnapshot(` - "浙江省 - - - - - - - - 宁波市" - `); + expect(dataContent).toMatchSnapshot(); }); it('should copy correct data with data filtered', async () => { @@ -281,9 +271,7 @@ describe('List Table Core Data Process', () => { }); }); - const cell = s2.facet - .getCells() - .filter((cell) => cell instanceof TableSeriesNumberCell)[3]; + const cell = s2.facet.getSeriesNumberCells()[3]; s2.interaction.changeState({ cells: [getCellMeta(cell)], @@ -296,7 +284,9 @@ describe('List Table Core Data Process', () => { s2.interaction.changeState({ stateName: InteractionStateName.ALL_SELECTED, }); + expect(getCopyPlainContent(s2).split('\n').length).toEqual(16); + // clear filter condition s2.emit(S2Event.RANGE_FILTER, { filterKey: 'province', @@ -314,9 +304,7 @@ describe('List Table Core Data Process', () => { ]); }); - const cell = s2.facet - .getCells() - .filter((cell) => cell instanceof TableSeriesNumberCell)[1]; + const cell = s2.facet.getSeriesNumberCells()[1]; s2.interaction.changeState({ cells: [getCellMeta(cell)], @@ -325,13 +313,15 @@ describe('List Table Core Data Process', () => { const data = getCopyPlainContent(s2); expect(data).toBe('1 浙江省 宁波市 家具 沙发 7234'); + s2.interaction.changeState({ stateName: InteractionStateName.ALL_SELECTED, }); + expect(getCopyPlainContent(s2).split('\n').length).toEqual(33); }); - it('should copy correct data with \n data', async () => { + it('should copy correct data with "\n" data', async () => { const newLineText = `1 2`; const sheet = new TableSheet( @@ -356,12 +346,8 @@ describe('List Table Core Data Process', () => { await sheet.render(); const cell = sheet.facet - .getCells() - .filter( - (cell) => - cell.cellType === CellType.DATA_CELL && - !(cell instanceof TableSeriesNumberCell), - )[20]; + .getDataCells() + .filter((cell) => !(cell instanceof TableSeriesNumberCell))[20]; sheet.interaction.changeState({ cells: [getCellMeta(cell)], @@ -409,6 +395,7 @@ describe('List Table Core Data Process', () => { it('should copy row data when select data row cell', async () => { s2.setOptions({ + showSeriesNumber: false, interaction: { selectedCellHighlight: { currentRow: true, @@ -425,15 +412,13 @@ describe('List Table Core Data Process', () => { stateName: InteractionStateName.SELECTED, }); - expect(getCopyPlainContent(s2)).toMatchInlineSnapshot(` - "1 浙江省 杭州市 家具 ### 问题摘要 - - **会话地址**: 7789" - `); - expect(getCopyPlainContent(s2).split(NewTab).length).toBe(6); + expect(getCopyPlainContent(s2)).toEqual('浙江省'); + expect(getCopyPlainContent(s2).split(NewTab).length).toBe(1); }); it('should support custom copy matrix transformer', async () => { s2.setOptions({ + showSeriesNumber: false, interaction: { customTransformer: () => { return { @@ -1348,9 +1333,55 @@ describe('Tree Table Core Data Process', () => { 总计 26193 23516 49709 12321 16838" `); }); + + it('should copy normal data with header for custom field name', async () => { + s2.setOptions({ + interaction: { + copyWithHeader: true, + }, + }); + s2.setDataCfg({ + meta: [ + { + field: 'number', + name: '数量', + }, + ], + }); + await s2.render(); + + setSelectedVisibleCell(); + + expect(getSelectedData(s2)).toMatchSnapshot(); + }); + + it('should copy normal data with header for custom field formatter if enable copyWithFormat', async () => { + s2.setOptions({ + interaction: { + copyWithHeader: true, + copyWithFormat: true, + }, + }); + s2.setDataCfg({ + meta: [ + { + field: 'number', + name: '数量', + formatter: (value) => `${value}-@`, + }, + ], + }); + await s2.render(); + + setSelectedVisibleCell(); + + expect(getSelectedData(s2)).toMatchSnapshot(); + }); }); describe('Pivot Table getBrushHeaderCopyable', () => { + let s2: SpreadSheet; + const dataCfg = assembleDataCfg({ meta: [], fields: { @@ -1359,6 +1390,7 @@ describe('Pivot Table getBrushHeaderCopyable', () => { values: ['number'], }, }); + const options = assembleOptions({ hierarchyType: 'grid', interaction: { @@ -1367,9 +1399,8 @@ describe('Pivot Table getBrushHeaderCopyable', () => { }, }); - const s2 = new PivotSheet(getContainer(), dataCfg, options); - beforeEach(async () => { + s2 = new PivotSheet(getContainer(), dataCfg, options); await s2.render(); }); @@ -1396,6 +1427,56 @@ describe('Pivot Table getBrushHeaderCopyable', () => { `); }); + test('should copy all row data in grid mode with formatter', async () => { + s2.setDataCfg({ + fields: { + valueInCols: false, + }, + meta: [ + { + field: 'number', + name: '数值', + }, + ], + }); + await s2.render(); + + const cells = s2.facet.getRowCells(); + + s2.interaction.changeState({ + cells: map(cells, getCellMeta), + stateName: InteractionStateName.SELECTED, + onUpdateCells: (root) => { + root.updateCells(cells); + }, + }); + + expect(getCopyPlainContent(s2)).toMatchSnapshot(); + }); + + test('should copy all original row data in grid mode if contains text ellipses', () => { + s2.setOptions({ + style: { + rowCell: { + // 展示省略号 + width: 10, + }, + }, + }); + + const cells = s2.facet.getRowCells(); + + s2.interaction.changeState({ + cells: map(cells, getCellMeta), + stateName: InteractionStateName.SELECTED, + onUpdateCells: (root) => { + root.updateCells(s2.facet.getRowCells()); + }, + }); + + expect(getSelectedData(s2)).toMatchSnapshot(); + }); + test('should copy all col data in grid mode', () => { const cells = s2.facet.getColCells(); @@ -1406,8 +1487,8 @@ describe('Pivot Table getBrushHeaderCopyable', () => { root.updateCells(cells); }, }); - // 列头高度 + // 列头高度 expect(getCopyPlainContent(s2)).toMatchInlineSnapshot(` "家具 家具 办公用品 办公用品 桌子 沙发 笔 纸张 @@ -1415,6 +1496,59 @@ describe('Pivot Table getBrushHeaderCopyable', () => { `); }); + test('should copy all col data in grid mode with formatter', async () => { + s2.setDataCfg({ + meta: [ + { + field: 'number', + name: '数值', + }, + ], + }); + await s2.render(); + + const cells = s2.facet.getColCells(); + + s2.interaction.changeState({ + cells: map(cells, getCellMeta), + stateName: InteractionStateName.SELECTED, + onUpdateCells: (root) => { + root.updateCells(cells); + }, + }); + + expect(getCopyPlainContent(s2)).toMatchInlineSnapshot(` + "家具 家具 办公用品 办公用品 + 桌子 沙发 笔 纸张 + 数值 数值 数值 数值" + `); + }); + + test('should copy all col data in grid mode for custom field meta', async () => { + s2.setDataCfg({ + meta: [ + { + field: 'number', + name: '数量', + }, + ], + }); + + await s2.render(); + + const cells = s2.facet.getColCells(); + + s2.interaction.changeState({ + cells: map(cells, getCellMeta), + stateName: InteractionStateName.SELECTED, + onUpdateCells: (root) => { + root.updateCells(cells); + }, + }); + + expect(getSelectedData(s2)).toMatchSnapshot(); + }); + test('should copy selection row data in grid mode', async () => { const sheet = new PivotSheet( getContainer(), @@ -1523,6 +1657,7 @@ describe('Pivot Table getBrushHeaderCopyable', () => { root.updateCells(sheet.facet.getColCells()); }, }); + expect(getCopyPlainContent(sheet)).toMatchInlineSnapshot(` "浙江省 浙江省 杭州市 绍兴市" @@ -1598,14 +1733,6 @@ describe('Pivot Table getBrushHeaderCopyable', () => { const copyableList = getSelectedData(sheet); - expect(copyableList[0].content).toMatchInlineSnapshot(` - "家具 家具 家具 办公用品 办公用品 - 桌子 沙发 小计 笔 纸张 - number number 小计 number number" - `); - - expect(copyableList[1].content).toMatchInlineSnapshot( - `"
家具家具家具办公用品办公用品
桌子沙发小计纸张
numbernumber小计numbernumber
"`, - ); + expect(copyableList).toMatchSnapshot(); }); }); diff --git a/packages/s2-core/__tests__/unit/utils/export/export-pivot-spec.ts b/packages/s2-core/__tests__/unit/utils/export/export-pivot-spec.ts index 969537bb3c..2a46ca4d72 100644 --- a/packages/s2-core/__tests__/unit/utils/export/export-pivot-spec.ts +++ b/packages/s2-core/__tests__/unit/utils/export/export-pivot-spec.ts @@ -1,15 +1,9 @@ -import { - asyncGetAllPlainData, - copyData, - NewLine, - NewTab, - PivotSheet, -} from '@antv/s2'; -import { getContainer } from 'tests/util/helpers'; -import { assembleDataCfg, assembleOptions } from 'tests/util'; +import { NewLine, NewTab, PivotSheet, asyncGetAllPlainData } from '@antv/s2'; import { map, omit } from 'lodash'; import { data as originData } from 'tests/data/mock-dataset.json'; -import { CopyMIMEType } from '@/utils/export/interface'; +import { assembleDataCfg, assembleOptions } from 'tests/util'; +import { getContainer } from 'tests/util/helpers'; +import { CopyMIMEType } from '@/common/interface/export'; describe('PivotSheet Export Test', () => { let pivotSheet: PivotSheet; @@ -50,7 +44,7 @@ describe('PivotSheet Export Test', () => { } await s2.render(); - const data = copyData({ + const data = await asyncGetAllPlainData({ sheetInstance: s2, split: NewTab, }); @@ -93,7 +87,7 @@ describe('PivotSheet Export Test', () => { } await s2.render(); - const data = copyData({ + const data = await asyncGetAllPlainData({ sheetInstance: s2, }); @@ -131,7 +125,7 @@ describe('PivotSheet Export Test', () => { ); await s2.render(); - const data = copyData({ + const data = await asyncGetAllPlainData({ sheetInstance: s2, split: NewTab, formatOptions: true, @@ -171,7 +165,7 @@ describe('PivotSheet Export Test', () => { ); await s2.render(); - const data = copyData({ + const data = await asyncGetAllPlainData({ sheetInstance: s2, split: NewTab, formatOptions: true, @@ -218,7 +212,7 @@ describe('PivotSheet Export Test', () => { ); await s2.render(); - const data = copyData({ + const data = await asyncGetAllPlainData({ sheetInstance: s2, split: NewTab, }); @@ -258,7 +252,7 @@ describe('PivotSheet Export Test', () => { ); await s2.render(); - const data = copyData({ + const data = await asyncGetAllPlainData({ sheetInstance: s2, split: NewTab, }); @@ -298,7 +292,7 @@ describe('PivotSheet Export Test', () => { ); await s2.render(); - const data = copyData({ + const data = await asyncGetAllPlainData({ sheetInstance: s2, split: NewTab, }); @@ -341,7 +335,7 @@ describe('PivotSheet Export Test', () => { ); await s2.render(); - const data = copyData({ + const data = await asyncGetAllPlainData({ sheetInstance: s2, split: NewTab, formatOptions: { isFormatHeader: true }, @@ -377,7 +371,7 @@ describe('PivotSheet Export Test', () => { await s2.render(); - const data = copyData({ + const data = await asyncGetAllPlainData({ sheetInstance: s2, split: NewTab, formatOptions: { isFormatData: true }, @@ -427,7 +421,35 @@ describe('PivotSheet Export Test', () => { ); await s2.render(); - const data = copyData({ + const data = await asyncGetAllPlainData({ + sheetInstance: s2, + split: NewTab, + }); + + expect(data).toMatchSnapshot(); + }); + + it('should export correct data when series number', async () => { + const s2 = new PivotSheet( + getContainer(), + assembleDataCfg({ + meta: [], + fields: { + valueInCols: true, + columns: ['province', 'city'], + rows: ['type', 'sub_type'], + values: ['number'], + }, + }), + assembleOptions({ + hierarchyType: 'grid', + showSeriesNumber: true, + interaction: { enableCopy: true, copyWithHeader: true }, + }), + ); + + await s2.render(); + const data = await asyncGetAllPlainData({ sheetInstance: s2, split: NewTab, }); @@ -436,20 +458,23 @@ describe('PivotSheet Export Test', () => { " province 浙江省 浙江省 浙江省 浙江省 四川省 四川省 四川省 四川省 city 杭州市 绍兴市 宁波市 舟山市 成都市 绵阳市 南充市 乐山市 type sub_type number number number number number number number number - 家具 - 家具 桌子 2367 3877 4342 1723 1822 1943 2330 - 家具 沙发 632 7234 834 2451 2244 2333 2445 - 办公用品 - 办公用品 笔 - 办公用品 纸张 1354 1523 1634 4004 3077 3551 352" + 家具 桌子 7789 2367 3877 4342 1723 1822 1943 2330 + 家具 沙发 5343 632 7234 834 2451 2244 2333 2445 + 办公用品 笔 945 1304 1145 1432 2335 245 2457 2458 + 办公用品 纸张 1343 1354 1523 1634 4004 3077 3551 352" `); + + const rows = data.split(NewLine); + + expect(rows[0].split(NewTab)[1]).toEqual('province'); + expect(rows[1].split(NewTab)[1]).toEqual('city'); }); - it('should export correct data when series number', async () => { + it('should export correct data with formatter', async () => { const s2 = new PivotSheet( getContainer(), assembleDataCfg({ - meta: [], + meta: [{ field: 'number', name: '数值' }], fields: { valueInCols: true, columns: ['province', 'city'], @@ -459,13 +484,12 @@ describe('PivotSheet Export Test', () => { }), assembleOptions({ hierarchyType: 'grid', - showSeriesNumber: true, interaction: { enableCopy: true, copyWithHeader: true }, }), ); await s2.render(); - const data = copyData({ + const data = await asyncGetAllPlainData({ sheetInstance: s2, split: NewTab, }); @@ -473,7 +497,7 @@ describe('PivotSheet Export Test', () => { expect(data).toMatchInlineSnapshot(` " province 浙江省 浙江省 浙江省 浙江省 四川省 四川省 四川省 四川省 city 杭州市 绍兴市 宁波市 舟山市 成都市 绵阳市 南充市 乐山市 - type sub_type number number number number number number number number + type sub_type 数值 数值 数值 数值 数值 数值 数值 数值 家具 桌子 7789 2367 3877 4342 1723 1822 1943 2330 家具 沙发 5343 632 7234 834 2451 2244 2333 2445 办公用品 笔 945 1304 1145 1432 2335 245 2457 2458 @@ -497,7 +521,7 @@ describe('PivotSheet Export Test', () => { await s2.render(); - const data = copyData({ + const data = await asyncGetAllPlainData({ sheetInstance: s2, split: NewTab, customTransformer: () => { @@ -513,11 +537,13 @@ describe('PivotSheet Export Test', () => { }); // https://github.com/antvis/S2/issues/2236 - it('should export correct data When the split separator is configured', () => { - const data = copyData({ + it('should export correct data When the split separator is configured', async () => { + const data = await asyncGetAllPlainData({ sheetInstance: pivotSheet, split: ',', - formatOptions: { isFormatHeader: true }, + formatOptions: { + isFormatHeader: true, + }, }); expect(data).toMatchInlineSnapshot(` diff --git a/packages/s2-core/__tests__/unit/utils/export/export-spec.ts b/packages/s2-core/__tests__/unit/utils/export/export-spec.ts new file mode 100644 index 0000000000..00f03a3b42 --- /dev/null +++ b/packages/s2-core/__tests__/unit/utils/export/export-spec.ts @@ -0,0 +1,473 @@ +import { asyncGetAllPlainData } from '../../../../src/utils'; +import { assembleDataCfg, assembleOptions } from '../../../util'; +import { getContainer } from '../../../util/helpers'; +import { PivotSheet, TableSheet } from '@/sheet-type'; + +describe('TableSheet Export Test', () => { + it('should export correct data with series number', async () => { + const s2 = new TableSheet( + getContainer(), + assembleDataCfg({ + meta: [ + { + field: 'type', + name: '产品类型', + formatter: (type) => (type ? `${type}产品` : ''), + }, + ], + fields: { + columns: ['province', 'city', 'type', 'sub_type', 'number'], + }, + }), + assembleOptions({ + showSeriesNumber: true, + }), + ); + + await s2.render(); + const data = await asyncGetAllPlainData({ + sheetInstance: s2, + split: '\t', + formatOptions: { + isFormatHeader: true, + }, + }); + + const rows = data.split('\n'); + const headers = rows[0].split('\t'); + + expect(rows).toHaveLength(78); + expect(rows).toMatchSnapshot(); + + // 6列数据 包括序列号 + rows.forEach((row) => { + expect(row.split('\t')).toHaveLength(6); + }); + + expect(headers).toMatchSnapshot(); + }); + + it('should export correct data with no series number', async () => { + const s2 = new TableSheet( + getContainer(), + assembleDataCfg({ + meta: [], + fields: { + columns: ['province', 'city', 'type', 'sub_type', 'number'], + }, + }), + assembleOptions({ + showSeriesNumber: false, + }), + ); + + await s2.render(); + const data = await asyncGetAllPlainData({ + sheetInstance: s2, + split: '\t', + }); + const rows = data.split('\n'); + const headers = rows[0].split('\t'); + + expect(rows).toHaveLength(78); + expect(rows).toMatchSnapshot(); + + // 5列数据 不包括序列号 + rows.forEach((e) => { + expect(e.split('\t')).toHaveLength(5); + }); + expect(headers).toMatchSnapshot(); + }); +}); + +describe('PivotSheet Export Test', () => { + it('should export correct data in grid mode', async () => { + const s2 = new PivotSheet( + getContainer(), + assembleDataCfg({ + meta: [], + fields: { + valueInCols: true, + columns: ['province', 'city', 'type', 'sub_type', 'number'], + }, + }), + assembleOptions({ hierarchyType: 'grid' }), + ); + + await s2.render(); + const data = await asyncGetAllPlainData({ + sheetInstance: s2, + split: '\t', + }); + const rows = data.split('\n'); + + expect(rows).toHaveLength(14); + expect(rows[0].split('\t')[1]).toEqual('province'); + expect(rows[1].split('\t')[1]).toEqual('city'); + + rows.forEach((e) => { + expect(e.split('\t')).toHaveLength(34); + }); + }); + + it('should export correct data in tree mode', async () => { + const s2 = new PivotSheet( + getContainer(), + assembleDataCfg({ + meta: [], + fields: { + valueInCols: true, + columns: ['province', 'city', 'type', 'sub_type', 'number'], + }, + }), + assembleOptions({ + hierarchyType: 'tree', + }), + ); + + await s2.render(); + const data = await asyncGetAllPlainData({ + sheetInstance: s2, + split: '\t', + }); + const rows = data.split('\n'); + + expect(rows).toHaveLength(16); + expect(rows[0].split('\t')[1]).toEqual('province'); + expect(rows[1].split('\t')[1]).toEqual('city'); + + rows.forEach((e) => { + expect(e.split('\t')).toHaveLength(34); + }); + }); + + // 因为导出的数据单测,很难看出问题,所以提供图片 + 代码的模式查看: + // https://gw.alipayobjects.com/zos/antfincdn/AU83KF1Sq/6fb3f3e6-0064-4ef8-a5c3-b1333fb59adf.png + it('should export correct data in tree mode and row collapseAll is true', async () => { + const s2 = new PivotSheet( + getContainer(), + assembleDataCfg(), + assembleOptions({ + hierarchyType: 'tree', + style: { + rowCell: { + collapseAll: true, + }, + }, + }), + ); + + await s2.render(); + const data = await asyncGetAllPlainData({ + sheetInstance: s2, + split: '\t', + }); + const rows = data.split('\n'); + + expect(rows).toHaveLength(5); + expect(rows[0].split('\t').length).toEqual(5); + expect(rows[0].split('\t')[0]).toEqual('类别'); + expect(rows[0].split('\t')[1]).toEqual('家具'); + expect(rows[1].split('\t')[0]).toEqual('子类别'); + expect(rows[1].split('\t')[1]).toEqual('桌子'); + expect(rows[2].split('\t')[0]).toEqual('省份'); + expect(rows[2].split('\t')[1]).toEqual('数量'); + }); + + // https://gw.alipayobjects.com/zos/antfincdn/PyrWwocNf/56d0914b-159a-4293-8615-6c1308bf4b3a.png + it('should export correct data in tree mode and hierarchyCollapse is false', async () => { + const s2 = new PivotSheet( + getContainer(), + assembleDataCfg(), + assembleOptions({ + hierarchyType: 'tree', + style: { + rowCell: { + collapseAll: false, + }, + }, + }), + ); + + await s2.render(); + const data = await asyncGetAllPlainData({ + sheetInstance: s2, + split: '\t', + }); + const rows = data.split('\n'); + + expect(rows).toHaveLength(13); + expect(rows[0].split('\t').length).toEqual(6); + expect(rows[0].split('\t')[1]).toEqual('类别'); + expect(rows[0].split('\t')[2]).toEqual('家具'); + expect(rows[1].split('\t')[1]).toEqual('子类别'); + expect(rows[1].split('\t')[2]).toEqual('桌子'); + expect(rows[2].split('\t')[0]).toEqual('省份'); + expect(rows[2].split('\t')[1]).toEqual('城市'); + expect(rows[2].split('\t')[2]).toEqual('数量'); + }); + + it('should export correct data in grid mode with valueInCols is false', async () => { + const s2 = new PivotSheet( + getContainer(), + assembleDataCfg({ + meta: [], + fields: { + valueInCols: false, + columns: ['province', 'city', 'type', 'sub_type', 'number'], + }, + }), + assembleOptions({ + hierarchyType: 'grid', + }), + ); + + await s2.render(); + const data = await asyncGetAllPlainData({ + sheetInstance: s2, + split: '\t', + }); + const rows = data.split('\n'); + + expect(rows).toHaveLength(13); + rows.forEach((e) => { + expect(e.split('\t')).toHaveLength(35); + }); + }); + + it('should export correct data in grid mode with totals in col', async () => { + const s2 = new PivotSheet( + getContainer(), + assembleDataCfg({ + fields: { + valueInCols: true, + columns: ['province', 'city', 'type', 'sub_type', 'number'], + }, + }), + assembleOptions({ + hierarchyType: 'grid', + totals: { + row: { + showGrandTotals: true, + showSubTotals: true, + subTotalsDimensions: ['province'], + }, + col: { + showGrandTotals: true, + showSubTotals: true, + subTotalsDimensions: ['type'], + }, + }, + }), + ); + + await s2.render(); + const data = await asyncGetAllPlainData({ + sheetInstance: s2, + split: '\t', + }); + const rows = data.split('\n'); + + expect(rows).toHaveLength(17); + rows.forEach((e) => { + expect(e.split('\t')).toHaveLength(53); + }); + }); + + it('should export correct data in grid mode with grouped totals in col', async () => { + const s2 = new PivotSheet( + getContainer(), + assembleDataCfg({ + fields: { + valueInCols: true, + columns: ['province', 'city', 'type', 'sub_type', 'number'], + }, + }), + assembleOptions({ + hierarchyType: 'grid', + totals: { + row: { + showGrandTotals: true, + showSubTotals: true, + subTotalsDimensions: ['province'], + }, + col: { + grandTotalsGroupDimensions: ['city', 'type'], + subTotalsGroupDimensions: ['sub_type'], + showGrandTotals: true, + showSubTotals: true, + subTotalsDimensions: ['type'], + }, + }, + }), + ); + + await s2.render(); + const data = await asyncGetAllPlainData({ + sheetInstance: s2, + split: '\t', + }); + const rows = data.split('\n'); + + expect(rows).toHaveLength(17); + rows.forEach((e) => { + expect(e.split('\t')).toHaveLength(60); + }); + }); + + it('should export correct data in grid mode with grouped totals in row', async () => { + const s2 = new PivotSheet( + getContainer(), + assembleDataCfg({ + fields: { + valueInCols: false, + columns: ['province', 'city', 'type', 'sub_type', 'number'], + }, + }), + assembleOptions({ + hierarchyType: 'grid', + totals: { + row: { + showGrandTotals: true, + showSubTotals: true, + subTotalsDimensions: ['province'], + }, + col: { + grandTotalsGroupDimensions: ['city', 'sub_type', 'province'], + subTotalsGroupDimensions: ['sub_type'], + showGrandTotals: true, + showSubTotals: true, + subTotalsDimensions: ['type'], + }, + }, + }), + ); + + await s2.render(); + const data = await asyncGetAllPlainData({ + sheetInstance: s2, + split: '\t', + }); + const rows = data.split('\n'); + + expect(rows).toHaveLength(16); + rows.forEach((e) => { + expect(e.split('\t')).toHaveLength(63); + }); + }); + it('should export correct data in grid mode with totals in row', async () => { + const s2 = new PivotSheet( + getContainer(), + assembleDataCfg({ + fields: { + valueInCols: false, + columns: ['province', 'city', 'type', 'sub_type', 'number'], + }, + }), + assembleOptions({ + hierarchyType: 'grid', + totals: { + row: { + showGrandTotals: true, + showSubTotals: true, + subTotalsDimensions: ['province'], + }, + col: { + showGrandTotals: true, + showSubTotals: true, + subTotalsDimensions: ['type'], + }, + }, + }), + ); + + await s2.render(); + const data = await asyncGetAllPlainData({ + sheetInstance: s2, + split: '\t', + }); + const rows = data.split('\n'); + + expect(rows).toHaveLength(16); + rows.forEach((e) => { + expect(e.split('\t')).toHaveLength(54); + }); + }); + + it('should export correct data when isFormat: {isFormatHeader: true}', async () => { + const s2 = new PivotSheet( + getContainer(), + assembleDataCfg({ + meta: [ + { + field: 'province', + formatter: (value) => { + return `${value}-province`; + }, + }, + { + field: 'type', + formatter: (value) => { + return `${value}-type`; + }, + }, + ], + fields: { + valueInCols: true, + columns: ['province', 'city'], + rows: ['type', 'sub_type'], + values: ['number'], + }, + }), + assembleOptions({}), + ); + + await s2.render(); + const data = await asyncGetAllPlainData({ + sheetInstance: s2, + split: '\t', + formatOptions: { + isFormatHeader: true, + }, + }); + const rows = data.split('\n'); + + expect(rows).toHaveLength(7); + expect(rows[0].split('\t')[1]).toEqual('province'); + expect(rows[0].split('\t')[2]).toEqual('浙江省-province'); + expect(rows[1].split('\t')[1]).toEqual('city'); + expect(rows[3].split('\t')[0]).toEqual('家具-type'); + }); + + it('should export correct $$extra$$ field name', async () => { + const s2 = new PivotSheet( + getContainer(), + assembleDataCfg({ + meta: [ + { + field: 'number', + name: '数值', + }, + ], + fields: { + valueInCols: true, + columns: ['province', 'city'], + rows: ['type', 'sub_type'], + values: ['number'], + }, + }), + assembleOptions({}), + ); + + await s2.render(); + const data = await asyncGetAllPlainData({ + sheetInstance: s2, + split: '\t', + formatOptions: false, + }); + const rows = data.split('\n'); + const headers = rows[2].split('\t'); + + expect(headers).toMatchSnapshot(); + }); +}); diff --git a/packages/s2-core/__tests__/unit/utils/export/export-table-spec.ts b/packages/s2-core/__tests__/unit/utils/export/export-table-spec.ts index fca32be09c..726ebc946d 100644 --- a/packages/s2-core/__tests__/unit/utils/export/export-table-spec.ts +++ b/packages/s2-core/__tests__/unit/utils/export/export-table-spec.ts @@ -3,9 +3,9 @@ import { data as originData } from 'tests/data/mock-dataset.json'; import { assembleDataCfg, assembleOptions } from '../../../util'; import { getContainer } from '../../../util/helpers'; import { TableSheet } from '@/sheet-type'; -import { asyncGetAllPlainData, copyData } from '@/utils'; +import { asyncGetAllPlainData } from '@/utils'; import { NewTab, NewLine } from '@/common'; -import { CopyMIMEType } from '@/utils/export/interface'; +import { CopyMIMEType } from '@/common/interface/export'; describe('TableSheet Export Test', () => { it('should export correct data with series number', async () => { @@ -61,7 +61,7 @@ describe('TableSheet Export Test', () => { ]); } - const data = copyData({ + const data = await asyncGetAllPlainData({ sheetInstance: s2, split: NewTab, formatOptions: { @@ -99,7 +99,7 @@ describe('TableSheet Export Test', () => { ); await s2.render(); - const data = copyData({ + const data = await asyncGetAllPlainData({ sheetInstance: s2, split: NewTab, }); @@ -140,7 +140,7 @@ describe('TableSheet Export Test', () => { ); await s2.render(); - const data = copyData({ + const data = await asyncGetAllPlainData({ sheetInstance: s2, split: NewTab, formatOptions: true, @@ -155,6 +155,7 @@ describe('TableSheet Export Test', () => { 浙江省-province 家具-type 沙发 5343" `); }); + it('should support custom export matrix transformer', async () => { const s2 = new TableSheet( getContainer(), @@ -170,7 +171,7 @@ describe('TableSheet Export Test', () => { ); await s2.render(); - const data = copyData({ + const data = await asyncGetAllPlainData({ sheetInstance: s2, split: NewTab, formatOptions: true, @@ -200,7 +201,7 @@ describe('TableSheet Export Test', () => { ); await tableSheet.render(); - const data = copyData({ + const data = await asyncGetAllPlainData({ sheetInstance: tableSheet, split: ',', }); diff --git a/packages/s2-core/__tests__/unit/utils/export/index-spec.ts b/packages/s2-core/__tests__/unit/utils/export/utils-spec.ts similarity index 94% rename from packages/s2-core/__tests__/unit/utils/export/index-spec.ts rename to packages/s2-core/__tests__/unit/utils/export/utils-spec.ts index acf738bb79..547081bb92 100644 --- a/packages/s2-core/__tests__/unit/utils/export/index-spec.ts +++ b/packages/s2-core/__tests__/unit/utils/export/utils-spec.ts @@ -1,6 +1,6 @@ -import { copyToClipboard } from '@/utils/export'; +import { copyToClipboard } from '@/utils/export/utils'; -describe('Copy Tests', () => { +describe('Export & Copy Utils Tests', () => { test('should async copy text to clipboard', async () => { const text = '222'; diff --git a/packages/s2-core/__tests__/unit/utils/facet-spec.ts b/packages/s2-core/__tests__/unit/utils/facet-spec.ts index 8267cdcb2b..b44bff8a28 100644 --- a/packages/s2-core/__tests__/unit/utils/facet-spec.ts +++ b/packages/s2-core/__tests__/unit/utils/facet-spec.ts @@ -1,30 +1,10 @@ import { - getSubTotalNodeWidthOrHeightByLevel, getIndexRangeWithOffsets, getAdjustedRowScrollX, getAdjustedScrollOffset, } from '@/utils/facet'; describe('Facet util test', () => { - test('should get correct width of subTotal node', () => { - const sampleNodesForAllLevels = [ - { - id: 'root[&]测试', - value: '测试', - isSubTotals: true, - width: 20, - level: 0, - }, - ]; - - expect( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - getSubTotalNodeWidthOrHeightByLevel(sampleNodesForAllLevels, -1, 'width'), - ).toEqual(20); - expect(getSubTotalNodeWidthOrHeightByLevel([], -1, 'width')).toEqual(0); - }); - test('should get correct index range for given offsets', () => { const offsets = [0, 30, 60, 90, 120, 150, 160, 170, 190]; diff --git a/packages/s2-core/__tests__/unit/utils/g-mini-charts-spec.ts b/packages/s2-core/__tests__/unit/utils/g-mini-charts-spec.ts index c560ca9177..5e88113ef6 100644 --- a/packages/s2-core/__tests__/unit/utils/g-mini-charts-spec.ts +++ b/packages/s2-core/__tests__/unit/utils/g-mini-charts-spec.ts @@ -3,7 +3,7 @@ import { getContainer } from 'tests/util/helpers'; import { forEach, map } from 'lodash'; import { data } from 'tests/data/mock-dataset.json'; import type { RangeColors } from '../../../src/common/interface/theme'; -import { PivotSheet } from '@/sheet-type'; +import { PivotSheet, SpreadSheet } from '@/sheet-type'; import { CellType, MiniChartTypes, type S2CellType } from '@/common'; import { getBulletRangeColor, @@ -312,6 +312,7 @@ describe('MiniCharts Utils Tests', () => { }); describe('drawInterval Test', () => { + let s2: SpreadSheet; const dataCfg = assembleDataCfg({ meta: [], fields: { @@ -324,6 +325,7 @@ describe('drawInterval Test', () => { const horizontalBorderWidth = getTheme({})?.dataCell?.cell?.horizontalBorderWidth ?? 1; + const options = assembleOptions({ style: { dataCell: { @@ -334,9 +336,8 @@ describe('drawInterval Test', () => { conditions: {}, }); - const s2 = new PivotSheet(getContainer(), dataCfg, options); - beforeEach(async () => { + s2 = new PivotSheet(getContainer(), dataCfg, options); await s2.render(); }); diff --git a/packages/s2-core/__tests__/unit/utils/hide-columns-spec.ts b/packages/s2-core/__tests__/unit/utils/hide-columns-spec.ts index fd08727dc0..03c02f07c8 100644 --- a/packages/s2-core/__tests__/unit/utils/hide-columns-spec.ts +++ b/packages/s2-core/__tests__/unit/utils/hide-columns-spec.ts @@ -56,6 +56,10 @@ describe('Hide Columns Tests', () => { mockSpreadSheetInstance.render = jest.fn(); mockSpreadSheetInstance.interaction = { reset: jest.fn(), + isSelectedState: jest.fn(), + intercepts: new Set(), + getActiveCells: jest.fn(() => []), + clearHoverTimer: jest.fn(), } as unknown as RootInteraction; mockSpreadSheetInstance.isTableMode = () => true; mockSpreadSheetInstance.isPivotMode = () => false; @@ -344,6 +348,427 @@ describe('Hide Columns Tests', () => { ]); }); + // https://github.com/antvis/S2/issues/2194 + test('should hidden group columns for fields (5 => 2)', async () => { + for (const field of ['5', '4', '3', '2']) { + // eslint-disable-next-line no-await-in-loop + await hideColumnsByThunkGroup(mockSpreadSheetInstance, [field]); + } + + expect(mockSpreadSheetInstance.store.get('hiddenColumnsDetail')) + .toMatchInlineSnapshot(` + Array [ + Object { + "displaySiblingNode": Object { + "next": null, + "prev": Object { + "colIndex": 4, + "field": "4", + "id": "id-4", + }, + }, + "hideColumnNodes": Array [ + Object { + "colIndex": 5, + "field": "5", + "id": "id-5", + }, + ], + }, + Object { + "displaySiblingNode": Object { + "next": null, + "prev": Object { + "colIndex": 3, + "field": "3", + "id": "id-3", + }, + }, + "hideColumnNodes": Array [ + Object { + "colIndex": 4, + "field": "4", + "id": "id-4", + }, + ], + }, + Object { + "displaySiblingNode": Object { + "next": null, + "prev": Object { + "colIndex": 2, + "field": "2", + "id": "id-2", + }, + }, + "hideColumnNodes": Array [ + Object { + "colIndex": 3, + "field": "3", + "id": "id-3", + }, + ], + }, + Object { + "displaySiblingNode": Object { + "next": null, + "prev": Object { + "colIndex": 1, + "field": "1", + "id": "id-1", + }, + }, + "hideColumnNodes": Array [ + Object { + "colIndex": 2, + "field": "2", + "id": "id-2", + }, + ], + }, + ] + `); + }); + + test('should hidden group columns for fields (3 => 5)', async () => { + await hideColumnsByThunkGroup(mockSpreadSheetInstance, ['3']); + await hideColumnsByThunkGroup(mockSpreadSheetInstance, ['5']); + + expect(mockSpreadSheetInstance.store.get('hiddenColumnsDetail')) + .toMatchInlineSnapshot(` + Array [ + Object { + "displaySiblingNode": Object { + "next": Object { + "colIndex": 4, + "field": "4", + "id": "id-4", + }, + "prev": Object { + "colIndex": 2, + "field": "2", + "id": "id-2", + }, + }, + "hideColumnNodes": Array [ + Object { + "colIndex": 3, + "field": "3", + "id": "id-3", + }, + ], + }, + Object { + "displaySiblingNode": Object { + "next": null, + "prev": Object { + "colIndex": 4, + "field": "4", + "id": "id-4", + }, + }, + "hideColumnNodes": Array [ + Object { + "colIndex": 5, + "field": "5", + "id": "id-5", + }, + ], + }, + ] + `); + }); + + test('should get empty next sibling nodes when always hidden last column for fields (1 => 2)', async () => { + await hideColumnsByThunkGroup(mockSpreadSheetInstance, ['1']); + await hideColumnsByThunkGroup(mockSpreadSheetInstance, ['2']); + + expect(mockSpreadSheetInstance.store.get('hiddenColumnsDetail')) + .toMatchInlineSnapshot(` + Array [ + Object { + "displaySiblingNode": Object { + "next": Object { + "colIndex": 2, + "field": "2", + "id": "id-2", + }, + "prev": null, + }, + "hideColumnNodes": Array [ + Object { + "colIndex": 1, + "field": "1", + "id": "id-1", + }, + ], + }, + Object { + "displaySiblingNode": Object { + "next": Object { + "colIndex": 3, + "field": "3", + "id": "id-3", + }, + "prev": null, + }, + "hideColumnNodes": Array [ + Object { + "colIndex": 2, + "field": "2", + "id": "id-2", + }, + ], + }, + ] + `); + }); + + test('should get empty next sibling nodes when always hidden last column for fields (1 => 3)', async () => { + await hideColumnsByThunkGroup(mockSpreadSheetInstance, ['1']); + await hideColumnsByThunkGroup(mockSpreadSheetInstance, ['3']); + + expect(mockSpreadSheetInstance.store.get('hiddenColumnsDetail')) + .toMatchInlineSnapshot(` + Array [ + Object { + "displaySiblingNode": Object { + "next": Object { + "colIndex": 2, + "field": "2", + "id": "id-2", + }, + "prev": null, + }, + "hideColumnNodes": Array [ + Object { + "colIndex": 1, + "field": "1", + "id": "id-1", + }, + ], + }, + Object { + "displaySiblingNode": Object { + "next": Object { + "colIndex": 4, + "field": "4", + "id": "id-4", + }, + "prev": Object { + "colIndex": 2, + "field": "2", + "id": "id-2", + }, + }, + "hideColumnNodes": Array [ + Object { + "colIndex": 3, + "field": "3", + "id": "id-3", + }, + ], + }, + ] + `); + }); + + test('should get empty next sibling nodes when always hidden last column for fields (5 => 4)', async () => { + await hideColumnsByThunkGroup(mockSpreadSheetInstance, ['5']); + await hideColumnsByThunkGroup(mockSpreadSheetInstance, ['4']); + + expect(mockSpreadSheetInstance.store.get('hiddenColumnsDetail')) + .toMatchInlineSnapshot(` + Array [ + Object { + "displaySiblingNode": Object { + "next": null, + "prev": Object { + "colIndex": 4, + "field": "4", + "id": "id-4", + }, + }, + "hideColumnNodes": Array [ + Object { + "colIndex": 5, + "field": "5", + "id": "id-5", + }, + ], + }, + Object { + "displaySiblingNode": Object { + "next": null, + "prev": Object { + "colIndex": 3, + "field": "3", + "id": "id-3", + }, + }, + "hideColumnNodes": Array [ + Object { + "colIndex": 4, + "field": "4", + "id": "id-4", + }, + ], + }, + ] + `); + }); + + test('should get correctly sibling nodes when hidden first and last column for fields (1 => 5)', async () => { + await hideColumnsByThunkGroup(mockSpreadSheetInstance, ['1']); + await hideColumnsByThunkGroup(mockSpreadSheetInstance, ['5']); + + expect(mockSpreadSheetInstance.store.get('hiddenColumnsDetail')) + .toMatchInlineSnapshot(` + Array [ + Object { + "displaySiblingNode": Object { + "next": Object { + "colIndex": 2, + "field": "2", + "id": "id-2", + }, + "prev": null, + }, + "hideColumnNodes": Array [ + Object { + "colIndex": 1, + "field": "1", + "id": "id-1", + }, + ], + }, + Object { + "displaySiblingNode": Object { + "next": null, + "prev": Object { + "colIndex": 4, + "field": "4", + "id": "id-4", + }, + }, + "hideColumnNodes": Array [ + Object { + "colIndex": 5, + "field": "5", + "id": "id-5", + }, + ], + }, + ] + `); + }); + + test('should get correctly sibling nodes when hidden odd columns for fields (2 => 4)', async () => { + await hideColumnsByThunkGroup(mockSpreadSheetInstance, ['2']); + await hideColumnsByThunkGroup(mockSpreadSheetInstance, ['4']); + + expect(mockSpreadSheetInstance.store.get('hiddenColumnsDetail')) + .toMatchInlineSnapshot(` + Array [ + Object { + "displaySiblingNode": Object { + "next": Object { + "colIndex": 3, + "field": "3", + "id": "id-3", + }, + "prev": Object { + "colIndex": 1, + "field": "1", + "id": "id-1", + }, + }, + "hideColumnNodes": Array [ + Object { + "colIndex": 2, + "field": "2", + "id": "id-2", + }, + ], + }, + Object { + "displaySiblingNode": Object { + "next": Object { + "colIndex": 5, + "field": "5", + "id": "id-5", + }, + "prev": Object { + "colIndex": 3, + "field": "3", + "id": "id-3", + }, + }, + "hideColumnNodes": Array [ + Object { + "colIndex": 4, + "field": "4", + "id": "id-4", + }, + ], + }, + ] + `); + }); + + test('should get correctly sibling nodes when hidden near columns for fields (2 => 3)', async () => { + await hideColumnsByThunkGroup(mockSpreadSheetInstance, ['2']); + await hideColumnsByThunkGroup(mockSpreadSheetInstance, ['3']); + + expect(mockSpreadSheetInstance.store.get('hiddenColumnsDetail')) + .toMatchInlineSnapshot(` + Array [ + Object { + "displaySiblingNode": Object { + "next": Object { + "colIndex": 3, + "field": "3", + "id": "id-3", + }, + "prev": Object { + "colIndex": 1, + "field": "1", + "id": "id-1", + }, + }, + "hideColumnNodes": Array [ + Object { + "colIndex": 2, + "field": "2", + "id": "id-2", + }, + ], + }, + Object { + "displaySiblingNode": Object { + "next": Object { + "colIndex": 4, + "field": "4", + "id": "id-4", + }, + "prev": Object { + "colIndex": 1, + "field": "1", + "id": "id-1", + }, + }, + "hideColumnNodes": Array [ + Object { + "colIndex": 3, + "field": "3", + "id": "id-3", + }, + ], + }, + ] + `); + }); + test('should skip hidden group columns if hidden column fields not change', async () => { await hideColumnsByThunkGroup(mockSpreadSheetInstance, []); diff --git a/packages/s2-core/__tests__/unit/utils/interaction/hover-event-spec.ts b/packages/s2-core/__tests__/unit/utils/interaction/hover-event-spec.ts index 391d77684a..589b1c90d4 100644 --- a/packages/s2-core/__tests__/unit/utils/interaction/hover-event-spec.ts +++ b/packages/s2-core/__tests__/unit/utils/interaction/hover-event-spec.ts @@ -27,7 +27,7 @@ jest.mock('@/cell', () => { }); describe('Hover Event Utils Tests', () => { - describe('getActiveHoverRowColCells test', () => { + describe('#getActiveHoverRowColCells', () => { test('should return correct result for getActiveHoverRowColCells', () => { const cells = [ new ColCell({} as unknown as Node, {} as unknown as SpreadSheet), @@ -45,7 +45,7 @@ describe('Hover Event Utils Tests', () => { }); }); - describe('updateAllColHeaderCellState test', () => { + describe('#updateAllColHeaderCellState', () => { test('should return correct result for updateAllColHeaderCellState', () => { const cells = [ new ColCell({} as unknown as Node, {} as unknown as SpreadSheet), diff --git a/packages/s2-core/__tests__/unit/utils/interaction/resize-spec.ts b/packages/s2-core/__tests__/unit/utils/interaction/resize-spec.ts index 24059c0f90..8da6764df0 100644 --- a/packages/s2-core/__tests__/unit/utils/interaction/resize-spec.ts +++ b/packages/s2-core/__tests__/unit/utils/interaction/resize-spec.ts @@ -33,7 +33,7 @@ describe('Resize Utils Tests', () => { let s2: SpreadSheet; - beforeAll(() => { + beforeEach(() => { MockSpreadSheet.mockClear(); s2 = new MockSpreadSheet(); diff --git a/packages/s2-core/__tests__/unit/utils/interaction/state-controller-spec.ts b/packages/s2-core/__tests__/unit/utils/interaction/state-controller-spec.ts index b7f16f6d51..93faee639f 100644 --- a/packages/s2-core/__tests__/unit/utils/interaction/state-controller-spec.ts +++ b/packages/s2-core/__tests__/unit/utils/interaction/state-controller-spec.ts @@ -1,10 +1,10 @@ -import { getCellMeta } from '@/utils/interaction/select-event'; -import type { RowCell } from '@/cell/row-cell'; +import type { RowCell } from '../../../../src/cell'; import { CellType, InteractionStateName } from '@/common/constant/interaction'; import type { S2Options } from '@/common/interface'; import { Store } from '@/common/store'; import { RootInteraction } from '@/interaction/root'; import { SpreadSheet } from '@/sheet-type'; +import { getCellMeta } from '@/utils/interaction/select-event'; import { clearState, setState } from '@/utils/interaction/state-controller'; jest.mock('@/sheet-type'); diff --git a/packages/s2-core/__tests__/unit/utils/merge-cell-spec.ts b/packages/s2-core/__tests__/unit/utils/merge-cell-spec.ts index e33ef59157..c5c70a4069 100644 --- a/packages/s2-core/__tests__/unit/utils/merge-cell-spec.ts +++ b/packages/s2-core/__tests__/unit/utils/merge-cell-spec.ts @@ -20,12 +20,11 @@ import { import type { RootInteraction } from '@/interaction/root'; import type { MergedCellInfo, - S2CellType, TempMergedCell, ViewMeta, } from '@/common/interface'; import type { BaseFacet } from '@/facet'; -import type { MergedCell } from '@/cell'; +import type { DataCell, MergedCell } from '@/cell'; jest.mock('@/sheet-type'); @@ -35,7 +34,7 @@ describe('Merge Cells Test', () => { let mockOneCellEdges: [number, number][][] = []; let mockTwoCellEdges: [number, number][][] = []; let mockMergeCellInfo: MergedCellInfo[] = []; - let mockAllVisibleCells: S2CellType[] = []; + let mockAllVisibleCells: DataCell[] = []; beforeEach(() => { mockInstance = new MockSpreadSheet(); @@ -112,7 +111,7 @@ describe('Merge Cells Test', () => { mockAllVisibleCells = [ { getMeta: jest.fn().mockReturnValue(mockMergeCellInfo[2]) }, { getMeta: jest.fn().mockReturnValue(mockMergeCellInfo[3]) }, - ] as unknown as S2CellType[]; + ] as unknown as DataCell[]; }); test('should get none active cells info', () => { @@ -204,7 +203,7 @@ describe('Merge Cells Test', () => { }, ]; - expect(getPolygonPoints(mockCells as unknown as S2CellType[])).toEqual( + expect(getPolygonPoints(mockCells as unknown as DataCell[])).toEqual( mockResult, ); }); diff --git a/packages/s2-core/__tests__/unit/utils/sort-action-spec.tsx b/packages/s2-core/__tests__/unit/utils/sort-action-spec.tsx index 3f0c6bc0e5..1031a7f419 100644 --- a/packages/s2-core/__tests__/unit/utils/sort-action-spec.tsx +++ b/packages/s2-core/__tests__/unit/utils/sort-action-spec.tsx @@ -14,7 +14,7 @@ import { type S2DataConfig, VALUE_FIELD, } from '@/common'; -import { PivotSheet } from '@/sheet-type'; +import { PivotSheet, SpreadSheet } from '@/sheet-type'; import { PivotDataSet, type SortActionParams } from '@/data-set'; import { CellData } from '@/data-set/cell-data'; @@ -509,6 +509,10 @@ describe('GetSortByMeasureValues Total Fallback Tests', () => { sheet.dataSet = dataSet; }); + afterEach(() => { + sheet.destroy(); + }); + test('should sort by col total', () => { // 根据列(类别)的总和排序 const sortParam: SortParam = { @@ -531,7 +535,6 @@ describe('GetSortByMeasureValues Total Fallback Tests', () => { { price: 41.5, type: '纸张', - [EXTRA_FIELD]: 'price', }, 'price', ), @@ -539,7 +542,6 @@ describe('GetSortByMeasureValues Total Fallback Tests', () => { { price: 37, type: '笔', - [EXTRA_FIELD]: 'price', }, 'price', ), @@ -567,7 +569,6 @@ describe('GetSortByMeasureValues Total Fallback Tests', () => { { price: 33, province: '吉林', - [EXTRA_FIELD]: 'price', }, 'price', ), @@ -575,7 +576,6 @@ describe('GetSortByMeasureValues Total Fallback Tests', () => { { price: 45.5, province: '浙江', - [EXTRA_FIELD]: 'price', }, 'price', ), @@ -675,7 +675,6 @@ describe('GetSortByMeasureValues Total Fallback Tests', () => { province: '浙江', city: '杭州', price: 3, - [EXTRA_FIELD]: 'price', }, 'price', ), @@ -684,7 +683,6 @@ describe('GetSortByMeasureValues Total Fallback Tests', () => { province: '浙江', city: '舟山', price: 42.5, - [EXTRA_FIELD]: 'price', }, 'price', ), @@ -693,7 +691,6 @@ describe('GetSortByMeasureValues Total Fallback Tests', () => { province: '吉林', city: '长春', price: 13, - [EXTRA_FIELD]: 'price', }, 'price', ), @@ -702,7 +699,6 @@ describe('GetSortByMeasureValues Total Fallback Tests', () => { province: '吉林', city: '白山', price: 20, - [EXTRA_FIELD]: 'price', }, 'price', ), @@ -772,3 +768,67 @@ describe('GetSortByMeasureValues Total Fallback Tests', () => { ]); }); }); + +describe('total group dimension sort test', () => { + let sheet: SpreadSheet; + + beforeEach(() => { + const currentOptions = { + totals: { + col: { + grandTotalsGroupDimensions: ['city'], + showGrandTotals: true, + }, + }, + } as S2Options; + + const dataConfig = { + ...sortData, + data: [ + ...sortData.data, + { + city: '杭州', + type: '纸张', + price: '999', + }, + { + city: '杭州', + type: '笔', + price: '666', + }, + ], + fields: { + rows: ['type'], + columns: ['province', 'city'], + values: ['price'], + }, + }; + + sheet = new PivotSheet(getContainer(), dataConfig, currentOptions); + sheet.render(); + }); + + afterEach(() => { + sheet.destroy(); + }); + test('should sort by col total with group', () => { + // 根据列(类别)的总和排序 + const sortParam: SortParam = { + sortFieldId: 'type', + sortByMeasure: TOTAL_VALUE, + sortMethod: 'desc', + query: { + [EXTRA_FIELD]: 'price', + city: '杭州', + }, + }; + + const params: SortActionParams = { + dataSet: sheet.dataSet as PivotDataSet, + sortParam, + }; + const measureValues = getSortByMeasureValues(params); + + expect(measureValues).toMatchSnapshot(); + }); +}); diff --git a/packages/s2-core/__tests__/unit/utils/text-spec.ts b/packages/s2-core/__tests__/unit/utils/text-spec.ts index 10cf637e1a..4829de77ab 100644 --- a/packages/s2-core/__tests__/unit/utils/text-spec.ts +++ b/packages/s2-core/__tests__/unit/utils/text-spec.ts @@ -8,6 +8,8 @@ import { getCellWidth, getEmptyPlaceholder, getContentAreaForMultiData, + isZeroOrEmptyValue, + isUnchangedValue, } from '@/utils/text'; const isHD = window.devicePixelRatio >= 2; @@ -274,3 +276,70 @@ describe('Text Utils Tests', () => { ]); }); }); + +describe('isZeroOrEmptyValue', () => { + test('should return true for zero values', () => { + expect(isZeroOrEmptyValue('0.00%')).toBe(true); + expect(isZeroOrEmptyValue('-0.00%')).toBe(true); + expect(isZeroOrEmptyValue('0.0万亿')).toBe(true); + expect(isZeroOrEmptyValue('-0.0万亿')).toBe(true); + expect(isZeroOrEmptyValue('0.00万')).toBe(true); + expect(isZeroOrEmptyValue('-0.00万')).toBe(true); + expect(isZeroOrEmptyValue('0')).toBe(true); + expect(isZeroOrEmptyValue('-0')).toBe(true); + expect(isZeroOrEmptyValue(0)).toBe(true); + expect(isZeroOrEmptyValue(-0)).toBe(true); + }); + + test('should return false for non-zero values', () => { + expect(isZeroOrEmptyValue('0.5%')).toBe(false); + expect(isZeroOrEmptyValue('-0.5%')).toBe(false); + expect(isZeroOrEmptyValue('0.01万亿')).toBe(false); + expect(isZeroOrEmptyValue('-0.01万亿')).toBe(false); + expect(isZeroOrEmptyValue('1')).toBe(false); + expect(isZeroOrEmptyValue('-1')).toBe(false); + expect(isZeroOrEmptyValue(0.1)).toBe(false); + expect(isZeroOrEmptyValue(-0.1)).toBe(false); + }); + + test('should return true for non-numeric values', () => { + expect(isZeroOrEmptyValue('abc')).toBe(true); + expect(isZeroOrEmptyValue('')).toBe(true); + expect(isZeroOrEmptyValue(null as any)).toBe(true); + expect(isZeroOrEmptyValue(undefined as any)).toBe(true); + }); +}); + +describe('isUnchangedValue', () => { + test('should return true for zero values', () => { + expect(isUnchangedValue(0, 123)).toBeTruthy(); + expect(isUnchangedValue('0', 'abc')).toBeTruthy(); + }); + + test('should return true for empty values', () => { + expect(isUnchangedValue('', 'abc')).toBeTruthy(); + expect(isUnchangedValue(null as any, 123)).toBeTruthy(); + expect(isUnchangedValue(undefined as any, 123)).toBeTruthy(); + }); + + test('should return true for unchanged values', () => { + expect(isUnchangedValue('test', 'test')).toBeTruthy(); + expect(isUnchangedValue(123, 123)).toBeTruthy(); + }); + + test('should return true for numberless changed values', () => { + expect(isUnchangedValue('test', 'abc')).toBeTruthy(); + }); + + test('should return false for numeric changed values', () => { + expect(isUnchangedValue(123, 456)).toBeFalsy(); + }); + + test('should return true for negative zero', () => { + expect(isUnchangedValue(-0, 123)).toBeTruthy(); + }); + + test('should return false for negative values', () => { + expect(isUnchangedValue(-123, 123)).toBeFalsy(); + }); +}); diff --git a/packages/s2-core/__tests__/unit/utils/tooltip-spec.ts b/packages/s2-core/__tests__/unit/utils/tooltip-spec.ts index d0a348b6a5..00cbc509af 100644 --- a/packages/s2-core/__tests__/unit/utils/tooltip-spec.ts +++ b/packages/s2-core/__tests__/unit/utils/tooltip-spec.ts @@ -2,9 +2,9 @@ import { createFakeSpreadSheet, createMockCellInfo, createPivotSheet, + createTableSheet, getContainer, } from 'tests/util/helpers'; - import { omit } from 'lodash'; import * as dataConfig from 'tests/data/mock-dataset.json'; import { CellData } from '@/data-set/cell-data'; @@ -62,7 +62,7 @@ describe('Tooltip Utils Tests', () => { height: 1000, }; - beforeAll(() => { + beforeEach(() => { s2 = createFakeSpreadSheet(); tooltipContainer = { getBoundingClientRect: () => @@ -873,6 +873,159 @@ describe('Tooltip Utils Tests', () => { }, ); }); + + describe('Tooltip Get Data Tests For TableSheet', () => { + beforeEach(() => { + s2 = createTableSheet( + { showSeriesNumber: true }, + { useSimpleData: false }, + ); + s2.render(); + }); + + afterEach(() => { + s2.destroy(); + }); + + test('should get correctly summaries of selected col cell', () => { + const typeColCell = s2.facet.getColLeafNodes()[1].belongsCell!; + const subTypeColCell = s2.facet.getColLeafNodes()[2].belongsCell!; + + expect(getMockTooltipData(typeColCell)).toMatchInlineSnapshot(` + Object { + "description": "类别说明。。", + "details": null, + "headInfo": null, + "infos": undefined, + "interpretation": undefined, + "name": null, + "summaries": Array [ + Object { + "name": "", + "selectedData": Array [ + "家具", + "家具", + "家具", + "家具", + "家具", + "家具", + "家具", + "家具", + "办公用品", + "办公用品", + "办公用品", + "办公用品", + "办公用品", + "办公用品", + "办公用品", + "办公用品", + "家具", + "家具", + "家具", + "家具", + "家具", + "家具", + "家具", + "家具", + "办公用品", + "办公用品", + "办公用品", + "办公用品", + "办公用品", + "办公用品", + "办公用品", + "办公用品", + ], + "value": "", + }, + ], + "tips": undefined, + } + `); + expect(getMockTooltipData(subTypeColCell)).toMatchInlineSnapshot(` + Object { + "description": "子类别说明。。", + "details": null, + "headInfo": null, + "infos": undefined, + "interpretation": undefined, + "name": null, + "summaries": Array [ + Object { + "name": "", + "selectedData": Array [ + "桌子", + "桌子", + "桌子", + "桌子", + "沙发", + "沙发", + "沙发", + "沙发", + "笔", + "笔", + "笔", + "笔", + "纸张", + "纸张", + "纸张", + "纸张", + "桌子", + "桌子", + "桌子", + "桌子", + "沙发", + "沙发", + "沙发", + "沙发", + "笔", + "笔", + "笔", + "笔", + "纸张", + "纸张", + "纸张", + "纸张", + ], + "value": "", + }, + ], + "tips": undefined, + } + `); + }); + + test('should get correctly summaries of selected series number cell', () => { + const seriesCell = s2.facet.getDataCells()[0]; + + expect(getMockTooltipData(seriesCell)).toMatchInlineSnapshot(` + Object { + "description": undefined, + "details": null, + "headInfo": null, + "infos": undefined, + "interpretation": undefined, + "name": null, + "summaries": Array [ + Object { + "name": "", + "selectedData": Array [ + Object { + "city": "杭州市", + "number": 7789, + "province": "浙江省", + "sub_type": "桌子", + "type": "家具", + }, + ], + "value": "", + }, + ], + "tips": undefined, + } + `); + }); + }); }); test('should set container style', () => { diff --git a/packages/s2-core/__tests__/util/helpers.ts b/packages/s2-core/__tests__/util/helpers.ts index 35e9226cda..596827ab0f 100644 --- a/packages/s2-core/__tests__/util/helpers.ts +++ b/packages/s2-core/__tests__/util/helpers.ts @@ -7,21 +7,31 @@ import { FederatedMouseEvent, FederatedPointerEvent, type CanvasConfig, + Group, } from '@antv/g'; import { omit } from 'lodash'; import * as simpleDataConfig from 'tests/data/simple-data.json'; import * as dataConfig from 'tests/data/mock-dataset.json'; import { Renderer } from '@antv/g-canvas'; -import type { BaseDataSet, Node } from '../../src'; +import { getTheme, type BaseDataSet, type Node, Hierarchy } from '../../src'; + +import { assembleOptions, assembleDataCfg } from '.'; import { RootInteraction } from '@/interaction/root'; import { Store } from '@/common/store'; -import type { S2CellType, S2Options, ViewMeta } from '@/common/interface'; +import type { + InternalFullyTheme, + LayoutResult, + S2CellType, + S2DataConfig, + S2Options, + ViewMeta, +} from '@/common/interface'; import { PivotSheet, SpreadSheet, TableSheet } from '@/sheet-type'; import type { BaseTooltip } from '@/ui/tooltip'; import { customMerge } from '@/utils/merge'; -import { DEFAULT_OPTIONS } from '@/common/constant'; +import { DEFAULT_OPTIONS, FrozenGroupType } from '@/common/constant'; import type { BaseFacet } from '@/facet'; -import type { PanelBBox } from '@/facet/bbox/panelBBox'; +import type { PanelBBox } from '@/facet/bbox/panel-bbox'; export const parseCSV = (csv: string, header?: string[]) => { const DELIMITER = ','; @@ -53,7 +63,11 @@ export const sleep = async (timeout = 0) => { }); }; -export const createFakeSpreadSheet = () => { +export const createFakeSpreadSheet = (config?: { + s2Options?: Partial; + s2DataConfig?: Partial; +}) => { + const { s2Options = {}, s2DataConfig = {} } = config || {}; const container = getContainer(); class FakeSpreadSheet extends EE { @@ -66,20 +80,15 @@ export const createFakeSpreadSheet = () => { const s2 = new FakeSpreadSheet() as unknown as SpreadSheet; - s2.options = { - ...DEFAULT_OPTIONS, - hdAdapter: false, - }; - s2.dataCfg = { - meta: [], - data: [], - fields: { - rows: [], - columns: [], - values: [], + s2.options = assembleOptions( + { + ...DEFAULT_OPTIONS, + hdAdapter: false, }, - sortParams: [], - }; + s2Options, + ); + + s2.dataCfg = assembleDataCfg({ sortParams: [] }, s2DataConfig); s2.container = new Canvas({ width: DEFAULT_OPTIONS.width!, height: DEFAULT_OPTIONS.height!, @@ -91,41 +100,52 @@ export const createFakeSpreadSheet = () => { getCellMultiData() { return []; }, + getField: jest.fn(), } as unknown as any; + + const layoutResult: LayoutResult = { + rowLeafNodes: [], + colLeafNodes: [], + rowNodes: [], + colNodes: [], + colsHierarchy: new Hierarchy(), + rowsHierarchy: new Hierarchy(), + }; + s2.facet = { panelBBox: { maxX: s2.options.width, maxY: s2.options.height, } as PanelBBox, - panelGroup: { - getChildren() { - return []; - }, - }, - foregroundGroup: { - getChildren() { - return []; - }, - }, - layoutResult: { - getCellMeta: jest.fn(), - rowLeafNodes: [], - colLeafNodes: [], - rowNodes: [], - colNodes: [], - }, + panelGroup: s2.container.appendChild(new Group()), + foregroundGroup: s2.container.appendChild(new Group()), + backgroundGroup: s2.container.appendChild(new Group()), + layoutResult, + getLayoutResult: () => layoutResult, getCellMeta: jest.fn(), getCellById: jest.fn(), - getCellChildrenNodes: jest.fn(), - getCells: jest.fn(), - getColCells: jest.fn(), - getRowCells: jest.fn(), - getDataCells: jest.fn(), - getRowNodes: jest.fn(), - getRowLeafNodes: jest.fn(), - getColNodes: jest.fn(), - getColLeafNodes: jest.fn(), - getInitColLeafNodes: jest.fn(), + getCellChildrenNodes: () => [], + getCells: () => [], + getColCells: () => [], + getRowCells: () => [], + getDataCells: () => [], + getRowNodes: () => [], + getRowLeafNodes: () => [], + getColNodes: () => [], + getColLeafNodes: () => [], + getInitColLeafNodes: () => [], + getHeaderCells: () => [], + getHiddenColumnsInfo: jest.fn(), + getCellAdaptiveHeight: jest.fn(), + getRowLeafNodeByIndex: jest.fn(), + getColLeafNodeByIndex: jest.fn(), + frozenGroupInfo: { + [FrozenGroupType.FROZEN_ROW]: {}, + [FrozenGroupType.FROZEN_COL]: {}, + [FrozenGroupType.FROZEN_TRAILING_ROW]: {}, + [FrozenGroupType.FROZEN_TRAILING_COL]: {}, + }, + cornerBBox: {}, } as unknown as BaseFacet; s2.container.render = jest.fn(); s2.store = new Store(); @@ -148,11 +168,25 @@ export const createFakeSpreadSheet = () => { s2.getCell = jest.fn(); s2.isHierarchyTreeType = jest.fn(); s2.facet.getRowNodes = jest.fn().mockReturnValue([]); + s2.facet.getCells = jest.fn().mockReturnValue([]); s2.getCanvasElement = () => s2.container.getContextService().getDomElement() as HTMLCanvasElement; s2.isCustomHeaderFields = jest.fn(() => false); s2.isCustomRowFields = jest.fn(() => false); s2.isCustomColumnFields = jest.fn(() => false); + s2.isValueInCols = jest.fn(); + s2.isCustomHeaderFields = jest.fn(); + s2.isCustomColumnFields = jest.fn(); + s2.isCustomRowFields = jest.fn(); + s2.getTotalsConfig = jest.fn(); + s2.getLayoutWidthType = jest.fn(); + s2.enableFrozenHeaders = jest.fn(); + s2.measureTextWidth = jest.fn(); + s2.isFrozenRowHeader = jest.fn(); + s2.theme = getTheme({ + name: 'default', + spreadsheet: s2, + }) as InternalFullyTheme; const interaction = new RootInteraction(s2 as unknown as SpreadSheet); @@ -275,7 +309,7 @@ export const createFederatedMouseEvent = ( }; export const createTableSheet = ( - s2Options: S2Options, + s2Options: S2Options | null, { useSimpleData } = { useSimpleData: true }, ) => new TableSheet( diff --git a/packages/s2-core/__tests__/util/index.ts b/packages/s2-core/__tests__/util/index.ts index ef6dfcd39d..195f07e5d6 100644 --- a/packages/s2-core/__tests__/util/index.ts +++ b/packages/s2-core/__tests__/util/index.ts @@ -1,36 +1,38 @@ -import { data, totalData, meta } from 'tests/data/mock-dataset.json'; +import { data, meta, totalData } from 'tests/data/mock-dataset.json'; import { + DEFAULT_DATA_CONFIG, DEFAULT_OPTIONS, + S2Event, + SpreadSheet, type S2DataConfig, type S2Options, - DEFAULT_DATA_CONFIG, - SpreadSheet, - S2Event, } from '@/index'; import { customMerge } from '@/utils'; -export const assembleOptions = (...options: Partial[]) => - customMerge( - DEFAULT_OPTIONS, - { debug: false, width: 600, height: 600 }, - ...options, - ); +export const assembleOptions = (...options: Partial[]) => { + const s2Options: S2Options = { + debug: false, + width: 600, + height: 600, + }; + + return customMerge(DEFAULT_OPTIONS, s2Options, ...options); +}; -export const assembleDataCfg = (...dataCfg: Partial[]) => - customMerge( - DEFAULT_DATA_CONFIG, - { - fields: { - rows: ['province', 'city'], - columns: ['type', 'sub_type'], - values: ['number'], - valueInCols: true, - }, - meta, - data: data.concat(totalData as any), +export const assembleDataCfg = (...dataCfg: Partial[]) => { + const s2DataCfg: S2DataConfig = { + fields: { + rows: ['province', 'city'], + columns: ['type', 'sub_type'], + values: ['number'], + valueInCols: true, }, - ...dataCfg, - ); + meta, + data: data.concat(totalData as any), + }; + + return customMerge(DEFAULT_DATA_CONFIG, s2DataCfg, ...dataCfg); +}; export const TOTALS_OPTIONS: S2Options['totals'] = { row: { diff --git a/packages/s2-core/package.json b/packages/s2-core/package.json index 44d1402cb0..2c47ca6b39 100644 --- a/packages/s2-core/package.json +++ b/packages/s2-core/package.json @@ -46,9 +46,10 @@ "build:umd": "cross-env FORMAT=umd rollup -c rollup.config.mjs", "build:analysis": "cross-env FORMAT=esm ANALYSIS=true rollup -c rollup.config.mjs", "build:dts": "run-s dts:*", + "build:size-limit": "size-limit", + "build:size-limit-json": "yarn build:size-limit --json", "dts:build": "tsc -p tsconfig.declaration.json", "dts:extract": "cross-env LIB=s2-core node ../../scripts/dts.js", - "bundle:size": "bundlesize", "watch": "rimraf esm && pnpm build:esm -w", "test:live": "node ./scripts/test-live.mjs", "sync-event": "node ./scripts/sync-event.mjs", @@ -85,14 +86,15 @@ "*.css", "dist/*" ], - "bundlesize": [ + "size-limit": [ { "path": "./dist/index.min.js", - "maxSize": "300 kB" + "import": "{ createComponent }", + "limit": "200 kB" }, { "path": "./dist/style.min.css", - "maxSize": "5 kB" + "limit": "5 kB" } ], "publishConfig": { diff --git a/packages/s2-core/src/cell/base-cell.ts b/packages/s2-core/src/cell/base-cell.ts index e94f7f4b23..435531a4cc 100644 --- a/packages/s2-core/src/cell/base-cell.ts +++ b/packages/s2-core/src/cell/base-cell.ts @@ -156,6 +156,12 @@ export abstract class BaseCell extends Group { condition: Condition, ): ConditionMappingResult | undefined | null; + protected abstract getBackgroundColor(): { + backgroundColor: string | undefined; + backgroundColorOpacity: number | undefined; + intelligentReverseTextColor: boolean; + }; + public constructor( meta: T, spreadsheet: SpreadSheet, @@ -300,6 +306,14 @@ export abstract class BaseCell extends Group { return this.linkFieldShape; } + public getBackgroundShape() { + return this.backgroundShape; + } + + public getStateShapes() { + return this.stateShapes; + } + protected getResizeAreaStyle(): ResizeArea { return this.getStyle('resizeArea') as ResizeArea; } @@ -353,7 +367,7 @@ export abstract class BaseCell extends Group { } /** - * 绘制hover悬停,刷选的外框 + * 绘制 hover 悬停,刷选的外框 */ protected drawInteractiveBorderShape() { this.stateShapes.set( @@ -361,27 +375,11 @@ export abstract class BaseCell extends Group { renderRect(this, { ...this.getBBoxByType(CellClipBox.PADDING_BOX), visibility: 'hidden', + pointerEvents: 'none', }), ); } - protected abstract getBackgroundColor(): { - backgroundColor: string | undefined; - backgroundColorOpacity: number | undefined; - intelligentReverseTextColor: boolean; - }; - - protected drawBackgroundShape() { - const { backgroundColor, backgroundColorOpacity } = - this.getBackgroundColor(); - - this.backgroundShape = renderRect(this, { - ...this.getBBoxByType(), - fill: backgroundColor, - fillOpacity: backgroundColorOpacity, - }); - } - /** * 交互使用的背景色 */ @@ -391,10 +389,22 @@ export abstract class BaseCell extends Group { renderRect(this, { ...this.getBBoxByType(), visibility: 'hidden', + pointerEvents: 'none', }), ); } + protected drawBackgroundShape() { + const { backgroundColor, backgroundColorOpacity } = + this.getBackgroundColor(); + + this.backgroundShape = renderRect(this, { + ...this.getBBoxByType(), + fill: backgroundColor, + fillOpacity: backgroundColorOpacity, + }); + } + public renderTextShape( style: TextStyleProps, options?: RenderTextShapeOptions, @@ -750,4 +760,20 @@ export abstract class BaseCell extends Group { return getIconTotalWidth(this.groupedIcons[position], iconStyle); } + + protected getCrossBackgroundColor(rowIndex: number) { + const { crossBackgroundColor, backgroundColorOpacity } = + this.getStyle().cell; + + if (crossBackgroundColor && rowIndex % 2 === 0) { + // 隔行颜色的配置 + // 偶数行展示灰色背景,因为index是从0开始的 + return { backgroundColorOpacity, backgroundColor: crossBackgroundColor }; + } + + return { + backgroundColorOpacity, + backgroundColor: this.getStyle().cell.backgroundColor, + }; + } } diff --git a/packages/s2-core/src/cell/col-cell.ts b/packages/s2-core/src/cell/col-cell.ts index 60236dc167..ae843dac63 100644 --- a/packages/s2-core/src/cell/col-cell.ts +++ b/packages/s2-core/src/cell/col-cell.ts @@ -99,11 +99,41 @@ export class ColCell extends HeaderCell { return false; } - protected getTextPosition(): PointLike { - const { isLeaf } = this.meta; + /** + * 计算文本位置时候需要,留给后代根据情况(固定列)覆盖 + * @param viewport + * @returns viewport + */ + protected handleViewport(): AreaRange { + /** + * p(x, y) + * +----------------------+ x + * | +---------------> + * | viewport | |ColCell | + * | |-|---------+ + * +--------------------|-+ + * | + * y | + * v + * + * 将 viewport 坐标(p)映射到 col header 的坐标体系中,简化计算逻辑 + * + */ const { width, cornerWidth = 0, scrollX = 0 } = this.getHeaderConfig(); const scrollContainsRowHeader = !this.spreadsheet.isFrozenRowHeader(); + + const viewport: AreaRange = { + start: scrollX - (scrollContainsRowHeader ? cornerWidth : 0), + size: width + (scrollContainsRowHeader ? cornerWidth : 0), + }; + + return viewport; + } + + protected getTextPosition(): PointLike { + const { isLeaf } = this.meta; + const textStyle = this.getTextStyle(); const contentBox = this.getBBoxByType(CellClipBox.CONTENT_BOX); const iconStyle = this.getIconStyle()!; @@ -137,26 +167,7 @@ export class ColCell extends HeaderCell { return { x: textX, y: textY }; } - /** - * p(x, y) - * +----------------------+ x - * | +---------------> - * | viewport | |ColCell | - * | |-|---------+ - * +--------------------|-+ - * | - * y | - * v - * - * 将 viewport 坐标(p)映射到 col header 的坐标体系中,简化计算逻辑 - * - */ - const viewport: AreaRange = { - start: scrollX - (scrollContainsRowHeader ? cornerWidth : 0), - size: width + (scrollContainsRowHeader ? cornerWidth : 0), - }; - - this.handleViewport(viewport); + const viewport = this.handleViewport(); const { cell, icon } = this.getStyle()!; const { textAlign, textBaseline } = this.getTextStyle(); @@ -270,7 +281,7 @@ export class ColCell extends HeaderCell { style: { ...attrs.style, x: 0, - y: y + height - resizeStyle.size! / 2, + y: y + height - resizeStyle.size!, width: resizeAreaWidth, }, }, @@ -369,7 +380,7 @@ export class ColCell extends HeaderCell { { style: { ...attrs.style, - x: offsetX + width - resizeStyle.size! / 2, + x: offsetX + width - resizeStyle.size!, y: offsetY, height, }, @@ -435,11 +446,22 @@ export class ColCell extends HeaderCell { } this.addExpandColumnSplitLine(); - this.addExpandColumnIcon(); + this.addExpandColumnIcons(); } - protected addExpandColumnIcon() { - const iconConfig = this.getExpandColumnIconConfig(); + protected addExpandColumnIcons() { + const isLastColumn = this.isLastColumn(); + + this.addExpandColumnIcon(isLastColumn); + + // 如果当前节点的兄弟节点 (前/后) 都被隐藏了, 隐藏后当前节点变为最后一个节点, 需要渲染两个展开按钮, 一个展开[前], 一个展开[后] + if (this.isAllDisplaySiblingNodeHidden() && isLastColumn) { + this.addExpandColumnIcon(false); + } + } + + private addExpandColumnIcon(isLastColumn: boolean) { + const iconConfig = this.getExpandColumnIconConfig(isLastColumn); const icon = renderIcon(this, { ...iconConfig, name: 'ExpandColIcon', @@ -452,12 +474,12 @@ export class ColCell extends HeaderCell { } // 在隐藏的下一个兄弟节点的起始坐标显示隐藏提示线和展开按钮, 如果是尾元素, 则显示在前一个兄弟节点的结束坐标 - protected getExpandColumnIconConfig() { + protected getExpandColumnIconConfig(isLastColumn: boolean) { const { size = 0 } = this.getExpandIconTheme(); const { x, y, width, height } = this.getBBoxByType(); const baseIconX = x - size; - const iconX = this.isLastColumn() ? baseIconX + width : baseIconX; + const iconX = isLastColumn ? baseIconX + width : baseIconX; const iconY = y + height / 2 - size / 2; return { @@ -472,12 +494,20 @@ export class ColCell extends HeaderCell { return isLastColumnAfterHidden(this.spreadsheet, this.meta.id); } - /** - * 计算文本位置时候需要,留给后代根据情况(固定列)覆盖 - * @param viewport - * @returns viewport - */ - protected handleViewport(viewport: AreaRange): AreaRange { - return viewport; + protected isAllDisplaySiblingNodeHidden() { + const { id } = this.meta; + const lastHiddenColumnDetail = this.spreadsheet.store.get( + 'hiddenColumnsDetail', + [], + ); + + const isPrevSiblingNodeHidden = lastHiddenColumnDetail.find( + ({ displaySiblingNode }) => displaySiblingNode?.next?.id === id, + ); + const isNextSiblingNodeHidden = lastHiddenColumnDetail.find( + ({ displaySiblingNode }) => displaySiblingNode?.prev?.id === id, + ); + + return isNextSiblingNodeHidden && isPrevSiblingNodeHidden; } } diff --git a/packages/s2-core/src/cell/corner-cell.ts b/packages/s2-core/src/cell/corner-cell.ts index dd61264bab..2d484d095b 100644 --- a/packages/s2-core/src/cell/corner-cell.ts +++ b/packages/s2-core/src/cell/corner-cell.ts @@ -7,7 +7,7 @@ import { ResizeDirectionType, S2Event, } from '../common/constant'; -import type { FormatResult, TextTheme } from '../common/interface'; +import type { FormatResult } from '../common/interface'; import { CellBorderPosition, CellClipBox } from '../common/interface'; import { CornerNodeType } from '../common/interface/node'; import { CustomRect } from '../engine'; @@ -15,6 +15,7 @@ import type { CornerHeaderConfig } from '../facet/header/interface'; import { getHorizontalTextIconPosition, getVerticalIconPosition, + getVerticalTextPosition, } from '../utils/cell/cell'; import { formattedFieldValue } from '../utils/cell/header-cell'; import { renderTreeIcon } from '../utils/g-renders'; @@ -40,8 +41,6 @@ export class CornerCell extends HeaderCell { return [CellBorderPosition.TOP, CellBorderPosition.LEFT]; } - public update() {} - protected initCell() { super.initCell(); this.resetTextAndConditionIconShapes(); @@ -51,53 +50,7 @@ export class CornerCell extends HeaderCell { this.drawActionAndConditionIcons(); this.drawBorders(); this.drawResizeArea(); - } - - public drawTextShape() { - const { x, y, height, width } = this.getBBoxByType(CellClipBox.CONTENT_BOX); - const textStyle = this.getTextStyle(); - const cornerText = this.getFieldValue(); - const maxWidth = this.getMaxTextWidth(); - - const { textX, leftIconX, rightIconX } = getHorizontalTextIconPosition({ - bbox: { - x: x + this.getTreeIconWidth(), - y, - width: width - this.getTreeIconWidth(), - height, - }, - textAlign: textStyle.textAlign!, - textWidth: this.getActualTextWidth(), - groupedIcons: this.groupedIcons, - iconStyle: this.getIconStyle()!, - }); - - const textY = y + height / 2; - - this.renderTextShape({ - ...textStyle, - x: textX, - y: textY, - text: cornerText, - wordWrapWidth: maxWidth, - }); - - const { size = 0 } = this.getStyle()!.icon!; - const iconY = getVerticalIconPosition( - size, - y + height / 2, - size, - textStyle.textBaseline!, - ); - - this.leftIconPosition = { - x: leftIconX, - y: iconY, - }; - this.rightIconPosition = { - x: rightIconX, - y: iconY, - }; + this.update(); } /** @@ -228,7 +181,7 @@ export class CornerCell extends HeaderCell { { style: { ...attrs.style, - x: offsetX + width - resizeStyle.size! / 2, + x: offsetX + width - resizeStyle.size!, y: offsetY, height: this.isLastRowCornerCell() ? headerHeight : height, }, @@ -249,20 +202,6 @@ export class CornerCell extends HeaderCell { return this.showTreeIcon() ? size! + margin!.right! : 0; } - protected getTextStyle(): TextTheme { - const { text, bolderText } = this.getStyle()!; - const cornerTextStyle = this.isBolderText() ? text : bolderText; - - const textStyle = - this.getContainConditionMappingResultTextStyle(cornerTextStyle); - - return { - ...textStyle, - // 角头因为要折行,所以在都是按照 middle 来计算,这里写死,不然用户配置了 baseline,会导致计算错误 - textBaseline: 'middle', - }; - } - protected getMaxTextWidth(): number { const { width } = this.getBBoxByType(CellClipBox.CONTENT_BOX); @@ -272,9 +211,46 @@ export class CornerCell extends HeaderCell { } protected getTextPosition(): PointLike { + const contentBox = this.getBBoxByType(CellClipBox.CONTENT_BOX); + const { x, y, height, width } = contentBox; + + const textStyle = this.getTextStyle(); + + const { textX, leftIconX, rightIconX } = getHorizontalTextIconPosition({ + bbox: { + x: x + this.getTreeIconWidth(), + y, + width: width - this.getTreeIconWidth(), + height, + }, + textAlign: textStyle.textAlign!, + textWidth: this.getActualTextWidth(), + groupedIcons: this.groupedIcons, + iconStyle: this.getIconStyle()!, + }); + + const textY = getVerticalTextPosition(contentBox, textStyle.textBaseline!); + + const { size = 0 } = this.getStyle()!.icon!; + const iconY = getVerticalIconPosition( + size, + textY, + textStyle.fontSize!, + textStyle.textBaseline!, + ); + + this.leftIconPosition = { + x: leftIconX, + y: iconY, + }; + this.rightIconPosition = { + x: rightIconX, + y: iconY, + }; + return { - x: 0, - y: 0, + x: textX, + y: textY, }; } diff --git a/packages/s2-core/src/cell/data-cell.ts b/packages/s2-core/src/cell/data-cell.ts index 76a086c80c..a81850c762 100644 --- a/packages/s2-core/src/cell/data-cell.ts +++ b/packages/s2-core/src/cell/data-cell.ts @@ -28,6 +28,7 @@ import type { MultiData, TextTheme, ViewMeta, + ViewMetaData, ViewMetaIndexType, } from '../common/interface'; import { @@ -37,7 +38,7 @@ import { type IconCondition, type InteractionStateTheme, } from '../common/interface'; -import { getFieldValueOfViewMetaData } from '../data-set/cell-data'; +import { CellData } from '../data-set/cell-data'; import { getHorizontalTextIconPosition, getVerticalIconPosition, @@ -67,6 +68,12 @@ import type { RawData } from './../common/interface/s2DataConfig'; * 3、left rect area is interval(in left) and text(in right) */ export class DataCell extends BaseCell { + /** + * 用于 merge cell 中用于绘制 border 的位置信息 + * @see packages/s2-core/src/facet/base-facet.ts L1319 + */ + position: [rowIndex: number, colIndex: number]; + // condition icon 坐标 iconPosition: PointLike; @@ -180,15 +187,18 @@ export class DataCell extends BaseCell { return; } - if (this.spreadsheet.options.interaction?.hoverHighlight) { + const { currentRow, currentCol } = + this.spreadsheet.interaction.getHoverHighlight(); + + if (currentRow || currentCol) { // 如果当前是hover,要绘制出十字交叉的行列样式 const currentColIndex = this.meta.colIndex; const currentRowIndex = this.meta.rowIndex; // 当视图内的 cell 行列 index 与 hover 的 cell 一致,绘制hover的十字样式 if ( - currentColIndex === currentHoverCell?.colIndex || - currentRowIndex === currentHoverCell?.rowIndex + (currentCol && currentColIndex === currentHoverCell?.colIndex) || + (currentRow && currentRowIndex === currentHoverCell?.rowIndex) ) { this.updateByState(InteractionStateName.HOVER); } else { @@ -259,18 +269,18 @@ export class DataCell extends BaseCell { this.generateIconConfig(); this.drawBackgroundShape(); this.drawInteractiveBgShape(); + if (!this.shouldHideRowSubtotalData()) { this.drawConditionIntervalShape(); } - this.drawInteractiveBorderShape(); - if (!this.shouldHideRowSubtotalData()) { this.drawTextShape(); this.drawConditionIconShapes(); } this.drawBorders(); + this.drawInteractiveBorderShape(); this.update(); } @@ -308,17 +318,24 @@ export class DataCell extends BaseCell { } protected shouldHideRowSubtotalData() { + const { rowId, rowIndex } = this.meta; + // 如果该格子是被下钻的格子,下钻格子本身来说是明细格子,因为下钻变成了小计格子,是应该展示的 + const drillDownIdPathMap = this.spreadsheet.store.get('drillDownIdPathMap'); + + if (drillDownIdPathMap?.has(rowId!)) { + return false; + } + const { row = {} } = this.spreadsheet.options.totals ?? {}; - const { rowIndex } = this.meta; - const node = this.spreadsheet.facet.getRowLeafNodes()[rowIndex]; + const node = this.spreadsheet.facet?.getRowLeafNodeByIndex(rowIndex); const isRowSubTotal = !node?.isGrandTotals && node?.isTotals; - /* + /** * 在树状结构时,如果单元格本身是行小计,但是行小计配置又未开启时 - * 不过能否查到实际的数据,都不应该展示 + * 不管能否查到实际的数据,都不应该展示 */ return ( - this.spreadsheet.options.hierarchyType === 'tree' && + this.spreadsheet.isHierarchyTreeType() && !row.showSubTotals && isRowSubTotal ); @@ -329,7 +346,7 @@ export class DataCell extends BaseCell { return { value: null, - /* + /** * 这里使用默认的placeholder,而不是空字符串,是为了防止后续使用用户自定义的placeholder * 比如用户自定义 placeholder 为 0, 那行小计也会显示0,也很有迷惑性,显示 - 更为合理 */ @@ -392,18 +409,12 @@ export class DataCell extends BaseCell { } public getBackgroundColor() { - const cellStyle = this.getStyle()?.cell; - const { crossBackgroundColor, backgroundColorOpacity } = cellStyle!; - - let backgroundColor = cellStyle!.backgroundColor; - - if (crossBackgroundColor && this.meta.rowIndex % 2 === 0) { - /* - * 隔行颜色的配置 - * 偶数行展示灰色背景,因为index是从0开始的 - */ - backgroundColor = crossBackgroundColor; - } + const backgroundColorByCross = this.getCrossBackgroundColor( + this.meta.rowIndex, + ); + const backgroundColor = backgroundColorByCross.backgroundColor; + const backgroundColorOpacity = + backgroundColorByCross.backgroundColorOpacity; if (this.shouldHideRowSubtotalData()) { return { @@ -419,7 +430,7 @@ export class DataCell extends BaseCell { ); } - // dataCell根据state 改变当前样式, + // dataCell 根据 state 改变当前样式, protected changeRowColSelectState(indexType: ViewMetaIndexType) { const { interaction } = this.spreadsheet; const currentIndex = get(this.meta, indexType); @@ -481,7 +492,7 @@ export class DataCell extends BaseCell { ? this.spreadsheet.dataSet.getCellData({ query: { rowIndex: this.meta.rowIndex }, }) - : getFieldValueOfViewMetaData(this.meta.data); + : CellData.getFieldValue(this.meta.data as ViewMetaData); return condition?.mapping(value, rowDataInfo as RawData, this); } diff --git a/packages/s2-core/src/cell/header-cell.ts b/packages/s2-core/src/cell/header-cell.ts index f2039de51b..69a5dee2a5 100644 --- a/packages/s2-core/src/cell/header-cell.ts +++ b/packages/s2-core/src/cell/header-cell.ts @@ -82,7 +82,7 @@ export abstract class HeaderCell< return super.shouldInit() && !this.isShallowRender(); } - protected handleRestOptions(...[headerConfig]: [T]) { + protected handleRestOptions(...[headerConfig]: [T, unknown]) { this.headerConfig = { ...headerConfig }; const { value, query } = this.meta; @@ -139,13 +139,32 @@ export abstract class HeaderCell< } protected getFormattedFieldValue(): FormatResult { - const { value, field } = this.meta; + const { isTotalRoot, isGrandTotals, value } = this.meta; - const formatter = this.spreadsheet.dataSet.getFieldFormatter(field); - // TODO: formatter 简化成两个参数 formatter(value, this,meta) - const formattedValue = formatter! - ? formatter(value, undefined, this.meta) - : value; + const formatter = this.spreadsheet.dataSet.getFieldFormatter( + this.meta.field, + ); + + /** + * 如果是 table mode,列头不需要被格式化 + * 树状模式下,小计是父维度本身,需要被格式化,此时只有总计才不需要被格式化 + * 平铺模式下,总计/小计 文字单元格,不需要被格式化 + * 自定义树模式下,没有总计小计概念,isTotals 均为 false, 所以不受影响 + */ + let shouldFormat = true; + + if (this.spreadsheet.isTableMode()) { + shouldFormat = false; + } else if (this.spreadsheet.isHierarchyTreeType()) { + shouldFormat = !(isGrandTotals && isTotalRoot); + } else { + shouldFormat = !isTotalRoot; + } + + const formattedValue = + shouldFormat && formatter + ? formatter(value, undefined, this.meta) + : value; return { formattedValue, @@ -415,7 +434,11 @@ export abstract class HeaderCell< public update() { const { interaction } = this.spreadsheet; const stateInfo = interaction?.getState(); - const cells = interaction?.getCells([CellType.COL_CELL, CellType.ROW_CELL]); + const cells = interaction?.getCells([ + CellType.CORNER_CELL, + CellType.COL_CELL, + CellType.ROW_CELL, + ]); if (!first(cells)) { return; @@ -423,6 +446,7 @@ export abstract class HeaderCell< switch (stateInfo?.stateName) { case InteractionStateName.SELECTED: + case InteractionStateName.BRUSH_SELECTED: this.handleSelect(cells, stateInfo?.nodes); break; case InteractionStateName.HOVER_FOCUS: diff --git a/packages/s2-core/src/cell/index.ts b/packages/s2-core/src/cell/index.ts index c6ca335870..80545190fd 100644 --- a/packages/s2-core/src/cell/index.ts +++ b/packages/s2-core/src/cell/index.ts @@ -4,11 +4,11 @@ import { CornerCell } from './corner-cell'; import { DataCell } from './data-cell'; import { HeaderCell } from './header-cell'; import { MergedCell } from './merged-cell'; -import { RowCell } from './row-cell'; import { TableColCell } from './table-col-cell'; import { TableCornerCell } from './table-corner-cell'; import { TableDataCell } from './table-data-cell'; import { TableSeriesNumberCell } from './table-series-number-cell'; +import { RowCell } from './row-cell'; import { SeriesNumberCell } from './series-number-cell'; export { @@ -16,7 +16,6 @@ export { TableColCell, TableSeriesNumberCell, TableDataCell, - SeriesNumberCell, RowCell, ColCell, DataCell, @@ -24,4 +23,5 @@ export { CornerCell, BaseCell, HeaderCell, + SeriesNumberCell, }; diff --git a/packages/s2-core/src/cell/merged-cell.ts b/packages/s2-core/src/cell/merged-cell.ts index 064fef3a09..b3d84810b2 100644 --- a/packages/s2-core/src/cell/merged-cell.ts +++ b/packages/s2-core/src/cell/merged-cell.ts @@ -1,18 +1,21 @@ import { isEmpty, isObject } from 'lodash'; import { CellType } from '../common/constant'; -import type { ViewMeta } from '../common/interface'; -import type { S2CellType } from '../common/interface/interaction'; +import { CellBorderPosition, type ViewMeta } from '../common/interface'; import type { SpreadSheet } from '../sheet-type'; -import { renderPolygon } from '../utils/g-renders'; -import { getPolygonPoints } from '../utils/interaction/merge-cell'; +import { renderLine, renderPolygon } from '../utils/g-renders'; +import { + getPolygonPoints, + getRightAndBottomCells, +} from '../utils/interaction/merge-cell'; import { drawObjectText } from '../utils/text'; +import { getBorderPositionAndStyle } from '../utils'; import { DataCell } from './data-cell'; /** * Cell for panelGroup area */ export class MergedCell extends DataCell { - public cells: S2CellType[]; + public cells: DataCell[]; public get cellType() { return CellType.MERGED_CELL; @@ -20,13 +23,13 @@ export class MergedCell extends DataCell { public constructor( spreadsheet: SpreadSheet, - cells: S2CellType[], + cells: DataCell[], meta?: ViewMeta, ) { super(meta!, spreadsheet, cells); } - handleRestOptions(...[cells]: [S2CellType[]]) { + handleRestOptions(...[cells]: [DataCell[]]) { this.cells = cells; } @@ -35,9 +38,9 @@ export class MergedCell extends DataCell { protected initCell() { this.resetTextAndConditionIconShapes(); // TODO:1、交互态扩展; 2、合并后的单元格文字布局及文字内容(目前参考Excel合并后只保留第一个单元格子的数据) - this.conditions = this.spreadsheet.options.conditions!; this.drawBackgroundShape(); this.drawTextShape(); + this.drawBorders(); } /** @@ -49,7 +52,6 @@ export class MergedCell extends DataCell { this.backgroundShape = renderPolygon(this, { points: allPoints, - stroke: cellTheme!.horizontalBorderColor, fill: cellTheme!.backgroundColor, }); } @@ -65,4 +67,54 @@ export class MergedCell extends DataCell { super.drawTextShape(); } } + + override drawBorders(): void { + const { right, bottom, bottomRightCornerCell } = getRightAndBottomCells( + this.cells, + ); + + right.forEach((cell) => { + const { position, style } = getBorderPositionAndStyle( + CellBorderPosition.RIGHT, + cell.getBBoxByType(), + cell.getStyle()?.cell!, + ); + + renderLine(this, { ...position, ...style }); + }); + + bottom.forEach((cell) => { + const { position, style } = getBorderPositionAndStyle( + CellBorderPosition.BOTTOM, + cell.getBBoxByType(), + cell.getStyle()?.cell!, + ); + + renderLine(this, { ...position, ...style }); + }); + + bottomRightCornerCell.forEach((cell) => { + const { x, y, width, height } = cell.getBBoxByType(); + const { + horizontalBorderWidth = 0, + verticalBorderWidth = 0, + verticalBorderColor, + } = cell.getStyle()?.cell!; + + const x1 = x + width - verticalBorderWidth / 2; + const x2 = x1; + const y1 = y + height - horizontalBorderWidth; + const y2 = y + height; + + renderLine(this, { + x1, + x2, + y1, + y2, + lineWidth: verticalBorderWidth, + stroke: verticalBorderColor, + strokeOpacity: verticalBorderWidth, + }); + }); + } } diff --git a/packages/s2-core/src/cell/row-cell.ts b/packages/s2-core/src/cell/row-cell.ts index bb8d1bb688..2fcf24e1a4 100644 --- a/packages/s2-core/src/cell/row-cell.ts +++ b/packages/s2-core/src/cell/row-cell.ts @@ -1,7 +1,8 @@ import type { PointLike } from '@antv/g'; -import { find, get } from 'lodash'; +import { find, get, merge } from 'lodash'; import { CellType, + FrozenGroupType, KEY_GROUP_ROW_RESIZE_AREA, ResizeAreaEffect, ResizeDirectionType, @@ -26,6 +27,7 @@ import { getResizeAreaAttrs, } from '../utils/interaction/resize'; import { isMobile } from '../utils/is-mobile'; +import type { FrozenFacet } from '../facet/frozen-facet'; import type { SimpleBBox } from './../engine/interface'; import { adjustTextIconPositionWhileScrolling } from './../utils/cell/text-scrolling'; import { shouldAddResizeArea } from './../utils/interaction/resize'; @@ -64,6 +66,16 @@ export class RowCell extends HeaderCell { this.update(); } + public getBackgroundColor() { + const { backgroundColor, backgroundColorOpacity } = + this.getCrossBackgroundColor(this.meta.rowIndex); + + return merge( + { backgroundColor, backgroundColorOpacity }, + this.getBackgroundConditionFill(), + ); + } + protected showTreeIcon() { return this.spreadsheet.isHierarchyTreeType() && !this.meta.isLeaf; } @@ -232,6 +244,7 @@ export class RowCell extends HeaderCell { viewportHeight: headerHeight, scrollX = 0, scrollY = 0, + spreadsheet, } = this.getHeaderConfig(); const resizeAreaBBox: SimpleBBox = { @@ -241,25 +254,30 @@ export class RowCell extends HeaderCell { height: resizeStyle.size!, }; + const isFrozen = this.getMeta().isFrozen; + + const frozenRowGroupHeight = (spreadsheet.facet as FrozenFacet) + .frozenGroupInfo[FrozenGroupType.FROZEN_ROW]?.height; + const resizeClipAreaBBox: SimpleBBox = { x: 0, - y: 0, + y: frozenRowGroupHeight, width: headerWidth, height: headerHeight, }; if ( + !isFrozen && !shouldAddResizeArea(resizeAreaBBox, resizeClipAreaBBox, { scrollX, scrollY, - }) || - !position + }) ) { return; } const offsetX = position.x + x - scrollX; - const offsetY = position.y + y - scrollY; + const offsetY = position.y + y - (isFrozen ? 0 : scrollY); const resizeAreaWidth = this.spreadsheet.isFrozenRowHeader() ? headerWidth - position.x - (x - scrollX) @@ -282,7 +300,7 @@ export class RowCell extends HeaderCell { style: { ...attrs.style, x: offsetX, - y: offsetY + height - resizeStyle.size! / 2, + y: offsetY + height - resizeStyle.size!, width: resizeAreaWidth, }, }, @@ -355,16 +373,26 @@ export class RowCell extends HeaderCell { }; } + protected handleViewport() { + const { scrollY, viewportHeight, spreadsheet } = this.getHeaderConfig(); + + const frozenRowGroupHeight = (spreadsheet.facet as FrozenFacet) + .frozenGroupInfo[FrozenGroupType.FROZEN_ROW].height; + + const viewport: AreaRange = { + start: this.getMeta().isFrozen ? 0 : scrollY! + frozenRowGroupHeight, + size: viewportHeight - frozenRowGroupHeight, + }; + + return viewport; + } + protected getTextPosition(): PointLike { - const { scrollY, viewportHeight } = this.getHeaderConfig(); const textArea = this.getTextArea(); const textStyle = this.getTextStyle(); const { cell, icon: iconStyle } = this.getStyle(); - const viewport: AreaRange = { - start: scrollY!, - size: viewportHeight, - }; + const viewport = this.handleViewport(); const { textStart } = adjustTextIconPositionWhileScrolling( viewport, diff --git a/packages/s2-core/src/cell/table-col-cell.ts b/packages/s2-core/src/cell/table-col-cell.ts index 8f8875f506..11cc2c1d99 100644 --- a/packages/s2-core/src/cell/table-col-cell.ts +++ b/packages/s2-core/src/cell/table-col-cell.ts @@ -1,6 +1,7 @@ import { find } from 'lodash'; import { ColCell } from '../cell/col-cell'; import { + FrozenGroupType, HORIZONTAL_RESIZE_AREA_KEY_PRE, KEY_GROUP_FROZEN_COL_RESIZE_AREA, } from '../common/constant'; @@ -8,15 +9,14 @@ import type { FormatResult } from '../common/interface'; import type { AreaRange } from '../common/interface/scroll'; import type { SimpleBBox } from '../engine'; import type { BaseHeaderConfig } from '../facet/header'; -import { getNodeRoot, isFrozenCol, isFrozenTrailingCol } from '../facet/utils'; import { formattedFieldValue } from '../utils/cell/header-cell'; import { renderRect } from '../utils/g-renders'; import { getOrCreateResizeAreaGroupById, shouldAddResizeArea, } from '../utils/interaction/resize'; -import { getFrozenColWidth } from '../utils/layout/frozen'; import { getSortTypeIcon } from '../utils/sort-action'; +import type { FrozenFacet } from '../facet/frozen-facet'; export class TableColCell extends ColCell { protected handleRestOptions(...[headerConfig]: [BaseHeaderConfig]) { @@ -33,18 +33,6 @@ export class TableColCell extends ColCell { }; } - protected isFrozenCell() { - const { colCount = 0, trailingColCount = 0 } = - this.spreadsheet.options.frozen!; - const colNodes = this.spreadsheet.facet?.getColNodes(0); - const { colIndex } = getNodeRoot(this.meta); - - return ( - isFrozenCol(colIndex, colCount) || - isFrozenTrailingCol(colIndex, trailingColCount, colNodes?.length) - ); - } - protected getFormattedFieldValue(): FormatResult { return formattedFieldValue( this.meta, @@ -53,7 +41,7 @@ export class TableColCell extends ColCell { } protected shouldAddVerticalResizeArea() { - if (this.isFrozenCell()) { + if (this.getMeta().isFrozen) { return true; } @@ -62,6 +50,7 @@ export class TableColCell extends ColCell { scrollY, width: headerWidth, height: headerHeight, + spreadsheet, } = this.getHeaderConfig(); const { x, y, width, height } = this.getBBoxByType(); const resizeStyle = this.getResizeAreaStyle(); @@ -73,17 +62,15 @@ export class TableColCell extends ColCell { height, }; - const frozenWidth = getFrozenColWidth( - this.spreadsheet.facet.getColLeafNodes(), - this.spreadsheet.options.frozen!, - ); + const frozenGroupInfo = (spreadsheet.facet as FrozenFacet).frozenGroupInfo; + const colWidth = frozenGroupInfo[FrozenGroupType.FROZEN_COL].width; + const trailingColWidth = + frozenGroupInfo[FrozenGroupType.FROZEN_TRAILING_COL].width; + const resizeClipAreaBBox: SimpleBBox = { - x: frozenWidth.frozenColWidth, + x: colWidth, y: 0, - width: - headerWidth - - frozenWidth.frozenColWidth - - frozenWidth.frozenTrailingColWidth, + width: headerWidth - colWidth - trailingColWidth, height: headerHeight, }; @@ -97,7 +84,7 @@ export class TableColCell extends ColCell { const { x, y } = this.meta; const { scrollX = 0, position } = this.getHeaderConfig(); - if (this.isFrozenCell()) { + if (this.getMeta().isFrozen) { return { x: position?.x + x, y: position?.y + y, @@ -111,9 +98,7 @@ export class TableColCell extends ColCell { } protected getColResizeArea() { - const isFrozenCell = this.isFrozenCell(); - - if (!isFrozenCell) { + if (!this.getMeta().isFrozen) { return super.getColResizeArea(); } @@ -142,16 +127,20 @@ export class TableColCell extends ColCell { } protected drawBackgroundShape() { - const { backgroundColor } = this.getStyle()!.cell!; + const { backgroundColor, backgroundColorOpacity } = + this.getStyle()!.cell! || {}; this.backgroundShape = renderRect(this, { ...this.getBBoxByType(), fill: backgroundColor, + fillOpacity: backgroundColorOpacity, }); } - protected handleViewport(viewport: AreaRange): AreaRange { - if (this.isFrozenCell()) { + protected handleViewport(): AreaRange { + const viewport = super.handleViewport(); + + if (this.getMeta().isFrozen) { viewport.start = 0; } diff --git a/packages/s2-core/src/cell/table-data-cell.ts b/packages/s2-core/src/cell/table-data-cell.ts index 2f73f83cc9..13fa924d50 100644 --- a/packages/s2-core/src/cell/table-data-cell.ts +++ b/packages/s2-core/src/cell/table-data-cell.ts @@ -1,6 +1,7 @@ import { Frame } from '../facet/header/frame'; import { DataCell } from '../cell/data-cell'; import { + FrozenGroupType, KEY_GROUP_FROZEN_ROW_RESIZE_AREA, KEY_GROUP_ROW_RESIZE_AREA, ResizeAreaEffect, @@ -13,8 +14,10 @@ import { import { getOrCreateResizeAreaGroupById, getResizeAreaAttrs, + shouldAddResizeArea, } from '../utils/interaction/resize'; -import { CustomRect } from '../engine'; +import { CustomRect, type SimpleBBox } from '../engine'; +import type { FrozenFacet } from '../facet/frozen-facet'; import { BaseCell } from './base-cell'; export class TableDataCell extends DataCell { @@ -32,7 +35,7 @@ export class TableDataCell extends DataCell { protected shouldDrawResizeArea() { // 每一行直绘制一条贯穿式 resize 热区 - const id = String(this.meta.rowIndex); + const id = `${this.meta.rowIndex}`; const resizeArea = getOrCreateResizeAreaGroupById( this.spreadsheet, @@ -95,6 +98,36 @@ export class TableDataCell extends DataCell { let offsetY = y + headerHeight + Frame.getHorizontalBorderWidth(this.spreadsheet); + const frozenGroupInfo = (this.spreadsheet.facet as FrozenFacet) + .frozenGroupInfo; + const rowHeight = frozenGroupInfo[FrozenGroupType.FROZEN_ROW].height; + const rowTrailingHeight = + frozenGroupInfo[FrozenGroupType.FROZEN_TRAILING_ROW].height; + + const resizeAreaBBox: SimpleBBox = { + x: 0, + y: y + height - resizeStyle.size!, + width: headerWidth, + height: resizeStyle.size!, + }; + const resizeClipAreaBBox: SimpleBBox = { + x: 0, + y: rowHeight, + width: headerWidth, + height: + this.spreadsheet.facet.panelBBox.height - rowHeight - rowTrailingHeight, + }; + + if ( + !isFrozen && + !shouldAddResizeArea(resizeAreaBBox, resizeClipAreaBBox, { + scrollX: 0, + scrollY, + }) + ) { + return; + } + if (!isFrozen) { offsetY -= scrollY + paginationSy; } diff --git a/packages/s2-core/src/cell/table-series-number-cell.ts b/packages/s2-core/src/cell/table-series-number-cell.ts index 8464f6500c..d5a0faef26 100644 --- a/packages/s2-core/src/cell/table-series-number-cell.ts +++ b/packages/s2-core/src/cell/table-series-number-cell.ts @@ -6,7 +6,7 @@ export class TableSeriesNumberCell extends TableDataCell { public get cellType() { /* * 在行列冻结并且开启序号时 - * 那么在如果行头冻结2列,并且CellTypes设置成以前的RowCell时,【FrozenRowGroup的分割线】和【左上角和左下角的边框样式】样式会混乱 + * 如果行头冻结 2 列,并且 CellTypes 设置成以前的 RowCell 时,【 FrozenRowGroup 的分割线】和【左上角和左下角的边框样式】样式会混乱 * 因此下层在选择到序号时,需要将 cellType 修改为 RowCell, 保证交互逻辑统一: * packages/s2-core/src/utils/interaction/select-event.ts -> getCellMeta */ diff --git a/packages/s2-core/src/common/constant/basic.ts b/packages/s2-core/src/common/constant/basic.ts index afdf8dd69b..680fb04a8b 100644 --- a/packages/s2-core/src/common/constant/basic.ts +++ b/packages/s2-core/src/common/constant/basic.ts @@ -1,33 +1,21 @@ import { i18n } from '../i18n'; -export const VALUE_FIELD = '$$value$$'; -export const EXTRA_FIELD = '$$extra$$'; -export const EXTRA_COLUMN_FIELD = '$$extra_column$$'; - -export const SERIES_NUMBER_FIELD = '$$series_number$$'; - -export const TOTAL_VALUE = '$$total$$'; -export const MULTI_VALUE = '$$multi$$'; - -export const NULL_SYMBOL_ID = '$$null$$'; -export const UNDEFINED_SYMBOL_ID = '$$undefined$$'; - +// 约定这个 z-index 为 0 的 container 作为基准 export const BACK_GROUND_GROUP_CONTAINER_Z_INDEX = 0; -/* - * foregroundGroup 上的 children 层叠顺序 - * 约定这个 z-index 为 0 的 container 作为基准 - */ +// foregroundGroup 上的 children 层叠顺序 export const FRONT_GROUND_GROUP_CONTAINER_Z_INDEX = 3; -export const FRONT_GROUND_GROUP_COL_SCROLL_Z_INDEX = 3; -export const FRONT_GROUND_GROUP_COL_FROZEN_Z_INDEX = 4; +export const FRONT_GROUND_GROUP_SCROLL_Z_INDEX = 3; +export const FRONT_GROUND_GROUP_FROZEN_Z_INDEX = 4; export const FRONT_GROUND_GROUP_RESIZE_AREA_Z_INDEX = 5; export const FRONT_GROUND_GROUP_BRUSH_SELECTION_Z_INDEX = 5; // panelGroup 上的 children 层叠顺序 export const PANEL_GROUP_GROUP_CONTAINER_Z_INDEX = 1; export const PANEL_GROUP_SCROLL_GROUP_Z_INDEX = 1; -export const PANEL_GROUP_FROZEN_GROUP_Z_INDEX = 2; +export const PANEL_GRID_GROUP_Z_INDEX = 2; +export const PANEL_MERGE_GROUP_Z_INDEX = 3; +export const PANEL_GROUP_FROZEN_GROUP_Z_INDEX = 4; // group's key export const KEY_GROUP_BACK_GROUND = 'backGroundGroup'; @@ -52,6 +40,8 @@ export const KEY_GROUP_COL_SCROLL = 'colScrollGroup'; export const KEY_GROUP_COL_FROZEN = 'colFrozenGroup'; export const KEY_GROUP_COL_FROZEN_TRAILING = 'colFrozenTrailingGroup'; export const KEY_GROUP_GRID_GROUP = 'gridGroup'; +export const KEY_GROUP_ROW_SCROLL = 'rowScrollGroup'; +export const KEY_GROUP_ROW_HEADER_FROZEN = 'rowHeaderFrozenGroup'; export const HORIZONTAL_RESIZE_AREA_KEY_PRE = 'horizontal-resize-area-'; @@ -73,9 +63,6 @@ export enum MiniChartTypes { Bullet = 'bullet', } -// 线条 linecap 样式 -export const SQUARE_LINE_CAP = 'square'; - export const getDefaultSeriesNumberText = (defaultText?: string) => defaultText ?? i18n('序号'); diff --git a/packages/s2-core/src/common/constant/copy.ts b/packages/s2-core/src/common/constant/copy.ts index 8ba009648e..d3818f24a8 100644 --- a/packages/s2-core/src/common/constant/copy.ts +++ b/packages/s2-core/src/common/constant/copy.ts @@ -5,6 +5,7 @@ export enum CopyType { } export const NewLine = '\r\n'; + export const NewTab = '\t'; // 每次异步渲染数据的阈值 diff --git a/packages/s2-core/src/common/constant/field.ts b/packages/s2-core/src/common/constant/field.ts new file mode 100644 index 0000000000..6a677e5b88 --- /dev/null +++ b/packages/s2-core/src/common/constant/field.ts @@ -0,0 +1,12 @@ +// 值字段的 id 是固定的! +export const VALUE_FIELD = '$$value$$'; +export const EXTRA_FIELD = '$$extra$$'; +export const ORIGIN_FIELD = '$$origin$$'; +export const EXTRA_COLUMN_FIELD = '$$extra_column$$'; +export const TOTAL_VALUE = '$$total$$'; +export const MULTI_VALUE = '$$multi$$'; +export const SERIES_NUMBER_FIELD = '$$series_number$$'; +export const EMPTY_FIELD_VALUE = '$$empty_field_value$$'; +export const EMPTY_EXTRA_FIELD_PLACEHOLDER = '$$empty_extra_placeholder$$'; +export const NULL_SYMBOL_ID = '$$null$$'; +export const UNDEFINED_SYMBOL_ID = '$$undefined$$'; diff --git a/packages/s2-core/src/common/constant/index.ts b/packages/s2-core/src/common/constant/index.ts index 8bb7b84fac..c9e8259d6d 100644 --- a/packages/s2-core/src/common/constant/index.ts +++ b/packages/s2-core/src/common/constant/index.ts @@ -1,3 +1,4 @@ +export * from './field'; export * from './events'; export * from './basic'; export * from './classnames'; @@ -12,3 +13,4 @@ export * from './resize'; export * from './copy'; export * from './pagination'; export * from './node'; +export * from './query'; diff --git a/packages/s2-core/src/common/constant/interaction.ts b/packages/s2-core/src/common/constant/interaction.ts index 0d775a572c..c8f7f8a641 100644 --- a/packages/s2-core/src/common/constant/interaction.ts +++ b/packages/s2-core/src/common/constant/interaction.ts @@ -17,6 +17,7 @@ export enum InteractionName { export enum InteractionStateName { ALL_SELECTED = 'allSelected', SELECTED = 'selected', + BRUSH_SELECTED = 'brushSelected', UNSELECTED = 'unselected', HOVER = 'hover', HOVER_FOCUS = 'hoverFocus', diff --git a/packages/s2-core/src/common/constant/node.ts b/packages/s2-core/src/common/constant/node.ts index 9f0023a63f..d24907d1ca 100644 --- a/packages/s2-core/src/common/constant/node.ts +++ b/packages/s2-core/src/common/constant/node.ts @@ -1,4 +1,3 @@ export const ROOT_NODE_ID = 'root'; - export const NODE_ID_SEPARATOR = '[&]'; export const ROOT_BEGINNING_REGEX = /^root\[&\]*/; diff --git a/packages/s2-core/src/common/constant/options.ts b/packages/s2-core/src/common/constant/options.ts index 7481fe55c4..2dc5fa57b3 100644 --- a/packages/s2-core/src/common/constant/options.ts +++ b/packages/s2-core/src/common/constant/options.ts @@ -10,7 +10,13 @@ import { EMPTY_PLACEHOLDER } from './basic'; export const MIN_DEVICE_PIXEL_RATIO = 1; -export enum LayoutWidthTypes { +/** + * 布局类型: + * adaptive: 行列等宽,均分整个 canvas 画布宽度 + * colAdaptive:列等宽,行头紧凑布局,列等分画布宽度减去行头宽度的剩余宽度 + * compact:行列紧凑布局,指标维度少的时候无法布满整个画布 + */ +export enum LayoutWidthType { Adaptive = 'adaptive', ColAdaptive = 'colAdaptive', Compact = 'compact', @@ -21,7 +27,7 @@ export const SPLIT_LINE_WIDTH = 1; export const DEFAULT_TREE_ROW_CELL_WIDTH = 120; export const DEFAULT_STYLE: S2Style = { - layoutWidthType: LayoutWidthTypes.Adaptive, + layoutWidthType: LayoutWidthType.Adaptive, rowCell: { showTreeLeafNodeAlignDot: false, widthByField: null, @@ -112,7 +118,7 @@ export const DEFAULT_MOBILE_OPTIONS: S2Options = { width: mobileWidth - 40, height: 380, style: { - layoutWidthType: LayoutWidthTypes.ColAdaptive, + layoutWidthType: LayoutWidthType.ColAdaptive, }, interaction: { hoverHighlight: false, diff --git a/packages/s2-core/src/common/constant/query.ts b/packages/s2-core/src/common/constant/query.ts new file mode 100644 index 0000000000..7f642628c3 --- /dev/null +++ b/packages/s2-core/src/common/constant/query.ts @@ -0,0 +1,6 @@ +export enum QueryDataType { + /* 获取所有的数据 */ + All = 'all', + /* 只需要明细数据 */ + DetailOnly = 'detailOnly', +} diff --git a/packages/s2-core/src/common/constant/total.ts b/packages/s2-core/src/common/constant/total.ts deleted file mode 100644 index 128ebdb566..0000000000 --- a/packages/s2-core/src/common/constant/total.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { TotalSelectionsOfMultiData } from '../../data-set/interface'; - -export const DEFAULT_TOTAL_SELECTIONS: TotalSelectionsOfMultiData = { - row: { - grandTotalOnly: false, - subTotalOnly: false, - totalDimensions: true, - }, - column: { - grandTotalOnly: false, - subTotalOnly: false, - totalDimensions: true, - }, -}; - -export enum DataSelectType { - // 获取所有的数据 - All = 'all', - // 只需要明细数据 - DetailOnly = 'detailOny', - // 只需要总计/小计数据 - TotalOnly = 'totalOnly', -} diff --git a/packages/s2-core/src/common/i18n/en_US.ts b/packages/s2-core/src/common/i18n/en_US.ts index 88e8e2429a..fb88221d5c 100644 --- a/packages/s2-core/src/common/i18n/en_US.ts +++ b/packages/s2-core/src/common/i18n/en_US.ts @@ -1,7 +1,7 @@ export const EN_US = { 小计: 'Total', 总计: 'Total', - 总和: 'SUM', + 总和: '(SUM)', 项: 'items', 已选择: 'selected', 序号: 'Index', @@ -15,4 +15,5 @@ export const EN_US = { 升序: 'ASC', 降序: 'DESC', 不排序: 'No order', + ',': ', ', }; diff --git a/packages/s2-core/src/common/i18n/zh_CN.ts b/packages/s2-core/src/common/i18n/zh_CN.ts index 6e677d4684..45913deb78 100644 --- a/packages/s2-core/src/common/i18n/zh_CN.ts +++ b/packages/s2-core/src/common/i18n/zh_CN.ts @@ -1,7 +1,7 @@ export const ZH_CN = { 小计: '小计', 总计: '总计', - 总和: '总和', + 总和: '(总和)', 项: '项', 已选择: '已选择', 序号: '序号', @@ -16,4 +16,5 @@ export const ZH_CN = { 降序: '降序', 组内降序: '组内降序', 不排序: '不排序', + ',': ',', }; diff --git a/packages/s2-core/src/common/icons/gui-icon.ts b/packages/s2-core/src/common/icons/gui-icon.ts index 5c12f6f101..6812659e49 100644 --- a/packages/s2-core/src/common/icons/gui-icon.ts +++ b/packages/s2-core/src/common/icons/gui-icon.ts @@ -1,9 +1,10 @@ /** - * @Description: 请严格要求 svg 的 viewBox,若设计产出的 svg 不是此规格,请叫其修改为 '0 0 1024 1024' + * @description: 请严格要求 svg 的 viewBox,若设计产出的 svg 不是此规格,请叫其修改为 '0 0 1024 1024' */ import { Group, type ImageStyleProps } from '@antv/g'; -import { omit, clone } from 'lodash'; +import { clone, omit } from 'lodash'; import { CustomImage } from '../../engine'; +import { DebuggerUtil } from '../debug'; import { getIcon } from './factory'; const STYLE_PLACEHOLDER = ' { - // 加载完成后,当前 Cell 可能已经销毁了 + // 异步加载完成后,当前 Cell 可能已经销毁了 if (this.destroyed) { + DebuggerUtil.getInstance().logger(`GuiIcon ${name} destroyed.`); + return; } image.attr('img', value); this.appendChild(image); }) - .catch((event: Event) => { + .catch((event: string | Event) => { + // 如果是 TypeError, 则是 G 底层渲染有问题, 其他场景才报加载异常的错误 + if (event instanceof TypeError) { + // eslint-disable-next-line no-console + console.warn(`GuiIcon ${name} destroyed:`, event); + + return; + } + // eslint-disable-next-line no-console - console.error(`GuiIcon ${name} load failed`, event); + console.error(`GuiIcon ${name} load failed:`, event); }); } } diff --git a/packages/s2-core/src/common/interface/basic.ts b/packages/s2-core/src/common/interface/basic.ts index 65b1d3ac64..5480d9511b 100644 --- a/packages/s2-core/src/common/interface/basic.ts +++ b/packages/s2-core/src/common/interface/basic.ts @@ -6,6 +6,7 @@ import type { IconPosition, RawData, ResizeInfo, + SimpleData, } from '../../common/interface'; import type { FrameConfig } from '../../common/interface/frame'; import type { Query } from '../../data-set'; @@ -27,7 +28,7 @@ export type { GetCellMeta, LayoutResult } from './facet'; */ export type Formatter = ( v: unknown, - data?: ViewMetaData | ViewMetaData[], + data?: SimpleData | ViewMetaData | ViewMetaData[], meta?: Node | ViewMeta, ) => string; @@ -68,14 +69,6 @@ export enum CellClipBox { CONTENT_BOX = 'contentBox', } -/** - * 布局类型: - * adaptive: 行列等宽,均分整个 canvas 画布宽度 - * colAdaptive:列等宽,行头紧凑布局,列等分画布宽度减去行头宽度的剩余宽度 - * compact:行列紧凑布局,指标维度少的时候无法布满整个画布 - */ -export type LayoutWidthType = 'adaptive' | 'colAdaptive' | 'compact'; - export interface Meta { /** * 字段 id @@ -179,6 +172,15 @@ export interface Total { */ subTotalsDimensions?: string[]; + /** + * 总计分组 + */ + grandTotalsGroupDimensions?: string[]; + /** + * 小计分组 + */ + subTotalsGroupDimensions?: string[]; + /** * 总计布局位置,默认是下或右 */ @@ -221,7 +223,7 @@ export interface Sort { sortByMeasure?: string; /** 筛选条件,缩小排序范围 */ - query?: Record; + query?: Query; /** 组内排序用来显示icon */ type?: string; @@ -380,7 +382,7 @@ export type DataCellCallback = (viewMeta: ViewMeta) => DataCell; export type MergedCellCallback = ( spreadsheet: SpreadSheet, - cells: S2CellType[], + cells: DataCell[], meta?: ViewMeta, ) => MergedCell; @@ -399,7 +401,7 @@ export interface MergedCellInfo { } export type TempMergedCell = { - cells: S2CellType[]; + cells: DataCell[]; viewMeta: ViewMeta; }; @@ -418,7 +420,7 @@ export interface ViewMeta { // cell's height height: number; // cell origin data raws(multiple data) - data: ViewMetaData; + data: ViewMetaData | SimpleData | undefined; // cell' row index (in rowLeafNodes) rowIndex: number; // cell' col index (in colLeafNodes) diff --git a/packages/s2-core/src/common/interface/emitter.ts b/packages/s2-core/src/common/interface/emitter.ts index 9d22472eee..7b4319c519 100644 --- a/packages/s2-core/src/common/interface/emitter.ts +++ b/packages/s2-core/src/common/interface/emitter.ts @@ -1,23 +1,29 @@ import type { FederatedPointerEvent as CanvasEvent } from '@antv/g'; -import type { SpreadSheet } from '../../sheet-type'; -import type { DataCell } from '../../cell/data-cell'; -import type { RowCell } from '../../cell/row-cell'; +import type { + CornerCell, + MergedCell, + RowCell, + SeriesNumberCell, +} from '../../cell'; import type { ColCell } from '../../cell/col-cell'; +import type { DataCell } from '../../cell/data-cell'; import type { S2Event } from '../../common/constant'; import type { CellMeta, CellScrollPosition, Data, + FilterParam, HiddenColumnsInfo, LayoutResult, RowCellCollapsedParams, S2CellType, + S2Style, + SortParams, } from '../../common/interface'; -import type { FilterParam, SortParams, S2Style } from '../../common/interface'; import type { RawData } from '../../common/interface/s2DataConfig'; -import type { CopyableList } from '../../utils/export/interface'; import type { Node } from '../../facet/layout/node'; -import type { CornerCell, MergedCell, SeriesNumberCell } from '../../cell'; +import type { SpreadSheet } from '../../sheet-type'; +import type { CopyableList } from './export'; import type { ResizeInfo } from './resize'; type CanvasEventHandler = (event: CanvasEvent) => void; diff --git a/packages/s2-core/src/utils/export/interface.ts b/packages/s2-core/src/common/interface/export.ts similarity index 94% rename from packages/s2-core/src/utils/export/interface.ts rename to packages/s2-core/src/common/interface/export.ts index 328702343e..4480aab47f 100644 --- a/packages/s2-core/src/utils/export/interface.ts +++ b/packages/s2-core/src/common/interface/export.ts @@ -1,6 +1,6 @@ import type { SpreadSheet } from '../../sheet-type'; -import type { DataItem, CellMeta } from '../../common'; -import { EXTRA_FIELD } from '../../common'; +import type { DataItem, CellMeta } from '..'; +import { EXTRA_FIELD } from '..'; export type MatrixPlainTransformer = ( data: DataItem[][], diff --git a/packages/s2-core/src/common/interface/index.ts b/packages/s2-core/src/common/interface/index.ts index 8183cf5f81..610f33fbff 100644 --- a/packages/s2-core/src/common/interface/index.ts +++ b/packages/s2-core/src/common/interface/index.ts @@ -17,3 +17,5 @@ export * from './facet'; export * from './style'; export * from './collapse'; export * from './text'; +export * from './utils'; +export * from './export'; diff --git a/packages/s2-core/src/common/interface/interaction.ts b/packages/s2-core/src/common/interface/interaction.ts index af373c1665..c2024be3d6 100644 --- a/packages/s2-core/src/common/interface/interaction.ts +++ b/packages/s2-core/src/common/interface/interaction.ts @@ -1,10 +1,3 @@ -import type { SimpleBBox } from '../../engine'; -import type { - InteractionStateName, - CellType, - InterceptType, - ScrollbarPositionType, -} from '../constant'; import type { BaseCell, ColCell, @@ -16,14 +9,21 @@ import type { TableSeriesNumberCell, } from '../../cell'; import type { HeaderCell } from '../../cell/header-cell'; +import type { SeriesNumberCell } from '../../cell/series-number-cell'; +import type { SimpleBBox } from '../../engine'; import type { Node } from '../../facet/layout/node'; +import type { RootInteraction } from '../../interaction'; import type { BaseEvent } from '../../interaction/base-event'; import type { SpreadSheet } from '../../sheet-type'; -import type { RootInteraction } from '../../interaction'; -import type { SeriesNumberCell } from '../../cell/series-number-cell'; -import type { Transformer } from '../../utils/export/interface'; -import type { ResizeInteractionOptions } from './resize'; +import type { + CellType, + InteractionStateName, + InterceptType, + ScrollbarPositionType, +} from '../constant'; +import type { Transformer } from './export'; import type { ViewMeta } from './basic'; +import type { ResizeInteractionOptions } from './resize'; export type S2CellType = | DataCell @@ -136,7 +136,7 @@ export interface HoverFocusOptions { duration?: number; } -export interface BrushSelection { +export interface BrushSelectionOptions { dataCell?: boolean; rowCell?: boolean; colCell?: boolean; @@ -163,7 +163,7 @@ export interface InteractionOptions { /** * 十字器高亮效果 */ - hoverHighlight?: boolean; + hoverHighlight?: InteractionCellHighlightOptions | boolean; /** * 悬停聚焦, 800ms 后会显示其对应 tooltip, 可以自定义 duration @@ -215,7 +215,7 @@ export interface InteractionOptions { /** * 刷选 */ - brushSelection?: BrushSelection | boolean; + brushSelection?: BrushSelectionOptions | boolean; /** * 多选 Command/Ctrl + click @@ -246,7 +246,7 @@ export interface InteractionOptions { /** * 选中单元格高亮联动 (高亮所对应行头/列头, 高亮当前行/当前列) */ - selectedCellHighlight?: boolean | InteractionCellSelectedHighlightOptions; + selectedCellHighlight?: boolean | InteractionCellHighlightOptions; /** * 滚动到边界的行为 @@ -255,6 +255,11 @@ export interface InteractionOptions { */ overscrollBehavior?: 'auto' | 'none' | 'contain' | null; + /** + * 表格滚动后是否触发 hover + */ + hoverAfterScroll?: boolean; + /** * 自定义交互 * @see https://s2.antv.antgroup.com/manual/advanced/interaction/custom @@ -262,7 +267,7 @@ export interface InteractionOptions { customInteractions?: CustomInteraction[]; } -export interface InteractionCellSelectedHighlightOptions { +export interface InteractionCellHighlightOptions { /** 高亮行头 */ rowHeader?: boolean; /** 高亮列头 */ diff --git a/packages/s2-core/src/common/interface/resize.ts b/packages/s2-core/src/common/interface/resize.ts index 0b04116ae5..f51ff2fe03 100644 --- a/packages/s2-core/src/common/interface/resize.ts +++ b/packages/s2-core/src/common/interface/resize.ts @@ -35,6 +35,8 @@ export interface ResizeGuideLinePosition { export interface ResizePosition { offsetX?: number; offsetY?: number; + clientX?: number; + clientY?: number; } export interface ResizeDetail { diff --git a/packages/s2-core/src/common/interface/s2DataConfig.ts b/packages/s2-core/src/common/interface/s2DataConfig.ts index 3ace8790ce..ce21a19d7a 100644 --- a/packages/s2-core/src/common/interface/s2DataConfig.ts +++ b/packages/s2-core/src/common/interface/s2DataConfig.ts @@ -1,8 +1,4 @@ -import type { - EXTRA_FIELD, - MiniChartTypes, - VALUE_FIELD, -} from '../constant/basic'; +import type { EXTRA_FIELD, MiniChartTypes, VALUE_FIELD } from '../constant'; import type { Fields, FilterParam, Meta, SortParams } from './basic'; export interface BaseChartData { @@ -88,7 +84,7 @@ export type ExtraData = { [VALUE_FIELD]: string | DataItem; }; -export type Data = (RawData & ExtraData) | undefined | null; +export type Data = RawData & ExtraData; export interface CustomTreeNode { /** @@ -136,5 +132,3 @@ export interface S2DataConfig { filterParams?: FilterParam[]; [key: string]: unknown; } - -export type FlattingIndexesData = RawData[][] | RawData[] | RawData; diff --git a/packages/s2-core/src/common/interface/s2Options.ts b/packages/s2-core/src/common/interface/s2Options.ts index 8531a8ce5c..53c2e92a72 100644 --- a/packages/s2-core/src/common/interface/s2Options.ts +++ b/packages/s2-core/src/common/interface/s2Options.ts @@ -123,11 +123,6 @@ export interface S2BasicOptions< */ placeholder?: ((meta: Record) => string) | string; - // /** - // * 自定义 DPR, 默认 "window.devicePixelRatio" - // */ - // devicePixelRatio?: number; - /** * 设备类型: pc / mobile */ @@ -257,6 +252,11 @@ export interface S2PivotSheetFrozenOptions { * 当值为 boolean 时,true 对应冻结最大区域为 0.5, false 对应 0 */ rowHeader?: boolean | number; + + /** + * 是否冻结首行 (适用于总计置于顶部, 树状模式等场景) + */ + firstRow?: boolean; } export interface S2TableSheetFrozenOptions { @@ -281,11 +281,13 @@ export interface S2TableSheetFrozenOptions { trailingColCount?: number; } +export type HierarchyType = 'grid' | 'tree'; + export interface S2PivotSheetOptions { /** * 行头布局类型, grid: 平铺网格 | tree: 树状结构 */ - hierarchyType?: 'grid' | 'tree'; + hierarchyType?: HierarchyType; /** * 小计/总计配置 diff --git a/packages/s2-core/src/common/interface/store.ts b/packages/s2-core/src/common/interface/store.ts index 275ae28764..5ee5bcdfef 100644 --- a/packages/s2-core/src/common/interface/store.ts +++ b/packages/s2-core/src/common/interface/store.ts @@ -79,7 +79,7 @@ export interface StoreKey { /** * 原始数据配置 */ - originalDataCfg: S2DataConfig; + originalDataCfg: Partial; /** * 可视区域包裹盒模型 diff --git a/packages/s2-core/src/common/interface/style.ts b/packages/s2-core/src/common/interface/style.ts index 4532fa7a2d..f3cce47eea 100644 --- a/packages/s2-core/src/common/interface/style.ts +++ b/packages/s2-core/src/common/interface/style.ts @@ -1,5 +1,5 @@ import type { Node } from '../../facet/layout/node'; -import type { LayoutWidthType } from './basic'; +import type { LayoutWidthType } from '../constant'; export type CellCustomSize = | null diff --git a/packages/s2-core/src/common/interface/theme.ts b/packages/s2-core/src/common/interface/theme.ts index e9fd0691b2..06bccdb352 100644 --- a/packages/s2-core/src/common/interface/theme.ts +++ b/packages/s2-core/src/common/interface/theme.ts @@ -167,6 +167,9 @@ export interface CellTheme { /** 交互态 */ interactionState?: InteractionState; + + /** 单元格边线虚线 */ + borderDash?: LineStyleProps['lineDash']; } export interface IconTheme { @@ -268,6 +271,8 @@ export interface SplitLine { /** 线性变化右侧颜色 */ right: string; }; + /** 分割线虚线 */ + borderDash?: LineStyleProps['lineDash']; } export interface DefaultCellTheme extends GridAnalysisCellTheme { /** 粗体文本样式 */ diff --git a/packages/s2-core/src/common/interface/tooltip.ts b/packages/s2-core/src/common/interface/tooltip.ts index 2b8f8db802..5d3ef1c20e 100644 --- a/packages/s2-core/src/common/interface/tooltip.ts +++ b/packages/s2-core/src/common/interface/tooltip.ts @@ -1,10 +1,10 @@ import type { FederatedPointerEvent as CanvasEvent } from '@antv/g'; import type * as CSS from 'csstype'; -import type { Data, Point, S2CellType } from '../../common/interface'; +import type { Point, S2CellType, ViewMetaData } from '../../common/interface'; import type { SpreadSheet } from '../../sheet-type'; import type { BaseTooltip } from '../../ui/tooltip'; -export type TooltipDataItem = Data; +export type TooltipDataItem = ViewMetaData; export interface TooltipOperatorMenuInfo { key: string; diff --git a/packages/s2-core/src/common/interface/utils.ts b/packages/s2-core/src/common/interface/utils.ts new file mode 100644 index 0000000000..503aba346b --- /dev/null +++ b/packages/s2-core/src/common/interface/utils.ts @@ -0,0 +1,3 @@ +export type PickEssential = { + [K in keyof O as Pick, K> extends Pick ? never : K]: O[K]; +}; diff --git a/packages/s2-core/src/data-set/base-data-set.ts b/packages/s2-core/src/data-set/base-data-set.ts index 7f78355960..42292cbe0c 100644 --- a/packages/s2-core/src/data-set/base-data-set.ts +++ b/packages/s2-core/src/data-set/base-data-set.ts @@ -3,6 +3,7 @@ import { find, get, identity, + isEmpty, isNil, isString, map, @@ -10,7 +11,7 @@ import { memoize, min, } from 'lodash'; -import type { CellMeta, CustomHeaderField, RowData } from '../common'; +import type { CellMeta, CustomHeaderField, ViewMeta } from '../common'; import { CellType } from '../common'; import type { Fields, @@ -20,8 +21,8 @@ import type { RawData, S2CellType, S2DataConfig, + SimpleData, SortParams, - ViewMeta, ViewMetaData, } from '../common/interface'; import type { ValueRange } from '../common/interface/condition'; @@ -32,6 +33,7 @@ import { setValueRangeState, } from '../utils/condition/state-controller'; import { generateExtraFieldMeta } from '../utils/dataset/pivot-data-set'; +import type { Indexes } from '../utils/indexes'; import type { GetCellDataParams, Query } from './interface'; import type { GetCellMultiDataParams } from './index'; @@ -52,9 +54,9 @@ export abstract class BaseDataSet { public originData: RawData[]; /** - * 二维索引数据 + * 索引数据 */ - public indexesData: RawData[][] | RawData[]; + public indexesData: Record; /** * 高级排序, 组内排序 @@ -69,8 +71,11 @@ export abstract class BaseDataSet { /** * 表格实例 */ - protected spreadsheet: SpreadSheet; + public spreadsheet: SpreadSheet; + /** + * 展示数据 + */ protected displayData: RawData[]; public constructor(spreadsheet: SpreadSheet) { @@ -207,16 +212,13 @@ export abstract class BaseDataSet { this.sortParams = sortParams; this.filterParams = filterParams; this.displayData = this.originData; - this.indexesData = []; + this.indexesData = {}; } /** * 添加 (角头/数值虚拟字段) 格式化信息 */ - public getFieldMetaWithExtraField( - meta: Meta[] = [], - defaultExtraFieldText: string, - ): Meta[] { + public processMeta(meta: Meta[] = [], defaultExtraFieldText: string): Meta[] { const newMeta: Meta[] = [ ...meta, generateExtraFieldMeta( @@ -233,6 +235,15 @@ export abstract class BaseDataSet { return this.displayData; } + public isEmpty() { + return isEmpty(this.getDisplayDataSet()); + } + + // https://github.com/antvis/S2/issues/2255 + public getEmptyViewIndexes(): Indexes { + return [] as unknown as Indexes; + } + public getValueRangeByField(field: string): ValueRange { const cacheRange = getValueRangeState(this.spreadsheet, field); @@ -292,11 +303,13 @@ export abstract class BaseDataSet { */ public abstract getCellData( params: GetCellDataParams, - ): ViewMetaData | undefined; + ): ViewMetaData | SimpleData | undefined; /** * 获取批量的单元格数据 * 如果 query 为空, 则返回全量数据 + * @description 默认获取符合 query 的所有数据,包括小计总计等汇总数据; + * 如果只希望获取明细数据,请使用 { queryType: QueryDataType.DetailOnly } */ public abstract getCellMultiData( params: GetCellMultiDataParams, @@ -310,8 +323,9 @@ export abstract class BaseDataSet { } /** - * get a row cells data including cell - * @param cellMeta + * 查询当前整行数据 */ - public abstract getRowData(cellMeta: CellMeta): RowData; + public abstract getRowData( + cellMeta: CellMeta | ViewMeta | Node, + ): ViewMetaData[] | ViewMetaData; } diff --git a/packages/s2-core/src/data-set/cell-data.ts b/packages/s2-core/src/data-set/cell-data.ts index cc4b3aa010..c112ad9738 100644 --- a/packages/s2-core/src/data-set/cell-data.ts +++ b/packages/s2-core/src/data-set/cell-data.ts @@ -1,7 +1,7 @@ /* eslint-disable no-empty-function */ import type { ViewMetaData } from '../common/interface/basic'; -import { EXTRA_FIELD, VALUE_FIELD } from '../common/constant/basic'; import type { RawData } from '../common/interface/s2DataConfig'; +import { ORIGIN_FIELD, EXTRA_FIELD, VALUE_FIELD } from '../common/constant'; export class CellData { constructor( @@ -9,20 +9,24 @@ export class CellData { private extraField: string, ) {} - static getCellDataList(raw: RawData, extraFields: string[]) { - return extraFields.map((field) => new CellData(raw, field)); + static getCellData(raw: RawData, extraField: string) { + return new CellData(raw, extraField); } - getOrigin() { - return this.raw; + static getCellDataList(raw: RawData, extraFields: string[]) { + return extraFields.map((field) => CellData.getCellData(raw, field)); } - getValueByField(field: string) { - if (field === VALUE_FIELD || field === EXTRA_FIELD) { - return this[field]; + static getFieldValue(data: ViewMetaData, field: string = '') { + if (data instanceof CellData) { + return field ? data.getValueByField(field) : data[ORIGIN_FIELD]; } - return this.raw?.[field]; + return data?.[field]; + } + + get [ORIGIN_FIELD]() { + return this.raw; } get [EXTRA_FIELD]() { @@ -30,14 +34,14 @@ export class CellData { } get [VALUE_FIELD]() { - return this.raw?.[this.extraField]; + return this.raw[this.extraField]; } -} -export const getFieldValueOfViewMetaData = (data: ViewMetaData, field = '') => { - if (data instanceof CellData) { - return field ? data.getValueByField(field) : data.getOrigin(); - } + getValueByField(field: string) { + if (field === VALUE_FIELD || field === EXTRA_FIELD) { + return this[field]; + } - return data?.[field]; -}; + return this.raw[field]; + } +} diff --git a/packages/s2-core/src/data-set/custom-grid-pivot-data-set.ts b/packages/s2-core/src/data-set/custom-grid-pivot-data-set.ts index 719b2827ed..a35feb5445 100644 --- a/packages/s2-core/src/data-set/custom-grid-pivot-data-set.ts +++ b/packages/s2-core/src/data-set/custom-grid-pivot-data-set.ts @@ -8,7 +8,7 @@ export class CustomGridPivotDataSet extends CustomTreePivotDataSet { const rows = valueInCols ? [EXTRA_FIELD] : [...(dataCfg.fields.rows || []), EXTRA_FIELD]; - const meta = this.getFieldMetaWithExtraField(dataCfg.meta!, i18n('数值')); + const meta = this.processMeta(dataCfg.meta!, i18n('数值')); return { ...dataCfg, diff --git a/packages/s2-core/src/data-set/custom-tree-pivot-data-set.ts b/packages/s2-core/src/data-set/custom-tree-pivot-data-set.ts index c5b02d6173..4fea6af241 100644 --- a/packages/s2-core/src/data-set/custom-tree-pivot-data-set.ts +++ b/packages/s2-core/src/data-set/custom-tree-pivot-data-set.ts @@ -1,9 +1,10 @@ -import { get } from 'lodash'; +import { get, type PropertyPath } from 'lodash'; import { EXTRA_FIELD } from '../common/constant'; -import type { Meta, S2DataConfig } from '../common/interface'; import { i18n } from '../common/i18n'; +import type { Meta, S2DataConfig } from '../common/interface'; import { getDataPath, + getDataPathPrefix, transformDimensionsValues, } from '../utils/dataset/pivot-data-set'; import { CellData } from './cell-data'; @@ -12,7 +13,7 @@ import { PivotDataSet } from './pivot-data-set'; export class CustomTreePivotDataSet extends PivotDataSet { getCellData(params: GetCellMultiDataParams) { - const { query } = params; + const { query = {} } = params || {}; const { columns, rows } = this.fields; const rowDimensionValues = transformDimensionsValues( query, @@ -27,12 +28,15 @@ export class CustomTreePivotDataSet extends PivotDataSet { colDimensionValues, rowPivotMeta: this.rowPivotMeta, colPivotMeta: this.colPivotMeta, + rowFields: rows as string[], + colFields: columns as string[], + prefix: getDataPathPrefix(rows as string[], columns as string[]), }); - const rawData = get(this.indexesData, path); + const rawData = get(this.indexesData, path as PropertyPath); if (rawData) { - return new CellData(rawData, query[EXTRA_FIELD]); + return CellData.getCellData(rawData, query[EXTRA_FIELD]); } } @@ -45,10 +49,7 @@ export class CustomTreePivotDataSet extends PivotDataSet { */ const updatedDataCfg = super.processDataCfg(dataCfg); - const newMeta: Meta[] = this.getFieldMetaWithExtraField( - dataCfg.meta, - i18n('指标'), - ); + const newMeta: Meta[] = this.processMeta(dataCfg.meta, i18n('指标')); return { ...updatedDataCfg, diff --git a/packages/s2-core/src/data-set/index.ts b/packages/s2-core/src/data-set/index.ts index 8d658cf4fa..f0ffc95172 100644 --- a/packages/s2-core/src/data-set/index.ts +++ b/packages/s2-core/src/data-set/index.ts @@ -1,17 +1,17 @@ import { BaseDataSet } from './base-data-set'; +import { CustomGridPivotDataSet } from './custom-grid-pivot-data-set'; +import { CustomTreePivotDataSet } from './custom-tree-pivot-data-set'; import { PivotDataSet } from './pivot-data-set'; import { TableDataSet } from './table-data-set'; -import { CustomTreePivotDataSet } from './custom-tree-pivot-data-set'; -import { CustomGridPivotDataSet } from './custom-grid-pivot-data-set'; export { CellData } from './cell-data'; export { BaseDataSet, + CustomGridPivotDataSet, + CustomTreePivotDataSet, PivotDataSet, TableDataSet, - CustomTreePivotDataSet, - CustomGridPivotDataSet, }; export * from './interface'; diff --git a/packages/s2-core/src/data-set/interface.ts b/packages/s2-core/src/data-set/interface.ts index 3ba7fa5fa1..af8de99a3f 100644 --- a/packages/s2-core/src/data-set/interface.ts +++ b/packages/s2-core/src/data-set/interface.ts @@ -1,26 +1,20 @@ -import type { BaseFields, SortParam } from '../common/interface'; +import type { QueryDataType } from '../common'; +import type { RawData, SortParam } from '../common/interface'; import type { Node } from '../facet/layout/node'; import type { CellData } from './cell-data'; import type { PivotDataSet } from './pivot-data-set'; export type Query = Record; -export type TotalSelection = { - grandTotalOnly?: boolean; - subTotalOnly?: boolean; - totalDimensions?: boolean | string[]; -}; - -export type TotalSelectionsOfMultiData = { - row?: TotalSelection; - column?: TotalSelection; -}; - export type PivotMetaValue = { - // field level index + // 当前维度结合父级维度生成的完整 id 信息 + id: string; + // 当前维度结合父级维度生成的完整 dimensions 信息,主要是预防 field 数据本身出现 [&] 导致维度信息识别不正确 + dimensions: string[]; + // 当前维度 + value: string; level: number; children: PivotMeta; - // field name childField?: string; }; @@ -28,32 +22,33 @@ export type PivotMeta = Map; export type SortedDimensionValues = Record; +export interface OnFirstCreateParams { + careRepeated?: boolean; + // 维度 id,如 city + dimension: string; + // 完整维度信息:'四川省[&]成都市' + dimensionPath: string; +} + +export type DataPath = (number | string | undefined)[]; + export type DataPathParams = { rowDimensionValues: string[]; colDimensionValues: string[]; - shouldCreateOrUpdate?: boolean; + rowPivotMeta: PivotMeta; + colPivotMeta: PivotMeta; + rowFields: string[]; + colFields: string[]; + // first create data path + isFirstCreate?: boolean; // callback when pivot map create node - onCreate?: (params: { - // 维度 id,如 city - dimension: string; - // 维度数组 ['四川省', '成都市'] - dimensionPath: string[]; - }) => void; - rowPivotMeta?: PivotMeta; - colPivotMeta?: PivotMeta; -} & BaseFields; - -export type DataPath = (number | string)[]; + onFirstCreate?: (params: OnFirstCreateParams) => void; + prefix?: string; +}; export interface GetCellDataParams { - /** - * 查询条件 - */ - query: Query; - - /** - * 是否是汇总节点 - */ + // search query + query?: Query; isTotals?: boolean; /** @@ -65,19 +60,35 @@ export interface GetCellDataParams { * 是否是行头 */ isRow?: boolean; + // use with isTotals + totalStatus?: TotalStatus; +} + +export interface CheckAccordQueryParams { + // item of sortedDimensionValues,es: "浙江省[&]杭州市[&]家具[&]桌子" + dimensionValues: string; + query: Query; + // rows or columns + dimensions: string[]; + field: string; +} + +export interface TotalStatus { + isRowTotal: boolean; + isRowSubTotal: boolean; + isColTotal: boolean; + isColSubTotal: boolean; } export interface GetCellMultiDataParams { /** * 查询条件 */ - query: Query; - + query?: Query; /** - * 汇总 + * 查询类型 */ - totals?: TotalSelectionsOfMultiData; - + queryType?: QueryDataType; /** * 下钻 */ @@ -92,3 +103,12 @@ export interface SortActionParams { sortByValues?: string[]; isSortByMeasure?: boolean; } + +export interface SortPivotMetaParams { + pivotMeta: PivotMeta; + dimensions: string[]; + sortedDimensionValues: string[]; + sortFieldId: string; +} + +export type FlattingIndexesData = RawData[][] | RawData[] | RawData; diff --git a/packages/s2-core/src/data-set/pivot-data-set.ts b/packages/s2-core/src/data-set/pivot-data-set.ts index 7d43da9bcd..721f64f720 100644 --- a/packages/s2-core/src/data-set/pivot-data-set.ts +++ b/packages/s2-core/src/data-set/pivot-data-set.ts @@ -10,57 +10,58 @@ import { get, has, includes, + indexOf, isArray, isEmpty, isNumber, - isUndefined, map, + some, uniq, unset, + type PropertyPath, + omit, } from 'lodash'; -import type { CellMeta } from '../common'; +import { + QueryDataType, + type CellMeta, + type CustomHeaderFields, + type Data, +} from '../common'; import { EXTRA_FIELD, MULTI_VALUE, - NODE_ID_SEPARATOR, TOTAL_VALUE, VALUE_FIELD, } from '../common/constant'; -import { DataSelectType } from '../common/constant/total'; -import { DebuggerUtil, DEBUG_TRANSFORM_DATA } from '../common/debug'; +import { DEBUG_TRANSFORM_DATA, DebuggerUtil } from '../common/debug'; import { i18n } from '../common/i18n'; import type { - CustomHeaderFields, - FlattingIndexesData, Formatter, Meta, PartDrillDownDataCache, PartDrillDownFieldInLevel, RawData, - RowData, S2DataConfig, - TotalsStatus, ViewMeta, } from '../common/interface'; import { Node } from '../facet/layout/node'; -import { - filterTotal, - flattenIndexesData, - getAggregationAndCalcFuncByQuery, - getListBySorted, - getTotalSelection, -} from '../utils/data-set-operate'; +import { getAggregationAndCalcFuncByQuery } from '../utils/data-set-operate'; import { deleteMetaById, filterExtraDimension, + flattenIndexesData, getDataPath, - getDimensionsWithoutPathPre, - shouldQueryMultiData, + getDataPathPrefix, + getExistValues, + getFlattenDimensionValues, + getSatisfiedPivotMetaValues, + isMultiValue, transformDimensionsValues, transformIndexesData, + type TransformResult, } from '../utils/dataset/pivot-data-set'; import { calcActionByType } from '../utils/number-calculate'; -import { handleSortAction } from '../utils/sort-action'; +import { getSortedPivotMeta, handleSortAction } from '../utils/sort-action'; import { BaseDataSet } from './base-data-set'; import { CellData } from './cell-data'; import type { @@ -69,8 +70,7 @@ import type { PivotMeta, Query, SortedDimensionValues, - TotalSelection, - TotalSelectionsOfMultiData, + TotalStatus, } from './interface'; export class PivotDataSet extends BaseDataSet { @@ -83,6 +83,10 @@ export class PivotDataSet extends BaseDataSet { // sorted dimension values public sortedDimensionValues: SortedDimensionValues; + getExistValuesByDataItem(data: RawData, values: string[]) { + return getExistValues(data, values); + } + /** * When data related config changed, we need * 1、re-process config @@ -92,27 +96,43 @@ export class PivotDataSet extends BaseDataSet { */ public setDataCfg(dataCfg: S2DataConfig) { super.setDataCfg(dataCfg); + const { rows } = this.fields; + this.sortedDimensionValues = {}; this.rowPivotMeta = new Map(); this.colPivotMeta = new Map(); + this.transformIndexesData(this.originData, rows as string[]); + this.handleDimensionValuesSort(); + } + + public transformIndexesData( + data: RawData[], + rows: string[], + ): TransformResult { + const { columns, values, valueInCols } = this.fields; + + let result!: TransformResult; DebuggerUtil.getInstance().debugCallback(DEBUG_TRANSFORM_DATA, () => { - const { rows, columns, values } = this.fields; - const { indexesData } = transformIndexesData({ - rows, - columns, - values, - originData: this.originData, + result = transformIndexesData({ + rows: rows as string[], + columns: columns as string[], + values: values!, + valueInCols: valueInCols!, + data, indexesData: this.indexesData, sortedDimensionValues: this.sortedDimensionValues, rowPivotMeta: this.rowPivotMeta, colPivotMeta: this.colPivotMeta, + getExistValuesByDataItem: this.getExistValuesByDataItem, }); - - this.indexesData = indexesData; + this.indexesData = result.indexesData; + this.rowPivotMeta = result.rowPivotMeta; + this.colPivotMeta = result.colPivotMeta; + this.sortedDimensionValues = result.sortedDimensionValues; }); - this.handleDimensionValuesSort(); + return result; } /** @@ -126,7 +146,6 @@ export class PivotDataSet extends BaseDataSet { drillDownData: RawData[], rowNode: Node, ) { - const { columns, values } = this.fields; const currentRowFields = Node.getFieldPath(rowNode, true); const nextRowFields = [...currentRowFields, extraRowField]; const store = this.spreadsheet.store; @@ -138,33 +157,16 @@ export class PivotDataSet extends BaseDataSet { if (idPathMap.has(rowNodeId)) { // the current node has a drill-down field, clean it forEach(idPathMap.get(rowNodeId), (path) => { - unset(this.indexesData, path); + unset(this.indexesData, path as unknown as PropertyPath); }); deleteMetaById(this.rowPivotMeta, rowNodeId); } // 3、转换数据 - const { - paths: drillDownDataPaths, - indexesData, - rowPivotMeta, - colPivotMeta, - sortedDimensionValues, - } = transformIndexesData({ - rows: nextRowFields, - columns, - values, - originData: drillDownData, - indexesData: this.indexesData, - sortedDimensionValues: this.sortedDimensionValues, - rowPivotMeta: this.rowPivotMeta, - colPivotMeta: this.colPivotMeta, - }); - - this.indexesData = indexesData; - this.rowPivotMeta = rowPivotMeta!; - this.colPivotMeta = colPivotMeta!; - this.sortedDimensionValues = sortedDimensionValues; + const { paths: drillDownDataPaths } = this.transformIndexesData( + drillDownData, + nextRowFields, + ); /* * 4、record data paths by nodeId @@ -184,7 +186,7 @@ export class PivotDataSet extends BaseDataSet { const idPathMap = store.get('drillDownIdPathMap'); if (!idPathMap) { - return; + return false; } const drillDownDataCache = store.get( @@ -198,12 +200,13 @@ export class PivotDataSet extends BaseDataSet { if (currentIdPathMap) { forEach(currentIdPathMap, (path) => { - unset(this.indexesData, path); + unset(this.indexesData, path as unknown as PropertyPath); }); } // 2. 删除 rowPivotMeta 当前下钻层级对应 meta 信息 deleteMetaById(this.rowPivotMeta, rowNodeId); + // 3. 删除下钻缓存路径 idPathMap.delete(rowNodeId); @@ -245,6 +248,8 @@ export class PivotDataSet extends BaseDataSet { } store.set('drillDownIdPathMap', idPathMap); + + return true; } /** @@ -271,9 +276,33 @@ export class PivotDataSet extends BaseDataSet { }); this.sortedDimensionValues[sortFieldId] = result; + this.handlePivotMetaSort(sortFieldId, result); }); }; + protected handlePivotMetaSort( + sortFieldId: string, + sortedDimensionValues: string[], + ) { + const { rows, columns } = this.fields; + + if (includes(rows, sortFieldId)) { + this.rowPivotMeta = getSortedPivotMeta({ + pivotMeta: this.rowPivotMeta, + dimensions: rows as string[], + sortFieldId, + sortedDimensionValues, + }); + } else if (includes(columns, sortFieldId)) { + this.colPivotMeta = getSortedPivotMeta({ + pivotMeta: this.colPivotMeta, + dimensions: columns as string[], + sortFieldId, + sortedDimensionValues, + }); + } + } + public processDataCfg(dataCfg: S2DataConfig): S2DataConfig { const { data, meta = [], fields, sortParams = [] } = dataCfg; const { @@ -297,7 +326,7 @@ export class PivotDataSet extends BaseDataSet { : uniq([...rows, EXTRA_FIELD]); } - const newMeta: Meta[] = this.getFieldMetaWithExtraField(meta, i18n('数值')); + const newMeta: Meta[] = this.processMeta(meta, i18n('数值')); return { data, @@ -312,75 +341,68 @@ export class PivotDataSet extends BaseDataSet { }; } - public getDimensionValues(field: string, query?: Query): string[] { + protected getFieldsAndPivotMetaByField(field: string) { const { rows = [], columns = [] } = this.fields || {}; - let meta: PivotMeta = new Map(); - let dimensions: CustomHeaderFields = []; - - if (includes(rows, field)) { - meta = this.rowPivotMeta; - dimensions = rows; - } else if (includes(columns, field)) { - meta = this.colPivotMeta; - dimensions = columns as string[]; - } - if (!isEmpty(query)) { - let sortedMeta: string[] = []; - const dimensionValuePath = []; - - for (const dimension of dimensions) { - const value: string = get(query, dimension as string); + if (rows.includes(field)) { + return { + dimensions: rows as string[], + pivotMeta: this.rowPivotMeta, + }; + } - dimensionValuePath.push(`${value}`); - const cacheKey = dimensionValuePath.join(`${NODE_ID_SEPARATOR}`); + if (columns.includes(field)) { + return { + dimensions: columns as string[], + pivotMeta: this.colPivotMeta, + }; + } - if (meta.has(value) && !isUndefined(value)) { - const childField = meta.get(value)?.childField; + return {}; + } - meta = meta.get(value)!.children; - if ( - find(this.sortParams, (item) => item.sortFieldId === childField) && - this.sortedDimensionValues[childField!] - ) { - const dimensionValues = this.sortedDimensionValues[ - childField! - ]?.filter((item) => item?.includes(cacheKey)); + public getDimensionValues(field: string, query: Query = {}): string[] { + const { pivotMeta, dimensions } = this.getFieldsAndPivotMetaByField(field); - sortedMeta = getDimensionsWithoutPathPre([...dimensionValues]); - } else { - sortedMeta = [...meta.keys()]; - } - } - } - if (isEmpty(sortedMeta)) { - return []; - } - - return filterTotal(getListBySorted([...meta.keys()], sortedMeta)); + if (!pivotMeta || !dimensions) { + return []; } - if (this.sortedDimensionValues[field]) { - return filterTotal( - getDimensionsWithoutPathPre([...this.sortedDimensionValues[field]]), - ); - } + const dimensionValues = transformDimensionsValues( + query, + dimensions, + MULTI_VALUE, + ); + + const values = getSatisfiedPivotMetaValues({ + pivotMeta, + dimensionValues, + fields: dimensions, + fieldIdx: indexOf(dimensions, field), + queryType: QueryDataType.DetailOnly, + sortedDimensionValues: this.sortedDimensionValues, + }); - return filterTotal([...meta.keys()]); + return uniq(values.map((v) => v.value)); } - getTotalValue(query: Query) { + getTotalValue(query: Query, totalStatus?: TotalStatus) { + const effectiveStatus = some(totalStatus); + const status = effectiveStatus ? totalStatus! : this.getTotalStatus(query); const { aggregation, calcFunc } = getAggregationAndCalcFuncByQuery( - this.getTotalStatus(query) as TotalsStatus, - this.spreadsheet.options?.totals!, + status, + this.spreadsheet.options?.totals, ) || {}; const calcAction = calcActionByType[aggregation!]; // 前端计算汇总值 if (calcAction || !!calcFunc) { - const data = this.getCellMultiData({ query }); + const data = this.getCellMultiData({ + query, + queryType: QueryDataType.DetailOnly, + }); let totalValue: number | null = null; if (calcFunc) { @@ -389,18 +411,18 @@ export class PivotDataSet extends BaseDataSet { totalValue = calcAction(data, VALUE_FIELD)!; } - return new CellData( - { ...query, [query[EXTRA_FIELD]]: totalValue }, + return CellData.getCellData( + { ...omit(query, [EXTRA_FIELD]), [query[EXTRA_FIELD]]: totalValue }, query[EXTRA_FIELD], ); } } public getCellData(params: GetCellDataParams) { - const { query = {}, rowNode, isTotals = false } = params || {}; + const { query = {}, rowNode, isTotals = false, totalStatus } = params || {}; const { rows: originRows, columns } = this.fields; - let rows = originRows; + let rows = originRows as string[]; const drillDownIdPathMap = this.spreadsheet?.store.get('drillDownIdPathMap'); @@ -412,15 +434,13 @@ export class PivotDataSet extends BaseDataSet { (parentPath) => rowNode?.id.startsWith(parentPath), ); - // 如果是下钻结点,小计行维度在 originRows 中并不存在 - if (!isTotals || isDrillDown) { - rows = Node.getFieldPath(rowNode!, isDrillDown) ?? originRows; + // 如果是下钻结点,行维度在 originRows 中并不存在 + if (rowNode && isDrillDown) { + rows = + Node.getFieldPath(rowNode, isDrillDown) ?? (originRows as string[]); } - const rowDimensionValues = transformDimensionsValues( - query, - rows as string[], - ); + const rowDimensionValues = transformDimensionsValues(query, rows); const colDimensionValues = transformDimensionsValues( query, columns as string[], @@ -430,17 +450,20 @@ export class PivotDataSet extends BaseDataSet { colDimensionValues, rowPivotMeta: this.rowPivotMeta, colPivotMeta: this.colPivotMeta, + rowFields: rows as string[], + colFields: columns as string[], + prefix: getDataPathPrefix(rows as string[], columns as string[]), }); - const rawData = get(this.indexesData, path); + const rawData = get(this.indexesData, path as PropertyPath); if (rawData) { // 如果已经有数据则取已有数据 - return new CellData(rawData, query[EXTRA_FIELD]); + return CellData.getCellData(rawData, query[EXTRA_FIELD]); } if (isTotals) { - return this.getTotalValue(query); + return this.getTotalValue(query, totalStatus); } } @@ -450,7 +473,7 @@ export class PivotDataSet extends BaseDataSet { if (isSubTotal) { const firstDimension = find(dimensions, (item) => !has(query, item)); - return firstDimension && firstDimension !== first(dimensions); + return !!(firstDimension && firstDimension !== first(dimensions)); } return every(dimensions, (item) => !has(query, item)); @@ -464,135 +487,111 @@ export class PivotDataSet extends BaseDataSet { }; }; - protected getMultiDataQueryPath(query: Query, drillDownFields?: string[]) { - const { rows = [], columns = [] } = this.fields; - const totalRows = !isEmpty(drillDownFields) - ? uniq(rows.concat(drillDownFields!)) - : rows; + protected getQueryExtraFields(query: Query): string[] { + const { values = [] } = this.fields; + const extra = query[EXTRA_FIELD]; + + if (extra) { + return includes(values, extra) ? [extra] : []; + } + + return values; + } + + public getCellMultiData(params: GetCellMultiDataParams) { + const { + query = {}, + queryType = QueryDataType.All, + drillDownFields = [], + } = params || {}; + + if (isEmpty(query)) { + // 如果查询的 query 为空,这样的场景其实没有意义,如果用户想获取全量数据,可以直接从 data 中获取 + // eslint-disable-next-line no-console + console.warn( + `query: ${query} shouldn't be empty, you can get all data from dataCfg if you're intended.\n you should use { EXTRA_FIELD: xxx} as least if you want query all specific value data`, + ); + } + + const { rows, columns } = this.fields; + const totalRows: string[] = !isEmpty(drillDownFields) + ? (rows as string[]).concat(drillDownFields!) + : (rows as string[]); const rowDimensionValues = transformDimensionsValues( query, - totalRows as string[], + totalRows, MULTI_VALUE, ); - const colDimensionValues = transformDimensionsValues( query, columns as string[], MULTI_VALUE, ); - const path: (string | number)[] = getDataPath({ + const { rowQueries, colQueries } = getFlattenDimensionValues({ rowDimensionValues, colDimensionValues, rowPivotMeta: this.rowPivotMeta, colPivotMeta: this.colPivotMeta, + rowFields: totalRows, + colFields: columns as string[], + sortedDimensionValues: this.sortedDimensionValues, + queryType, }); - return { path, rows: totalRows, columns }; - } - - protected getQueryExtraFields(query: Query) { - const { values } = this.fields; - - return query?.[EXTRA_FIELD] ? [query[EXTRA_FIELD]] : values; - } - - protected getTotalSelectionByDimensions( - rows: string[], - columns: string[], - totals?: TotalSelectionsOfMultiData, - ) { - const getTotalSelectTypes = ( - dimensions: string[], - totalSelection?: TotalSelection, - ) => { - const { grandTotalOnly, subTotalOnly, totalDimensions } = - totalSelection || ({} as TotalSelection); - - return filterExtraDimension(dimensions).map((dimension, idx) => { - let type = DataSelectType.DetailOnly; - - if ( - totalDimensions === true || - includes(totalDimensions as string[], dimension) - ) { - type = DataSelectType.All; - } + const prefix = getDataPathPrefix(totalRows, columns as string[]); + const all: RawData[] = []; + + for (const rowQuery of rowQueries) { + for (const colQuery of colQueries) { + const path = getDataPath({ + rowDimensionValues: rowQuery, + colDimensionValues: colQuery, + rowPivotMeta: this.rowPivotMeta, + colPivotMeta: this.colPivotMeta, + rowFields: totalRows, + colFields: columns as string[], + prefix, + }); - // 如果当前可以选择总计/小计数据,则进一步根据 grandTotalOnly 以及 subTotalOnly 收缩范围 - if ( - type === DataSelectType.All && - ((idx === 0 && grandTotalOnly) || (idx !== 0 && subTotalOnly)) - ) { - type = DataSelectType.TotalOnly; + let hadMultiField = false; + let result: any = this.indexesData; + + for (let i = 0; i < path.length; i++) { + const current = path[i]; + + if (hadMultiField) { + if (isMultiValue(current)) { + result = flattenIndexesData(result, queryType); + } else { + result = compact( + map(result, (item) => item?.[current as string | number]), + ); + } + } else if (isMultiValue(current)) { + hadMultiField = true; + result = compact([result]); + i--; + } else { + result = result?.[current as string | number]; + } } - - return type; - }); - }; - - totals = getTotalSelection(totals); - - const rowSelectTypes = getTotalSelectTypes(rows, totals?.row); - const columnSelectTypes = getTotalSelectTypes(columns, totals?.column); - - return rowSelectTypes.concat(columnSelectTypes); - } - - public getCellMultiData(params: GetCellMultiDataParams) { - const { query, totals, drillDownFields } = params; - const { path, rows, columns } = this.getMultiDataQueryPath( - query, - drillDownFields, - ); - - const selectTypes = this.getTotalSelectionByDimensions( - rows as string[], - columns as string[], - totals, - ); - - let hadMultiField = false; - let result: FlattingIndexesData = this.indexesData; - - /* - * TODO: 原本的展开逻辑在有下钻的情况下,应该是有问题的, - * 因为下钻数据是放在原本的明细数据里面, 在对象里面添加了 1,2,3 这样的属性值来储存下钻数据 - * 由于对下钻整体的逻辑还不是很清楚,这里只是对原来的逻辑进行效率优化 - * 后续如果需要进行下钻优化,这里也需要同时处理 - */ - for (let i = 0; i < path.length; i++) { - const current = path[i]; - - if (hadMultiField) { - if (shouldQueryMultiData(current)) { - result = flattenIndexesData( - result, - selectTypes[i], - ) as FlattingIndexesData; - } else { - result = map(result!, (item) => get(item, current)); + // 如果每一个维度都是被指定好的,那么最终获取的数据就是单个的 + if (isArray(result)) { + all.push(...(result as Data[])); + } else if (result) { + all.push(result); } - } else if (shouldQueryMultiData(current)) { - hadMultiField = true; - result = [result] as FlattingIndexesData; - i--; - } else { - result = get(result, current); } } - // 如果每一个维度都是被指定好的,那么最终获取的数据就是单个的 - if (!isArray(result)) { - result = [result]; - } - - const extraFields = this.getQueryExtraFields(query)!; + const extraFields = this.getQueryExtraFields(query); - return flatMap(compact(result as RawData[]), (item) => - CellData.getCellDataList(item, extraFields), - ); + // 多个 extra field 有时对应的同一个对象,需要进行去重 + return flatMap(uniq(all), (item) => { + return item ? CellData.getCellDataList(item, extraFields) : []; + }); } public getFieldFormatter(field: string, cellMeta?: ViewMeta): Formatter { @@ -644,7 +643,7 @@ export class PivotDataSet extends BaseDataSet { return isNumber(customValueOrder); } - public getRowData(cellMeta: CellMeta): RowData { + public getRowData(cellMeta: CellMeta) { return this.getCellMultiData({ query: cellMeta.rowQuery! }); } } diff --git a/packages/s2-core/src/data-set/table-data-set.ts b/packages/s2-core/src/data-set/table-data-set.ts index 7c3bcd4569..72e7d233e4 100644 --- a/packages/s2-core/src/data-set/table-data-set.ts +++ b/packages/s2-core/src/data-set/table-data-set.ts @@ -1,10 +1,14 @@ -import { each, orderBy, filter, includes, isFunction, isObject } from 'lodash'; -import { isAscSort, isDescSort } from '..'; -import type { S2DataConfig, RawData, Data } from '../common/interface'; +import { each, filter, hasIn, isFunction, isObject, orderBy } from 'lodash'; import type { CellMeta } from '../common'; -import type { RowData } from '../common/interface/basic'; -import type { GetCellMultiDataParams, Query } from './interface'; +import type { + Data, + RawData, + S2DataConfig, + SimpleData, +} from '../common/interface'; +import { isAscSort, isDescSort } from '../utils/sort-action'; import { BaseDataSet } from './base-data-set'; +import type { GetCellMultiDataParams } from './interface'; export class TableDataSet extends BaseDataSet { public processDataCfg(dataCfg: S2DataConfig): S2DataConfig { @@ -21,15 +25,13 @@ export class TableDataSet extends BaseDataSet { * 返回顶部冻结行 * @returns */ - protected getStartRows() { - const { rowCount } = this.spreadsheet.options.frozen!; + protected getStartFrozenRows(displayData: RawData[]): RawData[] { + const { rowCount } = this.spreadsheet.options.frozen! || {}; if (!rowCount) { return []; } - const { displayData } = this; - return displayData.slice(0, rowCount); } @@ -37,48 +39,44 @@ export class TableDataSet extends BaseDataSet { * 返回底部冻结行 * @returns */ - protected getEndRows() { - const { trailingRowCount } = this.spreadsheet.options.frozen!; + protected getEndFrozenRows(displayData: RawData[]): RawData[] { + const { trailingRowCount } = this.spreadsheet.options.frozen! || {}; // 没有冻结行时返回空数组 if (!trailingRowCount) { return []; } - const { displayData } = this; - return displayData.slice(-trailingRowCount); } - /** - * 返回可移动的非冻结行 - * @returns - */ - protected getMovableRows(): RawData[] { - const { trailingRowCount, rowCount } = this.spreadsheet.options.frozen!; + protected getDisplayData(displayData: RawData[]): RawData[] { + const startFrozenRows = this.getStartFrozenRows(displayData); + const endFrozenRows = this.getEndFrozenRows(displayData); - return this.displayData.slice( - rowCount || 0, - -trailingRowCount! || undefined, + const data = displayData.slice( + startFrozenRows.length || 0, + -endFrozenRows.length || undefined, ); + + return [...startFrozenRows, ...data, ...endFrozenRows]; } handleDimensionValueFilter = () => { each(this.filterParams, ({ filterKey, filteredValues, customFilter }) => { - const defaultFilterFunc = (row: Query) => - !includes(filteredValues, row[filterKey]); - - this.displayData = [ - ...this.getStartRows(), - ...filter(this.getMovableRows(), (row) => { - if (customFilter) { - return customFilter(row) && defaultFilterFunc(row); - } + const filteredValuesSet = new Set(filteredValues); + const defaultFilterFunc = (row: RawData) => + !filteredValuesSet.has(row[filterKey]); - return defaultFilterFunc(row); - }), - ...this.getEndRows(), - ]; + const filteredData = filter(this.displayData, (row) => { + if (customFilter) { + return customFilter(row) && defaultFilterFunc(row); + } + + return defaultFilterFunc(row); + }); + + this.displayData = this.getDisplayData(filteredData); }); }; @@ -92,7 +90,7 @@ export class TableDataSet extends BaseDataSet { return; } - let data = this.getMovableRows(); + let data = this.displayData; const restData: RawData[] = []; @@ -151,11 +149,7 @@ export class TableDataSet extends BaseDataSet { } // For frozen options - this.displayData = [ - ...this.getStartRows(), - ...sortedData, - ...this.getEndRows(), - ]; + this.displayData = this.getDisplayData(sortedData); }); }; @@ -163,25 +157,44 @@ export class TableDataSet extends BaseDataSet { return []; } - public getCellData({ query }: GetCellMultiDataParams): Data { + public getCellData({ query = {} }: GetCellMultiDataParams = {}): + | Data + | SimpleData + | undefined { if (this.displayData.length === 0 && query['rowIndex'] === 0) { - return; + return undefined; } const rowData = this.displayData[query['rowIndex']]; - if (!('col' in query) || !isObject(rowData)) { + if (!hasIn(query, 'field') || !isObject(rowData)) { return rowData as Data; } - return rowData[query['col']] as unknown as Data; + return rowData[query['field']] as SimpleData; } - public getCellMultiData(): Data[] { - return this.displayData as Data[]; + public getCellMultiData({ query = {} }: GetCellMultiDataParams = {}): Data[] { + if (!query) { + return this.displayData as Data[]; + } + + const rowData = this.displayData[query['rowIndex']] + ? [this.displayData[query['rowIndex']]] + : this.displayData; + + if (!hasIn(query, 'field')) { + return rowData as Data[]; + } + + return rowData.map((item) => item[query['field']]) as Data[]; } - public getRowData(cellMeta: CellMeta): RowData { - return this.getCellData({ query: { rowIndex: cellMeta.rowIndex } }); + public getRowData(cell: CellMeta) { + return this.getCellData({ + query: { + rowIndex: cell.rowIndex, + }, + }) as Data; } } diff --git a/packages/s2-core/src/facet/README-adjustTotalNodesCoordinate.md b/packages/s2-core/src/facet/README-adjustTotalNodesCoordinate.md new file mode 100644 index 0000000000..3998b5fcf6 --- /dev/null +++ b/packages/s2-core/src/facet/README-adjustTotalNodesCoordinate.md @@ -0,0 +1,57 @@ +## 小计总计节点布局逻辑 + +1. 计算一个 multipleMap ,表示每个 level 的汇总节点实际占据一个单元格 +2. 遍历所有汇总单元格,根据 multipleMap 中的值,计算宽或高为合并多个单元格 + +🌰 例子: + +rows: [ 省份, 城市, 类别, 子类别 ] + +Total - row - totalDimensionGroup: [ 城市, 类别 ] + +Total - row - subTotalDimensionGroup: [ 子类别 ] + +row + +### multipleMap 如何计算 - getMultipleMap 函数 + +行总计 multipleMap: + +1. 初始化一个长度与 rows 相同的数组 [ 1, 1, 1 ,1 ] +2. 从后往前判断 totalDimensionGroup.includes(field) + - 第一个处理子类别,不做处理 + - 第二个处理 类别,判断为 false,将类别数值向前移动[ 1, 2, 0, 1] + - 第三个判断 城市,判断为 false,将城市数值向前移动[ 3, 0, 0, 1] + - 第四个 省份,首个维度不做遍历 +3. multipleMap 结果为 [ 3, 0, 0, 1] + +行小计 multipleMap: + +1. 初始化一个长度与 rows 相同的数组 [ 1, 1, 1 ,1 ] +2. 从后往前判断 subTotalDimensionGroup.includes(field) + - 第一个处理子类别,判断为 true,不做处理 + - 第二个处理 类别,判断为 false,将类别数值向前移动[ 1, 1, 2, 0] + - 第三个判断 城市,判断为 flase,不做处理 + - 第四个 省份,首个维度不做遍历 +3. multipleMap 结果为 [ 1, 1, 2, 0] + +### multipleMap 如何使用 - adjustTotalNodesCoordinate 函数 + +总计的 multipleMap = [ 1 , 1 ,2 ,0 ] + +multipleMap 表示: + +- row 的第一第二个维度占据一个单元格 +- 第三个维度合并两个单元格 +- 第四个维度没有节点 + +小计的 multipleMap = [ 3,0,0,1 ] + +multipleMap 表示: + +- 小计结点,row的第一个维度合并三个单元格 +- 小计结点的最后一个维度占据一个单元格 + +当小计节点出现在第二个维度,则合并量为‘最近的上一个不为0的值’ - level 差 + +即实际该小计节点实际的 multipleMap 为 [1,2,0,1] diff --git a/packages/s2-core/src/facet/base-facet.ts b/packages/s2-core/src/facet/base-facet.ts index 1cd3eeec29..8375b25398 100644 --- a/packages/s2-core/src/facet/base-facet.ts +++ b/packages/s2-core/src/facet/base-facet.ts @@ -47,6 +47,7 @@ import { KEY_GROUP_PANEL_SCROLL, KEY_GROUP_ROW_INDEX_RESIZE_AREA, KEY_GROUP_ROW_RESIZE_AREA, + OriginEventType, PANEL_GROUP_GROUP_CONTAINER_Z_INDEX, PANEL_GROUP_SCROLL_GROUP_Z_INDEX, S2Event, @@ -54,9 +55,9 @@ import { } from '../common/constant'; import { DEFAULT_PAGE_INDEX } from '../common/constant/pagination'; import { - DebuggerUtil, DEBUG_HEADER_LAYOUT, DEBUG_VIEW_RENDER, + DebuggerUtil, } from '../common/debug'; import type { AdjustLeafNodesParams, @@ -83,22 +84,24 @@ import { getAdjustedRowScrollX, getAdjustedScrollOffset } from '../utils/facet'; import { getAllChildCells } from '../utils/get-all-child-cells'; import { getColsForGrid, getRowsForGrid } from '../utils/grid'; import { diffPanelIndexes, type PanelIndexes } from '../utils/indexes'; -import { isMobile } from '../utils/is-mobile'; -import { CornerBBox } from './bbox/cornerBBox'; -import { PanelBBox } from './bbox/panelBBox'; +import { isMobile, isWindows } from '../utils/is-mobile'; +import { floor } from '../utils/math'; +import { CornerBBox } from './bbox/corner-bbox'; +import { PanelBBox } from './bbox/panel-bbox'; import { ColHeader, CornerHeader, Frame, RowHeader, SeriesNumberHeader, + type RowHeaderConfig, } from './header'; import type { Hierarchy } from './layout/hierarchy'; import type { ViewCellHeights } from './layout/interface'; import { Node } from './layout/node'; import { WheelEvent as MobileWheel } from './mobile/wheelEvent'; import { - calculateInViewIndexes, + areAllFieldsEmpty, getCellRange, optimizeScrollXY, translateGroup, @@ -141,7 +144,7 @@ export abstract class BaseFacet { * 当前布局节点信息 * @description 内部消费, 外部调用请使用 facet.getLayoutResult() */ - private layoutResult: LayoutResult; + protected layoutResult: LayoutResult; public viewCellWidths: number[]; @@ -171,8 +174,17 @@ export abstract class BaseFacet { protected abstract doLayout(): LayoutResult; + protected abstract clip(scrollX: number, scrollY: number): void; + + public abstract calculateXYIndexes( + scrollX: number, + scrollY: number, + ): PanelIndexes; + public abstract getViewCellHeights(): ViewCellHeights; + public abstract addDataCell(cell: DataCell): void; + public abstract getCellMeta( rowIndex: number, colIndex: number, @@ -212,7 +224,6 @@ export abstract class BaseFacet { protected initGroups() { const container = this.spreadsheet.container; - // the main three layer groups this.backgroundGroup = container.appendChild( new Group({ name: KEY_GROUP_BACK_GROUND, @@ -221,6 +232,7 @@ export abstract class BaseFacet { ); this.initPanelGroups(); + this.foregroundGroup = container.appendChild( new Group({ name: KEY_GROUP_FORE_GROUND, @@ -356,11 +368,25 @@ export abstract class BaseFacet { colsHierarchy.sampleNodesForAllLevels = compact( sampleMaxHeightNodesForAllLevels, ); + colsHierarchy.sampleNodesForAllLevels.forEach((levelSampleNode) => { levelSampleNode.height = this.getColNodeHeight( levelSampleNode, colsHierarchy, ); + if (levelSampleNode.level === 0) { + levelSampleNode.y = 0; + } else { + const preLevelSample = colsHierarchy.sampleNodesForAllLevels[ + levelSampleNode.level - 1 + ] ?? { + y: 0, + height: 0, + }; + + levelSampleNode.y = preLevelSample.y + preLevelSample.height; + } + colsHierarchy.height += levelSampleNode.height; }); } @@ -410,6 +436,7 @@ export abstract class BaseFacet { const { deltaX, deltaY, x, y } = ev; // The coordinates of mobile and pc are three times different + // TODO: 手指快速往上滚动时, deltaY 有时会为负数, 导致向下滚动时然后回弹, 看起来就像表格在抖动, 需要判断滚动方向, next 版本未复现 this.onWheel({ ...originEvent, deltaX, @@ -429,6 +456,10 @@ export abstract class BaseFacet { * Start render, call from outside */ public render() { + if (areAllFieldsEmpty(this.spreadsheet.dataCfg.fields)) { + return; + } + this.adjustScrollOffset(); this.renderHeaders(); this.renderScrollBars(); @@ -528,7 +559,7 @@ export abstract class BaseFacet { const offset = get(scrollOffset, key); if (!isUndefined(offset)) { - this.spreadsheet.store.set(key, Math.floor(offset)); + this.spreadsheet.store.set(key, floor(offset)); } }); }; @@ -566,7 +597,7 @@ export abstract class BaseFacet { const { current = DEFAULT_PAGE_INDEX, pageSize } = pagination; const total = this.viewCellHeights.getTotalLength(); - const pageCount = Math.floor((total - 1) / pageSize) + 1; + const pageCount = floor((total - 1) / pageSize) + 1; this.spreadsheet.emit(S2Event.LAYOUT_PAGINATION, { pageSize, @@ -602,35 +633,6 @@ export abstract class BaseFacet { this.viewCellHeights = this.getViewCellHeights(); }; - /** - * The purpose of this rewrite is to take into account that when rowHeader supports scrollbars - *the panel viewable area must vary with the horizontal distance of the scroll - * @param scrollX - * @param scrollY - * @public - */ - public calculateXYIndexes(scrollX: number, scrollY: number): PanelIndexes { - const { viewportHeight: height, viewportWidth: width } = this.panelBBox; - - const indexes = calculateInViewIndexes({ - scrollX, - scrollY, - widths: this.viewCellWidths, - heights: this.viewCellHeights, - viewport: { - width, - height, - x: 0, - y: 0, - }, - rowRemainWidth: this.getRealScrollX(this.cornerBBox.width), - }); - - return { - center: indexes, - }; - } - getRealScrollX = (scrollX: number, hRowScroll = 0) => this.spreadsheet.isFrozenRowHeader() ? hRowScroll : scrollX; @@ -710,19 +712,25 @@ export abstract class BaseFacet { ); this.timer = timer((elapsed) => { - const ratio = Math.min(elapsed / duration, 1); - const [scrollX, scrollY, rowHeaderScrollX] = interpolate(ratio); - - this.setScrollOffset({ - rowHeaderScrollX, - scrollX, - scrollY, - }); - this.startScroll(); + try { + const ratio = Math.min(elapsed / duration, 1); + const [scrollX, scrollY, rowHeaderScrollX] = interpolate(ratio); + + this.setScrollOffset({ + rowHeaderScrollX, + scrollX, + scrollY, + }); + this.startScroll(); - if (elapsed > duration) { + if (elapsed > duration) { + this.timer.stop(); + cb?.(); + } + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); this.timer.stop(); - cb?.(); } }); }; @@ -829,7 +837,7 @@ export abstract class BaseFacet { ScrollType.ScrollChange, ({ offset }: ScrollChangeParams) => { const newOffset = this.getValidScrollBarOffset(offset, maxOffset); - const rowHeaderScrollX = Math.floor(newOffset); + const rowHeaderScrollX = floor(newOffset); this.setScrollOffset({ rowHeaderScrollX }); @@ -871,7 +879,7 @@ export abstract class BaseFacet { clamp(offset, 0, maxOffset); renderHScrollBar = (width: number, realWidth: number, scrollX: number) => { - if (Math.floor(width) < Math.floor(realWidth)) { + if (floor(width) < floor(realWidth)) { const halfScrollSize = this.scrollBarSize / 2; const { maxY } = this.getScrollbarPosition(); const isScrollContainsRowHeader = !this.spreadsheet.isFrozenRowHeader(); @@ -1158,9 +1166,16 @@ export abstract class BaseFacet { }; private stopScrollChaining = (event: WheelEvent) => { - event?.preventDefault?.(); + if (event?.cancelable) { + event?.preventDefault?.(); + } + // 移动端的 prevent 存在于 originalEvent上 - (event as unknown as GraphEvent)?.originalEvent?.preventDefault?.(); + const mobileEvent = (event as unknown as GraphEvent)?.originalEvent; + + if (mobileEvent?.cancelable) { + mobileEvent?.preventDefault?.(); + } }; onWheel = (event: WheelEvent) => { @@ -1168,8 +1183,9 @@ export abstract class BaseFacet { let { deltaX, deltaY, offsetX, offsetY } = event; const { shiftKey } = event; - // 按住shift时,固定为水平方向滚动 - if (shiftKey) { + // Windows 环境,按住 shift 时,固定为水平方向滚动,macOS 环境默认有该行为 + // see https://github.com/antvis/S2/issues/2198 + if (shiftKey && isWindows()) { offsetX = offsetX - deltaX + deltaY; deltaX = deltaY; offsetY -= deltaY; @@ -1265,26 +1281,21 @@ export abstract class BaseFacet { scrollY, KEY_GROUP_ROW_INDEX_RESIZE_AREA, ); - this.cornerHeader.onCorScroll( + this.cornerHeader?.onCorScroll( this.getRealScrollX(scrollX, hRowScroll), KEY_GROUP_CORNER_RESIZE_AREA, ); - this.centerFrame.onChangeShadowVisibility( + this.centerFrame?.onChangeShadowVisibility( scrollX, this.getRealWidth() - this.panelBBox.width, ); - this.centerFrame.onBorderScroll(this.getRealScrollX(scrollX)); - this.columnHeader.onColScroll(scrollX, KEY_GROUP_COL_RESIZE_AREA); + this.centerFrame?.onBorderScroll(this.getCenterFrameScrollX(scrollX)); + this.columnHeader?.onColScroll(scrollX, KEY_GROUP_COL_RESIZE_AREA); } - addDataCell = (cell: DataCell) => { - this.panelScrollGroup?.appendChild(cell); - - setTimeout(() => { - this.spreadsheet.emit(S2Event.DATA_CELL_RENDER, cell); - this.spreadsheet.emit(S2Event.LAYOUT_CELL_RENDER, cell); - }, 100); - }; + protected getCenterFrameScrollX(scrollX: number) { + return this.getRealScrollX(scrollX); + } realDataCellRender = (scrollX: number, scrollY: number) => { const indexes = this.calculateXYIndexes(scrollX, scrollY); @@ -1305,6 +1316,11 @@ export abstract class BaseFacet { if (viewMeta) { const cell = this.spreadsheet.options.dataCell?.(viewMeta)!; + if (!cell) { + return; + } + + cell.position = [j, i]; // mark cell for removing cell.name = `${i}-${j}`; this.addDataCell(cell); @@ -1431,20 +1447,24 @@ export abstract class BaseFacet { this.foregroundGroup.appendChild(this.centerFrame); } + protected getRowHeaderCfg(): RowHeaderConfig { + const { y, viewportHeight, viewportWidth, height } = this.panelBBox; + const seriesNumberWidth = this.getSeriesNumberWidth(); + + return { + width: this.cornerBBox.width, + height, + viewportWidth, + viewportHeight, + position: { x: seriesNumberWidth, y }, + nodes: this.layoutResult.rowNodes, + spreadsheet: this.spreadsheet, + }; + } + protected getRowHeader(): RowHeader | null { if (!this.rowHeader) { - const { y, viewportHeight, viewportWidth, height } = this.panelBBox; - const seriesNumberWidth = this.getSeriesNumberWidth(); - - return new RowHeader({ - width: this.cornerBBox.width, - height, - viewportWidth, - viewportHeight, - position: { x: seriesNumberWidth, y }, - nodes: this.layoutResult.rowNodes, - spreadsheet: this.spreadsheet, - }); + return new RowHeader(this.getRowHeaderCfg()); } return this.rowHeader; @@ -1524,8 +1544,12 @@ export abstract class BaseFacet { protected getGridInfo = () => { const [colMin, colMax, rowMin, rowMax] = this.preCellIndexes!.center; - const cols = getColsForGrid(colMin, colMax, this.layoutResult.colLeafNodes); - const rows = getRowsForGrid(rowMin, rowMax, this.viewCellHeights); + const cols = getColsForGrid( + colMin!, + colMax!, + this.layoutResult.colLeafNodes, + ); + const rows = getRowsForGrid(rowMin!, rowMax!, this.viewCellHeights); return { cols, @@ -1550,12 +1574,7 @@ export abstract class BaseFacet { scrollY: originalScrollY, rowHeaderScrollX, } = this.getScrollOffset(); - const defaultScrollY = originalScrollY + this.getPaginationScrollY(); - const scrollY = getAdjustedScrollOffset( - defaultScrollY, - this.viewCellHeights.getTotalHeight(), - this.panelBBox.viewportHeight, - ); + const scrollY = originalScrollY + this.getPaginationScrollY(); this.spreadsheet.hideTooltip(); this.spreadsheet.interaction.clearHoverTimer(); @@ -1563,6 +1582,9 @@ export abstract class BaseFacet { this.realDataCellRender(scrollX, scrollY); this.updatePanelScrollGroup(); this.translateRelatedGroups(scrollX, scrollY, rowHeaderScrollX); + + this.clip(scrollX, scrollY); + if (!skipScrollEvent) { this.emitScrollEvent({ scrollX, scrollY, rowHeaderScrollX }); } @@ -1575,11 +1597,31 @@ export abstract class BaseFacet { } protected onAfterScroll = debounce(() => { - const { interaction } = this.spreadsheet; + const { interaction, container } = this.spreadsheet; // 如果是选中单元格状态, 则继续保留 hover 拦截, 避免滚动后 hover 清空已选单元格 if (!interaction.isSelectedState()) { - this.spreadsheet.interaction.removeIntercepts([InterceptType.HOVER]); + interaction.removeIntercepts([InterceptType.HOVER]); + + if (interaction.getHoverAfterScroll()) { + // https://github.com/antvis/S2/issues/2222 + const canvasMousemoveEvent = + interaction.eventController.canvasMousemoveEvent; + + if (canvasMousemoveEvent) { + const { x, y } = canvasMousemoveEvent; + const shape = container.document.elementFromPointSync(x, y); + + if (shape) { + container.emit(OriginEventType.POINTER_MOVE, { + ...canvasMousemoveEvent, + shape, + target: shape, + timestamp: performance.now(), + }); + } + } + } } }, 300); @@ -1607,8 +1649,9 @@ export abstract class BaseFacet { return; } - return hiddenColumnsDetail.find((detail) => - detail.hideColumnNodes.some((node) => node.id === columnNode.id), + return hiddenColumnsDetail.find( + (detail) => + detail?.hideColumnNodes?.some((node) => node.id === columnNode.id), ); } @@ -1732,6 +1775,10 @@ export abstract class BaseFacet { return colNodes.filter((node) => node.level === level); } + public getTopLevelColNodes() { + return this.getColNodes(0); + } + /** * 根据 id 获取指定列头节点 * @example facet.getColNodeById('root[&]节点1[&]数值') diff --git a/packages/s2-core/src/facet/bbox/baseBBox.ts b/packages/s2-core/src/facet/bbox/base-bbox.ts similarity index 100% rename from packages/s2-core/src/facet/bbox/baseBBox.ts rename to packages/s2-core/src/facet/bbox/base-bbox.ts diff --git a/packages/s2-core/src/facet/bbox/cornerBBox.ts b/packages/s2-core/src/facet/bbox/corner-bbox.ts similarity index 93% rename from packages/s2-core/src/facet/bbox/cornerBBox.ts rename to packages/s2-core/src/facet/bbox/corner-bbox.ts index e7ca126e02..6aebf3f4b8 100644 --- a/packages/s2-core/src/facet/bbox/cornerBBox.ts +++ b/packages/s2-core/src/facet/bbox/corner-bbox.ts @@ -1,6 +1,7 @@ import { clamp, isBoolean } from 'lodash'; import { DEFAULT_CORNER_MAX_WIDTH_RATIO } from '../../common/constant'; -import { BaseBBox } from './baseBBox'; +import { floor } from '../../utils/math'; +import { BaseBBox } from './base-bbox'; export class CornerBBox extends BaseBBox { calculateBBox() { @@ -26,7 +27,7 @@ export class CornerBBox extends BaseBBox { return colCell?.height; } - return Math.floor(colsHierarchy.height); + return floor(colsHierarchy.height); } private getCornerBBoxHeight() { @@ -38,7 +39,7 @@ export class CornerBBox extends BaseBBox { private getCornerBBoxWidth() { const { rowsHierarchy } = this.layoutResult; - this.originalWidth = Math.floor( + this.originalWidth = floor( rowsHierarchy.width + this.facet.getSeriesNumberWidth(), ); @@ -87,6 +88,6 @@ export class CornerBBox extends BaseBBox { clippedWidth = maxCornerBBoxWidth; } - return Math.floor(clippedWidth); + return floor(clippedWidth); } } diff --git a/packages/s2-core/src/facet/bbox/panelBBox.ts b/packages/s2-core/src/facet/bbox/panel-bbox.ts similarity index 85% rename from packages/s2-core/src/facet/bbox/panelBBox.ts rename to packages/s2-core/src/facet/bbox/panel-bbox.ts index aa27d03aad..3cea077be9 100644 --- a/packages/s2-core/src/facet/bbox/panelBBox.ts +++ b/packages/s2-core/src/facet/bbox/panel-bbox.ts @@ -1,5 +1,6 @@ +import { floor } from '../../utils/math'; import { Frame } from '../header/frame'; -import { BaseBBox } from './baseBBox'; +import { BaseBBox } from './base-bbox'; export class PanelBBox extends BaseBBox { calculateBBox() { @@ -8,8 +9,8 @@ export class PanelBBox extends BaseBBox { const { cornerBBox } = this.facet; const cornerPosition = { - x: Math.floor(cornerBBox.maxX), - y: Math.floor(cornerBBox.maxY), + x: floor(cornerBBox.maxX), + y: floor(cornerBBox.maxY), }; // splitLine 也应该占位,panelBBox = canvasBBox - cornerBBox - splitLineBBox @@ -30,10 +31,10 @@ export class PanelBBox extends BaseBBox { this.width = panelWidth; this.height = panelHeight; this.viewportHeight = Math.abs( - Math.floor(Math.min(panelHeight, this.originalHeight)), + floor(Math.min(panelHeight, this.originalHeight)), ); this.viewportWidth = Math.abs( - Math.floor(Math.min(panelWidth, this.originalWidth)), + floor(Math.min(panelWidth, this.originalWidth)), ); this.maxX = this.x + this.viewportWidth; this.maxY = this.y + this.viewportHeight; diff --git a/packages/s2-core/src/facet/frozen-facet.ts b/packages/s2-core/src/facet/frozen-facet.ts new file mode 100644 index 0000000000..5045e4b778 --- /dev/null +++ b/packages/s2-core/src/facet/frozen-facet.ts @@ -0,0 +1,775 @@ +import { Group, Rect, type LineStyleProps } from '@antv/g'; +import { last } from 'lodash'; +import type { DataCell } from '../cell'; +import { + FRONT_GROUND_GROUP_FROZEN_Z_INDEX, + FrozenCellGroupMap, + FrozenGroupType, + KEY_GROUP_FROZEN_SPLIT_LINE, + KEY_GROUP_PANEL_FROZEN_BOTTOM, + KEY_GROUP_PANEL_FROZEN_COL, + KEY_GROUP_PANEL_FROZEN_ROW, + KEY_GROUP_PANEL_FROZEN_TOP, + KEY_GROUP_PANEL_FROZEN_TRAILING_COL, + KEY_GROUP_PANEL_FROZEN_TRAILING_ROW, + PANEL_GROUP_FROZEN_GROUP_Z_INDEX, + S2Event, + SPLIT_LINE_WIDTH, +} from '../common/constant'; +import type { SimpleBBox } from '../engine'; +import { FrozenGroup } from '../group/frozen-group'; +import { getValidFrozenOptions, renderLine } from '../utils'; +import { + getColsForGrid, + getFrozenRowsForGrid, + getRowsForGrid, +} from '../utils/grid'; +import type { Indexes, PanelIndexes } from '../utils/indexes'; +import { floor } from '../utils/math'; +import { BaseFacet } from './base-facet'; +import { Frame } from './header/frame'; +import { Node } from './layout/node'; +import { + calculateFrozenCornerCells, + calculateInViewIndexes, + getFrozenDataCellType, + getFrozenLeafNodesCount, + splitInViewIndexesWithFrozen, + translateGroup, +} from './utils'; + +/** + * Defines the row freeze abstract standard interface + */ +export abstract class FrozenFacet extends BaseFacet { + public rowOffsets: number[]; + + public frozenGroupInfo = { + [FrozenGroupType.FROZEN_COL]: { + width: 0, + x: 0, + range: [] as number[], + }, + [FrozenGroupType.FROZEN_TRAILING_COL]: { + width: 0, + x: 0, + range: [] as number[], + }, + [FrozenGroupType.FROZEN_ROW]: { + height: 0, + y: 0, + range: [] as number[], + }, + [FrozenGroupType.FROZEN_TRAILING_ROW]: { + height: 0, + y: 0, + range: [] as number[], + }, + } satisfies Record< + FrozenGroupType, + { + width?: number; + height?: number; + x?: number; + y?: number; + range: number[]; + } + >; + + public panelScrollGroupIndexes: Indexes = [0, 0, 0, 0]; + + protected override initPanelGroups(): void { + super.initPanelGroups(); + [ + this.frozenRowGroup, + this.frozenColGroup, + this.frozenTrailingRowGroup, + this.frozenTrailingColGroup, + this.frozenTopGroup, + this.frozenBottomGroup, + ] = [ + KEY_GROUP_PANEL_FROZEN_ROW, + KEY_GROUP_PANEL_FROZEN_COL, + KEY_GROUP_PANEL_FROZEN_TRAILING_ROW, + KEY_GROUP_PANEL_FROZEN_TRAILING_COL, + KEY_GROUP_PANEL_FROZEN_TOP, + KEY_GROUP_PANEL_FROZEN_BOTTOM, + ].map((name) => { + const frozenGroup = new FrozenGroup({ + name, + zIndex: PANEL_GROUP_FROZEN_GROUP_Z_INDEX, + s2: this.spreadsheet, + }); + + this.panelGroup.appendChild(frozenGroup); + + return frozenGroup; + }); + } + + protected getFrozenOptions() { + const colLength = this.getColLeafNodes().length; + const cellRange = this.getCellRange(); + + return getValidFrozenOptions( + this.spreadsheet.options.frozen!, + colLength, + cellRange.end - cellRange.start + 1, + ); + } + + public calculateFrozenGroupInfo() { + const { + colCount = 0, + rowCount = 0, + trailingColCount = 0, + trailingRowCount = 0, + } = this.getFrozenOptions(); + + const topLevelColNodes = this.getTopLevelColNodes(); + const viewCellHeights = this.viewCellHeights; + const cellRange = this.getCellRange(); + const { frozenCol, frozenTrailingCol, frozenRow, frozenTrailingRow } = + this.frozenGroupInfo; + + if (colCount > 0) { + frozenCol.width = + topLevelColNodes[colCount - 1].x + topLevelColNodes[colCount - 1].width; + frozenCol.x = 0; + frozenCol.range = [0, colCount - 1]; + } + + if (rowCount > 0) { + frozenRow.height = + viewCellHeights.getCellOffsetY(cellRange.start + rowCount) - + viewCellHeights.getCellOffsetY(cellRange.start); + frozenRow.y = 0; + frozenRow.range = [cellRange.start, cellRange.start + rowCount - 1]; + } + + if (trailingColCount > 0) { + frozenTrailingCol.width = + topLevelColNodes[topLevelColNodes.length - 1].x - + topLevelColNodes[topLevelColNodes.length - trailingColCount].x + + topLevelColNodes[topLevelColNodes.length - 1].width; + frozenTrailingCol.x = this.panelBBox.width - frozenTrailingCol.width; + frozenTrailingCol.range = [ + topLevelColNodes.length - trailingColCount, + topLevelColNodes.length - 1, + ]; + } + + if (trailingRowCount > 0) { + frozenTrailingRow.height = + viewCellHeights.getCellOffsetY(cellRange.end + 1) - + viewCellHeights.getCellOffsetY(cellRange.end + 1 - trailingRowCount); + frozenTrailingRow.y = this.panelBBox.height - frozenTrailingRow.height; + frozenTrailingRow.range = [ + cellRange.end - trailingRowCount + 1, + cellRange.end, + ]; + } + } + + protected getFinalViewport() { + const { viewportHeight: height, viewportWidth: width } = this.panelBBox; + + const { + colCount = 0, + rowCount = 0, + trailingColCount = 0, + trailingRowCount = 0, + } = this.getFrozenOptions(); + + const finalViewport: SimpleBBox = { + width, + height, + x: 0, + y: 0, + }; + + if (colCount > 0 || trailingColCount > 0) { + const { frozenTrailingCol, frozenCol } = this.frozenGroupInfo; + + finalViewport.width -= frozenTrailingCol.width! + frozenCol.width!; + finalViewport.x += frozenCol.width!; + } + + if (rowCount > 0 || trailingRowCount > 0) { + const { frozenRow, frozenTrailingRow } = this.frozenGroupInfo; + + // canvas 高度小于 row height 和 trailingRow height 的时候 height 为 0 + if ( + finalViewport.height < + frozenRow.height! + frozenTrailingRow.height! + ) { + finalViewport.height = 0; + finalViewport.y = 0; + } else { + finalViewport.height -= frozenRow.height! + frozenTrailingRow.height!; + finalViewport.y += frozenRow.height!; + } + } + + return finalViewport; + } + + public calculateXYIndexes(scrollX: number, scrollY: number): PanelIndexes { + const colLength = this.getColLeafNodes().length; + const cellRange = this.getCellRange(); + + const { + colCount = 0, + rowCount = 0, + trailingColCount = 0, + trailingRowCount = 0, + } = this.getFrozenOptions(); + + const finalViewport: SimpleBBox = this.getFinalViewport(); + + const indexes = + this.spreadsheet.isTableMode() && this.spreadsheet.dataSet?.isEmpty?.() + ? this.spreadsheet.dataSet.getEmptyViewIndexes() + : calculateInViewIndexes({ + scrollX, + scrollY, + widths: this.viewCellWidths, + heights: this.viewCellHeights, + viewport: finalViewport, + rowRemainWidth: this.getRealScrollX(this.cornerBBox.width), + }); + + this.panelScrollGroupIndexes = indexes; + + const { colCount: realColCount, trailingColCount: realTrailingColCount } = + this.getRealFrozenColumns(colCount, trailingColCount); + + return splitInViewIndexesWithFrozen( + indexes, + { + colCount: realColCount, + trailingColCount: realTrailingColCount, + rowCount, + trailingRowCount, + }, + colLength, + cellRange, + ); + } + + addDataCell = (cell: DataCell) => { + const { + rowCount = 0, + colCount = 0, + trailingRowCount = 0, + trailingColCount = 0, + } = this.getFrozenOptions(); + + const colLength = this.getColNodes().length; + const cellRange = this.getCellRange(); + const { colCount: realColCount, trailingColCount: realTrailingColCount } = + this.getRealFrozenColumns(colCount, trailingColCount); + + const frozenCellType = getFrozenDataCellType( + cell.getMeta(), + { + rowCount, + trailingRowCount, + colCount: realColCount, + trailingColCount: realTrailingColCount, + }, + colLength, + cellRange, + ); + + const groupName = FrozenCellGroupMap[frozenCellType]; + + if (groupName) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const group = this[groupName] as Group; + + group.appendChild(cell); + } + + setTimeout(() => { + this.spreadsheet.emit(S2Event.DATA_CELL_RENDER, cell); + this.spreadsheet.emit(S2Event.LAYOUT_CELL_RENDER, cell); + }, 100); + }; + + addFrozenCell = (colIndex: number, rowIndex: number, group: Group) => { + const viewMeta = this.getCellMeta(rowIndex, colIndex); + + if (viewMeta) { + viewMeta.isFrozenCorner = true; + const cell = this.spreadsheet.options.dataCell?.(viewMeta)!; + + group.appendChild(cell); + } + }; + + protected updateFrozenGroupGrid(): void { + [ + FrozenGroupType.FROZEN_COL, + FrozenGroupType.FROZEN_ROW, + FrozenGroupType.FROZEN_TRAILING_COL, + FrozenGroupType.FROZEN_TRAILING_ROW, + ].forEach((key) => { + if (!this.frozenGroupInfo[key].range) { + return; + } + + let cols: number[] = []; + let rows: number[] = []; + + if (key.toLowerCase().includes('row')) { + const [rowMin, rowMax] = this.frozenGroupInfo[key].range || []; + + cols = this.gridInfo.cols; + rows = getRowsForGrid(rowMin, rowMax, this.viewCellHeights); + + if (key === FrozenGroupType.FROZEN_TRAILING_ROW) { + const top = + this.frozenGroupInfo[FrozenGroupType.FROZEN_TRAILING_ROW].y; + + rows = getFrozenRowsForGrid( + rowMin, + rowMax, + top, + this.viewCellHeights, + ); + } + } else { + const [colMin, colMax] = this.frozenGroupInfo[key].range || []; + const nodes = this.getTopLevelColNodes(); + + cols = getColsForGrid(colMin, colMax, nodes); + rows = this.gridInfo.rows; + } + + this[`${key}Group`].updateGrid( + { + cols, + rows, + }, + `${key}Group`, + ); + }); + } + + public updatePanelScrollGroup(): void { + super.updatePanelScrollGroup(); + this.updateFrozenGroupGrid(); + } + + protected translateRelatedGroups( + scrollX: number, + scrollY: number, + hRowScroll: number, + ) { + super.translateRelatedGroups(scrollX, scrollY, hRowScroll); + this.translateFrozenGroups(); + this.updateRowResizeArea(); + this.renderFrozenGroupSplitLine(scrollX, scrollY); + } + + protected translateFrozenGroups = () => { + const { scrollY, scrollX } = this.getScrollOffset(); + const paginationScrollY = this.getPaginationScrollY(); + + const { x, y } = this.panelBBox; + + translateGroup(this.frozenTopGroup, x, y - paginationScrollY); + translateGroup(this.frozenBottomGroup, x, y); + + translateGroup(this.frozenRowGroup, x - scrollX, y - paginationScrollY); + translateGroup(this.frozenTrailingRowGroup, x - scrollX, y); + + translateGroup(this.frozenColGroup, x, y - scrollY - paginationScrollY); + translateGroup( + this.frozenTrailingColGroup, + x, + y - scrollY - paginationScrollY, + ); + }; + + protected updateRowResizeArea() {} + + // eslint-disable-next-line max-lines-per-function + protected renderFrozenGroupSplitLine = (scrollX: number, scrollY: number) => { + const { + width: panelWidth, + height: panelHeight, + viewportWidth, + viewportHeight, + x: panelBBoxStartX, + y: panelBBoxStartY, + } = this.panelBBox; + + const topLevelColNodes = this.getTopLevelColNodes(); + const cellRange = this.getCellRange(); + const { + rowCount: frozenRowCount = 0, + colCount: frozenColCount = 0, + trailingColCount: frozenTrailingColCount = 0, + trailingRowCount: frozenTrailingRowCount = 0, + } = this.getFrozenOptions(); + + // 在分页条件下需要额外处理 Y 轴滚动值 + const relativeScrollY = Math.floor(scrollY - this.getPaginationScrollY()); + + // scroll boundary + const maxScrollX = Math.max(0, last(this.viewCellWidths)! - viewportWidth); + const maxScrollY = Math.max( + 0, + this.viewCellHeights.getCellOffsetY(cellRange.end + 1) - + this.viewCellHeights.getCellOffsetY(cellRange.start) - + viewportHeight, + ); + + // remove previous split line group + this.foregroundGroup.getElementById(KEY_GROUP_FROZEN_SPLIT_LINE)?.remove(); + + const { splitLine } = this.spreadsheet.theme; + const splitLineGroup = this.foregroundGroup.appendChild( + new Group({ + id: KEY_GROUP_FROZEN_SPLIT_LINE, + style: { + zIndex: FRONT_GROUND_GROUP_FROZEN_Z_INDEX, + }, + }), + ); + + const verticalBorderStyle: Partial = { + lineWidth: SPLIT_LINE_WIDTH, + stroke: splitLine?.verticalBorderColor, + opacity: splitLine?.verticalBorderColorOpacity, + }; + + const horizontalBorderStyle: Partial = { + lineWidth: SPLIT_LINE_WIDTH, + stroke: splitLine?.horizontalBorderColor, + opacity: splitLine?.horizontalBorderColorOpacity, + }; + + const frameVerticalBorderWidth = Frame.getVerticalBorderWidth( + this.spreadsheet, + ); + + if (frozenColCount > 0) { + const x = topLevelColNodes.reduce((prev, item, idx) => { + if (idx < frozenColCount) { + return prev + item.width; + } + + return prev; + }, 0); + + const height = + (frozenTrailingRowCount > 0 ? panelHeight : viewportHeight) + + panelBBoxStartY; + + renderLine(splitLineGroup, { + ...verticalBorderStyle, + x1: x + panelBBoxStartX, + x2: x + panelBBoxStartX, + y1: 0, + y2: height, + }); + + if (splitLine?.showShadow && scrollX > 0) { + splitLineGroup.appendChild( + new Rect({ + style: { + x: x + panelBBoxStartX, + y: 0, + width: splitLine?.shadowWidth!, + height, + fill: this.getShadowFill(0), + }, + }), + ); + } + } + + if (frozenRowCount > 0) { + const y = + panelBBoxStartY + + this.getTotalHeightForRange( + cellRange.start, + cellRange.start + frozenRowCount - 1, + ); + const width = frozenTrailingColCount > 0 ? panelWidth : viewportWidth; + + renderLine(splitLineGroup, { + ...horizontalBorderStyle, + x1: 0, + x2: width + frameVerticalBorderWidth, + y1: y, + y2: y, + }); + + if (splitLine?.showShadow && relativeScrollY > 0) { + splitLineGroup.appendChild( + new Rect({ + style: { + x: 0, + y, + width: width + frameVerticalBorderWidth, + height: splitLine?.shadowWidth!, + fill: this.getShadowFill(90), + }, + }), + ); + } + } + + if (frozenTrailingColCount > 0) { + const { x } = + topLevelColNodes[topLevelColNodes.length - frozenTrailingColCount]; + const height = + (frozenTrailingRowCount ? panelHeight : viewportHeight) + + panelBBoxStartY; + + renderLine(splitLineGroup, { + ...verticalBorderStyle, + x1: x + panelBBoxStartX, + x2: x + panelBBoxStartX, + y1: 0, + y2: height, + }); + + if (splitLine?.showShadow && floor(scrollX) < floor(maxScrollX)) { + splitLineGroup.appendChild( + new Rect({ + style: { + x: x + panelBBoxStartX - splitLine.shadowWidth!, + y: 0, + width: splitLine.shadowWidth!, + height, + fill: this.getShadowFill(180), + }, + }), + ); + } + } + + if (frozenTrailingRowCount > 0) { + const y = + this.panelBBox.maxY - + this.getTotalHeightForRange( + cellRange.end - frozenTrailingRowCount + 1, + cellRange.end, + ); + const width = frozenTrailingColCount > 0 ? panelWidth : viewportWidth; + + renderLine(splitLineGroup, { + ...horizontalBorderStyle, + x1: 0, + x2: width + frameVerticalBorderWidth, + y1: y, + y2: y, + }); + + if (splitLine?.showShadow && relativeScrollY < floor(maxScrollY)) { + splitLineGroup.appendChild( + new Rect({ + style: { + x: 0, + y: y - splitLine.shadowWidth!, + width: width + frameVerticalBorderWidth, + height: splitLine.shadowWidth!, + fill: this.getShadowFill(270), + }, + }), + ); + } + } + }; + + protected init(): void { + super.init(); + } + + public render(): void { + this.calculateFrozenGroupInfo(); + this.renderFrozenPanelCornerGroup(); + super.render(); + } + + protected override getCenterFrameScrollX(scrollX: number): number { + if (this.getFrozenOptions().colCount! > 0) { + return 0; + } + + return super.getCenterFrameScrollX(scrollX); + } + + protected renderFrozenPanelCornerGroup = () => { + const topLevelNodes = this.getTopLevelColNodes(); + const cellRange = this.getCellRange(); + + const { + rowCount: frozenRowCount = 0, + colCount: frozenColCount = 0, + trailingRowCount: frozenTrailingRowCount = 0, + trailingColCount: frozenTrailingColCount = 0, + } = this.getFrozenOptions(); + + const { colCount, trailingColCount } = getFrozenLeafNodesCount( + topLevelNodes, + frozenColCount, + frozenTrailingColCount, + ); + + const result = calculateFrozenCornerCells( + { + rowCount: frozenRowCount, + colCount, + trailingRowCount: frozenTrailingRowCount, + trailingColCount, + }, + this.getColLeafNodes().length, + cellRange, + ); + + Object.keys(result).forEach((key) => { + const cells = result[key]; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const group = this[FrozenCellGroupMap[key]] as Group; + + if (group) { + cells.forEach((cell) => { + this.addFrozenCell(cell.x, cell.y, group); + }); + } + }); + }; + + getRealFrozenColumns = ( + colCount: number, + trailingColCount: number, + ): { colCount: number; trailingColCount: number } => { + if (colCount || trailingColCount) { + const nodes = this.getTopLevelColNodes(); + + return getFrozenLeafNodesCount(nodes, colCount, trailingColCount); + } + + return { + colCount, + trailingColCount, + }; + }; + + public getCellHeightByRowIndex(rowIndex: number) { + if (this.rowOffsets) { + return this.getRowCellHeight({ id: String(rowIndex) } as Node); + } + + return this.getDefaultCellHeight(); + } + + public getTotalHeightForRange = (start: number, end: number) => { + if (start < 0 || end < 0) { + return 0; + } + + if (this.rowOffsets) { + return this.rowOffsets[end + 1] - this.rowOffsets[start]; + } + + let totalHeight = 0; + + for (let index = start; index < end + 1; index++) { + const height = this.getDefaultCellHeight(); + + totalHeight += height; + } + + return totalHeight; + }; + + protected getDefaultCellHeight(): number { + return this.getRowCellHeight(null as unknown as Node); + } + + protected getShadowFill = (angle: number) => { + const { splitLine } = this.spreadsheet.theme; + + return `l (${angle}) 0:${splitLine?.shadowColors?.left} 1:${splitLine?.shadowColors?.right}`; + }; + + protected clip() { + const { frozenGroupInfo, spreadsheet } = this; + + const isFrozenRowHeader = spreadsheet.isFrozenRowHeader(); + + const frozenColGroupWidth = + frozenGroupInfo[FrozenGroupType.FROZEN_COL].width; + const frozenTrailingColWidth = + frozenGroupInfo[FrozenGroupType.FROZEN_TRAILING_COL].width; + const frozenRowGroupHeight = + frozenGroupInfo[FrozenGroupType.FROZEN_ROW].height; + const frozenTrailingRowHeight = + frozenGroupInfo[FrozenGroupType.FROZEN_TRAILING_ROW].height; + + const panelScrollGroupClipX = + (isFrozenRowHeader ? this.panelBBox.x : 0) + frozenColGroupWidth; + const panelScrollGroupClipY = this.panelBBox.y + frozenRowGroupHeight; + const panelScrollGroupClipWidth = + this.panelBBox.width - + frozenColGroupWidth - + frozenTrailingColWidth + + (isFrozenRowHeader ? 0 : this.panelBBox.x); + const panelScrollGroupClipHeight = + this.panelBBox.height - frozenRowGroupHeight - frozenTrailingRowHeight; + + this.panelScrollGroup.style.clipPath = new Rect({ + style: { + x: panelScrollGroupClipX, + y: panelScrollGroupClipY, + width: panelScrollGroupClipWidth, + height: panelScrollGroupClipHeight, + }, + }); + + this.frozenColGroup.style.clipPath = new Rect({ + style: { + x: this.panelBBox.x, + y: panelScrollGroupClipY, + width: frozenColGroupWidth, + height: panelScrollGroupClipHeight, + }, + }); + + this.frozenTrailingColGroup.style.clipPath = new Rect({ + style: { + x: this.panelBBox.x + this.panelBBox.width - frozenTrailingColWidth, + y: panelScrollGroupClipY, + width: frozenTrailingColWidth, + height: panelScrollGroupClipHeight, + }, + }); + + this.frozenRowGroup.style.clipPath = new Rect({ + style: { + x: panelScrollGroupClipX, + y: this.panelBBox.y, + width: panelScrollGroupClipWidth, + height: frozenRowGroupHeight, + }, + }); + + this.frozenTrailingRowGroup.style.clipPath = new Rect({ + style: { + x: panelScrollGroupClipX, + y: this.panelBBox.y + this.panelBBox.height - frozenTrailingRowHeight, + width: panelScrollGroupClipWidth, + height: frozenTrailingRowHeight, + }, + }); + } +} diff --git a/packages/s2-core/src/facet/header/col.ts b/packages/s2-core/src/facet/header/col.ts index 6f5f2c9ab5..3fd284c6a5 100644 --- a/packages/s2-core/src/facet/header/col.ts +++ b/packages/s2-core/src/facet/header/col.ts @@ -1,8 +1,8 @@ -import { Group, Rect, type DisplayObject } from '@antv/g'; +import { Group, Rect } from '@antv/g'; import { each } from 'lodash'; import { ColCell } from '../../cell/col-cell'; import { - FRONT_GROUND_GROUP_COL_SCROLL_Z_INDEX, + FRONT_GROUND_GROUP_SCROLL_Z_INDEX, KEY_GROUP_COL_SCROLL, S2Event, } from '../../common/constant'; @@ -17,21 +17,20 @@ import type { ColHeaderConfig } from './interface'; export class ColHeader extends BaseHeader { protected scrollGroup: Group; - protected background: DisplayObject; - constructor(config: ColHeaderConfig) { super(config); - this.initScrollGroup(); } protected getCellInstance(node: Node) { + const headerConfig = this.getHeaderConfig(); + const { spreadsheet } = this.getHeaderConfig(); const { colCell } = spreadsheet.options; return ( - colCell?.(node, spreadsheet, this.headerConfig) || - new ColCell(node, spreadsheet, this.headerConfig) + colCell?.(node, spreadsheet, headerConfig) || + new ColCell(node, spreadsheet, headerConfig) ); } @@ -39,7 +38,7 @@ export class ColHeader extends BaseHeader { this.scrollGroup = this.appendChild( new Group({ name: KEY_GROUP_COL_SCROLL, - style: { zIndex: FRONT_GROUND_GROUP_COL_SCROLL_Z_INDEX }, + style: { zIndex: FRONT_GROUND_GROUP_SCROLL_Z_INDEX }, }), ); } @@ -58,13 +57,14 @@ export class ColHeader extends BaseHeader { } protected clip() { - const { height, spreadsheet } = this.getHeaderConfig(); + const { height, width, spreadsheet, position } = this.getHeaderConfig(); + const isFrozenRowHeader = spreadsheet.isFrozenRowHeader(); this.scrollGroup.style.clipPath = new Rect({ style: { - x: 0, - y: 0, - width: spreadsheet.options.width!, + x: isFrozenRowHeader ? position.x : 0, + y: isFrozenRowHeader ? position.y : 0, + width: isFrozenRowHeader ? width : position.x + width, height, }, }); @@ -72,7 +72,6 @@ export class ColHeader extends BaseHeader { public clear() { this.scrollGroup?.removeChildren(); - this.background?.remove(); } // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -101,12 +100,14 @@ export class ColHeader extends BaseHeader { each(nodes, (node) => { if (this.isColCellInRect(node)) { + const group = this.getCellGroup(node); + + node.isFrozen = group !== this.scrollGroup; + const cell = this.getCellInstance(node); node.belongsCell = cell; - const group = this.getCellGroup(node); - group?.appendChild(cell); spreadsheet.emit(S2Event.COL_CELL_RENDER, cell as ColCell); spreadsheet.emit(S2Event.LAYOUT_CELL_RENDER, cell); diff --git a/packages/s2-core/src/facet/header/corner.ts b/packages/s2-core/src/facet/header/corner.ts index 0b7cb4f848..c076d5b3d6 100644 --- a/packages/s2-core/src/facet/header/corner.ts +++ b/packages/s2-core/src/facet/header/corner.ts @@ -3,8 +3,8 @@ import { includes } from 'lodash'; import { CornerCell } from '../../cell/corner-cell'; import type { S2CellType } from '../../common/interface'; import { CornerNodeType } from '../../common/interface/node'; -import type { CornerBBox } from '../bbox/cornerBBox'; -import type { PanelBBox } from '../bbox/panelBBox'; +import type { CornerBBox } from '../bbox/corner-bbox'; +import type { PanelBBox } from '../bbox/panel-bbox'; import { Node } from '../layout/node'; import { translateGroupX } from '../utils'; import { S2Event } from '../../common'; @@ -150,7 +150,7 @@ export class CornerHeader extends BaseHeader { if (spreadsheet.isHierarchyTreeType()) { const cornerText = this.getTreeCornerText(options); const cornerNode: Node = new Node({ - id: '', + id: cornerText, field: '', value: cornerText, }); @@ -178,7 +178,7 @@ export class CornerHeader extends BaseHeader { const value = spreadsheet.dataSet.getFieldName(field); const cornerNode: Node = new Node({ - id: '', + id: field, field, value, }); @@ -206,7 +206,7 @@ export class CornerHeader extends BaseHeader { const value = spreadsheet.dataSet.getFieldName(field); const cNode = new Node({ - id: '', + id: field, field, value, }); diff --git a/packages/s2-core/src/facet/header/frame.ts b/packages/s2-core/src/facet/header/frame.ts index f1458677b4..2f09fcafab 100644 --- a/packages/s2-core/src/facet/header/frame.ts +++ b/packages/s2-core/src/facet/header/frame.ts @@ -3,6 +3,7 @@ import { renderLine } from '.././../utils/g-renders'; import type { FrameConfig } from '../../common/interface'; import { translateGroup } from '../utils'; import type { SpreadSheet } from '../../sheet-type/spread-sheet'; +import { floor } from '../../utils/math'; export class Frame extends Group { declare cfg: FrameConfig; @@ -72,8 +73,7 @@ export class Frame extends Group { public onChangeShadowVisibility(scrollX: number, maxScrollX: number) { this.cfg.showViewportLeftShadow = scrollX > 0; // baseFacet#renderHScrollBar render condition - this.cfg.showViewportRightShadow = - Math.floor(scrollX) < Math.floor(maxScrollX); + this.cfg.showViewportRightShadow = floor(scrollX) < floor(maxScrollX); this.render(); } diff --git a/packages/s2-core/src/facet/header/index.ts b/packages/s2-core/src/facet/header/index.ts index be55354e98..3001a3417d 100644 --- a/packages/s2-core/src/facet/header/index.ts +++ b/packages/s2-core/src/facet/header/index.ts @@ -3,4 +3,5 @@ export { CornerHeader } from './corner'; export { Frame } from './frame'; export { RowHeader } from './row'; export { SeriesNumberHeader } from './series-number'; + export * from './interface'; diff --git a/packages/s2-core/src/facet/header/row.ts b/packages/s2-core/src/facet/header/row.ts index dafbd5eaf2..0d08e50847 100644 --- a/packages/s2-core/src/facet/header/row.ts +++ b/packages/s2-core/src/facet/header/row.ts @@ -1,9 +1,17 @@ -import { Rect } from '@antv/g'; -import { each, isEmpty } from 'lodash'; +import { Group, Rect } from '@antv/g'; +import { each } from 'lodash'; import { RowCell } from '../../cell'; import type { Node } from '../layout/node'; -import { translateGroup } from '../utils'; -import { S2Event } from '../../common'; +import { getFrozenRowCfgPivot, translateGroup } from '../utils'; +import { + FRONT_GROUND_GROUP_FROZEN_Z_INDEX, + FRONT_GROUND_GROUP_SCROLL_Z_INDEX, + FrozenGroupType, + KEY_GROUP_ROW_HEADER_FROZEN, + KEY_GROUP_ROW_SCROLL, + S2Event, +} from '../../common'; +import type { FrozenFacet } from '../frozen-facet'; import { BaseHeader } from './base'; import type { RowHeaderConfig } from './interface'; @@ -11,12 +19,34 @@ import type { RowHeaderConfig } from './interface'; * Row Header for SpreadSheet */ export class RowHeader extends BaseHeader { + public scrollGroup: Group; + + public frozenRowGroup: Group; + constructor(config: RowHeaderConfig) { super(config); + this.initGroups(); } - protected getCellInstance(node: Node): RowCell { + private initGroups() { + this.scrollGroup = this.appendChild( + new Group({ + name: KEY_GROUP_ROW_SCROLL, + style: { zIndex: FRONT_GROUND_GROUP_SCROLL_Z_INDEX }, + }), + ); + + this.frozenRowGroup = this.appendChild( + new Group({ + name: KEY_GROUP_ROW_HEADER_FROZEN, + style: { zIndex: FRONT_GROUND_GROUP_FROZEN_Z_INDEX }, + }), + ); + } + + public getCellInstance(node: Node): RowCell { const headerConfig = this.getHeaderConfig(); + const { spreadsheet } = headerConfig; const { rowCell } = spreadsheet.options; @@ -26,53 +56,68 @@ export class RowHeader extends BaseHeader { ); } - protected layout() { + // row'cell only show when visible + protected isRowCellInRect(node: Node): boolean { const { - nodes, - spreadsheet, width, viewportHeight, + position, scrollY = 0, scrollX = 0, - position, } = this.getHeaderConfig(); - const rowCell = spreadsheet?.options?.rowCell; - // row'cell only show when visible - const rowCellInRect = (node: Node): boolean => { - return ( - // bottom - viewportHeight + scrollY > node.y && - // top - scrollY < node.y + node.height && - // left - width - position.x + scrollX > node.x && - // right - scrollX - position.x < node.x + node.width - ); - }; + if (this.isFrozenRow(node)) { + return true; + } + + return ( + // bottom + viewportHeight + scrollY > node.y && + // top + scrollY < node.y + node.height && + // left + width - position.x + scrollX > node.x && + // right + scrollX - position.x < node.x + node.width + ); + } + + public isFrozenRow(node: Node): boolean { + const { spreadsheet } = this.headerConfig; + const { facet } = spreadsheet; + const { rowCount = 0 } = getFrozenRowCfgPivot( + spreadsheet.options, + facet.getRowNodes(), + ); + return rowCount > 0 && node.rowIndex >= 0 && node.rowIndex < rowCount; + } + + protected getCellGroup(item: Node): Group { + if (this.isFrozenRow(item)) { + return this.frozenRowGroup; + } + + return this.scrollGroup; + } + + protected layout() { + const { nodes, spreadsheet } = this.getHeaderConfig(); + + // row'cell only show when visible each(nodes, (node) => { - if (rowCellInRect(node) && node.height !== 0) { - let cell: RowCell | null = null; + if (this.isRowCellInRect(node) && node.height !== 0) { + const group = this.getCellGroup(node); - // 首先由外部控制UI展示 - if (rowCell) { - cell = rowCell(node, spreadsheet, this.headerConfig); - } + node.isFrozen = group !== this.scrollGroup; - // 如果外部没处理,就用默认的 - if (isEmpty(cell) && spreadsheet.isPivotMode()) { - cell = new RowCell(node, spreadsheet, this.headerConfig); - } + const cell = this.getCellInstance(node); node.belongsCell = cell; - if (cell) { - this.appendChild(cell); - spreadsheet.emit(S2Event.ROW_CELL_RENDER, cell); - spreadsheet.emit(S2Event.LAYOUT_CELL_RENDER, cell); - } + group.appendChild(cell); + spreadsheet.emit(S2Event.ROW_CELL_RENDER, cell); + spreadsheet.emit(S2Event.LAYOUT_CELL_RENDER, cell); } }); } @@ -80,20 +125,40 @@ export class RowHeader extends BaseHeader { protected offset() { const { scrollX = 0, scrollY = 0, position } = this.getHeaderConfig(); - // 向右多移动的 seriesNumberWidth 是序号的宽度 - translateGroup(this, position.x - scrollX, position.y - scrollY); + const translateX = position.x - scrollX; + + translateGroup(this.scrollGroup, translateX, position.y - scrollY); + translateGroup(this.frozenRowGroup, translateX, position.y); } protected clip(): void { - const { width, height, viewportHeight } = this.getHeaderConfig(); + const { width, viewportHeight, position, spreadsheet } = + this.getHeaderConfig(); - this.style.clipPath = new Rect({ + const frozenRowGroupHeight = (spreadsheet.facet as FrozenFacet) + .frozenGroupInfo[FrozenGroupType.FROZEN_ROW].height; + + this.scrollGroup.style.clipPath = new Rect({ + style: { + x: spreadsheet.facet.cornerBBox.x, + y: position.y + frozenRowGroupHeight, + width, + height: viewportHeight, + }, + }); + + this.frozenRowGroup.style.clipPath = new Rect({ style: { - x: 0, - y: 0, + x: spreadsheet.facet.cornerBBox.x, + y: position.y, width, - height: height + viewportHeight, + height: frozenRowGroupHeight, }, }); } + + public clear() { + this.scrollGroup?.removeChildren(); + this.frozenRowGroup?.removeChildren(); + } } diff --git a/packages/s2-core/src/facet/header/series-number.ts b/packages/s2-core/src/facet/header/series-number.ts index 768ee4a14e..388017f368 100644 --- a/packages/s2-core/src/facet/header/series-number.ts +++ b/packages/s2-core/src/facet/header/series-number.ts @@ -2,7 +2,7 @@ import { Rect } from '@antv/g'; import { each } from 'lodash'; import { SeriesNumberCell } from '../../cell/series-number-cell'; import type { SpreadSheet } from '../../sheet-type/index'; -import type { PanelBBox } from '../bbox/panelBBox'; +import type { PanelBBox } from '../bbox/panel-bbox'; import type { Hierarchy } from '../layout/hierarchy'; import type { Node } from '../layout/node'; import { translateGroup } from '../utils'; @@ -65,12 +65,13 @@ export class SeriesNumberHeader extends BaseHeader { } public clip(): void { - const { width, height, viewportHeight } = this.getHeaderConfig(); + const { width, height, viewportHeight, position, spreadsheet } = + this.getHeaderConfig(); this.style.clipPath = new Rect({ style: { - x: 0, - y: 0, + x: spreadsheet.facet.cornerBBox.x, + y: position.y, width, height: height + viewportHeight, }, diff --git a/packages/s2-core/src/facet/header/table-col.ts b/packages/s2-core/src/facet/header/table-col.ts index 8df8a0d84d..c21ef890f0 100644 --- a/packages/s2-core/src/facet/header/table-col.ts +++ b/packages/s2-core/src/facet/header/table-col.ts @@ -1,14 +1,14 @@ import { Group, Rect, type RectStyleProps } from '@antv/g'; import { TableColCell, TableCornerCell } from '../../cell'; import { - FRONT_GROUND_GROUP_COL_FROZEN_Z_INDEX, + FRONT_GROUND_GROUP_FROZEN_Z_INDEX, + FrozenGroupType, KEY_GROUP_COL_FROZEN, KEY_GROUP_COL_FROZEN_TRAILING, KEY_GROUP_FROZEN_COL_RESIZE_AREA, SERIES_NUMBER_FIELD, } from '../../common/constant'; import type { SpreadSheet } from '../../sheet-type'; -import { getFrozenColWidth } from '../../utils/layout/frozen'; import type { Node } from '../layout/node'; import { getFrozenLeafNodesCount, @@ -17,6 +17,7 @@ import { isFrozenTrailingCol, translateGroupX, } from '../utils'; +import type { FrozenFacet } from '../frozen-facet'; import { ColHeader } from './col'; import type { ColHeaderConfig } from './interface'; @@ -30,7 +31,6 @@ export class TableColHeader extends ColHeader { constructor(config: ColHeaderConfig) { super(config); - this.initFrozenColGroups(); } @@ -63,7 +63,7 @@ export class TableColHeader extends ColHeader { this.frozenColGroup = this.appendChild( new Group({ name: KEY_GROUP_COL_FROZEN, - style: { zIndex: FRONT_GROUND_GROUP_COL_FROZEN_Z_INDEX }, + style: { zIndex: FRONT_GROUND_GROUP_FROZEN_Z_INDEX }, }), ); } @@ -72,30 +72,14 @@ export class TableColHeader extends ColHeader { this.frozenTrailingColGroup = this.appendChild( new Group({ name: KEY_GROUP_COL_FROZEN_TRAILING, - style: { zIndex: FRONT_GROUND_GROUP_COL_FROZEN_Z_INDEX }, + style: { zIndex: FRONT_GROUND_GROUP_FROZEN_Z_INDEX }, }), ); } } - protected isFrozenCell(meta: Node) { - const { spreadsheet } = this.getHeaderConfig(); - const { - colCount: frozenColCount = 0, - trailingColCount: frozenTrailingColCount = 0, - } = spreadsheet.options.frozen!; - const { colIndex } = meta; - const colLeafNodes = spreadsheet.facet.getColLeafNodes(); - - return ( - isFrozenCol(colIndex, frozenColCount) || - isFrozenTrailingCol(colIndex, frozenTrailingColCount, colLeafNodes.length) - ); - } - public clear() { super.clear(); - this.frozenTrailingColGroup?.removeChildren(); this.frozenColGroup?.removeChildren(); @@ -116,37 +100,23 @@ export class TableColHeader extends ColHeader { const leftLeafNode = getLeftLeafNode(node); const topLevelNodes = spreadsheet.facet.getColNodes(0); - const { - colCount: frozenColCount, - trailingColCount: frozenTrailingColCount, - } = getFrozenLeafNodesCount(topLevelNodes, colCount, trailingColCount); - return { - leftLeafNodeColIndex: leftLeafNode.colIndex, - frozenColCount, - frozenTrailingColCount, colLength: topLevelNodes.length, + leftLeafNodeColIndex: leftLeafNode.colIndex, + ...getFrozenLeafNodesCount(topLevelNodes, colCount, trailingColCount), }; } protected getCellGroup(node: Node): Group { - const { - leftLeafNodeColIndex, - frozenColCount, - frozenTrailingColCount, - colLength, - } = this.getColFrozenOptionsByNode(node); + const { colLength, leftLeafNodeColIndex, colCount, trailingColCount } = + this.getColFrozenOptionsByNode(node); - if (isFrozenCol(leftLeafNodeColIndex, frozenColCount)) { + if (isFrozenCol(leftLeafNodeColIndex, colCount)) { return this.frozenColGroup; } if ( - isFrozenTrailingCol( - leftLeafNodeColIndex, - frozenTrailingColCount, - colLength, - ) + isFrozenTrailingCol(leftLeafNodeColIndex, trailingColCount, colLength) ) { return this.frozenTrailingColGroup; } @@ -155,20 +125,12 @@ export class TableColHeader extends ColHeader { } protected isColCellInRect(node: Node): boolean { - const { - leftLeafNodeColIndex, - frozenColCount, - frozenTrailingColCount, - colLength, - } = this.getColFrozenOptionsByNode(node); + const { leftLeafNodeColIndex, colLength, colCount, trailingColCount } = + this.getColFrozenOptionsByNode(node); if ( - isFrozenCol(leftLeafNodeColIndex, frozenColCount) || - isFrozenTrailingCol( - leftLeafNodeColIndex, - frozenTrailingColCount, - colLength, - ) + isFrozenCol(leftLeafNodeColIndex, colCount) || + isFrozenTrailingCol(leftLeafNodeColIndex, trailingColCount, colLength) ) { return true; } @@ -177,17 +139,16 @@ export class TableColHeader extends ColHeader { } public getScrollGroupClipBBox = (): RectStyleProps => { - const { width, height, spreadsheet } = this.getHeaderConfig(); - const topLevelNodes = spreadsheet.facet.getColNodes(0); - const { frozenColWidth, frozenTrailingColWidth } = getFrozenColWidth( - topLevelNodes, - spreadsheet.options.frozen!, - ); - const scrollGroupWidth = width - frozenColWidth - frozenTrailingColWidth; + const { width, height, spreadsheet, position } = this.getHeaderConfig(); + const frozenGroupInfo = (spreadsheet.facet as FrozenFacet).frozenGroupInfo; + const colWidth = frozenGroupInfo[FrozenGroupType.FROZEN_COL].width; + const trailingColWidth = + frozenGroupInfo[FrozenGroupType.FROZEN_TRAILING_COL].width; + const scrollGroupWidth = width - colWidth - trailingColWidth; return { - x: frozenColWidth, - y: 0, + x: position.x + colWidth, + y: position.y, width: scrollGroupWidth, height, }; diff --git a/packages/s2-core/src/facet/header/util.ts b/packages/s2-core/src/facet/header/util.ts index a268178d2c..8da2016ef2 100644 --- a/packages/s2-core/src/facet/header/util.ts +++ b/packages/s2-core/src/facet/header/util.ts @@ -40,6 +40,8 @@ export const getSeriesNumberNodes = ( sNode.height = isHierarchyTreeType ? node.getTotalHeightForTreeHierarchy() : node.height; + sNode.isLeaf = true; + sNode.spreadsheet = spreadsheet; return sNode; }); diff --git a/packages/s2-core/src/facet/layout/build-gird-hierarchy.ts b/packages/s2-core/src/facet/layout/build-gird-hierarchy.ts index 42cd6c35db..162b839078 100644 --- a/packages/s2-core/src/facet/layout/build-gird-hierarchy.ts +++ b/packages/s2-core/src/facet/layout/build-gird-hierarchy.ts @@ -1,37 +1,18 @@ -import { isEmpty, isUndefined } from 'lodash'; -import { EXTRA_FIELD } from '../../common/constant'; -import type { SpreadSheet } from '../../sheet-type'; +import { isEmpty } from 'lodash'; +import { EMPTY_FIELD_VALUE, EXTRA_FIELD } from '../../common/constant'; import { addTotals } from '../../utils/layout/add-totals'; import { generateHeaderNodes } from '../../utils/layout/generate-header-nodes'; import { getDimsCondition } from '../../utils/layout/get-dims-condition-by-node'; +import { whetherLeafByLevel } from '../../utils/layout/whether-leaf-by-level'; import type { FieldValue, GridHeaderParams } from '../layout/interface'; import { layoutArrange } from '../layout/layout-hooks'; import { TotalMeasure } from '../layout/total-measure'; +import { filterOutDetail } from '../../utils/data-set-operate'; +import { TotalClass } from './total-class'; -const hideValueColumn = ( - spreadsheet: SpreadSheet, - fieldValues: FieldValue[], - field: string, -) => { - const hideMeasure = spreadsheet.options.style?.colCell?.hideValue ?? false; - const { valueInCols } = spreadsheet.dataSet.fields; - - for (const value of fieldValues) { - if (hideMeasure && valueInCols && field === EXTRA_FIELD) { - fieldValues.splice(fieldValues.indexOf(value), 1); - } - } -}; - -/** - * Build grid hierarchy in rows or columns - * - * @param params - */ -export const buildGridHierarchy = (params: GridHeaderParams) => { +const buildTotalGridHierarchy = (params: GridHeaderParams) => { const { addTotalMeasureInTotal, - addMeasureInTotalQuery, parentNode, currentField, fields, @@ -40,73 +21,127 @@ export const buildGridHierarchy = (params: GridHeaderParams) => { } = params; const index = fields.indexOf(currentField); - - const { values = [] } = spreadsheet.dataSet.fields; + const dataSet = spreadsheet.dataSet; + const { values = [] } = dataSet.fields; const fieldValues: FieldValue[] = []; - let query: Record = {}; - - if (parentNode.isTotals) { - // add total measures - if (addTotalMeasureInTotal) { - query = getDimsCondition(parentNode.parent!, true); - // add total measures - fieldValues.push(...values.map((v) => new TotalMeasure(v))); + let query: Record = {}; + const totalsConfig = spreadsheet.getTotalsConfig(currentField); + const dimensionGroup = parentNode.isGrandTotals + ? totalsConfig.grandTotalsGroupDimensions + : totalsConfig.subTotalsGroupDimensions; + + if (dimensionGroup?.includes(currentField)) { + query = getDimsCondition(parentNode); + const dimValues = dataSet.getDimensionValues(currentField, query); + + fieldValues.push( + ...(dimValues || []).map( + (value) => + new TotalClass({ + label: value, + isSubTotals: parentNode.isSubTotals!, + isGrandTotals: parentNode.isGrandTotals!, + isTotalRoot: false, + }), + ), + ); + if (isEmpty(fieldValues) && currentField) { + fieldValues.push(EMPTY_FIELD_VALUE); } + } else if (addTotalMeasureInTotal && currentField === EXTRA_FIELD) { + // add total measures + query = getDimsCondition(parentNode); + fieldValues.push(...values.map((v) => new TotalMeasure(v))); + } else if (whetherLeafByLevel({ spreadsheet, level: index, fields })) { + // 如果最后一级没有分组维度,则将上一个结点设为叶子节点 + parentNode.isLeaf = true; + hierarchy.pushIndexNode(parentNode); + parentNode.rowIndex = hierarchy.getIndexNodes().length - 1; + + return; } else { - // field(dimension)'s all values - query = getDimsCondition(parentNode, true); + // 如果是空维度,则跳转到下一级 level + buildTotalGridHierarchy({ ...params, currentField: fields[index + 1] }); - const dimValues = spreadsheet.dataSet.getDimensionValues( - currentField, - query, - ); + return; + } - const arrangedValues = layoutArrange( - spreadsheet, - dimValues, - parentNode, - currentField, - ); + const displayFieldValues = filterOutDetail(fieldValues as string[]); - fieldValues.push(...(arrangedValues || [])); + generateHeaderNodes({ + ...params, + fieldValues: displayFieldValues, + level: index, + parentNode, + query, + }); +}; - // add skeleton for empty data +const buildNormalGridHierarchy = (params: GridHeaderParams) => { + const { parentNode, currentField, fields, spreadsheet } = params; + const dataSet = spreadsheet.dataSet; + const { values = [] } = dataSet.fields; - const fieldName = spreadsheet.dataSet.getFieldName(currentField); + const index = fields.indexOf(currentField); - if (isEmpty(fieldValues)) { - if (currentField === EXTRA_FIELD) { - fieldValues.push(...(values || [])); - } else { - fieldValues.push(fieldName); - } - } + const fieldValues: FieldValue[] = []; - // hide value in columns - hideValueColumn(spreadsheet, fieldValues, currentField); - // add totals if needed - addTotals({ - currentField, - lastField: fields[index - 1], - isFirstField: index === 0, - fieldValues, - spreadsheet, - }); - } + let query: Record = {}; - const displayFieldValues = fieldValues.filter((value) => !isUndefined(value)); + // field(dimension)'s all values + query = getDimsCondition(parentNode, true); - generateHeaderNodes({ + const dimValues = dataSet.getDimensionValues(currentField, query); + + const arrangedValues = layoutArrange( spreadsheet, + dimValues, + parentNode, currentField, - fields, + ); + + fieldValues.push(...(arrangedValues || [])); + + // add skeleton for empty data + + if (isEmpty(fieldValues) && currentField) { + if (currentField === EXTRA_FIELD) { + fieldValues.push(...values); + } else { + fieldValues.push(EMPTY_FIELD_VALUE); + } + } + + // add totals if needed + addTotals({ + currentField, + lastField: fields[index - 1], + isFirstField: index === 0, + fieldValues, + spreadsheet, + }); + + const displayFieldValues = filterOutDetail(fieldValues as string[]); + + generateHeaderNodes({ + ...params, fieldValues: displayFieldValues, - hierarchy, - parentNode, level: index, + parentNode, query, - addMeasureInTotalQuery, - addTotalMeasureInTotal, }); }; + +/** + * Build grid hierarchy in rows or columns + * + * @param params + */ +export const buildGridHierarchy = (params: GridHeaderParams) => { + if (params.parentNode.isTotals) { + buildTotalGridHierarchy(params); + } else { + buildNormalGridHierarchy(params); + } +}; diff --git a/packages/s2-core/src/facet/layout/build-row-tree-hierarchy.ts b/packages/s2-core/src/facet/layout/build-row-tree-hierarchy.ts index a779f5d286..5b1c6aa21d 100644 --- a/packages/s2-core/src/facet/layout/build-row-tree-hierarchy.ts +++ b/packages/s2-core/src/facet/layout/build-row-tree-hierarchy.ts @@ -1,8 +1,7 @@ import { isNumber } from 'lodash'; -import { i18n, NODE_ID_SEPARATOR, ROOT_NODE_ID } from '../../common'; -import type { PivotDataSet } from '../../data-set'; +import { i18n } from '../../common'; import type { SpreadSheet } from '../../sheet-type'; -import { filterTotal, getListBySorted } from '../../utils/data-set-operate'; +import { filterOutDetail } from '../../utils/data-set-operate'; import { generateId } from '../../utils/layout/generate-id'; import type { FieldValue, TreeHeaderParams } from '../layout/interface'; import { layoutArrange, layoutHierarchy } from '../layout/layout-hooks'; @@ -25,13 +24,16 @@ const addTotals = ( const func = totalsConfig.reverseGrandTotalsLayout ? 'unshift' : 'push'; fieldValues[func]( - new TotalClass(totalsConfig.grandTotalsLabel!, false, true), + new TotalClass({ + label: totalsConfig.grandTotalsLabel!, + isGrandTotals: true, + isSubTotals: false, + isTotalRoot: false, + }), ); } }; -const NODE_ID_PREFIX_LEN = (ROOT_NODE_ID + NODE_ID_SEPARATOR).length; - /** * Only row header has tree hierarchy, in this scene: * 1、value in rows is not work => valueInCols is ineffective @@ -39,40 +41,12 @@ const NODE_ID_PREFIX_LEN = (ROOT_NODE_ID + NODE_ID_SEPARATOR).length; * @param params */ export const buildRowTreeHierarchy = (params: TreeHeaderParams) => { - const { - parentNode, - currentField = '', - level, - hierarchy, - pivotMeta, - spreadsheet, - } = params; - const { collapseFields, collapseAll, expandDepth } = - spreadsheet.options.style?.rowCell!; + const { spreadsheet, parentNode, currentField, level, hierarchy, pivotMeta } = + params; const { query, id: parentId } = parentNode; - const isDrillDownItem = spreadsheet.dataCfg.fields.rows?.length! <= level; - const sortedDimensionValues = - (spreadsheet.dataSet as PivotDataSet)?.sortedDimensionValues?.[ - currentField - ] || []; - - const unsortedDimValues = filterTotal(Array.from(pivotMeta.keys())); - const dimValues = getListBySorted( - unsortedDimValues, - sortedDimensionValues, - (dimVal) => { - /* - * 根据父节点 id,修改 unsortedDimValues 里用于比较的值,使其格式与 sortedDimensionValues 排序值一致 - * unsortedDimValues:['成都', '绵阳'] - * sortedDimensionValues: ['四川[&]成都'] - */ - if (ROOT_NODE_ID === parentId) { - return dimVal; - } - - return generateId(parentId, dimVal).slice(NODE_ID_PREFIX_LEN); - }, - ); + const isDrillDownItem = spreadsheet.dataCfg?.fields?.rows?.length! <= level; + + const dimValues = filterOutDetail(Array.from(pivotMeta.keys())); let fieldValues: FieldValue[] = layoutArrange( spreadsheet, @@ -119,6 +93,8 @@ export const buildRowTreeHierarchy = (params: TreeHeaderParams) => { const nodeId = generateId(parentId, value); + const { collapseFields, collapseAll, expandDepth } = + spreadsheet.options.style?.rowCell!; /* * 行头收起/展开配置优先级:collapseFields -> expandDepth -> collapseAll * 优先从读取 collapseFields 中的特定 node 的值 diff --git a/packages/s2-core/src/facet/layout/interface.ts b/packages/s2-core/src/facet/layout/interface.ts index 2ad8638826..c190d00327 100644 --- a/packages/s2-core/src/facet/layout/interface.ts +++ b/packages/s2-core/src/facet/layout/interface.ts @@ -39,15 +39,8 @@ export interface TotalParams { spreadsheet: SpreadSheet; } -export interface HeaderNodesParams { - spreadsheet: SpreadSheet; - currentField: string; - fields: string[]; +export interface HeaderNodesParams extends GridHeaderParams { fieldValues: FieldValue[]; - addTotalMeasureInTotal: boolean; - addMeasureInTotalQuery: boolean; - hierarchy: Hierarchy; - parentNode: Node; level: number; query: Record; } @@ -67,7 +60,7 @@ export interface TreeHeaderParams { spreadsheet: SpreadSheet; parentNode: Node; hierarchy: Hierarchy; - currentField: string | undefined; + currentField: string; level: number; pivotMeta: PivotMeta; } @@ -101,3 +94,9 @@ export interface CustomTreeHeaderParams { hierarchy: Hierarchy; tree: CustomTreeNode[]; } + +export interface WhetherLeafParams { + spreadsheet: SpreadSheet; + fields: string[]; + level: number; +} diff --git a/packages/s2-core/src/facet/layout/layout-hooks.ts b/packages/s2-core/src/facet/layout/layout-hooks.ts index 21675b6fb1..ccb0220352 100644 --- a/packages/s2-core/src/facet/layout/layout-hooks.ts +++ b/packages/s2-core/src/facet/layout/layout-hooks.ts @@ -36,7 +36,13 @@ export const layoutHierarchy = ( const hiddenColumnNode = spreadsheet?.facet?.getHiddenColumnsInfo(currentNode); - if (hiddenColumnNode) { + if ( + hiddenColumnNode && + // fix: Only hiding the column headers is supported to prevent the row subtotals from being hidden when the IDs of the row totals and column totals are the same. + spreadsheet?.dataSet?.fields?.columns?.find( + (field) => field === currentNode?.field, + ) + ) { return false; } diff --git a/packages/s2-core/src/facet/layout/node.ts b/packages/s2-core/src/facet/layout/node.ts index 94343bcda0..359902d0e6 100644 --- a/packages/s2-core/src/facet/layout/node.ts +++ b/packages/s2-core/src/facet/layout/node.ts @@ -29,6 +29,7 @@ export interface BaseNodeConfig { isSubTotals?: boolean; isCollapsed?: boolean | null; isGrandTotals?: boolean; + isTotalRoot?: boolean; hierarchy?: Hierarchy; isPivotMode?: boolean; seriesNumberWidth?: number; @@ -130,8 +131,12 @@ export class Node { public isSubTotals?: boolean; + public isTotalRoot?: boolean; + public hiddenChildNodeInfo?: HiddenColumnsInfo | null; + public isFrozen?: boolean; + public extra?: { description?: string; isCustomNode?: boolean; @@ -152,6 +157,7 @@ export class Node { isGrandTotals, isSubTotals, isCollapsed, + isTotalRoot, hierarchy, isPivotMode, seriesNumberWidth, @@ -183,6 +189,7 @@ export class Node { this.isGrandTotals = isGrandTotals; this.isSubTotals = isSubTotals; this.belongsCell = belongsCell; + this.isTotalRoot = isTotalRoot; this.extra = extra; } diff --git a/packages/s2-core/src/facet/layout/total-class.ts b/packages/s2-core/src/facet/layout/total-class.ts index 51df2d35c1..719687895f 100644 --- a/packages/s2-core/src/facet/layout/total-class.ts +++ b/packages/s2-core/src/facet/layout/total-class.ts @@ -1,6 +1,16 @@ /** * Class to mark '小计' & '总计' */ + +export interface TotalClassConfig { + label: string; + // 是否属于小计汇总格 + isSubTotals: boolean; + // 是否属于总计汇总格 + isGrandTotals: boolean; + // 是否是”小计“、”总计“单元格本身 + isTotalRoot?: boolean; +} export class TotalClass { public label: string; @@ -8,13 +18,15 @@ export class TotalClass { public isGrandTotals: boolean; - public constructor( - label: string, - isSubTotals = false, - isGrandTotals = false, - ) { + // 是否为 小计/总计 根结点,即 value = “小计”,单元格,此类结点不参与 query + public isTotalRoot: boolean; + + public constructor(params: TotalClassConfig) { + const { label, isSubTotals, isGrandTotals, isTotalRoot = false } = params; + this.label = label; this.isSubTotals = isSubTotals; this.isGrandTotals = isGrandTotals; + this.isTotalRoot = isTotalRoot; } } diff --git a/packages/s2-core/src/facet/pivot-facet.ts b/packages/s2-core/src/facet/pivot-facet.ts index 59253edf41..6c9a2f7a32 100644 --- a/packages/s2-core/src/facet/pivot-facet.ts +++ b/packages/s2-core/src/facet/pivot-facet.ts @@ -1,11 +1,10 @@ +import { Group, Rect, type LineStyleProps } from '@antv/g'; import { filter, - find, forEach, get, isArray, isEmpty, - isNil, isNumber, keys, last, @@ -19,37 +18,36 @@ import { import { ColCell, RowCell, SeriesNumberCell } from '../cell'; import { DEFAULT_TREE_ROW_CELL_WIDTH, + FRONT_GROUND_GROUP_FROZEN_Z_INDEX, + FrozenGroupType, + KEY_GROUP_FROZEN_SPLIT_LINE, LAYOUT_SAMPLE_COUNT, + SPLIT_LINE_WIDTH, type IconTheme, type MultiData, type ViewMeta, } from '../common'; -import { EXTRA_FIELD, LayoutWidthTypes, VALUE_FIELD } from '../common/constant'; +import { EXTRA_FIELD, LayoutWidthType, VALUE_FIELD } from '../common/constant'; import { CellType } from '../common/constant/interaction'; import { DebuggerUtil } from '../common/debug'; import type { LayoutResult, SimpleData } from '../common/interface'; import type { PivotDataSet } from '../data-set/pivot-data-set'; -import type { SpreadSheet } from '../sheet-type'; -import { safeJsonParse } from '../utils'; +import { renderLine, safeJsonParse } from '../utils'; import { getDataCellId } from '../utils/cell/data-cell'; import { getActionIconConfig } from '../utils/cell/header-cell'; -import { - getIndexRangeWithOffsets, - getSubTotalNodeWidthOrHeightByLevel, -} from '../utils/facet'; +import { getIndexRangeWithOffsets } from '../utils/facet'; +import { getRowsForGrid } from '../utils/grid'; +import { floor } from '../utils/math'; import { getCellWidth } from '../utils/text'; -import { BaseFacet } from './base-facet'; +import { FrozenFacet } from './frozen-facet'; import { Frame } from './header'; import { buildHeaderHierarchy } from './layout/build-header-hierarchy'; import type { Hierarchy } from './layout/hierarchy'; import { layoutCoordinate } from './layout/layout-hooks'; import { Node } from './layout/node'; +import { getFrozenRowCfgPivot } from './utils'; -export class PivotFacet extends BaseFacet { - public constructor(spreadsheet: SpreadSheet) { - super(spreadsheet); - } - +export class PivotFacet extends FrozenFacet { get rowCellTheme() { return this.spreadsheet.theme.rowCell!.cell; } @@ -217,7 +215,9 @@ export class PivotFacet extends BaseFacet { const colNodeHeight = this.getColNodeHeight(currentNode, colsHierarchy); currentNode.height = - currentNode.isGrandTotals && currentNode.isLeaf + currentNode.isGrandTotals && + !currentNode.isTotalMeasure && + currentNode.isLeaf ? colsHierarchy.height : colNodeHeight; @@ -232,21 +232,105 @@ export class PivotFacet extends BaseFacet { this.autoCalculateColNodeWidthAndX(colLeafNodes); if (!isEmpty(this.spreadsheet.options.totals?.col)) { - this.adjustGrandTotalNodesCoordinate(colsHierarchy); - this.adjustSubTotalNodesCoordinate(colsHierarchy); + this.adjustTotalNodesCoordinate({ + hierarchy: colsHierarchy, + isRowHeader: false, + isSubTotal: true, + }); + this.adjustTotalNodesCoordinate({ + hierarchy: colsHierarchy, + isRowHeader: false, + isSubTotal: false, + }); + } + } + + // please read README-adjustTotalNodesCoordinate.md to understand this function + private getMultipleMap( + hierarchy: Hierarchy, + isRowHeader?: boolean, + isSubTotal?: boolean, + ) { + const { maxLevel } = hierarchy; + const dataSet = this.spreadsheet.dataSet; + const { totals } = this.spreadsheet.options; + const moreThanOneValue = dataSet.moreThanOneValue(); + const { rows, columns } = dataSet.fields; + const fields = isRowHeader ? rows : columns; + const totalConfig = isRowHeader ? totals!.row : totals!.col; + const dimensionGroup = isSubTotal + ? totalConfig?.subTotalsGroupDimensions || [] + : totalConfig?.grandTotalsGroupDimensions || []; + const multipleMap: number[] = Array.from({ length: maxLevel + 1 }, () => 1); + + for (let level = maxLevel; level > 0; level--) { + const currentField = fields![level] as string; + // 若不符合【分组维度包含此维度】或【者指标维度下非单指标维度】,此表头单元格为空,将宽高合并到上级单元格 + const existValueField = currentField === EXTRA_FIELD && moreThanOneValue; + + if (!(dimensionGroup.includes(currentField) || existValueField)) { + multipleMap[level - 1] += multipleMap[level]; + multipleMap[level] = 0; + } } + + return multipleMap; + } + + // please read README-adjustTotalNodesCoordinate.md to understand this function + private adjustTotalNodesCoordinate(params: { + hierarchy: Hierarchy; + isRowHeader?: boolean; + isSubTotal?: boolean; + }) { + const { hierarchy, isRowHeader, isSubTotal } = params; + const multipleMap = this.getMultipleMap(hierarchy, isRowHeader, isSubTotal); + const totalNodes = filter(hierarchy.getNodes(), (node: Node) => + isSubTotal ? node.isSubTotals : node.isGrandTotals, + ) as Node[]; + const key = isRowHeader ? 'width' : 'height'; + + forEach(totalNodes, (node: Node) => { + let multiple = multipleMap[node.level]; + + // 小计根节点若为 0,则改为最近上级倍数 - level 差 + if (!multiple && isSubTotal) { + let lowerLevelIndex = 1; + + while (multiple < 1) { + multiple = + multipleMap[node.level - lowerLevelIndex] - lowerLevelIndex; + lowerLevelIndex++; + } + } + + let res = 0; + + for (let i = 0; i < multiple; i++) { + res += get( + hierarchy.sampleNodesForAllLevels?.find( + (sampleNode) => sampleNode.level === node.level + i, + ), + [key], + 0, + ); + } + node[key] = res; + }); } /** - * Auto Auto Auto column no-leaf node's width and x coordinate + * Auto column no-leaf node's width and x coordinate * @param colLeafNodes */ private autoCalculateColNodeWidthAndX(colLeafNodes: Node[]) { let prevColParent: Node | null = null; + let i = 0; + const leafNodes = colLeafNodes.slice(0); - while (leafNodes.length) { - const node = leafNodes.shift(); + while (i < leafNodes.length) { + const node = leafNodes[i++]; const parentNode = node?.parent; if (prevColParent !== parentNode && parentNode) { @@ -269,14 +353,14 @@ export class PivotFacet extends BaseFacet { } private calculateColLeafNodesWidth( - col: Node, + colNode: Node, colLeafNodes: Node[], rowLeafNodes: Node[], rowHeaderWidth: number, ): number { const { colCell } = this.spreadsheet.options.style!; - const cellDraggedWidth = this.getColCellDraggedWidth(col); + const cellDraggedWidth = this.getColCellDraggedWidth(colNode); // 1. 拖拽后的宽度优先级最高 if (isNumber(cellDraggedWidth)) { @@ -284,91 +368,15 @@ export class PivotFacet extends BaseFacet { } // 2. 其次是自定义, 返回 null 则使用默认宽度 - const cellCustomWidth = this.getCellCustomSize(col, colCell?.width!); + const cellCustomWidth = this.getCellCustomSize(colNode, colCell?.width!); if (isNumber(cellCustomWidth)) { return cellCustomWidth; } // 3. 紧凑布局 - if (this.spreadsheet.getLayoutWidthType() === LayoutWidthTypes.Compact) { - const { - bolderText: colCellTextStyle, - cell: colCellStyle, - icon: colIconStyle, - } = this.spreadsheet.theme.colCell!; - - // leaf node rough width - const cellFormatter = this.spreadsheet.dataSet.getFieldFormatter( - col.field, - ); - const leafNodeLabel = cellFormatter?.(col.value) ?? col.value; - const iconWidth = this.getExpectedCellIconWidth( - CellType.COL_CELL, - this.spreadsheet.isValueInCols() && - this.spreadsheet.options.showDefaultHeaderActionIcon!, - colIconStyle!, - ); - const leafNodeRoughWidth = - this.spreadsheet.measureTextWidthRoughly(leafNodeLabel) + iconWidth; - - // 采样 50 个 label,逐个计算找出最长的 label - let maxDataLabel = ''; - let maxDataLabelWidth = 0; - - for (let index = 0; index < LAYOUT_SAMPLE_COUNT; index++) { - const rowNode = rowLeafNodes[index]; - - if (rowNode) { - const cellData = ( - this.spreadsheet.dataSet as PivotDataSet - ).getCellData({ - query: { ...col.query, ...rowNode.query }, - rowNode, - isTotals: - col.isTotals || - col.isTotalMeasure || - rowNode.isTotals || - rowNode.isTotalMeasure, - }); - - if (cellData) { - // 总小计格子不一定有数据 - const valueData = cellData?.[VALUE_FIELD]; - const formattedValue = - this.spreadsheet.dataSet.getFieldFormatter( - cellData[EXTRA_FIELD], - )?.(valueData) ?? valueData; - const cellLabel = `${formattedValue}`; - const cellLabelWidth = - this.spreadsheet.measureTextWidthRoughly(cellLabel); - - if (cellLabelWidth > maxDataLabelWidth) { - maxDataLabel = cellLabel; - maxDataLabelWidth = cellLabelWidth; - } - } - } - } - - // compare result - const isLeafNodeWidthLonger = leafNodeRoughWidth > maxDataLabelWidth; - const maxLabel = isLeafNodeWidthLonger ? leafNodeLabel : maxDataLabel; - const appendedWidth = isLeafNodeWidthLonger ? iconWidth : 0; - - DebuggerUtil.getInstance().logger( - 'Max Label In Col:', - col.field, - maxLabel, - ); - - return ( - this.spreadsheet.measureTextWidth(maxLabel, colCellTextStyle) + - colCellStyle!.padding!.left! + - colCellStyle!.padding!.right! + - colCellStyle!.verticalBorderWidth! * 2 + - appendedWidth - ); + if (this.spreadsheet.getLayoutWidthType() === LayoutWidthType.Compact) { + return this.getCompactGridColNodeWidth(colNode, rowLeafNodes); } /** @@ -376,7 +384,7 @@ export class PivotFacet extends BaseFacet { * 4.1 树状自定义 */ if (this.spreadsheet.isHierarchyTreeType()) { - return this.getAdaptTreeColWidth(col, colLeafNodes, rowLeafNodes); + return this.getAdaptTreeColWidth(colNode, colLeafNodes, rowLeafNodes); } // 4.2 网格自定义 @@ -531,8 +539,16 @@ export class PivotFacet extends BaseFacet { }); this.autoCalculateRowNodeHeightAndY(rowLeafNodes); if (!isEmpty(this.spreadsheet.options.totals?.row)) { - this.adjustGrandTotalNodesCoordinate(rowsHierarchy, true); - this.adjustSubTotalNodesCoordinate(rowsHierarchy, true); + this.adjustTotalNodesCoordinate({ + hierarchy: rowsHierarchy, + isRowHeader: true, + isSubTotal: false, + }); + this.adjustTotalNodesCoordinate({ + hierarchy: rowsHierarchy, + isRowHeader: true, + isSubTotal: true, + }); } } } @@ -544,10 +560,11 @@ export class PivotFacet extends BaseFacet { private autoCalculateRowNodeHeightAndY(rowLeafNodes: Node[]) { // 3、in grid type, all no-leaf node's height, y are auto calculated let prevRowParent = null; + let i = 0; const leafNodes = rowLeafNodes.slice(0); - while (leafNodes.length) { - const node = leafNodes.shift(); + while (i < leafNodes.length) { + const node = leafNodes[i++]; const parent = node?.parent; if (prevRowParent !== parent && parent) { @@ -563,116 +580,6 @@ export class PivotFacet extends BaseFacet { } } - /** - * @description adjust the coordinate of total nodes and their children - * @param hierarchy Hierarchy - * @param isRowHeader boolean - */ - private adjustGrandTotalNodesCoordinate( - hierarchy: Hierarchy, - isRowHeader?: boolean, - ) { - const moreThanOneValue = this.spreadsheet.dataSet.moreThanOneValue(); - const { maxLevel } = hierarchy; - const grandTotalNode = find( - hierarchy.getNodes(0), - (node: Node) => node.isGrandTotals, - ); - - if (!(grandTotalNode instanceof Node)) { - return; - } - - const grandTotalChildren = grandTotalNode.children; - - // 总计节点层级 (有且有两级) - if (isRowHeader) { - // 填充行总单元格宽度 - grandTotalNode.width = hierarchy.width; - // 调整其叶子节点位置和宽度 - forEach(grandTotalChildren, (node: Node) => { - const maxLevelNode = hierarchy.getNodes(maxLevel)[0]; - - node.x = maxLevelNode.x; - node.width = maxLevelNode.width; - }); - } else if (maxLevel > 1 || (maxLevel <= 1 && !moreThanOneValue)) { - /* - * 只有当列头总层级大于1级或列头为1级单指标时总计格高度才需要填充 - * 填充列总计单元格高度 - */ - const grandTotalChildrenHeight = grandTotalChildren?.[0]?.height ?? 0; - - grandTotalNode.height = hierarchy.height - grandTotalChildrenHeight; - // 调整其叶子节点位置, 以非小计行为准 - const positionY = - find(hierarchy.getNodes(maxLevel), (node: Node) => !node.isTotalMeasure) - ?.y || 0; - - forEach(grandTotalChildren, (node: Node) => { - node.y = positionY; - }); - } - } - - /** - * @description adust the coordinate of subTotal nodes when there is just one value - * @param hierarchy Hierarchy - * @param isRowHeader boolean - */ - private adjustSubTotalNodesCoordinate( - hierarchy: Hierarchy, - isRowHeader?: boolean, - ) { - const subTotalNodes = hierarchy - .getNodes() - .filter((node) => node.isSubTotals); - - if (isEmpty(subTotalNodes)) { - return; - } - - const { maxLevel } = hierarchy; - - forEach(subTotalNodes, (subTotalNode: Node) => { - const subTotalChildNode = subTotalNode.children; - - if (isRowHeader) { - // 填充行总单元格宽度 - subTotalNode.width = getSubTotalNodeWidthOrHeightByLevel( - hierarchy.sampleNodesForAllLevels, - subTotalNode.level, - 'width', - ); - - // 调整其叶子节点位置 - forEach(subTotalChildNode, (node: Node) => { - node.x = hierarchy.getNodes(maxLevel)[0].x; - }); - } else { - // 填充列总单元格高度 - const totalHeight = getSubTotalNodeWidthOrHeightByLevel( - hierarchy.sampleNodesForAllLevels, - subTotalNode.level, - 'height', - ); - const subTotalNodeChildrenHeight = subTotalChildNode?.[0]?.height ?? 0; - - subTotalNode.height = totalHeight - subTotalNodeChildrenHeight; - // 调整其叶子节点位置, 以非小计行为准 - const positionY = - find( - hierarchy.getNodes(maxLevel), - (node: Node) => !node.isTotalMeasure, - )?.y || 0; - - forEach(subTotalChildNode, (node: Node) => { - node.y = positionY; - }); - } - }); - } - /** * 计算 grid 模式下 node 宽度 * @param node @@ -693,9 +600,9 @@ export class PivotFacet extends BaseFacet { return cellCustomWidth; } - if (this.spreadsheet.getLayoutWidthType() !== LayoutWidthTypes.Adaptive) { + if (this.spreadsheet.getLayoutWidthType() !== LayoutWidthType.Adaptive) { // compact or colAdaptive - return this.getCompactGridRowWidth(node); + return this.getCompactGridRowNodeWidth(node); } // adaptive @@ -727,7 +634,7 @@ export class PivotFacet extends BaseFacet { return Math.max( getCellWidth(dataCell!, this.getColLabelLength(col, rowLeafNodes)), - Math.floor((availableWidth - rowHeaderWidth) / colSize), + floor((availableWidth - rowHeaderWidth) / colSize), ); } @@ -798,15 +705,12 @@ export class PivotFacet extends BaseFacet { const colSize = Math.max(1, rowHeaderColSize + colHeaderColSize); if (!rowHeaderWidth) { - return Math.max( - getCellWidth(dataCell!), - Math.floor(availableWidth / colSize), - ); + return Math.max(getCellWidth(dataCell!), floor(availableWidth / colSize)); } return Math.max( getCellWidth(dataCell!), - Math.floor((availableWidth - rowHeaderWidth) / colHeaderColSize), + floor((availableWidth - rowHeaderWidth) / colHeaderColSize), ); } @@ -821,7 +725,7 @@ export class PivotFacet extends BaseFacet { // 1. 用户拖拽或手动指定的行头宽度优先级最高 const customRowWidth = this.getCellCustomSize(null, rowCell?.width!); - if (!isNil(customRowWidth)) { + if (isNumber(customRowWidth)) { return customRowWidth; } @@ -857,7 +761,7 @@ export class PivotFacet extends BaseFacet { * @param node 目标节点 * @returns 宽度 */ - private getCompactGridRowWidth(node: Node): number { + private getCompactGridRowNodeWidth(node: Node): number { const { bolderText: rowTextStyle, icon: rowIconStyle, @@ -916,6 +820,94 @@ export class PivotFacet extends BaseFacet { return Math.max(rowNodeWidth, fieldNameNodeWidth); } + private getCompactGridColNodeWidth(colNode: Node, rowLeafNodes: Node[]) { + const { + bolderText: colCellTextStyle, + cell: colCellStyle, + icon: colIconStyle, + } = this.spreadsheet.theme.colCell!; + const { text: dataCellTextStyle } = this.spreadsheet.theme.dataCell; + + // leaf node rough width + const cellFormatter = this.spreadsheet.dataSet.getFieldFormatter( + colNode.field, + ); + const leafNodeLabel = cellFormatter?.(colNode.value) ?? colNode.value; + const iconWidth = this.getExpectedCellIconWidth( + CellType.COL_CELL, + this.spreadsheet.isValueInCols() && + this.spreadsheet.options.showDefaultHeaderActionIcon!, + colIconStyle!, + ); + const leafNodeRoughWidth = + this.spreadsheet.measureTextWidthRoughly(leafNodeLabel) + iconWidth; + + // 采样 50 个 label,逐个计算找出最长的 label + let maxDataLabel = ''; + let maxDataLabelWidth = 0; + + for (let index = 0; index < LAYOUT_SAMPLE_COUNT; index++) { + const rowNode = rowLeafNodes[index]; + + if (rowNode) { + const cellData = (this.spreadsheet.dataSet as PivotDataSet).getCellData( + { + query: { ...colNode.query, ...rowNode.query }, + rowNode, + isTotals: + colNode.isTotals || + colNode.isTotalMeasure || + rowNode.isTotals || + rowNode.isTotalMeasure, + }, + ); + + if (cellData) { + // 总小计格子不一定有数据 + const valueData = cellData?.[VALUE_FIELD]; + const formattedValue = + this.spreadsheet.dataSet.getFieldFormatter(cellData[EXTRA_FIELD])?.( + valueData, + ) ?? valueData; + const cellLabel = `${formattedValue}`; + const cellLabelWidth = + this.spreadsheet.measureTextWidthRoughly(cellLabel); + + if (cellLabelWidth > maxDataLabelWidth) { + maxDataLabel = cellLabel; + maxDataLabelWidth = cellLabelWidth; + } + } + } + } + + // compare result + const isLeafNodeWidthLonger = leafNodeRoughWidth > maxDataLabelWidth; + const maxLabel = isLeafNodeWidthLonger ? leafNodeLabel : maxDataLabel; + const appendedWidth = isLeafNodeWidthLonger ? iconWidth : 0; + + DebuggerUtil.getInstance().logger( + 'Max Label In Col:', + colNode.field, + maxLabel, + maxDataLabelWidth, + ); + + // 取列头/数值字体最大的文本宽度 https://github.com/antvis/S2/issues/2385 + const maxTextWidth = this.spreadsheet.measureTextWidth(maxLabel, { + ...colCellTextStyle, + fontSize: Math.max(dataCellTextStyle.fontSize, colCellTextStyle.fontSize), + }); + + return ( + maxTextWidth + + colCellStyle!.padding!.left! + + colCellStyle!.padding!.right! + + colCellStyle!.verticalBorderWidth! * 2 + + appendedWidth + ); + } + public getViewCellHeights() { const rowLeafNodes = this.getRowLeafNodes(); @@ -951,4 +943,96 @@ export class PivotFacet extends BaseFacet { (element: SeriesNumberCell) => element instanceof SeriesNumberCell, ) as unknown[] as SeriesNumberCell[]; } + + protected updateFrozenGroupGrid(): void { + [FrozenGroupType.FROZEN_ROW].forEach((key) => { + if (!this.frozenGroupInfo[key].range) { + return; + } + + let cols: number[] = []; + let rows: number[] = []; + + if (key.toLowerCase().includes('row')) { + const [rowMin, rowMax] = this.frozenGroupInfo[key].range || []; + + cols = this.gridInfo.cols; + rows = getRowsForGrid(rowMin, rowMax, this.viewCellHeights); + } + + this[`${key}Group`].updateGrid( + { + cols, + rows, + }, + `${key}Group`, + ); + }); + } + + protected getFrozenOptions() { + return getFrozenRowCfgPivot( + this.spreadsheet.options, + this.layoutResult.rowNodes, + ); + } + + public enableFrozenFirstRow(): boolean { + return !!this.getFrozenOptions().rowCount; + } + + protected renderFrozenGroupSplitLine = (scrollX: number, scrollY: number) => { + this.foregroundGroup.getElementById(KEY_GROUP_FROZEN_SPLIT_LINE)?.remove(); + if (this.enableFrozenFirstRow()) { + // 在分页条件下需要额外处理 Y 轴滚动值 + const relativeScrollY = floor(scrollY - this.getPaginationScrollY()); + const splitLineGroup = this.foregroundGroup.appendChild( + new Group({ + id: KEY_GROUP_FROZEN_SPLIT_LINE, + style: { + zIndex: FRONT_GROUND_GROUP_FROZEN_Z_INDEX, + }, + }), + ); + + const { splitLine } = this.spreadsheet.theme; + + const horizontalBorderStyle: Partial = { + lineWidth: SPLIT_LINE_WIDTH, + stroke: splitLine?.horizontalBorderColor, + opacity: splitLine?.horizontalBorderColorOpacity, + }; + + const cellRange = this.getCellRange(); + const y = + this.panelBBox.y + + this.getTotalHeightForRange(cellRange.start, cellRange.start); + const width = + this.cornerBBox.width + + Frame.getVerticalBorderWidth(this.spreadsheet) + + this.panelBBox.viewportWidth; + + renderLine(splitLineGroup, { + ...horizontalBorderStyle, + x1: 0, + x2: width, + y1: y, + y2: y, + }); + + if (splitLine!.showShadow && relativeScrollY > 0) { + splitLineGroup.appendChild( + new Rect({ + style: { + x: 0, + y, + width, + height: splitLine?.shadowWidth!, + fill: this.getShadowFill(90), + }, + }), + ); + } + } + }; } diff --git a/packages/s2-core/src/facet/table-facet.ts b/packages/s2-core/src/facet/table-facet.ts index 5cba6012e0..9393666d63 100644 --- a/packages/s2-core/src/facet/table-facet.ts +++ b/packages/s2-core/src/facet/table-facet.ts @@ -1,131 +1,70 @@ -import { Group, Rect, type LineStyleProps } from '@antv/g'; +import { Group } from '@antv/g'; import { isBoolean, isNumber, keys, last, maxBy, set } from 'lodash'; import { TableColCell, TableDataCell, TableSeriesNumberCell } from '../cell'; import { - FRONT_GROUND_GROUP_COL_FROZEN_Z_INDEX, KEY_GROUP_FROZEN_ROW_RESIZE_AREA, - KEY_GROUP_FROZEN_SPLIT_LINE, - KEY_GROUP_PANEL_FROZEN_BOTTOM, - KEY_GROUP_PANEL_FROZEN_COL, - KEY_GROUP_PANEL_FROZEN_ROW, - KEY_GROUP_PANEL_FROZEN_TOP, - KEY_GROUP_PANEL_FROZEN_TRAILING_COL, - KEY_GROUP_PANEL_FROZEN_TRAILING_ROW, KEY_GROUP_ROW_RESIZE_AREA, - LayoutWidthTypes, - PANEL_GROUP_FROZEN_GROUP_Z_INDEX, + LayoutWidthType, S2Event, SERIES_NUMBER_FIELD, - SPLIT_LINE_WIDTH, } from '../common/constant'; -import { FrozenCellGroupMap, FrozenGroupType } from '../common/constant/frozen'; import { DebuggerUtil } from '../common/debug'; import type { FilterParam, LayoutResult, ResizeInteractionOptions, - S2CellType, SortParams, TableSortParam, ViewMeta, - ViewMetaData, } from '../common/interface'; import type { TableDataSet } from '../data-set'; -import type { SimpleBBox } from '../engine'; -import { FrozenGroup } from '../group/frozen-group'; import type { SpreadSheet } from '../sheet-type'; import { getDataCellId } from '../utils/cell/data-cell'; import { getOccupiedWidthForTableCol } from '../utils/cell/table-col-cell'; import { getIndexRangeWithOffsets } from '../utils/facet'; -import { renderLine } from '../utils/g-renders'; import { getAllChildCells } from '../utils/get-all-child-cells'; -import { - getColsForGrid, - getFrozenRowsForGrid, - getRowsForGrid, -} from '../utils/grid'; -import type { Indexes, PanelIndexes } from '../utils/indexes'; import { getValidFrozenOptions } from '../utils/layout/frozen'; -import { BaseFacet } from './base-facet'; -import { CornerBBox } from './bbox/cornerBBox'; -import { Frame, type SeriesNumberHeader } from './header'; -import type { ColHeader } from './header/col'; -import { TableColHeader } from './header/table-col'; +import { floor } from '../utils/math'; +import { CornerBBox } from './bbox/corner-bbox'; +import { FrozenFacet } from './frozen-facet'; +import { ColHeader, Frame } from './header'; import { buildHeaderHierarchy } from './layout/build-header-hierarchy'; import { Hierarchy } from './layout/hierarchy'; import { layoutCoordinate } from './layout/layout-hooks'; import { Node } from './layout/node'; -import { - calculateFrozenCornerCells, - calculateInViewIndexes, - getFrozenDataCellType, - getFrozenLeafNodesCount, - isFrozenTrailingRow, - splitInViewIndexesWithFrozen, - translateGroup, -} from './utils'; - -export class TableFacet extends BaseFacet { - public declare rowOffsets: number[]; - - public frozenGroupInfo: Record< - FrozenGroupType, - { - width?: number; - height?: number; - range?: number[]; - } - > = { - [FrozenGroupType.FROZEN_COL]: { - width: 0, - }, - [FrozenGroupType.FROZEN_ROW]: { - height: 0, - }, - [FrozenGroupType.FROZEN_TRAILING_ROW]: { - height: 0, - }, - [FrozenGroupType.FROZEN_TRAILING_COL]: { - width: 0, - }, - }; - - public panelScrollGroupIndexes: Indexes = [] as unknown as Indexes; +import { getFrozenLeafNodesCount, isFrozenTrailingRow } from './utils'; +import { TableColHeader } from './header/table-col'; +export class TableFacet extends FrozenFacet { public constructor(spreadsheet: SpreadSheet) { super(spreadsheet); - this.spreadsheet.on(S2Event.RANGE_SORT, this.onSortHandler); this.spreadsheet.on(S2Event.RANGE_FILTER, this.onFilterHandler); } - protected override initPanelGroups(): void { - super.initPanelGroups(); - [ - this.frozenRowGroup, - this.frozenColGroup, - this.frozenTrailingRowGroup, - this.frozenTrailingColGroup, - this.frozenTopGroup, - this.frozenBottomGroup, - ] = [ - KEY_GROUP_PANEL_FROZEN_ROW, - KEY_GROUP_PANEL_FROZEN_COL, - KEY_GROUP_PANEL_FROZEN_TRAILING_ROW, - KEY_GROUP_PANEL_FROZEN_TRAILING_COL, - KEY_GROUP_PANEL_FROZEN_TOP, - KEY_GROUP_PANEL_FROZEN_BOTTOM, - ].map((name) => { - const frozenGroup = new FrozenGroup({ - name, - zIndex: PANEL_GROUP_FROZEN_GROUP_Z_INDEX, - s2: this.spreadsheet, - }); + public init() { + this.initRowOffsets(); + super.init(); + } - this.panelGroup.appendChild(frozenGroup); + protected initRowOffsets() { + const heightByField = + this.spreadsheet.options.style?.rowCell?.heightByField; - return frozenGroup; - }); + if (keys(heightByField!).length) { + const data = this.spreadsheet.dataSet.getDisplayDataSet(); + + this.rowOffsets = [0]; + let lastOffset = 0; + + data.forEach((_, rowIndex) => { + const currentHeight = this.getCellHeightByRowIndex(rowIndex); + const currentOffset = lastOffset + currentHeight; + + this.rowOffsets.push(currentOffset); + lastOffset = currentOffset; + }); + } } private onSortHandler = (sortParams: SortParams) => { @@ -188,7 +127,7 @@ export class TableFacet extends BaseFacet { if (oldIndex !== -1) { if (unFilter) { // remove filter params on current key if passed an empty filterValues field - oldConfig.splice(oldIndex); + oldConfig.splice(oldIndex, 1); } else { // if filter with same key already exists, replace it oldConfig[oldIndex] = params; @@ -210,27 +149,15 @@ export class TableFacet extends BaseFacet { return this.spreadsheet.theme.dataCell?.cell; } - override clearAllGroup() { - super.clearAllGroup(); - this.frozenRowGroup.removeChildren(); - this.frozenColGroup.removeChildren(); - this.frozenTrailingRowGroup.removeChildren(); - this.frozenTrailingColGroup.removeChildren(); - this.frozenTopGroup.removeChildren(); - this.frozenBottomGroup.removeChildren(); - } - public destroy(): void { super.destroy(); - const s2 = this.spreadsheet; - - s2.off(S2Event.RANGE_SORT, this.onSortHandler); - s2.off(S2Event.RANGE_FILTER, this.onFilterHandler); + this.spreadsheet.off(S2Event.RANGE_SORT, this.onSortHandler); + this.spreadsheet.off(S2Event.RANGE_FILTER, this.onFilterHandler); } protected calculateCornerBBox() { const { colsHierarchy } = this.getLayoutResult(); - const height = Math.floor(colsHierarchy.height); + const height = floor(colsHierarchy.height); this.cornerBBox = new CornerBBox(this); @@ -278,19 +205,18 @@ export class TableFacet extends BaseFacet { const { showSeriesNumber } = this.spreadsheet.options; const cellHeight = this.getCellHeightByRowIndex(rowIndex); const cellRange = this.getCellRange(); - const { trailingRowCount: frozenTrailingRowCount = 0 } = - getValidFrozenOptions( - this.spreadsheet.options.frozen!, - colLeafNodes.length, - cellRange.end - cellRange.start + 1, - ); + const { trailingRowCount = 0 } = getValidFrozenOptions( + this.spreadsheet.options.frozen!, + colLeafNodes.length, + cellRange.end - cellRange.start + 1, + ); - let data: ViewMetaData | number; + let data; const x = colNode.x; let y = this.viewCellHeights.getCellOffsetY(rowIndex); - if (isFrozenTrailingRow(rowIndex, cellRange.end, frozenTrailingRowCount)) { + if (isFrozenTrailingRow(rowIndex, cellRange.end, trailingRowCount)) { y = this.panelBBox.height - this.getTotalHeightForRange(rowIndex, cellRange.end); @@ -301,7 +227,7 @@ export class TableFacet extends BaseFacet { } else { data = this.spreadsheet.dataSet.getCellData({ query: { - col: colNode.field, + field: colNode.field, rowIndex, }, }); @@ -331,7 +257,7 @@ export class TableFacet extends BaseFacet { const { dataCell } = this.spreadsheet.options.style!; const { showSeriesNumber } = this.spreadsheet.options; - if (this.spreadsheet.getLayoutWidthType() !== LayoutWidthTypes.Compact) { + if (this.spreadsheet.getLayoutWidthType() !== LayoutWidthType.Compact) { const seriesNumberWidth = this.getSeriesNumberWidth(); const colHeaderColSize = colLeafNodes.length - (showSeriesNumber ? 1 : 0); const canvasW = @@ -339,16 +265,23 @@ export class TableFacet extends BaseFacet { seriesNumberWidth - Frame.getVerticalBorderWidth(this.spreadsheet); - // TODO: 向下取整, 导致单元格未撑满 canvas, 在冻结情况下会有问题, 代冻结重构后解决 + // TODO: 向下取整, 导致单元格未撑满 canvas, 在冻结情况下会有问题, 待冻结重构后解决 return Math.max( dataCell?.width!, - Math.floor(canvasW / Math.max(1, colHeaderColSize)), + floor(canvasW / Math.max(1, colHeaderColSize)), ); } return dataCell?.width ?? 0; } + public getContentHeight(): number { + const { getTotalHeight } = this.getViewCellHeights(); + const { colsHierarchy } = this.layoutResult; + + return getTotalHeight() + colsHierarchy.height; + } + protected getColNodeHeight(colNode: Node, colsHierarchy: Hierarchy) { const colCell = new TableColCell(colNode, this.spreadsheet, { shallowRender: true, @@ -397,19 +330,20 @@ export class TableFacet extends BaseFacet { } const topLevelNodes = colsHierarchy.getNodes(0); - const { trailingColCount: frozenTrailingColCount = 0 } = - getValidFrozenOptions( - this.spreadsheet.options.frozen!, - topLevelNodes.length, - ); + const { trailingColCount = 0 } = getValidFrozenOptions( + this.spreadsheet.options.frozen!, + topLevelNodes.length, + ); preLeafNode = Node.blankNode(); - const { width } = this.getCanvasSize(); + const width = + this.getCanvasSize().width - + Frame.getVerticalBorderWidth(this.spreadsheet); - if (frozenTrailingColCount > 0) { + if (trailingColCount > 0) { const { trailingColCount: realFrozenTrailingColCount } = - getFrozenLeafNodesCount(topLevelNodes, 0, frozenTrailingColCount); + getFrozenLeafNodesCount(topLevelNodes, 0, trailingColCount); const leafNodes = allNodes.filter((node) => node.isLeaf); for (let i = 1; i <= realFrozenTrailingColCount; i++) { @@ -482,13 +416,13 @@ export class TableFacet extends BaseFacet { let colWidth: number; - if (layoutWidthType === LayoutWidthTypes.Compact) { - const datas = dataSet.getDisplayDataSet(); + if (layoutWidthType === LayoutWidthType.Compact) { + const data = dataSet.getDisplayDataSet(); const formatter = dataSet.getFieldFormatter(colNode.field); // 采样前50,找出表身最长的数据 const maxLabel = maxBy( - datas + data ?.slice(0, 50) .map( (data) => @@ -536,41 +470,7 @@ export class TableFacet extends BaseFacet { return colWidth; } - protected getDefaultCellHeight(): number { - return this.getRowCellHeight(null as unknown as Node); - } - - public getCellHeightByRowIndex(rowIndex: number) { - if (this.rowOffsets) { - return this.getRowCellHeight({ id: String(rowIndex) } as Node); - } - - return this.getDefaultCellHeight(); - } - - protected initRowOffsets() { - const heightByField = - this.spreadsheet.options.style?.rowCell?.heightByField; - - if (keys(heightByField!).length) { - const data = this.spreadsheet.dataSet.getDisplayDataSet(); - - this.rowOffsets = [0]; - let lastOffset = 0; - - data.forEach((_, rowIndex) => { - const currentHeight = this.getCellHeightByRowIndex(rowIndex); - const currentOffset = lastOffset + currentHeight; - - this.rowOffsets.push(currentOffset); - lastOffset = currentOffset; - }); - } - } - public getViewCellHeights() { - this.initRowOffsets(); - const defaultCellHeight = this.getDefaultCellHeight(); return { @@ -594,13 +494,7 @@ export class TableFacet extends BaseFacet { return this.rowOffsets[offset]; } - let totalOffset = 0; - - for (let index = 0; index < offset; index++) { - totalOffset += defaultCellHeight; - } - - return totalOffset; + return offset * defaultCellHeight; }, getTotalLength: () => this.spreadsheet.dataSet.getDisplayDataSet().length, @@ -614,12 +508,12 @@ export class TableFacet extends BaseFacet { ); } - const yMin = Math.floor(minHeight / defaultCellHeight); + const yMin = floor(minHeight / defaultCellHeight, 0); // 防止数组index溢出导致报错 const yMax = maxHeight % defaultCellHeight === 0 ? maxHeight / defaultCellHeight - 1 - : Math.floor(maxHeight / defaultCellHeight); + : floor(maxHeight / defaultCellHeight, 0); return { start: Math.max(0, yMin), @@ -629,383 +523,6 @@ export class TableFacet extends BaseFacet { }; } - protected translateFrozenGroups = () => { - const { scrollY, scrollX } = this.getScrollOffset(); - const paginationScrollY = this.getPaginationScrollY(); - - const { x, y } = this.panelBBox; - - translateGroup(this.frozenTopGroup, x, y - paginationScrollY); - translateGroup(this.frozenBottomGroup, x, y); - - translateGroup(this.frozenRowGroup, x - scrollX, y - paginationScrollY); - translateGroup(this.frozenTrailingRowGroup, x - scrollX, y); - - translateGroup(this.frozenColGroup, x, y - scrollY - paginationScrollY); - translateGroup( - this.frozenTrailingColGroup, - x, - y - scrollY - paginationScrollY, - ); - }; - - public getTotalHeightForRange = (start: number, end: number) => { - if (start < 0 || end < 0) { - return 0; - } - - if (this.rowOffsets) { - return this.rowOffsets[end + 1] - this.rowOffsets[start]; - } - - let totalHeight = 0; - - for (let index = start; index < end + 1; index++) { - const height = this.getDefaultCellHeight(); - - totalHeight += height; - } - - return totalHeight; - }; - - private getShadowFill = (angle: number) => { - const { splitLine } = this.spreadsheet.theme; - - return `l (${angle}) 0:${splitLine?.shadowColors?.left} 1:${splitLine?.shadowColors?.right}`; - }; - - // eslint-disable-next-line max-lines-per-function - protected renderFrozenGroupSplitLine = (scrollX: number, scrollY: number) => { - const { - width: panelWidth, - height: panelHeight, - viewportWidth, - viewportHeight, - x: panelBBoxStartX, - y: panelBBoxStartY, - } = this.panelBBox; - - const topLevelColNodes = this.getTopLevelColNodes(); - const cellRange = this.getCellRange(); - const dataLength = cellRange.end - cellRange.start; - const { - rowCount: frozenRowCount = 0, - colCount: frozenColCount = 0, - trailingColCount: frozenTrailingColCount = 0, - trailingRowCount: frozenTrailingRowCount = 0, - } = getValidFrozenOptions( - this.spreadsheet.options.frozen!, - topLevelColNodes.length, - dataLength, - ); - - // 在分页条件下需要额外处理 Y 轴滚动值 - const relativeScrollY = Math.floor(scrollY - this.getPaginationScrollY()); - - // scroll boundary - const maxScrollX = Math.max(0, last(this.viewCellWidths)! - viewportWidth); - const maxScrollY = Math.max( - 0, - this.viewCellHeights.getCellOffsetY(cellRange.end + 1) - - this.viewCellHeights.getCellOffsetY(cellRange.start) - - viewportHeight, - ); - - // remove previous split line group - this.foregroundGroup.getElementById(KEY_GROUP_FROZEN_SPLIT_LINE)?.remove(); - - const { splitLine } = this.spreadsheet.theme; - const splitLineGroup = this.foregroundGroup.appendChild( - new Group({ - id: KEY_GROUP_FROZEN_SPLIT_LINE, - style: { - zIndex: FRONT_GROUND_GROUP_COL_FROZEN_Z_INDEX, - }, - }), - ); - - const verticalBorderStyle: Partial = { - lineWidth: SPLIT_LINE_WIDTH, - stroke: splitLine?.verticalBorderColor, - opacity: splitLine?.verticalBorderColorOpacity, - }; - - const horizontalBorderStyle: Partial = { - lineWidth: SPLIT_LINE_WIDTH, - stroke: splitLine?.horizontalBorderColor, - opacity: splitLine?.horizontalBorderColorOpacity, - }; - - const frameVerticalBorderWidth = Frame.getVerticalBorderWidth( - this.spreadsheet, - ); - - if (frozenColCount > 0) { - const x = topLevelColNodes.reduce((prev, item, idx) => { - if (idx < frozenColCount) { - return prev + item.width; - } - - return prev; - }, 0); - - const height = frozenTrailingRowCount > 0 ? panelHeight : viewportHeight; - - renderLine(splitLineGroup, { - ...verticalBorderStyle, - x1: x + panelBBoxStartX, - x2: x + panelBBoxStartX, - y1: panelBBoxStartY, - y2: panelBBoxStartY + height, - }); - - if (splitLine?.showShadow && scrollX > 0) { - splitLineGroup.appendChild( - new Rect({ - style: { - x: x + panelBBoxStartX, - y: panelBBoxStartY, - width: splitLine?.shadowWidth!, - height, - fill: this.getShadowFill(0), - }, - }), - ); - } - } - - if (frozenRowCount > 0) { - const y = - panelBBoxStartY + - this.getTotalHeightForRange( - cellRange.start, - cellRange.start + frozenRowCount - 1, - ); - const width = frozenTrailingColCount > 0 ? panelWidth : viewportWidth; - - renderLine(splitLineGroup, { - ...horizontalBorderStyle, - x1: 0, - x2: width + frameVerticalBorderWidth, - y1: y, - y2: y, - }); - - if (splitLine?.showShadow && relativeScrollY > 0) { - splitLineGroup.appendChild( - new Rect({ - style: { - x: 0, - y, - width: width + frameVerticalBorderWidth, - height: splitLine?.shadowWidth!, - fill: this.getShadowFill(90), - }, - }), - ); - } - } - - if (frozenTrailingColCount > 0) { - const { x } = - topLevelColNodes[topLevelColNodes.length - frozenTrailingColCount]; - const height = frozenTrailingRowCount ? panelHeight : viewportHeight; - - renderLine(splitLineGroup, { - ...verticalBorderStyle, - x1: x, - x2: x, - y1: panelBBoxStartY, - y2: panelBBoxStartY + height, - }); - - if ( - splitLine?.showShadow && - Math.floor(scrollX) < Math.floor(maxScrollX) - ) { - splitLineGroup.appendChild( - new Rect({ - style: { - x: x - splitLine.shadowWidth!, - y: panelBBoxStartY, - width: splitLine.shadowWidth!, - height, - fill: this.getShadowFill(180), - }, - }), - ); - } - } - - if (frozenTrailingRowCount > 0) { - const y = - this.panelBBox.maxY - - this.getTotalHeightForRange( - cellRange.end - frozenTrailingRowCount + 1, - cellRange.end, - ); - const width = frozenTrailingColCount > 0 ? panelWidth : viewportWidth; - - renderLine(splitLineGroup, { - ...horizontalBorderStyle, - x1: 0, - x2: width + frameVerticalBorderWidth, - y1: y, - y2: y, - }); - - if (splitLine?.showShadow && relativeScrollY < Math.floor(maxScrollY)) { - splitLineGroup.appendChild( - new Rect({ - style: { - x: 0, - y: y - splitLine.shadowWidth!, - width: width + frameVerticalBorderWidth, - height: splitLine.shadowWidth!, - fill: this.getShadowFill(270), - }, - }), - ); - } - } - }; - - protected renderFrozenPanelCornerGroup = () => { - const topLevelNodes = this.getTopLevelColNodes(); - const cellRange = this.getCellRange(); - - const { - rowCount: frozenRowCount = 0, - colCount: frozenColCount = 0, - trailingRowCount: frozenTrailingRowCount = 0, - trailingColCount: frozenTrailingColCount = 0, - } = getValidFrozenOptions( - this.spreadsheet.options.frozen!, - topLevelNodes.length, - cellRange.end - cellRange.start + 1, - ); - - const { colCount, trailingColCount } = getFrozenLeafNodesCount( - topLevelNodes, - frozenColCount, - frozenTrailingColCount, - ); - - const result = calculateFrozenCornerCells( - { - rowCount: frozenRowCount, - colCount, - trailingRowCount: frozenTrailingRowCount, - trailingColCount, - }, - this.getColLeafNodes().length, - cellRange, - ); - - Object.keys(result).forEach((key) => { - const cells = result[key]; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const group = this[FrozenCellGroupMap[key]] as Group; - - if (group) { - cells.forEach((cell) => { - this.addFrozenCell(cell.x, cell.y, group); - }); - } - }); - }; - - addFrozenCell = (colIndex: number, rowIndex: number, group: Group) => { - const viewMeta = this.getCellMeta(rowIndex, colIndex); - - if (viewMeta) { - viewMeta.isFrozenCorner = true; - const cell = this.spreadsheet.options.dataCell?.(viewMeta)!; - - group.appendChild(cell); - } - }; - - getRealFrozenColumns = ( - frozenColCount: number, - frozenTrailingColCount: number, - ): { colCount: number; trailingColCount: number } => { - if (frozenColCount || frozenTrailingColCount) { - const nodes = this.getTopLevelColNodes(); - - return getFrozenLeafNodesCount( - nodes, - frozenColCount, - frozenTrailingColCount, - ); - } - - return { - colCount: frozenColCount, - trailingColCount: frozenTrailingColCount, - }; - }; - - addDataCell = (cell: S2CellType) => { - const { - rowCount: frozenRowCount = 0, - colCount: frozenColCount = 0, - trailingRowCount: frozenTrailingRowCount = 0, - trailingColCount: frozenTrailingColCount = 0, - } = this.spreadsheet.options.frozen!; - - const colLength = this.getColNodes().length; - const cellRange = this.getCellRange(); - const { colCount, trailingColCount } = this.getRealFrozenColumns( - frozenColCount, - frozenTrailingColCount, - ); - - const frozenCellType = getFrozenDataCellType( - cell.getMeta(), - { - rowCount: frozenRowCount, - trailingRowCount: frozenTrailingRowCount, - colCount, - trailingColCount, - }, - colLength, - cellRange, - ); - - const groupName = FrozenCellGroupMap[frozenCellType]; - - if (groupName) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const group = this[groupName] as Group; - - group.appendChild(cell); - } - }; - - protected getColHeader(): ColHeader { - if (!this.columnHeader) { - const { x, width, viewportHeight, viewportWidth } = this.panelBBox; - - return new TableColHeader({ - width, - height: this.cornerBBox.height, - viewportWidth, - viewportHeight, - cornerWidth: this.cornerBBox.width, - position: { x, y: 0 }, - nodes: this.getColNodes(), - sortParam: this.spreadsheet.store.get('sortParam'), - spreadsheet: this.spreadsheet, - }); - } - - return this.columnHeader; - } - protected updateRowResizeArea() { const { resize } = this.spreadsheet.options.interaction!; @@ -1042,299 +559,32 @@ export class TableFacet extends BaseFacet { }); } - public render() { - this.calculateFrozenGroupInfo(); - this.renderFrozenPanelCornerGroup(); - super.render(); - } - - private getFrozenOptions = () => { - const colLength = this.getColLeafNodes().length; - const cellRange = this.getCellRange(); - - return getValidFrozenOptions( - this.spreadsheet.options.frozen!, - colLength, - cellRange.end - cellRange.start + 1, - ); - }; - - public calculateFrozenGroupInfo() { - const { - colCount: frozenColCount = 0, - rowCount: frozenRowCount = 0, - trailingColCount: frozenTrailingColCount = 0, - trailingRowCount: frozenTrailingRowCount = 0, - } = this.getFrozenOptions(); - - const topLevelColNodes = this.getTopLevelColNodes(); - const viewCellHeights = this.viewCellHeights; - const cellRange = this.getCellRange(); - const { frozenCol, frozenTrailingCol, frozenRow, frozenTrailingRow } = - this.frozenGroupInfo; - - if (frozenColCount > 0) { - frozenCol.width = - topLevelColNodes[frozenColCount - 1].x + - topLevelColNodes[frozenColCount - 1].width - - 0; - frozenCol.range = [0, frozenColCount - 1]; - } - - if (frozenRowCount > 0) { - frozenRow.height = - viewCellHeights.getCellOffsetY(cellRange.start + frozenRowCount) - - viewCellHeights.getCellOffsetY(cellRange.start); - frozenRow.range = [cellRange.start, cellRange.start + frozenRowCount - 1]; - } - - if (frozenTrailingColCount > 0) { - frozenTrailingCol.width = - topLevelColNodes[topLevelColNodes.length - 1].x - - topLevelColNodes[topLevelColNodes.length - frozenTrailingColCount].x + - topLevelColNodes[topLevelColNodes.length - 1].width; - frozenTrailingCol.range = [ - topLevelColNodes.length - frozenTrailingColCount, - topLevelColNodes.length - 1, - ]; - } - - if (frozenTrailingRowCount > 0) { - frozenTrailingRow.height = - viewCellHeights.getCellOffsetY(cellRange.end + 1) - - viewCellHeights.getCellOffsetY( - cellRange.end + 1 - frozenTrailingRowCount, - ); - frozenTrailingRow.range = [ - cellRange.end - frozenTrailingRowCount + 1, - cellRange.end, - ]; - } - } - protected getRowHeader() { return null; } - protected getSeriesNumberHeader(): SeriesNumberHeader | null { - return null; - } - - protected translateRelatedGroups( - scrollX: number, - scrollY: number, - hRowScroll: number, - ) { - super.translateRelatedGroups(scrollX, scrollY, hRowScroll); - this.translateFrozenGroups(); - this.updateRowResizeArea(); - this.renderFrozenGroupSplitLine(scrollX, scrollY); - } - - public calculateXYIndexes(scrollX: number, scrollY: number): PanelIndexes { - const colLength = this.getColLeafNodes().length; - const cellRange = this.getCellRange(); - - const { viewportHeight: height, viewportWidth: width } = this.panelBBox; - - const { - colCount: frozenColCount = 0, - rowCount: frozenRowCount = 0, - trailingColCount: frozenTrailingColCount = 0, - trailingRowCount: frozenTrailingRowCount = 0, - } = this.getFrozenOptions(); - - const finalViewport: SimpleBBox = { - width, - height, - x: 0, - y: 0, - }; - - if (frozenTrailingColCount > 0 || frozenColCount > 0) { - const { frozenTrailingCol, frozenCol } = this.frozenGroupInfo; - - finalViewport.width -= frozenTrailingCol.width! + frozenCol.width!; - finalViewport.x += frozenCol.width!; - } - - if (frozenTrailingRowCount > 0 || frozenRowCount > 0) { - const { frozenRow, frozenTrailingRow } = this.frozenGroupInfo; - - // canvas 高度小于 row height 和 trailingRow height 的时候 height 为 0 - if ( - finalViewport.height < - frozenRow.height! + frozenTrailingRow.height! - ) { - finalViewport.height = 0; - finalViewport.y = 0; - } else { - finalViewport.height -= frozenRow.height! + frozenTrailingRow.height!; - finalViewport.y += frozenRow.height!; - } - } - - const indexes = calculateInViewIndexes({ - scrollX, - scrollY, - widths: this.viewCellWidths, - heights: this.viewCellHeights, - viewport: finalViewport, - rowRemainWidth: this.getRealScrollX(this.cornerBBox.width), - }); - - this.panelScrollGroupIndexes = indexes; - - const { colCount, trailingColCount } = this.getRealFrozenColumns( - frozenColCount, - frozenTrailingColCount, - ); - - return splitInViewIndexesWithFrozen( - indexes, - { - colCount, - rowCount: frozenRowCount, - trailingColCount, - trailingRowCount: frozenTrailingRowCount, - }, - colLength, - cellRange, - ); - } - - // 对 panelScrollGroup 以及四个方向的 frozenGroup 做 Clip,避免有透明度时冻结分组和滚动分组展示重叠 - protected clip(scrollX: number, scrollY: number) { - const paginationScrollY = this.getPaginationScrollY(); - const { - frozenRowGroup, - frozenColGroup, - frozenTrailingColGroup, - frozenTrailingRowGroup, - } = this; - const frozenColGroupWidth = frozenColGroup.getBBox().width; - const frozenRowGroupHeight = frozenRowGroup.getBBox().height; - const frozenTrailingColBBox = frozenTrailingColGroup.getBBox(); - const frozenTrailingRowGroupHeight = - frozenTrailingRowGroup.getBBox().height; - const panelScrollGroupWidth = - this.panelBBox.width - - frozenColGroupWidth - - frozenTrailingColGroup.getBBox().width; - const panelScrollGroupHeight = - this.panelBBox.height - - frozenRowGroupHeight - - frozenTrailingRowGroupHeight; - - frozenRowGroup.style.clipPath = new Rect({ - style: { - x: scrollX + frozenColGroupWidth, - y: paginationScrollY, - width: panelScrollGroupWidth, - height: frozenRowGroupHeight, - }, - }); - - frozenTrailingRowGroup.style.clipPath = new Rect({ - style: { - x: scrollX + frozenColGroupWidth, - y: frozenTrailingRowGroup.getBBox().top, - width: panelScrollGroupWidth, - height: frozenTrailingRowGroupHeight, - }, - }); - - const colClipArea = { - y: scrollY + frozenRowGroupHeight, - height: panelScrollGroupHeight, - }; - - frozenColGroup.style.clipPath = new Rect({ - style: { - ...colClipArea, - x: 0, - width: frozenColGroupWidth, - }, - }); - - frozenTrailingColGroup.style.clipPath = new Rect({ - style: { - ...colClipArea, - x: frozenTrailingColBBox.left, - width: frozenTrailingColBBox.width, - }, - }); - - const rowResizeGroup = this.foregroundGroup.getElementById( - KEY_GROUP_ROW_RESIZE_AREA, - ); + protected getColHeader(): ColHeader { + if (!this.columnHeader) { + const { x, width, viewportHeight, viewportWidth } = this.panelBBox; - if (rowResizeGroup) { - rowResizeGroup.style.clipPath = new Rect({ - style: { - x: 0, - y: this.panelBBox.y + frozenRowGroupHeight, - width: - this.panelBBox.width + - Frame.getVerticalBorderWidth(this.spreadsheet), - height: panelScrollGroupHeight, - }, + return new TableColHeader({ + width, + height: this.cornerBBox.height, + viewportWidth, + viewportHeight, + cornerWidth: this.cornerBBox.width, + position: { x, y: 0 }, + nodes: this.getColNodes(), + sortParam: this.spreadsheet.store.get('sortParam'), + spreadsheet: this.spreadsheet, }); } - } - public getTopLevelColNodes() { - return this.getColNodes(0); + return this.columnHeader; } - public updatePanelScrollGroup() { - super.updatePanelScrollGroup(); - - [ - FrozenGroupType.FROZEN_COL, - FrozenGroupType.FROZEN_ROW, - FrozenGroupType.FROZEN_TRAILING_COL, - FrozenGroupType.FROZEN_TRAILING_ROW, - ].forEach((key) => { - if (!this.frozenGroupInfo[key].range) { - return; - } - - let cols: number[] = []; - let rows: number[] = []; - - if (key.toLowerCase().includes('row')) { - const [rowMin, rowMax] = this.frozenGroupInfo[key].range || []; - - cols = this.gridInfo.cols; - rows = getRowsForGrid(rowMin, rowMax, this.viewCellHeights); - - if (key === FrozenGroupType.FROZEN_TRAILING_ROW) { - const { top } = this.frozenTrailingRowGroup.getBBox(); - - rows = getFrozenRowsForGrid( - rowMin, - rowMax, - Math.ceil(top), - this.viewCellHeights, - ); - } - } else { - const [colMin, colMax] = this.frozenGroupInfo[key].range || []; - const nodes = this.getTopLevelColNodes(); - - cols = getColsForGrid(colMin, colMax, nodes); - rows = this.gridInfo.rows; - } - - this[`${key}Group`].updateGrid( - { - cols, - rows, - }, - `${key}Group`, - ); - }); + protected getSeriesNumberHeader() { + return null; } /** @@ -1342,9 +592,8 @@ export class TableFacet extends BaseFacet { * @description 明细表序号单元格是基于 DataCell 实现 */ public getSeriesNumberCells(): TableSeriesNumberCell[] { - // @ts-ignore - return this.getDataCells().filter( - (cell) => cell instanceof TableSeriesNumberCell, - ); + return this.getDataCells().filter((cell) => { + return cell.getMeta().valueField === SERIES_NUMBER_FIELD; + }) as TableSeriesNumberCell[]; } } diff --git a/packages/s2-core/src/facet/utils.ts b/packages/s2-core/src/facet/utils.ts index 20cbd631bb..d17a8c33e3 100644 --- a/packages/s2-core/src/facet/utils.ts +++ b/packages/s2-core/src/facet/utils.ts @@ -1,16 +1,19 @@ import type { Group } from '@antv/g'; -import { findIndex, isNil } from 'lodash'; +import { findIndex, isEmpty, isNil } from 'lodash'; import type { FrozenCellIndex } from '../common/constant/frozen'; import { FrozenCellType } from '../common/constant/frozen'; import { DEFAULT_PAGE_INDEX } from '../common/constant/pagination'; import type { CustomHeaderFields, + Fields, Pagination, + S2Options, + S2PivotSheetFrozenOptions, S2TableSheetFrozenOptions, ScrollSpeedRatio, } from '../common/interface'; -import type { SimpleBBox } from '../engine'; import type { Indexes } from '../utils/indexes'; +import type { SimpleBBox } from '../engine'; import type { ViewCellHeights } from './layout/interface'; import type { Node } from './layout/node'; @@ -272,54 +275,50 @@ export const splitInViewIndexesWithFrozen = ( }, ) => { const { - colCount: frozenColCount = 0, - rowCount: frozenRowCount = 0, - trailingColCount: frozenTrailingColCount = 0, - trailingRowCount: frozenTrailingRowCount = 0, + colCount = 0, + rowCount = 0, + trailingColCount = 0, + trailingRowCount = 0, } = frozenOptions; const centerIndexes: Indexes = [...indexes]; // Cut off frozen cells from centerIndexes - if (isFrozenCol(centerIndexes[0], frozenColCount)) { - centerIndexes[0] = frozenColCount; + if (isFrozenCol(centerIndexes[0], colCount)) { + centerIndexes[0] = colCount; } - if ( - isFrozenTrailingCol(centerIndexes[1], frozenTrailingColCount, colLength) - ) { - centerIndexes[1] = colLength - frozenTrailingColCount - 1; + if (isFrozenTrailingCol(centerIndexes[1], trailingColCount, colLength)) { + centerIndexes[1] = colLength - trailingColCount - 1; } - if (isFrozenRow(centerIndexes[2], cellRange.start, frozenRowCount)) { - centerIndexes[2] = cellRange.start + frozenRowCount; + if (isFrozenRow(centerIndexes[2], cellRange.start, rowCount)) { + centerIndexes[2] = cellRange.start + rowCount; } - if ( - isFrozenTrailingRow(centerIndexes[3], cellRange.end, frozenTrailingRowCount) - ) { - centerIndexes[3] = cellRange.end - frozenTrailingRowCount; + if (isFrozenTrailingRow(centerIndexes[3], cellRange.end, trailingRowCount)) { + centerIndexes[3] = cellRange.end - trailingRowCount; } // Calculate indexes for four frozen groups const frozenRowIndexes: Indexes = [...centerIndexes]; frozenRowIndexes[2] = cellRange.start; - frozenRowIndexes[3] = cellRange.start + frozenRowCount - 1; + frozenRowIndexes[3] = cellRange.start + rowCount - 1; const frozenColIndexes: Indexes = [...centerIndexes]; frozenColIndexes[0] = 0; - frozenColIndexes[1] = frozenColCount - 1; + frozenColIndexes[1] = colCount - 1; const frozenTrailingRowIndexes: Indexes = [...centerIndexes]; - frozenTrailingRowIndexes[2] = cellRange.end + 1 - frozenTrailingRowCount; + frozenTrailingRowIndexes[2] = cellRange.end + 1 - trailingRowCount; frozenTrailingRowIndexes[3] = cellRange.end; const frozenTrailingColIndexes: Indexes = [...centerIndexes]; - frozenTrailingColIndexes[0] = colLength - frozenTrailingColCount; + frozenTrailingColIndexes[0] = colLength - trailingColCount; frozenTrailingColIndexes[1] = colLength - 1; return { @@ -355,17 +354,15 @@ export const getCellRange = ( /** * 给定一个一层的 node 数组以及左右固定列的数量,计算出实际固定列(叶子节点)的数量 * @param nodes - * @param frozenColCount - * @param frozenTrailingColCount + * @param colCount + * @param trailingColCount * @returns {colCount, trailingColCount} */ export const getFrozenLeafNodesCount = ( nodes: Node[], - frozenColCount: number, - frozenTrailingColCount: number, + colCount: number, + trailingColCount: number, ): { colCount: number; trailingColCount: number } => { - let colCount = frozenColCount; - let trailingColCount = frozenTrailingColCount; const getLeafNodesCount = (node: Node) => { if (node.isLeaf) { return 1; @@ -382,17 +379,17 @@ export const getFrozenLeafNodesCount = ( return 0; }; - if (frozenColCount) { - colCount = nodes.slice(0, frozenColCount).reduce((count, node) => { + if (colCount) { + colCount = nodes.slice(0, colCount).reduce((count, node) => { count += getLeafNodesCount(node); return count; }, 0); } - if (frozenTrailingColCount) { + if (trailingColCount) { trailingColCount = nodes - .slice(nodes.length - frozenTrailingColCount) + .slice(nodes.length - trailingColCount) .reduce((count, node) => { count += getLeafNodesCount(node); @@ -473,3 +470,55 @@ export const getLeftLeafNode = (node: Node): Node => { return firstNode.isLeaf ? firstNode : getLeftLeafNode(firstNode); }; +/** + * fields 的 rows、columns、values 值都为空时,返回 true + * @param {Fields} fields + * @return {boolean} + */ +export const areAllFieldsEmpty = (fields: Fields) => { + return ( + isEmpty(fields.rows) && isEmpty(fields.columns) && isEmpty(fields.values) + ); +}; + +/** + * get frozen options pivot-sheet (business limit) + * @param options + * @returns + */ +export const getFrozenRowCfgPivot = ( + options: S2Options, + rowNodes: Node[], +): S2PivotSheetFrozenOptions & + S2TableSheetFrozenOptions & { + rowHeight: number; + } => { + /** + * series number cell 可以自定义布局,和 row cell 不一定是 1 对 1 的关系 + * showSeriesNumber 暂时禁用 首行冻结 + * */ + const { pagination, frozen, hierarchyType, showSeriesNumber } = options; + + const enablePagination = pagination && pagination.pageSize; + let firstRow = false; + const headNode = rowNodes?.[0]; + + if (!enablePagination && !showSeriesNumber && frozen?.firstRow) { + const treeMode = hierarchyType === 'tree'; + + // tree mode + // first node no children: entire row + firstRow = treeMode || headNode?.children?.length === 0; + } + + const effectiveFrozenFirstRow = firstRow && !!headNode; + + return { + rowCount: effectiveFrozenFirstRow ? 1 : 0, + colCount: 0, + trailingColCount: 0, + trailingRowCount: 0, + firstRow, + rowHeight: effectiveFrozenFirstRow ? headNode.height : 0, + }; +}; diff --git a/packages/s2-core/src/group/grid-group.ts b/packages/s2-core/src/group/grid-group.ts index 8080a190a3..9c166c0361 100644 --- a/packages/s2-core/src/group/grid-group.ts +++ b/packages/s2-core/src/group/grid-group.ts @@ -1,5 +1,9 @@ import { Group } from '@antv/g'; -import { KEY_GROUP_GRID_GROUP, SQUARE_LINE_CAP } from '../common/constant'; +import { last } from 'lodash'; +import { + KEY_GROUP_GRID_GROUP, + PANEL_GRID_GROUP_Z_INDEX, +} from '../common/constant'; import type { GridInfo } from '../common/interface'; import type { GridGroupConstructorParameters } from '../common/interface/group'; import type { SpreadSheet } from '../sheet-type/spread-sheet'; @@ -26,44 +30,39 @@ export class GridGroup extends Group { }; public updateGrid = (gridInfo: GridInfo, id = KEY_GROUP_GRID_GROUP) => { - const bbox = this.getBBox(); - const { theme } = this.s2; - - const style = theme.dataCell!.cell; - if (!this.gridGroup || !this.getElementById(id)) { this.gridGroup = this.appendChild( new Group({ id, + style: { + zIndex: PANEL_GRID_GROUP_Z_INDEX, + }, }), ); + } else { + this.gridGroup.removeChildren(); } - this.gridGroup.removeChildren(); + const width = last(gridInfo.cols) ?? 0; + const height = last(gridInfo.rows) ?? 0; + const { theme } = this.s2; + + const style = theme.dataCell!.cell; const verticalBorderWidth = style?.verticalBorderWidth; this.gridInfo = gridInfo; - - /* - * line 在绘制时,包围盒计算有点问题,会带入lineWidth - * 比如传入的 x1=0, x2=10, lineWidth=20 - * 最后line得出来的包围盒 minX=-10, maxX=20,会将lineWidth/2纳入计算中 - * 最后就会导致更新过程中,GridGroup的包围盒不断被放大 - * 因此在传入时,将这部分坐标减去,并结合lineCap将这部分绘制出来,达到内容区域绘制不变,包围盒计算正确的目的 - */ const halfVerticalBorderWidthBorderWidth = verticalBorderWidth! / 2; this.gridInfo.cols.forEach((x) => { renderLine(this.gridGroup, { x1: x - halfVerticalBorderWidthBorderWidth, x2: x - halfVerticalBorderWidthBorderWidth, - y1: halfVerticalBorderWidthBorderWidth, - y2: Math.floor(bbox.height - halfVerticalBorderWidthBorderWidth), + y1: 0, + y2: height, stroke: style!.verticalBorderColor, strokeOpacity: style!.verticalBorderColorOpacity, lineWidth: verticalBorderWidth, - lineCap: SQUARE_LINE_CAP, }); }); @@ -72,17 +71,14 @@ export class GridGroup extends Group { this.gridInfo.rows.forEach((y) => { renderLine(this.gridGroup, { - x1: halfHorizontalBorderWidth, - x2: Math.floor(bbox.width - halfHorizontalBorderWidth), + x1: 0, + x2: width, y1: y - halfHorizontalBorderWidth, y2: y - halfHorizontalBorderWidth, stroke: style!.horizontalBorderColor, strokeOpacity: style!.horizontalBorderColorOpacity, lineWidth: horizontalBorderWidth, - lineCap: SQUARE_LINE_CAP, }); }); - - this.gridGroup.toFront(); }; } diff --git a/packages/s2-core/src/group/panel-scroll-group.ts b/packages/s2-core/src/group/panel-scroll-group.ts index 315355e3b7..226bcc3505 100644 --- a/packages/s2-core/src/group/panel-scroll-group.ts +++ b/packages/s2-core/src/group/panel-scroll-group.ts @@ -4,7 +4,10 @@ import type { GridGroupConstructorParameters } from '../common/interface/group'; import { updateMergedCells } from '../utils/interaction/merge-cell'; import { S2Event } from '../common'; import type { MergedCell } from './../cell/merged-cell'; -import { KEY_GROUP_MERGED_CELLS } from './../common/constant/basic'; +import { + KEY_GROUP_MERGED_CELLS, + PANEL_MERGE_GROUP_Z_INDEX, +} from './../common/constant/basic'; import { GridGroup } from './grid-group'; export class PanelScrollGroup extends GridGroup { @@ -27,6 +30,9 @@ export class PanelScrollGroup extends GridGroup { this.mergedCellsGroup = this.appendChild( new Group({ id: KEY_GROUP_MERGED_CELLS, + style: { + zIndex: PANEL_MERGE_GROUP_Z_INDEX, + }, }), ); } @@ -34,7 +40,6 @@ export class PanelScrollGroup extends GridGroup { updateMergedCells() { this.initMergedCellsGroup(); updateMergedCells(this.s2, this.mergedCellsGroup); - this.mergedCellsGroup.toFront(); } addMergeCell(mergedCell: MergedCell) { diff --git a/packages/s2-core/src/interaction/base-interaction/click/data-cell-click.ts b/packages/s2-core/src/interaction/base-interaction/click/data-cell-click.ts index a316e2e1ca..ca459b8069 100644 --- a/packages/s2-core/src/interaction/base-interaction/click/data-cell-click.ts +++ b/packages/s2-core/src/interaction/base-interaction/click/data-cell-click.ts @@ -1,4 +1,5 @@ import type { FederatedPointerEvent as CanvasEvent } from '@antv/g'; +import { forEach } from 'lodash'; import type { DataCell } from '../../../cell/data-cell'; import { InteractionStateName, @@ -9,41 +10,30 @@ import type { TooltipData, TooltipOperatorOptions, ViewMeta, + ViewMetaData, } from '../../../common/interface'; import { getCellMeta, afterSelectDataCells, + getRowCellForSelectedCell, } from '../../../utils/interaction/select-event'; import { getTooltipOptions, getTooltipVisibleOperator, } from '../../../utils/tooltip'; import { BaseEvent, type BaseEventImplement } from '../../base-event'; +import { updateAllColHeaderCellState } from '../../../utils/interaction'; export class DataCellClick extends BaseEvent implements BaseEventImplement { - private clickTimer: number; - - private clickCount = 0; - public bindEvents() { this.bindDataCellClick(); } - private countClick() { - window.clearTimeout(this.clickTimer); - this.clickTimer = window.setTimeout(() => { - this.clickCount = 0; - }, 200); - this.clickCount++; - } - private bindDataCellClick() { this.spreadsheet.on(S2Event.DATA_CELL_CLICK, (event: CanvasEvent) => { - this.countClick(); - event.stopPropagation(); - const { interaction } = this.spreadsheet; + const { interaction, facet } = this.spreadsheet; interaction.clearHoverTimer(); @@ -67,12 +57,15 @@ export class DataCellClick extends BaseEvent implements BaseEventImplement { interaction.addIntercepts([InterceptType.HOVER]); if (interaction.isSelectedCell(cell)) { - /** - * 双击时不触发选择态 reset - * g5.0 mouseup 底层监听的是 pointerup,detail 为 0,需自行判断是否双击 - */ - if (this.clickCount <= 1) { + // https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail,使用 detail 属性来判断是否是双击,双击时不触发选择态 reset + if ((event.originalEvent as UIEvent)?.detail === 1) { interaction.reset(); + + // https://github.com/antvis/S2/issues/2447 + this.spreadsheet.emit( + S2Event.GLOBAL_SELECTED, + interaction.getActiveCells(), + ); } return; @@ -85,6 +78,31 @@ export class DataCellClick extends BaseEvent implements BaseEventImplement { }); this.spreadsheet.emit(S2Event.GLOBAL_SELECTED, [cell]); this.showTooltip(event, meta); + + // 点击单元格,高亮对应的行头、列头 + const { rowId, colId, spreadsheet } = meta; + const { colHeader, rowHeader } = interaction.getSelectedCellHighlight(); + + if (colHeader) { + updateAllColHeaderCellState( + colId, + facet.getColCells(), + InteractionStateName.SELECTED, + ); + } + + if (rowHeader) { + if (rowId) { + const allRowHeaderCells = getRowCellForSelectedCell( + meta, + spreadsheet, + ); + + forEach(allRowHeaderCells, (rowCell) => { + rowCell.updateByState(InteractionStateName.SELECTED); + }); + } + } }); } @@ -111,7 +129,7 @@ export class DataCellClick extends BaseEvent implements BaseEventImplement { const onlyShowCellText = this.spreadsheet.isTableMode(); const cellData = onlyShowCellText ? ({ - ...currentCellMeta, + ...(currentCellMeta as ViewMetaData), value: value || fieldValue, valueField: field || valueField, } as TooltipData) diff --git a/packages/s2-core/src/interaction/base-interaction/click/row-column-click.ts b/packages/s2-core/src/interaction/base-interaction/click/row-column-click.ts index 1a7be05a58..8874c90730 100644 --- a/packages/s2-core/src/interaction/base-interaction/click/row-column-click.ts +++ b/packages/s2-core/src/interaction/base-interaction/click/row-column-click.ts @@ -1,10 +1,10 @@ import type { FederatedPointerEvent as CanvasEvent } from '@antv/g'; -import { difference } from 'lodash'; +import { difference, findLast } from 'lodash'; import { CellType, + getTooltipOperatorHiddenColumnsMenu, InterceptType, S2Event, - getTooltipOperatorHiddenColumnsMenu, } from '../../../common/constant'; import type { TooltipBaseOperatorMenuItem, @@ -20,7 +20,10 @@ import { hideColumnsByThunkGroup, isEqualDisplaySiblingNodeId, } from '../../../utils/hide-columns'; -import { isMultiSelectionKey } from '../../../utils/interaction/select-event'; +import { + isMouseEventWithMeta, + isMultiSelectionKey, +} from '../../../utils/interaction/select-event'; import { getTooltipOptions, getTooltipVisibleOperator, @@ -37,6 +40,12 @@ export class RowColumnClick extends BaseEvent implements BaseEventImplement { this.bindColCellClick(); this.bindRowCellClick(); this.bindTableColExpand(); + this.bindMouseMove(); + } + + public reset() { + this.isMultiSelection = false; + this.spreadsheet.interaction.removeIntercepts([InterceptType.CLICK]); } private bindKeyboardDown() { @@ -53,8 +62,16 @@ export class RowColumnClick extends BaseEvent implements BaseEventImplement { private bindKeyboardUp() { this.spreadsheet.on(S2Event.GLOBAL_KEYBOARD_UP, (event: KeyboardEvent) => { if (isMultiSelectionKey(event)) { - this.isMultiSelection = false; - this.spreadsheet.interaction.removeIntercepts([InterceptType.CLICK]); + this.reset(); + } + }); + } + + private bindMouseMove() { + this.spreadsheet.on(S2Event.GLOBAL_MOUSE_MOVE, (event) => { + // 当快捷键被系统拦截后,按需补充调用一次 reset + if (this.isMultiSelection && !isMouseEventWithMeta(event)) { + this.reset(); } }); } @@ -198,8 +215,10 @@ export class RowColumnClick extends BaseEvent implements BaseEventImplement { 'hiddenColumnsDetail', [], ); + + // 当前单元格的前/后节点都被隐藏时, 会出现两个展开按钮, 优先展开靠右的 const { hideColumnNodes = [] } = - lastHiddenColumnsDetail.find(({ displaySiblingNode }) => + findLast(lastHiddenColumnsDetail, ({ displaySiblingNode }) => isEqualDisplaySiblingNodeId(displaySiblingNode, node.id), ) || {}; diff --git a/packages/s2-core/src/interaction/base-interaction/click/row-text-click.ts b/packages/s2-core/src/interaction/base-interaction/click/row-text-click.ts index 68ec6275ec..8b080cf2a9 100644 --- a/packages/s2-core/src/interaction/base-interaction/click/row-text-click.ts +++ b/packages/s2-core/src/interaction/base-interaction/click/row-text-click.ts @@ -1,7 +1,7 @@ import type { FederatedPointerEvent as CanvasEvent } from '@antv/g'; import { InterceptType, S2Event } from '../../../common/constant'; import type { RawData } from '../../../common/interface'; -import { getFieldValueOfViewMetaData } from '../../../data-set/cell-data'; +import { CellData } from '../../../data-set/cell-data'; import type { Node } from '../../../facet/layout/node'; import { BaseEvent, type BaseEventImplement } from '../../base-event'; import type { Data } from '../../../common/interface'; @@ -41,14 +41,9 @@ export class RowTextClick extends BaseEvent implements BaseEventImplement { const data = this.spreadsheet.dataSet.getCellMultiData({ query: leafNode?.query!, - totals: { - row: { - totalDimensions: true, - }, - }, })[0]; - const originalData = getFieldValueOfViewMetaData(data) as RawData; + const originalData = CellData.getFieldValue(data) as RawData; return { ...originalData, diff --git a/packages/s2-core/src/interaction/base-interaction/hover.ts b/packages/s2-core/src/interaction/base-interaction/hover.ts index ed8221937f..81f07fdf16 100644 --- a/packages/s2-core/src/interaction/base-interaction/hover.ts +++ b/packages/s2-core/src/interaction/base-interaction/hover.ts @@ -32,15 +32,24 @@ export class HoverEvent extends BaseEvent implements BaseEventImplement { public updateRowColCells(meta: ViewMeta) { const { rowId, colId } = meta; - const { facet } = this.spreadsheet; + const { facet, interaction } = this.spreadsheet; updateAllColHeaderCellState( colId, facet.getColCells(), InteractionStateName.HOVER, ); + const { rowHeader, colHeader } = interaction.getHoverHighlight(); - if (rowId) { + if (colHeader) { + updateAllColHeaderCellState( + colId, + facet.getColCells(), + InteractionStateName.HOVER, + ); + } + + if (rowHeader && rowId) { // update rowHeader cells const allRowHeaderCells = getActiveHoverRowColCells( rowId, @@ -88,8 +97,12 @@ export class HoverEvent extends BaseEvent implements BaseEventImplement { }; if (interactionOptions?.hoverHighlight) { - // highlight all the row and column cells which the cell belongs to - this.updateRowColCells(meta); + const { rowHeader, colHeader } = interaction.getHoverHighlight(); + + if (rowHeader || colHeader) { + // highlight all the row and column cells which the cell belongs to + this.updateRowColCells(meta); + } } const data = this.getCellData(meta, onlyShowCellText); @@ -155,7 +168,7 @@ export class HoverEvent extends BaseEvent implements BaseEventImplement { isTotals: meta.isTotals, hideSummary: true, onlyShowCellText: true, - enableFormat: this.spreadsheet.isPivotMode(), + enableFormat: true, }; const data = this.getCellData(meta, options.onlyShowCellText); @@ -213,11 +226,13 @@ export class HoverEvent extends BaseEvent implements BaseEventImplement { stateName: InteractionStateName.HOVER, }); - this.showEllipsisTooltip(event, cell); - if (interactionOptions?.hoverHighlight) { - // highlight all the row and column cells which the cell belongs to - this.updateRowColCells(meta); + const { rowHeader, colHeader } = interaction.getHoverHighlight(); + + if (rowHeader || colHeader) { + // highlight all the row and column cells which the cell belongs to + this.updateRowColCells(meta); + } } if (interactionOptions?.hoverFocus) { @@ -240,9 +255,7 @@ export class HoverEvent extends BaseEvent implements BaseEventImplement { public bindCornerCellHover() { this.spreadsheet.on(S2Event.CORNER_CELL_HOVER, (event: CanvasEvent) => { - const cell = this.spreadsheet.getCell(event.target); - - this.showEllipsisTooltip(event, cell); + this.handleHeaderHover(event); }); } } diff --git a/packages/s2-core/src/interaction/brush-selection/base-brush-selection.ts b/packages/s2-core/src/interaction/brush-selection/base-brush-selection.ts index 52979f5252..16d0a3f3d3 100644 --- a/packages/s2-core/src/interaction/brush-selection/base-brush-selection.ts +++ b/packages/s2-core/src/interaction/brush-selection/base-brush-selection.ts @@ -1,8 +1,8 @@ import { - type FederatedPointerEvent as CanvasEvent, + Rect, type DisplayObject, + type FederatedPointerEvent as CanvasEvent, type PointLike, - Rect, } from '@antv/g'; import { cloneDeep, isEmpty, isNil, map, throttle } from 'lodash'; import { ColCell, DataCell, RowCell } from '../../cell'; @@ -24,7 +24,6 @@ import type { BrushRange, OffsetConfig, OnUpdateCells, - Point, S2CellType, ViewMeta, } from '../../common/interface'; @@ -111,7 +110,7 @@ export class BaseBrushSelection } // 默认是 Data cell 的绘制区 - protected isPointInCanvas(point: Point): boolean { + protected isPointInCanvas(point: PointLike): boolean { const { height, width } = this.spreadsheet.facet.getCanvasSize(); const { minX, minY } = this.spreadsheet.facet.panelBBox; @@ -144,7 +143,10 @@ export class BaseBrushSelection this.mouseMoveDistanceFromCanvas = Math.abs(deltaVal); }; - public formatBrushPointForScroll = (delta: Point, isRowHeader = false) => { + public formatBrushPointForScroll = ( + delta: PointLike, + isRowHeader = false, + ) => { const { x, y } = delta; const { facet } = this.spreadsheet; const { minX, maxX } = isRowHeader ? facet.cornerBBox : facet.panelBBox; @@ -205,7 +207,7 @@ export class BaseBrushSelection let min = 0; const frozenRowRange = frozenInfo?.frozenRow?.range; - if (frozenRowRange) { + if (frozenRowRange?.[1]) { min = frozenRowRange[1] + 1; } @@ -216,7 +218,7 @@ export class BaseBrushSelection let max = facet.getCellRange().end; const frozenTrailingRowRange = frozenInfo?.frozenTrailingRow?.range; - if (frozenTrailingRowRange) { + if (frozenTrailingRowRange?.[0]) { max = frozenTrailingRowRange[0] - 1; } @@ -234,7 +236,7 @@ export class BaseBrushSelection let min = 0; const frozenColRange = frozenInfo?.frozenCol?.range; - if (frozenColRange) { + if (frozenColRange?.[1]) { min = frozenColRange[1] + 1; } @@ -245,7 +247,7 @@ export class BaseBrushSelection let max = facet.getColLeafNodes().length - 1; const frozenTrailingColRange = frozenInfo?.frozenTrailingCol?.range; - if (frozenTrailingColRange) { + if (frozenTrailingColRange?.[0]) { max = frozenTrailingColRange[0] - 1; } @@ -765,7 +767,7 @@ export class BaseBrushSelection } }; - public autoBrushScroll(point: Point, isRowHeader = false) { + public autoBrushScroll(point: PointLike, isRowHeader = false) { this.clearAutoScroll(); if (!this.isPointInCanvas(point)) { @@ -813,7 +815,7 @@ export class BaseBrushSelection protected updateSelectedCells() {} - protected getPrepareSelectMaskPosition(brushRange: BrushRange): Point { + protected getPrepareSelectMaskPosition(brushRange: BrushRange): PointLike { return { x: brushRange.start.x, y: brushRange.start.y, diff --git a/packages/s2-core/src/interaction/brush-selection/col-brush-selection.ts b/packages/s2-core/src/interaction/brush-selection/col-brush-selection.ts index 0d8f865ed7..3892a326c5 100644 --- a/packages/s2-core/src/interaction/brush-selection/col-brush-selection.ts +++ b/packages/s2-core/src/interaction/brush-selection/col-brush-selection.ts @@ -26,6 +26,10 @@ export class ColCellBrushSelection extends BaseBrushSelection { protected bindMouseDown() { this.spreadsheet.on(S2Event.COL_CELL_MOUSE_DOWN, (event) => { + if (!this.spreadsheet.interaction.getBrushSelection().colCell) { + return; + } + super.mouseDown(event); }); } @@ -96,16 +100,16 @@ export class ColCellBrushSelection extends BaseBrushSelection { ); } - // 最终刷选的cell + // 最终刷选的 cell protected updateSelectedCells() { const { interaction, facet } = this.spreadsheet; interaction.changeState({ cells: map(this.brushRangeCells, getCellMeta), - stateName: InteractionStateName.SELECTED, onUpdateCells: (root) => { root.updateCells(facet.getColCells()); }, + stateName: InteractionStateName.BRUSH_SELECTED, }); this.spreadsheet.emit( diff --git a/packages/s2-core/src/interaction/brush-selection/data-cell-brush-selection.ts b/packages/s2-core/src/interaction/brush-selection/data-cell-brush-selection.ts index 9b8d7adc98..d3ff5b802a 100644 --- a/packages/s2-core/src/interaction/brush-selection/data-cell-brush-selection.ts +++ b/packages/s2-core/src/interaction/brush-selection/data-cell-brush-selection.ts @@ -22,6 +22,10 @@ export class DataCellBrushSelection extends BaseBrushSelection { protected bindMouseDown() { this.spreadsheet.on(S2Event.DATA_CELL_MOUSE_DOWN, (event) => { + if (!this.spreadsheet.interaction.getBrushSelection().dataCell) { + return; + } + super.mouseDown(event); this.resetScrollDelta(); }); @@ -99,13 +103,14 @@ export class DataCellBrushSelection extends BaseBrushSelection { return metas; }; - // 最终刷选的cell + // 最终刷选的 cell protected updateSelectedCells() { const brushRange = this.getBrushRange(); const selectedCellMetas = this.getSelectedCellMetas(brushRange); this.spreadsheet.interaction.changeState({ cells: selectedCellMetas, + // TODO: 怕上层有直接消费 stateName, 暂时保留, 2.0 版本改成 InteractionStateName.BRUSH_SELECTED stateName: InteractionStateName.SELECTED, onUpdateCells: afterSelectDataCells, }); diff --git a/packages/s2-core/src/interaction/brush-selection/row-brush-selection.ts b/packages/s2-core/src/interaction/brush-selection/row-brush-selection.ts index 226cba7768..14b30eabe1 100644 --- a/packages/s2-core/src/interaction/brush-selection/row-brush-selection.ts +++ b/packages/s2-core/src/interaction/brush-selection/row-brush-selection.ts @@ -1,6 +1,5 @@ import { isNil, last, map } from 'lodash'; import { RowCell } from '../../cell'; - import { InterceptType, S2Event } from '../../common/constant'; import { InteractionBrushSelectionStage, @@ -13,9 +12,9 @@ import type { Point, ViewMeta, } from '../../common/interface'; +import type { BBox } from '../../engine'; import type { Node } from '../../facet/layout/node'; import { getCellMeta } from '../../utils/interaction/select-event'; -import type { BBox } from '../../engine'; import { BaseBrushSelection } from './base-brush-selection'; export class RowCellBrushSelection extends BaseBrushSelection { @@ -25,6 +24,10 @@ export class RowCellBrushSelection extends BaseBrushSelection { protected bindMouseDown() { this.spreadsheet.on(S2Event.ROW_CELL_MOUSE_DOWN, (event) => { + if (!this.spreadsheet.interaction.getBrushSelection().rowCell) { + return; + } + super.mouseDown(event); }); } @@ -102,7 +105,7 @@ export class RowCellBrushSelection extends BaseBrushSelection { this.spreadsheet.interaction.changeState({ cells: selectedCellMetas, - stateName: InteractionStateName.SELECTED, + stateName: InteractionStateName.BRUSH_SELECTED, onUpdateCells: this.onUpdateCells, }); @@ -125,16 +128,15 @@ export class RowCellBrushSelection extends BaseBrushSelection { return this.spreadsheet.facet.getRowNodes().filter(this.isInBrushRange); }; - private getScrollBrushRangeCells(nodes: Node[]) { + private getScrollBrushRangeCells(nodes: Node[]): RowCell[] { return nodes.map((node) => { - const visibleCell = this.getVisibleBrushRangeCells(node.id); + const visibleCell = this.getVisibleBrushRangeCells(node.id) as RowCell; if (visibleCell) { return visibleCell; } - // TODO: 先暂时不考虑自定义单元格的情况, next 分支把这些单元格 (包括自定义单元格) 都放在了 s2.options.rowCell 里 - return new RowCell(node, this.spreadsheet); + return this.spreadsheet.facet.rowHeader!.getCellInstance(node); }); } diff --git a/packages/s2-core/src/interaction/data-cell-multi-selection.ts b/packages/s2-core/src/interaction/data-cell-multi-selection.ts index b2640e2127..a7f03e197c 100644 --- a/packages/s2-core/src/interaction/data-cell-multi-selection.ts +++ b/packages/s2-core/src/interaction/data-cell-multi-selection.ts @@ -11,6 +11,7 @@ import type { CellMeta, S2CellType, ViewMeta } from '../common/interface'; import { getCellMeta, isMultiSelectionKey, + isMouseEventWithMeta, } from '../utils/interaction/select-event'; import { getCellsTooltipData } from '../utils/tooltip'; import { afterSelectDataCells } from '../utils/interaction/select-event'; @@ -26,6 +27,7 @@ export class DataCellMultiSelection this.bindKeyboardDown(); this.bindDataCellClick(); this.bindKeyboardUp(); + this.bindMouseMove(); } public reset() { @@ -53,6 +55,15 @@ export class DataCellMultiSelection }); } + private bindMouseMove() { + this.spreadsheet.on(S2Event.GLOBAL_MOUSE_MOVE, (event) => { + // 当快捷键被系统拦截后,按需补充调用一次 reset + if (this.isMultiSelection && !isMouseEventWithMeta(event)) { + this.reset(); + } + }); + } + private getSelectedCells(cell: S2CellType) { const id = cell.getMeta().id; const { interaction } = this.spreadsheet; @@ -85,6 +96,10 @@ export class DataCellMultiSelection if (isEmpty(selectedCells)) { interaction.clearState(); this.spreadsheet.hideTooltip(); + this.spreadsheet.emit( + S2Event.GLOBAL_SELECTED, + interaction.getActiveCells(), + ); return; } diff --git a/packages/s2-core/src/interaction/event-controller.ts b/packages/s2-core/src/interaction/event-controller.ts index 6482d607b4..83c6c34029 100644 --- a/packages/s2-core/src/interaction/event-controller.ts +++ b/packages/s2-core/src/interaction/event-controller.ts @@ -5,7 +5,6 @@ import { type Group, } from '@antv/g'; import { each, get, hasIn, isEmpty, isNil } from 'lodash'; -import { CustomImage } from '../engine'; import { GuiIcon } from '../common'; import { CellType, @@ -17,6 +16,7 @@ import { SHAPE_STYLE_MAP, } from '../common/constant'; import type { EmitterType, ResizeInfo } from '../common/interface'; +import { CustomImage } from '../engine'; import type { SpreadSheet } from '../sheet-type'; import { getSelectedData } from '../utils/export/copy'; import { keyEqualTo } from '../utils/export/method'; @@ -55,6 +55,8 @@ export class EventController { public isCanvasEffect = false; + public canvasMousemoveEvent: CanvasEvent; + constructor(spreadsheet: SpreadSheet) { this.spreadsheet = spreadsheet; this.bindEvents(); @@ -146,7 +148,7 @@ export class EventController { return; } - /* + /** * 全局有 mouseUp 和 click 事件, 当刷选完成后会同时触发, 当选中单元格后, 会同时触发 click 对应的 reset 事件 * 所以如果是 刷选过程中 引起的 click(mousedown + mouseup) 事件, 则不需要重置 */ @@ -175,8 +177,12 @@ export class EventController { return; } + interaction.reset(); this.spreadsheet.emit(S2Event.GLOBAL_RESET, event); - interaction?.reset(); + this.spreadsheet.emit( + S2Event.GLOBAL_SELECTED, + interaction.getActiveCells(), + ); } private isMouseEvent(event: Event): event is MouseEvent { @@ -184,6 +190,36 @@ export class EventController { return hasIn(event, 'clientX') && hasIn(event, 'clientY'); } + public isMatchElement(event: MouseEvent) { + const canvas = this.spreadsheet.getCanvasElement(); + const { target } = event; + + return ( + target === canvas || + target instanceof DisplayObject || + target instanceof Canvas + ); + } + + public isMatchPoint(event: MouseEvent) { + /** + * 这里不能使用 bounding rect 的 width 和 height, 高清适配后 canvas 实际宽高会变 + * 比如实际 400 * 300 => hd (800 * 600) + * 从视觉来看, 虽然点击了空白处, 但其实还是处于 放大后的 canvas 区域, 所以还需要额外判断一下坐标 + */ + const canvas = this.spreadsheet.getCanvasElement(); + const { width, height } = this.getContainerRect(); + const { x, y } = canvas.getBoundingClientRect() || {}; + const { clientX, clientY } = event; + + return ( + clientX <= x + width && + clientX >= x && + clientY <= y + height && + clientY >= y + ); + } + private isMouseOnTheCanvasContainer(event: Event) { if (this.isMouseEvent(event)) { const canvas = this.spreadsheet.getCanvasElement(); @@ -192,38 +228,27 @@ export class EventController { return false; } - const { x, y } = canvas.getBoundingClientRect() || {}; - - /* - * 这里不能使用 bounding rect 的 width 和 height, 高清适配后 canvas 实际宽高会变 - * 比如实际 400 * 300 => hd (800 * 600) - * 从视觉来看, 虽然点击了空白处, 但其实还是处于 放大后的 canvas 区域, 所以还需要额外判断一下坐标 - */ - const { width, height } = this.getContainerRect(); - - const { target: eventTarget, clientX, clientY } = event; - - return ( - (eventTarget === canvas || - eventTarget instanceof DisplayObject || - eventTarget instanceof Canvas) && - clientX <= x + width && - clientX >= x && - clientY <= y + height && - clientY >= y - ); + return this.isMatchElement(event) && this.isMatchPoint(event); } return false; } private getContainerRect() { - const { maxX, maxY } = this.spreadsheet.facet?.panelBBox || {}; - const { width, height } = this.spreadsheet.options; + const { facet, options } = this.spreadsheet; + const scrollBar = facet?.hRowScrollBar || facet?.hScrollBar; + const { maxX, maxY } = facet?.panelBBox || {}; + const { width = 0, height = 0 } = options; + + /** + * https://github.com/antvis/S2/issues/2376 + * 横向的滚动条在表格外 (Canvas 内), 点击滚动条(含滑道区域) 不应该重置交互 + */ + const trackHeight = scrollBar?.theme?.size || 0; return { - width: Math.min(width!, maxX), - height: Math.min(height!, maxY), + width: Math.min(width, maxX), + height: Math.min(height, maxY + trackHeight), }; } @@ -357,6 +382,8 @@ export class EventController { }; private onCanvasMousemove = (event: CanvasEvent) => { + this.canvasMousemoveEvent = event; + if (this.isResizeArea(event)) { this.activeResizeArea(event); this.spreadsheet.emit( diff --git a/packages/s2-core/src/interaction/range-selection.ts b/packages/s2-core/src/interaction/range-selection.ts index aa44170782..4aecb085cb 100644 --- a/packages/s2-core/src/interaction/range-selection.ts +++ b/packages/s2-core/src/interaction/range-selection.ts @@ -1,5 +1,5 @@ -import type { FederatedPointerEvent as Event } from '@antv/g'; -import { inRange, isNil, range } from 'lodash'; +import { inRange, isEmpty, isNil, range } from 'lodash'; +import type { FederatedPointerEvent } from '@antv/g'; import { DataCell } from '../cell'; import { CellType, @@ -22,6 +22,7 @@ export class RangeSelection extends BaseEvent implements BaseEventImplement { this.bindDataCellClick(); this.bindColCellClick(); this.bindKeyboardUp(); + this.bindMouseMove(); } public reset() { @@ -49,21 +50,30 @@ export class RangeSelection extends BaseEvent implements BaseEventImplement { }); } + private bindMouseMove() { + // 当快捷键被系统拦截后,按需补充调用一次 reset + this.spreadsheet.on(S2Event.GLOBAL_MOUSE_MOVE, (event) => { + if (this.isRangeSelection && !event.shiftKey) { + this.reset(); + } + }); + } + private bindColCellClick() { if (this.spreadsheet.isTableMode()) { // series-number click - this.spreadsheet.on(S2Event.ROW_CELL_CLICK, (event: Event) => { + this.spreadsheet.on(S2Event.ROW_CELL_CLICK, (event) => { this.handleColClick(event); }); } - this.spreadsheet.on(S2Event.COL_CELL_CLICK, (event: Event) => { + this.spreadsheet.on(S2Event.COL_CELL_CLICK, (event) => { this.handleColClick(event); }); } private bindDataCellClick() { - this.spreadsheet.on(S2Event.DATA_CELL_CLICK, (event: Event) => { + this.spreadsheet.on(S2Event.DATA_CELL_CLICK, (event) => { event.stopPropagation(); const cell = this.spreadsheet.getCell(event.target) as DataCell; const meta = cell.getMeta(); @@ -123,7 +133,7 @@ export class RangeSelection extends BaseEvent implements BaseEventImplement { }); } - private handleColClick = (event: Event) => { + private handleColClick = (event: FederatedPointerEvent) => { event.stopPropagation(); const { interaction, facet } = this.spreadsheet; const cell = this.spreadsheet.getCell(event.target); @@ -183,6 +193,10 @@ export class RangeSelection extends BaseEvent implements BaseEventImplement { stateName: InteractionStateName.SELECTED, }); } else { + if (isEmpty(interaction.getCells())) { + interaction.removeIntercepts([InterceptType.HOVER]); + } + this.spreadsheet.store.set('lastClickedCell', cell); } diff --git a/packages/s2-core/src/interaction/root.ts b/packages/s2-core/src/interaction/root.ts index 7fa9e23ef8..7c34578467 100644 --- a/packages/s2-core/src/interaction/root.ts +++ b/packages/s2-core/src/interaction/root.ts @@ -2,23 +2,23 @@ import { concat, find, forEach, isBoolean, isEmpty, isNil, map } from 'lodash'; import type { MergedCell } from '../cell'; import { CellType, + INTERACTION_STATE_INFO_KEY, InteractionName, InteractionStateName, - INTERACTION_STATE_INFO_KEY, InterceptType, S2Event, } from '../common/constant'; import type { - BrushSelection, + BrushSelectionOptions, BrushSelectionInfo, CellMeta, CustomInteraction, + InteractionCellHighlightOptions, InteractionStateInfo, Intercept, MergedCellInfo, S2CellType, SelectHeaderCellInfo, - InteractionCellSelectedHighlightOptions, } from '../common/interface'; import type { Node } from '../facet/layout/node'; import type { SpreadSheet } from '../sheet-type'; @@ -36,14 +36,14 @@ import { } from './base-interaction/click'; import { CornerCellClick } from './base-interaction/click/corner-cell-click'; import { HoverEvent } from './base-interaction/hover'; -import { EventController } from './event-controller'; -import { RangeSelection } from './range-selection'; -import { SelectedCellMove } from './selected-cell-move'; -import { DataCellBrushSelection } from './brush-selection/data-cell-brush-selection'; import { ColCellBrushSelection } from './brush-selection/col-brush-selection'; +import { DataCellBrushSelection } from './brush-selection/data-cell-brush-selection'; import { RowCellBrushSelection } from './brush-selection/row-brush-selection'; import { DataCellMultiSelection } from './data-cell-multi-selection'; +import { EventController } from './event-controller'; +import { RangeSelection } from './range-selection'; import { RowColumnResize } from './row-column-resize'; +import { SelectedCellMove } from './selected-cell-move'; export class RootInteraction { public spreadsheet: SpreadSheet; @@ -146,7 +146,10 @@ export class RootInteraction { } public isSelectedState() { - return this.isStateOf(InteractionStateName.SELECTED); + return ( + this.isStateOf(InteractionStateName.SELECTED) || + this.isStateOf(InteractionStateName.BRUSH_SELECTED) + ); } public isAllSelectedState() { @@ -184,7 +187,7 @@ export class RootInteraction { // 获取 cells 中在可视区域内的实例列表 public getActiveCells(): S2CellType[] { const ids = this.getCells().map((item) => item.id); - const allCells = this.spreadsheet.facet.getCells(); + const allCells = this.spreadsheet.facet?.getCells(); // 这里的顺序要以 ids 中的顺序为准,代表点击 cell 的顺序 return map(ids, (id) => @@ -325,7 +328,7 @@ export class RootInteraction { } private getBrushSelectionInfo( - brushSelection?: boolean | BrushSelection, + brushSelection?: boolean | BrushSelectionOptions, ): BrushSelectionInfo { if (isBoolean(brushSelection)) { return { @@ -542,7 +545,7 @@ export class RootInteraction { return this.hoverTimer; } - public getSelectedCellHighlight(): InteractionCellSelectedHighlightOptions { + public getSelectedCellHighlight(): InteractionCellHighlightOptions { const { selectedCellHighlight } = this.spreadsheet.options.interaction!; if (isBoolean(selectedCellHighlight)) { @@ -559,7 +562,7 @@ export class RootInteraction { colHeader = false, currentRow = false, currentCol = false, - } = (selectedCellHighlight as unknown as InteractionCellSelectedHighlightOptions) ?? + } = (selectedCellHighlight as unknown as InteractionCellHighlightOptions) ?? {}; return { @@ -569,4 +572,59 @@ export class RootInteraction { currentCol, }; } + + public getHoverAfterScroll(): boolean { + return this.spreadsheet.options.interaction!.hoverAfterScroll!; + } + + public getHoverHighlight(): InteractionCellHighlightOptions { + const { hoverHighlight } = this.spreadsheet.options.interaction!; + + if (isBoolean(hoverHighlight)) { + return { + rowHeader: hoverHighlight, + colHeader: hoverHighlight, + currentRow: hoverHighlight, + currentCol: hoverHighlight, + }; + } + + const { + rowHeader = false, + colHeader = false, + currentRow = false, + currentCol = false, + } = hoverHighlight ?? ({} as InteractionCellHighlightOptions); + + return { + rowHeader, + colHeader, + currentRow, + currentCol, + }; + } + + public getBrushSelection(): BrushSelectionOptions { + const { brushSelection } = this.spreadsheet.options.interaction!; + + if (isBoolean(brushSelection)) { + return { + dataCell: brushSelection, + rowCell: brushSelection, + colCell: brushSelection, + }; + } + + const { + dataCell = false, + rowCell = false, + colCell = false, + } = brushSelection ?? ({} as BrushSelectionOptions); + + return { + dataCell, + rowCell, + colCell, + }; + } } diff --git a/packages/s2-core/src/interaction/row-column-resize.ts b/packages/s2-core/src/interaction/row-column-resize.ts index a73e598085..f2145bbc64 100644 --- a/packages/s2-core/src/interaction/row-column-resize.ts +++ b/packages/s2-core/src/interaction/row-column-resize.ts @@ -1,4 +1,10 @@ -import { Group, type DisplayObject, type PathStyleProps, Path } from '@antv/g'; +import { + FederatedPointerEvent, + Group, + Path, + type DisplayObject, + type PathStyleProps, +} from '@antv/g'; import { clone, isEmpty, throttle } from 'lodash'; import type { ResizeInteractionOptions, @@ -23,6 +29,7 @@ import type { ResizePosition, } from '../common/interface/resize'; import { CustomRect } from '../engine'; +import { floor } from '../utils/math'; import { BaseEvent, type BaseEventImplement } from './base-interaction'; export class RowColumnResize extends BaseEvent implements BaseEventImplement { @@ -124,10 +131,7 @@ export class RowColumnResize extends BaseEvent implements BaseEventImplement { } private updateResizeGuideLinePosition( - offset: { - x: number; - y: number; - }, + event: FederatedPointerEvent, resizeInfo: ResizeInfo, ) { const resizeShapes = this.getResizeShapes(); @@ -160,7 +164,8 @@ export class RowColumnResize extends BaseEvent implements BaseEventImplement { ['M', offsetX + width - halfSize, offsetY], ['L', offsetX + width - halfSize, guideLineMaxHeight], ]); - this.resizeStartPosition.offsetX = offset.x; + this.resizeStartPosition.offsetX = event.offsetX; + this.resizeStartPosition.clientX = event.clientX; return; } @@ -173,7 +178,8 @@ export class RowColumnResize extends BaseEvent implements BaseEventImplement { ['M', offsetX, offsetY + height - halfSize], ['L', guideLineMaxWidth, offsetY + height - halfSize], ]); - this.resizeStartPosition.offsetY = offset.y; + this.resizeStartPosition.offsetY = event.offsetY; + this.resizeStartPosition.clientY = event.clientY; } private bindMouseDown() { @@ -192,13 +198,7 @@ export class RowColumnResize extends BaseEvent implements BaseEventImplement { this.spreadsheet.interaction.addIntercepts([InterceptType.RESIZE]); this.setResizeTarget(shape); this.showResizeGroup(); - this.updateResizeGuideLinePosition( - { - x: event.offsetX, - y: event.offsetY, - }, - resizeInfo, - ); + this.updateResizeGuideLinePosition(event, resizeInfo); }); } @@ -574,14 +574,14 @@ export class RowColumnResize extends BaseEvent implements BaseEventImplement { ); const { start, end } = this.getResizeGuideLinePosition(); - const resizedWidth = Math.floor( + const resizedWidth = floor( end.x - start.x + (defaultResizeInfo.type === ResizeDirectionType.Horizontal ? defaultResizeInfo.size : 0), ); - const resizedHeight = Math.floor( + const resizedHeight = floor( end.y - start.y + (defaultResizeInfo.type === ResizeDirectionType.Vertical diff --git a/packages/s2-core/src/interaction/selected-cell-move.ts b/packages/s2-core/src/interaction/selected-cell-move.ts index 71f7eed0fe..6f9d956406 100644 --- a/packages/s2-core/src/interaction/selected-cell-move.ts +++ b/packages/s2-core/src/interaction/selected-cell-move.ts @@ -6,6 +6,7 @@ import { calculateInViewIndexes } from '../facet/utils'; import type { SpreadSheet } from '../sheet-type'; import { getDataCellId } from '../utils'; import { getRangeIndex, selectCells } from '../utils/interaction/select-event'; +import { floor } from '../utils/math'; import { BaseEvent, type BaseEventImplement } from './base-interaction'; const SelectedCellMoveMap = [ @@ -265,22 +266,22 @@ export class SelectedCellMove extends BaseEvent implements BaseEventImplement { const { viewportHeight: height, viewportWidth: width } = facet.panelBBox; const splitLineStyle = get(spreadsheet, 'theme.splitLine'); const frozenColWidth = frozenColGroup - ? Math.floor( + ? floor( frozenColGroup.getBBox().width - splitLineStyle.verticalBorderWidth / 2, ) : 0; const frozenTrailingColWidth = frozenTrailingColGroup - ? Math.floor(frozenTrailingColGroup.getBBox().width) + ? floor(frozenTrailingColGroup.getBBox().width) : 0; const frozenRowHeight = frozenRowGroup - ? Math.floor( + ? floor( frozenRowGroup.getBBox().height - splitLineStyle.horizontalBorderWidth / 2, ) : 0; const frozenTrailingRowHeight = frozenTrailingRowGroup - ? Math.floor(frozenTrailingRowGroup.getBBox().height) + ? floor(frozenTrailingRowGroup.getBBox().height) : 0; const indexes = calculateInViewIndexes({ diff --git a/packages/s2-core/src/sheet-type/pivot-sheet.ts b/packages/s2-core/src/sheet-type/pivot-sheet.ts index 7783c8a422..2cce6f7bd2 100644 --- a/packages/s2-core/src/sheet-type/pivot-sheet.ts +++ b/packages/s2-core/src/sheet-type/pivot-sheet.ts @@ -37,6 +37,13 @@ export class PivotSheet extends SpreadSheet { return false; } + public getContentHeight() { + return this.facet.getContentHeight(); + } + + /** + * Check if is pivot mode + */ public isPivotMode(): boolean { return true; } @@ -65,8 +72,9 @@ export class PivotSheet extends SpreadSheet { public clearDrillDownData(rowNodeId?: string, preventRender?: boolean) { if (this.dataSet instanceof PivotDataSet) { - this.dataSet.clearDrillDownData(rowNodeId); - if (!preventRender) { + const cleaned = this.dataSet.clearDrillDownData(rowNodeId); + + if (cleaned && !preventRender) { // 重置当前交互 this.interaction.reset(); this.render(false); diff --git a/packages/s2-core/src/sheet-type/spread-sheet.ts b/packages/s2-core/src/sheet-type/spread-sheet.ts index ac13d77501..1585c1e20d 100644 --- a/packages/s2-core/src/sheet-type/spread-sheet.ts +++ b/packages/s2-core/src/sheet-type/spread-sheet.ts @@ -23,6 +23,7 @@ import { import { BaseCell } from '../cell'; import { InterceptType, + LayoutWidthType, S2Event, getTooltipOperatorSortMenus, getTooltipOperatorTableSortMenus, @@ -38,7 +39,6 @@ import type { Fields, InteractionOptions, InternalFullyTheme, - LayoutWidthType, OffsetConfig, Pagination, S2CellType, @@ -249,10 +249,12 @@ export abstract class SpreadSheet extends EE { } private initInteraction() { + this.interaction?.destroy?.(); this.interaction = new RootInteraction(this); } private initTooltip() { + this.tooltip?.destroy?.(); this.tooltip = this.renderTooltip(); if (!(this.tooltip instanceof BaseTooltip)) { // eslint-disable-next-line no-console @@ -357,9 +359,14 @@ export abstract class SpreadSheet extends EE { * Group sort params kept in {@see store} and * Priority: group sort > advanced sort * @param dataCfg - * @param reset reset: true, 直接使用用户传入的 DataCfg ,不再与上次数据进行合并 + * @param reset 是否使用传入的 dataCfg 重置已保存的 dataCfg + * + * @example setDataCfg(dataCfg, true) 直接使用传入的 DataCfg,不再与上次数据进行合并 */ - public setDataCfg(dataCfg: S2DataConfig, reset?: boolean) { + public setDataCfg( + dataCfg: T extends true ? S2DataConfig : Partial, + reset?: T, + ) { this.store.set('originalDataCfg', dataCfg); if (reset) { this.dataCfg = getSafetyDataConfig(dataCfg); @@ -373,12 +380,17 @@ export abstract class SpreadSheet extends EE { public setOptions(options: Partial, reset?: boolean) { this.hideTooltip(); + if (reset) { this.options = getSafetyOptions(options); } else { this.options = customMerge(this.options, options); } + if (reset || options.tooltip?.render) { + this.initTooltip(); + } + this.registerIcons(); } @@ -572,6 +584,10 @@ export abstract class SpreadSheet extends EE { ); } + protected isCellType(cell?: CellEventTarget) { + return cell instanceof BaseCell; + } + // 获取当前 cell 实例 public getCell( target: CellEventTarget, @@ -580,8 +596,8 @@ export abstract class SpreadSheet extends EE { // 一直索引到 g 顶层的 canvas 来检查是否在指定的cell中 while (parent && !(parent instanceof Canvas)) { - if (parent instanceof BaseCell) { - // 在单元格中,返回 true + if (this.isCellType(parent)) { + // 在单元格中,返回true return parent as T; } @@ -618,12 +634,12 @@ export abstract class SpreadSheet extends EE { : false; return { + grandTotalsLabel: i18n('总计'), + subTotalsLabel: i18n('小计'), + grandTotalsGroupDimensions: [], + subTotalsGroupDimensions: [], + ...totalConfig, showSubTotals, - showGrandTotals: totalConfig.showGrandTotals, - reverseGrandTotalsLayout: totalConfig.reverseGrandTotalsLayout, - reverseSubTotalsLayout: totalConfig.reverseSubTotalsLayout, - grandTotalsLabel: totalConfig.grandTotalsLabel || i18n('总计'), - subTotalsLabel: totalConfig.subTotalsLabel || i18n('小计'), }; } diff --git a/packages/s2-core/src/theme/index.ts b/packages/s2-core/src/theme/index.ts index 31ded99ace..84ce9d1088 100644 --- a/packages/s2-core/src/theme/index.ts +++ b/packages/s2-core/src/theme/index.ts @@ -1,5 +1,9 @@ /* eslint-disable max-lines-per-function */ -import { FONT_FAMILY, INTERVAL_BAR_HEIGHT } from '../common/constant'; +import { + FONT_FAMILY, + INTERVAL_BAR_HEIGHT, + LayoutWidthType, +} from '../common/constant'; import type { DefaultCellTheme, S2Theme, @@ -24,20 +28,27 @@ export const getTheme = ( } = themeCfg?.palette || getPalette(themeCfg?.name); const isTable = themeCfg?.spreadsheet?.isTableMode(); + const isCompactMode = + themeCfg?.spreadsheet?.getLayoutWidthType() === LayoutWidthType.Compact; const boldTextDefaultFontWeight = isWindows() ? 'bold' : 700; - const getHeaderCellTextOverflow = (): TextTheme => ({ - wordWrap: true, - maxLines: 1, - textOverflow: 'ellipsis', - }); + const getHeaderCellTextOverflow = (): TextTheme => { + return { + wordWrap: true, + maxLines: 1, + textOverflow: 'ellipsis', + }; + }; - const getDataCellTextOverflow = (): TextTheme => ({ - wordWrap: true, - // 数值单元格不建议文字换行, 通常是展示数值, 会有歧义 (明细表除外, 自行覆盖主题配置) - maxLines: 1, - textOverflow: 'ellipsis', - }); + const getDataCellTextOverflow = (): TextTheme => { + return { + // 紧凑模式下文本内容自适应, 不显示省略号 + wordWrap: !isCompactMode, + // 数值单元格不建议文字换行, 通常是展示数值, 会有歧义 (明细表除外, 自行覆盖主题配置) + maxLines: 1, + textOverflow: isCompactMode ? '' : 'ellipsis', + }; + }; const getDataCell = (): DefaultCellTheme => ({ bolderText: { @@ -187,7 +198,7 @@ export const getTheme = ( return { // ------------- Headers ------------------- cornerCell: { - bolderText: { + text: { fontFamily: FONT_FAMILY, fontSize: 12, fontWeight: boldTextDefaultFontWeight, @@ -197,13 +208,13 @@ export const getTheme = ( textBaseline: 'middle', ...getHeaderCellTextOverflow(), }, - text: { + bolderText: { fontFamily: FONT_FAMILY, fontSize: 12, fontWeight: boldTextDefaultFontWeight, fill: basicColors[0], opacity: 1, - textAlign: 'right', + textAlign: isTable ? 'center' : 'right', textBaseline: 'middle', ...getHeaderCellTextOverflow(), }, @@ -219,6 +230,8 @@ export const getTheme = ( // ----------- border width -------------- horizontalBorderWidth: 1, verticalBorderWidth: 1, + // -------------- border dash ----------------- + borderDash: [], // -------------- layout ----------------- padding: { top: 4, @@ -296,6 +309,8 @@ export const getTheme = ( // ----------- bottom border width -------------- horizontalBorderWidth: 1, verticalBorderWidth: 1, + // -------------- border dash ----------------- + borderDash: [], // -------------- layout ----------------- padding: { top: 4, @@ -395,6 +410,8 @@ export const getTheme = ( // ----------- border width -------------- horizontalBorderWidth: 1, verticalBorderWidth: 1, + // -------------- border dash ----------------- + borderDash: [], // -------------- layout ----------------- padding: { top: 4, @@ -498,6 +515,7 @@ export const getTheme = ( left: 'rgba(0,0,0,0.1)', right: 'rgba(0,0,0,0)', }, + borderDash: [], }, // ------------- prepareSelectMask ----------------- prepareSelectMask: { diff --git a/packages/s2-core/src/utils/cell/cell.ts b/packages/s2-core/src/utils/cell/cell.ts index 171de172bb..8b04d6a7f9 100644 --- a/packages/s2-core/src/utils/cell/cell.ts +++ b/packages/s2-core/src/utils/cell/cell.ts @@ -1,3 +1,5 @@ +import type { LineStyleProps } from '@antv/g'; +import { isEmpty } from 'lodash'; import type { SimpleBBox } from '../../engine'; import { CellClipBox, @@ -195,9 +197,15 @@ export const getBorderPositionAndStyle = ( verticalBorderWidth = 0, verticalBorderColor, verticalBorderColorOpacity, + borderDash, } = style; - const borderStyle = [ + // TODO: 如果是空数组 G 底层绘制会报错 + const lineDash: LineStyleProps['lineDash'] = isEmpty(borderDash) + ? '' + : borderDash; + + const borderStyle: Partial = [ CellBorderPosition.TOP, CellBorderPosition.BOTTOM, ].includes(position) @@ -205,11 +213,13 @@ export const getBorderPositionAndStyle = ( lineWidth: horizontalBorderWidth, stroke: horizontalBorderColor, strokeOpacity: horizontalBorderColorOpacity, + lineDash, } : { lineWidth: verticalBorderWidth, stroke: verticalBorderColor, strokeOpacity: verticalBorderColorOpacity, + lineDash, }; let x1 = 0; diff --git a/packages/s2-core/src/utils/data-set-operate.ts b/packages/s2-core/src/utils/data-set-operate.ts index 7717d76fad..753dbe2f4a 100644 --- a/packages/s2-core/src/utils/data-set-operate.ts +++ b/packages/s2-core/src/utils/data-set-operate.ts @@ -1,27 +1,16 @@ -import { every, flatMap, get, has, isArray, keys } from 'lodash'; +import { isArray, flattenDeep } from 'lodash'; import { - DataSelectType, - DEFAULT_TOTAL_SELECTIONS, -} from '../common/constant/total'; -import { TOTAL_VALUE } from '../common/constant/basic'; -import type { - RawData, - Totals, - TotalsStatus, - FlattingIndexesData, - CustomHeaderFields, - CalcTotals, -} from '../common/interface'; -import type { TotalSelectionsOfMultiData } from '../data-set/interface'; -import { customMerge } from './merge'; -import { filterExtraDimension } from './dataset/pivot-data-set'; + EMPTY_EXTRA_FIELD_PLACEHOLDER, + TOTAL_VALUE, +} from '../common/constant/field'; +import type { CalcTotals, Totals, TotalsStatus } from '../common/interface'; export const getListBySorted = ( list: string[], sorted: string[], mapValue?: (val: string) => string, -) => - list.sort((a, b) => { +) => { + return list.sort((a, b) => { if (mapValue) { a = mapValue(a); b = mapValue(b); @@ -44,37 +33,34 @@ export const getListBySorted = ( return ia - ib; }); +}; -export const filterTotal = (values: string[] = []) => - values.filter((v) => v !== TOTAL_VALUE); - -export const getFieldKeysByDimensionValues = ( - dimensionValues: string[] | undefined[], - dimensions: CustomHeaderFields, +export const filterOutDetail = (values: string[] = []) => { + return values.filter( + (v) => v !== TOTAL_VALUE && v !== EMPTY_EXTRA_FIELD_PLACEHOLDER, + ); +}; +export const customFlattenDeep = ( + data: Record[] | Record, ) => { - const result: string[] = []; - - dimensionValues?.forEach((item, index) => { - if (item === undefined) { - if (dimensions[index]) { - result.push(dimensions[index] as string); - } - } - }); + if (!isArray(data)) { + return [data]; + } - return result; + return flattenDeep(data); }; /** * arr1包含arr2,将arr2排到最后 * */ -export const sortByItems = (arr1: string[], arr2: string[]) => - arr1?.filter((item) => !arr2?.includes(item))?.concat(arr2); +export const sortByItems = (arr1: string[], arr2: string[]) => { + return arr1?.filter((item) => !arr2?.includes(item))?.concat(arr2); +}; export function getAggregationAndCalcFuncByQuery( totalsStatus: TotalsStatus, - totalsOptions?: Totals, + totalsOptions?: Totals | null, ) { const { isRowTotal, isRowSubTotal, isColTotal, isColSubTotal } = totalsStatus; const { row, col } = totalsOptions || {}; @@ -104,88 +90,3 @@ export function getAggregationAndCalcFuncByQuery( getCalcTotals(rowCalcSubTotals, isRowSubTotal) ); } - -/** - * 判断是普通单元格数据还是总计或小计 - * @param ids - * @param data - * @returns - */ -export function isTotalData(ids: string[], data: RawData): boolean { - return !every(filterExtraDimension(ids), (id) => has(data, id)); -} - -export function getTotalSelection(totals = {} as TotalSelectionsOfMultiData) { - return customMerge( - DEFAULT_TOTAL_SELECTIONS, - totals, - ); -} - -export function flattenIndexesData( - data: FlattingIndexesData, - selectType: DataSelectType = DataSelectType.All, -) { - if (!isArray(data)) { - return [data]; - } - - return flatMap(data, (dimensionData) => { - if (!isArray(dimensionData)) { - return [dimensionData]; - } - - // 数组的第 0 项是总计/小计专位,从第 1 项开始是明细数据 - const startIdx = selectType === DataSelectType.DetailOnly ? 1 : 0; - const length = selectType === DataSelectType.TotalOnly ? 1 : undefined; - - return dimensionData.slice(startIdx, length).filter(Boolean); - }); -} - -export const flatten = (data: Record[] | Record) => { - const result = []; - - if (Array.isArray(data)) { - // 总计小计在数组里面,以 undefine作为key, 直接forEach的话会漏掉总计小计 - const containsTotal = 'undefined' in data; - const itemLength = data.length + (containsTotal ? 1 : 0); - - let i = 0; - - while (i < itemLength) { - // eslint-disable-next-line dot-notation - const current = - i === data.length ? data['undefined' as unknown as number] : data[i]; - - i++; - - if (current && 'undefined' in current) { - keys(current).forEach((ki) => { - result.push(current[ki]); - }); - } else if (Array.isArray(current)) { - result.push(...current); - } else { - result.push(current); - } - } - } else { - result.push(data); - } - - return result; -}; - -export const flattenDeep = (data: Record[] | Record) => - keys(data)?.reduce((pre, next) => { - const item = get(data, next); - - if (Array.isArray(item)) { - pre = pre.concat(flattenDeep(item)); - } else { - pre?.push(item as unknown as never); - } - - return pre; - }, []); diff --git a/packages/s2-core/src/utils/dataset/pivot-data-set.ts b/packages/s2-core/src/utils/dataset/pivot-data-set.ts index f81c264476..013c4654df 100644 --- a/packages/s2-core/src/utils/dataset/pivot-data-set.ts +++ b/packages/s2-core/src/utils/dataset/pivot-data-set.ts @@ -1,52 +1,129 @@ -import { forEach, has, intersection, last, set, find, get } from 'lodash'; -import type { CellData } from '../../data-set/cell-data'; import { + compact, + find, + flatMap, + forEach, + get, + indexOf, + intersection, + isArray, + isEmpty, + isNull, + last, + set, + sortBy, +} from 'lodash'; +import { + EMPTY_EXTRA_FIELD_PLACEHOLDER, EXTRA_FIELD, + MULTI_VALUE, NODE_ID_SEPARATOR, + QueryDataType, ROOT_NODE_ID, TOTAL_VALUE, - MULTI_VALUE, } from '../../common/constant'; -import type { BaseFields, RawData } from '../../common/interface'; +import type { Meta } from '../../common/interface/basic'; +import type { PickEssential } from '../../common/interface/utils'; +import type { CellData } from '../../data-set/cell-data'; import type { DataPath, DataPathParams, + FlattingIndexesData, + OnFirstCreateParams, PivotMeta, + PivotMetaValue, SortedDimensionValues, + TotalStatus, } from '../../data-set/interface'; -import type { Meta } from '../../common/interface/basic'; +import type { Node } from '../../facet/layout/node'; +import type { RawData } from '../../common'; export function filterExtraDimension(dimensions: string[] = []) { return dimensions.filter((d) => d !== EXTRA_FIELD); } -export function transformDimensionsValues( - record: RawData, - dimensions: string[], - placeholder: string = TOTAL_VALUE, -): string[] { - return filterExtraDimension(dimensions).map((dimension) => - !has(record, dimension) ? placeholder : String(record[dimension]), - ); -} - -export function shouldQueryMultiData(pathValue: string | number) { +export function isMultiValue(pathValue: string | number | undefined) { return pathValue === MULTI_VALUE; } /** - * Get dimensions without path pre - * dimensions: ['辽宁省[&]芜湖市[&]家具[&]椅子'] - * return ['椅子'] + * Transform from origin single data to correct dimension values + * data: { + * price: 16, + * province: '辽宁省', + * city: '芜湖市', + * category: '家具', + * subCategory: '椅子', + * } + * dimensions: [province, city] + * return [辽宁省, 芜湖市] * + * @param record * @param dimensions */ -export function getDimensionsWithoutPathPre(dimensions: string[]) { - return dimensions.map((item) => { - const splitArr = item?.split(NODE_ID_SEPARATOR); - return splitArr?.[splitArr?.length! - 1] || item; - }); +export function transformDimensionsValues( + record: RawData = {}, + dimensions: string[] = [], + placeholder = TOTAL_VALUE, +): string[] { + return dimensions.reduce((res: string[], dimension: string) => { + const value = record[dimension]; + + if (!(dimension in record)) { + res.push(placeholder); + } else { + res.push(String(value)); + } + + return res; + }, []); +} + +export function getExistValues(data: RawData, values: string[]) { + const result = values.filter((v) => v in data); + + if (isEmpty(result)) { + result.push(EMPTY_EXTRA_FIELD_PLACEHOLDER); + } + + return result; +} + +export function transformDimensionsValuesWithExtraFields( + record: RawData = {}, + dimensions: string[] = [], + values: string[] | null, +) { + const result = []; + + function transform(data: RawData, fields: string[], valueField?: string) { + return fields.reduce((res: string[], dimension: string) => { + const value = data[dimension]; + + if (!(dimension in data)) { + if (dimension === EXTRA_FIELD && valueField) { + res.push(valueField); + } else { + res.push(TOTAL_VALUE); + } + } else { + res.push(String(value)); + } + + return res; + }, []); + } + + if (values) { + values.forEach((value) => { + result.push(transform(record, dimensions, value)); + }); + } else { + result.push(transform(record, dimensions)); + } + + return result; } /** @@ -85,6 +162,13 @@ export function getDimensionsWithParentPath( ?.filter(Boolean); } +export function getDataPathPrefix(rowFields: string[], colFields: string[]) { + return rowFields + .concat(colFields) + .filter((i) => i !== EXTRA_FIELD) + .join(NODE_ID_SEPARATOR); +} + /** * Transform a single data to path * { @@ -106,73 +190,75 @@ export function getDimensionsWithParentPath( */ export function getDataPath(params: DataPathParams): DataPath { const { - rows, - columns, - values, rowDimensionValues, colDimensionValues, - shouldCreateOrUpdate, - onCreate, + isFirstCreate, + onFirstCreate, + rowFields, + colFields, rowPivotMeta, colPivotMeta, + prefix = '', } = params; - // TODO: extract as a layout hook - const appendValues = () => { - const map = new Map(); - - values?.forEach((v, idx) => { - map.set(v, { - level: idx, - children: new Map(), - }); - }); - - return map; - }; - - /* - * 根据行、列维度值生成对应的 path路径,始终将总计小计置于第 0 位,明细数据从第 1 位开始,有两个情况: - * 如果是汇总格子: path = [0,0,0,0] path中会存在 0的值(这里在indexesData里面会映射) - * 如果是明细格子: path = [1,1,1] 数字均不为0 - */ + // 根据行、列维度值生成对应的 path 路径,始终将总计小计置于第 0 位,明细数据从第 1 位开始,有两个情况: + // 如果是汇总格子: path = [0, 0, 0, 0] path 中会存在 0 的值 + // 如果是明细格子: path = [1, 1, 1] 数字均不为 0 const getPath = ( dimensions: string[], dimensionValues: string[], pivotMeta: PivotMeta, - ): DataPath => { + careRepeated: boolean, + ) => { let currentMeta = pivotMeta; const path: DataPath = []; for (let i = 0; i < dimensionValues.length; i++) { const value = dimensionValues[i]; - const isTotal = value === TOTAL_VALUE; - if (shouldCreateOrUpdate && !currentMeta.has(value)) { + if (isFirstCreate && currentMeta && !currentMeta?.has(value)) { + const currentDimensions = dimensionValues + .slice(0, i + 1) + .map((it) => String(it)); + const id = currentDimensions.join(NODE_ID_SEPARATOR); + + const isTotal = value === TOTAL_VALUE; + + let level; + + if (isTotal) { + level = 0; + } else if (currentMeta.has(TOTAL_VALUE)) { + level = currentMeta.size; + } else { + level = currentMeta.size + 1; + } + currentMeta.set(value, { - level: isTotal ? 0 : currentMeta.size + 1, - childField: dimensions?.[i + 1], - children: - dimensions?.[i + 1] === EXTRA_FIELD ? appendValues() : new Map(), + id, + dimensions: currentDimensions, + value, + level, + children: new Map(), }); - onCreate?.({ + onFirstCreate?.({ dimension: dimensions?.[i], - dimensionPath: dimensionValues.slice(0, i + 1), + dimensionPath: id, + careRepeated, }); } - const meta = currentMeta.get(value); + const meta = currentMeta?.get(value); - // 只出现在 getCellMultiData 中, 使用特殊的 value 指明当前复合选择 - if (value === MULTI_VALUE) { - path.push(value); - } else { - path.push(meta?.level!); - } + path.push(isMultiValue(value) ? value : meta?.level); if (meta) { - if (shouldCreateOrUpdate && meta.childField !== dimensions?.[i + 1]) { - meta.childField = dimensions?.[i + 1]; + const childDimension = dimensions?.[i + 1]; + + if (isFirstCreate && meta.childField !== childDimension) { + // mark the child field + // NOTE: should take more care when reset meta.childField to undefined, the meta info is shared with brother nodes. + meta.childField = childDimension; } currentMeta = meta?.children; @@ -182,38 +268,47 @@ export function getDataPath(params: DataPathParams): DataPath { return path; }; - const rowPath = getPath(rows as string[], rowDimensionValues, rowPivotMeta!); - const colPath = getPath( - columns as string[], - colDimensionValues, - colPivotMeta!, - ); - const result = rowPath.concat(...colPath); + const rowPath = getPath(rowFields, rowDimensionValues, rowPivotMeta, false); + const colPath = getPath(colFields, colDimensionValues, colPivotMeta, true); - return result; + return [prefix, ...rowPath, ...colPath]; } - -interface Param extends BaseFields { - originData: RawData[]; - indexesData: RawData[][] | RawData[]; +interface Param { + rows: string[]; + columns: string[]; + values: string[]; + valueInCols: boolean; + data: RawData[]; + indexesData: Record; sortedDimensionValues: SortedDimensionValues; rowPivotMeta?: PivotMeta; colPivotMeta?: PivotMeta; + getExistValuesByDataItem?: (data: RawData, values: string[]) => string[]; +} + +export interface TransformResult { + paths: DataPath[]; + indexesData: Record; + rowPivotMeta: PivotMeta; + colPivotMeta: PivotMeta; + sortedDimensionValues: SortedDimensionValues; } /** - * 转换原始数据为多维数组数据 + * 转换原始数据为二维数组数据 */ -export function transformIndexesData(params: Param) { +export function transformIndexesData(params: Param): TransformResult { const { rows, columns, values, - originData = [], - indexesData = [], + valueInCols, + data = [], + indexesData = {}, sortedDimensionValues, rowPivotMeta, colPivotMeta, + getExistValuesByDataItem, } = params; const paths: DataPath[] = []; @@ -229,64 +324,68 @@ export function transformIndexesData(params: Param) { const onFirstCreate = ({ dimension, dimensionPath, - }: { - // 维度 id,如 city - dimension: string; - // 维度数组 ['四川省', '成都市'] - dimensionPath: string[]; - }) => { - if (repeatedDimensionSet.has(dimension)) { - /* - * 当行、列都配置了同一维度字段时,因为 getDataPath 先处理行、再处理列 - * 所有重复字段的维度值无需再加入到 sortedDimensionValues - */ + careRepeated = true, + }: OnFirstCreateParams) => { + if (careRepeated && repeatedDimensionSet.has(dimension)) { + // 当行、列都配置了同一维度字段时,因为 getDataPath 先处理行、再处理列 + // 所有重复字段的维度值无需再加入到 sortedDimensionValues return; } ( sortedDimensionValues[dimension] || (sortedDimensionValues[dimension] = []) - ).push( - // 拼接维度路径 [1, undefined] => ['1', 'undefined'] => '1[&]undefined - dimensionPath.map((it) => `${it}`).join(NODE_ID_SEPARATOR), - ); + ).push(dimensionPath); }; - originData.forEach((data) => { + const prefix = getDataPathPrefix(rows, columns as string[]); + + data.forEach((item: RawData) => { // 空数据没有意义,直接跳过 - if (!data) { + if (!item || isEmpty(item)) { return; } - const rowDimensionValues = transformDimensionsValues( - data, - rows as string[], - ); - const colDimensionValues = transformDimensionsValues( - data, - columns as string[], - ); - const path = getDataPath({ - rowDimensionValues, - colDimensionValues, - rowPivotMeta, - colPivotMeta, - shouldCreateOrUpdate: true, - onCreate: onFirstCreate, + const existValues = getExistValuesByDataItem + ? getExistValuesByDataItem(item, values) + : getExistValues(item, values); + + const multiRowDimensionValues = transformDimensionsValuesWithExtraFields( + item, rows, + valueInCols ? null : existValues, + ); + const multiColDimensionValues = transformDimensionsValuesWithExtraFields( + item, columns, - values, - }); + valueInCols ? existValues : null, + ); + + for (const rowDimensionValues of multiRowDimensionValues) { + for (const colDimensionValues of multiColDimensionValues) { + const path = getDataPath({ + rowDimensionValues, + colDimensionValues, + rowPivotMeta: rowPivotMeta!, + colPivotMeta: colPivotMeta!, + rowFields: rows, + colFields: columns, + isFirstCreate: true, + onFirstCreate, + prefix, + }); - paths.push(path); - set(indexesData, path, data); + paths.push(path); + set(indexesData, path as (string | number)[], item); + } + } }); return { paths, indexesData, - rowPivotMeta, - colPivotMeta, + rowPivotMeta: rowPivotMeta!, + colPivotMeta: colPivotMeta!, sortedDimensionValues, }; } @@ -340,3 +439,206 @@ export function generateExtraFieldMeta( return extraFieldMeta; } + +export function getHeaderTotalStatus(row: Node, col: Node): TotalStatus { + return { + isRowTotal: row.isGrandTotals!, + isRowSubTotal: row.isSubTotals!, + isColTotal: col.isGrandTotals!, + isColSubTotal: col.isSubTotals!, + }; +} + +/** + * 检查 getMultiData 时,传入的 query 是否是包含总计、小计分组的场景 + * MULTI_VALUE 后面再出现具体的名字,就表明是分组场景 + * 以 rows: [province, city] 为例 + * 如果是: [四川, MULTI_VALUE] => 代表获取四川下面的所有 city + * 如果是: [MULTI_VALUE, 成都] => 这种结果就是所有成都的小计分组 + * 每个 province 下面的 city 都不一样的 + * 就算换成 [province, sex] => [MULTI_VALUE, 女] 这样的形式,去获取所有 province 下的女性,但是每个 province 下的女性的 index 也可能不同 + * 需要将其拓展成多个结构 => [MULTI_VALUE, 女] => [[四川,女], [北京,女], ....] => [[1,1],[2,1],[3,2]....] + */ +export function existDimensionTotalGroup(path: string[]) { + let multiIdx = null; + + for (let i = 0; i < path.length; i++) { + const element = path[i]; + + if (isMultiValue(element)) { + multiIdx = i; + } else if (!isNull(multiIdx) && multiIdx < i) { + return true; + } + } + + return false; +} + +export function getSatisfiedPivotMetaValues(params: { + pivotMeta: PivotMeta; + dimensionValues: string[]; + fieldIdx: number; + queryType: QueryDataType; + fields: string[]; + sortedDimensionValues: SortedDimensionValues; +}) { + const { + pivotMeta, + dimensionValues, + fieldIdx, + queryType, + fields, + sortedDimensionValues, + } = params; + const rootContainer = { + children: pivotMeta, + } as PivotMetaValue; + + let metaValueList = [rootContainer]; + + function flattenMetaValue(list: PivotMetaValue[], field: string) { + const allValues = flatMap(list, (metaValue) => { + const values = [...metaValue.children.values()]; + + return values.filter( + (v) => + v.value !== EMPTY_EXTRA_FIELD_PLACEHOLDER && + (queryType === QueryDataType.All ? true : v.value !== TOTAL_VALUE), + ); + }); + + if (list.length > 1) { + // 从不同父维度中获取的子维度需要再排一次,比如province => city 按照字母倒序,那么在获取了所有 province 的 city 后需要再排一次 + const sortedDimensionValue = sortedDimensionValues[field] ?? []; + + return sortBy(allValues, (item) => + indexOf(sortedDimensionValue, item.id), + ); + } + + return allValues; + } + + for (let i = 0; i <= fieldIdx; i++) { + const dimensionValue = dimensionValues[i]; + const field = fields[i]; + + if (isMultiValue(dimensionValue)) { + metaValueList = flattenMetaValue(metaValueList, field); + } else { + metaValueList = metaValueList + .map((v) => v.children.get(dimensionValue)!) + .filter(Boolean); + } + } + + return metaValueList; +} + +export function flattenDimensionValues(params: { + dimensionValues: string[]; + pivotMeta: PivotMeta; + fields: string[]; + sortedDimensionValues: SortedDimensionValues; + queryType?: QueryDataType; +}) { + const { + dimensionValues, + pivotMeta, + fields, + sortedDimensionValues, + queryType = QueryDataType.All, + } = params; + + if (!existDimensionTotalGroup(dimensionValues)) { + return [dimensionValues]; + } + + const metaValues = getSatisfiedPivotMetaValues({ + pivotMeta, + dimensionValues, + fieldIdx: dimensionValues.length - 1, + queryType, + fields, + sortedDimensionValues, + }); + + return metaValues.map((v) => v.dimensions); +} + +export function getFlattenDimensionValues( + params: PickEssential & { + sortedDimensionValues: SortedDimensionValues; + queryType: QueryDataType; + }, +) { + const { + rowFields, + rowDimensionValues, + rowPivotMeta, + colFields, + colDimensionValues, + colPivotMeta, + queryType, + sortedDimensionValues, + } = params; + const rowQueries = flattenDimensionValues({ + dimensionValues: rowDimensionValues, + pivotMeta: rowPivotMeta, + fields: rowFields, + sortedDimensionValues, + queryType, + }); + const colQueries = flattenDimensionValues({ + dimensionValues: colDimensionValues, + pivotMeta: colPivotMeta, + fields: colFields, + sortedDimensionValues, + queryType, + }); + + return { + rowQueries, + colQueries, + }; +} + +export function flattenIndexesData( + data: FlattingIndexesData, + queryType: QueryDataType, +): FlattingIndexesData { + if (!data) { + return []; + } + + if (!isArray(data)) { + return compact([data]); + } + + return flatMap(data, (dimensionData: RawData[]) => { + if (!isArray(dimensionData)) { + return compact([dimensionData]) as RawData[]; + } + + // 数组的第 0 项是总计/小计专位,从第 1 项开始是明细数据 + const startIdx = queryType === QueryDataType.DetailOnly ? 1 : 0; + + return compact(dimensionData.slice(startIdx)); + }) as unknown as FlattingIndexesData; +} + +/** + * Get dimensions without path pre + * dimensions: ['辽宁省[&]芜湖市[&]家具[&]椅子'] + * return ['椅子'] + * + * @param dimensions + */ +export function getDimensionsWithoutPathPre(dimensions: string[]) { + return dimensions.map((item) => { + const splitArr = item?.split(NODE_ID_SEPARATOR); + + return splitArr[splitArr?.length - 1] ?? item; + }); +} diff --git a/packages/s2-core/src/utils/export/copy/base-data-cell-copy.ts b/packages/s2-core/src/utils/export/copy/base-data-cell-copy.ts index 3d7497032a..769500f8e3 100644 --- a/packages/s2-core/src/utils/export/copy/base-data-cell-copy.ts +++ b/packages/s2-core/src/utils/export/copy/base-data-cell-copy.ts @@ -4,9 +4,9 @@ import type { CopyablePlain, CopyAndExportUnifyConfig, SheetCopyConstructorParams, -} from '../interface'; +} from '../../../common/interface/export'; import { type DataItem, NewTab } from '../../../common'; -import { CopyMIMEType } from '../interface'; +import { CopyMIMEType } from '../../../common/interface/export'; import { unifyConfig } from './common'; // BaseDataCellCopy class diff --git a/packages/s2-core/src/utils/export/copy/common.ts b/packages/s2-core/src/utils/export/copy/common.ts index dacbc57b90..d6976f6664 100644 --- a/packages/s2-core/src/utils/export/copy/common.ts +++ b/packages/s2-core/src/utils/export/copy/common.ts @@ -12,7 +12,7 @@ import { type MatrixPlainTransformer, type MatrixHTMLTransformer, CopyMIMEType, -} from '../interface'; +} from '../../../common/interface/export'; import type { BaseDataSet } from './../../../data-set/base-data-set'; // 把 string[][] 矩阵转换成 CopyablePlain diff --git a/packages/s2-core/src/utils/export/copy/core.ts b/packages/s2-core/src/utils/export/copy/core.ts index d06016f8fe..561355b51f 100644 --- a/packages/s2-core/src/utils/export/copy/core.ts +++ b/packages/s2-core/src/utils/export/copy/core.ts @@ -7,14 +7,14 @@ import { type S2CellType, } from '../../../common'; import type { SpreadSheet } from '../../../sheet-type'; -import { copyToClipboard } from '../index'; +import { copyToClipboard } from '../utils'; import type { ColCell, RowCell } from '../../../cell'; import { getSelectedCols, getSelectedRows } from '../method'; import { type CopyableList, type CopyAllDataParams, CopyMIMEType, -} from '../interface'; +} from '../../../common/interface/export'; import { getBrushHeaderCopyable } from './pivot-header-copy'; import { processSelectedAllPivot, @@ -42,9 +42,8 @@ export const getHeaderNodeFromMeta = ( /** * 返回选中数据单元格生成的二维数组( CellMeta[][]) * @param { CellMeta[] } cells - * @return { CellMeta[][] } */ -const getSelectedCellsMeta = (cells: CellMeta[]) => { +const getSelectedCellsMeta = (cells: CellMeta[]): CellMeta[][] => { if (!cells?.length) { return []; } diff --git a/packages/s2-core/src/utils/export/copy/index.ts b/packages/s2-core/src/utils/export/copy/index.ts index 3cc9bd16be..123563161b 100644 --- a/packages/s2-core/src/utils/export/copy/index.ts +++ b/packages/s2-core/src/utils/export/copy/index.ts @@ -1,4 +1,4 @@ -import type { SheetCopyConstructorParams } from '../interface'; +import type { SheetCopyConstructorParams } from '../../../common/interface/export'; import { getSelectedData } from './core'; import { PivotDataCellCopy } from './pivot-data-cell-copy'; diff --git a/packages/s2-core/src/utils/export/copy/pivot-data-cell-copy.ts b/packages/s2-core/src/utils/export/copy/pivot-data-cell-copy.ts index 1df16a9f85..8c8471dbce 100644 --- a/packages/s2-core/src/utils/export/copy/pivot-data-cell-copy.ts +++ b/packages/s2-core/src/utils/export/copy/pivot-data-cell-copy.ts @@ -15,7 +15,7 @@ import type { CopyableList, MeasureQuery, SheetCopyConstructorParams, -} from '../interface'; +} from '../../../common/interface/export'; import { convertString, getColNodeFieldFromNode, @@ -23,6 +23,7 @@ import { getSelectedCols, getSelectedRows, } from '../method'; +import type { CellData } from '../../../data-set'; import type { BaseDataSet } from './../../../data-set/base-data-set'; import { BaseDataCellCopy } from './base-data-cell-copy'; import { @@ -231,7 +232,7 @@ export class PivotDataCellCopy extends BaseDataCellCopy { dataSet, ); - const fieldValue = cellData?.[VALUE_FIELD]; + const fieldValue = (cellData as CellData)?.[VALUE_FIELD]; const isChartData = isPlainObject( (fieldValue as MultiData)?.values, ); diff --git a/packages/s2-core/src/utils/export/copy/pivot-header-copy.ts b/packages/s2-core/src/utils/export/copy/pivot-header-copy.ts index 999938f8a9..273f1a3184 100644 --- a/packages/s2-core/src/utils/export/copy/pivot-header-copy.ts +++ b/packages/s2-core/src/utils/export/copy/pivot-header-copy.ts @@ -1,6 +1,6 @@ import { filter, isEmpty, map, max, repeat, zip } from 'lodash'; import type { ColCell, RowCell } from '../../../cell'; -import type { CopyableList } from '../interface'; +import type { CopyableList } from '../../../common/interface/export'; import { getAllLevels, getHeaderList } from '../method'; import { CellType, NODE_ID_SEPARATOR } from '../../../common'; import { matrixHtmlTransformer, matrixPlainTextTransformer } from './common'; diff --git a/packages/s2-core/src/utils/export/copy/table-copy.ts b/packages/s2-core/src/utils/export/copy/table-copy.ts index 90d3f280e1..92117589f8 100644 --- a/packages/s2-core/src/utils/export/copy/table-copy.ts +++ b/packages/s2-core/src/utils/export/copy/table-copy.ts @@ -12,7 +12,7 @@ import type { CopyableList, CopyAllDataParams, SheetCopyConstructorParams, -} from '../interface'; +} from '../../../common/interface/export'; import { convertString, getColNodeFieldFromNode, diff --git a/packages/s2-core/src/utils/export/index.ts b/packages/s2-core/src/utils/export/index.ts index bcb3b7787f..57837de931 100644 --- a/packages/s2-core/src/utils/export/index.ts +++ b/packages/s2-core/src/utils/export/index.ts @@ -1,128 +1,12 @@ -import { concat, get } from 'lodash'; -import { - CopyMIMEType, - type Copyable, - type CopyableItem, - type FormatOptions, - type CopyableList, - type CopyAllDataParams, -} from './interface'; -import { processAllSelected, processAllSelectedAsync } from './copy/core'; -import { getNodeFormatData, assembleMatrix, getMaxRowLen } from './copy/common'; +import type { + CopyableList, + FormatOptions, +} from '../../common/interface/export'; +import { assembleMatrix, getMaxRowLen, getNodeFormatData } from './copy/common'; import { getHeaderList } from './method'; -export const copyToClipboardByExecCommand = (data: Copyable): Promise => - new Promise((resolve, reject) => { - let content: string; - - if (Array.isArray(data)) { - content = get( - data.filter((item) => item.type === CopyMIMEType.PLAIN), - '[0].content', - '', - ) as string; - } else { - content = data.content || ''; - } - - const textarea = document.createElement('textarea'); - - textarea.value = content; - document.body.appendChild(textarea); - // 开启 preventScroll, 防止页面有滚动条时触发滚动 - textarea.focus({ preventScroll: true }); - textarea.select(); - - const success = document.execCommand('copy'); - - document.body.removeChild(textarea); - - if (success) { - resolve(); - } else { - reject(); - } - }); - -export const copyToClipboardByClipboard = (data: Copyable): Promise => - navigator.clipboard - .write([ - new ClipboardItem( - concat(data).reduce((prev, copyable: CopyableItem) => { - const { type, content } = copyable; - - return { - ...prev, - [type]: new Blob([content], { type }), - }; - }, {}), - ), - ]) - .catch(() => copyToClipboardByExecCommand(data)); - -export const copyToClipboard = ( - data: Copyable | string, - sync = false, -): Promise => { - let copyableItem: Copyable; - - if (typeof data === 'string') { - copyableItem = { - content: data, - type: CopyMIMEType.PLAIN, - }; - } else { - copyableItem = data; - } - - if (!navigator.clipboard || !window.ClipboardItem || sync) { - return copyToClipboardByExecCommand(copyableItem); - } - - return copyToClipboardByClipboard(copyableItem); -}; - -export const download = (str: string, fileName: string) => { - try { - const link = document.createElement('a'); - - link.download = `${fileName}.csv`; - // Avoid errors in Chinese encoding. - const dataBlob = new Blob([`\ufeff${str}`], { - type: 'text/csv;charset=utf-8', - }); - - link.href = URL.createObjectURL(dataBlob); - link.click(); - URL.revokeObjectURL(link.href); - } catch (e) { - // eslint-disable-next-line no-console - console.error(e); - } -}; - -/** - * Copy data - * @param sheetInstance - * @param split - * @param formatOptions 是否格式化数据 - * @param customTransformer - * @param isAsyncExport 是否异步导出 - * @deprecated 后续将废弃方法,将使用 asyncGetAllPlainData - */ -// TODO: 改名 -export const copyData = (params: CopyAllDataParams) => { - const result = processAllSelected(params); - - return result[0].content; -}; - -export const asyncGetAllPlainData = async (params: CopyAllDataParams) => { - const result = await processAllSelectedAsync(params); - - return result[0].content; -}; - export type { CopyableList, FormatOptions }; export { assembleMatrix, getMaxRowLen, getNodeFormatData }; export { getHeaderList }; +export * from './utils'; +export * from './copy'; diff --git a/packages/s2-core/src/utils/export/utils.ts b/packages/s2-core/src/utils/export/utils.ts new file mode 100644 index 0000000000..d1f1eb2745 --- /dev/null +++ b/packages/s2-core/src/utils/export/utils.ts @@ -0,0 +1,137 @@ +import { concat, get } from 'lodash'; +import { + CopyMIMEType, + type CopyAllDataParams, + type Copyable, + type CopyableItem, +} from '../../common/interface/export'; +import { processAllSelected, processAllSelectedAsync } from './copy/core'; + +/** + * 同步复制 + */ +export const copyToClipboardByExecCommand = (data: Copyable): Promise => + new Promise((resolve, reject) => { + let content: string; + + if (Array.isArray(data)) { + content = get( + data.filter((item) => item.type === CopyMIMEType.PLAIN), + '[0].content', + '', + ) as string; + } else { + content = data.content || ''; + } + + const textarea = document.createElement('textarea'); + + textarea.value = content; + document.body.appendChild(textarea); + // 开启 preventScroll, 防止页面有滚动条时触发滚动 + textarea.focus({ preventScroll: true }); + textarea.select(); + + const success = document.execCommand('copy'); + + document.body.removeChild(textarea); + + if (success) { + resolve(); + } else { + reject(); + } + }); + +/** + * 异步复制 + */ +export const copyToClipboardByClipboard = (data: Copyable): Promise => + navigator.clipboard + .write([ + new ClipboardItem( + concat(data).reduce((prev, copyable: CopyableItem) => { + const { type, content } = copyable; + // eslint-disable-next-line no-control-regex + const contentToCopy = content.replace(/\x00/g, ''); + + return { + ...prev, + [type]: new Blob([contentToCopy], { type }), + }; + }, {}), + ), + ]) + .catch(() => copyToClipboardByExecCommand(data)); + +/** + * @name 复制数据 + * @param data 数据源 + * @param sync 同步复制 + */ +export const copyToClipboard = ( + data: Copyable | string, + sync = false, +): Promise => { + let copyableItem: Copyable; + + if (typeof data === 'string') { + copyableItem = { + content: data, + type: CopyMIMEType.PLAIN, + }; + } else { + copyableItem = data; + } + + if (!navigator.clipboard || !window.ClipboardItem || sync) { + return copyToClipboardByExecCommand(copyableItem); + } + + return copyToClipboardByClipboard(copyableItem); +}; + +/** + * @name 导出数据 + * @param dataString 数据源 (字符串) + * @param fileName 文件名 (格式为 csv) + */ +export const download = (dataString: string, fileName: string) => { + try { + const link = document.createElement('a'); + + link.download = `${fileName}.csv`; + // Avoid errors in Chinese encoding. + const dataBlob = new Blob([`\ufeff${dataString}`], { + type: 'text/csv;charset=utf-8', + }); + + link.href = URL.createObjectURL(dataBlob); + link.click(); + URL.revokeObjectURL(link.href); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + } +}; + +/** + * 同步获取文本数据 + * @param params CopyAllDataParams + * @deprecated 后续将废弃方法,将使用 asyncGetAllPlainData + */ + +export const getAllPlainData = (params: CopyAllDataParams) => { + const result = processAllSelected(params); + + return result[0].content; +}; + +/** + * @name 异步获取文本数据 + */ +export const asyncGetAllPlainData = async (params: CopyAllDataParams) => { + const result = await processAllSelectedAsync(params); + + return result[0].content; +}; diff --git a/packages/s2-core/src/utils/hide-columns.ts b/packages/s2-core/src/utils/hide-columns.ts index 1167af1ae5..c0198f0298 100644 --- a/packages/s2-core/src/utils/hide-columns.ts +++ b/packages/s2-core/src/utils/hide-columns.ts @@ -154,9 +154,18 @@ export const hideColumns = async ( ...lastHiddenColumnFields, ]); + const isAllNearToHiddenColumnNodes = getHiddenColumnNodes( + spreadsheet, + hiddenColumnFields, + ).every((node, i, nodes) => { + const nextNode = nodes[i + 1]; + + return !nextNode || Math.abs(node.colIndex - nextNode.colIndex) === 1; + }); + const displaySiblingNode = getHiddenColumnDisplaySiblingNode( spreadsheet, - selectedColumnFields, + isAllNearToHiddenColumnNodes ? hiddenColumnFields : selectedColumnFields, ); const currentHiddenColumnsInfo: HiddenColumnsInfo = { diff --git a/packages/s2-core/src/utils/index.ts b/packages/s2-core/src/utils/index.ts index def125c3d1..bde0ca1822 100644 --- a/packages/s2-core/src/utils/index.ts +++ b/packages/s2-core/src/utils/index.ts @@ -5,8 +5,7 @@ export * from './layout'; export * from './text'; export * from './color'; export * from './theme'; -export * from './export/index'; -export * from './export/copy'; +export * from './export'; export * from './interaction'; export * from './g-renders'; export * from './g-mini-charts'; @@ -18,3 +17,4 @@ export * from './cell/cell'; export * from './cell/data-cell'; export * from './sort-action'; export * from './inject-css-text'; +export * from './math'; diff --git a/packages/s2-core/src/utils/interaction/merge-cell.ts b/packages/s2-core/src/utils/interaction/merge-cell.ts index a1fb8f6e77..3b2ee286d3 100644 --- a/packages/s2-core/src/utils/interaction/merge-cell.ts +++ b/packages/s2-core/src/utils/interaction/merge-cell.ts @@ -4,6 +4,7 @@ import { filter, find, forEach, + includes, isEmpty, isEqual, map, @@ -12,12 +13,12 @@ import { MergedCell } from '../../cell/merged-cell'; import { CellType } from '../../common/constant'; import type { MergedCellInfo, - S2CellType, TempMergedCell, ViewMeta, MergedCellCallback, } from '../../common/interface'; import type { SpreadSheet } from '../../sheet-type'; +import type { DataCell } from '../../cell'; /** * according to the coordinates of the starting point of the rectangle, @@ -86,7 +87,7 @@ export const getNextEdge = ( * return all the points of the polygon * @param cells the collection of information of cells which needed be merged */ -export const getPolygonPoints = (cells: S2CellType[]) => { +export const getPolygonPoints = (cells: DataCell[]) => { let allEdges: [number, number][][] = []; cells.forEach((cell) => { @@ -111,6 +112,53 @@ export const getPolygonPoints = (cells: S2CellType[]) => { return allPoints; }; +export const getRightAndBottomCells = (cells: DataCell[]) => { + const right: DataCell[] = []; + const bottom: DataCell[] = []; + const bottomRightCornerCell: DataCell[] = []; + + cells.forEach((cell) => { + const [row, col] = cell.position; + + if ( + !find( + cells, + (temp) => temp.position[0] === row + 1 && temp.position[1] === col, + ) + ) { + bottom.push(cell); + } + + if ( + !find( + cells, + (temp) => temp.position[1] === col + 1 && temp.position[0] === row, + ) + ) { + right.push(cell); + } + }); + + // 在绘制了 right border 后,如果它上面的 cell 也是 merge cell 中的,且无需绘制 right 时,需要单独为其位置 bottomRight corner 的 border,反正连线会断 + right.forEach((cell) => { + const [row, col] = cell.position; + const top = find( + cells, + (temp) => temp.position[0] === row - 1 && temp.position[1] === col, + ); + + if (top && !includes(right, top)) { + bottomRightCornerCell.push(top); + } + }); + + return { + bottom, + right, + bottomRightCornerCell, + }; +}; + /** * get cells on the outside of visible area through mergeCellInfo * @param invisibleCellInfo @@ -120,7 +168,7 @@ export const getInvisibleInfo = ( invisibleCellInfo: MergedCellInfo[], sheet: SpreadSheet, ) => { - const cells: S2CellType[] = []; + const cells: DataCell[] = []; let viewMeta: ViewMeta | undefined; forEach(invisibleCellInfo, (cellInfo) => { @@ -148,14 +196,14 @@ export const getInvisibleInfo = ( */ export const getVisibleInfo = ( cellsInfos: MergedCellInfo[], - allVisibleCells: S2CellType[], + allVisibleCells: DataCell[], ) => { - const cells: S2CellType[] = []; + const cells: DataCell[] = []; const invisibleCellInfo: MergedCellInfo[] = []; let cellsMeta: ViewMeta | Node | undefined; forEach(cellsInfos, (cellInfo: MergedCellInfo) => { - const findCell = find(allVisibleCells, (cell: S2CellType) => { + const findCell = find(allVisibleCells, (cell: DataCell) => { const meta = cell?.getMeta?.(); if ( @@ -164,7 +212,7 @@ export const getVisibleInfo = ( ) { return cell; } - }) as S2CellType; + }) as DataCell; if (findCell) { cells.push(findCell); @@ -186,7 +234,7 @@ export const getVisibleInfo = ( * @param sheet */ export const getTempMergedCell = ( - allVisibleCells: S2CellType[], + allVisibleCells: DataCell[], sheet?: SpreadSheet, cellsInfos: MergedCellInfo[] = [], ): TempMergedCell => { @@ -195,7 +243,7 @@ export const getTempMergedCell = ( allVisibleCells, ); let viewMeta: ViewMeta | Node | undefined = cellsMeta; - let mergedAllCells: S2CellType[] = cells; + let mergedAllCells: DataCell[] = cells; // some cells are invisible and some cells are visible const isPartiallyVisible = invisibleCellInfo?.length > 0 && @@ -247,21 +295,21 @@ export const getActiveCellsInfo = (sheet: SpreadSheet) => { /** * 创建 merged cell 实例 - * @param spreasheet 表格实例 + * @param spreadsheet 表格实例 * @param cells 待合并的单元格 * @param meta 元信息 * @returns */ export const getMergedCellInstance: MergedCellCallback = ( - spreasheet, + spreadsheet, cells, meta, ) => { - if (spreasheet.options.mergedCell) { - return spreasheet.options.mergedCell(spreasheet, cells, meta); + if (spreadsheet.options.mergedCell) { + return spreadsheet.options.mergedCell(spreadsheet, cells, meta); } - return new MergedCell(spreasheet, cells, meta); + return new MergedCell(spreadsheet, cells, meta); }; /** @@ -315,7 +363,7 @@ export const removeUnmergedCellsInfo = ( removeMergedCell: MergedCell, mergedCellsInfo: MergedCellInfo[][], ): MergedCellInfo[][] => { - const removeCellInfo = map(removeMergedCell.cells, (cell: S2CellType) => { + const removeCellInfo = map(removeMergedCell.cells, (cell: DataCell) => { return { colIndex: cell.getMeta().colIndex, rowIndex: cell.getMeta().rowIndex, diff --git a/packages/s2-core/src/utils/interaction/select-event.ts b/packages/s2-core/src/utils/interaction/select-event.ts index 99b62bbcf9..b35da0dbb8 100644 --- a/packages/s2-core/src/utils/interaction/select-event.ts +++ b/packages/s2-core/src/utils/interaction/select-event.ts @@ -30,6 +30,10 @@ export const isMultiSelectionKey = (e: KeyboardEvent) => e.key as InteractionKeyboardKey, ); +export const isMouseEventWithMeta = (e: MouseEvent) => { + return e.ctrlKey || e.metaKey; +}; + export const getCellMeta = (cell: S2CellType): CellMeta => { const meta = cell.getMeta(); const { id, colIndex, rowIndex, rowQuery } = meta || {}; diff --git a/packages/s2-core/src/utils/layout/add-totals.ts b/packages/s2-core/src/utils/layout/add-totals.ts index e268699275..48e47b238c 100644 --- a/packages/s2-core/src/utils/layout/add-totals.ts +++ b/packages/s2-core/src/utils/layout/add-totals.ts @@ -16,7 +16,12 @@ export const addTotals = (params: TotalParams) => { // check to see if grand total is added if (totalsConfig?.showGrandTotals) { action = totalsConfig.reverseGrandTotalsLayout ? 'unshift' : 'push'; - totalValue = new TotalClass(totalsConfig.grandTotalsLabel!, false, true); + totalValue = new TotalClass({ + label: totalsConfig.grandTotalsLabel!, + isSubTotals: false, + isGrandTotals: true, + isTotalRoot: true, + }); } } else if ( /** @@ -30,7 +35,12 @@ export const addTotals = (params: TotalParams) => { currentField !== EXTRA_FIELD ) { action = totalsConfig.reverseSubTotalsLayout ? 'unshift' : 'push'; - totalValue = new TotalClass(totalsConfig.subTotalsLabel!, true); + totalValue = new TotalClass({ + label: totalsConfig.subTotalsLabel!, + isSubTotals: true, + isGrandTotals: false, + isTotalRoot: true, + }); } if (action) { diff --git a/packages/s2-core/src/utils/layout/frozen.ts b/packages/s2-core/src/utils/layout/frozen.ts index 87de79e883..e43919bf03 100644 --- a/packages/s2-core/src/utils/layout/frozen.ts +++ b/packages/s2-core/src/utils/layout/frozen.ts @@ -1,5 +1,4 @@ import type { S2TableSheetFrozenOptions } from '../../common/interface'; -import type { Node } from '../../facet/layout/node'; export const getValidFrozenOptions = ( defaultFrozenOptions: S2TableSheetFrozenOptions, @@ -37,33 +36,3 @@ export const getValidFrozenOptions = ( return frozenOptions; }; - -export const getFrozenColWidth = ( - colLeafNodes: Node[], - options: S2TableSheetFrozenOptions, -) => { - const result = { - frozenColWidth: 0, - frozenTrailingColWidth: 0, - }; - - if (!options.colCount && !options.trailingColCount) { - return result; - } - - const { - colCount: frozenColCount = 0, - trailingColCount: frozenTrailingColCount = 0, - } = getValidFrozenOptions(options, colLeafNodes.length); - - for (let i = 0; i < frozenColCount; i++) { - result.frozenColWidth += colLeafNodes[i].width; - } - - for (let i = 0; i < frozenTrailingColCount; i++) { - result.frozenTrailingColWidth += - colLeafNodes[colLeafNodes.length - 1 - i].width; - } - - return result; -}; diff --git a/packages/s2-core/src/utils/layout/generate-header-nodes.ts b/packages/s2-core/src/utils/layout/generate-header-nodes.ts index b6aab33910..87ebd7281a 100644 --- a/packages/s2-core/src/utils/layout/generate-header-nodes.ts +++ b/packages/s2-core/src/utils/layout/generate-header-nodes.ts @@ -1,5 +1,4 @@ -import { includes } from 'lodash'; -import { EXTRA_FIELD } from '../../common/constant'; +import { EMPTY_FIELD_VALUE, EXTRA_FIELD } from '../../common/constant'; import { i18n } from '../../common/i18n'; import { buildGridHierarchy } from '../../facet/layout/build-gird-hierarchy'; import type { HeaderNodesParams } from '../../facet/layout/interface'; @@ -8,6 +7,7 @@ import { Node } from '../../facet/layout/node'; import { TotalClass } from '../../facet/layout/total-class'; import { TotalMeasure } from '../../facet/layout/total-measure'; import { generateId } from '../../utils/layout/generate-id'; +import { whetherLeafByLevel } from './whether-leaf-by-level'; // eslint-disable-next-line max-lines-per-function export const generateHeaderNodes = (params: HeaderNodesParams) => { @@ -23,16 +23,16 @@ export const generateHeaderNodes = (params: HeaderNodesParams) => { addTotalMeasureInTotal, spreadsheet, } = params; - const { colCell } = spreadsheet.options.style!; for (const [index, fieldValue] of fieldValues.entries()) { const isTotals = fieldValue instanceof TotalClass; const isTotalMeasure = fieldValue instanceof TotalMeasure; let value: string; - let nodeQuery; + let nodeQuery: Record; let isLeaf = false; let isGrandTotals = false; let isSubTotals = false; + let isTotalRoot = false; let adjustedField = currentField; if (isTotals) { @@ -40,27 +40,29 @@ export const generateHeaderNodes = (params: HeaderNodesParams) => { isGrandTotals = totalClass.isGrandTotals; isSubTotals = totalClass.isSubTotals; + isTotalRoot = totalClass.isTotalRoot; value = i18n((fieldValue as TotalClass).label); - if (addMeasureInTotalQuery) { - // root[&]四川[&]总计 => {province: '四川', EXTRA_FIELD: 'price'} - nodeQuery = { - ...query, - [EXTRA_FIELD]: spreadsheet?.dataSet?.fields?.values?.[0], - }; - isLeaf = true; + if (isTotalRoot) { + nodeQuery = query; } else { // root[&]四川[&]总计 => {province: '四川'} - nodeQuery = query; - if (!addTotalMeasureInTotal) { - isLeaf = true; - } + nodeQuery = { ...query, [currentField]: value }; + } + + if (addMeasureInTotalQuery) { + // root[&]四川[&]总计 => {province: '四川', EXTRA_FIELD: 'price'} + nodeQuery[EXTRA_FIELD] = spreadsheet?.dataSet?.fields.values![0]; } + + isLeaf = whetherLeafByLevel({ spreadsheet, level, fields }); } else if (isTotalMeasure) { value = i18n((fieldValue as TotalMeasure).label); // root[&]四川[&]总计[&]price => {province: '四川',EXTRA_FIELD: 'price' } nodeQuery = { ...query, [EXTRA_FIELD]: value }; adjustedField = EXTRA_FIELD; - isLeaf = true; + isGrandTotals = parentNode.isGrandTotals!; + isSubTotals = parentNode.isSubTotals!; + isLeaf = whetherLeafByLevel({ spreadsheet, level, fields }); } else if (spreadsheet.isTableMode()) { value = fieldValue; adjustedField = fields[index]; @@ -69,16 +71,18 @@ export const generateHeaderNodes = (params: HeaderNodesParams) => { } else { value = fieldValue; // root[&]四川[&]成都 => {province: '四川', city: '成都' } - nodeQuery = { ...query, [currentField]: value }; - const isValueInCols = spreadsheet.dataCfg.fields?.valueInCols ?? true; - const isHideValue = - colCell?.hideValue && isValueInCols && includes(fields, EXTRA_FIELD); - const extraSize = isHideValue ? 2 : 1; + // 子维度的维值为空时, 使用父级节点的 query, 避免查询不到数据 + nodeQuery = + value === EMPTY_FIELD_VALUE + ? { ...query } + : { ...query, [currentField]: value }; - isLeaf = level === fields.length - extraSize; + isLeaf = whetherLeafByLevel({ spreadsheet, level, fields }); } - const nodeId = generateId(parentNode.id, value); + // 优先找序列字段对应的格式化名称, 找不到则是普通维值 + const formattedValue = spreadsheet.dataSet.getFieldName(value) || value; + const nodeId = generateId(parentNode.id, formattedValue); if (!nodeId) { return; @@ -89,15 +93,16 @@ export const generateHeaderNodes = (params: HeaderNodesParams) => { // create new header nodes const node = new Node({ id: nodeId, - value, + value: formattedValue, level, field: adjustedField, parent: parentNode, - isTotals, + isTotals: isTotals || isTotalMeasure, isGrandTotals, isSubTotals, isTotalMeasure, isCollapsed, + isTotalRoot, hierarchy, query: nodeQuery, spreadsheet, @@ -115,6 +120,7 @@ export const generateHeaderNodes = (params: HeaderNodesParams) => { const hiddenColumnsInfo = spreadsheet?.facet?.getHiddenColumnsInfo(node); if (hiddenColumnsInfo && parentNode) { + // hiddenChildNodeInfo 属性在 S2 中没有用到,但是没删怕外部有使用,已标记为废弃 parentNode.hiddenChildNodeInfo = hiddenColumnsInfo; } diff --git a/packages/s2-core/src/utils/layout/generate-id.ts b/packages/s2-core/src/utils/layout/generate-id.ts index f13150849d..c510086e9e 100644 --- a/packages/s2-core/src/utils/layout/generate-id.ts +++ b/packages/s2-core/src/utils/layout/generate-id.ts @@ -10,8 +10,8 @@ import { * Row and column header node id generator. */ -export const generateId = (...ids: string[]): string => - ids +export const generateId = (...ids: string[]): string => { + return ids .map((value) => { if (isUndefined(value)) { return UNDEFINED_SYMBOL_ID; @@ -24,9 +24,10 @@ export const generateId = (...ids: string[]): string => return String(value); }) .join(NODE_ID_SEPARATOR); +}; -export const resolveId = (id = '') => - id +export const resolveId = (id = '') => { + return id .split(NODE_ID_SEPARATOR) .reduce<(string | null | undefined)[]>((result, current) => { if (current === ROOT_NODE_ID) { @@ -43,3 +44,4 @@ export const resolveId = (id = '') => return result; }, []); +}; diff --git a/packages/s2-core/src/utils/layout/get-dims-condition-by-node.ts b/packages/s2-core/src/utils/layout/get-dims-condition-by-node.ts index 67d20b26b0..f02ce4278b 100644 --- a/packages/s2-core/src/utils/layout/get-dims-condition-by-node.ts +++ b/packages/s2-core/src/utils/layout/get-dims-condition-by-node.ts @@ -1,19 +1,21 @@ +import type { Query } from '../../data-set'; +import { EMPTY_FIELD_VALUE } from '../../common/constant'; import type { Node } from '../../facet/layout/node'; export function getDimsCondition(parent: Node, force?: boolean) { - const cond: Record = {}; - let p = parent; + const cond: Query = {}; + let p: Node | undefined = parent; while (p && p.field) { /** * 当为表格布局时,小计行的内容是“小计”不需要作为筛选条件 * 当为树状布局时,force可以强行指定小计行,即父类目作为筛选条件 */ - if (!p.isTotals || force) { + if ((!p.isTotalRoot || force) && p.value !== EMPTY_FIELD_VALUE) { cond[p.field] = p.value; } - p = p.parent!; + p = p.parent; } return cond; diff --git a/packages/s2-core/src/utils/layout/whether-leaf-by-level.ts b/packages/s2-core/src/utils/layout/whether-leaf-by-level.ts new file mode 100644 index 0000000000..7a10280d4b --- /dev/null +++ b/packages/s2-core/src/utils/layout/whether-leaf-by-level.ts @@ -0,0 +1,18 @@ +import { includes } from 'lodash'; +import type { WhetherLeafParams } from '../../facet/layout/interface'; +import { EXTRA_FIELD } from '../../common'; + +export const whetherLeafByLevel = (params: WhetherLeafParams) => { + const { spreadsheet, level, fields } = params; + const { options, dataSet } = spreadsheet; + const moreThanOneValue = dataSet.moreThanOneValue(); + const isValueInCols = spreadsheet.dataCfg.fields?.valueInCols ?? true; + const isHideMeasure = + options?.style?.colCell?.hideValue && + isValueInCols && + !moreThanOneValue && + includes(fields, EXTRA_FIELD); + const extraSize = isHideMeasure ? 2 : 1; + + return level === fields.length - extraSize; +}; diff --git a/packages/s2-core/src/utils/math.ts b/packages/s2-core/src/utils/math.ts new file mode 100644 index 0000000000..45d7aeaf4b --- /dev/null +++ b/packages/s2-core/src/utils/math.ts @@ -0,0 +1,5 @@ +import { floor as innerFloor } from 'lodash'; + +export function floor(value: number, precision = 2) { + return innerFloor(value, precision); +} diff --git a/packages/s2-core/src/utils/merge.ts b/packages/s2-core/src/utils/merge.ts index 0551769d98..3bb4346e32 100644 --- a/packages/s2-core/src/utils/merge.ts +++ b/packages/s2-core/src/utils/merge.ts @@ -1,11 +1,11 @@ -import { isArray, isEmpty, mergeWith, uniq, isEqual, isString } from 'lodash'; +import { isArray, isEmpty, isEqual, isString, mergeWith, uniq } from 'lodash'; import { DEFAULT_DATA_CONFIG } from '../common/constant/dataConfig'; import { DEFAULT_OPTIONS } from '../common/constant/options'; import type { + CustomHeaderFields, + Fields, S2DataConfig, S2Options, - Fields, - CustomHeaderFields, } from '../common/interface'; export const customMerge = (...objects: unknown[]): T => { diff --git a/packages/s2-core/src/utils/number-calculate.ts b/packages/s2-core/src/utils/number-calculate.ts index 9892202cee..7fbb662273 100644 --- a/packages/s2-core/src/utils/number-calculate.ts +++ b/packages/s2-core/src/utils/number-calculate.ts @@ -1,5 +1,5 @@ import Decimal from 'decimal.js'; -import { getFieldValueOfViewMetaData } from '../data-set/cell-data'; +import { CellData } from '../data-set/cell-data'; import { Aggregation, type ViewMetaData } from '../common/interface'; export const isNotNumber = (value: unknown) => { @@ -38,9 +38,7 @@ const processFieldValues = ( } return data.reduce>((resultArr, item) => { - const fieldValue = getFieldValueOfViewMetaData(item, field) as - | string - | number; + const fieldValue = CellData.getFieldValue(item, field) as string | number; const notNumber = isNotNumber(fieldValue); diff --git a/packages/s2-core/src/utils/sort-action.ts b/packages/s2-core/src/utils/sort-action.ts index 128bd508e3..2d48cf58ef 100644 --- a/packages/s2-core/src/utils/sort-action.ts +++ b/packages/s2-core/src/utils/sort-action.ts @@ -1,13 +1,14 @@ import { compact, - concat, endsWith, + flatMap, includes, indexOf, isEmpty, isNil, keys, map, + sortBy, split, toUpper, uniq, @@ -15,18 +16,21 @@ import { import { EXTRA_FIELD, NODE_ID_SEPARATOR, + ORIGIN_FIELD, + QueryDataType, TOTAL_VALUE, } from '../common/constant'; import type { Fields, SortMethod, SortParam } from '../common/interface'; import type { PivotDataSet, Query } from '../data-set'; import type { CellData } from '../data-set/cell-data'; -import type { SortActionParams } from '../data-set/interface'; +import type { + PivotMeta, + PivotMetaValue, + SortActionParams, + SortPivotMetaParams, +} from '../data-set/interface'; import { getLeafColumnsWithKey } from '../facet/utils'; -import { - getListBySorted, - isTotalData, - sortByItems, -} from '../utils/data-set-operate'; +import { getListBySorted, sortByItems } from '../utils/data-set-operate'; import { filterExtraDimension, getDimensionsWithParentPath, @@ -284,26 +288,28 @@ export const getSortByMeasureValues = ( const { dataSet, sortParam, originValues } = params; const { fields } = dataSet!; const { sortByMeasure, query, sortFieldId } = sortParam!; - // 按 query 查出所有数据 - const dataList = dataSet!.getCellMultiData({ query: query! }); - const columns = getLeafColumnsWithKey(fields.columns); + + if (sortByMeasure !== TOTAL_VALUE) { + const dataList = dataSet!.getCellMultiData({ + query, + queryType: QueryDataType.DetailOnly, + }); + + return dataList; + } /** * 按明细数据 * 需要过滤查询出的总/小计“汇总数据” */ - if (sortByMeasure !== TOTAL_VALUE) { - const rowColFields = concat(fields.rows, columns) as string[]; - - return dataList.filter( - (dataItem) => - /* - * 过滤出包含所有行列维度的数据 - * 若缺失任意 field,则是汇总数据,需要过滤掉 - */ - !isTotalData(rowColFields, dataItem.getOrigin()), - ); - } + + const dataList = dataSet!.getCellMultiData({ + query, + queryType: QueryDataType.All, + }); + + // 按 query 查出所有数据 + const columns = getLeafColumnsWithKey(fields.columns); /** * 按汇总值进行排序 @@ -314,11 +320,11 @@ export const getSortByMeasureValues = ( const isSortFieldInRow = includes(fields.rows, sortFieldId); // 排序字段所在一侧的全部字段 const sortFields = filterExtraDimension( - (isSortFieldInRow ? fields.rows : fields.columns) as string[], + (isSortFieldInRow ? fields.rows : columns) as string[], ); // 与排序交叉的另一侧全部字段 const oppositeFields = filterExtraDimension( - (isSortFieldInRow ? fields.columns : fields.rows) as string[], + (isSortFieldInRow ? columns : fields.rows) as string[], ); const fieldAfterSortField = sortFields[sortFields.indexOf(sortFieldId) + 1]; @@ -328,7 +334,7 @@ export const getSortByMeasureValues = ( ); const totalDataList = dataList.filter((dataItem) => { - const dataItemKeys = new Set(keys(dataItem.getOrigin())); + const dataItemKeys = new Set(keys(dataItem[ORIGIN_FIELD])); if (!dataItemKeys.has(sortFieldId)) { /* @@ -418,3 +424,35 @@ export const getSortTypeIcon = ( return 'SortDown'; } }; + +/** + * 对 pivot meta 中的内容进行排序,返回新的 sorted pivot meta + */ +export const getSortedPivotMeta = (params: SortPivotMetaParams) => { + const { pivotMeta, dimensions, sortedDimensionValues, sortFieldId } = params; + const rootContainer = { + children: pivotMeta, + } as PivotMetaValue; + let metaValueList = [rootContainer]; + + for (const dimension of dimensions) { + if (dimension !== sortFieldId) { + metaValueList = flatMap(metaValueList, (metaValue) => { + return [...metaValue.children.values()]; + }); + } else { + metaValueList.forEach((metaValue) => { + const values = [...metaValue.children.values()]; + + const entities = sortBy(values, (value) => { + return indexOf(sortedDimensionValues, value.id); + }).map((value) => [value.value, value] as [string, PivotMetaValue]); + + metaValue.children = new Map(entities) as PivotMeta; + }); + break; + } + } + + return rootContainer.children; +}; diff --git a/packages/s2-core/src/utils/text.ts b/packages/s2-core/src/utils/text.ts index 9e22337e56..bb0756e413 100644 --- a/packages/s2-core/src/utils/text.ts +++ b/packages/s2-core/src/utils/text.ts @@ -15,6 +15,7 @@ import type { ColCell } from '../cell'; import { CellType, ELLIPSIS_SYMBOL, + EMPTY_FIELD_VALUE, EMPTY_PLACEHOLDER, } from '../common/constant'; import { @@ -47,10 +48,11 @@ export const getDisplayText = ( text: string | number | null | undefined, placeholder?: string, ) => { - const empty = placeholder ?? EMPTY_PLACEHOLDER; + const emptyPlaceholder = placeholder ?? EMPTY_PLACEHOLDER; + // 对应维度缺少维度数据时, 会使用 EMPTY_FIELD_VALUE 填充, 实际渲染时统一转成 "-" + const isEmptyText = isNil(text) || text === '' || text === EMPTY_FIELD_VALUE; - // [null, undefined, ''] will return empty - return isNil(text) || text === '' ? empty : `${text}`; + return isEmptyText ? emptyPlaceholder : `${text}`; }; /** @@ -268,7 +270,7 @@ export const getEllipsisText = ({ * Two cases needed to be considered since the derived value could be number or string. * @param value */ -export const isUpDataValue = (value: number | string): boolean => { +export const isUpDataValue = (value: SimpleData): boolean => { if (isNumber(value)) { return value >= 0; } @@ -276,6 +278,32 @@ export const isUpDataValue = (value: number | string): boolean => { return !!value && !trim(value).startsWith('-'); }; +/** + * Determines whether the data is actually equal to 0 or empty or nil + * example: "0.00%" => true + * @param value + */ +export const isZeroOrEmptyValue = (value: SimpleData): boolean => { + return ( + isNil(value) || + value === '' || + Number(String(value).replace(/[^0-9.]+/g, '')) === 0 + ); +}; + +/** + * Determines whether the data is actually equal to 0 or empty or nil or equals to compareValue + * example: "0.00%" => true + * @param value + * @param compareValue + */ +export const isUnchangedValue = ( + value: SimpleData, + compareValue: SimpleData, +): boolean => { + return isZeroOrEmptyValue(value) || value === compareValue; +}; + /** * 根据单元格对齐方式计算文本的 x 坐标 * @param x 单元格的 x 坐标 @@ -311,9 +339,8 @@ const calX = ( const getDrawStyle = (cell: S2CellType) => { const { isTotals } = cell.getMeta(); const isMeasureField = (cell as ColCell).isMeasureField?.(); - const cellStyle: InternalFullyCellTheme = cell.getStyle( - isMeasureField ? CellType.COL_CELL : CellType.DATA_CELL, - ); + + const cellStyle = cell.getStyle(cell.cellType || CellType.DATA_CELL); let textStyle: TextTheme | undefined; diff --git a/packages/s2-core/src/utils/theme.ts b/packages/s2-core/src/utils/theme.ts index bbbeb61f91..106c48c5e6 100644 --- a/packages/s2-core/src/utils/theme.ts +++ b/packages/s2-core/src/utils/theme.ts @@ -1,6 +1,6 @@ import { PALETTE_MAP, STYLE_ELEMENT_ID } from '../common/constant'; import type { Palette, ThemeName } from '../common/interface/theme'; -import DarkVars from '../styles/theme/dark.less'; +import DarkVars from '../styles/theme/dark.less?inline'; import { injectCssText } from './inject-css-text'; /** diff --git a/packages/s2-core/src/utils/tooltip.ts b/packages/s2-core/src/utils/tooltip.ts index f457d8c998..77806b6b41 100644 --- a/packages/s2-core/src/utils/tooltip.ts +++ b/packages/s2-core/src/utils/tooltip.ts @@ -62,7 +62,7 @@ import type { TooltipPosition, TooltipSummaryOptions, } from '../common/interface/tooltip'; -import { getFieldValueOfViewMetaData } from '../data-set/cell-data'; +import { CellData } from '../data-set/cell-data'; import type { Node as S2Node } from '../facet/layout/node'; import { getLeafColumnsWithKey } from '../facet/utils'; import type { SpreadSheet } from '../sheet-type'; @@ -212,7 +212,7 @@ export const getListItem = ( const formatter = getFieldFormatter(spreadsheet, field); // 非数值类型的 data 不展示 (趋势分析表/迷你图/G2 图表),上层通过自定义 tooltip 的方式去自行定制 - const dataValue = getFieldValueOfViewMetaData(data, field); + const dataValue = CellData.getFieldValue(data, field); const displayDataValue = isObject(dataValue) ? null : dataValue; const value = formatter( @@ -234,7 +234,7 @@ export const getFieldList = ( const currentFields = filter( concat([], fields), (field) => - field !== EXTRA_FIELD && getFieldValueOfViewMetaData(activeData, field), + field !== EXTRA_FIELD && CellData.getFieldValue(activeData, field), ); return map(currentFields, (field: string) => @@ -489,11 +489,15 @@ export const getSummaries = (params: SummaryParam): TooltipSummaryOptions[] => { const isTableMode = spreadsheet.isTableMode(); if (isTableMode && options?.onlyShowCellText) { - const selectedCellsData = spreadsheet.dataSet.getCellMultiData({ - query: {}, - }); - - return [{ selectedData: selectedCellsData as Data[], name: '', value: '' }]; + const meta = targetCell?.getMeta(); + // 如果是列头, 获取当前列所有数据, 其他则获取当前整行 (1条数据) + const selectedCellsData = ( + meta?.field + ? spreadsheet.dataSet.getCellMultiData({ query: { field: meta.field } }) + : [spreadsheet.dataSet.getRowData(meta as ViewMeta)] + ) as ViewMetaData[]; + + return [{ selectedData: selectedCellsData, name: '', value: '' }]; } // 拿到选择的所有 dataCell的数据 @@ -579,8 +583,8 @@ export const getTooltipData = (params: TooltipDataParam): TooltipData => { } else if (options.onlyShowCellText) { // 行列头hover & 明细表所有hover - const value = getFieldValueOfViewMetaData(firstCellInfo, 'value') as string; - const valueField = getFieldValueOfViewMetaData( + const value = CellData.getFieldValue(firstCellInfo, 'value') as string; + const valueField = CellData.getFieldValue( firstCellInfo, 'valueField', ) as string; diff --git a/packages/s2-react/CHANGELOG.md b/packages/s2-react/CHANGELOG.md index edda629d83..04d4ca6dd4 100644 --- a/packages/s2-react/CHANGELOG.md +++ b/packages/s2-react/CHANGELOG.md @@ -1,10 +1,61 @@ # [@antv/s2-react-v2.0.0-next.9](https://github.com/antvis/S2/compare/@antv/s2-react-v2.0.0-next.8...@antv/s2-react-v2.0.0-next.9) (2023-12-12) +* **table-sheet:** 修复明细表 tooltip 展示了错误的汇总数据的问题 ([#2457](https://github.com/antvis/S2/issues/2457)) ([51bc110](https://github.com/antvis/S2/commit/51bc1105d8c52bd46cc89dfe2698b0fe42745c69)) +* **table-sheet:** 修复明细表排序后开启行列冻结,冻结行展示错误 close [#2388](https://github.com/antvis/S2/issues/2388) ([#2453](https://github.com/antvis/S2/issues/2453)) ([741e27a](https://github.com/antvis/S2/commit/741e27aab78b4b415d5f9e49760b401c93a84ca9)) + +### Features + +* 交叉表支持冻结首行能力 ([#2416](https://github.com/antvis/S2/issues/2416)) ([b81b795](https://github.com/antvis/S2/commit/b81b7957b9e8b8e1fbac9ebc6cacdf45a14e5412)) + +# [@antv/s2-react-v1.44.3](https://github.com/antvis/S2/compare/@antv/s2-react-v1.44.2...@antv/s2-react-v1.44.3) (2023-12-01) ### Bug Fixes -* 修复 React 18 StrictMode 同时挂载两个表格的问题 ([#2432](https://github.com/antvis/S2/issues/2432)) ([7c4da43](https://github.com/antvis/S2/commit/7c4da435976076e5babe21012524694986286210)) +* **copy:** 修复刷选复制行列头时,数值单元格未格式化 & 存在省略号时未复制原始值 ([#2410](https://github.com/antvis/S2/issues/2410)) ([708fde4](https://github.com/antvis/S2/commit/708fde479bb48b941445b3adaf1f56cf5cb6b301)) +* 修复中英文标点符号 ([#2442](https://github.com/antvis/S2/issues/2442)) ([17a2d00](https://github.com/antvis/S2/commit/17a2d00f13ff1db4cc8236176b2a26c5212a2dbd)) + +# [@antv/s2-react-v1.44.2](https://github.com/antvis/S2/compare/@antv/s2-react-v1.44.1...@antv/s2-react-v1.44.2) (2023-11-10) + +### Bug Fixes + +* 修复趋势分析表自定义列头 tooltip 后错误的使用行头的 tooltip ([#2399](https://github.com/antvis/S2/issues/2399)) ([0310c2f](https://github.com/antvis/S2/commit/0310c2f7f6ea054a79f0fc71b972ee1d3dd1c649)) + +# [@antv/s2-react-v1.44.1](https://github.com/antvis/S2/compare/@antv/s2-react-v1.44.0...@antv/s2-react-v1.44.1) (2023-10-27) + +### Bug Fixes + +* **interaction:** 修复拖动水平滚动条后单元格选中状态被重置 close [#2376](https://github.com/antvis/S2/issues/2376) ([#2380](https://github.com/antvis/S2/issues/2380)) ([b2e9700](https://github.com/antvis/S2/commit/b2e97008122f5320342fd069a08f6e821a5c9ad6)) +* **layout:** 修复在紧凑模式列头宽度未按文本自适应 close [#2385](https://github.com/antvis/S2/issues/2385) ([#2392](https://github.com/antvis/S2/issues/2392)) ([2edd99c](https://github.com/antvis/S2/commit/2edd99c367116bad661a02893a303311787eb647)) + +# [@antv/s2-react-v1.44.0](https://github.com/antvis/S2/compare/@antv/s2-react-v1.43.0...@antv/s2-react-v1.44.0) (2023-09-22) + +### Features + +* 对比值无波动时也显示灰色 ([#2351](https://github.com/antvis/S2/issues/2351)) ([12f2d02](https://github.com/antvis/S2/commit/12f2d0268d447ec99a1227ffedd5ed266d93e86b)) +* 小计/总计功能,支持按维度分组汇总 ([#2346](https://github.com/antvis/S2/issues/2346)) ([20e608f](https://github.com/antvis/S2/commit/20e608f2447e9ffdb135d98e4cc7f39f1cfb308d)) + +# [@antv/s2-react-v1.43.0](https://github.com/antvis/S2/compare/@antv/s2-react-v1.42.1...@antv/s2-react-v1.43.0) (2023-09-09) + +### Bug Fixes + +* **interaction:** 修复行头滚动刷选范围判断错误 ([8b080af](https://github.com/antvis/S2/commit/8b080afccdd4bebc157e0d569d36b2b04175a522)) +### Features + +* 对比值无波动时也显示灰色 ([#2351](https://github.com/antvis/S2/issues/2351)) ([12f2d02](https://github.com/antvis/S2/commit/12f2d0268d447ec99a1227ffedd5ed266d93e86b)) + +# [@antv/s2-react-v1.42.2-alpha.1](https://github.com/antvis/S2/compare/@antv/s2-react-v1.42.1...@antv/s2-react-v1.42.2-alpha.1) (2023-09-07) + +### Bug Fixes + +* **interaction:** 修复行头滚动刷选范围判断错误 ([8b080af](https://github.com/antvis/S2/commit/8b080afccdd4bebc157e0d569d36b2b04175a522)) +* **scroll:** 修复移动端快速滚动时控制台报错 close [#2266](https://github.com/antvis/S2/issues/2266) ([#2302](https://github.com/antvis/S2/issues/2302)) ([4ccc03d](https://github.com/antvis/S2/commit/4ccc03d50ef6622774a8c9e3599c988d2a7e126e)) + +# [@antv/s2-react-v1.37.1-alpha.2](https://github.com/antvis/S2/compare/@antv/s2-react-v1.37.1-alpha.1...@antv/s2-react-v1.37.1-alpha.2) (2023-03-08) + +### Bug Fixes + +* 修复 React 18 StrictMode 同时挂载两个表格的问题 ([#2432](https://github.com/antvis/S2/issues/2432)) ([7c4da43](https://github.com/antvis/S2/commit/7c4da435976076e5babe21012524694986286210)) ### Features @@ -12,16 +63,14 @@ # [@antv/s2-react-v2.0.0-next.8](https://github.com/antvis/S2/compare/@antv/s2-react-v2.0.0-next.7...@antv/s2-react-v2.0.0-next.8) (2023-11-22) - ### Features * headerActionIcons 支持细粒度配置 & 修复异步渲染导致无法获取实例的问题 ([#2301](https://github.com/antvis/S2/issues/2301)) ([b2d6f1f](https://github.com/antvis/S2/commit/b2d6f1fb04d3fa73129669fc7d2dec84943252db)) * **layout:** 单元格支持渲染多行文本 ([#2383](https://github.com/antvis/S2/issues/2383)) ([e3b919a](https://github.com/antvis/S2/commit/e3b919a4f37d600a0f516944edf4eed8b2c0174d)) * 支持 antd v5 ([#2413](https://github.com/antvis/S2/issues/2413)) ([299c7bf](https://github.com/antvis/S2/commit/299c7bfe2e86838153273c92dd6d2b72917cfdea)) -* 支持 React 18 (兼容 React 16/17) ([#2373](https://github.com/antvis/S2/issues/2373)) ([25ce9b0](https://github.com/antvis/S2/commit/25ce9b0ccc3e609d8add09b3209f6f981dc1dc4e)) +* 支持 React 18 (兼容 React 16/17) ([#2373](https://github.com/antvis/S2/issues/2373)) ([25ce9b0](https://github.com/antvis/S2/commit/25ce9b0ccc3e609d8add09b3209f6f981dc1dc4e)) * 支持自定义 G 5.0 插件和配置 ([#2423](https://github.com/antvis/S2/issues/2423)) ([cc6c47f](https://github.com/antvis/S2/commit/cc6c47fd0927125bbc378fe6914becfcbe1b0acd)) - ### BREAKING CHANGES * 移除 devicePixelRatio 和 supportsCSSTransform @@ -75,11 +124,11 @@ ### Features * 使用 requestIdleCallback 处理数据大量导出的情况 ([#2272](https://github.com/antvis/S2/issues/2272)) ([42a5551](https://github.com/antvis/S2/commit/42a55516dd369d9ab5579b52fbc9900b0ad81858)) -* 同步复制支持自定义transformer ([#2201](https://github.com/antvis/S2/issues/2201)) ([9003767](https://github.com/antvis/S2/commit/9003767d584248b9d122f299326fd14753961883)) +* 同步复制支持自定义 transformer ([#2201](https://github.com/antvis/S2/issues/2201)) ([9003767](https://github.com/antvis/S2/commit/9003767d584248b9d122f299326fd14753961883)) * 增加暗黑主题 ([#2130](https://github.com/antvis/S2/issues/2130)) ([51dbdcf](https://github.com/antvis/S2/commit/51dbdcf564b387a3fd1809a71016f3a91eebde38)) * 文本和图标的条件格式支持主题配置 ([#2267](https://github.com/antvis/S2/issues/2267)) ([c332c68](https://github.com/antvis/S2/commit/c332c687dfb7be1d07b79b44934f78c1947cc466)) * 行列头兼容 condition icon 和 action icons ([#2161](https://github.com/antvis/S2/issues/2161)) ([1df4286](https://github.com/antvis/S2/commit/1df42860f6a12d3cb182ba7633c4984a04e62890)) -* 适配g5.0异步渲染 ([#2251](https://github.com/antvis/S2/issues/2251)) ([069d03d](https://github.com/antvis/S2/commit/069d03d299429c2ffab3e20d56ecd6bb30119ffd)) +* 适配 g5.0 异步渲染 ([#2251](https://github.com/antvis/S2/issues/2251)) ([069d03d](https://github.com/antvis/S2/commit/069d03d299429c2ffab3e20d56ecd6bb30119ffd)) ### Performance Improvements @@ -97,10 +146,10 @@ * **interaction:** 点击角头后支持选中所对应那一列的行头 close [#2073](https://github.com/antvis/S2/issues/2073) ([#2081](https://github.com/antvis/S2/issues/2081)) ([ad2b5d8](https://github.com/antvis/S2/commit/ad2b5d87edf4c529d7c9a5e1348e893e14547ef3)) * **interaction:** 行头支持滚动刷选 ([#2087](https://github.com/antvis/S2/issues/2087)) ([65c3f3b](https://github.com/antvis/S2/commit/65c3f3b6a37709c0fa684b0f5717d3b349251e48)) -* 修改文档、添加用例演示、修改方法名drawLinkFieldShapLogic -> drawLinkField ([7f2bd69](https://github.com/antvis/S2/commit/7f2bd690bd703b8e4d678c03b9fc79db30848ca3)) -* 增加dataCell 下划线测试用例及demo ([a5efe17](https://github.com/antvis/S2/commit/a5efe17bda06cc8eba633cbea9c56ceb8b8c703e)) +* 修改文档、添加用例演示、修改方法名 drawLinkFieldShapLogic -> drawLinkField ([7f2bd69](https://github.com/antvis/S2/commit/7f2bd690bd703b8e4d678c03b9fc79db30848ca3)) +* 增加 dataCell 下划线测试用例及 demo ([a5efe17](https://github.com/antvis/S2/commit/a5efe17bda06cc8eba633cbea9c56ceb8b8c703e)) * 提取跳转链接下划线 公共逻辑 到 BaseCell 类 ([34dbbb3](https://github.com/antvis/S2/commit/34dbbb3bdf028cb96508dcead724d9ac9bcc1ab9)) -* 移除switcher按钮important样式 ([#2139](https://github.com/antvis/S2/issues/2139)) ([d4f9197](https://github.com/antvis/S2/commit/d4f9197f1c3d1b3cfc2bdfb8d375413a2daa79c3)) +* 移除 switcher 按钮 important 样式 ([#2139](https://github.com/antvis/S2/issues/2139)) ([d4f9197](https://github.com/antvis/S2/commit/d4f9197f1c3d1b3cfc2bdfb8d375413a2daa79c3)) # [@antv/s2-react-v2.0.0-next.5](https://github.com/antvis/S2/compare/@antv/s2-react-v2.0.0-next.4...@antv/s2-react-v2.0.0-next.5) (2023-04-23) @@ -112,8 +161,8 @@ ### Bug Fixes -* **layout:** 修复存在列总计但不存在列小计时, 隐藏其兄弟节点后单元格坐标偏移 close [#1993](https://github.com/antvis/S2/issues/1993) ([#2047](https://github.com/antvis/S2/issues/2047)) ([2ae663e](https://github.com/antvis/S2/commit/2ae663e1c46a3c8cb04b79d357fc033314f4cf77)) -* **layout:** 修复存在多列头多数值且数值置于行头时,列总计单元格高度不对 close [#1715](https://github.com/antvis/S2/issues/1715) [#2049](https://github.com/antvis/S2/issues/2049) ([#2051](https://github.com/antvis/S2/issues/2051)) ([a415f46](https://github.com/antvis/S2/commit/a415f465e8fa355a5b68d556f6fa645e3a72b5b7)) +* **layout:** 修复存在列总计但不存在列小计时,隐藏其兄弟节点后单元格坐标偏移 close [#1993](https://github.com/antvis/S2/issues/1993) ([#2047](https://github.com/antvis/S2/issues/2047)) ([2ae663e](https://github.com/antvis/S2/commit/2ae663e1c46a3c8cb04b79d357fc033314f4cf77)) +* **layout:** 修复存在多列头多数值且数值置于行头时,列总计单元格高度不对 close [#1715](https://github.com/antvis/S2/issues/1715) [#2049](https://github.com/antvis/S2/issues/2049) ([#2051](https://github.com/antvis/S2/issues/2051)) ([a415f46](https://github.com/antvis/S2/commit/a415f465e8fa355a5b68d556f6fa645e3a72b5b7)) * **layout:** 修复无列头时行头对应的角头不显示 close [#1929](https://github.com/antvis/S2/issues/1929) ([#2026](https://github.com/antvis/S2/issues/2026)) ([c073578](https://github.com/antvis/S2/commit/c073578dc008ef83a2877041830be18f827c7341)) ### Features @@ -125,8 +174,8 @@ ### Bug Fixes -* **layout:** 修复存在列总计但不存在列小计时, 隐藏其兄弟节点后单元格坐标偏移 close [#1993](https://github.com/antvis/S2/issues/1993) ([#2047](https://github.com/antvis/S2/issues/2047)) ([2ae663e](https://github.com/antvis/S2/commit/2ae663e1c46a3c8cb04b79d357fc033314f4cf77)) -* **layout:** 修复存在多列头多数值且数值置于行头时,列总计单元格高度不对 close [#1715](https://github.com/antvis/S2/issues/1715) [#2049](https://github.com/antvis/S2/issues/2049) ([#2051](https://github.com/antvis/S2/issues/2051)) ([a415f46](https://github.com/antvis/S2/commit/a415f465e8fa355a5b68d556f6fa645e3a72b5b7)) +* **layout:** 修复存在列总计但不存在列小计时,隐藏其兄弟节点后单元格坐标偏移 close [#1993](https://github.com/antvis/S2/issues/1993) ([#2047](https://github.com/antvis/S2/issues/2047)) ([2ae663e](https://github.com/antvis/S2/commit/2ae663e1c46a3c8cb04b79d357fc033314f4cf77)) +* **layout:** 修复存在多列头多数值且数值置于行头时,列总计单元格高度不对 close [#1715](https://github.com/antvis/S2/issues/1715) [#2049](https://github.com/antvis/S2/issues/2049) ([#2051](https://github.com/antvis/S2/issues/2051)) ([a415f46](https://github.com/antvis/S2/commit/a415f465e8fa355a5b68d556f6fa645e3a72b5b7)) * **layout:** 修复无列头时行头对应的角头不显示 close [#1929](https://github.com/antvis/S2/issues/1929) ([#2026](https://github.com/antvis/S2/issues/2026)) ([c073578](https://github.com/antvis/S2/commit/c073578dc008ef83a2877041830be18f827c7341)) ### Features @@ -137,7 +186,7 @@ ### Bug Fixes -* 列头label存在数组,复制导出列头层级补齐错误 ([#1990](https://github.com/antvis/S2/issues/1990)) ([ec62409](https://github.com/antvis/S2/commit/ec62409b688c5dd5e39a93f5b292d909496ed830)) +* 列头 label 存在数组,复制导出列头层级补齐错误 ([#1990](https://github.com/antvis/S2/issues/1990)) ([ec62409](https://github.com/antvis/S2/commit/ec62409b688c5dd5e39a93f5b292d909496ed830)) ### Features @@ -148,7 +197,7 @@ ### Bug Fixes * **interaction:** 修复自定义列头时无法调整第一列的叶子节点高度 close [#1979](https://github.com/antvis/S2/issues/1979) ([#2038](https://github.com/antvis/S2/issues/2038)) ([a632ab1](https://github.com/antvis/S2/commit/a632ab19193b19ab80f456ab3ce19740dce0e52b)) -* 列头label存在数组,复制导出列头层级补齐错误 ([#1990](https://github.com/antvis/S2/issues/1990)) ([ec62409](https://github.com/antvis/S2/commit/ec62409b688c5dd5e39a93f5b292d909496ed830)) +* 列头 label 存在数组,复制导出列头层级补齐错误 ([#1990](https://github.com/antvis/S2/issues/1990)) ([ec62409](https://github.com/antvis/S2/commit/ec62409b688c5dd5e39a93f5b292d909496ed830)) ### Code Refactoring @@ -159,11 +208,11 @@ * selected cell highlight ([#1878](https://github.com/antvis/S2/issues/1878)) ([3e11a37](https://github.com/antvis/S2/commit/3e11a37bf94f758379ba2819ec5d8b3251708814)) * 单元格宽高配置增强 close [#1895](https://github.com/antvis/S2/issues/1895) ([#1981](https://github.com/antvis/S2/issues/1981)) ([ec6736f](https://github.com/antvis/S2/commit/ec6736f108801e1129c4d3fd29d13d1fbff2a1d2)) * 折叠展开重构 & 简化行头 tree 相关配置 ([#2030](https://github.com/antvis/S2/issues/2030)) ([0f3ea3b](https://github.com/antvis/S2/commit/0f3ea3b5c668137bc2fcb53bd186a41b34140e25)) -* 暴露afterRealCellRender,这样能够更灵活的使用datacell ([#1970](https://github.com/antvis/S2/issues/1970)) ([66c5ab9](https://github.com/antvis/S2/commit/66c5ab9992c51b475be8acaf9a198d49f3114a49)) +* 暴露 afterRealCellRender,这样能够更灵活的使用 datacell ([#1970](https://github.com/antvis/S2/issues/1970)) ([66c5ab9](https://github.com/antvis/S2/commit/66c5ab9992c51b475be8acaf9a198d49f3114a49)) ### BREAKING CHANGES -* s2Options.tooltip 和 s2Options.style API 命名更改, 移除 trend 操作项 +* s2Options.tooltip 和 s2Options.style API 命名更改,移除 trend 操作项 * refactor: tree 相关配置移动到 rowCell 下 @@ -226,13 +275,6 @@ * 2.0-next * 2.0 * -======= - -# [@antv/s2-react-v1.32.0](https://github.com/antvis/S2/compare/@antv/s2-react-v1.31.0...@antv/s2-react-v1.32.0) (2022-11-21) - -### Features - -* 明细表支持多级表头 ([#1921](https://github.com/antvis/S2/issues/1921)) ([47cdbdc](https://github.com/antvis/S2/commit/47cdbdccafbd7f19a05550a483a42aac11a93778)), closes [#1687](https://github.com/antvis/S2/issues/1687) [#1801](https://github.com/antvis/S2/issues/1801) ### Features diff --git a/packages/s2-react/README.md b/packages/s2-react/README.md index 18aa74b5e1..8881ab1bc7 100644 --- a/packages/s2-react/README.md +++ b/packages/s2-react/README.md @@ -106,7 +106,7 @@ const s2DataConfig = { ```ts const s2Options = { width: 600, - height: 480, + height: 480 } ``` @@ -118,18 +118,17 @@ const s2Options = {
``` -```ts +```tsx +import ReactDOM from 'react-dom' import { SheetComponent } from '@antv/s2-react'; import '@antv/s2-react/dist/style.min.css'; -const container = document.getElementById('container'); - ReactDOM.render( , - document.getElementById('container'), + document.getElementById('container') ); ``` diff --git a/packages/s2-react/__tests__/spreadsheet/drill-down-spec.tsx b/packages/s2-react/__tests__/spreadsheet/drill-down-spec.tsx index b50db9179b..bc3f5ffe52 100644 --- a/packages/s2-react/__tests__/spreadsheet/drill-down-spec.tsx +++ b/packages/s2-react/__tests__/spreadsheet/drill-down-spec.tsx @@ -1,4 +1,4 @@ -import { customMerge, GuiIcon, Node, RowCell, SpreadSheet } from '@antv/s2'; +import { customMerge, Node, SpreadSheet } from '@antv/s2'; import { waitFor } from '@testing-library/react'; import { get, noop } from 'lodash'; import React from 'react'; @@ -32,18 +32,13 @@ const partDrillDownParams: SheetComponentsProps['partDrillDown'] = { }), }; -const findDrillDownIcon = (instance: SpreadSheet) => { - const rowHeaderActionIcons = get( - (instance.facet.rowHeader?.children as RowCell[]).find( - (item) => item.getActualText() === '杭州', - ), - 'actionIcons', - [], - ); - - return rowHeaderActionIcons.find( - (icon: GuiIcon) => get(icon, 'cfg.name') === 'DrillDownIcon', - ); +const findDrillDownIcon = (s2: SpreadSheet) => { + const rowHeaderActionIcons = s2.facet + ?.getRowCells() + ?.find((cell) => cell.getActualText() === '杭州') + ?.getActionIcons(); + + return rowHeaderActionIcons?.find((icon) => icon.name === 'DrillDownIcon'); }; describe('Spread Sheet Drill Down Tests', () => { diff --git a/packages/s2-react/__tests__/spreadsheet/filter-sheet-spec.tsx b/packages/s2-react/__tests__/spreadsheet/filter-sheet-spec.tsx index 80dad59db9..d92a210ab7 100644 --- a/packages/s2-react/__tests__/spreadsheet/filter-sheet-spec.tsx +++ b/packages/s2-react/__tests__/spreadsheet/filter-sheet-spec.tsx @@ -1,43 +1,22 @@ /* eslint-disable no-console */ -import { - DeviceType, - S2Event, - SpreadSheet, - TableSheet, - type S2DataConfig, - type S2MountContainer, - type S2Options, -} from '@antv/s2'; +import { DeviceType, S2Event, SpreadSheet, type S2DataConfig } from '@antv/s2'; import { Button, Space } from 'antd'; import React from 'react'; -import { act } from 'react-dom/test-utils'; -import { getMockData, renderComponent, sleep } from '../util/helpers'; +import { waitFor } from '@testing-library/react'; +import type { Root } from 'react-dom/client'; import { - SheetComponent, - type SheetComponentOptions, - type SheetComponentsProps, -} from '@/components'; + getContainer, + getMockData, + renderComponent, + sleep, +} from '../util/helpers'; +import { SheetComponent, type SheetComponentOptions } from '@/components'; const data = getMockData('../data/tableau-supermarket.csv'); -let spreadSheet: SpreadSheet; - -const onMounted = - (ref: React.MutableRefObject) => - ( - dom: S2MountContainer, - dataCfg: S2DataConfig, - options: SheetComponentsProps['options'], - ) => { - const s2 = new TableSheet(dom, dataCfg, options as S2Options); - - ref.current = s2; - spreadSheet = s2; - - return s2; - }; +let s2: SpreadSheet; -const columns = [ +const columns: string[] = [ 'order_id', 'order_date', 'ship_date', @@ -55,9 +34,9 @@ const columns = [ 'count', 'discount', 'profit', -] as const; +]; -const meta = [ +const meta: S2DataConfig['meta'] = [ { field: 'count', name: '销售个数', @@ -75,7 +54,7 @@ function MainLayout() { }, meta, data, - } as unknown as S2DataConfig; + }; const options: SheetComponentOptions = { width: 800, @@ -84,6 +63,7 @@ function MainLayout() { device: DeviceType.PC, interaction: { enableCopy: true, + linkFields: ['order_id', 'customer_name'], }, style: { dataCell: { @@ -102,7 +82,7 @@ function MainLayout() { }, }; - const s2Ref = React.useRef(undefined); + const s2Ref = React.useRef(null); return ( @@ -116,7 +96,6 @@ function MainLayout() { > Filter - { + s2 = spreadsheet; + }} /> ); } describe('table sheet filter spec', () => { - renderComponent(); + let container: HTMLDivElement; + let unmount: Root['unmount']; - test('filter customer_type values', async () => { - spreadSheet.emit(S2Event.RANGE_FILTER, { - filterKey: 'customer_type', - filteredValues: ['消费者'], - }); + const filterKey = 'customer_type'; + const filteredValue = '消费者'; - await sleep(50); + beforeEach(() => { + container = getContainer(); - expect(spreadSheet.facet.getCellRange()).toStrictEqual({ - end: 467, - start: 0, - }); + unmount = renderComponent(); }); - test('reset filter params on customer_type', async () => { - spreadSheet.emit(S2Event.RANGE_FILTER, { - filterKey: 'customer_type', - filteredValues: ['消费者'], - }); + afterEach(() => { + container?.remove(); + unmount?.(); + }); + + test('filter customer_type values', async () => { + await waitFor(() => { + s2.emit(S2Event.RANGE_FILTER, { + filterKey, + filteredValues: [filteredValue], + }); - spreadSheet.emit(S2Event.RANGE_FILTER, { - filterKey: 'customer_type', - filteredValues: [], + expect(s2.facet.getCellRange()).toStrictEqual({ + end: 465, + start: 0, + }); + expect(s2.dataSet.getDisplayDataSet()).toHaveLength(466); + expect( + s2.dataSet + .getDisplayDataSet() + .some((item) => item['customer_type'] === filteredValue), + ).toBeFalsy(); }); + }); - await sleep(50); + test('reset filter params on customer_type', async () => { + await waitFor(() => { + s2.emit(S2Event.RANGE_FILTER, { + filterKey, + filteredValues: [filteredValue], + }); - expect(spreadSheet.facet.getCellRange()).toStrictEqual({ - end: 999, - start: 0, + s2.emit(S2Event.RANGE_FILTER, { + filterKey, + filteredValues: [], + }); + + expect(s2.facet.getCellRange()).toStrictEqual({ + end: 999, + start: 0, + }); + expect(s2.dataSet.getDisplayDataSet()).toHaveLength(1000); }); }); test('filtered event fired with new data', async () => { - let dataLength = 0; - - spreadSheet.on(S2Event.RANGE_FILTERED, (data) => { - dataLength = data.length; - }); + await waitFor(async () => { + let dataLength = 0; + + s2.on(S2Event.RANGE_FILTERED, (data) => { + dataLength = data.length; + expect(data.length).toStrictEqual(466); + expect(s2.dataSet.getDisplayDataSet()).toHaveLength(466); + expect( + s2.dataSet + .getDisplayDataSet() + .some((item) => item['customer_type'] === filteredValue), + ).toBeFalsy(); + }); - spreadSheet.emit(S2Event.RANGE_FILTER, { - filterKey: 'customer_type', - filteredValues: ['消费者'], - }); + s2.emit(S2Event.RANGE_FILTER, { + filterKey, + filteredValues: [filteredValue], + }); - await sleep(50); + await sleep(50); - expect(dataLength).toStrictEqual(468); + expect(dataLength).toStrictEqual(466); + }); }); test('falsy/nullish data should not be filtered with irrelevant filter params', async () => { - let dataLength = 0; + await waitFor(async () => { + let dataLength = 0; - spreadSheet.on(S2Event.RANGE_FILTERED, (data) => { - dataLength = data.length; - }); + s2.on(S2Event.RANGE_FILTERED, (data) => { + dataLength = data.length; + expect(data.length).toStrictEqual(1000); + expect(s2.dataSet.getDisplayDataSet()).toHaveLength(1000); + }); - act(() => { - spreadSheet.emit(S2Event.RANGE_FILTER, { + s2.emit(S2Event.RANGE_FILTER, { filterKey: 'express_type', filteredValues: ['消费者'], }); - }); - await sleep(50); + await sleep(200); - expect(dataLength).toStrictEqual(468); + expect(dataLength).toStrictEqual(1000); + }); }); }); diff --git a/packages/s2-react/__tests__/spreadsheet/pagination-spec.tsx b/packages/s2-react/__tests__/spreadsheet/pagination-spec.tsx index 34ea40d934..3517077d08 100644 --- a/packages/s2-react/__tests__/spreadsheet/pagination-spec.tsx +++ b/packages/s2-react/__tests__/spreadsheet/pagination-spec.tsx @@ -1,7 +1,14 @@ -import { SpreadSheet, setLang, type LangType, type Pagination } from '@antv/s2'; +import { + SpreadSheet, + setLang, + type LangType, + type Pagination, + type S2DataConfig, +} from '@antv/s2'; import { waitFor } from '@testing-library/react'; import React from 'react'; import type { Root } from 'react-dom/client'; +import { pivotSheetDataCfg } from '../../playground/config'; import { SheetComponent, type SheetComponentsProps } from '../../src'; import * as mockDataConfig from '../data/simple-data.json'; import { renderComponent } from '../util/helpers'; @@ -16,6 +23,8 @@ const s2Options: SheetComponentsProps['options'] = { hierarchyType: 'grid', }; +let s2: SpreadSheet; + describe('Pagination Tests', () => { let unmount: Root['unmount']; @@ -45,7 +54,7 @@ describe('Pagination Tests', () => { unmount = renderComponent( { spreadsheet = instance; @@ -79,7 +88,7 @@ describe('Pagination Tests', () => { showQuickJumper: true, } as Pagination, }} - dataCfg={mockDataConfig as any} + dataCfg={mockDataConfig as S2DataConfig} showPagination onMounted={(instance) => { spreadsheet = instance; @@ -97,4 +106,31 @@ describe('Pagination Tests', () => { ).toBeFalsy(); }); }); + + test('should row header cell render text position based on the actual cell height when pagination is show', async () => { + renderComponent( + { + s2 = instance; + }} + showPagination + />, + ); + + await waitFor(() => { + const rowCell = s2.facet.getRowCells()[0]; + + expect(rowCell.getTextShape().parsedStyle.y).toBe(15); + }); + }); }); diff --git a/packages/s2-react/__tests__/unit/components/sheets/strategy-sheet/__snapshots__/index-spec.tsx.snap b/packages/s2-react/__tests__/unit/components/sheets/strategy-sheet/__snapshots__/index-spec.tsx.snap index cb8da06994..38c5b643e0 100644 --- a/packages/s2-react/__tests__/unit/components/sheets/strategy-sheet/__snapshots__/index-spec.tsx.snap +++ b/packages/s2-react/__tests__/unit/components/sheets/strategy-sheet/__snapshots__/index-spec.tsx.snap @@ -15,6 +15,36 @@ exports[` Tests StrategySheet Export Tests should export correct 指标E 自定义节点D " `; +exports[` Tests StrategySheet Export Tests should export correct data for custom corner text 1`] = ` +" 日期 2022-09 2022-10 2022-11 2021年净增完成度 趋势 2022 + 指标 数值 环比 同比 数值 环比 数值 环比 同比 净增完成度 趋势 数值 环比 +自定义节点A - +自定义节点A 指标A 377 3877 4324 42% - - 377 +自定义节点A 指标A 指标B 377 324 377 324 -0.02 - - 377 324 +自定义节点A 指标A 自定义节点B +自定义节点A 指标A 指标C 324 377 0 - - 324 +自定义节点A 指标A 指标D 377 324 377 324 0.02 - - 377 324 +自定义节点A 自定义节点E +指标E 377 324 0.02 - - +指标E 自定义节点C +指标E 自定义节点D " +`; + +exports[` Tests StrategySheet Export Tests should export correct data for default corner text 1`] = ` +" 日期 2022-09 2022-10 2022-11 2021年净增完成度 趋势 2022 + 指标 数值 环比 同比 数值 环比 数值 环比 同比 净增完成度 趋势 数值 环比 +自定义节点A - +自定义节点A 指标A 377 3877 4324 42% - - 377 +自定义节点A 指标A 指标B 377 324 377 324 -0.02 - - 377 324 +自定义节点A 指标A 自定义节点B +自定义节点A 指标A 指标C 324 377 0 - - 324 +自定义节点A 指标A 指标D 377 324 377 324 0.02 - - 377 324 +自定义节点A 自定义节点E +指标E 377 324 0.02 - - +指标E 自定义节点C +指标E 自定义节点D " +`; + exports[` Tests StrategySheet Export Tests should export correct data for empty cell 1`] = ` " 日期 2022-09 2022-10 2022-11 2021年净增完成度 趋势 2022 指标 数值 环比 同比 数值 环比 数值 环比 同比 净增完成度 趋势 数值 环比 diff --git a/packages/s2-react/__tests__/unit/components/sheets/strategy-sheet/custom-tooltip/index-spec.tsx b/packages/s2-react/__tests__/unit/components/sheets/strategy-sheet/custom-tooltip/index-spec.tsx index c45a84b423..f25ae6607a 100644 --- a/packages/s2-react/__tests__/unit/components/sheets/strategy-sheet/custom-tooltip/index-spec.tsx +++ b/packages/s2-react/__tests__/unit/components/sheets/strategy-sheet/custom-tooltip/index-spec.tsx @@ -72,4 +72,28 @@ describe('StrategySheet Tooltip Tests', () => { expect(screen.getAllByText('customDerivedValue')).toHaveLength(3); expect(screen.getAllByText('customDerivedValue')).toMatchSnapshot(); }); + + // cli 的方式得到的结果是错, 基于 test:live 就是对的, 不知道为啥 + test.skip('should render overflow wrap description for row tooltip', () => { + const description = `test_`.repeat(40); + const mockDescCellInfo = createMockCellInfo('test', { + extra: { + description, + }, + }); + + const { container } = render( + , + ); + + const { width, height } = container! + .querySelector('.s2-strategy-sheet-tooltip-description-text')! + .getBoundingClientRect(); + + expect(Math.floor(width)).toBeCloseTo(937); + expect(Math.floor(height)).toBeCloseTo(37); + }); }); diff --git a/packages/s2-react/__tests__/unit/components/sheets/strategy-sheet/index-spec.tsx b/packages/s2-react/__tests__/unit/components/sheets/strategy-sheet/index-spec.tsx index 3c81121e27..b63fbc5ebf 100644 --- a/packages/s2-react/__tests__/unit/components/sheets/strategy-sheet/index-spec.tsx +++ b/packages/s2-react/__tests__/unit/components/sheets/strategy-sheet/index-spec.tsx @@ -1,29 +1,29 @@ /* eslint-disable max-classes-per-file */ import { CellType, - customMerge, - getCellMeta, + CornerNodeType, InteractionStateName, + RowCell, SpreadSheet, - type S2DataConfig, + customMerge, + getCellMeta, type GEvent, - RowCell, + type S2DataConfig, } from '@antv/s2'; -import React from 'react'; -import { get } from 'lodash'; import { waitFor } from '@testing-library/react'; -import { getContainer, renderComponent } from '../../../../util/helpers'; +import React from 'react'; import { - StrategySheetDataConfig, StrategyOptions, + StrategySheetDataConfig, } from '../../../../data/strategy-data'; +import { getContainer, renderComponent } from '../../../../util/helpers'; +import { strategyCopy } from '@/components/export/strategy-copy'; import { SheetComponent, StrategySheetColCell, StrategySheetDataCell, type SheetComponentOptions, } from '@/components'; -import { strategyCopy } from '@/components/export/strategy-copy'; describe(' Tests', () => { let s2: SpreadSheet; @@ -132,6 +132,28 @@ describe(' Tests', () => { }, ); + test('should get current cell custom tooltip content', () => { + renderStrategySheet({ + tooltip: { + enable: true, + rowCell: { + content: () =>
{CellType.ROW_CELL}
, + }, + dataCell: { + content: () =>
{CellType.DATA_CELL}
, + }, + }, + }); + + jest.spyOn(s2, 'getCellType').mockReturnValueOnce(CellType.COL_CELL); + + s2.showTooltipWithInfo({} as GEvent, []); + + [CellType.ROW_CELL, CellType.DATA_CELL].forEach((content) => { + expect(s2.tooltip.container!.innerText).not.toEqual(content); + }); + }); + test('should render correctly KPI bullet column measure text', async () => { renderStrategySheet( { @@ -167,6 +189,37 @@ describe(' Tests', () => { }); }); + test('should get custom corner extra field text', async () => { + const cornerExtraFieldText = '自定义'; + const s2DataCfg: Partial = { + fields: { + ...StrategySheetDataConfig.fields, + valueInCols: false, + }, + }; + + const s2Options: SheetComponentOptions = { + cornerExtraFieldText, + }; + + renderStrategySheet(s2Options, { + ...StrategySheetDataConfig, + ...s2DataCfg, + }); + + await waitFor(() => { + const cornerNode = s2.facet + .getCornerNodes() + .find((node) => node.cornerType === CornerNodeType.Row); + + const textList = s2.facet.getCornerNodes().map((node) => node.value); + const cornerText = `自定义节点A/指标E/${cornerExtraFieldText}`; + + expect(textList).toEqual([cornerText, '日期']); + expect(cornerNode!.value).toEqual(cornerText); + }); + }); + test('should format corner date field', async () => { renderStrategySheet( { @@ -185,11 +238,11 @@ describe(' Tests', () => { ); await waitFor(() => { - const textList = s2.facet.cornerHeader.children.map((element) => - get(element, 'actualText'), - ); + const cornerTextList = s2.facet + .getCornerCells() + .map((cell) => cell.getActualText()); - expect(textList).toEqual(['自定义节点A/指标E/指标', '日期']); + expect(cornerTextList).toEqual(['自定义节点A/指标E/数值', '日期']); }); }); @@ -212,11 +265,11 @@ describe(' Tests', () => { ); await waitFor(() => { - const textList = s2.facet.cornerHeader.children.map((element) => - get(element, 'actualText'), - ); + const cornerTextList = s2.facet + .getCornerCells() + .map((cell) => cell.getActualText()); - expect(textList).toEqual(['测试', '日期']); + expect(cornerTextList).toEqual(['测试', '日期']); }); }); @@ -265,6 +318,30 @@ describe(' Tests', () => { }); }); + test('should export correct data for default corner text', async () => { + await waitFor(() => { + s2.setOptions({ + cornerText: undefined, + }); + + const result = strategyCopy(s2, '\t', true); + + expect(result).toMatchSnapshot(); + }); + }); + + test('should export correct data for custom corner text', async () => { + await waitFor(() => { + s2.setOptions({ + cornerText: '自定义', + }); + + const result = strategyCopy(s2, '\t', true); + + expect(result).toMatchSnapshot(); + }); + }); + test('should export correct data for multi different cycle compare data', async () => { await waitFor(() => { /* diff --git a/packages/s2-react/__tests__/unit/components/tooltip/__snapshots__/index-spec.tsx.snap b/packages/s2-react/__tests__/unit/components/tooltip/__snapshots__/index-spec.tsx.snap index 7958b81bef..36062f9a4a 100644 --- a/packages/s2-react/__tests__/unit/components/tooltip/__snapshots__/index-spec.tsx.snap +++ b/packages/s2-react/__tests__/unit/components/tooltip/__snapshots__/index-spec.tsx.snap @@ -28,7 +28,7 @@ exports[`Tooltip Common Components Tests render TooltipHead 1`] = `
- 一二线城市,有信用卡 / 学生 / 20岁以下 + 一二线城市,有信用卡/学生/20岁以下
`; @@ -54,7 +54,7 @@ exports[`Tooltip Common Components Tests render TooltipSummary 1`] = ` - A人群 (总和) + A人群(总和) - B人群 (总和) + B人群(总和) - 差值 (总和) + 差值(总和) { }); test('render TooltipDetail', () => { - const list = [ + const list: TooltipDetailListItem[] = [ { name: '20岁以下', value: '20.5%', @@ -173,8 +175,11 @@ describe('Tooltip Common Components Tests', () => { }); test('render TooltipHead', () => { - const cols = [{ name: '所在城市', value: '一二线城市' }]; - const rows = [ + const cols: TooltipDetailListItem[] = [ + { name: '所在城市', value: '一二线城市' }, + ]; + + const rows: TooltipDetailListItem[] = [ { name: '类别', value: '有信用卡' }, { name: '职业', value: '学生' }, { name: '年龄分布', value: '20岁以下' }, @@ -185,11 +190,11 @@ describe('Tooltip Common Components Tests', () => { ); expect(asFragment()).toMatchSnapshot(); - expect(getByText('一二线城市,有信用卡 / 学生 / 20岁以下')).toBeTruthy(); + expect(getByText('一二线城市,有信用卡/学生/20岁以下')).toBeTruthy(); }); test('render TooltipSummary', () => { - const summaries = [ + const summaries: TooltipSummaryOptions[] = [ { name: 'A人群', selectedData: Array(30), value: '495.48 %' }, { name: 'B人群', selectedData: Array(30), value: '494.52%' }, { name: '差值', selectedData: Array(30), value: '+381%' }, diff --git a/packages/s2-react/__tests__/unit/hooks/useEvents-spec.ts b/packages/s2-react/__tests__/unit/hooks/useEvents-spec.ts index 9ab2db6c48..d39a4ddd3c 100644 --- a/packages/s2-react/__tests__/unit/hooks/useEvents-spec.ts +++ b/packages/s2-react/__tests__/unit/hooks/useEvents-spec.ts @@ -1,14 +1,14 @@ -import { renderHook, act } from '@testing-library/react-hooks'; import { + GEvent, PivotSheet, S2Event, SpreadSheet, - GEvent, type S2DataConfig, type S2Options, } from '@antv/s2'; -import { createMockCellInfo, getContainer } from 'tests/util/helpers'; +import { act, renderHook } from '@testing-library/react-hooks'; import * as mockDataConfig from 'tests/data/simple-data.json'; +import { createMockCellInfo, getContainer } from 'tests/util/helpers'; import type { SheetComponentsProps } from '../../../src/components'; import { useCellEvent, useEvents, useS2Event } from '@/hooks'; diff --git a/packages/s2-react/__tests__/util/helpers.ts b/packages/s2-react/__tests__/util/helpers.ts index cc03b16f16..2dc7385da4 100644 --- a/packages/s2-react/__tests__/util/helpers.ts +++ b/packages/s2-react/__tests__/util/helpers.ts @@ -45,7 +45,7 @@ export function getMockSheetInstance(Sheet: typeof SpreadSheet = PivotSheet) { export const createMockCellInfo = ( cellId: string, - { colIndex = 0, rowIndex = 0 } = {}, + { colIndex = 0, rowIndex = 0, extra = {} } = {}, ) => { const mockCellViewMeta: Partial = { id: cellId, @@ -75,6 +75,7 @@ export const createMockCellInfo = ( getFieldName: jest.fn(), }, } as unknown as SpreadSheet, + extra, }; const mockCellMeta = omit(mockCellViewMeta, 'update'); const mockCell = { diff --git a/packages/s2-react/package.json b/packages/s2-react/package.json index c0729d5a72..c14e701d09 100644 --- a/packages/s2-react/package.json +++ b/packages/s2-react/package.json @@ -44,10 +44,11 @@ "build:umd": "cross-env FORMAT=umd vite build", "build:analysis": "cross-env FORMAT=es ANALYSIS=true vite build", "build:dts": "run-s dts:*", + "build:size-limit": "size-limit", + "build:size-limit-json": "pnpm build:size-limit --json", "watch": "rimraf esm && pnpm build:esm -w", "dts:build": "tsc -p tsconfig.declaration.json", "dts:extract": "cross-env LIB=s2-react node ../../scripts/dts.js", - "bundle:size": "bundlesize", "test": "jest --passWithNoTests", "test:coverage": "pnpm test -- --coverage", "test:ci": "pnpm test -- --maxWorkers=3", @@ -93,14 +94,15 @@ "react-dom": "^18.2.0", "vite-plugin-svgr": "^2.2.2" }, - "bundlesize": [ + "size-limit": [ { "path": "./dist/index.min.js", - "maxSize": "650 kB" + "import": "{ createComponent }", + "limit": "70 kB" }, { "path": "./dist/style.min.css", - "maxSize": "205 kB" + "limit": "5 kB" } ], "publishConfig": { diff --git a/packages/s2-react/playground/components/GridAnalysisSheet.tsx b/packages/s2-react/playground/components/GridAnalysisSheet.tsx index 654029aaee..173240e931 100644 --- a/packages/s2-react/playground/components/GridAnalysisSheet.tsx +++ b/packages/s2-react/playground/components/GridAnalysisSheet.tsx @@ -1,5 +1,6 @@ import { isUpDataValue, SpreadSheet } from '@antv/s2'; import React from 'react'; +import { LayoutWidthType } from '@antv/s2'; import { SheetComponent, type SheetComponentOptions, @@ -15,7 +16,7 @@ export const mockGridAnalysisOptions: SheetComponentOptions = { selectedCellsSpotlight: true, }, style: { - layoutWidthType: 'colAdaptive', + layoutWidthType: LayoutWidthType.ColAdaptive, rowCell: { width: 80, height: 100, diff --git a/packages/s2-react/playground/config.tsx b/packages/s2-react/playground/config.tsx index 54006e552d..38bf9ddfb7 100644 --- a/packages/s2-react/playground/config.tsx +++ b/packages/s2-react/playground/config.tsx @@ -7,6 +7,8 @@ import { type CustomTreeNode, type S2DataConfig, type S2TableSheetFrozenOptions, + customMerge, + type ThemeCfg, } from '@antv/s2'; import { getBaseSheetComponentOptions } from '@antv/s2-shared'; import type { SliderSingleProps } from 'antd'; @@ -74,6 +76,39 @@ export const pivotSheetDataCfg: S2DataConfig = { export const pivotSheetMultiLineTextDataCfg = PivotSheetMultiLineTextDataCfg; +export const pivotSheetDataCfgForCompactMode = customMerge( + pivotSheetDataCfg, + { + data: [ + ...pivotSheetDataCfg.data, + { + province: '浙江', + city: '杭州', + type: '笔', + price: '11111111', + }, + { + province: '浙江', + city: '杭州', + type: '纸张', + price: '2', + }, + { + province: '浙江', + city: '舟山', + type: '笔', + price: '2', + }, + { + province: '浙江', + city: '舟山', + type: '纸张', + price: '133.333', + }, + ], + }, +); + export const s2ConditionsOptions: SheetComponentOptions['conditions'] = { text: [ { @@ -292,6 +327,8 @@ export const s2Options: SheetComponentOptions = { interaction: { enableCopy: true, copyWithFormat: true, + copyWithHeader: true, + hoverAfterScroll: true, // 防止 mac 触摸板横向滚动触发浏览器返回, 和移动端下拉刷新 overscrollBehavior: 'none', brushSelection: { @@ -300,7 +337,7 @@ export const s2Options: SheetComponentOptions = { rowCell: true, }, resize: { - rowResizeType: ResizeType.CURRENT, + rowResizeType: ResizeType.ALL, colResizeType: ResizeType.CURRENT, }, }, @@ -346,5 +383,10 @@ export const sliderOptions: SliderSingleProps = { }, }; +export const s2ThemeConfig: ThemeCfg = { + name: 'default', + theme: {}, +}; + export const defaultOptions = getBaseSheetComponentOptions(s2Options); diff --git a/packages/s2-react/playground/drill-down.tsx b/packages/s2-react/playground/drill-down.tsx index bcdc8c02ad..88b87355f3 100644 --- a/packages/s2-react/playground/drill-down.tsx +++ b/packages/s2-react/playground/drill-down.tsx @@ -1,4 +1,4 @@ -import type { PivotDataSet, RawData } from '@antv/s2'; +import { ORIGIN_FIELD, type PivotDataSet, type RawData } from '@antv/s2'; import type { PartDrillDownInfo } from '@antv/s2-shared'; import { forEach, random } from 'lodash'; import React from 'react'; @@ -29,15 +29,12 @@ export const partDrillDown: PartDrillDown = { fetchData: (meta, drillFields) => new Promise((resolve) => { // 弹窗 -> 选择 -> 请求数据 - const preDrillDownfield = - meta.spreadsheet.store.get('drillDownNode')?.field; const dataSet = meta.spreadsheet.dataSet as PivotDataSet; const field = drillFields[0]; const rowData = dataSet .getCellMultiData({ query: meta.query!, - drillDownFields: [preDrillDownfield], }) .filter( (item) => @@ -47,7 +44,7 @@ export const partDrillDown: PartDrillDown = { const drillDownData: RawData[] = []; forEach(rowData, (data) => { - const { number, sub_type: subType, type } = data.getOrigin(); + const { number, sub_type: subType, type } = data[ORIGIN_FIELD]; const number0 = random(50, number as number); const number1 = (number as number) - number0; const dataItem0: RawData = { diff --git a/packages/s2-react/playground/index.html b/packages/s2-react/playground/index.html index 13576bda2a..901bfef23c 100644 --- a/packages/s2-react/playground/index.html +++ b/packages/s2-react/playground/index.html @@ -8,6 +8,5 @@
- - \ No newline at end of file + diff --git a/packages/s2-react/playground/index.tsx b/packages/s2-react/playground/index.tsx index f1b4a295b5..722ce26b66 100644 --- a/packages/s2-react/playground/index.tsx +++ b/packages/s2-react/playground/index.tsx @@ -12,7 +12,7 @@ import { getPalette, type CustomHeaderFields, type HeaderActionIconProps, - type InteractionCellSelectedHighlightOptions, + type InteractionCellHighlightOptions, type InteractionOptions, type S2DataConfig, type TargetCellInfo, @@ -48,22 +48,25 @@ import reactPkg from '../package.json'; import type { SheetComponentOptions } from '../src'; import { SheetComponent } from '../src'; import { ConfigProvider } from '../src/components/config-provider'; +import { ChartSheet } from './components/ChartSheet'; import { CustomGrid } from './components/CustomGrid'; import { CustomTree } from './components/CustomTree'; import { EditableSheet } from './components/EditableSheet'; import { GridAnalysisSheet } from './components/GridAnalysisSheet'; +import { LinkGroup } from './components/LinkGroup'; import { MobileSheetComponent } from './components/Mobile'; import { PluginsSheet } from './components/Plugins'; import { ResizeConfig } from './components/ResizeConfig'; import { StrategySheet } from './components/StrategySheet'; -import { LinkGroup } from './components/LinkGroup'; import { TableSheetFrozenOptions, defaultOptions, pivotSheetDataCfg, + pivotSheetDataCfgForCompactMode, pivotSheetMultiLineTextDataCfg, s2ConditionsOptions, s2Options, + s2ThemeConfig, sliderOptions, tableSheetDataCfg, tableSheetMultipleColumns, @@ -71,12 +74,18 @@ import { } from './config'; import { PlaygroundContext } from './context/playground.context'; import { partDrillDown } from './drill-down'; -import { onSheetMounted } from './utils'; -import { ChartSheet } from './components/ChartSheet'; import './index.less'; type TableSheetColumnType = 'single' | 'multiple'; +const onSheetMounted = (s2: SpreadSheet) => { + console.log('onSheetMounted: ', s2); + // @ts-ignore + window.s2 = s2; + // @ts-ignore + window.g_instances = [s2.container]; +}; + const CustomTooltip = () => (
自定义 Tooltip
1
@@ -99,9 +108,7 @@ function MainLayout() { ); const [showPagination, setShowPagination] = React.useState(false); const [showTotals, setShowTotals] = React.useState(false); - const [themeCfg, setThemeCfg] = React.useState({ - name: 'default', - }); + const [themeCfg, setThemeCfg] = React.useState(s2ThemeConfig); const [themeColor, setThemeColor] = React.useState('#FFF'); const [showCustomTooltip, setShowCustomTooltip] = React.useState(false); const [adaptive, setAdaptive] = React.useState(false); @@ -269,10 +276,30 @@ function MainLayout() { clearInterval(scrollTimer.current!); }); + useUpdateEffect(() => { + switch (options!.style!.layoutWidthType) { + case 'compact': + updateOptions({ + style: { + dataCell: { + width: 200, + }, + }, + }); + setDataCfg(pivotSheetDataCfgForCompactMode); + break; + + default: + updateOptions({ + style: DEFAULT_STYLE, + }); + setDataCfg(pivotSheetDataCfg); + } + }, [options.style!.layoutWidthType]); + // ================== Config ======================== const mergedOptions: SheetComponentOptions = customMerge( - {}, { pagination: showPagination && { pageSize: 10, @@ -416,7 +443,9 @@ function MainLayout() { 行列等宽 @@ -541,6 +570,30 @@ function MainLayout() { }} disabled={sheetType === 'table'} /> + + { + updateOptions({ + frozen: { + firstRow: checked, + }, + }); + }} + disabled={ + sheetType === 'table' || + (mergedOptions.hierarchyType === 'grid' && + (!mergedOptions?.totals?.row + ?.showGrandTotals || + !mergedOptions?.totals?.row + ?.reverseGrandTotalsLayout)) + } + /> + { let selectedCellHighlight: | boolean - | InteractionCellSelectedHighlightOptions = - false; + | InteractionCellHighlightOptions = false; const oldIdx = type.findIndex( (typeItem: any) => isBoolean(typeItem), ); @@ -1232,9 +1284,9 @@ function MainLayout() { { updateOptions({ interaction: { @@ -1463,7 +1515,11 @@ function MainLayout() { { key: 'editable', label: '编辑表', - children: , + children: ( + + ), }, { key: 'mobile', diff --git a/packages/s2-react/playground/utils.ts b/packages/s2-react/playground/utils.ts index b331180c4c..c638769034 100644 --- a/packages/s2-react/playground/utils.ts +++ b/packages/s2-react/playground/utils.ts @@ -1,5 +1,7 @@ +/* eslint-disable no-restricted-imports */ /* eslint-disable no-console */ import type { SpreadSheet } from '@antv/s2'; +import _ from 'lodash'; export const onSheetMounted = (s2: SpreadSheet) => { console.log('onSheetMounted: ', s2); @@ -7,4 +9,5 @@ export const onSheetMounted = (s2: SpreadSheet) => { window.s2 = s2; // @ts-ignore window.g_instances = [s2.container]; + window._ = _; }; diff --git a/packages/s2-react/src/components/advanced-sort/index.tsx b/packages/s2-react/src/components/advanced-sort/index.tsx index 8253fa0781..a0e63ceafb 100644 --- a/packages/s2-react/src/components/advanced-sort/index.tsx +++ b/packages/s2-react/src/components/advanced-sort/index.tsx @@ -5,6 +5,7 @@ import { type SortParam, SpreadSheet, TOTAL_VALUE, + EXTRA_FIELD, } from '@antv/s2'; import { Button, Cascader, Form, Layout, Modal, Radio, Select } from 'antd'; import cx from 'classnames'; @@ -124,10 +125,7 @@ export const AdvancedSort: React.FC = ({ ); }; - const handleCustomSort = ( - dimension: Dimension, - splitOrders: string[] = [], - ) => { + const handleCustomSort = (dimension: Dimension, splitOrders?: string[]) => { handleCustom(); setCurrentDimension(dimension); if (splitOrders) { @@ -194,7 +192,7 @@ export const AdvancedSort: React.FC = ({ current.sortMethod = sortMethod; current.query = { - $$extra$$: rule[1], + [EXTRA_FIELD]: rule[1], }; } else if (rule[0] === 'sortBy') { current.sortBy = currentSortBy; @@ -328,7 +326,7 @@ export const AdvancedSort: React.FC = ({ } = item || {}; return ( - +