├── .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
│ ├── data
│ │ ├── README.md
│ │ └── index.json
│ ├── index.md
│ ├── log.md
│ ├── log.ts
│ ├── tension.md
│ └── tension.ts
├── getting-started.md
├── index.md
└── related.md
├── eslint.config.mjs
├── package.json
├── rollup.config.js
├── src
├── __tests__
│ └── createChart.ts
├── controllers
│ ├── LogarithmicParallelCoordinatesController.ts
│ ├── ParallelCoordinatesController.spec.ts
│ ├── ParallelCoordinatesController.ts
│ ├── __image_snapshots__
│ │ ├── parallel-coordinates-controller-spec-ts-pcp-default-1-snap.png
│ │ ├── parallel-coordinates-controller-spec-ts-pcp-log-1-snap.png
│ │ └── parallel-coordinates-controller-spec-ts-pcp-tension-1-snap.png
│ ├── __tests__
│ │ └── mtcars.ts
│ ├── index.ts
│ └── patchController.ts
├── elements
│ ├── LineSegment.ts
│ ├── LinearAxis.ts
│ └── index.ts
├── index.ts
├── index.umd.ts
└── scales
│ ├── PCPScale.ts
│ └── index.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 Parallel Coordinate Plots
2 |
3 | [![NPM Package][npm-image]][npm-url] [![Github Actions][github-actions-image]][github-actions-url]
4 |
5 | Chart.js module for charting parallel coordinate plots (PCPs). Adding new chart type: `pcp`.
6 |
7 | 
8 |
9 | [![Open in CodePen][codepen]](https://codepen.io/sgratzl/pen/wvKQvyM)
10 |
11 | 
12 |
13 | 
14 |
15 | [![Open in CodePen][codepen]](https://codepen.io/sgratzl/pen/KKdrKZW)
16 |
17 | ## Related Plugins
18 |
19 | Check out also my other chart.js plugins:
20 |
21 | - [chartjs-chart-boxplot](https://github.com/sgratzl/chartjs-chart-boxplot) for rendering boxplots and violin plots
22 | - [chartjs-chart-error-bars](https://github.com/sgratzl/chartjs-chart-error-bars) for rendering errors bars to bars and line charts
23 | - [chartjs-chart-geo](https://github.com/sgratzl/chartjs-chart-geo) for rendering map, bubble maps, and choropleth charts
24 | - [chartjs-chart-graph](https://github.com/sgratzl/chartjs-chart-graph) for rendering graphs, trees, and networks
25 | - [chartjs-chart-venn](https://github.com/sgratzl/chartjs-chart-venn) for rendering venn and euler diagrams
26 | - [chartjs-chart-wordcloud](https://github.com/sgratzl/chartjs-chart-wordcloud) for rendering word clouds
27 | - [chartjs-plugin-hierarchical](https://github.com/sgratzl/chartjs-plugin-hierarchical) for rendering hierarchical categorical axes which can be expanded and collapsed
28 |
29 | ## Install
30 |
31 | ```bash
32 | npm install --save chart.js chartjs-chart-pcp
33 | ```
34 |
35 | ## Usage
36 |
37 | see [Examples](https://www.sgratzl.com/chartjs-chart-pcp/examples/)
38 |
39 | [![Open in CodePen][codepen]](https://codepen.io/sgratzl/pen/wvKQvyM)
40 |
41 | ## PCP
42 |
43 | ### Data Structure
44 |
45 | the data items are the regular data items along with their labels. For each attribute there is a dataset. e.g., in the following example there are three items (A, B, C) with three axes / features (f1, f2, f3).
46 |
47 | ```js
48 | const objs = [
49 | { label: 'A', f1: 5, f2: 3, f4: 3 },
50 | { label: 'B', f1: 2, f2: 1, f4: 8 },
51 | { label: 'C', f1: 10, f2: 6, f4: 2 },
52 | ];
53 | const attrs = ['f1', 'f2', 'f3'];
54 | const config = {
55 | type: 'pcp',
56 | data: {
57 | labels: objs.map((obj) => obj.label),
58 | datasets: attrs.map((attr) => ({
59 | label: attr,
60 | data: objs.map((obj) => obj[attr]),
61 | })),
62 | },
63 | options: {},
64 | };
65 | ```
66 |
67 | ### Styling of elements
68 |
69 | Two new elements were added: `lineSegment` as a subclass of line for a line segment between two axes and `linearAxis` for representing the vertical axis as a wrapper around a linear scale.
70 |
71 | see https://github.com/sgratzl/chartjs-chart-pcp/blob/develop/src/elements/lineSegment.ts#L3-L9
72 |
73 | see https://github.com/sgratzl/chartjs-chart-pcp/blob/develop/src/elements/axis.ts#L13-L21
74 |
75 | ## Scaling
76 |
77 | The Parallel Coordinates controller `pcp` uses a linear scale. There is also the `logarithmicPcp` that uses a logarithmic scale.
78 | Using chart.js hybrid charts, one can specify the type per dataset. e.g.,
79 |
80 | ```js
81 | const config = {
82 | type: 'pcp',
83 | data: {
84 | labels: ['1', '2', '3'],
85 | datasets: [
86 | {
87 | label: 'A',
88 | data: [1, 2, 3]
89 | },
90 | {
91 | type: 'logarithmicPcp',
92 | label: 'B',
93 | data: [1, 2, 10000]
94 | }
95 | })),
96 | },
97 | options: {},
98 | };
99 | ```
100 |
101 | ### ESM and Tree Shaking
102 |
103 | 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.
104 |
105 | Variant A:
106 |
107 | ```js
108 | import Chart from 'chart.js';
109 | import { ParallelCoordinatesController, LinearAxis, LineSegment, PCPScale } from 'chartjs-chart-pcp';
110 |
111 | Chart.register(ParallelCoordinatesController, PCPScale, LineSegment);
112 | Chart.registry.addElements(LinearAxis);
113 | ...
114 |
115 | new Chart(ctx, {
116 | type: ParallelCoordinatesController.id,
117 | data: [...],
118 | });
119 | ```
120 |
121 | Variant B:
122 |
123 | ```js
124 | import { ParallelCoordinatesChart } from 'chartjs-chart-pcp';
125 |
126 | new ParallelCoordinatesChart(ctx, {
127 | data: [...],
128 | });
129 | ```
130 |
131 | ## Development Environment
132 |
133 | ```sh
134 | npm i -g yarn
135 | yarn install
136 | yarn sdks vscode
137 | ```
138 |
139 | ### Common commands
140 |
141 | ```sh
142 | yarn compile
143 | yarn test
144 | yarn lint
145 | yarn fix
146 | yarn build
147 | yarn docs
148 | ```
149 |
150 | [npm-image]: https://badge.fury.io/js/chartjs-chart-pcp.svg
151 | [npm-url]: https://npmjs.org/package/chartjs-chart-pcp
152 | [github-actions-image]: https://github.com/sgratzl/chartjs-chart-pcp/workflows/ci/badge.svg
153 | [github-actions-url]: https://github.com/sgratzl/chartjs-chart-pcp/actions
154 | [codepen]: https://img.shields.io/badge/CodePen-open-blue?logo=codepen
155 |
--------------------------------------------------------------------------------
/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: 'Log Scale', link: '/examples/log' },
29 | { text: 'Tension', link: '/examples/tension' },
30 | ],
31 | },
32 | {
33 | text: 'API',
34 | collapsed: true,
35 | items: typedocSidebar,
36 | },
37 | ],
38 |
39 | socialLinks: [{ icon: 'github', link: repository.url.replace('.git', '') }],
40 |
41 | footer: {
42 | message: `Released under the ${license} license.`,
46 | copyright: `Copyright © 2019-present ${author.name}`,
47 | },
48 |
49 | editLink: {
50 | pattern: `${repository.url.replace('.git', '')}/edit/main/docs/:path`,
51 | },
52 |
53 | search: {
54 | provider: 'local',
55 | },
56 | },
57 | });
58 |
--------------------------------------------------------------------------------
/docs/.vitepress/theme/index.ts:
--------------------------------------------------------------------------------
1 | import Theme from 'vitepress/theme';
2 | import { createTypedChart } from 'vue-chartjs';
3 | import { CategoryScale, Tooltip, LinearScale, LogarithmicScale, Legend } from 'chart.js';
4 | import {
5 | LineSegment,
6 | ParallelCoordinatesController,
7 | PCPScale,
8 | LinearAxis,
9 | LogarithmicAxis,
10 | LogarithmicParallelCoordinatesController,
11 | } from '../../../src';
12 |
13 | export default {
14 | ...Theme,
15 | enhanceApp({ app }) {
16 | app.component(
17 | 'PCPChart',
18 | createTypedChart('pcp', [
19 | CategoryScale,
20 | Tooltip,
21 | Legend,
22 | LinearScale,
23 | LogarithmicScale,
24 | LineSegment,
25 | PCPScale,
26 | ParallelCoordinatesController,
27 | LogarithmicParallelCoordinatesController,
28 | ])
29 | );
30 | },
31 | };
32 |
--------------------------------------------------------------------------------
/docs/examples/basic.ts:
--------------------------------------------------------------------------------
1 | import type { ChartConfiguration } from 'chart.js';
2 | import { Chart } from 'chart.js';
3 | import { LinearAxis, LogarithmicAxis } from '../../src';
4 |
5 | // #region data
6 | import mtcars from './data/index.json';
7 |
8 | const attrs = ['mpg', 'hp', 'wt', 'qsec', 'gear', 'drat', 'disp', 'cyl', 'am'];
9 |
10 | export const data: ChartConfiguration<'pcp'>['data'] = {
11 | labels: mtcars.map((c) => c.model),
12 | datasets: attrs.map((attr) => ({
13 | label: attr,
14 | data: mtcars.map((c) => c[attr]),
15 | })),
16 | };
17 |
18 | // #endregion
19 |
20 | Chart.registry.addElements(LinearAxis, LogarithmicAxis);
21 |
22 | // #region config
23 | export const config: ChartConfiguration<'pcp'> = {
24 | type: 'pcp',
25 | data,
26 | };
27 | // #endregion config
28 |
--------------------------------------------------------------------------------
/docs/examples/data/README.md:
--------------------------------------------------------------------------------
1 | https://raw.githubusercontent.com/derhuerst/mtcars/master/index.json
2 |
--------------------------------------------------------------------------------
/docs/examples/data/index.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "model": "Mazda RX4",
4 | "mpg": 21,
5 | "cyl": 6,
6 | "disp": 160,
7 | "hp": 110,
8 | "drat": 3.9,
9 | "wt": 2.62,
10 | "qsec": 16.46,
11 | "vs": 0,
12 | "am": 1,
13 | "gear": 4,
14 | "carb": 4
15 | },
16 | {
17 | "model": "Mazda RX4 Wag",
18 | "mpg": 21,
19 | "cyl": 6,
20 | "disp": 160,
21 | "hp": 110,
22 | "drat": 3.9,
23 | "wt": 2.875,
24 | "qsec": 17.02,
25 | "vs": 0,
26 | "am": 1,
27 | "gear": 4,
28 | "carb": 4
29 | },
30 | {
31 | "model": "Datsun 710",
32 | "mpg": 22.8,
33 | "cyl": 4,
34 | "disp": 108,
35 | "hp": 93,
36 | "drat": 3.85,
37 | "wt": 2.32,
38 | "qsec": 18.61,
39 | "vs": 1,
40 | "am": 1,
41 | "gear": 4,
42 | "carb": 1
43 | },
44 | {
45 | "model": "Hornet 4 Drive",
46 | "mpg": 21.4,
47 | "cyl": 6,
48 | "disp": 258,
49 | "hp": 110,
50 | "drat": 3.08,
51 | "wt": 3.215,
52 | "qsec": 19.44,
53 | "vs": 1,
54 | "am": 0,
55 | "gear": 3,
56 | "carb": 1
57 | },
58 | {
59 | "model": "Hornet Sportabout",
60 | "mpg": 18.7,
61 | "cyl": 8,
62 | "disp": 360,
63 | "hp": 175,
64 | "drat": 3.15,
65 | "wt": 3.44,
66 | "qsec": 17.02,
67 | "vs": 0,
68 | "am": 0,
69 | "gear": 3,
70 | "carb": 2
71 | },
72 | {
73 | "model": "Valiant",
74 | "mpg": 18.1,
75 | "cyl": 6,
76 | "disp": 225,
77 | "hp": 105,
78 | "drat": 2.76,
79 | "wt": 3.46,
80 | "qsec": 20.22,
81 | "vs": 1,
82 | "am": 0,
83 | "gear": 3,
84 | "carb": 1
85 | },
86 | {
87 | "model": "Duster 360",
88 | "mpg": 14.3,
89 | "cyl": 8,
90 | "disp": 360,
91 | "hp": 245,
92 | "drat": 3.21,
93 | "wt": 3.57,
94 | "qsec": 15.84,
95 | "vs": 0,
96 | "am": 0,
97 | "gear": 3,
98 | "carb": 4
99 | },
100 | {
101 | "model": "Merc 240D",
102 | "mpg": 24.4,
103 | "cyl": 4,
104 | "disp": 146.7,
105 | "hp": 62,
106 | "drat": 3.69,
107 | "wt": 3.19,
108 | "qsec": 20,
109 | "vs": 1,
110 | "am": 0,
111 | "gear": 4,
112 | "carb": 2
113 | },
114 | {
115 | "model": "Merc 230",
116 | "mpg": 22.8,
117 | "cyl": 4,
118 | "disp": 140.8,
119 | "hp": 95,
120 | "drat": 3.92,
121 | "wt": 3.15,
122 | "qsec": 22.9,
123 | "vs": 1,
124 | "am": 0,
125 | "gear": 4,
126 | "carb": 2
127 | },
128 | {
129 | "model": "Merc 280",
130 | "mpg": 19.2,
131 | "cyl": 6,
132 | "disp": 167.6,
133 | "hp": 123,
134 | "drat": 3.92,
135 | "wt": 3.44,
136 | "qsec": 18.3,
137 | "vs": 1,
138 | "am": 0,
139 | "gear": 4,
140 | "carb": 4
141 | },
142 | {
143 | "model": "Merc 280C",
144 | "mpg": 17.8,
145 | "cyl": 6,
146 | "disp": 167.6,
147 | "hp": 123,
148 | "drat": 3.92,
149 | "wt": 3.44,
150 | "qsec": 18.9,
151 | "vs": 1,
152 | "am": 0,
153 | "gear": 4,
154 | "carb": 4
155 | },
156 | {
157 | "model": "Merc 450SE",
158 | "mpg": 16.4,
159 | "cyl": 8,
160 | "disp": 275.8,
161 | "hp": 180,
162 | "drat": 3.07,
163 | "wt": 4.07,
164 | "qsec": 17.4,
165 | "vs": 0,
166 | "am": 0,
167 | "gear": 3,
168 | "carb": 3
169 | },
170 | {
171 | "model": "Merc 450SL",
172 | "mpg": 17.3,
173 | "cyl": 8,
174 | "disp": 275.8,
175 | "hp": 180,
176 | "drat": 3.07,
177 | "wt": 3.73,
178 | "qsec": 17.6,
179 | "vs": 0,
180 | "am": 0,
181 | "gear": 3,
182 | "carb": 3
183 | },
184 | {
185 | "model": "Merc 450SLC",
186 | "mpg": 15.2,
187 | "cyl": 8,
188 | "disp": 275.8,
189 | "hp": 180,
190 | "drat": 3.07,
191 | "wt": 3.78,
192 | "qsec": 18,
193 | "vs": 0,
194 | "am": 0,
195 | "gear": 3,
196 | "carb": 3
197 | },
198 | {
199 | "model": "Cadillac Fleetwood",
200 | "mpg": 10.4,
201 | "cyl": 8,
202 | "disp": 472,
203 | "hp": 205,
204 | "drat": 2.93,
205 | "wt": 5.25,
206 | "qsec": 17.98,
207 | "vs": 0,
208 | "am": 0,
209 | "gear": 3,
210 | "carb": 4
211 | },
212 | {
213 | "model": "Lincoln Continental",
214 | "mpg": 10.4,
215 | "cyl": 8,
216 | "disp": 460,
217 | "hp": 215,
218 | "drat": 3,
219 | "wt": 5.424,
220 | "qsec": 17.82,
221 | "vs": 0,
222 | "am": 0,
223 | "gear": 3,
224 | "carb": 4
225 | },
226 | {
227 | "model": "Chrysler Imperial",
228 | "mpg": 14.7,
229 | "cyl": 8,
230 | "disp": 440,
231 | "hp": 230,
232 | "drat": 3.23,
233 | "wt": 5.345,
234 | "qsec": 17.42,
235 | "vs": 0,
236 | "am": 0,
237 | "gear": 3,
238 | "carb": 4
239 | },
240 | {
241 | "model": "Fiat 128",
242 | "mpg": 32.4,
243 | "cyl": 4,
244 | "disp": 78.7,
245 | "hp": 66,
246 | "drat": 4.08,
247 | "wt": 2.2,
248 | "qsec": 19.47,
249 | "vs": 1,
250 | "am": 1,
251 | "gear": 4,
252 | "carb": 1
253 | },
254 | {
255 | "model": "Honda Civic",
256 | "mpg": 30.4,
257 | "cyl": 4,
258 | "disp": 75.7,
259 | "hp": 52,
260 | "drat": 4.93,
261 | "wt": 1.615,
262 | "qsec": 18.52,
263 | "vs": 1,
264 | "am": 1,
265 | "gear": 4,
266 | "carb": 2
267 | },
268 | {
269 | "model": "Toyota Corolla",
270 | "mpg": 33.9,
271 | "cyl": 4,
272 | "disp": 71.1,
273 | "hp": 65,
274 | "drat": 4.22,
275 | "wt": 1.835,
276 | "qsec": 19.9,
277 | "vs": 1,
278 | "am": 1,
279 | "gear": 4,
280 | "carb": 1
281 | },
282 | {
283 | "model": "Toyota Corona",
284 | "mpg": 21.5,
285 | "cyl": 4,
286 | "disp": 120.1,
287 | "hp": 97,
288 | "drat": 3.7,
289 | "wt": 2.465,
290 | "qsec": 20.01,
291 | "vs": 1,
292 | "am": 0,
293 | "gear": 3,
294 | "carb": 1
295 | },
296 | {
297 | "model": "Dodge Challenger",
298 | "mpg": 15.5,
299 | "cyl": 8,
300 | "disp": 318,
301 | "hp": 150,
302 | "drat": 2.76,
303 | "wt": 3.52,
304 | "qsec": 16.87,
305 | "vs": 0,
306 | "am": 0,
307 | "gear": 3,
308 | "carb": 2
309 | },
310 | {
311 | "model": "AMC Javelin",
312 | "mpg": 15.2,
313 | "cyl": 8,
314 | "disp": 304,
315 | "hp": 150,
316 | "drat": 3.15,
317 | "wt": 3.435,
318 | "qsec": 17.3,
319 | "vs": 0,
320 | "am": 0,
321 | "gear": 3,
322 | "carb": 2
323 | },
324 | {
325 | "model": "Camaro Z28",
326 | "mpg": 13.3,
327 | "cyl": 8,
328 | "disp": 350,
329 | "hp": 245,
330 | "drat": 3.73,
331 | "wt": 3.84,
332 | "qsec": 15.41,
333 | "vs": 0,
334 | "am": 0,
335 | "gear": 3,
336 | "carb": 4
337 | },
338 | {
339 | "model": "Pontiac Firebird",
340 | "mpg": 19.2,
341 | "cyl": 8,
342 | "disp": 400,
343 | "hp": 175,
344 | "drat": 3.08,
345 | "wt": 3.845,
346 | "qsec": 17.05,
347 | "vs": 0,
348 | "am": 0,
349 | "gear": 3,
350 | "carb": 2
351 | },
352 | {
353 | "model": "Fiat X1-9",
354 | "mpg": 27.3,
355 | "cyl": 4,
356 | "disp": 79,
357 | "hp": 66,
358 | "drat": 4.08,
359 | "wt": 1.935,
360 | "qsec": 18.9,
361 | "vs": 1,
362 | "am": 1,
363 | "gear": 4,
364 | "carb": 1
365 | },
366 | {
367 | "model": "Porsche 914-2",
368 | "mpg": 26,
369 | "cyl": 4,
370 | "disp": 120.3,
371 | "hp": 91,
372 | "drat": 4.43,
373 | "wt": 2.14,
374 | "qsec": 16.7,
375 | "vs": 0,
376 | "am": 1,
377 | "gear": 5,
378 | "carb": 2
379 | },
380 | {
381 | "model": "Lotus Europa",
382 | "mpg": 30.4,
383 | "cyl": 4,
384 | "disp": 95.1,
385 | "hp": 113,
386 | "drat": 3.77,
387 | "wt": 1.513,
388 | "qsec": 16.9,
389 | "vs": 1,
390 | "am": 1,
391 | "gear": 5,
392 | "carb": 2
393 | },
394 | {
395 | "model": "Ford Pantera L",
396 | "mpg": 15.8,
397 | "cyl": 8,
398 | "disp": 351,
399 | "hp": 264,
400 | "drat": 4.22,
401 | "wt": 3.17,
402 | "qsec": 14.5,
403 | "vs": 0,
404 | "am": 1,
405 | "gear": 5,
406 | "carb": 4
407 | },
408 | {
409 | "model": "Ferrari Dino",
410 | "mpg": 19.7,
411 | "cyl": 6,
412 | "disp": 145,
413 | "hp": 175,
414 | "drat": 3.62,
415 | "wt": 2.77,
416 | "qsec": 15.5,
417 | "vs": 0,
418 | "am": 1,
419 | "gear": 5,
420 | "carb": 6
421 | },
422 | {
423 | "model": "Maserati Bora",
424 | "mpg": 15,
425 | "cyl": 8,
426 | "disp": 301,
427 | "hp": 335,
428 | "drat": 3.54,
429 | "wt": 3.57,
430 | "qsec": 14.6,
431 | "vs": 0,
432 | "am": 1,
433 | "gear": 5,
434 | "carb": 8
435 | },
436 | {
437 | "model": "Volvo 142E",
438 | "mpg": 21.4,
439 | "cyl": 4,
440 | "disp": 121,
441 | "hp": 109,
442 | "drat": 4.11,
443 | "wt": 2.78,
444 | "qsec": 18.6,
445 | "vs": 1,
446 | "am": 1,
447 | "gear": 4,
448 | "carb": 2
449 | }
450 | ]
451 |
--------------------------------------------------------------------------------
/docs/examples/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Examples
3 | ---
4 |
5 | # Examples
6 |
7 |
10 |
11 | ## Parallel Coordinates Plot
12 |
13 |
17 |
18 | ### Code
19 |
20 | :::code-group
21 |
22 | <<< ./basic.ts#config [config]
23 |
24 | <<< ./basic.ts#data [data]
25 |
26 | :::
27 |
--------------------------------------------------------------------------------
/docs/examples/log.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Logarithmic Scale
3 | ---
4 |
5 | # Logarithmic Scale
6 |
7 |
10 |
11 |
15 |
16 | ### Code
17 |
18 | :::code-group
19 |
20 | <<< ./log.ts#config [config]
21 |
22 | <<< ./log.ts#data [data]
23 |
24 | :::
25 |
--------------------------------------------------------------------------------
/docs/examples/log.ts:
--------------------------------------------------------------------------------
1 | import type { ChartConfiguration } from 'chart.js';
2 | import type {} from '../../src';
3 | import { Chart } from 'chart.js';
4 | import { LinearAxis, LogarithmicAxis } from '../../src';
5 |
6 | // #region data
7 | import mtcars from './data/index.json';
8 |
9 | const attrs = ['mpg', 'hp', 'wt', 'qsec', 'gear', 'drat', 'disp', 'cyl', 'am'];
10 |
11 | export const data: ChartConfiguration<'pcp'>['data'] = {
12 | labels: mtcars.map((c) => c.model),
13 | datasets: attrs.map((attr, i) => ({
14 | type: i === 1 ? 'logarithmicPcp' : undefined,
15 | label: attr,
16 | data: mtcars.map((c) => c[attr]),
17 | })),
18 | };
19 |
20 | // #endregion
21 |
22 | Chart.registry.addElements(LinearAxis, LogarithmicAxis);
23 |
24 | // #region config
25 | export const config: ChartConfiguration<'pcp'> = {
26 | type: 'pcp',
27 | data,
28 | };
29 | // #endregion config
30 |
--------------------------------------------------------------------------------
/docs/examples/tension.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Tension
3 | ---
4 |
5 | # Tension
6 |
7 |
10 |
11 |
15 |
16 | ### Code
17 |
18 | :::code-group
19 |
20 | <<< ./basic.ts#config [config]
21 |
22 | <<< ./tension.ts#data [data]
23 |
24 | :::
25 |
--------------------------------------------------------------------------------
/docs/examples/tension.ts:
--------------------------------------------------------------------------------
1 | import type { ChartConfiguration } from 'chart.js';
2 | import type {} from '../../src';
3 | import { data } from './basic';
4 |
5 | // #region config
6 | export const config: ChartConfiguration<'pcp'> = {
7 | type: 'pcp',
8 | data,
9 | options: {
10 | elements: {
11 | lineSegment: {
12 | tension: 0.3,
13 | },
14 | },
15 | },
16 | };
17 | // #endregion config
18 |
--------------------------------------------------------------------------------
/docs/getting-started.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Getting Started
3 | ---
4 |
5 | Chart.js module for charting parallel coordinate plots (PCPs). Adding new chart type: `pcp`.
6 |
7 | 
8 |
9 | ## Install
10 |
11 | ```sh
12 | npm install chart.js chartjs-chart-pcp
13 | ```
14 |
15 | ## Usage
16 |
17 | see [Examples](./examples/)
18 |
19 | and [CodePen](https://codepen.io/sgratzl/pen/wvKQvyM)
20 |
21 | ## Configuration
22 |
23 | ### Data Structure
24 |
25 | the data items are the regular data items along with their labels. For each attribute there is a dataset. e.g., in the following example there are three items (A, B, C) with three axes / features (f1, f2, f3).
26 |
27 | ```js
28 | const objs = [
29 | { label: 'A', f1: 5, f2: 3, f4: 3 },
30 | { label: 'B', f1: 2, f2: 1, f4: 8 },
31 | { label: 'C', f1: 10, f2: 6, f4: 2 },
32 | ];
33 | const attrs = ['f1', 'f2', 'f3'];
34 | const config = {
35 | type: 'pcp',
36 | data: {
37 | labels: objs.map((obj) => obj.label),
38 | datasets: attrs.map((attr) => ({
39 | label: attr,
40 | data: objs.map((obj) => obj[attr]),
41 | })),
42 | },
43 | options: {},
44 | };
45 | ```
46 |
47 | ### Styling
48 |
49 | Two new elements were added: `lineSegment` as a subclass of line for a line segment between two axes and `linearAxis` for representing the vertical axis as a wrapper around a linear scale.
50 |
51 | see [ILineSegmentOptions](/api/interfaces/ILineSegmentOptions.html) and [ILinearAxisOptions](/api/type-aliases/ILinearAxisOptions.html)
52 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | # https://vitepress.dev/reference/default-theme-home-page
3 | layout: home
4 |
5 | hero:
6 | name: 'chartjs-chart-pcp'
7 | text: 'chart.js plugin'
8 | tagline: Chart.js module for charting parallel coordinate plots
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-pcp",
3 | "description": "Chart.js module for charting parallel coordinate plots",
4 | "version": "4.3.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-pcp",
12 | "bugs": {
13 | "url": "https://github.com/sgratzl/chartjs-chart-pcp/issues"
14 | },
15 | "keywords": [
16 | "chart.js",
17 | "pcp"
18 | ],
19 | "repository": {
20 | "type": "git",
21 | "url": "https://github.com/sgratzl/chartjs-chart-pcp.git"
22 | },
23 | "global": "ChartPCP",
24 | "type": "module",
25 | "main": "build/index.js",
26 | "module": "build/index.js",
27 | "require": "build/index.cjs",
28 | "umd": "build/index.umd.js",
29 | "unpkg": "build/index.umd.min.js",
30 | "jsdelivr": "build/index.umd.min.js",
31 | "types": "build/index.d.ts",
32 | "exports": {
33 | ".": {
34 | "import": "./build/index.js",
35 | "require": "./build/index.cjs",
36 | "scripts": "./build/index.umd.min.js",
37 | "types": "./build/index.d.ts"
38 | }
39 | },
40 | "sideEffects": false,
41 | "files": [
42 | "build",
43 | "src/**/*.ts"
44 | ],
45 | "peerDependencies": {
46 | "chart.js": "^4.1.0"
47 | },
48 | "browserslist": [
49 | "Firefox ESR",
50 | "last 2 Chrome versions",
51 | "last 2 Firefox versions"
52 | ],
53 | "devDependencies": {
54 | "@chiogen/rollup-plugin-terser": "^7.1.3",
55 | "@eslint/js": "~9.14.0",
56 | "@rollup/plugin-commonjs": "^28.0.1",
57 | "@rollup/plugin-node-resolve": "^15.3.0",
58 | "@rollup/plugin-replace": "^6.0.1",
59 | "@rollup/plugin-typescript": "^12.1.1",
60 | "@types/jest-image-snapshot": "^6.4.0",
61 | "@types/node": "^22.9.0",
62 | "@yarnpkg/sdks": "^3.2.0",
63 | "canvas": "^2.11.2",
64 | "canvas-5-polyfill": "^0.1.5",
65 | "chart.js": "^4.4.6",
66 | "eslint": "~9.14.0",
67 | "eslint-plugin-prettier": "^5.2.1",
68 | "jest-image-snapshot": "^6.4.0",
69 | "jsdom": "^25.0.1",
70 | "prettier": "^3.3.3",
71 | "rimraf": "^6.0.1",
72 | "rollup": "^4.27.2",
73 | "rollup-plugin-cleanup": "^3.2.1",
74 | "rollup-plugin-dts": "^6.1.1",
75 | "ts-jest": "^29.2.5",
76 | "tslib": "^2.8.1",
77 | "typedoc": "^0.26.11",
78 | "typedoc-plugin-markdown": "^4.2.10",
79 | "typedoc-vitepress-theme": "^1.0.2",
80 | "typescript": "^5.6.3",
81 | "typescript-eslint": "^8.14.0",
82 | "vite": "^5.4.11",
83 | "vitepress": "^1.5.0",
84 | "vitest": "^2.1.5",
85 | "vue": "^3.5.13",
86 | "vue-chartjs": "^5.3.2"
87 | },
88 | "scripts": {
89 | "clean": "rimraf --glob build node_modules \"*.tgz\" \"*.tsbuildinfo\"",
90 | "compile": "tsc -b tsconfig.c.json",
91 | "start": "yarn run watch",
92 | "watch": "rollup -c -w",
93 | "build": "rollup -c",
94 | "test": "vitest --passWithNoTests",
95 | "test:watch": "yarn run test --watch",
96 | "test:coverage": "yarn run test --coverage",
97 | "lint": "yarn run eslint && yarn run prettier",
98 | "fix": "yarn run eslint:fix && yarn run prettier:write",
99 | "prettier:write": "prettier \"*\" \"*/**\" --write",
100 | "prettier": "prettier \"*\" \"*/**\" --check",
101 | "eslint": "eslint src --cache",
102 | "eslint:fix": "yarn run eslint --fix",
103 | "prepare": "yarn run build",
104 | "docs:api": "typedoc --options typedoc.json",
105 | "docs:dev": "vitepress dev docs",
106 | "docs:build": "yarn run docs:api && vitepress build docs",
107 | "docs:preview": "vitepress preview docs"
108 | },
109 | "packageManager": "yarn@4.5.1"
110 | }
111 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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' });
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/LogarithmicParallelCoordinatesController.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Chart,
3 | ControllerDatasetOptions,
4 | ScriptableAndArrayOptions,
5 | CommonHoverOptions,
6 | ChartItem,
7 | ChartConfiguration,
8 | ScriptableContext,
9 | CartesianScaleTypeRegistry,
10 | } from 'chart.js';
11 | import { LineSegment, LogarithmicAxis, ILineSegmentOptions, ILogarithmicAxisOptions } from '../elements';
12 | import { ParallelCoordinatesController } from './ParallelCoordinatesController';
13 | import patchController from './patchController';
14 | import { PCPScale } from '../scales';
15 |
16 | export class LogarithmicParallelCoordinatesController extends ParallelCoordinatesController {
17 | static readonly id = 'logarithmicPcp';
18 |
19 | /**
20 | * @hidden
21 | */
22 | static readonly defaults: any = /* #__PURE__ */ {
23 | ...ParallelCoordinatesController.defaults,
24 | datasetElementType: LogarithmicAxis.id,
25 | };
26 |
27 | /**
28 | * @hidden
29 | */
30 | static readonly overrides: any = /* #__PURE__ */ ParallelCoordinatesController.overrides;
31 | }
32 |
33 | export interface ILogarithmicParallelCoordinatesControllerDatasetOptions
34 | extends Omit,
35 | Omit,
36 | ScriptableAndArrayOptions>,
37 | ScriptableAndArrayOptions> {
38 | stack: string;
39 | }
40 |
41 | export type ILogarithmicParallelCoordinatesChartOptions = ILogarithmicAxisOptions;
42 |
43 | declare module 'chart.js' {
44 | interface ChartTypeRegistry {
45 | logarithmicPcp: {
46 | chartOptions: ILogarithmicParallelCoordinatesChartOptions;
47 | datasetOptions: ILogarithmicParallelCoordinatesControllerDatasetOptions;
48 | defaultDataPoint: number;
49 | metaExtensions: Record;
50 | parsedDataType: { y: number };
51 | scales: keyof CartesianScaleTypeRegistry;
52 | };
53 | }
54 | }
55 |
56 | export class LogarithmicParallelCoordinatesChart extends Chart<
57 | 'logarithmicPcp',
58 | DATA,
59 | LABEL
60 | > {
61 | static id = LogarithmicParallelCoordinatesController.id;
62 |
63 | constructor(item: ChartItem, config: Omit, 'type'>) {
64 | super(
65 | item,
66 | patchController(
67 | 'logarithmicPcp',
68 | config,
69 | LogarithmicParallelCoordinatesController,
70 | [LogarithmicAxis, LineSegment],
71 | PCPScale
72 | )
73 | );
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/controllers/ParallelCoordinatesController.spec.ts:
--------------------------------------------------------------------------------
1 | import { registry } from 'chart.js';
2 | import createChart from '../__tests__/createChart';
3 | import { ParallelCoordinatesController } from './ParallelCoordinatesController';
4 | import { LogarithmicParallelCoordinatesController } from './LogarithmicParallelCoordinatesController';
5 | import mtcars from './__tests__/mtcars';
6 | import { LineSegment, LinearAxis, LogarithmicAxis } from '../elements';
7 | import { PCPScale } from '../scales';
8 | import { describe, beforeAll, test } from 'vitest';
9 | describe('pcp', () => {
10 | beforeAll(() => {
11 | registry.addControllers(ParallelCoordinatesController, LogarithmicParallelCoordinatesController);
12 | registry.addElements(LineSegment, LinearAxis, LogarithmicAxis);
13 | registry.addScales(PCPScale);
14 | });
15 | test('default', () => {
16 | const attrs = ['mpg', 'hp', 'wt', 'qsec', 'gear', 'drat', 'disp', 'cyl'];
17 | const chart = createChart(
18 | {
19 | type: 'pcp',
20 | data: {
21 | labels: mtcars.map((c) => c.model),
22 | datasets: attrs.map((attr) => ({
23 | label: attr,
24 | data: mtcars.map((c) => (c as any)[attr]),
25 | })),
26 | },
27 | options: {
28 | scales: {
29 | x: {
30 | display: false,
31 | },
32 | },
33 | elements: {
34 | linearAxis: {
35 | display: false,
36 | },
37 | logarithmicAxis: {
38 | display: false,
39 | },
40 | } as any,
41 | },
42 | },
43 | 1000,
44 | 500
45 | );
46 | return chart.toMatchImageSnapshot();
47 | });
48 | test('log', () => {
49 | const attrs = ['mpg', 'hp', 'wt', 'qsec', 'gear', 'drat', 'disp', 'cyl'];
50 | const chart = createChart(
51 | {
52 | type: 'pcp',
53 | data: {
54 | labels: mtcars.map((c) => c.model),
55 | datasets: attrs.map((attr, i) => ({
56 | type: i === 1 ? 'logarithmicPcp' : undefined,
57 | label: attr,
58 | data: mtcars.map((c) => (c as any)[attr]),
59 | })),
60 | },
61 | options: {
62 | scales: {
63 | x: {
64 | display: false,
65 | },
66 | },
67 | elements: {
68 | linearAxis: {
69 | display: false,
70 | },
71 | logarithmicAxis: {
72 | display: false,
73 | },
74 | } as any,
75 | },
76 | },
77 | 1000,
78 | 500
79 | );
80 | return chart.toMatchImageSnapshot();
81 | });
82 | test('tension', () => {
83 | const attrs = ['mpg', 'hp', 'wt', 'qsec', 'gear', 'drat', 'disp', 'cyl'];
84 | const chart = createChart(
85 | {
86 | type: 'pcp',
87 | data: {
88 | labels: mtcars.map((c) => c.model),
89 | datasets: attrs.map((attr) => ({
90 | label: attr,
91 | data: mtcars.map((c) => (c as any)[attr]),
92 | })),
93 | },
94 | options: {
95 | scales: {
96 | x: {
97 | display: false,
98 | },
99 | },
100 | elements: {
101 | linearAxis: {
102 | tension: 0.3,
103 | display: false,
104 | },
105 | logarithmicAxis: {
106 | display: false,
107 | },
108 | } as any,
109 | },
110 | },
111 | 1000,
112 | 500
113 | );
114 | return chart.toMatchImageSnapshot();
115 | });
116 | });
117 |
--------------------------------------------------------------------------------
/src/controllers/ParallelCoordinatesController.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Chart,
3 | DatasetController,
4 | ChartItem,
5 | ControllerDatasetOptions,
6 | ScriptableAndArrayOptions,
7 | CommonHoverOptions,
8 | TooltipItem,
9 | UpdateMode,
10 | ChartComponent,
11 | ChartMeta,
12 | ChartConfiguration,
13 | ScriptableContext,
14 | Element,
15 | CartesianScaleTypeRegistry,
16 | } from 'chart.js';
17 | import { splineCurve } from 'chart.js/helpers';
18 |
19 | import { LinearAxis, LineSegment, ILinearAxisOptions, ILineSegmentOptions, ILineSegmentProps } from '../elements';
20 | import { PCPScale } from '../scales';
21 | import patchController from './patchController';
22 |
23 | interface IExtendedChartMeta {
24 | _metas: ChartMeta[];
25 | _metaIndex: number;
26 | }
27 |
28 | export type AnyObject = Record;
29 |
30 | export class ParallelCoordinatesController extends DatasetController<
31 | 'pcp',
32 | LineSegment & Element,
33 | LinearAxis & Element
34 | > {
35 | /**
36 | * @hidden
37 | */
38 | declare datasetElementType: ChartComponent;
39 |
40 | /**
41 | * @hidden
42 | */
43 | declare dataElementType: ChartComponent;
44 |
45 | private declare _type: string;
46 |
47 | /**
48 | * @hidden
49 | */
50 | initialize(): void {
51 | super.initialize();
52 | this.enableOptionSharing = true;
53 | }
54 |
55 | /**
56 | * @hidden
57 | */
58 | linkScales(): void {
59 | const ds = this.getDataset() as any;
60 | ds.yAxisID = ds.label;
61 | super.linkScales();
62 | this._cachedMeta.vScale = this._cachedMeta.dataset as any;
63 | this._cachedMeta.vScale = this._cachedMeta.dataset as any;
64 | }
65 |
66 | private resolveAxisOptions(mode: UpdateMode) {
67 | return this.resolveDatasetElementOptions(mode) as unknown as ILinearAxisOptions;
68 | }
69 |
70 | /**
71 | * @hidden
72 | */
73 | addElements(): void {
74 | super.addElements();
75 | const meta = this._cachedMeta;
76 | const scale = meta.dataset as LinearAxis;
77 | meta.yScale = scale;
78 | meta.vScale = scale;
79 |
80 | Object.assign(scale, {
81 | id: meta.yAxisID,
82 | type: this.datasetElementType.id,
83 | chart: this.chart,
84 | ctx: this.chart.ctx,
85 | });
86 | const options = this.resolveAxisOptions('reset');
87 | // workaround for now
88 | Object.assign(options, { setContext: () => 0 });
89 | scale.init(options);
90 | }
91 |
92 | /**
93 | * @hidden
94 | */
95 | update(mode: UpdateMode): void {
96 | // from front to back
97 |
98 | const meta = this._cachedMeta as unknown as IExtendedChartMeta;
99 | meta._metas = this.chart.getSortedVisibleDatasetMetas();
100 | meta._metaIndex = meta._metas.indexOf(this._cachedMeta);
101 | if (meta._metaIndex < 0) {
102 | return;
103 | }
104 |
105 | const axis = this._cachedMeta.dataset;
106 | if (axis) {
107 | this.updateAxis(axis, mode);
108 | }
109 |
110 | const elements = this._cachedMeta.data || [];
111 | this.updateElements(elements, 0, elements.length, mode);
112 | }
113 |
114 | /**
115 | * @hidden
116 | */
117 | draw(): void {
118 | // from back to front
119 | const meta = this._cachedMeta;
120 | const metaE = meta as unknown as IExtendedChartMeta;
121 | const elements = meta.data || [];
122 | const { ctx } = this.chart;
123 | if (metaE._metaIndex < 0) {
124 | return;
125 | }
126 |
127 | if (meta.dataset) {
128 | meta.dataset.draw(ctx);
129 | }
130 | if (meta._metaIndex === 0) {
131 | return;
132 | }
133 | elements.forEach((elem) => {
134 | elem.draw(ctx);
135 | });
136 | }
137 |
138 | /**
139 | * @hidden
140 | */
141 | updateAxis(axis: LinearAxis & Element, mode: UpdateMode): void {
142 | const meta = this._cachedMeta;
143 | const metaE = meta as unknown as IExtendedChartMeta;
144 | const x = meta.xScale?.getPixelForTick(metaE._metaIndex) ?? 0;
145 |
146 | const baseOptions = this.resolveDatasetElementOptions(mode) as unknown as ILinearAxisOptions;
147 | const properties = {
148 | x,
149 | top: this.chart.chartArea.top,
150 | bottom: this.chart.chartArea.bottom,
151 | options: {
152 | ...baseOptions,
153 | position: metaE._metaIndex > 0 ? 'right' : 'left',
154 | },
155 | };
156 | super.updateElement(axis, undefined, properties, mode);
157 | axis.update();
158 | }
159 |
160 | /**
161 | * @hidden
162 | */
163 | updateElements(
164 | rectangles: (LineSegment & Element)[],
165 | start: number,
166 | count: number,
167 | mode: UpdateMode
168 | ): void {
169 | const reset = mode === 'reset';
170 | const meta = this._cachedMeta as unknown as IExtendedChartMeta;
171 |
172 | const firstOpts = this.resolveDataElementOptions(start, mode);
173 | const dummyShared = {};
174 | const sharedOptions = this.getSharedOptions(firstOpts) ?? dummyShared;
175 | const includeOptions = this.includeOptions(mode, sharedOptions);
176 | const getPoint = (metaIndex: number, index: number, defaultValue: { x: number; y: number }) => {
177 | const m = meta._metas[metaIndex];
178 | if (!m) {
179 | return defaultValue;
180 | }
181 | const x = this._cachedMeta.xScale?.getPixelForTick(metaIndex) ?? 0;
182 | const yScale = m.vScale;
183 | const y = reset
184 | ? yScale?.getBasePixel()
185 | : yScale?.getPixelForValue((m._parsed[index] as Record)[yScale?.axis ?? 'y'], index);
186 |
187 | return {
188 | x,
189 | y: y == null || Number.isNaN(y) ? defaultValue.y : y,
190 | };
191 | };
192 |
193 | this.updateSharedOptions(sharedOptions, mode, firstOpts);
194 |
195 | for (let i = start; i < start + count; i += 1) {
196 | const options: typeof firstOpts =
197 | sharedOptions === dummyShared ? this.resolveDataElementOptions(i, mode) : sharedOptions;
198 |
199 | const xy = getPoint(meta._metaIndex, i, { x: 0, y: 0 });
200 | const xyPrevious = getPoint(meta._metaIndex - 1, i, xy);
201 |
202 | const properties: Partial & { options?: ILineSegmentOptions } = {
203 | x: xyPrevious.x,
204 | y: xyPrevious.y,
205 | x1: xy.x,
206 | y1: xy.y,
207 | };
208 |
209 | if (options.tension) {
210 | const xyPrevPrevious = getPoint(meta._metaIndex - 2, i, xyPrevious);
211 | const xyNext = getPoint(meta._metaIndex + 1, i, xy);
212 |
213 | const controlPoints = splineCurve(xyPrevPrevious, xyPrevious, xy, options.tension as number);
214 | const controlPoints1 = splineCurve(xyPrevious, xy, xyNext, options.tension as number);
215 |
216 | properties.xCPn = controlPoints.next.x;
217 | properties.yCPn = controlPoints.next.y;
218 | properties.xCPp1 = controlPoints1.previous.x;
219 | properties.yCPp1 = controlPoints1.previous.y;
220 | }
221 |
222 | if (includeOptions) {
223 | properties.options = options as any;
224 | }
225 | this.updateElement(rectangles[i], i, properties, mode);
226 | }
227 | }
228 |
229 | private _findOtherControllers() {
230 | const metas = this.chart.getSortedVisibleDatasetMetas();
231 | return metas.filter(
232 | (meta) => (meta.controller as any) !== this && meta.controller instanceof ParallelCoordinatesController
233 | );
234 | }
235 |
236 | /**
237 | * @hidden
238 | */
239 | removeBaseHoverStyle(
240 | element: LineSegment & Element,
241 | datasetIndex: number,
242 | index: number
243 | ): void {
244 | super.removeHoverStyle(element, datasetIndex, index);
245 | }
246 |
247 | /**
248 | * @hidden
249 | */
250 | removeHoverStyle(element: LineSegment & Element, datasetIndex: number, index: number): void {
251 | super.removeHoverStyle(element, datasetIndex, index);
252 | this._findOtherControllers().forEach((meta) => {
253 | (meta.controller as unknown as ParallelCoordinatesController).removeBaseHoverStyle(
254 | meta.data[index] as any,
255 | meta.index,
256 | index
257 | );
258 | });
259 | }
260 |
261 | /**
262 | * @hidden
263 | */
264 | setBaseHoverStyle(element: LineSegment & Element, datasetIndex: number, index: number): void {
265 | super.setHoverStyle(element, datasetIndex, index);
266 | }
267 |
268 | /**
269 | * @hidden
270 | */
271 | setHoverStyle(element: LineSegment & Element, datasetIndex: number, index: number): void {
272 | super.setHoverStyle(element, datasetIndex, index);
273 | this._findOtherControllers().forEach((meta) => {
274 | (meta.controller as unknown as ParallelCoordinatesController).setBaseHoverStyle(
275 | meta.data[index] as any,
276 | meta.index,
277 | index
278 | );
279 | });
280 | }
281 |
282 | static readonly id: string = 'pcp';
283 |
284 | /**
285 | * @hidden
286 | */
287 | static readonly defaults: any = /* #__PURE__ */ {
288 | datasetElementType: LinearAxis.id,
289 | dataElementType: LineSegment.id,
290 | animations: {
291 | numbers: {
292 | type: 'number',
293 | properties: ['x', 'y', 'x1', 'y1', 'axisWidth', 'xCPn', 'yCPn', 'xCPp1', 'yCPp1', 'borderWidth'],
294 | },
295 | },
296 | };
297 |
298 | /**
299 | * @hidden
300 | */
301 | static readonly overrides: any = /* #__PURE__ */ {
302 | scales: {
303 | x: {
304 | type: PCPScale.id,
305 | offset: true,
306 | grid: {
307 | drawBorder: false,
308 | display: false,
309 | },
310 | },
311 | },
312 |
313 | plugins: {
314 | tooltip: {
315 | callbacks: {
316 | title() {
317 | return '';
318 | },
319 | label(tooltipItem: TooltipItem<'pcp'>) {
320 | const label = tooltipItem.chart.data?.labels?.[tooltipItem.dataIndex];
321 | const ds = tooltipItem.chart
322 | .getSortedVisibleDatasetMetas()
323 | .map((d) => `${d.label}=${d.controller.getDataset().data[tooltipItem.dataIndex]}`);
324 |
325 | return `${label}(${ds.join(', ')})`;
326 | },
327 | },
328 | },
329 | },
330 | };
331 | }
332 |
333 | export interface IParallelCoordinatesControllerDatasetOptions
334 | extends Omit,
335 | Omit,
336 | ScriptableAndArrayOptions>,
337 | ScriptableAndArrayOptions> {
338 | stack: string;
339 | }
340 |
341 | export type IParallelCoordinatesChartOptions = ILinearAxisOptions;
342 |
343 | declare module 'chart.js' {
344 | interface ChartTypeRegistry {
345 | pcp: {
346 | chartOptions: IParallelCoordinatesChartOptions;
347 | datasetOptions: IParallelCoordinatesControllerDatasetOptions;
348 | defaultDataPoint: number;
349 | metaExtensions: Record;
350 | parsedDataType: { y: number };
351 | scales: keyof CartesianScaleTypeRegistry;
352 | };
353 | }
354 | }
355 |
356 | export class ParallelCoordinatesChart extends Chart<
357 | 'pcp',
358 | DATA,
359 | LABEL
360 | > {
361 | static id = ParallelCoordinatesController.id;
362 |
363 | constructor(item: ChartItem, config: Omit, 'type'>) {
364 | super(item, patchController('pcp', config, ParallelCoordinatesController, [LinearAxis, LineSegment], PCPScale));
365 | }
366 | }
367 |
--------------------------------------------------------------------------------
/src/controllers/__image_snapshots__/parallel-coordinates-controller-spec-ts-pcp-default-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sgratzl/chartjs-chart-pcp/56d5edc6ef1b764609ac6a61d3d589dcaa564f66/src/controllers/__image_snapshots__/parallel-coordinates-controller-spec-ts-pcp-default-1-snap.png
--------------------------------------------------------------------------------
/src/controllers/__image_snapshots__/parallel-coordinates-controller-spec-ts-pcp-log-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sgratzl/chartjs-chart-pcp/56d5edc6ef1b764609ac6a61d3d589dcaa564f66/src/controllers/__image_snapshots__/parallel-coordinates-controller-spec-ts-pcp-log-1-snap.png
--------------------------------------------------------------------------------
/src/controllers/__image_snapshots__/parallel-coordinates-controller-spec-ts-pcp-tension-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sgratzl/chartjs-chart-pcp/56d5edc6ef1b764609ac6a61d3d589dcaa564f66/src/controllers/__image_snapshots__/parallel-coordinates-controller-spec-ts-pcp-tension-1-snap.png
--------------------------------------------------------------------------------
/src/controllers/__tests__/mtcars.ts:
--------------------------------------------------------------------------------
1 | // https://raw.githubusercontent.com/derhuerst/mtcars/master/index.json
2 | const data = [
3 | {
4 | model: 'Mazda RX4',
5 | mpg: 21,
6 | cyl: 6,
7 | disp: 160,
8 | hp: 110,
9 | drat: 3.9,
10 | wt: 2.62,
11 | qsec: 16.46,
12 | vs: 0,
13 | am: 1,
14 | gear: 4,
15 | carb: 4,
16 | },
17 | {
18 | model: 'Mazda RX4 Wag',
19 | mpg: 21,
20 | cyl: 6,
21 | disp: 160,
22 | hp: 110,
23 | drat: 3.9,
24 | wt: 2.875,
25 | qsec: 17.02,
26 | vs: 0,
27 | am: 1,
28 | gear: 4,
29 | carb: 4,
30 | },
31 | {
32 | model: 'Datsun 710',
33 | mpg: 22.8,
34 | cyl: 4,
35 | disp: 108,
36 | hp: 93,
37 | drat: 3.85,
38 | wt: 2.32,
39 | qsec: 18.61,
40 | vs: 1,
41 | am: 1,
42 | gear: 4,
43 | carb: 1,
44 | },
45 | {
46 | model: 'Hornet 4 Drive',
47 | mpg: 21.4,
48 | cyl: 6,
49 | disp: 258,
50 | hp: 110,
51 | drat: 3.08,
52 | wt: 3.215,
53 | qsec: 19.44,
54 | vs: 1,
55 | am: 0,
56 | gear: 3,
57 | carb: 1,
58 | },
59 | {
60 | model: 'Hornet Sportabout',
61 | mpg: 18.7,
62 | cyl: 8,
63 | disp: 360,
64 | hp: 175,
65 | drat: 3.15,
66 | wt: 3.44,
67 | qsec: 17.02,
68 | vs: 0,
69 | am: 0,
70 | gear: 3,
71 | carb: 2,
72 | },
73 | {
74 | model: 'Valiant',
75 | mpg: 18.1,
76 | cyl: 6,
77 | disp: 225,
78 | hp: 105,
79 | drat: 2.76,
80 | wt: 3.46,
81 | qsec: 20.22,
82 | vs: 1,
83 | am: 0,
84 | gear: 3,
85 | carb: 1,
86 | },
87 | {
88 | model: 'Duster 360',
89 | mpg: 14.3,
90 | cyl: 8,
91 | disp: 360,
92 | hp: 245,
93 | drat: 3.21,
94 | wt: 3.57,
95 | qsec: 15.84,
96 | vs: 0,
97 | am: 0,
98 | gear: 3,
99 | carb: 4,
100 | },
101 | {
102 | model: 'Merc 240D',
103 | mpg: 24.4,
104 | cyl: 4,
105 | disp: 146.7,
106 | hp: 62,
107 | drat: 3.69,
108 | wt: 3.19,
109 | qsec: 20,
110 | vs: 1,
111 | am: 0,
112 | gear: 4,
113 | carb: 2,
114 | },
115 | {
116 | model: 'Merc 230',
117 | mpg: 22.8,
118 | cyl: 4,
119 | disp: 140.8,
120 | hp: 95,
121 | drat: 3.92,
122 | wt: 3.15,
123 | qsec: 22.9,
124 | vs: 1,
125 | am: 0,
126 | gear: 4,
127 | carb: 2,
128 | },
129 | {
130 | model: 'Merc 280',
131 | mpg: 19.2,
132 | cyl: 6,
133 | disp: 167.6,
134 | hp: 123,
135 | drat: 3.92,
136 | wt: 3.44,
137 | qsec: 18.3,
138 | vs: 1,
139 | am: 0,
140 | gear: 4,
141 | carb: 4,
142 | },
143 | {
144 | model: 'Merc 280C',
145 | mpg: 17.8,
146 | cyl: 6,
147 | disp: 167.6,
148 | hp: 123,
149 | drat: 3.92,
150 | wt: 3.44,
151 | qsec: 18.9,
152 | vs: 1,
153 | am: 0,
154 | gear: 4,
155 | carb: 4,
156 | },
157 | {
158 | model: 'Merc 450SE',
159 | mpg: 16.4,
160 | cyl: 8,
161 | disp: 275.8,
162 | hp: 180,
163 | drat: 3.07,
164 | wt: 4.07,
165 | qsec: 17.4,
166 | vs: 0,
167 | am: 0,
168 | gear: 3,
169 | carb: 3,
170 | },
171 | {
172 | model: 'Merc 450SL',
173 | mpg: 17.3,
174 | cyl: 8,
175 | disp: 275.8,
176 | hp: 180,
177 | drat: 3.07,
178 | wt: 3.73,
179 | qsec: 17.6,
180 | vs: 0,
181 | am: 0,
182 | gear: 3,
183 | carb: 3,
184 | },
185 | {
186 | model: 'Merc 450SLC',
187 | mpg: 15.2,
188 | cyl: 8,
189 | disp: 275.8,
190 | hp: 180,
191 | drat: 3.07,
192 | wt: 3.78,
193 | qsec: 18,
194 | vs: 0,
195 | am: 0,
196 | gear: 3,
197 | carb: 3,
198 | },
199 | {
200 | model: 'Cadillac Fleetwood',
201 | mpg: 10.4,
202 | cyl: 8,
203 | disp: 472,
204 | hp: 205,
205 | drat: 2.93,
206 | wt: 5.25,
207 | qsec: 17.98,
208 | vs: 0,
209 | am: 0,
210 | gear: 3,
211 | carb: 4,
212 | },
213 | {
214 | model: 'Lincoln Continental',
215 | mpg: 10.4,
216 | cyl: 8,
217 | disp: 460,
218 | hp: 215,
219 | drat: 3,
220 | wt: 5.424,
221 | qsec: 17.82,
222 | vs: 0,
223 | am: 0,
224 | gear: 3,
225 | carb: 4,
226 | },
227 | {
228 | model: 'Chrysler Imperial',
229 | mpg: 14.7,
230 | cyl: 8,
231 | disp: 440,
232 | hp: 230,
233 | drat: 3.23,
234 | wt: 5.345,
235 | qsec: 17.42,
236 | vs: 0,
237 | am: 0,
238 | gear: 3,
239 | carb: 4,
240 | },
241 | {
242 | model: 'Fiat 128',
243 | mpg: 32.4,
244 | cyl: 4,
245 | disp: 78.7,
246 | hp: 66,
247 | drat: 4.08,
248 | wt: 2.2,
249 | qsec: 19.47,
250 | vs: 1,
251 | am: 1,
252 | gear: 4,
253 | carb: 1,
254 | },
255 | {
256 | model: 'Honda Civic',
257 | mpg: 30.4,
258 | cyl: 4,
259 | disp: 75.7,
260 | hp: 52,
261 | drat: 4.93,
262 | wt: 1.615,
263 | qsec: 18.52,
264 | vs: 1,
265 | am: 1,
266 | gear: 4,
267 | carb: 2,
268 | },
269 | {
270 | model: 'Toyota Corolla',
271 | mpg: 33.9,
272 | cyl: 4,
273 | disp: 71.1,
274 | hp: 65,
275 | drat: 4.22,
276 | wt: 1.835,
277 | qsec: 19.9,
278 | vs: 1,
279 | am: 1,
280 | gear: 4,
281 | carb: 1,
282 | },
283 | {
284 | model: 'Toyota Corona',
285 | mpg: 21.5,
286 | cyl: 4,
287 | disp: 120.1,
288 | hp: 97,
289 | drat: 3.7,
290 | wt: 2.465,
291 | qsec: 20.01,
292 | vs: 1,
293 | am: 0,
294 | gear: 3,
295 | carb: 1,
296 | },
297 | {
298 | model: 'Dodge Challenger',
299 | mpg: 15.5,
300 | cyl: 8,
301 | disp: 318,
302 | hp: 150,
303 | drat: 2.76,
304 | wt: 3.52,
305 | qsec: 16.87,
306 | vs: 0,
307 | am: 0,
308 | gear: 3,
309 | carb: 2,
310 | },
311 | {
312 | model: 'AMC Javelin',
313 | mpg: 15.2,
314 | cyl: 8,
315 | disp: 304,
316 | hp: 150,
317 | drat: 3.15,
318 | wt: 3.435,
319 | qsec: 17.3,
320 | vs: 0,
321 | am: 0,
322 | gear: 3,
323 | carb: 2,
324 | },
325 | {
326 | model: 'Camaro Z28',
327 | mpg: 13.3,
328 | cyl: 8,
329 | disp: 350,
330 | hp: 245,
331 | drat: 3.73,
332 | wt: 3.84,
333 | qsec: 15.41,
334 | vs: 0,
335 | am: 0,
336 | gear: 3,
337 | carb: 4,
338 | },
339 | {
340 | model: 'Pontiac Firebird',
341 | mpg: 19.2,
342 | cyl: 8,
343 | disp: 400,
344 | hp: 175,
345 | drat: 3.08,
346 | wt: 3.845,
347 | qsec: 17.05,
348 | vs: 0,
349 | am: 0,
350 | gear: 3,
351 | carb: 2,
352 | },
353 | {
354 | model: 'Fiat X1-9',
355 | mpg: 27.3,
356 | cyl: 4,
357 | disp: 79,
358 | hp: 66,
359 | drat: 4.08,
360 | wt: 1.935,
361 | qsec: 18.9,
362 | vs: 1,
363 | am: 1,
364 | gear: 4,
365 | carb: 1,
366 | },
367 | {
368 | model: 'Porsche 914-2',
369 | mpg: 26,
370 | cyl: 4,
371 | disp: 120.3,
372 | hp: 91,
373 | drat: 4.43,
374 | wt: 2.14,
375 | qsec: 16.7,
376 | vs: 0,
377 | am: 1,
378 | gear: 5,
379 | carb: 2,
380 | },
381 | {
382 | model: 'Lotus Europa',
383 | mpg: 30.4,
384 | cyl: 4,
385 | disp: 95.1,
386 | hp: 113,
387 | drat: 3.77,
388 | wt: 1.513,
389 | qsec: 16.9,
390 | vs: 1,
391 | am: 1,
392 | gear: 5,
393 | carb: 2,
394 | },
395 | {
396 | model: 'Ford Pantera L',
397 | mpg: 15.8,
398 | cyl: 8,
399 | disp: 351,
400 | hp: 264,
401 | drat: 4.22,
402 | wt: 3.17,
403 | qsec: 14.5,
404 | vs: 0,
405 | am: 1,
406 | gear: 5,
407 | carb: 4,
408 | },
409 | {
410 | model: 'Ferrari Dino',
411 | mpg: 19.7,
412 | cyl: 6,
413 | disp: 145,
414 | hp: 175,
415 | drat: 3.62,
416 | wt: 2.77,
417 | qsec: 15.5,
418 | vs: 0,
419 | am: 1,
420 | gear: 5,
421 | carb: 6,
422 | },
423 | {
424 | model: 'Maserati Bora',
425 | mpg: 15,
426 | cyl: 8,
427 | disp: 301,
428 | hp: 335,
429 | drat: 3.54,
430 | wt: 3.57,
431 | qsec: 14.6,
432 | vs: 0,
433 | am: 1,
434 | gear: 5,
435 | carb: 8,
436 | },
437 | {
438 | model: 'Volvo 142E',
439 | mpg: 21.4,
440 | cyl: 4,
441 | disp: 121,
442 | hp: 109,
443 | drat: 4.11,
444 | wt: 2.78,
445 | qsec: 18.6,
446 | vs: 1,
447 | am: 1,
448 | gear: 4,
449 | carb: 2,
450 | },
451 | ];
452 |
453 | export default data;
454 |
--------------------------------------------------------------------------------
/src/controllers/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ParallelCoordinatesController';
2 | export * from './LogarithmicParallelCoordinatesController';
3 |
--------------------------------------------------------------------------------
/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/LineSegment.ts:
--------------------------------------------------------------------------------
1 | import type { AnyObject } from '../controllers/ParallelCoordinatesController';
2 | import { ChartType, Element, LineElement, LineOptions, ScriptableAndArrayOptions, ScriptableContext } from 'chart.js';
3 |
4 | export interface ILineSegmentOptions extends LineOptions {
5 | /**
6 | * line tension > 0 (e.g., 0.3) to create bezier curves
7 | * @default 0
8 | */
9 | tension: number;
10 | }
11 |
12 | export interface ILineSegmentProps {
13 | x: number;
14 | y: number;
15 | x1: number;
16 | y1: number;
17 | xCPn: number;
18 | yCPn: number;
19 | xCPp1: number;
20 | yCPp1: number;
21 | }
22 |
23 | export class LineSegment extends Element {
24 | /**
25 | * @hidden
26 | */
27 |
28 | _getLineParts(props: Pick): { d: number; k: number } {
29 | // y = x * k + d
30 | const k = (props.y1 - props.y) / (props.x1 - props.x);
31 | const d = props.y - props.x * k;
32 | return { d, k };
33 | }
34 |
35 | /**
36 | * @hidden
37 | */
38 | inRange(mouseX: number, mouseY: number, useFinalPosition: boolean): boolean {
39 | const props = this.getProps(['x', 'x1', 'y', 'y1'], useFinalPosition) as unknown as ILineSegmentProps;
40 | const dk = this._getLineParts(props);
41 | const targetY = mouseX * dk.k + dk.d;
42 | const targetX = (mouseY - dk.d) / dk.k + dk.d;
43 | const range = this.options.borderWidth * 2;
44 | return (
45 | (Math.abs(mouseY - targetY) < range || Math.abs(mouseX - targetX) < range) &&
46 | mouseX + range >= props.x &&
47 | mouseX - range <= props.x1 &&
48 | mouseY + range >= Math.min(props.y, props.y1) &&
49 | mouseY - range <= Math.max(props.y, props.y1)
50 | );
51 | }
52 |
53 | /**
54 | * @hidden
55 | */
56 | tooltipPosition(useFinalPosition: boolean): { x: number; y: number; padding: number } {
57 | const props = this.getProps(['x', 'x1', 'y', 'y1'], useFinalPosition) as unknown as ILineSegmentProps;
58 | return {
59 | x: (props.x1 + props.x) / 2,
60 | y: (props.y1 + props.y) / 2,
61 | padding: this.options.borderWidth,
62 | };
63 | }
64 |
65 | /**
66 | * @hidden
67 | */
68 | getCenterPoint(useFinalPosition: boolean): { x: number; y: number } {
69 | const props = this.getProps(['x', 'x1', 'y', 'y1'], useFinalPosition) as unknown as ILineSegmentProps;
70 | return {
71 | x: (props.x1 + props.x) / 2,
72 | y: (props.y1 + props.y) / 2,
73 | };
74 | }
75 |
76 | /**
77 | * @hidden
78 | */
79 | inXRange(mouseX: number, useFinalPosition: boolean): boolean {
80 | const props = this.getProps(['x', 'x1'], useFinalPosition) as unknown as ILineSegmentProps;
81 | const range = this.options.borderWidth * 2;
82 | return mouseX + range >= props.x && mouseX - range <= props.x1;
83 | }
84 |
85 | /**
86 | * @hidden
87 | */
88 | inYRange(mouseY: number, useFinalPosition: boolean): boolean {
89 | const props = this.getProps(['y', 'y1'], useFinalPosition) as unknown as ILineSegmentProps;
90 | const range = this.options.borderWidth * 2;
91 | return mouseY + range >= Math.min(props.y, props.y1) && mouseY - range <= Math.max(props.y, props.y1);
92 | }
93 |
94 | /**
95 | * @hidden
96 | */
97 | draw(ctx: CanvasRenderingContext2D): void {
98 | const props = this.getProps([
99 | 'x',
100 | 'x1',
101 | 'y',
102 | 'y1',
103 | 'xCPn',
104 | 'yCPn',
105 | 'xCPp1',
106 | 'yCPp1',
107 | ]) as unknown as ILineSegmentProps;
108 | const { options } = this;
109 | ctx.save();
110 |
111 | // Stroke Line Options
112 | ctx.lineCap = options.borderCapStyle;
113 | ctx.setLineDash(options.borderDash || []);
114 | ctx.lineDashOffset = options.borderDashOffset;
115 | ctx.lineJoin = options.borderJoinStyle;
116 | ctx.lineWidth = options.borderWidth;
117 | ctx.strokeStyle = options.borderColor;
118 |
119 | // Stroke Line
120 | ctx.beginPath();
121 |
122 | ctx.moveTo(props.x, props.y);
123 | if (options.tension) {
124 | ctx.bezierCurveTo(props.xCPn, props.yCPn, props.xCPp1, props.yCPp1, props.x1, props.y1);
125 | } else {
126 | ctx.lineTo(props.x1, props.y1);
127 | }
128 |
129 | ctx.stroke();
130 | ctx.restore();
131 | }
132 |
133 | static readonly id = 'lineSegment';
134 |
135 | /**
136 | * @hidden
137 | */
138 | static readonly defaults = /* #__PURE__ */ {
139 | ...LineElement.defaults,
140 | hoverBorderWidth: 4,
141 | hoverBorderColor: 'rgba(0,0,0,0.8)',
142 | borderCapStyle: 'round',
143 | tension: 0,
144 | };
145 |
146 | /**
147 | * @hidden
148 | */
149 | static readonly defaultRoutes = LineElement.defaultRoutes;
150 |
151 | /**
152 | * @hidden
153 | */
154 | static readonly descriptors = (LineElement as any).descriptors;
155 | }
156 |
157 | declare module 'chart.js' {
158 | export interface ElementOptionsByType {
159 | lineSegment: ScriptableAndArrayOptions>;
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/src/elements/LinearAxis.ts:
--------------------------------------------------------------------------------
1 | import {
2 | LinearScale,
3 | LogarithmicScale,
4 | defaults,
5 | LogarithmicScaleOptions,
6 | CartesianScaleOptions,
7 | Element,
8 | LinearScaleOptions,
9 | ScriptableAndArrayOptions,
10 | ScriptableContext,
11 | ChartType,
12 | } from 'chart.js';
13 | import { merge } from 'chart.js/helpers';
14 |
15 | export interface IAxisOptions extends CartesianScaleOptions {
16 | // all options from
17 | // https://www.chartjs.org/docs/latest/axes/cartesian/linear.html#linear-cartesian-axis
18 | /**
19 | * width of the visible axis
20 | * @default 30
21 | */
22 | axisWidth: number;
23 | }
24 |
25 | export interface IAxisProps {
26 | x: number;
27 | y: number;
28 | width: number;
29 | height: number;
30 | top: number;
31 | bottom: number;
32 | left: number;
33 | right: number;
34 | }
35 |
36 | const scaleDefaults = {
37 | axis: 'y',
38 | // a dummy scriptable option to enforce a context environment
39 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
40 | dummyOption: (_ctx: unknown) => 0,
41 | axisWidth: 10,
42 | position: 'right',
43 | };
44 |
45 | export type ILinearAxisOptions = IAxisOptions & LinearScaleOptions;
46 |
47 | export class LinearAxis extends LinearScale {
48 | static readonly id = 'linearAxis';
49 |
50 | static readonly defaults: any = /* #__PURE__ */ merge({}, [defaults.scale, LinearScale.defaults, scaleDefaults]);
51 |
52 | static readonly descriptors = /* #__PURE__ */ {
53 | _scriptable: (name: string): boolean =>
54 | !name.startsWith('before') && !name.startsWith('after') && name !== 'callback' && name !== 'parser',
55 | _indexable: (name: string): boolean => name !== 'borderDash' && name !== 'tickBorderDash',
56 | };
57 |
58 | constructor() {
59 | super({});
60 | }
61 |
62 | update(): number {
63 | const w = this.options.axisWidth;
64 | // copy since it could return self
65 | const props = {
66 | ...(this as unknown as Element).getProps([
67 | 'width',
68 | 'height',
69 | 'top',
70 | 'bottom',
71 | 'left',
72 | 'right',
73 | ]),
74 | };
75 | const h = props.bottom - props.top;
76 | this.left = 0;
77 | this.right = w;
78 | this.top = props.top;
79 | this.bottom = props.bottom;
80 |
81 | const r = super.update(w, h);
82 |
83 | this.top = props.top;
84 | this.bottom = props.bottom;
85 | this.configure();
86 | return r as any;
87 | }
88 |
89 | _computeLabelArea(): void {
90 | return undefined;
91 | }
92 |
93 | draw(c: unknown): void {
94 | const ctx = c as CanvasRenderingContext2D;
95 | ctx.save();
96 | const props = (this as unknown as Element).getProps([
97 | 'x',
98 | 'width',
99 | 'height',
100 | 'top',
101 | 'bottom',
102 | 'left',
103 | 'right',
104 | ]);
105 |
106 | const w = this.options.axisWidth;
107 | if (this.options.position === 'left') {
108 | ctx.translate(props.x - w, 0);
109 | } else {
110 | ctx.translate(props.x, 0);
111 | }
112 | super.draw(props);
113 | ctx.restore();
114 | }
115 | }
116 |
117 | export type ILogarithmicAxisOptions = IAxisOptions & LogarithmicScaleOptions;
118 |
119 | export class LogarithmicAxis extends LogarithmicScale {
120 | static readonly id = 'logarithmicAxis';
121 |
122 | /**
123 | * @hidden
124 | */
125 | static readonly defaults: any = /* #__PURE__ */ merge({}, [defaults.scale, LogarithmicScale.defaults, scaleDefaults]);
126 |
127 | /**
128 | * @hidden
129 | */
130 | static readonly descriptors = /* #__PURE__ */ LinearAxis.descriptors;
131 |
132 | constructor() {
133 | super({});
134 | }
135 |
136 | /**
137 | * @hidden
138 | */
139 |
140 | _computeLabelArea(): void {
141 | return undefined;
142 | }
143 |
144 | /**
145 | * @hidden
146 | */
147 | update(): number {
148 | return LinearAxis.prototype.update.call(this);
149 | }
150 |
151 | /**
152 | * @hidden
153 | */
154 | draw(c: unknown): void {
155 | return LinearAxis.prototype.draw.call(this, c);
156 | }
157 | }
158 |
159 | declare module 'chart.js' {
160 | export interface ElementOptionsByType {
161 | linearAxis: ScriptableAndArrayOptions>;
162 | logarithmicAxis: ScriptableAndArrayOptions>;
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/src/elements/index.ts:
--------------------------------------------------------------------------------
1 | export * from './LinearAxis';
2 | export * from './LineSegment';
3 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './controllers';
2 | export * from './elements';
3 | export * from './scales';
4 |
--------------------------------------------------------------------------------
/src/index.umd.ts:
--------------------------------------------------------------------------------
1 | import { registry } from 'chart.js';
2 | import { ParallelCoordinatesController, LogarithmicParallelCoordinatesController } from './controllers';
3 | import { LineSegment, LinearAxis, LogarithmicAxis } from './elements';
4 | import { PCPScale } from './scales';
5 |
6 | export * from '.';
7 |
8 | registry.addControllers(ParallelCoordinatesController, LogarithmicParallelCoordinatesController);
9 | registry.addElements(LineSegment, LinearAxis, LogarithmicAxis);
10 | registry.addScales(PCPScale);
11 |
--------------------------------------------------------------------------------
/src/scales/PCPScale.ts:
--------------------------------------------------------------------------------
1 | import { CategoryScale, CategoryScaleOptions } from 'chart.js';
2 |
3 | export class PCPScale extends CategoryScale {
4 | getLabels(): string[] {
5 | const { datasets } = this.chart.data;
6 | return this.getMatchingVisibleMetas().map((meta) => {
7 | const ds = datasets[meta.index];
8 | return ds.label ?? '';
9 | });
10 | }
11 |
12 | static readonly id = 'pcp';
13 |
14 | /**
15 | * @hidden
16 | */
17 | static readonly defaults: any = CategoryScale.defaults;
18 | }
19 |
20 | declare module 'chart.js' {
21 | export interface ScaleTypeRegistry {
22 | pcp: {
23 | options: CategoryScaleOptions;
24 | };
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/scales/index.ts:
--------------------------------------------------------------------------------
1 | export * from './PCPScale';
2 |
--------------------------------------------------------------------------------
/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-pcp",
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 |
--------------------------------------------------------------------------------