diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 0000000..e5b6d8d --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,8 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works +with multi-package repos, or single-package repos to help you version and publish your code. You can +find the full documentation for it [in our repository](https://github.com/changesets/changesets) + +We have a quick list of common questions to get you started engaging with this project in +[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 0000000..dc04c94 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.0.3/schema.json", + "changelog": ["@changesets/changelog-github", { "repo": "antvis/GPT-Vis" }], + "commit": false, + "fixed": [], + "linked": [], + "access": "restricted", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": [] +} diff --git a/.changeset/pretty-weeks-knock.md b/.changeset/pretty-weeks-knock.md new file mode 100644 index 0000000..d9c4972 --- /dev/null +++ b/.changeset/pretty-weeks-knock.md @@ -0,0 +1,5 @@ +--- +'@antv/gpt-vis': patch +--- + +feat: 20 + Visual components diff --git a/.dumirc.ts b/.dumirc.ts new file mode 100644 index 0000000..bd2df23 --- /dev/null +++ b/.dumirc.ts @@ -0,0 +1,26 @@ +import { defineConfig } from 'dumi'; + +export default defineConfig({ + outputPath: 'docs-dist', + favicons: ['https://gw.alipayobjects.com/zos/antfincdn/FLrTNDvlna/antv.png'], + themeConfig: { + name: 'GPT-Vis', + logo: 'https://gw.alipayobjects.com/zos/antfincdn/FLrTNDvlna/antv.png', + footer: `Open-source MIT Licensed | Copyright © 2024 +
+Powered by Antv`, + socialLinks: { + github: 'https://github.com/antvis/GPT-Vis', + }, + }, + externals: { + 'mapbox-gl': 'window.mapboxgl', + 'maplibre-gl': 'window.maplibregl', + }, + theme: { + '@c-primary': '#691eff', + '@s-content-width': '100%', + '@s-content-padding': '48px', + '@s-sidebar-width': '300px', + }, +}); diff --git a/.editorconfig b/.editorconfig index e717f5e..7e3649a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,3 +11,6 @@ insert_final_newline = true [*.md] trim_trailing_whitespace = false + +[Makefile] +indent_style = tab diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 2898340..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - extends: require.resolve('@umijs/lint/dist/config/eslint'), -}; diff --git a/.fatherrc.ts b/.fatherrc.ts new file mode 100644 index 0000000..e983fef --- /dev/null +++ b/.fatherrc.ts @@ -0,0 +1,49 @@ +import { defineConfig } from 'father'; +import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; + +export default defineConfig({ + esm: { + output: 'dist/esm', + }, + cjs: { + output: 'dist/cjs', + }, + // https://github.com/umijs/father/blob/master/docs/config.md#umd + umd: { + name: 'GPTVis', + output: { + path: 'dist/umd', + filename: 'index.min.js', + }, + platform: 'browser', + targets: { + chrome: 51, + }, + externals: { + lodash: '_', + 'lodash-es': '_', + react: 'React', + 'mapbox-gl': { + root: 'mapboxgl', + commonjs: 'mapbox-gl', + commonjs2: 'mapbox-gl', + amd: 'mapbox-gl', + }, + 'maplibre-gl': { + root: 'maplibregl', + commonjs: 'maplibre-gl', + commonjs2: 'maplibre-gl', + amd: 'maplibre-gl', + }, + }, + chainWebpack(memo) { + // 关闭压缩方便调试,默认开启 + // memo.optimization.minimize(false); + + memo + .plugin('webpack-bundle-analyzer') + .use(BundleAnalyzerPlugin, [{ analyzerMode: 'static', openAnalyzer: false }]); + return memo; + }, + }, +}); diff --git a/.github/.gitleaks.toml b/.github/.gitleaks.toml new file mode 100644 index 0000000..8dca9e6 --- /dev/null +++ b/.github/.gitleaks.toml @@ -0,0 +1,124 @@ +title = "gitleaks config" +[[rules]] + description = "AWS Manager ID" + regex = '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}''' + tags = ["key", "AWS"] +[[rules]] + description = "AWS Secret Key" + regex = '''(?i)aws(.{0,20})?(?-i)['\"][0-9a-zA-Z\/+]{40}['\"]''' + tags = ["key", "AWS"] +[[rules]] + description = "AWS MWS key" + regex = '''amzn\.mws\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}''' + tags = ["key", "AWS", "MWS"] +[[rules]] + description = "Facebook Secret Key" + regex = '''(?i)(facebook|fb)(.{0,20})?(?-i)['\"][0-9a-f]{32}['\"]''' + tags = ["key", "Facebook"] +[[rules]] + description = "Facebook Client ID" + regex = '''(?i)(facebook|fb)(.{0,20})?['\"][0-9]{13,17}['\"]''' + tags = ["key", "Facebook"] +[[rules]] + description = "Twitter Secret Key" + regex = '''(?i)twitter(.{0,20})?['\"][0-9a-z]{35,44}['\"]''' + tags = ["key", "Twitter"] +[[rules]] + description = "Twitter Client ID" + regex = '''(?i)twitter(.{0,20})?['\"][0-9a-z]{18,25}['\"]''' + tags = ["client", "Twitter"] +[[rules]] + description = "Github" + regex = '''(?i)github(.{0,20})?(?-i)['\"][0-9a-zA-Z]{35,40}['\"]''' + tags = ["key", "Github"] +[[rules]] + description = "Github Token" + regex = '''[0-9a-zA-Z]{35,40}''' + tags = ["key", "Github Token"] +[[rules]] + description = "Alibaba" + regex = '''(alibaba|antfin)-inc''' + tags = ["key", "Alibaba"] +[[rules]] + description = "antfin" + regex = '''(?i)antfin(.{0,20})?(?-i)['\"][0-9a-zA-Z]{35,40}['\"]''' + tags = ["key", "Antfin"] +[[rules]] + description = "LinkedIn Client ID" + regex = '''(?i)linkedin(.{0,20})?(?-i)['\"][0-9a-z]{12}['\"]''' + tags = ["client", "LinkedIn"] +[[rules]] + description = "LinkedIn Secret Key" + regex = '''(?i)linkedin(.{0,20})?['\"][0-9a-z]{16}['\"]''' + tags = ["secret", "LinkedIn"] +[[rules]] + description = "Slack" + regex = '''xox[baprs]-([0-9a-zA-Z]{10,48})?''' + tags = ["key", "Slack"] +[[rules]] + description = "Asymmetric Private Key" + regex = '''-----BEGIN ((EC|PGP|DSA|RSA|OPENSSH) )?PRIVATE KEY( BLOCK)?-----''' + tags = ["key", "AsymmetricPrivateKey"] +[[rules]] + description = "Public Key" + regex = '''ssh-rsa''' + tags = ["keys", "public key"] +[[rules]] + description = "Gitlab Key" + regex = '''privateToken|private-token''' + tags = ["keys", "Gitlab"] +[[rules]] + description = "Generic Credential" + regex = '''(?i)(api_key|apikey|secret)(.{0,20})?['|"][0-9a-zA-Z]{16,45}['|"]''' + tags = ["key", "API", "generic"] +[[rules]] + description = "Google API key" + regex = '''AIza[0-9A-Za-z\\-_]{35}''' + tags = ["key", "Google"] +[[rules]] + description = "Heroku API key" + regex = '''(?i)heroku(.{0,20})?['"][0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}['"]''' + tags = ["key", "Heroku"] +[[rules]] + description = "MailChimp API key" + regex = '''(?i)(mailchimp|mc)(.{0,20})?['"][0-9a-f]{32}-us[0-9]{1,2}['"]''' + tags = ["key", "Mailchimp"] +[[rules]] + description = "Mailgun API key" + regex = '''(?i)(mailgun|mg)(.{0,20})?['"][0-9a-z]{32}['"]''' + tags = ["key", "Mailgun"] +[[rules]] + description = "PayPal Braintree access token" + regex = '''access_token\$production\$[0-9a-z]{16}\$[0-9a-f]{32}''' + tags = ["key", "Paypal"] +[[rules]] + description = "Picatic API key" + regex = '''sk_live_[0-9a-z]{32}''' + tags = ["key", "Picatic"] +[[rules]] + description = "SendGrid API Key" + regex = '''SG\.[\w_]{16,32}\.[\w_]{16,64}''' + tags = ["key", "SendGrid"] +[[rules]] + description = "Slack Webhook" + regex = '''https://hooks.slack.com/services/T[a-zA-Z0-9_]{8}/B[a-zA-Z0-9_]{8}/[a-zA-Z0-9_]{24}''' + tags = ["key", "slack"] +[[rules]] + description = "Stripe API key" + regex = '''(?i)stripe(.{0,20})?['\"][sk|rk]_live_[0-9a-zA-Z]{24}''' + tags = ["key", "Stripe"] +[[rules]] + description = "Square access token" + regex = '''sq0atp-[0-9A-Za-z\-_]{22}''' + tags = ["key", "square"] +[[rules]] + description = "Square OAuth secret" + regex = '''sq0csp-[0-9A-Za-z\\-_]{43}''' + tags = ["key", "square"] +[[rules]] + description = "Twilio API key" + regex = '''(?i)twilio(.{0,20})?['\"][0-9a-f]{32}['\"]''' + tags = ["key", "twilio"] +[whitelist] + description = "Whitelisted files" + file = '''(^\.?gitleaks.toml$|(.*?)(jpg|gif|doc|pdf|bin)|package-lock\.json$)''' diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..9265590 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,13 @@ +### PR includes + + + +- [ ] fixed #0 +- [ ] add / modify test cases +- [ ] documents, demos + +### Screenshot + +| Before | After | +| ------ | ----- | +| ❌ | ✅ | diff --git a/.github/actions/prepare-install/action.yml b/.github/actions/prepare-install/action.yml new file mode 100644 index 0000000..c1796de --- /dev/null +++ b/.github/actions/prepare-install/action.yml @@ -0,0 +1,24 @@ +name: 'Prepare Install' +description: 'Prepare repository and install all dependencies' + +inputs: + node-version: + description: 'The node version to setup' + required: true + +runs: + using: 'composite' + steps: + - uses: pnpm/action-setup@v3 + with: + version: 9 + + - name: Use Node.js ${{ inputs.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + # cache: 'pnpm' # pnpm-lock.yaml caching dependencies to speed up workflows + + - name: Install dependencies + shell: bash + run: pnpm install diff --git a/.github/release.yaml b/.github/release.yaml new file mode 100644 index 0000000..6caf9dd --- /dev/null +++ b/.github/release.yaml @@ -0,0 +1,24 @@ +# for .github/workflows/release-label.yml +# These labels can be used in conjunction GitHub's [automatically generated release notes](https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes) + +changelog: + exclude: + labels: + - ignore-for-release + - pr(chore) + categories: + - title: 🎉 New Features + labels: + - pr(feature) + - title: 📖 Documentation Changes + labels: + - pr(documentation) + - title: 🐛 Bug Fixes + labels: + - pr(bugfix) + - title: 🛠 Breaking Changes + labels: + - pr(breaking) + - title: Other Changes + labels: + - '*' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..96d4084 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,68 @@ +name: CI + +on: + push: + branches: [master] + paths: + - 'src/**' + - '__tests__/**' + pull_request: + branches: + - '**' + paths: + - 'src/**' + - '__tests__/**' + +permissions: + contents: read # to fetch code (actions/checkout) + +concurrency: + group: '${{ github.workflow }} - ${{ github.head_ref || github.ref }}' + cancel-in-progress: true + +jobs: + lint: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18.x] + steps: + - uses: actions/checkout@v4 + - name: Install dependencies + uses: ./.github/actions/prepare-install + with: + node-version: ${{ matrix.node-version }} + - name: Check + run: | + pnpm check-deps + pnpm check-format + - name: Lint + run: pnpm lint:ts + + # unit-test: + # runs-on: ubuntu-latest + # strategy: + # matrix: + # node-version: [18.x] + # steps: + # - uses: actions/checkout@v4 + # - name: Install dependencies + # uses: ./.github/actions/prepare-install + # with: + # node-version: ${{ matrix.node-version }} + # - name: Unit Test + # run: pnpm test:unit + + size-test: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18.x] + steps: + - uses: actions/checkout@v4 + - name: Install dependencies + uses: ./.github/actions/prepare-install + with: + node-version: ${{ matrix.node-version }} + - name: Szie Test With Build + run: pnpm test:size diff --git a/.github/workflows/create-bumb-version-pr.yml b/.github/workflows/create-bumb-version-pr.yml new file mode 100644 index 0000000..8d9fc96 --- /dev/null +++ b/.github/workflows/create-bumb-version-pr.yml @@ -0,0 +1,48 @@ +name: Create bump version PR + +on: + workflow_dispatch: + +concurrency: + group: '${{ github.workflow }} - ${{ github.ref }}' + cancel-in-progress: true + +jobs: + bump-version-pr: + name: Bump version PR + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18.x] + steps: + - uses: actions/checkout@v4 + - name: Install dependencies + uses: ./.github/actions/prepare-install + with: + node-version: ${{ matrix.node-version }} + + - name: Pre build ckeck + run: pnpm build + + - name: Create Version Pull Request + id: changesets + uses: changesets/action@v1 + with: + version: pnpm publish-version + commit: 'chore: version' + title: 'chore: version' + setupGitUser: true + createGithubReleases: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: After changesets + run: | + if [[ ${{ steps.changesets.outputs.hasChangesets }} == false ]]; then + # No changesets + echo "::error title=No changesets found::All changesets are empty, not creating PR, please add changeset" + exit 1 + else + # Created Pull Request + echo "::notice title=PR is created::Created bump version PR address is: https://github.com/${{github.repository}}/pull/${{steps.changesets.outputs.pullRequestNumber}}" + fi diff --git a/.github/workflows/deploy-site.yml b/.github/workflows/deploy-site.yml new file mode 100644 index 0000000..5eb150c --- /dev/null +++ b/.github/workflows/deploy-site.yml @@ -0,0 +1,40 @@ +name: Deploy Site + +on: + workflow_dispatch: + release: + # released: only latest release will trigger + types: [released] + +concurrency: + group: '${{ github.workflow }} - ${{ github.ref }}' + cancel-in-progress: true + +jobs: + publish-site: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18.x] + steps: + - uses: actions/checkout@v4 + - name: Install dependencies + uses: ./.github/actions/prepare-install + with: + node-version: ${{ matrix.node-version }} + - run: pnpm docs:build + - run: | + echo "gpt-vis.antv.vision" > docs-dist/CNAME + - run: | + cd docs-dist + git init + git config --local user.name antv + git config --local user.email antv@antfin.com + git add . + git commit -m "update by release action" + - uses: ad-m/github-push-action@master + with: + github_token: ${{secrets.GITHUB_TOKEN}} + directory: docs-dist + branch: gh-pages + force: true diff --git a/.github/workflows/gitleaks.yml b/.github/workflows/gitleaks.yml new file mode 100644 index 0000000..4cde88c --- /dev/null +++ b/.github/workflows/gitleaks.yml @@ -0,0 +1,15 @@ +name: Gitleaks + +on: push + +jobs: + gitleaks: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: '1' + - name: gitleaks-action + uses: zricethezav/gitleaks-action@v1.6.0 + with: + config-path: .github/.gitleaks.toml diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml new file mode 100644 index 0000000..743318f --- /dev/null +++ b/.github/workflows/preview.yml @@ -0,0 +1,25 @@ +name: Surge PR Preview + +on: + pull_request: + paths: + - 'docs/**' + - 'src/**' +jobs: + preview: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18.x] + steps: + - uses: actions/checkout@v4 + - name: Install dependencies + uses: ./.github/actions/prepare-install + with: + node-version: ${{ matrix.node-version }} + - uses: afc163/surge-preview@v1 + with: + surge_token: ${{ secrets.SURGE_TOKEN }} + github_token: ${{ secrets.GITHUB_TOKEN }} + dist: docs-dist + build: pn docs:build diff --git a/.github/workflows/release-label.yml b/.github/workflows/release-label.yml new file mode 100644 index 0000000..4de49ad --- /dev/null +++ b/.github/workflows/release-label.yml @@ -0,0 +1,15 @@ +name: Conventional Release Labels + +# Warning, do not check out untrusted code with +# the pull_request_target event. +on: + pull_request_target: + types: [opened, edited] +jobs: + label: + runs-on: ubuntu-latest + steps: + - uses: bcoe/conventional-release-labels@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + type_labels: '{"feat": "pr(feature)", "fix": "pr(bugfix)", "breaking": "pr(breaking)", "docs": "pr(documentation)", "chore": "pr(chore)"}' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..4af1c28 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,94 @@ +name: Release + +on: + workflow_dispatch: + pull_request_review: + types: [submitted] + +concurrency: + group: '${{ github.workflow }} - ${{ github.head_ref || github.ref }}' + cancel-in-progress: true + +jobs: + release: + # created version approved PR by changesets bot or manual dispatch + if: | + ( + github.event_name == 'pull_request_review' && + github.event.pull_request.user.type == 'Bot' && + startswith(github.event.pull_request.head.ref, 'changeset-release/') && + github.event.review.state == 'APPROVED' + ) || + github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18.x] + outputs: + published: ${{ steps.changesets.outputs.published }} + publishedPackages: ${{ steps.changesets.outputs.publishedPackages }} + + steps: + - uses: actions/checkout@v4 + - name: Install dependencies + uses: ./.github/actions/prepare-install + with: + node-version: ${{ matrix.node-version }} + + - name: Release NPM and Github + id: changesets + uses: changesets/action@v1 + env: + # need personal access token when Triggering a workflow from a workflow + # triggering release event of Release Notify and Deploy Site + # https://docs.github.com/en/actions/using-workflows/triggering-a-workflow#triggering-a-workflow-from-a-workflow + GITHUB_TOKEN: ${{ secrets.YJ_GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.YJ_NPM_TOKEN }} + with: + publish: pnpm publish-package + createGithubReleases: true + + # when release not publish + - name: Released not publish + if: steps.changesets.outputs.published == 'false' + run: | + if [[ ${{ steps.changesets.outputs.hasChangesets }} == false ]]; then + # No changesets + echo "::error title=No changesets found::All changesets are empty, please add changeset" + else + # Released failed + echo "::error title=Publish failed::Released NPM or Github failed" + fi + exit 1 + + # when release published successful + release-succeeded: + needs: release + if: needs.release.outputs.published == 'true' + runs-on: ubuntu-latest + steps: + - name: Release succeeded comment + if: github.event_name == 'pull_request_review' + uses: peter-evans/create-or-update-comment@v4 + with: + issue-number: ${{ github.event.pull_request.number }} + body: | + Released to successful 🎉 + + - Released [NPM][1] + - Released [Github][2] + + `${{ needs.release.outputs.publishedPackages }}` + + [1]: https://www.npmjs.com/package/@antv/GPT-vis?activeTab=versions + [2]: https://github.com/${{github.repository}}/releases + reactions: 'rocket' + + - name: automerge + if: github.event_name == 'pull_request_review' + uses: 'pascalgn/automerge-action@v0.16.2' + env: + MERGE_LABELS: '' + PULL_REQUEST: ${{ github.event.pull_request.number }} + MERGE_METHOD: 'squash' + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 64f1da6..fc3ead2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,30 +1,81 @@ + +# Created by https://www.gitignore.io/api/node + +### Node ### # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* -lerna-debug.log* yarn.lock package-lock.json pnpm-lock.yaml -# Sys -.DS_Store -.idea +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output -# Node +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories node_modules/ -.npmrc -# Build -dist -lib -esm -es +#history +.history/ -# Test -coverage +jspm_packages/ -# Bundle visualizer -stats.html +# Typescript v1 declaration files +# typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + + +# End of https://www.gitignore.io/api/node + +.DS_Store +.cache +.idea + +# Build +dist +docs-dist +.dumi diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100644 index 0000000..f0abd67 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,3 @@ +#!/bin/sh + +npx --no -- commitlint --edit "$1" diff --git a/.husky/pre-commit b/.husky/pre-commit old mode 100755 new mode 100644 index d24fdfc..3b608ae --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,3 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" +#!/bin/sh npx lint-staged diff --git a/.prettierignore b/.prettierignore index 6a50e20..0c1459a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,18 @@ -/dist -*.yaml +*.svg +.DS_Store +.gitignore +.prettierignore +.editorconfig +.eslintignore +node_modules +yarn.lock +yarn-error.log +npm-debug.log +CNAME +LICENSE +.cache +.history + +# output dist +dist/** + diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..772dd4f --- /dev/null +++ b/.prettierrc @@ -0,0 +1,10 @@ +{ + "printWidth": 100, + "singleQuote": true, + "trailingComma": "all", + "arrowParens": "always", + "overrides": [ + { "files": ".prettierrc", "options": { "parser": "json" } } + ], + "plugins": ["prettier-plugin-organize-imports"] +} diff --git a/.prettierrc.js b/.prettierrc.js deleted file mode 100644 index e6df093..0000000 --- a/.prettierrc.js +++ /dev/null @@ -1,19 +0,0 @@ -module.exports = { - pluginSearchDirs: false, - plugins: [ - require.resolve('prettier-plugin-organize-imports'), - require.resolve('prettier-plugin-packagejson'), - ], - printWidth: 80, - proseWrap: 'never', - singleQuote: true, - trailingComma: 'all', - overrides: [ - { - files: '*.md', - options: { - proseWrap: 'preserve', - }, - }, - ], -}; diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..021ac36 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "dbaeumer.vscode-eslint", + "editorconfig.editorconfig", + "esbenp.prettier-vscode" + ], + "unwantedRecommendations": ["hookyqr.beautify", "dbaeumer.jshint"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d465499 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + // An array of language ids which should be validated by ESLint + "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"], + // required to make the extension pickup the flat config + "eslint.useFlatConfig": true, + + // typescript auto-format settings + "typescript.tsdk": "node_modules/typescript/lib", + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true +} diff --git a/LICENSE b/LICENSE index b0c7139..769de4e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ -MIT License +The MIT License (MIT) -Copyright (c) +Copyright (c) 2024 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.md b/README.md index 426cd63..869c864 100644 --- a/README.md +++ b/README.md @@ -1,78 +1,93 @@ -

-GPT-VIS -

+ English | [简体中文](./README.zh-CN.md) + +

GPT-Vis

+ Components for GPTs, generative AI, and LLM projects. Not only UI Components. -![GPT-vis logo-06](https://github.com/eosphoros-ai/GPT-Vis/assets/17919400/c8804ffb-d3d6-45d3-846f-cf217681ab05) +

+ Document • + Knowledge • + Huggingface • + Agent Demo +

+
+
-## 特性 +
-- 🤖 **LLM 相关**:针对 LLM 对话式交互,以及服务端序列化输出而设计,方便快速集成到 AI 应用中。 -- 🍡 **丰富组件**:内置有 20+ 美观好看的常用 UI 组件,满足常规需求。 -- 🔨 **易于扩展**:对于自己的特殊 UI 定制需求,提供方便的扩展机制和架构设计。 +## ✨ Features -## 安装 +- 🤖 **LLM Protocol**: A visual protocol for LLM Agent cards, designed for LLM conversational interaction and service serialized output, to facilitate rapid integration into AI applications. +- 🍡 **LLM Component**: Developed components for LLM applications, with 20+ commonly used VIS components built-in, providing convenient expansion mechanism and architecture design for customized UI requirements. +- 📈 **LLM access**: Chart knowledge base and chart recommendation model for seamless access to LLM, directly output visual cards for LLM, and provide knowledge base and recommended model solutions for Agent. -使用 NodeJS 包管理工具安装依赖。 +## 📦 Installation ```bash -$npm i --save gpt-vis +$ npm i @antv/gpt-vis --save ``` -## 快速使用 +## 🔨 Usage + +```jsx +import { GPTVis } from '@antv/gpt-vis'; -快速使用 gpt-vis渲染出对话卡片的 UI。 +const markdownContent = ` +# GPT-VIS \n\nComponents for GPTs, generative AI, and LLM projects. Not only UI Components. -```tsx -import { Conversation, Components } from 'gpt-vis'; +Here’s a visualization of Haidilao's food delivery revenue from 2013 to 2022. You can see a steady increase over the years, with notable *growth* particularly in recent years. -function Demo() { - // 服务端返回的协议内容 - const content = - '# GPT-VIS \n\nComponents for GPTs, generative AI, and LLM projects. Not only UI Components.'; +\`\`\`vis-chart +{ "type": "line","data": [{"time":2013,"value":59.3},{"time":2014,"value":64.4},{"time":2015,"value":68.9},{"time":2016,"value":74.4},{"time":2017,"value":82.7},{"time":2018,"value":91.9},{"time":2019,"value":99.1},{"time":2020,"value":101.6},{"time":2021,"value":114.4},{"time":2022,"value":121}] } +\`\`\` +`; - return {content}; -} +export default () => { + return {markdownContent}; +}; ``` -使用自定义的 UI 组件。UI 渲染最终使用 markdown 格式,所以自定义的方式有两种,一种是基于 markdown code 标签去扩展语言,一种是扩展标签。 +## 🛠 Custom renderer -````tsx -import { Conversation, Components } from 'gpt-vis'; +```jsx +import { GPTVis, withDefaultChartCode, VisText } from '@antv/gpt-vis'; -const custom = { - 'my-ui': () => {}, -}; +const markdownContent = ` +本月共产生决策数量2,783个,环比增长15.2%高优先级决策占比56.2%,呈现稳定上升趋势,预计下月将突破3,000大关。 + +\`\`\`my-ui +my data +\`\`\` +`; -function Demo() { - // 服务端返回的协议内容 - const content = '# GPT-VIS \n\n```my-ui\n{"value": "1"}```'; +const customRenderers = { 'my-ui': ({ children }) =>
{children}
}; +const components = { + 'vis-text': VisText, + code: withDefaultChartCode({ languageRenderers: customRenderers }), +}; - return ( - - {content} - - ); -} -```` +export default () => { + return {markdownContent}; +}; +``` -## Development +## 💻 Development ```bash # install dependencies -$ npm install +$ pnpm install # develop library by docs demo -$ npm run dev +$ pnpm dev # build library source code -$ npm run build +$ pnpm build ``` ## License -MIT +[MIT](./LICENSE) diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 0000000..c95bdf1 --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,93 @@ + [English](./README.md) | 简体中文 + +

GPT-Vis

+ +
+ +Components for GPTs, generative AI, and LLM projects. Not only UI Components. + +

+ 文档 • + 知识库 • + Huggingface • + 体验 Agent +

+ +
+ +
+ +
+ +## 特性 + +- 🤖 **LLM 协议**:面向 LLM Agent 卡片的可视化协议,针对 LLM 对话式交互,以及服务序列化输出而设计,方便快速集成到 AI 应用中。 +- 🍡 **LLM 组件**:面向 LLM 应用研发组件, 内置有 20+ 常用 VIS 组件,对于定制 UI 需求,提供方便的扩展机制和架构设计。 +- 📈 **LLM 接入**:面向 LLM 无缝接入的图表知识库和图表推荐模型,针对 LLM 直接输出可视化卡片,为 Agent 提供知识库以及推荐模型方案。 + +## 📦 安装 + +```bash +$ npm i @antv/gpt-vis --save +``` + +## 🔨 使用 + +```jsx +import { GPTVis } from '@antv/gpt-vis'; + +const markdownContent = ` +# GPT-VIS \n\nComponents for GPTs, generative AI, and LLM projects. Not only UI Components. + +Here’s a visualization of Haidilao's food delivery revenue from 2013 to 2022. You can see a steady increase over the years, with notable *growth* particularly in recent years. + +\`\`\`vis-chart +{ "type": "line","data": [{"time":2013,"value":59.3},{"time":2014,"value":64.4},{"time":2015,"value":68.9},{"time":2016,"value":74.4},{"time":2017,"value":82.7},{"time":2018,"value":91.9},{"time":2019,"value":99.1},{"time":2020,"value":101.6},{"time":2021,"value":114.4},{"time":2022,"value":121}] } +\`\`\` +`; + +export default () => { + return {markdownContent}; +}; +``` + +## 🛠 定制渲染器 + +```jsx +import { GPTVis, withDefaultChartCode, VisText } from '@antv/gpt-vis'; + +const markdownContent = ` +本月共产生决策数量2,783个,环比增长15.2%高优先级决策占比56.2%,呈现稳定上升趋势,预计下月将突破3,000大关。 + +\`\`\`my-ui +my data +\`\`\` +`; + +const customRenderers = { 'my-ui': ({ children }) =>
{children}
}; +const components = { + 'vis-text': VisText, + code: withDefaultChartCode({ languageRenderers: customRenderers }), +}; + +export default () => { + return {markdownContent}; +}; +``` + +## 💻 本地开发 + +```bash +# install dependencies +$ pnpm install + +# develop library by docs demo +$ pnpm dev + +# build library source code +$ pnpm build +``` + +## License + +[MIT](./LICENSE) diff --git a/__tests__/version.spec.ts b/__tests__/version.spec.ts new file mode 100644 index 0000000..7a24115 --- /dev/null +++ b/__tests__/version.spec.ts @@ -0,0 +1,9 @@ +import pkg from '../package.json'; +import { version } from '../src'; + +describe('version', () => { + it('should match the `version` field of package.json', () => { + const expected = pkg.version; + expect(version).toBe(expected); + }); +}); diff --git a/commitlint.config.ts b/commitlint.config.ts new file mode 100644 index 0000000..f2886c8 --- /dev/null +++ b/commitlint.config.ts @@ -0,0 +1,25 @@ +export default { + extends: ['@commitlint/config-conventional'], + rules: { + 'type-enum': [ + 2, + 'always', + [ + 'build', + 'chore', + 'ci', + 'docs', + 'feat', + 'fix', + 'perf', + 'refactor', + 'revert', + 'style', + 'test', + 'deps', + 'wip', + ], + ], + 'header-max-length': [2, 'always', 100], + }, +}; diff --git a/dev/demo/basic.ts b/dev/demo/basic.ts deleted file mode 100644 index cb9a297..0000000 --- a/dev/demo/basic.ts +++ /dev/null @@ -1,21 +0,0 @@ -export const Basic = ` -# [GPT-VIS](https://github.com/eosphoros-ai/GPT-VIS) - -Components for GPTs, generative AI, and LLM projects. Not only UI Components.; - -
- -Some *emphasis* and strong! - -
- -Here is some JavaScript code: - -~~~js -const greeting = 'hello, world'; - -const sayHello = (greeting) => {} - -sayHello(greeting); -~~~ -`; diff --git a/dev/demo/index.ts b/dev/demo/index.ts deleted file mode 100644 index d0b140f..0000000 --- a/dev/demo/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Basic } from './basic'; diff --git a/dev/index.html b/dev/index.html deleted file mode 100644 index 1496fd8..0000000 --- a/dev/index.html +++ /dev/null @@ -1,7 +0,0 @@ - - - - -
-
- \ No newline at end of file diff --git a/dev/main.tsx b/dev/main.tsx deleted file mode 100644 index db92fe4..0000000 --- a/dev/main.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React, { StrictMode } from 'react'; -import { createRoot } from 'react-dom/client'; -import { Components, Conversation } from '../src'; -import * as markdowns from './demo'; - -const select = document.getElementById('selector') as HTMLSelectElement; -select.onchange = () => { - const { value } = select; - history.pushState({ value }, '', `?name=${value}`); - // eslint-disable-next-line @typescript-eslint/no-use-before-define - render(); -}; -select.style.display = 'block'; -select.style.minWidth = '120px'; - -Object.keys(markdowns).forEach((d) => { - const option = document.createElement('option'); - option.textContent = d; - option.value = d; - select.append(option); -}); - -let root; -/** - * 渲染 markdown 组件 - */ -function render() { - if (root) root.unmount(); - const markdown = markdowns[select.value]; - - root = createRoot(document.getElementById('root')!); - - root.render( - - {markdown} - , - ); -} - -const initialValue = new URL(location as any).searchParams.get( - 'name', -) as string; -if (markdowns[initialValue]) select.value = initialValue; - -render(); diff --git a/docs/guide/antd-design-x.md b/docs/guide/antd-design-x.md new file mode 100644 index 0000000..e5b1a0b --- /dev/null +++ b/docs/guide/antd-design-x.md @@ -0,0 +1,170 @@ +--- +title: 在 Antd Design X 中使用 +nav: { title: '指南', order: 0 } +toc: content +order: 1 +--- + +## 使用 Markdown 协议 + +1.定义图表 Markdown 代码块 + +```js +const markdownContent = ` +## GPT-VIS +Components for GPTs, generative AI, and LLM projects. Not only UI Components. + + \`\`\`vis-chart + { + "type": "pie", + "data": [ + { "category": "分类一", "value": 27 }, + { "category": "分类二", "value": 25 }, + { "category": "分类三", "value": 18 }, + { "category": "分类四", "value": 15 }, + { "category": "分类五", "value": 10 }, + { "category": "其他", "value": 5 } + ] + } +\`\`\``; +``` + +2.扩展聊天气泡渲染 + +```tsx | pure +import { Bubble, type BubbleProps } from '@ant-design/x'; +import { GPTVis } from '@antv/gpt-vis'; + +const RenderMarkdown: BubbleProps['messageRender'] = (content) => {content}; + +export default () => { + return ( + + ); +}; +``` + +```tsx +import { Bubble, type BubbleProps } from '@ant-design/x'; +import { GPTVis } from '@antv/gpt-vis'; + +const bgStyle = { + display: 'grid', + gridGap: '20px 0', + background: '#f7f7f7', + padding: 20, + borderRadius: 8, +}; + +const markdownContent = ` +## GPT-VIS +Components for GPTs, generative AI, and LLM projects. Not only UI Components. + +\`\`\`vis-chart + { + "type": "pie", + "data": [ + { "category": "分类一", "value": 27 }, + { "category": "分类二", "value": 25 }, + { "category": "分类三", "value": 18 }, + { "category": "分类四", "value": 15 }, + { "category": "分类五", "value": 10 }, + { "category": "其他", "value": 5 } + ] + } +\`\`\``; + +const RenderMarkdown: BubbleProps['messageRender'] = (content) => {content}; + +export default () => { + return ( +
+ +
+ ); +}; +``` + +## 使用结构化的数据 + +1. 定义你的图表数据 + +```js +const mockdata = [ + { category: '分类一', value: 27 }, + { category: '分类二', value: 25 }, + { category: '分类三', value: 18 }, + { category: '分类四', value: 15 }, + { category: '分类五', value: 10 }, + { category: '其他', value: 5 }, +]; +``` + +2. 渲染聊天气泡 + +```tsx | pure +import { Pie } from '@antv/gpt-vis'; +import { Bubble } from '@ant-design/x'; + +export default () => { + return ( + } + styles={{ content: { background: '#fff' } }} + /> + ); +}; +``` + +```tsx +import { Pie } from '@antv/gpt-vis'; +import { Bubble } from '@ant-design/x'; + +const bgStyle = { + display: 'grid', + gridGap: '20px 0', + background: '#f7f7f7', + padding: 20, + borderRadius: 8, +}; + +const mockdata = [ + { category: '分类一', value: 27 }, + { category: '分类二', value: 25 }, + { category: '分类三', value: 18 }, + { category: '分类四', value: 15 }, + { category: '分类五', value: 10 }, + { category: '其他', value: 5 }, +]; + +export default () => { + return ( +
+ } + avatar={{ + src: 'https://mdn.alipayobjects.com/huamei_je4oko/afts/img/A*6LRBT7rjOkQAAAAAAAAAAAAADsZ-AQ/original', + }} + variant="shadow" + styles={{ content: { background: '#fff' } }} + /> +
+ ); +}; +``` diff --git a/docs/guide/custome-markdown.md b/docs/guide/custome-markdown.md new file mode 100644 index 0000000..df82b56 --- /dev/null +++ b/docs/guide/custome-markdown.md @@ -0,0 +1,34 @@ +--- +title: 扩展 MD 渲染器使用 +nav: { title: '指南', order: 0 } +toc: content +order: 2 +--- + +## 扩展 react-markdown 使用 + +```tsx | pure +import React from 'react'; +import Markdown from 'react-markdown'; +import { withChartCode, withDefaultChartCode, ChartType, Line } from '@antv/gpt-vis'; + +const markdownContent = ` +# GPT-VIS \n\nComponents for GPTs, generative AI, and LLM projects. Not only UI Components. + +Here’s a visualization of Haidilao's food delivery revenue from 2013 to 2022. You can see a steady increase over the years, with notable *growth* particularly in recent years. + +\`\`\`vis-chart +{ "type": "line","data": [{"time":2013,"value":59.3},{"time":2014,"value":64.4},{"time":2015,"value":68.9},{"time":2016,"value":74.4},{"time":2017,"value":82.7},{"time":2018,"value":91.9},{"time":2019,"value":99.1},{"time":2020,"value":101.6},{"time":2021,"value":114.4},{"time":2022,"value":121}] } +\`\`\` +`; + +const CodeBlock = withChartCode({ + components: { [ChartType.Line]: Line }, +}); +// 或 +// const CodeBlock = withDefaultChartCode(); + +export default () => { + return {markdownContent}; +}; +``` diff --git a/docs/guide/customize-style.md b/docs/guide/customize-style.md new file mode 100644 index 0000000..754f27a --- /dev/null +++ b/docs/guide/customize-style.md @@ -0,0 +1,276 @@ +--- +title: 定制样式 +nav: { title: '指南', order: 0 } +toc: content +order: 3 +--- + +通过在 [ConfigProvider](/components/config-provider) 中传入样式属性,来配置图表组件的全局样式。 + +## 定制组件级样式 + +给各个组件定制样式 + +```tsx | pure +import { ConfigProvider } from '@antv/gpt-vis'; + +// 设置甜甜圈样式 +const pieConfig = { + legend: false, + innerRadius: 0.6, + style: { + stroke: '#fff', + inset: 1, + radius: 10, + }, +}; + +// 面积图设置渐变色 +const areaConfig = { + style: { + fill: 'linear-gradient(-90deg, white 0%, darkgreen 100%)', + }, + line: { + style: { + stroke: 'darkgreen', + strokeWidth: 2, + }, + tooltip: false, + }, +}; + +// 地图设置图标 +const pinMapConfig = { + markerStyle: { + iconPath: + 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*JZf9T6psSzkAAAAAAAAAAAAADmJ7AQ/original', + }, +}; + +export default () => { + return ( + + // ... + + ); +}; +``` + +```tsx +import { Bubble, type BubbleProps } from '@ant-design/x'; +import { ConfigProvider, GPTVis } from '@antv/gpt-vis'; + +// 设置甜甜圈样式 +const pieConfig = { + legend: false, + innerRadius: 0.6, + style: { + stroke: '#fff', + inset: 1, + radius: 10, + }, +}; + +// 面积图设置渐变色 +const areaConfig = { + style: { + fill: 'linear-gradient(-90deg, white 0%, darkgreen 100%)', + }, + line: { + style: { + stroke: 'darkgreen', + strokeWidth: 2, + }, + tooltip: false, + }, +}; + +// 地图设置图标 +const pinMapConfig = { + markerStyle: { + iconPath: + 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*JZf9T6psSzkAAAAAAAAAAAAADmJ7AQ/original', + }, +}; + +const markdownContent = ` +\`\`\`vis-chart +{ + "type": "area", + "data": [{"time":2013,"value":59.3},{"time":2014,"value":64.4},{"time":2015,"value":68.9},{"time":2016,"value":74.4},{"time":2017,"value":82.7},{"time":2018,"value":91.9},{"time":2019,"value":99.1},{"time":2020,"value":101.6},{"time":2021,"value":114.4},{"time":2022,"value":121}] +} +\`\`\` + +\`\`\`vis-chart +{ + "type": "pie", + "data": [ + { "category": "分类一", "value": 27 }, + { "category": "分类二", "value": 25 }, + { "category": "分类三", "value": 18 }, + { "category": "分类四", "value": 15 }, + { "category": "分类五", "value": 10 }, + { "category": "其他", "value": 5 } + ] +} +\`\`\` + +\`\`\`vis-chart + { + "type": "pin-map", + "data": [ + { "label": "杨梅岭", "longitude": 120.118362, "latitude": 30.217175 }, + { "label": "理安寺", "longitude": 120.112958, "latitude": 30.207319 }, + { "label": "九溪烟树", "longitude": 120.11335, "latitude": 30.202395 }, + { "label": "飞来峰", "longitude": 120.100549, "latitude": 30.236875 }, + { "label": "灵隐寺", "longitude": 120.101406, "latitude": 30.240826 }, + { "label": "天竺三寺", "longitude": 120.105337, "latitude": 30.236818 }, + { "label": "杭州植物园", "longitude": 120.116979, "latitude": 30.252876 }, + { "label": "杭州花圃", "longitude": 120.127654, "latitude": 30.245663 }, + { "label": "苏堤", "longitude": 120.135764, "latitude": 30.251448 }, + { "label": "虎跑公园", "longitude": 120.130095, "latitude": 30.207505 }, + { "label": "玉皇飞云", "longitude": 120.145323, "latitude": 30.214993 }, + { "label": "长桥公园", "longitude": 120.155057, "latitude": 30.232985 } + ] +} +\`\`\` +`; + +const bgStyle = { + display: 'grid', + gridGap: '20px 0', + background: '#f7f7f7', + padding: 20, + borderRadius: 8, +}; + +const RenderMarkdown: BubbleProps['messageRender'] = (content) => {content}; + +export default () => { + return ( + +
+ +
+
+ ); +}; +``` + +## 定制图表级主题 + +```tsx | pure +import { ConfigProvider } from '@antv/gpt-vis'; + +export default () => { + return ( + + // ... + + ); +}; +``` + +```tsx +import { Bubble, type BubbleProps } from '@ant-design/x'; +import { ConfigProvider, GPTVis } from '@antv/gpt-vis'; + +const markdownContent = ` +\`\`\`vis-chart +{ + "type": "area", + "data": [{"time":2013,"value":59.3},{"time":2014,"value":64.4},{"time":2015,"value":68.9},{"time":2016,"value":74.4},{"time":2017,"value":82.7},{"time":2018,"value":91.9},{"time":2019,"value":99.1},{"time":2020,"value":101.6},{"time":2021,"value":114.4},{"time":2022,"value":121}] +} +\`\`\` + +\`\`\`vis-chart +{ + "type": "pie", + "data": [ + { "category": "分类一", "value": 27 }, + { "category": "分类二", "value": 25 }, + { "category": "分类三", "value": 18 }, + { "category": "分类四", "value": 15 }, + { "category": "分类五", "value": 10 }, + { "category": "其他", "value": 5 } + ] +} +\`\`\` + +\`\`\`vis-chart + { + "type": "pin-map", + "data": [ + { "label": "杨梅岭", "longitude": 120.118362, "latitude": 30.217175 }, + { "label": "理安寺", "longitude": 120.112958, "latitude": 30.207319 }, + { "label": "九溪烟树", "longitude": 120.11335, "latitude": 30.202395 }, + { "label": "飞来峰", "longitude": 120.100549, "latitude": 30.236875 }, + { "label": "灵隐寺", "longitude": 120.101406, "latitude": 30.240826 }, + { "label": "天竺三寺", "longitude": 120.105337, "latitude": 30.236818 }, + { "label": "杭州植物园", "longitude": 120.116979, "latitude": 30.252876 }, + { "label": "杭州花圃", "longitude": 120.127654, "latitude": 30.245663 }, + { "label": "苏堤", "longitude": 120.135764, "latitude": 30.251448 }, + { "label": "虎跑公园", "longitude": 120.130095, "latitude": 30.207505 }, + { "label": "玉皇飞云", "longitude": 120.145323, "latitude": 30.214993 }, + { "label": "长桥公园", "longitude": 120.155057, "latitude": 30.232985 } + ] +} +\`\`\` +`; + +const bgStyle = { + display: 'grid', + gridGap: '20px 0', + background: '#f7f7f7', + padding: 20, + borderRadius: 8, +}; + +const RenderMarkdown: BubbleProps['messageRender'] = (content) => {content}; + +export default () => { + return ( + +
+ +
+
+ ); +}; +``` + +更多用法详见 [ConfigProvider](/components/config-provider) diff --git a/docs/guide/quick-start.md b/docs/guide/quick-start.md new file mode 100644 index 0000000..97b2b26 --- /dev/null +++ b/docs/guide/quick-start.md @@ -0,0 +1,61 @@ +--- +title: 快速上手 +nav: { title: '指南', order: 0 } +toc: content +order: 0 +--- + +## ⏬ 安装 + +```shell +$ npm install @antv/gpt-vis --save +``` + +## 🌰 使用 + +```tsx | pure +import React from 'react'; +import { GPTVis } from '@antv/gpt-vis'; + +const markdownContent = ` +# GPT-VIS \n\nComponents for GPTs, generative AI, and LLM projects. Not only UI Components. + +Here’s a visualization of Haidilao's food delivery revenue from 2013 to 2022. You can see a steady increase over the years, with notable *growth* particularly in recent years. + +\`\`\`vis-chart +{ "type": "line","data": [{"time":2013,"value":59.3},{"time":2014,"value":64.4},{"time":2015,"value":68.9},{"time":2016,"value":74.4},{"time":2017,"value":82.7},{"time":2018,"value":91.9},{"time":2019,"value":99.1},{"time":2020,"value":101.6},{"time":2021,"value":114.4},{"time":2022,"value":121}] } +\`\`\` +`; + +export default () => { + return {markdownContent}; +}; +``` + +## 🛠 定制渲染器 + +业务定制 UI 场景,提供方便的扩展机制,支持标签和代码块扩展。 + +```tsx | pure +import { GPTVis, withDefaultChartCode, VisText } from '@antv/gpt-vis'; + +const markdownContent = ` +\`\`\`my-ui +my data +\`\`\` + +本月共产生决策数量2,783个,环比增长15.2%高优先级决策占比56.2%,呈现稳定上升趋势,预计下月将突破3,000大关。 +`; + +const customRenderers = { + 'my-ui': ({ children }) =>
{children}
, +}; +const components = { + 'vis-text': VisText, + code: withDefaultChartCode({ languageRenderers: customRenderers }), +}; + +export default () => { + return {markdownContent}; +}; +``` diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..606dd8d --- /dev/null +++ b/docs/index.md @@ -0,0 +1,11 @@ +--- +title: GPT-Vis - Components for GPTs +hero: + title: GPT-Vis + description: Components for GPTs, generative AI, and LLM projects. Not only UI Components. + actions: + - text: Start + link: /guide/quick-start + - text: GitHub + link: https://github.com/antvis/GPT-Vis +--- diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..4a3637f --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,105 @@ +// @ts-check + +import eslint from '@eslint/js'; +import prettierConfig from 'eslint-config-prettier'; +import jestPlugin from 'eslint-plugin-jest'; +import jsxA11yPlugin from 'eslint-plugin-jsx-a11y'; +import reactPlugin from 'eslint-plugin-react'; +import reactHooks from 'eslint-plugin-react-hooks'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; + +import { dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +export default tseslint.config( + // config ignores files, equal `.eslintignore` + { + ignores: [ + '**/node_modules/**', + '**/dist/**', + '**/.cache/**', + '**/.history/**', + '**/.idea/**', + './*.?*.ts', + + // Files of the build + 'dist/*', + ], + }, + + // extends configs + eslint.configs.recommended, + + // basic config + { + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + globals: { + ...globals.es2021, + ...globals.browser, + }, + }, + rules: { + 'no-unused-vars': 'warn', + 'no-unsafe-optional-chaining': 'warn', + 'no-constant-condition': 'off', + 'no-prototype-builtins': 'off', + 'no-case-declarations': 'off', + 'no-useless-catch': 'off', + 'prefer-rest-params': 'off', + }, + }, + + // ts files linting + { + files: ['**/*.{ts,tsx}'], + extends: [...tseslint.configs.recommended], + languageOptions: { + parserOptions: { + project: './tsconfig.eslint.json', + tsconfigRootDir: __dirname, + }, + }, + rules: { + '@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }], + '@typescript-eslint/no-loss-of-precision': 0, + '@typescript-eslint/no-inferrable-types': 0, + '@typescript-eslint/ban-types': 0, + '@typescript-eslint/ban-ts-comment': 0, + '@typescript-eslint/no-empty-function': 0, + '@typescript-eslint/explicit-module-boundary-types': 0, + '@typescript-eslint/no-explicit-any': 0, + '@typescript-eslint/no-duplicate-enum-values': 0, + '@typescript-eslint/no-use-before-define': 0, + '@typescript-eslint/no-empty-object-type': 0, + }, + }, + + // react tsx files linting + { + files: ['**/*.{tsx}'], + ...reactPlugin.configs.recommended, + ...reactHooks.configs.recommended, + plugins: { + 'jsx-a11y': jsxA11yPlugin, + }, + rules: {}, + }, + + // + // test jest file linting + // + { + files: ['test/**/*.spec.ts'], + ...jestPlugin.configs['flat/recommended'], + rules: {}, + }, + + // after all eslint plugins configs to override, see https://github.com/prettier/eslint-config-prettier + // @ts-ignore + prettierConfig, +); diff --git a/jest.config.ts b/jest.config.ts new file mode 100644 index 0000000..5442e15 --- /dev/null +++ b/jest.config.ts @@ -0,0 +1,18 @@ +import type { Config } from 'jest'; + +const esm = ['internmap', 'd3-*'].join('|'); + +const config: Config = { + preset: 'ts-jest', + testEnvironment: 'jsdom', + collectCoverage: false, + testRegex: '(/__tests__/.*\\.(test|spec))\\.ts$', + collectCoverageFrom: ['src/**/*.ts'], + // Transform esm to cjs. + transformIgnorePatterns: [ + `/node_modules/(?!(${esm}))`, + `/node_modules/.pnpm/(?!(${esm}))`, + ], +}; + +export default config; diff --git a/package.json b/package.json index c709f5f..6c7a199 100644 --- a/package.json +++ b/package.json @@ -1,79 +1,132 @@ { - "name": "GPT-Vis", + "name": "@antv/gpt-vis", "version": "0.0.1", - "description": "GPT-Vis", - "repository": "git@github.com:eosphoros-ai/GPT-Vis.git", + "description": "Components for GPTs, generative AI, and LLM projects. Not only UI Components.", + "keywords": [ + "antv", + "GPTs", + "LLM", + "vis", + "LUI" + ], + "homepage": "https://gpt-vis.antv.com", + "repository": { + "type": "git", + "url": "https://github.com/antvis/GPT-Vis" + }, "license": "MIT", + "author": "antvis", "sideEffects": false, - "module": "es/index.js", - "types": "es/index.d.ts", + "type": "module", + "main": "dist/cjs/index.js", + "module": "dist/esm/index.js", + "typings": "dist/esm/index.d.ts", + "jsdelivr": "dist/umd/index.min.js", + "unpkg": "dist/umd/index.min.js", "files": [ - "src", - "lib", - "es" + "dist", + "README.md", + "!dist/umd/report.html" ], "scripts": { - "build": "npm run build:lib && npm run build:es", - "build:es": "rm -rf es && tsc --module esnext --outDir es", - "build:lib": "rm -rf lib && tsc --module commonjs --outDir lib", - "dev": "vite dev", - "prepublishOnly": "npm run build" + "prepare": "husky", + "start": "pnpm dev", + "dev": "dumi dev", + "build": "father build", + "docs:build": "dumi build", + "docs:build-analyze": "ANALYZE=1 dumi build", + "docs:deploy": "gh-pages -d docs-dist", + "deploy": "pnpm docs:build && pnpm docs:deploy", + "check-deps": "father doctor", + "check-format": "prettier './**/*.{ts,tsx,js,jsx,json,md,css,less}' --check", + "format": "prettier './**/*.{ts,tsx,js,jsx,json,md,css,less}' --write", + "lint:ts": "eslint src/**", + "lint:ts-fix": "pnpm lint:ts --fix", + "test:unit": "jest --config ./jest.config.ts", + "test:size": "pnpm build && limit-size", + "changeset": "changeset add", + "publish-version": "changeset version && node scripts/sync-version", + "publish-package": "pnpm build && changeset publish" }, - "husky": { - "hooks": { - "pre-commit": "lint-staged" - } + "dependencies": { + "@ant-design/graphs": "^2.0.0-beta.4", + "@ant-design/icons": "^5.4.0", + "@ant-design/plots": "^2.2.5", + "@ant-design/x": "^1.0.0-alpha.12", + "@antv/l7": "^2.22.0", + "@antv/larkmap": "^1.5.1", + "@babel/runtime": "^7.18.0", + "lodash": "^4.17.21", + "react-markdown": "^9.0.1", + "rehype-raw": "^7.0.0", + "styled-components": "^6.0.7" + }, + "devDependencies": { + "@changesets/changelog-github": "^0.5.0", + "@changesets/cli": "^2.27.9", + "@commitlint/cli": "^19.5.0", + "@commitlint/config-conventional": "^19.5.0", + "@types/jest": "^29.5.14", + "@types/lodash": "^4.17.13", + "@types/react": "^18.0.0", + "antd": "^5.0.0", + "dumi": "^2.4.13", + "eslint": "^9.14.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-jest": "^28.9.0", + "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-react": "^7.37.2", + "eslint-plugin-react-hooks": "^5.0.0", + "father": "^4.5.1", + "gh-pages": "^6.2.0", + "globals": "^15.12.0", + "husky": "^9.1.6", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "limit-size": "^0.1.4", + "lint-staged": "^15.2.10", + "npm-run-all": "^4.1.5", + "prettier": "^3.3.3", + "prettier-plugin-organize-imports": "^4.1.0", + "react": "^18.0.0", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", + "typescript": "^5.6.3", + "typescript-eslint": "^8.13.0", + "webpack-bundle-analyzer": "^4.10.2" + }, + "peerDependencies": { + "antd": "^5.0.0", + "react": ">=16.9.0" + }, + "engines": { + "node": ">=18", + "pnpm": ">=8" }, "lint-staged": { "*.{md,json}": [ - "prettier --write --no-error-on-unmatched-pattern" + "prettier --cache --write --no-error-on-unmatched-pattern" ], "*.{css,less}": [ - "prettier --write" + "prettier --cache --write" ], "*.{js,jsx}": [ "eslint --fix", - "prettier --write" + "prettier --cache --write" ], "*.{ts,tsx}": [ "eslint --fix", - "prettier --parser=typescript --write" + "prettier --cache --write" ] }, - "dependencies": { - "@ant-design/icons": "^5.2.6", - "@antv/ava": "^3.3.1", - "@berryv/g2-react": "^0.1.0", - "antd": "^5.12.2", - "copy-to-clipboard": "^3.3.3", - "lodash": "^4.17.21", - "react-markdown": "^8.0.7", - "react-syntax-highlighter": "^15.5.0", - "rehype-raw": "6.1.1", - "sql-formatter": "^14.0.0" - }, - "devDependencies": { - "@types/lodash": "^4.14.202", - "@types/react": "^18.2.45", - "@types/react-dom": "^18.2.18", - "@types/react-syntax-highlighter": "^15.5.11", - "@umijs/lint": "^4.0.89", - "@umijs/plugins": "^4.0.89", - "eslint": "^8.56.0", - "husky": "^8.0.3", - "lint-staged": "^15.2.0", - "prettier": "^3.1.1", - "prettier-plugin-organize-imports": "^3.2.4", - "prettier-plugin-packagejson": "^2.4.7", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "tailwindcss": "^3.3.6", - "typescript": "^5.4.5", - "vite": "^5.2.13" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - }, - "authors": [] + "limit-size": [ + { + "path": "dist/umd/index.min.js", + "limit": "5 Mb" + } + ], + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + } } diff --git a/scripts/sync-version.js b/scripts/sync-version.js new file mode 100644 index 0000000..7a4e8b1 --- /dev/null +++ b/scripts/sync-version.js @@ -0,0 +1,26 @@ +import { exec } from 'node:child_process'; +import { writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import pkg from '../package.json' with { type: 'json' }; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +writeFileSync( + join(__dirname, '..', 'src', 'version.ts'), + `export default '${pkg.version}'`, + 'utf8', +); + +exec('git add .', (error, stdout, stderr) => { + if (error) { + console.log(`sync version error: ${error.message}`); + return; + } + if (stderr) { + console.log(`sync version stderr: ${stderr}`); + return; + } + console.log(`sync version success.`); +}); diff --git a/src/Area/demos/common.tsx b/src/Area/demos/common.tsx new file mode 100644 index 0000000..47a8a0e --- /dev/null +++ b/src/Area/demos/common.tsx @@ -0,0 +1,20 @@ +import { Area } from '@antv/gpt-vis'; +import React from 'react'; + +const data = [ + { time: '1991', value: 3 }, + { time: '1992', value: 4 }, + { time: '1993', value: 3.5 }, + { time: '1994', value: 5 }, + { time: '1995', value: 4.9 }, + { time: '1996', value: 6 }, + { time: '1997', value: 7 }, + { time: '1998', value: 9 }, + { time: '1999', value: 13 }, +]; + +export default () => { + return ( + + ); +}; diff --git a/src/Area/demos/markdown.tsx b/src/Area/demos/markdown.tsx new file mode 100644 index 0000000..66b109c --- /dev/null +++ b/src/Area/demos/markdown.tsx @@ -0,0 +1,55 @@ +import type { BubbleProps } from '@ant-design/x'; +import { Bubble } from '@ant-design/x'; +import { Area, ChartType, GPTVis, withChartCode } from '@antv/gpt-vis'; +import React from 'react'; + +const markdownContent = ` +当然了,以下是为你绘制的一个面积图 + +\`\`\`vis-chart +{ + "type": "area", + "data": [{"time":2013,"value":59.3},{"time":2014,"value":64.4},{"time":2015,"value":68.9},{"time":2016,"value":74.4},{"time":2017,"value":82.7},{"time":2018,"value":91.9},{"time":2019,"value":99.1},{"time":2020,"value":101.6},{"time":2021,"value":114.4},{"time":2022,"value":121}], + "axisXTitle": "year", + "axisYTitle": "GDP" +} +\`\`\` +`; + +const bgStyle = { + display: 'grid', + gridGap: '20px 0', + padding: 20, + background: '#f7f7f7', + borderRadius: 8, +}; + +const CodeComponent = withChartCode({ + components: { [ChartType.Area]: Area }, + style: { width: 350 }, +}); +const RenderMarkdown: BubbleProps['messageRender'] = (content) => ( + {content} +); + +export default () => ( +
+ + +
+); diff --git a/src/Area/demos/stack.tsx b/src/Area/demos/stack.tsx new file mode 100644 index 0000000..c71ba1f --- /dev/null +++ b/src/Area/demos/stack.tsx @@ -0,0 +1,117 @@ +import { Bubble, type BubbleProps } from '@ant-design/x'; +import { Area, ChartType, GPTVis, withChartCode } from '@antv/gpt-vis'; +import React from 'react'; + +const markdownContent = ` +当然了,以下是为你绘制的一个面积图 + +\`\`\`vis-chart +{ + "type": "area", + "data": [ + { + "time": "1974", + "value": 107, + "group": "Gas flaring" + }, + { + "time": "1974", + "value": 208, + "group": "Renewables" + }, + { + "time": "1974", + "value": 356, + "group": "Fossil fuels" + }, + { + "time": "1975", + "value": 173, + "group": "Gas flaring" + }, + { + "time": "1975", + "value": 415, + "group": "Renewables" + }, + { + "time": "1975", + "value": 364, + "group": "Fossil fuels" + }, + { + "time": "1976", + "value": 117, + "group": "Gas flaring" + }, + { + "time": "1976", + "value": 220, + "group": "Renewables" + }, + { + "time": "1976", + "value": 373, + "group": "Fossil fuels" + }, + { + "time": "1977", + "value": 122, + "group": "Gas flaring" + }, + { + "time": "1977", + "value": 225, + "group": "Renewables" + }, + { + "time": "1977", + "value": 382, + "group": "Fossil fuels" + } + ], + "stack": true, + "axisXTitle": "year", + "axisYTitle": "value" +} +\`\`\` +`; + +const bgStyle = { + display: 'grid', + gridGap: '20px 0', + background: '#f7f7f7', + padding: 20, + borderRadius: 8, +}; + +const CodeComponent = withChartCode({ + components: { [ChartType.Area]: Area }, + style: { width: 350 }, +}); + +const RenderMarkdown: BubbleProps['messageRender'] = (content) => ( + {content} +); + +export default () => ( +
+ + +
+); diff --git a/src/Area/index.md b/src/Area/index.md new file mode 100644 index 0000000..1dc975e --- /dev/null +++ b/src/Area/index.md @@ -0,0 +1,53 @@ +--- +order: 4 +group: + order: 1 + title: 统计图 +demo: { cols: 2 } +nav: { title: '组件', order: 1 } +--- + +# Area 面积图 + +## 代码演示 + +单独使用 + +使用 Markdown 协议 +堆叠面积图 + +## Spec + +```json +{ + "type": "area", + "data": [ + { "time": 2018, "value": 91.9 }, + { "time": 2019, "value": 99.1 }, + { "time": 2020, "value": 101.6 }, + { "time": 2021, "value": 114.4 }, + { "time": 2022, "value": 121 } + ] +} +``` + +## API + +### AreaProps + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| ---------- | -------------- | -------- | ------ | -------------------------------------------------------------------------------------------------- | +| data | AreaDataItem[] | 是 | - | 数据 | +| stack | boolean | 否 | - | 是否开启堆叠,开启堆叠面积图需数据中含有 group 字段 | +| title | string | 否 | - | 图表的标题 | +| axisXTitle | string | 否 | - | x 轴的标题 | +| axisYTitle | string | 否 | - | y 轴的标题 | +| ... | - | - | - | 更多属性,详见 [Ant Design Charts ](https://ant-design-charts.antgroup.com/options/plots/overview) | + +### AreaDataItem + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| ----- | ------ | -------- | ------ | -------------- | +| time | string | 是 | - | 数据的时序名称 | +| value | number | 是 | - | 数据的值 | +| group | string | 否 | - | 数据分组名称 | diff --git a/src/Area/index.tsx b/src/Area/index.tsx new file mode 100644 index 0000000..c9d9952 --- /dev/null +++ b/src/Area/index.tsx @@ -0,0 +1,43 @@ +import type { AreaConfig } from '@ant-design/plots'; +import { Area as ADCArea } from '@ant-design/plots'; +import { get } from 'lodash'; +import React from 'react'; +import { usePlotConfig } from '../ConfigProvider/hooks'; +import type { BasePlotProps } from '../types'; + +type AreaDataItem = { + time: string | number; + value: number; + [key: string]: string | number; +}; + +export type AreaProps = BasePlotProps & Partial; + +const defaultConfig = (props: AreaConfig): AreaConfig => { + const { data, xField = 'time', yField = 'value' } = props; + const hasGroupField = get(data, '[0].group') !== undefined; + const axisYTitle = get(props, 'axis.y.title'); + const defalutStyle = hasGroupField ? {} : { opacity: 0.6 }; + + return { + xField, + yField, + style: defalutStyle, + colorField: hasGroupField ? 'group' : undefined, + tooltip: (d: Record) => { + const tooltipName = axisYTitle || d[xField as string]; + return { + name: tooltipName, + value: d[yField as string], + }; + }, + }; +}; + +const Area = (props: AreaProps) => { + const config = usePlotConfig('Area', defaultConfig, props); + + return ; +}; + +export default Area; diff --git a/src/Bar/demos/common.tsx b/src/Bar/demos/common.tsx new file mode 100644 index 0000000..fa921f7 --- /dev/null +++ b/src/Bar/demos/common.tsx @@ -0,0 +1,229 @@ +import { Bar } from '@antv/gpt-vis'; +import React from 'react'; + +const data = [ + { + 城市: '七台河', + 销售额: 52827.32, + }, + { + 城市: '万县', + 销售额: 16921.576, + }, + { + 城市: '三亚', + 销售额: 22698.396, + }, + { + 城市: '三岔子', + 销售额: 3262.98, + }, + { + 城市: '上海', + 销售额: 182450.567, + }, + { + 城市: '上虞', + 销售额: 10672.872, + }, + { + 城市: '东丰', + 销售额: 1785.84, + }, + { + 城市: '东台', + 销售额: 2789.892, + }, + { + 城市: '东宁', + 销售额: 2706.2, + }, + { + 城市: '东村', + 销售额: 13692.14, + }, + { + 城市: '东海', + 销售额: 4508.28, + }, + { + 城市: '东胜', + 销售额: 12766.068, + }, + { + 城市: '东莞', + 销售额: 10165.89, + }, + { + 城市: '东营', + 销售额: 17153.92, + }, + { + 城市: '中枢', + 销售额: 1050.42, + }, + { + 城市: '丰县', + 销售额: 10309.516, + }, + { + 城市: '丰镇', + 销售额: 3507.336, + }, + { + 城市: '临水', + 销售额: 21443.1, + }, + { + 城市: '临江', + 销售额: 36496.74, + }, + { + 城市: '临汾', + 销售额: 26205.48, + }, + { + 城市: '临沂', + 销售额: 97200.74, + }, + { + 城市: '临海', + 销售额: 7071.456, + }, + { + 城市: '临清', + 销售额: 38676.12, + }, + { + 城市: '丹东', + 销售额: 45447.612, + }, + { + 城市: '丹江口', + 销售额: 4879.616, + }, + { + 城市: '丽水', + 销售额: 3983.616, + }, + { + 城市: '义乌', + 销售额: 34511.624, + }, + { + 城市: '义马', + 销售额: 1144.024, + }, + { + 城市: '乌海', + 销售额: 16096.64, + }, + { + 城市: '乌达', + 销售额: 3474.66, + }, + { + 城市: '乐城', + 销售额: 3241, + }, + { + 城市: '乐山', + 销售额: 12561.892, + }, + { + 城市: '九台', + 销售额: 32535.944, + }, + { + 城市: '九江', + 销售额: 29890.7, + }, + { + 城市: '二道江', + 销售额: 4461.24, + }, + { + 城市: '云浮', + 销售额: 6351.212, + }, + { + 城市: '云阳', + 销售额: 24699.64, + }, + { + 城市: '五常', + 销售额: 3771.46, + }, + { + 城市: '亳州', + 销售额: 16961.14, + }, + { + 城市: '仙居', + 销售额: 6868.512, + }, + { + 城市: '仙桃', + 销售额: 16600.164, + }, + { + 城市: '仪征', + 销售额: 8566.628, + }, + { + 城市: '任丘', + 销售额: 12106.78, + }, + { + 城市: '余下', + 销售额: 103.04, + }, + { + 城市: '余姚', + 销售额: 12621.84, + }, + { + 城市: '佛山', + 销售额: 7500.388, + }, + { + 城市: '佳木斯', + 销售额: 63263.34, + }, + { + 城市: '依兰', + 销售额: 26874.82, + }, + { + 城市: '保定', + 销售额: 124133.1, + }, + { + 城市: '信宜', + 销售额: 4771.06, + }, + { + 城市: '信阳', + 销售额: 38849.412, + }, + { + 城市: '兖州', + 销售额: 28648.2, + }, + { + 城市: '公主岭', + 销售额: 21210.756, + }, + { + 城市: '六合', + 销售额: 5775.98, + }, + { + 城市: '兰州', + 销售额: 42543.144, + }, +]; + +export default () => { + return ; +}; diff --git a/src/Bar/demos/group.tsx b/src/Bar/demos/group.tsx new file mode 100644 index 0000000..9dc2be7 --- /dev/null +++ b/src/Bar/demos/group.tsx @@ -0,0 +1,72 @@ +import { Bubble, type BubbleProps } from '@ant-design/x'; +import { Bar, ChartType, GPTVis, withChartCode } from '@antv/gpt-vis'; +import React from 'react'; + +const markdownContent = ` +当然了,以下是为你绘制的一个条形图 + +\`\`\`vis-chart +{ + "type": "bar", + "data": [ + { "group": "London", "category": "Jan.", "value": 18.9 }, + { "group": "London", "category": "Feb.", "value": 28.8 }, + { "group": "London", "category": "Mar.", "value": 39.3 }, + { "group": "London", "category": "Apr.", "value": 81.4 }, + { "group": "London", "category": "May.", "value": 47 }, + { "group": "London", "category": "Jun.", "value": 20.3 }, + { "group": "London", "category": "Jul.", "value": 24 }, + { "group": "London", "category": "Aug.", "value": 35.6 }, + { "group": "Berlin", "category": "Jan.", "value": 12.4 }, + { "group": "Berlin", "category": "Feb.", "value": 23.2 }, + { "group": "Berlin", "category": "Mar.", "value": 34.5 }, + { "group": "Berlin", "category": "Apr.", "value": 99.7 }, + { "group": "Berlin", "category": "May.", "value": 52.6 }, + { "group": "Berlin", "category": "Jun.", "value": 35.5 }, + { "group": "Berlin", "category": "Jul.", "value": 37.4 }, + { "group": "Berlin", "category": "Aug.", "value": 42.4 } + ], + "group": true, + "axisXTitle": "month", + "axisYTitle": "value" +} +\`\`\` +`; + +const bgStyle = { + display: 'grid', + gridGap: '20px 0', + background: '#f7f7f7', + padding: 20, + borderRadius: 8, +}; + +const CodeComponent = withChartCode({ + components: { [ChartType.Bar]: Bar }, +}); + +const RenderMarkdown: BubbleProps['messageRender'] = (content) => ( + {content} +); + +export default () => ( +
+ + +
+); diff --git a/src/Bar/demos/markdown.tsx b/src/Bar/demos/markdown.tsx new file mode 100644 index 0000000..e24da85 --- /dev/null +++ b/src/Bar/demos/markdown.tsx @@ -0,0 +1,63 @@ +import { Bubble, type BubbleProps } from '@ant-design/x'; +import { Bar, ChartType, GPTVis, withChartCode } from '@antv/gpt-vis'; +import React from 'react'; + +const markdownContent = ` +当然了,以下是为你绘制的一个条形图 + +\`\`\`vis-chart +{ + "type": "bar", + "data": [ + { "category": "1951 年", "value": 38 }, + { "category": "1952 年", "value": 52 }, + { "category": "1956 年", "value": 61 }, + { "category": "1957 年", "value": 145 }, + { "category": "1958 年", "value": 48 }, + { "category": "1959 年", "value": 38 }, + { "category": "1960 年", "value": 38 }, + { "category": "1962 年", "value": 38 } + ], + "axisXTitle": "year", + "axisYTitle": "sales" +} +\`\`\` +`; + +const bgStyle = { + display: 'grid', + gridGap: '20px 0', + background: '#f7f7f7', + padding: 20, + borderRadius: 8, +}; + +const CodeComponent = withChartCode({ + components: { [ChartType.Bar]: Bar }, +}); + +const RenderMarkdown: BubbleProps['messageRender'] = (content) => ( + {content} +); + +export default () => ( +
+ + +
+); diff --git a/src/Bar/demos/stack.tsx b/src/Bar/demos/stack.tsx new file mode 100644 index 0000000..9255d9a --- /dev/null +++ b/src/Bar/demos/stack.tsx @@ -0,0 +1,71 @@ +import { Bubble, type BubbleProps } from '@ant-design/x'; +import { Bar, ChartType, GPTVis, withChartCode } from '@antv/gpt-vis'; +import React from 'react'; + +const markdownContent = ` +当然了,以下是为你绘制的一个条形图 + +\`\`\`vis-chart +{ + "type": "bar", + "data": [ + { "group": "London", "category": "Jan.", "value": 18.9 }, + { "group": "London", "category": "Feb.", "value": 28.8 }, + { "group": "London", "category": "Mar.", "value": 39.3 }, + { "group": "London", "category": "Apr.", "value": 81.4 }, + { "group": "London", "category": "May.", "value": 47 }, + { "group": "London", "category": "Jun.", "value": 20.3 }, + { "group": "London", "category": "Jul.", "value": 24 }, + { "group": "London", "category": "Aug.", "value": 35.6 }, + { "group": "Berlin", "category": "Jan.", "value": 12.4 }, + { "group": "Berlin", "category": "Feb.", "value": 23.2 }, + { "group": "Berlin", "category": "Mar.", "value": 34.5 }, + { "group": "Berlin", "category": "Apr.", "value": 99.7 }, + { "group": "Berlin", "category": "May.", "value": 52.6 }, + { "group": "Berlin", "category": "Jun.", "value": 35.5 }, + { "group": "Berlin", "category": "Jul.", "value": 37.4 }, + { "group": "Berlin", "category": "Aug.", "value": 42.4 } + ], + "stack": true, + "axisXTitle": "month", + "axisYTitle": "value" +} +\`\`\` +`; + +const bgStyle = { + display: 'grid', + gridGap: '20px 0', + background: '#f7f7f7', + padding: 20, + borderRadius: 8, +}; + +const CodeComponent = withChartCode({ + components: { [ChartType.Bar]: Bar }, +}); +const RenderMarkdown: BubbleProps['messageRender'] = (content) => ( + {content} +); + +export default () => ( +
+ + +
+); diff --git a/src/Bar/index.md b/src/Bar/index.md new file mode 100644 index 0000000..49a65e4 --- /dev/null +++ b/src/Bar/index.md @@ -0,0 +1,53 @@ +--- +order: 4 +group: + order: 1 + title: 统计图 +demo: { cols: 2 } +--- + +# Bar 条形图 + +## 代码演示 + +单独使用 + +使用 Markdown 协议 + +分组条形图 +堆叠条形图 + +## Spec + +```json +{ + "type": "bar", + "data": [ + { "category": "<分类一>", "value": <数值> }, + { "category": "<分类二>", "value": <数值> }, + { "category": "<分类三>", "value": <数值> } + ] +} +``` + +## API + +### BarProps + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| ---------- | ------------- | -------- | ------ | -------------------------------------------------------------------------------------------------- | +| data | BarDataItem[] | 是 | - | 数据 | +| title | string | 否 | - | 图表的标题 | +| group | boolean | 否 | - | 是否开启分组,开启分组条形图需数据中含有 group 字段 | +| stack | boolean | 否 | - | 是否开启堆叠,开启堆叠条形图需数据中含有 group 字段 | +| axisXTitle | string | 否 | - | x 轴的标题 | +| axisYTitle | string | 否 | - | y 轴的标题 | +| ... | - | - | - | 更多属性,详见 [Ant Design Charts ](https://ant-design-charts.antgroup.com/options/plots/overview) | + +### BarDataItem + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| -------- | ------ | -------- | ------ | ------------ | +| category | string | 是 | - | 数据分类名称 | +| value | number | 是 | - | 数据分类值 | +| group | number | 否 | - | 数据分组名称 | diff --git a/src/Bar/index.tsx b/src/Bar/index.tsx new file mode 100644 index 0000000..399eacf --- /dev/null +++ b/src/Bar/index.tsx @@ -0,0 +1,46 @@ +import type { BarConfig } from '@ant-design/plots'; +import { Bar as ADCBar } from '@ant-design/plots'; +import { get } from 'lodash'; +import React from 'react'; +import { usePlotConfig } from '../ConfigProvider/hooks'; +import type { BasePlotProps } from '../types'; + +type BarDataItem = { + category: string; + value: number; + [key: string]: string | number; +}; + +export type BarProps = BasePlotProps & Partial; + +const defaultConfig = (props: BarConfig): BarConfig => { + const { data, xField = 'category', yField = 'value' } = props; + const hasGroupField = get(data, '[0].group') !== undefined; + const axisYTitle = get(props, 'axis.y.title'); + + return { + xField, + yField, + colorField: hasGroupField ? 'group' : undefined, + tooltip: (d) => { + const tooltipName = axisYTitle || d[xField as string]; + return { + name: tooltipName, + value: d[yField as string], + }; + }, + style: { + // 圆角样式 + radiusTopLeft: 5, + radiusTopRight: 5, + }, + }; +}; + +const Bar = (props: BarProps) => { + const config = usePlotConfig('Bar', defaultConfig, props); + + return ; +}; + +export default Bar; diff --git a/src/ChartCodeRender/Loading.tsx b/src/ChartCodeRender/Loading.tsx new file mode 100644 index 0000000..d951e7f --- /dev/null +++ b/src/ChartCodeRender/Loading.tsx @@ -0,0 +1,30 @@ +import { LoadingOutlined } from '@ant-design/icons'; +import React from 'react'; +import styled from 'styled-components'; + +const StyledLoading = styled.div` + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + height: 300px; + background-image: linear-gradient(135deg, #e3f3ff 0%, #f1eeff 100%); + color: rgba(0, 0, 0, 88%); + + &-icon { + margin-bottom: 6px; + } +`; + +const Loading = () => { + return ( + +
+ +
+

数据生成中

+
+ ); +}; + +export default Loading; diff --git a/src/ChartCodeRender/VisChart.tsx b/src/ChartCodeRender/VisChart.tsx new file mode 100644 index 0000000..d75f968 --- /dev/null +++ b/src/ChartCodeRender/VisChart.tsx @@ -0,0 +1,77 @@ +import React, { memo, useRef, useState } from 'react'; +import styled, { createGlobalStyle } from 'styled-components'; +import Loading from './Loading'; +import type { ChartComponents, ChartJson } from './type'; + +const StyledGPTVis = styled.div` + min-width: 300px; + height: 300px; + max-width: 100%; +`; +const GlobalStyles = createGlobalStyle` + pre:has(.gpt-vis) { + overflow: hidden; + } +`; + +type RenderVisChartProps = { + content: string; + components: ChartComponents; + debug?: boolean; + loadingTimeout: number; + style?: React.CSSProperties; +}; + +export const RenderVisChart: React.FC = memo( + ({ style, content, components, debug, loadingTimeout }) => { + const timeoutRef = useRef(); + const [loading, setLoading] = useState(true); + let chartJson: ChartJson; + + try { + chartJson = JSON.parse(content); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (e) { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + if (debug) { + console.warn('GPT-Vis withChartCode parse content timeout'); + } + } + timeoutRef.current = setTimeout(() => { + setLoading(false); + }, loadingTimeout); + + if (loading) { + return ( + + + + ); + } + + return

Chart generation timeout.

; + } + + const { type, ...chartProps } = chartJson; + const ChartComponent = components[type]; + + // debug mode print chartJson + if (debug) { + console.log('GPT-Vis withChartCode get chartJson parse from vis-chart code block', chartJson); + } + + // If the chart type is not supported, display an error message + if (!ChartComponent) { + return

{`Chart type "${type}" is not supported.`}

; + } + + // Render the supported chart component with data + return ( + + + + + ); + }, +); diff --git a/src/ChartCodeRender/demos/common.tsx b/src/ChartCodeRender/demos/common.tsx new file mode 100644 index 0000000..a68d42f --- /dev/null +++ b/src/ChartCodeRender/demos/common.tsx @@ -0,0 +1,35 @@ +import { ChartType, Column, GPTVis, withChartCode } from '@antv/gpt-vis'; +import React from 'react'; + +const markdownContent = ` +\`\`\`vis-chart +{ + "type": "column", + "data": [ + { "category": "第一产业", "value": 7200.0 }, + { "category": "第二产业", "value": 36600.0 }, + { "category": "第三产业" ,"value": 41000.0 }, + { "category": "第四产业" ,"value": 21000.0 }, + { "category": "其他产业" ,"value": 81000.0 } + ] +} +\`\`\` +`; + +// 自定义代码块渲染组件,NOTE: withChartCode 不要直接放入函数内部,避免重复渲染抖动问题!!! +const CodeComponent = withChartCode({ + components: { [ChartType.Column]: Column }, + debug: true, +}); + +export default () => { + return ( + + {markdownContent} + + ); +}; diff --git a/src/ChartCodeRender/demos/default.tsx b/src/ChartCodeRender/demos/default.tsx new file mode 100644 index 0000000..b3d543d --- /dev/null +++ b/src/ChartCodeRender/demos/default.tsx @@ -0,0 +1,42 @@ +import { GPTVis, withDefaultChartCode } from '@antv/gpt-vis'; +import React from 'react'; + +const commonString = ` +当然可以!在 JavaScript 中,你可以使用加法运算符 \`+\` 来实现两个数字的相加。以下是一个简单的函数,它接受两个参数 \`a\` 和 \`b\`,并返回它们的和: + +\`\`\`javascript\nfunction add(a, b) {\n return a + b;\n}\n\n// 示例用法\nconst result = add(3, 4);\nconsole.log(result); // 输出:7\n\`\`\`\n\n在这个示例中,我们定义了一个名为 \`add\` 的函数,它接受两个参数 \`a\` 和 \`b\`。函数的主体只有一行代码,使用加法运算符 \`+\` 将 \`a\` 和 \`b\` 相加,并返回结果。\n\n然后,我们调用 +`; + +const markdownContent = ` +\`\`\`vis-chart +{ + "type": "pie", + "data": [ + { "category": "分类一", "value": 27 }, + { "category": "分类二", "value": 25 }, + { "category": "分类三", "value": 18 }, + { "category": "分类四", "value": 15 }, + { "category": "分类五", "value": 10 }, + { "category": "其他", "value": 5 } + ] +} +\`\`\` +`; + +// 自定义代码块渲染组件,NOTE: withDefaultChartCode 不要直接放入函数内部,避免重复渲染抖动问题!!! +const CodeComponent = withDefaultChartCode({ + debug: true, +}); + +export default () => ( +
+ {commonString} + + {markdownContent} + +
+); diff --git a/src/ChartCodeRender/demos/extra.tsx b/src/ChartCodeRender/demos/extra.tsx new file mode 100644 index 0000000..38a1ca1 --- /dev/null +++ b/src/ChartCodeRender/demos/extra.tsx @@ -0,0 +1,49 @@ +import type { CodeBlockComponent } from '@antv/gpt-vis'; +import { GPTVis, withDefaultChartCode } from '@antv/gpt-vis'; +import React from 'react'; + +/** + * 自定义代码块渲染器 + */ +const MyUIRenderer: CodeBlockComponent = ({ children }) => { + return
{children}
; +}; + +const markdownContent = ` +\`\`\`my-ui +my data ... +\`\`\` + +\`\`\`vis-chart +{ + "type": "pie", + "data": [ + { "category": "分类一", "value": 27 }, + { "category": "分类二", "value": 25 }, + { "category": "分类三", "value": 18 }, + { "category": "分类四", "value": 15 }, + { "category": "分类五", "value": 10 }, + { "category": "其他", "value": 5 } + ] +} +\`\`\` +`; + +// 自定义代码块渲染组件,NOTE: withDefaultChartCode 不要直接放入函数内部,避免重复渲染抖动问题!!! +const CodeComponent = withDefaultChartCode({ + languageRenderers: { + 'my-ui': MyUIRenderer, + }, +}); + +export default () => ( +
+ + {markdownContent} + +
+); diff --git a/src/ChartCodeRender/demos/stream.tsx b/src/ChartCodeRender/demos/stream.tsx new file mode 100644 index 0000000..c125197 --- /dev/null +++ b/src/ChartCodeRender/demos/stream.tsx @@ -0,0 +1,110 @@ +import { Bubble, type BubbleProps } from '@ant-design/x'; +import { ChartType, Column, GPTVis, withChartCode } from '@antv/gpt-vis'; +import React, { useEffect, useRef, useState } from 'react'; + +const markdownContent = ` +当然了,以下是为你绘制的一个柱状图 + +\`\`\`vis-chart +{ + "type": "column", + "data": [ + { "category": "第一产业", "value": 7200.0 }, + { "category": "第二产业", "value": 36600.0 }, + { "category": "第三产业" ,"value": 41000.0 } + ] +} +\`\`\` +`; + +const bgStyle = { + display: 'grid', + gridGap: '20px 0', + background: '#f7f7f7', + padding: 20, + borderRadius: 8, +}; + +// 自定义代码块渲染组件,NOTE: withChartCode 不要直接放入函数内部,避免重复渲染抖动问题!!! +const CodeComponent = withChartCode({ + components: { [ChartType.Column]: Column }, + loadingTimeout: 3000, +}); + +const RenderMarkdown: BubbleProps['messageRender'] = (content) => ( + {content} +); + +const useStreamText = () => { + const [text, setText] = useState(''); + const nowTextRef = useRef(''); + const timerRef = useRef(null); + + /** 模拟流式输出markdownContent */ + const streamOutput = () => { + timerRef.current = setInterval(() => { + const step = parseInt((Math.random() * 10).toString(), 20); + const nowText = + nowTextRef.current + + markdownContent.substring(nowTextRef.current.length, nowTextRef.current.length + step); + nowTextRef.current = nowText; + setText(nowText); + if (text.length === markdownContent.length - 1) { + clearTimeout(timerRef.current); + } + }, 200); + }; + + const restart = () => { + if (timerRef.current) clearTimeout(timerRef.current); + timerRef.current = null; + nowTextRef.current = ''; + setText(''); + streamOutput(); + }; + + return [text, restart] as const; +}; + +export default () => { + const [text, restart] = useStreamText(); + + useEffect(() => { + restart(); + }, []); + + return ( +
+ + + + + +
+ ); +}; diff --git a/src/ChartCodeRender/index.md b/src/ChartCodeRender/index.md new file mode 100644 index 0000000..1929c59 --- /dev/null +++ b/src/ChartCodeRender/index.md @@ -0,0 +1,46 @@ +--- +order: 2 +group: + order: 10 + title: 其他 +--- + +# withChartCode 拓展代码块渲染 + +自定义拓展 Markdown 代码块渲染,将代码块自定义可视化。 + +:::warning +这是一条警告信息 +`withChartCode`、`withDefaultChartCode` 方法不要直接放入函数内部,避免重复渲染造成抖动问题!!!如需放入函数内部,用 `useMemo` 缓存一下。 +::: + +## 使用 withChartCode + + + +## 使用 withDefaultChartCode + +`withDefaultChartCode`包含了[默认的图表](https://github.com/antvis/GPT-Vis/tree/main/src/export.ts#L76),接入简单 + + + +## 在流式输出中使用 + + + +## 拓展其他 code 的自定义渲染 + + + +## API + +### WithChartCodeOptions + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| ----------------- | --------------------- | -------- | ------- | -------------------------------------- | +| components | `ChartComponents` | 否 | - | 要额外加载的图表组件 | +| languageRenderers | `LanguageRenderers` | 否 | - | 自定义其它语言代码块渲染器 | +| defaultRenderer | `CodeRenderer` | 否 | - | 默认的代码渲染器 | +| debug | `boolean` | 否 | `false` | 打开调试日志 | +| loadingTimeout | `number` | 否 | - | 设置 loading 动画的超时时间,默认为 5s | +| style | `React.CSSProperties` | 否 | - | 图表样式,配置容器样式 | diff --git a/src/ChartCodeRender/index.tsx b/src/ChartCodeRender/index.tsx new file mode 100644 index 0000000..ea3fb23 --- /dev/null +++ b/src/ChartCodeRender/index.tsx @@ -0,0 +1,78 @@ +import { get } from 'lodash'; +import React from 'react'; +import { DEFAULT_CHART_COMPONENTS } from '../export'; +import type { CodeBlockComponent, WithChartCodeOptions } from './type'; +import { RenderVisChart } from './VisChart'; + +const RenderDefaultCode: CodeBlockComponent = (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { children, className = '', node, ...rest } = props; + return ( + + {children} + + ); +}; + +const withCodeBlock = (options: WithChartCodeOptions): CodeBlockComponent => { + // Render code block component + return function CodeBlock(props) { + const { children, className = '' } = props; + const content = String(children).trim(); + const isVisChart = className.includes('language-vis-chart'); + const { + components, + languageRenderers, + defaultRenderer: DefaultRenderer, + debug, + loadingTimeout = 5000, + style, + } = options; + + // If the code block is a VisChart, render the corresponding chart component + if (isVisChart) { + return ( + + ); + } + + // If the code block math extraRenderer languageName, the corresponding extra languageRenderers component + const languageName = className.match(/language-(.*)/)?.[1] || ''; + const extraLanguageRenderers = languageRenderers; + const ExtraRendererComponent = extraLanguageRenderers && extraLanguageRenderers[languageName]; + if (ExtraRendererComponent) { + return ; + } + + // If the code block is not a VisChart, render plain code + return DefaultRenderer ? : ; + }; +}; + +// Create a higher-order component (HOC) with chart code +export const withChartCode = (options: WithChartCodeOptions): CodeBlockComponent => { + return withCodeBlock(options); +}; + +/** + * Includes built-in chart components such as line charts, pie charts, etc. + * @param componentsArray + * @returns + */ +export const withDefaultChartCode = ( + options?: Partial, +): CodeBlockComponent => { + return withChartCode({ + ...options, + components: { + ...DEFAULT_CHART_COMPONENTS, + ...get(options, 'components', {}), + }, + }); +}; diff --git a/src/ChartCodeRender/type.ts b/src/ChartCodeRender/type.ts new file mode 100644 index 0000000..74e75bc --- /dev/null +++ b/src/ChartCodeRender/type.ts @@ -0,0 +1,59 @@ +import type { FC } from 'react'; +import type { Components, ExtraProps } from 'react-markdown'; + +export type WithChartCodeOptions = { + /** + * 要额外加载的图表组件 + */ + components: ChartComponents; + /** + * 自定义其它语言代码块渲染器 + */ + languageRenderers?: LanguageRenderers; + /** + * 默认的代码渲染器 + */ + defaultRenderer?: CodeRenderer; + /** + * 打开调试日志 + */ + debug?: boolean; + /** + * 设置loading动画的超时时间,默认为 5s + */ + loadingTimeout?: number; + /** + * 图表样式,配置容器样式 + */ + style?: React.CSSProperties; +}; +/** + * 图表渲染数据接口,后续拓展,这里只是写个示例 + */ +export interface ChartJson { + type: string; + data: any; +} + +/** + * 图表组件字典 + */ +export interface ChartComponents { + [key: string]: FC; +} + +/** + * 代码块渲染器接口 + */ +export type CodeBlockComponent = Components['code']; + +type CodeRenderer = FC< + React.ClassAttributes & React.HTMLAttributes & ExtraProps +>; + +/** + * 自定义其它语言代码块渲染器 + */ +interface LanguageRenderers { + [key: string]: CodeRenderer; +} diff --git a/src/Column/demos/common.tsx b/src/Column/demos/common.tsx new file mode 100644 index 0000000..b20ce25 --- /dev/null +++ b/src/Column/demos/common.tsx @@ -0,0 +1,26 @@ +import { Column } from '@antv/gpt-vis'; +import React from 'react'; + +const data = [ + { type: '1-3秒', value: 0.16 }, + { type: '4-10秒', value: 0.125 }, + { type: '11-30秒', value: 0.24 }, + { type: '31-60秒', value: 0.19 }, + { type: '1-3分', value: 0.22 }, + { type: '3-10分', value: 0.05 }, + { type: '10-30分', value: 0.01 }, + { type: '30+分', value: 0.015 }, +]; + +export default () => { + return ( + + ); +}; diff --git a/src/Column/demos/group.tsx b/src/Column/demos/group.tsx new file mode 100644 index 0000000..74a2cff --- /dev/null +++ b/src/Column/demos/group.tsx @@ -0,0 +1,66 @@ +import { Bubble, type BubbleProps } from '@ant-design/x'; +import { ChartType, Column, GPTVis, withChartCode } from '@antv/gpt-vis'; +import React from 'react'; + +const markdownContent = ` +以下是为你绘制的一个柱状图 + +\`\`\`vis-chart +{ + "type": "column", + "data": [ + { "category": "北京", "value": 825.6, "group": "油车" }, + { "category": "北京", "value": 60.2, "group": "新能源汽车" }, + { "category": "上海", "value": 450, "group": "油车" }, + { "category": "上海", "value": 95, "group": "新能源汽车" }, + { "category": "深圳", "value": 506, "group": "油车" }, + { "category": "深圳", "value": 76.7, "group": "新能源汽车" }, + { "category": "广州", "value": 976.6, "group": "油车" }, + { "category": "广州", "value": 97.2, "group": "新能源汽车" }, + { "category": "杭州", "value": 651.2, "group": "油车" }, + { "category": "杭州", "value": 62, "group": "新能源汽车" } + ], + "group": true, + "axisXTitle": "城市", + "axisYTitle": "售量" +} +\`\`\` +`; + +const bgStyle = { + display: 'grid', + gridGap: '20px 0', + background: '#f7f7f7', + padding: 20, + borderRadius: 8, +}; + +const CodeComponent = withChartCode({ + components: { [ChartType.Column]: Column }, +}); + +const RenderMarkdown: BubbleProps['messageRender'] = (content) => ( + {content} +); + +export default () => ( +
+ + +
+); diff --git a/src/Column/demos/markdown.tsx b/src/Column/demos/markdown.tsx new file mode 100644 index 0000000..43b497f --- /dev/null +++ b/src/Column/demos/markdown.tsx @@ -0,0 +1,55 @@ +import { Bubble, type BubbleProps } from '@ant-design/x'; +import { ChartType, Column, GPTVis, withChartCode } from '@antv/gpt-vis'; +import React from 'react'; + +const markdownContent = ` +当然了,以下是为你绘制的一个柱状图 + +\`\`\`vis-chart +{ + "type": "column", + "data": [{"category":2013,"value":59.3},{"category":2014,"value":64.4},{"category":2015,"value":68.9},{"category":2016,"value":74.4},{"category":2017,"value":82.7},{"category":2018,"value":91.9},{"category":2019,"value":99.1},{"category":2020,"value":101.6},{"category":2021,"value":114.4},{"category":2022,"value":121}], + "axisXTitle": "year", + "axisYTitle": "GDP" +} +\`\`\` +`; + +const bgStyle = { + display: 'grid', + gridGap: '20px 0', + background: '#f7f7f7', + padding: 20, + borderRadius: 8, +}; + +const CodeComponent = withChartCode({ + components: { [ChartType.Column]: Column }, + style: { width: 350 }, +}); + +const RenderMarkdown: BubbleProps['messageRender'] = (content) => ( + {content} +); + +export default () => ( +
+ + +
+); diff --git a/src/Column/demos/stack.tsx b/src/Column/demos/stack.tsx new file mode 100644 index 0000000..ec02f0c --- /dev/null +++ b/src/Column/demos/stack.tsx @@ -0,0 +1,66 @@ +import { Bubble, type BubbleProps } from '@ant-design/x'; +import { ChartType, Column, GPTVis, withChartCode } from '@antv/gpt-vis'; +import React from 'react'; + +const markdownContent = ` +以下是为你绘制的一个柱状图 + +\`\`\`vis-chart +{ + "type": "column", + "data": [ + { "category": "北京", "value": 825.6, "group": "油车" }, + { "category": "北京", "value": 60.2, "group": "新能源汽车" }, + { "category": "上海", "value": 450, "group": "油车" }, + { "category": "上海", "value": 95, "group": "新能源汽车" }, + { "category": "深圳", "value": 506, "group": "油车" }, + { "category": "深圳", "value": 76.7, "group": "新能源汽车" }, + { "category": "广州", "value": 976.6, "group": "油车" }, + { "category": "广州", "value": 97.2, "group": "新能源汽车" }, + { "category": "杭州", "value": 651.2, "group": "油车" }, + { "category": "杭州", "value": 62, "group": "新能源汽车" } + ], + "stack": true, + "axisXTitle": "城市", + "axisYTitle": "售量" +} +\`\`\` +`; + +const bgStyle = { + display: 'grid', + gridGap: '20px 0', + background: '#f7f7f7', + padding: 20, + borderRadius: 8, +}; + +const CodeComponent = withChartCode({ + components: { [ChartType.Column]: Column }, +}); + +const RenderMarkdown: BubbleProps['messageRender'] = (content) => ( + {content} +); + +export default () => ( +
+ + +
+); diff --git a/src/Column/index.md b/src/Column/index.md new file mode 100644 index 0000000..5c6cfb8 --- /dev/null +++ b/src/Column/index.md @@ -0,0 +1,53 @@ +--- +order: 2 +group: + order: 1 + title: 统计图 +demo: { cols: 2 } +--- + +# Column 柱形图 + +## 代码演示 + +单独使用 + +使用 Markdown 协议 + +分组柱形图 +堆叠柱形图 + +## Spec + +```json +{ + "type": "column", + "data": [ + { "category": "分类一", "value": 91.9 }, + { "category": "分类二", "value": 99.1 }, + { "category": "分类三", "value": 101.6 } + ] +} +``` + +## API + +### ColumnProps + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| ---------- | ---------------- | -------- | ------ | -------------------------------------------------------------------------------------------------- | +| data | ColumnDataItem[] | 是 | - | 数据 | +| group | boolean | 否 | - | 是否开启分组,开启分组柱形图需数据中含有 group 字段 | +| stack | boolean | 否 | - | 是否开启堆叠,开启堆叠柱形图需数据中含有 group 字段 | +| title | string | 否 | - | 图表的标题 | +| axisXTitle | string | 否 | - | x 轴的标题 | +| axisYTitle | string | 否 | - | y 轴的标题 | +| ... | - | - | - | 更多属性,详见 [Ant Design Charts ](https://ant-design-charts.antgroup.com/options/plots/overview) | + +### ColumnDataItem + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| -------- | ------ | -------- | ------ | ------------ | +| category | string | 是 | - | 数据分类名称 | +| value | number | 是 | - | 数据分类值 | +| group | number | 否 | - | 数据分组名称 | diff --git a/src/Column/index.tsx b/src/Column/index.tsx new file mode 100644 index 0000000..f8ea256 --- /dev/null +++ b/src/Column/index.tsx @@ -0,0 +1,46 @@ +import type { ColumnConfig } from '@ant-design/plots'; +import { Column as ADCColumn } from '@ant-design/plots'; +import { get } from 'lodash'; +import React from 'react'; +import { usePlotConfig } from '../ConfigProvider/hooks'; +import type { BasePlotProps } from '../types'; + +export type ColumnDataItem = { + category: string | number; + value: number; + [key: string]: string | number; +}; + +export type ColumnProps = BasePlotProps & Partial; + +const defaultConfig = (props: ColumnConfig): ColumnConfig => { + const { data, xField = 'category', yField = 'value' } = props; + const hasGroupField = get(data, '[0].group') !== undefined; + const axisYTitle = get(props, 'axis.y.title'); + + return { + xField, + yField, + colorField: hasGroupField ? 'group' : undefined, + tooltip: (d) => { + const tooltipName = axisYTitle || d[xField as string]; + return { + name: tooltipName, + value: d[yField as string], + }; + }, + style: { + // 圆角样式 + radiusTopLeft: 10, + radiusTopRight: 10, + }, + }; +}; + +const Column = (props: ColumnProps) => { + const config = usePlotConfig('Column', defaultConfig, props); + + return ; +}; + +export default Column; diff --git a/src/ConfigProvider/context.ts b/src/ConfigProvider/context.ts new file mode 100644 index 0000000..f44072c --- /dev/null +++ b/src/ConfigProvider/context.ts @@ -0,0 +1,5 @@ +import React from 'react'; +import { DEFAULT_GLOBAL_CONFIG } from '../constants'; +import type { GlobalConfig } from '../types'; + +export const ConfigContext = React.createContext(DEFAULT_GLOBAL_CONFIG); diff --git a/src/ConfigProvider/demos/components-config.tsx b/src/ConfigProvider/demos/components-config.tsx new file mode 100644 index 0000000..058d808 --- /dev/null +++ b/src/ConfigProvider/demos/components-config.tsx @@ -0,0 +1,37 @@ +import { ConfigProvider, GPTVis } from '@antv/gpt-vis'; +import React from 'react'; + +const content = ` + ~~~vis-chart +{ + "type": "pie", + "data": [ + { "category": "分类一", "value": 27 }, + { "category": "分类二", "value": 25 }, + { "category": "分类三", "value": 18 }, + { "category": "分类四", "value": 15 }, + { "category": "分类五", "value": 10 }, + { "category": "其他", "value": 5 } + ] +} +~~~`; + +const pieConfig = { + legend: false, + innerRadius: 0.6, + style: { + stroke: '#fff', + inset: 1, + radius: 10, + }, +}; + +export default () => ( + + {content} + +); diff --git a/src/ConfigProvider/demos/graph-components-config.tsx b/src/ConfigProvider/demos/graph-components-config.tsx new file mode 100644 index 0000000..ed6b3e3 --- /dev/null +++ b/src/ConfigProvider/demos/graph-components-config.tsx @@ -0,0 +1,64 @@ +import { ConfigProvider, GPTVis } from '@antv/gpt-vis'; +import React from 'react'; + +const content = ` + ~~~vis-chart +{ + "type": "mind-map", + "data": { + "name": "台风形成的因素", + "children": [ + { + "name": "气象条件", + "children": [ + { "name": "温暖的海水" }, + { "name": "气压分布" }, + { "name": "湿度水平" }, + { "name": "风的切变" } + ] + }, + { + "name": "地理环境", + "children": [ + { "name": "大陆架的形状与深度" }, + { "name": "海洋暖流的分布" }, + { "name": "热带地区的气候特征" }, + { "name": "岛屿的影响" } + ] + } + ] + } +} +~~~`; + +const mindmapConfig = { + type: 'linear', + direction: 'right', + behaviors: (behaviors) => [ + // console.log(behaviors) 👉 [{ key: 'zoom-canvas', type: 'zoom-canvas' }, { key: 'drag-canvas', type: 'drag-canvas' }] + // 默认启用两个交互,缩放画布和拖拽画布。此处移除缩放画布并添加拖拽元素 + ...behaviors.filter((behavior) => behavior.key !== 'zoom-canvas'), + { + key: 'drag-element', + type: 'drag-element', + }, + ], + transforms: (prev) => [ + // 默认节点支持折叠展开,此处禁用 + ...prev.filter((transform) => transform.key !== 'collapse-expand-react-node'), + { + ...prev.find((transform) => transform.key === 'collapse-expand-react-node'), + enable: false, + }, + ], +}; + +export default () => ( + + {content} + +); diff --git a/src/ConfigProvider/demos/map-config.tsx b/src/ConfigProvider/demos/map-config.tsx new file mode 100644 index 0000000..3e15729 --- /dev/null +++ b/src/ConfigProvider/demos/map-config.tsx @@ -0,0 +1,47 @@ +import { ConfigProvider, GPTVis } from '@antv/gpt-vis'; +import React from 'react'; + +const content = ` + ~~~vis-chart + { + "type": "pin-map", + "data": [ + { + "longitude": 120.210792, + "latitude": 30.246026, + "label": "杭州" + }, + { + "longitude": 121.473667, + "latitude": 31.230525, + "label": "上海" + }, + { + "longitude": 120.585294, + "latitude": 31.299758, + "label": "苏州" + }, + { + "longitude": 118.796624, + "latitude": 32.059344, + "label": "南京" + } + ] + } +~~~`; + +export default () => ( + + {content} + +); diff --git a/src/ConfigProvider/demos/plot-theme.tsx b/src/ConfigProvider/demos/plot-theme.tsx new file mode 100644 index 0000000..2b78ff8 --- /dev/null +++ b/src/ConfigProvider/demos/plot-theme.tsx @@ -0,0 +1,31 @@ +import { ConfigProvider, GPTVis } from '@antv/gpt-vis'; +import React from 'react'; + +const content = ` + ~~~vis-chart +{ + "type": "line", + "data": [ + { "time": "2015 年", "value": 1700, "group": "出生人口" }, + { "time": "2015 年", "value": 965, "group": "死亡人口" }, + { "time": "2016 年", "value": 1500, "group": "出生人口" }, + { "time": "2016 年", "value": 846, "group": "死亡人口" }, + { "time": "2017 年", "value": 1200, "group": "出生人口" }, + { "time": "2017 年", "value": 782, "group": "死亡人口" }, + { "time": "2018 年", "value": 1250, "group": "出生人口" }, + { "time": "2018 年", "value": 762, "group": "死亡人口" }, + { "time": "2019 年", "value": 1290, "group": "出生人口" }, + { "time": "2019 年", "value": 862, "group": "死亡人口" }, + { "time": "2020 年", "value": 1100, "group": "出生人口" }, + { "time": "2020 年", "value": 962, "group": "死亡人口" } + ], + "axisXTitle": "year", + "axisYTitle": "count" +} +~~~`; + +export default () => ( + + {content} + +); diff --git a/src/ConfigProvider/hooks/index.ts b/src/ConfigProvider/hooks/index.ts new file mode 100644 index 0000000..4852f57 --- /dev/null +++ b/src/ConfigProvider/hooks/index.ts @@ -0,0 +1 @@ +export * from './useConfig'; diff --git a/src/ConfigProvider/hooks/useConfig.ts b/src/ConfigProvider/hooks/useConfig.ts new file mode 100644 index 0000000..102975e --- /dev/null +++ b/src/ConfigProvider/hooks/useConfig.ts @@ -0,0 +1,93 @@ +import type { GraphOptions } from '@ant-design/graphs'; +import type { CommonConfig } from '@ant-design/plots'; +import React from 'react'; +import type { MapProps } from '../../Map'; +import type { Charts } from '../../types'; +import { mergeGraphOptions } from '../../utils/config'; +import { transform2ADCProps } from '../../utils/plot'; +import { ConfigContext } from '../context'; + +function useConfig() { + const context = React.useContext(ConfigContext); + return context; +} + +export function useComponentGlobalConfig(name: Charts) { + const globalConfig = useConfig(); + const { components = {} } = globalConfig; + const config = components?.[name]; + + return config; +} + +function usePlotGlobalConfig(name: Charts) { + const componentConfig = useComponentGlobalConfig(name); + const { plot: plotConfig } = useConfig(); + const config = { + ...plotConfig, + ...componentConfig, + }; + + return config; +} + +export function usePlotConfig( + name: Charts, + defaultConfig: Partial | ((props: Partial) => Partial), + props: Partial, +): Partial { + const transformedProps = transform2ADCProps(props); + + const _defaultConfig = + typeof defaultConfig === 'function' ? defaultConfig(transformedProps) : defaultConfig; + + const globalConfig = usePlotGlobalConfig(name); + + const config = { + ..._defaultConfig, + ...globalConfig, + ...transformedProps, + }; + + return config; +} + +function useMapGlobalConfig(name: Charts) { + const componentConfig = useComponentGlobalConfig(name); + const { map: mapConfig } = useConfig(); + const transformedProps = { + mapType: mapConfig?.style, + token: mapConfig?.token, + }; + + const config = { + ...transformedProps, + ...componentConfig, + }; + + return config; +} + +export function useMapConfig(name: Charts, props: T) { + const globalConfig = useMapGlobalConfig(name); + + const mapConfig = { + ...globalConfig, + ...props, + }; + + return mapConfig; +} + +function useGraphGlobalConfig(name: Charts) { + // TODO: global config + const componentConfig = useComponentGlobalConfig(name); + + return componentConfig || {}; +} + +export function useGraphConfig(name: Charts, defaultConfig: T, props: T) { + const globalConfig = useGraphGlobalConfig(name); + + return mergeGraphOptions(defaultConfig, globalConfig, props); +} diff --git a/src/ConfigProvider/index.md b/src/ConfigProvider/index.md new file mode 100644 index 0000000..aa43d0d --- /dev/null +++ b/src/ConfigProvider/index.md @@ -0,0 +1,60 @@ +--- +order: 3 +group: + order: 10 + title: 其他 +demo: { cols: 2 } +--- + +# ConfigProvider 全局化配置 + +## 代码演示 + +设置统计图表主题 + +设置地图 token 和样式 +设置组件的默认属性 +设置关系图组件的默认属性 + +## API + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| ---------- | ------------------------ | -------- | ------ | -------- | +| plot | `PlotGlobalConfig` | 否 | - | 图表配置 | +| map | `MapGlobalConfig` | 否 | - | 地图配置 | +| components | `ComponentsGlobalConfig` | 否 | - | 组件配置 | + +### PlotGlobalConfig + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| ----- | --------- | -------- | ------ | ---------------------------------------------------------------------------------------------------- | +| theme | `G2Theme` | 否 | - | 统计图表主题,详见 [plots theme](https://ant-design-charts.antgroup.com/options/plots/theme/academy) | + +### MapGlobalConfig + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| ----- | ------------------------------------------ | -------- | ------ | -------------- | +| style | `'normal' | 'light' | 'dark' | string` | 否 | - | 地图样式 | +| token | `string` | 是 | - | 高德地图 token | + +### ComponentsGlobalConfig + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| ----------------- | -------------------------- | -------- | ------ | -------------------------------------------------------------------------------------------------------------------------- | +| Line | `LineConfig` | 否 | - | 折线图组件默认属性,详见 [Ant Design Charts ](https://ant-design-charts.antgroup.com/options/plots/overview) | +| Column | `ColumnConfig` | 否 | - | 柱形图组件默认属性,详见 [Ant Design Charts ](https://ant-design-charts.antgroup.com/options/plots/overview) | +| Pie | `PieConfig` | 否 | - | 饼图组件默认属性,详见 [Ant Design Charts ](https://ant-design-charts.antgroup.com/options/plots/overview) | +| Area | `AreaConfig` | 否 | - | 面积图组件默认属性,详见 [Ant Design Charts ](https://ant-design-charts.antgroup.com/options/plots/overview) | +| Bar | `BarConfig` | 否 | - | 条形图组件默认属性,详见 [Ant Design Charts ](https://ant-design-charts.antgroup.com/options/plots/overview) | +| Scatter | `ScatterConfig` | 否 | - | 散点图组件默认属性,详见 [Ant Design Charts ](https://ant-design-charts.antgroup.com/options/plots/overview) | +| Radar | `RadarConfig` | 否 | - | 雷达图组件默认属性,详见 [Ant Design Charts ](https://ant-design-charts.antgroup.com/options/plots/overview) | +| Treemap | `TreemapConfig` | 否 | - | 矩阵树图组件默认属性,详见 [Ant Design Charts ](https://ant-design-charts.antgroup.com/options/plots/overview) | +| WordCloud | `WordCloudConfig` | 否 | - | 词语图图组件默认属性,详见 [Ant Design Charts ](https://ant-design-charts.antgroup.com/options/plots/overview) | +| VisText | `TextConfig` | 否 | - | 可视文本组件个性化定制,详见 [VisText](/components/text#globalconfigcomponentsvistext) | +| PinMap | `PinMapConfig` | 否 | - | 标注地图组件默认属性,详见 [PinMap](/components/pin-map/api) | +| PathMa | `PathMapConfig` | 否 | - | 路径地图组件默认属性,详见 [PathMa](/components/path-map/api) | +| HeatMap | `TextConfig` | 否 | - | 热力地图组件默认属性,详见 [HeatMap](/components/heat-map/api) | +| MindMap | `MindMapOptions` | 否 | - | 思维导图组件默认属性,详见 [Ant Design Charts](https://ant-design-charts.antgroup.com/options/graphs/mind-map) | +| FlowDiagram | `FlowGraphOptions` | 否 | - | 流程图组件默认属性,详见 [Ant Design Charts](https://ant-design-charts.antgroup.com/options/graphs/flow-graph) | +| NetworkGraph | `NetworkGraphOptions` | 否 | - | 网络图组件默认属性,详见 [Ant Design Charts](https://ant-design-charts.antgroup.com/options/graphs/network-graph) | +| OrganizationChart | `OrganizationChartOptions` | 否 | - | 组织架构图组件默认属性,详见 [Ant Design Charts](https://ant-design-charts.antgroup.com/options/graphs/organization-chart) | diff --git a/src/ConfigProvider/index.tsx b/src/ConfigProvider/index.tsx new file mode 100644 index 0000000..d62f835 --- /dev/null +++ b/src/ConfigProvider/index.tsx @@ -0,0 +1,18 @@ +import React, { useMemo } from 'react'; +import type { GlobalConfig } from '../types'; +import { mergeGlobalConfig } from '../utils/config'; +import { ConfigContext } from './context'; + +export type ConfigProviderProps = { + children?: React.ReactNode; +} & GlobalConfig; + +const ConfigProvider: React.FC = (props) => { + const { children, ...config } = props; + + const contextValue = useMemo(() => mergeGlobalConfig(config), []); + + return {children}; +}; + +export default ConfigProvider; diff --git a/src/DualAxes/demos/common.tsx b/src/DualAxes/demos/common.tsx new file mode 100644 index 0000000..46853b0 --- /dev/null +++ b/src/DualAxes/demos/common.tsx @@ -0,0 +1,62 @@ +import type { DualAxesProps } from '@antv/gpt-vis'; +import { DualAxes } from '@antv/gpt-vis'; +import React from 'react'; + +const config: DualAxesProps = { + categories: [ + '2020-08-20', + '2020-08-21', + '2020-08-22', + '2020-08-23', + '2020-08-24', + '2020-08-25', + '2020-08-26', + '2020-08-27', + '2020-08-28', + '2020-08-29', + '2020-08-30', + '2020-08-31', + '2020-09-01', + '2020-09-02', + '2020-09-03', + '2020-09-04', + '2020-09-05', + '2020-09-06', + '2020-09-07', + '2020-09-08', + '2020-09-09', + '2020-09-10', + '2020-09-11', + '2020-09-12', + '2020-09-13', + '2020-09-14', + '2020-09-15', + '2020-09-16', + '2020-09-17', + '2020-09-18', + ], + series: [ + { + type: 'column', + data: [ + 10868, 8786, 10824, 7860, 13253, 17015, 19298, 13937, 11541, 15244, 14247, 9402, 10440, + 9345, 18459, 9763, 11074, 11770, 12206, 11434, 16218, 11914, 16781, 10555, 10899, 10713, 0, + 0, 20357, 10424, + ], + axisYTitle: '消耗时间', + }, + { + type: 'line', + data: [ + 649.483, 1053.7, 679.817, 638.117, 843.3, 1092.983, 1036.317, 1031.9, 803.467, 830.733, + 709.867, 665.233, 696.367, 692.867, 936.017, 782.867, 653.8, 856.683, 777.15, 773.283, + 833.3, 793.517, 894.45, 725.55, 709.967, 787.6, 644.183, 1066.65, 932.45, 753.583, + ], + axisYTitle: '完成时间', + }, + ], +}; + +export default () => { + return ; +}; diff --git a/src/DualAxes/demos/markdown.tsx b/src/DualAxes/demos/markdown.tsx new file mode 100644 index 0000000..fc346ae --- /dev/null +++ b/src/DualAxes/demos/markdown.tsx @@ -0,0 +1,65 @@ +import { Bubble, type BubbleProps } from '@ant-design/x'; +import { ChartType, DualAxes, GPTVis, withChartCode } from '@antv/gpt-vis'; +import React from 'react'; + +const markdownContent = ` +当然了,以下是为你绘制的一个双轴图 + +\`\`\`vis-chart +{ + "type": "dual-axes", + "categories": ["2020", "2021", "2022"], + "series": [ + { + "type": "column", + "data": [500, 600, 700], + "axisYTitle": "amount" + }, + { + "type": "line", + "data": [10, 12, 15], + "axisYTitle": "rate" + } + ] +} +\`\`\` +`; + +const bgStyle = { + display: 'grid', + gridGap: '20px 0', + background: '#f7f7f7', + padding: 20, + borderRadius: 8, +}; + +const CodeComponent = withChartCode({ + components: { [ChartType.DualAxes]: DualAxes }, + style: { width: 350 }, +}); + +const RenderMarkdown: BubbleProps['messageRender'] = (content) => ( + {content} +); + +export default () => ( +
+ + +
+); diff --git a/src/DualAxes/demos/multiple.tsx b/src/DualAxes/demos/multiple.tsx new file mode 100644 index 0000000..56ad3a0 --- /dev/null +++ b/src/DualAxes/demos/multiple.tsx @@ -0,0 +1,122 @@ +import { Bubble, type BubbleProps } from '@ant-design/x'; +import { ChartType, DualAxes, GPTVis, withChartCode } from '@antv/gpt-vis'; +import React from 'react'; + +const markdownContent = ` +当然了,以下是为你绘制的一个多轴图 + +\`\`\`vis-chart +{ + "type": "dual-axes", + "categories": [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec" + ], + "series": [ + { + "type": "column", + "data": [ + 26, + 59, + 90, + 264, + 287, + 707, + 1756, + 1822, + 487, + 188, + 60, + 23 + ], + "axisYTitle": "总体增长量" + }, + { + "type": "line", + "data": [ + 2.6, + 5.9, + 9, + 26.4, + 28.7, + 70.7, + 175.6, + 182.2, + 48.7, + 18.8, + 6, + 2.3 + ], + "axisYTitle": "A增长率" + }, + { + "type": "line", + "data": [ + 2, + 2.2, + 3.3, + 4.5, + 6.3, + 10.2, + 20.3, + 23.4, + 23, + 16.5, + 12, + 6.2 + ], + "axisYTitle": "B增长率" + } + ] +} +\`\`\` +`; + +const bgStyle = { + display: 'grid', + gridGap: '20px 0', + background: '#f7f7f7', + padding: 20, + borderRadius: 8, +}; + +const CodeComponent = withChartCode({ + components: { [ChartType.DualAxes]: DualAxes }, + style: { width: 350 }, +}); + +const RenderMarkdown: BubbleProps['messageRender'] = (content) => ( + {content} +); + +export default () => ( +
+ + +
+); diff --git a/src/DualAxes/index.md b/src/DualAxes/index.md new file mode 100644 index 0000000..81a311f --- /dev/null +++ b/src/DualAxes/index.md @@ -0,0 +1,59 @@ +--- +order: 10 +group: + order: 1 + title: 统计图 +demo: { cols: 2 } +--- + +# DualAxes 双轴图 + +## 代码演示 + +单独使用 + +使用 Markdown 协议 +多轴图 + +## Spec + +```json +{ + "type": "dual-axes", + "categories": ["2018", "2019", "2020", "2021", "2022"], + "title": "2018-2022销售额与利润率", + "axisXTitle": "年份", + "series": [ + { + "type": "column", + "data": [91.9, 99.1, 101.6, 114.4, 121], + "axisYTitle": "销售额" + }, + { + "type": "line", + "data": [0.055, 0.06, 0.062, 0.07, 0.075], + "axisYTitle": "利润率" + } + ] +} +``` + +## API + +### DualAxesProps + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| ---------- | ---------------- | -------- | ------ | -------------------------------------------------------------------------------------------------- | +| categories | string[] | 是 | - | x 轴对应的数据 | +| series | SeriesDataItem[] | 是 | - | 子图的数据 | +| title | string | 否 | - | 图表的标题 | +| axisXTitle | string | 否 | - | x 轴的标题 | +| ... | - | - | - | 更多属性,详见 [Ant Design Charts ](https://ant-design-charts.antgroup.com/options/plots/overview) | + +### SeriesDataItem + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| ---------- | -------- | -------- | ------ | ---------- | +| type | string | 是 | - | 子图的类型 | +| data | number[] | 是 | - | 子图的数据 | +| axisYTitle | string | 否 | - | y 轴的标题 | diff --git a/src/DualAxes/index.tsx b/src/DualAxes/index.tsx new file mode 100644 index 0000000..8d9a45f --- /dev/null +++ b/src/DualAxes/index.tsx @@ -0,0 +1,55 @@ +import type { DualAxesConfig } from '@ant-design/plots'; +import { DualAxes as ADCDualAxes } from '@ant-design/plots'; +import React, { useMemo } from 'react'; +import { usePlotConfig } from '../ConfigProvider/hooks'; +import { transform } from './util'; + +export type DualAxesProps = Partial & { + categories: string[]; + axisXTitle?: string; + series: DualAxesSeriesItem[]; + legendTypeList: string[]; +}; + +export type DualAxesSeriesItem = { + type: string; + data: number[]; + axisYTitle?: string; +}; + +const defaultConfig = (props: DualAxesProps): DualAxesConfig => { + const { xField = 'category', axisXTitle, legendTypeList } = props; + return { + xField, + axis: { + x: { + title: axisXTitle, + }, + }, + legend: { + color: { + itemMarker: (v: string, i: number) => { + return legendTypeList[i]; + }, + }, + }, + scale: { + color: { + palette: 'category10', + }, + }, + }; +}; + +const DualAxes = (props: DualAxesProps) => { + const { categories, series, ...others } = props; + const transformData = useMemo(() => transform(series, categories), [props]); + const config = usePlotConfig('DualAxes', defaultConfig, { + ...others, + ...transformData, + }); + + return ; +}; + +export default DualAxes; diff --git a/src/DualAxes/util.ts b/src/DualAxes/util.ts new file mode 100644 index 0000000..e7646d7 --- /dev/null +++ b/src/DualAxes/util.ts @@ -0,0 +1,57 @@ +import type { DualAxesSeriesItem } from '.'; +import { ChartType } from '../types'; + +export function transform(series: DualAxesSeriesItem[], categories: string[]) { + const newChildren = series.map((item: any, index: number) => { + const { type, axisYTitle, ...others } = item; + + const defaultYField = axisYTitle || `value_${index + 1}`; + const baseConfig = { + ...others, + yField: defaultYField, + axis: { y: { title: axisYTitle } }, + // data放在最外层 + data: undefined, + }; + + if (type === ChartType.Column) { + return { ...baseConfig, type: 'interval' }; + } + + if (type === ChartType.Line) { + return { + ...baseConfig, + type, + shapeField: 'smooth', + axis: { y: { position: 'right', title: axisYTitle } }, + style: { lineWidth: 2 }, + }; + } + + return baseConfig; + }); + + const newData = categories.map((item: string, index: number) => { + const temp = { + category: item, + } as { + category: string; + [key: string]: any; + }; + series.forEach((s: DualAxesSeriesItem, i: number) => { + const defaultYField = s.axisYTitle || `value_${i + 1}`; + temp[defaultYField] = s.data[index]; + }); + return temp; + }); + + const legendTypeList = series.map((item: any) => { + return item.type === ChartType.Line ? 'smooth' : 'rect'; + }); + + return { + children: newChildren, + data: newData, + legendTypeList, + }; +} diff --git a/src/FlowDiagram/demos/common.tsx b/src/FlowDiagram/demos/common.tsx new file mode 100644 index 0000000..0c3b119 --- /dev/null +++ b/src/FlowDiagram/demos/common.tsx @@ -0,0 +1,26 @@ +import { FlowDiagram } from '@antv/gpt-vis'; +import React from 'react'; + +const data = { + nodes: [ + { name: '访问注册页面' }, + { name: '填写并提交注册表单' }, + { name: '验证用户信息' }, + { name: '创建新用户账户' }, + { name: '提示修改错误信息' }, + { name: '发送验证邮件' }, + { name: '点击验证链接' }, + { name: '注册成功,跳转到登录页面' }, + ], + edges: [ + { source: '访问注册页面', target: '填写并提交注册表单' }, + { source: '填写并提交注册表单', target: '验证用户信息' }, + { source: '验证用户信息', target: '创建新用户账户', name: '信息无误' }, + { source: '验证用户信息', target: '提示修改错误信息', name: '信息有误' }, + { source: '创建新用户账户', target: '发送验证邮件' }, + { source: '发送验证邮件', target: '点击验证链接' }, + { source: '点击验证链接', target: '注册成功,跳转到登录页面' }, + ], +}; + +export default () => ; diff --git a/src/FlowDiagram/demos/markdown.tsx b/src/FlowDiagram/demos/markdown.tsx new file mode 100644 index 0000000..54fd245 --- /dev/null +++ b/src/FlowDiagram/demos/markdown.tsx @@ -0,0 +1,69 @@ +import { Bubble, type BubbleProps } from '@ant-design/x'; +import { ChartType, FlowDiagram, GPTVis, withChartCode } from '@antv/gpt-vis'; +import React from 'react'; + +const markdownContent = ` +当然了,以下是为你绘制的一个流程图 + +\`\`\`vis-chart +{ + "type": "flow-diagram", + "data": { + "nodes": [ + { "name": "客户下单" }, + { "name": "系统生成订单" }, + { "name": "仓库拣货" }, + { "name": "仓库打包" }, + { "name": "物流配送" }, + { "name": "客户收货" } + ], + "edges": [ + { "source": "客户下单", "target": "系统生成订单" }, + { "source": "系统生成订单", "target": "仓库拣货" }, + { "source": "仓库拣货", "target": "仓库打包" }, + { "source": "仓库打包", "target": "物流配送" }, + { "source": "物流配送", "target": "客户收货" } + ] + } +} +\`\`\` +`; + +const bgStyle = { + display: 'grid', + gridGap: '20px 0', + background: '#f7f7f7', + padding: 20, + borderRadius: 8, +}; + +const CodeComponent = withChartCode({ + components: { [ChartType.FlowDiagram]: FlowDiagram }, + style: { width: 500, height: 250 }, +}); + +const RenderMarkdown: BubbleProps['messageRender'] = (content) => ( + {content} +); + +export default () => ( +
+ + +
+); diff --git a/src/FlowDiagram/index.md b/src/FlowDiagram/index.md new file mode 100644 index 0000000..816f8ea --- /dev/null +++ b/src/FlowDiagram/index.md @@ -0,0 +1,61 @@ +--- +order: 2 +group: + order: 3 + title: 关系图 +--- + +# FlowDiagram 流程图 + +流程图,用于直观地表示过程或系统的步骤和决策点。 + +## 代码演示 + +### 单独使用 + + + +### 使用 Markdown 协议 + + + +## Spec + +```json +{ + "type": "flow-diagram", + "data": { + "nodes": [{ "name": "node1" }, { "name": "node2" }], + "edges": [{ "source": "node1", "target": "node2", "name": "edge1" }] + } +} +``` + +## API + +### FlowDiagramProps + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| ---- | ----------------- | -------- | ------ | ---- | +| data | `FlowDiagramData` | 是 | - | 数据 | + +### FlowDiagramData + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| ----- | ------------------- | -------- | ------ | ---------------------------------------------- | +| nodes | `FlowDiagramNode[]` | 是 | - | 网络图中的节点数组,每个节点表示一个实体 | +| edges | `FlowDiagramEdge[]` | 是 | - | 网络图中的边数组,每条边表示两个节点之间的关系 | + +### FlowDiagramNode + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| ---- | -------- | -------- | ------ | ---------------------------------- | +| name | `string` | 是 | - | 节点的名称,必须唯一,用于标识节点 | + +### FlowDiagramEdge + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| ------ | -------- | -------- | ------ | ------------------------------------------------------- | +| source | `string` | 是 | - | 边的起始节点名称,指向 `FlowDiagramNode` 的 `name` 属性 | +| target | `string` | 是 | - | 边的目标节点名称,指向 `FlowDiagramNode` 的 `name` 属性 | +| name | `string` | 否 | - | 边的名称,用于标识边 | diff --git a/src/FlowDiagram/index.tsx b/src/FlowDiagram/index.tsx new file mode 100644 index 0000000..4d09691 --- /dev/null +++ b/src/FlowDiagram/index.tsx @@ -0,0 +1,75 @@ +import type { FlowGraphOptions, G6 } from '@ant-design/graphs'; +import { FlowGraph as ADCFlowGraph, RCNode } from '@ant-design/graphs'; +import React, { useMemo } from 'react'; +import { useGraphConfig } from '../ConfigProvider/hooks'; +import type { GraphProps } from '../types'; +import { visGraphData2GraphData } from '../utils/graph'; + +const { TextNode } = RCNode; + +export interface FlowDiagramProps extends GraphProps {} + +const defaultConfig: FlowGraphOptions = { + autoResize: true, + node: { + style: { + component: (d: G6.NodeData) => { + const isActive = d.states?.includes('active'); + return ( + + ); + }, + size: [140, 32], + }, + animation: { enter: false }, + }, + edge: { + style: { + endArrow: true, + labelBackground: true, + labelMaxLines: 2, + labelMaxWidth: '40%', + labelWordWrap: true, + }, + state: { + active: { + halo: false, + labelWordWrap: false, + stroke: '#001f98', + }, + }, + animation: { enter: false }, + }, + behaviors: (prev) => [ + ...prev, + { + type: 'hover-activate-neighbors', + onHover: (e: G6.IPointerEvent) => { + e.view.setCursor('pointer'); + }, + onHoverEnd: (e: G6.IPointerEvent) => { + e.view.setCursor('default'); + }, + }, + ], +}; + +const FlowDiagram: React.FC = (props) => { + const { data: propsData, ...restProps } = props; + + const data = useMemo(() => visGraphData2GraphData(propsData), [propsData]); + + const config = useGraphConfig('FlowDiagram', defaultConfig, restProps); + + return ; +}; + +export default FlowDiagram; diff --git a/src/GPTVis/demos/code.tsx b/src/GPTVis/demos/code.tsx new file mode 100644 index 0000000..e3de201 --- /dev/null +++ b/src/GPTVis/demos/code.tsx @@ -0,0 +1,44 @@ +import type { CodeBlockComponent } from '@antv/gpt-vis'; +import { ChartType, GPTVis, PinMap, withChartCode } from '@antv/gpt-vis'; +import React from 'react'; + +/** + * 自定义代码块渲染器 + */ +const MyUIRenderer: CodeBlockComponent = ({ children }) => { + return
{children}
; +}; + +const components = { + code: withChartCode({ + languageRenderers: { kotlin: MyUIRenderer }, + components: { [ChartType.PinMap]: PinMap }, + }), +}; + +const content = ` +\`\`\`my-ui +my data ... +\`\`\` + +\`\`\`vis-chart +{ + "type": "pin-map", + "data": [ + { "label": "杨梅岭", "longitude": 120.118362, "latitude": 30.217175 }, + { "label": "理安寺", "longitude": 120.112958, "latitude": 30.207319 }, + { "label": "九溪烟树", "longitude": 120.11335, "latitude": 30.202395 }, + { "label": "飞来峰", "longitude": 120.100549, "latitude": 30.236875 }, + { "label": "灵隐寺", "longitude": 120.101406, "latitude": 30.240826 }, + { "label": "天竺三寺", "longitude": 120.105337, "latitude": 30.236818 }, + { "label": "杭州植物园", "longitude": 120.116979, "latitude": 30.252876 }, + { "label": "杭州花圃", "longitude": 120.127654, "latitude": 30.245663 }, + { "label": "苏堤", "longitude": 120.135764, "latitude": 30.251448 }, + { "label": "虎跑公园", "longitude": 120.130095, "latitude": 30.207505 }, + { "label": "玉皇飞云", "longitude": 120.145323, "latitude": 30.214993 }, + { "label": "长桥公园", "longitude": 120.155057, "latitude": 30.232985 } + ] +} +\`\`\` +`; +export default () => {content}; diff --git a/src/GPTVis/demos/default.tsx b/src/GPTVis/demos/default.tsx new file mode 100644 index 0000000..aa340b6 --- /dev/null +++ b/src/GPTVis/demos/default.tsx @@ -0,0 +1,23 @@ +import { GPTVis } from '@antv/gpt-vis'; +import React from 'react'; + +const content = `# GPT-VIS \n\nComponents for GPTs, generative AI, and LLM projects. Not only UI Components. + +\`\`\`vis-chart +{ + "type": "pie", + "data": [ + { "category": "分类一", "value": 27 }, + { "category": "分类二", "value": 25 }, + { "category": "分类三", "value": 18 }, + { "category": "分类四", "value": 15 }, + { "category": "分类五", "value": 10 }, + { "category": "其他", "value": 5 } + ] +} +\`\`\` +`; + +export default () => { + return {content}; +}; diff --git a/src/GPTVis/demos/tag.tsx b/src/GPTVis/demos/tag.tsx new file mode 100644 index 0000000..489ff0c --- /dev/null +++ b/src/GPTVis/demos/tag.tsx @@ -0,0 +1,48 @@ +import { GPTVis, withDefaultChartCode } from '@antv/gpt-vis'; +import React from 'react'; + +const components = { + // Rewrite `a` style + a(props: any) { + const { href, children } = props; + return ( + + {children} + + ); + }, + // Rewrite `em`s (`*like so*`) to `i` with a color. + em(props: any) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { node, ...rest } = props; + return ( + + ); + }, + code: withDefaultChartCode(), +}; + +const content = ` +# Haidilao's Food Delivery Revenue (2013-2022) + +Here’s a visualization of [Haidilao](/)'s food delivery revenue from 2013 to 2022. You can see a steady increase over the years, with notable *growth* particularly in recent years. + +\`\`\`vis-chart +{ + "type": "line", + "data": [{"time":2013,"value":59.3},{"time":2014,"value":64.4},{"time":2015,"value":68.9},{"time":2016,"value":74.4},{"time":2017,"value":82.7},{"time":2018,"value":91.9},{"time":2019,"value":99.1},{"time":2020,"value":101.6},{"time":2021,"value":114.4},{"time":2022,"value":121}] +} +\`\`\` +`; +export default () => {content}; diff --git a/src/GPTVis/index.md b/src/GPTVis/index.md new file mode 100644 index 0000000..232d97a --- /dev/null +++ b/src/GPTVis/index.md @@ -0,0 +1,26 @@ +--- +order: 1 +group: + order: 10 + title: 其他 +--- + +# GPTVis 协议渲染器 + +GPTVis 协议的 Markdown 渲染器,基于 Markdown 语法扩展 `vis-chart` 语法块,支持自定义渲染组件。 + +## 基础使用 + + + +## 自定义标签渲染 + + + +## 自定义 code 渲染块 + + + +## API + +继承 [react-markdown](https://github.com/remarkjs/react-markdown#options) 组件全部属性。 diff --git a/src/GPTVis/index.tsx b/src/GPTVis/index.tsx new file mode 100644 index 0000000..040ad00 --- /dev/null +++ b/src/GPTVis/index.tsx @@ -0,0 +1,30 @@ +import React, { memo } from 'react'; +import type { Options } from 'react-markdown'; +import Markdown from 'react-markdown'; +import rehypeRaw from 'rehype-raw'; +import { withDefaultChartCode } from '../ChartCodeRender'; + +export interface GPTVisProps extends Options { + /** 自定义 markdown components样式 */ + components?: + | Options['components'] + | { + [key: string]: (props: any) => React.ReactNode; + }; +} + +const CodeBlock = withDefaultChartCode(); + +const GPTVis: React.FC = ({ children, components, rehypePlugins, ...rest }) => { + return ( + + {children} + + ); +}; + +export default memo(GPTVis); diff --git a/src/HeatMap/index.md b/src/HeatMap/index.md new file mode 100644 index 0000000..8bbcca6 --- /dev/null +++ b/src/HeatMap/index.md @@ -0,0 +1,237 @@ +--- +order: 3 +group: + order: 2 + title: 地图 +--- + +# HeatMap 热力地图 + +热力地图,直观展示数据密度与频率的空间分布。 + +## 代码演示 + +### 单独使用 + +```jsx +import React from 'react'; +import { HeatMap } from '@antv/gpt-vis'; + +const data = [ + { + longitude: 121.473117, + latitude: 31.230125, + value: 20, + }, + { + longitude: 121.473337, + latitude: 31.230325, + value: 100, + }, + { + longitude: 121.473557, + latitude: 31.230525, + value: 300, + }, + { + longitude: 121.473777, + latitude: 31.230725, + value: 600, + }, + { + longitude: 121.473997, + latitude: 31.230925, + value: 1000, + }, +]; + +export default () => ; +``` + +### 使用 Markdown 协议 + +```tsx +import { Bubble, type BubbleProps } from '@ant-design/x'; +import { HeatMap, withChartCode, ChartType, GPTVis } from '@antv/gpt-vis'; + +const bgStyle = { + display: 'grid', + gridGap: '20px 0', + background: '#f7f7f7', + padding: 20, + borderRadius: 8, +}; + +const markdownContent = ` + ~~~vis-chart + { + "type": "heat-map", + "data": [ + { + "longitude": 121.474856, + "latitude": 31.249162, + "value": 800 + }, + { + "longitude": 121.499718, + "latitude": 31.239703, + "value": 1000 + }, + { + "longitude": 121.48612, + "latitude": 31.24166, + "value": 1200 + }, + { + "longitude": 121.449895, + "latitude": 31.228609, + "value": 500 + }, + { + "longitude": 121.449486, + "latitude": 31.222042, + "value": 900 + }, + { + "longitude": 121.431826, + "latitude": 31.204638, + "value": 400 + }, + { + "longitude": 121.438573, + "latitude": 31.204188, + "value": 1000 + }, + { + "longitude": 121.448453, + "latitude": 31.222341, + "value": 300 + }, + { + "longitude": 121.474856, + "latitude": 31.249162, + "value": 800 + }, + { + "longitude": 121.473688, + "latitude": 31.249921, + "value": 1000 + }, + { + "longitude": 121.449895, + "latitude": 31.228609, + "value": 500 + }, + { + "longitude": 121.449486, + "latitude": 31.222042, + "value": 900 + }, + { + "longitude": 121.431826, + "latitude": 31.204638, + "value": 400 + }, + { + "longitude": 121.438573, + "latitude": 31.204188, + "value": 1000 + }, + { + "longitude": 121.448453, + "latitude": 31.222341, + "value": 300 + }, + { + "longitude": 121.448997, + "latitude": 31.203590, + "value": 400 + } + ] + } +~~~`; + +const CodeComponent = withChartCode({ + components: { [ChartType.HeatMap]: HeatMap }, + style: { width: 500 }, +}); + +const RenderMarkdown: BubbleProps['messageRender'] = (content) => ( + {content} +); + +export default () => { + return ( +
+ + +
+ ); +}; +``` + +## Spec + +```json +{ + "type": "heat-map", + "data": [ + { + "longitude": 121.473117, + "latitude": 31.230125, + "value": 20 + }, + { + "longitude": 121.473337, + "latitude": 31.230325, + "value": 100 + }, + { + "longitude": 121.473557, + "latitude": 31.230525, + "value": 300 + }, + { + "longitude": 121.473777, + "latitude": 31.230725, + "value": 600 + }, + { + "longitude": 121.473997, + "latitude": 31.230925, + "value": 1000 + } + ] +} +``` + +## API + +### HeatMapProps + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| ---- | ----------------- | -------- | ------ | ---- | +| data | HeatMapDataItem[] | 是 | - | 数据 | + +### HeatMapDataItem + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| --------- | ------ | -------- | ------ | ------ | +| longitude | number | 是 | - | 经度 | +| latitude | number | 是 | - | 纬度 | +| value | number | 是 | - | 热力值 | diff --git a/src/HeatMap/index.tsx b/src/HeatMap/index.tsx new file mode 100644 index 0000000..5d23c8c --- /dev/null +++ b/src/HeatMap/index.tsx @@ -0,0 +1,57 @@ +import type { HeatmapLayerProps } from '@antv/larkmap'; +import { HeatmapLayer } from '@antv/larkmap'; +import React, { useMemo, type FC } from 'react'; +import { useMapConfig } from '../ConfigProvider/hooks'; +import type { MapProps } from '../Map'; +import Map from '../Map'; + +const heapLayerOptions: Omit = { + autoFit: true, + shape: 'heatmap' as const, + size: { + field: 'value', + value: [0, 1], + }, + style: { + intensity: 3, + radius: 20, + opacity: 1, + rampColors: { + colors: ['#FF4818', '#F7B74A', '#FFF598', '#F27DEB', '#8C1EB2', '#421EB2'], + positions: [0, 0.2, 0.4, 0.6, 0.8, 1.0], + }, + }, +}; + +type GeoData = { + longitude: number; + latitude: number; + value: number; +}; + +export type HeatMapProps = MapProps & { + data?: GeoData[]; +}; + +const HeatMap: FC = (props) => { + const { children, data = [], ...mapConfigRest } = useMapConfig('HeatMap', props); + + const source = useMemo( + () => ({ + data, + parser: { type: 'json', x: 'longitude', y: 'latitude' }, + }), + [data], + ); + + return ( + <> + + + {children} + + + ); +}; + +export default HeatMap; diff --git a/src/Histogram/demos/common.tsx b/src/Histogram/demos/common.tsx new file mode 100644 index 0000000..ed8dc16 --- /dev/null +++ b/src/Histogram/demos/common.tsx @@ -0,0 +1,71 @@ +import { Histogram } from '@antv/gpt-vis'; +import React from 'react'; + +const data = [ + { value: 1.2 }, + { value: 3.4 }, + { value: 3.7 }, + { value: 4.3 }, + { value: 5.2 }, + { value: 5.8 }, + { value: 6.1 }, + { value: 6.5 }, + { value: 6.8 }, + { value: 7.1 }, + { value: 7.3 }, + { value: 7.7 }, + { value: 8.3 }, + { value: 8.6 }, + { value: 8.8 }, + { value: 9.1 }, + { value: 9.2 }, + { value: 9.4 }, + { value: 9.5 }, + { value: 9.7 }, + { value: 10.5 }, + { value: 10.7 }, + { value: 10.8 }, + { value: 11.0 }, + { value: 11.0 }, + { value: 11.1 }, + { value: 11.2 }, + { value: 11.3 }, + { value: 11.4 }, + { value: 11.4 }, + { value: 11.7 }, + { value: 12.0 }, + { value: 12.9 }, + { value: 12.9 }, + { value: 13.3 }, + { value: 13.7 }, + { value: 13.8 }, + { value: 13.9 }, + { value: 14.0 }, + { value: 14.2 }, + { value: 14.5 }, + { value: 15 }, + { value: 15.2 }, + { value: 15.6 }, + { value: 16.0 }, + { value: 16.3 }, + { value: 17.3 }, + { value: 17.5 }, + { value: 17.9 }, + { value: 18.0 }, + { value: 18.0 }, + { value: 20.6 }, + { value: 21 }, + { value: 23.4 }, +]; + +export default () => { + return ( + + ); +}; diff --git a/src/Histogram/demos/markdown.tsx b/src/Histogram/demos/markdown.tsx new file mode 100644 index 0000000..de7e0d9 --- /dev/null +++ b/src/Histogram/demos/markdown.tsx @@ -0,0 +1,343 @@ +import { Bubble, type BubbleProps } from '@ant-design/x'; +import { ChartType, GPTVis, Histogram, withChartCode } from '@antv/gpt-vis'; +import React from 'react'; + +const markdownContent = ` +当然了,以下是为你绘制的一个直方图 + +\`\`\`vis-chart +{ + "type": "histogram", + "data": [ + { + "value": 45 + }, + { + "value": 55 + }, + { + "value": 67 + }, + { + "value": 50 + }, + { + "value": 42 + }, + { + "value": 61 + }, + { + "value": 33 + }, + { + "value": 76 + }, + { + "value": 59 + }, + { + "value": 51 + }, + { + "value": 70 + }, + { + "value": 63 + }, + { + "value": 50 + }, + { + "value": 48 + }, + { + "value": 37 + }, + { + "value": 72 + }, + { + "value": 55 + }, + { + "value": 52 + }, + { + "value": 50 + }, + { + "value": 46 + }, + { + "value": 65 + }, + { + "value": 68 + }, + { + "value": 60 + }, + { + "value": 45 + }, + { + "value": 54 + }, + { + "value": 75 + }, + { + "value": 49 + }, + { + "value": 56 + }, + { + "value": 47 + }, + { + "value": 51 + }, + { + "value": 62 + }, + { + "value": 53 + }, + { + "value": 71 + }, + { + "value": 50 + }, + { + "value": 40 + }, + { + "value": 57 + }, + { + "value": 36 + }, + { + "value": 69 + }, + { + "value": 42 + }, + { + "value": 63 + }, + { + "value": 44 + }, + { + "value": 64 + }, + { + "value": 77 + }, + { + "value": 59 + }, + { + "value": 50 + }, + { + "value": 53 + }, + { + "value": 61 + }, + { + "value": 48 + }, + { + "value": 58 + }, + { + "value": 66 + }, + { + "value": 51 + }, + { + "value": 39 + }, + { + "value": 60 + }, + { + "value": 56 + }, + { + "value": 57 + }, + { + "value": 67 + }, + { + "value": 64 + }, + { + "value": 53 + }, + { + "value": 73 + }, + { + "value": 50 + }, + { + "value": 45 + }, + { + "value": 61 + }, + { + "value": 58 + }, + { + "value": 54 + }, + { + "value": 68 + }, + { + "value": 41 + }, + { + "value": 62 + }, + { + "value": 50 + }, + { + "value": 46 + }, + { + "value": 70 + }, + { + "value": 42 + }, + { + "value": 69 + }, + { + "value": 55 + }, + { + "value": 60 + }, + { + "value": 51 + }, + { + "value": 66 + }, + { + "value": 48 + }, + { + "value": 59 + }, + { + "value": 52 + }, + { + "value": 63 + }, + { + "value": 57 + }, + { + "value": 61 + }, + { + "value": 74 + }, + { + "value": 65 + }, + { + "value": 55 + }, + { + "value": 47 + }, + { + "value": 53 + }, + { + "value": 68 + }, + { + "value": 62 + }, + { + "value": 49 + }, + { + "value": 58 + }, + { + "value": 66 + }, + { + "value": 50 + }, + { + "value": 44 + }, + { + "value": 72 + }, + { + "value": 41 + } + ], + "binNumber": 10 +} +\`\`\` +`; + +const bgStyle = { + display: 'grid', + gridGap: '20px 0', + background: '#f7f7f7', + padding: 20, + borderRadius: 8, +}; + +const CodeComponent = withChartCode({ + components: { [ChartType.Histogram]: Histogram }, + style: { width: 350 }, +}); + +const RenderMarkdown: BubbleProps['messageRender'] = (content) => ( + {content} +); + +export default () => ( +
+ + +
+); diff --git a/src/Histogram/index.md b/src/Histogram/index.md new file mode 100644 index 0000000..838f19f --- /dev/null +++ b/src/Histogram/index.md @@ -0,0 +1,44 @@ +--- +order: 5 +group: + order: 1 + title: 统计图 +demo: { cols: 2 } +--- + +# Histogram 直方图 + +## 代码演示 + +单独使用 + +使用 Markdown 协议 + +## Spec + +```json +{ + "type": "histogram", + "data": [{ "value": 2 }, { "value": 5 }, { "value": 8 }, { "value": 3 }], + "binNumber": 4 +} +``` + +## API + +### HistogramProps + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| ---------- | ------------------- | -------- | ------ | -------------------------------------------------------------------------------------------------- | +| data | HistogramDataItem[] | 是 | - | 数据 | +| binNumber | number | 否 | - | 区间个数,用于定义直方图的区间数量 | +| title | string | 否 | - | 图表的标题 | +| axisXTitle | string | 否 | - | x 轴的标题 | +| axisYTitle | string | 否 | - | y 轴的标题 | +| ... | - | - | - | 更多属性,详见 [Ant Design Charts ](https://ant-design-charts.antgroup.com/options/plots/overview) | + +### HistogramDataItem + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| ----- | ------ | -------- | ------ | -------- | +| value | number | 是 | - | 数据的值 | diff --git a/src/Histogram/index.tsx b/src/Histogram/index.tsx new file mode 100644 index 0000000..59759ae --- /dev/null +++ b/src/Histogram/index.tsx @@ -0,0 +1,33 @@ +import type { HistogramConfig } from '@ant-design/plots'; +import { Histogram as ADCHistogram } from '@ant-design/plots'; +import React from 'react'; +import { usePlotConfig } from '../ConfigProvider/hooks'; +import type { BasePlotProps } from '../types'; + +type HistogramDataItem = { + value: number; + [key: string]: string | number; +}; +// binNumber和binWidth为互斥属性,选其一即可 +type ADCHistogramConfig = Omit; + +export type HistogramProps = BasePlotProps & Partial; + +const defaultConfig = (props: HistogramConfig): ADCHistogramConfig => { + const { data, binField = 'value', binNumber } = props; + + return { + data, + binField, + binNumber, + style: { inset: 0.5 }, + }; +}; + +const Histogram = (props: HistogramProps) => { + const config = usePlotConfig('Histogram', defaultConfig, props); + + return ; +}; + +export default Histogram; diff --git a/src/IndentedTree/demos/common.tsx b/src/IndentedTree/demos/common.tsx new file mode 100644 index 0000000..17c8dba --- /dev/null +++ b/src/IndentedTree/demos/common.tsx @@ -0,0 +1,63 @@ +import { IndentedTree } from '@antv/gpt-vis'; +import React from 'react'; + +const data = { + name: 'my-project', + children: [ + { + name: 'src', + children: [ + { + name: 'components', + children: [ + { + name: 'Header.tsx', + }, + { + name: 'Footer.tsx', + }, + ], + }, + { + name: 'pages', + children: [ + { + name: 'Home.tsx', + }, + { + name: 'About.tsx', + }, + ], + }, + { + name: 'App.tsx', + }, + { + name: 'index.tsx', + }, + ], + }, + { + name: 'public', + children: [ + { + name: 'index.html', + }, + { + name: 'favicon.ico', + }, + ], + }, + { + name: 'package.json', + }, + { + name: 'tsconfig.json', + }, + { + name: 'README.md', + }, + ], +}; + +export default () => ; diff --git a/src/IndentedTree/demos/markdown.tsx b/src/IndentedTree/demos/markdown.tsx new file mode 100644 index 0000000..255326f --- /dev/null +++ b/src/IndentedTree/demos/markdown.tsx @@ -0,0 +1,79 @@ +import { Bubble, type BubbleProps } from '@ant-design/x'; +import { ChartType, GPTVis, IndentedTree, withChartCode } from '@antv/gpt-vis'; +import React from 'react'; + +const markdownContent = ` +当然了,以下是为你绘制的一个缩进树 + +\`\`\`vis-chart +{ + "type": "indented-tree", + "data": { + "name": "导航菜单", + "children": [ + { "name": "首页" }, + { + "name": "产品", + "children": [ + { + "name": "产品分类1", + "children": [{ "name": "产品1-1" }, { "name": "产品1-2" }] + }, + { + "name": "产品分类2", + "children": [{ "name": "产品2-1" }, { "name": "产品2-2" }] + } + ] + }, + { + "name": "关于我们", + "children": [{ "name": "公司简介" }, { "name": "团队介绍" }] + }, + { + "name": "服务", + "children": [{ "name": "咨询服务" }, { "name": "技术支持" }] + }, + { "name": "联系我们" } + ] + } +} +\`\`\` +`; + +const bgStyle = { + display: 'grid', + gridGap: '20px 0', + background: '#f7f7f7', + padding: 20, + borderRadius: 8, +}; + +const CodeComponent = withChartCode({ + components: { [ChartType.IndentedTree]: IndentedTree }, +}); + +const RenderMarkdown: BubbleProps['messageRender'] = (content) => ( + {content} +); + +export default () => ( +
+ + +
+); diff --git a/src/IndentedTree/index.md b/src/IndentedTree/index.md new file mode 100644 index 0000000..84e6ef1 --- /dev/null +++ b/src/IndentedTree/index.md @@ -0,0 +1,47 @@ +--- +order: 5 +group: + order: 3 + title: 关系图 +demo: { cols: 2 } +--- + +# IndentedTree 缩进树 + +缩进树,用于直观地展示层级结构和父子关系。 + +## 代码演示 + +单独使用 +使用 Markdown 协议 + +## Spec + +```json +{ + "type": "indented-tree", + "data": { + "name": "node1", + "children": [ + { "name": "node 1-1", "children": [{ "name": "node 1-1-1" }] }, + { "name": "node 1-2" }, + { "name": "node 1-3" } + ] + } +} +``` + +## API + +### IndentedTreeProps + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| ---- | ------------------ | -------- | ------ | ---- | +| data | `IndentedTreeData` | 是 | - | 数据 | + +### IndentedTreeData + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| -------- | -------------------- | -------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| name | `string` | 是 | - | 节点的名称,用于显示在思维导图的节点上 | +| children | `IndentedTreeData[]` | 否 | - | 当前节点的子节点集合。如果当前节点没有子节点,该字段可以省略。每个子节点本身也是一个 `IndentedTreeData` 对象,这意味着它可以包含自己的子节点,从而递归地构建出一个多层次的树状结构 | diff --git a/src/IndentedTree/index.tsx b/src/IndentedTree/index.tsx new file mode 100644 index 0000000..5372e18 --- /dev/null +++ b/src/IndentedTree/index.tsx @@ -0,0 +1,39 @@ +import type { G6, IndentedTreeOptions } from '@ant-design/graphs'; +import { IndentedTree as ADCIndentedTree } from '@ant-design/graphs'; +import React, { useMemo } from 'react'; +import { useGraphConfig } from '../ConfigProvider/hooks'; +import type { TreeGraphProps } from '../types'; +import { visTreeData2GraphData } from '../utils/graph'; + +export interface IndentedTreeProps extends TreeGraphProps {} + +const defaultConfig: IndentedTreeOptions = { + type: 'linear', + autoFit: 'view', + autoResize: true, + node: { animation: { update: false, translate: false } }, + edge: { animation: { update: false, translate: false } }, + transforms: (prev) => [ + ...prev.filter( + (transform) => (transform as G6.BaseTransformOptions).type !== 'collapse-expand-react-node', + ), + { + ...(prev.find( + (transform) => (transform as G6.BaseTransformOptions).type === 'collapse-expand-react-node', + ) as G6.BaseTransformOptions), + enable: true, + }, + ], +}; + +const IndentedTree: React.FC = (props) => { + const { data: propsData, ...restProps } = props; + + const data = useMemo(() => visTreeData2GraphData(propsData), [propsData]); + + const config = useGraphConfig('IndentedTree', defaultConfig, restProps); + + return ; +}; + +export default IndentedTree; diff --git a/src/Line/demos/category.tsx b/src/Line/demos/category.tsx new file mode 100644 index 0000000..b6eac66 --- /dev/null +++ b/src/Line/demos/category.tsx @@ -0,0 +1,114 @@ +import { Bubble, type BubbleProps } from '@ant-design/x'; +import { ChartType, GPTVis, Line, withChartCode } from '@antv/gpt-vis'; +import React from 'react'; + +const markdownContent = ` +以下是为你绘制的一个折线图 + +\`\`\`vis-chart +{ + "type": "line", + "data": [ + { + "time": "1974", + "value": 107, + "group": "Gas flaring" + }, + { + "time": "1974", + "value": 208, + "group": "Renewables" + }, + { + "time": "1974", + "value": 356, + "group": "Fossil fuels" + }, + { + "time": "1975", + "value": 173, + "group": "Gas flaring" + }, + { + "time": "1975", + "value": 415, + "group": "Renewables" + }, + { + "time": "1975", + "value": 364, + "group": "Fossil fuels" + }, + { + "time": "1976", + "value": 117, + "group": "Gas flaring" + }, + { + "time": "1976", + "value": 220, + "group": "Renewables" + }, + { + "time": "1976", + "value": 373, + "group": "Fossil fuels" + }, + { + "time": "1977", + "value": 122, + "group": "Gas flaring" + }, + { + "time": "1977", + "value": 225, + "group": "Renewables" + }, + { + "time": "1977", + "value": 382, + "group": "Fossil fuels" + } + ] +} +\`\`\` +`; + +const bgStyle = { + display: 'grid', + gridGap: '20px 0', + background: '#f7f7f7', + padding: 20, + borderRadius: 8, +}; + +const CodeComponent = withChartCode({ + components: { [ChartType.Line]: Line }, + style: { width: 350 }, +}); + +const RenderMarkdown: BubbleProps['messageRender'] = (content) => ( + {content} +); + +export default () => ( +
+ + +
+); diff --git a/src/Line/demos/common.tsx b/src/Line/demos/common.tsx new file mode 100644 index 0000000..17599cf --- /dev/null +++ b/src/Line/demos/common.tsx @@ -0,0 +1,20 @@ +import { Line } from '@antv/gpt-vis'; +import React from 'react'; + +const data = [ + { time: '1991', value: 3 }, + { time: '1992', value: 4 }, + { time: '1993', value: 3.5 }, + { time: '1994', value: 5 }, + { time: '1995', value: 4.9 }, + { time: '1996', value: 6 }, + { time: '1997', value: 7 }, + { time: '1998', value: 9 }, + { time: '1999', value: 13 }, +]; + +export default () => { + return ( + + ); +}; diff --git a/src/Line/demos/markdown.tsx b/src/Line/demos/markdown.tsx new file mode 100644 index 0000000..e1a1a1d --- /dev/null +++ b/src/Line/demos/markdown.tsx @@ -0,0 +1,55 @@ +import { Bubble, type BubbleProps } from '@ant-design/x'; +import { ChartType, GPTVis, Line, withChartCode } from '@antv/gpt-vis'; +import React from 'react'; + +const markdownContent = ` +当然了,以下是为你绘制的一个折线图 + +\`\`\`vis-chart +{ + "type": "line", + "data": [{"time":2013,"value":59.3},{"time":2014,"value":64.4},{"time":2015,"value":68.9},{"time":2016,"value":74.4},{"time":2017,"value":82.7},{"time":2018,"value":91.9},{"time":2019,"value":99.1},{"time":2020,"value":101.6},{"time":2021,"value":114.4},{"time":2022,"value":121}], + "axisXTitle": "year", + "axisYTitle": "GDP" +} +\`\`\` +`; + +const bgStyle = { + display: 'grid', + gridGap: '20px 0', + background: '#f7f7f7', + padding: 20, + borderRadius: 8, +}; + +const CodeComponent = withChartCode({ + components: { [ChartType.Line]: Line }, + style: { width: 350 }, +}); + +const RenderMarkdown: BubbleProps['messageRender'] = (content) => ( + {content} +); + +export default () => ( +
+ + +
+); diff --git a/src/Line/index.md b/src/Line/index.md new file mode 100644 index 0000000..99f9929 --- /dev/null +++ b/src/Line/index.md @@ -0,0 +1,51 @@ +--- +order: 1 +group: + order: 1 + title: 统计图 +demo: { cols: 2 } +--- + +# Line 折线图 + +## 代码演示 + +单独使用 + +使用 Markdown 协议 +多折线图 + +## Spec + +```json +{ + "type": "line", + "data": [ + { "time": 2018, "value": 91.9 }, + { "time": 2019, "value": 99.1 }, + { "time": 2020, "value": 101.6 }, + { "time": 2021, "value": 114.4 }, + { "time": 2022, "value": 121 } + ] +} +``` + +## API + +### LineProps + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| ---------- | -------------- | -------- | ------ | -------------------------------------------------------------------------------------------------- | +| data | LineDataItem[] | 是 | - | 数据 | +| title | string | 否 | - | 图表的标题 | +| axisXTitle | string | 否 | - | x 轴的标题 | +| axisYTitle | string | 否 | - | y 轴的标题 | +| ... | - | - | - | 更多属性,详见 [Ant Design Charts ](https://ant-design-charts.antgroup.com/options/plots/overview) | + +### LineDataItem + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| ----- | ------ | -------- | ------ | -------------- | +| time | string | 是 | - | 数据的时序名称 | +| value | number | 是 | - | 数据的值 | +| group | string | 否 | - | 数据分组名称 | diff --git a/src/Line/index.tsx b/src/Line/index.tsx new file mode 100644 index 0000000..46b6535 --- /dev/null +++ b/src/Line/index.tsx @@ -0,0 +1,41 @@ +import type { LineConfig } from '@ant-design/plots'; +import { Line as ADCLine } from '@ant-design/plots'; +import { get } from 'lodash'; +import React from 'react'; +import { usePlotConfig } from '../ConfigProvider/hooks'; +import type { BasePlotProps } from '../types'; + +export type LineDataItem = { + time: string | number; + value: number; + [key: string]: string | number; +}; + +export type LineProps = BasePlotProps & Partial; + +const defaultConfig = (props: LineConfig): LineConfig => { + const { data, xField = 'time', yField = 'value' } = props; + const hasGroupField = get(data, '[0].group') !== undefined; + const axisYTitle = get(props, 'axis.y.title'); + + return { + xField, + yField, + colorField: hasGroupField ? 'group' : undefined, + tooltip: (d) => { + const tooltipName = axisYTitle || d[xField as string]; + return { + name: tooltipName, + value: d[yField as string], + }; + }, + }; +}; + +const Line = (props: LineProps) => { + const config = usePlotConfig('Line', defaultConfig, props); + + return ; +}; + +export default Line; diff --git a/src/Map/data.json b/src/Map/data.json new file mode 100644 index 0000000..dff60af --- /dev/null +++ b/src/Map/data.json @@ -0,0 +1,305 @@ +{ + "days": "4天", + "title": "杭州西湖赏桂之旅", + "city": "杭州", + "summary": "杭州的秋天,桂花盛开,香气四溢。杭小微为您推荐四条赏桂路线,让您在湖山之间,近距离感受杭州的浪漫。", + "locations": [ + "石屋洞", + "满觉陇", + "杨梅岭", + "理安寺", + "九溪烟树", + "飞来峰", + "灵隐寺", + "天竺三寺", + "杭州植物园", + "杭州花圃", + "苏堤", + "孤山", + "虎跑公园", + "玉皇飞云", + "长桥公园" + ], + "thumbnail": "http://store.is.autonavi.com/showpic/a8638ebc8d3b4a388974b0c9cc6051f3", + "details": [ + { + "day": "第1天", + "id": 0, + "title": "第1天", + "detail": [ + { + "name": "石屋洞", + "description": "一座遍植桂花的江南庭院,赏景品茗,美到无法言说。", + "id": "B0G3RHAU1A", + "location": [120.130638, 30.219835], + "cityname": "杭州市", + "address": "一座遍植桂花的江南庭院,赏景品茗,美到无法言说。", + "type": "风景名胜;风景名胜相关;旅游景点", + "photos": [ + "http://store.is.autonavi.com/showpic/a8638ebc8d3b4a388974b0c9cc6051f3", + "https://aos-comment.amap.com/B0G3RHAU1A/comment/4081b98294905eb6c4f03fb3a15bde46_2048_2048_80.jpg", + "https://aos-comment.amap.com/B0G3RHAU1A/comment/245E4E3C_DB29_41A8_84E2_88A6C319359F_L0_001_2048_1536__1668077914572_a8508018.jpg" + ], + "openTime": "全天开放", + "rating": "4.7", + "cost": [], + "title": "石屋洞" + }, + { + "name": "满觉陇", + "description": "赏桂“顶流”目的地,各类珍稀品种的桂花组成了“桂花瀑布”,走进其中就能发现新西湖十景“满陇桂雨”这个名字的妙处。", + "id": "BV11343845", + "location": [120.128125, 30.219386], + "cityname": "杭州市", + "address": "赏桂“顶流”目的地,各类珍稀品种的桂花组成了“桂花瀑布”,走进其中就能发现新西湖十景“满陇桂雨”这个名字的妙处。", + "type": "交通设施服务;公交车站;公交车站相关", + "photos": [ + "https://aos-comment.amap.com/BV11343845/comment/3d926a998413f8dd339eb27f9f6d6ae2_2048_2048_80.jpg", + "https://aos-comment.amap.com/BV11343845/comment/1945875b7d33194f96a6328fef5cb7ce_2048_2048_80.jpg" + ], + "openTime": "全天开放", + "rating": [], + "cost": [], + "title": "满觉陇" + }, + { + "name": "杨梅岭", + "description": "精致的打卡点,散落着多个博物馆、村落,有象征“事事如意”的网红柿子树、可以俯瞰西湖的秋千,更有经典的烟霞三洞,带来第一缕桂香的少儿公园。", + "id": "B023B0ACLZ", + "location": [120.118362, 30.217175], + "cityname": "杭州市", + "address": "精致的打卡点,散落着多个博物馆、村落,有象征“事事如意”的网红柿子树、可以俯瞰西湖的秋千,更有经典的烟霞三洞,带来第一缕桂香的少儿公园。", + "type": "风景名胜;风景名胜;国家级景点", + "photos": [ + "https://aos-comment.amap.com/B023B0ACLZ/comment/0e75682f46fb455890c3ce646897201c_2048_2048_80.jpg", + "https://aos-comment.amap.com/B023B0ACLZ/comment/fee22ab4ae625b9faa7019f7fedbd872_2048_2048_80.jpg", + "https://aos-comment.amap.com/B023B0ACLZ/headerImg/6050a39568530c5fb095d8025248d3f3_2048_2048_80.jpg" + ], + "openTime": "全天开放", + "rating": "4.8", + "cost": [], + "title": "杨梅岭" + }, + { + "name": "理安寺", + "description": "", + "id": "B0FFFDE5LV", + "location": [120.112958, 30.207319], + "cityname": "杭州市", + "address": "", + "type": "风景名胜;风景名胜;寺庙道观", + "photos": [ + "http://store.is.autonavi.com/showpic/b5a0fc92c07383efeab973714e5d66ce", + "https://store.is.autonavi.com/showpic/0BC01430C60248FDB8C3E56C64926804", + "http://store.is.autonavi.com/showpic/30e9cda185aef570d8d7c5749e229785" + ], + "openTime": "全天开放", + "rating": "4.7", + "cost": [], + "title": "理安寺" + }, + { + "name": "九溪烟树", + "description": "秋天的九溪也堪称杭城徒步“天花板”,行走在潺潺作响的溪流之间,感受微风卷起一路的隐隐花香,身心都得到了治愈。", + "id": "B0FFHMH4J8", + "location": [120.11335, 30.202395], + "cityname": "杭州市", + "address": "秋天的九溪也堪称杭城徒步“天花板”,行走在潺潺作响的溪流之间,感受微风卷起一路的隐隐花香,身心都得到了治愈。", + "type": "风景名胜;风景名胜;风景名胜", + "photos": [ + "http://store.is.autonavi.com/showpic/a79a5a8c6a341147c3c47d6a9d53db84", + "http://store.is.autonavi.com/showpic/66058afc1e0a15e1dd96057a384fe73c", + "http://store.is.autonavi.com/showpic/b0cf38d0d35223936a0c7df11c9be2c5" + ], + "openTime": "全天开放", + "rating": "4.9", + "cost": [], + "title": "九溪烟树" + } + ] + }, + { + "day": "第2天", + "id": 1, + "title": "第2天", + "detail": [ + { + "name": "飞来峰", + "description": "古代名刹,从飞来峰进入,便可以见到这一路风光。", + "id": "B023B09DPT", + "location": [120.100549, 30.236875], + "cityname": "杭州市", + "address": "古代名刹,从飞来峰进入,便可以见到这一路风光。", + "type": "地名地址信息;自然地名;山", + "photos": [ + "https://aos-comment.amap.com/B023B09DPT/comment/f3a976ca78db5c7214084ca101398a17_2048_2048_80.jpg", + "https://aos-comment.amap.com/B023B09DPT/comment/ef271bff0902bceed60aa6471fc6da36_2048_2048_80.jpg", + "https://aos-comment.amap.com/B023B09DPT/comment/a61e430e8c7b5b3e11a866aa4af55c68_2048_2048_80.jpg" + ], + "openTime": "全天开放", + "rating": [], + "cost": [], + "title": "飞来峰" + }, + { + "name": "灵隐寺", + "description": "", + "id": "B023B02842", + "location": [120.101406, 30.240826], + "cityname": "杭州市", + "address": "", + "type": "风景名胜;风景名胜;国家级景点", + "photos": [ + "http://store.is.autonavi.com/showpic/4aa0a6a1b6ee72c9833441f363cbb43a", + "http://store.is.autonavi.com/showpic/c7cccce2249dbb7aa013ee0b06888e9d", + "http://store.is.autonavi.com/showpic/69e2dbe5322886d53ed57057e9ab5514" + ], + "openTime": "全天开放", + "rating": "5.0", + "cost": "75.00", + "title": "灵隐寺" + }, + { + "name": "天竺三寺", + "description": "通称上天竺法喜讲寺、中天竺法净禅寺、三天竺法镜讲寺,每处点位自成风景。", + "id": "B023B1D0D7", + "location": [120.105337, 30.236818], + "cityname": "杭州市", + "address": "通称上天竺法喜讲寺、中天竺法净禅寺、三天竺法镜讲寺,每处点位自成风景。", + "type": "风景名胜;风景名胜;寺庙道观", + "photos": [ + "http://store.is.autonavi.com/showpic/939552e3d75b06078ac793d45d07e9be", + "http://store.is.autonavi.com/showpic/267df72456caeb1ebb24221a3583384e", + "http://store.is.autonavi.com/showpic/8ccaa44716883c2a563ba57180af6922" + ], + "openTime": "全天开放", + "rating": "4.3", + "cost": [], + "title": "天竺三寺" + } + ] + }, + { + "day": "第3天", + "id": 2, + "title": "第3天", + "detail": [ + { + "name": "杭州植物园", + "description": "一千余株桂花,品种多、树龄长、规模大,还有一株丹桂王,让植物爱好者流连忘返。", + "id": "B023B09LTB", + "location": [120.116979, 30.252876], + "cityname": "杭州市", + "address": "一千余株桂花,品种多、树龄长、规模大,还有一株丹桂王,让植物爱好者流连忘返。", + "type": "风景名胜;公园广场;植物园", + "photos": [ + "http://store.is.autonavi.com/showpic/cd40a50a625f3772ba2bbc115422ae5a", + "http://store.is.autonavi.com/showpic/e29859ae9a1be77e84eb86d56f7ff4d2", + "http://store.is.autonavi.com/showpic/f207a7316fd5da51239a570d8f484158" + ], + "openTime": "全天开放", + "rating": "4.9", + "cost": "10.00", + "title": "杭州植物园" + }, + { + "name": "杭州花圃", + "description": "一进正门就能闻见桂花的香气,金桂夹杂在大片的银桂之间,花圃内也有专门的赏桂景点“金秋桂满”,桂花的数量和质量都非常可观。", + "id": "B023B09ORY", + "location": [120.127654, 30.245663], + "cityname": "杭州市", + "address": "一进正门就能闻见桂花的香气,金桂夹杂在大片的银桂之间,花圃内也有专门的赏桂景点“金秋桂满”,桂花的数量和质量都非常可观。", + "type": "风景名胜;风景名胜;风景名胜", + "photos": [ + "http://store.is.autonavi.com/showpic/8df57a06a54b587dbaee20a63b03eb4c", + "http://store.is.autonavi.com/showpic/51f27d9e13ddb1edae4c2b7568d3732c", + "http://store.is.autonavi.com/showpic/787bab72a3971313f75384bf62f29e9c" + ], + "openTime": "全天开放", + "rating": "4.8", + "cost": [], + "title": "杭州花圃" + }, + { + "name": "苏堤", + "description": "欣赏西湖美景的同时,沿途也有桂花可寻。", + "id": "B0FFGQ6OHF", + "location": [120.135764, 30.251448], + "cityname": "杭州市", + "address": "欣赏西湖美景的同时,沿途也有桂花可寻。", + "type": "地名地址信息;交通地名;道路名", + "photos": [ + "http://store.is.autonavi.com/showpic/b211a305fb6e6bee83f281954db59c54", + "http://store.is.autonavi.com/showpic/e21048b71a5aacdd5fd9c64ecfd1cedd", + "http://store.is.autonavi.com/showpic/287332c8a30aa8eeeabfc2ce7ff45b6e" + ], + "openTime": "全天开放", + "rating": [], + "cost": [], + "title": "苏堤" + } + ] + }, + { + "day": "第4天", + "id": 3, + "title": "第4天", + "detail": [ + { + "name": "虎跑公园", + "description": "曲径深幽的江南园林中,有着馥郁芬芳的百年老桂树,一走近就香气扑鼻。", + "id": "B023B08Q06", + "location": [120.130095, 30.207505], + "cityname": "杭州市", + "address": "曲径深幽的江南园林中,有着馥郁芬芳的百年老桂树,一走近就香气扑鼻。", + "type": "风景名胜;公园广场;公园", + "photos": [ + "http://store.is.autonavi.com/showpic/806ddeb10e5d81026f9a380b153686f0", + "http://store.is.autonavi.com/showpic/64221359470ab89bf1994cb7f0c32df6", + "http://store.is.autonavi.com/showpic/7137b9d90ed98fe2cf77cd3b2cbd43d2" + ], + "openTime": "全天开放", + "rating": "4.8", + "cost": "15.00", + "title": "虎跑公园" + }, + { + "name": "玉皇飞云", + "description": "上山路上每隔几米就能看见桂花树,秋高气爽的天气里爬到半山腰,即可一览八卦田风光。", + "id": "B0FFFDSY6F", + "location": [120.145323, 30.214993], + "cityname": "杭州市", + "address": "上山路上每隔几米就能看见桂花树,秋高气爽的天气里爬到半山腰,即可一览八卦田风光。", + "type": "风景名胜;风景名胜;风景名胜", + "photos": [ + "http://store.is.autonavi.com/showpic/0209116fc1ba3159757fb0dc17deac21", + "http://store.is.autonavi.com/showpic/bce42b9835f919dbcec73dc0aba4d001", + "http://store.is.autonavi.com/showpic/30e26d0cd77cae6a7b3016142880d5d5" + ], + "openTime": "全天开放", + "rating": "4.2", + "cost": [], + "title": "玉皇飞云" + }, + { + "name": "长桥公园", + "description": "有历史记忆也有风光美景,有口皆碑的绝美落日观赏地,站在桂花树下看波光粼粼的湖面上,一轮红日逐渐下沉,一路上的疲惫也一定会消去几分。", + "id": "B023B01EAA", + "location": [120.155057, 30.232985], + "cityname": "杭州市", + "address": "有历史记忆也有风光美景,有口皆碑的绝美落日观赏地,站在桂花树下看波光粼粼的湖面上,一轮红日逐渐下沉,一路上的疲惫也一定会消去几分。", + "type": "风景名胜;公园广场;公园", + "photos": [ + "http://store.is.autonavi.com/showpic/74b51d5141a58ac662b09957c7db6713", + "http://store.is.autonavi.com/showpic/d6349365855846ad6c37600ddf1f4083", + "http://store.is.autonavi.com/showpic/40b3d10b36daae77717c820479bd8d55" + ], + "openTime": "全天开放", + "rating": "4.8", + "cost": [], + "title": "长桥公园" + } + ] + } + ] +} diff --git a/src/Map/index.md b/src/Map/index.md new file mode 100644 index 0000000..2e99e71 --- /dev/null +++ b/src/Map/index.md @@ -0,0 +1,27 @@ +--- +order: 1 +group: + order: 2 + title: 地图 +debug: true +--- + +### 代码示例 + +```jsx +import React, { useEffect, useMemo } from 'react'; +import { Map } from '@antv/gpt-vis'; + +export default () => { + const mapConfig = { + mapType: 'light', + scale: 17, + longitude: 120.130638, + latitude: 30.219835, + skew: 0, + rotate: 0, + }; + + return ; +}; +``` diff --git a/src/Map/index.tsx b/src/Map/index.tsx new file mode 100644 index 0000000..2bf0821 --- /dev/null +++ b/src/Map/index.tsx @@ -0,0 +1,49 @@ +import type { ILayer, Scene } from '@antv/l7'; +import { LarkMap } from '@antv/larkmap'; +import React, { type FC } from 'react'; +import type { BaseMapProps } from '../types'; +import { formatMapStyle, setMapContext, setMapView, setMarkers, setPolyline } from '../utils/map'; + +export type MapProps = Omit, 'data'>; + +const Map: FC = (props) => { + const { className, containerStyle, children } = props; + const allLayers: ILayer[] = []; + const mapConfig = formatMapStyle(props); + + const onSceneLoaded = async (scene: Scene) => { + // 初始地图视野 + setMapView(props, scene); + // 初始化地图资源和状态 + await setMapContext(props, scene); + + // 添加线图层 + if (props.polyline) { + const polylineLayer = setPolyline(props.polyline || []); + allLayers.push(...polylineLayer); + } + + // 添加标记 + if (props.markers) { + const markerLayer = setMarkers(props.markers || []); + allLayers.push(...markerLayer); + } + + allLayers.forEach((item) => { + scene.addLayer(item); + }); + }; + + return ( + + {children} + + ); +}; + +export default Map; diff --git a/src/MindMap/demos/common.tsx b/src/MindMap/demos/common.tsx new file mode 100644 index 0000000..57db742 --- /dev/null +++ b/src/MindMap/demos/common.tsx @@ -0,0 +1,26 @@ +import { MindMap } from '@antv/gpt-vis'; +import React from 'react'; + +const data = { + name: '项目计划', + children: [ + { + name: '研究阶段', + children: [{ name: '市场调研' }, { name: '技术可行性分析' }], + }, + { + name: '设计阶段', + children: [{ name: '产品功能确定' }, { name: 'UI 设计' }], + }, + { + name: '开发阶段', + children: [{ name: '编写代码' }, { name: '单元测试' }], + }, + { + name: '测试阶段', + children: [{ name: '功能测试' }, { name: '性能测试' }], + }, + ], +}; + +export default () => ; diff --git a/src/MindMap/demos/markdown.tsx b/src/MindMap/demos/markdown.tsx new file mode 100644 index 0000000..c0cb9b5 --- /dev/null +++ b/src/MindMap/demos/markdown.tsx @@ -0,0 +1,75 @@ +import { Bubble, type BubbleProps } from '@ant-design/x'; +import { ChartType, GPTVis, MindMap, withChartCode } from '@antv/gpt-vis'; +import React from 'react'; + +const markdownContent = ` +当然了,以下是为你绘制的一个思维导图 + +\`\`\`vis-chart +{ + "type": "mind-map", + "data": { + "name": "台风形成的因素", + "children": [ + { + "name": "气象条件", + "children": [ + { "name": "温暖的海水" }, + { "name": "气压分布" }, + { "name": "湿度水平" }, + { "name": "风的切变" } + ] + }, + { + "name": "地理环境", + "children": [ + { "name": "大陆架的形状与深度" }, + { "name": "海洋暖流的分布" }, + { "name": "热带地区的气候特征" }, + { "name": "岛屿的影响" } + ] + } + ] + } +} +\`\`\` +`; + +const bgStyle = { + display: 'grid', + gridGap: '20px 0', + background: '#f7f7f7', + padding: 20, + borderRadius: 8, +}; + +const CodeComponent = withChartCode({ + components: { [ChartType.MindMap]: MindMap }, + style: { width: 500 }, +}); + +const RenderMarkdown: BubbleProps['messageRender'] = (content) => ( + {content} +); + +export default () => ( +
+ + +
+); diff --git a/src/MindMap/index.md b/src/MindMap/index.md new file mode 100644 index 0000000..6707ed0 --- /dev/null +++ b/src/MindMap/index.md @@ -0,0 +1,51 @@ +--- +order: 1 +group: + order: 3 + title: 关系图 +--- + +# MindMap 思维导图 + +思维导图,直观地展示信息的层次结构和关联关系。 + +## 代码演示 + +### 单独使用 + + + +### 使用 Markdown 协议 + + + +## Spec + +```json +{ + "type": "mind-map", + "data": { + "name": "main topic", + "children": [ + { "name": "topic 1", "children": [{ "name": "sub topic 1-1" }] }, + { "name": "topic 2" }, + { "name": "topic 3" } + ] + } +} +``` + +## API + +### MindMapProps + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| ---- | ------------- | -------- | ------ | ---- | +| data | `MindMapData` | 是 | - | 数据 | + +### MindMapData + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| -------- | --------------- | -------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| name | `string` | 是 | - | 节点的名称,用于显示在思维导图的节点上 | +| children | `MindMapData[]` | 否 | - | 当前节点的子节点集合。如果当前节点没有子节点,该字段可以省略。每个子节点本身也是一个 `MindMapData` 对象,这意味着它可以包含自己的子节点,从而递归地构建出一个多层次的树状结构 | diff --git a/src/MindMap/index.tsx b/src/MindMap/index.tsx new file mode 100644 index 0000000..9d6b759 --- /dev/null +++ b/src/MindMap/index.tsx @@ -0,0 +1,41 @@ +import type { G6, MindMapOptions } from '@ant-design/graphs'; +import { MindMap as ADCMindMap } from '@ant-design/graphs'; +import type { FC } from 'react'; +import React, { useMemo } from 'react'; +import { useGraphConfig } from '../ConfigProvider/hooks'; +import type { TreeGraphProps } from '../types'; +import { visTreeData2GraphData } from '../utils/graph'; + +const defaultConfig: MindMapOptions = { + type: 'boxed', + autoFit: 'view', + autoResize: true, + padding: 2, + node: { animation: { translate: false, update: false } }, + edge: { animation: { translate: false, update: false } }, + transforms: (prev) => [ + ...prev.filter( + (transform) => (transform as G6.BaseTransformOptions).type !== 'collapse-expand-react-node', + ), + { + ...(prev.find( + (transform) => (transform as G6.BaseTransformOptions).type === 'collapse-expand-react-node', + ) as G6.BaseTransformOptions), + enable: true, + }, + ], +}; + +export interface MindMapProps extends TreeGraphProps {} + +const MindMap: FC = (props) => { + const { data: propsData, ...restProps } = props; + + const data = useMemo(() => visTreeData2GraphData(propsData), [propsData]); + + const config = useGraphConfig('MindMap', defaultConfig, restProps); + + return ; +}; + +export default MindMap; diff --git a/src/NetworkGraph/demos/common.tsx b/src/NetworkGraph/demos/common.tsx new file mode 100644 index 0000000..5032f73 --- /dev/null +++ b/src/NetworkGraph/demos/common.tsx @@ -0,0 +1,19 @@ +import { NetworkGraph } from '@antv/gpt-vis'; +import React from 'react'; + +const data = { + nodes: [ + { name: '哈利·波特' }, + { name: '赫敏·格兰杰' }, + { name: '罗恩·韦斯莱' }, + { name: '伏地魔' }, + ], + edges: [ + { source: '哈利·波特', target: '赫敏·格兰杰', name: '朋友' }, + { source: '哈利·波特', target: '罗恩·韦斯莱', name: '朋友' }, + { source: '哈利·波特', target: '伏地魔', name: '敌人' }, + { source: '伏地魔', target: '哈利·波特', name: '试图杀死' }, + ], +}; + +export default () => ; diff --git a/src/NetworkGraph/demos/markdown.tsx b/src/NetworkGraph/demos/markdown.tsx new file mode 100644 index 0000000..e599b7b --- /dev/null +++ b/src/NetworkGraph/demos/markdown.tsx @@ -0,0 +1,66 @@ +import { Bubble, type BubbleProps } from '@ant-design/x'; +import { ChartType, GPTVis, NetworkGraph, withChartCode } from '@antv/gpt-vis'; +import React from 'react'; + +const markdownContent = ` +当然了,以下是为你绘制的一个网络图 + +\`\`\`vis-chart +{ + "type": "network-graph", + "data": { + "nodes": [ + { "name": "哈利·波特" }, + { "name": "赫敏·格兰杰" }, + { "name": "罗恩·韦斯莱" }, + { "name": "伏地魔" } + ], + "edges": [ + { "source": "哈利·波特", "target": "赫敏·格兰杰", "name": "朋友" }, + { "source": "哈利·波特", "target": "罗恩·韦斯莱", "name": "朋友" }, + { "source": "哈利·波特", "target": "伏地魔", "name": "敌人" }, + { "source": "伏地魔", "target": "哈利·波特", "name": "试图杀死" } + ] + } +} +\`\`\` +`; + +const bgStyle = { + display: 'grid', + gridGap: '20px 0', + background: '#f7f7f7', + padding: 20, + borderRadius: 8, +}; + +const CodeComponent = withChartCode({ + components: { [ChartType.NetworkGraph]: NetworkGraph }, + style: { width: 400 }, +}); + +const RenderMarkdown: BubbleProps['messageRender'] = (content) => ( + {content} +); + +export default () => ( +
+ + +
+); diff --git a/src/NetworkGraph/index.md b/src/NetworkGraph/index.md new file mode 100644 index 0000000..df22035 --- /dev/null +++ b/src/NetworkGraph/index.md @@ -0,0 +1,61 @@ +--- +order: 1 +group: + order: 3 + title: 关系图 +--- + +# NetworkGraph 网络图 + +网络图,又名力导向图,用于展示节点(实体)之间的关系(边),直观地表示复杂的网络结构。 + +## 代码演示 + +### 单独使用 + + + +### 使用 Markdown 协议 + + + +## Spec + +```json +{ + "type": "network-graph", + "data": { + "nodes": [{ "name": "node1" }, { "name": "node2" }], + "edges": [{ "source": "node1", "target": "node2", "name": "edge1" }] + } +} +``` + +## API + +### NetworkGraphProps + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| ---- | ------------------ | -------- | ------ | ---- | +| data | `NetworkGraphData` | 是 | - | 数据 | + +### NetworkGraphData + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| ----- | -------------------- | -------- | ------ | ---------------------------------------------- | +| nodes | `NetworkGraphNode[]` | 是 | - | 网络图中的节点数组,每个节点表示一个实体 | +| edges | `NetworkGraphEdge[]` | 是 | - | 网络图中的边数组,每条边表示两个节点之间的关系 | + +### NetworkGraphNode + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| ---- | -------- | -------- | ------ | ---------------------------------- | +| name | `string` | 是 | - | 节点的名称,必须唯一,用于标识节点 | + +### NetworkGraphEdge + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| ------ | -------- | -------- | ------ | -------------------------------------------------------- | +| source | `string` | 是 | - | 边的起始节点名称,指向 `NetworkGraphNode` 的 `name` 属性 | +| target | `string` | 是 | - | 边的目标节点名称,指向 `NetworkGraphNode` 的 `name` 属性 | +| name | `string` | 是 | - | 边的名称,用于标识边 | diff --git a/src/NetworkGraph/index.tsx b/src/NetworkGraph/index.tsx new file mode 100644 index 0000000..0b043d2 --- /dev/null +++ b/src/NetworkGraph/index.tsx @@ -0,0 +1,48 @@ +import type { NetworkGraphOptions } from '@ant-design/graphs'; +import { NetworkGraph as ADCNetworkGraph } from '@ant-design/graphs'; +import React, { useMemo } from 'react'; +import { useGraphConfig } from '../ConfigProvider/hooks'; +import type { GraphProps } from '../types'; +import { visGraphData2GraphData } from '../utils/graph'; + +export interface NetworkGraphProps extends GraphProps {} + +const defaultConfig: NetworkGraphOptions = { + autoResize: true, + node: { + style: { + size: 28, + labelFontSize: 10, + labelBackground: true, + }, + animation: { + enter: false, + }, + }, + edge: { + style: { + labelFontSize: 10, + labelBackground: true, + endArrow: true, + }, + animation: { enter: false }, + }, + behaviors: (prev) => [...prev, { key: 'hover-activate', type: 'hover-activate', degree: 1 }], + transforms: (prev) => [...prev, 'process-parallel-edges'], + layout: { + type: 'force', + animation: false, + }, +}; + +const NetworkGraph: React.FC = (props) => { + const { data: propsData, ...restProps } = props; + + const data = useMemo(() => visGraphData2GraphData(propsData), [propsData]); + + const config = useGraphConfig('NetworkGraph', defaultConfig, restProps); + + return ; +}; + +export default NetworkGraph; diff --git a/src/OrganizationChart/demos/common.tsx b/src/OrganizationChart/demos/common.tsx new file mode 100644 index 0000000..eb3a9bf --- /dev/null +++ b/src/OrganizationChart/demos/common.tsx @@ -0,0 +1,39 @@ +import { OrganizationChart } from '@antv/gpt-vis'; +import React from 'react'; + +const data = { + name: 'Alice Johnson', + description: 'Chief Technology Officer', + children: [ + { + name: 'Bob Smith', + description: 'Senior Software Engineer', + children: [ + { + name: 'Charlie Brown', + description: 'Software Engineer', + }, + { + name: 'Diana White', + description: 'Software Engineer', + }, + ], + }, + { + name: 'Eve Black', + description: 'IT Support Department Head', + children: [ + { + name: 'Frank Green', + description: 'IT Support Specialist', + }, + { + name: 'Grace Blue', + description: 'IT Support Specialist', + }, + ], + }, + ], +}; + +export default () => ; diff --git a/src/OrganizationChart/demos/markdown.tsx b/src/OrganizationChart/demos/markdown.tsx new file mode 100644 index 0000000..2aaf432 --- /dev/null +++ b/src/OrganizationChart/demos/markdown.tsx @@ -0,0 +1,62 @@ +import { Bubble, type BubbleProps } from '@ant-design/x'; +import { ChartType, GPTVis, OrganizationChart, withChartCode } from '@antv/gpt-vis'; +import React from 'react'; + +const markdownContent = ` +当然了,以下是为你绘制的一个组织架构图 + +\`\`\`vis-chart +{ + "type": "organization-chart", + "data": { + "name": "Eric Joplin", + "description": "Chief Executive Officer", + "children": [ + { + "name": "Linda Newland", + "description": "Chief Executive Assistant" + } + ] + } +} +\`\`\` +`; + +const bgStyle = { + display: 'grid', + gridGap: '20px 0', + background: '#f7f7f7', + padding: 20, + borderRadius: 8, +}; + +const CodeComponent = withChartCode({ + components: { [ChartType.OrganizationChart]: OrganizationChart }, + style: { width: 600, height: 250 }, +}); + +const RenderMarkdown: BubbleProps['messageRender'] = (content) => ( + {content} +); + +export default () => ( +
+ + +
+); diff --git a/src/OrganizationChart/index.md b/src/OrganizationChart/index.md new file mode 100644 index 0000000..4aef93e --- /dev/null +++ b/src/OrganizationChart/index.md @@ -0,0 +1,54 @@ +--- +order: 4 +group: + order: 3 + title: 关系图 +--- + +# OrganizationChart 组织架构图 + +组织架构图,用于展示组织内部的层级结构和部门关系。 + +## 代码演示 + +### 单独使用 + + + +### 使用 Markdown 协议 + + + +## Spec + +```json +{ + "type": "organization-chart", + "data": { + "name": "Eric Joplin", + "description": "Chief Executive Officer", + "children": [ + { + "name": "Linda Newland", + "description": "Chief Executive Assistant" + } + ] + } +} +``` + +## API + +### OrganizationChartProps + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| ---- | ----------------------- | -------- | ------ | ---- | +| data | `OrganizationChartData` | 是 | - | 数据 | + +### OrganizationChartData + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| ----------- | ------------------------- | -------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| name | `string` | 是 | - | 节点的名称,表示职位或部门的名称,必须唯一 | +| description | `string` | 否 | - | 节点的描述信息,可以包含职位职责或部门简介等 | +| children | `OrganizationChartData[]` | 否 | - | 节点数组,表示下级职位或部门。如果当前节点没有子节点,该字段可以省略。每个子节点本身也是一个 `OrganizationChartData` 对象,这意味着它可以包含自己的子节点,从而递归地构建出一个多层次的树状结构 | diff --git a/src/OrganizationChart/index.tsx b/src/OrganizationChart/index.tsx new file mode 100644 index 0000000..ffb5009 --- /dev/null +++ b/src/OrganizationChart/index.tsx @@ -0,0 +1,68 @@ +import type { G6, OrganizationChartOptions } from '@ant-design/graphs'; +import { OrganizationChart as ADCOrganizationChart, RCNode } from '@ant-design/graphs'; +import React, { useMemo } from 'react'; +import { useGraphConfig } from '../ConfigProvider/hooks'; +import type { TreeGraphProps } from '../types'; +import { visTreeData2GraphData } from '../utils/graph'; + +const { OrganizationChartNode } = RCNode; + +export interface OrganizationChartProps extends TreeGraphProps {} + +const defaultConfig: OrganizationChartOptions = { + padding: [40, 0, 0, 120], + autoFit: 'view', + autoResize: true, + node: { + style: { + component: (d: G6.NodeData) => { + const isActive = d.states?.includes('active'); + return ( + + ); + }, + size: [280, 80], + }, + }, + edge: { + state: { + active: { + stroke: '#1890ff', + halo: false, + }, + }, + }, + behaviors: (prev) => [...prev, 'hover-activate-neighbors'], + transforms: (prev) => [ + ...prev.filter((t) => (t as G6.BaseTransformOptions).type !== 'collapse-expand-react-node'), + { + ...(prev.find( + (t) => (t as G6.BaseTransformOptions).type === 'collapse-expand-react-node', + ) as G6.BaseTransformOptions), + enable: true, + iconOffsetY: 24, + }, + ], + animation: false, +}; + +const OrganizationChart: React.FC = (props) => { + const { data: propsData, ...restProps } = props; + + const data = useMemo(() => visTreeData2GraphData(propsData), [propsData]); + + const config = useGraphConfig( + 'OrganizationChart', + defaultConfig, + restProps, + ); + + return ; +}; + +export default OrganizationChart; diff --git a/src/PathMap/demos/default.tsx b/src/PathMap/demos/default.tsx new file mode 100644 index 0000000..0506375 --- /dev/null +++ b/src/PathMap/demos/default.tsx @@ -0,0 +1,45 @@ +import { PathMap } from '@antv/gpt-vis'; +import React from 'react'; + +const data = [ + { + points: [ + { longitude: 120.130638, latitude: 30.219835, label: '石屋洞' }, + { longitude: 120.128125, latitude: 30.219386, label: '满觉陇' }, + { longitude: 120.118362, latitude: 30.217175, label: '杨梅岭' }, + { longitude: 120.112958, latitude: 30.207319, label: '理安寺' }, + { longitude: 120.11335, latitude: 30.202395, label: '九溪烟树' }, + ], + }, + { + points: [ + { longitude: 120.100549, latitude: 30.236875, label: '飞来峰' }, + { longitude: 120.101406, latitude: 30.240826, label: '灵隐寺' }, + { longitude: 120.105337, latitude: 30.236818, label: '天竺三寺' }, + ], + }, + { + points: [ + { longitude: 120.116979, latitude: 30.252876, label: '杭州植物园' }, + { longitude: 120.127654, latitude: 30.245663, label: '杭州花圃' }, + { longitude: 120.135764, latitude: 30.251448, label: '苏堤' }, + ], + }, + { + points: [ + { longitude: 120.130095, latitude: 30.207505, label: '虎跑公园' }, + { longitude: 120.145323, latitude: 30.214993, label: '玉皇飞云' }, + { longitude: 120.155057, latitude: 30.232985, label: '长桥公园' }, + ], + }, +]; +const routeData = data.map((item) => { + return { + path: item, + markers: item.points, + }; +}); + +export default () => { + return ; +}; diff --git a/src/PathMap/demos/line.tsx b/src/PathMap/demos/line.tsx new file mode 100644 index 0000000..5c3a8ce --- /dev/null +++ b/src/PathMap/demos/line.tsx @@ -0,0 +1,70 @@ +import { PathMap } from '@antv/gpt-vis'; +import React from 'react'; + +const data = [ + { + points: [ + { longitude: 120.130638, latitude: 30.219835, label: '石屋洞' }, + { longitude: 120.128125, latitude: 30.219386, label: '满觉陇' }, + { longitude: 120.118362, latitude: 30.217175, label: '杨梅岭' }, + { longitude: 120.112958, latitude: 30.207319, label: '理安寺' }, + { longitude: 120.11335, latitude: 30.202395, label: '九溪烟树' }, + ], + }, + { + points: [ + { longitude: 120.100549, latitude: 30.236875, label: '飞来峰' }, + { longitude: 120.101406, latitude: 30.240826, label: '灵隐寺' }, + { longitude: 120.105337, latitude: 30.236818, label: '天竺三寺' }, + ], + }, + { + points: [ + { longitude: 120.116979, latitude: 30.252876, label: '杭州植物园' }, + { longitude: 120.127654, latitude: 30.245663, label: '杭州花圃' }, + { longitude: 120.135764, latitude: 30.251448, label: '苏堤' }, + ], + }, + { + points: [ + { longitude: 120.130095, latitude: 30.207505, label: '虎跑公园' }, + { longitude: 120.145323, latitude: 30.214993, label: '玉皇飞云' }, + { longitude: 120.155057, latitude: 30.232985, label: '长桥公园' }, + ], + }, +]; +const routeData = data.map((item) => { + return { + path: item, + markers: item.points, + }; +}); + +const markerStyle = { + iconPath: + 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*ufrWTqCNCScAAAAAAAAAAAAADmJ7AQ/original', + width: 5, + anchorY: 1, + label: { + color: '#735142', + fontSize: 10, + bgColor: '#ffffff', + }, +}; + +const pathStyle = { + color: '#86f', + width: 1, + dottedLine: true, +}; + +export default () => { + return ( + + ); +}; diff --git a/src/PathMap/demos/markdown.tsx b/src/PathMap/demos/markdown.tsx new file mode 100644 index 0000000..def92e9 --- /dev/null +++ b/src/PathMap/demos/markdown.tsx @@ -0,0 +1,156 @@ +import { Bubble, type BubbleProps } from '@ant-design/x'; +import { ChartType, ConfigProvider, GPTVis, PathMap, withChartCode } from '@antv/gpt-vis'; +import React from 'react'; + +const bgStyle = { + display: 'grid', + gridGap: '20px 0', + background: '#f7f7f7', + padding: 20, + borderRadius: 8, +}; + +const markdownContent = ` + ~~~vis-chart + { + "type": "path-map", + "data": [ + { + "path": { + "points": [ + { + "longitude": 120.130638, + "latitude": 30.219835, + "label": "石屋洞" + }, + { + "longitude": 120.128125, + "latitude": 30.219386, + "label": "满觉陇" + }, + { + "longitude": 120.118362, + "latitude": 30.217175, + "label": "杨梅岭" + }, + { + "longitude": 120.112958, + "latitude": 30.207319, + "label": "理安寺" + }, + { + "longitude": 120.11335, + "latitude": 30.202395, + "label": "九溪烟树" + } + ] + }, + "markers": [ + { + "longitude": 120.130638, + "latitude": 30.219835, + "label": "石屋洞" + }, + { + "longitude": 120.128125, + "latitude": 30.219386, + "label": "满觉陇" + }, + { + "longitude": 120.118362, + "latitude": 30.217175, + "label": "杨梅岭" + }, + { + "longitude": 120.112958, + "latitude": 30.207319, + "label": "理安寺" + }, + { + "longitude": 120.11335, + "latitude": 30.202395, + "label": "九溪烟树" + } + ] + }, + { + "path": { + "points": [ + { + "longitude": 120.100549, + "latitude": 30.236875, + "label": "飞来峰" + }, + { + "longitude": 120.101406, + "latitude": 30.240826, + "label": "灵隐寺" + }, + { + "longitude": 120.105337, + "latitude": 30.236818, + "label": "天竺三寺" + } + ] + }, + "markers": [ + { + "longitude": 120.100549, + "latitude": 30.236875, + "label": "飞来峰" + }, + { + "longitude": 120.101406, + "latitude": 30.240826, + "label": "灵隐寺" + }, + { + "longitude": 120.105337, + "latitude": 30.236818, + "label": "天竺三寺" + } + ] + } + ] +} +~~~`; + +const CodeComponent = withChartCode({ + components: { [ChartType.PathMap]: PathMap }, + style: { width: 500 }, +}); + +const RenderMarkdown: BubbleProps['messageRender'] = (content) => ( + {content} +); + +export default () => { + return ( + +
+ + +
+
+ ); +}; diff --git a/src/PathMap/index.md b/src/PathMap/index.md new file mode 100644 index 0000000..85f461b --- /dev/null +++ b/src/PathMap/index.md @@ -0,0 +1,158 @@ +--- +order: 2 +group: + order: 2 + title: 地图 +demo: { cols: 2 } +--- + +# PathMap 路径地图 + +路径地图,用于可视化路径线路数据。 + +## 代码演示 + +默认地图 +自定义样式 + +使用 Markdown 协议 + +## Spec + +```json +{ + "type": "path-map", + "data": [ + { + "path": { + "points": [ + { + "longitude": 120.130638, + "latitude": 30.219835, + "label": "石屋洞" + }, + { + "longitude": 120.128125, + "latitude": 30.219386, + "label": "满觉陇" + }, + { + "longitude": 120.118362, + "latitude": 30.217175, + "label": "杨梅岭" + }, + { + "longitude": 120.112958, + "latitude": 30.207319, + "label": "理安寺" + }, + { + "longitude": 120.11335, + "latitude": 30.202395, + "label": "九溪烟树" + } + ] + }, + "markers": [ + { + "longitude": 120.130638, + "latitude": 30.219835, + "label": "石屋洞" + }, + { + "longitude": 120.128125, + "latitude": 30.219386, + "label": "满觉陇" + }, + { + "longitude": 120.118362, + "latitude": 30.217175, + "label": "杨梅岭" + }, + { + "longitude": 120.112958, + "latitude": 30.207319, + "label": "理安寺" + }, + { + "longitude": 120.11335, + "latitude": 30.202395, + "label": "九溪烟树" + } + ] + }, + { + "path": { + "points": [ + { + "longitude": 120.100549, + "latitude": 30.236875, + "label": "飞来峰" + }, + { + "longitude": 120.101406, + "latitude": 30.240826, + "label": "灵隐寺" + }, + { + "longitude": 120.105337, + "latitude": 30.236818, + "label": "天竺三寺" + } + ] + }, + "markers": [ + { + "longitude": 120.100549, + "latitude": 30.236875, + "label": "飞来峰" + }, + { + "longitude": 120.101406, + "latitude": 30.240826, + "label": "灵隐寺" + }, + { + "longitude": 120.105337, + "latitude": 30.236818, + "label": "天竺三寺" + } + ] + } + ] +} +``` + +## API + +### PathMapProps + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| ----------- | ---------- | -------- | ------ | ---------- | +| data | RoutData[] | 是 | - | 数据 | +| markerStyle | Marker | 否 | - | 标记点样式 | +| pathStyle | Polyline | 否 | - | 线样式 | + +### RoutData + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| ------- | ------------ | -------- | ------ | -------- | +| markers | MarkerData[] | 否 | - | 路径标注 | +| path | Polyline | 是 | - | 路径轨迹 | + +### Polyline + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| ---------- | -------- | -------- | ------ | -------- | +| points | LngLat[] | 是 | - | 路径标注 | +| width | Polyline | 否 | 2 | 轨迹宽度 | +| color | string | 否 | #16f | 颜色 | +| dottedLine | boolean | 否 | false | 是否虚线 | + +### MarkerData + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| --------- | ------ | -------- | ------ | -------- | +| longitude | number | 是 | - | 经度 | +| latitude | number | 是 | - | 纬度 | +| label | number | 是 | - | 文字标注 | diff --git a/src/PathMap/index.tsx b/src/PathMap/index.tsx new file mode 100644 index 0000000..3138cb0 --- /dev/null +++ b/src/PathMap/index.tsx @@ -0,0 +1,33 @@ +import React, { useMemo, type FC } from 'react'; +import { useMapConfig } from '../ConfigProvider/hooks'; +import type { MapProps } from '../Map'; +import Map from '../Map'; +import type { PathMap as _PathMap, MarkerData, Polyline } from '../types/map'; +import { formatMakerStyle, formatPolylineStyle } from '../utils/map'; + +export type PathMapProps = MapProps & _PathMap; + +const PathMap: FC = (props) => { + const { data = [], markerStyle = {}, pathStyle = {}, ...rest } = useMapConfig('PathMap', props); + + const markerdata = useMemo(() => { + const markers: MarkerData[] = []; + data.forEach((item) => { + if (item.markers) { + markers.push(...item.markers); + } + }); + return formatMakerStyle(markers, markerStyle); + }, [data, markerStyle]); + + const linedata = useMemo(() => { + const lines = data.map((item) => { + return item.path; + }) as Polyline[]; + return formatPolylineStyle(lines, pathStyle); + }, [data, pathStyle]); + + return ; +}; + +export default PathMap; diff --git a/src/Pie/demos/common.tsx b/src/Pie/demos/common.tsx new file mode 100644 index 0000000..cbf18fa --- /dev/null +++ b/src/Pie/demos/common.tsx @@ -0,0 +1,15 @@ +import { Pie } from '@antv/gpt-vis'; +import React from 'react'; + +const data = [ + { category: '分类一', value: 27 }, + { category: '分类二', value: 25 }, + { category: '分类三', value: 18 }, + { category: '分类四', value: 15 }, + { category: '分类五', value: 10 }, + { category: '其他', value: 5 }, +]; + +export default () => { + return ; +}; diff --git a/src/Pie/demos/markdown.tsx b/src/Pie/demos/markdown.tsx new file mode 100644 index 0000000..6ba190b --- /dev/null +++ b/src/Pie/demos/markdown.tsx @@ -0,0 +1,61 @@ +import { Bubble, type BubbleProps } from '@ant-design/x'; +import { ChartType, GPTVis, Pie, withChartCode } from '@antv/gpt-vis'; +import React from 'react'; + +const markdownContent = ` +以下是为你绘制的一个饼图 + +\`\`\`vis-chart +{ + "type": "pie", + "data": [ + { "category": "分类一", "value": 27 }, + { "category": "分类二", "value": 25 }, + { "category": "分类三", "value": 18 }, + { "category": "分类四", "value": 15 }, + { "category": "分类五", "value": 10 }, + { "category": "其他", "value": 5 } + ], + "innerRadius": 0.6 +} +\`\`\` +`; + +const bgStyle = { + display: 'grid', + gridGap: '20px 0', + background: '#f7f7f7', + padding: 20, + borderRadius: 8, +}; + +const CodeComponent = withChartCode({ + components: { [ChartType.Pie]: Pie }, + style: { width: 350, height: 350 }, +}); + +const RenderMarkdown: BubbleProps['messageRender'] = (content) => ( + {content} +); + +export default () => ( +
+ + +
+); diff --git a/src/Pie/index.md b/src/Pie/index.md new file mode 100644 index 0000000..968fd79 --- /dev/null +++ b/src/Pie/index.md @@ -0,0 +1,47 @@ +--- +order: 3 +group: + order: 1 + title: 统计图 +--- + +# Pie 饼图 + +## 代码演示 + +单独使用 +使用 Markdown 协议 + +## Spec + +```json +{ + "type": "pie", + "data": [ + { "category": "分类一", "value": 27 }, + { "category": "分类二", "value": 25 }, + { "category": "分类三", "value": 18 }, + { "category": "分类四", "value": 15 }, + { "category": "分类五", "value": 10 }, + { "category": "其他", "value": 5 } + ] +} +``` + +## API + +### PieProps + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| ----------- | ------------- | -------- | ------ | -------------------------------------------------------------------------------------------------- | +| data | PieDataItem[] | 是 | - | 饼图数据 | +| title | string | 否 | - | 图表的标题 | +| innerRadius | number | 否 | - | 饼图内半径,设置为环图 | +| ... | - | - | - | 更多属性,详见 [Ant Design Charts ](https://ant-design-charts.antgroup.com/options/plots/overview) | + +### PieDataItem + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| -------- | ------ | -------- | ------ | -------------- | +| category | string | 是 | - | 扇形区域的名称 | +| value | number | 是 | - | 扇形区域的值 | diff --git a/src/Pie/index.text.tsx b/src/Pie/index.text.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/Pie/index.tsx b/src/Pie/index.tsx new file mode 100644 index 0000000..1885a2d --- /dev/null +++ b/src/Pie/index.tsx @@ -0,0 +1,65 @@ +import type { PieConfig } from '@ant-design/plots'; +import { Pie as ADCPie } from '@ant-design/plots'; +import { round, sumBy } from 'lodash'; +import React from 'react'; +import { usePlotConfig } from '../ConfigProvider/hooks'; +import type { BasePlotProps } from '../types'; + +/** + * PieDataItem is the type for each data item in the pie chart. + * It includes the name of the sector and its corresponding value. + * @param category The name of the sector. + * @param value The value of the sector. + */ +type PieDataItem = { + category: string; + value: number; + [key: string]: string | number; +}; + +/** + * the props for the Pie + * @param data pie data + */ +export type PieProps = BasePlotProps & Partial; + +const defaultConfig = (props: PieConfig): PieConfig => { + const { data = [], angleField = 'value', colorField = 'category' } = props; + const sumValue = sumBy(data, angleField); + + return { + angleField, + colorField, + tooltip: (d) => { + return { + name: d[colorField as string], + value: d[angleField as string], + }; + }, + label: { + position: 'outside', + // text: angleField, + text: (d: Record) => + `${d[colorField as string]}: ${round((d[angleField as string] / sumValue) * 100, 1)}%`, + }, + legend: { + color: { + title: false, + position: 'top', + }, + }, + interaction: { + elementSelect: { + single: true, + }, + }, + }; +}; + +const Pie = (props: PieProps) => { + const config = usePlotConfig('Pie', defaultConfig, props); + + return ; +}; + +export default Pie; diff --git a/src/PinMap/demos/default.tsx b/src/PinMap/demos/default.tsx new file mode 100644 index 0000000..8ae4812 --- /dev/null +++ b/src/PinMap/demos/default.tsx @@ -0,0 +1,32 @@ +import { PinMap } from '@antv/gpt-vis'; +import React from 'react'; + +export default () => { + const data = [ + { + label: '石屋洞', + longitude: 120.130638, + latitude: 30.219835, + cityname: '杭州市', + }, + { + label: '满觉陇', + longitude: 120.128125, + latitude: 30.219386, + }, + { label: '杨梅岭', longitude: 120.118362, latitude: 30.217175 }, + { label: '理安寺', longitude: 120.112958, latitude: 30.207319 }, + { label: '九溪烟树', longitude: 120.11335, latitude: 30.202395 }, + { label: '飞来峰', longitude: 120.100549, latitude: 30.236875 }, + { label: '灵隐寺', longitude: 120.101406, latitude: 30.240826 }, + { label: '天竺三寺', longitude: 120.105337, latitude: 30.236818 }, + { label: '杭州植物园', longitude: 120.116979, latitude: 30.252876 }, + { label: '杭州花圃', longitude: 120.127654, latitude: 30.245663 }, + { label: '苏堤', longitude: 120.135764, latitude: 30.251448 }, + { label: '虎跑公园', longitude: 120.130095, latitude: 30.207505 }, + { label: '玉皇飞云', longitude: 120.145323, latitude: 30.214993 }, + { label: '长桥公园', longitude: 120.155057, latitude: 30.232985 }, + ]; + + return ; +}; diff --git a/src/PinMap/demos/markdown.tsx b/src/PinMap/demos/markdown.tsx new file mode 100644 index 0000000..f40734e --- /dev/null +++ b/src/PinMap/demos/markdown.tsx @@ -0,0 +1,81 @@ +import { Bubble, type BubbleProps } from '@ant-design/x'; +import { ChartType, ConfigProvider, GPTVis, PinMap, withChartCode } from '@antv/gpt-vis'; +import React from 'react'; + +const bgStyle = { + display: 'grid', + gridGap: '20px 0', + background: '#f7f7f7', + padding: 20, + borderRadius: 8, +}; + +const RenderMarkdown: BubbleProps['messageRender'] = (content) => ( + {content} +); + +const markdownContent = ` + ~~~vis-chart + { + "type": "pin-map", + "data": [ + { "label": "杨梅岭", "longitude": 120.118362, "latitude": 30.217175 }, + { "label": "理安寺", "longitude": 120.112958, "latitude": 30.207319 }, + { "label": "九溪烟树", "longitude": 120.11335, "latitude": 30.202395 }, + { "label": "飞来峰", "longitude": 120.100549, "latitude": 30.236875 }, + { "label": "灵隐寺", "longitude": 120.101406, "latitude": 30.240826 }, + { "label": "天竺三寺", "longitude": 120.105337, "latitude": 30.236818 }, + { "label": "杭州植物园", "longitude": 120.116979, "latitude": 30.252876 }, + { "label": "杭州花圃", "longitude": 120.127654, "latitude": 30.245663 }, + { "label": "苏堤", "longitude": 120.135764, "latitude": 30.251448 }, + { "label": "虎跑公园", "longitude": 120.130095, "latitude": 30.207505 }, + { "label": "玉皇飞云", "longitude": 120.145323, "latitude": 30.214993 }, + { "label": "长桥公园", "longitude": 120.155057, "latitude": 30.232985 } + ] +} +~~~`; + +const CodeComponent = withChartCode({ + components: { [ChartType.PinMap]: PinMap }, + debug: true, + style: { width: 500 }, +}); + +export default () => { + return ( + +
+ + +
+
+ ); +}; diff --git a/src/PinMap/demos/marker.tsx b/src/PinMap/demos/marker.tsx new file mode 100644 index 0000000..8808a9e --- /dev/null +++ b/src/PinMap/demos/marker.tsx @@ -0,0 +1,48 @@ +import { PinMap } from '@antv/gpt-vis'; +import React from 'react'; +export default () => { + const data = [ + { + label: '石屋洞', + longitude: 120.130638, + latitude: 30.219835, + cityname: '杭州市', + }, + { + label: '满觉陇', + longitude: 120.128125, + latitude: 30.219386, + }, + { label: '杨梅岭', longitude: 120.118362, latitude: 30.217175 }, + { label: '理安寺', longitude: 120.112958, latitude: 30.207319 }, + { label: '九溪烟树', longitude: 120.11335, latitude: 30.202395 }, + { label: '飞来峰', longitude: 120.100549, latitude: 30.236875 }, + { label: '灵隐寺', longitude: 120.101406, latitude: 30.240826 }, + { label: '天竺三寺', longitude: 120.105337, latitude: 30.236818 }, + { label: '杭州植物园', longitude: 120.116979, latitude: 30.252876 }, + { label: '杭州花圃', longitude: 120.127654, latitude: 30.245663 }, + { label: '苏堤', longitude: 120.135764, latitude: 30.251448 }, + { label: '虎跑公园', longitude: 120.130095, latitude: 30.207505 }, + { label: '玉皇飞云', longitude: 120.145323, latitude: 30.214993 }, + { label: '长桥公园', longitude: 120.155057, latitude: 30.232985 }, + ]; + const markerStyle = { + iconPath: + 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*ufrWTqCNCScAAAAAAAAAAAAADmJ7AQ/original', + width: 5, + anchorY: -1, + label: { + color: '#735142', + fontSize: 10, + bgColor: '#ffffff', + }, + }; + + return ( + + ); +}; diff --git a/src/PinMap/index.md b/src/PinMap/index.md new file mode 100644 index 0000000..ff6dbd7 --- /dev/null +++ b/src/PinMap/index.md @@ -0,0 +1,58 @@ +--- +order: 1 +group: + order: 2 + title: 地图 +demo: { cols: 2 } +--- + +# PinMap 点标注地图 + +点标注地图,用于可视化展示 POI 点位数据。 + +## 代码演示 + +默认地图 +自定义样式 + +使用 Markdown 协议 + +## Spec + +```js +{ + "type": "pin-map", + "data": [ + { label: '杨梅岭', longitude: 120.118362, latitude: 30.217175 }, + { label: '理安寺', longitude: 120.112958, latitude: 30.207319 }, + { label: '九溪烟树', longitude: 120.11335, latitude: 30.202395 }, + { label: '飞来峰', longitude: 120.100549, latitude: 30.236875 }, + { label: '灵隐寺', longitude: 120.101406, latitude: 30.240826 }, + { label: '天竺三寺', longitude: 120.105337, latitude: 30.236818 }, + { label: '杭州植物园', longitude: 120.116979, latitude: 30.252876 }, + { label: '杭州花圃', longitude: 120.127654, latitude: 30.245663 }, + { label: '苏堤', longitude: 120.135764, latitude: 30.251448 }, + { label: '虎跑公园', longitude: 120.130095, latitude: 30.207505 }, + { label: '玉皇飞云', longitude: 120.145323, latitude: 30.214993 }, + { label: '长桥公园', longitude: 120.155057, latitude: 30.232985 }, + ] +} + +``` + +## API + +### PinMapProps + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| ----------- | ------------ | -------- | ------ | ---------- | +| data | MarkerData[] | 是 | - | 数据 | +| markerStyle | Marker | 否 | - | 标记点样式 | + +### MarkerData + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| --------- | ------ | -------- | ------ | -------- | +| longitude | number | 是 | - | 经度 | +| latitude | number | 是 | - | 纬度 | +| label | number | 是 | - | 文字标注 | diff --git a/src/PinMap/index.tsx b/src/PinMap/index.tsx new file mode 100644 index 0000000..6edb8ad --- /dev/null +++ b/src/PinMap/index.tsx @@ -0,0 +1,18 @@ +import React, { useMemo, type FC } from 'react'; +import { useMapConfig } from '../ConfigProvider/hooks'; +import type { MapProps } from '../Map'; +import Map from '../Map'; +import type { PinMap as PinMapType } from '../types/map'; +import { formatMakerStyle } from '../utils/map'; + +export type PinMapProps = PinMapType & MapProps; + +const PinMap: FC = (props) => { + const { data = [], markerStyle = {}, ...rest } = useMapConfig('PinMap', props); + + const markerdata = useMemo(() => formatMakerStyle(data, markerStyle), [data, markerStyle]); + + return ; +}; + +export default PinMap; diff --git a/src/PinMap/maker.svg b/src/PinMap/maker.svg new file mode 100644 index 0000000..ea2757f --- /dev/null +++ b/src/PinMap/maker.svg @@ -0,0 +1 @@ + diff --git a/src/Radar/demos/category.tsx b/src/Radar/demos/category.tsx new file mode 100644 index 0000000..dcc0612 --- /dev/null +++ b/src/Radar/demos/category.tsx @@ -0,0 +1,65 @@ +import { Bubble, type BubbleProps } from '@ant-design/x'; +import { ChartType, GPTVis, Radar, withChartCode } from '@antv/gpt-vis'; +import React from 'react'; + +const markdownContent = ` +当然了,以下是为你绘制的一个雷达图 + +\`\`\`vis-chart +{ + "type": "radar", + "data": [ + { "group": "Apple", "name": "Vitamin C", "value": 5 }, + { "group": "Apple", "name": "Fiber", "value": 7 }, + { "group": "Apple", "name": "Sugar", "value": 6 }, + { "group": "Apple", "name": "Protein", "value": 2 }, + { "group": "Apple", "name": "Iron", "value": 3 }, + { "group": "Apple", "name": "Calcium", "value": 2 }, + { "group": "Banana", "name": "Vitamin C", "value": 4 }, + { "group": "Banana", "name": "Fiber", "value": 5 }, + { "group": "Banana", "name": "Sugar", "value": 7 }, + { "group": "Banana", "name": "Protein", "value": 3 }, + { "group": "Banana", "name": "Iron", "value": 2 }, + { "group": "Banana", "name": "Calcium", "value": 3 } + ] +} +\`\`\` +`; + +const bgStyle = { + display: 'grid', + gridGap: '20px 0', + background: '#f7f7f7', + padding: 20, + borderRadius: 8, +}; + +const CodeComponent = withChartCode({ + components: { [ChartType.Radar]: Radar }, +}); + +const RenderMarkdown: BubbleProps['messageRender'] = (content) => ( + {content} +); + +export default () => ( +
+ + +
+); diff --git a/src/Radar/demos/common.tsx b/src/Radar/demos/common.tsx new file mode 100644 index 0000000..0e3d1d7 --- /dev/null +++ b/src/Radar/demos/common.tsx @@ -0,0 +1,15 @@ +import { Radar } from '@antv/gpt-vis'; +import React from 'react'; + +const data = [ + { name: '沟通能力', value: 2 }, + { name: '协作能力', value: 3 }, + { name: '领导能力', value: 2 }, + { name: '学习能力', value: 5 }, + { name: '创新能力', value: 6 }, + { name: '技术能力', value: 9 }, +]; + +export default () => { + return ; +}; diff --git a/src/Radar/demos/markdown.tsx b/src/Radar/demos/markdown.tsx new file mode 100644 index 0000000..3fe498f --- /dev/null +++ b/src/Radar/demos/markdown.tsx @@ -0,0 +1,59 @@ +import { Bubble, type BubbleProps } from '@ant-design/x'; +import { ChartType, GPTVis, Radar, withChartCode } from '@antv/gpt-vis'; +import React from 'react'; + +const markdownContent = ` +当然了,以下是为你绘制的一个雷达图 + +\`\`\`vis-chart +{ + "type": "radar", + "data": [ + { "name": "Vitamin C", "value": 7 }, + { "name": "Fiber", "value": 6 }, + { "name": "Sugar", "value": 5 }, + { "name": "Protein", "value": 4 }, + { "name": "Iron", "value": 3 }, + { "name": "Calcium", "value": 2 } + ] +} +\`\`\` +`; + +const bgStyle = { + display: 'grid', + gridGap: '20px 0', + background: '#f7f7f7', + padding: 20, + borderRadius: 8, +}; + +const CodeComponent = withChartCode({ + components: { [ChartType.Radar]: Radar }, +}); + +const RenderMarkdown: BubbleProps['messageRender'] = (content) => ( + {content} +); + +export default () => ( +
+ + +
+); diff --git a/src/Radar/index.md b/src/Radar/index.md new file mode 100644 index 0000000..9b573be --- /dev/null +++ b/src/Radar/index.md @@ -0,0 +1,50 @@ +--- +order: 11 +group: + order: 1 + title: 统计图 +demo: { cols: 2 } +--- + +# Radar 雷达图 + +## 代码演示 + +单独使用 + +使用 Markdown 协议 +分组雷达图 + +## Spec + +```json +{ + "type": "radar", + "data": [ + { "name": "沟通能力", "value": 2 }, + { "name": "协作能力", "value": 3 }, + { "name": "领导能力", "value": 2 }, + { "name": "学习能力", "value": 5 }, + { "name": "创新能力", "value": 6 }, + { "name": "技术能力", "value": 9 } + ] +} +``` + +## API + +### RadarProps + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| ----- | --------------- | -------- | ------ | -------------------------------------------------------------------------------------------------- | +| data | RadarDataItem[] | 是 | - | 数据 | +| title | string | 否 | - | 图表的标题 | +| ... | - | - | - | 更多属性,详见 [Ant Design Charts ](https://ant-design-charts.antgroup.com/options/plots/overview) | + +### RadarDataItem + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| ----- | ------ | -------- | ------ | ------------ | +| name | string | 是 | - | 数据分类名称 | +| value | number | 是 | - | 数据的值 | +| group | string | 否 | - | 数据分组名称 | diff --git a/src/Radar/index.tsx b/src/Radar/index.tsx new file mode 100644 index 0000000..dcd71b8 --- /dev/null +++ b/src/Radar/index.tsx @@ -0,0 +1,65 @@ +import type { RadarConfig } from '@ant-design/plots'; +import { Radar as ADCRadar } from '@ant-design/plots'; +import { get } from 'lodash'; +import React from 'react'; +import { usePlotConfig } from '../ConfigProvider/hooks'; +import type { BasePlotProps } from '../types'; + +export type RadarDataItem = { + name: string; + value: number; + [key: string]: string | number; +}; + +export type RadarProps = BasePlotProps & Partial; + +const defaultConfig = (props: RadarConfig): RadarConfig => { + const { data, xField = 'name', yField = 'value' } = props; + const hasGroupField = get(data, '[0].group') !== undefined; + + return { + xField, + yField, + colorField: hasGroupField ? 'group' : undefined, + area: { + style: { + fillOpacity: 0.5, + }, + }, + scale: { + x: { + padding: 0.6, + align: 0, + }, + y: { + nice: true, + }, + }, + axis: { + x: { + title: false, + grid: true, + }, + y: { + zIndex: 1, + title: false, + gridConnect: 'line', + gridLineWidth: 1, + }, + }, + tooltip: (d) => { + return { + name: d[xField as string], + value: d[yField as string], + }; + }, + }; +}; + +const Radar = (props: RadarProps) => { + const config = usePlotConfig('Radar', defaultConfig, props); + + return ; +}; + +export default Radar; diff --git a/src/Scatter/demos/common.tsx b/src/Scatter/demos/common.tsx new file mode 100644 index 0000000..027ff48 --- /dev/null +++ b/src/Scatter/demos/common.tsx @@ -0,0 +1,2044 @@ +import { Scatter } from '@antv/gpt-vis'; +import React from 'react'; + +const data = [ + { + x: 161.2, + y: 51.6, + }, + { + x: 167.5, + y: 59, + }, + { + x: 159.5, + y: 49.2, + }, + { + x: 157, + y: 63, + }, + { + x: 155.8, + y: 53.6, + }, + { + x: 170, + y: 59, + }, + { + x: 159.1, + y: 47.6, + }, + { + x: 166, + y: 69.8, + }, + { + x: 176.2, + y: 66.8, + }, + { + x: 160.2, + y: 75.2, + }, + { + x: 172.5, + y: 55.2, + }, + { + x: 170.9, + y: 54.2, + }, + { + x: 172.9, + y: 62.5, + }, + { + x: 153.4, + y: 42, + }, + { + x: 160, + y: 50, + }, + { + x: 147.2, + y: 49.8, + }, + { + x: 168.2, + y: 49.2, + }, + { + x: 175, + y: 73.2, + }, + { + x: 157, + y: 47.8, + }, + { + x: 167.6, + y: 68.8, + }, + { + x: 159.5, + y: 50.6, + }, + { + x: 175, + y: 82.5, + }, + { + x: 166.8, + y: 57.2, + }, + { + x: 176.5, + y: 87.8, + }, + { + x: 170.2, + y: 72.8, + }, + { + x: 174, + y: 54.5, + }, + { + x: 173, + y: 59.8, + }, + { + x: 179.9, + y: 67.3, + }, + { + x: 170.5, + y: 67.8, + }, + { + x: 160, + y: 47, + }, + { + x: 154.4, + y: 46.2, + }, + { + x: 162, + y: 55, + }, + { + x: 176.5, + y: 83, + }, + { + x: 160, + y: 54.4, + }, + { + x: 152, + y: 45.8, + }, + { + x: 162.1, + y: 53.6, + }, + { + x: 170, + y: 73.2, + }, + { + x: 160.2, + y: 52.1, + }, + { + x: 161.3, + y: 67.9, + }, + { + x: 166.4, + y: 56.6, + }, + { + x: 168.9, + y: 62.3, + }, + { + x: 163.8, + y: 58.5, + }, + { + x: 167.6, + y: 54.5, + }, + { + x: 160, + y: 50.2, + }, + { + x: 161.3, + y: 60.3, + }, + { + x: 167.6, + y: 58.3, + }, + { + x: 165.1, + y: 56.2, + }, + { + x: 160, + y: 50.2, + }, + { + x: 170, + y: 72.9, + }, + { + x: 157.5, + y: 59.8, + }, + { + x: 167.6, + y: 61, + }, + { + x: 160.7, + y: 69.1, + }, + { + x: 163.2, + y: 55.9, + }, + { + x: 152.4, + y: 46.5, + }, + { + x: 157.5, + y: 54.3, + }, + { + x: 168.3, + y: 54.8, + }, + { + x: 180.3, + y: 60.7, + }, + { + x: 165.5, + y: 60, + }, + { + x: 165, + y: 62, + }, + { + x: 164.5, + y: 60.3, + }, + { + x: 156, + y: 52.7, + }, + { + x: 160, + y: 74.3, + }, + { + x: 163, + y: 62, + }, + { + x: 165.7, + y: 73.1, + }, + { + x: 161, + y: 80, + }, + { + x: 162, + y: 54.7, + }, + { + x: 166, + y: 53.2, + }, + { + x: 174, + y: 75.7, + }, + { + x: 172.7, + y: 61.1, + }, + { + x: 167.6, + y: 55.7, + }, + { + x: 151.1, + y: 48.7, + }, + { + x: 164.5, + y: 52.3, + }, + { + x: 163.5, + y: 50, + }, + { + x: 152, + y: 59.3, + }, + { + x: 169, + y: 62.5, + }, + { + x: 164, + y: 55.7, + }, + { + x: 161.2, + y: 54.8, + }, + { + x: 155, + y: 45.9, + }, + { + x: 170, + y: 70.6, + }, + { + x: 176.2, + y: 67.2, + }, + { + x: 170, + y: 69.4, + }, + { + x: 162.5, + y: 58.2, + }, + { + x: 170.3, + y: 64.8, + }, + { + x: 164.1, + y: 71.6, + }, + { + x: 169.5, + y: 52.8, + }, + { + x: 163.2, + y: 59.8, + }, + { + x: 154.5, + y: 49, + }, + { + x: 159.8, + y: 50, + }, + { + x: 173.2, + y: 69.2, + }, + { + x: 170, + y: 55.9, + }, + { + x: 161.4, + y: 63.4, + }, + { + x: 169, + y: 58.2, + }, + { + x: 166.2, + y: 58.6, + }, + { + x: 159.4, + y: 45.7, + }, + { + x: 162.5, + y: 52.2, + }, + { + x: 159, + y: 48.6, + }, + { + x: 162.8, + y: 57.8, + }, + { + x: 159, + y: 55.6, + }, + { + x: 179.8, + y: 66.8, + }, + { + x: 162.9, + y: 59.4, + }, + { + x: 161, + y: 53.6, + }, + { + x: 151.1, + y: 73.2, + }, + { + x: 168.2, + y: 53.4, + }, + { + x: 168.9, + y: 69, + }, + { + x: 173.2, + y: 58.4, + }, + { + x: 171.8, + y: 56.2, + }, + { + x: 178, + y: 70.6, + }, + { + x: 164.3, + y: 59.8, + }, + { + x: 163, + y: 72, + }, + { + x: 168.5, + y: 65.2, + }, + { + x: 166.8, + y: 56.6, + }, + { + x: 172.7, + y: 105.2, + }, + { + x: 163.5, + y: 51.8, + }, + { + x: 169.4, + y: 63.4, + }, + { + x: 167.8, + y: 59, + }, + { + x: 159.5, + y: 47.6, + }, + { + x: 167.6, + y: 63, + }, + { + x: 161.2, + y: 55.2, + }, + { + x: 160, + y: 45, + }, + { + x: 163.2, + y: 54, + }, + { + x: 162.2, + y: 50.2, + }, + { + x: 161.3, + y: 60.2, + }, + { + x: 149.5, + y: 44.8, + }, + { + x: 157.5, + y: 58.8, + }, + { + x: 163.2, + y: 56.4, + }, + { + x: 172.7, + y: 62, + }, + { + x: 155, + y: 49.2, + }, + { + x: 156.5, + y: 67.2, + }, + { + x: 164, + y: 53.8, + }, + { + x: 160.9, + y: 54.4, + }, + { + x: 162.8, + y: 58, + }, + { + x: 167, + y: 59.8, + }, + { + x: 160, + y: 54.8, + }, + { + x: 160, + y: 43.2, + }, + { + x: 168.9, + y: 60.5, + }, + { + x: 158.2, + y: 46.4, + }, + { + x: 156, + y: 64.4, + }, + { + x: 160, + y: 48.8, + }, + { + x: 167.1, + y: 62.2, + }, + { + x: 158, + y: 55.5, + }, + { + x: 167.6, + y: 57.8, + }, + { + x: 156, + y: 54.6, + }, + { + x: 162.1, + y: 59.2, + }, + { + x: 173.4, + y: 52.7, + }, + { + x: 159.8, + y: 53.2, + }, + { + x: 170.5, + y: 64.5, + }, + { + x: 159.2, + y: 51.8, + }, + { + x: 157.5, + y: 56, + }, + { + x: 161.3, + y: 63.6, + }, + { + x: 162.6, + y: 63.2, + }, + { + x: 160, + y: 59.5, + }, + { + x: 168.9, + y: 56.8, + }, + { + x: 165.1, + y: 64.1, + }, + { + x: 162.6, + y: 50, + }, + { + x: 165.1, + y: 72.3, + }, + { + x: 166.4, + y: 55, + }, + { + x: 160, + y: 55.9, + }, + { + x: 152.4, + y: 60.4, + }, + { + x: 170.2, + y: 69.1, + }, + { + x: 162.6, + y: 84.5, + }, + { + x: 170.2, + y: 55.9, + }, + { + x: 158.8, + y: 55.5, + }, + { + x: 172.7, + y: 69.5, + }, + { + x: 167.6, + y: 76.4, + }, + { + x: 162.6, + y: 61.4, + }, + { + x: 167.6, + y: 65.9, + }, + { + x: 156.2, + y: 58.6, + }, + { + x: 175.2, + y: 66.8, + }, + { + x: 172.1, + y: 56.6, + }, + { + x: 162.6, + y: 58.6, + }, + { + x: 160, + y: 55.9, + }, + { + x: 165.1, + y: 59.1, + }, + { + x: 182.9, + y: 81.8, + }, + { + x: 166.4, + y: 70.7, + }, + { + x: 165.1, + y: 56.8, + }, + { + x: 177.8, + y: 60, + }, + { + x: 165.1, + y: 58.2, + }, + { + x: 175.3, + y: 72.7, + }, + { + x: 154.9, + y: 54.1, + }, + { + x: 158.8, + y: 49.1, + }, + { + x: 172.7, + y: 75.9, + }, + { + x: 168.9, + y: 55, + }, + { + x: 161.3, + y: 57.3, + }, + { + x: 167.6, + y: 55, + }, + { + x: 165.1, + y: 65.5, + }, + { + x: 175.3, + y: 65.5, + }, + { + x: 157.5, + y: 48.6, + }, + { + x: 163.8, + y: 58.6, + }, + { + x: 167.6, + y: 63.6, + }, + { + x: 165.1, + y: 55.2, + }, + { + x: 165.1, + y: 62.7, + }, + { + x: 168.9, + y: 56.6, + }, + { + x: 162.6, + y: 53.9, + }, + { + x: 164.5, + y: 63.2, + }, + { + x: 176.5, + y: 73.6, + }, + { + x: 168.9, + y: 62, + }, + { + x: 175.3, + y: 63.6, + }, + { + x: 159.4, + y: 53.2, + }, + { + x: 160, + y: 53.4, + }, + { + x: 170.2, + y: 55, + }, + { + x: 162.6, + y: 70.5, + }, + { + x: 167.6, + y: 54.5, + }, + { + x: 162.6, + y: 54.5, + }, + { + x: 160.7, + y: 55.9, + }, + { + x: 160, + y: 59, + }, + { + x: 157.5, + y: 63.6, + }, + { + x: 162.6, + y: 54.5, + }, + { + x: 152.4, + y: 47.3, + }, + { + x: 170.2, + y: 67.7, + }, + { + x: 165.1, + y: 80.9, + }, + { + x: 172.7, + y: 70.5, + }, + { + x: 165.1, + y: 60.9, + }, + { + x: 170.2, + y: 63.6, + }, + { + x: 170.2, + y: 54.5, + }, + { + x: 170.2, + y: 59.1, + }, + { + x: 161.3, + y: 70.5, + }, + { + x: 167.6, + y: 52.7, + }, + { + x: 167.6, + y: 62.7, + }, + { + x: 165.1, + y: 86.3, + }, + { + x: 162.6, + y: 66.4, + }, + { + x: 152.4, + y: 67.3, + }, + { + x: 168.9, + y: 63, + }, + { + x: 170.2, + y: 73.6, + }, + { + x: 175.2, + y: 62.3, + }, + { + x: 175.2, + y: 57.7, + }, + { + x: 160, + y: 55.4, + }, + { + x: 165.1, + y: 104.1, + }, + { + x: 174, + y: 55.5, + }, + { + x: 170.2, + y: 77.3, + }, + { + x: 160, + y: 80.5, + }, + { + x: 167.6, + y: 64.5, + }, + { + x: 167.6, + y: 72.3, + }, + { + x: 167.6, + y: 61.4, + }, + { + x: 154.9, + y: 58.2, + }, + { + x: 162.6, + y: 81.8, + }, + { + x: 175.3, + y: 63.6, + }, + { + x: 171.4, + y: 53.4, + }, + { + x: 157.5, + y: 54.5, + }, + { + x: 165.1, + y: 53.6, + }, + { + x: 160, + y: 60, + }, + { + x: 174, + y: 73.6, + }, + { + x: 162.6, + y: 61.4, + }, + { + x: 174, + y: 55.5, + }, + { + x: 162.6, + y: 63.6, + }, + { + x: 161.3, + y: 60.9, + }, + { + x: 156.2, + y: 60, + }, + { + x: 149.9, + y: 46.8, + }, + { + x: 169.5, + y: 57.3, + }, + { + x: 160, + y: 64.1, + }, + { + x: 175.3, + y: 63.6, + }, + { + x: 169.5, + y: 67.3, + }, + { + x: 160, + y: 75.5, + }, + { + x: 172.7, + y: 68.2, + }, + { + x: 162.6, + y: 61.4, + }, + { + x: 157.5, + y: 76.8, + }, + { + x: 176.5, + y: 71.8, + }, + { + x: 164.4, + y: 55.5, + }, + { + x: 160.7, + y: 48.6, + }, + { + x: 174, + y: 66.4, + }, + { + x: 163.8, + y: 67.3, + }, + { + x: 174, + y: 65.6, + }, + { + x: 175.3, + y: 71.8, + }, + { + x: 193.5, + y: 80.7, + }, + { + x: 186.5, + y: 72.6, + }, + { + x: 187.2, + y: 78.8, + }, + { + x: 181.5, + y: 74.8, + }, + { + x: 184, + y: 86.4, + }, + { + x: 184.5, + y: 78.4, + }, + { + x: 175, + y: 62, + }, + { + x: 184, + y: 81.6, + }, + { + x: 180, + y: 76.6, + }, + { + x: 177.8, + y: 83.6, + }, + { + x: 192, + y: 90, + }, + { + x: 176, + y: 74.6, + }, + { + x: 174, + y: 71, + }, + { + x: 184, + y: 79.6, + }, + { + x: 192.7, + y: 93.8, + }, + { + x: 171.5, + y: 70, + }, + { + x: 173, + y: 72.4, + }, + { + x: 176, + y: 85.9, + }, + { + x: 176, + y: 78.8, + }, + { + x: 180.5, + y: 77.8, + }, + { + x: 172.7, + y: 66.2, + }, + { + x: 176, + y: 86.4, + }, + { + x: 173.5, + y: 81.8, + }, + { + x: 178, + y: 89.6, + }, + { + x: 180.3, + y: 82.8, + }, + { + x: 180.3, + y: 76.4, + }, + { + x: 164.5, + y: 63.2, + }, + { + x: 173, + y: 60.9, + }, + { + x: 183.5, + y: 74.8, + }, + { + x: 175.5, + y: 70, + }, + { + x: 188, + y: 72.4, + }, + { + x: 189.2, + y: 84.1, + }, + { + x: 172.8, + y: 69.1, + }, + { + x: 170, + y: 59.5, + }, + { + x: 182, + y: 67.2, + }, + { + x: 170, + y: 61.3, + }, + { + x: 177.8, + y: 68.6, + }, + { + x: 184.2, + y: 80.1, + }, + { + x: 186.7, + y: 87.8, + }, + { + x: 171.4, + y: 84.7, + }, + { + x: 172.7, + y: 73.4, + }, + { + x: 175.3, + y: 72.1, + }, + { + x: 180.3, + y: 82.6, + }, + { + x: 182.9, + y: 88.7, + }, + { + x: 188, + y: 84.1, + }, + { + x: 177.2, + y: 94.1, + }, + { + x: 172.1, + y: 74.9, + }, + { + x: 167, + y: 59.1, + }, + { + x: 169.5, + y: 75.6, + }, + { + x: 174, + y: 86.2, + }, + { + x: 172.7, + y: 75.3, + }, + { + x: 182.2, + y: 87.1, + }, + { + x: 164.1, + y: 55.2, + }, + { + x: 163, + y: 57, + }, + { + x: 171.5, + y: 61.4, + }, + { + x: 184.2, + y: 76.8, + }, + { + x: 174, + y: 86.8, + }, + { + x: 174, + y: 72.2, + }, + { + x: 177, + y: 71.6, + }, + { + x: 186, + y: 84.8, + }, + { + x: 167, + y: 68.2, + }, + { + x: 171.8, + y: 66.1, + }, + { + x: 182, + y: 72, + }, + { + x: 167, + y: 64.6, + }, + { + x: 177.8, + y: 74.8, + }, + { + x: 164.5, + y: 70, + }, + { + x: 192, + y: 101.6, + }, + { + x: 175.5, + y: 63.2, + }, + { + x: 171.2, + y: 79.1, + }, + { + x: 181.6, + y: 78.9, + }, + { + x: 167.4, + y: 67.7, + }, + { + x: 181.1, + y: 66, + }, + { + x: 177, + y: 68.2, + }, + { + x: 174.5, + y: 63.9, + }, + { + x: 177.5, + y: 72, + }, + { + x: 170.5, + y: 56.8, + }, + { + x: 182.4, + y: 74.5, + }, + { + x: 197.1, + y: 90.9, + }, + { + x: 180.1, + y: 93, + }, + { + x: 175.5, + y: 80.9, + }, + { + x: 180.6, + y: 72.7, + }, + { + x: 184.4, + y: 68, + }, + { + x: 175.5, + y: 70.9, + }, + { + x: 180.6, + y: 72.5, + }, + { + x: 177, + y: 72.5, + }, + { + x: 177.1, + y: 83.4, + }, + { + x: 181.6, + y: 75.5, + }, + { + x: 176.5, + y: 73, + }, + { + x: 175, + y: 70.2, + }, + { + x: 174, + y: 73.4, + }, + { + x: 165.1, + y: 70.5, + }, + { + x: 177, + y: 68.9, + }, + { + x: 192, + y: 102.3, + }, + { + x: 176.5, + y: 68.4, + }, + { + x: 169.4, + y: 65.9, + }, + { + x: 182.1, + y: 75.7, + }, + { + x: 179.8, + y: 84.5, + }, + { + x: 175.3, + y: 87.7, + }, + { + x: 184.9, + y: 86.4, + }, + { + x: 177.3, + y: 73.2, + }, + { + x: 167.4, + y: 53.9, + }, + { + x: 178.1, + y: 72, + }, + { + x: 168.9, + y: 55.5, + }, + { + x: 157.2, + y: 58.4, + }, + { + x: 180.3, + y: 83.2, + }, + { + x: 170.2, + y: 72.7, + }, + { + x: 177.8, + y: 64.1, + }, + { + x: 172.7, + y: 72.3, + }, + { + x: 165.1, + y: 65, + }, + { + x: 186.7, + y: 86.4, + }, + { + x: 165.1, + y: 65, + }, + { + x: 174, + y: 88.6, + }, + { + x: 175.3, + y: 84.1, + }, + { + x: 185.4, + y: 66.8, + }, + { + x: 177.8, + y: 75.5, + }, + { + x: 180.3, + y: 93.2, + }, + { + x: 180.3, + y: 82.7, + }, + { + x: 177.8, + y: 58, + }, + { + x: 177.8, + y: 79.5, + }, + { + x: 177.8, + y: 78.6, + }, + { + x: 177.8, + y: 71.8, + }, + { + x: 177.8, + y: 116.4, + }, + { + x: 163.8, + y: 72.2, + }, + { + x: 188, + y: 83.6, + }, + { + x: 198.1, + y: 85.5, + }, + { + x: 175.3, + y: 90.9, + }, + { + x: 166.4, + y: 85.9, + }, + { + x: 190.5, + y: 89.1, + }, + { + x: 166.4, + y: 75, + }, + { + x: 177.8, + y: 77.7, + }, + { + x: 179.7, + y: 86.4, + }, + { + x: 172.7, + y: 90.9, + }, + { + x: 190.5, + y: 73.6, + }, + { + x: 185.4, + y: 76.4, + }, + { + x: 168.9, + y: 69.1, + }, + { + x: 167.6, + y: 84.5, + }, + { + x: 175.3, + y: 64.5, + }, + { + x: 170.2, + y: 69.1, + }, + { + x: 190.5, + y: 108.6, + }, + { + x: 177.8, + y: 86.4, + }, + { + x: 190.5, + y: 80.9, + }, + { + x: 177.8, + y: 87.7, + }, + { + x: 184.2, + y: 94.5, + }, + { + x: 176.5, + y: 80.2, + }, + { + x: 177.8, + y: 72, + }, + { + x: 180.3, + y: 71.4, + }, + { + x: 171.4, + y: 72.7, + }, + { + x: 172.7, + y: 84.1, + }, + { + x: 172.7, + y: 76.8, + }, + { + x: 177.8, + y: 63.6, + }, + { + x: 177.8, + y: 80.9, + }, + { + x: 182.9, + y: 80.9, + }, + { + x: 170.2, + y: 85.5, + }, + { + x: 167.6, + y: 68.6, + }, + { + x: 175.3, + y: 67.7, + }, + { + x: 165.1, + y: 66.4, + }, + { + x: 185.4, + y: 102.3, + }, + { + x: 181.6, + y: 70.5, + }, + { + x: 172.7, + y: 95.9, + }, + { + x: 190.5, + y: 84.1, + }, + { + x: 179.1, + y: 87.3, + }, + { + x: 175.3, + y: 71.8, + }, + { + x: 170.2, + y: 65.9, + }, + { + x: 193, + y: 95.9, + }, + { + x: 171.4, + y: 91.4, + }, + { + x: 177.8, + y: 81.8, + }, + { + x: 177.8, + y: 96.8, + }, + { + x: 167.6, + y: 69.1, + }, + { + x: 167.6, + y: 82.7, + }, + { + x: 180.3, + y: 75.5, + }, + { + x: 182.9, + y: 79.5, + }, + { + x: 176.5, + y: 73.6, + }, + { + x: 186.7, + y: 91.8, + }, + { + x: 188, + y: 84.1, + }, + { + x: 188, + y: 85.9, + }, + { + x: 177.8, + y: 81.8, + }, + { + x: 174, + y: 82.5, + }, + { + x: 177.8, + y: 80.5, + }, + { + x: 171.4, + y: 70, + }, + { + x: 185.4, + y: 81.8, + }, + { + x: 185.4, + y: 84.1, + }, + { + x: 188, + y: 90.5, + }, + { + x: 188, + y: 91.4, + }, + { + x: 182.9, + y: 89.1, + }, + { + x: 176.5, + y: 85, + }, + { + x: 175.3, + y: 69.1, + }, + { + x: 175.3, + y: 73.6, + }, + { + x: 188, + y: 80.5, + }, + { + x: 188, + y: 82.7, + }, + { + x: 175.3, + y: 86.4, + }, + { + x: 170.5, + y: 67.7, + }, + { + x: 179.1, + y: 92.7, + }, + { + x: 177.8, + y: 93.6, + }, + { + x: 175.3, + y: 70.9, + }, + { + x: 182.9, + y: 75, + }, + { + x: 170.8, + y: 93.2, + }, + { + x: 188, + y: 93.2, + }, + { + x: 180.3, + y: 77.7, + }, + { + x: 177.8, + y: 61.4, + }, + { + x: 185.4, + y: 94.1, + }, + { + x: 168.9, + y: 75, + }, + { + x: 185.4, + y: 83.6, + }, + { + x: 180.3, + y: 85.5, + }, + { + x: 174, + y: 73.9, + }, + { + x: 167.6, + y: 66.8, + }, + { + x: 182.9, + y: 87.3, + }, + { + x: 160, + y: 72.3, + }, + { + x: 180.3, + y: 88.6, + }, + { + x: 167.6, + y: 75.5, + }, + { + x: 186.7, + y: 101.4, + }, + { + x: 175.3, + y: 91.1, + }, + { + x: 175.3, + y: 67.3, + }, + { + x: 175.9, + y: 77.7, + }, + { + x: 175.3, + y: 81.8, + }, + { + x: 179.1, + y: 75.5, + }, + { + x: 181.6, + y: 84.5, + }, + { + x: 177.8, + y: 76.6, + }, + { + x: 182.9, + y: 85, + }, + { + x: 177.8, + y: 102.5, + }, + { + x: 184.2, + y: 77.3, + }, + { + x: 179.1, + y: 71.8, + }, + { + x: 176.5, + y: 87.9, + }, + { + x: 188, + y: 94.3, + }, + { + x: 174, + y: 70.9, + }, + { + x: 167.6, + y: 64.5, + }, + { + x: 170.2, + y: 77.3, + }, + { + x: 167.6, + y: 72.3, + }, + { + x: 188, + y: 87.3, + }, + { + x: 174, + y: 80, + }, + { + x: 176.5, + y: 82.3, + }, + { + x: 180.3, + y: 73.6, + }, + { + x: 167.6, + y: 74.1, + }, + { + x: 188, + y: 85.9, + }, + { + x: 180.3, + y: 73.2, + }, + { + x: 167.6, + y: 76.3, + }, + { + x: 183, + y: 65.9, + }, + { + x: 183, + y: 90.9, + }, + { + x: 179.1, + y: 89.1, + }, + { + x: 170.2, + y: 62.3, + }, + { + x: 177.8, + y: 82.7, + }, + { + x: 179.1, + y: 79.1, + }, + { + x: 190.5, + y: 98.2, + }, + { + x: 177.8, + y: 84.1, + }, + { + x: 180.3, + y: 83.2, + }, + { + x: 180.3, + y: 83.2, + }, +]; + +export default () => { + return ( + + ); +}; diff --git a/src/Scatter/demos/markdown.tsx b/src/Scatter/demos/markdown.tsx new file mode 100644 index 0000000..3aaa2d8 --- /dev/null +++ b/src/Scatter/demos/markdown.tsx @@ -0,0 +1,105 @@ +import { Bubble, type BubbleProps } from '@ant-design/x'; +import { ChartType, GPTVis, Scatter, withChartCode } from '@antv/gpt-vis'; +import React from 'react'; + +const markdownContent = ` +当然了,以下是为你绘制的一个散点图 + +\`\`\`vis-chart +{ + "type": "scatter", + "data": [ + { "x": 22, "y": 3000 }, + { "x": 23, "y": 3200 }, + { "x": 24, "y": 3100 }, + { "x": 25, "y": 3500 }, + { "x": 26, "y": 3300 }, + { "x": 27, "y": 3600 }, + { "x": 28, "y": 4000 }, + { "x": 29, "y": 3900 }, + { "x": 30, "y": 4200 }, + { "x": 31, "y": 4100 }, + { "x": 32, "y": 4500 }, + { "x": 33, "y": 4700 }, + { "x": 34, "y": 4600 }, + { "x": 35, "y": 4800 }, + { "x": 36, "y": 5000 }, + { "x": 37, "y": 5200 }, + { "x": 38, "y": 5100 }, + { "x": 39, "y": 5500 }, + { "x": 40, "y": 5300 }, + { "x": 41, "y": 5700 }, + { "x": 42, "y": 5600 }, + { "x": 43, "y": 5900 }, + { "x": 44, "y": 5800 }, + { "x": 45, "y": 6000 }, + { "x": 46, "y": 6100 }, + { "x": 47, "y": 6500 }, + { "x": 48, "y": 6300 }, + { "x": 49, "y": 6700 }, + { "x": 50, "y": 6400 }, + { "x": 51, "y": 6800 }, + { "x": 52, "y": 6900 }, + { "x": 53, "y": 7100 }, + { "x": 54, "y": 7000 }, + { "x": 55, "y": 7300 }, + { "x": 56, "y": 7200 }, + { "x": 57, "y": 7500 }, + { "x": 58, "y": 7400 }, + { "x": 59, "y": 7600 }, + { "x": 60, "y": 7700 }, + { "x": 61, "y": 7900 }, + { "x": 62, "y": 7800 }, + { "x": 63, "y": 8000 }, + { "x": 64, "y": 8100 }, + { "x": 65, "y": 8200 }, + { "x": 66, "y": 8300 }, + { "x": 67, "y": 8500 }, + { "x": 68, "y": 8400 }, + { "x": 69, "y": 8600 }, + { "x": 70, "y": 8700 } + ], + "axisXTitle": "age", + "axisYTitle": "income" +} +\`\`\` +`; + +const bgStyle = { + display: 'grid', + gridGap: '20px 0', + background: '#f7f7f7', + padding: 20, + borderRadius: 8, +}; + +const CodeComponent = withChartCode({ + components: { [ChartType.Scatter]: Scatter }, + style: { width: 350 }, +}); + +const RenderMarkdown: BubbleProps['messageRender'] = (content) => ( + {content} +); + +export default () => ( +
+ + +
+); diff --git a/src/Scatter/index.md b/src/Scatter/index.md new file mode 100644 index 0000000..e7c57a2 --- /dev/null +++ b/src/Scatter/index.md @@ -0,0 +1,48 @@ +--- +order: 6 +group: + order: 1 + title: 统计图 +demo: { cols: 2 } +--- + +# Scatter 散点图 + +## 代码演示 + +单独使用 + +使用 Markdown 协议 + +## Spec + +```json +{ + "type": "scatter", + "data": [ + { "x": 10, "y": 15 }, + { "x": 20, "y": 25 }, + { "x": 30, "y": 35 }, + { "x": 40, "y": 45 } + ] +} +``` + +## API + +### ScatterProps + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| ---------- | ----------------- | -------- | ------ | -------------------------------------------------------------------------------------------------- | +| data | ScatterDataItem[] | 是 | - | 数据 | +| title | string | 否 | - | 图表的标题 | +| axisXTitle | string | 否 | - | x 轴的标题 | +| axisYTitle | string | 否 | - | y 轴的标题 | +| ... | - | - | - | 更多属性,详见 [Ant Design Charts ](https://ant-design-charts.antgroup.com/options/plots/overview) | + +### ScatterDataItem + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| ---- | ------ | -------- | ------ | ---------------- | +| x | number | 是 | - | X 轴上的数值变量 | +| y | number | 是 | - | Y 轴上的数值变量 | diff --git a/src/Scatter/index.tsx b/src/Scatter/index.tsx new file mode 100644 index 0000000..02aecfc --- /dev/null +++ b/src/Scatter/index.tsx @@ -0,0 +1,38 @@ +import type { ScatterConfig } from '@ant-design/plots'; +import { Scatter as ADCScatter } from '@ant-design/plots'; +import { get } from 'lodash'; +import React from 'react'; +import { usePlotConfig } from '../ConfigProvider/hooks'; +import type { BasePlotProps } from '../types'; + +type ScatterDataItem = { + x: number; + y: number; + [key: string]: string | number; +}; + +export type ScatterProps = BasePlotProps & Partial; + +const defaultConfig = (props: ScatterConfig): ScatterConfig => { + const { data, xField = 'x', yField = 'y' } = props; + const axisXTitle = get(props, 'axis.x.title'); + const axisYTitle = get(props, 'axis.y.title'); + + return { + data, + xField, + yField, + tooltip: [ + { channel: 'x', name: axisXTitle || 'x' }, + { channel: 'y', name: axisYTitle || 'y' }, + ], + }; +}; + +const Scatter = (props: ScatterProps) => { + const config = usePlotConfig('Scatter', defaultConfig, props); + + return ; +}; + +export default Scatter; diff --git a/src/Text/VisText.tsx b/src/Text/VisText.tsx new file mode 100644 index 0000000..f534f6f --- /dev/null +++ b/src/Text/VisText.tsx @@ -0,0 +1,44 @@ +import { Tooltip, Typography } from 'antd'; +import { pick, toString } from 'lodash'; +import React from 'react'; +import { useComponentGlobalConfig } from '../ConfigProvider/hooks'; +import { type TextConfig, STATICS_KEY } from './config'; +import type { VisTextProps } from './types'; + +const { Text } = Typography; + +function renderPrefixSuffix( + symbol: string, + staticsConfig: TextConfig[typeof STATICS_KEY], + props: VisTextProps, +) { + if (!symbol) return null; + const Component = staticsConfig?.[symbol]; + if (Component) return ; + return symbol; +} + +const VisText = (props: VisTextProps) => { + const { children, className, style, type, origin } = props; + const textConfig = useComponentGlobalConfig('VisText'); + const encoding = type ? textConfig?.[type] : {}; + const staticsConfig = textConfig?.[STATICS_KEY]; + return ( + // TODO @羽熙 暂时简单处理 tooltip 直接显示 origin,后续可以根据 origin 类型分类处理 + + + {renderPrefixSuffix(encoding?.prefix, staticsConfig, props)} + {children} + {renderPrefixSuffix(encoding?.suffix, staticsConfig, props)} + + + ); +}; + +export default VisText; diff --git a/src/Text/config.tsx b/src/Text/config.tsx new file mode 100644 index 0000000..d801872 --- /dev/null +++ b/src/Text/config.tsx @@ -0,0 +1,106 @@ +import type { CSSProperties } from 'react'; +import { TEXT_THEME } from './constants'; +import { ArrowDown, ArrowUp } from './custom-icons'; +import { ProportionChart, SingleLineChart } from './mini-charts'; +import type { VisTextProps } from './types'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const INNER_ENTITY_TYPES = [ + 'metric_name', // 指标名 + 'metric_value', // 主指标值 + 'other_metric_value', // 其他指标值 + 'time_desc', // 时间描述 + 'dim_value', // 维值 + 'trend_desc', // 趋势描述 + 'delta_value', // 变化值(非正非负) + 'ratio_value', // 变化率(非正非负) + 'delta_value_pos', // 变化值正 + 'delta_value_neg', // 变化值负 + 'ratio_value_pos', // 变化率正 + 'ratio_value_neg', // 变化率负 + 'proportion', // 占比 + 'contribute_ratio', // 贡献度 +] as const; + +export type InnerEntityType = (typeof INNER_ENTITY_TYPES)[number]; + +export type TextEntityType = InnerEntityType | string; + +export type TextEncoding = Partial< + Pick & { + /** 前缀 */ + prefix: string; + /** 后缀 */ + suffix: string; + + // TODO @羽熙 支持更多文本映射通道,eg underline 等 + } +>; + +export const STATICS_KEY = '__statics' as const; + +/** 文本组件配置信息 */ +export type TextConfig = + // 实体短语类型映射 + Record & { + // 静态组件映射, 如 icon 和 mimi-chart + [STATICS_KEY]: Record>; + }; + +export const DEFAULT_VIS_TEXT_CONFIG: TextConfig = { + [STATICS_KEY]: { + 'icon:arrow-up': ArrowUp, + 'icon:arrow-down': ArrowDown, + 'mini-chart:proportion': ProportionChart, + // TODO @羽熙 当前仅支持单折线图,之后可以考虑支持多折线图,在组件内实现分支判断 + 'mini-chart:line': SingleLineChart, + }, + metric_name: { + color: TEXT_THEME.black88, + fontWeight: 500, + }, + metric_value: { + color: TEXT_THEME.primaryColor, + }, + other_metric_value: { + color: TEXT_THEME.black65, + }, + delta_value: { + color: TEXT_THEME.black65, + }, + ratio_value: { + color: TEXT_THEME.black65, + }, + delta_value_pos: { + color: TEXT_THEME.posColor, + prefix: '+', + }, + delta_value_neg: { + color: TEXT_THEME.negColor, + prefix: '-', + }, + ratio_value_pos: { + color: TEXT_THEME.posColor, + prefix: 'icon:arrow-up', + }, + ratio_value_neg: { + color: TEXT_THEME.negColor, + prefix: 'icon:arrow-down', + }, + contribute_ratio: { + color: TEXT_THEME.statisticsInsightColor, + }, + trend_desc: { + color: TEXT_THEME.statisticsInsightColor, + suffix: 'mini-chart:line', + }, + dim_value: { + color: TEXT_THEME.black88, + }, + time_desc: { + color: TEXT_THEME.black88, + }, + proportion: { + suffix: 'mini-chart:proportion', + }, +}; diff --git a/src/Text/constants.ts b/src/Text/constants.ts new file mode 100644 index 0000000..ed7cd4f --- /dev/null +++ b/src/Text/constants.ts @@ -0,0 +1,9 @@ +export const TEXT_THEME = { + fontSizeBase: 14, + primaryColor: '#1677FF', + black88: 'rgba(0, 0, 0, 0.88)', + black65: 'rgba(0, 0, 0, 0.65)', + posColor: '#FA541C', + negColor: '#13A8A8', + statisticsInsightColor: '#1F0352', +}; diff --git a/src/Text/custom-icons/index.tsx b/src/Text/custom-icons/index.tsx new file mode 100644 index 0000000..31efd5f --- /dev/null +++ b/src/Text/custom-icons/index.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +const MARGIN_RIGHT = 2; +export const ArrowUp = () => ( + + + + + +); +export const ArrowDown = () => ( + + + + + +); diff --git a/src/Text/demos/common.tsx b/src/Text/demos/common.tsx new file mode 100644 index 0000000..847b714 --- /dev/null +++ b/src/Text/demos/common.tsx @@ -0,0 +1,50 @@ +import { VisText } from '@antv/gpt-vis'; +import { Descriptions, Space } from 'antd'; +import React from 'react'; + +export default () => { + return ( + + + DAU + + + + 90.1w + + + + 30% + + + + 100.33 + 100.33 + + + + + 30% + 30% + + + + 20% + + + + 趋势上涨 + + + + 杭州 + + + 2021-10-11 + + + 30% + + + ); +}; diff --git a/src/Text/demos/custom-markdown.tsx b/src/Text/demos/custom-markdown.tsx new file mode 100644 index 0000000..aa8ad63 --- /dev/null +++ b/src/Text/demos/custom-markdown.tsx @@ -0,0 +1,64 @@ +import { CaretDownOutlined, CaretUpOutlined } from '@ant-design/icons'; +import { Bubble, type BubbleProps } from '@ant-design/x'; +import { ConfigProvider, GPTVis, VisText } from '@antv/gpt-vis'; +import React from 'react'; + +const markdownContent = ` +DAU1.23亿,环比昨日 80万(2.3%),建议保持关注。 +`; + +const bgStyle = { + display: 'grid', + gridGap: '20px 0', + background: '#f7f7f7', + padding: 20, + borderRadius: 8, +}; + +const RenderMarkdown: BubbleProps['messageRender'] = (content) => ( + {content} +); + +export default () => ( + +
+ +
+
+); diff --git a/src/Text/demos/markdown.tsx b/src/Text/demos/markdown.tsx new file mode 100644 index 0000000..6718e1f --- /dev/null +++ b/src/Text/demos/markdown.tsx @@ -0,0 +1,47 @@ +import { Bubble, type BubbleProps } from '@ant-design/x'; +import { GPTVis, VisText } from '@antv/gpt-vis'; +import React from 'react'; + +const markdownContent = ` +### 决策数量: +本月共产生决策数量2,783个,环比增长15.2%,同比增长23.5%。其中,营销部门贡献最多,占比38.7%;其次是产品部门,占比27.3%高优先级决策占比56.2%,较上月上升3.8个百分点。决策数量呈现稳定上升趋势,预计下月将突破3,000大关。 + +### 决策准确率: +本月整体决策准确率87.6%,环比下降1.2个百分点,但仍高于年度目标2.6%财务部门表现最佳,准确率达94.3%人力资源部门表现欠佳,准确率为76.8%,建议加强培训。数据驱动型决策的准确率(91.2%)显著高于经验驱动型决策82.4%),凸显了数据分析的重要性。 + +### 决策执行率: +本月决策执行率82.3%,环比提升5.7个百分点,创下近6个月新高。其中,高优先级决策执行率达95.6%低优先级决策执行率仅为68.7%技术部门的执行率最高,达91.2%市场部门最低,为74.5%。建议关注市场部门的决策执行障碍,提供必要支持。 + +### 决策收益: +本月决策收益带来的总收益为1,275万元,同比增长31.8%,超出预期18.6%产品决策贡献最大,占总收益的42.3%;其次是营销决策,占27.6%高风险决策的平均收益(87.3万元/个)显著高于低风险决策23.1万元/个)。建议在风险可控的前提下,适当增加高收益潜力的决策比例。 + +### 数据质量: +本月数据质量评分为88.5分(满分100分),环比提升提升2.3分。数据完整性93.2分)和及时性91.7分)表现优异,而准确性84.6分)和一致性82.5分)仍有提升空间。财务数据质量最高,达95.3分;用户行为数据质量最低,为81.2分。建议优化用户行为数据采集流程,提高数据准确性一致性。 + +`; + +const bgStyle = { + display: 'grid', + gridGap: '20px 0', + background: '#f7f7f7', + padding: 20, + borderRadius: 8, +}; + +const RenderMarkdown: BubbleProps['messageRender'] = (content) => ( + {content} +); + +export default () => ( +
+ +
+); diff --git a/src/Text/demos/playground-dataset-check.tsx b/src/Text/demos/playground-dataset-check.tsx new file mode 100644 index 0000000..a01bbc7 --- /dev/null +++ b/src/Text/demos/playground-dataset-check.tsx @@ -0,0 +1,177 @@ +import { UserOutlined } from '@ant-design/icons'; +import { Bubble, type BubbleProps } from '@ant-design/x'; +import { ConfigProvider, GPTVis, VisText } from '@antv/gpt-vis'; +import type { UploadProps } from 'antd'; +import { Button, message, Row, Space, Upload, type GetProp } from 'antd'; +import type { CSSProperties } from 'react'; +import React, { useState } from 'react'; + +const presetDataset = [ + { + prompt: '本季度销售额达到3.45亿,环比增长500万(+1.5%),主要得益于线上渠道的持续增长。', + answer: + '本季度销售额 达到 3.45亿,环比增长 500万1.5%),主要得益于 线上渠道 的持续增长。', + }, + { + prompt: '用户活跃度达到800万,较上月提高80万(+11%),用户留存率保持在90%。', + answer: + '用户活跃度 达到 800万,较上月提高 80万11%),用户留存率 保持在 90%。', + }, + { + prompt: '新用户注册数达到50万,环比增长20万(+66.7%),主要来自社交媒体的推广效果。', + answer: + '新用户注册数 达到 50万,环比增长 20万66.7%),主要来自 社交媒体 的推广效果。', + }, + { + prompt: '市场份额达到15%,相比去年提升3个百分点,主要原因是产品的质量提升和服务的优化。', + answer: + '市场份额 达到 15%,相比去年提升 3个百分点,主要原因是 产品 的质量提升和 服务 的优化。', + }, + { + prompt: '本月广告收入达到1.2亿,环比下降200万(-1.6%),受到市场竞争加剧的影响。', + answer: + '广告收入 达到 1.2亿,环比下降 200万1.6%),受到 市场竞争 加剧的影响。', + }, + { + prompt: '客户满意度评分提升至88分,较上季度增加了5分,反映了服务质量的显著改善。', + answer: + '客户满意度评分 提升至 88分,较上季度增加了 5分,反映了 服务质量 的显著改善。', + }, + { + prompt: '移动端访问量达到900万,环比增长150万(+20%),显示出用户对移动体验的偏好。', + answer: + '移动端访问量 达到 900万,环比增长 150万20%),显示出用户对 移动体验 的偏好。', + }, + { + prompt: '在线教育平台的课程完成率达到了75%,较上月提升了10个百分点,体现了用户学习积极性。', + answer: + '课程完成率 达到了 75%,较上月提升了 10个百分点,体现了 用户 学习积极性。', + }, + { + prompt: '电商平台的退货率降低至5%,相比去年下降了2个百分点,优化了客户购物体验。', + answer: + '退货率 降低至 5%,相比去年下降了 2个百分点,优化了 客户购物体验。', + }, + { + prompt: '年度利润达到1.5亿,较去年增长3000万(+25%),主要由于成本控制措施的实施。', + answer: + '年度利润 达到 1.5亿,较去年增长 3000万25%),主要由于 成本控制措施 的实施。', + }, +]; + +const bgStyle: CSSProperties = { + display: 'grid', + gridGap: '20px 0', + background: '#f7f7f7', + padding: 20, + borderRadius: 8, + maxHeight: 800, + overflowY: 'auto', +}; + +const RenderMarkdown: BubbleProps['messageRender'] = (content) => ( + {content} +); + +const roles: GetProp = { + ai: { + placement: 'start', + avatar: { + src: 'https://mdn.alipayobjects.com/huamei_je4oko/afts/img/A*6LRBT7rjOkQAAAAAAAAAAAAADsZ-AQ/original', + }, + typing: { step: 5, interval: 20 }, + messageRender: RenderMarkdown, + style: { + maxWidth: '80%', + }, + }, + user: { + placement: 'end', + avatar: { icon: , style: { background: '#87d068' } }, + }, +}; + +export default () => { + const [dataset, setDataset] = useState(presetDataset); + + const beforeUpload: UploadProps['beforeUpload'] = (file) => { + const isJsonl = file.type === 'application/json' || file.name.endsWith('.jsonl'); + + if (!isJsonl) { + message.error('You can only upload JSONL files!'); + } + + return isJsonl; + }; + + const handleUpload: UploadProps['onChange'] = (rcFile) => { + const file = rcFile.fileList[0]?.originFileObj; + const reader = new FileReader(); + reader.onload = (e) => { + const content = e.target?.result; + try { + setDataset(JSON.parse(content as string)); + } catch (e) { + message.error(`${e}`); + } + }; + if (file) reader.readAsText(file); + return false; + }; + + const handleClear = () => { + setDataset([]); + }; + + return ( + <> + + + + + + + + +
+ + { + return [ + { + key: i + 'prompt', + role: 'user', + content: prompt, + }, + { + key: i + 'answer', + role: 'ai', + content: answer, + }, + ]; + }) + .flat()} + /> + +
+ + ); +}; diff --git a/src/Text/index.md b/src/Text/index.md new file mode 100644 index 0000000..e96d599 --- /dev/null +++ b/src/Text/index.md @@ -0,0 +1,65 @@ +--- +order: 1 +group: + order: 3 + title: 文本 +--- + +# VisText 文本 + +## 代码演示 + +### 单独使用 + + + +### 使用 Markdown 协议 + + + +### 使用 ConfigProvider 全局配置,个性化定制 + +- 修改内置 entityType 的 encoding 样式,如将‘红涨绿跌’该为‘红跌绿涨’; +- 为自定义 entityType 定义 encoding 样式,如增加‘行动建议’实体类型; + + + +## API + +### VisTextProps + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| -------- | ------ | -------- | ------ | -------------------------------------------------- | +| type | string | 否 | - | 实体类型 | +| children | string | 否 | - | 文本内容 | +| origin | any | 否 | - | 原始数据,比如未经格式化的指标值、占比、趋势详情等 | + +### GlobalConfig.components.VisText + +通过 `ConfigProvider` 全局配置,个性化定制 VisText 组件。 + +```tsx | pure + + +; + +/** 文本组件配置信息 */ +export type TextConfig = + // 实体短语类型映射 + Record & { + // 静态组件映射, 如 icon 和 mimi-chart + __statics: Record>; + }; +``` + +## Playground + +### 用于训练数据集的校验 + +该组件可以用于对文本训练数据集的校验,对比文本增强前后的效果对比。 + + diff --git a/src/Text/index.ts b/src/Text/index.ts new file mode 100644 index 0000000..6476247 --- /dev/null +++ b/src/Text/index.ts @@ -0,0 +1,3 @@ +export { DEFAULT_VIS_TEXT_CONFIG } from './config'; +export type { VisTextProps } from './types'; +export { default as VisText } from './VisText'; diff --git a/src/Text/mini-charts/hooks/getElementFontSize.ts b/src/Text/mini-charts/hooks/getElementFontSize.ts new file mode 100644 index 0000000..0de1a02 --- /dev/null +++ b/src/Text/mini-charts/hooks/getElementFontSize.ts @@ -0,0 +1,35 @@ +import { TEXT_THEME } from '../../constants'; + +function getStyle(ele: Element, style: string): string | undefined { + return window.getComputedStyle + ? // @ts-ignore style as any + window.getComputedStyle(ele, null)[style] + : // @ts-ignore currentStyle for IE + ele?.currentStyle?.[style]; +} + +function isAbsoluteUnitPx(str: string | undefined): boolean | undefined { + return str?.endsWith('px'); +} + +function getPxNumber(str: string): number | undefined { + const removeUnit = str.replace(/px$/, ''); + const resultNumber = Number(removeUnit); + if (!Number.isNaN(resultNumber)) return resultNumber; + return undefined; +} + +export function getElementFontSize(ele: Element, defaultSize = TEXT_THEME.fontSizeBase): number { + const FONT_SIZE = 'font-size'; + const eleFontSizeStr = getStyle(ele, FONT_SIZE); + if (eleFontSizeStr && isAbsoluteUnitPx(eleFontSizeStr)) { + const px = getPxNumber(eleFontSizeStr); + if (px) return px; + } + const bodyFontSizeStr = getStyle(window.document.body, FONT_SIZE); + if (bodyFontSizeStr && isAbsoluteUnitPx(bodyFontSizeStr)) { + const px = getPxNumber(bodyFontSizeStr); + if (px) return px; + } + return defaultSize; +} diff --git a/src/Text/mini-charts/hooks/index.ts b/src/Text/mini-charts/hooks/index.ts new file mode 100644 index 0000000..48c1d90 --- /dev/null +++ b/src/Text/mini-charts/hooks/index.ts @@ -0,0 +1 @@ +export { useSvgWrapper } from './useSvgWrapper'; diff --git a/src/Text/mini-charts/hooks/useSvgWrapper.tsx b/src/Text/mini-charts/hooks/useSvgWrapper.tsx new file mode 100644 index 0000000..fa0f0a9 --- /dev/null +++ b/src/Text/mini-charts/hooks/useSvgWrapper.tsx @@ -0,0 +1,31 @@ +import type { PropsWithChildren } from 'react'; +import React, { useLayoutEffect, useRef, useState } from 'react'; +import { TEXT_THEME } from '../../constants'; +import { getElementFontSize } from './getElementFontSize'; + +type SvgProps = PropsWithChildren>; + +export const useSvgWrapper = () => { + const ele = useRef(null); + const [fontSize, setFontSize] = useState(TEXT_THEME.fontSizeBase); + useLayoutEffect(() => { + if (ele.current) { + setFontSize(getElementFontSize(ele.current, TEXT_THEME.fontSizeBase)); + } + }, []); + const Svg = ({ children, ...otherProps }: SvgProps) => { + return ( + + {children} + + ); + }; + return [Svg, fontSize] as const; +}; diff --git a/src/Text/mini-charts/index.ts b/src/Text/mini-charts/index.ts new file mode 100644 index 0000000..d0994bb --- /dev/null +++ b/src/Text/mini-charts/index.ts @@ -0,0 +1,2 @@ +export * from './line'; +export * from './proportion'; diff --git a/src/Text/mini-charts/line/SingleLineChart.tsx b/src/Text/mini-charts/line/SingleLineChart.tsx new file mode 100644 index 0000000..bc68edf --- /dev/null +++ b/src/Text/mini-charts/line/SingleLineChart.tsx @@ -0,0 +1,41 @@ +import { isArray, isString, size } from 'lodash'; +import React, { useMemo } from 'react'; +import type { VisTextProps } from '../../types'; +import { useSvgWrapper } from '../hooks'; +import { useLineCompute } from './useLineCompute'; + +const LINEAR_FILL_COLOR_ID = 'wsc-line-fill'; + +const LINE_STROKE_COLOR = '#5B8FF9'; + +function getLineChartData(origin: any) { + if (isArray(origin)) return origin; + if (isString(origin)) { + try { + const data = JSON.parse(origin); + if (isArray(data)) return data; + } catch (e) { + console.warn(e, `${origin} is not a valid json string`); + } + } +} + +export const SingleLineChart: React.FC = ({ origin }) => { + const [Svg, fontSize] = useSvgWrapper(); + const data = useMemo(() => getLineChartData(origin), [origin]); + const { width, height, linePath, polygonPath } = useLineCompute(fontSize, data); + return ( + size(data) > 0 && ( + + + + + + + + {linePath && } + {polygonPath && } + + ) + ); +}; diff --git a/src/Text/mini-charts/line/index.ts b/src/Text/mini-charts/line/index.ts new file mode 100644 index 0000000..c570dff --- /dev/null +++ b/src/Text/mini-charts/line/index.ts @@ -0,0 +1 @@ +export { SingleLineChart } from './SingleLineChart'; diff --git a/src/Text/mini-charts/line/scaleLinear.ts b/src/Text/mini-charts/line/scaleLinear.ts new file mode 100644 index 0000000..cdf3d94 --- /dev/null +++ b/src/Text/mini-charts/line/scaleLinear.ts @@ -0,0 +1,14 @@ +export type Domain = [number, number]; +export type Range = [number, number]; + +export type Scale = (n: number) => number; + +export const scaleLinear = + (domain: Domain, range: Range): Scale => + (n) => { + const [d1, d2] = domain; + const [r1, r2] = range; + // Returns the intermediate value when the range is zero. + if (r1 === r2) return (d2 - d1) / 2; + return (n / (r2 - r1)) * (d2 - d1); + }; diff --git a/src/Text/mini-charts/line/useLineCompute.ts b/src/Text/mini-charts/line/useLineCompute.ts new file mode 100644 index 0000000..f52efa2 --- /dev/null +++ b/src/Text/mini-charts/line/useLineCompute.ts @@ -0,0 +1,87 @@ +import { useEffect, useState } from 'react'; +import { TEXT_THEME } from '../../constants'; +import type { Scale } from './scaleLinear'; +import { scaleLinear } from './scaleLinear'; + +// adjust to draw line width +const SCALE_ADJUST = 2; + +class Line { + protected data: number[] = []; + + protected size: number = TEXT_THEME.fontSizeBase; + + protected height: number = this.size; + + protected width: number = this.getWidth(); + + protected xScale: Scale | undefined; + + protected yScale: Scale | undefined; + + protected points: [number, number][] = []; + + constructor(size: number, data: number[] | undefined) { + this.size = size; + if (data) { + this.data = data; + this.compute(); + } + } + + protected getWidth() { + return Math.max(this.size * 2, this.data?.length * 2); + } + + protected compute() { + if (!this.data) return; + this.height = this.size; + this.width = this.getWidth(); + this.xScale = scaleLinear([0, this.width], [0, this.data?.length - 1]); + const [min, max] = [Math.min(...this.data), Math.max(...this.data)]; + this.yScale = scaleLinear([SCALE_ADJUST, this.height - SCALE_ADJUST], [min, max]); + this.points = this.data.map((item, index) => [ + this.xScale!(index), + this.height - this.yScale!(item), + ]); + } + + getLinePath(): null | string { + if (!this.data?.length || !this.xScale || !this.yScale) return null; + const path = this.points.reduce((prev, [x, y], index) => { + if (index === 0) return `M${x} ${y}`; + return `${prev} L ${x} ${y}`; + }, ''); + return path; + } + + getPolygonPath(): null | string { + if (!this.data?.length || !this.xScale || !this.yScale) return null; + const polygonPoints = [...this.points]; + const last = this.points[this.points.length - 1]; + polygonPoints.push([last[0], this.height]); + polygonPoints.push([0, this.height]); + const startPoint = this.points[0]; + polygonPoints.push(startPoint); + + const path = polygonPoints.reduce((prev, [x, y]) => `${prev} ${x},${y}`, ''); + return path; + } + + getContainer() { + return [this.width, this.height]; + } +} + +export const useLineCompute = (size: number, data: undefined | number[]) => { + const [line, setLine] = useState(new Line(size, data)); + useEffect(() => { + setLine(new Line(size, data)); + }, [size, data]); + return { + width: line.getContainer()[0], + height: line.getContainer()[1], + linePath: line.getLinePath(), + polygonPath: line.getPolygonPath(), + }; +}; diff --git a/src/Text/mini-charts/proportion/getArcPath.ts b/src/Text/mini-charts/proportion/getArcPath.ts new file mode 100644 index 0000000..44a271d --- /dev/null +++ b/src/Text/mini-charts/proportion/getArcPath.ts @@ -0,0 +1,24 @@ +/** + * make data between 0 ~ 1 + */ +function normalizeProportion(data: number | undefined) { + if (typeof data !== 'number') return 0; + if (data > 1) return 1; + if (data < 0) return 0; + return data; +} + +export function getArcPath(size: number, data: number) { + const cx = size / 2; + const cy = size / 2; + const r = size / 2; + const angle = normalizeProportion(data) * 2 * Math.PI; + const dx = cx + r * Math.sin(angle); + const dy = cy - r * Math.cos(angle); + const path = ` + M${cx} ${0} + A ${cx} ${cy} 0 ${angle > Math.PI ? 1 : 0} 1 ${dx} ${dy} + L ${cx} ${cy} Z + `; + return path; +} diff --git a/src/Text/mini-charts/proportion/index.tsx b/src/Text/mini-charts/proportion/index.tsx new file mode 100644 index 0000000..a471936 --- /dev/null +++ b/src/Text/mini-charts/proportion/index.tsx @@ -0,0 +1,50 @@ +import { isNaN, isString, toNumber } from 'lodash'; +import React, { useMemo } from 'react'; +import type { VisTextProps } from '../../types'; +import { useSvgWrapper } from '../hooks/useSvgWrapper'; +import { getArcPath } from './getArcPath'; + +const SHADOW_COLOR = '#CDDDFD'; +const FILL_COLOR = '#3471F9'; + +function getProportionNumber( + children: string | string[], // markdown 中传入的 children 文本为 string[] + origin?: any, +): number | undefined { + let result: number | undefined; + const originNumber = toNumber(origin); + if (!isNaN(originNumber)) { + result = originNumber; + } else { + let text; + if (isString(children)) { + text = children; + } else if (Array.isArray(children) && isString(children[0])) { + text = children[0]; + } + if (text?.endsWith?.('%')) { + const percentageStr = text?.replace(/%$/, ''); + const proportionNumber = toNumber(percentageStr); + if (!isNaN(proportionNumber)) result = proportionNumber / 100; + } + } + return result; +} + +export const ProportionChart: React.FC = ({ origin, children }) => { + const data = useMemo(() => getProportionNumber(children, origin), [children, origin]); + const [Svg, fontSize] = useSvgWrapper(); + const r = fontSize / 2; + return ( + data && ( + + + {data >= 1 ? ( + + ) : ( + + )} + + ) + ); +}; diff --git a/src/Text/types.ts b/src/Text/types.ts new file mode 100644 index 0000000..852c467 --- /dev/null +++ b/src/Text/types.ts @@ -0,0 +1,24 @@ +import type { CSSProperties, PropsWithChildren } from 'react'; +import type { TextEntityType } from './config'; + +/** + * the props for the Text + */ +export type VisTextProps = PropsWithChildren<{ + // common props + style?: CSSProperties; + className?: string; + + children: string; // 明确指出 children 是文本 + + /** 类型,包括内置的类型,以及 string 用于用户自行扩展 */ + type?: TextEntityType; + /** + * 原始数值 + * 用于传递各种情况的原始数据值,比如: + * 1. metric_value: 未经过格式化的指标值,原始数据;eg 1.234 + * 2. proportion: 百分比,eg 0.37 + * 3. trend_desc: 趋势值,eg [1, 2, 6, 18, 24, 48] + */ + origin?: any; +}>; diff --git a/src/Treemap/demos/common.tsx b/src/Treemap/demos/common.tsx new file mode 100644 index 0000000..7c1f351 --- /dev/null +++ b/src/Treemap/demos/common.tsx @@ -0,0 +1,29 @@ +import { Treemap } from '@antv/gpt-vis'; +import React from 'react'; + +const data = [ + { name: '分类 1', value: 560 }, + { name: '分类 2', value: 500 }, + { name: '分类 3', value: 150 }, + { name: '分类 4', value: 140 }, + { name: '分类 5', value: 115 }, + { name: '分类 6', value: 95 }, + { name: '分类 7', value: 90 }, + { name: '分类 8', value: 75 }, + { name: '分类 9', value: 98 }, + { name: '分类 10', value: 60 }, + { name: '分类 11', value: 45 }, + { name: '分类 12', value: 40 }, + { name: '分类 13', value: 40 }, + { name: '分类 14', value: 35 }, + { name: '分类 15', value: 40 }, + { name: '分类 16', value: 40 }, + { name: '分类 17', value: 40 }, + { name: '分类 18', value: 30 }, + { name: '分类 19', value: 28 }, + { name: '分类 20', value: 16 }, +]; + +export default () => { + return ; +}; diff --git a/src/Treemap/demos/markdown.tsx b/src/Treemap/demos/markdown.tsx new file mode 100644 index 0000000..79b343f --- /dev/null +++ b/src/Treemap/demos/markdown.tsx @@ -0,0 +1,63 @@ +import { Bubble, type BubbleProps } from '@ant-design/x'; +import { ChartType, GPTVis, Treemap, withChartCode } from '@antv/gpt-vis'; +import React from 'react'; + +const markdownContent = ` +当然了,以下是为你绘制的一个矩阵树图 + +\`\`\`vis-chart +{ + "type": "treemap", + "data": [ + { + "name": "产品A", + "value": 500, + "children": [ + { "name": "子产品A1", "value": 200 }, + { "name": "子产品A2", "value": 300 } + ] + }, + { "name": "产品B", "value": 400 } + ] +} + +\`\`\` +`; + +const bgStyle = { + display: 'grid', + gridGap: '20px 0', + background: '#f7f7f7', + padding: 20, + borderRadius: 8, +}; + +const CodeComponent = withChartCode({ + components: { [ChartType.Treemap]: Treemap }, +}); + +const RenderMarkdown: BubbleProps['messageRender'] = (content) => ( + {content} +); + +export default () => ( +
+ + +
+); diff --git a/src/Treemap/index.md b/src/Treemap/index.md new file mode 100644 index 0000000..436e042 --- /dev/null +++ b/src/Treemap/index.md @@ -0,0 +1,60 @@ +--- +order: 8 +group: + order: 1 + title: 统计图 +demo: { cols: 2 } +--- + +# Treemap 矩阵树图 + +## 代码演示 + +单独使用 + +使用 Markdown 协议 + +## Spec + +```json +{ + "type": "treemap", + "data": [ + { + "name": "<分类名称一>", + "value": <数值>, + "children": [ + { "name": "<子分类名称>", "value": <数值> }, + { "name": "<子分类名称>", "value": <数值> }, + { "name": "<子分类名称>", "value": <数值> } + ] + }, + { + "name": "<分类名称二>", + "value": <数值>, + "children": [ + { "name": "<子分类名称>", "value": <数值> }, + { "name": "<子分类名称>", "value": <数值> } + ] + } + ], +} +``` + +## API + +### TreemapProps + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| ----- | ---------- | -------- | ------ | -------------------------------------------------------------------------------------------------- | +| data | TreeNode[] | 是 | - | 数据 | +| title | string | 否 | - | 图表的标题 | +| ... | - | - | - | 更多属性,详见 [Ant Design Charts ](https://ant-design-charts.antgroup.com/options/plots/overview) | + +### TreeNode + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| -------- | ---------- | -------- | ------ | -------------- | +| name | string | 是 | - | 分类名称 | +| value | number | 是 | - | 分类的数值大小 | +| children | TreeNode[] | 否 | - | 子分类列表 | diff --git a/src/Treemap/index.tsx b/src/Treemap/index.tsx new file mode 100644 index 0000000..dc3244b --- /dev/null +++ b/src/Treemap/index.tsx @@ -0,0 +1,55 @@ +import type { TreemapConfig } from '@ant-design/plots'; +import { Treemap as ADCTreemap } from '@ant-design/plots'; +import React, { useMemo } from 'react'; +import { usePlotConfig } from '../ConfigProvider/hooks'; +import type { BasePlotProps } from '../types'; + +type TreeNode = { + name: string; + value: number; + children?: TreeNode[]; +}; +// binNumber和binWidth为互斥属性,选其一即可 +type ADCTreemapConfig = TreemapConfig & { + valueField: string; +}; +export type TreemapProps = BasePlotProps & Partial; + +const defaultConfig = (props: ADCTreemapConfig): TreemapConfig => { + const { valueField = 'value' } = props; + return { + legend: false, + layout: { + tile: 'treemapBinary', + paddingInner: 3, + }, + valueField, + tooltip: { + items: [ + (d) => { + return { + name: d.data?.name, + value: d[valueField], + }; + }, + ], + }, + style: { fillOpacity: 0.8, labelFontSize: 12 }, + }; +}; +const transform = (data: TreeNode[]) => { + return { + name: 'root', + children: data, + }; +}; + +const Treemap = (props: TreemapProps) => { + const config = usePlotConfig('Treemap', defaultConfig, props); + const { data, ...others } = config; + const transformData = useMemo(() => transform(data), [data]); + + return ; +}; + +export default Treemap; diff --git a/src/WordCloud/index.md b/src/WordCloud/index.md new file mode 100644 index 0000000..484ae0b --- /dev/null +++ b/src/WordCloud/index.md @@ -0,0 +1,232 @@ +--- +order: 7 +group: + order: 1 + title: 统计图 +--- + +# WordCloud 词云图 + +词云图通过把文本中常出现的关键词放大,让用户更容易找到重要信息。 + +## 代码演示 + +### 单独使用 + +```jsx +import React from 'react'; +import { WordCloud } from '@antv/gpt-vis'; + +const data = [ + { value: 11.3865516372, text: '物质' }, + { value: 7.75434839431, text: '万物' }, + { value: 9.29550746599, text: '感官' }, + { value: 6.89126871927, text: '事物' }, + { value: 11.739204307083542, text: '会变' }, + { value: 9.29550746599, text: '感官' }, + { value: 8.70772080109, text: '认知' }, + { value: 13.68728134056, text: '元素' }, + { value: 8.29487558568, text: '世间' }, + { value: 8.77077893705, text: '肉眼' }, + { value: 8.10461990121, text: '大自然' }, + { value: 7.69410172525, text: '粒子' }, + { value: 7.65457088649, text: '分割' }, + { value: 21.6192703972, text: '积木' }, + { value: 10.7226238216, text: '永恒不变' }, + { value: 7.14957618304, text: '原子' }, + { value: 7.95787827685, text: '尺度' }, + { value: 7.36109169636, text: '衡量' }, + { value: 11.7034530746, text: '明辨是非' }, + { value: 9.14102377386, text: '存在' }, + { value: 6.90950076485, text: '理性' }, + { value: 12.06102341102, text: '模式' }, + { value: 11.739204307083542, text: '理型' }, + { value: 11.3865516372, text: '物质' }, + { value: 9.54656840164, text: '东西' }, + { value: 8.73505881114, text: '世界' }, + { value: 34.45634359635, text: '事物' }, + { value: 27.886522397969998, text: '感官' }, + { value: 6.47412857958, text: '痛苦' }, + { value: 6.06866347147, text: '避免' }, + { value: 5.02315619812, text: '形式' }, + { value: 4.80149232937, text: '方式' }, + { value: 21.209681572, text: '万事万物' }, + { value: 7.48068272383, text: '上帝' }, + { value: 7.37711534583, text: '一体' }, + { value: 7.36834335975, text: '宇宙' }, + { value: 9.17328983326, text: '赦免' }, + { value: 8.04274449749, text: '拯救' }, + { value: 7.48068272383, text: '上帝' }, + { value: 14.96136544766, text: '上帝' }, + { value: 7.95787827685, text: '世人' }, + { value: 14.96136544766, text: '上帝' }, + { value: 11.67082488616, text: '创造' }, + { value: 9.80633308975, text: '虚空' }, + { value: 8.73505881114, text: '世界' }, + { value: 16.57509909118, text: '启示' }, + { value: 14.51638170122, text: '信仰' }, + { value: 13.8190015297, text: '理性' }, + { value: 10.5684731418, text: '观感' }, + { value: 12.57964972316, text: '地球' }, + { value: 12.45583951066, text: '太阳' }, + { value: 11.39201164604, text: '运行' }, + { value: 14.35128801962, text: '物体' }, + { value: 11.18052531558, text: '状态' }, + { value: 10.0505300503, text: '轴心' }, + { value: 9.56994431169, text: '星球' }, + { value: 9.29550746599, text: '感官' }, + { value: 21.52693202943, text: '物体' }, + { value: 7.36834335975, text: '宇宙' }, + { value: 22.44204817149, text: '上帝' }, + { value: 12.1089181827, text: '主宰世界' }, + { value: 10.8096351986, text: '牵线' }, + { value: 10.6818018271, text: '自然法则' }, + { value: 18.59101493198, text: '感官' }, + { value: 15.51728049278, text: '心灵' }, + { value: 10.1394775363, text: '任何事物' }, + { value: 14.95161725614, text: '哲学家' }, + { value: 13.78796485028, text: '感受' }, + { value: 10.3743171274, text: '天主' }, + { value: 10.2117981979, text: '质料' }, + { value: 9.14102377386, text: '存在' }, + { value: 14.96136544766, text: '上帝' }, + { value: 14.35850390238, text: '头顶' }, + { value: 12.5143832909, text: '因果律' }, + { value: 11.0674643079, text: '根植' }, + { value: 16.20923980242, text: '大自然' }, + { value: 13.10258821671, text: '世界' }, + { value: 11.3865516372, text: '物质' }, + { value: 10.1164880181, text: '无休止' }, + { value: 13.10258821671, text: '世界' }, + { value: 11.23942650284, text: '精神' }, + { value: 10.1394775363, text: '脚踏实地' }, + { value: 26.6063055869, text: '阶段' }, + { value: 13.0408437271, text: '真实' }, + { value: 10.48883682488, text: '是否' }, + { value: 17.0798274558, text: '物质' }, + { value: 11.739204307083542, text: '新的' }, + { value: 11.23942650284, text: '精神' }, + { value: 10.4471578861, text: '改变' }, + { value: 10.21443458184, text: '造成' }, + { value: 16.20923980242, text: '一艘' }, + { value: 15.07529909688, text: '航行' }, + { value: 11.739204307083542, text: '人则' }, + { value: 11.7034530746, text: '物竞天择' }, + { value: 10.5684731418, text: '适者生存' }, + { value: 19.89886786678, text: '潜意识' }, + { value: 12.76848869812, text: '意识' }, +]; +export default () => ; +``` + +### 使用 Markdown 协议 + +```tsx +import { Bubble, type BubbleProps } from '@ant-design/x'; +import { WordCloud, withChartCode, ChartType, GPTVis } from '@antv/gpt-vis'; + +const bgStyle = { + display: 'grid', + gridGap: '20px 0', + background: '#f7f7f7', + padding: 20, + borderRadius: 8, +}; + +const markdownContent = ` + ~~~vis-chart + { + "type": "word-cloud", + "data": [ + { "value": 9, "text": "AntV" }, + { "value": 8, "text": "F2" }, + { "value": 8, "text": "G2" }, + { "value": 8, "text": "G6" }, + { "value": 8, "text": "DataSet" }, + { "value": 8, "text": "墨者学院" }, + { "value": 6, "text": "Analysis" }, + { "value": 6, "text": "Data Mining" }, + { "value": 6, "text": "Data Vis" }, + { "value": 6, "text": "Design" }, + { "value": 6, "text": "Grammar" }, + { "value": 6, "text": "Graphics" }, + { "value": 6, "text": "Graph" }, + { "value": 6, "text": "Hierarchy" }, + { "value": 6, "text": "Labeling" }, + { "value": 6, "text": "Layout" }, + { "value": 6, "text": "Quantitative" }, + { "value": 6, "text": "Relation" }, + { "value": 6, "text": "Statistics" }, + { "value": 6, "text": "可视化" }, + { "value": 6, "text": "数据" }, + { "value": 6, "text": "数据可视化" } + ] + } +~~~`; + +const CodeComponent = withChartCode({ + components: { [ChartType.WordCloud]: WordCloud }, + style: { width: 400 }, +}); + +const RenderMarkdown: BubbleProps['messageRender'] = (content) => ( + {content} +); + +export default () => { + return ( +
+ + +
+ ); +}; +``` + +## Spec + +```json +{ + "type": "word-cloud", + "data": [ + { "value": 11.739204307083542, "text": "水是" }, + { "value": 9.23723855786, "text": "之源" }, + { "value": 7.75434839431, "text": "万物" }, + { "value": 11.3865516372, "text": "物质" }, + { "value": 7.75434839431, "text": "万物" }, + { "value": 5.83541244308, "text": "创造" }, + { "value": 4.27215339948, "text": "形成" } + ] +} +``` + +## API + +### WordCloudProps + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| ----- | ------------------- | -------- | ------ | ---------- | +| data | WordCloudDataItem[] | 是 | - | 数据 | +| title | string | 否 | - | 图表的标题 | + +### WordCloudDataItem + +| 属性 | 类型 | 是否必传 | 默认值 | 说明 | +| ----- | ------ | -------- | ------ | ---- | +| text | string | 是 | - | 文本 | +| value | number | 是 | - | 词频 | diff --git a/src/WordCloud/index.tsx b/src/WordCloud/index.tsx new file mode 100644 index 0000000..28943c9 --- /dev/null +++ b/src/WordCloud/index.tsx @@ -0,0 +1,28 @@ +import type { WordCloudConfig } from '@ant-design/plots'; +import { WordCloud as ADCWordCloud } from '@ant-design/plots'; +import React, { type FC } from 'react'; +import { usePlotConfig } from '../ConfigProvider/hooks'; +import type { BasePlotProps } from '../types'; + +type WordCloudDataItem = { + text: string; + value: number; +}; + +const defaultConfig = { + autoFit: true, + layout: { fontSize: [20, 100] }, + colorField: 'text', + legend: false, + tooltip: false, +}; + +export type WordCloudProps = BasePlotProps & Partial; + +const WordCloud: FC = (props) => { + const config = usePlotConfig('WordCloud', defaultConfig, props); + + return ; +}; + +export default WordCloud; diff --git a/src/components/A/index.tsx b/src/components/A/index.tsx deleted file mode 100644 index e3d1667..0000000 --- a/src/components/A/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { LinkOutlined } from '@ant-design/icons'; -import React from 'react'; - -export type AProps = { - /** - * 链接地址 - */ - href: string; - /** - * - */ - children: string; -}; - -export const A: React.FC = ({ href, children }) => { - console.log(222, children, href); - return ( - - ); -}; diff --git a/src/components/Code/index.tsx b/src/components/Code/index.tsx deleted file mode 100644 index 3bbfb3d..0000000 --- a/src/components/Code/index.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; - -export type CodeProps = { - className: string; - inline: boolean; - style: CSSStyleDeclaration; - children: JSX.Element; -}; - -export const Reference: React.FC = () => { - return
; -}; diff --git a/src/components/index.ts b/src/components/index.ts deleted file mode 100644 index 2c77bbd..0000000 --- a/src/components/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { Components as ComponentsType } from 'react-markdown'; -import { A } from './A'; - -export const Components: ComponentsType = { - a: A, -}; diff --git a/src/constants/index.ts b/src/constants/index.ts new file mode 100644 index 0000000..ec88e86 --- /dev/null +++ b/src/constants/index.ts @@ -0,0 +1,10 @@ +import type { GlobalConfig } from '../types'; + +import { DEFAULT_VIS_TEXT_CONFIG } from '../Text'; + +export const DEFAULT_GLOBAL_CONFIG: GlobalConfig = { + map: { style: 'light' }, + components: { + VisText: DEFAULT_VIS_TEXT_CONFIG, + }, +}; diff --git a/src/core/Conversation/index.tsx b/src/core/Conversation/index.tsx deleted file mode 100644 index 24db827..0000000 --- a/src/core/Conversation/index.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import Markdown, { Options } from 'react-markdown'; -import rehypeRaw from 'rehype-raw'; - -export type ConversationProps = { - /** - * 需要被渲染的 markdown 字符串 - */ - children: string; - /** - * 注册的组件 - */ - components?: Options['components']; - /** - * 注册的 rehype 插件 - */ - // rehypePlugins: Options['rehypePlugins']; -}; - -export const Conversation: React.FC = ({ - children, - components, -}) => { - console.log('components', components, children); - return ( - - {children} - - ); -}; diff --git a/src/core/index.ts b/src/core/index.ts deleted file mode 100644 index cfb9ea9..0000000 --- a/src/core/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Conversation } from './Conversation'; diff --git a/src/export.ts b/src/export.ts new file mode 100644 index 0000000..aeff9d9 --- /dev/null +++ b/src/export.ts @@ -0,0 +1,77 @@ +import { default as Area, type AreaProps } from './Area'; +import { default as Bar, type BarProps } from './Bar'; +import { default as Column, type ColumnProps } from './Column'; +import { default as DualAxes, type DualAxesProps } from './DualAxes'; +import { default as FlowDiagram, type FlowDiagramProps } from './FlowDiagram'; +import { default as HeatMap, type HeatMapProps } from './HeatMap'; +import { default as Histogram, type HistogramProps } from './Histogram'; +import { default as Line, type LineProps } from './Line'; +import { default as MindMap, type MindMapProps } from './MindMap'; +import { default as NetworkGraph, type NetworkGraphProps } from './NetworkGraph'; +import { default as PathMap, type PathMapProps } from './PathMap'; +import { default as Pie, type PieProps } from './Pie'; +import { default as PinMap, type PinMapProps } from './PinMap'; +import { default as Radar, type RadarProps } from './Radar'; +import { default as Scatter, type ScatterProps } from './Scatter'; +import { default as Treemap, type TreemapProps } from './Treemap'; +import { ChartType } from './types'; +import { default as WordCloud, type WordCloudProps } from './WordCloud'; + +export { default as ConfigProvider, type ConfigProviderProps } from './ConfigProvider'; +export { default as IndentedTree, type IndentedTreeProps } from './IndentedTree'; +export { default as Map, type MapProps } from './Map'; +export { default as OrganizationChart, type OrganizationChartProps } from './OrganizationChart'; + +export { + Area, + Bar, + Column, + DualAxes, + FlowDiagram, + HeatMap, + Histogram, + Line, + MindMap, + NetworkGraph, + PathMap, + Pie, + PinMap, + Radar, + Scatter, + Treemap, + WordCloud, + type AreaProps, + type BarProps, + type ColumnProps, + type DualAxesProps, + type FlowDiagramProps, + type HeatMapProps, + type HistogramProps, + type LineProps, + type MindMapProps, + type NetworkGraphProps, + type PathMapProps, + type PieProps, + type PinMapProps, + type RadarProps, + type ScatterProps, + type TreemapProps, + type WordCloudProps, +}; + +export { VisText, type VisTextProps } from './Text'; + +export const DEFAULT_CHART_COMPONENTS: Record> = { + [ChartType.Line]: Line, + [ChartType.Column]: Column, + [ChartType.Pie]: Pie, + [ChartType.Bar]: Bar, + [ChartType.Area]: Area, + [ChartType.Scatter]: Scatter, + [ChartType.PinMap]: PinMap, + [ChartType.PathMap]: PathMap, + [ChartType.HeatMap]: HeatMap, + [ChartType.MindMap]: MindMap, + [ChartType.FlowDiagram]: FlowDiagram, + [ChartType.NetworkGraph]: NetworkGraph, +}; diff --git a/src/global.d.ts b/src/global.d.ts deleted file mode 100644 index 22f0fe6..0000000 --- a/src/global.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -declare namespace JSX { - interface IntrinsicElements { - 'chart-view': React.DetailedHTMLProps< - React.HTMLAttributes, - HTMLElement - >; - 'custom-view': React.DetailedHTMLProps< - React.HTMLAttributes, - HTMLElement - >; - 'references-view': React.DetailedHTMLProps< - React.HTMLAttributes & { - references: any; - }, - HTMLElement - >; - 'summary-view': React.DetailedHTMLProps< - React.HTMLAttributes, - HTMLElement - >; - } -} diff --git a/src/index.css b/src/index.css deleted file mode 100644 index 0ca23c5..0000000 --- a/src/index.css +++ /dev/null @@ -1,7 +0,0 @@ -.markdown p { - margin-block-end: 12px; -} - -.markdown blockquote { - margin-block-end: 12px; -} diff --git a/src/index.ts b/src/index.ts index 192078f..9ca8e80 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,8 @@ -export { Components } from './components'; -export { Conversation } from './core/Conversation'; +export * from './export'; +export { default as version } from './version'; -import './index.css'; +export { withChartCode, withDefaultChartCode } from './ChartCodeRender'; +export type { CodeBlockComponent, WithChartCodeOptions } from './ChartCodeRender/type'; +export { default as GPTVis, type GPTVisProps } from './GPTVis'; + +export * from './types'; diff --git a/src/types/chart.ts b/src/types/chart.ts new file mode 100644 index 0000000..76b1f85 --- /dev/null +++ b/src/types/chart.ts @@ -0,0 +1,77 @@ +import type { LarkMapProps } from '@antv/larkmap'; +import type { CSSProperties, ReactNode } from 'react'; +import type { Map } from './map'; + +export type { LarkMapProps }; + +export enum ChartType { + Pie = 'pie', + Column = 'column', + Line = 'line', + Area = 'area', + Scatter = 'scatter', + Histogram = 'histogram', + Treemap = 'treemap', + Bar = 'bar', + WordCloud = 'word-cloud', + DualAxes = 'dual-axes', + Radar = 'radar', + PinMap = 'pin-map', + PathMap = 'path-map', + HeatMap = 'heat-map', + MindMap = 'mind-map', + NetworkGraph = 'network-graph', + FlowDiagram = 'flow-diagram', + OrganizationChart = 'organization-chart', + IndentedTree = 'indented-tree', + VisText = 'vis-text', +} + +export type Charts = keyof typeof ChartType; + +export interface BaseChartProps { + containerStyle?: CSSProperties; + className?: string; + children?: ReactNode; +} + +export interface BasePlotProps extends BaseChartProps { + data: T[]; + axisXTitle?: string; + axisYTitle?: string; +} + +export interface BaseMapProps extends BaseChartProps, Map { + data: T[]; + // 高德地图密钥 + token?: string; +} + +export interface BaseGraphProps extends BaseChartProps { + data: T; +} + +interface GraphNode { + name: string; +} + +interface GraphEdge { + source: string; + target: string; + name?: string; +} + +export interface GraphData { + nodes: GraphNode[]; + edges: GraphEdge[]; +} + +export interface TreeGraphData { + name: string; + children?: TreeGraphData[]; + [key: string]: any; +} + +export interface GraphProps extends BaseGraphProps {} + +export interface TreeGraphProps extends BaseGraphProps {} diff --git a/src/types/config.ts b/src/types/config.ts new file mode 100644 index 0000000..3c6091a --- /dev/null +++ b/src/types/config.ts @@ -0,0 +1,23 @@ +import type { G2 } from '@ant-design/plots'; +import type { Charts } from './chart'; + +type PlotGlobalConfig = { + theme?: G2.Theme; + [key: string]: any; +}; + +type MapGlobalConfig = { + style?: 'normal' | 'light' | 'dark' | string; + token?: string; + enableZoom?: boolean; + enableRotate?: boolean; + [key: string]: any; +}; + +type ComponentsGlobalConfig = Partial>>; + +export type GlobalConfig = { + plot?: PlotGlobalConfig; + map?: MapGlobalConfig; + components?: ComponentsGlobalConfig; +}; diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..196b998 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,2 @@ +export * from './chart'; +export type { GlobalConfig } from './config'; diff --git a/src/types/map.ts b/src/types/map.ts new file mode 100644 index 0000000..21c0159 --- /dev/null +++ b/src/types/map.ts @@ -0,0 +1,248 @@ +type AsyncCall< + T = { [k: string | number | symbol]: any }, + Success = any, + Fail = any, + Complete = any, +> = ( + params: T & { + success: (params: Success) => void; + fail: (params: Fail) => void; + complete?: (params: Complete) => void; + }, +) => void; + +/** + * 地图组件上下文对象 + * + * @see https://opendocs.alipay.com/mini/api/mapcontext + */ +export interface MapContext { + /** + * 设置地图底图类型 + * + * @see https://opendocs.alipay.com/mini/api/setmaptype + */ + setMapType: AsyncCall<{ mapType: 0 | 1 | 2 | 3 | 4 }>; + /** + * 平移缩放到指定区域 + * + * @see https://opendocs.alipay.com/mini/api/includepoints + */ + includePoints: AsyncCall<{ + points: Array<{ longitude: number; latitude: number }>; + padding?: [number, number, number, number]; + }>; +} + +export interface MapInstance { + getContext: () => MapContext; +} + +/** + * 经纬度坐标点 + */ +export interface LngLat { + longitude: number; + latitude: number; +} + +/** + * 标记点 + * + * 更多属性信息查看 @see https://opendocs.alipay.com/mini/component/map#markers + */ +export interface Marker extends LngLat { + id: string; + iconPath?: string; + width?: number; + height?: number; + alpha?: number; + anchorX?: number; + anchorY?: number; + label?: { + content?: string; + color?: string; + fontSize?: number; + borderRadius?: number; + bgColor?: string; + padding?: number; + }; +} + +/** + * 折线 + * + * 更多属性信息查看 @see https://opendocs.alipay.com/mini/component/map#polyline + */ +export interface Polyline { + points: LngLat[]; + // + width?: number; + color?: string; + dottedLine?: boolean; + // + iconPath?: string; + iconWidth?: number; + colorList?: string[]; +} + +/** + * 面 + * + * @see https://opendocs.alipay.com/mini/component/map?pathHash=c9886630#polygon + */ +export interface Polygon { + points: LngLat[]; + color?: string; + fillColor?: string; + width?: number; +} + +/** + * @see https://opendocs.alipay.com/mini/component/map + */ +export interface Map { + id?: string; + /** + * @title 地图类型 + * @description 底图的样式 + * + * - 0 标准地图 + * - 1 卫星地图 + * - 2 夜视地图 + * - 3 导航地图 + * - 4 公交地图 + */ + mapType?: 0 | 1 | 2 | 3 | 4 | string; + /** + * @title 展示实时定位点 + * @description 展示用户当前带箭头的实时定位点 + */ + showLocation?: boolean; + /** + * @title 中心纬度 + * @description + */ + latitude?: number; + /** + * @title 中心经度 + * @description + */ + longitude?: number; + /** + * @title 缩放级别 + * @description 取值范围为 5-18 + * @default "16" + */ + scale?: number; + /** + * @title 倾斜角度 + * @description 范围 0 ~ 40 , 关于 z 轴的倾 + * @default "0" + */ + skew?: number; + /** + * @title 顺时针旋转的角度 + * @description 范围 0 ~ 360 + * @default "0" + */ + rotate?: number; + /** + * @title 视野范围 + * @description 视野将进行小范围延伸包含传入的坐标 + */ + includePoints?: LngLat[]; + /** + * @title 视野边界 + * @description 视野在地图 padding 范围内展示 + */ + includePadding?: { left: number; right: number; top: number; bottom: number }; + /** + * @title 是否启用缩放 + * @description 缩放交互开关 + * @default true + */ + enableZoom?: boolean; + /** + * @title 是否启用拖动 + * @description 拖动交互开关 + * @default true + */ + enableScroll?: boolean; + /** + * @title 是否启用旋转 + * @description 旋转交互开关 + * @default true + */ + enableRotate?: boolean; + + // * ------------------------------------------------------------ 数据 + + /** + * @title 标记点 + * @description 可在地图上展示多个标记点 + */ + markers?: Marker[]; + /** + * @title 折线 + * @description 可在地图上展示多个折线 + */ + polyline?: Polyline[]; + /** + * @title 面 + * @description 可在地图上展示多个面 + */ + polygon?: Polygon[]; + + // * ------------------------------------------------------------ 事件 + + /** + * @title 地图视野变化事件 + * @description 拖动/旋转/缩放地图时触发 + */ + onRegionChange?: (event: { + type: 'begin' | 'end'; + latitude: number; + longitude: number; + scale: number; + skew: number; + rotate: number; + causedBy: 'update' | 'gesture'; + }) => void; + /** + * @title 地图点击事件 + * @description 点击地图时触发 + */ + onTap?: (event: { latitude: number; longitude: number }) => void; + /** + * @title 标记点击事件 + * @description 点击标记及其标签时触发 + */ + onMarkerTap?: (event: { markerId: string; latitude: number; longitude: number }) => void; + /** + * @title 地图初始化完成事件 + * @description 地图初始化完成即将开始渲染第一帧时触发 + */ + onInitComplete?: () => void; +} + +export interface MarkerData extends Omit { + id?: string; + label?: string | Marker['label']; +} + +export interface PinMap extends Map { + data?: MarkerData[]; + markerStyle?: Partial; +} + +export interface RouteData { + markers?: MarkerData[]; + path?: Polyline; +} + +export interface PathMap extends Map { + data?: RouteData[]; + markerStyle?: Partial; + pathStyle?: Partial; +} diff --git a/src/utils/config.ts b/src/utils/config.ts new file mode 100644 index 0000000..1144263 --- /dev/null +++ b/src/utils/config.ts @@ -0,0 +1,59 @@ +import { isPlainObject, mergeWith } from 'lodash'; +import { DEFAULT_GLOBAL_CONFIG } from '../constants'; +import type { GlobalConfig } from '../types'; + +export const mergeGlobalConfig = (config: GlobalConfig) => { + const globalConfig = mergeWith({}, DEFAULT_GLOBAL_CONFIG, config, (objValue) => { + if (Array.isArray(objValue)) { + return objValue; + } + }); + + return globalConfig; +}; + +function mergeGraphFunctions( + key: string, + currValue: (value: any) => any, + prevValue: (value: any) => any, +) { + if (['plugins', 'behaviors', 'transforms'].includes(key)) { + return (prev: any[]) => currValue(prev); + } else { + return function (datum: any) { + // @ts-ignore this refers to the graph instance + const value = currValue.call(this, datum); + if (isPlainObject(value)) return mergeGraphOptions(prevValue, value); + return value; + }; + } +} + +export function mergeGraphOptions(...options: Record[]) { + if (options.length === 0) return {}; + + const merged = { ...options[0] }; + + for (let i = 1; i < options.length; i++) { + const currentOptions = options[i]; + + for (const key in currentOptions) { + if (currentOptions.hasOwnProperty(key)) { + const currValue = currentOptions[key]; + const prevValue = merged[key]; + + if (['component', 'data'].includes(key)) { + merged[key] = currValue; + } else if (typeof currValue === 'function') { + merged[key] = mergeGraphFunctions(key, currValue, prevValue); + } else if (isPlainObject(currValue) && isPlainObject(prevValue)) { + merged[key] = mergeGraphOptions(prevValue, currValue); + } else { + merged[key] = currValue; + } + } + } + } + + return merged; +} diff --git a/src/utils/graph.ts b/src/utils/graph.ts new file mode 100644 index 0000000..b507326 --- /dev/null +++ b/src/utils/graph.ts @@ -0,0 +1,39 @@ +// FIXME: 全量导入 G6 模块 +import { G6 } from '@ant-design/graphs'; +import type { GraphData, TreeGraphData } from '../types'; + +const { treeToGraphData } = G6; + +export function visTreeData2GraphData(data: TreeGraphData) { + return treeToGraphData(data as unknown as G6.TreeData, { + getNodeData: (datum, depth) => { + datum.id = datum.name; + datum.depth = depth; + if (!datum.children) return datum as G6.NodeData; + const { children, ...restDatum } = datum; + return { + ...restDatum, + children: children.map((child) => child.name), + } as G6.NodeData; + }, + getEdgeData: (source, target) => + ({ + source: source.name, + target: target.name, + }) as G6.EdgeData, + }); +} + +export function visGraphData2GraphData(data: GraphData) { + return { + nodes: data.nodes.map((node) => ({ + id: node.name, + style: { labelText: node.name }, + })), + edges: data.edges.map((edge) => ({ + source: edge.source, + target: edge.target, + style: { labelText: edge.name }, + })), + }; +} diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..111a393 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1 @@ +export * from './map'; diff --git a/src/utils/map/context.ts b/src/utils/map/context.ts new file mode 100644 index 0000000..956551c --- /dev/null +++ b/src/utils/map/context.ts @@ -0,0 +1,21 @@ +import type { Scene } from '@antv/l7'; +import type { Map, Marker } from '../../types/map'; +import { urlToMarkerId } from './util'; + +export function setMapContext(props: Map, scene: Scene) { + //初始化Image + if (props.markers) { + const icons = new Set( + props.markers + .filter((item: Marker) => item.iconPath !== undefined) + .map((item: Marker) => item.iconPath), + ) as unknown as string[]; + + return Promise.all( + Array.from(icons.values()).map(async (url: string) => { + const id = urlToMarkerId(url); + return await scene.addImage(id, url); + }), + ); + } +} diff --git a/src/utils/map/controls.ts b/src/utils/map/controls.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/map/index.ts b/src/utils/map/index.ts new file mode 100644 index 0000000..8a1bfa0 --- /dev/null +++ b/src/utils/map/index.ts @@ -0,0 +1,6 @@ +export * from './context'; +export * from './markers'; +export * from './polyline'; +export * from './style'; +export * from './util'; +export * from './view'; diff --git a/src/utils/map/markers.ts b/src/utils/map/markers.ts new file mode 100644 index 0000000..9d56252 --- /dev/null +++ b/src/utils/map/markers.ts @@ -0,0 +1,69 @@ +import { PointLayer } from '@antv/l7'; +import { isObject } from 'lodash'; +import type { Marker as MarkerProps } from '../../types/map'; +import { urlToMarkerId } from './util'; + +export function setMarkers(data: MarkerProps[]) { + const items = data.map((item: MarkerProps) => { + return { + ...item, + label: isObject(item.label) ? item.label!.content : item.label, + color: item.label?.color, + bgColor: item.label?.bgColor, + fontSize: item.label?.fontSize, + offsets: [item.anchorX || 0, item.anchorY || -1], + iconPath: item.iconPath ? urlToMarkerId(item.iconPath) : undefined, + }; + }); + const icons = items.filter((item) => item.iconPath !== undefined); + const texts = items.filter((item) => item.label !== undefined); + const layers = []; + if (texts.length > 0) { + const offsets = texts[0].offsets; + const fontSize = texts[0].fontSize || 10; + const text = new PointLayer({ + zIndex: 2, + }) + .source(texts, { + parser: { + type: 'json', + x: 'longitude', + y: 'latitude', + }, + }) + .shape('label', 'text') + .size('fontSize') + .color('color') + .style({ + opacity: 1, + textOffset: [offsets[0], -2 * offsets[1] * fontSize], + fontWeight: 600, + textAnchor: 'center', + stroke: texts[0].bgColor || '#ffffff', // 描边颜色 + strokeWidth: 2, // 描边宽度 + strokeOpacity: 1.0, + padding: [10, 10], + }); + layers.push(text); + } + if (icons.length !== 0) { + const offsets = icons[0].offsets; + + const width = icons[0].width || 10; + const iconLayer = new PointLayer() + .source(icons, { + parser: { + type: 'json', + x: 'longitude', + y: 'latitude', + }, + }) + .shape('iconPath') + .size('width') + .style({ + offsets: [offsets[0], offsets[1] * width], + }); + layers.push(iconLayer); + } + return layers; +} diff --git a/src/utils/map/polyline.ts b/src/utils/map/polyline.ts new file mode 100644 index 0000000..1590a37 --- /dev/null +++ b/src/utils/map/polyline.ts @@ -0,0 +1,31 @@ +import { LineLayer } from '@antv/l7'; +import type { Polyline as PolylinePorps } from '../../types/map'; + +export function setPolyline(data: PolylinePorps[]) { + const lineData = data.map((item: PolylinePorps) => { + const coord = item.points.map((point) => [point.longitude, point.latitude]); + return { + ...item, + coordinates: coord, + color: item.color, + width: item.width, + }; + }); + const isdash = lineData[0].dottedLine; + const lineLayer = new LineLayer() + .source(lineData, { + parser: { + type: 'json', + coordinates: 'coordinates', + }, + }) + .size('width') + .shape('line') + .color('color') + .style({ + opacity: 1.0, + lineType: isdash ? 'dash' : 'solid', + dashArray: [3, 1], + }); + return [lineLayer]; +} diff --git a/src/utils/map/style.ts b/src/utils/map/style.ts new file mode 100644 index 0000000..8f02444 --- /dev/null +++ b/src/utils/map/style.ts @@ -0,0 +1,98 @@ +import { isObject } from 'lodash'; +import type { MapProps } from '../../Map'; +import type { LarkMapProps } from '../../types'; +import type { Marker, MarkerData, Polyline } from '../../types/map'; + +const DefaultMapConfig = { + mapOptions: { + center: [120.210792, 30.246026] as [number, number], + zoom: 16, + maxZoom: 18, + pitch: 0, + rotation: 0, + zoomEnable: true, + pitchEnable: true, + }, +}; + +const formatMapStyle = (props: MapProps) => { + const config: LarkMapProps = { + mapOptions: { + ...DefaultMapConfig.mapOptions, + center: + props.longitude && props.latitude + ? ([props.longitude, props.latitude] as [number, number]) + : DefaultMapConfig.mapOptions.center, + zoom: props.scale || DefaultMapConfig.mapOptions.zoom, + pitch: props.skew || DefaultMapConfig.mapOptions.pitch, + rotation: props.rotate || DefaultMapConfig.mapOptions.rotation, + zoomEnable: props.enableZoom || DefaultMapConfig.mapOptions.zoomEnable, + pitchEnable: props.enableRotate || DefaultMapConfig.mapOptions.pitchEnable, + // 地图底图样式 + style: props?.mapType, + // 高德地图密钥 + token: props?.token, + }, + logoVisible: false, + }; + return config; +}; + +const DefaultMarkerStyle = { + width: 12, + anchorX: 0, + anchorY: 1, + label: { + content: '', + color: '#000000', + fontSize: 10, + borderRadius: 5, + bgColor: '#ffffff', + padding: 5, + }, + iconPath: + 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*3XdDTbsQ84QAAAAAAAAAAAAADmJ7AQ/original', +}; + +const formatMakerStyle = (data: MarkerData[], markerStyle: Partial) => { + const labelStyle = Object.assign({}, DefaultMarkerStyle.label, markerStyle?.label); + // label 优先级 data > markerStyle > DefaultMarkerStyle + return data.map((marker: MarkerData, index: number) => { + const { label, id, ...rest } = marker; + return { + ...DefaultMarkerStyle, + ...markerStyle, + ...rest, + label: { + ...labelStyle, + ...(isObject(label) ? label : { content: label }), + }, + id: id || index.toString(), + }; + }); +}; + +const DefaultPolylineStyle = { + width: 2, + color: '#16f', + dottedLine: false, + zIndex: 1, +}; + +const formatPolylineStyle = (data: Polyline[] = [], polylineStyle: Partial) => { + return data.map((item: any) => { + return { + ...DefaultPolylineStyle, + ...polylineStyle, + ...item, + }; + }); +}; + +export { + DefaultMarkerStyle, + DefaultPolylineStyle, + formatMakerStyle, + formatMapStyle, + formatPolylineStyle, +}; diff --git a/src/utils/map/util.ts b/src/utils/map/util.ts new file mode 100644 index 0000000..ecf2a02 --- /dev/null +++ b/src/utils/map/util.ts @@ -0,0 +1,13 @@ +export function urlToMarkerId(url: string) { + let hash = 0; + if (url.length > 0) { + for (let i = 0; i < url.length; i++) { + hash = ((hash << 5) - hash + url.charCodeAt(i)) | 0; + } + } + // 将哈希值转换为四位数 + const id = Math.abs(hash % 10000) + .toString() + .padStart(4, '0'); + return `marker-${id}`; +} diff --git a/src/utils/map/view.ts b/src/utils/map/view.ts new file mode 100644 index 0000000..2eddd22 --- /dev/null +++ b/src/utils/map/view.ts @@ -0,0 +1,77 @@ +import type { Scene } from '@antv/l7'; +import type { Map } from '../../types/map'; + +function fitIncludePoints( + includePoints: { longitude: number; latitude: number }[], + scene: Scene, + includePadding: Map['includePadding'], +) { + if (includePoints.length === 1) { + scene.setCenter([includePoints[0].longitude, includePoints[0].latitude]); + } else { + const bounds = [180, 90, -180, -90]; + includePoints.forEach((point: any) => { + if (bounds[0] > point.longitude) { + bounds[0] = point.longitude; + } + if (bounds[1] > point.latitude) { + bounds[1] = point.latitude; + } + if (bounds[2] < point.longitude) { + bounds[2] = point.longitude; + } + if (bounds[3] < point.latitude) { + bounds[3] = point.latitude; + } + }); + const { left = 20, right = 20, bottom = 20, top = 20 } = includePadding || {}; + const padding = [left, top, right, bottom]; + // @ts-ignore + scene.map.setBounds(bounds, false, padding); + } +} + +export const setMapView = (props: Map, scene: Scene) => { + // 单个点,多个点 + if (props.includePoints) { + fitIncludePoints(props.includePoints, scene, props.includePadding); + } else { + const points: { longitude: number; latitude: number }[] = []; + if (props.markers) { + props.markers.forEach((item) => { + points.push({ longitude: item.longitude, latitude: item.latitude }); + }); + } + if (props.polyline) { + props.polyline.forEach((item) => { + item.points.forEach((point) => { + points.push({ longitude: point.longitude, latitude: point.latitude }); + }); + }); + } + if (points.length) { + fitIncludePoints(points, scene, props.includePadding); + } + } + + if (props.enableZoom !== undefined) { + scene.setMapStatus({ + zoomEnable: props.enableZoom, + }); + } + if (props.enableRotate !== undefined) { + scene.setMapStatus({ + rotateEnable: props.enableRotate, + }); + } + if (props.enableScroll !== undefined) { + scene.setMapStatus({ + dragEnable: props.enableScroll, + }); + } + + if (props.onInitComplete) { + scene.off('loaded', props.onInitComplete); + scene.on('loaded', props.onInitComplete); + } +}; diff --git a/src/utils/plot.ts b/src/utils/plot.ts new file mode 100644 index 0000000..f7a65fb --- /dev/null +++ b/src/utils/plot.ts @@ -0,0 +1,63 @@ +import { isUndefined } from 'lodash'; + +const ADC_ENCODE_FIELDS = new Map([ + ['x', 'xField'], + ['y', 'yField'], + ['series', 'seriesField'], + ['size', 'sizeField'], + ['color', 'colorField'], + ['shape', 'shapeField'], + ['y', 'angleField'], +]); + +/** + * 将 G2 encode 写法转换为 ADC Plot 字段映射 + */ +function visG2Encode2ADCEncode>(config: T) { + const encodeConfig = config.encode; + if (!encodeConfig) return config; + + const _config = { ...config }; + for (const field of encodeConfig) { + const adcField = ADC_ENCODE_FIELDS.get(field); + if (adcField) { + // @ts-expect-error + _config[adcField] = encodeConfig[field]; + } + } + + return _config; +} + +/** + * 将缩写的 axisTitle 转换为 G2 axis 配置 + */ +function axisTitle2G2axis>(config: T) { + const { axisXTitle, axisYTitle, axis } = config; + + if (!isUndefined(axis)) return config; + + const _config = { axis: {}, ...config }; + + if (axisXTitle) { + // @ts-expect-error + _config.axis.x = { title: axisXTitle }; + } + + if (axisYTitle) { + // @ts-expect-error + _config.axis.y = { title: axisYTitle }; + } + + return _config; +} + +/** + * 将 GPT-Vis 图表的配置转换为 ADC 的配置 + */ +export function transform2ADCProps>(props: T) { + const transformedEncode = visG2Encode2ADCEncode(props); + const transformedAxis = axisTitle2G2axis(transformedEncode); + + return transformedAxis; +} diff --git a/src/version.ts b/src/version.ts new file mode 100644 index 0000000..8b2ab62 --- /dev/null +++ b/src/version.ts @@ -0,0 +1 @@ +export default '0.0.1'; diff --git a/tailwind.config.js b/tailwind.config.js deleted file mode 100644 index 1db67d1..0000000 --- a/tailwind.config.js +++ /dev/null @@ -1,12 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -module.exports = { - content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'], - darkMode: 'class', - theme: { - fontFamily: { - sans: ['"Josefin Sans"'], - }, - extend: {}, - }, - plugins: [], -}; diff --git a/tailwind.css b/tailwind.css deleted file mode 100644 index cc826d3..0000000 --- a/tailwind.css +++ /dev/null @@ -1,7 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -svg { - display: inline-block; -} \ No newline at end of file diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json new file mode 100644 index 0000000..4131afc --- /dev/null +++ b/tsconfig.eslint.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true + }, + // paths intend to lint + "include": [ + "./*.?*.ts", + "src", + // test workspace lint + "__tests__" + ], + "exclude": ["node_modules", "dist"] +} diff --git a/tsconfig.json b/tsconfig.json index c3185b8..a579fbc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,22 +1,18 @@ { "compilerOptions": { - "strictNullChecks": true, + "target": "esnext", "module": "esnext", "moduleResolution": "node", - "esModuleInterop": true, - "experimentalDecorators": true, "jsx": "react", - "jsxFactory": "React.createElement", - "jsxFragmentFactory": "React.Fragment", - "noUnusedParameters": true, - "noUnusedLocals": true, - "noImplicitAny": true, - "target": "es6", - "lib": ["dom", "es2017"], + "esModuleInterop": true, + "strict": true, "skipLibCheck": true, - "stripInternal": true, - "declaration": true + "declaration": true, + "resolveJsonModule": true, + "baseUrl": "./", + "paths": { + "@antv/gpt-vis": ["./src/index.ts"] + } }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "es"] + "exclude": ["node_modules", "dist"] } diff --git a/vite.config.js b/vite.config.js deleted file mode 100644 index 5d00302..0000000 --- a/vite.config.js +++ /dev/null @@ -1,7 +0,0 @@ -import path from 'path'; -import { defineConfig } from 'vite'; - -export default defineConfig({ - root: path.resolve('./dev'), - server: { port: 8080, open: '/' }, -});