├── .gitattributes
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── config.yml
│ ├── feature_request.md
│ └── question.md
├── dependabot.yml
├── release-drafter.yml
└── workflows
│ ├── ci.yml
│ ├── create_release.yml
│ ├── deploy_website.yml
│ └── release_helper.yml
├── .gitignore
├── .prettierignore
├── .prettierrc.cjs
├── .vscode
└── settings.json
├── .yarn
└── releases
│ └── yarn-4.5.1.cjs
├── .yarnrc.yml
├── LICENSE
├── README.md
├── docs
├── .vitepress
│ ├── config.ts
│ └── theme
│ │ └── index.ts
├── examples
│ ├── basic.ts
│ ├── index.md
│ └── stroke.md
├── getting-started.md
├── index.md
└── related.md
├── eslint.config.mjs
├── package.json
├── rollup.config.js
├── samples
└── default.html
├── src
├── __tests__
│ └── createChart.ts
├── controllers
│ ├── WordCloudController.spec.ts
│ ├── WordCloudController.ts
│ ├── __image_snapshots__
│ │ └── word-cloud-controller-spec-ts-default-default-1-snap.png
│ ├── index.ts
│ └── patchController.ts
├── elements
│ ├── WordElement.ts
│ └── index.ts
├── index.ts
└── index.umd.ts
├── tsconfig.c.json
├── tsconfig.json
├── typedoc.json
├── vitest.config.ts
└── yarn.lock
/.gitattributes:
--------------------------------------------------------------------------------
1 | # These settings are for any web project
2 |
3 | # Handle line endings automatically for files detected as text
4 | # and leave all files detected as binary untouched.
5 | * text=auto eol=lf
6 |
7 | #
8 | # The above will handle all files NOT found below
9 | #
10 |
11 | #
12 | ## These files are text and should be normalized (Convert crlf => lf)
13 | #
14 |
15 | # source code
16 | *.php text
17 | *.css text
18 | *.sass text
19 | *.scss text
20 | *.less text
21 | *.styl text
22 | *.js text
23 | *.jsx text
24 | *.ts text
25 | *.tsx text
26 | *.coffee text
27 | *.json text
28 | *.htm text
29 | *.html text
30 | *.xml text
31 | *.txt text
32 | *.ini text
33 | *.inc text
34 | *.pl text
35 | *.rb text
36 | *.py text
37 | *.scm text
38 | *.sql text
39 | *.sh text eof=LF
40 | *.bat text
41 |
42 | # templates
43 | *.hbt text
44 | *.jade text
45 | *.haml text
46 | *.hbs text
47 | *.dot text
48 | *.tmpl text
49 | *.phtml text
50 |
51 | # server config
52 | .htaccess text
53 |
54 | # git config
55 | .gitattributes text
56 | .gitignore text
57 |
58 | # code analysis config
59 | .jshintrc text
60 | .jscsrc text
61 | .jshintignore text
62 | .csslintrc text
63 |
64 | # misc config
65 | *.yaml text
66 | *.yml text
67 | .editorconfig text
68 |
69 | # build config
70 | *.npmignore text
71 | *.bowerrc text
72 | Dockerfile text eof=LF
73 |
74 | # Heroku
75 | Procfile text
76 | .slugignore text
77 |
78 | # Documentation
79 | *.md text
80 | LICENSE text
81 | AUTHORS text
82 |
83 |
84 | #
85 | ## These files are binary and should be left untouched
86 | #
87 |
88 | # (binary is a macro for -text -diff)
89 | *.png binary
90 | *.jpg binary
91 | *.jpeg binary
92 | *.gif binary
93 | *.ico binary
94 | *.mov binary
95 | *.mp4 binary
96 | *.mp3 binary
97 | *.flv binary
98 | *.fla binary
99 | *.swf binary
100 | *.gz binary
101 | *.zip binary
102 | *.7z binary
103 | *.ttf binary
104 | *.pyc binary
105 | *.pdf binary
106 |
107 | # Source files
108 | # ============
109 | *.pxd text
110 | *.py text
111 | *.py3 text
112 | *.pyw text
113 | *.pyx text
114 | *.sh text eol=lf
115 | *.json text
116 |
117 | # Binary files
118 | # ============
119 | *.db binary
120 | *.p binary
121 | *.pkl binary
122 | *.pyc binary
123 | *.pyd binary
124 | *.pyo binary
125 |
126 | # Note: .db, .p, and .pkl files are associated
127 | # with the python modules ``pickle``, ``dbm.*``,
128 | # ``shelve``, ``marshal``, ``anydbm``, & ``bsddb``
129 | # (among others).
130 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [sgratzl]
4 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🐛 Bug report
3 | about: If something isn't working as expected 🤔.
4 | title: ''
5 | labels: 'bug'
6 | assignees: ''
7 | ---
8 |
9 |
10 |
11 | When I...
12 |
13 | **To Reproduce**
14 |
15 |
17 |
18 | 1.
19 |
20 | **Expected behavior**
21 |
22 |
23 |
24 | **Screenshots**
25 |
26 |
27 |
28 | **Context**
29 |
30 | - Version:
31 | - Browser:
32 |
33 | **Additional context**
34 |
35 |
36 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: true
2 | # contact_links:
3 | # - name: Samuel Gratzl
4 | # url: https://www.sgratzl.com
5 | # about: Please ask and answer questions here.
6 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🚀 Feature Request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: 'enhancement'
6 | assignees: ''
7 | ---
8 |
9 |
10 |
11 | It would be great if ...
12 |
13 | **User story**
14 |
15 |
16 |
17 | **Additional context**
18 |
19 |
20 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/question.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🤗 Question
3 | about: ask question about the library (usage, features,...)
4 | title: ''
5 | labels: 'question'
6 | assignees: ''
7 | ---
8 |
9 |
13 |
14 | I'm having the following question...
15 |
16 | **Screenshots / Sketches**
17 |
18 |
19 |
20 | **Context**
21 |
22 | - Version:
23 | - Browser:
24 |
25 | **Additional context**
26 |
27 |
28 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | updates:
4 | - package-ecosystem: 'github-actions'
5 | directory: '/'
6 | schedule:
7 | interval: 'monthly'
8 | target-branch: 'dev'
9 | labels:
10 | - 'dependencies'
11 | - 'chore'
12 | - package-ecosystem: 'npm'
13 | directory: '/'
14 | schedule:
15 | interval: 'monthly'
16 | target-branch: 'dev'
17 | labels:
18 | - 'dependencies'
19 | - 'chore'
20 |
--------------------------------------------------------------------------------
/.github/release-drafter.yml:
--------------------------------------------------------------------------------
1 | name-template: 'v$RESOLVED_VERSION'
2 | tag-template: 'v$RESOLVED_VERSION'
3 | categories:
4 | - title: '🚀 Features'
5 | labels:
6 | - 'enhancement'
7 | - 'feature'
8 | - title: '🐛 Bugs Fixes'
9 | labels:
10 | - 'bug'
11 | - title: 'Documentation'
12 | labels:
13 | - 'documentation'
14 | - title: '🧰 Development'
15 | labels:
16 | - 'chore'
17 | change-template: '- #$NUMBER $TITLE'
18 | change-title-escapes: '\<*_&`#@'
19 | template: |
20 | $CHANGES
21 |
22 | Thanks to $CONTRIBUTORS
23 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on:
4 | - push
5 | - pull_request
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v4
12 | - uses: actions/setup-node@v4
13 | with:
14 | node-version: 20
15 | - run: npm i -g yarn
16 | - run: yarn config set checksumBehavior ignore
17 | - name: Cache Node.js modules
18 | uses: actions/cache@v4
19 | with:
20 | path: |
21 | ./.yarn/cache
22 | ./.yarn/unplugged
23 | key: ${{ runner.os }}-yarn2-v5-${{ hashFiles('**/yarn.lock') }}
24 | restore-keys: |
25 | ${{ runner.os }}-yarn2-v5
26 | - run: yarn install
27 | - run: yarn build
28 | - run: yarn lint
29 | - run: yarn test
30 | - uses: actions/upload-artifact@v4
31 | if: failure()
32 | with:
33 | name: diff outputs
34 | path: src/**/__diff_output__/*.png
35 | - run: yarn docs:build
36 |
--------------------------------------------------------------------------------
/.github/workflows/create_release.yml:
--------------------------------------------------------------------------------
1 | name: Create Release
2 | on:
3 | workflow_dispatch:
4 | inputs:
5 | versionName:
6 | description: 'Semantic Version Number (i.e., 5.5.0 or patch, minor, major, prepatch, preminor, premajor, prerelease)'
7 | required: true
8 | default: patch
9 | preid:
10 | description: 'Pre Release Identifier (i.e., alpha, beta)'
11 | required: true
12 | default: alpha
13 | jobs:
14 | create_release:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - name: Check out code
18 | uses: actions/checkout@v4
19 | with:
20 | ref: main
21 | ssh-key: ${{ secrets.PRIVATE_SSH_KEY }}
22 | - name: Reset main branch
23 | run: |
24 | git fetch origin dev:dev
25 | git reset --hard origin/dev
26 | - name: Change version number
27 | id: version
28 | run: |
29 | echo "next_tag=$(npm version --no-git-tag-version ${{ github.event.inputs.versionName }} --preid ${{ github.event.inputs.preid }})" >> $GITHUB_OUTPUT
30 | - name: Create pull request into main
31 | uses: peter-evans/create-pull-request@v7
32 | with:
33 | branch: release/${{ steps.version.outputs.next_tag }}
34 | commit-message: 'chore: release ${{ steps.version.outputs.next_tag }}'
35 | base: main
36 | title: Release ${{ steps.version.outputs.next_tag }}
37 | labels: chore
38 | assignees: sgratzl
39 | body: |
40 | Releasing ${{ steps.version.outputs.next_tag }}.
41 |
--------------------------------------------------------------------------------
/.github/workflows/deploy_website.yml:
--------------------------------------------------------------------------------
1 | name: Deploy Website
2 | on:
3 | workflow_dispatch: {}
4 | push:
5 | branches:
6 | - main
7 | jobs:
8 | deploy:
9 | runs-on: ubuntu-latest
10 | permissions:
11 | pages: write
12 | id-token: write
13 | environment:
14 | name: github-pages
15 | url: ${{ steps.deployment.outputs.page_url }}
16 | steps:
17 | - uses: actions/checkout@v4
18 | with:
19 | fetch-depth: 0
20 | - uses: actions/setup-node@v4
21 | with:
22 | node-version: 20
23 | cache: npm
24 | - run: npm i -g yarn
25 | - run: yarn config set checksumBehavior ignore
26 | - name: Cache Node.js modules
27 | uses: actions/cache@v4
28 | with:
29 | path: |
30 | ./.yarn/cache
31 | ./.yarn/unplugged
32 | key: ${{ runner.os }}-yarn2-v5-${{ hashFiles('**/yarn.lock') }}
33 | restore-keys: |
34 | ${{ runner.os }}-yarn2-v5
35 | - run: yarn install
36 | - run: yarn docs:build
37 | - uses: actions/configure-pages@v5
38 | - uses: actions/upload-pages-artifact@v3
39 | with:
40 | path: docs/.vitepress/dist
41 | - name: Deploy
42 | id: deployment
43 | uses: actions/deploy-pages@v4
44 |
--------------------------------------------------------------------------------
/.github/workflows/release_helper.yml:
--------------------------------------------------------------------------------
1 | name: Release Helper
2 | on:
3 | push:
4 | branches:
5 | - main
6 |
7 | jobs:
8 | correct_repository:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: fail on fork
12 | if: github.repository_owner != 'sgratzl'
13 | run: exit 1
14 |
15 | create_release:
16 | needs: correct_repository
17 | runs-on: ubuntu-latest
18 | steps:
19 | - name: Check out code
20 | uses: actions/checkout@v4
21 | - uses: actions/setup-node@v4
22 | with:
23 | node-version: 20
24 | - name: Extract version
25 | id: extract_version
26 | run: |
27 | node -pe "'version=' + require('./package.json').version" >> $GITHUB_OUTPUT
28 | node -pe "'npm_tag=' + (require('./package.json').version.includes('-') ? 'next' : 'latest')" >> $GITHUB_OUTPUT
29 | - name: Print version
30 | run: |
31 | echo "releasing ${{ steps.extract_version.outputs.version }} with tag ${{ steps.extract_version.outputs.npm_tag }}"
32 | - name: Create Release
33 | id: create_release
34 | uses: release-drafter/release-drafter@v6
35 | env:
36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
37 | with:
38 | name: v${{ steps.extract_version.outputs.version }}
39 | tag: v${{ steps.extract_version.outputs.version }}
40 | version: ${{ steps.extract_version.outputs.version }}
41 | prerelease: ${{ needs.create_release.outputs.tag_name == 'next' }}
42 | publish: true
43 | outputs:
44 | version: ${{ steps.extract_version.outputs.version }}
45 | npm_tag: ${{ steps.extract_version.outputs.npm_tag }}
46 | upload_url: ${{ steps.create_release.outputs.upload_url }}
47 | tag_name: ${{ steps.create_release.outputs.tag_name }}
48 |
49 | build_assets:
50 | needs: create_release
51 | runs-on: ubuntu-latest
52 | steps:
53 | - name: Check out code
54 | uses: actions/checkout@v4
55 | - uses: actions/setup-node@v4
56 | with:
57 | node-version: 20
58 | - run: npm i -g yarn
59 | - run: yarn config set checksumBehavior ignore
60 | - name: Cache Node.js modules
61 | uses: actions/cache@v4
62 | with:
63 | path: |
64 | ./.yarn/cache
65 | ./.yarn/unplugged
66 | key: ${{ runner.os }}-yarn2-v5-${{ hashFiles('**/yarn.lock') }}
67 | restore-keys: |
68 | ${{ runner.os }}-yarn2-v5
69 | - run: yarn install
70 | - run: yarn build
71 | - run: yarn pack
72 | - name: Upload Release Asset
73 | uses: AButler/upload-release-assets@v3.0
74 | with:
75 | files: 'package.tgz'
76 | repo-token: ${{ secrets.GITHUB_TOKEN }}
77 | release-tag: ${{ needs.create_release.outputs.tag_name }}
78 | - name: Pack Publish
79 | run: |
80 | yarn config set npmAuthToken "${{ secrets.NPM_TOKEN }}"
81 | yarn pack
82 | yarn npm publish --tag "${{ needs.create_release.outputs.npm_tag }}"
83 |
84 | sync_dev:
85 | needs: correct_repository
86 | runs-on: ubuntu-latest
87 | steps:
88 | - name: Check out code
89 | uses: actions/checkout@v4
90 | with:
91 | ref: dev
92 | ssh-key: ${{ secrets.PRIVATE_SSH_KEY }}
93 | - name: Reset dev branch
94 | run: |
95 | git fetch origin main:main
96 | git merge main
97 | git push
98 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | /coverage
7 | /node_modules
8 | .npm
9 | .yarn/*
10 | !.yarn/patches
11 | !.yarn/releases
12 | !.yarn/plugins
13 | !.yarn/versions
14 | .pnp.*
15 |
16 | # Build files
17 | /.tmp
18 | /build
19 |
20 | *.tgz
21 | /.vscode/extensions.json
22 | *.tsbuildinfo
23 | .eslintcache
24 | __diff_output__
25 |
26 | docs/.vitepress/dist
27 | docs/.vitepress/cache
28 | docs/.vitepress/config.ts.timestamp*
29 | docs/api/
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | /.pnp*
2 | /.yarnrc.yml
3 | /.yarn
4 | /build
5 | /docs/.vitepress/cache
6 | /docs/.vitepress/dist
7 | /docs/.vitepress/config.ts.timestamp*
8 | /docs/api
9 | /coverage
10 | /.gitattributes
11 | /.gitignore
12 | /.prettierignore
13 | /LICENSE
14 | /yarn.lock
15 | /.vscode
16 | *.png
17 | *.tgz
18 | *.tsbuildinfo
19 | .eslintcache
20 | .nojekyll
21 |
--------------------------------------------------------------------------------
/.prettierrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | printWidth: 120,
3 | semi: true,
4 | singleQuote: true,
5 | trailingComma: 'es5',
6 | };
7 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnSave": true,
3 | "editor.formatOnType": true,
4 | "[javascript]": {
5 | "editor.defaultFormatter": "esbenp.prettier-vscode"
6 | },
7 | "[typescript]": {
8 | "editor.defaultFormatter": "esbenp.prettier-vscode"
9 | },
10 | "[json]": {
11 | "editor.defaultFormatter": "esbenp.prettier-vscode"
12 | },
13 | "[yaml]": {
14 | "editor.defaultFormatter": "esbenp.prettier-vscode"
15 | },
16 | "npm.packageManager": "yarn",
17 | "eslint.nodePath": ".yarn/sdks",
18 | "prettier.prettierPath": ".yarn/sdks/prettier/index.cjs",
19 | "files.eol": "\n",
20 | "typescript.tsdk": ".yarn/sdks/typescript/lib",
21 | "typescript.enablePromptUseWorkspaceTsdk": true,
22 | "editor.detectIndentation": false,
23 | "editor.tabSize": 2,
24 | "search.exclude": {
25 | "**/.yarn": true,
26 | "**/.pnp.*": true
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | yarnPath: .yarn/releases/yarn-4.5.1.cjs
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019-2023 Samuel Gratzl
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Chart.js Word Clouds
2 |
3 | [![NPM Package][npm-image]][npm-url] [![Github Actions][github-actions-image]][github-actions-url]
4 |
5 | Chart.js module for charting word or tag clouds. Adding new chart type: `wordCloud`.
6 |
7 | 
8 |
9 | ## Related Plugins
10 |
11 | Check out also my other chart.js plugins:
12 |
13 | - [chartjs-chart-boxplot](https://github.com/sgratzl/chartjs-chart-boxplot) for rendering boxplots and violin plots
14 | - [chartjs-chart-error-bars](https://github.com/sgratzl/chartjs-chart-error-bars) for rendering errors bars to bars and line charts
15 | - [chartjs-chart-geo](https://github.com/sgratzl/chartjs-chart-geo) for rendering map, bubble maps, and choropleth charts
16 | - [chartjs-chart-graph](https://github.com/sgratzl/chartjs-chart-graph) for rendering graphs, trees, and networks
17 | - [chartjs-chart-pcp](https://github.com/sgratzl/chartjs-chart-pcp) for rendering parallel coordinate plots
18 | - [chartjs-chart-venn](https://github.com/sgratzl/chartjs-chart-venn) for rendering venn and euler diagrams
19 | - [chartjs-plugin-hierarchical](https://github.com/sgratzl/chartjs-plugin-hierarchical) for rendering hierarchical categorical axes which can be expanded and collapsed
20 |
21 | ## Install
22 |
23 | ```bash
24 | npm install --save chart.js chartjs-chart-wordcloud
25 | ```
26 |
27 | ## Usage
28 |
29 | see [Examples](https://www.sgratzl.com/chartjs-chart-wordcloud/examples/)
30 |
31 | or at this [![Open in CodePen][codepen]](https://codepen.io/sgratzl/pen/WNwzYgy)
32 |
33 | ## Word Cloud
34 |
35 | ### Data Structure
36 |
37 | ```ts
38 | const config = {
39 | type: 'wordCloud',
40 | data: {
41 | // text
42 | labels: ['Hello', 'world', 'normally', 'you', 'want', 'more', 'words', 'than', 'this'],
43 | datasets: [
44 | {
45 | label: 'DS',
46 | // size in pixel
47 | data: [90, 80, 70, 60, 50, 40, 30, 20, 10],
48 | },
49 | ],
50 | },
51 | options: {},
52 | };
53 | ```
54 |
55 | ### Styling of elements
56 |
57 | A word has the basic FontSpec styling options (family, color, ...). In addition it has several options regarding rotating the text.
58 |
59 | Controller options:
60 |
61 | https://github.com/sgratzl/chartjs-chart-wordcloud/blob/14ac8327c2209c0d8f89fdd6cd86d2b2d7daedce/src/controllers/WordCloudController.ts#L184-L193
62 |
63 | Word element options:
64 |
65 | https://github.com/sgratzl/chartjs-chart-wordcloud/blob/14ac8327c2209c0d8f89fdd6cd86d2b2d7daedce/src/elements/WordElement.ts#L3-L29
66 |
67 | ## ESM and Tree Shaking
68 |
69 | The ESM build of the library supports tree shaking thus having no side effects. As a consequence the chart.js library won't be automatically manipulated nor new controllers automatically registered. One has to manually import and register them.
70 |
71 | Variant A:
72 |
73 | ```js
74 | import { Chart } from 'chart.js';
75 | import { WordCloudController, WordElement } from 'chartjs-chart-wordcloud';
76 |
77 | Chart.register(WordCloudController, WordElement);
78 | ...
79 |
80 | new Chart(ctx, {
81 | type: WordCloudController.id,
82 | data: [...],
83 | });
84 | ```
85 |
86 | Variant B:
87 |
88 | ```js
89 | import { WordCloudChart } from 'chartjs-chart-wordcloud';
90 |
91 | new WordCloudChart(ctx, {
92 | data: [...],
93 | });
94 | ```
95 |
96 | ## Development Environment
97 |
98 | ```sh
99 | npm i -g yarn
100 | yarn install
101 | yarn sdks vscode
102 | ```
103 |
104 | ### Common commands
105 |
106 | ```sh
107 | yarn compile
108 | yarn test
109 | yarn lint
110 | yarn fix
111 | yarn build
112 | yarn docs
113 | ```
114 |
115 | [mit-image]: https://img.shields.io/badge/License-MIT-yellow.svg
116 | [mit-url]: https://opensource.org/licenses/MIT
117 | [npm-image]: https://badge.fury.io/js/chartjs-chart-wordcloud.svg
118 | [npm-url]: https://npmjs.org/package/chartjs-chart-wordcloud
119 | [github-actions-image]: https://github.com/sgratzl/chartjs-chart-wordcloud/workflows/ci/badge.svg
120 | [github-actions-url]: https://github.com/sgratzl/chartjs-chart-wordcloud/actions
121 | [codepen]: https://img.shields.io/badge/CodePen-open-blue?logo=codepen
122 |
--------------------------------------------------------------------------------
/docs/.vitepress/config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitepress';
2 | import { name, description, repository, license, author } from '../../package.json';
3 | import typedocSidebar from '../api/typedoc-sidebar.json';
4 |
5 | const cleanName = name.replace('@sgratzl/', '');
6 |
7 | // https://vitepress.dev/reference/site-config
8 | export default defineConfig({
9 | title: cleanName,
10 | description,
11 | base: `/${cleanName}/`,
12 | useWebFonts: false,
13 | themeConfig: {
14 | // https://vitepress.dev/reference/default-theme-config
15 | nav: [
16 | { text: 'Home', link: '/' },
17 | { text: 'Getting Started', link: '/getting-started' },
18 | { text: 'Examples', link: '/examples/' },
19 | { text: 'API', link: '/api/' },
20 | { text: 'Related Plugins', link: '/related' },
21 | ],
22 |
23 | sidebar: [
24 | {
25 | text: 'Examples',
26 | items: [
27 | { text: 'Basic', link: '/examples/' },
28 | { text: 'Stroke', link: '/examples/stroke' },
29 | ],
30 | },
31 | {
32 | text: 'API',
33 | collapsed: true,
34 | items: typedocSidebar,
35 | },
36 | ],
37 |
38 | socialLinks: [{ icon: 'github', link: repository.url.replace('.git', '') }],
39 |
40 | footer: {
41 | message: `Released under the ${license} license.`,
45 | copyright: `Copyright © 2019-present ${author.name}`,
46 | },
47 |
48 | editLink: {
49 | pattern: `${repository.url.replace('.git', '')}/edit/main/docs/:path`,
50 | },
51 |
52 | search: {
53 | provider: 'local',
54 | },
55 | },
56 | });
57 |
--------------------------------------------------------------------------------
/docs/.vitepress/theme/index.ts:
--------------------------------------------------------------------------------
1 | import Theme from 'vitepress/theme';
2 | import { createTypedChart } from 'vue-chartjs';
3 | import { Tooltip, LinearScale } from 'chart.js';
4 | import { WordCloudController, WordElement } from '../../../src';
5 |
6 | export default {
7 | ...Theme,
8 | enhanceApp({ app }) {
9 | app.component(
10 | 'WordCloudChart',
11 | createTypedChart('wordCloud', [Tooltip, LinearScale, WordCloudController, WordElement])
12 | );
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/docs/examples/basic.ts:
--------------------------------------------------------------------------------
1 | import type { ChartConfiguration } from 'chart.js';
2 | import {} from '../../src';
3 |
4 | // #region data
5 |
6 | const words = [
7 | { key: 'word', value: 10 },
8 | { key: 'words', value: 8 },
9 | { key: 'sprite', value: 7 },
10 | { key: 'placed', value: 5 },
11 | { key: 'layout', value: 4 },
12 | { key: 'algorithm', value: 4 },
13 | { key: 'area', value: 4 },
14 | { key: 'without', value: 3 },
15 | { key: 'step', value: 3 },
16 | { key: 'bounding', value: 3 },
17 | { key: 'retrieve', value: 3 },
18 | { key: 'operation', value: 3 },
19 | { key: 'collision', value: 3 },
20 | { key: 'candidate', value: 3 },
21 | { key: '32', value: 2 },
22 | { key: 'placement', value: 2 },
23 | { key: 'time', value: 2 },
24 | { key: 'possible', value: 2 },
25 | { key: 'even', value: 2 },
26 | { key: 'simple', value: 2 },
27 | { key: 'starting', value: 2 },
28 | { key: 'previously', value: 2 },
29 | { key: 'move', value: 2 },
30 | { key: 'perform', value: 2 },
31 | { key: 'hierarchical', value: 2 },
32 | { key: 'draw', value: 2 },
33 | { key: 'pixel', value: 2 },
34 | { key: 'data', value: 2 },
35 | { key: 'separately', value: 2 },
36 | { key: 'expensive', value: 2 },
37 | { key: 'pixels', value: 2 },
38 | { key: 'masks', value: 2 },
39 | { key: 'implementation', value: 2 },
40 | { key: 'detection', value: 2 },
41 | { key: 'larger', value: 2 },
42 | { key: 'whole', value: 2 },
43 | { key: 'comparing', value: 2 },
44 | { key: 'box', value: 2 },
45 | { key: 'large', value: 2 },
46 | { key: 'think', value: 2 },
47 | { key: 'version', value: 2 },
48 | { key: 'single', value: 2 },
49 | { key: 'tree', value: 2 },
50 | { key: 'Cloud', value: 1 },
51 | { key: 'Generator', value: 1 },
52 | { key: 'Works', value: 1 },
53 | { key: 'positioning', value: 1 },
54 | { key: 'overlap', value: 1 },
55 | { key: 'available', value: 1 },
56 | { key: 'GitHub', value: 1 },
57 | { key: 'open', value: 1 },
58 | { key: 'source', value: 1 },
59 | { key: 'license', value: 1 },
60 | { key: 'd3cloud', value: 1 },
61 | { key: 'Note', value: 1 },
62 | { key: 'code', value: 1 },
63 | { key: 'converting', value: 1 },
64 | { key: 'text', value: 1 },
65 | { key: 'rendering', value: 1 },
66 | { key: 'final', value: 1 },
67 | { key: 'output', value: 1 },
68 | { key: 'requires', value: 1 },
69 | { key: 'additional', value: 1 },
70 | { key: 'development', value: 1 },
71 | { key: 'quite', value: 1 },
72 | { key: 'slow', value: 1 },
73 | { key: 'hundred', value: 1 },
74 | { key: 'run', value: 1 },
75 | { key: 'asynchronously', value: 1 },
76 | { key: 'configurable', value: 1 },
77 | { key: 'size', value: 1 },
78 | { key: 'makes', value: 1 },
79 | { key: 'animate', value: 1 },
80 | { key: 'stuttering', value: 1 },
81 | { key: 'recommended', value: 1 },
82 | { key: 'always', value: 1 },
83 | { key: 'use', value: 1 },
84 | { key: 'animations', value: 1 },
85 | { key: 'prevents', value: 1 },
86 | { key: 'browsers', value: 1 },
87 | { key: 'event', value: 1 },
88 | { key: 'loop', value: 1 },
89 | { key: 'blocking', value: 1 },
90 | { key: 'placing', value: 1 },
91 | { key: 'incredibly', value: 1 },
92 | { key: 'important', value: 1 },
93 | { key: 'Attempt', value: 1 },
94 | { key: 'place', value: 1 },
95 | { key: 'point', value: 1 },
96 | { key: 'usually', value: 1 },
97 | { key: 'near', value: 1 },
98 | { key: 'middle', value: 1 },
99 | { key: 'somewhere', value: 1 },
100 | { key: 'central', value: 1 },
101 | { key: 'horizontal', value: 1 },
102 | { key: 'line', value: 1 },
103 | { key: 'intersects', value: 1 },
104 | { key: 'one', value: 1 },
105 | { key: 'along', value: 1 },
106 | { key: 'increasing', value: 1 },
107 | { key: 'spiral', value: 1 },
108 | { key: 'Repeat', value: 1 },
109 | { key: 'intersections', value: 1 },
110 | { key: 'found', value: 1 },
111 | { key: 'hard', value: 1 },
112 | { key: 'part', value: 1 },
113 | { key: 'making', value: 1 },
114 | { key: 'efficiently', value: 1 },
115 | { key: 'According', value: 1 },
116 | { key: 'Jonathan', value: 1 },
117 | { key: 'Feinberg', value: 1 },
118 | { key: 'Wordle', value: 1 },
119 | { key: 'uses', value: 1 },
120 | { key: 'combination', value: 1 },
121 | { key: 'boxes', value: 1 },
122 | { key: 'quadtrees', value: 1 },
123 | { key: 'achieve', value: 1 },
124 | { key: 'reasonable', value: 1 },
125 | { key: 'speeds', value: 1 },
126 | { key: 'Glyphs', value: 1 },
127 | { key: 'JavaScript', value: 1 },
128 | { key: 'isnt', value: 1 },
129 | { key: 'way', value: 1 },
130 | { key: 'precise', value: 1 },
131 | { key: 'glyph', value: 1 },
132 | { key: 'shapes', value: 1 },
133 | { key: 'via', value: 1 },
134 | { key: 'DOM', value: 1 },
135 | { key: 'except', value: 1 },
136 | { key: 'perhaps', value: 1 },
137 | { key: 'SVG', value: 1 },
138 | { key: 'fonts', value: 1 },
139 | { key: 'Instead', value: 1 },
140 | { key: 'hidden', value: 1 },
141 | { key: 'canvas', value: 1 },
142 | { key: 'element', value: 1 },
143 | { key: 'Retrieving', value: 1 },
144 | { key: 'many', value: 1 },
145 | { key: 'batch', value: 1 },
146 | { key: 'Sprites', value: 1 },
147 | { key: 'initial', value: 1 },
148 | { key: 'performed', value: 1 },
149 | { key: 'using', value: 1 },
150 | { key: 'doesnt', value: 1 },
151 | { key: 'copy', value: 1 },
152 | { key: 'appropriate', value: 1 },
153 | { key: 'position', value: 1 },
154 | { key: 'representing', value: 1 },
155 | { key: 'advantage', value: 1 },
156 | { key: 'involves', value: 1 },
157 | { key: 'relevant', value: 1 },
158 | { key: 'rather', value: 1 },
159 | { key: 'previous', value: 1 },
160 | { key: 'Somewhat', value: 1 },
161 | { key: 'surprisingly', value: 1 },
162 | { key: 'lowlevel', value: 1 },
163 | { key: 'hack', value: 1 },
164 | { key: 'made', value: 1 },
165 | { key: 'tremendous', value: 1 },
166 | { key: 'difference', value: 1 },
167 | { key: 'constructing', value: 1 },
168 | { key: 'compressed', value: 1 },
169 | { key: 'blocks', value: 1 },
170 | { key: '1bit', value: 1 },
171 | { key: '32bit', value: 1 },
172 | { key: 'integers', value: 1 },
173 | { key: 'thus', value: 1 },
174 | { key: 'reducing', value: 1 },
175 | { key: 'number', value: 1 },
176 | { key: 'checks', value: 1 },
177 | { key: 'memory', value: 1 },
178 | { key: 'times', value: 1 },
179 | { key: 'fact', value: 1 },
180 | { key: 'turned', value: 1 },
181 | { key: 'beat', value: 1 },
182 | { key: 'quadtree', value: 1 },
183 | { key: 'everything', value: 1 },
184 | { key: 'tried', value: 1 },
185 | { key: 'areas', value: 1 },
186 | { key: 'font', value: 1 },
187 | { key: 'sizes', value: 1 },
188 | { key: 'primarily', value: 1 },
189 | { key: 'needs', value: 1 },
190 | { key: 'test', value: 1 },
191 | { key: 'per', value: 1 },
192 | { key: 'whereas', value: 1 },
193 | { key: 'compare', value: 1 },
194 | { key: 'every', value: 1 },
195 | { key: 'overlaps', value: 1 },
196 | { key: 'slightly', value: 1 },
197 | { key: 'Another', value: 1 },
198 | { key: 'possibility', value: 1 },
199 | { key: 'merge', value: 1 },
200 | { key: 'fairly', value: 1 },
201 | { key: 'though', value: 1 },
202 | { key: 'compared', value: 1 },
203 | { key: 'analagous', value: 1 },
204 | { key: 'mask', value: 1 },
205 | { key: 'essentially', value: 1 },
206 | { key: 'ORing', value: 1 },
207 | { key: 'block', value: 1 },
208 | ];
209 |
210 | export const data: ChartConfiguration<'wordCloud'>['data'] = {
211 | labels: words.map((d) => d.key),
212 | datasets: [
213 | {
214 | label: '',
215 | data: words.map((d) => 10 + d.value * 10),
216 | },
217 | ],
218 | };
219 |
220 | // #endregion
221 |
222 | // #region config
223 | export const config: ChartConfiguration<'wordCloud'> = {
224 | type: 'wordCloud',
225 | data,
226 | };
227 | // #endregion config
228 |
229 | // #region stroke
230 | export const stroke: ChartConfiguration<'wordCloud'> = {
231 | type: 'wordCloud',
232 | data,
233 | options: {
234 | elements: {
235 | word: {
236 | strokeStyle: 'red',
237 | strokeWidth: 8,
238 | },
239 | },
240 | },
241 | };
242 | // #endregion stroke
243 |
--------------------------------------------------------------------------------
/docs/examples/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Examples
3 | ---
4 |
5 | # Examples
6 |
7 |
10 |
11 | ## Word Cloud
12 |
13 |
14 |
18 |
19 |
20 | ### Code
21 |
22 | :::code-group
23 |
24 | <<< ./basic.ts#config [config]
25 |
26 | <<< ./basic.ts#data [data]
27 |
28 | :::
29 |
--------------------------------------------------------------------------------
/docs/examples/stroke.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Stroke
3 | ---
4 |
5 | # Stroke
6 |
7 |
10 |
11 | ## Word Cloud
12 |
13 |
14 |
18 |
19 |
20 | ### Code
21 |
22 | :::code-group
23 |
24 | <<< ./basic.ts#stroke [config]
25 |
26 | <<< ./basic.ts#data [data]
27 |
28 | :::
29 |
--------------------------------------------------------------------------------
/docs/getting-started.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Getting Started
3 | ---
4 |
5 | Chart.js module for charting word or tag clouds. Adding new chart type: `wordCloud`.
6 |
7 | 
8 |
9 | ## Install
10 |
11 | ```sh
12 | npm install chart.js chartjs-chart-wordcloud
13 | ```
14 |
15 | ## Usage
16 |
17 | see [Examples](./examples/)
18 |
19 | and [CodePen](https://codepen.io/sgratzl/pen/WNwzYgy)
20 |
21 | ## Configuration
22 |
23 | ### Data Structure
24 |
25 | ```ts
26 | const config = {
27 | type: 'wordCloud',
28 | data: {
29 | // text
30 | labels: ['Hello', 'world', 'normally', 'you', 'want', 'more', 'words', 'than', 'this'],
31 | datasets: [
32 | {
33 | label: 'DS',
34 | // size in pixel
35 | data: [90, 80, 70, 60, 50, 40, 30, 20, 10],
36 | },
37 | ],
38 | },
39 | options: {},
40 | };
41 | ```
42 |
43 | ### Styling of elements
44 |
45 | A word has the basic FontSpec styling options (family, color, ...). In addition it has several options regarding rotating the text.
46 |
47 | Controller options: [IWordControllerOptions](/api/interfaces/IWordCloudControllerDatasetOptions.html)
48 | Word element options: [IWordElementOptions](/api/interfaces/IWordElementOptions.html)
49 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | # https://vitepress.dev/reference/default-theme-home-page
3 | layout: home
4 |
5 | hero:
6 | name: 'chartjs-chart-wordcloud'
7 | text: 'chart.js plugin'
8 | tagline: Chart.js module for charting word or tag clouds
9 | actions:
10 | - theme: brand
11 | text: Getting Started
12 | link: /getting-started
13 | - theme: alt
14 | text: Examples
15 | link: /examples/
16 | - theme: alt
17 | text: API
18 | link: /api/
19 | # features:
20 | # - title: Feature A
21 | # details: Lorem ipsum dolor sit amet, consectetur adipiscing elit
22 | ---
23 |
--------------------------------------------------------------------------------
/docs/related.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Related Plugins
3 | ---
4 |
5 | There are several related chart.js plugins providing additional functionality and chart types:
6 |
7 | - [chartjs-chart-boxplot](https://github.com/sgratzl/chartjs-chart-boxplot) for rendering boxplots and violin charts
8 | - [chartjs-chart-error-bars](https://github.com/sgratzl/chartjs-chart-error-bars) for rendering errors bars to bars and line charts
9 | - [chartjs-chart-funnel](https://github.com/sgratzl/chartjs-chart-funnel) for rendering funnel charts
10 | - [chartjs-chart-geo](https://github.com/sgratzl/chartjs-chart-geo) for rendering map, bubble maps, and choropleth charts
11 | - [chartjs-chart-graph](https://github.com/sgratzl/chartjs-chart-graph) for rendering graphs, trees, and networks
12 | - [chartjs-chart-pcp](https://github.com/sgratzl/chartjs-chart-pcp) for rendering parallel coordinate plots
13 | - [chartjs-chart-venn](https://github.com/sgratzl/chartjs-chart-venn) for rendering venn and euler diagrams
14 | - [chartjs-chart-wordcloud](https://github.com/sgratzl/chartjs-chart-wordcloud) for rendering word clouds
15 | - [chartjs-plugin-hierarchical](https://github.com/sgratzl/chartjs-plugin-hierarchical) for rendering hierarchical categorical axes which can be expanded and collapsed
16 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import eslint from '@eslint/js';
4 | import tseslint from 'typescript-eslint';
5 | import prettier from 'eslint-plugin-prettier';
6 |
7 | export default tseslint.config(eslint.configs.recommended, ...tseslint.configs.recommended, {
8 | plugins: { prettier },
9 | rules: {
10 | '@typescript-eslint/no-explicit-any': 'off',
11 | 'max-classes-per-file': 'off',
12 | 'no-underscore-dangle': 'off',
13 | 'import/extensions': 'off',
14 | },
15 | });
16 |
17 | // import path from "node:path";
18 | // import { fileURLToPath } from "node:url";
19 | // import js from "@eslint/js";
20 | // import { FlatCompat } from "@eslint/eslintrc";
21 |
22 | // const __filename = fileURLToPath(import.meta.url);
23 | // const __dirname = path.dirname(__filename);
24 | // const compat = new FlatCompat({
25 | // baseDirectory: __dirname,
26 | // recommendedConfig: js.configs.recommended,
27 | // allConfig: js.configs.all
28 | // });
29 |
30 | // export default [...fixupConfigRules(compat.extends(
31 | // "airbnb-typescript",
32 | // "react-app",
33 | // "plugin:prettier/recommended",
34 | // "prettier",
35 | // )), {
36 | // plugins: {
37 | // prettier: fixupPluginRules(prettier),
38 | // },
39 |
40 | // languageOptions: {
41 | // ecmaVersion: 5,
42 | // sourceType: "script",
43 |
44 | // parserOptions: {
45 | // project: "./tsconfig.eslint.json",
46 | // },
47 | // },
48 |
49 | // settings: {
50 | // react: {
51 | // version: "99.99.99",
52 | // },
53 | // },
54 |
55 | // rules: {
56 | // "@typescript-eslint/no-explicit-any": "off",
57 | // "max-classes-per-file": "off",
58 | // "no-underscore-dangle": "off",
59 | // "import/extensions": "off",
60 | // },
61 | // }];
62 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chartjs-chart-wordcloud",
3 | "description": "Chart.js module for word clouds",
4 | "version": "4.4.4",
5 | "author": {
6 | "name": "Samuel Gratzl",
7 | "email": "sam@sgratzl.com",
8 | "url": "https://www.sgratzl.com"
9 | },
10 | "license": "MIT",
11 | "homepage": "https://github.com/sgratzl/chartjs-chart-wordcloud",
12 | "bugs": {
13 | "url": "https://github.com/sgratzl/chartjs-chart-wordcloud/issues"
14 | },
15 | "keywords": [
16 | "chart.js",
17 | "word-cloud",
18 | "tag-cloud"
19 | ],
20 | "repository": {
21 | "type": "git",
22 | "url": "https://github.com/sgratzl/chartjs-chart-wordcloud.git"
23 | },
24 | "global": "ChartWordCloud",
25 | "type": "module",
26 | "main": "build/index.js",
27 | "module": "build/index.js",
28 | "require": "build/index.cjs",
29 | "umd": "build/index.umd.js",
30 | "unpkg": "build/index.umd.min.js",
31 | "jsdelivr": "build/index.umd.min.js",
32 | "types": "build/index.d.ts",
33 | "exports": {
34 | ".": {
35 | "import": "./build/index.js",
36 | "require": "./build/index.cjs",
37 | "scripts": "./build/index.umd.min.js",
38 | "types": "./build/index.d.ts"
39 | }
40 | },
41 | "sideEffects": false,
42 | "files": [
43 | "build",
44 | "src/**/*.ts"
45 | ],
46 | "peerDependencies": {
47 | "chart.js": "^4.1.0"
48 | },
49 | "browserslist": [
50 | "Firefox ESR",
51 | "last 2 Chrome versions",
52 | "last 2 Firefox versions"
53 | ],
54 | "devDependencies": {
55 | "@chiogen/rollup-plugin-terser": "^7.1.3",
56 | "@eslint/js": "~9.14.0",
57 | "@rollup/plugin-commonjs": "^28.0.1",
58 | "@rollup/plugin-node-resolve": "^15.3.0",
59 | "@rollup/plugin-replace": "^6.0.1",
60 | "@rollup/plugin-typescript": "^12.1.1",
61 | "@types/jest-image-snapshot": "^6.4.0",
62 | "@types/node": "^22.9.0",
63 | "@yarnpkg/sdks": "^3.2.0",
64 | "canvas": "^2.11.2",
65 | "canvas-5-polyfill": "^0.1.5",
66 | "chart.js": "^4.4.6",
67 | "eslint": "~9.14.0",
68 | "eslint-plugin-prettier": "^5.2.1",
69 | "jest-image-snapshot": "^6.4.0",
70 | "jsdom": "^25.0.1",
71 | "prettier": "^3.3.3",
72 | "rimraf": "^6.0.1",
73 | "rollup": "^4.27.2",
74 | "rollup-plugin-cleanup": "^3.2.1",
75 | "rollup-plugin-dts": "^6.1.1",
76 | "ts-jest": "^29.2.5",
77 | "tslib": "^2.8.1",
78 | "typedoc": "^0.26.11",
79 | "typedoc-plugin-markdown": "^4.2.10",
80 | "typedoc-vitepress-theme": "^1.0.2",
81 | "typescript": "^5.6.3",
82 | "typescript-eslint": "^8.14.0",
83 | "vite": "^5.4.11",
84 | "vitepress": "^1.5.0",
85 | "vitest": "^2.1.5",
86 | "vue": "^3.5.13",
87 | "vue-chartjs": "^5.3.2"
88 | },
89 | "scripts": {
90 | "clean": "rimraf --glob build node_modules \"*.tgz\" \"*.tsbuildinfo\"",
91 | "compile": "tsc -b tsconfig.c.json",
92 | "start": "yarn run watch",
93 | "watch": "rollup -c -w",
94 | "build": "rollup -c",
95 | "test": "vitest --passWithNoTests",
96 | "test:watch": "yarn run test --watch",
97 | "test:coverage": "yarn run test --coverage",
98 | "lint": "yarn run eslint && yarn run prettier",
99 | "fix": "yarn run eslint:fix && yarn run prettier:write",
100 | "prettier:write": "prettier \"*\" \"*/**\" --write",
101 | "prettier": "prettier \"*\" \"*/**\" --check",
102 | "eslint": "eslint src --cache",
103 | "eslint:fix": "yarn run eslint --fix",
104 | "prepare": "yarn run build",
105 | "docs:api": "typedoc --options typedoc.json",
106 | "docs:dev": "vitepress dev docs",
107 | "docs:build": "yarn run docs:api && vitepress build docs",
108 | "docs:preview": "vitepress preview docs"
109 | },
110 | "dependencies": {
111 | "@types/d3-cloud": "^1.2.9",
112 | "d3-cloud": "^1.2.7"
113 | },
114 | "packageManager": "yarn@4.5.1"
115 | }
116 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import commonjs from '@rollup/plugin-commonjs';
2 | import resolve from '@rollup/plugin-node-resolve';
3 | import cleanup from 'rollup-plugin-cleanup';
4 | import dts from 'rollup-plugin-dts';
5 | import typescript from '@rollup/plugin-typescript';
6 | import { terser } from '@chiogen/rollup-plugin-terser';
7 | import replace from '@rollup/plugin-replace';
8 |
9 | import fs from 'fs';
10 |
11 | const pkg = JSON.parse(fs.readFileSync('./package.json'));
12 |
13 | function resolveYear() {
14 | // Extract copyrights from the LICENSE.
15 | const license = fs.readFileSync('./LICENSE', 'utf-8').toString();
16 | const matches = Array.from(license.matchAll(/\(c\) (\d+-\d+)/gm));
17 | if (!matches || matches.length === 0) {
18 | return 2021;
19 | }
20 | return matches[matches.length - 1][1];
21 | }
22 | const year = resolveYear();
23 |
24 | const banner = `/**
25 | * ${pkg.name}
26 | * ${pkg.homepage}
27 | *
28 | * Copyright (c) ${year} ${pkg.author.name} <${pkg.author.email}>
29 | */
30 | `;
31 |
32 | /**
33 | * defines which formats (umd, esm, cjs, types) should be built when watching
34 | */
35 | const watchOnly = ['umd'];
36 |
37 | const isDependency = (v) => Object.keys(pkg.dependencies || {}).some((e) => e === v || v.startsWith(`${e}/`));
38 | const isPeerDependency = (v) => Object.keys(pkg.peerDependencies || {}).some((e) => e === v || v.startsWith(`${e}/`));
39 |
40 | export default function Config(options) {
41 | const buildFormat = (format) => {
42 | return !options.watch || watchOnly.includes(format);
43 | };
44 | const commonOutput = {
45 | sourcemap: true,
46 | banner,
47 | globals: {
48 | 'chart.js': 'Chart',
49 | 'chart.js/helpers': 'Chart.helpers',
50 | },
51 | };
52 |
53 | const base = {
54 | input: './src/index.ts',
55 | external: (v) => isDependency(v) || isPeerDependency(v),
56 | plugins: [
57 | typescript(),
58 | resolve({
59 | mainFields: ['module', 'main'],
60 | extensions: ['.mjs', '.cjs', '.js', '.jsx', '.json', '.node'],
61 | // modulesOnly: true,
62 | }),
63 | commonjs(),
64 | replace({
65 | preventAssignment: true,
66 | values: {
67 | // eslint-disable-next-line no-undef
68 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) || 'production',
69 | __VERSION__: JSON.stringify(pkg.version),
70 | },
71 | }),
72 | cleanup({
73 | comments: ['some', 'ts', 'ts3s'],
74 | extensions: ['ts', 'tsx', 'js', 'jsx'],
75 | include: './src/**/*',
76 | }),
77 | ],
78 | };
79 | return [
80 | buildFormat('esm') && {
81 | ...base,
82 | output: {
83 | ...commonOutput,
84 | file: pkg.module,
85 | format: 'esm',
86 | },
87 | },
88 | buildFormat('cjs') && {
89 | ...base,
90 | output: {
91 | ...commonOutput,
92 | file: pkg.require,
93 | format: 'cjs',
94 | },
95 | external: (v) => (isDependency(v) || isPeerDependency(v)) && ['d3-'].every((di) => !v.includes(di)),
96 | },
97 | (buildFormat('umd') || buildFormat('umd-min')) && {
98 | ...base,
99 | input: './src/index.umd.ts',
100 | output: [
101 | buildFormat('umd') && {
102 | ...commonOutput,
103 | file: pkg.umd,
104 | format: 'umd',
105 | name: pkg.global,
106 | },
107 | buildFormat('umd-min') && {
108 | ...commonOutput,
109 | file: pkg.unpkg,
110 | format: 'umd',
111 | name: pkg.global,
112 | plugins: [terser()],
113 | },
114 | ].filter(Boolean),
115 | external: (v) => isPeerDependency(v),
116 | },
117 | buildFormat('types') && {
118 | ...base,
119 | output: {
120 | ...commonOutput,
121 | file: pkg.types,
122 | format: 'es',
123 | },
124 | plugins: [
125 | dts({
126 | respectExternal: true,
127 | compilerOptions: {
128 | skipLibCheck: true,
129 | skipDefaultLibCheck: true,
130 | },
131 | }),
132 | ],
133 | },
134 | ].filter(Boolean);
135 | }
136 |
--------------------------------------------------------------------------------
/samples/default.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Sample
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
245 |
246 |
247 |
--------------------------------------------------------------------------------
/src/__tests__/createChart.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import { expect } from 'vitest';
4 | import { Chart, ChartConfiguration, defaults, ChartType, DefaultDataPoint } from 'chart.js';
5 | import { toMatchImageSnapshot, MatchImageSnapshotOptions } from 'jest-image-snapshot';
6 | import 'canvas-5-polyfill';
7 |
8 | expect.extend({ toMatchImageSnapshot });
9 |
10 | function toBuffer(canvas: HTMLCanvasElement) {
11 | return new Promise((resolve) => {
12 | canvas.toBlob((b) => {
13 | const file = new FileReader();
14 | file.onload = () => resolve(Buffer.from(file.result as ArrayBuffer));
15 |
16 | file.readAsArrayBuffer(b!);
17 | });
18 | });
19 | }
20 |
21 | export async function expectMatchSnapshot(canvas: HTMLCanvasElement): Promise {
22 | const image = await toBuffer(canvas);
23 | expect(image).toMatchImageSnapshot();
24 | }
25 |
26 | export interface ChartHelper, LABEL = string> {
27 | chart: Chart;
28 | canvas: HTMLCanvasElement;
29 | ctx: CanvasRenderingContext2D;
30 | toMatchImageSnapshot(options?: MatchImageSnapshotOptions): Promise;
31 | }
32 |
33 | export default function createChart<
34 | TYPE extends ChartType,
35 | DATA extends unknown[] = DefaultDataPoint,
36 | LABEL = string,
37 | >(config: ChartConfiguration, width = 800, height = 600): ChartHelper {
38 | const canvas = document.createElement('canvas');
39 | canvas.width = width;
40 | canvas.height = height;
41 | Object.assign(defaults.font, { family: "'Courier New', sans-serif" });
42 | // defaults.color = 'transparent';
43 |
44 | config.options = {
45 | responsive: false,
46 | animation: {
47 | duration: 1,
48 | },
49 | plugins: {
50 | legend: {
51 | display: false,
52 | },
53 | title: {
54 | display: false,
55 | },
56 | },
57 | ...(config.options || {}),
58 | } as any;
59 |
60 | const ctx = canvas.getContext('2d')!;
61 |
62 | const t = new Chart(ctx, config);
63 |
64 | return {
65 | chart: t,
66 | canvas,
67 | ctx,
68 | async toMatchImageSnapshot(options?: MatchImageSnapshotOptions) {
69 | await new Promise((resolve) => setTimeout(resolve, 100));
70 |
71 | const image = await toBuffer(canvas);
72 | expect(image).toMatchImageSnapshot(options);
73 | },
74 | };
75 | }
76 |
--------------------------------------------------------------------------------
/src/controllers/WordCloudController.spec.ts:
--------------------------------------------------------------------------------
1 | import { LinearScale, registry } from 'chart.js';
2 | import { WordCloudController } from './WordCloudController';
3 | import { WordElement } from '../elements';
4 | import createChart from '../__tests__/createChart';
5 | import { describe, beforeAll, test } from 'vitest';
6 | describe('default', () => {
7 | beforeAll(() => {
8 | registry.addControllers(WordCloudController);
9 | registry.addElements(WordElement);
10 | registry.addScales(LinearScale);
11 | });
12 | test('default', () => {
13 | const words = ['Hello', 'world', 'normally', 'you', 'want', 'more', 'words', 'than', 'this'];
14 | const data = {
15 | labels: words,
16 | datasets: [
17 | {
18 | label: '',
19 | data: words.map((_, i) => 10 + (i / words.length) * 90),
20 | randomRotationSeed: 'x',
21 | },
22 | ],
23 | };
24 | const chart = createChart(
25 | {
26 | type: WordCloudController.id,
27 | data,
28 | options: {},
29 | },
30 | 1000,
31 | 500
32 | );
33 |
34 | return chart.toMatchImageSnapshot({
35 | failureThreshold: 1.85,
36 | failureThresholdType: 'percent',
37 | });
38 | });
39 | });
40 |
--------------------------------------------------------------------------------
/src/controllers/WordCloudController.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AnimationOptions,
3 | Chart,
4 | DatasetController,
5 | UpdateMode,
6 | ChartItem,
7 | ScriptableAndArrayOptions,
8 | ControllerDatasetOptions,
9 | CommonHoverOptions,
10 | ChartConfiguration,
11 | ScriptableContext,
12 | VisualElement,
13 | CartesianScaleTypeRegistry,
14 | CoreChartOptions,
15 | } from 'chart.js';
16 | import { toFont } from 'chart.js/helpers';
17 | import layout from 'd3-cloud';
18 | import { WordElement, IWordElementOptions, IWordElementProps } from '../elements';
19 | import patchController from './patchController';
20 |
21 | function rnd(seed: string | number = Date.now()) {
22 | // Adapted from http://indiegamr.com/generate-repeatable-random-numbers-in-js/
23 | let s = typeof seed === 'number' ? seed : Array.from(seed).reduce((acc, v) => acc + v.charCodeAt(0), 0);
24 | return () => {
25 | s = (s * 9301 + 49297) % 233280;
26 | return s / 233280;
27 | };
28 | }
29 |
30 | interface ICloudWord extends IWordElementProps {
31 | options: IWordElementOptions;
32 | index: number;
33 | }
34 |
35 | export class WordCloudController extends DatasetController<'wordCloud', WordElement> {
36 | static readonly id = 'wordCloud';
37 |
38 | /**
39 | * @hidden
40 | */
41 | static readonly defaults = /* #__PURE__ */ {
42 | datasets: {
43 | animation: {
44 | colors: {
45 | properties: ['color', 'strokeStyle'],
46 | },
47 | numbers: {
48 | properties: ['x', 'y', 'size', 'rotate'],
49 | },
50 | },
51 | },
52 | maintainAspectRatio: false,
53 | dataElementType: WordElement.id,
54 | };
55 |
56 | /**
57 | * @hidden
58 | */
59 | static readonly overrides = /* #__PURE__ */ {
60 | scales: {
61 | x: {
62 | type: 'linear',
63 | min: -1,
64 | max: 1,
65 | display: false,
66 | },
67 | y: {
68 | type: 'linear',
69 | min: -1,
70 | max: 1,
71 | display: false,
72 | },
73 | },
74 | };
75 |
76 | private readonly wordLayout = layout()
77 | .text((d) => d.text)
78 | .padding((d) => d.options.padding)
79 | .rotate((d) => d.options.rotate)
80 | .font((d) => d.options.family)
81 | .fontSize((d) => d.options.size)
82 | .fontStyle((d) => d.options.style)
83 | .fontWeight((d) => d.options.weight ?? 1);
84 |
85 | /**
86 | * @hidden
87 | */
88 | rand: () => number = Math.random;
89 |
90 | /**
91 | * @hidden
92 | */
93 | update(mode: UpdateMode): void {
94 | super.update(mode);
95 | const dsOptions = (this as any).options as IWordCloudControllerDatasetOptions;
96 | this.rand = rnd(dsOptions.randomRotationSeed ?? this.chart.id);
97 | const meta = this._cachedMeta;
98 |
99 | const elems = (meta.data || []) as unknown as WordElement[];
100 | this.updateElements(elems, 0, elems.length, mode);
101 | }
102 |
103 | /**
104 | * @hidden
105 | */
106 | updateElements(elems: WordElement[], start: number, count: number, mode: UpdateMode): void {
107 | this.wordLayout.stop();
108 | const dsOptions = (this as any).options as IWordCloudControllerDatasetOptions;
109 | const xScale = this._cachedMeta.xScale as { left: number; right: number };
110 | const yScale = this._cachedMeta.yScale as { top: number; bottom: number };
111 |
112 | const w = xScale.right - xScale.left;
113 | const h = yScale.bottom - yScale.top;
114 | const labels = this.chart.data.labels as string[];
115 |
116 | const growOptions: IAutoGrowOptions = {
117 | maxTries: 3,
118 | scalingFactor: 1.2,
119 | };
120 | // update with configured options
121 | Object.assign(growOptions, dsOptions?.autoGrow ?? {});
122 |
123 | const words: (ICloudWord & Record)[] = [];
124 | for (let i = start; i < start + count; i += 1) {
125 | const o = this.resolveDataElementOptions(i, mode) as unknown as IWordElementOptions;
126 | if (o.rotate == null) {
127 | o.rotate = WordElement.computeRotation(o, this.rand);
128 | }
129 | const properties: ICloudWord & Record = {
130 | options: { ...toFont(o), ...o },
131 | x: this._cachedMeta.xScale?.getPixelForDecimal(0.5) ?? 0,
132 | y: this._cachedMeta.yScale?.getPixelForDecimal(0.5) ?? 0,
133 | width: 10,
134 | height: 10,
135 | scale: 1,
136 | index: i,
137 | text: labels[i],
138 | };
139 | words.push(properties);
140 | }
141 | if (mode === 'reset') {
142 | words.forEach((tag) => {
143 | this.updateElement(elems[tag.index], tag.index, tag, mode);
144 | });
145 | return;
146 | }
147 | // syncish since no time limit is set
148 | this.wordLayout.random(this.rand).words(words);
149 |
150 | const run = (factor = 1, tries = growOptions.maxTries): void => {
151 | this.wordLayout
152 | .size([w * factor, h * factor])
153 | .on('end', (tags, bounds) => {
154 | if (tags.length < labels.length) {
155 | if (tries > 0) {
156 | // try again with a factor of 1.2
157 | const f =
158 | typeof growOptions.scalingFactor === 'function'
159 | ? growOptions.scalingFactor(factor, tags, labels.length)
160 | : factor * growOptions.scalingFactor;
161 | run(f, tries - 1);
162 | return;
163 | }
164 |
165 | console.warn(`cannot fit all text elements in ${growOptions.maxTries} tries`);
166 | }
167 | const wb = bounds[1].x - bounds[0].x;
168 | const hb = bounds[1].y - bounds[0].y;
169 |
170 | const scale = dsOptions.fit ? Math.min(w / wb, h / hb) : 1;
171 | const indices = new Set(labels.map((_, i) => i));
172 | tags.forEach((tag) => {
173 | indices.delete(tag.index);
174 | this.updateElement(
175 | elems[tag.index],
176 | tag.index,
177 | {
178 | options: tag.options,
179 | scale,
180 | x: xScale.left + scale * tag.x + w / 2,
181 | y: yScale.top + scale * tag.y + h / 2,
182 | width: scale * tag.width,
183 | height: scale * tag.height,
184 | text: tag.text,
185 | },
186 | mode
187 | );
188 | });
189 | // hide rest
190 | indices.forEach((i) => this.updateElement(elems[i], i, { scale: 0 }, mode));
191 | })
192 | .start();
193 | };
194 | run();
195 | }
196 |
197 | /**
198 | * @hidden
199 | */
200 | draw(): void {
201 | const elements = this._cachedMeta.data as unknown as VisualElement[];
202 | const { ctx } = this.chart;
203 | elements.forEach((elem) => elem.draw(ctx));
204 | }
205 |
206 | /**
207 | * @hidden
208 | */
209 | getLabelAndValue(index: number): { label: string; value: any } {
210 | const r = super.getLabelAndValue(index);
211 | const labels = this.chart.data.labels as string[];
212 | r.label = labels[index];
213 | return r;
214 | }
215 | }
216 |
217 | export interface IAutoGrowOptions {
218 | /**
219 | * @default 3
220 | */
221 | maxTries: number;
222 | /**
223 | * @default 1.2
224 | */
225 | scalingFactor: number | ((currentFactor: number, fitted: ICloudWord[], total: number) => number);
226 | }
227 |
228 | export interface IWordCloudControllerDatasetOptions
229 | extends ControllerDatasetOptions,
230 | ScriptableAndArrayOptions>,
231 | ScriptableAndArrayOptions>,
232 | AnimationOptions<'wordCloud'> {
233 | /**
234 | * whether to fit the word cloud to the map, by scaling to the actual bounds
235 | * @default false
236 | */
237 | fit: boolean;
238 |
239 | /**
240 | * configures the automatic growing of the canvas in case not all words can be fitted onto the screen
241 | * @default { maxTries: 3, scalingFactor: 1.2}
242 | */
243 | autoGrow: IAutoGrowOptions;
244 |
245 | /**
246 | * specifies the random seed that should be used for randomly rotating words if needed
247 | * @default the current chart id
248 | */
249 | randomRotationSeed: string;
250 | }
251 |
252 | declare module 'chart.js' {
253 | interface ChartTypeRegistry {
254 | wordCloud: {
255 | chartOptions: CoreChartOptions<'wordCloud'>;
256 | datasetOptions: IWordCloudControllerDatasetOptions;
257 | defaultDataPoint: number;
258 | metaExtensions: Record;
259 | parsedDataType: { x: number };
260 | scales: keyof CartesianScaleTypeRegistry;
261 | };
262 | }
263 | }
264 |
265 | export class WordCloudChart extends Chart<'wordCloud', DATA, LABEL> {
266 | static id = WordCloudController.id;
267 |
268 | constructor(item: ChartItem, config: Omit, 'type'>) {
269 | super(item, patchController('wordCloud', config, WordCloudController, WordElement));
270 | }
271 | }
272 |
--------------------------------------------------------------------------------
/src/controllers/__image_snapshots__/word-cloud-controller-spec-ts-default-default-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sgratzl/chartjs-chart-wordcloud/5daf1a0dc9cd0af4a1d1c4b20fb6f2c6f0b51a34/src/controllers/__image_snapshots__/word-cloud-controller-spec-ts-default-default-1-snap.png
--------------------------------------------------------------------------------
/src/controllers/index.ts:
--------------------------------------------------------------------------------
1 | export * from './WordCloudController';
2 |
--------------------------------------------------------------------------------
/src/controllers/patchController.ts:
--------------------------------------------------------------------------------
1 | import { registry, DatasetControllerChartComponent, ChartComponent } from 'chart.js';
2 |
3 | export default function patchController(
4 | type: TYPE,
5 | config: T,
6 | controller: DatasetControllerChartComponent,
7 | elements: ChartComponent | ChartComponent[] = [],
8 | scales: ChartComponent | ChartComponent[] = []
9 | ): T & { type: TYPE } {
10 | registry.addControllers(controller);
11 | if (Array.isArray(elements)) {
12 | registry.addElements(...elements);
13 | } else {
14 | registry.addElements(elements);
15 | }
16 | if (Array.isArray(scales)) {
17 | registry.addScales(...scales);
18 | } else {
19 | registry.addScales(scales);
20 | }
21 | const c = config as any;
22 | c.type = type;
23 | return c;
24 | }
25 |
--------------------------------------------------------------------------------
/src/elements/WordElement.ts:
--------------------------------------------------------------------------------
1 | import { Element, FontSpec, VisualElement, ScriptableAndArrayOptions, ScriptableContext, ChartType } from 'chart.js';
2 | import { toFont } from 'chart.js/helpers';
3 |
4 | export interface IWordElementOptions extends FontSpec, Record {
5 | color: CanvasRenderingContext2D['fillStyle'];
6 | /**
7 | * CanvasContext2D.strokeStyle config for rendering a stroke around the text
8 | * @default undefined
9 | */
10 | strokeStyle: CanvasRenderingContext2D['strokeStyle'];
11 | /**
12 | * CanvasContext2D.lineWith for stroke
13 | * @default undefined
14 | */
15 | strokeWidth?: CanvasRenderingContext2D['lineWidth'];
16 | /**
17 | * rotation of the word
18 | * @default undefined then it will be randomly derived given the other constraints
19 | */
20 | rotate: number;
21 | /**
22 | * number of rotation steps between min and max rotation
23 | * @default 2
24 | */
25 | rotationSteps: number;
26 | /**
27 | * angle in degree for the min rotation
28 | * @default -90
29 | */
30 | minRotation: number;
31 | /**
32 | * angle in degree for the max rotation
33 | * @default 0
34 | */
35 | maxRotation: number;
36 | /**
37 | * padding around each word while doing the layout
38 | * @default 1
39 | */
40 | padding: number;
41 | }
42 |
43 | export interface IWordElementHoverOptions {
44 | /**
45 | * hover variant of color
46 | */
47 | hoverColor: CanvasRenderingContext2D['fillStyle'];
48 | /**
49 | * hover variant of size
50 | */
51 | hoverSize: FontSpec['size'];
52 | /**
53 | * hover variant of style
54 | */
55 | hoverStyle: FontSpec['style'];
56 | /**
57 | * hover variant of weight
58 | */
59 | hoverWeight: FontSpec['weight'];
60 | /**
61 | * hover variant of stroke style
62 | * @default undefined
63 | */
64 | hoverStrokeStyle: CanvasRenderingContext2D['strokeStyle'];
65 | /**
66 | * hover variant of stroke width
67 | * @default undefined
68 | */
69 | hoverStrokeWidth?: CanvasRenderingContext2D['lineWidth'];
70 | }
71 |
72 | export interface IWordElementProps {
73 | x: number;
74 | y: number;
75 | width: number;
76 | height: number;
77 | scale: number;
78 | text: string;
79 | }
80 |
81 | export class WordElement extends Element implements VisualElement {
82 | static readonly id = 'word';
83 |
84 | /**
85 | * @hidden
86 | */
87 | static readonly defaults: any = /* #__PURE__ */ {
88 | // rotate: 0,
89 | minRotation: -90,
90 | maxRotation: 0,
91 | rotationSteps: 2,
92 | padding: 1,
93 | strokeStyle: undefined,
94 | strokeWidth: undefined,
95 | size: (ctx) => {
96 | const v = (ctx.parsed as unknown as { y: number }).y;
97 | return v;
98 | },
99 | hoverColor: '#ababab',
100 | } as Partial>>;
101 |
102 | /**
103 | * @hidden
104 | */
105 | static readonly defaultRoutes = /* #__PURE__ */ {
106 | color: 'color',
107 | family: 'font.family',
108 | style: 'font.style',
109 | weight: 'font.weight',
110 | lineHeight: 'font.lineHeight',
111 | };
112 |
113 | /**
114 | * @hidden
115 | */
116 | static computeRotation(o: IWordElementOptions, rnd: () => number): number {
117 | if (o.rotationSteps <= 1) {
118 | return 0;
119 | }
120 | if (o.minRotation === o.maxRotation) {
121 | return o.minRotation;
122 | }
123 | const base = Math.min(o.rotationSteps, Math.floor(rnd() * o.rotationSteps)) / (o.rotationSteps - 1);
124 | const range = o.maxRotation - o.minRotation;
125 | return o.minRotation + base * range;
126 | }
127 |
128 | /**
129 | * @hidden
130 | */
131 | inRange(mouseX: number, mouseY: number): boolean {
132 | const p = this.getProps(['x', 'y', 'width', 'height', 'scale']);
133 | if (p.scale <= 0) {
134 | return false;
135 | }
136 | const x = Number.isNaN(mouseX) ? p.x : mouseX;
137 | const y = Number.isNaN(mouseY) ? p.y : mouseY;
138 | return x >= p.x - p.width / 2 && x <= p.x + p.width / 2 && y >= p.y - p.height / 2 && y <= p.y + p.height / 2;
139 | }
140 |
141 | /**
142 | * @hidden
143 | */
144 | inXRange(mouseX: number): boolean {
145 | return this.inRange(mouseX, Number.NaN);
146 | }
147 |
148 | /**
149 | * @hidden
150 | */
151 | inYRange(mouseY: number): boolean {
152 | return this.inRange(Number.NaN, mouseY);
153 | }
154 |
155 | /**
156 | * @hidden
157 | */
158 | getCenterPoint(): { x: number; y: number } {
159 | return this.getProps(['x', 'y']);
160 | }
161 |
162 | /**
163 | * @hidden
164 | */
165 | tooltipPosition(): { x: number; y: number } {
166 | return this.getCenterPoint();
167 | }
168 |
169 | /**
170 | * @hidden
171 | */
172 | draw(ctx: CanvasRenderingContext2D): void {
173 | const { options } = this;
174 | const props = this.getProps(['x', 'y', 'width', 'height', 'text', 'scale']);
175 | if (props.scale <= 0) {
176 | return;
177 | }
178 | ctx.save();
179 | const f = toFont({ ...options, size: options.size * props.scale });
180 | ctx.font = f.string;
181 | // console.log(ctx.font);
182 | ctx.fillStyle = options.color;
183 | ctx.textAlign = 'center';
184 | // ctx.textBaseline = 'top';
185 | ctx.translate(props.x, props.y);
186 | // ctx.strokeRect(-props.width / 2, -props.height / 2, props.width, props.height);
187 | ctx.rotate((options.rotate / 180) * Math.PI);
188 | if (options.strokeStyle) {
189 | if (options.strokeWidth != null) {
190 | ctx.lineWidth = options.strokeWidth;
191 | }
192 | ctx.strokeStyle = options.strokeStyle;
193 | ctx.strokeText(props.text, 0, 0);
194 | }
195 | ctx.fillText(props.text, 0, 0);
196 |
197 | ctx.restore();
198 | }
199 | }
200 |
201 | declare module 'chart.js' {
202 | export interface ElementOptionsByType {
203 | word: ScriptableAndArrayOptions>;
204 | }
205 | }
206 |
--------------------------------------------------------------------------------
/src/elements/index.ts:
--------------------------------------------------------------------------------
1 | export * from './WordElement';
2 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './controllers';
2 | export * from './elements';
3 |
--------------------------------------------------------------------------------
/src/index.umd.ts:
--------------------------------------------------------------------------------
1 | import { registry } from 'chart.js';
2 | import { WordCloudController } from './controllers';
3 | import { WordElement } from './elements';
4 |
5 | export * from '.';
6 |
7 | registry.addControllers(WordCloudController);
8 | registry.addElements(WordElement);
9 |
--------------------------------------------------------------------------------
/tsconfig.c.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "build",
5 | "declaration": true,
6 | "declarationMap": true,
7 | "noEmit": true,
8 | "composite": true
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2018",
4 | "module": "ESNext",
5 | "lib": ["DOM", "ES2020"],
6 | "importHelpers": false,
7 | "declaration": false,
8 | "sourceMap": true,
9 | "strict": true,
10 | "removeComments": true,
11 | "verbatimModuleSyntax": false,
12 | "experimentalDecorators": true,
13 | "forceConsistentCasingInFileNames": true,
14 | "strictBindCallApply": true,
15 | "stripInternal": true,
16 | "resolveJsonModule": true,
17 | "noUnusedLocals": true,
18 | "noUnusedParameters": true,
19 | "noImplicitReturns": true,
20 | "noFallthroughCasesInSwitch": true,
21 | "moduleResolution": "Bundler",
22 | "jsx": "react",
23 | "esModuleInterop": true,
24 | "rootDir": "./src",
25 | "baseUrl": "./",
26 | "noEmit": true,
27 | "paths": {
28 | "@": ["./src"],
29 | "*": ["*", "node_modules/*"],
30 | // workaround for: https://github.com/vitest-dev/vitest/issues/4567
31 | "rollup/parseAst": ["./node_modules/rollup/dist/parseAst"]
32 | }
33 | },
34 | "include": ["src/**/*.ts", "src/**/*.tsx", "docs/**/*.tsx"]
35 | }
36 |
--------------------------------------------------------------------------------
/typedoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://typedoc.org/schema.json",
3 | "entryPoints": ["./src"],
4 | "plugin": ["typedoc-plugin-markdown", "typedoc-vitepress-theme"],
5 | "name": "chartjs-chart-wordcloud",
6 | "out": "./docs/api",
7 | "docsRoot": "./docs/",
8 | "readme": "none",
9 | "sidebar": {
10 | "pretty": true
11 | },
12 | "theme": "default",
13 | "excludeExternals": true,
14 | "excludeInternal": true,
15 | "excludePrivate": true,
16 | "includeVersion": true,
17 | "categorizeByGroup": true,
18 | "cleanOutputDir": true,
19 | "hideGenerator": true
20 | }
21 |
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { defineConfig } from 'vite';
3 |
4 | export default defineConfig({
5 | test: {
6 | environment: 'jsdom',
7 | root: './src',
8 | },
9 | });
10 |
--------------------------------------------------------------------------------