├── .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 │ ├── bar.md │ ├── bar.ts │ ├── basic.ts │ ├── horizontalBar.md │ ├── horizontalBar.ts │ ├── index.md │ ├── line.md │ ├── line.ts │ ├── lineScatter.md │ ├── lineScatter.ts │ ├── lineTime.md │ ├── lineTime.ts │ ├── multibar.md │ ├── multibar.ts │ ├── polarArea.md │ ├── polarArea.ts │ ├── scatter.md │ └── scatter.ts ├── getting-started.md ├── index.md └── related.md ├── eslint.config.mjs ├── package.json ├── rollup.config.js ├── src ├── __tests__ │ └── createChart.ts ├── animate.ts ├── controllers │ ├── BarWithErrorBarsController.spec.ts │ ├── BarWithErrorBarsController.ts │ ├── LineWithErrorBarsController.spec.ts │ ├── LineWithErrorBarsController.ts │ ├── PolarAreaWithErrorBarsController.spec.ts │ ├── PolarAreaWithErrorBarsController.ts │ ├── ScatterWithErrorBarsController.spec.ts │ ├── ScatterWithErrorBarsController.ts │ ├── __image_snapshots__ │ │ ├── bar-with-error-bars-controller-spec-ts-bar-default-1-snap.png │ │ ├── bar-with-error-bars-controller-spec-ts-bar-time-scale-1-snap.png │ │ ├── line-with-error-bars-controller-spec-ts-line-default-1-snap.png │ │ ├── line-with-error-bars-controller-spec-ts-line-linear-scale-1-snap.png │ │ ├── line-with-error-bars-controller-spec-ts-line-linear-scale-2-snap.png │ │ ├── line-with-error-bars-controller-spec-ts-line-time-scale-1-snap.png │ │ ├── line-with-error-bars-controller-spec-ts-line-time-scale-2-snap.png │ │ ├── polar-area-with-error-bars-controller-spec-ts-bar-default-1-snap.png │ │ └── scatter-with-error-bars-controller-spec-ts-bar-default-1-snap.png │ ├── base.ts │ ├── index.ts │ ├── patchController.ts │ ├── tooltip.ts │ └── utils.ts ├── elements │ ├── ArcWithErrorBar.ts │ ├── BarWithErrorBar.ts │ ├── PointWithErrorBar.ts │ ├── index.ts │ └── render.ts ├── index.ts └── index.umd.ts ├── tsconfig.c.json ├── tsconfig.json ├── typedoc.json ├── vitest.config.ts └── yarn.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | # These settings are for any web project 2 | 3 | # Handle line endings automatically for files detected as text 4 | # and leave all files detected as binary untouched. 5 | * text=auto eol=lf 6 | 7 | # 8 | # The above will handle all files NOT found below 9 | # 10 | 11 | # 12 | ## These files are text and should be normalized (Convert crlf => lf) 13 | # 14 | 15 | # source code 16 | *.php text 17 | *.css text 18 | *.sass text 19 | *.scss text 20 | *.less text 21 | *.styl text 22 | *.js text 23 | *.jsx text 24 | *.ts text 25 | *.tsx text 26 | *.coffee text 27 | *.json text 28 | *.htm text 29 | *.html text 30 | *.xml text 31 | *.txt text 32 | *.ini text 33 | *.inc text 34 | *.pl text 35 | *.rb text 36 | *.py text 37 | *.scm text 38 | *.sql text 39 | *.sh text eof=LF 40 | *.bat text 41 | 42 | # templates 43 | *.hbt text 44 | *.jade text 45 | *.haml text 46 | *.hbs text 47 | *.dot text 48 | *.tmpl text 49 | *.phtml text 50 | 51 | # server config 52 | .htaccess text 53 | 54 | # git config 55 | .gitattributes text 56 | .gitignore text 57 | 58 | # code analysis config 59 | .jshintrc text 60 | .jscsrc text 61 | .jshintignore text 62 | .csslintrc text 63 | 64 | # misc config 65 | *.yaml text 66 | *.yml text 67 | .editorconfig text 68 | 69 | # build config 70 | *.npmignore text 71 | *.bowerrc text 72 | Dockerfile text eof=LF 73 | 74 | # Heroku 75 | Procfile text 76 | .slugignore text 77 | 78 | # Documentation 79 | *.md text 80 | LICENSE text 81 | AUTHORS text 82 | 83 | 84 | # 85 | ## These files are binary and should be left untouched 86 | # 87 | 88 | # (binary is a macro for -text -diff) 89 | *.png binary 90 | *.jpg binary 91 | *.jpeg binary 92 | *.gif binary 93 | *.ico binary 94 | *.mov binary 95 | *.mp4 binary 96 | *.mp3 binary 97 | *.flv binary 98 | *.fla binary 99 | *.swf binary 100 | *.gz binary 101 | *.zip binary 102 | *.7z binary 103 | *.ttf binary 104 | *.pyc binary 105 | *.pdf binary 106 | 107 | # Source files 108 | # ============ 109 | *.pxd text 110 | *.py text 111 | *.py3 text 112 | *.pyw text 113 | *.pyx text 114 | *.sh text eol=lf 115 | *.json text 116 | 117 | # Binary files 118 | # ============ 119 | *.db binary 120 | *.p binary 121 | *.pkl binary 122 | *.pyc binary 123 | *.pyd binary 124 | *.pyo binary 125 | 126 | # Note: .db, .p, and .pkl files are associated 127 | # with the python modules ``pickle``, ``dbm.*``, 128 | # ``shelve``, ``marshal``, ``anydbm``, & ``bsddb`` 129 | # (among others). 130 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [sgratzl] 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug report 3 | about: If something isn't working as expected 🤔. 4 | title: '' 5 | labels: 'bug' 6 | assignees: '' 7 | --- 8 | 9 | 10 | 11 | When I... 12 | 13 | **To Reproduce** 14 | 15 | 17 | 18 | 1. 19 | 20 | **Expected behavior** 21 | 22 | 23 | 24 | **Screenshots** 25 | 26 | 27 | 28 | **Context** 29 | 30 | - Version: 31 | - Browser: 32 | 33 | **Additional context** 34 | 35 | 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | # contact_links: 3 | # - name: Samuel Gratzl 4 | # url: https://www.sgratzl.com 5 | # about: Please ask and answer questions here. 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🚀 Feature Request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: 'enhancement' 6 | assignees: '' 7 | --- 8 | 9 | 10 | 11 | It would be great if ... 12 | 13 | **User story** 14 | 15 | 16 | 17 | **Additional context** 18 | 19 | 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🤗 Question 3 | about: ask question about the library (usage, features,...) 4 | title: '' 5 | labels: 'question' 6 | assignees: '' 7 | --- 8 | 9 | 13 | 14 | I'm having the following question... 15 | 16 | **Screenshots / Sketches** 17 | 18 | 19 | 20 | **Context** 21 | 22 | - Version: 23 | - Browser: 24 | 25 | **Additional context** 26 | 27 | 28 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: 'github-actions' 5 | directory: '/' 6 | schedule: 7 | interval: 'monthly' 8 | target-branch: 'dev' 9 | labels: 10 | - 'dependencies' 11 | - 'chore' 12 | - package-ecosystem: 'npm' 13 | directory: '/' 14 | schedule: 15 | interval: 'monthly' 16 | target-branch: 'dev' 17 | labels: 18 | - 'dependencies' 19 | - 'chore' 20 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$RESOLVED_VERSION' 2 | tag-template: 'v$RESOLVED_VERSION' 3 | categories: 4 | - title: '🚀 Features' 5 | labels: 6 | - 'enhancement' 7 | - 'feature' 8 | - title: '🐛 Bugs Fixes' 9 | labels: 10 | - 'bug' 11 | - title: 'Documentation' 12 | labels: 13 | - 'documentation' 14 | - title: '🧰 Development' 15 | labels: 16 | - 'chore' 17 | change-template: '- #$NUMBER $TITLE' 18 | change-title-escapes: '\<*_&`#@' 19 | template: | 20 | $CHANGES 21 | 22 | Thanks to $CONTRIBUTORS 23 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: actions/setup-node@v4 13 | with: 14 | node-version: 20 15 | - run: npm i -g yarn 16 | - run: yarn config set checksumBehavior ignore 17 | - name: Cache Node.js modules 18 | uses: actions/cache@v4 19 | with: 20 | path: | 21 | ./.yarn/cache 22 | ./.yarn/unplugged 23 | key: ${{ runner.os }}-yarn2-v5-${{ hashFiles('**/yarn.lock') }} 24 | restore-keys: | 25 | ${{ runner.os }}-yarn2-v5 26 | - run: yarn install 27 | - run: yarn build 28 | - run: yarn lint 29 | - run: yarn test 30 | - uses: actions/upload-artifact@v4 31 | if: failure() 32 | with: 33 | name: diff outputs 34 | path: src/**/__diff_output__/*.png 35 | - run: yarn docs:build 36 | -------------------------------------------------------------------------------- /.github/workflows/create_release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | versionName: 6 | description: 'Semantic Version Number (i.e., 5.5.0 or patch, minor, major, prepatch, preminor, premajor, prerelease)' 7 | required: true 8 | default: patch 9 | preid: 10 | description: 'Pre Release Identifier (i.e., alpha, beta)' 11 | required: true 12 | default: alpha 13 | jobs: 14 | create_release: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Check out code 18 | uses: actions/checkout@v4 19 | with: 20 | ref: main 21 | ssh-key: ${{ secrets.PRIVATE_SSH_KEY }} 22 | - name: Reset main branch 23 | run: | 24 | git fetch origin dev:dev 25 | git reset --hard origin/dev 26 | - name: Change version number 27 | id: version 28 | run: | 29 | echo "next_tag=$(npm version --no-git-tag-version ${{ github.event.inputs.versionName }} --preid ${{ github.event.inputs.preid }})" >> $GITHUB_OUTPUT 30 | - name: Create pull request into main 31 | uses: peter-evans/create-pull-request@v7 32 | with: 33 | branch: release/${{ steps.version.outputs.next_tag }} 34 | commit-message: 'chore: release ${{ steps.version.outputs.next_tag }}' 35 | base: main 36 | title: Release ${{ steps.version.outputs.next_tag }} 37 | labels: chore 38 | assignees: sgratzl 39 | body: | 40 | Releasing ${{ steps.version.outputs.next_tag }}. 41 | -------------------------------------------------------------------------------- /.github/workflows/deploy_website.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Website 2 | on: 3 | workflow_dispatch: {} 4 | push: 5 | branches: 6 | - main 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | pages: write 12 | id-token: write 13 | environment: 14 | name: github-pages 15 | url: ${{ steps.deployment.outputs.page_url }} 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: 20 23 | cache: npm 24 | - run: npm i -g yarn 25 | - run: yarn config set checksumBehavior ignore 26 | - name: Cache Node.js modules 27 | uses: actions/cache@v4 28 | with: 29 | path: | 30 | ./.yarn/cache 31 | ./.yarn/unplugged 32 | key: ${{ runner.os }}-yarn2-v5-${{ hashFiles('**/yarn.lock') }} 33 | restore-keys: | 34 | ${{ runner.os }}-yarn2-v5 35 | - run: yarn install 36 | - run: yarn docs:build 37 | - uses: actions/configure-pages@v5 38 | - uses: actions/upload-pages-artifact@v3 39 | with: 40 | path: docs/.vitepress/dist 41 | - name: Deploy 42 | id: deployment 43 | uses: actions/deploy-pages@v4 44 | -------------------------------------------------------------------------------- /.github/workflows/release_helper.yml: -------------------------------------------------------------------------------- 1 | name: Release Helper 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | correct_repository: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: fail on fork 12 | if: github.repository_owner != 'sgratzl' 13 | run: exit 1 14 | 15 | create_release: 16 | needs: correct_repository 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Check out code 20 | uses: actions/checkout@v4 21 | - uses: actions/setup-node@v4 22 | with: 23 | node-version: 20 24 | - name: Extract version 25 | id: extract_version 26 | run: | 27 | node -pe "'version=' + require('./package.json').version" >> $GITHUB_OUTPUT 28 | node -pe "'npm_tag=' + (require('./package.json').version.includes('-') ? 'next' : 'latest')" >> $GITHUB_OUTPUT 29 | - name: Print version 30 | run: | 31 | echo "releasing ${{ steps.extract_version.outputs.version }} with tag ${{ steps.extract_version.outputs.npm_tag }}" 32 | - name: Create Release 33 | id: create_release 34 | uses: release-drafter/release-drafter@v6 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | with: 38 | name: v${{ steps.extract_version.outputs.version }} 39 | tag: v${{ steps.extract_version.outputs.version }} 40 | version: ${{ steps.extract_version.outputs.version }} 41 | prerelease: ${{ needs.create_release.outputs.tag_name == 'next' }} 42 | publish: true 43 | outputs: 44 | version: ${{ steps.extract_version.outputs.version }} 45 | npm_tag: ${{ steps.extract_version.outputs.npm_tag }} 46 | upload_url: ${{ steps.create_release.outputs.upload_url }} 47 | tag_name: ${{ steps.create_release.outputs.tag_name }} 48 | 49 | build_assets: 50 | needs: create_release 51 | runs-on: ubuntu-latest 52 | steps: 53 | - name: Check out code 54 | uses: actions/checkout@v4 55 | - uses: actions/setup-node@v4 56 | with: 57 | node-version: 20 58 | - run: npm i -g yarn 59 | - run: yarn config set checksumBehavior ignore 60 | - name: Cache Node.js modules 61 | uses: actions/cache@v4 62 | with: 63 | path: | 64 | ./.yarn/cache 65 | ./.yarn/unplugged 66 | key: ${{ runner.os }}-yarn2-v5-${{ hashFiles('**/yarn.lock') }} 67 | restore-keys: | 68 | ${{ runner.os }}-yarn2-v5 69 | - run: yarn install 70 | - run: yarn build 71 | - run: yarn pack 72 | - name: Upload Release Asset 73 | uses: AButler/upload-release-assets@v3.0 74 | with: 75 | files: 'package.tgz' 76 | repo-token: ${{ secrets.GITHUB_TOKEN }} 77 | release-tag: ${{ needs.create_release.outputs.tag_name }} 78 | - name: Pack Publish 79 | run: | 80 | yarn config set npmAuthToken "${{ secrets.NPM_TOKEN }}" 81 | yarn pack 82 | yarn npm publish --tag "${{ needs.create_release.outputs.npm_tag }}" 83 | 84 | sync_dev: 85 | needs: correct_repository 86 | runs-on: ubuntu-latest 87 | steps: 88 | - name: Check out code 89 | uses: actions/checkout@v4 90 | with: 91 | ref: dev 92 | ssh-key: ${{ secrets.PRIVATE_SSH_KEY }} 93 | - name: Reset dev branch 94 | run: | 95 | git fetch origin main:main 96 | git merge main 97 | git push 98 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | /coverage 7 | /node_modules 8 | .npm 9 | .yarn/* 10 | !.yarn/patches 11 | !.yarn/releases 12 | !.yarn/plugins 13 | !.yarn/versions 14 | .pnp.* 15 | 16 | # Build files 17 | /.tmp 18 | /build 19 | 20 | *.tgz 21 | /.vscode/extensions.json 22 | *.tsbuildinfo 23 | .eslintcache 24 | __diff_output__ 25 | 26 | docs/.vitepress/dist 27 | docs/.vitepress/cache 28 | docs/.vitepress/config.ts.timestamp* 29 | docs/api/ -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /.pnp* 2 | /.yarnrc.yml 3 | /.yarn 4 | /build 5 | /docs/.vitepress/cache 6 | /docs/.vitepress/dist 7 | /docs/.vitepress/config.ts.timestamp* 8 | /docs/api 9 | /coverage 10 | /.gitattributes 11 | /.gitignore 12 | /.prettierignore 13 | /LICENSE 14 | /yarn.lock 15 | /.vscode 16 | *.png 17 | *.tgz 18 | *.tsbuildinfo 19 | .eslintcache 20 | .nojekyll 21 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 120, 3 | semi: true, 4 | singleQuote: true, 5 | trailingComma: 'es5', 6 | }; 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.formatOnType": true, 4 | "[javascript]": { 5 | "editor.defaultFormatter": "esbenp.prettier-vscode" 6 | }, 7 | "[typescript]": { 8 | "editor.defaultFormatter": "esbenp.prettier-vscode" 9 | }, 10 | "[json]": { 11 | "editor.defaultFormatter": "esbenp.prettier-vscode" 12 | }, 13 | "[yaml]": { 14 | "editor.defaultFormatter": "esbenp.prettier-vscode" 15 | }, 16 | "npm.packageManager": "yarn", 17 | "eslint.nodePath": ".yarn/sdks", 18 | "prettier.prettierPath": ".yarn/sdks/prettier/index.cjs", 19 | "files.eol": "\n", 20 | "typescript.tsdk": ".yarn/sdks/typescript/lib", 21 | "typescript.enablePromptUseWorkspaceTsdk": true, 22 | "editor.detectIndentation": false, 23 | "editor.tabSize": 2, 24 | "search.exclude": { 25 | "**/.yarn": true, 26 | "**/.pnp.*": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | yarnPath: .yarn/releases/yarn-4.5.1.cjs 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2023 Samuel Gratzl 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chart.js Error Bars 2 | 3 | [![License: MIT][mit-image]][mit-url] [![NPM Package][npm-image]][npm-url] [![Github Actions][github-actions-image]][github-actions-url] 4 | 5 | Chart.js module for charting error bars plots. This plugin extends the several char types (`bar`, `line`, `scatter`, `polarArea`) 6 | with their error bar equivalent (`barWithErrorBars`, `lineWithErrorBars`, `scatterWithErrorBars`, `polarAreaWithErrorBars`). 7 | 8 | Bar Chart 9 | 10 | ![bar char with error bars](https://user-images.githubusercontent.com/4129778/65203797-1a9e3b00-da5a-11e9-9de7-9cbcf75dfeda.png) 11 | 12 | Horizontal Bar Chart 13 | 14 | ![horizontal bar chart with error bars](https://user-images.githubusercontent.com/4129778/65203796-1a9e3b00-da5a-11e9-9c43-db503679178c.png) 15 | 16 | Line Chart 17 | 18 | ![line chart with error bars](https://user-images.githubusercontent.com/4129778/65203795-1a05a480-da5a-11e9-98fa-05440371485f.png) 19 | 20 | Scatterplot 21 | 22 | ![scatter plot with error bars](https://user-images.githubusercontent.com/4129778/65203792-1a05a480-da5a-11e9-9073-6e849d42af64.png) 23 | 24 | Polar Area plot 25 | 26 | ![polar area plot with error bars](https://user-images.githubusercontent.com/4129778/65203794-1a05a480-da5a-11e9-9b17-316ecc6ae0d9.png) 27 | 28 | ## Related Plugins 29 | 30 | Check out also my other chart.js plugins: 31 | 32 | - [chartjs-chart-boxplot](https://github.com/sgratzl/chartjs-chart-boxplot) for rendering boxplots and violin plots 33 | - [chartjs-chart-geo](https://github.com/sgratzl/chartjs-chart-geo) for rendering map, bubble maps, and choropleth charts 34 | - [chartjs-chart-graph](https://github.com/sgratzl/chartjs-chart-graph) for rendering graphs, trees, and networks 35 | - [chartjs-chart-pcp](https://github.com/sgratzl/chartjs-chart-pcp) for rendering parallel coordinate plots 36 | - [chartjs-chart-venn](https://github.com/sgratzl/chartjs-chart-venn) for rendering venn and euler diagrams 37 | - [chartjs-chart-wordcloud](https://github.com/sgratzl/chartjs-chart-wordcloud) for rendering word clouds 38 | - [chartjs-plugin-hierarchical](https://github.com/sgratzl/chartjs-plugin-hierarchical) for rendering hierarchical categorical axes which can be expanded and collapsed 39 | 40 | ## Install 41 | 42 | ```bash 43 | npm install --save chart.js chartjs-chart-error-bars 44 | ``` 45 | 46 | ## Usage 47 | 48 | see [Examples](https://www.sgratzl.com/chartjs-chart-error-bars/examples) 49 | 50 | and [![Open in CodePen][codepen]](https://codepen.io/sgratzl/pen/ZEbqmqx) 51 | 52 | ## Styling 53 | 54 | Several new styling keys are added to the individual chart types 55 | 56 | ## Data structure 57 | 58 | The data structure depends on the chart type. It uses the fact that chart.js is supporting scatterplots. Thus, it is already prepared for object values. 59 | 60 | ### Chart types: `bar` and `line` 61 | 62 | see TypeScript Interface: 63 | 64 | [IErrorBarXDataPoint](https://github.com/sgratzl/chartjs-chart-error-bars/blob/main/src/controllers/base.ts#L3-L16) 65 | 66 | ### Chart type: `bar` with `indexAxis: 'y'` 67 | 68 | [IErrorBarYDataPoint](https://github.com/sgratzl/chartjs-chart-error-bars/blob/main/src/controllers/base.ts#L18-L31) 69 | 70 | ### Chart type: `scatter` 71 | 72 | a combination of the previous two ones 73 | 74 | [IErrorBarXDataPoint](https://github.com/sgratzl/chartjs-chart-error-bars/blob/main/src/controllers/base.ts#L3-L16) 75 | 76 | and 77 | 78 | [IErrorBarYDataPoint](https://github.com/sgratzl/chartjs-chart-error-bars/blob/main/src/controllers/base.ts#L18-L31) 79 | 80 | ### Chart type: `polarArea` 81 | 82 | [IErrorBarRDataPoint](https://github.com/sgratzl/chartjs-chart-error-bars/blob/main/src/controllers/base.ts#L33-L46) 83 | 84 | ## Multiple Error Bars 85 | 86 | Multiple error bars are supported. 87 | 88 | ![multiple error bars](https://user-images.githubusercontent.com/4129778/65359671-3d039600-dbcb-11e9-905e-1dd22b5e8783.png) 89 | 90 | ### Styling 91 | 92 | The styling options support different array version. 93 | 94 | **Note**: as with other chart.js style options, using an array will be one value per dataset. Thus, to specify the values for different error bars, one needs to wrap it in an object with a `v` key having the value itself. The outer for the dataset, the inner for the error bars. 95 | 96 | see TypeScript interface: 97 | 98 | [IErrorBarOptions](https://github.com/sgratzl/chartjs-chart-error-bars/blob/main/src/elements/render.ts#L17-L54) 99 | 100 | ### Data structure 101 | 102 | Just use array of numbers for the corresponding data structures attributes (`xMin`, `xMax`, `yMin`, `yMax`). The error bars will be rendered in reversed order. Thus, by convention the most inner error bar is in the first place. 103 | 104 | e.g. 105 | 106 | ```ts 107 | { 108 | y: 4, 109 | yMin: [2, 1], 110 | yMax: [5, 6] 111 | } 112 | ``` 113 | 114 | ### ESM and Tree Shaking 115 | 116 | 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. 117 | 118 | Variant A: 119 | 120 | ```js 121 | import Chart, { LinearScale, CategoryScale } from 'chart.js'; 122 | import { BarWithErrorBarsController, BarWithErrorBar } from 'chartjs-chart-error-bars'; 123 | 124 | // register controller in chart.js and ensure the defaults are set 125 | Chart.register(BarWithErrorBarsController, BarWithErrorBar, LinearScale, CategoryScale); 126 | 127 | const chart = new Chart(document.getElementById('canvas').getContext('2d'), { 128 | type: BarWithErrorBarsController.id, 129 | data: { 130 | labels: ['A', 'B'], 131 | datasets: [ 132 | { 133 | data: [ 134 | { 135 | y: 4, 136 | yMin: 1, 137 | yMax: 6, 138 | }, 139 | { 140 | y: 2, 141 | yMin: 1, 142 | yMax: 4, 143 | }, 144 | ], 145 | }, 146 | ], 147 | }, 148 | }); 149 | ``` 150 | 151 | Variant B: 152 | 153 | ```js 154 | import { BarWithErrorBarsChart } from 'chartjs-chart-error-bars'; 155 | 156 | const chart = new BarWithErrorBarsChart(document.getElementById('canvas').getContext('2d'), { 157 | data: { 158 | //... 159 | }, 160 | }); 161 | ``` 162 | 163 | ## Development Environment 164 | 165 | ```sh 166 | npm i -g yarn 167 | yarn install 168 | yarn sdks vscode 169 | ``` 170 | 171 | ### Building 172 | 173 | ```sh 174 | yarn install 175 | yarn build 176 | ``` 177 | 178 | [mit-image]: https://img.shields.io/badge/License-MIT-yellow.svg 179 | [mit-url]: https://opensource.org/licenses/MIT 180 | [npm-image]: https://badge.fury.io/js/chartjs-chart-error-bars.svg 181 | [npm-url]: https://npmjs.org/package/chartjs-chart-error-bars 182 | [github-actions-image]: https://github.com/sgratzl/chartjs-chart-error-bars/workflows/ci/badge.svg 183 | [github-actions-url]: https://github.com/sgratzl/chartjs-chart-error-bars/actions 184 | [codepen]: https://img.shields.io/badge/CodePen-open-blue?logo=codepen 185 | -------------------------------------------------------------------------------- /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: 'Bar Chart', link: '/examples/bar' }, 29 | { text: 'Line Chart', link: '/examples/line' }, 30 | { text: 'Scatter Chart', link: '/examples/scatter' }, 31 | { text: 'Polar Area Chart', link: '/examples/polarArea' }, 32 | { text: 'Horizontal Bar Chart', link: '/examples/horizontalBar' }, 33 | { text: 'Multiple Error Bars', link: '/examples/multibar' }, 34 | { text: 'Line As Scatter Chart', link: '/examples/lineScatter' }, 35 | { text: 'Line Timeseries Chart', link: '/examples/lineTime' }, 36 | ], 37 | }, 38 | { 39 | text: 'API', 40 | collapsed: true, 41 | items: typedocSidebar, 42 | }, 43 | ], 44 | 45 | socialLinks: [{ icon: 'github', link: repository.url.replace('.git', '') }], 46 | 47 | footer: { 48 | message: `Released under the ${license} license.`, 52 | copyright: `Copyright © 2019-present ${author.name}`, 53 | }, 54 | 55 | editLink: { 56 | pattern: `${repository.url.replace('.git', '')}/edit/main/docs/:path`, 57 | }, 58 | 59 | search: { 60 | provider: 'local', 61 | }, 62 | }, 63 | }); 64 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import Theme from 'vitepress/theme'; 2 | import { createTypedChart } from 'vue-chartjs'; 3 | import { 4 | LinearScale, 5 | CategoryScale, 6 | RadialLinearScale, 7 | Tooltip, 8 | Colors, 9 | BarElement, 10 | LineElement, 11 | PointElement, 12 | BarController, 13 | LineController, 14 | TimeScale, 15 | } from 'chart.js'; 16 | import { 17 | ArcWithErrorBar, 18 | BarWithErrorBar, 19 | BarWithErrorBarsController, 20 | LineWithErrorBarsController, 21 | PolarAreaWithErrorBarsController, 22 | PointWithErrorBar, 23 | ScatterWithErrorBarsController, 24 | } from '../../../src'; 25 | 26 | export default { 27 | ...Theme, 28 | enhanceApp({ app }) { 29 | const deps = [ 30 | LinearScale, 31 | CategoryScale, 32 | RadialLinearScale, 33 | TimeScale, 34 | ArcWithErrorBar, 35 | BarWithErrorBar, 36 | BarWithErrorBarsController, 37 | LineWithErrorBarsController, 38 | PolarAreaWithErrorBarsController, 39 | PointWithErrorBar, 40 | ScatterWithErrorBarsController, 41 | Tooltip, 42 | Colors, 43 | BarElement, 44 | LineElement, 45 | PointElement, 46 | BarController, 47 | LineController, 48 | ]; 49 | app.component('BarWithErrorBarsChart', createTypedChart('barWithErrorBars', deps)); 50 | app.component('LineWithErrorBarsChart', createTypedChart('lineWithErrorBars', deps)); 51 | app.component('PolarAreaWithErrorBarsChart', createTypedChart('polarAreaWithErrorBars', deps)); 52 | app.component('ScatterWithErrorBarsChart', createTypedChart('scatterWithErrorBars', deps)); 53 | }, 54 | }; 55 | -------------------------------------------------------------------------------- /docs/examples/bar.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Bar With Error Bars 3 | --- 4 | 5 | # Bar With Error Bars 6 | 7 | 10 | 11 | 15 | 16 | ### Code 17 | 18 | <<< ./bar.ts#config 19 | -------------------------------------------------------------------------------- /docs/examples/bar.ts: -------------------------------------------------------------------------------- 1 | import type { ChartConfiguration } from 'chart.js'; 2 | import type {} from '../../src'; 3 | 4 | // #region config 5 | export const config: ChartConfiguration<'barWithErrorBars'> = { 6 | type: 'barWithErrorBars', 7 | data: { 8 | labels: ['A', 'B'], 9 | datasets: [ 10 | { 11 | data: [ 12 | { 13 | y: 4, 14 | yMin: 1, 15 | yMax: 6, 16 | }, 17 | { 18 | y: 2, 19 | yMin: 1, 20 | yMax: 4, 21 | }, 22 | ], 23 | }, 24 | ], 25 | }, 26 | }; 27 | // #endregion config 28 | -------------------------------------------------------------------------------- /docs/examples/basic.ts: -------------------------------------------------------------------------------- 1 | import type { ChartConfiguration } from 'chart.js'; 2 | import type {} from '../../src'; 3 | 4 | // #region bar 5 | export const bar: ChartConfiguration<'barWithErrorBars'> = { 6 | type: 'barWithErrorBars', 7 | data: { 8 | labels: ['A', 'B'], 9 | datasets: [ 10 | { 11 | data: [ 12 | { 13 | y: 4, 14 | yMin: 1, 15 | yMax: 6, 16 | }, 17 | { 18 | y: 2, 19 | yMin: 1, 20 | yMax: 4, 21 | }, 22 | ], 23 | }, 24 | ], 25 | }, 26 | }; 27 | // #endregion bar 28 | 29 | // #region scatter 30 | export const scatter: ChartConfiguration<'scatterWithErrorBars'> = { 31 | type: 'scatterWithErrorBars', 32 | data: { 33 | labels: ['A', 'B'], 34 | datasets: [ 35 | { 36 | data: [ 37 | { 38 | x: 2, 39 | xMin: 1, 40 | xMax: 3, 41 | y: 4, 42 | yMin: 1, 43 | yMax: 6, 44 | }, 45 | { 46 | x: 7, 47 | xMin: 6, 48 | xMax: 9, 49 | y: 2, 50 | yMin: 1, 51 | yMax: 4, 52 | }, 53 | ], 54 | }, 55 | ], 56 | }, 57 | }; 58 | // #endregion scatter 59 | -------------------------------------------------------------------------------- /docs/examples/horizontalBar.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Horizontal Bar With Error Bars 3 | --- 4 | 5 | # Horizontal Bar With Error Bars 6 | 7 | 10 | 11 | 15 | 16 | ### Code 17 | 18 | <<< ./horizontalBar.ts#config 19 | -------------------------------------------------------------------------------- /docs/examples/horizontalBar.ts: -------------------------------------------------------------------------------- 1 | import type { ChartConfiguration } from 'chart.js'; 2 | import type {} from '../../src'; 3 | 4 | // #region config 5 | export const config: ChartConfiguration<'barWithErrorBars'> = { 6 | type: 'barWithErrorBars', 7 | data: { 8 | labels: ['A', 'B'], 9 | datasets: [ 10 | { 11 | data: [ 12 | { 13 | x: 4, 14 | xMin: 1, 15 | xMax: 6, 16 | }, 17 | { 18 | x: 2, 19 | xMin: 1, 20 | xMax: 4, 21 | }, 22 | ], 23 | }, 24 | ], 25 | }, 26 | options: { 27 | indexAxis: 'y', 28 | }, 29 | }; 30 | // #endregion config 31 | -------------------------------------------------------------------------------- /docs/examples/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Examples 3 | --- 4 | 5 | # Examples 6 | 7 | 13 | 14 | ## Bar With Error Bars 15 | 16 | 20 | 21 | ### Code 22 | 23 | <<< ./bar.ts#config 24 | 25 | ## Line With Error Bars 26 | 27 | 31 | 32 | ### Code 33 | 34 | <<< ./line.ts#config 35 | 36 | ## Scatter With Error Bars 37 | 38 | 42 | 43 | ### Code 44 | 45 | <<< ./scatter.ts#config 46 | 47 | ## Polar Area With Error Bars 48 | 49 | 53 | 54 | ### Code 55 | 56 | <<< ./polarArea.ts#config 57 | -------------------------------------------------------------------------------- /docs/examples/line.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Line With Error Bars 3 | --- 4 | 5 | # Lien With Error Bars 6 | 7 | 10 | 11 | 15 | 16 | ### Code 17 | 18 | <<< ./line.ts#config 19 | -------------------------------------------------------------------------------- /docs/examples/line.ts: -------------------------------------------------------------------------------- 1 | import type { ChartConfiguration } from 'chart.js'; 2 | import type {} from '../../src'; 3 | 4 | // #region config 5 | export const config: ChartConfiguration<'lineWithErrorBars'> = { 6 | type: 'lineWithErrorBars', 7 | data: { 8 | labels: ['A', 'B'], 9 | datasets: [ 10 | { 11 | data: [ 12 | { 13 | y: 4, 14 | yMin: 1, 15 | yMax: 6, 16 | }, 17 | { 18 | y: 2, 19 | yMin: 1, 20 | yMax: 4, 21 | }, 22 | ], 23 | }, 24 | ], 25 | }, 26 | }; 27 | // #endregion config 28 | -------------------------------------------------------------------------------- /docs/examples/lineScatter.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Line As Scatter With Error Bars 3 | --- 4 | 5 | # Lien As Scatter With Error Bars 6 | 7 | 10 | 11 | 15 | 16 | ### Code 17 | 18 | <<< ./lineScatter.ts#config 19 | -------------------------------------------------------------------------------- /docs/examples/lineScatter.ts: -------------------------------------------------------------------------------- 1 | import type { ChartConfiguration } from 'chart.js'; 2 | import type {} from '../../src'; 3 | 4 | // #region config 5 | export const config: ChartConfiguration<'lineWithErrorBars'> = { 6 | type: 'lineWithErrorBars', 7 | data: { 8 | datasets: [ 9 | { 10 | data: [ 11 | { 12 | x: 1, 13 | y: 4, 14 | yMin: 1, 15 | yMax: 6, 16 | }, 17 | { 18 | x: 0.5, 19 | y: 2, 20 | yMin: 1, 21 | yMax: 4, 22 | }, 23 | { 24 | x: 3, 25 | y: 2, 26 | yMin: 1, 27 | yMax: 4, 28 | }, 29 | ], 30 | }, 31 | ], 32 | }, 33 | options: { 34 | scales: { 35 | // x: { 36 | // type: 'category', 37 | // labels: ['A', 'B'] 38 | // }, 39 | x: { type: 'linear' }, 40 | }, 41 | }, 42 | }; 43 | // #endregion config 44 | -------------------------------------------------------------------------------- /docs/examples/lineTime.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Line TimeSeries With Error Bars 3 | --- 4 | 5 | # Line TimeSeries With Error Bars 6 | 7 | 10 | 11 | 15 | 16 | ### Code 17 | 18 | <<< ./lineTime.ts#config 19 | -------------------------------------------------------------------------------- /docs/examples/lineTime.ts: -------------------------------------------------------------------------------- 1 | import type { ChartConfiguration } from 'chart.js'; 2 | import 'chartjs-adapter-date-fns'; 3 | import type {} from '../../src'; 4 | 5 | // #region config 6 | export const config: ChartConfiguration<'lineWithErrorBars'> = { 7 | type: 'lineWithErrorBars', 8 | data: { 9 | datasets: [ 10 | { 11 | data: [ 12 | { 13 | x: '2016-12-25' as unknown, 14 | y: 4, 15 | yMin: 1, 16 | yMax: 6, 17 | }, 18 | { 19 | x: '2017-12-25' as unknown, 20 | y: 6, 21 | yMin: 2, 22 | yMax: 8, 23 | }, 24 | { 25 | x: '2018-12-25' as unknown, 26 | y: 2, 27 | yMin: 1, 28 | yMax: 4, 29 | }, 30 | ], 31 | }, 32 | ], 33 | }, 34 | options: { 35 | scales: { 36 | x: { 37 | type: 'time', 38 | // type: 'timeseries', 39 | }, 40 | }, 41 | }, 42 | }; 43 | // #endregion config 44 | -------------------------------------------------------------------------------- /docs/examples/multibar.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Bar With Multiple Error Bars 3 | --- 4 | 5 | # Bar With Multiple Error Bars 6 | 7 | 10 | 11 | 15 | 16 | ### Code 17 | 18 | <<< ./multibar.ts#config 19 | -------------------------------------------------------------------------------- /docs/examples/multibar.ts: -------------------------------------------------------------------------------- 1 | import type { ChartConfiguration } from 'chart.js'; 2 | import type {} from '../../src'; 3 | 4 | // #region config 5 | export const config: ChartConfiguration<'barWithErrorBars'> = { 6 | type: 'barWithErrorBars', 7 | data: { 8 | labels: ['A', 'B'], 9 | datasets: [ 10 | { 11 | data: [ 12 | { 13 | y: 4, 14 | yMin: [2, 1], 15 | yMax: [5, 6], 16 | }, 17 | { 18 | y: 2, 19 | yMin: [1, 0], 20 | yMax: [3, 7], 21 | }, 22 | ], 23 | }, 24 | ], 25 | }, 26 | }; 27 | // #endregion config 28 | -------------------------------------------------------------------------------- /docs/examples/polarArea.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Polar Area With Error Bars 3 | --- 4 | 5 | # Polar Area With Error Bars 6 | 7 | 10 | 11 | 15 | 16 | ### Code 17 | 18 | <<< ./polarArea.ts#config 19 | -------------------------------------------------------------------------------- /docs/examples/polarArea.ts: -------------------------------------------------------------------------------- 1 | import type { ChartConfiguration } from 'chart.js'; 2 | import type {} from '../../src'; 3 | 4 | // #region config 5 | export const config: ChartConfiguration<'polarAreaWithErrorBars'> = { 6 | type: 'polarAreaWithErrorBars', 7 | data: { 8 | labels: ['A', 'B'], 9 | datasets: [ 10 | { 11 | data: [ 12 | { 13 | r: 4, 14 | rMin: 1, 15 | rMax: 6, 16 | }, 17 | { 18 | r: 2, 19 | rMin: 1, 20 | rMax: 4, 21 | }, 22 | ], 23 | }, 24 | ], 25 | }, 26 | }; 27 | // #endregion config 28 | -------------------------------------------------------------------------------- /docs/examples/scatter.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Scatter With Error Bars 3 | --- 4 | 5 | # Scatter With Error Bars 6 | 7 | 10 | 11 | 15 | 16 | ### Code 17 | 18 | <<< ./scatter.ts#config 19 | -------------------------------------------------------------------------------- /docs/examples/scatter.ts: -------------------------------------------------------------------------------- 1 | import type { ChartConfiguration } from 'chart.js'; 2 | import type {} from '../../src'; 3 | 4 | // #region config 5 | export const config: ChartConfiguration<'scatterWithErrorBars'> = { 6 | type: 'scatterWithErrorBars', 7 | data: { 8 | labels: ['A', 'B'], 9 | datasets: [ 10 | { 11 | data: [ 12 | { 13 | x: 2, 14 | xMin: 1, 15 | xMax: 3, 16 | y: 4, 17 | yMin: 1, 18 | yMax: 6, 19 | }, 20 | { 21 | x: 7, 22 | xMin: 6, 23 | xMax: 9, 24 | y: 2, 25 | yMin: 1, 26 | yMax: 4, 27 | }, 28 | ], 29 | }, 30 | ], 31 | }, 32 | }; 33 | // #endregion config 34 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started 3 | --- 4 | 5 | # Getting Started 6 | 7 | Chart.js module for charting error bars plots. This plugin extends the several char types (`bar`, `line`, `scatter`, `polarArea`) 8 | with their error bar equivalent (`barWithErrorBars`, `lineWithErrorBars`, `scatterWithErrorBars`, `polarAreaWithErrorBars`). 9 | 10 | ## Install 11 | 12 | ```sh 13 | npm install chart.js chartjs-chart-error-bars 14 | ``` 15 | 16 | ## Usage 17 | 18 | see [Examples](./examples/) 19 | 20 | and [CodePen](https://codepen.io/sgratzl/pen/ZEbqmqx) 21 | 22 | ## Configuration 23 | 24 | ### Data Structure 25 | 26 | #### Chart types: `bar` and `line` 27 | 28 | see TypeScript Interface: [IErrorBarXDataPoint](/api/interfaces/IErrorBarXDataPoint.md) 29 | 30 | #### Chart type: `bar` with `indexAxis: 'y'` 31 | 32 | see [IErrorBarYDataPoint](/api/interfaces/IErrorBarYDataPoint.md) 33 | 34 | #### Chart type: `scatter` 35 | 36 | a combination of the previous two ones [IErrorBarXDataPoint](/api/interfaces/IErrorBarXDataPoint.md) and [IErrorBarYDataPoint](/api/interfaces/IErrorBarYDataPoint.md) 37 | 38 | #### Chart type: `polarArea` 39 | 40 | see [IErrorBarRDataPoint](/api/interfaces/IErrorBarRDataPoint.md) 41 | 42 | ### Styling 43 | 44 | The styling options support different array version. 45 | 46 | **Note**: as with other chart.js style options, using an array will be one value per dataset. Thus, to specify the values for different error bars, one needs to wrap it in an object with a `v` key having the value itself. The outer for the dataset, the inner for the error bars. 47 | 48 | see TypeScript interface: [IErrorBarOptions](/api/interfaces/IErrorBarOptions.md) 49 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: 'chartjs-chart-error-bars' 7 | text: 'chart.js plugin' 8 | tagline: Chart.js module for charting error bars 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-error-bars", 3 | "description": "Chart.js module for charting error bars", 4 | "version": "4.4.4", 5 | "author": { 6 | "name": "Samuel Gratzl", 7 | "email": "samu@sgratzl.com", 8 | "url": "https://www.sgratzl.com" 9 | }, 10 | "license": "MIT", 11 | "homepage": "https://github.com/sgratzl/chartjs-chart-error-bars", 12 | "bugs": { 13 | "url": "https://github.com/sgratzl/chartjs-chart-error-bars/issues" 14 | }, 15 | "keywords": [ 16 | "chart.js", 17 | "error-bars" 18 | ], 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/sgratzl/chartjs-chart-error-bars.git" 22 | }, 23 | "global": "ChartErrorBars", 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 | "chartjs-adapter-date-fns": "^3.0.0", 67 | "date-fns": "^4.1.0", 68 | "eslint": "~9.14.0", 69 | "eslint-plugin-prettier": "^5.2.1", 70 | "jest-image-snapshot": "^6.4.0", 71 | "jsdom": "^25.0.1", 72 | "prettier": "^3.3.3", 73 | "rimraf": "^6.0.1", 74 | "rollup": "^4.27.2", 75 | "rollup-plugin-cleanup": "^3.2.1", 76 | "rollup-plugin-dts": "^6.1.1", 77 | "ts-jest": "^29.2.5", 78 | "tslib": "^2.8.1", 79 | "typedoc": "^0.26.11", 80 | "typedoc-plugin-markdown": "^4.2.10", 81 | "typedoc-vitepress-theme": "^1.0.2", 82 | "typescript": "^5.6.3", 83 | "typescript-eslint": "^8.14.0", 84 | "vite": "^5.4.11", 85 | "vitepress": "^1.5.0", 86 | "vitest": "^2.1.5", 87 | "vue": "^3.5.13", 88 | "vue-chartjs": "^5.3.2" 89 | }, 90 | "scripts": { 91 | "clean": "rimraf --glob build node_modules \"*.tgz\" \"*.tsbuildinfo\"", 92 | "compile": "tsc -b tsconfig.c.json", 93 | "start": "yarn run watch", 94 | "watch": "rollup -c -w", 95 | "build": "rollup -c", 96 | "test": "vitest --passWithNoTests", 97 | "test:watch": "yarn run test --watch", 98 | "test:coverage": "yarn run test --coverage", 99 | "lint": "yarn run eslint && yarn run prettier", 100 | "fix": "yarn run eslint:fix && yarn run prettier:write", 101 | "prettier:write": "prettier \"*\" \"*/**\" --write", 102 | "prettier": "prettier \"*\" \"*/**\" --check", 103 | "eslint": "eslint src --cache", 104 | "eslint:fix": "yarn run eslint --fix", 105 | "prepare": "yarn run build", 106 | "docs:api": "typedoc --options typedoc.json", 107 | "docs:dev": "vitepress dev docs", 108 | "docs:build": "yarn run docs:api && vitepress build docs", 109 | "docs:preview": "vitepress preview docs" 110 | }, 111 | "packageManager": "yarn@4.5.1" 112 | } 113 | -------------------------------------------------------------------------------- /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 = 300, height = 300): 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/animate.ts: -------------------------------------------------------------------------------- 1 | import { color } from 'chart.js/helpers'; 2 | import { styleKeys } from './elements/render'; 3 | import { allModelKeys } from './controllers/utils'; 4 | 5 | const interpolators = { 6 | color(from: string, to: string, factor: number) { 7 | const f = from || 'transparent'; 8 | const t = to || 'transparent'; 9 | if (f === t) { 10 | return to; 11 | } 12 | const c0 = color(f); 13 | const c1 = c0.valid && color(t); 14 | return c1 && c1.valid ? c1.mix(c0, factor).hexString() : to; 15 | }, 16 | number(from: number, to: number, factor: number) { 17 | if (from === to) { 18 | return to; 19 | } 20 | return from + (to - from) * factor; 21 | }, 22 | }; 23 | 24 | function interpolateArrayOption( 25 | from: T | T[] | { v: T[] }, 26 | to: T | T[] | { v: T[] }, 27 | factor: number, 28 | type: 'string' | 'number', 29 | interpolator: (from: T, to: T, factor: number) => T 30 | ): 31 | | T 32 | | T[] 33 | | { 34 | v: T[]; 35 | } { 36 | if (typeof from === type && typeof to === type) { 37 | return interpolator(from as T, to as T, factor); 38 | } 39 | if (Array.isArray(from) && Array.isArray(to)) { 40 | return from.map((f, i) => interpolator(f, to[i], factor)); 41 | } 42 | const isV = (t: T | T[] | { v: T[] }): t is { v: T[] } => t && Array.isArray((t as { v: T[] }).v); 43 | 44 | if (isV(from) && isV(to)) { 45 | return { v: from.v.map((f, i) => interpolator(f, to.v[i], factor)) }; 46 | } 47 | return to; 48 | } 49 | 50 | function interpolateNumberOptionArray( 51 | from: number[], 52 | to: number[], 53 | factor: number 54 | ): number | number[] | { v: number[] } { 55 | return interpolateArrayOption(from, to, factor, 'number', interpolators.number); 56 | } 57 | 58 | function interpolateColorOptionArray( 59 | from: string[], 60 | to: string[], 61 | factor: number 62 | ): string | string[] | { v: string[] } { 63 | return interpolateArrayOption( 64 | from, 65 | to, 66 | factor, 67 | 'string', 68 | interpolators.color as (from: string, to: string, factor: number) => string 69 | ); 70 | } 71 | 72 | export const animationHints = { 73 | animations: { 74 | numberArray: { 75 | fn: interpolateNumberOptionArray, 76 | properties: allModelKeys.concat( 77 | styleKeys.filter((d) => !d.endsWith('Color')), 78 | ['rMin', 'rMax'] 79 | ), 80 | }, 81 | colorArray: { 82 | fn: interpolateColorOptionArray, 83 | properties: styleKeys.filter((d) => d.endsWith('Color')), 84 | }, 85 | }, 86 | }; 87 | 88 | export default animationHints; 89 | -------------------------------------------------------------------------------- /src/controllers/BarWithErrorBarsController.spec.ts: -------------------------------------------------------------------------------- 1 | import { registry, LinearScale, CategoryScale, TimeScale } from 'chart.js'; 2 | import createChart from '../__tests__/createChart'; 3 | import { BarWithErrorBarsController } from './BarWithErrorBarsController'; 4 | import { BarWithErrorBar } from '../elements'; 5 | import 'chartjs-adapter-date-fns'; 6 | import { describe, beforeAll, test } from 'vitest'; 7 | describe('bar', () => { 8 | beforeAll(() => { 9 | registry.addControllers(BarWithErrorBarsController); 10 | registry.addElements(BarWithErrorBar); 11 | registry.addScales(LinearScale, CategoryScale, TimeScale); 12 | }); 13 | test('default', () => { 14 | return createChart({ 15 | type: BarWithErrorBarsController.id, 16 | data: { 17 | labels: ['A', 'B'], 18 | datasets: [ 19 | { 20 | data: [ 21 | { 22 | y: 4, 23 | yMin: 1, 24 | yMax: 6, 25 | }, 26 | { 27 | y: 2, 28 | yMin: 1, 29 | yMax: 4, 30 | }, 31 | ], 32 | }, 33 | ], 34 | }, 35 | options: { 36 | scales: { 37 | x: { 38 | display: false, 39 | }, 40 | y: { 41 | display: false, 42 | }, 43 | }, 44 | }, 45 | }).toMatchImageSnapshot(); 46 | }); 47 | test('time scale', () => { 48 | return createChart({ 49 | type: BarWithErrorBarsController.id, 50 | data: { 51 | labels: ['A', 'B'], 52 | datasets: [ 53 | { 54 | data: [ 55 | { 56 | x: '2017-12-25', 57 | y: 4, 58 | yMin: 1, 59 | yMax: 6, 60 | }, 61 | { 62 | x: '2018-12-25', 63 | y: 2, 64 | yMin: 1, 65 | yMax: 4, 66 | }, 67 | { 68 | x: '2019-12-25', 69 | y: 3, 70 | yMin: 2, 71 | yMax: 5, 72 | }, 73 | ], 74 | }, 75 | ], 76 | }, 77 | options: { 78 | scales: { 79 | x: { 80 | type: 'time', 81 | display: false, 82 | min: '2017-01-01', 83 | max: '2020-01-01', 84 | }, 85 | y: { 86 | display: false, 87 | }, 88 | }, 89 | }, 90 | }).toMatchImageSnapshot(); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /src/controllers/BarWithErrorBarsController.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Chart, 3 | BarController, 4 | LinearScale, 5 | CategoryScale, 6 | Scale, 7 | ChartMeta, 8 | UpdateMode, 9 | ScriptableAndArrayOptions, 10 | ScriptableContext, 11 | ChartConfiguration, 12 | ChartItem, 13 | BarControllerDatasetOptions, 14 | Element, 15 | BarControllerChartOptions, 16 | CartesianScaleTypeRegistry, 17 | } from 'chart.js'; 18 | import { merge } from 'chart.js/helpers'; 19 | import { calculateScale, isNumericScale } from './utils'; 20 | import type { IErrorBarOptions } from '../elements/render'; 21 | import { BarWithErrorBar } from '../elements'; 22 | import { generateBarTooltip } from './tooltip'; 23 | import { animationHints } from '../animate'; 24 | import { 25 | getMinMax, 26 | parseErrorNumberData, 27 | parseErrorLabelData, 28 | IErrorBarXDataPoint, 29 | IErrorBarXYDataPoint, 30 | IErrorBarYDataPoint, 31 | } from './base'; 32 | import patchController from './patchController'; 33 | 34 | export class BarWithErrorBarsController extends BarController { 35 | /** 36 | * @hidden 37 | */ 38 | getMinMax(scale: Scale, canStack: boolean): { min: number; max: number } { 39 | return getMinMax(scale, (patchedScale) => super.getMinMax(patchedScale, canStack)); 40 | } 41 | 42 | /** 43 | * @hidden 44 | */ 45 | protected parsePrimitiveData(meta: ChartMeta, data: any[], start: number, count: number): Record[] { 46 | const parsed = super.parsePrimitiveData(meta, data, start, count); 47 | this.parseErrorData(parsed, meta, data, start, count); 48 | return parsed; 49 | } 50 | 51 | /** 52 | * @hidden 53 | */ 54 | protected parseObjectData(meta: ChartMeta, data: any[], start: number, count: number): Record[] { 55 | const parsed = super.parseObjectData(meta, data, start, count); 56 | this.parseErrorData(parsed, meta, data, start, count); 57 | return parsed; 58 | } 59 | 60 | private parseErrorData( 61 | parsed: Record[], 62 | meta: ChartMeta, 63 | data: any[], 64 | start: number, 65 | count: number 66 | ) { 67 | parseErrorNumberData(parsed, meta.vScale!, data, start, count); 68 | 69 | const iScale = meta.iScale as Scale; 70 | const hasNumberIScale = isNumericScale(iScale); 71 | if (hasNumberIScale) { 72 | parseErrorNumberData(parsed, meta.iScale!, data, start, count); 73 | } else { 74 | parseErrorLabelData(parsed, meta.iScale!, start, count); 75 | } 76 | } 77 | 78 | /** 79 | * @hidden 80 | */ 81 | updateElement( 82 | element: Element, 83 | index: number | undefined, 84 | properties: Record, 85 | mode: UpdateMode 86 | ): void { 87 | // inject the other error bar related properties 88 | if (typeof index === 'number') { 89 | calculateScale( 90 | properties, 91 | this.getParsed(index) as Partial, 92 | index, 93 | this._cachedMeta.vScale as LinearScale, 94 | mode === 'reset' 95 | ); 96 | } 97 | super.updateElement(element, index, properties, mode); 98 | } 99 | 100 | static readonly id = 'barWithErrorBars'; 101 | 102 | /** 103 | * @hidden 104 | */ 105 | static readonly defaults: any = /* #__PURE__ */ merge({}, [ 106 | BarController.defaults, 107 | animationHints, 108 | { 109 | dataElementType: BarWithErrorBar.id, 110 | }, 111 | ]); 112 | 113 | /** 114 | * @hidden 115 | */ 116 | static readonly overrides: any = /* #__PURE__ */ merge({}, [ 117 | (BarController as any).overrides, 118 | { 119 | plugins: { 120 | tooltip: { 121 | callbacks: { 122 | label: generateBarTooltip, 123 | }, 124 | }, 125 | }, 126 | }, 127 | ]); 128 | 129 | /** 130 | * @hidden 131 | */ 132 | static readonly defaultRoutes = BarController.defaultRoutes; 133 | } 134 | 135 | export interface BarWithErrorBarsControllerDatasetOptions 136 | extends BarControllerDatasetOptions, 137 | ScriptableAndArrayOptions> {} 138 | 139 | declare module 'chart.js' { 140 | export interface ChartTypeRegistry { 141 | barWithErrorBars: { 142 | chartOptions: BarControllerChartOptions; 143 | datasetOptions: BarWithErrorBarsControllerDatasetOptions; 144 | defaultDataPoint: IErrorBarXDataPoint | IErrorBarYDataPoint; 145 | scales: keyof CartesianScaleTypeRegistry; 146 | metaExtensions: Record; 147 | parsedDataType: (IErrorBarXDataPoint | IErrorBarYDataPoint) & ChartTypeRegistry['bar']['parsedDataType']; 148 | }; 149 | } 150 | } 151 | 152 | export class BarWithErrorBarsChart extends Chart< 153 | 'barWithErrorBars', 154 | DATA, 155 | LABEL 156 | > { 157 | static id = BarWithErrorBarsController.id; 158 | 159 | constructor(item: ChartItem, config: Omit, 'type'>) { 160 | super( 161 | item, 162 | patchController('barWithErrorBars', config, BarWithErrorBarsController, BarWithErrorBar, [ 163 | LinearScale, 164 | CategoryScale, 165 | ]) 166 | ); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/controllers/LineWithErrorBarsController.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | registry, 3 | LinearScale, 4 | CategoryScale, 5 | LineElement, 6 | TimeScale, 7 | TimeSeriesScale, 8 | LogarithmicScale, 9 | } from 'chart.js'; 10 | import 'chartjs-adapter-date-fns'; 11 | import createChart from '../__tests__/createChart'; 12 | import { LineWithErrorBarsController } from './LineWithErrorBarsController'; 13 | import { PointWithErrorBar } from '../elements'; 14 | import { describe, beforeAll, test } from 'vitest'; 15 | describe('line', () => { 16 | beforeAll(() => { 17 | registry.addControllers(LineWithErrorBarsController); 18 | registry.addElements(PointWithErrorBar, LineElement); 19 | registry.addScales(LinearScale, CategoryScale, TimeScale, TimeSeriesScale, LogarithmicScale); 20 | }); 21 | test('default', () => { 22 | return createChart({ 23 | type: LineWithErrorBarsController.id, 24 | data: { 25 | labels: ['A', 'B'], 26 | datasets: [ 27 | { 28 | data: [ 29 | { 30 | y: 4, 31 | yMin: 1, 32 | yMax: 6, 33 | }, 34 | { 35 | y: 2, 36 | yMin: 1, 37 | yMax: 4, 38 | }, 39 | ], 40 | }, 41 | ], 42 | }, 43 | options: { 44 | scales: { 45 | x: { 46 | display: false, 47 | }, 48 | y: { 49 | display: false, 50 | }, 51 | }, 52 | }, 53 | }).toMatchImageSnapshot(); 54 | }); 55 | 56 | test('linear scale', () => { 57 | return createChart({ 58 | type: LineWithErrorBarsController.id, 59 | data: { 60 | datasets: [ 61 | { 62 | data: [ 63 | { 64 | x: 1, 65 | y: 4, 66 | yMin: 1, 67 | yMax: 6, 68 | }, 69 | { 70 | x: 2, 71 | y: 2, 72 | yMin: 1, 73 | yMax: 4, 74 | }, 75 | ], 76 | }, 77 | ], 78 | }, 79 | options: { 80 | scales: { 81 | x: { 82 | type: 'linear', 83 | display: false, 84 | }, 85 | y: { 86 | display: false, 87 | }, 88 | }, 89 | }, 90 | }).toMatchImageSnapshot(); 91 | }); 92 | 93 | test('linear scale', () => { 94 | return createChart({ 95 | type: LineWithErrorBarsController.id, 96 | data: { 97 | datasets: [ 98 | { 99 | data: [ 100 | { 101 | x: 1, 102 | y: 4, 103 | yMin: 1, 104 | yMax: 6, 105 | }, 106 | { 107 | x: 2, 108 | y: 2, 109 | yMin: 1, 110 | yMax: 4, 111 | }, 112 | ], 113 | }, 114 | ], 115 | }, 116 | options: { 117 | scales: { 118 | x: { 119 | type: 'logarithmic', 120 | display: false, 121 | }, 122 | y: { 123 | display: false, 124 | }, 125 | }, 126 | }, 127 | }).toMatchImageSnapshot(); 128 | }); 129 | 130 | test('time scale', () => { 131 | return createChart({ 132 | type: LineWithErrorBarsController.id, 133 | data: { 134 | datasets: [ 135 | { 136 | data: [ 137 | { 138 | x: '2016-12-25', 139 | y: 4, 140 | yMin: 1, 141 | yMax: 6, 142 | }, 143 | { 144 | x: '2017-12-25', 145 | y: 6, 146 | yMin: 2, 147 | yMax: 8, 148 | }, 149 | { 150 | x: '2018-12-25', 151 | y: 2, 152 | yMin: 1, 153 | yMax: 4, 154 | }, 155 | ], 156 | }, 157 | ], 158 | }, 159 | options: { 160 | scales: { 161 | x: { 162 | type: 'time', 163 | display: false, 164 | }, 165 | y: { 166 | display: false, 167 | }, 168 | }, 169 | }, 170 | }).toMatchImageSnapshot(); 171 | }); 172 | 173 | test('time scale', () => { 174 | return createChart({ 175 | type: LineWithErrorBarsController.id, 176 | data: { 177 | datasets: [ 178 | { 179 | data: [ 180 | { 181 | x: '2016-12-25', 182 | y: 4, 183 | yMin: 1, 184 | yMax: 6, 185 | }, 186 | { 187 | x: '2017-12-25', 188 | y: 6, 189 | yMin: 2, 190 | yMax: 8, 191 | }, 192 | { 193 | x: '2018-12-25', 194 | y: 2, 195 | yMin: 1, 196 | yMax: 4, 197 | }, 198 | ], 199 | }, 200 | ], 201 | }, 202 | options: { 203 | scales: { 204 | x: { 205 | type: 'timeseries', 206 | display: false, 207 | }, 208 | y: { 209 | display: false, 210 | }, 211 | }, 212 | }, 213 | }).toMatchImageSnapshot(); 214 | }); 215 | }); 216 | -------------------------------------------------------------------------------- /src/controllers/LineWithErrorBarsController.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Chart, 3 | LineController, 4 | LinearScale, 5 | CategoryScale, 6 | ChartItem, 7 | Scale, 8 | ChartConfiguration, 9 | ChartMeta, 10 | LineControllerDatasetOptions, 11 | ScriptableAndArrayOptions, 12 | UpdateMode, 13 | ScriptableContext, 14 | Element, 15 | LineControllerChartOptions, 16 | CartesianScaleTypeRegistry, 17 | } from 'chart.js'; 18 | import { merge } from 'chart.js/helpers'; 19 | import { calculateScale, isNumericScale } from './utils'; 20 | import type { IErrorBarOptions } from '../elements/render'; 21 | import { PointWithErrorBar } from '../elements'; 22 | import { generateBarTooltip } from './tooltip'; 23 | import { animationHints } from '../animate'; 24 | import { 25 | getMinMax, 26 | parseErrorNumberData, 27 | parseErrorLabelData, 28 | IErrorBarXDataPoint, 29 | IErrorBarXYDataPoint, 30 | IErrorBarYDataPoint, 31 | } from './base'; 32 | import patchController from './patchController'; 33 | 34 | export class LineWithErrorBarsController extends LineController { 35 | /** 36 | * @hidden 37 | */ 38 | getMinMax(scale: Scale, canStack: boolean): { min: number; max: number } { 39 | return getMinMax(scale, (patchedScale) => super.getMinMax(patchedScale, canStack)); 40 | } 41 | 42 | /** 43 | * @hidden 44 | */ 45 | protected parsePrimitiveData(meta: ChartMeta, data: any[], start: number, count: number): Record[] { 46 | const parsed = super.parsePrimitiveData(meta, data, start, count); 47 | this.parseErrorData(parsed, meta, data, start, count); 48 | return parsed; 49 | } 50 | 51 | /** 52 | * @hidden 53 | */ 54 | protected parseObjectData(meta: ChartMeta, data: any[], start: number, count: number): Record[] { 55 | const parsed = super.parseObjectData(meta, data, start, count); 56 | this.parseErrorData(parsed, meta, data, start, count); 57 | return parsed; 58 | } 59 | 60 | /** 61 | * @hidden 62 | */ 63 | private parseErrorData( 64 | parsed: Record[], 65 | meta: ChartMeta, 66 | data: any[], 67 | start: number, 68 | count: number 69 | ) { 70 | parseErrorNumberData(parsed, meta.vScale!, data, start, count); 71 | 72 | const iScale = meta.iScale as Scale; 73 | const hasNumberIScale = isNumericScale(iScale); 74 | if (hasNumberIScale) { 75 | parseErrorNumberData(parsed, meta.iScale!, data, start, count); 76 | } else { 77 | parseErrorLabelData(parsed, meta.iScale!, start, count); 78 | } 79 | } 80 | 81 | /** 82 | * @hidden 83 | */ 84 | updateElement( 85 | element: Element, 86 | index: number | undefined, 87 | properties: Record, 88 | mode: UpdateMode 89 | ): void { 90 | // inject the other error bar related properties 91 | if (element instanceof PointWithErrorBar && typeof index === 'number') { 92 | this.updateElementScale(index, properties, mode); 93 | } 94 | super.updateElement(element, index, properties, mode); 95 | } 96 | 97 | /** 98 | * @hidden 99 | */ 100 | protected updateElementScale(index: number, properties: Record, mode: UpdateMode): void { 101 | // inject the other error bar related properties 102 | calculateScale( 103 | properties, 104 | this.getParsed(index) as Partial, 105 | index, 106 | this._cachedMeta.vScale as LinearScale, 107 | mode === 'reset' 108 | ); 109 | 110 | const iScale = this._cachedMeta.iScale as Scale; 111 | const hasNumberIScale = isNumericScale(iScale); 112 | if (hasNumberIScale) { 113 | calculateScale( 114 | properties, 115 | this.getParsed(index) as Partial, 116 | index, 117 | this._cachedMeta.iScale as LinearScale, 118 | mode === 'reset' 119 | ); 120 | } 121 | } 122 | 123 | /** 124 | * @hidden 125 | */ 126 | updateElements(points: Element[], start: number, count: number, mode: UpdateMode) { 127 | const reset = mode === 'reset'; 128 | const c = this.chart as unknown as { _animationsDisabled: boolean }; 129 | const directUpdate = c._animationsDisabled || reset || mode === 'none'; 130 | // directUpdate not supported hack it 131 | super.updateElements(points, start, count, mode); 132 | 133 | if (!directUpdate) { 134 | return; 135 | } 136 | // manually update since with optimizations updateElement is not called 137 | for (let i = start; i < start + count; ++i) { 138 | const point = points[i]; 139 | // inject the other error bar related properties 140 | if (point instanceof PointWithErrorBar) { 141 | this.updateElementScale(i, point as unknown as Record, mode); 142 | } 143 | } 144 | } 145 | 146 | static readonly id = 'lineWithErrorBars'; 147 | 148 | /** 149 | * @hidden 150 | */ 151 | static readonly defaults: any = /* #__PURE__ */ merge({}, [ 152 | LineController.defaults, 153 | animationHints, 154 | { 155 | dataElementType: PointWithErrorBar.id, 156 | }, 157 | ]); 158 | 159 | /** 160 | * @hidden 161 | */ 162 | static readonly overrides: any = /* #__PURE__ */ merge({}, [ 163 | (LineController as any).overrides, 164 | { 165 | plugins: { 166 | tooltip: { 167 | callbacks: { 168 | label: generateBarTooltip, 169 | }, 170 | }, 171 | }, 172 | }, 173 | ]); 174 | 175 | /** 176 | * @hidden 177 | */ 178 | static readonly defaultRoutes = LineController.defaultRoutes; 179 | } 180 | 181 | export interface LineWithErrorBarsControllerDatasetOptions 182 | extends LineControllerDatasetOptions, 183 | ScriptableAndArrayOptions> {} 184 | 185 | declare module 'chart.js' { 186 | export interface ChartTypeRegistry { 187 | lineWithErrorBars: { 188 | chartOptions: LineControllerChartOptions; 189 | datasetOptions: LineWithErrorBarsControllerDatasetOptions; 190 | defaultDataPoint: IErrorBarXDataPoint | IErrorBarYDataPoint; 191 | scales: keyof CartesianScaleTypeRegistry; 192 | metaExtensions: Record; 193 | parsedDataType: (IErrorBarXDataPoint | IErrorBarYDataPoint) & ChartTypeRegistry['line']['parsedDataType']; 194 | }; 195 | } 196 | } 197 | 198 | export class LineWithErrorBarsChart extends Chart< 199 | 'lineWithErrorBars', 200 | DATA, 201 | LABEL 202 | > { 203 | static id = LineWithErrorBarsController.id; 204 | 205 | constructor(item: ChartItem, config: Omit, 'type'>) { 206 | super( 207 | item, 208 | patchController('lineWithErrorBars', config, LineWithErrorBarsController, PointWithErrorBar, [ 209 | LinearScale, 210 | CategoryScale, 211 | ]) 212 | ); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/controllers/PolarAreaWithErrorBarsController.spec.ts: -------------------------------------------------------------------------------- 1 | import { registry, RadialLinearScale } from 'chart.js'; 2 | import createChart from '../__tests__/createChart'; 3 | import { PolarAreaWithErrorBarsController } from './PolarAreaWithErrorBarsController'; 4 | import { ArcWithErrorBar } from '../elements'; 5 | import { describe, beforeAll, test } from 'vitest'; 6 | describe('bar', () => { 7 | beforeAll(() => { 8 | registry.addControllers(PolarAreaWithErrorBarsController); 9 | registry.addElements(ArcWithErrorBar); 10 | registry.addScales(RadialLinearScale); 11 | }); 12 | test('default', () => { 13 | return createChart({ 14 | type: PolarAreaWithErrorBarsController.id, 15 | data: { 16 | labels: ['A', 'B'], 17 | datasets: [ 18 | { 19 | data: [ 20 | { 21 | r: 4, 22 | rMin: 1, 23 | rMax: 6, 24 | }, 25 | { 26 | r: 2, 27 | rMin: 1, 28 | rMax: 4, 29 | }, 30 | ], 31 | }, 32 | ], 33 | }, 34 | options: { 35 | scales: { 36 | r: { 37 | display: false, 38 | }, 39 | }, 40 | }, 41 | }).toMatchImageSnapshot(); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/controllers/PolarAreaWithErrorBarsController.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Chart, 3 | PolarAreaController, 4 | RadialLinearScale, 5 | Scale, 6 | UpdateMode, 7 | Element, 8 | ChartMeta, 9 | ChartItem, 10 | ChartConfiguration, 11 | PolarAreaControllerDatasetOptions, 12 | ScriptableAndArrayOptions, 13 | ScriptableContext, 14 | PolarAreaControllerChartOptions, 15 | CartesianScaleTypeRegistry, 16 | } from 'chart.js'; 17 | import { merge } from 'chart.js/helpers'; 18 | import { calculatePolarScale } from './utils'; 19 | import { getMinMax, IErrorBarRDataPoint, parseErrorNumberData } from './base'; 20 | import { generateTooltipPolar } from './tooltip'; 21 | import { animationHints } from '../animate'; 22 | import { ArcWithErrorBar } from '../elements'; 23 | import type { IErrorBarOptions } from '../elements/render'; 24 | import patchController from './patchController'; 25 | 26 | export class PolarAreaWithErrorBarsController extends PolarAreaController { 27 | /** 28 | * @hidden 29 | */ 30 | getMinMaxImpl(scale: Scale) { 31 | // new version doesn't use scale.axis wrongly 32 | const t = this._cachedMeta; 33 | const e = { 34 | min: Number.POSITIVE_INFINITY, 35 | max: Number.NEGATIVE_INFINITY, 36 | }; 37 | t.data.forEach((_, i) => { 38 | const s = (this.getParsed(i) as any)[scale.axis] as number; 39 | if (Number.isNaN(s) || !this.chart.getDataVisibility(i)) { 40 | return; 41 | } 42 | if (s < e.min) { 43 | e.min = s; 44 | } 45 | if (s > e.max) { 46 | e.max = s; 47 | } 48 | }); 49 | return e; 50 | } 51 | 52 | /** 53 | * @hidden 54 | */ 55 | getMinMax(scale: Scale): { min: number; max: number } { 56 | return getMinMax(scale, (patchedScale) => this.getMinMaxImpl(patchedScale)); 57 | } 58 | 59 | /** 60 | * @hidden 61 | */ 62 | countVisibleElements(): number { 63 | const meta = this._cachedMeta; 64 | return meta.data.reduce((acc, _, index) => { 65 | // use different data lookup 66 | if (!Number.isNaN((meta._parsed[index] as { r: number }).r) && this.chart.getDataVisibility(index)) { 67 | return acc + 1; 68 | } 69 | return acc; 70 | }, 0); 71 | } 72 | 73 | // _computeAngle(index: number, mode, defaultAngle: number): number { 74 | // return this.chart.getDataVisibility(index) 75 | // ? toRadians(this.resolveDataElementOptions(index, mode).angle || defaultAngle) 76 | // : 0; 77 | // } 78 | 79 | // _computeAngle(index: number): number { 80 | // const meta = this._cachedMeta; 81 | // const count = (meta as any).count as number; 82 | // // use different data lookup 83 | // if (Number.isNaN((meta._parsed[index] as { r: number }).r) || !this.chart.getDataVisibility(index)) { 84 | // return 0; 85 | // } 86 | // const context = (this as any).getContext(index, true); 87 | // return resolve([(this.chart.options as any).elements.arc.angle, (2 * Math.PI) / count], context, index); 88 | // } 89 | 90 | protected parsePrimitiveData(meta: ChartMeta, data: any[], start: number, count: number): Record[] { 91 | const parsed = super.parsePrimitiveData(meta, data, start, count); 92 | this.parseErrorData(parsed, meta, data, start, count); 93 | return parsed; 94 | } 95 | 96 | protected parseObjectData(meta: ChartMeta, data: any[], start: number, count: number): Record[] { 97 | const parsed = super.parseObjectData(meta, data, start, count); 98 | this.parseErrorData(parsed, meta, data, start, count); 99 | return parsed; 100 | } 101 | 102 | private parseErrorData( 103 | parsed: Record[], 104 | meta: ChartMeta, 105 | data: any[], 106 | start: number, 107 | count: number 108 | ) { 109 | const scale = meta.rScale!; 110 | for (let i = 0; i < count; i += 1) { 111 | const index = i + start; 112 | const item = data[index]; 113 | const v = scale.parse(item[scale.axis], index); 114 | parsed[i] = { 115 | [scale.axis]: v, 116 | }; 117 | } 118 | parseErrorNumberData(parsed, scale, data, start, count); 119 | } 120 | 121 | /** 122 | * @hidden 123 | */ 124 | updateElement( 125 | element: Element, 126 | index: number | undefined, 127 | properties: Record, 128 | mode: UpdateMode 129 | ): void { 130 | if (typeof index === 'number') { 131 | calculatePolarScale( 132 | properties, 133 | this.getParsed(index) as IErrorBarRDataPoint, 134 | this._cachedMeta.rScale as RadialLinearScale, 135 | mode === 'reset', 136 | this.chart.options 137 | ); 138 | } 139 | super.updateElement(element, index, properties, mode); 140 | } 141 | 142 | /** 143 | * @hidden 144 | */ 145 | updateElements(arcs: Element[], start: number, count: number, mode: UpdateMode): void { 146 | const scale = this.chart.scales.r as RadialLinearScale; 147 | const bak = scale.getDistanceFromCenterForValue; 148 | scale.getDistanceFromCenterForValue = function getDistanceFromCenterForValue(v) { 149 | if (typeof v === 'number') { 150 | return bak.call(this, v); 151 | } 152 | return bak.call(this, (v as any).r); 153 | }; 154 | super.updateElements(arcs, start, count, mode); 155 | scale.getDistanceFromCenterForValue = bak; 156 | } 157 | 158 | static readonly id = 'polarAreaWithErrorBars'; 159 | 160 | /** 161 | * @hidden 162 | */ 163 | static readonly defaults: any = /* #__PURE__ */ merge({}, [ 164 | PolarAreaController.defaults, 165 | animationHints, 166 | { 167 | dataElementType: ArcWithErrorBar.id, 168 | }, 169 | ]); 170 | 171 | /** 172 | * @hidden 173 | */ 174 | static readonly overrides: any = /* #__PURE__ */ merge({}, [ 175 | (PolarAreaController as any).overrides, 176 | { 177 | plugins: { 178 | tooltip: { 179 | callbacks: { 180 | label: generateTooltipPolar, 181 | }, 182 | }, 183 | }, 184 | }, 185 | ]); 186 | 187 | /** 188 | * @hidden 189 | */ 190 | static readonly defaultRoutes = PolarAreaController.defaultRoutes; 191 | } 192 | 193 | export interface PolarAreaWithErrorBarsControllerDatasetOptions 194 | extends PolarAreaControllerDatasetOptions, 195 | ScriptableAndArrayOptions> {} 196 | 197 | declare module 'chart.js' { 198 | export interface ChartTypeRegistry { 199 | polarAreaWithErrorBars: { 200 | chartOptions: PolarAreaControllerChartOptions; 201 | datasetOptions: PolarAreaWithErrorBarsControllerDatasetOptions; 202 | defaultDataPoint: IErrorBarRDataPoint; 203 | scales: keyof CartesianScaleTypeRegistry; 204 | metaExtensions: Record; 205 | parsedDataType: IErrorBarRDataPoint & ChartTypeRegistry['polarArea']['parsedDataType']; 206 | }; 207 | } 208 | } 209 | 210 | export class PolarAreaWithErrorBarsChart extends Chart< 211 | 'polarAreaWithErrorBars', 212 | DATA, 213 | LABEL 214 | > { 215 | static id = PolarAreaWithErrorBarsController.id; 216 | 217 | constructor(item: ChartItem, config: Omit, 'type'>) { 218 | super( 219 | item, 220 | patchController('polarAreaWithErrorBars', config, PolarAreaWithErrorBarsController, ArcWithErrorBar, [ 221 | RadialLinearScale, 222 | ]) 223 | ); 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /src/controllers/ScatterWithErrorBarsController.spec.ts: -------------------------------------------------------------------------------- 1 | import { registry, LinearScale } from 'chart.js'; 2 | import createChart from '../__tests__/createChart'; 3 | import { ScatterWithErrorBarsController } from './ScatterWithErrorBarsController'; 4 | import { PointWithErrorBar } from '../elements'; 5 | import { describe, beforeAll, test } from 'vitest'; 6 | describe('bar', () => { 7 | beforeAll(() => { 8 | registry.addControllers(ScatterWithErrorBarsController); 9 | registry.addElements(PointWithErrorBar); 10 | registry.addScales(LinearScale); 11 | }); 12 | test('default', () => { 13 | return createChart({ 14 | type: ScatterWithErrorBarsController.id, 15 | data: { 16 | labels: ['A', 'B'], 17 | datasets: [ 18 | { 19 | data: [ 20 | { 21 | x: 2, 22 | xMin: 1, 23 | xMax: 3, 24 | y: 4, 25 | yMin: 1, 26 | yMax: 6, 27 | }, 28 | { 29 | x: 7, 30 | xMin: 6, 31 | xMax: 9, 32 | y: 2, 33 | yMin: 1, 34 | yMax: 4, 35 | }, 36 | ], 37 | }, 38 | ], 39 | }, 40 | options: { 41 | scales: { 42 | x: { 43 | display: false, 44 | }, 45 | y: { 46 | display: false, 47 | }, 48 | }, 49 | }, 50 | }).toMatchImageSnapshot(); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/controllers/ScatterWithErrorBarsController.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Chart, 3 | ScatterController, 4 | LinearScale, 5 | ChartItem, 6 | ChartConfiguration, 7 | ChartMeta, 8 | ScatterControllerDatasetOptions, 9 | Scale, 10 | ScriptableAndArrayOptions, 11 | UpdateMode, 12 | LineController, 13 | ScriptableContext, 14 | Element, 15 | ScatterControllerChartOptions, 16 | CartesianScaleTypeRegistry, 17 | } from 'chart.js'; 18 | import { merge } from 'chart.js/helpers'; 19 | import { calculateScale } from './utils'; 20 | import { getMinMax, IErrorBarXYDataPoint, parseErrorNumberData } from './base'; 21 | import { generateTooltipScatter } from './tooltip'; 22 | import { animationHints } from '../animate'; 23 | import { PointWithErrorBar } from '../elements'; 24 | import type { IErrorBarOptions } from '../elements/render'; 25 | import patchController from './patchController'; 26 | 27 | export class ScatterWithErrorBarsController extends ScatterController { 28 | /** 29 | * @hidden 30 | */ 31 | getMinMax(scale: Scale, canStack: boolean): { min: number; max: number } { 32 | return getMinMax(scale, (patchedScale) => super.getMinMax(patchedScale, canStack)); 33 | } 34 | 35 | protected parsePrimitiveData(meta: ChartMeta, data: any[], start: number, count: number): Record[] { 36 | const parsed = super.parsePrimitiveData(meta, data, start, count); 37 | this.parseErrorData(parsed, meta, data, start, count); 38 | return parsed; 39 | } 40 | 41 | protected parseObjectData(meta: ChartMeta, data: any[], start: number, count: number): Record[] { 42 | const parsed = super.parseObjectData(meta, data, start, count); 43 | this.parseErrorData(parsed, meta, data, start, count); 44 | return parsed; 45 | } 46 | 47 | private parseErrorData( 48 | parsed: Record[], 49 | meta: ChartMeta, 50 | data: any[], 51 | start: number, 52 | count: number 53 | ) { 54 | parseErrorNumberData(parsed, meta.xScale!, data, start, count); 55 | 56 | parseErrorNumberData(parsed, meta.yScale!, data, start, count); 57 | } 58 | 59 | /** 60 | * @hidden 61 | */ 62 | updateElement( 63 | element: Element, 64 | index: number | undefined, 65 | properties: Record, 66 | mode: UpdateMode 67 | ): void { 68 | // inject the other error bar related properties 69 | if (element instanceof PointWithErrorBar && typeof index === 'number') { 70 | this.updateElementScale(index, properties, mode); 71 | } 72 | super.updateElement(element, index, properties, mode); 73 | } 74 | 75 | protected updateElementScale(index: number, properties: Record, mode: UpdateMode): void { 76 | // inject the other error bar related properties 77 | calculateScale( 78 | properties, 79 | this.getParsed(index) as Partial, 80 | index, 81 | this._cachedMeta.xScale as LinearScale, 82 | mode === 'reset' 83 | ); 84 | calculateScale( 85 | properties, 86 | this.getParsed(index) as Partial, 87 | index, 88 | this._cachedMeta.yScale as LinearScale, 89 | mode === 'reset' 90 | ); 91 | } 92 | 93 | /** 94 | * @hidden 95 | */ 96 | updateElements(points: Element[], start: number, count: number, mode: UpdateMode) { 97 | const reset = mode === 'reset'; 98 | const c = this.chart as unknown as { _animationsDisabled: boolean }; 99 | const directUpdate = c._animationsDisabled || reset || mode === 'none'; 100 | // directUpdate not supported hack it 101 | super.updateElements(points, start, count, mode); 102 | 103 | if (!directUpdate) { 104 | return; 105 | } 106 | // manually update since with optimizations updateElement is not called 107 | for (let i = start; i < start + count; ++i) { 108 | const point = points[i]; 109 | // inject the other error bar related properties 110 | if (point instanceof PointWithErrorBar) { 111 | this.updateElementScale(i, point as unknown as Record, mode); 112 | } 113 | } 114 | } 115 | 116 | static readonly id = 'scatterWithErrorBars'; 117 | 118 | /** 119 | * @hidden 120 | */ 121 | static readonly defaults: any = /* #__PURE__ */ merge({}, [ 122 | ScatterController.defaults, 123 | animationHints, 124 | { 125 | dataElementType: PointWithErrorBar.id, 126 | }, 127 | ]); 128 | 129 | /** 130 | * @hidden 131 | */ 132 | static readonly overrides: any = /* #__PURE__ */ merge({}, [ 133 | (ScatterController as any).overrides, 134 | { 135 | plugins: { 136 | tooltip: { 137 | callbacks: { 138 | label: generateTooltipScatter, 139 | }, 140 | }, 141 | }, 142 | }, 143 | ]); 144 | 145 | /** 146 | * @hidden 147 | */ 148 | static readonly defaultRoutes = LineController.defaultRoutes; 149 | } 150 | 151 | export interface ScatterWithErrorBarsControllerDatasetOptions 152 | extends ScatterControllerDatasetOptions, 153 | ScriptableAndArrayOptions> {} 154 | 155 | declare module 'chart.js' { 156 | export interface ChartTypeRegistry { 157 | scatterWithErrorBars: { 158 | chartOptions: ScatterControllerChartOptions; 159 | datasetOptions: ScatterWithErrorBarsControllerDatasetOptions; 160 | defaultDataPoint: IErrorBarXYDataPoint; 161 | scales: keyof CartesianScaleTypeRegistry; 162 | metaExtensions: Record; 163 | parsedDataType: IErrorBarXYDataPoint & ChartTypeRegistry['scatter']['parsedDataType']; 164 | }; 165 | } 166 | } 167 | 168 | export class ScatterWithErrorBarsChart extends Chart< 169 | 'scatterWithErrorBars', 170 | DATA, 171 | LABEL 172 | > { 173 | static id = ScatterWithErrorBarsController.id; 174 | 175 | constructor(item: ChartItem, config: Omit, 'type'>) { 176 | super( 177 | item, 178 | patchController('scatterWithErrorBars', config, ScatterWithErrorBarsController, PointWithErrorBar, [LinearScale]) 179 | ); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/controllers/__image_snapshots__/bar-with-error-bars-controller-spec-ts-bar-default-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgratzl/chartjs-chart-error-bars/3adcdfe9aa1f96afd527195d0716612a8f382e1c/src/controllers/__image_snapshots__/bar-with-error-bars-controller-spec-ts-bar-default-1-snap.png -------------------------------------------------------------------------------- /src/controllers/__image_snapshots__/bar-with-error-bars-controller-spec-ts-bar-time-scale-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgratzl/chartjs-chart-error-bars/3adcdfe9aa1f96afd527195d0716612a8f382e1c/src/controllers/__image_snapshots__/bar-with-error-bars-controller-spec-ts-bar-time-scale-1-snap.png -------------------------------------------------------------------------------- /src/controllers/__image_snapshots__/line-with-error-bars-controller-spec-ts-line-default-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgratzl/chartjs-chart-error-bars/3adcdfe9aa1f96afd527195d0716612a8f382e1c/src/controllers/__image_snapshots__/line-with-error-bars-controller-spec-ts-line-default-1-snap.png -------------------------------------------------------------------------------- /src/controllers/__image_snapshots__/line-with-error-bars-controller-spec-ts-line-linear-scale-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgratzl/chartjs-chart-error-bars/3adcdfe9aa1f96afd527195d0716612a8f382e1c/src/controllers/__image_snapshots__/line-with-error-bars-controller-spec-ts-line-linear-scale-1-snap.png -------------------------------------------------------------------------------- /src/controllers/__image_snapshots__/line-with-error-bars-controller-spec-ts-line-linear-scale-2-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgratzl/chartjs-chart-error-bars/3adcdfe9aa1f96afd527195d0716612a8f382e1c/src/controllers/__image_snapshots__/line-with-error-bars-controller-spec-ts-line-linear-scale-2-snap.png -------------------------------------------------------------------------------- /src/controllers/__image_snapshots__/line-with-error-bars-controller-spec-ts-line-time-scale-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgratzl/chartjs-chart-error-bars/3adcdfe9aa1f96afd527195d0716612a8f382e1c/src/controllers/__image_snapshots__/line-with-error-bars-controller-spec-ts-line-time-scale-1-snap.png -------------------------------------------------------------------------------- /src/controllers/__image_snapshots__/line-with-error-bars-controller-spec-ts-line-time-scale-2-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgratzl/chartjs-chart-error-bars/3adcdfe9aa1f96afd527195d0716612a8f382e1c/src/controllers/__image_snapshots__/line-with-error-bars-controller-spec-ts-line-time-scale-2-snap.png -------------------------------------------------------------------------------- /src/controllers/__image_snapshots__/polar-area-with-error-bars-controller-spec-ts-bar-default-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgratzl/chartjs-chart-error-bars/3adcdfe9aa1f96afd527195d0716612a8f382e1c/src/controllers/__image_snapshots__/polar-area-with-error-bars-controller-spec-ts-bar-default-1-snap.png -------------------------------------------------------------------------------- /src/controllers/__image_snapshots__/scatter-with-error-bars-controller-spec-ts-bar-default-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgratzl/chartjs-chart-error-bars/3adcdfe9aa1f96afd527195d0716612a8f382e1c/src/controllers/__image_snapshots__/scatter-with-error-bars-controller-spec-ts-bar-default-1-snap.png -------------------------------------------------------------------------------- /src/controllers/base.ts: -------------------------------------------------------------------------------- 1 | import type { Scale } from 'chart.js'; 2 | 3 | export interface IErrorBarXDataPoint { 4 | /** 5 | * the actual value 6 | */ 7 | x: number; 8 | /** 9 | * the minimal absolute error bar value 10 | */ 11 | xMin: number | number[]; 12 | /** 13 | * the maximal absolute error bar value 14 | */ 15 | xMax: number | number[]; 16 | } 17 | 18 | export interface IErrorBarYDataPoint { 19 | /** 20 | * the actual value 21 | */ 22 | y: number; 23 | /** 24 | * the minimal absolute error bar value 25 | */ 26 | yMin: number | number[]; 27 | /** 28 | * the maximal absolute error bar value 29 | */ 30 | yMax: number | number[]; 31 | } 32 | 33 | export interface IErrorBarRDataPoint { 34 | /** 35 | * the actual value 36 | */ 37 | r: number; 38 | /** 39 | * the minimal absolute error bar value 40 | */ 41 | rMin: number | number[]; 42 | /** 43 | * the maximal absolute error bar value 44 | */ 45 | rMax: number | number[]; 46 | } 47 | 48 | export interface IErrorBarXYDataPoint extends IErrorBarXDataPoint, IErrorBarYDataPoint {} 49 | 50 | export function getMinMax( 51 | scale: Scale, 52 | superMethod: (scale: Scale) => { min: number; max: number } 53 | ): { min: number; max: number } { 54 | const { axis } = scale; 55 | 56 | scale.axis = `${axis}MinMin`; 57 | const { min } = superMethod(scale); 58 | 59 | scale.axis = `${axis}MaxMax`; 60 | const { max } = superMethod(scale); 61 | 62 | scale.axis = axis; 63 | return { min, max }; 64 | } 65 | 66 | function computeExtrema(v: number, vm: number | number[], op: (...args: number[]) => number) { 67 | if (Array.isArray(vm)) { 68 | return op(v, ...vm); 69 | } 70 | if (typeof vm === 'number') { 71 | return op(v, vm); 72 | } 73 | return v; 74 | } 75 | 76 | export function parseErrorNumberData(parsed: any[], scale: Scale, data: any[], start: number, count: number): void { 77 | const axis = typeof scale === 'string' ? scale : scale.axis; 78 | const vMin = `${axis}Min`; 79 | const vMax = `${axis}Max`; 80 | const vMinMin = `${axis}MinMin`; 81 | const vMaxMax = `${axis}MaxMax`; 82 | for (let i = 0; i < count; i += 1) { 83 | const index = i + start; 84 | const p = parsed[i]; 85 | p[vMin] = typeof data[index] === 'object' ? data[index][vMin] : null; 86 | p[vMax] = typeof data[index] === 'object' ? data[index][vMax] : null; 87 | p[vMinMin] = computeExtrema(p[axis], p[vMin], Math.min); 88 | p[vMaxMax] = computeExtrema(p[axis], p[vMax], Math.max); 89 | } 90 | } 91 | 92 | export function parseErrorLabelData(parsed: any[], scale: Scale, start: number, count: number): void { 93 | const { axis } = scale; 94 | const labels = scale.getLabels(); 95 | for (let i = 0; i < count; i += 1) { 96 | const index = i + start; 97 | const p = parsed[i]; 98 | p[axis] = scale.parse(labels[index], index); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/controllers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './BarWithErrorBarsController'; 2 | export * from './LineWithErrorBarsController'; 3 | export * from './ScatterWithErrorBarsController'; 4 | export * from './PolarAreaWithErrorBarsController'; 5 | export type { IErrorBarRDataPoint, IErrorBarXDataPoint, IErrorBarXYDataPoint, IErrorBarYDataPoint } from './base'; 6 | -------------------------------------------------------------------------------- /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/controllers/tooltip.ts: -------------------------------------------------------------------------------- 1 | import { Tooltip, PolarAreaController, TooltipItem, TooltipModel } from 'chart.js'; 2 | import { modelKeys } from './utils'; 3 | import type { IErrorBarRDataPoint, IErrorBarXYDataPoint } from './base'; 4 | 5 | function reverseOrder(v: T | T[]) { 6 | return Array.isArray(v) ? v.slice().reverse() : v; 7 | } 8 | 9 | export function generateBarTooltip(this: TooltipModel<'bar'>, item: TooltipItem<'bar'>): string { 10 | const keys = modelKeys((item.element as any).horizontal); 11 | const base = (Tooltip as any).defaults.callbacks.label.call(this, item); 12 | const v = item.chart.data.datasets[item.datasetIndex].data[item.dataIndex] as unknown as IErrorBarXYDataPoint; 13 | if (v == null || keys.every((k) => v[k] == null)) { 14 | return base; 15 | } 16 | return `${base} (${reverseOrder(v[keys[0]])} .. ${v[keys[1]]})`; 17 | } 18 | 19 | export function generateTooltipScatter(item: TooltipItem<'scatter'>): string { 20 | const v = item.chart.data.datasets[item.datasetIndex].data[item.dataIndex] as unknown as IErrorBarXYDataPoint; 21 | 22 | const subLabel = (base: string | number | boolean, horizontal: boolean) => { 23 | const keys = modelKeys(horizontal); 24 | if (v == null || keys.every((k) => v[k] == null)) { 25 | return base; 26 | } 27 | return `${base} [${reverseOrder(v[keys[0]])} .. ${v[keys[1]]}]`; 28 | }; 29 | 30 | return `(${subLabel(item.label, true)}, ${subLabel(item.parsed.y, false)})`; 31 | } 32 | 33 | export function generateTooltipPolar(this: TooltipModel<'polarArea'>, item: TooltipItem<'polarArea'>): string { 34 | const base = (PolarAreaController as any).overrides.plugins.tooltip.callbacks.label.call(this, item); 35 | const v = item.chart.data.datasets[item.datasetIndex].data[item.dataIndex] as unknown as IErrorBarRDataPoint; 36 | 37 | const keys = ['rMin', 'rMax'] as const; 38 | if (v == null || keys.every((k) => v[k] == null)) { 39 | return base; 40 | } 41 | return `${base} [${reverseOrder(v[keys[0]])} .. ${v[keys[1]]}]`; 42 | } 43 | -------------------------------------------------------------------------------- /src/controllers/utils.ts: -------------------------------------------------------------------------------- 1 | import type { LinearScale, RadialLinearScale, Scale } from 'chart.js'; 2 | import type { IErrorBarRDataPoint, IErrorBarXYDataPoint } from './base'; 3 | 4 | export const allModelKeys = ['xMin', 'xMax', 'yMin', 'yMax']; 5 | 6 | export function modelKeys(horizontal: boolean): (keyof IErrorBarXYDataPoint)[] { 7 | return (horizontal ? allModelKeys.slice(0, 2) : allModelKeys.slice(2)) as any; 8 | } 9 | 10 | export function calculateScale( 11 | properties: any, 12 | data: Partial, 13 | index: number, 14 | scale: LinearScale, 15 | reset?: boolean 16 | ): void { 17 | const keys = [`${scale.axis}Min`, `${scale.axis}Max`] as const; 18 | const base = scale.getBasePixel(); 19 | 20 | for (const key of keys) { 21 | const v = data[key as keyof IErrorBarXYDataPoint]; 22 | if (Array.isArray(v)) { 23 | properties[key] = v.map((d) => (reset ? base : scale.getPixelForValue(d, index))); 24 | } else if (typeof v === 'number') { 25 | properties[key] = reset ? base : scale.getPixelForValue(v, index); 26 | } else { 27 | properties[key] = null; // reset 28 | } 29 | } 30 | } 31 | 32 | export function calculatePolarScale( 33 | properties: any, 34 | data: IErrorBarRDataPoint, 35 | scale: RadialLinearScale, 36 | reset: boolean, 37 | 38 | options: any 39 | ): void { 40 | const animationOpts = options.animation; 41 | const keys = [`${scale.axis}Min`, `${scale.axis}Max`]; 42 | 43 | const toAngle = (v: number) => { 44 | const valueRadius = scale.getDistanceFromCenterForValue(v); 45 | const resetRadius = animationOpts.animateScale ? 0 : valueRadius; 46 | return reset ? resetRadius : valueRadius; 47 | }; 48 | 49 | for (const key of keys) { 50 | const v = data[key as keyof IErrorBarRDataPoint]; 51 | if (Array.isArray(v)) { 52 | properties[key] = v.map(toAngle); 53 | } else if (typeof v === 'number') { 54 | properties[key] = toAngle(v); 55 | } else { 56 | properties[key] = null; // reset 57 | } 58 | } 59 | } 60 | 61 | const NUMERIC_SCALE_TYPES = ['linear', 'logarithmic', 'time', 'timeseries']; 62 | 63 | export function isNumericScale(scale: Scale): boolean { 64 | return NUMERIC_SCALE_TYPES.includes(scale.type); 65 | } 66 | -------------------------------------------------------------------------------- /src/elements/ArcWithErrorBar.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ArcElement, 3 | ArcOptions, 4 | ArcHoverOptions, 5 | ChartType, 6 | ScriptableAndArrayOptions, 7 | ScriptableContext, 8 | } from 'chart.js'; 9 | import { renderErrorBarArc, errorBarDefaults, errorBarDescriptors, IErrorBarOptions } from './render'; 10 | 11 | export default class ArcWithErrorBar extends ArcElement { 12 | /** 13 | * @hidden 14 | */ 15 | draw(ctx: CanvasRenderingContext2D): void { 16 | super.draw(ctx); 17 | 18 | renderErrorBarArc(this, ctx); 19 | } 20 | 21 | static readonly id = 'arcWithErrorBar'; 22 | 23 | /** 24 | * @hidden 25 | */ 26 | static readonly defaults: any = /* #__PURE__ */ { ...ArcElement.defaults, ...errorBarDefaults }; 27 | 28 | /** 29 | * @hidden 30 | */ 31 | static readonly defaultRoutes = ArcElement.defaultRoutes; 32 | 33 | /** 34 | * @hidden 35 | */ 36 | static readonly descriptors = errorBarDescriptors; 37 | } 38 | 39 | declare module 'chart.js' { 40 | export interface ElementOptionsByType { 41 | arcWithErrorBar: ScriptableAndArrayOptions< 42 | IErrorBarOptions & ArcOptions & ArcHoverOptions, 43 | ScriptableContext 44 | >; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/elements/BarWithErrorBar.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BarElement, 3 | BarHoverOptions, 4 | BarOptions, 5 | ChartType, 6 | ScriptableAndArrayOptions, 7 | ScriptableContext, 8 | } from 'chart.js'; 9 | import { renderErrorBar, errorBarDefaults, errorBarDescriptors, IErrorBarOptions } from './render'; 10 | 11 | export default class BarWithErrorBar extends BarElement { 12 | /** 13 | * @hidden 14 | */ 15 | draw(ctx: CanvasRenderingContext2D): void { 16 | super.draw(ctx); 17 | 18 | renderErrorBar(this, ctx); 19 | } 20 | 21 | static readonly id = 'barWithErrorBar'; 22 | 23 | /** 24 | * @hidden 25 | */ 26 | static readonly defaults: any = /* #__PURE__ */ { ...BarElement.defaults, ...errorBarDefaults }; 27 | 28 | /** 29 | * @hidden 30 | */ 31 | static readonly defaultRoutes = BarElement.defaultRoutes; 32 | 33 | /** 34 | * @hidden 35 | */ 36 | static readonly descriptors = errorBarDescriptors; 37 | } 38 | 39 | declare module 'chart.js' { 40 | export interface ElementOptionsByType { 41 | barWithErrorBar: ScriptableAndArrayOptions< 42 | IErrorBarOptions & BarOptions & BarHoverOptions, 43 | ScriptableContext 44 | >; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/elements/PointWithErrorBar.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PointElement, 3 | PointOptions, 4 | PointHoverOptions, 5 | ChartType, 6 | ScriptableAndArrayOptions, 7 | ScriptableContext, 8 | } from 'chart.js'; 9 | import { renderErrorBar, errorBarDefaults, errorBarDescriptors, IErrorBarOptions } from './render'; 10 | 11 | export default class PointWithErrorBar extends PointElement { 12 | /** 13 | * @hidden 14 | */ 15 | 16 | draw(ctx: CanvasRenderingContext2D, area?: any): void { 17 | (super.draw.call as any)(this, ctx, area); 18 | 19 | renderErrorBar(this as any, ctx); 20 | } 21 | 22 | static readonly id = 'pointWithErrorBar'; 23 | 24 | /** 25 | * @hidden 26 | */ 27 | static readonly defaults: any = /* #__PURE__ */ { ...PointElement.defaults, ...errorBarDefaults }; 28 | 29 | /** 30 | * @hidden 31 | */ 32 | static readonly defaultRoutes = PointElement.defaultRoutes; 33 | 34 | /** 35 | * @hidden 36 | */ 37 | static readonly descriptors = errorBarDescriptors; 38 | } 39 | 40 | declare module 'chart.js' { 41 | export interface ElementOptionsByType { 42 | pointWithErrorBar: ScriptableAndArrayOptions< 43 | IErrorBarOptions & PointOptions & PointHoverOptions, 44 | ScriptableContext 45 | >; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/elements/index.ts: -------------------------------------------------------------------------------- 1 | export { default as BarWithErrorBar } from './BarWithErrorBar'; 2 | export { default as PointWithErrorBar } from './PointWithErrorBar'; 3 | export { default as ArcWithErrorBar } from './ArcWithErrorBar'; 4 | export type { IErrorBarOptions } from './render'; 5 | -------------------------------------------------------------------------------- /src/elements/render.ts: -------------------------------------------------------------------------------- 1 | import type { Element, ArcProps } from 'chart.js'; 2 | 3 | export const errorBarDefaults = { 4 | errorBarLineWidth: { v: [1, 3] }, 5 | errorBarColor: { v: ['#2c2c2c', '#1f1f1f'] }, 6 | errorBarWhiskerLineWidth: { v: [1, 3] }, 7 | errorBarWhiskerRatio: { v: [0.2, 0.25] }, 8 | errorBarWhiskerSize: { v: [20, 24] }, 9 | errorBarWhiskerColor: { v: ['#2c2c2c', '#1f1f1f'] }, 10 | }; 11 | 12 | export const errorBarDescriptors = /* #__PURE__ */ { 13 | _scriptable: true, 14 | _indexable: (name: string): boolean => name !== 'v', 15 | }; 16 | 17 | export interface IErrorBarOptions { 18 | /** 19 | * line width of the center line 20 | * @default {v: [1, 3]} 21 | * scriptable 22 | */ 23 | errorBarLineWidth: number | { v: number[] }; 24 | /** 25 | * color of the center line 26 | * @default {v: ['#2c2c2c', '#1f1f1f']} 27 | * scriptable 28 | */ 29 | errorBarColor: string | { v: string[] }; 30 | /** 31 | * line width of the whisker lines 32 | * @default {v: [1, 3]} 33 | * scriptable 34 | */ 35 | errorBarWhiskerLineWidth: number | { v: number[] }; 36 | /** 37 | * width of the whiskers in relation to the bar width, use `0` to force a fixed with, see below 38 | * @default {v: [0.2, 0.25]} 39 | * scriptable 40 | */ 41 | errorBarWhiskerRatio: number | { v: number[] }; 42 | /** 43 | * pixel width of the whiskers for non bar chart cases 44 | * @default {v: [20, 24]} 45 | * scriptable 46 | */ 47 | errorBarWhiskerSize: number | { v: number[] }; 48 | /** 49 | * color of the whisker lines 50 | * @default {v: ['#2c2c2c', '#1f1f1f']} 51 | * scriptable 52 | */ 53 | errorBarWhiskerColor: string | { v: string[] }; 54 | } 55 | 56 | export const styleKeys = Object.keys(errorBarDefaults); 57 | 58 | function resolveMulti(vMin: number | number[], vMax: number | number[]) { 59 | const vMinArr = Array.isArray(vMin) ? vMin : [vMin]; 60 | const vMaxArr = Array.isArray(vMax) ? vMax : [vMax]; 61 | 62 | if (vMinArr.length === vMaxArr.length) { 63 | return vMinArr.map((v, i) => [v, vMaxArr[i]]); 64 | } 65 | const max = Math.max(vMinArr.length, vMaxArr.length); 66 | 67 | return Array(max).map((_, i) => [vMinArr[i % vMinArr.length], vMaxArr[i % vMaxArr.length]]); 68 | } 69 | 70 | function resolveOption(val: T | { v: T[] }, index: number): T; 71 | function resolveOption(val: readonly T[], index: number): T; 72 | function resolveOption(val: T | { v: T[] } | readonly T[], index: number) { 73 | if (typeof val === 'string' || typeof val === 'number') { 74 | return val; 75 | } 76 | const v = Array.isArray(val) ? val : (val as unknown as { v: T[] }).v; 77 | return v[index % v.length]; 78 | } 79 | 80 | function calculateHalfSize(total: number | null, options: IErrorBarOptions, i: number) { 81 | const ratio = resolveOption(options.errorBarWhiskerRatio, i); 82 | if (total != null && ratio > 0) { 83 | return total * ratio * 0.5; 84 | } 85 | const size = resolveOption(options.errorBarWhiskerSize, i); 86 | return size * 0.5; 87 | } 88 | 89 | function drawErrorBarVertical( 90 | props: IErrorBarProps, 91 | vMin: null | number | number[], 92 | vMax: null | number | number[], 93 | options: IErrorBarOptions, 94 | ctx: CanvasRenderingContext2D 95 | ) { 96 | ctx.save(); 97 | ctx.translate(props.x, 0); 98 | 99 | const bars = resolveMulti(vMin == null ? props.y : vMin, vMax == null ? props.y : vMax); 100 | 101 | bars.reverse().forEach(([mi, ma], j) => { 102 | const i = bars.length - j - 1; 103 | const halfWidth = calculateHalfSize(props.width, options, i); 104 | // center line 105 | ctx.lineWidth = resolveOption(options.errorBarLineWidth, i); 106 | ctx.strokeStyle = resolveOption(options.errorBarColor, i); 107 | ctx.beginPath(); 108 | ctx.moveTo(0, mi); 109 | ctx.lineTo(0, ma); 110 | ctx.stroke(); 111 | 112 | // whisker 113 | ctx.lineWidth = resolveOption(options.errorBarWhiskerLineWidth, i); 114 | ctx.strokeStyle = resolveOption(options.errorBarWhiskerColor, i); 115 | ctx.beginPath(); 116 | ctx.moveTo(-halfWidth, mi); 117 | ctx.lineTo(halfWidth, mi); 118 | ctx.moveTo(-halfWidth, ma); 119 | ctx.lineTo(halfWidth, ma); 120 | ctx.stroke(); 121 | }); 122 | 123 | ctx.restore(); 124 | } 125 | 126 | function drawErrorBarHorizontal( 127 | props: IErrorBarProps, 128 | vMin: null | number | number[], 129 | vMax: null | number | number[], 130 | options: IErrorBarOptions, 131 | ctx: CanvasRenderingContext2D 132 | ) { 133 | ctx.save(); 134 | ctx.translate(0, props.y); 135 | 136 | const bars = resolveMulti(vMin == null ? props.x : vMin, vMax == null ? props.x : vMax); 137 | 138 | bars.reverse().forEach(([mi, ma], j) => { 139 | const i = bars.length - j - 1; 140 | const halfHeight = calculateHalfSize(props.height, options, i); 141 | // center line 142 | ctx.lineWidth = resolveOption(options.errorBarLineWidth, i); 143 | ctx.strokeStyle = resolveOption(options.errorBarColor, i); 144 | ctx.beginPath(); 145 | ctx.moveTo(mi, 0); 146 | ctx.lineTo(ma, 0); 147 | ctx.stroke(); 148 | 149 | // whisker 150 | ctx.lineWidth = resolveOption(options.errorBarWhiskerLineWidth, i); 151 | ctx.strokeStyle = resolveOption(options.errorBarWhiskerColor, i); 152 | ctx.beginPath(); 153 | ctx.moveTo(mi, -halfHeight); 154 | ctx.lineTo(mi, halfHeight); 155 | ctx.moveTo(ma, -halfHeight); 156 | ctx.lineTo(ma, halfHeight); 157 | ctx.stroke(); 158 | }); 159 | 160 | ctx.restore(); 161 | } 162 | 163 | export interface IErrorBarProps { 164 | x: number; 165 | y: number; 166 | width: number; 167 | height: number; 168 | xMin?: number | number[]; 169 | yMin?: number | number[]; 170 | xMax?: number | number[]; 171 | yMax?: number | number[]; 172 | } 173 | 174 | export function renderErrorBar

(elem: Element, ctx: CanvasRenderingContext2D): void { 175 | const props = elem.getProps(['x', 'y', 'width', 'height', 'xMin', 'xMax', 'yMin', 'yMax']); 176 | if (props.xMin != null || props.xMax != null) { 177 | drawErrorBarHorizontal(props, props.xMin ?? null, props.xMax ?? null, elem.options as any, ctx); 178 | } 179 | if (props.yMin != null || props.yMax != null) { 180 | drawErrorBarVertical(props, props.yMin ?? null, props.yMax ?? null, elem.options as any, ctx); 181 | } 182 | } 183 | 184 | /** 185 | * @param {number} vMin 186 | * @param {number} vMax 187 | * @param {CanvasRenderingContext2D} ctx 188 | */ 189 | function drawErrorBarArc( 190 | props: ArcProps, 191 | vMin: null | number | number[], 192 | vMax: null | number | number[], 193 | options: IErrorBarOptions, 194 | ctx: CanvasRenderingContext2D 195 | ) { 196 | ctx.save(); 197 | ctx.translate(props.x, props.y); // move to center 198 | 199 | const angle = (props.startAngle + props.endAngle) / 2; 200 | const cosAngle = Math.cos(angle); 201 | const sinAngle = Math.sin(angle); 202 | // perpendicular 203 | const v = { 204 | x: -sinAngle, 205 | y: cosAngle, 206 | }; 207 | const length = Math.sqrt(v.x * v.x + v.y * v.y); 208 | v.x /= length; 209 | v.y /= length; 210 | 211 | const bars = resolveMulti(vMin ?? props.outerRadius, vMax ?? props.outerRadius); 212 | 213 | bars.reverse().forEach(([mi, ma], j) => { 214 | const i = bars.length - j - 1; 215 | 216 | const minCos = mi * cosAngle; 217 | const minSin = mi * sinAngle; 218 | const maxCos = ma * cosAngle; 219 | const maxSin = ma * sinAngle; 220 | 221 | const halfHeight = calculateHalfSize(null, options, i); 222 | const eX = v.x * halfHeight; 223 | const eY = v.y * halfHeight; 224 | 225 | // center line 226 | ctx.lineWidth = resolveOption(options.errorBarLineWidth, i); 227 | ctx.strokeStyle = resolveOption(options.errorBarColor, i); 228 | ctx.beginPath(); 229 | ctx.moveTo(minCos, minSin); 230 | ctx.lineTo(maxCos, maxSin); 231 | ctx.stroke(); 232 | 233 | // whisker 234 | ctx.lineWidth = resolveOption(options.errorBarWhiskerLineWidth, i); 235 | ctx.strokeStyle = resolveOption(options.errorBarWhiskerColor, i); 236 | ctx.beginPath(); 237 | ctx.moveTo(minCos + eX, minSin + eY); 238 | ctx.lineTo(minCos - eX, minSin - eY); 239 | ctx.moveTo(maxCos + eX, maxSin + eY); 240 | ctx.lineTo(maxCos - eX, maxSin - eY); 241 | ctx.stroke(); 242 | }); 243 | 244 | ctx.restore(); 245 | } 246 | 247 | export function renderErrorBarArc(elem: any, ctx: CanvasRenderingContext2D): void { 248 | const props = elem.getProps(['x', 'y', 'startAngle', 'endAngle', 'rMin', 'rMax', 'outerRadius']); 249 | if (props.rMin != null || props.rMax != null) { 250 | drawErrorBarArc(props, props.rMin, props.rMax, elem.options as any, ctx); 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './elements'; 2 | export * from './controllers'; 3 | -------------------------------------------------------------------------------- /src/index.umd.ts: -------------------------------------------------------------------------------- 1 | import { registry } from 'chart.js'; 2 | import { 3 | BarWithErrorBarsController, 4 | LineWithErrorBarsController, 5 | PolarAreaWithErrorBarsController, 6 | ScatterWithErrorBarsController, 7 | } from './controllers'; 8 | import { BarWithErrorBar, ArcWithErrorBar, PointWithErrorBar } from './elements'; 9 | 10 | export * from '.'; 11 | 12 | registry.addControllers( 13 | BarWithErrorBarsController, 14 | LineWithErrorBarsController, 15 | PolarAreaWithErrorBarsController, 16 | ScatterWithErrorBarsController 17 | ); 18 | registry.addElements(BarWithErrorBar, ArcWithErrorBar, PointWithErrorBar); 19 | -------------------------------------------------------------------------------- /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-error-bars", 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 | --------------------------------------------------------------------------------