diff --git a/.browserslistrc b/.browserslistrc new file mode 100644 index 00000000000..6a847fd0f76 --- /dev/null +++ b/.browserslistrc @@ -0,0 +1,4 @@ +defaults +not IE 11 +not IE_Mob 11 +maintained node versions diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 00000000000..81a86a8b1cc --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,25 @@ +version: "2" +plugins: + duplication: + enabled: true + config: + languages: + - javascript + fixme: + enabled: true +checks: + argument-count: + config: + threshold: 5 + method-complexity: + config: + threshold: 7 +exclude_patterns: + - "dist/" + - "docs/" + - "scripts/" + - "test/" + - "*.js" + - "*.json" + - "*.md" + - ".*" diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000000..063c99ae989 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# https://editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.html] +indent_style = tab +indent_size = 4 diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000000..15caa4d6118 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +dist/* +test/integration/react-browser/* diff --git a/.eslintrc.yml b/.eslintrc.yml new file mode 100644 index 00000000000..d4a2057ca13 --- /dev/null +++ b/.eslintrc.yml @@ -0,0 +1,61 @@ +extends: + - chartjs + - plugin:es/restrict-to-es2018 + - plugin:markdown/recommended + +settings: + es: + aggressive: true + +env: + es6: true + browser: true + node: true + +parserOptions: + ecmaVersion: 2022 + sourceType: module + ecmaFeatures: + impliedStrict: true + modules: true + +plugins: ['html', 'es'] + +rules: + class-methods-use-this: "off" + complexity: ["warn", 10] + max-statements: ["warn", 30] + no-empty-function: "off" + no-use-before-define: ["error", { "functions": false }] + # disable everything, except Rest/Spread Properties in ES2018 + es/no-import-meta: "off" + es/no-async-iteration: "error" + es/no-malformed-template-literals: "error" + es/no-regexp-lookbehind-assertions: "error" + es/no-regexp-named-capture-groups: "error" + es/no-regexp-s-flag: "error" + es/no-regexp-unicode-property-escapes: "error" + es/no-dynamic-import: "off" + +overrides: + - files: ['**/*.ts'] + parser: '@typescript-eslint/parser' + plugins: + - '@typescript-eslint' + extends: + - chartjs + - plugin:@typescript-eslint/recommended + + rules: + complexity: ["warn", 10] + max-statements: ["warn", 30] + # Replace stock eslint rules with typescript-eslint equivalents for proper + # TypeScript support. + indent: "off" + "@typescript-eslint/indent": ["error", 2] + no-use-before-define: "off" + '@typescript-eslint/no-use-before-define': "error" + no-shadow: "off" + '@typescript-eslint/no-shadow': "error" + space-before-function-paren: "off" + '@typescript-eslint/space-before-function-paren': [2, never] diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000000..a926215c1f0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,14 @@ + diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 00000000000..ffccce5de0b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,84 @@ +name: Bug Report +description: Something went awry +labels: ["type: bug"] +body: + - type: markdown + attributes: + value: | + Need help or support? + Please don't open an issue! Head to https://stackoverflow.com/questions/tagged/chart.js. + + - type: markdown + attributes: + value: "Bug reports MUST be submitted with an interactive example: https://codepen.io/leelenaleee/pen/WNyJXEe." + + - type: markdown + attributes: + value: Chart.js versions lower then 4.x are NOT supported anymore, new issues will be disregarded. + + - type: textarea + attributes: + label: Expected behavior + description: Tell us what should happen. + validations: + required: true + + - type: textarea + attributes: + label: Current behavior + description: Tell us what happens instead of the expected behavior. + validations: + required: true + + - type: input + attributes: + label: Reproducible sample + description: | + Please provide issue reproduction. + You can use [this codepen](https://codepen.io/leelenaleee/pen/WNyJXEe) to make a reproducible sample. + + Major framework wrappers for chart.js templates: + [vue-chart-3 sandbox (Vue)](https://codesandbox.io/s/vue-chart-3-chart-js-issue-template-bpg7k?file=/src/App.vue) + [ng2-charts sandbox (Angular)](https://codesandbox.io/s/ng2charts-chart-js-issue-template-fhezt?file=/src/app/app.component.ts) + [react-chartjs-2 sandbox (React)](https://codesandbox.io/p/sandbox/react-chartjs-2-chart-js-issue-template-v4-forked-lqz5tn?file=%2Fsrc%2FApp.tsx) + + For typescript issues you can make use of [this TS Playground](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAbzgYQBYENZwL5wGZQQhwDkAxhrAHQBWAziQNwCwAUGwG6ZxkwAecALxwAJhDIBXEAFMAdjCoBzaTACiAG2kz5AIQCeASREAKAEQg9aTDFMBKOOjpwAEgBUAsgBlk6WVzoaWnIwLKxcUHAWVljCstIA7iiUMMa8fAA0iGxwOXAwemDSAFyk6sBxJOnZuSLoMOglCNW5ueroAEbS6nQlANqmAErSIqaZpjrqEtKjcKYAml3qEPEzpgDiUNJyqwAKElBgmqsA8lC+yqYAulWsLS219XQqPXC9Tbd3n22d6iUkAMRwCB4OAANQgMGkDBun0+DwarwAjAAmTKIgCcmQAzJkAKyZVFwLHXZp3bCXUnYGG5CBgGDACCyF7vT50MjoTTM0ktPiNbl3fk5KmCuB6PkfWFwEXYfkyiU4NjYWyMIA) to make a reproducible sample. + + If filing a bug against `master`, you may reference the latest code via + https://www.chartjs.org/dist/master/chart.umd.min.js (changing the filename to + point at the file you need as appropriate). Do not rely on these files for + production purposes as they may be removed at any time. + validations: + required: true + + - type: textarea + attributes: + label: Optional extra steps/info to reproduce + + - type: textarea + attributes: + label: Possible solution + description: If you have suggestions on a fix for the bug. + + - type: textarea + attributes: + label: Context + description: | + How has this issue affected you? What are you trying to accomplish? + Providing context helps us come up with a solution that is most useful in the real world. + + - type: input + attributes: + label: chart.js version + description: Which version of `chart.js` are you using? + placeholder: "v0.0.0" + validations: + required: true + + - type: input + attributes: + label: Browser name and version + + - type: input + attributes: + label: Link to your project diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000000..c01d78e9aff --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Support, Help, and Advice + url: https://stackoverflow.com/questions/tagged/chart.js + about: Need help or support? Head to https://stackoverflow.com/questions/tagged/chart.js diff --git a/.github/ISSUE_TEMPLATE/docs.yml b/.github/ISSUE_TEMPLATE/docs.yml new file mode 100644 index 00000000000..1461fcf1537 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/docs.yml @@ -0,0 +1,31 @@ +name: Documentation +description: Are the docs lacking or missing something? +labels: ["type: documentation"] +body: + - type: checkboxes + attributes: + label: "Documentation Is:" + options: + - label: Missing or needed? + - label: Confusing + - label: Not sure? + + - type: textarea + attributes: + label: Please Explain in Detail... + validations: + required: true + + - type: textarea + attributes: + label: Your Proposal for Changes + validations: + required: true + + - type: input + attributes: + label: Example + description: | + Provide a link to a live example demonstrating the issue or feature to be documented: + Normal: https://codepen.io/pen?template=BapRepQ + TS: [TS Playground](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAbzgYQBYENZwL5wGZQQhwDkAxhrAHQBWAziQNwCwAUGwG6ZxkwAecALxwAJhDIBXEAFMAdjCoBzaTACiAG2kz5AIQCeASREAKAEQg9aTDFMBKOOjpwAEgBUAsgBlk6WVzoaWnIwLKxcUHAWVljCstIA7iiUMMa8fAA0iGxwOXAwemDSAFyk6sBxJOnZuSLoMOglCNW5ueroAEbS6nQlANqmAErSIqaZpjrqEtKjcKYAml3qEPEzpgDiUNJyqwAKElBgmqsA8lC+yqYAulWsLS219XQqPXC9Tbd3n22d6iUkAMRwCB4OAANQgMGkDBun0+DwarwAjAAmTKIgCcmQAzJkAKyZVFwLHXZp3bCXUnYGG5CBgGDACCyF7vT50MjoTTM0ktPiNbl3fk5KmCuB6PkfWFwEXYfkyiU4NjYWyMIA) diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 00000000000..2795b1edb5a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,28 @@ +name: Feature Request +description: Suggest an idea +labels: ["type: enhancement"] +body: + - type: markdown + attributes: + value: | + Most features should start as plugins outside of Chart.js + (https://www.chartjs.org/docs/latest/developers/plugins.html). + Please consider whether your changes are useful for all users, or if this is + specific to your usecase and a Chart.js plugin would be more appropriate. + + Need help or tech support? Please don't open an issue! + Head to https://stackoverflow.com/questions/tagged/chart.js + + - type: textarea + attributes: + label: Feature Proposal + description: | + What are you trying to accomplish? + Providing context helps us come up with a solution that is most useful in the real world + validations: + required: true + + - type: textarea + attributes: + label: Possible Implementation + description: Not obligatory, but suggest ideas for how to implement the addition or change diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000000..8ab50f66b12 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,11 @@ + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000000..5ace4600a1f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 00000000000..636045e034d --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,54 @@ +name-template: 'v$RESOLVED_VERSION' +tag-template: 'v$RESOLVED_VERSION' +categories: + - title: 'Breaking Changes' + labels: + - 'breaking change' + - title: 'Enhancements' + labels: + - 'type: enhancement' + - title: 'Performance' + labels: + - 'type: performance' + - title: 'Bugs Fixed' + labels: + - 'type: bug' + - title: 'Types' + labels: + - 'type: types' + - title: 'Documentation' + labels: + - 'type: documentation' + - title: 'Development' + labels: + - 'type: chore' + - 'dependencies' +exclude-labels: + - 'type: infrastructure' +change-template: '- #$NUMBER $TITLE' +change-title-escapes: '\<*_&`#@' +version-resolver: + major: + labels: + - 'breaking change' + minor: + labels: + - 'type: enhancement' + patch: + labels: + - 'type: bug' + - 'type: chore' + - 'type: types' + default: patch +template: | + # Essential Links + + * [npm](https://www.npmjs.com/package/chart.js) + * [Migration guide](https://www.chartjs.org/docs/$RESOLVED_VERSION/migration/v4-migration.html) + * [Docs](https://www.chartjs.org/docs/$RESOLVED_VERSION/) + * [API](https://www.chartjs.org/docs/$RESOLVED_VERSION/api/) + * [Samples](https://www.chartjs.org/docs/$RESOLVED_VERSION/samples/information.html) + + $CHANGES + + Thanks to $CONTRIBUTORS diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000000..1ac8ca6e9ed --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,114 @@ +name: CI + +on: + push: + branches: + - master + - "2.9" + pull_request: + branches: + - master + - "2.9" + workflow_dispatch: +permissions: + contents: read + +jobs: + build: + permissions: + checks: write # for coverallsapp/github-action to create new checks + contents: read # for dorny/paths-filter to fetch a list of changed files + pull-requests: read # for dorny/paths-filter to read pull requests + runs-on: ${{ matrix.os }} + + outputs: + coveralls: ${{ steps.changes.outputs.src }} + + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + fail-fast: false + + steps: + - uses: actions/checkout@v6 + - uses: pnpm/action-setup@v4.2.0 + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version: 16 + cache: pnpm + - uses: dorny/paths-filter@v3 + id: changes + with: + filters: | + docs: + - 'docs/**' + - 'package.json' + - 'tsconfig.json' + src: + - 'src/**' + - 'package.json' + test: + - 'test/**' + - 'karma.conf.js' + - 'package.json' + types: + - 'package.json' + - 'tsconfig.json' + - name: Install + run: pnpm install + - name: Lint + run: pnpm run lint + - name: Build + run: pnpm run build + - name: Test + if: | + (steps.changes.outputs.src == 'true' || + steps.changes.outputs.test == 'true') && + runner.os != 'Windows' + run: | + pnpm run build + if [ "${{ runner.os }}" == "macOS" ]; then + pnpm run test-ci --browsers chrome,safari + else + xvfb-run --auto-servernum pnpm run test-ci + fi + shell: bash + - name: Package + if: steps.changes.outputs.docs == 'true' + run: | + pnpm run docs + pnpm pack + - name: Coveralls Parallel - Chrome + if: | + steps.changes.outputs.src == 'true' && + runner.os != 'Windows' + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.github_token }} + path-to-lcov: './coverage/chrome/lcov.info' + flag-name: ${{ matrix.os }}-chrome + parallel: true + - name: Coveralls Parallel - Firefox + if: | + steps.changes.outputs.src == 'true' && + runner.os != 'Windows' + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.github_token }} + path-to-lcov: './coverage/firefox/lcov.info' + flag-name: ${{ matrix.os }}-firefox + parallel: true + + finish: + permissions: + checks: write # for coverallsapp/github-action to create new checks + needs: build + runs-on: ubuntu-latest + steps: + - name: Coveralls Finished + if: needs.build.outputs.coveralls == 'true' + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.github_token }} + parallel-finished: true diff --git a/.github/workflows/compressed-size.yml b/.github/workflows/compressed-size.yml new file mode 100644 index 00000000000..dc6d1b35e47 --- /dev/null +++ b/.github/workflows/compressed-size.yml @@ -0,0 +1,23 @@ +name: Compressed Size + +on: [pull_request] + +permissions: + contents: read + +jobs: + build: + + permissions: + checks: write # for preactjs/compressed-size-action to create and update the checks + contents: read # for actions/checkout to fetch code + issues: write # for preactjs/compressed-size-action to create comments + pull-requests: write # for preactjs/compressed-size-action to write a PR review + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + - uses: pnpm/action-setup@v4.2.0 + - uses: preactjs/compressed-size-action@v2 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 00000000000..71acca1cf6f --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,43 @@ +# This workflow publishes new documentation to https://chartjs.org/docs/master after every commit +name: Deploy docs + +on: + push: + branches: + - master + +permissions: + contents: read + +jobs: + correct_repository: + permissions: + contents: none + runs-on: ubuntu-latest + steps: + - name: fail on fork + if: github.repository_owner != 'chartjs' + run: exit 1 + + build: + needs: correct_repository + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: pnpm/action-setup@v4.2.0 + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version: 16 + cache: pnpm + - name: Package & Deploy Docs + run: | + pnpm install + pnpm run build + ./scripts/docs-config.sh "master" + pnpm run docs + pnpm pack + ./scripts/deploy-docs.sh "master" + env: + GITHUB_TOKEN: ${{ secrets.GH_AUTH_TOKEN }} + GH_AUTH_EMAIL: ${{ secrets.GH_AUTH_EMAIL }} diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 00000000000..04609ba4def --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,31 @@ +name: Release Drafter + +on: + push: + branches: + - master + workflow_dispatch: + +permissions: + contents: read + +jobs: + correct_repository: + permissions: + contents: none + runs-on: ubuntu-latest + steps: + - name: fail on fork + if: github.repository_owner != 'chartjs' + run: exit 1 + + update_release_draft: + permissions: + contents: write # for release-drafter/release-drafter to create a github release + pull-requests: write # for release-drafter/release-drafter to add label to PR + needs: correct_repository + runs-on: ubuntu-latest + steps: + - uses: release-drafter/release-drafter@v6 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000000..57b860941c3 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,96 @@ +name: Release + +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + setup: + permissions: + contents: none + runs-on: ubuntu-latest + outputs: + version: ${{ steps.trim.outputs.version }} + steps: + - id: trim + run: echo "version=${TAG:1}" >> $GITHUB_OUTPUT + env: + TAG: ${{ github.event.release.tag_name }} + + release: + permissions: + contents: write # for actions/upload-release-asset to upload release asset + needs: setup + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: pnpm/action-setup@v4.2.0 + - uses: actions/setup-node@v6 + with: + registry-url: https://registry.npmjs.org/ + node-version: 16 + cache: pnpm + - name: Setup and build + run: | + pnpm install + pnpm install -g json + json -I -f package.json -e "this.version=\"$VERSION\"" + pnpm run build + ./scripts/docs-config.sh "$VERSION" release + pnpm run docs + pnpm pack + env: + VERSION: ${{ needs.setup.outputs.version }} + - name: Publish to NPM + run: ./scripts/publish.sh + env: + NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} + VERSION: ${{ needs.setup.outputs.version }} + - name: Deploy Docs + run: ./scripts/deploy-docs.sh "$VERSION" release + env: + GITHUB_TOKEN: ${{ secrets.GH_AUTH_TOKEN }} + GH_AUTH_EMAIL: ${{ secrets.GH_AUTH_EMAIL }} + VERSION: ${{ needs.setup.outputs.version }} + - name: Upload NPM package file + id: upload-npm-package-file + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ needs.setup.outputs.version }} + with: + upload_url: ${{ github.event.release.upload_url }} + asset_path: ${{ format('chart.js-{0}.tgz', needs.setup.outputs.version) }} + asset_name: ${{ format('chart.js-{0}.tgz', needs.setup.outputs.version) }} + asset_content_type: application/gzip + release-tag: + needs: [setup, release] + runs-on: ubuntu-latest + if: "!github.event.release.prerelease" + steps: + - uses: actions/checkout@v6 + - uses: pnpm/action-setup@v4.2.0 + - uses: actions/setup-node@v6 + with: + registry-url: https://registry.npmjs.org/ + node-version: 16 + cache: pnpm + - name: Setup and build + run: | + pnpm install + pnpm install -g json + json -I -f package.json -e "this.version=\"$VERSION\"" + pnpm run build + ./scripts/docs-config.sh "$VERSION" + pnpm run docs + env: + VERSION: ${{ needs.setup.outputs.version }} + - name: Deploy Docs + run: ./scripts/deploy-docs.sh "$VERSION" + env: + GITHUB_TOKEN: ${{ secrets.GH_AUTH_TOKEN }} + GH_AUTH_EMAIL: ${{ secrets.GH_AUTH_EMAIL }} + VERSION: ${{ needs.setup.outputs.version }} diff --git a/.gitignore b/.gitignore index 1986497d09e..828d3f91bd0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,38 @@ +# Deployment +/coverage +/custom +/dist +/gh-pages -.DS_Store +# Node.js +node_modules/ +npm-debug.log* + +# Docs +.cache-loader +build/ -node_modules/* -custom/* +# Generated type docs +docs/api +docs/.vuepress/dist + +# Development +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local +.idea +.project +.settings +.vscode +.zed +*.log +*.swp +*.stackdump -docs/index.md +# Generated +/test/types/autogen*.ts -.settings/ +# Eslint +.eslintcache diff --git a/.htmllintrc b/.htmllintrc new file mode 100644 index 00000000000..1ab933490de --- /dev/null +++ b/.htmllintrc @@ -0,0 +1,19 @@ +{ + "indent-style": "tabs", + "line-end-style": false, + "attr-quote-style": "double", + "spec-char-escape": false, + "attr-bans": [ + "align", + "background", + "bgcolor", + "border", + "frameborder", + "longdesc", + "marginwidth", + "marginheight", + "scrolling" + ], + "tag-bans": [ "b", "i" ], + "id-class-style": false +} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index c66ea5fb776..00000000000 --- a/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -language: node_js -node_js: - - "0.11" - - "0.10" - -before_script: - - npm install - -script: - - gulp test - -notifications: - slack: chartjs:pcfCZR6ugg5TEcaLtmIfQYuA diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index f579bf42805..00000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,55 +0,0 @@ -Contributing to Chart.js -======================== - -Contributions to Chart.js are welcome and encouraged, but please have a look through the guidelines in this document before raising an issue, or writing code for the project. - - -Using issues ------------- - -The [issue tracker](https://github.com/nnnick/Chart.js/issues) is the preferred channel for reporting bugs, requesting new features and submitting pull requests. - -If you're suggesting a new chart type, please take a look at [writing new chart types](https://github.com/nnnick/Chart.js/blob/master/docs/06-Advanced.md#writing-new-chart-types) in the documentation, and some of the [community extensions](https://github.com/nnnick/Chart.js/blob/master/docs/06-Advanced.md#community-extensions) that have been created already. - -To keep the library lightweight for everyone, it's unlikely we'll add many more chart types to the core of Chart.js, but issues are a good medium to design and spec out how new chart types could work and look. - -Please do not use issues for support requests. For help using Chart.js, please take a look at the [`chartjs`](http://stackoverflow.com/questions/tagged/chartjs) tag on Stack Overflow. - - -Reporting bugs --------------- - -Well structured, detailed bug reports are hugely valuable for the project. - -Guidlines for reporting bugs: - - - Check the issue search to see if it has already been reported - - Isolate the problem to a simple test case - - Provide a demonstration of the problem on [JS Bin](http://jsbin.com) or similar - -Please provide any additional details associated with the bug, if it's browser or screen density specific, or only happens with a certain configuration or data. - - -Pull requests -------------- - -Clear, concise pull requests are excellent at continuing the project's community driven growth. But please review [these guidelines](https://github.com/blog/1943-how-to-write-the-perfect-pull-request) and the guidelines below before starting work on the project. - -Guidlines: - - - Please create an issue first: - - For bugs, we can discuss the fixing approach - - For enhancements, we can discuss if it is within the project scope and avoid duplicate effort - - Please make changes to the files in [`/src`](https://github.com/nnnick/Chart.js/tree/master/src), not `Chart.js` or `Chart.min.js` in the repo root directory, this avoids merge conflicts - - Tabs for indentation, not spaces please - - If adding new functionality, please also update the relevant `.md` file in [`/docs`](https://github.com/nnnick/Chart.js/tree/master/docs) - - Please make your commits in logical sections with clear commit messages - -Joining the project -------------- - - Active committers and contributors are invited to introduce yourself and request commit access to this project. Please send an email to hello@nickdownie.com or file an issue. - -License -------- - -By contributing your code, you agree to license your contribution under the [MIT license](https://github.com/nnnick/Chart.js/blob/master/LICENSE.md). diff --git a/Chart.js b/Chart.js deleted file mode 100644 index c264262ba73..00000000000 --- a/Chart.js +++ /dev/null @@ -1,3477 +0,0 @@ -/*! - * Chart.js - * http://chartjs.org/ - * Version: 1.0.2 - * - * Copyright 2015 Nick Downie - * Released under the MIT license - * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md - */ - - -(function(){ - - "use strict"; - - //Declare root variable - window in the browser, global on the server - var root = this, - previous = root.Chart; - - //Occupy the global variable of Chart, and create a simple base class - var Chart = function(context){ - var chart = this; - this.canvas = context.canvas; - - this.ctx = context; - - //Variables global to the chart - var computeDimension = function(element,dimension) - { - if (element['offset'+dimension]) - { - return element['offset'+dimension]; - } - else - { - return document.defaultView.getComputedStyle(element).getPropertyValue(dimension); - } - } - - var width = this.width = computeDimension(context.canvas,'Width'); - var height = this.height = computeDimension(context.canvas,'Height'); - - // Firefox requires this to work correctly - context.canvas.width = width; - context.canvas.height = height; - - var width = this.width = context.canvas.width; - var height = this.height = context.canvas.height; - this.aspectRatio = this.width / this.height; - //High pixel density displays - multiply the size of the canvas height/width by the device pixel ratio, then scale. - helpers.retinaScale(this); - - return this; - }; - //Globally expose the defaults to allow for user updating/changing - Chart.defaults = { - global: { - // Boolean - Whether to animate the chart - animation: true, - - // Number - Number of animation steps - animationSteps: 60, - - // String - Animation easing effect - animationEasing: "easeOutQuart", - - // Boolean - If we should show the scale at all - showScale: true, - - // Boolean - If we want to override with a hard coded scale - scaleOverride: false, - - // ** Required if scaleOverride is true ** - // Number - The number of steps in a hard coded scale - scaleSteps: null, - // Number - The value jump in the hard coded scale - scaleStepWidth: null, - // Number - The scale starting value - scaleStartValue: null, - - // String - Colour of the scale line - scaleLineColor: "rgba(0,0,0,.1)", - - // Number - Pixel width of the scale line - scaleLineWidth: 1, - - // Boolean - Whether to show labels on the scale - scaleShowLabels: true, - - // Interpolated JS string - can access value - scaleLabel: "<%=value%>", - - // Boolean - Whether the scale should stick to integers, and not show any floats even if drawing space is there - scaleIntegersOnly: true, - - // Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value - scaleBeginAtZero: false, - - // String - Scale label font declaration for the scale label - scaleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", - - // Number - Scale label font size in pixels - scaleFontSize: 12, - - // String - Scale label font weight style - scaleFontStyle: "normal", - - // String - Scale label font colour - scaleFontColor: "#666", - - // Boolean - whether or not the chart should be responsive and resize when the browser does. - responsive: false, - - // Boolean - whether to maintain the starting aspect ratio or not when responsive, if set to false, will take up entire container - maintainAspectRatio: true, - - // Boolean - Determines whether to draw tooltips on the canvas or not - attaches events to touchmove & mousemove - showTooltips: true, - - // Boolean - Determines whether to draw built-in tooltip or call custom tooltip function - customTooltips: false, - - // Array - Array of string names to attach tooltip events - tooltipEvents: ["mousemove", "touchstart", "touchmove", "mouseout"], - - // String - Tooltip background colour - tooltipFillColor: "rgba(0,0,0,0.8)", - - // String - Tooltip label font declaration for the scale label - tooltipFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", - - // Number - Tooltip label font size in pixels - tooltipFontSize: 14, - - // String - Tooltip font weight style - tooltipFontStyle: "normal", - - // String - Tooltip label font colour - tooltipFontColor: "#fff", - - // String - Tooltip title font declaration for the scale label - tooltipTitleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", - - // Number - Tooltip title font size in pixels - tooltipTitleFontSize: 14, - - // String - Tooltip title font weight style - tooltipTitleFontStyle: "bold", - - // String - Tooltip title font colour - tooltipTitleFontColor: "#fff", - - // Number - pixel width of padding around tooltip text - tooltipYPadding: 6, - - // Number - pixel width of padding around tooltip text - tooltipXPadding: 6, - - // Number - Size of the caret on the tooltip - tooltipCaretSize: 8, - - // Number - Pixel radius of the tooltip border - tooltipCornerRadius: 6, - - // Number - Pixel offset from point x to tooltip edge - tooltipXOffset: 10, - - // String - Template string for single tooltips - tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= value %>", - - // String - Template string for single tooltips - multiTooltipTemplate: "<%= value %>", - - // String - Colour behind the legend colour block - multiTooltipKeyBackground: '#fff', - - // Function - Will fire on animation progression. - onAnimationProgress: function(){}, - - // Function - Will fire on animation completion. - onAnimationComplete: function(){} - - } - }; - - //Create a dictionary of chart types, to allow for extension of existing types - Chart.types = {}; - - //Global Chart helpers object for utility methods and classes - var helpers = Chart.helpers = {}; - - //-- Basic js utility methods - var each = helpers.each = function(loopable,callback,self){ - var additionalArgs = Array.prototype.slice.call(arguments, 3); - // Check to see if null or undefined firstly. - if (loopable){ - if (loopable.length === +loopable.length){ - var i; - for (i=0; i= 0; i--) { - var currentItem = arrayToSearch[i]; - if (filterCallback(currentItem)){ - return currentItem; - } - } - }, - inherits = helpers.inherits = function(extensions){ - //Basic javascript inheritance based on the model created in Backbone.js - var parent = this; - var ChartElement = (extensions && extensions.hasOwnProperty("constructor")) ? extensions.constructor : function(){ return parent.apply(this, arguments); }; - - var Surrogate = function(){ this.constructor = ChartElement;}; - Surrogate.prototype = parent.prototype; - ChartElement.prototype = new Surrogate(); - - ChartElement.extend = inherits; - - if (extensions) extend(ChartElement.prototype, extensions); - - ChartElement.__super__ = parent.prototype; - - return ChartElement; - }, - noop = helpers.noop = function(){}, - uid = helpers.uid = (function(){ - var id=0; - return function(){ - return "chart-" + id++; - }; - })(), - warn = helpers.warn = function(str){ - //Method for warning of errors - if (window.console && typeof window.console.warn == "function") console.warn(str); - }, - amd = helpers.amd = (typeof define == 'function' && define.amd), - //-- Math methods - isNumber = helpers.isNumber = function(n){ - return !isNaN(parseFloat(n)) && isFinite(n); - }, - max = helpers.max = function(array){ - return Math.max.apply( Math, array ); - }, - min = helpers.min = function(array){ - return Math.min.apply( Math, array ); - }, - cap = helpers.cap = function(valueToCap,maxValue,minValue){ - if(isNumber(maxValue)) { - if( valueToCap > maxValue ) { - return maxValue; - } - } - else if(isNumber(minValue)){ - if ( valueToCap < minValue ){ - return minValue; - } - } - return valueToCap; - }, - getDecimalPlaces = helpers.getDecimalPlaces = function(num){ - if (num%1!==0 && isNumber(num)){ - return num.toString().split(".")[1].length; - } - else { - return 0; - } - }, - toRadians = helpers.radians = function(degrees){ - return degrees * (Math.PI/180); - }, - // Gets the angle from vertical upright to the point about a centre. - getAngleFromPoint = helpers.getAngleFromPoint = function(centrePoint, anglePoint){ - var distanceFromXCenter = anglePoint.x - centrePoint.x, - distanceFromYCenter = anglePoint.y - centrePoint.y, - radialDistanceFromCenter = Math.sqrt( distanceFromXCenter * distanceFromXCenter + distanceFromYCenter * distanceFromYCenter); - - - var angle = Math.PI * 2 + Math.atan2(distanceFromYCenter, distanceFromXCenter); - - //If the segment is in the top left quadrant, we need to add another rotation to the angle - if (distanceFromXCenter < 0 && distanceFromYCenter < 0){ - angle += Math.PI*2; - } - - return { - angle: angle, - distance: radialDistanceFromCenter - }; - }, - aliasPixel = helpers.aliasPixel = function(pixelWidth){ - return (pixelWidth % 2 === 0) ? 0 : 0.5; - }, - splineCurve = helpers.splineCurve = function(FirstPoint,MiddlePoint,AfterPoint,t){ - //Props to Rob Spencer at scaled innovation for his post on splining between points - //http://scaledinnovation.com/analytics/splines/aboutSplines.html - var d01=Math.sqrt(Math.pow(MiddlePoint.x-FirstPoint.x,2)+Math.pow(MiddlePoint.y-FirstPoint.y,2)), - d12=Math.sqrt(Math.pow(AfterPoint.x-MiddlePoint.x,2)+Math.pow(AfterPoint.y-MiddlePoint.y,2)), - fa=t*d01/(d01+d12),// scaling factor for triangle Ta - fb=t*d12/(d01+d12); - return { - inner : { - x : MiddlePoint.x-fa*(AfterPoint.x-FirstPoint.x), - y : MiddlePoint.y-fa*(AfterPoint.y-FirstPoint.y) - }, - outer : { - x: MiddlePoint.x+fb*(AfterPoint.x-FirstPoint.x), - y : MiddlePoint.y+fb*(AfterPoint.y-FirstPoint.y) - } - }; - }, - calculateOrderOfMagnitude = helpers.calculateOrderOfMagnitude = function(val){ - return Math.floor(Math.log(val) / Math.LN10); - }, - calculateScaleRange = helpers.calculateScaleRange = function(valuesArray, drawingSize, textSize, startFromZero, integersOnly){ - - //Set a minimum step of two - a point at the top of the graph, and a point at the base - var minSteps = 2, - maxSteps = Math.floor(drawingSize/(textSize * 1.5)), - skipFitting = (minSteps >= maxSteps); - - var maxValue = max(valuesArray), - minValue = min(valuesArray); - - // We need some degree of seperation here to calculate the scales if all the values are the same - // Adding/minusing 0.5 will give us a range of 1. - if (maxValue === minValue){ - maxValue += 0.5; - // So we don't end up with a graph with a negative start value if we've said always start from zero - if (minValue >= 0.5 && !startFromZero){ - minValue -= 0.5; - } - else{ - // Make up a whole number above the values - maxValue += 0.5; - } - } - - var valueRange = Math.abs(maxValue - minValue), - rangeOrderOfMagnitude = calculateOrderOfMagnitude(valueRange), - graphMax = Math.ceil(maxValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude), - graphMin = (startFromZero) ? 0 : Math.floor(minValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude), - graphRange = graphMax - graphMin, - stepValue = Math.pow(10, rangeOrderOfMagnitude), - numberOfSteps = Math.round(graphRange / stepValue); - - //If we have more space on the graph we'll use it to give more definition to the data - while((numberOfSteps > maxSteps || (numberOfSteps * 2) < maxSteps) && !skipFitting) { - if(numberOfSteps > maxSteps){ - stepValue *=2; - numberOfSteps = Math.round(graphRange/stepValue); - // Don't ever deal with a decimal number of steps - cancel fitting and just use the minimum number of steps. - if (numberOfSteps % 1 !== 0){ - skipFitting = true; - } - } - //We can fit in double the amount of scale points on the scale - else{ - //If user has declared ints only, and the step value isn't a decimal - if (integersOnly && rangeOrderOfMagnitude >= 0){ - //If the user has said integers only, we need to check that making the scale more granular wouldn't make it a float - if(stepValue/2 % 1 === 0){ - stepValue /=2; - numberOfSteps = Math.round(graphRange/stepValue); - } - //If it would make it a float break out of the loop - else{ - break; - } - } - //If the scale doesn't have to be an int, make the scale more granular anyway. - else{ - stepValue /=2; - numberOfSteps = Math.round(graphRange/stepValue); - } - - } - } - - if (skipFitting){ - numberOfSteps = minSteps; - stepValue = graphRange / numberOfSteps; - } - - return { - steps : numberOfSteps, - stepValue : stepValue, - min : graphMin, - max : graphMin + (numberOfSteps * stepValue) - }; - - }, - /* jshint ignore:start */ - // Blows up jshint errors based on the new Function constructor - //Templating methods - //Javascript micro templating by John Resig - source at http://ejohn.org/blog/javascript-micro-templating/ - template = helpers.template = function(templateString, valuesObject){ - - // If templateString is function rather than string-template - call the function for valuesObject - - if(templateString instanceof Function){ - return templateString(valuesObject); - } - - var cache = {}; - function tmpl(str, data){ - // Figure out if we're getting a template, or if we need to - // load the template - and be sure to cache the result. - var fn = !/\W/.test(str) ? - cache[str] = cache[str] : - - // Generate a reusable function that will serve as a template - // generator (and which will be cached). - new Function("obj", - "var p=[],print=function(){p.push.apply(p,arguments);};" + - - // Introduce the data as local variables using with(){} - "with(obj){p.push('" + - - // Convert the template into pure JavaScript - str - .replace(/[\r\t\n]/g, " ") - .split("<%").join("\t") - .replace(/((^|%>)[^\t]*)'/g, "$1\r") - .replace(/\t=(.*?)%>/g, "',$1,'") - .split("\t").join("');") - .split("%>").join("p.push('") - .split("\r").join("\\'") + - "');}return p.join('');" - ); - - // Provide some basic currying to the user - return data ? fn( data ) : fn; - } - return tmpl(templateString,valuesObject); - }, - /* jshint ignore:end */ - generateLabels = helpers.generateLabels = function(templateString,numberOfSteps,graphMin,stepValue){ - var labelsArray = new Array(numberOfSteps); - if (labelTemplateString){ - each(labelsArray,function(val,index){ - labelsArray[index] = template(templateString,{value: (graphMin + (stepValue*(index+1)))}); - }); - } - return labelsArray; - }, - //--Animation methods - //Easing functions adapted from Robert Penner's easing equations - //http://www.robertpenner.com/easing/ - easingEffects = helpers.easingEffects = { - linear: function (t) { - return t; - }, - easeInQuad: function (t) { - return t * t; - }, - easeOutQuad: function (t) { - return -1 * t * (t - 2); - }, - easeInOutQuad: function (t) { - if ((t /= 1 / 2) < 1) return 1 / 2 * t * t; - return -1 / 2 * ((--t) * (t - 2) - 1); - }, - easeInCubic: function (t) { - return t * t * t; - }, - easeOutCubic: function (t) { - return 1 * ((t = t / 1 - 1) * t * t + 1); - }, - easeInOutCubic: function (t) { - if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t; - return 1 / 2 * ((t -= 2) * t * t + 2); - }, - easeInQuart: function (t) { - return t * t * t * t; - }, - easeOutQuart: function (t) { - return -1 * ((t = t / 1 - 1) * t * t * t - 1); - }, - easeInOutQuart: function (t) { - if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t * t; - return -1 / 2 * ((t -= 2) * t * t * t - 2); - }, - easeInQuint: function (t) { - return 1 * (t /= 1) * t * t * t * t; - }, - easeOutQuint: function (t) { - return 1 * ((t = t / 1 - 1) * t * t * t * t + 1); - }, - easeInOutQuint: function (t) { - if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t * t * t; - return 1 / 2 * ((t -= 2) * t * t * t * t + 2); - }, - easeInSine: function (t) { - return -1 * Math.cos(t / 1 * (Math.PI / 2)) + 1; - }, - easeOutSine: function (t) { - return 1 * Math.sin(t / 1 * (Math.PI / 2)); - }, - easeInOutSine: function (t) { - return -1 / 2 * (Math.cos(Math.PI * t / 1) - 1); - }, - easeInExpo: function (t) { - return (t === 0) ? 1 : 1 * Math.pow(2, 10 * (t / 1 - 1)); - }, - easeOutExpo: function (t) { - return (t === 1) ? 1 : 1 * (-Math.pow(2, -10 * t / 1) + 1); - }, - easeInOutExpo: function (t) { - if (t === 0) return 0; - if (t === 1) return 1; - if ((t /= 1 / 2) < 1) return 1 / 2 * Math.pow(2, 10 * (t - 1)); - return 1 / 2 * (-Math.pow(2, -10 * --t) + 2); - }, - easeInCirc: function (t) { - if (t >= 1) return t; - return -1 * (Math.sqrt(1 - (t /= 1) * t) - 1); - }, - easeOutCirc: function (t) { - return 1 * Math.sqrt(1 - (t = t / 1 - 1) * t); - }, - easeInOutCirc: function (t) { - if ((t /= 1 / 2) < 1) return -1 / 2 * (Math.sqrt(1 - t * t) - 1); - return 1 / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1); - }, - easeInElastic: function (t) { - var s = 1.70158; - var p = 0; - var a = 1; - if (t === 0) return 0; - if ((t /= 1) == 1) return 1; - if (!p) p = 1 * 0.3; - if (a < Math.abs(1)) { - a = 1; - s = p / 4; - } else s = p / (2 * Math.PI) * Math.asin(1 / a); - return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p)); - }, - easeOutElastic: function (t) { - var s = 1.70158; - var p = 0; - var a = 1; - if (t === 0) return 0; - if ((t /= 1) == 1) return 1; - if (!p) p = 1 * 0.3; - if (a < Math.abs(1)) { - a = 1; - s = p / 4; - } else s = p / (2 * Math.PI) * Math.asin(1 / a); - return a * Math.pow(2, -10 * t) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) + 1; - }, - easeInOutElastic: function (t) { - var s = 1.70158; - var p = 0; - var a = 1; - if (t === 0) return 0; - if ((t /= 1 / 2) == 2) return 1; - if (!p) p = 1 * (0.3 * 1.5); - if (a < Math.abs(1)) { - a = 1; - s = p / 4; - } else s = p / (2 * Math.PI) * Math.asin(1 / a); - if (t < 1) return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p)); - return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) * 0.5 + 1; - }, - easeInBack: function (t) { - var s = 1.70158; - return 1 * (t /= 1) * t * ((s + 1) * t - s); - }, - easeOutBack: function (t) { - var s = 1.70158; - return 1 * ((t = t / 1 - 1) * t * ((s + 1) * t + s) + 1); - }, - easeInOutBack: function (t) { - var s = 1.70158; - if ((t /= 1 / 2) < 1) return 1 / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)); - return 1 / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2); - }, - easeInBounce: function (t) { - return 1 - easingEffects.easeOutBounce(1 - t); - }, - easeOutBounce: function (t) { - if ((t /= 1) < (1 / 2.75)) { - return 1 * (7.5625 * t * t); - } else if (t < (2 / 2.75)) { - return 1 * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75); - } else if (t < (2.5 / 2.75)) { - return 1 * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375); - } else { - return 1 * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375); - } - }, - easeInOutBounce: function (t) { - if (t < 1 / 2) return easingEffects.easeInBounce(t * 2) * 0.5; - return easingEffects.easeOutBounce(t * 2 - 1) * 0.5 + 1 * 0.5; - } - }, - //Request animation polyfill - http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/ - requestAnimFrame = helpers.requestAnimFrame = (function(){ - return window.requestAnimationFrame || - window.webkitRequestAnimationFrame || - window.mozRequestAnimationFrame || - window.oRequestAnimationFrame || - window.msRequestAnimationFrame || - function(callback) { - return window.setTimeout(callback, 1000 / 60); - }; - })(), - cancelAnimFrame = helpers.cancelAnimFrame = (function(){ - return window.cancelAnimationFrame || - window.webkitCancelAnimationFrame || - window.mozCancelAnimationFrame || - window.oCancelAnimationFrame || - window.msCancelAnimationFrame || - function(callback) { - return window.clearTimeout(callback, 1000 / 60); - }; - })(), - animationLoop = helpers.animationLoop = function(callback,totalSteps,easingString,onProgress,onComplete,chartInstance){ - - var currentStep = 0, - easingFunction = easingEffects[easingString] || easingEffects.linear; - - var animationFrame = function(){ - currentStep++; - var stepDecimal = currentStep/totalSteps; - var easeDecimal = easingFunction(stepDecimal); - - callback.call(chartInstance,easeDecimal,stepDecimal, currentStep); - onProgress.call(chartInstance,easeDecimal,stepDecimal); - if (currentStep < totalSteps){ - chartInstance.animationFrame = requestAnimFrame(animationFrame); - } else{ - onComplete.apply(chartInstance); - } - }; - requestAnimFrame(animationFrame); - }, - //-- DOM methods - getRelativePosition = helpers.getRelativePosition = function(evt){ - var mouseX, mouseY; - var e = evt.originalEvent || evt, - canvas = evt.currentTarget || evt.srcElement, - boundingRect = canvas.getBoundingClientRect(); - - if (e.touches){ - mouseX = e.touches[0].clientX - boundingRect.left; - mouseY = e.touches[0].clientY - boundingRect.top; - - } - else{ - mouseX = e.clientX - boundingRect.left; - mouseY = e.clientY - boundingRect.top; - } - - return { - x : mouseX, - y : mouseY - }; - - }, - addEvent = helpers.addEvent = function(node,eventType,method){ - if (node.addEventListener){ - node.addEventListener(eventType,method); - } else if (node.attachEvent){ - node.attachEvent("on"+eventType, method); - } else { - node["on"+eventType] = method; - } - }, - removeEvent = helpers.removeEvent = function(node, eventType, handler){ - if (node.removeEventListener){ - node.removeEventListener(eventType, handler, false); - } else if (node.detachEvent){ - node.detachEvent("on"+eventType,handler); - } else{ - node["on" + eventType] = noop; - } - }, - bindEvents = helpers.bindEvents = function(chartInstance, arrayOfEvents, handler){ - // Create the events object if it's not already present - if (!chartInstance.events) chartInstance.events = {}; - - each(arrayOfEvents,function(eventName){ - chartInstance.events[eventName] = function(){ - handler.apply(chartInstance, arguments); - }; - addEvent(chartInstance.chart.canvas,eventName,chartInstance.events[eventName]); - }); - }, - unbindEvents = helpers.unbindEvents = function (chartInstance, arrayOfEvents) { - each(arrayOfEvents, function(handler,eventName){ - removeEvent(chartInstance.chart.canvas, eventName, handler); - }); - }, - getMaximumWidth = helpers.getMaximumWidth = function(domNode){ - var container = domNode.parentNode; - // TODO = check cross browser stuff with this. - return container.clientWidth; - }, - getMaximumHeight = helpers.getMaximumHeight = function(domNode){ - var container = domNode.parentNode; - // TODO = check cross browser stuff with this. - return container.clientHeight; - }, - getMaximumSize = helpers.getMaximumSize = helpers.getMaximumWidth, // legacy support - retinaScale = helpers.retinaScale = function(chart){ - var ctx = chart.ctx, - width = chart.canvas.width, - height = chart.canvas.height; - - if (window.devicePixelRatio) { - ctx.canvas.style.width = width + "px"; - ctx.canvas.style.height = height + "px"; - ctx.canvas.height = height * window.devicePixelRatio; - ctx.canvas.width = width * window.devicePixelRatio; - ctx.scale(window.devicePixelRatio, window.devicePixelRatio); - } - }, - //-- Canvas methods - clear = helpers.clear = function(chart){ - chart.ctx.clearRect(0,0,chart.width,chart.height); - }, - fontString = helpers.fontString = function(pixelSize,fontStyle,fontFamily){ - return fontStyle + " " + pixelSize+"px " + fontFamily; - }, - longestText = helpers.longestText = function(ctx,font,arrayOfStrings){ - ctx.font = font; - var longest = 0; - each(arrayOfStrings,function(string){ - var textWidth = ctx.measureText(string).width; - longest = (textWidth > longest) ? textWidth : longest; - }); - return longest; - }, - drawRoundedRectangle = helpers.drawRoundedRectangle = function(ctx,x,y,width,height,radius){ - ctx.beginPath(); - ctx.moveTo(x + radius, y); - ctx.lineTo(x + width - radius, y); - ctx.quadraticCurveTo(x + width, y, x + width, y + radius); - ctx.lineTo(x + width, y + height - radius); - ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); - ctx.lineTo(x + radius, y + height); - ctx.quadraticCurveTo(x, y + height, x, y + height - radius); - ctx.lineTo(x, y + radius); - ctx.quadraticCurveTo(x, y, x + radius, y); - ctx.closePath(); - }; - - - //Store a reference to each instance - allowing us to globally resize chart instances on window resize. - //Destroy method on the chart will remove the instance of the chart from this reference. - Chart.instances = {}; - - Chart.Type = function(data,options,chart){ - this.options = options; - this.chart = chart; - this.id = uid(); - //Add the chart instance to the global namespace - Chart.instances[this.id] = this; - - // Initialize is always called when a chart type is created - // By default it is a no op, but it should be extended - if (options.responsive){ - this.resize(); - } - this.initialize.call(this,data); - }; - - //Core methods that'll be a part of every chart type - extend(Chart.Type.prototype,{ - initialize : function(){return this;}, - clear : function(){ - clear(this.chart); - return this; - }, - stop : function(){ - // Stops any current animation loop occuring - cancelAnimFrame(this.animationFrame); - return this; - }, - resize : function(callback){ - this.stop(); - var canvas = this.chart.canvas, - newWidth = getMaximumWidth(this.chart.canvas), - newHeight = this.options.maintainAspectRatio ? newWidth / this.chart.aspectRatio : getMaximumHeight(this.chart.canvas); - - canvas.width = this.chart.width = newWidth; - canvas.height = this.chart.height = newHeight; - - retinaScale(this.chart); - - if (typeof callback === "function"){ - callback.apply(this, Array.prototype.slice.call(arguments, 1)); - } - return this; - }, - reflow : noop, - render : function(reflow){ - if (reflow){ - this.reflow(); - } - if (this.options.animation && !reflow){ - helpers.animationLoop( - this.draw, - this.options.animationSteps, - this.options.animationEasing, - this.options.onAnimationProgress, - this.options.onAnimationComplete, - this - ); - } - else{ - this.draw(); - this.options.onAnimationComplete.call(this); - } - return this; - }, - generateLegend : function(){ - return template(this.options.legendTemplate,this); - }, - destroy : function(){ - this.clear(); - unbindEvents(this, this.events); - var canvas = this.chart.canvas; - - // Reset canvas height/width attributes starts a fresh with the canvas context - canvas.width = this.chart.width; - canvas.height = this.chart.height; - - // < IE9 doesn't support removeProperty - if (canvas.style.removeProperty) { - canvas.style.removeProperty('width'); - canvas.style.removeProperty('height'); - } else { - canvas.style.removeAttribute('width'); - canvas.style.removeAttribute('height'); - } - - delete Chart.instances[this.id]; - }, - showTooltip : function(ChartElements, forceRedraw){ - // Only redraw the chart if we've actually changed what we're hovering on. - if (typeof this.activeElements === 'undefined') this.activeElements = []; - - var isChanged = (function(Elements){ - var changed = false; - - if (Elements.length !== this.activeElements.length){ - changed = true; - return changed; - } - - each(Elements, function(element, index){ - if (element !== this.activeElements[index]){ - changed = true; - } - }, this); - return changed; - }).call(this, ChartElements); - - if (!isChanged && !forceRedraw){ - return; - } - else{ - this.activeElements = ChartElements; - } - this.draw(); - if(this.options.customTooltips){ - this.options.customTooltips(false); - } - if (ChartElements.length > 0){ - // If we have multiple datasets, show a MultiTooltip for all of the data points at that index - if (this.datasets && this.datasets.length > 1) { - var dataArray, - dataIndex; - - for (var i = this.datasets.length - 1; i >= 0; i--) { - dataArray = this.datasets[i].points || this.datasets[i].bars || this.datasets[i].segments; - dataIndex = indexOf(dataArray, ChartElements[0]); - if (dataIndex !== -1){ - break; - } - } - var tooltipLabels = [], - tooltipColors = [], - medianPosition = (function(index) { - - // Get all the points at that particular index - var Elements = [], - dataCollection, - xPositions = [], - yPositions = [], - xMax, - yMax, - xMin, - yMin; - helpers.each(this.datasets, function(dataset){ - dataCollection = dataset.points || dataset.bars || dataset.segments; - if (dataCollection[dataIndex] && dataCollection[dataIndex].hasValue()){ - Elements.push(dataCollection[dataIndex]); - } - }); - - helpers.each(Elements, function(element) { - xPositions.push(element.x); - yPositions.push(element.y); - - - //Include any colour information about the element - tooltipLabels.push(helpers.template(this.options.multiTooltipTemplate, element)); - tooltipColors.push({ - fill: element._saved.fillColor || element.fillColor, - stroke: element._saved.strokeColor || element.strokeColor - }); - - }, this); - - yMin = min(yPositions); - yMax = max(yPositions); - - xMin = min(xPositions); - xMax = max(xPositions); - - return { - x: (xMin > this.chart.width/2) ? xMin : xMax, - y: (yMin + yMax)/2 - }; - }).call(this, dataIndex); - - new Chart.MultiTooltip({ - x: medianPosition.x, - y: medianPosition.y, - xPadding: this.options.tooltipXPadding, - yPadding: this.options.tooltipYPadding, - xOffset: this.options.tooltipXOffset, - fillColor: this.options.tooltipFillColor, - textColor: this.options.tooltipFontColor, - fontFamily: this.options.tooltipFontFamily, - fontStyle: this.options.tooltipFontStyle, - fontSize: this.options.tooltipFontSize, - titleTextColor: this.options.tooltipTitleFontColor, - titleFontFamily: this.options.tooltipTitleFontFamily, - titleFontStyle: this.options.tooltipTitleFontStyle, - titleFontSize: this.options.tooltipTitleFontSize, - cornerRadius: this.options.tooltipCornerRadius, - labels: tooltipLabels, - legendColors: tooltipColors, - legendColorBackground : this.options.multiTooltipKeyBackground, - title: ChartElements[0].label, - chart: this.chart, - ctx: this.chart.ctx, - custom: this.options.customTooltips - }).draw(); - - } else { - each(ChartElements, function(Element) { - var tooltipPosition = Element.tooltipPosition(); - new Chart.Tooltip({ - x: Math.round(tooltipPosition.x), - y: Math.round(tooltipPosition.y), - xPadding: this.options.tooltipXPadding, - yPadding: this.options.tooltipYPadding, - fillColor: this.options.tooltipFillColor, - textColor: this.options.tooltipFontColor, - fontFamily: this.options.tooltipFontFamily, - fontStyle: this.options.tooltipFontStyle, - fontSize: this.options.tooltipFontSize, - caretHeight: this.options.tooltipCaretSize, - cornerRadius: this.options.tooltipCornerRadius, - text: template(this.options.tooltipTemplate, Element), - chart: this.chart, - custom: this.options.customTooltips - }).draw(); - }, this); - } - } - return this; - }, - toBase64Image : function(){ - return this.chart.canvas.toDataURL.apply(this.chart.canvas, arguments); - } - }); - - Chart.Type.extend = function(extensions){ - - var parent = this; - - var ChartType = function(){ - return parent.apply(this,arguments); - }; - - //Copy the prototype object of the this class - ChartType.prototype = clone(parent.prototype); - //Now overwrite some of the properties in the base class with the new extensions - extend(ChartType.prototype, extensions); - - ChartType.extend = Chart.Type.extend; - - if (extensions.name || parent.prototype.name){ - - var chartName = extensions.name || parent.prototype.name; - //Assign any potential default values of the new chart type - - //If none are defined, we'll use a clone of the chart type this is being extended from. - //I.e. if we extend a line chart, we'll use the defaults from the line chart if our new chart - //doesn't define some defaults of their own. - - var baseDefaults = (Chart.defaults[parent.prototype.name]) ? clone(Chart.defaults[parent.prototype.name]) : {}; - - Chart.defaults[chartName] = extend(baseDefaults,extensions.defaults); - - Chart.types[chartName] = ChartType; - - //Register this new chart type in the Chart prototype - Chart.prototype[chartName] = function(data,options){ - var config = merge(Chart.defaults.global, Chart.defaults[chartName], options || {}); - return new ChartType(data,config,this); - }; - } else{ - warn("Name not provided for this chart, so it hasn't been registered"); - } - return parent; - }; - - Chart.Element = function(configuration){ - extend(this,configuration); - this.initialize.apply(this,arguments); - this.save(); - }; - extend(Chart.Element.prototype,{ - initialize : function(){}, - restore : function(props){ - if (!props){ - extend(this,this._saved); - } else { - each(props,function(key){ - this[key] = this._saved[key]; - },this); - } - return this; - }, - save : function(){ - this._saved = clone(this); - delete this._saved._saved; - return this; - }, - update : function(newProps){ - each(newProps,function(value,key){ - this._saved[key] = this[key]; - this[key] = value; - },this); - return this; - }, - transition : function(props,ease){ - each(props,function(value,key){ - this[key] = ((value - this._saved[key]) * ease) + this._saved[key]; - },this); - return this; - }, - tooltipPosition : function(){ - return { - x : this.x, - y : this.y - }; - }, - hasValue: function(){ - return isNumber(this.value); - } - }); - - Chart.Element.extend = inherits; - - - Chart.Point = Chart.Element.extend({ - display: true, - inRange: function(chartX,chartY){ - var hitDetectionRange = this.hitDetectionRadius + this.radius; - return ((Math.pow(chartX-this.x, 2)+Math.pow(chartY-this.y, 2)) < Math.pow(hitDetectionRange,2)); - }, - draw : function(){ - if (this.display){ - var ctx = this.ctx; - ctx.beginPath(); - - ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2); - ctx.closePath(); - - ctx.strokeStyle = this.strokeColor; - ctx.lineWidth = this.strokeWidth; - - ctx.fillStyle = this.fillColor; - - ctx.fill(); - ctx.stroke(); - } - - - //Quick debug for bezier curve splining - //Highlights control points and the line between them. - //Handy for dev - stripped in the min version. - - // ctx.save(); - // ctx.fillStyle = "black"; - // ctx.strokeStyle = "black" - // ctx.beginPath(); - // ctx.arc(this.controlPoints.inner.x,this.controlPoints.inner.y, 2, 0, Math.PI*2); - // ctx.fill(); - - // ctx.beginPath(); - // ctx.arc(this.controlPoints.outer.x,this.controlPoints.outer.y, 2, 0, Math.PI*2); - // ctx.fill(); - - // ctx.moveTo(this.controlPoints.inner.x,this.controlPoints.inner.y); - // ctx.lineTo(this.x, this.y); - // ctx.lineTo(this.controlPoints.outer.x,this.controlPoints.outer.y); - // ctx.stroke(); - - // ctx.restore(); - - - - } - }); - - Chart.Arc = Chart.Element.extend({ - inRange : function(chartX,chartY){ - - var pointRelativePosition = helpers.getAngleFromPoint(this, { - x: chartX, - y: chartY - }); - - //Check if within the range of the open/close angle - var betweenAngles = (pointRelativePosition.angle >= this.startAngle && pointRelativePosition.angle <= this.endAngle), - withinRadius = (pointRelativePosition.distance >= this.innerRadius && pointRelativePosition.distance <= this.outerRadius); - - return (betweenAngles && withinRadius); - //Ensure within the outside of the arc centre, but inside arc outer - }, - tooltipPosition : function(){ - var centreAngle = this.startAngle + ((this.endAngle - this.startAngle) / 2), - rangeFromCentre = (this.outerRadius - this.innerRadius) / 2 + this.innerRadius; - return { - x : this.x + (Math.cos(centreAngle) * rangeFromCentre), - y : this.y + (Math.sin(centreAngle) * rangeFromCentre) - }; - }, - draw : function(animationPercent){ - - var easingDecimal = animationPercent || 1; - - var ctx = this.ctx; - - ctx.beginPath(); - - ctx.arc(this.x, this.y, this.outerRadius, this.startAngle, this.endAngle); - - ctx.arc(this.x, this.y, this.innerRadius, this.endAngle, this.startAngle, true); - - ctx.closePath(); - ctx.strokeStyle = this.strokeColor; - ctx.lineWidth = this.strokeWidth; - - ctx.fillStyle = this.fillColor; - - ctx.fill(); - ctx.lineJoin = 'bevel'; - - if (this.showStroke){ - ctx.stroke(); - } - } - }); - - Chart.Rectangle = Chart.Element.extend({ - draw : function(){ - var ctx = this.ctx, - halfWidth = this.width/2, - leftX = this.x - halfWidth, - rightX = this.x + halfWidth, - top = this.base - (this.base - this.y), - halfStroke = this.strokeWidth / 2; - - // Canvas doesn't allow us to stroke inside the width so we can - // adjust the sizes to fit if we're setting a stroke on the line - if (this.showStroke){ - leftX += halfStroke; - rightX -= halfStroke; - top += halfStroke; - } - - ctx.beginPath(); - - ctx.fillStyle = this.fillColor; - ctx.strokeStyle = this.strokeColor; - ctx.lineWidth = this.strokeWidth; - - // It'd be nice to keep this class totally generic to any rectangle - // and simply specify which border to miss out. - ctx.moveTo(leftX, this.base); - ctx.lineTo(leftX, top); - ctx.lineTo(rightX, top); - ctx.lineTo(rightX, this.base); - ctx.fill(); - if (this.showStroke){ - ctx.stroke(); - } - }, - height : function(){ - return this.base - this.y; - }, - inRange : function(chartX,chartY){ - return (chartX >= this.x - this.width/2 && chartX <= this.x + this.width/2) && (chartY >= this.y && chartY <= this.base); - } - }); - - Chart.Tooltip = Chart.Element.extend({ - draw : function(){ - - var ctx = this.chart.ctx; - - ctx.font = fontString(this.fontSize,this.fontStyle,this.fontFamily); - - this.xAlign = "center"; - this.yAlign = "above"; - - //Distance between the actual element.y position and the start of the tooltip caret - var caretPadding = this.caretPadding = 2; - - var tooltipWidth = ctx.measureText(this.text).width + 2*this.xPadding, - tooltipRectHeight = this.fontSize + 2*this.yPadding, - tooltipHeight = tooltipRectHeight + this.caretHeight + caretPadding; - - if (this.x + tooltipWidth/2 >this.chart.width){ - this.xAlign = "left"; - } else if (this.x - tooltipWidth/2 < 0){ - this.xAlign = "right"; - } - - if (this.y - tooltipHeight < 0){ - this.yAlign = "below"; - } - - - var tooltipX = this.x - tooltipWidth/2, - tooltipY = this.y - tooltipHeight; - - ctx.fillStyle = this.fillColor; - - // Custom Tooltips - if(this.custom){ - this.custom(this); - } - else{ - switch(this.yAlign) - { - case "above": - //Draw a caret above the x/y - ctx.beginPath(); - ctx.moveTo(this.x,this.y - caretPadding); - ctx.lineTo(this.x + this.caretHeight, this.y - (caretPadding + this.caretHeight)); - ctx.lineTo(this.x - this.caretHeight, this.y - (caretPadding + this.caretHeight)); - ctx.closePath(); - ctx.fill(); - break; - case "below": - tooltipY = this.y + caretPadding + this.caretHeight; - //Draw a caret below the x/y - ctx.beginPath(); - ctx.moveTo(this.x, this.y + caretPadding); - ctx.lineTo(this.x + this.caretHeight, this.y + caretPadding + this.caretHeight); - ctx.lineTo(this.x - this.caretHeight, this.y + caretPadding + this.caretHeight); - ctx.closePath(); - ctx.fill(); - break; - } - - switch(this.xAlign) - { - case "left": - tooltipX = this.x - tooltipWidth + (this.cornerRadius + this.caretHeight); - break; - case "right": - tooltipX = this.x - (this.cornerRadius + this.caretHeight); - break; - } - - drawRoundedRectangle(ctx,tooltipX,tooltipY,tooltipWidth,tooltipRectHeight,this.cornerRadius); - - ctx.fill(); - - ctx.fillStyle = this.textColor; - ctx.textAlign = "center"; - ctx.textBaseline = "middle"; - ctx.fillText(this.text, tooltipX + tooltipWidth/2, tooltipY + tooltipRectHeight/2); - } - } - }); - - Chart.MultiTooltip = Chart.Element.extend({ - initialize : function(){ - this.font = fontString(this.fontSize,this.fontStyle,this.fontFamily); - - this.titleFont = fontString(this.titleFontSize,this.titleFontStyle,this.titleFontFamily); - - this.height = (this.labels.length * this.fontSize) + ((this.labels.length-1) * (this.fontSize/2)) + (this.yPadding*2) + this.titleFontSize *1.5; - - this.ctx.font = this.titleFont; - - var titleWidth = this.ctx.measureText(this.title).width, - //Label has a legend square as well so account for this. - labelWidth = longestText(this.ctx,this.font,this.labels) + this.fontSize + 3, - longestTextWidth = max([labelWidth,titleWidth]); - - this.width = longestTextWidth + (this.xPadding*2); - - - var halfHeight = this.height/2; - - //Check to ensure the height will fit on the canvas - if (this.y - halfHeight < 0 ){ - this.y = halfHeight; - } else if (this.y + halfHeight > this.chart.height){ - this.y = this.chart.height - halfHeight; - } - - //Decide whether to align left or right based on position on canvas - if (this.x > this.chart.width/2){ - this.x -= this.xOffset + this.width; - } else { - this.x += this.xOffset; - } - - - }, - getLineHeight : function(index){ - var baseLineHeight = this.y - (this.height/2) + this.yPadding, - afterTitleIndex = index-1; - - //If the index is zero, we're getting the title - if (index === 0){ - return baseLineHeight + this.titleFontSize/2; - } else{ - return baseLineHeight + ((this.fontSize*1.5*afterTitleIndex) + this.fontSize/2) + this.titleFontSize * 1.5; - } - - }, - draw : function(){ - // Custom Tooltips - if(this.custom){ - this.custom(this); - } - else{ - drawRoundedRectangle(this.ctx,this.x,this.y - this.height/2,this.width,this.height,this.cornerRadius); - var ctx = this.ctx; - ctx.fillStyle = this.fillColor; - ctx.fill(); - ctx.closePath(); - - ctx.textAlign = "left"; - ctx.textBaseline = "middle"; - ctx.fillStyle = this.titleTextColor; - ctx.font = this.titleFont; - - ctx.fillText(this.title,this.x + this.xPadding, this.getLineHeight(0)); - - ctx.font = this.font; - helpers.each(this.labels,function(label,index){ - ctx.fillStyle = this.textColor; - ctx.fillText(label,this.x + this.xPadding + this.fontSize + 3, this.getLineHeight(index + 1)); - - //A bit gnarly, but clearing this rectangle breaks when using explorercanvas (clears whole canvas) - //ctx.clearRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); - //Instead we'll make a white filled block to put the legendColour palette over. - - ctx.fillStyle = this.legendColorBackground; - ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); - - ctx.fillStyle = this.legendColors[index].fill; - ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); - - - },this); - } - } - }); - - Chart.Scale = Chart.Element.extend({ - initialize : function(){ - this.fit(); - }, - buildYLabels : function(){ - this.yLabels = []; - - var stepDecimalPlaces = getDecimalPlaces(this.stepValue); - - for (var i=0; i<=this.steps; i++){ - this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)})); - } - this.yLabelWidth = (this.display && this.showLabels) ? longestText(this.ctx,this.font,this.yLabels) : 0; - }, - addXLabel : function(label){ - this.xLabels.push(label); - this.valuesCount++; - this.fit(); - }, - removeXLabel : function(){ - this.xLabels.shift(); - this.valuesCount--; - this.fit(); - }, - // Fitting loop to rotate x Labels and figure out what fits there, and also calculate how many Y steps to use - fit: function(){ - // First we need the width of the yLabels, assuming the xLabels aren't rotated - - // To do that we need the base line at the top and base of the chart, assuming there is no x label rotation - this.startPoint = (this.display) ? this.fontSize : 0; - this.endPoint = (this.display) ? this.height - (this.fontSize * 1.5) - 5 : this.height; // -5 to pad labels - - // Apply padding settings to the start and end point. - this.startPoint += this.padding; - this.endPoint -= this.padding; - - // Cache the starting height, so can determine if we need to recalculate the scale yAxis - var cachedHeight = this.endPoint - this.startPoint, - cachedYLabelWidth; - - // Build the current yLabels so we have an idea of what size they'll be to start - /* - * This sets what is returned from calculateScaleRange as static properties of this class: - * - this.steps; - this.stepValue; - this.min; - this.max; - * - */ - this.calculateYRange(cachedHeight); - - // With these properties set we can now build the array of yLabels - // and also the width of the largest yLabel - this.buildYLabels(); - - this.calculateXLabelRotation(); - - while((cachedHeight > this.endPoint - this.startPoint)){ - cachedHeight = this.endPoint - this.startPoint; - cachedYLabelWidth = this.yLabelWidth; - - this.calculateYRange(cachedHeight); - this.buildYLabels(); - - // Only go through the xLabel loop again if the yLabel width has changed - if (cachedYLabelWidth < this.yLabelWidth){ - this.calculateXLabelRotation(); - } - } - - }, - calculateXLabelRotation : function(){ - //Get the width of each grid by calculating the difference - //between x offsets between 0 and 1. - - this.ctx.font = this.font; - - var firstWidth = this.ctx.measureText(this.xLabels[0]).width, - lastWidth = this.ctx.measureText(this.xLabels[this.xLabels.length - 1]).width, - firstRotated, - lastRotated; - - - this.xScalePaddingRight = lastWidth/2 + 3; - this.xScalePaddingLeft = (firstWidth/2 > this.yLabelWidth + 10) ? firstWidth/2 : this.yLabelWidth + 10; - - this.xLabelRotation = 0; - if (this.display){ - var originalLabelWidth = longestText(this.ctx,this.font,this.xLabels), - cosRotation, - firstRotatedWidth; - this.xLabelWidth = originalLabelWidth; - //Allow 3 pixels x2 padding either side for label readability - var xGridWidth = Math.floor(this.calculateX(1) - this.calculateX(0)) - 6; - - //Max label rotate should be 90 - also act as a loop counter - while ((this.xLabelWidth > xGridWidth && this.xLabelRotation === 0) || (this.xLabelWidth > xGridWidth && this.xLabelRotation <= 90 && this.xLabelRotation > 0)){ - cosRotation = Math.cos(toRadians(this.xLabelRotation)); - - firstRotated = cosRotation * firstWidth; - lastRotated = cosRotation * lastWidth; - - // We're right aligning the text now. - if (firstRotated + this.fontSize / 2 > this.yLabelWidth + 8){ - this.xScalePaddingLeft = firstRotated + this.fontSize / 2; - } - this.xScalePaddingRight = this.fontSize/2; - - - this.xLabelRotation++; - this.xLabelWidth = cosRotation * originalLabelWidth; - - } - if (this.xLabelRotation > 0){ - this.endPoint -= Math.sin(toRadians(this.xLabelRotation))*originalLabelWidth + 3; - } - } - else{ - this.xLabelWidth = 0; - this.xScalePaddingRight = this.padding; - this.xScalePaddingLeft = this.padding; - } - - }, - // Needs to be overidden in each Chart type - // Otherwise we need to pass all the data into the scale class - calculateYRange: noop, - drawingArea: function(){ - return this.startPoint - this.endPoint; - }, - calculateY : function(value){ - var scalingFactor = this.drawingArea() / (this.min - this.max); - return this.endPoint - (scalingFactor * (value - this.min)); - }, - calculateX : function(index){ - var isRotated = (this.xLabelRotation > 0), - // innerWidth = (this.offsetGridLines) ? this.width - offsetLeft - this.padding : this.width - (offsetLeft + halfLabelWidth * 2) - this.padding, - innerWidth = this.width - (this.xScalePaddingLeft + this.xScalePaddingRight), - valueWidth = innerWidth/Math.max((this.valuesCount - ((this.offsetGridLines) ? 0 : 1)), 1), - valueOffset = (valueWidth * index) + this.xScalePaddingLeft; - - if (this.offsetGridLines){ - valueOffset += (valueWidth/2); - } - - return Math.round(valueOffset); - }, - update : function(newProps){ - helpers.extend(this, newProps); - this.fit(); - }, - draw : function(){ - var ctx = this.ctx, - yLabelGap = (this.endPoint - this.startPoint) / this.steps, - xStart = Math.round(this.xScalePaddingLeft); - if (this.display){ - ctx.fillStyle = this.textColor; - ctx.font = this.font; - each(this.yLabels,function(labelString,index){ - var yLabelCenter = this.endPoint - (yLabelGap * index), - linePositionY = Math.round(yLabelCenter), - drawHorizontalLine = this.showHorizontalLines; - - ctx.textAlign = "right"; - ctx.textBaseline = "middle"; - if (this.showLabels){ - ctx.fillText(labelString,xStart - 10,yLabelCenter); - } - - // This is X axis, so draw it - if (index === 0 && !drawHorizontalLine){ - drawHorizontalLine = true; - } - - if (drawHorizontalLine){ - ctx.beginPath(); - } - - if (index > 0){ - // This is a grid line in the centre, so drop that - ctx.lineWidth = this.gridLineWidth; - ctx.strokeStyle = this.gridLineColor; - } else { - // This is the first line on the scale - ctx.lineWidth = this.lineWidth; - ctx.strokeStyle = this.lineColor; - } - - linePositionY += helpers.aliasPixel(ctx.lineWidth); - - if(drawHorizontalLine){ - ctx.moveTo(xStart, linePositionY); - ctx.lineTo(this.width, linePositionY); - ctx.stroke(); - ctx.closePath(); - } - - ctx.lineWidth = this.lineWidth; - ctx.strokeStyle = this.lineColor; - ctx.beginPath(); - ctx.moveTo(xStart - 5, linePositionY); - ctx.lineTo(xStart, linePositionY); - ctx.stroke(); - ctx.closePath(); - - },this); - - each(this.xLabels,function(label,index){ - var xPos = this.calculateX(index) + aliasPixel(this.lineWidth), - // Check to see if line/bar here and decide where to place the line - linePos = this.calculateX(index - (this.offsetGridLines ? 0.5 : 0)) + aliasPixel(this.lineWidth), - isRotated = (this.xLabelRotation > 0), - drawVerticalLine = this.showVerticalLines; - - // This is Y axis, so draw it - if (index === 0 && !drawVerticalLine){ - drawVerticalLine = true; - } - - if (drawVerticalLine){ - ctx.beginPath(); - } - - if (index > 0){ - // This is a grid line in the centre, so drop that - ctx.lineWidth = this.gridLineWidth; - ctx.strokeStyle = this.gridLineColor; - } else { - // This is the first line on the scale - ctx.lineWidth = this.lineWidth; - ctx.strokeStyle = this.lineColor; - } - - if (drawVerticalLine){ - ctx.moveTo(linePos,this.endPoint); - ctx.lineTo(linePos,this.startPoint - 3); - ctx.stroke(); - ctx.closePath(); - } - - - ctx.lineWidth = this.lineWidth; - ctx.strokeStyle = this.lineColor; - - - // Small lines at the bottom of the base grid line - ctx.beginPath(); - ctx.moveTo(linePos,this.endPoint); - ctx.lineTo(linePos,this.endPoint + 5); - ctx.stroke(); - ctx.closePath(); - - ctx.save(); - ctx.translate(xPos,(isRotated) ? this.endPoint + 12 : this.endPoint + 8); - ctx.rotate(toRadians(this.xLabelRotation)*-1); - ctx.font = this.font; - ctx.textAlign = (isRotated) ? "right" : "center"; - ctx.textBaseline = (isRotated) ? "middle" : "top"; - ctx.fillText(label, 0, 0); - ctx.restore(); - },this); - - } - } - - }); - - Chart.RadialScale = Chart.Element.extend({ - initialize: function(){ - this.size = min([this.height, this.width]); - this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2); - }, - calculateCenterOffset: function(value){ - // Take into account half font size + the yPadding of the top value - var scalingFactor = this.drawingArea / (this.max - this.min); - - return (value - this.min) * scalingFactor; - }, - update : function(){ - if (!this.lineArc){ - this.setScaleSize(); - } else { - this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2); - } - this.buildYLabels(); - }, - buildYLabels: function(){ - this.yLabels = []; - - var stepDecimalPlaces = getDecimalPlaces(this.stepValue); - - for (var i=0; i<=this.steps; i++){ - this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)})); - } - }, - getCircumference : function(){ - return ((Math.PI*2) / this.valuesCount); - }, - setScaleSize: function(){ - /* - * Right, this is really confusing and there is a lot of maths going on here - * The gist of the problem is here: https://gist.github.com/nnnick/696cc9c55f4b0beb8fe9 - * - * Reaction: https://dl.dropboxusercontent.com/u/34601363/toomuchscience.gif - * - * Solution: - * - * We assume the radius of the polygon is half the size of the canvas at first - * at each index we check if the text overlaps. - * - * Where it does, we store that angle and that index. - * - * After finding the largest index and angle we calculate how much we need to remove - * from the shape radius to move the point inwards by that x. - * - * We average the left and right distances to get the maximum shape radius that can fit in the box - * along with labels. - * - * Once we have that, we can find the centre point for the chart, by taking the x text protrusion - * on each side, removing that from the size, halving it and adding the left x protrusion width. - * - * This will mean we have a shape fitted to the canvas, as large as it can be with the labels - * and position it in the most space efficient manner - * - * https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif - */ - - - // Get maximum radius of the polygon. Either half the height (minus the text width) or half the width. - // Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points - var largestPossibleRadius = min([(this.height/2 - this.pointLabelFontSize - 5), this.width/2]), - pointPosition, - i, - textWidth, - halfTextWidth, - furthestRight = this.width, - furthestRightIndex, - furthestRightAngle, - furthestLeft = 0, - furthestLeftIndex, - furthestLeftAngle, - xProtrusionLeft, - xProtrusionRight, - radiusReductionRight, - radiusReductionLeft, - maxWidthRadius; - this.ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily); - for (i=0;i furthestRight) { - furthestRight = pointPosition.x + halfTextWidth; - furthestRightIndex = i; - } - if (pointPosition.x - halfTextWidth < furthestLeft) { - furthestLeft = pointPosition.x - halfTextWidth; - furthestLeftIndex = i; - } - } - else if (i < this.valuesCount/2) { - // Less than half the values means we'll left align the text - if (pointPosition.x + textWidth > furthestRight) { - furthestRight = pointPosition.x + textWidth; - furthestRightIndex = i; - } - } - else if (i > this.valuesCount/2){ - // More than half the values means we'll right align the text - if (pointPosition.x - textWidth < furthestLeft) { - furthestLeft = pointPosition.x - textWidth; - furthestLeftIndex = i; - } - } - } - - xProtrusionLeft = furthestLeft; - - xProtrusionRight = Math.ceil(furthestRight - this.width); - - furthestRightAngle = this.getIndexAngle(furthestRightIndex); - - furthestLeftAngle = this.getIndexAngle(furthestLeftIndex); - - radiusReductionRight = xProtrusionRight / Math.sin(furthestRightAngle + Math.PI/2); - - radiusReductionLeft = xProtrusionLeft / Math.sin(furthestLeftAngle + Math.PI/2); - - // Ensure we actually need to reduce the size of the chart - radiusReductionRight = (isNumber(radiusReductionRight)) ? radiusReductionRight : 0; - radiusReductionLeft = (isNumber(radiusReductionLeft)) ? radiusReductionLeft : 0; - - this.drawingArea = largestPossibleRadius - (radiusReductionLeft + radiusReductionRight)/2; - - //this.drawingArea = min([maxWidthRadius, (this.height - (2 * (this.pointLabelFontSize + 5)))/2]) - this.setCenterPoint(radiusReductionLeft, radiusReductionRight); - - }, - setCenterPoint: function(leftMovement, rightMovement){ - - var maxRight = this.width - rightMovement - this.drawingArea, - maxLeft = leftMovement + this.drawingArea; - - this.xCenter = (maxLeft + maxRight)/2; - // Always vertically in the centre as the text height doesn't change - this.yCenter = (this.height/2); - }, - - getIndexAngle : function(index){ - var angleMultiplier = (Math.PI * 2) / this.valuesCount; - // Start from the top instead of right, so remove a quarter of the circle - - return index * angleMultiplier - (Math.PI/2); - }, - getPointPosition : function(index, distanceFromCenter){ - var thisAngle = this.getIndexAngle(index); - return { - x : (Math.cos(thisAngle) * distanceFromCenter) + this.xCenter, - y : (Math.sin(thisAngle) * distanceFromCenter) + this.yCenter - }; - }, - draw: function(){ - if (this.display){ - var ctx = this.ctx; - each(this.yLabels, function(label, index){ - // Don't draw a centre value - if (index > 0){ - var yCenterOffset = index * (this.drawingArea/this.steps), - yHeight = this.yCenter - yCenterOffset, - pointPosition; - - // Draw circular lines around the scale - if (this.lineWidth > 0){ - ctx.strokeStyle = this.lineColor; - ctx.lineWidth = this.lineWidth; - - if(this.lineArc){ - ctx.beginPath(); - ctx.arc(this.xCenter, this.yCenter, yCenterOffset, 0, Math.PI*2); - ctx.closePath(); - ctx.stroke(); - } else{ - ctx.beginPath(); - for (var i=0;i= 0; i--) { - if (this.angleLineWidth > 0){ - var outerPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max)); - ctx.beginPath(); - ctx.moveTo(this.xCenter, this.yCenter); - ctx.lineTo(outerPosition.x, outerPosition.y); - ctx.stroke(); - ctx.closePath(); - } - // Extra 3px out for some label spacing - var pointLabelPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max) + 5); - ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily); - ctx.fillStyle = this.pointLabelFontColor; - - var labelsCount = this.labels.length, - halfLabelsCount = this.labels.length/2, - quarterLabelsCount = halfLabelsCount/2, - upperHalf = (i < quarterLabelsCount || i > labelsCount - quarterLabelsCount), - exactQuarter = (i === quarterLabelsCount || i === labelsCount - quarterLabelsCount); - if (i === 0){ - ctx.textAlign = 'center'; - } else if(i === halfLabelsCount){ - ctx.textAlign = 'center'; - } else if (i < halfLabelsCount){ - ctx.textAlign = 'left'; - } else { - ctx.textAlign = 'right'; - } - - // Set the correct text baseline based on outer positioning - if (exactQuarter){ - ctx.textBaseline = 'middle'; - } else if (upperHalf){ - ctx.textBaseline = 'bottom'; - } else { - ctx.textBaseline = 'top'; - } - - ctx.fillText(this.labels[i], pointLabelPosition.x, pointLabelPosition.y); - } - } - } - } - }); - - // Attach global event to resize each chart instance when the browser resizes - helpers.addEvent(window, "resize", (function(){ - // Basic debounce of resize function so it doesn't hurt performance when resizing browser. - var timeout; - return function(){ - clearTimeout(timeout); - timeout = setTimeout(function(){ - each(Chart.instances,function(instance){ - // If the responsive flag is set in the chart instance config - // Cascade the resize event down to the chart. - if (instance.options.responsive){ - instance.resize(instance.render, true); - } - }); - }, 50); - }; - })()); - - - if (amd) { - define(function(){ - return Chart; - }); - } else if (typeof module === 'object' && module.exports) { - module.exports = Chart; - } - - root.Chart = Chart; - - Chart.noConflict = function(){ - root.Chart = previous; - return Chart; - }; - -}).call(this); - -(function(){ - "use strict"; - - var root = this, - Chart = root.Chart, - helpers = Chart.helpers; - - - var defaultConfig = { - //Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value - scaleBeginAtZero : true, - - //Boolean - Whether grid lines are shown across the chart - scaleShowGridLines : true, - - //String - Colour of the grid lines - scaleGridLineColor : "rgba(0,0,0,.05)", - - //Number - Width of the grid lines - scaleGridLineWidth : 1, - - //Boolean - Whether to show horizontal lines (except X axis) - scaleShowHorizontalLines: true, - - //Boolean - Whether to show vertical lines (except Y axis) - scaleShowVerticalLines: true, - - //Boolean - If there is a stroke on each bar - barShowStroke : true, - - //Number - Pixel width of the bar stroke - barStrokeWidth : 2, - - //Number - Spacing between each of the X value sets - barValueSpacing : 5, - - //Number - Spacing between data sets within X values - barDatasetSpacing : 1, - - //String - A legend template - legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
" - - }; - - - Chart.Type.extend({ - name: "Bar", - defaults : defaultConfig, - initialize: function(data){ - - //Expose options as a scope variable here so we can access it in the ScaleClass - var options = this.options; - - this.ScaleClass = Chart.Scale.extend({ - offsetGridLines : true, - calculateBarX : function(datasetCount, datasetIndex, barIndex){ - //Reusable method for calculating the xPosition of a given bar based on datasetIndex & width of the bar - var xWidth = this.calculateBaseWidth(), - xAbsolute = this.calculateX(barIndex) - (xWidth/2), - barWidth = this.calculateBarWidth(datasetCount); - - return xAbsolute + (barWidth * datasetIndex) + (datasetIndex * options.barDatasetSpacing) + barWidth/2; - }, - calculateBaseWidth : function(){ - return (this.calculateX(1) - this.calculateX(0)) - (2*options.barValueSpacing); - }, - calculateBarWidth : function(datasetCount){ - //The padding between datasets is to the right of each bar, providing that there are more than 1 dataset - var baseWidth = this.calculateBaseWidth() - ((datasetCount - 1) * options.barDatasetSpacing); - - return (baseWidth / datasetCount); - } - }); - - this.datasets = []; - - //Set up tooltip events on the chart - if (this.options.showTooltips){ - helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ - var activeBars = (evt.type !== 'mouseout') ? this.getBarsAtEvent(evt) : []; - - this.eachBars(function(bar){ - bar.restore(['fillColor', 'strokeColor']); - }); - helpers.each(activeBars, function(activeBar){ - activeBar.fillColor = activeBar.highlightFill; - activeBar.strokeColor = activeBar.highlightStroke; - }); - this.showTooltip(activeBars); - }); - } - - //Declare the extension of the default point, to cater for the options passed in to the constructor - this.BarClass = Chart.Rectangle.extend({ - strokeWidth : this.options.barStrokeWidth, - showStroke : this.options.barShowStroke, - ctx : this.chart.ctx - }); - - //Iterate through each of the datasets, and build this into a property of the chart - helpers.each(data.datasets,function(dataset,datasetIndex){ - - var datasetObject = { - label : dataset.label || null, - fillColor : dataset.fillColor, - strokeColor : dataset.strokeColor, - bars : [] - }; - - this.datasets.push(datasetObject); - - helpers.each(dataset.data,function(dataPoint,index){ - //Add a new point for each piece of data, passing any required data to draw. - datasetObject.bars.push(new this.BarClass({ - value : dataPoint, - label : data.labels[index], - datasetLabel: dataset.label, - strokeColor : dataset.strokeColor, - fillColor : dataset.fillColor, - highlightFill : dataset.highlightFill || dataset.fillColor, - highlightStroke : dataset.highlightStroke || dataset.strokeColor - })); - },this); - - },this); - - this.buildScale(data.labels); - - this.BarClass.prototype.base = this.scale.endPoint; - - this.eachBars(function(bar, index, datasetIndex){ - helpers.extend(bar, { - width : this.scale.calculateBarWidth(this.datasets.length), - x: this.scale.calculateBarX(this.datasets.length, datasetIndex, index), - y: this.scale.endPoint - }); - bar.save(); - }, this); - - this.render(); - }, - update : function(){ - this.scale.update(); - // Reset any highlight colours before updating. - helpers.each(this.activeElements, function(activeElement){ - activeElement.restore(['fillColor', 'strokeColor']); - }); - - this.eachBars(function(bar){ - bar.save(); - }); - this.render(); - }, - eachBars : function(callback){ - helpers.each(this.datasets,function(dataset, datasetIndex){ - helpers.each(dataset.bars, callback, this, datasetIndex); - },this); - }, - getBarsAtEvent : function(e){ - var barsArray = [], - eventPosition = helpers.getRelativePosition(e), - datasetIterator = function(dataset){ - barsArray.push(dataset.bars[barIndex]); - }, - barIndex; - - for (var datasetIndex = 0; datasetIndex < this.datasets.length; datasetIndex++) { - for (barIndex = 0; barIndex < this.datasets[datasetIndex].bars.length; barIndex++) { - if (this.datasets[datasetIndex].bars[barIndex].inRange(eventPosition.x,eventPosition.y)){ - helpers.each(this.datasets, datasetIterator); - return barsArray; - } - } - } - - return barsArray; - }, - buildScale : function(labels){ - var self = this; - - var dataTotal = function(){ - var values = []; - self.eachBars(function(bar){ - values.push(bar.value); - }); - return values; - }; - - var scaleOptions = { - templateString : this.options.scaleLabel, - height : this.chart.height, - width : this.chart.width, - ctx : this.chart.ctx, - textColor : this.options.scaleFontColor, - fontSize : this.options.scaleFontSize, - fontStyle : this.options.scaleFontStyle, - fontFamily : this.options.scaleFontFamily, - valuesCount : labels.length, - beginAtZero : this.options.scaleBeginAtZero, - integersOnly : this.options.scaleIntegersOnly, - calculateYRange: function(currentHeight){ - var updatedRanges = helpers.calculateScaleRange( - dataTotal(), - currentHeight, - this.fontSize, - this.beginAtZero, - this.integersOnly - ); - helpers.extend(this, updatedRanges); - }, - xLabels : labels, - font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily), - lineWidth : this.options.scaleLineWidth, - lineColor : this.options.scaleLineColor, - showHorizontalLines : this.options.scaleShowHorizontalLines, - showVerticalLines : this.options.scaleShowVerticalLines, - gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, - gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", - padding : (this.options.showScale) ? 0 : (this.options.barShowStroke) ? this.options.barStrokeWidth : 0, - showLabels : this.options.scaleShowLabels, - display : this.options.showScale - }; - - if (this.options.scaleOverride){ - helpers.extend(scaleOptions, { - calculateYRange: helpers.noop, - steps: this.options.scaleSteps, - stepValue: this.options.scaleStepWidth, - min: this.options.scaleStartValue, - max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) - }); - } - - this.scale = new this.ScaleClass(scaleOptions); - }, - addData : function(valuesArray,label){ - //Map the values array for each of the datasets - helpers.each(valuesArray,function(value,datasetIndex){ - //Add a new point for each piece of data, passing any required data to draw. - this.datasets[datasetIndex].bars.push(new this.BarClass({ - value : value, - label : label, - x: this.scale.calculateBarX(this.datasets.length, datasetIndex, this.scale.valuesCount+1), - y: this.scale.endPoint, - width : this.scale.calculateBarWidth(this.datasets.length), - base : this.scale.endPoint, - strokeColor : this.datasets[datasetIndex].strokeColor, - fillColor : this.datasets[datasetIndex].fillColor - })); - },this); - - this.scale.addXLabel(label); - //Then re-render the chart. - this.update(); - }, - removeData : function(){ - this.scale.removeXLabel(); - //Then re-render the chart. - helpers.each(this.datasets,function(dataset){ - dataset.bars.shift(); - },this); - this.update(); - }, - reflow : function(){ - helpers.extend(this.BarClass.prototype,{ - y: this.scale.endPoint, - base : this.scale.endPoint - }); - var newScaleProps = helpers.extend({ - height : this.chart.height, - width : this.chart.width - }); - this.scale.update(newScaleProps); - }, - draw : function(ease){ - var easingDecimal = ease || 1; - this.clear(); - - var ctx = this.chart.ctx; - - this.scale.draw(easingDecimal); - - //Draw all the bars for each dataset - helpers.each(this.datasets,function(dataset,datasetIndex){ - helpers.each(dataset.bars,function(bar,index){ - if (bar.hasValue()){ - bar.base = this.scale.endPoint; - //Transition then draw - bar.transition({ - x : this.scale.calculateBarX(this.datasets.length, datasetIndex, index), - y : this.scale.calculateY(bar.value), - width : this.scale.calculateBarWidth(this.datasets.length) - }, easingDecimal).draw(); - } - },this); - - },this); - } - }); - - -}).call(this); - -(function(){ - "use strict"; - - var root = this, - Chart = root.Chart, - //Cache a local reference to Chart.helpers - helpers = Chart.helpers; - - var defaultConfig = { - //Boolean - Whether we should show a stroke on each segment - segmentShowStroke : true, - - //String - The colour of each segment stroke - segmentStrokeColor : "#fff", - - //Number - The width of each segment stroke - segmentStrokeWidth : 2, - - //The percentage of the chart that we cut out of the middle. - percentageInnerCutout : 50, - - //Number - Amount of animation steps - animationSteps : 100, - - //String - Animation easing effect - animationEasing : "easeOutBounce", - - //Boolean - Whether we animate the rotation of the Doughnut - animateRotate : true, - - //Boolean - Whether we animate scaling the Doughnut from the centre - animateScale : false, - - //String - A legend template - legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(segments[i].label){%><%=segments[i].label%><%}%>
  • <%}%>
" - - }; - - - Chart.Type.extend({ - //Passing in a name registers this chart in the Chart namespace - name: "Doughnut", - //Providing a defaults will also register the deafults in the chart namespace - defaults : defaultConfig, - //Initialize is fired when the chart is initialized - Data is passed in as a parameter - //Config is automatically merged by the core of Chart.js, and is available at this.options - initialize: function(data){ - - //Declare segments as a static property to prevent inheriting across the Chart type prototype - this.segments = []; - this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2; - - this.SegmentArc = Chart.Arc.extend({ - ctx : this.chart.ctx, - x : this.chart.width/2, - y : this.chart.height/2 - }); - - //Set up tooltip events on the chart - if (this.options.showTooltips){ - helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ - var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : []; - - helpers.each(this.segments,function(segment){ - segment.restore(["fillColor"]); - }); - helpers.each(activeSegments,function(activeSegment){ - activeSegment.fillColor = activeSegment.highlightColor; - }); - this.showTooltip(activeSegments); - }); - } - this.calculateTotal(data); - - helpers.each(data,function(datapoint, index){ - this.addData(datapoint, index, true); - },this); - - this.render(); - }, - getSegmentsAtEvent : function(e){ - var segmentsArray = []; - - var location = helpers.getRelativePosition(e); - - helpers.each(this.segments,function(segment){ - if (segment.inRange(location.x,location.y)) segmentsArray.push(segment); - },this); - return segmentsArray; - }, - addData : function(segment, atIndex, silent){ - var index = atIndex || this.segments.length; - this.segments.splice(index, 0, new this.SegmentArc({ - value : segment.value, - outerRadius : (this.options.animateScale) ? 0 : this.outerRadius, - innerRadius : (this.options.animateScale) ? 0 : (this.outerRadius/100) * this.options.percentageInnerCutout, - fillColor : segment.color, - highlightColor : segment.highlight || segment.color, - showStroke : this.options.segmentShowStroke, - strokeWidth : this.options.segmentStrokeWidth, - strokeColor : this.options.segmentStrokeColor, - startAngle : Math.PI * 1.5, - circumference : (this.options.animateRotate) ? 0 : this.calculateCircumference(segment.value), - label : segment.label - })); - if (!silent){ - this.reflow(); - this.update(); - } - }, - calculateCircumference : function(value){ - return (Math.PI*2)*(Math.abs(value) / this.total); - }, - calculateTotal : function(data){ - this.total = 0; - helpers.each(data,function(segment){ - this.total += Math.abs(segment.value); - },this); - }, - update : function(){ - this.calculateTotal(this.segments); - - // Reset any highlight colours before updating. - helpers.each(this.activeElements, function(activeElement){ - activeElement.restore(['fillColor']); - }); - - helpers.each(this.segments,function(segment){ - segment.save(); - }); - this.render(); - }, - - removeData: function(atIndex){ - var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1; - this.segments.splice(indexToDelete, 1); - this.reflow(); - this.update(); - }, - - reflow : function(){ - helpers.extend(this.SegmentArc.prototype,{ - x : this.chart.width/2, - y : this.chart.height/2 - }); - this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2; - helpers.each(this.segments, function(segment){ - segment.update({ - outerRadius : this.outerRadius, - innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout - }); - }, this); - }, - draw : function(easeDecimal){ - var animDecimal = (easeDecimal) ? easeDecimal : 1; - this.clear(); - helpers.each(this.segments,function(segment,index){ - segment.transition({ - circumference : this.calculateCircumference(segment.value), - outerRadius : this.outerRadius, - innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout - },animDecimal); - - segment.endAngle = segment.startAngle + segment.circumference; - - segment.draw(); - if (index === 0){ - segment.startAngle = Math.PI * 1.5; - } - //Check to see if it's the last segment, if not get the next and update the start angle - if (index < this.segments.length-1){ - this.segments[index+1].startAngle = segment.endAngle; - } - },this); - - } - }); - - Chart.types.Doughnut.extend({ - name : "Pie", - defaults : helpers.merge(defaultConfig,{percentageInnerCutout : 0}) - }); - -}).call(this); -(function(){ - "use strict"; - - var root = this, - Chart = root.Chart, - helpers = Chart.helpers; - - var defaultConfig = { - - ///Boolean - Whether grid lines are shown across the chart - scaleShowGridLines : true, - - //String - Colour of the grid lines - scaleGridLineColor : "rgba(0,0,0,.05)", - - //Number - Width of the grid lines - scaleGridLineWidth : 1, - - //Boolean - Whether to show horizontal lines (except X axis) - scaleShowHorizontalLines: true, - - //Boolean - Whether to show vertical lines (except Y axis) - scaleShowVerticalLines: true, - - //Boolean - Whether the line is curved between points - bezierCurve : true, - - //Number - Tension of the bezier curve between points - bezierCurveTension : 0.4, - - //Boolean - Whether to show a dot for each point - pointDot : true, - - //Number - Radius of each point dot in pixels - pointDotRadius : 4, - - //Number - Pixel width of point dot stroke - pointDotStrokeWidth : 1, - - //Number - amount extra to add to the radius to cater for hit detection outside the drawn point - pointHitDetectionRadius : 20, - - //Boolean - Whether to show a stroke for datasets - datasetStroke : true, - - //Number - Pixel width of dataset stroke - datasetStrokeWidth : 2, - - //Boolean - Whether to fill the dataset with a colour - datasetFill : true, - - //String - A legend template - legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
" - - }; - - - Chart.Type.extend({ - name: "Line", - defaults : defaultConfig, - initialize: function(data){ - //Declare the extension of the default point, to cater for the options passed in to the constructor - this.PointClass = Chart.Point.extend({ - strokeWidth : this.options.pointDotStrokeWidth, - radius : this.options.pointDotRadius, - display: this.options.pointDot, - hitDetectionRadius : this.options.pointHitDetectionRadius, - ctx : this.chart.ctx, - inRange : function(mouseX){ - return (Math.pow(mouseX-this.x, 2) < Math.pow(this.radius + this.hitDetectionRadius,2)); - } - }); - - this.datasets = []; - - //Set up tooltip events on the chart - if (this.options.showTooltips){ - helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ - var activePoints = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : []; - this.eachPoints(function(point){ - point.restore(['fillColor', 'strokeColor']); - }); - helpers.each(activePoints, function(activePoint){ - activePoint.fillColor = activePoint.highlightFill; - activePoint.strokeColor = activePoint.highlightStroke; - }); - this.showTooltip(activePoints); - }); - } - - //Iterate through each of the datasets, and build this into a property of the chart - helpers.each(data.datasets,function(dataset){ - - var datasetObject = { - label : dataset.label || null, - fillColor : dataset.fillColor, - strokeColor : dataset.strokeColor, - pointColor : dataset.pointColor, - pointStrokeColor : dataset.pointStrokeColor, - points : [] - }; - - this.datasets.push(datasetObject); - - - helpers.each(dataset.data,function(dataPoint,index){ - //Add a new point for each piece of data, passing any required data to draw. - datasetObject.points.push(new this.PointClass({ - value : dataPoint, - label : data.labels[index], - datasetLabel: dataset.label, - strokeColor : dataset.pointStrokeColor, - fillColor : dataset.pointColor, - highlightFill : dataset.pointHighlightFill || dataset.pointColor, - highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor - })); - },this); - - this.buildScale(data.labels); - - - this.eachPoints(function(point, index){ - helpers.extend(point, { - x: this.scale.calculateX(index), - y: this.scale.endPoint - }); - point.save(); - }, this); - - },this); - - - this.render(); - }, - update : function(){ - this.scale.update(); - // Reset any highlight colours before updating. - helpers.each(this.activeElements, function(activeElement){ - activeElement.restore(['fillColor', 'strokeColor']); - }); - this.eachPoints(function(point){ - point.save(); - }); - this.render(); - }, - eachPoints : function(callback){ - helpers.each(this.datasets,function(dataset){ - helpers.each(dataset.points,callback,this); - },this); - }, - getPointsAtEvent : function(e){ - var pointsArray = [], - eventPosition = helpers.getRelativePosition(e); - helpers.each(this.datasets,function(dataset){ - helpers.each(dataset.points,function(point){ - if (point.inRange(eventPosition.x,eventPosition.y)) pointsArray.push(point); - }); - },this); - return pointsArray; - }, - buildScale : function(labels){ - var self = this; - - var dataTotal = function(){ - var values = []; - self.eachPoints(function(point){ - values.push(point.value); - }); - - return values; - }; - - var scaleOptions = { - templateString : this.options.scaleLabel, - height : this.chart.height, - width : this.chart.width, - ctx : this.chart.ctx, - textColor : this.options.scaleFontColor, - fontSize : this.options.scaleFontSize, - fontStyle : this.options.scaleFontStyle, - fontFamily : this.options.scaleFontFamily, - valuesCount : labels.length, - beginAtZero : this.options.scaleBeginAtZero, - integersOnly : this.options.scaleIntegersOnly, - calculateYRange : function(currentHeight){ - var updatedRanges = helpers.calculateScaleRange( - dataTotal(), - currentHeight, - this.fontSize, - this.beginAtZero, - this.integersOnly - ); - helpers.extend(this, updatedRanges); - }, - xLabels : labels, - font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily), - lineWidth : this.options.scaleLineWidth, - lineColor : this.options.scaleLineColor, - showHorizontalLines : this.options.scaleShowHorizontalLines, - showVerticalLines : this.options.scaleShowVerticalLines, - gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, - gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", - padding: (this.options.showScale) ? 0 : this.options.pointDotRadius + this.options.pointDotStrokeWidth, - showLabels : this.options.scaleShowLabels, - display : this.options.showScale - }; - - if (this.options.scaleOverride){ - helpers.extend(scaleOptions, { - calculateYRange: helpers.noop, - steps: this.options.scaleSteps, - stepValue: this.options.scaleStepWidth, - min: this.options.scaleStartValue, - max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) - }); - } - - - this.scale = new Chart.Scale(scaleOptions); - }, - addData : function(valuesArray,label){ - //Map the values array for each of the datasets - - helpers.each(valuesArray,function(value,datasetIndex){ - //Add a new point for each piece of data, passing any required data to draw. - this.datasets[datasetIndex].points.push(new this.PointClass({ - value : value, - label : label, - x: this.scale.calculateX(this.scale.valuesCount+1), - y: this.scale.endPoint, - strokeColor : this.datasets[datasetIndex].pointStrokeColor, - fillColor : this.datasets[datasetIndex].pointColor - })); - },this); - - this.scale.addXLabel(label); - //Then re-render the chart. - this.update(); - }, - removeData : function(){ - this.scale.removeXLabel(); - //Then re-render the chart. - helpers.each(this.datasets,function(dataset){ - dataset.points.shift(); - },this); - this.update(); - }, - reflow : function(){ - var newScaleProps = helpers.extend({ - height : this.chart.height, - width : this.chart.width - }); - this.scale.update(newScaleProps); - }, - draw : function(ease){ - var easingDecimal = ease || 1; - this.clear(); - - var ctx = this.chart.ctx; - - // Some helper methods for getting the next/prev points - var hasValue = function(item){ - return item.value !== null; - }, - nextPoint = function(point, collection, index){ - return helpers.findNextWhere(collection, hasValue, index) || point; - }, - previousPoint = function(point, collection, index){ - return helpers.findPreviousWhere(collection, hasValue, index) || point; - }; - - this.scale.draw(easingDecimal); - - - helpers.each(this.datasets,function(dataset){ - var pointsWithValues = helpers.where(dataset.points, hasValue); - - //Transition each point first so that the line and point drawing isn't out of sync - //We can use this extra loop to calculate the control points of this dataset also in this loop - - helpers.each(dataset.points, function(point, index){ - if (point.hasValue()){ - point.transition({ - y : this.scale.calculateY(point.value), - x : this.scale.calculateX(index) - }, easingDecimal); - } - },this); - - - // Control points need to be calculated in a seperate loop, because we need to know the current x/y of the point - // This would cause issues when there is no animation, because the y of the next point would be 0, so beziers would be skewed - if (this.options.bezierCurve){ - helpers.each(pointsWithValues, function(point, index){ - var tension = (index > 0 && index < pointsWithValues.length - 1) ? this.options.bezierCurveTension : 0; - point.controlPoints = helpers.splineCurve( - previousPoint(point, pointsWithValues, index), - point, - nextPoint(point, pointsWithValues, index), - tension - ); - - // Prevent the bezier going outside of the bounds of the graph - - // Cap puter bezier handles to the upper/lower scale bounds - if (point.controlPoints.outer.y > this.scale.endPoint){ - point.controlPoints.outer.y = this.scale.endPoint; - } - else if (point.controlPoints.outer.y < this.scale.startPoint){ - point.controlPoints.outer.y = this.scale.startPoint; - } - - // Cap inner bezier handles to the upper/lower scale bounds - if (point.controlPoints.inner.y > this.scale.endPoint){ - point.controlPoints.inner.y = this.scale.endPoint; - } - else if (point.controlPoints.inner.y < this.scale.startPoint){ - point.controlPoints.inner.y = this.scale.startPoint; - } - },this); - } - - - //Draw the line between all the points - ctx.lineWidth = this.options.datasetStrokeWidth; - ctx.strokeStyle = dataset.strokeColor; - ctx.beginPath(); - - helpers.each(pointsWithValues, function(point, index){ - if (index === 0){ - ctx.moveTo(point.x, point.y); - } - else{ - if(this.options.bezierCurve){ - var previous = previousPoint(point, pointsWithValues, index); - - ctx.bezierCurveTo( - previous.controlPoints.outer.x, - previous.controlPoints.outer.y, - point.controlPoints.inner.x, - point.controlPoints.inner.y, - point.x, - point.y - ); - } - else{ - ctx.lineTo(point.x,point.y); - } - } - }, this); - - ctx.stroke(); - - if (this.options.datasetFill && pointsWithValues.length > 0){ - //Round off the line by going to the base of the chart, back to the start, then fill. - ctx.lineTo(pointsWithValues[pointsWithValues.length - 1].x, this.scale.endPoint); - ctx.lineTo(pointsWithValues[0].x, this.scale.endPoint); - ctx.fillStyle = dataset.fillColor; - ctx.closePath(); - ctx.fill(); - } - - //Now draw the points over the line - //A little inefficient double looping, but better than the line - //lagging behind the point positions - helpers.each(pointsWithValues,function(point){ - point.draw(); - }); - },this); - } - }); - - -}).call(this); - -(function(){ - "use strict"; - - var root = this, - Chart = root.Chart, - //Cache a local reference to Chart.helpers - helpers = Chart.helpers; - - var defaultConfig = { - //Boolean - Show a backdrop to the scale label - scaleShowLabelBackdrop : true, - - //String - The colour of the label backdrop - scaleBackdropColor : "rgba(255,255,255,0.75)", - - // Boolean - Whether the scale should begin at zero - scaleBeginAtZero : true, - - //Number - The backdrop padding above & below the label in pixels - scaleBackdropPaddingY : 2, - - //Number - The backdrop padding to the side of the label in pixels - scaleBackdropPaddingX : 2, - - //Boolean - Show line for each value in the scale - scaleShowLine : true, - - //Boolean - Stroke a line around each segment in the chart - segmentShowStroke : true, - - //String - The colour of the stroke on each segement. - segmentStrokeColor : "#fff", - - //Number - The width of the stroke value in pixels - segmentStrokeWidth : 2, - - //Number - Amount of animation steps - animationSteps : 100, - - //String - Animation easing effect. - animationEasing : "easeOutBounce", - - //Boolean - Whether to animate the rotation of the chart - animateRotate : true, - - //Boolean - Whether to animate scaling the chart from the centre - animateScale : false, - - //String - A legend template - legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(segments[i].label){%><%=segments[i].label%><%}%>
  • <%}%>
" - }; - - - Chart.Type.extend({ - //Passing in a name registers this chart in the Chart namespace - name: "PolarArea", - //Providing a defaults will also register the deafults in the chart namespace - defaults : defaultConfig, - //Initialize is fired when the chart is initialized - Data is passed in as a parameter - //Config is automatically merged by the core of Chart.js, and is available at this.options - initialize: function(data){ - this.segments = []; - //Declare segment class as a chart instance specific class, so it can share props for this instance - this.SegmentArc = Chart.Arc.extend({ - showStroke : this.options.segmentShowStroke, - strokeWidth : this.options.segmentStrokeWidth, - strokeColor : this.options.segmentStrokeColor, - ctx : this.chart.ctx, - innerRadius : 0, - x : this.chart.width/2, - y : this.chart.height/2 - }); - this.scale = new Chart.RadialScale({ - display: this.options.showScale, - fontStyle: this.options.scaleFontStyle, - fontSize: this.options.scaleFontSize, - fontFamily: this.options.scaleFontFamily, - fontColor: this.options.scaleFontColor, - showLabels: this.options.scaleShowLabels, - showLabelBackdrop: this.options.scaleShowLabelBackdrop, - backdropColor: this.options.scaleBackdropColor, - backdropPaddingY : this.options.scaleBackdropPaddingY, - backdropPaddingX: this.options.scaleBackdropPaddingX, - lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0, - lineColor: this.options.scaleLineColor, - lineArc: true, - width: this.chart.width, - height: this.chart.height, - xCenter: this.chart.width/2, - yCenter: this.chart.height/2, - ctx : this.chart.ctx, - templateString: this.options.scaleLabel, - valuesCount: data.length - }); - - this.updateScaleRange(data); - - this.scale.update(); - - helpers.each(data,function(segment,index){ - this.addData(segment,index,true); - },this); - - //Set up tooltip events on the chart - if (this.options.showTooltips){ - helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ - var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : []; - helpers.each(this.segments,function(segment){ - segment.restore(["fillColor"]); - }); - helpers.each(activeSegments,function(activeSegment){ - activeSegment.fillColor = activeSegment.highlightColor; - }); - this.showTooltip(activeSegments); - }); - } - - this.render(); - }, - getSegmentsAtEvent : function(e){ - var segmentsArray = []; - - var location = helpers.getRelativePosition(e); - - helpers.each(this.segments,function(segment){ - if (segment.inRange(location.x,location.y)) segmentsArray.push(segment); - },this); - return segmentsArray; - }, - addData : function(segment, atIndex, silent){ - var index = atIndex || this.segments.length; - - this.segments.splice(index, 0, new this.SegmentArc({ - fillColor: segment.color, - highlightColor: segment.highlight || segment.color, - label: segment.label, - value: segment.value, - outerRadius: (this.options.animateScale) ? 0 : this.scale.calculateCenterOffset(segment.value), - circumference: (this.options.animateRotate) ? 0 : this.scale.getCircumference(), - startAngle: Math.PI * 1.5 - })); - if (!silent){ - this.reflow(); - this.update(); - } - }, - removeData: function(atIndex){ - var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1; - this.segments.splice(indexToDelete, 1); - this.reflow(); - this.update(); - }, - calculateTotal: function(data){ - this.total = 0; - helpers.each(data,function(segment){ - this.total += segment.value; - },this); - this.scale.valuesCount = this.segments.length; - }, - updateScaleRange: function(datapoints){ - var valuesArray = []; - helpers.each(datapoints,function(segment){ - valuesArray.push(segment.value); - }); - - var scaleSizes = (this.options.scaleOverride) ? - { - steps: this.options.scaleSteps, - stepValue: this.options.scaleStepWidth, - min: this.options.scaleStartValue, - max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) - } : - helpers.calculateScaleRange( - valuesArray, - helpers.min([this.chart.width, this.chart.height])/2, - this.options.scaleFontSize, - this.options.scaleBeginAtZero, - this.options.scaleIntegersOnly - ); - - helpers.extend( - this.scale, - scaleSizes, - { - size: helpers.min([this.chart.width, this.chart.height]), - xCenter: this.chart.width/2, - yCenter: this.chart.height/2 - } - ); - - }, - update : function(){ - this.calculateTotal(this.segments); - - helpers.each(this.segments,function(segment){ - segment.save(); - }); - - this.reflow(); - this.render(); - }, - reflow : function(){ - helpers.extend(this.SegmentArc.prototype,{ - x : this.chart.width/2, - y : this.chart.height/2 - }); - this.updateScaleRange(this.segments); - this.scale.update(); - - helpers.extend(this.scale,{ - xCenter: this.chart.width/2, - yCenter: this.chart.height/2 - }); - - helpers.each(this.segments, function(segment){ - segment.update({ - outerRadius : this.scale.calculateCenterOffset(segment.value) - }); - }, this); - - }, - draw : function(ease){ - var easingDecimal = ease || 1; - //Clear & draw the canvas - this.clear(); - helpers.each(this.segments,function(segment, index){ - segment.transition({ - circumference : this.scale.getCircumference(), - outerRadius : this.scale.calculateCenterOffset(segment.value) - },easingDecimal); - - segment.endAngle = segment.startAngle + segment.circumference; - - // If we've removed the first segment we need to set the first one to - // start at the top. - if (index === 0){ - segment.startAngle = Math.PI * 1.5; - } - - //Check to see if it's the last segment, if not get the next and update the start angle - if (index < this.segments.length - 1){ - this.segments[index+1].startAngle = segment.endAngle; - } - segment.draw(); - }, this); - this.scale.draw(); - } - }); - -}).call(this); -(function(){ - "use strict"; - - var root = this, - Chart = root.Chart, - helpers = Chart.helpers; - - - - Chart.Type.extend({ - name: "Radar", - defaults:{ - //Boolean - Whether to show lines for each scale point - scaleShowLine : true, - - //Boolean - Whether we show the angle lines out of the radar - angleShowLineOut : true, - - //Boolean - Whether to show labels on the scale - scaleShowLabels : false, - - // Boolean - Whether the scale should begin at zero - scaleBeginAtZero : true, - - //String - Colour of the angle line - angleLineColor : "rgba(0,0,0,.1)", - - //Number - Pixel width of the angle line - angleLineWidth : 1, - - //String - Point label font declaration - pointLabelFontFamily : "'Arial'", - - //String - Point label font weight - pointLabelFontStyle : "normal", - - //Number - Point label font size in pixels - pointLabelFontSize : 10, - - //String - Point label font colour - pointLabelFontColor : "#666", - - //Boolean - Whether to show a dot for each point - pointDot : true, - - //Number - Radius of each point dot in pixels - pointDotRadius : 3, - - //Number - Pixel width of point dot stroke - pointDotStrokeWidth : 1, - - //Number - amount extra to add to the radius to cater for hit detection outside the drawn point - pointHitDetectionRadius : 20, - - //Boolean - Whether to show a stroke for datasets - datasetStroke : true, - - //Number - Pixel width of dataset stroke - datasetStrokeWidth : 2, - - //Boolean - Whether to fill the dataset with a colour - datasetFill : true, - - //String - A legend template - legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
" - - }, - - initialize: function(data){ - this.PointClass = Chart.Point.extend({ - strokeWidth : this.options.pointDotStrokeWidth, - radius : this.options.pointDotRadius, - display: this.options.pointDot, - hitDetectionRadius : this.options.pointHitDetectionRadius, - ctx : this.chart.ctx - }); - - this.datasets = []; - - this.buildScale(data); - - //Set up tooltip events on the chart - if (this.options.showTooltips){ - helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ - var activePointsCollection = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : []; - - this.eachPoints(function(point){ - point.restore(['fillColor', 'strokeColor']); - }); - helpers.each(activePointsCollection, function(activePoint){ - activePoint.fillColor = activePoint.highlightFill; - activePoint.strokeColor = activePoint.highlightStroke; - }); - - this.showTooltip(activePointsCollection); - }); - } - - //Iterate through each of the datasets, and build this into a property of the chart - helpers.each(data.datasets,function(dataset){ - - var datasetObject = { - label: dataset.label || null, - fillColor : dataset.fillColor, - strokeColor : dataset.strokeColor, - pointColor : dataset.pointColor, - pointStrokeColor : dataset.pointStrokeColor, - points : [] - }; - - this.datasets.push(datasetObject); - - helpers.each(dataset.data,function(dataPoint,index){ - //Add a new point for each piece of data, passing any required data to draw. - var pointPosition; - if (!this.scale.animation){ - pointPosition = this.scale.getPointPosition(index, this.scale.calculateCenterOffset(dataPoint)); - } - datasetObject.points.push(new this.PointClass({ - value : dataPoint, - label : data.labels[index], - datasetLabel: dataset.label, - x: (this.options.animation) ? this.scale.xCenter : pointPosition.x, - y: (this.options.animation) ? this.scale.yCenter : pointPosition.y, - strokeColor : dataset.pointStrokeColor, - fillColor : dataset.pointColor, - highlightFill : dataset.pointHighlightFill || dataset.pointColor, - highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor - })); - },this); - - },this); - - this.render(); - }, - eachPoints : function(callback){ - helpers.each(this.datasets,function(dataset){ - helpers.each(dataset.points,callback,this); - },this); - }, - - getPointsAtEvent : function(evt){ - var mousePosition = helpers.getRelativePosition(evt), - fromCenter = helpers.getAngleFromPoint({ - x: this.scale.xCenter, - y: this.scale.yCenter - }, mousePosition); - - var anglePerIndex = (Math.PI * 2) /this.scale.valuesCount, - pointIndex = Math.round((fromCenter.angle - Math.PI * 1.5) / anglePerIndex), - activePointsCollection = []; - - // If we're at the top, make the pointIndex 0 to get the first of the array. - if (pointIndex >= this.scale.valuesCount || pointIndex < 0){ - pointIndex = 0; - } - - if (fromCenter.distance <= this.scale.drawingArea){ - helpers.each(this.datasets, function(dataset){ - activePointsCollection.push(dataset.points[pointIndex]); - }); - } - - return activePointsCollection; - }, - - buildScale : function(data){ - this.scale = new Chart.RadialScale({ - display: this.options.showScale, - fontStyle: this.options.scaleFontStyle, - fontSize: this.options.scaleFontSize, - fontFamily: this.options.scaleFontFamily, - fontColor: this.options.scaleFontColor, - showLabels: this.options.scaleShowLabels, - showLabelBackdrop: this.options.scaleShowLabelBackdrop, - backdropColor: this.options.scaleBackdropColor, - backdropPaddingY : this.options.scaleBackdropPaddingY, - backdropPaddingX: this.options.scaleBackdropPaddingX, - lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0, - lineColor: this.options.scaleLineColor, - angleLineColor : this.options.angleLineColor, - angleLineWidth : (this.options.angleShowLineOut) ? this.options.angleLineWidth : 0, - // Point labels at the edge of each line - pointLabelFontColor : this.options.pointLabelFontColor, - pointLabelFontSize : this.options.pointLabelFontSize, - pointLabelFontFamily : this.options.pointLabelFontFamily, - pointLabelFontStyle : this.options.pointLabelFontStyle, - height : this.chart.height, - width: this.chart.width, - xCenter: this.chart.width/2, - yCenter: this.chart.height/2, - ctx : this.chart.ctx, - templateString: this.options.scaleLabel, - labels: data.labels, - valuesCount: data.datasets[0].data.length - }); - - this.scale.setScaleSize(); - this.updateScaleRange(data.datasets); - this.scale.buildYLabels(); - }, - updateScaleRange: function(datasets){ - var valuesArray = (function(){ - var totalDataArray = []; - helpers.each(datasets,function(dataset){ - if (dataset.data){ - totalDataArray = totalDataArray.concat(dataset.data); - } - else { - helpers.each(dataset.points, function(point){ - totalDataArray.push(point.value); - }); - } - }); - return totalDataArray; - })(); - - - var scaleSizes = (this.options.scaleOverride) ? - { - steps: this.options.scaleSteps, - stepValue: this.options.scaleStepWidth, - min: this.options.scaleStartValue, - max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) - } : - helpers.calculateScaleRange( - valuesArray, - helpers.min([this.chart.width, this.chart.height])/2, - this.options.scaleFontSize, - this.options.scaleBeginAtZero, - this.options.scaleIntegersOnly - ); - - helpers.extend( - this.scale, - scaleSizes - ); - - }, - addData : function(valuesArray,label){ - //Map the values array for each of the datasets - this.scale.valuesCount++; - helpers.each(valuesArray,function(value,datasetIndex){ - var pointPosition = this.scale.getPointPosition(this.scale.valuesCount, this.scale.calculateCenterOffset(value)); - this.datasets[datasetIndex].points.push(new this.PointClass({ - value : value, - label : label, - x: pointPosition.x, - y: pointPosition.y, - strokeColor : this.datasets[datasetIndex].pointStrokeColor, - fillColor : this.datasets[datasetIndex].pointColor - })); - },this); - - this.scale.labels.push(label); - - this.reflow(); - - this.update(); - }, - removeData : function(){ - this.scale.valuesCount--; - this.scale.labels.shift(); - helpers.each(this.datasets,function(dataset){ - dataset.points.shift(); - },this); - this.reflow(); - this.update(); - }, - update : function(){ - this.eachPoints(function(point){ - point.save(); - }); - this.reflow(); - this.render(); - }, - reflow: function(){ - helpers.extend(this.scale, { - width : this.chart.width, - height: this.chart.height, - size : helpers.min([this.chart.width, this.chart.height]), - xCenter: this.chart.width/2, - yCenter: this.chart.height/2 - }); - this.updateScaleRange(this.datasets); - this.scale.setScaleSize(); - this.scale.buildYLabels(); - }, - draw : function(ease){ - var easeDecimal = ease || 1, - ctx = this.chart.ctx; - this.clear(); - this.scale.draw(); - - helpers.each(this.datasets,function(dataset){ - - //Transition each point first so that the line and point drawing isn't out of sync - helpers.each(dataset.points,function(point,index){ - if (point.hasValue()){ - point.transition(this.scale.getPointPosition(index, this.scale.calculateCenterOffset(point.value)), easeDecimal); - } - },this); - - - - //Draw the line between all the points - ctx.lineWidth = this.options.datasetStrokeWidth; - ctx.strokeStyle = dataset.strokeColor; - ctx.beginPath(); - helpers.each(dataset.points,function(point,index){ - if (index === 0){ - ctx.moveTo(point.x,point.y); - } - else{ - ctx.lineTo(point.x,point.y); - } - },this); - ctx.closePath(); - ctx.stroke(); - - ctx.fillStyle = dataset.fillColor; - ctx.fill(); - - //Now draw the points over the line - //A little inefficient double looping, but better than the line - //lagging behind the point positions - helpers.each(dataset.points,function(point){ - if (point.hasValue()){ - point.draw(); - } - }); - - },this); - - } - - }); - - - - - -}).call(this); \ No newline at end of file diff --git a/Chart.min.js b/Chart.min.js deleted file mode 100644 index 3a0a2c87345..00000000000 --- a/Chart.min.js +++ /dev/null @@ -1,11 +0,0 @@ -/*! - * Chart.js - * http://chartjs.org/ - * Version: 1.0.2 - * - * Copyright 2015 Nick Downie - * Released under the MIT license - * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md - */ -(function(){"use strict";var t=this,i=t.Chart,e=function(t){this.canvas=t.canvas,this.ctx=t;var i=function(t,i){return t["offset"+i]?t["offset"+i]:document.defaultView.getComputedStyle(t).getPropertyValue(i)},e=this.width=i(t.canvas,"Width"),n=this.height=i(t.canvas,"Height");t.canvas.width=e,t.canvas.height=n;var e=this.width=t.canvas.width,n=this.height=t.canvas.height;return this.aspectRatio=this.width/this.height,s.retinaScale(this),this};e.defaults={global:{animation:!0,animationSteps:60,animationEasing:"easeOutQuart",showScale:!0,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleIntegersOnly:!0,scaleBeginAtZero:!1,scaleFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",responsive:!1,maintainAspectRatio:!0,showTooltips:!0,customTooltips:!1,tooltipEvents:["mousemove","touchstart","touchmove","mouseout"],tooltipFillColor:"rgba(0,0,0,0.8)",tooltipFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",tooltipFontSize:14,tooltipFontStyle:"normal",tooltipFontColor:"#fff",tooltipTitleFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",tooltipTitleFontSize:14,tooltipTitleFontStyle:"bold",tooltipTitleFontColor:"#fff",tooltipYPadding:6,tooltipXPadding:6,tooltipCaretSize:8,tooltipCornerRadius:6,tooltipXOffset:10,tooltipTemplate:"<%if (label){%><%=label%>: <%}%><%= value %>",multiTooltipTemplate:"<%= value %>",multiTooltipKeyBackground:"#fff",onAnimationProgress:function(){},onAnimationComplete:function(){}}},e.types={};var s=e.helpers={},n=s.each=function(t,i,e){var s=Array.prototype.slice.call(arguments,3);if(t)if(t.length===+t.length){var n;for(n=0;n=0;s--){var n=t[s];if(i(n))return n}},s.inherits=function(t){var i=this,e=t&&t.hasOwnProperty("constructor")?t.constructor:function(){return i.apply(this,arguments)},s=function(){this.constructor=e};return s.prototype=i.prototype,e.prototype=new s,e.extend=r,t&&a(e.prototype,t),e.__super__=i.prototype,e}),c=s.noop=function(){},u=s.uid=function(){var t=0;return function(){return"chart-"+t++}}(),d=s.warn=function(t){window.console&&"function"==typeof window.console.warn&&console.warn(t)},p=s.amd="function"==typeof define&&define.amd,f=s.isNumber=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},g=s.max=function(t){return Math.max.apply(Math,t)},m=s.min=function(t){return Math.min.apply(Math,t)},v=(s.cap=function(t,i,e){if(f(i)){if(t>i)return i}else if(f(e)&&e>t)return e;return t},s.getDecimalPlaces=function(t){return t%1!==0&&f(t)?t.toString().split(".")[1].length:0}),S=s.radians=function(t){return t*(Math.PI/180)},x=(s.getAngleFromPoint=function(t,i){var e=i.x-t.x,s=i.y-t.y,n=Math.sqrt(e*e+s*s),o=2*Math.PI+Math.atan2(s,e);return 0>e&&0>s&&(o+=2*Math.PI),{angle:o,distance:n}},s.aliasPixel=function(t){return t%2===0?0:.5}),y=(s.splineCurve=function(t,i,e,s){var n=Math.sqrt(Math.pow(i.x-t.x,2)+Math.pow(i.y-t.y,2)),o=Math.sqrt(Math.pow(e.x-i.x,2)+Math.pow(e.y-i.y,2)),a=s*n/(n+o),h=s*o/(n+o);return{inner:{x:i.x-a*(e.x-t.x),y:i.y-a*(e.y-t.y)},outer:{x:i.x+h*(e.x-t.x),y:i.y+h*(e.y-t.y)}}},s.calculateOrderOfMagnitude=function(t){return Math.floor(Math.log(t)/Math.LN10)}),C=(s.calculateScaleRange=function(t,i,e,s,n){var o=2,a=Math.floor(i/(1.5*e)),h=o>=a,l=g(t),r=m(t);l===r&&(l+=.5,r>=.5&&!s?r-=.5:l+=.5);for(var c=Math.abs(l-r),u=y(c),d=Math.ceil(l/(1*Math.pow(10,u)))*Math.pow(10,u),p=s?0:Math.floor(r/(1*Math.pow(10,u)))*Math.pow(10,u),f=d-p,v=Math.pow(10,u),S=Math.round(f/v);(S>a||a>2*S)&&!h;)if(S>a)v*=2,S=Math.round(f/v),S%1!==0&&(h=!0);else if(n&&u>=0){if(v/2%1!==0)break;v/=2,S=Math.round(f/v)}else v/=2,S=Math.round(f/v);return h&&(S=o,v=f/S),{steps:S,stepValue:v,min:p,max:p+S*v}},s.template=function(t,i){function e(t,i){var e=/\W/.test(t)?new Function("obj","var p=[],print=function(){p.push.apply(p,arguments);};with(obj){p.push('"+t.replace(/[\r\t\n]/g," ").split("<%").join(" ").replace(/((^|%>)[^\t]*)'/g,"$1\r").replace(/\t=(.*?)%>/g,"',$1,'").split(" ").join("');").split("%>").join("p.push('").split("\r").join("\\'")+"');}return p.join('');"):s[t]=s[t];return i?e(i):e}if(t instanceof Function)return t(i);var s={};return e(t,i)}),w=(s.generateLabels=function(t,i,e,s){var o=new Array(i);return labelTemplateString&&n(o,function(i,n){o[n]=C(t,{value:e+s*(n+1)})}),o},s.easingEffects={linear:function(t){return t},easeInQuad:function(t){return t*t},easeOutQuad:function(t){return-1*t*(t-2)},easeInOutQuad:function(t){return(t/=.5)<1?.5*t*t:-0.5*(--t*(t-2)-1)},easeInCubic:function(t){return t*t*t},easeOutCubic:function(t){return 1*((t=t/1-1)*t*t+1)},easeInOutCubic:function(t){return(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2)},easeInQuart:function(t){return t*t*t*t},easeOutQuart:function(t){return-1*((t=t/1-1)*t*t*t-1)},easeInOutQuart:function(t){return(t/=.5)<1?.5*t*t*t*t:-0.5*((t-=2)*t*t*t-2)},easeInQuint:function(t){return 1*(t/=1)*t*t*t*t},easeOutQuint:function(t){return 1*((t=t/1-1)*t*t*t*t+1)},easeInOutQuint:function(t){return(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2)},easeInSine:function(t){return-1*Math.cos(t/1*(Math.PI/2))+1},easeOutSine:function(t){return 1*Math.sin(t/1*(Math.PI/2))},easeInOutSine:function(t){return-0.5*(Math.cos(Math.PI*t/1)-1)},easeInExpo:function(t){return 0===t?1:1*Math.pow(2,10*(t/1-1))},easeOutExpo:function(t){return 1===t?1:1*(-Math.pow(2,-10*t/1)+1)},easeInOutExpo:function(t){return 0===t?0:1===t?1:(t/=.5)<1?.5*Math.pow(2,10*(t-1)):.5*(-Math.pow(2,-10*--t)+2)},easeInCirc:function(t){return t>=1?t:-1*(Math.sqrt(1-(t/=1)*t)-1)},easeOutCirc:function(t){return 1*Math.sqrt(1-(t=t/1-1)*t)},easeInOutCirc:function(t){return(t/=.5)<1?-0.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1)},easeInElastic:function(t){var i=1.70158,e=0,s=1;return 0===t?0:1==(t/=1)?1:(e||(e=.3),st?-.5*s*Math.pow(2,10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e):s*Math.pow(2,-10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e)*.5+1)},easeInBack:function(t){var i=1.70158;return 1*(t/=1)*t*((i+1)*t-i)},easeOutBack:function(t){var i=1.70158;return 1*((t=t/1-1)*t*((i+1)*t+i)+1)},easeInOutBack:function(t){var i=1.70158;return(t/=.5)<1?.5*t*t*(((i*=1.525)+1)*t-i):.5*((t-=2)*t*(((i*=1.525)+1)*t+i)+2)},easeInBounce:function(t){return 1-w.easeOutBounce(1-t)},easeOutBounce:function(t){return(t/=1)<1/2.75?7.5625*t*t:2/2.75>t?1*(7.5625*(t-=1.5/2.75)*t+.75):2.5/2.75>t?1*(7.5625*(t-=2.25/2.75)*t+.9375):1*(7.5625*(t-=2.625/2.75)*t+.984375)},easeInOutBounce:function(t){return.5>t?.5*w.easeInBounce(2*t):.5*w.easeOutBounce(2*t-1)+.5}}),b=s.requestAnimFrame=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(t){return window.setTimeout(t,1e3/60)}}(),P=s.cancelAnimFrame=function(){return window.cancelAnimationFrame||window.webkitCancelAnimationFrame||window.mozCancelAnimationFrame||window.oCancelAnimationFrame||window.msCancelAnimationFrame||function(t){return window.clearTimeout(t,1e3/60)}}(),L=(s.animationLoop=function(t,i,e,s,n,o){var a=0,h=w[e]||w.linear,l=function(){a++;var e=a/i,r=h(e);t.call(o,r,e,a),s.call(o,r,e),i>a?o.animationFrame=b(l):n.apply(o)};b(l)},s.getRelativePosition=function(t){var i,e,s=t.originalEvent||t,n=t.currentTarget||t.srcElement,o=n.getBoundingClientRect();return s.touches?(i=s.touches[0].clientX-o.left,e=s.touches[0].clientY-o.top):(i=s.clientX-o.left,e=s.clientY-o.top),{x:i,y:e}},s.addEvent=function(t,i,e){t.addEventListener?t.addEventListener(i,e):t.attachEvent?t.attachEvent("on"+i,e):t["on"+i]=e}),k=s.removeEvent=function(t,i,e){t.removeEventListener?t.removeEventListener(i,e,!1):t.detachEvent?t.detachEvent("on"+i,e):t["on"+i]=c},F=(s.bindEvents=function(t,i,e){t.events||(t.events={}),n(i,function(i){t.events[i]=function(){e.apply(t,arguments)},L(t.chart.canvas,i,t.events[i])})},s.unbindEvents=function(t,i){n(i,function(i,e){k(t.chart.canvas,e,i)})}),R=s.getMaximumWidth=function(t){var i=t.parentNode;return i.clientWidth},T=s.getMaximumHeight=function(t){var i=t.parentNode;return i.clientHeight},A=(s.getMaximumSize=s.getMaximumWidth,s.retinaScale=function(t){var i=t.ctx,e=t.canvas.width,s=t.canvas.height;window.devicePixelRatio&&(i.canvas.style.width=e+"px",i.canvas.style.height=s+"px",i.canvas.height=s*window.devicePixelRatio,i.canvas.width=e*window.devicePixelRatio,i.scale(window.devicePixelRatio,window.devicePixelRatio))}),M=s.clear=function(t){t.ctx.clearRect(0,0,t.width,t.height)},W=s.fontString=function(t,i,e){return i+" "+t+"px "+e},z=s.longestText=function(t,i,e){t.font=i;var s=0;return n(e,function(i){var e=t.measureText(i).width;s=e>s?e:s}),s},B=s.drawRoundedRectangle=function(t,i,e,s,n,o){t.beginPath(),t.moveTo(i+o,e),t.lineTo(i+s-o,e),t.quadraticCurveTo(i+s,e,i+s,e+o),t.lineTo(i+s,e+n-o),t.quadraticCurveTo(i+s,e+n,i+s-o,e+n),t.lineTo(i+o,e+n),t.quadraticCurveTo(i,e+n,i,e+n-o),t.lineTo(i,e+o),t.quadraticCurveTo(i,e,i+o,e),t.closePath()};e.instances={},e.Type=function(t,i,s){this.options=i,this.chart=s,this.id=u(),e.instances[this.id]=this,i.responsive&&this.resize(),this.initialize.call(this,t)},a(e.Type.prototype,{initialize:function(){return this},clear:function(){return M(this.chart),this},stop:function(){return P(this.animationFrame),this},resize:function(t){this.stop();var i=this.chart.canvas,e=R(this.chart.canvas),s=this.options.maintainAspectRatio?e/this.chart.aspectRatio:T(this.chart.canvas);return i.width=this.chart.width=e,i.height=this.chart.height=s,A(this.chart),"function"==typeof t&&t.apply(this,Array.prototype.slice.call(arguments,1)),this},reflow:c,render:function(t){return t&&this.reflow(),this.options.animation&&!t?s.animationLoop(this.draw,this.options.animationSteps,this.options.animationEasing,this.options.onAnimationProgress,this.options.onAnimationComplete,this):(this.draw(),this.options.onAnimationComplete.call(this)),this},generateLegend:function(){return C(this.options.legendTemplate,this)},destroy:function(){this.clear(),F(this,this.events);var t=this.chart.canvas;t.width=this.chart.width,t.height=this.chart.height,t.style.removeProperty?(t.style.removeProperty("width"),t.style.removeProperty("height")):(t.style.removeAttribute("width"),t.style.removeAttribute("height")),delete e.instances[this.id]},showTooltip:function(t,i){"undefined"==typeof this.activeElements&&(this.activeElements=[]);var o=function(t){var i=!1;return t.length!==this.activeElements.length?i=!0:(n(t,function(t,e){t!==this.activeElements[e]&&(i=!0)},this),i)}.call(this,t);if(o||i){if(this.activeElements=t,this.draw(),this.options.customTooltips&&this.options.customTooltips(!1),t.length>0)if(this.datasets&&this.datasets.length>1){for(var a,h,r=this.datasets.length-1;r>=0&&(a=this.datasets[r].points||this.datasets[r].bars||this.datasets[r].segments,h=l(a,t[0]),-1===h);r--);var c=[],u=[],d=function(){var t,i,e,n,o,a=[],l=[],r=[];return s.each(this.datasets,function(i){t=i.points||i.bars||i.segments,t[h]&&t[h].hasValue()&&a.push(t[h])}),s.each(a,function(t){l.push(t.x),r.push(t.y),c.push(s.template(this.options.multiTooltipTemplate,t)),u.push({fill:t._saved.fillColor||t.fillColor,stroke:t._saved.strokeColor||t.strokeColor})},this),o=m(r),e=g(r),n=m(l),i=g(l),{x:n>this.chart.width/2?n:i,y:(o+e)/2}}.call(this,h);new e.MultiTooltip({x:d.x,y:d.y,xPadding:this.options.tooltipXPadding,yPadding:this.options.tooltipYPadding,xOffset:this.options.tooltipXOffset,fillColor:this.options.tooltipFillColor,textColor:this.options.tooltipFontColor,fontFamily:this.options.tooltipFontFamily,fontStyle:this.options.tooltipFontStyle,fontSize:this.options.tooltipFontSize,titleTextColor:this.options.tooltipTitleFontColor,titleFontFamily:this.options.tooltipTitleFontFamily,titleFontStyle:this.options.tooltipTitleFontStyle,titleFontSize:this.options.tooltipTitleFontSize,cornerRadius:this.options.tooltipCornerRadius,labels:c,legendColors:u,legendColorBackground:this.options.multiTooltipKeyBackground,title:t[0].label,chart:this.chart,ctx:this.chart.ctx,custom:this.options.customTooltips}).draw()}else n(t,function(t){var i=t.tooltipPosition();new e.Tooltip({x:Math.round(i.x),y:Math.round(i.y),xPadding:this.options.tooltipXPadding,yPadding:this.options.tooltipYPadding,fillColor:this.options.tooltipFillColor,textColor:this.options.tooltipFontColor,fontFamily:this.options.tooltipFontFamily,fontStyle:this.options.tooltipFontStyle,fontSize:this.options.tooltipFontSize,caretHeight:this.options.tooltipCaretSize,cornerRadius:this.options.tooltipCornerRadius,text:C(this.options.tooltipTemplate,t),chart:this.chart,custom:this.options.customTooltips}).draw()},this);return this}},toBase64Image:function(){return this.chart.canvas.toDataURL.apply(this.chart.canvas,arguments)}}),e.Type.extend=function(t){var i=this,s=function(){return i.apply(this,arguments)};if(s.prototype=o(i.prototype),a(s.prototype,t),s.extend=e.Type.extend,t.name||i.prototype.name){var n=t.name||i.prototype.name,l=e.defaults[i.prototype.name]?o(e.defaults[i.prototype.name]):{};e.defaults[n]=a(l,t.defaults),e.types[n]=s,e.prototype[n]=function(t,i){var o=h(e.defaults.global,e.defaults[n],i||{});return new s(t,o,this)}}else d("Name not provided for this chart, so it hasn't been registered");return i},e.Element=function(t){a(this,t),this.initialize.apply(this,arguments),this.save()},a(e.Element.prototype,{initialize:function(){},restore:function(t){return t?n(t,function(t){this[t]=this._saved[t]},this):a(this,this._saved),this},save:function(){return this._saved=o(this),delete this._saved._saved,this},update:function(t){return n(t,function(t,i){this._saved[i]=this[i],this[i]=t},this),this},transition:function(t,i){return n(t,function(t,e){this[e]=(t-this._saved[e])*i+this._saved[e]},this),this},tooltipPosition:function(){return{x:this.x,y:this.y}},hasValue:function(){return f(this.value)}}),e.Element.extend=r,e.Point=e.Element.extend({display:!0,inRange:function(t,i){var e=this.hitDetectionRadius+this.radius;return Math.pow(t-this.x,2)+Math.pow(i-this.y,2)=this.startAngle&&e.angle<=this.endAngle,o=e.distance>=this.innerRadius&&e.distance<=this.outerRadius;return n&&o},tooltipPosition:function(){var t=this.startAngle+(this.endAngle-this.startAngle)/2,i=(this.outerRadius-this.innerRadius)/2+this.innerRadius;return{x:this.x+Math.cos(t)*i,y:this.y+Math.sin(t)*i}},draw:function(t){var i=this.ctx;i.beginPath(),i.arc(this.x,this.y,this.outerRadius,this.startAngle,this.endAngle),i.arc(this.x,this.y,this.innerRadius,this.endAngle,this.startAngle,!0),i.closePath(),i.strokeStyle=this.strokeColor,i.lineWidth=this.strokeWidth,i.fillStyle=this.fillColor,i.fill(),i.lineJoin="bevel",this.showStroke&&i.stroke()}}),e.Rectangle=e.Element.extend({draw:function(){var t=this.ctx,i=this.width/2,e=this.x-i,s=this.x+i,n=this.base-(this.base-this.y),o=this.strokeWidth/2;this.showStroke&&(e+=o,s-=o,n+=o),t.beginPath(),t.fillStyle=this.fillColor,t.strokeStyle=this.strokeColor,t.lineWidth=this.strokeWidth,t.moveTo(e,this.base),t.lineTo(e,n),t.lineTo(s,n),t.lineTo(s,this.base),t.fill(),this.showStroke&&t.stroke()},height:function(){return this.base-this.y},inRange:function(t,i){return t>=this.x-this.width/2&&t<=this.x+this.width/2&&i>=this.y&&i<=this.base}}),e.Tooltip=e.Element.extend({draw:function(){var t=this.chart.ctx;t.font=W(this.fontSize,this.fontStyle,this.fontFamily),this.xAlign="center",this.yAlign="above";var i=this.caretPadding=2,e=t.measureText(this.text).width+2*this.xPadding,s=this.fontSize+2*this.yPadding,n=s+this.caretHeight+i;this.x+e/2>this.chart.width?this.xAlign="left":this.x-e/2<0&&(this.xAlign="right"),this.y-n<0&&(this.yAlign="below");var o=this.x-e/2,a=this.y-n;if(t.fillStyle=this.fillColor,this.custom)this.custom(this);else{switch(this.yAlign){case"above":t.beginPath(),t.moveTo(this.x,this.y-i),t.lineTo(this.x+this.caretHeight,this.y-(i+this.caretHeight)),t.lineTo(this.x-this.caretHeight,this.y-(i+this.caretHeight)),t.closePath(),t.fill();break;case"below":a=this.y+i+this.caretHeight,t.beginPath(),t.moveTo(this.x,this.y+i),t.lineTo(this.x+this.caretHeight,this.y+i+this.caretHeight),t.lineTo(this.x-this.caretHeight,this.y+i+this.caretHeight),t.closePath(),t.fill()}switch(this.xAlign){case"left":o=this.x-e+(this.cornerRadius+this.caretHeight);break;case"right":o=this.x-(this.cornerRadius+this.caretHeight)}B(t,o,a,e,s,this.cornerRadius),t.fill(),t.fillStyle=this.textColor,t.textAlign="center",t.textBaseline="middle",t.fillText(this.text,o+e/2,a+s/2)}}}),e.MultiTooltip=e.Element.extend({initialize:function(){this.font=W(this.fontSize,this.fontStyle,this.fontFamily),this.titleFont=W(this.titleFontSize,this.titleFontStyle,this.titleFontFamily),this.height=this.labels.length*this.fontSize+(this.labels.length-1)*(this.fontSize/2)+2*this.yPadding+1.5*this.titleFontSize,this.ctx.font=this.titleFont;var t=this.ctx.measureText(this.title).width,i=z(this.ctx,this.font,this.labels)+this.fontSize+3,e=g([i,t]);this.width=e+2*this.xPadding;var s=this.height/2;this.y-s<0?this.y=s:this.y+s>this.chart.height&&(this.y=this.chart.height-s),this.x>this.chart.width/2?this.x-=this.xOffset+this.width:this.x+=this.xOffset},getLineHeight:function(t){var i=this.y-this.height/2+this.yPadding,e=t-1;return 0===t?i+this.titleFontSize/2:i+(1.5*this.fontSize*e+this.fontSize/2)+1.5*this.titleFontSize},draw:function(){if(this.custom)this.custom(this);else{B(this.ctx,this.x,this.y-this.height/2,this.width,this.height,this.cornerRadius);var t=this.ctx;t.fillStyle=this.fillColor,t.fill(),t.closePath(),t.textAlign="left",t.textBaseline="middle",t.fillStyle=this.titleTextColor,t.font=this.titleFont,t.fillText(this.title,this.x+this.xPadding,this.getLineHeight(0)),t.font=this.font,s.each(this.labels,function(i,e){t.fillStyle=this.textColor,t.fillText(i,this.x+this.xPadding+this.fontSize+3,this.getLineHeight(e+1)),t.fillStyle=this.legendColorBackground,t.fillRect(this.x+this.xPadding,this.getLineHeight(e+1)-this.fontSize/2,this.fontSize,this.fontSize),t.fillStyle=this.legendColors[e].fill,t.fillRect(this.x+this.xPadding,this.getLineHeight(e+1)-this.fontSize/2,this.fontSize,this.fontSize)},this)}}}),e.Scale=e.Element.extend({initialize:function(){this.fit()},buildYLabels:function(){this.yLabels=[];for(var t=v(this.stepValue),i=0;i<=this.steps;i++)this.yLabels.push(C(this.templateString,{value:(this.min+i*this.stepValue).toFixed(t)}));this.yLabelWidth=this.display&&this.showLabels?z(this.ctx,this.font,this.yLabels):0},addXLabel:function(t){this.xLabels.push(t),this.valuesCount++,this.fit()},removeXLabel:function(){this.xLabels.shift(),this.valuesCount--,this.fit()},fit:function(){this.startPoint=this.display?this.fontSize:0,this.endPoint=this.display?this.height-1.5*this.fontSize-5:this.height,this.startPoint+=this.padding,this.endPoint-=this.padding;var t,i=this.endPoint-this.startPoint;for(this.calculateYRange(i),this.buildYLabels(),this.calculateXLabelRotation();i>this.endPoint-this.startPoint;)i=this.endPoint-this.startPoint,t=this.yLabelWidth,this.calculateYRange(i),this.buildYLabels(),tthis.yLabelWidth+10?e/2:this.yLabelWidth+10,this.xLabelRotation=0,this.display){var n,o=z(this.ctx,this.font,this.xLabels);this.xLabelWidth=o;for(var a=Math.floor(this.calculateX(1)-this.calculateX(0))-6;this.xLabelWidth>a&&0===this.xLabelRotation||this.xLabelWidth>a&&this.xLabelRotation<=90&&this.xLabelRotation>0;)n=Math.cos(S(this.xLabelRotation)),t=n*e,i=n*s,t+this.fontSize/2>this.yLabelWidth+8&&(this.xScalePaddingLeft=t+this.fontSize/2),this.xScalePaddingRight=this.fontSize/2,this.xLabelRotation++,this.xLabelWidth=n*o;this.xLabelRotation>0&&(this.endPoint-=Math.sin(S(this.xLabelRotation))*o+3)}else this.xLabelWidth=0,this.xScalePaddingRight=this.padding,this.xScalePaddingLeft=this.padding},calculateYRange:c,drawingArea:function(){return this.startPoint-this.endPoint},calculateY:function(t){var i=this.drawingArea()/(this.min-this.max);return this.endPoint-i*(t-this.min)},calculateX:function(t){var i=(this.xLabelRotation>0,this.width-(this.xScalePaddingLeft+this.xScalePaddingRight)),e=i/Math.max(this.valuesCount-(this.offsetGridLines?0:1),1),s=e*t+this.xScalePaddingLeft;return this.offsetGridLines&&(s+=e/2),Math.round(s)},update:function(t){s.extend(this,t),this.fit()},draw:function(){var t=this.ctx,i=(this.endPoint-this.startPoint)/this.steps,e=Math.round(this.xScalePaddingLeft);this.display&&(t.fillStyle=this.textColor,t.font=this.font,n(this.yLabels,function(n,o){var a=this.endPoint-i*o,h=Math.round(a),l=this.showHorizontalLines;t.textAlign="right",t.textBaseline="middle",this.showLabels&&t.fillText(n,e-10,a),0!==o||l||(l=!0),l&&t.beginPath(),o>0?(t.lineWidth=this.gridLineWidth,t.strokeStyle=this.gridLineColor):(t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor),h+=s.aliasPixel(t.lineWidth),l&&(t.moveTo(e,h),t.lineTo(this.width,h),t.stroke(),t.closePath()),t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor,t.beginPath(),t.moveTo(e-5,h),t.lineTo(e,h),t.stroke(),t.closePath()},this),n(this.xLabels,function(i,e){var s=this.calculateX(e)+x(this.lineWidth),n=this.calculateX(e-(this.offsetGridLines?.5:0))+x(this.lineWidth),o=this.xLabelRotation>0,a=this.showVerticalLines;0!==e||a||(a=!0),a&&t.beginPath(),e>0?(t.lineWidth=this.gridLineWidth,t.strokeStyle=this.gridLineColor):(t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor),a&&(t.moveTo(n,this.endPoint),t.lineTo(n,this.startPoint-3),t.stroke(),t.closePath()),t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor,t.beginPath(),t.moveTo(n,this.endPoint),t.lineTo(n,this.endPoint+5),t.stroke(),t.closePath(),t.save(),t.translate(s,o?this.endPoint+12:this.endPoint+8),t.rotate(-1*S(this.xLabelRotation)),t.font=this.font,t.textAlign=o?"right":"center",t.textBaseline=o?"middle":"top",t.fillText(i,0,0),t.restore()},this))}}),e.RadialScale=e.Element.extend({initialize:function(){this.size=m([this.height,this.width]),this.drawingArea=this.display?this.size/2-(this.fontSize/2+this.backdropPaddingY):this.size/2},calculateCenterOffset:function(t){var i=this.drawingArea/(this.max-this.min);return(t-this.min)*i},update:function(){this.lineArc?this.drawingArea=this.display?this.size/2-(this.fontSize/2+this.backdropPaddingY):this.size/2:this.setScaleSize(),this.buildYLabels()},buildYLabels:function(){this.yLabels=[];for(var t=v(this.stepValue),i=0;i<=this.steps;i++)this.yLabels.push(C(this.templateString,{value:(this.min+i*this.stepValue).toFixed(t)}))},getCircumference:function(){return 2*Math.PI/this.valuesCount},setScaleSize:function(){var t,i,e,s,n,o,a,h,l,r,c,u,d=m([this.height/2-this.pointLabelFontSize-5,this.width/2]),p=this.width,g=0;for(this.ctx.font=W(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily),i=0;ip&&(p=t.x+s,n=i),t.x-sp&&(p=t.x+e,n=i):i>this.valuesCount/2&&t.x-e0){var s,n=e*(this.drawingArea/this.steps),o=this.yCenter-n;if(this.lineWidth>0)if(t.strokeStyle=this.lineColor,t.lineWidth=this.lineWidth,this.lineArc)t.beginPath(),t.arc(this.xCenter,this.yCenter,n,0,2*Math.PI),t.closePath(),t.stroke();else{t.beginPath();for(var a=0;a=0;i--){if(this.angleLineWidth>0){var e=this.getPointPosition(i,this.calculateCenterOffset(this.max));t.beginPath(),t.moveTo(this.xCenter,this.yCenter),t.lineTo(e.x,e.y),t.stroke(),t.closePath()}var s=this.getPointPosition(i,this.calculateCenterOffset(this.max)+5);t.font=W(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily),t.fillStyle=this.pointLabelFontColor;var o=this.labels.length,a=this.labels.length/2,h=a/2,l=h>i||i>o-h,r=i===h||i===o-h;t.textAlign=0===i?"center":i===a?"center":a>i?"left":"right",t.textBaseline=r?"middle":l?"bottom":"top",t.fillText(this.labels[i],s.x,s.y)}}}}}),s.addEvent(window,"resize",function(){var t;return function(){clearTimeout(t),t=setTimeout(function(){n(e.instances,function(t){t.options.responsive&&t.resize(t.render,!0)})},50)}}()),p?define(function(){return e}):"object"==typeof module&&module.exports&&(module.exports=e),t.Chart=e,e.noConflict=function(){return t.Chart=i,e}}).call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleBeginAtZero:!0,scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,scaleShowHorizontalLines:!0,scaleShowVerticalLines:!0,barShowStroke:!0,barStrokeWidth:2,barValueSpacing:5,barDatasetSpacing:1,legendTemplate:'
    <% for (var i=0; i
  • <%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
'};i.Type.extend({name:"Bar",defaults:s,initialize:function(t){var s=this.options;this.ScaleClass=i.Scale.extend({offsetGridLines:!0,calculateBarX:function(t,i,e){var n=this.calculateBaseWidth(),o=this.calculateX(e)-n/2,a=this.calculateBarWidth(t);return o+a*i+i*s.barDatasetSpacing+a/2},calculateBaseWidth:function(){return this.calculateX(1)-this.calculateX(0)-2*s.barValueSpacing},calculateBarWidth:function(t){var i=this.calculateBaseWidth()-(t-1)*s.barDatasetSpacing;return i/t}}),this.datasets=[],this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getBarsAtEvent(t):[];this.eachBars(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),this.BarClass=i.Rectangle.extend({strokeWidth:this.options.barStrokeWidth,showStroke:this.options.barShowStroke,ctx:this.chart.ctx}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,bars:[]};this.datasets.push(s),e.each(i.data,function(e,n){s.bars.push(new this.BarClass({value:e,label:t.labels[n],datasetLabel:i.label,strokeColor:i.strokeColor,fillColor:i.fillColor,highlightFill:i.highlightFill||i.fillColor,highlightStroke:i.highlightStroke||i.strokeColor}))},this)},this),this.buildScale(t.labels),this.BarClass.prototype.base=this.scale.endPoint,this.eachBars(function(t,i,s){e.extend(t,{width:this.scale.calculateBarWidth(this.datasets.length),x:this.scale.calculateBarX(this.datasets.length,s,i),y:this.scale.endPoint}),t.save()},this),this.render()},update:function(){this.scale.update(),e.each(this.activeElements,function(t){t.restore(["fillColor","strokeColor"])}),this.eachBars(function(t){t.save()}),this.render()},eachBars:function(t){e.each(this.datasets,function(i,s){e.each(i.bars,t,this,s)},this)},getBarsAtEvent:function(t){for(var i,s=[],n=e.getRelativePosition(t),o=function(t){s.push(t.bars[i])},a=0;a<% for (var i=0; i
  • <%if(segments[i].label){%><%=segments[i].label%><%}%>
  • <%}%>'};i.Type.extend({name:"Doughnut",defaults:s,initialize:function(t){this.segments=[],this.outerRadius=(e.min([this.chart.width,this.chart.height])-this.options.segmentStrokeWidth/2)/2,this.SegmentArc=i.Arc.extend({ctx:this.chart.ctx,x:this.chart.width/2,y:this.chart.height/2}),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getSegmentsAtEvent(t):[];e.each(this.segments,function(t){t.restore(["fillColor"])}),e.each(i,function(t){t.fillColor=t.highlightColor}),this.showTooltip(i)}),this.calculateTotal(t),e.each(t,function(t,i){this.addData(t,i,!0)},this),this.render()},getSegmentsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.segments,function(t){t.inRange(s.x,s.y)&&i.push(t)},this),i},addData:function(t,i,e){var s=i||this.segments.length;this.segments.splice(s,0,new this.SegmentArc({value:t.value,outerRadius:this.options.animateScale?0:this.outerRadius,innerRadius:this.options.animateScale?0:this.outerRadius/100*this.options.percentageInnerCutout,fillColor:t.color,highlightColor:t.highlight||t.color,showStroke:this.options.segmentShowStroke,strokeWidth:this.options.segmentStrokeWidth,strokeColor:this.options.segmentStrokeColor,startAngle:1.5*Math.PI,circumference:this.options.animateRotate?0:this.calculateCircumference(t.value),label:t.label})),e||(this.reflow(),this.update())},calculateCircumference:function(t){return 2*Math.PI*(Math.abs(t)/this.total)},calculateTotal:function(t){this.total=0,e.each(t,function(t){this.total+=Math.abs(t.value)},this)},update:function(){this.calculateTotal(this.segments),e.each(this.activeElements,function(t){t.restore(["fillColor"])}),e.each(this.segments,function(t){t.save()}),this.render()},removeData:function(t){var i=e.isNumber(t)?t:this.segments.length-1;this.segments.splice(i,1),this.reflow(),this.update()},reflow:function(){e.extend(this.SegmentArc.prototype,{x:this.chart.width/2,y:this.chart.height/2}),this.outerRadius=(e.min([this.chart.width,this.chart.height])-this.options.segmentStrokeWidth/2)/2,e.each(this.segments,function(t){t.update({outerRadius:this.outerRadius,innerRadius:this.outerRadius/100*this.options.percentageInnerCutout})},this)},draw:function(t){var i=t?t:1;this.clear(),e.each(this.segments,function(t,e){t.transition({circumference:this.calculateCircumference(t.value),outerRadius:this.outerRadius,innerRadius:this.outerRadius/100*this.options.percentageInnerCutout},i),t.endAngle=t.startAngle+t.circumference,t.draw(),0===e&&(t.startAngle=1.5*Math.PI),e<% for (var i=0; i
  • <%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>'};i.Type.extend({name:"Line",defaults:s,initialize:function(t){this.PointClass=i.Point.extend({strokeWidth:this.options.pointDotStrokeWidth,radius:this.options.pointDotRadius,display:this.options.pointDot,hitDetectionRadius:this.options.pointHitDetectionRadius,ctx:this.chart.ctx,inRange:function(t){return Math.pow(t-this.x,2)0&&ithis.scale.endPoint?t.controlPoints.outer.y=this.scale.endPoint:t.controlPoints.outer.ythis.scale.endPoint?t.controlPoints.inner.y=this.scale.endPoint:t.controlPoints.inner.y0&&(s.lineTo(h[h.length-1].x,this.scale.endPoint),s.lineTo(h[0].x,this.scale.endPoint),s.fillStyle=t.fillColor,s.closePath(),s.fill()),e.each(h,function(t){t.draw()})},this)}})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",scaleBeginAtZero:!0,scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,scaleShowLine:!0,segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,legendTemplate:'
      <% for (var i=0; i
    • <%if(segments[i].label){%><%=segments[i].label%><%}%>
    • <%}%>
    '};i.Type.extend({name:"PolarArea",defaults:s,initialize:function(t){this.segments=[],this.SegmentArc=i.Arc.extend({showStroke:this.options.segmentShowStroke,strokeWidth:this.options.segmentStrokeWidth,strokeColor:this.options.segmentStrokeColor,ctx:this.chart.ctx,innerRadius:0,x:this.chart.width/2,y:this.chart.height/2}),this.scale=new i.RadialScale({display:this.options.showScale,fontStyle:this.options.scaleFontStyle,fontSize:this.options.scaleFontSize,fontFamily:this.options.scaleFontFamily,fontColor:this.options.scaleFontColor,showLabels:this.options.scaleShowLabels,showLabelBackdrop:this.options.scaleShowLabelBackdrop,backdropColor:this.options.scaleBackdropColor,backdropPaddingY:this.options.scaleBackdropPaddingY,backdropPaddingX:this.options.scaleBackdropPaddingX,lineWidth:this.options.scaleShowLine?this.options.scaleLineWidth:0,lineColor:this.options.scaleLineColor,lineArc:!0,width:this.chart.width,height:this.chart.height,xCenter:this.chart.width/2,yCenter:this.chart.height/2,ctx:this.chart.ctx,templateString:this.options.scaleLabel,valuesCount:t.length}),this.updateScaleRange(t),this.scale.update(),e.each(t,function(t,i){this.addData(t,i,!0)},this),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getSegmentsAtEvent(t):[];e.each(this.segments,function(t){t.restore(["fillColor"])}),e.each(i,function(t){t.fillColor=t.highlightColor}),this.showTooltip(i)}),this.render()},getSegmentsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.segments,function(t){t.inRange(s.x,s.y)&&i.push(t)},this),i},addData:function(t,i,e){var s=i||this.segments.length;this.segments.splice(s,0,new this.SegmentArc({fillColor:t.color,highlightColor:t.highlight||t.color,label:t.label,value:t.value,outerRadius:this.options.animateScale?0:this.scale.calculateCenterOffset(t.value),circumference:this.options.animateRotate?0:this.scale.getCircumference(),startAngle:1.5*Math.PI})),e||(this.reflow(),this.update())},removeData:function(t){var i=e.isNumber(t)?t:this.segments.length-1;this.segments.splice(i,1),this.reflow(),this.update()},calculateTotal:function(t){this.total=0,e.each(t,function(t){this.total+=t.value},this),this.scale.valuesCount=this.segments.length},updateScaleRange:function(t){var i=[];e.each(t,function(t){i.push(t.value)});var s=this.options.scaleOverride?{steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}:e.calculateScaleRange(i,e.min([this.chart.width,this.chart.height])/2,this.options.scaleFontSize,this.options.scaleBeginAtZero,this.options.scaleIntegersOnly);e.extend(this.scale,s,{size:e.min([this.chart.width,this.chart.height]),xCenter:this.chart.width/2,yCenter:this.chart.height/2})},update:function(){this.calculateTotal(this.segments),e.each(this.segments,function(t){t.save()}),this.reflow(),this.render()},reflow:function(){e.extend(this.SegmentArc.prototype,{x:this.chart.width/2,y:this.chart.height/2}),this.updateScaleRange(this.segments),this.scale.update(),e.extend(this.scale,{xCenter:this.chart.width/2,yCenter:this.chart.height/2}),e.each(this.segments,function(t){t.update({outerRadius:this.scale.calculateCenterOffset(t.value)})},this)},draw:function(t){var i=t||1;this.clear(),e.each(this.segments,function(t,e){t.transition({circumference:this.scale.getCircumference(),outerRadius:this.scale.calculateCenterOffset(t.value)},i),t.endAngle=t.startAngle+t.circumference,0===e&&(t.startAngle=1.5*Math.PI),e<% for (var i=0; i
  • <%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>'},initialize:function(t){this.PointClass=i.Point.extend({strokeWidth:this.options.pointDotStrokeWidth,radius:this.options.pointDotRadius,display:this.options.pointDot,hitDetectionRadius:this.options.pointHitDetectionRadius,ctx:this.chart.ctx}),this.datasets=[],this.buildScale(t),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getPointsAtEvent(t):[];this.eachPoints(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,pointColor:i.pointColor,pointStrokeColor:i.pointStrokeColor,points:[]};this.datasets.push(s),e.each(i.data,function(e,n){var o;this.scale.animation||(o=this.scale.getPointPosition(n,this.scale.calculateCenterOffset(e))),s.points.push(new this.PointClass({value:e,label:t.labels[n],datasetLabel:i.label,x:this.options.animation?this.scale.xCenter:o.x,y:this.options.animation?this.scale.yCenter:o.y,strokeColor:i.pointStrokeColor,fillColor:i.pointColor,highlightFill:i.pointHighlightFill||i.pointColor,highlightStroke:i.pointHighlightStroke||i.pointStrokeColor}))},this)},this),this.render()},eachPoints:function(t){e.each(this.datasets,function(i){e.each(i.points,t,this)},this)},getPointsAtEvent:function(t){var i=e.getRelativePosition(t),s=e.getAngleFromPoint({x:this.scale.xCenter,y:this.scale.yCenter},i),n=2*Math.PI/this.scale.valuesCount,o=Math.round((s.angle-1.5*Math.PI)/n),a=[];return(o>=this.scale.valuesCount||0>o)&&(o=0),s.distance<=this.scale.drawingArea&&e.each(this.datasets,function(t){a.push(t.points[o])}),a},buildScale:function(t){this.scale=new i.RadialScale({display:this.options.showScale,fontStyle:this.options.scaleFontStyle,fontSize:this.options.scaleFontSize,fontFamily:this.options.scaleFontFamily,fontColor:this.options.scaleFontColor,showLabels:this.options.scaleShowLabels,showLabelBackdrop:this.options.scaleShowLabelBackdrop,backdropColor:this.options.scaleBackdropColor,backdropPaddingY:this.options.scaleBackdropPaddingY,backdropPaddingX:this.options.scaleBackdropPaddingX,lineWidth:this.options.scaleShowLine?this.options.scaleLineWidth:0,lineColor:this.options.scaleLineColor,angleLineColor:this.options.angleLineColor,angleLineWidth:this.options.angleShowLineOut?this.options.angleLineWidth:0,pointLabelFontColor:this.options.pointLabelFontColor,pointLabelFontSize:this.options.pointLabelFontSize,pointLabelFontFamily:this.options.pointLabelFontFamily,pointLabelFontStyle:this.options.pointLabelFontStyle,height:this.chart.height,width:this.chart.width,xCenter:this.chart.width/2,yCenter:this.chart.height/2,ctx:this.chart.ctx,templateString:this.options.scaleLabel,labels:t.labels,valuesCount:t.datasets[0].data.length}),this.scale.setScaleSize(),this.updateScaleRange(t.datasets),this.scale.buildYLabels()},updateScaleRange:function(t){var i=function(){var i=[];return e.each(t,function(t){t.data?i=i.concat(t.data):e.each(t.points,function(t){i.push(t.value)})}),i}(),s=this.options.scaleOverride?{steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}:e.calculateScaleRange(i,e.min([this.chart.width,this.chart.height])/2,this.options.scaleFontSize,this.options.scaleBeginAtZero,this.options.scaleIntegersOnly);e.extend(this.scale,s)},addData:function(t,i){this.scale.valuesCount++,e.each(t,function(t,e){var s=this.scale.getPointPosition(this.scale.valuesCount,this.scale.calculateCenterOffset(t));this.datasets[e].points.push(new this.PointClass({value:t,label:i,x:s.x,y:s.y,strokeColor:this.datasets[e].pointStrokeColor,fillColor:this.datasets[e].pointColor}))},this),this.scale.labels.push(i),this.reflow(),this.update()},removeData:function(){this.scale.valuesCount--,this.scale.labels.shift(),e.each(this.datasets,function(t){t.points.shift()},this),this.reflow(),this.update()},update:function(){this.eachPoints(function(t){t.save()}),this.reflow(),this.render()},reflow:function(){e.extend(this.scale,{width:this.chart.width,height:this.chart.height,size:e.min([this.chart.width,this.chart.height]),xCenter:this.chart.width/2,yCenter:this.chart.height/2}),this.updateScaleRange(this.datasets),this.scale.setScaleSize(),this.scale.buildYLabels()},draw:function(t){var i=t||1,s=this.chart.ctx;this.clear(),this.scale.draw(),e.each(this.datasets,function(t){e.each(t.points,function(t,e){t.hasValue()&&t.transition(this.scale.getPointPosition(e,this.scale.calculateCenterOffset(t.value)),i)},this),s.lineWidth=this.options.datasetStrokeWidth,s.strokeStyle=t.strokeColor,s.beginPath(),e.each(t.points,function(t,i){0===i?s.moveTo(t.x,t.y):s.lineTo(t.x,t.y)},this),s.closePath(),s.stroke(),s.fillStyle=t.fillColor,s.fill(),e.each(t.points,function(t){t.hasValue()&&t.draw()})},this)}})}.call(this); \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md index e10bc0ff1fe..f216610fd7e 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,6 @@ -Copyright (c) 2013-2015 Nick Downie +The MIT License (MIT) + +Copyright (c) 2014-2024 Chart.js Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/MAINTAINING.md b/MAINTAINING.md new file mode 100644 index 00000000000..dada06d6fab --- /dev/null +++ b/MAINTAINING.md @@ -0,0 +1,34 @@ +# Maintaining + +## Release Process + +Chart.js relies on [Travis CI](https://travis-ci.org/) to automate the library [releases](https://github.com/chartjs/Chart.js/releases). + +### Releasing a New Version + +1. Update the release version on [GitHub](https://github.com/chartjs/Chart.js/releases/new) for the release drafted by the `release-drafter` tool +2. Publish the release +3. follow the build process on [GitHub Actions](https://github.com/chartjs/Chart.js/actions?query=workflow%3A%22Node.js+Package%22) + +Creation of this tag triggers a new build: + +* `Chart.js.zip` package is generated, containing dist files and examples +* `dist/*.js`, `types/*.ts`, and `Chart.js.zip` are attached to the GitHub release (downloads) +* A new npm package is published on [npmjs](https://www.npmjs.com/package/chart.js) + +Finally, [cdnjs](https://cdnjs.com/libraries/Chart.js) is automatically updated from the npm release. + +### Releasing a patch version + +If there is a need to create a patch version for an older release: + +1. Create a branch for the patch version (without the `v` prefix) +2. Cherry pick the needed commit(s) to that new branch from master +3. Trigger the release-drafter workflow on that branch from the actions. +4. Follow the procedure for [Releasing a New Version](#releasing-a-new-version) + +### Further Reading + +* [GitHub Action releases](https://github.com/chartjs/Chart.js/pull/7891) +* [dist/* files](https://github.com/chartjs/Chart.js/issues/3033) +* [cdnjs npm auto update](https://github.com/cdnjs/cdnjs/pull/8401) diff --git a/README.md b/README.md index 57e8fa5835b..df7334ee258 100644 --- a/README.md +++ b/README.md @@ -1,61 +1,38 @@ -[![](http://tannerlinsley.com/memes/chartjs.gif)](http://www.chartjs.org/docs/) +

    + + https://www.chartjs.org/
    +
    + Simple yet flexible JavaScript charting for designers & developers +

    -# Chart.js +

    + Downloads + GitHub Workflow Status + Coverage + Awesome + Discord +

    -[![Build Status](https://travis-ci.org/nnnick/Chart.js.svg?branch=master)](https://travis-ci.org/nnnick/Chart.js) [![Code Climate](https://codeclimate.com/github/nnnick/Chart.js/badges/gpa.svg)](https://codeclimate.com/github/nnnick/Chart.js) +## Documentation +All the links point to the new version 4 of the lib. -*Simple HTML5 Charts using the canvas element* [chartjs.org](http://www.chartjs.org) +* [Introduction](https://www.chartjs.org/docs/latest/) +* [Getting Started](https://www.chartjs.org/docs/latest/getting-started/index) +* [General](https://www.chartjs.org/docs/latest/general/data-structures) +* [Configuration](https://www.chartjs.org/docs/latest/configuration/index) +* [Charts](https://www.chartjs.org/docs/latest/charts/line) +* [Axes](https://www.chartjs.org/docs/latest/axes/index) +* [Developers](https://www.chartjs.org/docs/latest/developers/index) +* [Popular Extensions](https://github.com/chartjs/awesome) +* [Samples](https://www.chartjs.org/samples/) -## v1.0.2 Stable +In case you are looking for an older version of the docs, you will have to specify the specific version in the url like this: [https://www.chartjs.org/docs/2.9.4/](https://www.chartjs.org/docs/2.9.4/) -- NPM: `npm install chart.js --save` -- Bower: `bower install Chart.js --save` -- CDN: https://cdnjs.cloudflare.com/ajax/libs/Chart.js/1.0.2/Chart.min.js -- Zip: [Download](https://github.com/nnnick/Chart.js/archive/master.zip) +## Contributing -[Examples](https://github.com/nnnick/Chart.js/tree/master/samples) | [Documentation](http://www.chartjs.org/docs/) - -## v2.0 Beta - -- Release: [2.0.0-beta](https://github.com/nnnick/Chart.js/releases/tag/2.0.0-beta) -- Zip: [Download](https://github.com/nnnick/Chart.js/archive/2.0.0-beta.zip) - -Documentation for v2.0 is currently located [here](https://github.com/nnnick/Chart.js/tree/v2.0-dev/docs). - -## v2.0 Bleeding-Edge - -- Branch: [v2.0-dev](https://github.com/nnnick/Chart.js/tree/v2.0-dev) -- Zip: [Download](https://github.com/nnnick/Chart.js/archive/v2.0-dev.zip) - -The next generation and release of Chart.js (v2.0) has been well under way this year and we are very close to releasing some amazing new features including, but not limited to: -- Rewritten, optimized, and unit-tested -- New and improved scales (log, time, linear, category, multiple scales) -- Improved Tooltips and tooltip callbacks for customization -- Improved responsiveness and resizing -- Powerful support for adding, removing, changing, and updating data on the fly -- Animations for everything, including all elements, colors and tooltips -- Powerful customization when you need it. Automatic and dynamic when you don't. -- Excellent support for modern frameworks and modular build systems. -- Even more extensible via new element controllers, core scale classes, combo chart support, and hook systems -- Bug fixes, stability improvements, etc. - -#####Contributing to 2.0 -Submit PR's to the v2.0-dev branch. - -#####Building and Testing -`gulp build`, `gulp test`, `gulp watch --test` - -#####v1.x Status: Feature Complete -v1.x is now considered feature complete. PR's for bug fixes are still extremely welcome. Any open PR's for v1.x features will need to be reconsidered, refactored and resubmitted for v2.x (if the feature has not already been implemented). For questions on new features refer to the docs in the v2.0-dev branch - - -## Bugs, Issues and Contributing - -Before submitting an issue or a pull request to the project, please take a moment to look over the [contributing guidelines](https://github.com/nnnick/Chart.js/blob/master/CONTRIBUTING.md) first. - -For support using Chart.js, please post questions with the [`chartjs` tag on Stack Overflow](http://stackoverflow.com/questions/tagged/chartjs). +Instructions on building and testing Chart.js can be found in [the documentation](https://www.chartjs.org/docs/master/developers/contributing.html#building-and-testing). Before submitting an issue or a pull request, please take a moment to look over the [contributing guidelines](https://www.chartjs.org/docs/master/developers/contributing) first. For support, please post questions on [Stack Overflow](https://stackoverflow.com/questions/tagged/chart.js) with the `chart.js` tag. ## License -Chart.js is available under the [MIT license](https://github.com/nnnick/Chart.js/blob/master/LICENSE.md). +Chart.js is available under the [MIT license](LICENSE.md). diff --git a/auto/auto.cjs b/auto/auto.cjs new file mode 100644 index 00000000000..62e08b16dfc --- /dev/null +++ b/auto/auto.cjs @@ -0,0 +1,6 @@ +const chartjs = require('../dist/chart.cjs'); +const {Chart, registerables} = chartjs; + +Chart.register(...registerables); + +module.exports = Object.assign(Chart, chartjs); diff --git a/auto/auto.d.ts b/auto/auto.d.ts new file mode 100644 index 00000000000..fb1263ae5c9 --- /dev/null +++ b/auto/auto.d.ts @@ -0,0 +1,4 @@ +import {Chart} from '../dist/types.js'; + +export * from '../dist/types.js'; +export default Chart; diff --git a/auto/auto.js b/auto/auto.js new file mode 100644 index 00000000000..924a0f900ed --- /dev/null +++ b/auto/auto.js @@ -0,0 +1,6 @@ +import {Chart, registerables} from '../dist/chart.js'; + +Chart.register(...registerables); + +export * from '../dist/chart.js'; +export default Chart; diff --git a/auto/package.json b/auto/package.json new file mode 100644 index 00000000000..7e0ca323fb2 --- /dev/null +++ b/auto/package.json @@ -0,0 +1,14 @@ +{ + "name": "chart.js-auto", + "private": true, + "description": "Auto registering package. Exists to support bundlers without exports support such as webpack 4.", + "type": "module", + "main": "./auto.cjs", + "module": "./auto.js", + "exports": { + "types": "./auto.d.ts", + "import": "./auto.js", + "require": "./auto.cjs" + }, + "types": "./auto.d.ts" +} diff --git a/bower.json b/bower.json deleted file mode 100644 index a67620348c2..00000000000 --- a/bower.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "Chart.js", - "version": "1.0.2", - "description": "Simple HTML5 Charts using the canvas element", - "homepage": "https://github.com/nnnick/Chart.js", - "author": "nnnick", - "main": [ - "Chart.js" - ], - "ignore": [ - "**/*", - ".travis.yml", - "CONTRIBUTING.md", - "Chart.js", - "LICENSE.md", - "README.md", - "gulpfile.js", - "package.json" - ], - "dependencies": {} -} diff --git a/composer.json b/composer.json index 48d05b8dcb7..b332bb0f595 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,7 @@ "chart", "js" ], - "homepage": "http://www.chartjs.org/", + "homepage": "https://www.chartjs.org/", "license": "MIT", "authors": [ { diff --git a/docs/.vuepress/config.ts b/docs/.vuepress/config.ts new file mode 100644 index 00000000000..ccb310094cf --- /dev/null +++ b/docs/.vuepress/config.ts @@ -0,0 +1,395 @@ +import * as path from 'path'; +import markdownItInclude from 'markdown-it-include'; +import { DefaultThemeConfig, defineConfig, PluginTuple } from 'vuepress/config'; + +const docsVersion = "VERSION"; +const base: `/${string}/` = process.env.NODE_ENV === "development" ? '/docs/master/' : `/docs/${docsVersion}/`; + +export default defineConfig({ + title: 'Chart.js', + description: 'Open source HTML5 Charts for your website', + theme: 'chartjs', + base, + dest: path.resolve(__dirname, '../../dist/docs'), + head: [ + ['link', {rel: 'icon', href: '/favicon.ico'}], + ], + plugins: [ + 'tabs', + ['flexsearch'], + ['@vuepress/html-redirect', { + countdown: 0, + }], + [ + '@vuepress/google-analytics', + { + 'ga': 'UA-28909194-3' + } + ], + ['redirect', { + redirectors: [ + // Default sample page when accessing /samples. + {base: '/samples', alternative: ['information']}, + ], + }], + ['vuepress-plugin-code-copy', true], + ['vuepress-plugin-typedoc', { + entryPoints: ['../../src/types/index.d.ts'], + hideInPageTOC: true, + tsconfig: path.resolve(__dirname, '../../tsconfig.json'), + }, + ], + ['@simonbrunel/vuepress-plugin-versions', { + filters: { + suffix: (tag) => tag ? ` (${tag})` : '', + title: (v, vars) => { + return window.location.href.includes('master') ? 'Development (master)' : + vars.tag === 'latest' ? 'Latest (' + v + ')' : + v + (vars.tag ? ` (${vars.tag})` : '') + ' (outdated)'; + }, + }, + menu: { + text: '{{version|title}}', + items: [ + { + text: 'Documentation', + items: [ + { + text: 'Development (master)', + link: '/docs/master/', + }, + { + text: 'Latest version', + link: '/docs/latest/', + }, + { + type: 'versions', + text: '{{version}}{{tag|suffix}}', + link: '/docs/{{version}}/', + exclude: /^[01]\.|2\.[0-5]\./, + group: 'minor', + } + ] + }, + { + text: 'Release notes (5 latest)', + items: [ + { + type: 'versions', + limit: 5, + target: '_blank', + group: 'patch', + link: 'https://github.com/chartjs/Chart.js/releases/tag/v{{version}}' + } + ] + } + ] + }, + }], + ] as PluginTuple[], + chainWebpack(config) { + config.merge({ + resolve: { + alias: { + 'chart.js': path.resolve(__dirname, '../../dist/chart.js'), + } + } + }) + + config.module.rule('images').use('url-loader').tap(options => ({ + ...options, + esModule: false + })) + }, + markdown: { + extendMarkdown: md => { + md.use(markdownItInclude, path.resolve(__dirname, '../')); + } + }, + themeConfig: { + repo: 'chartjs/Chart.js', + logo: '/favicon.ico', + lastUpdated: 'Last Updated', + searchPlaceholder: 'Search...', + editLinks: false, + docsDir: 'docs', + chart: { + imports: [ + ['scripts/register.js'], + ['scripts/utils.js', 'Utils'], + ['scripts/helpers.js', 'helpers'], + ['scripts/components.js', 'components'] + ] + }, + nav: [ + {text: 'Home', link: '/'}, + {text: 'API', link: '/api/'}, + {text: 'Samples', link: `/samples/`}, + { + text: 'Ecosystem', + ariaLabel: 'Community Menu', + items: [ + { text: 'Awesome', link: 'https://github.com/chartjs/awesome' }, + { text: 'Discord', link: 'https://discord.gg/HxEguTK6av' }, + { text: 'Stack Overflow', link: 'https://stackoverflow.com/questions/tagged/chart.js' } + ] + } + ], + sidebar: { + '/api/': 'API', + '/samples/': [ + 'information', + { + title: 'Bar Charts', + children: [ + 'bar/border-radius', + 'bar/floating', + 'bar/horizontal', + 'bar/stacked', + 'bar/stacked-groups', + 'bar/vertical', + ] + }, + { + title: 'Line Charts', + children: [ + 'line/interpolation', + 'line/line', + 'line/multi-axis', + 'line/point-styling', + 'line/segments', + 'line/stepped', + 'line/styling', + ] + }, + { + title: 'Other charts', + children: [ + 'other-charts/bubble', + 'other-charts/combo-bar-line', + 'other-charts/doughnut', + 'other-charts/multi-series-pie', + 'other-charts/pie', + 'other-charts/polar-area', + 'other-charts/polar-area-center-labels', + 'other-charts/radar', + 'other-charts/radar-skip-points', + 'other-charts/scatter', + 'other-charts/scatter-multi-axis', + 'other-charts/stacked-bar-line', + ] + }, + { + title: 'Area charts', + children: [ + 'area/line-boundaries', + 'area/line-datasets', + 'area/line-drawtime', + 'area/line-stacked', + 'area/radar' + ] + }, + { + title: 'Scales', + children: [ + 'scales/linear-min-max', + 'scales/linear-min-max-suggested', + 'scales/linear-step-size', + 'scales/log', + 'scales/stacked', + 'scales/time-line', + 'scales/time-max-span', + 'scales/time-combo', + ] + }, + { + title: 'Scale Options', + children: [ + 'scale-options/center', + 'scale-options/grid', + 'scale-options/ticks', + 'scale-options/titles', + ] + }, + { + title: 'Legend', + children: [ + 'legend/events', + 'legend/html', + 'legend/point-style', + 'legend/position', + 'legend/title', + ] + }, + { + title: 'Title', + children: [ + 'title/alignment', + ] + }, + { + title: 'Subtitle', + children: [ + 'subtitle/basic', + ] + }, { + title: 'Tooltip', + children: [ + 'tooltip/content', + 'tooltip/html', + 'tooltip/interactions', + 'tooltip/point-style', + 'tooltip/position', + ] + }, + { + title: 'Scriptable Options', + children: [ + 'scriptable/bar', + 'scriptable/bubble', + 'scriptable/line', + 'scriptable/pie', + 'scriptable/polar', + 'scriptable/radar', + ] + }, + { + title: 'Animations', + children: [ + 'animations/delay', + 'animations/drop', + 'animations/loop', + 'animations/progressive-line', + 'animations/progressive-line-easing', + ] + }, + { + title: 'Advanced', + children: [ + 'advanced/data-decimation', + 'advanced/derived-axis-type', + 'advanced/derived-chart-type', + 'advanced/linear-gradient', + 'advanced/programmatic-events', + 'advanced/progress-bar', + 'advanced/radial-gradient', + ] + }, + { + title: 'Plugins', + children: [ + 'plugins/chart-area-border', + 'plugins/doughnut-empty-state', + 'plugins/quadrants', + ] + }, + 'utils' + ], + '/': [ + '', + { + title: 'Getting Started', + children: [ + 'getting-started/', + 'getting-started/installation', + 'getting-started/integration', + 'getting-started/usage', + 'getting-started/using-from-node-js', + ] + }, + { + title: 'General', + children: [ + 'general/accessibility', + 'general/colors', + 'general/data-structures', + 'general/fonts', + 'general/options', + 'general/padding', + 'general/performance' + ] + }, + { + title: 'Configuration', + children: [ + 'configuration/', + 'configuration/animations', + 'configuration/canvas-background', + 'configuration/decimation', + 'configuration/device-pixel-ratio', + 'configuration/elements', + 'configuration/interactions', + 'configuration/layout', + 'configuration/legend', + 'configuration/locale', + 'configuration/responsive', + 'configuration/subtitle', + 'configuration/title', + 'configuration/tooltip', + ] + }, + { + title: 'Chart Types', + children: [ + 'charts/area', + 'charts/bar', + 'charts/bubble', + 'charts/doughnut', + 'charts/line', + 'charts/mixed', + 'charts/polar', + 'charts/radar', + 'charts/scatter', + ] + }, + { + title: 'Axes', + children: [ + 'axes/', + { + title: 'Cartesian', + children: [ + 'axes/cartesian/', + 'axes/cartesian/category', + 'axes/cartesian/linear', + 'axes/cartesian/logarithmic', + 'axes/cartesian/time', + 'axes/cartesian/timeseries' + ], + }, + { + title: 'Radial', + children: [ + 'axes/radial/', + 'axes/radial/linear' + ], + }, + 'axes/labelling', + 'axes/styling' + ] + }, + { + title: 'Developers', + children: [ + 'developers/', + 'developers/api', + 'developers/axes', + 'developers/charts', + 'developers/contributing', + 'developers/plugins', + 'developers/publishing', + ['api/', 'TypeDoc'], + 'developers/updates', + ] + }, + { + title: 'Migration', + children: [ + 'migration/v4-migration', + 'migration/v3-migration', + ] + }, + ], + } as any + } as DefaultThemeConfig +}); diff --git a/docs/.vuepress/public/favicon.ico b/docs/.vuepress/public/favicon.ico new file mode 100644 index 00000000000..5192a328567 Binary files /dev/null and b/docs/.vuepress/public/favicon.ico differ diff --git a/docs/.vuepress/public/logo.png b/docs/.vuepress/public/logo.png new file mode 100644 index 00000000000..f6639e13e8c Binary files /dev/null and b/docs/.vuepress/public/logo.png differ diff --git a/docs/.vuepress/public/logo.svg b/docs/.vuepress/public/logo.svg new file mode 100644 index 00000000000..69be2425994 --- /dev/null +++ b/docs/.vuepress/public/logo.svg @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/docs/.vuepress/redirects b/docs/.vuepress/redirects new file mode 100644 index 00000000000..50ae48acf3e --- /dev/null +++ b/docs/.vuepress/redirects @@ -0,0 +1,4 @@ +/charts/ /charts/line.html +/general/ /general/data-structures.html +/samples/ /samples/information.html +/getting-started/v3-migration/ /migration/v3-migration.html diff --git a/docs/.vuepress/styles/index.styl b/docs/.vuepress/styles/index.styl new file mode 100644 index 00000000000..612daf1f3e6 --- /dev/null +++ b/docs/.vuepress/styles/index.styl @@ -0,0 +1,43 @@ +@require '~vuepress-plugin-tabs/dist/themes/default.styl' + +.theme-default-content + &:not(.custom) + max-width: unset + + .chart-view + max-width 800px + +.sidebar-group.is-sub-group.depth-1 + > .sidebar-group-items + border-left 1px solid rgba($accentColor, 0.25) + + > .sidebar-heading:not(.open) + border-left 1px solid rgba($accentColor, 0.25) + margin-left: 0 + + > .sidebar-heading + padding-left calc(1.475rem - 1px) + transition border-color .25s + padding 0.35rem 1.475rem + border-left-width 3px + margin-left -1px + font-size 1em + line-height 1.4 + opacity 1 !important + + &.active, &.open + border-left-color $accentColor + color $accentColor + font-weight bold + + >.arrow + display none + + >.sidebar-group-items + padding-left: 0 + +.sidebar-group.is-sub-group.depth-1:hover .sidebar-heading:not(.open) + color $accentColor + margin-left -1px + border-left 3px solid rgba($accentColor, 0.25) + padding-left calc(1.475rem - 1px) diff --git a/docs/00-Getting-Started.md b/docs/00-Getting-Started.md deleted file mode 100644 index a16692d53af..00000000000 --- a/docs/00-Getting-Started.md +++ /dev/null @@ -1,228 +0,0 @@ ---- -title: Getting started -anchor: getting-started ---- - -###Include Chart.js - -First we need to include the Chart.js library on the page. The library occupies a global variable of `Chart`. - -```html - -``` - -Alternatively, if you're using an AMD loader for JavaScript modules, that is also supported in the Chart.js core. Please note: the library will still occupy a global variable of `Chart`, even if it detects `define` and `define.amd`. If this is a problem, you can call `noConflict` to restore the global Chart variable to its previous owner. - -```javascript -// Using requirejs -require(['path/to/Chartjs'], function(Chart){ - // Use Chart.js as normal here. - - // Chart.noConflict restores the Chart global variable to its previous owner - // The function returns what was previously Chart, allowing you to reassign. - var Chartjs = Chart.noConflict(); - -}); -``` - -You can also grab Chart.js using bower: - -```bash -bower install Chart.js --save -``` - -or NPM: - -```bash -npm install chart.js --save -``` - -Also, Chart.js is available from CDN: - -https://cdnjs.cloudflare.com/ajax/libs/Chart.js/1.0.2/Chart.min.js - -###Creating a chart - -To create a chart, we need to instantiate the `Chart` class. To do this, we need to pass in the 2d context of where we want to draw the chart. Here's an example. - -```html - -``` - -```javascript -// Get the context of the canvas element we want to select -var ctx = document.getElementById("myChart").getContext("2d"); -var myNewChart = new Chart(ctx).PolarArea(data); -``` - -We can also get the context of our canvas with jQuery. To do this, we need to get the DOM node out of the jQuery collection, and call the `getContext("2d")` method on that. - -```javascript -// Get context with jQuery - using jQuery's .get() method. -var ctx = $("#myChart").get(0).getContext("2d"); -// This will get the first returned node in the jQuery collection. -var myNewChart = new Chart(ctx); -``` - -After we've instantiated the Chart class on the canvas we want to draw on, Chart.js will handle the scaling for retina displays. - -With the Chart class set up, we can go on to create one of the charts Chart.js has available. In the example below, we would be drawing a Polar area chart. - -```javascript -new Chart(ctx).PolarArea(data, options); -``` - -We call a method of the name of the chart we want to create. We pass in the data for that chart type, and the options for that chart as parameters. Chart.js will merge the global defaults with chart type specific defaults, then merge any options passed in as a second argument after data. - -###Global chart configuration - -This concept was introduced in Chart.js 1.0 to keep configuration DRY, and allow for changing options globally across chart types, avoiding the need to specify options for each instance, or the default for a particular chart type. - -Templates are based on micro templating by John Resig: - -http://ejohn.org/blog/javascript-micro-templating/ - -```javascript -Chart.defaults.global = { - // Boolean - Whether to animate the chart - animation: true, - - // Number - Number of animation steps - animationSteps: 60, - - // String - Animation easing effect - // Possible effects are: - // [easeInOutQuart, linear, easeOutBounce, easeInBack, easeInOutQuad, - // easeOutQuart, easeOutQuad, easeInOutBounce, easeOutSine, easeInOutCubic, - // easeInExpo, easeInOutBack, easeInCirc, easeInOutElastic, easeOutBack, - // easeInQuad, easeInOutExpo, easeInQuart, easeOutQuint, easeInOutCirc, - // easeInSine, easeOutExpo, easeOutCirc, easeOutCubic, easeInQuint, - // easeInElastic, easeInOutSine, easeInOutQuint, easeInBounce, - // easeOutElastic, easeInCubic] - animationEasing: "easeOutQuart", - - // Boolean - If we should show the scale at all - showScale: true, - - // Boolean - If we want to override with a hard coded scale - scaleOverride: false, - - // ** Required if scaleOverride is true ** - // Number - The number of steps in a hard coded scale - scaleSteps: null, - // Number - The value jump in the hard coded scale - scaleStepWidth: null, - // Number - The scale starting value - scaleStartValue: null, - - // String - Colour of the scale line - scaleLineColor: "rgba(0,0,0,.1)", - - // Number - Pixel width of the scale line - scaleLineWidth: 1, - - // Boolean - Whether to show labels on the scale - scaleShowLabels: true, - - // Interpolated JS string - can access value - scaleLabel: "<%=value%>", - - // Boolean - Whether the scale should stick to integers, not floats even if drawing space is there - scaleIntegersOnly: true, - - // Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value - scaleBeginAtZero: false, - - // String - Scale label font declaration for the scale label - scaleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", - - // Number - Scale label font size in pixels - scaleFontSize: 12, - - // String - Scale label font weight style - scaleFontStyle: "normal", - - // String - Scale label font colour - scaleFontColor: "#666", - - // Boolean - whether or not the chart should be responsive and resize when the browser does. - responsive: false, - - // Boolean - whether to maintain the starting aspect ratio or not when responsive, if set to false, will take up entire container - maintainAspectRatio: true, - - // Boolean - Determines whether to draw tooltips on the canvas or not - showTooltips: true, - - // Function - Determines whether to execute the customTooltips function instead of drawing the built in tooltips (See [Advanced - External Tooltips](#advanced-usage-external-tooltips)) - customTooltips: false, - - // Array - Array of string names to attach tooltip events - tooltipEvents: ["mousemove", "touchstart", "touchmove"], - - // String - Tooltip background colour - tooltipFillColor: "rgba(0,0,0,0.8)", - - // String - Tooltip label font declaration for the scale label - tooltipFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", - - // Number - Tooltip label font size in pixels - tooltipFontSize: 14, - - // String - Tooltip font weight style - tooltipFontStyle: "normal", - - // String - Tooltip label font colour - tooltipFontColor: "#fff", - - // String - Tooltip title font declaration for the scale label - tooltipTitleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", - - // Number - Tooltip title font size in pixels - tooltipTitleFontSize: 14, - - // String - Tooltip title font weight style - tooltipTitleFontStyle: "bold", - - // String - Tooltip title font colour - tooltipTitleFontColor: "#fff", - - // String - Tooltip title template - tooltipTitleTemplate: "<%= label%>", - - // Number - pixel width of padding around tooltip text - tooltipYPadding: 6, - - // Number - pixel width of padding around tooltip text - tooltipXPadding: 6, - - // Number - Size of the caret on the tooltip - tooltipCaretSize: 8, - - // Number - Pixel radius of the tooltip border - tooltipCornerRadius: 6, - - // Number - Pixel offset from point x to tooltip edge - tooltipXOffset: 10, - {% raw %} - // String - Template string for single tooltips - tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= value %>", - {% endraw %} - // String - Template string for multiple tooltips - multiTooltipTemplate: "<%= value %>", - - // Function - Will fire on animation progression. - onAnimationProgress: function(){}, - - // Function - Will fire on animation completion. - onAnimationComplete: function(){} -} -``` - -If for example, you wanted all charts created to be responsive, and resize when the browser window does, the following setting can be changed: - -```javascript -Chart.defaults.global.responsive = true; -``` - -Now, every time we create a chart, `options.responsive` will be `true`. diff --git a/docs/01-Line-Chart.md b/docs/01-Line-Chart.md deleted file mode 100644 index a4b508e776b..00000000000 --- a/docs/01-Line-Chart.md +++ /dev/null @@ -1,169 +0,0 @@ ---- -title: Line Chart -anchor: line-chart ---- -###Introduction -A line chart is a way of plotting data points on a line. - -Often, it is used to show trend data, and the comparison of two data sets. - -
    - -
    - -###Example usage -```javascript -var myLineChart = new Chart(ctx).Line(data, options); -``` -###Data structure - -```javascript -var data = { - labels: ["January", "February", "March", "April", "May", "June", "July"], - datasets: [ - { - label: "My First dataset", - fillColor: "rgba(220,220,220,0.2)", - strokeColor: "rgba(220,220,220,1)", - pointColor: "rgba(220,220,220,1)", - pointStrokeColor: "#fff", - pointHighlightFill: "#fff", - pointHighlightStroke: "rgba(220,220,220,1)", - data: [65, 59, 80, 81, 56, 55, 40] - }, - { - label: "My Second dataset", - fillColor: "rgba(151,187,205,0.2)", - strokeColor: "rgba(151,187,205,1)", - pointColor: "rgba(151,187,205,1)", - pointStrokeColor: "#fff", - pointHighlightFill: "#fff", - pointHighlightStroke: "rgba(151,187,205,1)", - data: [28, 48, 40, 19, 86, 27, 90] - } - ] -}; -``` - -The line chart requires an array of labels for each of the data points. This is shown on the X axis. -The data for line charts is broken up into an array of datasets. Each dataset has a colour for the fill, a colour for the line and colours for the points and strokes of the points. These colours are strings just like CSS. You can use RGBA, RGB, HEX or HSL notation. - -The label key on each dataset is optional, and can be used when generating a scale for the chart. - -### Chart options - -These are the customisation options specific to Line charts. These options are merged with the [global chart configuration options](#getting-started-global-chart-configuration), and form the options of the chart. - -```javascript -{ - - ///Boolean - Whether grid lines are shown across the chart - scaleShowGridLines : true, - - //String - Colour of the grid lines - scaleGridLineColor : "rgba(0,0,0,.05)", - - //Number - Width of the grid lines - scaleGridLineWidth : 1, - - //Boolean - Whether to show horizontal lines (except X axis) - scaleShowHorizontalLines: true, - - //Boolean - Whether to show vertical lines (except Y axis) - scaleShowVerticalLines: true, - - //Boolean - Whether the line is curved between points - bezierCurve : true, - - //Number - Tension of the bezier curve between points - bezierCurveTension : 0.4, - - //Boolean - Whether to show a dot for each point - pointDot : true, - - //Number - Radius of each point dot in pixels - pointDotRadius : 4, - - //Number - Pixel width of point dot stroke - pointDotStrokeWidth : 1, - - //Number - amount extra to add to the radius to cater for hit detection outside the drawn point - pointHitDetectionRadius : 20, - - //Boolean - Whether to show a stroke for datasets - datasetStroke : true, - - //Number - Pixel width of dataset stroke - datasetStrokeWidth : 2, - - //Boolean - Whether to fill the dataset with a colour - datasetFill : true, - {% raw %} - //String - A legend template - legendTemplate : "
      -legend\"><% for (var i=0; i
    • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
    • <%}%>
    " - {% endraw %} - - //Boolean - Whether to horizontally center the label and point dot inside the grid - offsetGridLines : false -}; -``` - -You can override these for your `Chart` instance by passing a second argument into the `Line` method as an object with the keys you want to override. - -For example, we could have a line chart without bezier curves between points by doing the following: - -```javascript -new Chart(ctx).Line(data, { - bezierCurve: false -}); -// This will create a chart with all of the default options, merged from the global config, -// and the Line chart defaults, but this particular instance will have `bezierCurve` set to false. -``` - -We can also change these defaults values for each Line type that is created, this object is available at `Chart.defaults.Line`. - - -### Prototype methods - -#### .getPointsAtEvent( event ) - -Calling `getPointsAtEvent(event)` on your Chart instance passing an argument of an event, or jQuery event, will return the point elements that are at that the same position of that event. - -```javascript -canvas.onclick = function(evt){ - var activePoints = myLineChart.getPointsAtEvent(evt); - // => activePoints is an array of points on the canvas that are at the same position as the click event. -}; -``` - -This functionality may be useful for implementing DOM based tooltips, or triggering custom behaviour in your application. - -#### .update( ) - -Calling `update()` on your Chart instance will re-render the chart with any updated values, allowing you to edit the value of multiple existing points, then render those in one animated render loop. - -```javascript -myLineChart.datasets[0].points[2].value = 50; -// Would update the first dataset's value of 'March' to be 50 -myLineChart.update(); -// Calling update now animates the position of March from 90 to 50. -``` - -#### .addData( valuesArray, label ) - -Calling `addData(valuesArray, label)` on your Chart instance passing an array of values for each dataset, along with a label for those points. - -```javascript -// The values array passed into addData should be one for each dataset in the chart -myLineChart.addData([40, 60], "August"); -// This new data will now animate at the end of the chart. -``` - -#### .removeData( ) - -Calling `removeData()` on your Chart instance will remove the first value for all datasets on the chart. - -```javascript -myLineChart.removeData(); -// The chart will remove the first point and animate other points into place -``` diff --git a/docs/02-Bar-Chart.md b/docs/02-Bar-Chart.md deleted file mode 100644 index 6911db900be..00000000000 --- a/docs/02-Bar-Chart.md +++ /dev/null @@ -1,149 +0,0 @@ ---- -title: Bar Chart -anchor: bar-chart ---- - -### Introduction -A bar chart is a way of showing data as bars. - -It is sometimes used to show trend data, and the comparison of multiple data sets side by side. - -
    - -
    - -### Example usage -```javascript -var myBarChart = new Chart(ctx).Bar(data, options); -``` - -### Data structure - -```javascript -var data = { - labels: ["January", "February", "March", "April", "May", "June", "July"], - datasets: [ - { - label: "My First dataset", - fillColor: "rgba(220,220,220,0.5)", - strokeColor: "rgba(220,220,220,0.8)", - highlightFill: "rgba(220,220,220,0.75)", - highlightStroke: "rgba(220,220,220,1)", - data: [65, 59, 80, 81, 56, 55, 40] - }, - { - label: "My Second dataset", - fillColor: "rgba(151,187,205,0.5)", - strokeColor: "rgba(151,187,205,0.8)", - highlightFill: "rgba(151,187,205,0.75)", - highlightStroke: "rgba(151,187,205,1)", - data: [28, 48, 40, 19, 86, 27, 90] - } - ] -}; -``` -The bar chart has the a very similar data structure to the line chart, and has an array of datasets, each with colours and an array of data. Again, colours are in CSS format. -We have an array of labels too for display. In the example, we are showing the same data as the previous line chart example. - -The label key on each dataset is optional, and can be used when generating a scale for the chart. - -### Chart Options - -These are the customisation options specific to Bar charts. These options are merged with the [global chart configuration options](#getting-started-global-chart-configuration), and form the options of the chart. - -```javascript -{ - //Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value - scaleBeginAtZero : true, - - //Boolean - Whether grid lines are shown across the chart - scaleShowGridLines : true, - - //String - Colour of the grid lines - scaleGridLineColor : "rgba(0,0,0,.05)", - - //Number - Width of the grid lines - scaleGridLineWidth : 1, - - //Boolean - Whether to show horizontal lines (except X axis) - scaleShowHorizontalLines: true, - - //Boolean - Whether to show vertical lines (except Y axis) - scaleShowVerticalLines: true, - - //Boolean - If there is a stroke on each bar - barShowStroke : true, - - //Number - Pixel width of the bar stroke - barStrokeWidth : 2, - - //Number - Spacing between each of the X value sets - barValueSpacing : 5, - - //Number - Spacing between data sets within X values - barDatasetSpacing : 1, - {% raw %} - //String - A legend template - legendTemplate : "
      -legend\"><% for (var i=0; i
    • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
    • <%}%>
    " - {% endraw %} -} -``` - -You can override these for your `Chart` instance by passing a second argument into the `Bar` method as an object with the keys you want to override. - -For example, we could have a bar chart without a stroke on each bar by doing the following: - -```javascript -new Chart(ctx).Bar(data, { - barShowStroke: false -}); -// This will create a chart with all of the default options, merged from the global config, -// and the Bar chart defaults but this particular instance will have `barShowStroke` set to false. -``` - -We can also change these defaults values for each Bar type that is created, this object is available at `Chart.defaults.Bar`. - -### Prototype methods - -#### .getBarsAtEvent( event ) - -Calling `getBarsAtEvent(event)` on your Chart instance passing an argument of an event, or jQuery event, will return the bar elements that are at that the same position of that event. - -```javascript -canvas.onclick = function(evt){ - var activeBars = myBarChart.getBarsAtEvent(evt); - // => activeBars is an array of bars on the canvas that are at the same position as the click event. -}; -``` - -This functionality may be useful for implementing DOM based tooltips, or triggering custom behaviour in your application. - -#### .update( ) - -Calling `update()` on your Chart instance will re-render the chart with any updated values, allowing you to edit the value of multiple existing points, then render those in one animated render loop. - -```javascript -myBarChart.datasets[0].bars[2].value = 50; -// Would update the first dataset's value of 'March' to be 50 -myBarChart.update(); -// Calling update now animates the position of March from 90 to 50. -``` - -#### .addData( valuesArray, label ) - -Calling `addData(valuesArray, label)` on your Chart instance passing an array of values for each dataset, along with a label for those bars. - -```javascript -// The values array passed into addData should be one for each dataset in the chart -myBarChart.addData([40, 60], "August"); -// The new data will now animate at the end of the chart. -``` - -#### .removeData( ) - -Calling `removeData()` on your Chart instance will remove the first value for all datasets on the chart. - -```javascript -myBarChart.removeData(); -// The chart will now animate and remove the first bar -``` diff --git a/docs/03-Radar-Chart.md b/docs/03-Radar-Chart.md deleted file mode 100644 index aff5a00db27..00000000000 --- a/docs/03-Radar-Chart.md +++ /dev/null @@ -1,177 +0,0 @@ ---- -title: Radar Chart -anchor: radar-chart ---- - -###Introduction -A radar chart is a way of showing multiple data points and the variation between them. - -They are often useful for comparing the points of two or more different data sets. - -
    - -
    - -###Example usage - -```javascript -var myRadarChart = new Chart(ctx).Radar(data, options); -``` - -###Data structure -```javascript -var data = { - labels: ["Eating", "Drinking", "Sleeping", "Designing", "Coding", "Cycling", "Running"], - datasets: [ - { - label: "My First dataset", - fillColor: "rgba(220,220,220,0.2)", - strokeColor: "rgba(220,220,220,1)", - pointColor: "rgba(220,220,220,1)", - pointStrokeColor: "#fff", - pointHighlightFill: "#fff", - pointHighlightStroke: "rgba(220,220,220,1)", - data: [65, 59, 90, 81, 56, 55, 40] - }, - { - label: "My Second dataset", - fillColor: "rgba(151,187,205,0.2)", - strokeColor: "rgba(151,187,205,1)", - pointColor: "rgba(151,187,205,1)", - pointStrokeColor: "#fff", - pointHighlightFill: "#fff", - pointHighlightStroke: "rgba(151,187,205,1)", - data: [28, 48, 40, 19, 96, 27, 100] - } - ] -}; -``` -For a radar chart, to provide context of what each point means, we include an array of strings that show around each point in the chart. -For the radar chart data, we have an array of datasets. Each of these is an object, with a fill colour, a stroke colour, a colour for the fill of each point, and a colour for the stroke of each point. We also have an array of data values. - -The label key on each dataset is optional, and can be used when generating a scale for the chart. - -### Chart options - -These are the customisation options specific to Radar charts. These options are merged with the [global chart configuration options](#getting-started-global-chart-configuration), and form the options of the chart. - - -```javascript -{ - //Boolean - Whether to show lines for each scale point - scaleShowLine : true, - - //Boolean - Whether we show the angle lines out of the radar - angleShowLineOut : true, - - //Boolean - Whether to show labels on the scale - scaleShowLabels : false, - - // Boolean - Whether the scale should begin at zero - scaleBeginAtZero : true, - - //String - Colour of the angle line - angleLineColor : "rgba(0,0,0,.1)", - - //Number - Pixel width of the angle line - angleLineWidth : 1, - - //String - Point label font declaration - pointLabelFontFamily : "'Arial'", - - //String - Point label font weight - pointLabelFontStyle : "normal", - - //Number - Point label font size in pixels - pointLabelFontSize : 10, - - //String - Point label font colour - pointLabelFontColor : "#666", - - //Boolean - Whether to show a dot for each point - pointDot : true, - - //Number - Radius of each point dot in pixels - pointDotRadius : 3, - - //Number - Pixel width of point dot stroke - pointDotStrokeWidth : 1, - - //Number - amount extra to add to the radius to cater for hit detection outside the drawn point - pointHitDetectionRadius : 20, - - //Boolean - Whether to show a stroke for datasets - datasetStroke : true, - - //Number - Pixel width of dataset stroke - datasetStrokeWidth : 2, - - //Boolean - Whether to fill the dataset with a colour - datasetFill : true, - {% raw %} - //String - A legend template - legendTemplate : "
      -legend\"><% for (var i=0; i
    • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
    • <%}%>
    " - {% endraw %} -} -``` - - -You can override these for your `Chart` instance by passing a second argument into the `Radar` method as an object with the keys you want to override. - -For example, we could have a radar chart without a point for each on piece of data by doing the following: - -```javascript -new Chart(ctx).Radar(data, { - pointDot: false -}); -// This will create a chart with all of the default options, merged from the global config, -// and the Bar chart defaults but this particular instance will have `pointDot` set to false. -``` - -We can also change these defaults values for each Radar type that is created, this object is available at `Chart.defaults.Radar`. - - -### Prototype methods - -#### .getPointsAtEvent( event ) - -Calling `getPointsAtEvent(event)` on your Chart instance passing an argument of an event, or jQuery event, will return the point elements that are at that the same position of that event. - -```javascript -canvas.onclick = function(evt){ - var activePoints = myRadarChart.getPointsAtEvent(evt); - // => activePoints is an array of points on the canvas that are at the same position as the click event. -}; -``` - -This functionality may be useful for implementing DOM based tooltips, or triggering custom behaviour in your application. - -#### .update( ) - -Calling `update()` on your Chart instance will re-render the chart with any updated values, allowing you to edit the value of multiple existing points, then render those in one animated render loop. - -```javascript -myRadarChart.datasets[0].points[2].value = 50; -// Would update the first dataset's value of 'Sleeping' to be 50 -myRadarChart.update(); -// Calling update now animates the position of Sleeping from 90 to 50. -``` - -#### .addData( valuesArray, label ) - -Calling `addData(valuesArray, label)` on your Chart instance passing an array of values for each dataset, along with a label for those points. - -```javascript -// The values array passed into addData should be one for each dataset in the chart -myRadarChart.addData([40, 60], "Dancing"); -// The new data will now animate at the end of the chart. -``` - -#### .removeData( ) - -Calling `removeData()` on your Chart instance will remove the first value for all datasets on the chart. - -```javascript -myRadarChart.removeData(); -// Other points will now animate to their correct positions. -``` \ No newline at end of file diff --git a/docs/04-Polar-Area-Chart.md b/docs/04-Polar-Area-Chart.md deleted file mode 100644 index 47c9a74d0c8..00000000000 --- a/docs/04-Polar-Area-Chart.md +++ /dev/null @@ -1,172 +0,0 @@ ---- -title: Polar Area Chart -anchor: polar-area-chart ---- -### Introduction -Polar area charts are similar to pie charts, but each segment has the same angle - the radius of the segment differs depending on the value. - -This type of chart is often useful when we want to show a comparison data similar to a pie chart, but also show a scale of values for context. - -
    - -
    - -### Example usage - -```javascript -new Chart(ctx).PolarArea(data, options); -``` - -### Data structure - -```javascript -var data = [ - { - value: 300, - color:"#F7464A", - highlight: "#FF5A5E", - label: "Red" - }, - { - value: 50, - color: "#46BFBD", - highlight: "#5AD3D1", - label: "Green" - }, - { - value: 100, - color: "#FDB45C", - highlight: "#FFC870", - label: "Yellow" - }, - { - value: 40, - color: "#949FB1", - highlight: "#A8B3C5", - label: "Grey" - }, - { - value: 120, - color: "#4D5360", - highlight: "#616774", - label: "Dark Grey" - } - -]; -``` -As you can see, for the chart data you pass in an array of objects, with a value and a colour. The value attribute should be a number, while the color attribute should be a string. Similar to CSS, for this string you can use HEX notation, RGB, RGBA or HSL. - -### Chart options - -These are the customisation options specific to Polar Area charts. These options are merged with the [global chart configuration options](#getting-started-global-chart-configuration), and form the options of the chart. - -```javascript -{ - //Boolean - Show a backdrop to the scale label - scaleShowLabelBackdrop : true, - - //String - The colour of the label backdrop - scaleBackdropColor : "rgba(255,255,255,0.75)", - - // Boolean - Whether the scale should begin at zero - scaleBeginAtZero : true, - - //Number - The backdrop padding above & below the label in pixels - scaleBackdropPaddingY : 2, - - //Number - The backdrop padding to the side of the label in pixels - scaleBackdropPaddingX : 2, - - //Boolean - Show line for each value in the scale - scaleShowLine : true, - - //Boolean - Stroke a line around each segment in the chart - segmentShowStroke : true, - - //String - The colour of the stroke on each segment. - segmentStrokeColor : "#fff", - - //Number - The width of the stroke value in pixels - segmentStrokeWidth : 2, - - //Number - Amount of animation steps - animationSteps : 100, - - //String - Animation easing effect. - animationEasing : "easeOutBounce", - - //Boolean - Whether to animate the rotation of the chart - animateRotate : true, - - //Boolean - Whether to animate scaling the chart from the centre - animateScale : false, - {% raw %} - //String - A legend template - legendTemplate : "
      -legend\"><% for (var i=0; i
    • \"><%if(segments[i].label){%><%=segments[i].label%><%}%>
    • <%}%>
    " - {% endraw %} -} -``` - -You can override these for your `Chart` instance by passing a second argument into the `PolarArea` method as an object with the keys you want to override. - -For example, we could have a polar area chart with a black stroke on each segment like so: - -```javascript -new Chart(ctx).PolarArea(data, { - segmentStrokeColor: "#000000" -}); -// This will create a chart with all of the default options, merged from the global config, -// and the PolarArea chart defaults but this particular instance will have `segmentStrokeColor` set to `"#000000"`. -``` - -We can also change these defaults values for each PolarArea type that is created, this object is available at `Chart.defaults.PolarArea`. - -### Prototype methods - -#### .getSegmentsAtEvent( event ) - -Calling `getSegmentsAtEvent(event)` on your Chart instance passing an argument of an event, or jQuery event, will return the segment elements that are at that the same position of that event. - -```javascript -canvas.onclick = function(evt){ - var activePoints = myPolarAreaChart.getSegmentsAtEvent(evt); - // => activePoints is an array of segments on the canvas that are at the same position as the click event. -}; -``` - -This functionality may be useful for implementing DOM based tooltips, or triggering custom behaviour in your application. - -#### .update( ) - -Calling `update()` on your Chart instance will re-render the chart with any updated values, allowing you to edit the value of multiple existing points, then render those in one animated render loop. - -```javascript -myPolarAreaChart.segments[1].value = 10; -// Would update the first dataset's value of 'Green' to be 10 -myPolarAreaChart.update(); -// Calling update now animates the position of Green from 50 to 10. -``` - -#### .addData( segmentData, index ) - -Calling `addData(segmentData, index)` on your Chart instance passing an object in the same format as in the constructor. There is an option second argument of 'index', this determines at what index the new segment should be inserted into the chart. - -```javascript -// An object in the same format as the original data source -myPolarAreaChart.addData({ - value: 130, - color: "#B48EAD", - highlight: "#C69CBE", - label: "Purple" -}); -// The new segment will now animate in. -``` - -#### .removeData( index ) - -Calling `removeData(index)` on your Chart instance will remove segment at that particular index. If none is provided, it will default to the last segment. - -```javascript -myPolarAreaChart.removeData(); -// Other segments will update to fill the empty space left. -``` diff --git a/docs/05-Pie-Doughnut-Chart.md b/docs/05-Pie-Doughnut-Chart.md deleted file mode 100644 index d50e2867cd4..00000000000 --- a/docs/05-Pie-Doughnut-Chart.md +++ /dev/null @@ -1,158 +0,0 @@ ---- -title: Pie & Doughnut Charts -anchor: doughnut-pie-chart ---- -###Introduction -Pie and doughnut charts are probably the most commonly used chart there are. They are divided into segments, the arc of each segment shows the proportional value of each piece of data. - -They are excellent at showing the relational proportions between data. - -Pie and doughnut charts are effectively the same class in Chart.js, but have one different default value - their `percentageInnerCutout`. This equates what percentage of the inner should be cut out. This defaults to `0` for pie charts, and `50` for doughnuts. - -They are also registered under two aliases in the `Chart` core. Other than their different default value, and different alias, they are exactly the same. - -
    - -
    - -
    - -
    - - -### Example usage - -```javascript -// For a pie chart -var myPieChart = new Chart(ctx[0]).Pie(data,options); - -// And for a doughnut chart -var myDoughnutChart = new Chart(ctx[1]).Doughnut(data,options); -``` - -### Data structure - -```javascript -var data = [ - { - value: 300, - color:"#F7464A", - highlight: "#FF5A5E", - label: "Red" - }, - { - value: 50, - color: "#46BFBD", - highlight: "#5AD3D1", - label: "Green" - }, - { - value: 100, - color: "#FDB45C", - highlight: "#FFC870", - label: "Yellow" - } -] -``` - -For a pie chart, you must pass in an array of objects with a value and an optional color property. The value attribute should be a number, Chart.js will total all of the numbers and calculate the relative proportion of each. The color attribute should be a string. Similar to CSS, for this string you can use HEX notation, RGB, RGBA or HSL. - -### Chart options - -These are the customisation options specific to Pie & Doughnut charts. These options are merged with the [global chart configuration options](#getting-started-global-chart-configuration), and form the options of the chart. - -```javascript -{ - //Boolean - Whether we should show a stroke on each segment - segmentShowStroke : true, - - //String - The colour of each segment stroke - segmentStrokeColor : "#fff", - - //Number - The width of each segment stroke - segmentStrokeWidth : 2, - - //Number - The percentage of the chart that we cut out of the middle - percentageInnerCutout : 50, // This is 0 for Pie charts - - //Number - Amount of animation steps - animationSteps : 100, - - //String - Animation easing effect - animationEasing : "easeOutBounce", - - //Boolean - Whether we animate the rotation of the Doughnut - animateRotate : true, - - //Boolean - Whether we animate scaling the Doughnut from the centre - animateScale : false, - {% raw %} - //String - A legend template - legendTemplate : "
      -legend\"><% for (var i=0; i
    • \"><%if(segments[i].label){%><%=segments[i].label%><%}%>
    • <%}%>
    " - {% endraw %} -} -``` -You can override these for your `Chart` instance by passing a second argument into the `Doughnut` method as an object with the keys you want to override. - -For example, we could have a doughnut chart that animates by scaling out from the centre like so: - -```javascript -new Chart(ctx).Doughnut(data, { - animateScale: true -}); -// This will create a chart with all of the default options, merged from the global config, -// and the Doughnut chart defaults but this particular instance will have `animateScale` set to `true`. -``` - -We can also change these default values for each Doughnut type that is created, this object is available at `Chart.defaults.Doughnut`. Pie charts also have a clone of these defaults available to change at `Chart.defaults.Pie`, with the only difference being `percentageInnerCutout` being set to 0. - -### Prototype methods - -#### .getSegmentsAtEvent( event ) - -Calling `getSegmentsAtEvent(event)` on your Chart instance passing an argument of an event, or jQuery event, will return the segment elements that are at the same position of that event. - -```javascript -canvas.onclick = function(evt){ - var activePoints = myDoughnutChart.getSegmentsAtEvent(evt); - // => activePoints is an array of segments on the canvas that are at the same position as the click event. -}; -``` - -This functionality may be useful for implementing DOM based tooltips, or triggering custom behaviour in your application. - -#### .update( ) - -Calling `update()` on your Chart instance will re-render the chart with any updated values, allowing you to edit the value of multiple existing points, then render those in one animated render loop. - -```javascript -myDoughnutChart.segments[1].value = 10; -// Would update the first dataset's value of 'Green' to be 10 -myDoughnutChart.update(); -// Calling update now animates the circumference of the segment 'Green' from 50 to 10. -// and transitions other segment widths -``` - -#### .addData( segmentData, index ) - -Calling `addData(segmentData, index)` on your Chart instance passing an object in the same format as in the constructor. There is an optional second argument of 'index', this determines at what index the new segment should be inserted into the chart. - -```javascript -// An object in the same format as the original data source -myDoughnutChart.addData({ - value: 130, - color: "#B48EAD", - highlight: "#C69CBE", - label: "Purple" -}); -// The new segment will now animate in. -``` - -#### .removeData( index ) - -Calling `removeData(index)` on your Chart instance will remove segment at that particular index. If none is provided, it will default to the last segment. - -```javascript -myDoughnutChart.removeData(); -// Other segments will update to fill the empty space left. -``` diff --git a/docs/06-Advanced.md b/docs/06-Advanced.md deleted file mode 100644 index 553bedf35a5..00000000000 --- a/docs/06-Advanced.md +++ /dev/null @@ -1,187 +0,0 @@ ---- -title: Advanced usage -anchor: advanced-usage ---- - - -### Prototype methods - -For each chart, there are a set of global prototype methods on the shared `ChartType` which you may find useful. These are available on all charts created with Chart.js, but for the examples, let's use a line chart we've made. - -```javascript -// For example: -var myLineChart = new Chart(ctx).Line(data); -``` - -#### .clear() - -Will clear the chart canvas. Used extensively internally between animation frames, but you might find it useful. - -```javascript -// Will clear the canvas that myLineChart is drawn on -myLineChart.clear(); -// => returns 'this' for chainability -``` - -#### .stop() - -Use this to stop any current animation loop. This will pause the chart during any current animation frame. Call `.render()` to re-animate. - -```javascript -// Stops the charts animation loop at its current frame -myLineChart.stop(); -// => returns 'this' for chainability -``` - -#### .resize() - -Use this to manually resize the canvas element. This is run each time the browser is resized, but you can call this method manually if you change the size of the canvas nodes container element. - -```javascript -// Resizes & redraws to fill its container element -myLineChart.resize(); -// => returns 'this' for chainability -``` - -#### .destroy() - -Use this to destroy any chart instances that are created. This will clean up any references stored to the chart object within Chart.js, along with any associated event listeners attached by Chart.js. - -```javascript -// Destroys a specific chart instance -myLineChart.destroy(); -``` - -#### .toBase64Image() - -This returns a base 64 encoded string of the chart in its current state. - -```javascript -myLineChart.toBase64Image(); -// => returns png data url of the image on the canvas -``` - -#### .generateLegend() - -Returns an HTML string of a legend for that chart. The template for this legend is at `legendTemplate` in the chart options. - -```javascript -myLineChart.generateLegend(); -// => returns HTML string of a legend for this chart -``` - -### External Tooltips - -You can enable custom tooltips in the global or chart configuration like so: - -```javascript -var myPieChart = new Chart(ctx).Pie(data, { - customTooltips: function(tooltip) { - - // tooltip will be false if tooltip is not visible or should be hidden - if (!tooltip) { - return; - } - - // Otherwise, tooltip will be an object with all tooltip properties like: - - // tooltip.caretHeight - // tooltip.caretPadding - // tooltip.chart - // tooltip.cornerRadius - // tooltip.fillColor - // tooltip.font... - // tooltip.text - // tooltip.x - // tooltip.y - // etc... - - }; -}); -``` - -See files `sample/pie-customTooltips.html` and `sample/line-customTooltips.html` for examples on how to get started. - - -### Writing new chart types - -Chart.js 1.0 has been rewritten to provide a platform for developers to create their own custom chart types, and be able to share and utilise them through the Chart.js API. - -The format is relatively simple, there are a set of utility helper methods under `Chart.helpers`, including things such as looping over collections, requesting animation frames, and easing equations. - -On top of this, there are also some simple base classes of Chart elements, these all extend from `Chart.Element`, and include things such as points, bars and scales. - -```javascript -Chart.Type.extend({ - // Passing in a name registers this chart in the Chart namespace - name: "Scatter", - // Providing a defaults will also register the deafults in the chart namespace - defaults : { - options: "Here", - available: "at this.options" - }, - // Initialize is fired when the chart is initialized - Data is passed in as a parameter - // Config is automatically merged by the core of Chart.js, and is available at this.options - initialize: function(data){ - this.chart.ctx // The drawing context for this chart - this.chart.canvas // the canvas node for this chart - }, - // Used to draw something on the canvas - draw: function() { - } -}); - -// Now we can create a new instance of our chart, using the Chart.js API -new Chart(ctx).Scatter(data); -// initialize is now run -``` - -### Extending existing chart types - -We can also extend existing chart types, and expose them to the API in the same way. Let's say for example, we might want to run some more code when we initialize every Line chart. - -```javascript -// Notice now we're extending the particular Line chart type, rather than the base class. -Chart.types.Line.extend({ - // Passing in a name registers this chart in the Chart namespace in the same way - name: "LineAlt", - initialize: function(data){ - console.log('My Line chart extension'); - Chart.types.Line.prototype.initialize.apply(this, arguments); - } -}); - -// Creates a line chart in the same way -new Chart(ctx).LineAlt(data); -// but this logs 'My Line chart extension' in the console. -``` - -### Community extensions - -- Stacked Bar Chart by @Regaddi -- Stacked Bar Chart by @tannerlinsley -- Error bars (bar and line charts) by @CAYdenberg -- Scatter chart (number & date scales are supported) by @dima117 - -### Creating custom builds - -Chart.js uses gulp to build the library into a single JavaScript file. We can use this same build script with custom parameters in order to build a custom version. - -Firstly, we need to ensure development dependencies are installed. With node and npm installed, after cloning the Chart.js repo to a local directory, and navigating to that directory in the command line, we can run the following: - -```bash -npm install -npm install -g gulp -``` - -This will install the local development dependencies for Chart.js, along with a CLI for the JavaScript task runner gulp. - -Now, we can run the `gulp build` task, and pass in a comma-separated list of types as an argument to build a custom version of Chart.js with only specified chart types. - -Here we will create a version of Chart.js with only Line, Radar and Bar charts included: - -```bash -gulp build --types=Line,Radar,Bar -``` - -This will output to the `/custom` directory, and write two files, Chart.js, and Chart.min.js with only those chart types included. diff --git a/docs/07-Notes.md b/docs/07-Notes.md deleted file mode 100644 index 8ba5a59dd78..00000000000 --- a/docs/07-Notes.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -title: Notes -anchor: notes ---- - -### Browser support -Browser support for the canvas element is available in all modern & major mobile browsers (caniuse.com/canvas). - -For IE8 & below, I would recommend using the polyfill ExplorerCanvas - available at https://code.google.com/p/explorercanvas/. It falls back to Internet explorer's format VML when canvas support is not available. Example use: - -```html - - - -``` - -Usually I would recommend feature detection to choose whether or not to load a polyfill, rather than IE conditional comments, however in this case, VML is a Microsoft proprietary format, so it will only work in IE. - -Some important points to note in my experience using ExplorerCanvas as a fallback. - -- Initialise charts on load rather than DOMContentReady when using the library, as sometimes a race condition will occur, and it will result in an error when trying to get the 2d context of a canvas. -- New VML DOM elements are being created for each animation frame and there is no hardware acceleration. As a result animation is usually slow and jerky, with flashing text. It is a good idea to dynamically turn off animation based on canvas support. I recommend using the excellent Modernizr to do this. -- When declaring fonts, the library explorercanvas requires the font name to be in single quotes inside the string. For example, instead of your scaleFontFamily property being simply "Arial", explorercanvas support, use "'Arial'" instead. Chart.js does this for default values. - -### Bugs & issues - -Please report these on the GitHub page - at github.com/nnnick/Chart.js. If you could include a link to a simple jsbin or similar to demonstrate the issue, that'd be really helpful. - - -### Contributing -New contributions to the library are welcome, just a couple of guidelines: - -- Tabs for indentation, not spaces please. -- Please ensure you're changing the individual files in `/src`, not the concatenated output in the `Chart.js` file in the root of the repo. -- Please check that your code will pass `jshint` code standards, `gulp jshint` will run this for you. -- Please keep pull requests concise, and document new functionality in the relevant `.md` file. -- Consider whether your changes are useful for all users, or if creating a Chart.js extension would be more appropriate. - -### License -Chart.js is open source and available under the MIT license. \ No newline at end of file diff --git a/docs/axes/_common.md b/docs/axes/_common.md new file mode 100644 index 00000000000..e6e3d4c876a --- /dev/null +++ b/docs/axes/_common.md @@ -0,0 +1,20 @@ +### Common options to all axes + +Namespace: `options.scales[scaleId]` + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| `type` | `string` | | Type of scale being employed. Custom scales can be created and registered with a string key. This allows changing the type of an axis for a chart. +| `alignToPixels` | `boolean` | `false` | Align pixel values to device pixels. +| `backgroundColor` | [`Color`](/general/colors.md) | | Background color of the scale area. +| `border` | `object` | | Border configuration. [more...](/axes/styling.md#border-configuration) +| `display` | `boolean`\|`string` | `true` | Controls the axis global visibility (visible when `true`, hidden when `false`). When `display: 'auto'`, the axis is visible only if at least one associated dataset is visible. +| `grid` | `object` | | Grid line configuration. [more...](/axes/styling.md#grid-line-configuration) +| `min` | `number` | | User defined minimum number for the scale, overrides minimum value from data. [more...](/axes/index.md#axis-range-settings) +| `max` | `number` | | User defined maximum number for the scale, overrides maximum value from data. [more...](/axes/index.md#axis-range-settings) +| `reverse` | `boolean` | `false` | Reverse the scale. +| `stacked` | `boolean`\|`string` | `false` | Should the data be stacked. [more...](/axes/index.md#stacking) +| `suggestedMax` | `number` | | Adjustment used when calculating the maximum data value. [more...](/axes/index.md#axis-range-settings) +| `suggestedMin` | `number` | | Adjustment used when calculating the minimum data value. [more...](/axes/index.md#axis-range-settings) +| `ticks` | `object` | | Tick configuration. [more...](/axes/index.md#tick-configuration) +| `weight` | `number` | `0` | The weight used to sort the axis. Higher weights are further away from the chart area. diff --git a/docs/axes/_common_ticks.md b/docs/axes/_common_ticks.md new file mode 100644 index 00000000000..2a7c9512fa6 --- /dev/null +++ b/docs/axes/_common_ticks.md @@ -0,0 +1,18 @@ +### Common tick options to all axes + +Namespace: `options.scales[scaleId].ticks` + +| Name | Type | Scriptable | Default | Description +| ---- | ---- | :-------------------------------: | ------- | ----------- +| `backdropColor` | [`Color`](../../general/colors.md) | Yes | `'rgba(255, 255, 255, 0.75)'` | Color of label backdrops. +| `backdropPadding` | [`Padding`](../../general/padding.md) | | `2` | Padding of label backdrop. +| `callback` | `function` | | | Returns the string representation of the tick value as it should be displayed on the chart. See [callback](/axes/labelling.md#creating-custom-tick-formats). +| `display` | `boolean` | | `true` | If true, show tick labels. +| `color` | [`Color`](/general/colors.md) | Yes | `Chart.defaults.color` | Color of ticks. +| `font` | `Font` | Yes | `Chart.defaults.font` | See [Fonts](/general/fonts.md) +| `major` | `object` | | `{}` | [Major ticks configuration](/axes/styling.md#major-tick-configuration). +| `padding` | `number` | | `3` | Sets the offset of the tick labels from the axis +| `showLabelBackdrop` | `boolean` | Yes | `true` for radial scale, `false` otherwise | If true, draw a background behind the tick labels. +| `textStrokeColor` | [`Color`](/general/colors.md) | Yes | `` | The color of the stroke around the text. +| `textStrokeWidth` | `number` | Yes | `0` | Stroke width around the text. +| `z` | `number` | | `0` | z-index of tick layer. Useful when ticks are drawn on chart area. Values <= 0 are drawn under datasets, > 0 on top. diff --git a/docs/axes/cartesian/_common.md b/docs/axes/cartesian/_common.md new file mode 100644 index 00000000000..b6b5a8e12f4 --- /dev/null +++ b/docs/axes/cartesian/_common.md @@ -0,0 +1,14 @@ +### Common options to all cartesian axes + +Namespace: `options.scales[scaleId]` + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| `bounds` | `string` | `'ticks'` | Determines the scale bounds. [more...](./index.md#scale-bounds) +| `clip` | `boolean` | `true` | If true, clip the dataset drawing against the size of the scale instead of chart area +| `position` | `string` \| `object` | | Position of the axis. [more...](./index.md#axis-position) +| `stack` | `string` | | Stack group. Axes at the same `position` with same `stack` are stacked. +| `stackWeight` | `number` | 1 | Weight of the scale in stack group. Used to determine the amount of allocated space for the scale within the group. +| `axis` | `string` | | Which type of axis this is. Possible values are: `'x'`, `'y'`. If not set, this is inferred from the first character of the ID which should be `'x'` or `'y'`. +| `offset` | `boolean` | `false` | If true, extra space is added to the both edges and the axis is scaled to fit into the chart area. This is set to `true` for a bar chart by default. +| `title` | `object` | | Scale title configuration. [more...](../labelling.md#scale-title-configuration) diff --git a/docs/axes/cartesian/_common_ticks.md b/docs/axes/cartesian/_common_ticks.md new file mode 100644 index 00000000000..ccc98588dd2 --- /dev/null +++ b/docs/axes/cartesian/_common_ticks.md @@ -0,0 +1,18 @@ +### Common tick options to all cartesian axes + +Namespace: `options.scales[scaleId].ticks` + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| `align` | `string` | `'center'` | The tick alignment along the axis. Can be `'start'`, `'center'`, `'end'`, or `'inner'`. `inner` alignment means align `start` for first tick and `end` for the last tick of horizontal axis +| `crossAlign` | `string` | `'near'` | The tick alignment perpendicular to the axis. Can be `'near'`, `'center'`, or `'far'`. See [Tick Alignment](/axes/cartesian/#tick-alignment) +| `sampleSize` | `number` | `ticks.length` | The number of ticks to examine when deciding how many labels will fit. Setting a smaller value will be faster, but may be less accurate when there is large variability in label length. +| `autoSkip` | `boolean` | `true` | If true, automatically calculates how many labels can be shown and hides labels accordingly. Labels will be rotated up to `maxRotation` before skipping any. Turn `autoSkip` off to show all labels no matter what. +| `autoSkipPadding` | `number` | `3` | Padding between the ticks on the horizontal axis when `autoSkip` is enabled. +| `includeBounds` | `boolean` | `true` | Should the defined `min` and `max` values be presented as ticks even if they are not "nice". +| `labelOffset` | `number` | `0` | Distance in pixels to offset the label from the centre point of the tick (in the x-direction for the x-axis, and the y-direction for the y-axis). *Note: this can cause labels at the edges to be cropped by the edge of the canvas* +| `maxRotation` | `number` | `50` | Maximum rotation for tick labels when rotating to condense labels. Note: Rotation doesn't occur until necessary. *Note: Only applicable to horizontal scales.* +| `minRotation` | `number` | `0` | Minimum rotation for tick labels. *Note: Only applicable to horizontal scales.* +| `mirror` | `boolean` | `false` | Flips tick labels around axis, displaying the labels inside the chart instead of outside. *Note: Only applicable to vertical scales.* +| `padding` | `number` | `0` | Padding between the tick label and the axis. When set on a vertical axis, this applies in the horizontal (X) direction. When set on a horizontal axis, this applies in the vertical (Y) direction. +| `maxTicksLimit` | `number` | `11` | Maximum number of ticks and gridlines to show. diff --git a/docs/axes/cartesian/category.md b/docs/axes/cartesian/category.md new file mode 100644 index 00000000000..3aa9fbfef65 --- /dev/null +++ b/docs/axes/cartesian/category.md @@ -0,0 +1,85 @@ +# Category Axis + +If the global configuration is used, labels are drawn from one of the label arrays included in the chart data. If only `data.labels` is defined, this will be used. If `data.xLabels` is defined and the axis is horizontal, this will be used. Similarly, if `data.yLabels` is defined and the axis is vertical, this property will be used. Using both `xLabels` and `yLabels` together can create a chart that uses strings for both the X and Y axes. + +Specifying any of the settings above defines the x-axis as `type: 'category'` if not defined otherwise. For more fine-grained control of category labels, it is also possible to add `labels` as part of the category axis definition. Doing so does not apply the global defaults. + +## Category Axis Definition + +Globally: + +```javascript +let chart = new Chart(ctx, { + type: ... + data: { + labels: ['January', 'February', 'March', 'April', 'May', 'June'], + datasets: ... + } +}); +``` + +As part of axis definition: + +```javascript +let chart = new Chart(ctx, { + type: ... + data: ... + options: { + scales: { + x: { + type: 'category', + labels: ['January', 'February', 'March', 'April', 'May', 'June'] + } + } + } +}); +``` + +## Configuration Options + +### Category Axis specific options + +Namespace: `options.scales[scaleId]` + +| Name | Type | Description +| ---- | ---- | ----------- +| `min` | `string`\|`number` | The minimum item to display. [more...](#min-max-configuration) +| `max` | `string`\|`number` | The maximum item to display. [more...](#min-max-configuration) +| `labels` | `string[]`\|`string[][]` | An array of labels to display. When an individual label is an array of strings, each item is rendered on a new line. + +!!!include(axes/cartesian/_common.md)!!! + +!!!include(axes/_common.md)!!! + +## Tick Configuration + +!!!include(axes/cartesian/_common_ticks.md)!!! + +!!!include(axes/_common_ticks.md)!!! + +## Min Max Configuration + +For both the `min` and `max` properties, the value must be `string` in the `labels` array or `numeric` value as an index of a label in that array. In the example below, the x axis would only display "March" through "June". + +```javascript +let chart = new Chart(ctx, { + type: 'line', + data: { + datasets: [{ + data: [10, 20, 30, 40, 50, 60] + }], + labels: ['January', 'February', 'March', 'April', 'May', 'June'] + }, + options: { + scales: { + x: { + min: 'March' + } + } + } +}); +``` + +## Internal data format + +Internally category scale uses label indices diff --git a/docs/axes/cartesian/index.md b/docs/axes/cartesian/index.md new file mode 100644 index 00000000000..a8c3b545887 --- /dev/null +++ b/docs/axes/cartesian/index.md @@ -0,0 +1,368 @@ +# Cartesian Axes + +Axes that follow a cartesian grid are known as 'Cartesian Axes'. Cartesian axes are used for line, bar, and bubble charts. Five cartesian axes are included in Chart.js by default. + +* [linear](./linear.md) +* [logarithmic](./logarithmic.md) +* [category](./category.md) +* [time](./time.md) +* [timeseries](./timeseries.md) + +## Visual Components + +A cartesian axis is composed of visual components that can be individually configured. These components are: + +* [border](#border) +* [grid lines](#grid-lines) +* [tick](#ticks-and-tick-marks) +* [tick mark](#ticks-and-tick-marks) +* [title](#title) + +### Border + +The axis border is drawn at the edge of the axis, beside the chart area. In the image below, it is drawn in red. + +```js chart-editor +// +const labels = Utils.months({count: 7}); +const data = { + labels: labels, + datasets: [{ + label: 'My First dataset', + backgroundColor: 'rgba(54, 162, 235, 0.5)', + borderColor: 'rgb(54, 162, 235)', + borderWidth: 1, + data: [10, 20, 30, 40, 50, 0, 5], + }] +}; +// + +// +const config = { + type: 'line', + data, + options: { + scales: { + x: { + border: { + color: 'red' + } + } + } + } +}; +// + +module.exports = { + actions: [], + config: config, +}; +``` + +### Grid lines + +The grid lines for an axis are drawn on the chart area. In the image below, they are red. + +```js chart-editor +// +const labels = Utils.months({count: 7}); +const data = { + labels: labels, + datasets: [{ + label: 'My First dataset', + backgroundColor: 'rgba(54, 162, 235, 0.5)', + borderColor: 'rgb(54, 162, 235)', + borderWidth: 1, + data: [10, 20, 30, 40, 50, 0, 5], + }] +}; +// + +// +const config = { + type: 'line', + data, + options: { + scales: { + x: { + grid: { + color: 'red', + borderColor: 'grey', + tickColor: 'grey' + } + } + } + } +}; +// + +module.exports = { + actions: [], + config: config, +}; +``` + +### Ticks and Tick Marks + +Ticks represent data values on the axis that appear as labels. The tick mark is the extension of the grid line from the axis border to the label. +In this example, the tick mark is drawn in red while the tick label is drawn in blue. + +```js chart-editor +// +const labels = Utils.months({count: 7}); +const data = { + labels: labels, + datasets: [{ + label: 'My First dataset', + backgroundColor: 'rgba(54, 162, 235, 0.5)', + borderColor: 'rgb(54, 162, 235)', + borderWidth: 1, + data: [10, 20, 30, 40, 50, 0, 5], + }] +}; +// + +// +const config = { + type: 'line', + data, + options: { + scales: { + x: { + grid: { + tickColor: 'red' + }, + ticks: { + color: 'blue', + } + } + } + } +}; +// + +module.exports = { + actions: [], + config: config, +}; +``` + +### Title + +The title component of the axis is used to label the data. In the example below, it is shown in red. + +```js chart-editor +// +const labels = Utils.months({count: 7}); +const data = { + labels: labels, + datasets: [{ + label: 'My First dataset', + backgroundColor: 'rgba(54, 162, 235, 0.5)', + borderColor: 'rgb(54, 162, 235)', + borderWidth: 1, + data: [10, 20, 30, 40, 50, 0, 5], + }] +}; +// + +// +const config = { + type: 'line', + data, + options: { + scales: { + x: { + title: { + color: 'red', + display: true, + text: 'Month' + } + } + } + } +}; +// + +module.exports = { + actions: [], + config: config, +}; +``` + +## Common Configuration + +:::tip Note +These are only the common options supported by all cartesian axes. Please see the specific axis documentation for all the available options for that axis. +::: + +!!!include(axes/cartesian/_common.md)!!! + +!!!include(axes/_common.md)!!! + +### Axis Position + +An axis can either be positioned at the edge of the chart, at the center of the chart area, or dynamically with respect to a data value. + +To position the axis at the edge of the chart, set the `position` option to one of: `'top'`, `'left'`, `'bottom'`, `'right'`. +To position the axis at the center of the chart area, set the `position` option to `'center'`. In this mode, either the `axis` option must be specified or the axis ID has to start with the letter 'x' or 'y'. This is so chart.js knows what kind of axis (horizontal or vertical) it is. +To position the axis with respect to a data value, set the `position` option to an object such as: + +```javascript +{ + x: -20 +} +``` + +This will position the axis at a value of -20 on the axis with ID "x". For cartesian axes, only 1 axis may be specified. + +### Scale Bounds + +The `bounds` property controls the scale boundary strategy (bypassed by `min`/`max` options). + +* `'data'`: makes sure data are fully visible, labels outside are removed +* `'ticks'`: makes sure ticks are fully visible, data outside are truncated + +### Tick Configuration + +:::tip Note +These are only the common tick options supported by all cartesian axes. Please see specific axis documentation for all of the available options for that axis. +::: + +!!!include(axes/cartesian/_common_ticks.md)!!! + +!!!include(axes/_common_ticks.md)!!! + +### Tick Alignment + +The alignment of ticks is primarily controlled using two settings on the tick configuration object: `align` and `crossAlign`. The `align` setting configures how labels align with the tick mark along the axis direction (i.e. horizontal for a horizontal axis and vertical for a vertical axis). The `crossAlign` setting configures how labels align with the tick mark in the perpendicular direction (i.e. vertical for a horizontal axis and horizontal for a vertical axis). In the example below, the `crossAlign` setting is used to left align the labels on the Y axis. + +```js chart-editor +// +const labels = Utils.months({count: 7}); +const data = { + labels: labels, + datasets: [{ + label: 'My First dataset', + backgroundColor: [ + 'rgba(255, 99, 132, 0.2)', + 'rgba(255, 159, 64, 0.2)', + 'rgba(255, 205, 86, 0.2)', + 'rgba(75, 192, 192, 0.2)', + 'rgba(54, 162, 235, 0.2)', + 'rgba(153, 102, 255, 0.2)', + 'rgba(201, 203, 207, 0.2)' + ], + borderColor: [ + 'rgb(255, 99, 132)', + 'rgb(255, 159, 64)', + 'rgb(255, 205, 86)', + 'rgb(75, 192, 192)', + 'rgb(54, 162, 235)', + 'rgb(153, 102, 255)', + 'rgb(201, 203, 207)' + ], + borderWidth: 1, + data: [65, 59, 80, 81, 56, 55, 40], + }] +}; +// + +// +const config = { + type: 'bar', + data, + options: { + indexAxis: 'y', + scales: { + y: { + ticks: { + crossAlign: 'far', + } + } + } + } +}; +// + +module.exports = { + actions: [], + config: config, +}; +``` + +:::tip Note +The `crossAlign` setting is only effective when these preconditions are met: + +* tick rotation is `0` +* axis position is `'top'`, '`left'`, `'bottom'` or `'right'` +::: + +### Axis ID + +The properties `dataset.xAxisID` or `dataset.yAxisID` have to match to `scales` property. This is especially needed if multi-axes charts are used. + +```javascript +const myChart = new Chart(ctx, { + type: 'line', + data: { + datasets: [{ + // This dataset appears on the first axis + yAxisID: 'first-y-axis' + }, { + // This dataset appears on the second axis + yAxisID: 'second-y-axis' + }] + }, + options: { + scales: { + 'first-y-axis': { + type: 'linear' + }, + 'second-y-axis': { + type: 'linear' + } + } + } +}); +``` + +## Creating Multiple Axes + +With cartesian axes, it is possible to create multiple X and Y axes. To do so, you can add multiple configuration objects to the `xAxes` and `yAxes` properties. When adding new axes, it is important to ensure that you specify the type of the new axes as default types are **not** used in this case. + +In the example below, we are creating two Y axes. We then use the `yAxisID` property to map the datasets to their correct axes. + +```javascript +const myChart = new Chart(ctx, { + type: 'line', + data: { + datasets: [{ + data: [20, 50, 100, 75, 25, 0], + label: 'Left dataset', + + // This binds the dataset to the left y axis + yAxisID: 'left-y-axis' + }, { + data: [0.1, 0.5, 1.0, 2.0, 1.5, 0], + label: 'Right dataset', + + // This binds the dataset to the right y axis + yAxisID: 'right-y-axis' + }], + labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'] + }, + options: { + scales: { + 'left-y-axis': { + type: 'linear', + position: 'left' + }, + 'right-y-axis': { + type: 'linear', + position: 'right' + } + } + } +}); +``` diff --git a/docs/axes/cartesian/linear.md b/docs/axes/cartesian/linear.md new file mode 100644 index 00000000000..fae3b98d11b --- /dev/null +++ b/docs/axes/cartesian/linear.md @@ -0,0 +1,100 @@ +# Linear Axis + +The linear scale is used to chart numerical data. It can be placed on either the x or y-axis. The scatter chart type automatically configures a line chart to use one of these scales for the x-axis. As the name suggests, linear interpolation is used to determine where a value lies on the axis. + +## Configuration Options + +### Linear Axis specific options + +Namespace: `options.scales[scaleId]` + +| Name | Type | Description +| ---- | ---- | ----------- +| `beginAtZero` | `boolean` | if true, scale will include 0 if it is not already included. +| `grace` | `number`\|`string` | Percentage (string ending with `%`) or amount (number) for added room in the scale range above and below data. [more...](#grace) + +!!!include(axes/cartesian/_common.md)!!! + +!!!include(axes/_common.md)!!! + +## Tick Configuration + +### Linear Axis specific tick options + +Namespace: `options.scales[scaleId].ticks` + +| Name | Type | Scriptable | Default | Description +| ---- | ---- | ------- | ------- | ----------- +| `count` | `number` | Yes | `undefined` | The number of ticks to generate. If specified, this overrides the automatic generation. +| `format` | `object` | Yes | | The [`Intl.NumberFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat) options used by the default label formatter +| `precision` | `number` | Yes | | if defined and `stepSize` is not specified, the step size will be rounded to this many decimal places. +| `stepSize` | `number` | Yes | | User-defined fixed step size for the scale. [more...](#step-size) + +!!!include(axes/cartesian/_common_ticks.md)!!! + +!!!include(axes/_common_ticks.md)!!! + +## Step Size + +If set, the scale ticks will be enumerated by multiple of `stepSize`, having one tick per increment. If not set, the ticks are labeled automatically using the nice numbers algorithm. + +This example sets up a chart with a y-axis that creates ticks at `0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5`. + +```javascript +let options = { + scales: { + y: { + max: 5, + min: 0, + ticks: { + stepSize: 0.5 + } + } + } +}; +``` + +## Grace + +If the value is a string ending with `%`, it's treated as a percentage. If a number, it's treated as a value. +The value is added to the maximum data value and subtracted from the minimum data. This extends the scale range as if the data values were that much greater. + +```js chart-editor +// +const labels = Utils.months({count: 7}); +const data = { + labels: ['Positive', 'Negative'], + datasets: [{ + data: [100, -50], + backgroundColor: 'rgb(255, 99, 132)' + }], +}; +// + +// +const config = { + type: 'bar', + data, + options: { + scales: { + y: { + type: 'linear', + grace: '5%' + } + }, + plugins: { + legend: false + } + } +}; +// + +module.exports = { + actions: [], + config: config, +}; +``` + +## Internal data format + +Internally, the linear scale uses numeric data. diff --git a/docs/axes/cartesian/logarithmic.md b/docs/axes/cartesian/logarithmic.md new file mode 100644 index 00000000000..52ab680cae2 --- /dev/null +++ b/docs/axes/cartesian/logarithmic.md @@ -0,0 +1,27 @@ +# Logarithmic Axis + +The logarithmic scale is used to chart numerical data. It can be placed on either the x or y-axis. As the name suggests, logarithmic interpolation is used to determine where a value lies on the axis. + +## Configuration Options + +!!!include(axes/cartesian/_common.md)!!! + +!!!include(axes/_common.md)!!! + +## Tick Configuration + +### Logarithmic Axis specific options + +Namespace: `options.scales[scaleId].ticks` + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| `format` | `object` | | The [`Intl.NumberFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat) options used by the default label formatter + +!!!include(axes/cartesian/_common_ticks.md)!!! + +!!!include(axes/_common_ticks.md)!!! + +## Internal data format + +Internally, the logarithmic scale uses numeric data. diff --git a/docs/axes/cartesian/time.md b/docs/axes/cartesian/time.md new file mode 100644 index 00000000000..50d5d625d19 --- /dev/null +++ b/docs/axes/cartesian/time.md @@ -0,0 +1,194 @@ +# Time Cartesian Axis + +The time scale is used to display times and dates. Data are spread according to the amount of time between data points. When building its ticks, it will automatically calculate the most comfortable unit based on the size of the scale. + +## Date Adapters + +The time scale **requires** both a date library and a corresponding adapter to be present. Please choose from the [available adapters](https://github.com/chartjs/awesome#adapters). + +## Data Sets + +### Input Data + +See [data structures](../../general/data-structures.md). + +### Date Formats + +When providing data for the time scale, Chart.js uses timestamps defined as milliseconds since the epoch (midnight January 1, 1970, UTC) internally. However, Chart.js also supports all of the formats that your chosen date adapter accepts. You should use timestamps if you'd like to set `parsing: false` for better performance. + +## Configuration Options + +### Time Axis specific options + +Namespace: `options.scales[scaleId]` + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| `min` | `number`\|`string` | | The minimum item to display. [more...](#min-max-configuration) +| `max` | `number`\|`string` | | The maximum item to display. [more...](#min-max-configuration) +| `suggestedMin` | `number`\|`string` | | The minimum item to display if there is no datapoint before it. [more...](../index.md#axis-range-settings) +| `suggestedMax` | `number`\|`string` | | The maximum item to display if there is no datapoint behind it. [more...](../index.md#axis-range-settings) +| `adapters.date` | `object` | `{}` | Options for adapter for external date library if that adapter needs or supports options +| `bounds` | `string` | `'data'` | Determines the scale bounds. [more...](./index.md#scale-bounds) +| `offsetAfterAutoskip` | `boolean` | `false` | If true, bar chart offsets are computed with auto skipped ticks. +| `ticks.source` | `string` | `'auto'` | How ticks are generated. [more...](#ticks-source) +| `time.displayFormats` | `object` | | Sets how different time units are displayed. [more...](#display-formats) +| `time.isoWeekday` | `boolean`\|`number` | `false` | If `boolean` and true and the unit is set to 'week', then the first day of the week will be Monday. Otherwise, it will be Sunday. If `number`, the index of the first day of the week (0 - Sunday, 6 - Saturday) +| `time.parser` | `string`\|`function` | | Custom parser for dates. [more...](#parser) +| `time.round` | `string` | `false` | If defined, dates will be rounded to the start of this unit. See [Time Units](#time-units) below for the allowed units. +| `time.tooltipFormat` | `string` | | The format string to use for the tooltip. +| `time.unit` | `string` | `false` | If defined, will force the unit to be a certain type. See [Time Units](#time-units) section below for details. +| `time.minUnit` | `string` | `'millisecond'` | The minimum display format to be used for a time unit. + +!!!include(axes/cartesian/_common.md)!!! + +!!!include(axes/_common.md)!!! + +#### Time Units + +The following time measurements are supported. The names can be passed as strings to the `time.unit` config option to force a certain unit. + +* `'millisecond'` +* `'second'` +* `'minute'` +* `'hour'` +* `'day'` +* `'week'` +* `'month'` +* `'quarter'` +* `'year'` + +For example, to create a chart with a time scale that always displayed units per month, the following config could be used. + +```javascript +const chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + scales: { + x: { + type: 'time', + time: { + unit: 'month' + } + } + } + } +}); +``` + +#### Display Formats + +You may specify a map of display formats with a key for each unit: + +* `millisecond` +* `second` +* `minute` +* `hour` +* `day` +* `week` +* `month` +* `quarter` +* `year` + +The format string used as a value depends on the date adapter you chose to use. + +For example, to set the display format for the `quarter` unit to show the month and year, the following config might be passed to the chart constructor. + +```javascript +const chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + scales: { + x: { + type: 'time', + time: { + displayFormats: { + quarter: 'MMM YYYY' + } + } + } + } + } +}); +``` + +#### Ticks Source + +The `ticks.source` property controls the ticks generation. + +* `'auto'`: generates "optimal" ticks based on scale size and time options +* `'data'`: generates ticks from data (including labels from data `{x|y}` objects) +* `'labels'`: generates ticks from user given `labels` ONLY + +#### Parser + +If this property is defined as a string, it is interpreted as a custom format to be used by the date adapter to parse the date. + +If this is a function, it must return a type that can be handled by your date adapter's `parse` method. + +## Min Max Configuration + +For both the `min` and `max` properties, the value must be `string` that is parsable by your date adapter or a number with the amount of milliseconds that have elapsed since UNIX epoch. +In the example below the x axis will start at 7 November 2021. + +```javascript +let chart = new Chart(ctx, { + type: 'line', + data: { + datasets: [{ + data: [{ + x: '2021-11-06 23:39:30', + y: 50 + }, { + x: '2021-11-07 01:00:28', + y: 60 + }, { + x: '2021-11-07 09:00:28', + y: 20 + }] + }], + }, + options: { + scales: { + x: { + min: '2021-11-07 00:00:00', + } + } + } +}); +``` + +## Changing the scale type from Time scale to Logarithmic/Linear scale. + +When changing the scale type from Time scale to Logarithmic/Linear scale, you need to add `bounds: 'ticks'` to the scale options. Changing the `bounds` parameter is necessary because its default value is the `'data'` for the Time scale. + +Initial config: + +```javascript +const chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + scales: { + x: { + type: 'time', + } + } + } +}); +``` + +Scale update: + +```javascript +chart.options.scales.x = { + type: 'logarithmic', + bounds: 'ticks' +}; +``` + +## Internal data format + +Internally time scale uses milliseconds since epoch diff --git a/docs/axes/cartesian/timeseries.md b/docs/axes/cartesian/timeseries.md new file mode 100644 index 00000000000..daac4df28bf --- /dev/null +++ b/docs/axes/cartesian/timeseries.md @@ -0,0 +1,23 @@ +# Time Series Axis + +The time series scale extends from the time scale and supports all the same options. However, for the time series scale, each data point is spread equidistant. + +## Example + +```javascript +const chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + scales: { + x: { + type: 'timeseries', + } + } + } +}); +``` + +## More details + +Please see [the time scale documentation](./time.md) for all other details. diff --git a/docs/axes/index.md b/docs/axes/index.md new file mode 100644 index 00000000000..13968f37422 --- /dev/null +++ b/docs/axes/index.md @@ -0,0 +1,186 @@ +# Axes + +Axes are an integral part of a chart. They are used to determine how data maps to a pixel value on the chart. In a cartesian chart, there is 1 or more X-axis and 1 or more Y-axis to map points onto the 2-dimensional canvas. These axes are known as ['cartesian axes'](./cartesian/). + +In a radial chart, such as a radar chart or a polar area chart, there is a single axis that maps points in the angular and radial directions. These are known as ['radial axes'](./radial/). + +Scales in Chart.js >v2.0 are significantly more powerful, but also different from those of v1.0. + +* Multiple X & Y axes are supported. +* A built-in label auto-skip feature detects would-be overlapping ticks and labels and removes every nth label to keep things displayed normally. +* Scale titles are supported. +* New scale types can be extended without writing an entirely new chart type. + +## Default scales + +The default `scaleId`'s for cartesian charts are `'x'` and `'y'`. For radial charts: `'r'`. +Each dataset is mapped to a scale for each axis (x, y or r) it requires. The scaleId's that a dataset is mapped to is determined by the `xAxisID`, `yAxisID` or `rAxisID`. +If the ID for an axis is not specified, the first scale for that axis is used. If no scale for an axis is found, a new scale is created. + +Some examples: + +The following chart will have `'x'` and `'y'` scales: + +```js +let chart = new Chart(ctx, { + type: 'line' +}); +``` + +The following chart will have scales `'x'` and `'myScale'`: + +```js +let chart = new Chart(ctx, { + type: 'bar', + data: { + datasets: [{ + data: [1, 2, 3] + }] + }, + options: { + scales: { + myScale: { + type: 'logarithmic', + position: 'right', // `axis` is determined by the position as `'y'` + } + } + } +}); +``` + +The following chart will have scales `'xAxis'` and `'yAxis'`: + +```js +let chart = new Chart(ctx, { + type: 'bar', + data: { + datasets: [{ + yAxisID: 'yAxis' + }] + }, + options: { + scales: { + xAxis: { + // The axis for this scale is determined from the first letter of the id as `'x'` + // It is recommended to specify `position` and / or `axis` explicitly. + type: 'time', + } + } + } +}); +``` + +The following chart will have `'r'` scale: + +```js +let chart = new Chart(ctx, { + type: 'radar' +}); +``` + +The following chart will have `'myScale'` scale: + +```js +let chart = new Chart(ctx, { + type: 'radar', + scales: { + myScale: { + axis: 'r' + } + } +}); +``` + +## Common Configuration + +:::tip Note +These are only the common options supported by all axes. Please see specific axis documentation for all the available options for that axis. +::: + +!!!include(axes/_common.md)!!! + +## Tick Configuration + +:::tip Note +These are only the common tick options supported by all axes. Please see specific axis documentation for all the available tick options for that axis. +::: + +!!!include(axes/_common_ticks.md)!!! + +## Axis Range Settings + +Given the number of axis range settings, it is important to understand how they all interact with each other. + +The `suggestedMax` and `suggestedMin` settings only change the data values that are used to scale the axis. These are useful for extending the range of the axis while maintaining the auto-fit behaviour. + +```javascript +let minDataValue = Math.min(mostNegativeValue, options.suggestedMin); +let maxDataValue = Math.max(mostPositiveValue, options.suggestedMax); +``` + +In this example, the largest positive value is 50, but the data maximum is expanded out to 100. However, because the lowest data value is below the `suggestedMin` setting, it is ignored. + +```javascript +let chart = new Chart(ctx, { + type: 'line', + data: { + datasets: [{ + label: 'First dataset', + data: [0, 20, 40, 50] + }], + labels: ['January', 'February', 'March', 'April'] + }, + options: { + scales: { + y: { + suggestedMin: 50, + suggestedMax: 100 + } + } + } +}); +``` + +In contrast to the `suggested*` settings, the `min` and `max` settings set explicit ends to the axes. When these are set, some data points may not be visible. + +## Stacking + +By default, data is not stacked. If the `stacked` option of the value scale (y-axis on horizontal chart) is `true`, positive and negative values are stacked separately. Additionally, a `stack` option can be defined per dataset to further divide into stack groups [more...](../general/data-structures/#dataset-configuration). +For some charts, you might want to stack positive and negative values together. That can be achieved by specifying `stacked: 'single'`. + +## Callbacks + +There are a number of config callbacks that can be used to change parameters in the scale at different points in the update process. The options are supplied at the top level of the axis options. + +Namespace: `options.scales[scaleId]` + +| Name | Arguments | Description +| ---- | --------- | ----------- +| `beforeUpdate` | `axis` | Callback called before the update process starts. +| `beforeSetDimensions` | `axis` | Callback that runs before dimensions are set. +| `afterSetDimensions` | `axis` | Callback that runs after dimensions are set. +| `beforeDataLimits` | `axis` | Callback that runs before data limits are determined. +| `afterDataLimits` | `axis` | Callback that runs after data limits are determined. +| `beforeBuildTicks` | `axis` | Callback that runs before ticks are created. +| `afterBuildTicks` | `axis` | Callback that runs after ticks are created. Useful for filtering ticks. +| `beforeTickToLabelConversion` | `axis` | Callback that runs before ticks are converted into strings. +| `afterTickToLabelConversion` | `axis` | Callback that runs after ticks are converted into strings. +| `beforeCalculateLabelRotation` | `axis` | Callback that runs before tick rotation is determined. +| `afterCalculateLabelRotation` | `axis` | Callback that runs after tick rotation is determined. +| `beforeFit` | `axis` | Callback that runs before the scale fits to the canvas. +| `afterFit` | `axis` | Callback that runs after the scale fits to the canvas. +| `afterUpdate` | `axis` | Callback that runs at the end of the update process. + +### Updating Axis Defaults + +The default configuration for a scale can be easily changed. All you need to do is set the new options to `Chart.defaults.scales[type]`. + +For example, to set the minimum value of 0 for all linear scales, you would do the following. Any linear scales created after this time would now have a minimum of 0. + +```javascript +Chart.defaults.scales.linear.min = 0; +``` + +## Creating New Axes + +To create a new axis, see the [developer docs](../developers/axes.md). diff --git a/docs/axes/labelling.md b/docs/axes/labelling.md new file mode 100644 index 00000000000..2c4a7f4b206 --- /dev/null +++ b/docs/axes/labelling.md @@ -0,0 +1,69 @@ +# Labeling Axes + +When creating a chart, you want to tell the viewer what data they are viewing. To do this, you need to label the axis. + +## Scale Title Configuration + +Namespace: `options.scales[scaleId].title`, it defines options for the scale title. Note that this only applies to cartesian axes. + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| `display` | `boolean` | `false` | If true, display the axis title. +| `align` | `string` | `'center'` | Alignment of the axis title. Possible options are `'start'`, `'center'` and `'end'` +| `text` | `string`\|`string[]` | `''` | The text for the title. (i.e. "# of People" or "Response Choices"). +| `color` | [`Color`](../general/colors.md) | `Chart.defaults.color` | Color of label. +| `strokeColor` | [`Color`](../general/colors.md) | | Color of text stroke. +| `strokeWidth` | `number` | | Size of stroke width, in pixels. +| `font` | `Font` | `Chart.defaults.font` | See [Fonts](../general/fonts.md) +| `padding` | [`Padding`](../general/padding.md) | `4` | Padding to apply around scale labels. Only `top`, `bottom` and `y` are implemented. + +## Creating Custom Tick Formats + +It is also common to want to change the tick marks to include information about the data type. For example, adding a dollar sign ('$'). +To do this, you need to override the `ticks.callback` method in the axis configuration. + +The method receives 3 arguments: + +* `value` - the tick value in the **internal data format** of the associated scale. For time scale, it is a timestamp. +* `index` - the tick index in the ticks array. +* `ticks` - the array containing all of the [tick objects](../api/interfaces/Tick). + +The call to the method is scoped to the scale. `this` inside the method is the scale object. + +If the callback returns `null` or `undefined` the associated grid line will be hidden. + +:::tip +The [category axis](../axes/cartesian/category), which is the default x-axis for line and bar charts, uses the `index` as internal data format. For accessing the label, use `this.getLabelForValue(value)`. [API: getLabelForValue](../api/classes/Scale.md#getlabelforvalue) +::: + +In the following example, every label of the Y-axis would be displayed with a dollar sign at the front. + +```javascript +const chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + scales: { + y: { + ticks: { + // Include a dollar sign in the ticks + callback: function(value, index, ticks) { + return '$' + value; + } + } + } + } + } +}); +``` + +Keep in mind that overriding `ticks.callback` means that you are responsible for all formatting of the label. Depending on your use case, you may want to call the default formatter and then modify its output. In the example above, that would look like: + +```javascript + // call the default formatter, forwarding `this` + return '$' + Chart.Ticks.formatters.numeric.apply(this, [value, index, ticks]); +``` + +Related samples: + +* [Tick configuration sample](../samples/scale-options/ticks) diff --git a/docs/axes/radial/index.md b/docs/axes/radial/index.md new file mode 100644 index 00000000000..6f265791cf2 --- /dev/null +++ b/docs/axes/radial/index.md @@ -0,0 +1,178 @@ +# Radial Axes + +Radial axes are used specifically for the radar and polar area chart types. These axes overlay the chart area, rather than being positioned on one of the edges. One radial axis is included by default in Chart.js. + +* [radialLinear](./linear.md) + +## Visual Components + +A radial axis is composed of visual components that can be individually configured. These components are: + +* [angle lines](#angle-lines) +* [grid lines](#grid-lines) +* [point labels](#point-labels) +* [ticks](#ticks) + +### Angle Lines + +The grid lines for an axis are drawn on the chart area. They stretch out from the center towards the edge of the canvas. In the example below, they are red. + +```js chart-editor +// +const labels = Utils.months({count: 7}); +const data = { + labels: labels, + datasets: [{ + label: 'My First dataset', + backgroundColor: 'rgba(54, 162, 235, 0.5)', + borderColor: 'rgb(54, 162, 235)', + borderWidth: 1, + data: [10, 20, 30, 40, 50, 0, 5], + }] +}; +// + +// +const config = { + type: 'radar', + data, + options: { + scales: { + r: { + angleLines: { + color: 'red' + } + } + } + } +}; +// + +module.exports = { + actions: [], + config: config, +}; +``` + +### Grid Lines + +The grid lines for an axis are drawn on the chart area. In the example below, they are red. + +```js chart-editor +// +const labels = Utils.months({count: 7}); +const data = { + labels: labels, + datasets: [{ + label: 'My First dataset', + backgroundColor: 'rgba(54, 162, 235, 0.5)', + borderColor: 'rgb(54, 162, 235)', + borderWidth: 1, + data: [10, 20, 30, 40, 50, 0, 5], + }] +}; +// + +// +const config = { + type: 'radar', + data, + options: { + scales: { + r: { + grid: { + color: 'red' + } + } + } + } +}; +// + +module.exports = { + actions: [], + config: config, +}; +``` + +### Point Labels + +The point labels indicate the value for each angle line. In the example below, they are red. + +```js chart-editor +// +const labels = Utils.months({count: 7}); +const data = { + labels: labels, + datasets: [{ + label: 'My First dataset', + backgroundColor: 'rgba(54, 162, 235, 0.5)', + borderColor: 'rgb(54, 162, 235)', + borderWidth: 1, + data: [10, 20, 30, 40, 50, 0, 5], + }] +}; +// + +// +const config = { + type: 'radar', + data, + options: { + scales: { + r: { + pointLabels: { + color: 'red' + } + } + } + } +}; +// + +module.exports = { + actions: [], + config: config, +}; +``` + +### Ticks + +The ticks are used to label values based on how far they are from the center of the axis. In the example below, they are red. + +```js chart-editor +// +const labels = Utils.months({count: 7}); +const data = { + labels: labels, + datasets: [{ + label: 'My First dataset', + backgroundColor: 'rgba(54, 162, 235, 0.5)', + borderColor: 'rgb(54, 162, 235)', + borderWidth: 1, + data: [10, 20, 30, 40, 50, 0, 5], + }] +}; +// + +// +const config = { + type: 'radar', + data, + options: { + scales: { + r: { + ticks: { + color: 'red' + } + } + } + } +}; +// + +module.exports = { + actions: [], + config: config, +}; +``` diff --git a/docs/axes/radial/linear.md b/docs/axes/radial/linear.md new file mode 100644 index 00000000000..10c2109421d --- /dev/null +++ b/docs/axes/radial/linear.md @@ -0,0 +1,168 @@ +# Linear Radial Axis + +The linear radial scale is used to chart numerical data. As the name suggests, linear interpolation is used to determine where a value lies in relation to the center of the axis. + +The following additional configuration options are provided by the radial linear scale. + +## Configuration Options + +### Linear Radial Axis specific options + +Namespace: `options.scales[scaleId]` + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| `animate` | `boolean` | `true` | Whether to animate scaling the chart from the centre +| `angleLines` | `object` | | Angle line configuration. [more...](#angle-line-options) +| `beginAtZero` | `boolean` | `false` | If true, scale will include 0 if it is not already included. +| `pointLabels` | `object` | | Point label configuration. [more...](#point-label-options) +| `startAngle` | `number` | `0` | Starting angle of the scale. In degrees, 0 is at top. + +### Common options for all axes + +Namespace: `options.scales[scaleId]` + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| `type` | `string` | | Type of scale being employed. Custom scales can be created and registered with a string key. This allows changing the type of an axis for a chart. +| `alignToPixels` | `boolean` | `false` | Align pixel values to device pixels. +| `backgroundColor` | [`Color`](/general/colors.md) | | Background color of the scale area. +| `display` | `boolean`\|`string` | `true` | Controls the axis global visibility (visible when `true`, hidden when `false`). When `display: 'auto'`, the axis is visible only if at least one associated dataset is visible. +| `grid` | `object` | | Grid line configuration. [more...](#grid-line-configuration) +| `min` | `number` | | User defined minimum number for the scale, overrides minimum value from data. [more...](/axes/index.md#axis-range-settings) +| `max` | `number` | | User defined maximum number for the scale, overrides maximum value from data. [more...](/axes/index.md#axis-range-settings) +| `reverse` | `boolean` | `false` | Reverse the scale. +| `stacked` | `boolean`\|`string` | `false` | Should the data be stacked. [more...](/axes/index.md#stacking) +| `suggestedMax` | `number` | | Adjustment used when calculating the maximum data value. [more...](/axes/index.md#axis-range-settings) +| `suggestedMin` | `number` | | Adjustment used when calculating the minimum data value. [more...](/axes/index.md#axis-range-settings) +| `ticks` | `object` | | Tick configuration. [more...](/axes/index.md#tick-configuration) +| `weight` | `number` | `0` | The weight used to sort the axis. Higher weights are further away from the chart area. + +## Tick Configuration + +### Linear Radial Axis specific tick options + +Namespace: `options.scales[scaleId].ticks` + +| Name | Type | Scriptable | Default | Description +| ---- | ---- | ------- | ------- | ----------- +| `count` | `number` | Yes | `undefined` | The number of ticks to generate. If specified, this overrides the automatic generation. +| `format` | `object` | Yes | | The [`Intl.NumberFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat) options used by the default label formatter +| `maxTicksLimit` | `number` | Yes | `11` | Maximum number of ticks and gridlines to show. +| `precision` | `number` | Yes | | If defined and `stepSize` is not specified, the step size will be rounded to this many decimal places. +| `stepSize` | `number` | Yes | | User defined fixed step size for the scale. [more...](#step-size) + +!!!include(axes/_common_ticks.md)!!! + +The scriptable context is described in [Options](../../general/options.md#tick) section. + +## Grid Line Configuration + +Namespace: `options.scales[scaleId].grid`, it defines options for the grid lines of the axis. + +| Name | Type | Scriptable | Indexable | Default | Description +| ---- | ---- | :-------------------------------: | :-----------------------------: | ------- | ----------- +| `borderDash` | `number[]` | | | `[]` | Length and spacing of dashes on grid lines. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash). +| `borderDashOffset` | `number` | Yes | | `0.0` | Offset for line dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset). +| `circular` | `boolean` | | | `false` | If true, gridlines are circular (on radar and polar area charts only). +| `color` | [`Color`](../general/colors.md) | Yes | Yes | `Chart.defaults.borderColor` | The color of the grid lines. If specified as an array, the first color applies to the first grid line, the second to the second grid line, and so on. +| `display` | `boolean` | | | `true` | If false, do not display grid lines for this axis. +| `lineWidth` | `number` | Yes | Yes | `1` | Stroke width of grid lines. + +The scriptable context is described in [Options](../general/options.md#tick) section. + +## Axis Range Settings + +Given the number of axis range settings, it is important to understand how they all interact with each other. + +The `suggestedMax` and `suggestedMin` settings only change the data values that are used to scale the axis. These are useful for extending the range of the axis while maintaining the auto-fit behaviour. + +```javascript +let minDataValue = Math.min(mostNegativeValue, options.ticks.suggestedMin); +let maxDataValue = Math.max(mostPositiveValue, options.ticks.suggestedMax); +``` + +In this example, the largest positive value is 50, but the data maximum is expanded out to 100. However, because the lowest data value is below the `suggestedMin` setting, it is ignored. + +```javascript +let chart = new Chart(ctx, { + type: 'radar', + data: { + datasets: [{ + label: 'First dataset', + data: [0, 20, 40, 50] + }], + labels: ['January', 'February', 'March', 'April'] + }, + options: { + scales: { + r: { + suggestedMin: 50, + suggestedMax: 100 + } + } + } +}); +``` + +In contrast to the `suggested*` settings, the `min` and `max` settings set explicit ends to the axes. When these are set, some data points may not be visible. + +## Step Size + +If set, the scale ticks will be enumerated by multiple of `stepSize`, having one tick per increment. If not set, the ticks are labeled automatically using the nice numbers algorithm. + +This example sets up a chart with a y axis that creates ticks at `0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5`. + +```javascript +let options = { + scales: { + r: { + max: 5, + min: 0, + ticks: { + stepSize: 0.5 + } + } + } +}; +``` + +## Angle Line Options + +The following options are used to configure angled lines that radiate from the center of the chart to the point labels. +Namespace: `options.scales[scaleId].angleLines` + +| Name | Type | Scriptable | Default | Description +| ---- | ---- | ------- | ------- | ----------- +| `display` | `boolean` | | `true` | If true, angle lines are shown. +| `color` | [`Color`](../../general/colors.md) | Yes | `Chart.defaults.borderColor` | Color of angled lines. +| `lineWidth` | `number` | Yes | `1` | Width of angled lines. +| `borderDash` | `number[]` | Yes1 | `[]` | Length and spacing of dashes on angled lines. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash). +| `borderDashOffset` | `number` | Yes | `0.0` | Offset for line dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset). + + 1. the `borderDash` setting only accepts a static value or a function. Passing an array of arrays is not supported. + +The scriptable context is described in [Options](../../general/options.md#pointLabel) section. + +## Point Label Options + +The following options are used to configure the point labels that are shown on the perimeter of the scale. +Namespace: `options.scales[scaleId].pointLabels` + +| Name | Type | Scriptable | Default | Description +| ---- | ---- | ------- | ------- | ----------- +| `backdropColor` | [`Color`](../../general/colors.md) | `true` | `undefined` | Background color of the point label. +| `backdropPadding` | [`Padding`](../../general/padding.md) | | `2` | Padding of label backdrop. +| `borderRadius` | `number`\|`object` | `true` | `0` | Border radius of the point label +| `display` | `boolean`\|`string` | | `true` | If true, point labels are shown. When `display: 'auto'`, the label is hidden if it overlaps with another label. +| `callback` | `function` | | | Callback function to transform data labels to point labels. The default implementation simply returns the current string. +| `color` | [`Color`](../../general/colors.md) | Yes | `Chart.defaults.color` | Color of label. +| `font` | `Font` | Yes | `Chart.defaults.font` | See [Fonts](../../general/fonts.md) +| `padding` | `number` | Yes | 5 | Padding between chart and point labels. +| [`centerPointLabels`](../../samples/other-charts/polar-area-center-labels.md) | `boolean` | | `false` | If true, point labels are centered. + +The scriptable context is described in [Options](../../general/options.md#pointLabel) section. + +## Internal data format + +Internally, the linear radial scale uses numeric data diff --git a/docs/axes/styling.md b/docs/axes/styling.md new file mode 100644 index 00000000000..20c2b93f412 --- /dev/null +++ b/docs/axes/styling.md @@ -0,0 +1,52 @@ +# Styling + +There are a number of options to allow styling an axis. There are settings to control [grid lines](#grid-line-configuration) and [ticks](#tick-configuration). + +## Grid Line Configuration + +Namespace: `options.scales[scaleId].grid`, it defines options for the grid lines that run perpendicular to the axis. + +| Name | Type | Scriptable | Indexable | Default | Description +| ---- | ---- | :-------------------------------: | :-----------------------------: | ------- | ----------- +| `circular` | `boolean` | | | `false` | If true, gridlines are circular (on radar and polar area charts only). +| `color` | [`Color`](../general/colors.md) | Yes | Yes | `Chart.defaults.borderColor` | The color of the grid lines. If specified as an array, the first color applies to the first grid line, the second to the second grid line, and so on. +| `display` | `boolean` | | | `true` | If false, do not display grid lines for this axis. +| `drawOnChartArea` | `boolean` | | | `true` | If true, draw lines on the chart area inside the axis lines. This is useful when there are multiple axes and you need to control which grid lines are drawn. +| `drawTicks` | `boolean` | | | `true` | If true, draw lines beside the ticks in the axis area beside the chart. +| `lineWidth` | `number` | Yes | Yes | `1` | Stroke width of grid lines. +| `offset` | `boolean` | | | `false` | If true, grid lines will be shifted to be between labels. This is set to `true` for a bar chart by default. +| `tickBorderDash` | `number[]` | Yes | Yes | `[]` | Length and spacing of the tick mark line. If not set, defaults to the grid line `borderDash` value. +| `tickBorderDashOffset` | `number` | Yes | Yes | | Offset for the line dash of the tick mark. If unset, defaults to the grid line `borderDashOffset` value +| `tickColor` | [`Color`](../general/colors.md) | Yes | Yes | | Color of the tick line. If unset, defaults to the grid line color. +| `tickLength` | `number` | | | `8` | Length in pixels that the grid lines will draw into the axis area. +| `tickWidth` | `number` | Yes | Yes | | Width of the tick mark in pixels. If unset, defaults to the grid line width. +| `z` | `number` | | | `-1` | z-index of the gridline layer. Values <= 0 are drawn under datasets, > 0 on top. + +The scriptable context is described in [Options](../general/options.md#tick) section. + +## Tick Configuration + +!!!include(axes/_common_ticks.md)!!! + +The scriptable context is described in [Options](../general/options.md#tick) section. + +## Major Tick Configuration + +Namespace: `options.scales[scaleId].ticks.major`, it defines options for the major tick marks that are generated by the axis. + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| `enabled` | `boolean` | `false` | If true, major ticks are generated. A major tick will affect autoskipping and `major` will be defined on ticks in the scriptable options context. + +## Border Configuration + +Namespace: `options.scales[scaleId].border`, it defines options for the border that run perpendicular to the axis. + +| Name | Type | Scriptable | Indexable | Default | Description +| ---- | ---- | :-------------------------------: | :-----------------------------: | ------- | ----------- +| `display` | `boolean` | | | `true` | If true, draw a border at the edge between the axis and the chart area. +| `color` | [`Color`](../general/colors.md) | | | `Chart.defaults.borderColor` | The color of the border line. +| `width` | `number` | | | `1` | The width of the border line. +| `dash` | `number[]` | Yes | | `[]` | Length and spacing of dashes on grid lines. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash). +| `dashOffset` | `number` | Yes | | `0.0` | Offset for line dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset). +| `z` | `number` | | | `0` | z-index of the border layer. Values <= 0 are drawn under datasets, > 0 on top. diff --git a/docs/charts/area.md b/docs/charts/area.md new file mode 100644 index 00000000000..0825a90e6dc --- /dev/null +++ b/docs/charts/area.md @@ -0,0 +1,109 @@ +# Area Chart + +Both [line](./line.md) and [radar](./radar.md) charts support a `fill` option on the dataset object which can be used to create space between two datasets or a dataset and a boundary, i.e. the scale `origin`, `start,` or `end` (see [filling modes](#filling-modes)). + +:::tip Note +This feature is implemented by the [`filler` plugin](https://github.com/chartjs/Chart.js/blob/master/src/plugins/plugin.filler/index.js). +::: + +## Filling modes + +| Mode | Type | Values | +| :--- | :--- | :--- | +| Absolute dataset index | `number` | `1`, `2`, `3`, ... | +| Relative dataset index | `string` | `'-1'`, `'-2'`, `'+1'`, ... | +| Boundary | `string` | `'start'`, `'end'`, `'origin'` | +| Disabled 1 | `boolean` | `false` | +| Stacked value below | `string` | `'stack'` | +| Axis value | `object` | `{ value: number; }` | +| Shape (fill inside line) | `string` | `'shape'` | + +> 1 for backward compatibility, `fill: true` is equivalent to `fill: 'origin'`
    + +### Example + +```javascript +new Chart(ctx, { + data: { + datasets: [ + {fill: 'origin'}, // 0: fill to 'origin' + {fill: '+2'}, // 1: fill to dataset 3 + {fill: 1}, // 2: fill to dataset 1 + {fill: false}, // 3: no fill + {fill: '-2'}, // 4: fill to dataset 2 + {fill: {value: 25}} // 5: fill to axis value 25 + ] + } +}); +``` + +If you need to support multiple colors when filling from one dataset to another, you may specify an object with the following option : + +| Param | Type | Description | +| :--- | :--- | :--- | +| `target` | `number`, `string`, `boolean`, `object` | The accepted values are the same as the filling mode values, so you may use absolute and relative dataset indexes and/or boundaries. | +| `above` | `Color` | If no color is set, the default color will be the background color of the chart. | +| `below` | `Color` | Same as the above. | + +### Example with multiple colors + +```javascript +new Chart(ctx, { + data: { + datasets: [ + { + fill: { + target: 'origin', + above: 'rgb(255, 0, 0)', // Area will be red above the origin + below: 'rgb(0, 0, 255)' // And blue below the origin + } + } + ] + } +}); +``` + +## Configuration + +Namespace: `options.plugins.filler` + +| Option | Type | Default | Description | +| :--- | :--- | :--- | :--- | +| `drawTime` | `string` | `beforeDatasetDraw` | Filler draw time. Supported values: `'beforeDraw'`, `'beforeDatasetDraw'`, `'beforeDatasetsDraw'` +| [`propagate`](#propagate) | `boolean` | `true` | Fill propagation when target is hidden. + +### propagate + +`propagate` takes a `boolean` value (default: `true`). + +If `true`, the fill area will be recursively extended to the visible target defined by the `fill` value of hidden dataset targets: + +#### Example using propagate + +```javascript +new Chart(ctx, { + data: { + datasets: [ + {fill: 'origin'}, // 0: fill to 'origin' + {fill: '-1'}, // 1: fill to dataset 0 + {fill: 1}, // 2: fill to dataset 1 + {fill: false}, // 3: no fill + {fill: '-2'} // 4: fill to dataset 2 + ] + }, + options: { + plugins: { + filler: { + propagate: true + } + } + } +}); +``` + +`propagate: true`: +-if dataset 2 is hidden, dataset 4 will fill to dataset 1 +-if dataset 2 and 1 are hidden, dataset 4 will fill to `'origin'` + +`propagate: false`: +-if dataset 2 and/or 4 are hidden, dataset 4 will not be filled diff --git a/docs/charts/bar.md b/docs/charts/bar.md new file mode 100644 index 00000000000..36587d4c087 --- /dev/null +++ b/docs/charts/bar.md @@ -0,0 +1,359 @@ +# Bar Chart + +A bar chart provides a way of showing data values represented as vertical bars. It is sometimes used to show trend data, and the comparison of multiple data sets side by side. + +```js chart-editor +// +const labels = Utils.months({count: 7}); +const data = { + labels: labels, + datasets: [{ + label: 'My First Dataset', + data: [65, 59, 80, 81, 56, 55, 40], + backgroundColor: [ + 'rgba(255, 99, 132, 0.2)', + 'rgba(255, 159, 64, 0.2)', + 'rgba(255, 205, 86, 0.2)', + 'rgba(75, 192, 192, 0.2)', + 'rgba(54, 162, 235, 0.2)', + 'rgba(153, 102, 255, 0.2)', + 'rgba(201, 203, 207, 0.2)' + ], + borderColor: [ + 'rgb(255, 99, 132)', + 'rgb(255, 159, 64)', + 'rgb(255, 205, 86)', + 'rgb(75, 192, 192)', + 'rgb(54, 162, 235)', + 'rgb(153, 102, 255)', + 'rgb(201, 203, 207)' + ], + borderWidth: 1 + }] +}; +// + +// +const config = { + type: 'bar', + data: data, + options: { + scales: { + y: { + beginAtZero: true + } + } + }, +}; +// + +module.exports = { + actions: [], + config: config, +}; +``` + +## Dataset Properties + +Namespaces: + +* `data.datasets[index]` - options for this dataset only +* `options.datasets.bar` - options for all bar datasets +* `options.elements.bar` - options for all [bar elements](../configuration/elements.md#bar-configuration) +* `options` - options for the whole chart + +The bar chart allows a number of properties to be specified for each dataset. +These are used to set display properties for a specific dataset. For example, +the color of the bars is generally set this way. +Only the `data` option needs to be specified in the dataset namespace. + +| Name | Type | [Scriptable](../general/options.md#scriptable-options) | [Indexable](../general/options.md#indexable-options) | Default +| ---- | ---- | :----: | :----: | ---- +| [`backgroundColor`](#styling) | [`Color`](../general/colors.md) | Yes | Yes | `'rgba(0, 0, 0, 0.1)'` +| [`base`](#general) | `number` | Yes | Yes | +| [`barPercentage`](#barpercentage) | `number` | - | - | `0.9` | +| [`barThickness`](#barthickness) | `number`\|`string` | - | - | | +| [`borderColor`](#styling) | [`Color`](../general/colors.md) | Yes | Yes | `'rgba(0, 0, 0, 0.1)'` +| [`borderSkipped`](#borderskipped) | `string`\|`boolean` | Yes | Yes | `'start'` +| [`borderWidth`](#borderwidth) | `number`\|`object` | Yes | Yes | `0` +| [`borderRadius`](#borderradius) | `number`\|`object` | Yes | Yes | `0` +| [`categoryPercentage`](#categorypercentage) | `number` | - | - | `0.8` | +| [`clip`](#general) | `number`\|`object`\|`false` | - | - | +| [`data`](#data-structure) | `object`\|`object[]`\| `number[]`\|`string[]` | - | - | **required** +| [`grouped`](#general) | `boolean` | - | - | `true` | +| [`hoverBackgroundColor`](#interactions) | [`Color`](../general/colors.md) | Yes | Yes | +| [`hoverBorderColor`](#interactions) | [`Color`](../general/colors.md) | Yes | Yes | +| [`hoverBorderWidth`](#interactions) | `number` | Yes | Yes | `1` +| [`hoverBorderRadius`](#interactions) | `number` | Yes | Yes | `0` +| [`indexAxis`](#general) | `string` | - | - | `'x'` +| [`inflateAmount`](#inflateamount) | `number`\|`'auto'` | Yes | Yes | `'auto'` +| [`maxBarThickness`](#maxbarthickness) | `number` | - | - | | +| [`minBarLength`](#styling) | `number` | - | - | | +| [`label`](#general) | `string` | - | - | `''` +| [`order`](#general) | `number` | - | - | `0` +| [`pointStyle`](../configuration/elements.md#point-styles) | [`pointStyle`](../configuration/elements.md#types) | Yes | - | `'circle'` +| [`skipNull`](#general) | `boolean` | - | - | | +| [`stack`](#general) | `string` | - | - | `'bar'` | +| [`xAxisID`](#general) | `string` | - | - | first x axis +| [`yAxisID`](#general) | `string` | - | - | first y axis + +All these values, if `undefined`, fallback to the scopes described in [option resolution](../general/options) + +### Example dataset configuration + +```javascript +data: { + datasets: [{ + barPercentage: 0.5, + barThickness: 6, + maxBarThickness: 8, + minBarLength: 2, + data: [10, 20, 30, 40, 50, 60, 70] + }] +}; +``` + +### General + +| Name | Description +| ---- | ---- +| `base` | Base value for the bar in data units along the value axis. If not set, defaults to the value axis base value. +| `clip` | How to clip relative to chartArea. Positive value allows overflow, negative value clips that many pixels inside chartArea. `0` = clip at chartArea. Clipping can also be configured per side: `clip: {left: 5, top: false, right: -2, bottom: 0}` +| `grouped` | Should the bars be grouped on index axis. When `true`, all the datasets at same index value will be placed next to each other centering on that index value. When `false`, each bar is placed on its actual index-axis value. +| `indexAxis` | The base axis of the dataset. `'x'` for vertical bars and `'y'` for horizontal bars. +| `label` | The label for the dataset which appears in the legend and tooltips. +| `order` | The drawing order of dataset. Also affects order for stacking, tooltip and legend. [more](mixed.md#drawing-order) +| `skipNull` | If `true`, null or undefined values will not be used for spacing calculations when determining bar size. +| `stack` | The ID of the group to which this dataset belongs to (when stacked, each group will be a separate stack). [more](#stacked-bar-chart) +| `xAxisID` | The ID of the x-axis to plot this dataset on. +| `yAxisID` | The ID of the y-axis to plot this dataset on. + +### Styling + +The style of each bar can be controlled with the following properties: + +| Name | Description +| ---- | ---- +| `backgroundColor` | The bar background color. +| `borderColor` | The bar border color. +| [`borderSkipped`](#borderskipped) | The edge to skip when drawing bar. +| [`borderWidth`](#borderwidth) | The bar border width (in pixels). +| [`borderRadius`](#borderradius) | The bar border radius (in pixels). +| `minBarLength` | Set this to ensure that bars have a minimum length in pixels. +| `pointStyle` | Style of the point for legend. [more...](../configuration/elements.md#point-styles) + +All these values, if `undefined`, fallback to the associated [`elements.bar.*`](../configuration/elements.md#bar-configuration) options. + +#### borderSkipped + +This setting is used to avoid drawing the bar stroke at the base of the fill, or disable the border radius. +In general, this does not need to be changed except when creating chart types +that derive from a bar chart. + +:::tip Note +For negative bars in a vertical chart, `top` and `bottom` are flipped. Same goes for `left` and `right` in a horizontal chart. +::: + +Options are: + +* `'start'` +* `'end'` +* `'middle'` (only valid on stacked bars: the borders between bars are skipped) +* `'bottom'` +* `'left'` +* `'top'` +* `'right'` +* `false` (don't skip any borders) +* `true` (skip all borders) + +#### borderWidth + +If this value is a number, it is applied to all sides of the rectangle (left, top, right, bottom), except [`borderSkipped`](#borderskipped). If this value is an object, the `left` property defines the left border width. Similarly, the `right`, `top`, and `bottom` properties can also be specified. Omitted borders and [`borderSkipped`](#borderskipped) are skipped. + +#### borderRadius + +If this value is a number, it is applied to all corners of the rectangle (topLeft, topRight, bottomLeft, bottomRight), except corners touching the [`borderSkipped`](#borderskipped). If this value is an object, the `topLeft` property defines the top-left corners border radius. Similarly, the `topRight`, `bottomLeft`, and `bottomRight` properties can also be specified. Omitted corners and those touching the [`borderSkipped`](#borderskipped) are skipped. For example if the `top` border is skipped, the border radius for the corners `topLeft` and `topRight` will be skipped as well. + +:::tip Stacked Charts +When the border radius is supplied as a number and the chart is stacked, the radius will only be applied to the bars that are at the edges of the stack or where the bar is floating. The object syntax can be used to override this behavior. +::: + +#### inflateAmount + +This option can be used to inflate the rects that are used to draw the bars. This can be used to hide artifacts between bars when [`barPercentage`](#barpercentage) * [`categoryPercentage`](#categorypercentage) is 1. The default value `'auto'` should work in most cases. + +### Interactions + +The interaction with each bar can be controlled with the following properties: + +| Name | Description +| ---- | ----------- +| `hoverBackgroundColor` | The bar background color when hovered. +| `hoverBorderColor` | The bar border color when hovered. +| `hoverBorderWidth` | The bar border width when hovered (in pixels). +| `hoverBorderRadius` | The bar border radius when hovered (in pixels). + +All these values, if `undefined`, fallback to the associated [`elements.bar.*`](../configuration/elements.md#bar-configuration) options. + +### barPercentage + +Percent (0-1) of the available width each bar should be within the category width. 1.0 will take the whole category width and put the bars right next to each other. [more...](#barpercentage-vs-categorypercentage) + +### categoryPercentage + +Percent (0-1) of the available width each category should be within the sample width. [more...](#barpercentage-vs-categorypercentage) + +### barThickness + +If this value is a number, it is applied to the width of each bar, in pixels. When this is enforced, `barPercentage` and `categoryPercentage` are ignored. + +If set to `'flex'`, the base sample widths are calculated automatically based on the previous and following samples so that they take the full available widths without overlap. Then, bars are sized using `barPercentage` and `categoryPercentage`. There is no gap when the percentage options are 1. This mode generates bars with different widths when data are not evenly spaced. + +If not set (default), the base sample widths are calculated using the smallest interval that prevents bar overlapping, and bars are sized using `barPercentage` and `categoryPercentage`. This mode always generates bars equally sized. + +### maxBarThickness + +Set this to ensure that bars are not sized thicker than this. + +## Scale Configuration + +The bar chart sets unique default values for the following configuration from the associated `scale` options: + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| `offset` | `boolean` | `true` | If true, extra space is added to both edges and the axis is scaled to fit into the chart area. +| `grid.offset` | `boolean` | `true` | If true, the bars for a particular data point fall between the grid lines. The grid line will move to the left by one half of the tick interval. If false, the grid line will go right down the middle of the bars. [more...](#offsetgridlines) + +### Example scale configuration + +```javascript +options = { + scales: { + x: { + grid: { + offset: true + } + } + } +}; +``` + +### Offset Grid Lines + +If true, the bars for a particular data point fall between the grid lines. The grid line will move to the left by one half of the tick interval, which is the space between the grid lines. If false, the grid line will go right down the middle of the bars. This is set to true for a category scale in a bar chart while false for other scales or chart types by default. + +## Default Options + +It is common to want to apply a configuration setting to all created bar charts. The global bar chart settings are stored in `Chart.overrides.bar`. Changing the global options only affects charts created after the change. Existing charts are not changed. + +## barPercentage vs categoryPercentage + +The following shows the relationship between the bar percentage option and the category percentage option. + +``` +// categoryPercentage: 1.0 +// barPercentage: 1.0 +Bar: | 1.0 | 1.0 | +Category: | 1.0 | +Sample: |===========| + +// categoryPercentage: 1.0 +// barPercentage: 0.5 +Bar: |.5| |.5| +Category: | 1.0 | +Sample: |==============| + +// categoryPercentage: 0.5 +// barPercentage: 1.0 +Bar: |1.0||1.0| +Category: | .5 | +Sample: |==================| +``` + +## Data Structure + +All the supported [data structures](../general/data-structures.md) can be used with bar charts. + +## Stacked Bar Chart + +Bar charts can be configured into stacked bar charts by changing the settings on the X and Y axes to enable stacking. Stacked bar charts can be used to show how one data series is made up of a number of smaller pieces. + +```javascript +const stackedBar = new Chart(ctx, { + type: 'bar', + data: data, + options: { + scales: { + x: { + stacked: true + }, + y: { + stacked: true + } + } + } +}); +``` + +## Horizontal Bar Chart + +A horizontal bar chart is a variation on a vertical bar chart. It is sometimes used to show trend data, and the comparison of multiple data sets side by side. +To achieve this, you will have to set the `indexAxis` property in the options object to `'y'`. +The default for this property is `'x'` and thus will show vertical bars. + +```js chart-editor +// +const labels = Utils.months({count: 7}); +const data = { + labels: labels, + datasets: [{ + axis: 'y', + label: 'My First Dataset', + data: [65, 59, 80, 81, 56, 55, 40], + fill: false, + backgroundColor: [ + 'rgba(255, 99, 132, 0.2)', + 'rgba(255, 159, 64, 0.2)', + 'rgba(255, 205, 86, 0.2)', + 'rgba(75, 192, 192, 0.2)', + 'rgba(54, 162, 235, 0.2)', + 'rgba(153, 102, 255, 0.2)', + 'rgba(201, 203, 207, 0.2)' + ], + borderColor: [ + 'rgb(255, 99, 132)', + 'rgb(255, 159, 64)', + 'rgb(255, 205, 86)', + 'rgb(75, 192, 192)', + 'rgb(54, 162, 235)', + 'rgb(153, 102, 255)', + 'rgb(201, 203, 207)' + ], + borderWidth: 1 + }] +}; +// + +// +const config = { + type: 'bar', + data, + options: { + indexAxis: 'y', + } +}; +// + +module.exports = { + actions: [], + config: config, +}; +``` + +### Horizontal Bar Chart config Options + +The configuration options for the horizontal bar chart are the same as for the [bar chart](#scale-configuration). However, any options specified on the x-axis in a bar chart, are applied to the y-axis in a horizontal bar chart. + +## Internal data format + +`{x, y, _custom}` where `_custom` is an optional object defining stacked bar properties: `{start, end, barStart, barEnd, min, max}`. `start` and `end` are the input values. Those two are repeated in `barStart` (closer to origin), `barEnd` (further from origin), `min` and `max`. diff --git a/docs/charts/bubble.md b/docs/charts/bubble.md new file mode 100644 index 00000000000..64bc390b4e2 --- /dev/null +++ b/docs/charts/bubble.md @@ -0,0 +1,133 @@ +# Bubble Chart + +A bubble chart is used to display three dimensions of data at the same time. The location of the bubble is determined by the first two dimensions and the corresponding horizontal and vertical axes. The third dimension is represented by the size of the individual bubbles. + +```js chart-editor +// +const data = { + datasets: [{ + label: 'First Dataset', + data: [{ + x: 20, + y: 30, + r: 15 + }, { + x: 40, + y: 10, + r: 10 + }], + backgroundColor: 'rgb(255, 99, 132)' + }] +}; +// + +// +const config = { + type: 'bubble', + data: data, + options: {} +}; +// + +module.exports = { + actions: [], + config: config, +}; +``` + +## Dataset Properties + +Namespaces: + +* `data.datasets[index]` - options for this dataset only +* `options.datasets.bubble` - options for all bubble datasets +* `options.elements.point` - options for all [point elements](../configuration/elements.md#point-configuration) +* `options` - options for the whole chart + +The bubble chart allows a number of properties to be specified for each dataset. These are used to set display properties for a specific dataset. For example, the colour of the bubbles is generally set this way. + +| Name | Type | [Scriptable](../general/options.md#scriptable-options) | [Indexable](../general/options.md#indexable-options) | Default +| ---- | ---- | :----: | :----: | ---- +| [`backgroundColor`](#styling) | [`Color`](../general/colors.md) | Yes | Yes | `'rgba(0, 0, 0, 0.1)'` +| [`borderColor`](#styling) | [`Color`](../general/colors.md) | Yes | Yes | `'rgba(0, 0, 0, 0.1)'` +| [`borderWidth`](#styling) | `number` | Yes | Yes | `3` +| [`clip`](#general) | `number`\|`object`\|`false` | - | - | `undefined` +| [`data`](#data-structure) | `object[]` | - | - | **required** +| [`drawActiveElementsOnTop`](#general) | `boolean` | Yes | Yes | `true` +| [`hoverBackgroundColor`](#interactions) | [`Color`](../general/colors.md) | Yes | Yes | `undefined` +| [`hoverBorderColor`](#interactions) | [`Color`](../general/colors.md) | Yes | Yes | `undefined` +| [`hoverBorderWidth`](#interactions) | `number` | Yes | Yes | `1` +| [`hoverRadius`](#interactions) | `number` | Yes | Yes | `4` +| [`hitRadius`](#interactions) | `number` | Yes | Yes | `1` +| [`label`](#general) | `string` | - | - | `undefined` +| [`order`](#general) | `number` | - | - | `0` +| [`pointStyle`](#styling) | [`pointStyle`](../configuration/elements.md#types) | Yes | Yes | `'circle'` +| [`rotation`](#styling) | `number` | Yes | Yes | `0` +| [`radius`](#styling) | `number` | Yes | Yes | `3` + +All these values, if `undefined`, fallback to the scopes described in [option resolution](../general/options) + +### General + +| Name | Description +| ---- | ---- +| `clip` | How to clip relative to chartArea. Positive value allows overflow, negative value clips that many pixels inside chartArea. `0` = clip at chartArea. Clipping can also be configured per side: `clip: {left: 5, top: false, right: -2, bottom: 0}` +| `drawActiveElementsOnTop` | Draw the active bubbles of a dataset over the other bubbles of the dataset +| `label` | The label for the dataset which appears in the legend and tooltips. +| `order` | The drawing order of dataset. Also affects order for tooltip and legend. [more](mixed.md#drawing-order) + +### Styling + +The style of each bubble can be controlled with the following properties: + +| Name | Description +| ---- | ---- +| `backgroundColor` | bubble background color. +| `borderColor` | bubble border color. +| `borderWidth` | bubble border width (in pixels). +| `pointStyle` | bubble [shape style](../configuration/elements.md#point-styles). +| `rotation` | bubble rotation (in degrees). +| `radius` | bubble radius (in pixels). + +All these values, if `undefined`, fallback to the associated [`elements.point.*`](../configuration/elements.md#point-configuration) options. + +### Interactions + +The interaction with each bubble can be controlled with the following properties: + +| Name | Description +| ---- | ----------- +| `hitRadius` | bubble **additional** radius for hit detection (in pixels). +| `hoverBackgroundColor` | bubble background color when hovered. +| `hoverBorderColor` | bubble border color when hovered. +| `hoverBorderWidth` | bubble border width when hovered (in pixels). +| `hoverRadius` | bubble **additional** radius when hovered (in pixels). + +All these values, if `undefined`, fallback to the associated [`elements.point.*`](../configuration/elements.md#point-configuration) options. + +## Default Options + +We can also change the default values for the Bubble chart type. Doing so will give all bubble charts created after this point the new defaults. The default configuration for the bubble chart can be accessed at `Chart.overrides.bubble`. + +## Data Structure + +Bubble chart datasets need to contain a `data` array of points, each point represented by an object containing the following properties: + +```javascript +{ + // X Value + x: number, + + // Y Value + y: number, + + // Bubble radius in pixels (not scaled). + r: number +} +``` + +**Important:** the radius property, `r` is **not** scaled by the chart, it is the raw radius in pixels of the bubble that is drawn on the canvas. + +## Internal data format + +`{x, y, _custom}` where `_custom` is the radius. diff --git a/docs/charts/doughnut.md b/docs/charts/doughnut.md new file mode 100644 index 00000000000..0209a8f9b99 --- /dev/null +++ b/docs/charts/doughnut.md @@ -0,0 +1,221 @@ +# Doughnut and Pie Charts + +Pie and doughnut charts are probably the most commonly used charts. They are divided into segments, the arc of each segment shows the proportional value of each piece of data. + +They are excellent at showing the relational proportions between data. + +Pie and doughnut charts are effectively the same class in Chart.js, but have one different default value - their `cutout`. This equates to what portion of the inner should be cut out. This defaults to `0` for pie charts, and `'50%'` for doughnuts. + +They are also registered under two aliases in the `Chart` core. Other than their different default value, and different alias, they are exactly the same. + +:::: tabs + +::: tab Doughnut + +```js chart-editor +// +const data = { + labels: [ + 'Red', + 'Blue', + 'Yellow' + ], + datasets: [{ + label: 'My First Dataset', + data: [300, 50, 100], + backgroundColor: [ + 'rgb(255, 99, 132)', + 'rgb(54, 162, 235)', + 'rgb(255, 205, 86)' + ], + hoverOffset: 4 + }] +}; +// + +// +const config = { + type: 'doughnut', + data: data, +}; +// + +module.exports = { + actions: [], + config: config, +}; +``` + +::: + +:::tab Pie + +```js chart-editor +// +const data = { + labels: [ + 'Red', + 'Blue', + 'Yellow' + ], + datasets: [{ + label: 'My First Dataset', + data: [300, 50, 100], + backgroundColor: [ + 'rgb(255, 99, 132)', + 'rgb(54, 162, 235)', + 'rgb(255, 205, 86)' + ], + hoverOffset: 4 + }] +}; +// + +// +const config = { + type: 'pie', + data: data, +}; +// + +module.exports = { + actions: [], + config: config, +}; +``` + +::: + +:::: + +## Dataset Properties + +Namespaces: + +* `data.datasets[index]` - options for this dataset only +* `options.datasets.doughnut` - options for all doughnut datasets +* `options.datasets.pie` - options for all pie datasets +* `options.elements.arc` - options for all [arc elements](../configuration/elements.md#arc-configuration) +* `options` - options for the whole chart + +The doughnut/pie chart allows a number of properties to be specified for each dataset. These are used to set display properties for a specific dataset. For example, the colours of the dataset's arcs are generally set this way. + +| Name | Type | [Scriptable](../general/options.md#scriptable-options) | [Indexable](../general/options.md#indexable-options) | Default +| ---- | ---- | :----: | :----: | ---- +| [`backgroundColor`](#styling) | [`Color`](../general/colors.md) | Yes | Yes | `'rgba(0, 0, 0, 0.1)'` +| [`borderAlign`](#border-alignment) | `'center'`\|`'inner'` | Yes | Yes | `'center'` +| [`borderColor`](#styling) | [`Color`](../general/colors.md) | Yes | Yes | `'#fff'` +| [`borderDash`](#styling) | `number[]` | Yes | - | `[]` +| [`borderDashOffset`](#styling) | `number` | Yes | - | `0.0` +| [`borderJoinStyle`](#styling) | `'round'`\|`'bevel'`\|`'miter'` | Yes | Yes | `undefined` +| [`borderRadius`](#border-radius) | `number`\|`object` | Yes | Yes | `0` +| [`borderWidth`](#styling) | `number` | Yes | Yes | `2` +| [`circumference`](#general) | `number` | - | - | `undefined` +| [`clip`](#general) | `number`\|`object`\|`false` | - | - | `undefined` +| [`data`](#data-structure) | `number[]` | - | - | **required** +| [`hoverBackgroundColor`](#interactions) | [`Color`](../general/colors.md) | Yes | Yes | `undefined` +| [`hoverBorderColor`](#interactions) | [`Color`](../general/colors.md) | Yes | Yes | `undefined` +| [`hoverBorderDash`](#interactions) | `number[]` | Yes | - | `undefined` +| [`hoverBorderDashOffset`](#interactions) | `number` | Yes | - | `undefined` +| [`hoverBorderJoinStyle`](#interactions) | `'round'`\|`'bevel'`\|`'miter'` | Yes | Yes | `undefined` +| [`hoverBorderWidth`](#interactions) | `number` | Yes | Yes | `undefined` +| [`hoverOffset`](#interactions) | `number` | Yes | Yes | `0` +| [`offset`](#styling) | `number`\|`number[]` | Yes | Yes | `0` +| [`rotation`](#general) | `number` | - | - | `undefined` +| [`spacing`](#styling) | `number` | - | - | `0` +| [`weight`](#styling) | `number` | - | - | `1` + +All these values, if `undefined`, fallback to the scopes described in [option resolution](../general/options) + +### General + +| Name | Description +| ---- | ---- +| `circumference` | Per-dataset override for the sweep that the arcs cover +| `clip` | How to clip relative to chartArea. Positive value allows overflow, negative value clips that many pixels inside chartArea. `0` = clip at chartArea. Clipping can also be configured per side: `clip: {left: 5, top: false, right: -2, bottom: 0}` +| `rotation` | Per-dataset override for the starting angle to draw arcs from + +### Styling + +The style of each arc can be controlled with the following properties: + +| Name | Description +| ---- | ---- +| `backgroundColor` | arc background color. +| `borderColor` | arc border color. +| `borderDash` | arc border length and spacing of dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash). +| `borderDashOffset` | arc border offset for line dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset). +| `borderJoinStyle` | arc border join style. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin). +| `borderWidth` | arc border width (in pixels). +| `offset` | arc offset (in pixels). +| `spacing` | Fixed arc offset (in pixels). Similar to `offset` but applies to all arcs. +| `weight` | The relative thickness of the dataset. Providing a value for weight will cause the pie or doughnut dataset to be drawn with a thickness relative to the sum of all the dataset weight values. + +All these values, if `undefined`, fallback to the associated [`elements.arc.*`](../configuration/elements.md#arc-configuration) options. + +### Border Alignment + +The following values are supported for `borderAlign`. + +* `'center'` (default) +* `'inner'` + +When `'center'` is set, the borders of arcs next to each other will overlap. When `'inner'` is set, it is guaranteed that all borders will not overlap. + +### Border Radius + +If this value is a number, it is applied to all corners of the arc (outerStart, outerEnd, innerStart, innerRight). If this value is an object, the `outerStart` property defines the outer-start corner's border radius. Similarly, the `outerEnd`, `innerStart`, and `innerEnd` properties can also be specified. + +### Interactions + +The interaction with each arc can be controlled with the following properties: + +| Name | Description +| ---- | ----------- +| `hoverBackgroundColor` | arc background color when hovered. +| `hoverBorderColor` | arc border color when hovered. +| `hoverBorderDash` | arc border length and spacing of dashes when hovered. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash). +| `hoverBorderDashOffset` | arc border offset for line dashes when hovered. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset). +| `hoverBorderJoinStyle` | arc border join style when hovered. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin). +| `hoverBorderWidth` | arc border width when hovered (in pixels). +| `hoverOffset` | arc offset when hovered (in pixels). + +All these values, if `undefined`, fallback to the associated [`elements.arc.*`](../configuration/elements.md#arc-configuration) options. + +## Config Options + +These are the customisation options specific to Pie & Doughnut charts. These options are looked up on access, and form together with the global chart configuration the options of the chart. + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| `cutout` | `number`\|`string` | `50%` - for doughnut, `0` - for pie | The portion of the chart that is cut out of the middle. If `string` and ending with '%', percentage of the chart radius. `number` is considered to be pixels. +| `radius` | `number`\|`string` | `100%` | The outer radius of the chart. If `string` and ending with '%', percentage of the maximum radius. `number` is considered to be pixels. +| `rotation` | `number` | 0 | Starting angle to draw arcs from. +| `circumference` | `number` | 360 | Sweep to allow arcs to cover. +| `animation.animateRotate` | `boolean` | `true` | If true, the chart will animate in with a rotation animation. This property is in the `options.animation` object. +| `animation.animateScale` | `boolean` | `false` | If true, will animate scaling the chart from the center outwards. + +## Default Options + +We can also change these default values for each Doughnut type that is created, this object is available at `Chart.overrides.doughnut`. Pie charts also have a clone of these defaults available to change at `Chart.overrides.pie`, with the only difference being `cutout` being set to 0. + +## Data Structure + +For a pie chart, datasets need to contain an array of data points. The data points should be a number, Chart.js will total all the numbers and calculate the relative proportion of each. + +You also need to specify an array of labels so that tooltips appear correctly. + +```javascript +data = { + datasets: [{ + data: [10, 20, 30] + }], + + // These labels appear in the legend and in the tooltips when hovering different arcs + labels: [ + 'Red', + 'Yellow', + 'Blue' + ] +}; +``` diff --git a/docs/charts/line.md b/docs/charts/line.md new file mode 100644 index 00000000000..1bbc9de01b9 --- /dev/null +++ b/docs/charts/line.md @@ -0,0 +1,293 @@ +# Line Chart + +A line chart is a way of plotting data points on a line. Often, it is used to show trend data, or the comparison of two data sets. + +```js chart-editor +// +const labels = Utils.months({count: 7}); +const data = { + labels: labels, + datasets: [{ + label: 'My First Dataset', + data: [65, 59, 80, 81, 56, 55, 40], + fill: false, + borderColor: 'rgb(75, 192, 192)', + tension: 0.1 + }] +}; +// + +// +const config = { + type: 'line', + data: data, +}; +// + +module.exports = { + actions: [], + config: config, +}; +``` + +## Dataset Properties + +Namespaces: + +* `data.datasets[index]` - options for this dataset only +* `options.datasets.line` - options for all line datasets +* `options.elements.line` - options for all [line elements](../configuration/elements.md#line-configuration) +* `options.elements.point` - options for all [point elements](../configuration/elements.md#point-configuration) +* `options` - options for the whole chart + +The line chart allows a number of properties to be specified for each dataset. These are used to set display properties for a specific dataset. For example, the colour of a line is generally set this way. + +| Name | Type | [Scriptable](../general/options.md#scriptable-options) | [Indexable](../general/options.md#indexable-options) | Default +| ---- | ---- | :----: | :----: | ---- +| [`backgroundColor`](#line-styling) | [`Color`](../general/colors.md) | Yes | - | `'rgba(0, 0, 0, 0.1)'` +| [`borderCapStyle`](#line-styling) | `string` | Yes | - | `'butt'` +| [`borderColor`](#line-styling) | [`Color`](../general/colors.md) | Yes | - | `'rgba(0, 0, 0, 0.1)'` +| [`borderDash`](#line-styling) | `number[]` | Yes | - | `[]` +| [`borderDashOffset`](#line-styling) | `number` | Yes | - | `0.0` +| [`borderJoinStyle`](#line-styling) | `'round'`\|`'bevel'`\|`'miter'` | Yes | - | `'miter'` +| [`borderWidth`](#line-styling) | `number` | Yes | - | `3` +| [`clip`](#general) | `number`\|`object`\|`false` | - | - | `undefined` +| [`cubicInterpolationMode`](#cubicinterpolationmode) | `string` | Yes | - | `'default'` +| [`data`](#data-structure) | `object`\|`object[]`\| `number[]`\|`string[]` | - | - | **required** +| [`drawActiveElementsOnTop`](#general) | `boolean` | Yes | Yes | `true` +| [`fill`](#line-styling) | `boolean`\|`string` | Yes | - | `false` +| [`hoverBackgroundColor`](#line-styling) | [`Color`](../general/colors.md) | Yes | - | `undefined` +| [`hoverBorderCapStyle`](#line-styling) | `string` | Yes | - | `undefined` +| [`hoverBorderColor`](#line-styling) | [`Color`](../general/colors.md) | Yes | - | `undefined` +| [`hoverBorderDash`](#line-styling) | `number[]` | Yes | - | `undefined` +| [`hoverBorderDashOffset`](#line-styling) | `number` | Yes | - | `undefined` +| [`hoverBorderJoinStyle`](#line-styling) | `'round'`\|`'bevel'`\|`'miter'` | Yes | - | `undefined` +| [`hoverBorderWidth`](#line-styling) | `number` | Yes | - | `undefined` +| [`indexAxis`](#general) | `string` | - | - | `'x'` +| [`label`](#general) | `string` | - | - | `''` +| [`order`](#general) | `number` | - | - | `0` +| [`pointBackgroundColor`](#point-styling) | `Color` | Yes | Yes | `'rgba(0, 0, 0, 0.1)'` +| [`pointBorderColor`](#point-styling) | `Color` | Yes | Yes | `'rgba(0, 0, 0, 0.1)'` +| [`pointBorderWidth`](#point-styling) | `number` | Yes | Yes | `1` +| [`pointHitRadius`](#point-styling) | `number` | Yes | Yes | `1` +| [`pointHoverBackgroundColor`](#interactions) | `Color` | Yes | Yes | `undefined` +| [`pointHoverBorderColor`](#interactions) | `Color` | Yes | Yes | `undefined` +| [`pointHoverBorderWidth`](#interactions) | `number` | Yes | Yes | `1` +| [`pointHoverRadius`](#interactions) | `number` | Yes | Yes | `4` +| [`pointRadius`](#point-styling) | `number` | Yes | Yes | `3` +| [`pointRotation`](#point-styling) | `number` | Yes | Yes | `0` +| [`pointStyle`](#point-styling) | [`pointStyle`](../configuration/elements.md#types) | Yes | Yes | `'circle'` +| [`segment`](#segment) | `object` | - | - | `undefined` +| [`showLine`](#line-styling) | `boolean` | - | - | `true` +| [`spanGaps`](#line-styling) | `boolean`\|`number` | - | - | `undefined` +| [`stack`](#general) | `string` | - | - | `'line'` | +| [`stepped`](#stepped) | `boolean`\|`string` | - | - | `false` +| [`tension`](#line-styling) | `number` | - | - | `0` +| [`xAxisID`](#general) | `string` | - | - | first x axis +| [`yAxisID`](#general) | `string` | - | - | first y axis + +All these values, if `undefined`, fallback to the scopes described in [option resolution](../general/options) + +### General + +| Name | Description +| ---- | ---- +| `clip` | How to clip relative to chartArea. Positive value allows overflow, negative value clips that many pixels inside chartArea. `0` = clip at chartArea. Clipping can also be configured per side: `clip: {left: 5, top: false, right: -2, bottom: 0}` +| `drawActiveElementsOnTop` | Draw the active points of a dataset over the other points of the dataset +| `indexAxis` | The base axis of the dataset. `'x'` for horizontal lines and `'y'` for vertical lines. +| `label` | The label for the dataset which appears in the legend and tooltips. +| `order` | The drawing order of dataset. Also affects order for stacking, tooltip and legend. [more](mixed.md#drawing-order) +| `stack` | The ID of the group to which this dataset belongs to (when stacked, each group will be a separate stack). [more](#stacked-area-chart) +| `xAxisID` | The ID of the x-axis to plot this dataset on. +| `yAxisID` | The ID of the y-axis to plot this dataset on. + +### Point Styling + +The style of each point can be controlled with the following properties: + +| Name | Description +| ---- | ---- +| `pointBackgroundColor` | The fill color for points. +| `pointBorderColor` | The border color for points. +| `pointBorderWidth` | The width of the point border in pixels. +| `pointHitRadius` | The pixel size of the non-displayed point that reacts to mouse events. +| `pointRadius` | The radius of the point shape. If set to 0, the point is not rendered. +| `pointRotation` | The rotation of the point in degrees. +| `pointStyle` | Style of the point. [more...](../configuration/elements.md#point-styles) + +All these values, if `undefined`, fallback first to the dataset options then to the associated [`elements.point.*`](../configuration/elements.md#point-configuration) options. + +### Line Styling + +The style of the line can be controlled with the following properties: + +| Name | Description +| ---- | ---- +| `backgroundColor` | The line fill color. +| `borderCapStyle` | Cap style of the line. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap). +| `borderColor` | The line color. +| `borderDash` | Length and spacing of dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash). +| `borderDashOffset` | Offset for line dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset). +| `borderJoinStyle` | Line joint style. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin). +| `borderWidth` | The line width (in pixels). +| `fill` | How to fill the area under the line. See [area charts](area.md). +| `tension` | Bezier curve tension of the line. Set to 0 to draw straightlines. This option is ignored if monotone cubic interpolation is used. +| `showLine` | If false, the line is not drawn for this dataset. +| `spanGaps` | If true, lines will be drawn between points with no or null data. If false, points with `null` data will create a break in the line. Can also be a number specifying the maximum gap length to span. The unit of the value depends on the scale used. + +If the value is `undefined`, the values fallback to the associated [`elements.line.*`](../configuration/elements.md#line-configuration) options. + +### Interactions + +The interaction with each point can be controlled with the following properties: + +| Name | Description +| ---- | ----------- +| `pointHoverBackgroundColor` | Point background color when hovered. +| `pointHoverBorderColor` | Point border color when hovered. +| `pointHoverBorderWidth` | Border width of point when hovered. +| `pointHoverRadius` | The radius of the point when hovered. + +### cubicInterpolationMode + +The following interpolation modes are supported. + +* `'default'` +* `'monotone'` + +The `'default'` algorithm uses a custom weighted cubic interpolation, which produces pleasant curves for all types of datasets. + +The `'monotone'` algorithm is more suited to `y = f(x)` datasets: it preserves monotonicity (or piecewise monotonicity) of the dataset being interpolated, and ensures local extremums (if any) stay at input data points. + +If left untouched (`undefined`), the global `options.elements.line.cubicInterpolationMode` property is used. + +### Segment + +Line segment styles can be overridden by scriptable options in the `segment` object. Currently, all of the `border*` and `backgroundColor` options are supported. The segment styles are resolved for each section of the line between each point. `undefined` fallbacks to main line styles. + +:::tip +To be able to style gaps, you need the [`spanGaps`](#line-styling) option enabled. +::: + +Context for the scriptable segment contains the following properties: + +* `type`: `'segment'` +* `p0`: first point element +* `p1`: second point element +* `p0DataIndex`: index of first point in the data array +* `p1DataIndex`: index of second point in the data array +* `datasetIndex`: dataset index + +[Example usage](../samples/line/segments.md) + +### Stepped + +The following values are supported for `stepped`. + +* `false`: No Step Interpolation (default) +* `true`: Step-before Interpolation (eq. `'before'`) +* `'before'`: Step-before Interpolation +* `'after'`: Step-after Interpolation +* `'middle'`: Step-middle Interpolation + +If the `stepped` value is set to anything other than false, `tension` will be ignored. + +## Default Options + +It is common to want to apply a configuration setting to all created line charts. The global line chart settings are stored in `Chart.overrides.line`. Changing the global options only affects charts created after the change. Existing charts are not changed. + +For example, to configure all line charts with `spanGaps = true` you would do: + +```javascript +Chart.overrides.line.spanGaps = true; +``` + +## Data Structure + +All the supported [data structures](../general/data-structures.md) can be used with line charts. + +## Stacked Area Chart + +Line charts can be configured into stacked area charts by changing the settings on the y-axis to enable stacking. Stacked area charts can be used to show how one data trend is made up of a number of smaller pieces. + +```javascript +const stackedLine = new Chart(ctx, { + type: 'line', + data: data, + options: { + scales: { + y: { + stacked: true + } + } + } +}); +``` + +## Vertical Line Chart + +A vertical line chart is a variation on the horizontal line chart. +To achieve this, you will have to set the `indexAxis` property in the options object to `'y'`. +The default for this property is `'x'` and thus will show horizontal lines. + +```js chart-editor +// +const labels = Utils.months({count: 7}); +const data = { + labels: labels, + datasets: [{ + axis: 'y', + label: 'My First Dataset', + data: [65, 59, 80, 81, 56, 55, 40], + fill: false, + backgroundColor: [ + 'rgba(255, 99, 132, 0.2)', + 'rgba(255, 159, 64, 0.2)', + 'rgba(255, 205, 86, 0.2)', + 'rgba(75, 192, 192, 0.2)', + 'rgba(54, 162, 235, 0.2)', + 'rgba(153, 102, 255, 0.2)', + 'rgba(201, 203, 207, 0.2)' + ], + borderColor: [ + 'rgb(255, 99, 132)', + 'rgb(255, 159, 64)', + 'rgb(255, 205, 86)', + 'rgb(75, 192, 192)', + 'rgb(54, 162, 235)', + 'rgb(153, 102, 255)', + 'rgb(201, 203, 207)' + ], + borderWidth: 1 + }] +}; +// + +// +const config = { + type: 'line', + data: data, + options: { + indexAxis: 'y', + scales: { + x: { + beginAtZero: true + } + } + } +}; +// + +module.exports = { + actions: [], + config: config, +}; +``` + +### Config Options + +The configuration options for the vertical line chart are the same as for the [line chart](#configuration-options). However, any options specified on the x-axis in a line chart, are applied to the y-axis in a vertical line chart. + +## Internal data format + +`{x, y}` diff --git a/docs/charts/mixed.md b/docs/charts/mixed.md new file mode 100644 index 00000000000..79bf9b6e01c --- /dev/null +++ b/docs/charts/mixed.md @@ -0,0 +1,98 @@ +# Mixed Chart Types + +With Chart.js, it is possible to create mixed charts that are a combination of two or more different chart types. A common example is a bar chart that also includes a line dataset. + +When creating a mixed chart, we specify the chart type on each dataset. + +```javascript +const mixedChart = new Chart(ctx, { + data: { + datasets: [{ + type: 'bar', + label: 'Bar Dataset', + data: [10, 20, 30, 40] + }, { + type: 'line', + label: 'Line Dataset', + data: [50, 50, 50, 50], + }], + labels: ['January', 'February', 'March', 'April'] + }, + options: options +}); +``` + +At this point, we have a chart rendering how we'd like. It's important to note that the default options for the charts are only considered at the dataset level and are not merged at the chart level in this case. + +```js chart-editor +// +const data = { + labels: [ + 'January', + 'February', + 'March', + 'April' + ], + datasets: [{ + type: 'bar', + label: 'Bar Dataset', + data: [10, 20, 30, 40], + borderColor: 'rgb(255, 99, 132)', + backgroundColor: 'rgba(255, 99, 132, 0.2)' + }, { + type: 'line', + label: 'Line Dataset', + data: [50, 50, 50, 50], + fill: false, + borderColor: 'rgb(54, 162, 235)' + }] +}; +// + +// +const config = { + type: 'scatter', + data: data, + options: { + scales: { + y: { + beginAtZero: true + } + } + } +}; +// + +module.exports = { + actions: [], + config: config, +}; +``` + +## Drawing order + + By default, datasets are drawn such that the first one is top-most. This can be altered by specifying `order` option to datasets. `order` defaults to `0`. Note that this also affects stacking, legend, and tooltip. So it's essentially the same as reordering the datasets. + +The `order` property behaves like a weight instead of a specific order, so the higher the number, the sooner that dataset is drawn on the canvas and thus other datasets with a lower order number will get drawn over it. + + ```javascript +const mixedChart = new Chart(ctx, { + type: 'bar', + data: { + datasets: [{ + label: 'Bar Dataset', + data: [10, 20, 30, 40], + // this dataset is drawn below + order: 2 + }, { + label: 'Line Dataset', + data: [10, 10, 10, 10], + type: 'line', + // this dataset is drawn on top + order: 1 + }], + labels: ['January', 'February', 'March', 'April'] + }, + options: options +}); +``` diff --git a/docs/charts/polar.md b/docs/charts/polar.md new file mode 100644 index 00000000000..068cebcce8b --- /dev/null +++ b/docs/charts/polar.md @@ -0,0 +1,163 @@ +# Polar Area Chart + +Polar area charts are similar to pie charts, but each segment has the same angle - the radius of the segment differs depending on the value. + +This type of chart is often useful when we want to show a comparison data similar to a pie chart, but also show a scale of values for context. + +```js chart-editor +// +const data = { + labels: [ + 'Red', + 'Green', + 'Yellow', + 'Grey', + 'Blue' + ], + datasets: [{ + label: 'My First Dataset', + data: [11, 16, 7, 3, 14], + backgroundColor: [ + 'rgb(255, 99, 132)', + 'rgb(75, 192, 192)', + 'rgb(255, 205, 86)', + 'rgb(201, 203, 207)', + 'rgb(54, 162, 235)' + ] + }] +}; +// + +// +const config = { + type: 'polarArea', + data: data, + options: {} +}; +// + +module.exports = { + actions: [], + config: config, +}; +``` + +## Dataset Properties + +Namespaces: + +* `data.datasets[index]` - options for this dataset only +* `options.datasets.polarArea` - options for all polarArea datasets +* `options.elements.arc` - options for all [arc elements](../configuration/elements.md#arc-configuration) +* `options` - options for the whole chart + +The following options can be included in a polar area chart dataset to configure options for that specific dataset. + +| Name | Type | [Scriptable](../general/options.md#scriptable-options) | [Indexable](../general/options.md#indexable-options) | Default +| ---- | ---- | :----: | :----: | ---- +| [`backgroundColor`](#styling) | [`Color`](../general/colors.md) | Yes | Yes | `'rgba(0, 0, 0, 0.1)'` +| [`borderAlign`](#border-alignment) | `'center'`\|`'inner'` | Yes | Yes | `'center'` +| [`borderColor`](#styling) | [`Color`](../general/colors.md) | Yes | Yes | `'#fff'` +| [`borderDash`](#styling) | `number[]` | Yes | - | `[]` +| [`borderDashOffset`](#styling) | `number` | Yes | - | `0.0` +| [`borderJoinStyle`](#styling) | `'round'`\|`'bevel'`\|`'miter'` | Yes | Yes | `undefined` +| [`borderWidth`](#styling) | `number` | Yes | Yes | `2` +| [`clip`](#general) | `number`\|`object`\|`false` | - | - | `undefined` +| [`data`](#data-structure) | `number[]` | - | - | **required** +| [`hoverBackgroundColor`](#interactions) | [`Color`](../general/colors.md) | Yes | Yes | `undefined` +| [`hoverBorderColor`](#interactions) | [`Color`](../general/colors.md) | Yes | Yes | `undefined` +| [`hoverBorderDash`](#interactions) | `number[]` | Yes | - | `undefined` +| [`hoverBorderDashOffset`](#interactions) | `number` | Yes | - | `undefined` +| [`hoverBorderJoinStyle`](#interactions) | `'round'`\|`'bevel'`\|`'miter'` | Yes | Yes | `undefined` +| [`hoverBorderWidth`](#interactions) | `number` | Yes | Yes | `undefined` +| [`circular`](#styling) | `boolean` | Yes | Yes | `true` + +All these values, if `undefined`, fallback to the scopes described in [option resolution](../general/options) + +### General + +| Name | Description +| ---- | ---- +| `clip` | How to clip relative to chartArea. Positive value allows overflow, negative value clips that many pixels inside chartArea. `0` = clip at chartArea. Clipping can also be configured per side: `clip: {left: 5, top: false, right: -2, bottom: 0}` + +### Styling + +The style of each arc can be controlled with the following properties: + +| Name | Description +| ---- | ---- +| `backgroundColor` | arc background color. +| `borderColor` | arc border color. +| `borderDash` | arc border length and spacing of dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash). +| `borderDashOffset` | arc border offset for line dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset). +| `borderJoinStyle` | arc border join style. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin). +| `borderWidth` | arc border width (in pixels). +| `circular` | By default the Arc is curved. If `circular: false` the Arc will be flat. + +All these values, if `undefined`, fallback to the associated [`elements.arc.*`](../configuration/elements.md#arc-configuration) options. + +### Border Alignment + +The following values are supported for `borderAlign`. + +* `'center'` (default) +* `'inner'` + +When `'center'` is set, the borders of arcs next to each other will overlap. When `'inner'` is set, it is guaranteed that all the borders do not overlap. + +### Interactions + +The interaction with each arc can be controlled with the following properties: + +| Name | Description +| ---- | ----------- +| `hoverBackgroundColor` | arc background color when hovered. +| `hoverBorderColor` | arc border color when hovered. +| `hoverBorderDash` | arc border length and spacing of dashes when hovered. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash). +| `hoverBorderDashOffset` | arc border offset for line dashes when hovered. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset). +| `hoverBorderJoinStyle` | arc border join style when hovered. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin). +| `hoverBorderWidth` | arc border width when hovered (in pixels). + +All these values, if `undefined`, fallback to the associated [`elements.arc.*`](../configuration/elements.md#arc-configuration) options. + +## Config Options + +These are the customisation options specific to Polar Area charts. These options are looked up on access, and form together with the [global chart default options](#default-options) the options of the chart. + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| `animation.animateRotate` | `boolean` | `true` | If true, the chart will animate in with a rotation animation. This property is in the `options.animation` object. +| `animation.animateScale` | `boolean` | `true` | If true, will animate scaling the chart from the center outwards. + +The polar area chart uses the [radialLinear](../axes/radial/linear.md) scale. Additional configuration is provided via the scale. + +## Default Options + +We can also change these default values for each PolarArea type that is created, this object is available at `Chart.overrides.polarArea`. Changing the global options only affects charts created after the change. Existing charts are not changed. + +For example, to configure all new polar area charts with `animateScale = false` you would do: + +```javascript +Chart.overrides.polarArea.animation.animateScale = false; +``` + +## Data Structure + +For a polar area chart, datasets need to contain an array of data points. The data points should be a number, Chart.js will total all of the numbers and calculate the relative proportion of each. + +You also need to specify an array of labels so that tooltips appear correctly for each slice. + +```javascript +data = { + datasets: [{ + data: [10, 20, 30] + }], + + // These labels appear in the legend and in the tooltips when hovering different arcs + labels: [ + 'Red', + 'Yellow', + 'Blue' + ] +}; +``` diff --git a/docs/charts/radar.md b/docs/charts/radar.md new file mode 100644 index 00000000000..745cf968b50 --- /dev/null +++ b/docs/charts/radar.md @@ -0,0 +1,209 @@ +# Radar Chart + +A radar chart is a way of showing multiple data points and the variation between them. + +They are often useful for comparing the points of two or more different data sets. + +```js chart-editor +// +const data = { + labels: [ + 'Eating', + 'Drinking', + 'Sleeping', + 'Designing', + 'Coding', + 'Cycling', + 'Running' + ], + datasets: [{ + label: 'My First Dataset', + data: [65, 59, 90, 81, 56, 55, 40], + fill: true, + backgroundColor: 'rgba(255, 99, 132, 0.2)', + borderColor: 'rgb(255, 99, 132)', + pointBackgroundColor: 'rgb(255, 99, 132)', + pointBorderColor: '#fff', + pointHoverBackgroundColor: '#fff', + pointHoverBorderColor: 'rgb(255, 99, 132)' + }, { + label: 'My Second Dataset', + data: [28, 48, 40, 19, 96, 27, 100], + fill: true, + backgroundColor: 'rgba(54, 162, 235, 0.2)', + borderColor: 'rgb(54, 162, 235)', + pointBackgroundColor: 'rgb(54, 162, 235)', + pointBorderColor: '#fff', + pointHoverBackgroundColor: '#fff', + pointHoverBorderColor: 'rgb(54, 162, 235)' + }] +}; +// + +// +const config = { + type: 'radar', + data: data, + options: { + elements: { + line: { + borderWidth: 3 + } + } + }, +}; +// + +module.exports = { + actions: [], + config: config, +}; +``` + +## Dataset Properties + +Namespaces: + +* `data.datasets[index]` - options for this dataset only +* `options.datasets.line` - options for all line datasets +* `options.elements.line` - options for all [line elements](../configuration/elements.md#line-configuration) +* `options.elements.point` - options for all [point elements](../configuration/elements.md#point-configuration) +* `options` - options for the whole chart + +The radar chart allows a number of properties to be specified for each dataset. These are used to set display properties for a specific dataset. For example, the colour of a line is generally set this way. + +| Name | Type | [Scriptable](../general/options.md#scriptable-options) | [Indexable](../general/options.md#indexable-options) | Default +| ---- | ---- | :----: | :----: | ---- +| [`backgroundColor`](#line-styling) | [`Color`](../general/colors.md) | Yes | - | `'rgba(0, 0, 0, 0.1)'` +| [`borderCapStyle`](#line-styling) | `string` | Yes | - | `'butt'` +| [`borderColor`](#line-styling) | [`Color`](../general/colors.md) | Yes | - | `'rgba(0, 0, 0, 0.1)'` +| [`borderDash`](#line-styling) | `number[]` | Yes | - | `[]` +| [`borderDashOffset`](#line-styling) | `number` | Yes | - | `0.0` +| [`borderJoinStyle`](#line-styling) | `'round'`\|`'bevel'`\|`'miter'` | Yes | - | `'miter'` +| [`borderWidth`](#line-styling) | `number` | Yes | - | `3` +| [`hoverBackgroundColor`](#line-styling) | [`Color`](../general/colors.md) | Yes | - | `undefined` +| [`hoverBorderCapStyle`](#line-styling) | `string` | Yes | - | `undefined` +| [`hoverBorderColor`](#line-styling) | [`Color`](../general/colors.md) | Yes | - | `undefined` +| [`hoverBorderDash`](#line-styling) | `number[]` | Yes | - | `undefined` +| [`hoverBorderDashOffset`](#line-styling) | `number` | Yes | - | `undefined` +| [`hoverBorderJoinStyle`](#line-styling) | `'round'`\|`'bevel'`\|`'miter'` | Yes | - | `undefined` +| [`hoverBorderWidth`](#line-styling) | `number` | Yes | - | `undefined` +| [`clip`](#general) | `number`\|`object`\|`false` | - | - | `undefined` +| [`data`](#data-structure) | `number[]` | - | - | **required** +| [`fill`](#line-styling) | `boolean`\|`string` | Yes | - | `false` +| [`label`](#general) | `string` | - | - | `''` +| [`order`](#general) | `number` | - | - | `0` +| [`tension`](#line-styling) | `number` | - | - | `0` +| [`pointBackgroundColor`](#point-styling) | `Color` | Yes | Yes | `'rgba(0, 0, 0, 0.1)'` +| [`pointBorderColor`](#point-styling) | `Color` | Yes | Yes | `'rgba(0, 0, 0, 0.1)'` +| [`pointBorderWidth`](#point-styling) | `number` | Yes | Yes | `1` +| [`pointHitRadius`](#point-styling) | `number` | Yes | Yes | `1` +| [`pointHoverBackgroundColor`](#interactions) | `Color` | Yes | Yes | `undefined` +| [`pointHoverBorderColor`](#interactions) | `Color` | Yes | Yes | `undefined` +| [`pointHoverBorderWidth`](#interactions) | `number` | Yes | Yes | `1` +| [`pointHoverRadius`](#interactions) | `number` | Yes | Yes | `4` +| [`pointRadius`](#point-styling) | `number` | Yes | Yes | `3` +| [`pointRotation`](#point-styling) | `number` | Yes | Yes | `0` +| [`pointStyle`](#point-styling) | [`pointStyle`](../configuration/elements.md#types) | Yes | Yes | `'circle'` +| [`spanGaps`](#line-styling) | `boolean` | - | - | `undefined` + +All these values, if `undefined`, fallback to the scopes described in [option resolution](../general/options) + +### General + +| Name | Description +| ---- | ---- +| `clip` | How to clip relative to chartArea. Positive value allows overflow, negative value clips that many pixels inside chartArea. `0` = clip at chartArea. Clipping can also be configured per side: `clip: {left: 5, top: false, right: -2, bottom: 0}` +| `label` | The label for the dataset which appears in the legend and tooltips. +| `order` | The drawing order of dataset. Also affects order for tooltip and legend. [more](mixed.md#drawing-order) + +### Point Styling + +The style of each point can be controlled with the following properties: + +| Name | Description +| ---- | ---- +| `pointBackgroundColor` | The fill color for points. +| `pointBorderColor` | The border color for points. +| `pointBorderWidth` | The width of the point border in pixels. +| `pointHitRadius` | The pixel size of the non-displayed point that reacts to mouse events. +| `pointRadius` | The radius of the point shape. If set to 0, the point is not rendered. +| `pointRotation` | The rotation of the point in degrees. +| `pointStyle` | Style of the point. [more...](../configuration/elements#point-styles) + +All these values, if `undefined`, fallback first to the dataset options then to the associated [`elements.point.*`](../configuration/elements.md#point-configuration) options. + +### Line Styling + +The style of the line can be controlled with the following properties: + +| Name | Description +| ---- | ---- +| `backgroundColor` | The line fill color. +| `borderCapStyle` | Cap style of the line. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap). +| `borderColor` | The line color. +| `borderDash` | Length and spacing of dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash). +| `borderDashOffset` | Offset for line dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset). +| `borderJoinStyle` | Line joint style. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin). +| `borderWidth` | The line width (in pixels). +| `fill` | How to fill the area under the line. See [area charts](area.md). +| `tension` | Bezier curve tension of the line. Set to 0 to draw straight lines. +| `spanGaps` | If true, lines will be drawn between points with no or null data. If false, points with `null` data will create a break in the line. + +If the value is `undefined`, the values fallback to the associated [`elements.line.*`](../configuration/elements.md#line-configuration) options. + +### Interactions + +The interaction with each point can be controlled with the following properties: + +| Name | Description +| ---- | ----------- +| `pointHoverBackgroundColor` | Point background color when hovered. +| `pointHoverBorderColor` | Point border color when hovered. +| `pointHoverBorderWidth` | Border width of point when hovered. +| `pointHoverRadius` | The radius of the point when hovered. + +## Scale Options + +The radar chart supports only a single scale. The options for this scale are defined in the `scales.r` property, which can be referenced from the [Linear Radial Axis page](../axes/radial/linear). + +```javascript +options = { + scales: { + r: { + angleLines: { + display: false + }, + suggestedMin: 50, + suggestedMax: 100 + } + } +}; +``` + +## Default Options + +It is common to want to apply a configuration setting to all created radar charts. The global radar chart settings are stored in `Chart.overrides.radar`. Changing the global options only affects charts created after the change. Existing charts are not changed. + +## Data Structure + +The `data` property of a dataset for a radar chart is specified as an array of numbers. Each point in the data array corresponds to the label at the same index. + +```javascript +data: [20, 10] +``` + +For a radar chart, to provide context of what each point means, we include an array of strings that show around each point in the chart. + +```javascript +data: { + labels: ['Running', 'Swimming', 'Eating', 'Cycling'], + datasets: [{ + data: [20, 10, 4, 2] + }] +} +``` + +## Internal data format + +`{x, y}` diff --git a/docs/charts/scatter.md b/docs/charts/scatter.md new file mode 100644 index 00000000000..c49611c3ca3 --- /dev/null +++ b/docs/charts/scatter.md @@ -0,0 +1,80 @@ +# Scatter Chart + +Scatter charts are based on basic line charts with the x-axis changed to a linear axis. To use a scatter chart, data must be passed as objects containing X and Y properties. The example below creates a scatter chart with 4 points. + +```js chart-editor +// +const data = { + datasets: [{ + label: 'Scatter Dataset', + data: [{ + x: -10, + y: 0 + }, { + x: 0, + y: 10 + }, { + x: 10, + y: 5 + }, { + x: 0.5, + y: 5.5 + }], + backgroundColor: 'rgb(255, 99, 132)' + }], +}; +// + +// +const config = { + type: 'scatter', + data: data, + options: { + scales: { + x: { + type: 'linear', + position: 'bottom' + } + } + } +}; +// + +module.exports = { + actions: [], + config: config, +}; +``` + +## Dataset Properties + +Namespaces: + +* `data.datasets[index]` - options for this dataset only +* `options.datasets.scatter` - options for all scatter datasets +* `options.elements.line` - options for all [line elements](../configuration/elements.md#line-configuration) +* `options.elements.point` - options for all [point elements](../configuration/elements.md#point-configuration) +* `options` - options for the whole chart + +The scatter chart supports all the same properties as the [line chart](./line.md#dataset-properties). +By default, the scatter chart will override the showLine property of the line chart to `false`. + +The index scale is of the type `linear`. This means, if you are using the labels array, the values have to be numbers or parsable to numbers, the same applies to the object format for the keys. + +## Data Structure + +Unlike the line chart where data can be supplied in two different formats, the scatter chart only accepts data in a point format. + +```javascript +data: [{ + x: 10, + y: 20 + }, { + x: 15, + y: 10 + }] +``` + +## Internal data format + +`{x, y}` diff --git a/docs/configuration/animations.md b/docs/configuration/animations.md new file mode 100644 index 00000000000..c1abc45c658 --- /dev/null +++ b/docs/configuration/animations.md @@ -0,0 +1,285 @@ +# Animations + +Chart.js animates charts out of the box. A number of options are provided to configure how the animation looks and how long it takes. + +:::: tabs + +::: tab "Looping tension [property]" + +```js chart-editor +// +const data = { + labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], + datasets: [{ + label: 'Looping tension', + data: [65, 59, 80, 81, 26, 55, 40], + fill: false, + borderColor: 'rgb(75, 192, 192)', + }] +}; +// + +// +const config = { + type: 'line', + data: data, + options: { + animations: { + tension: { + duration: 1000, + easing: 'linear', + from: 1, + to: 0, + loop: true + } + }, + scales: { + y: { // defining min and max so hiding the dataset does not change scale range + min: 0, + max: 100 + } + } + } +}; +// + +module.exports = { + actions: [], + config: config, +}; +``` + +::: + +::: tab "Hide and show [mode]" + +```js chart-editor +// +const data = { + labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], + datasets: [{ + label: 'Try hiding me', + data: [65, 59, 80, 81, 26, 55, 40], + fill: false, + borderColor: 'rgb(75, 192, 192)', + }] +}; +// + +// +const config = { + type: 'line', + data: data, + options: { + transitions: { + show: { + animations: { + x: { + from: 0 + }, + y: { + from: 0 + } + } + }, + hide: { + animations: { + x: { + to: 0 + }, + y: { + to: 0 + } + } + } + } + } +}; +// + +module.exports = { + actions: [], + config: config, +}; +``` + +::: + +:::: + +## Animation configuration + +Animation configuration consists of 3 keys. + +| Name | Type | Details +| ---- | ---- | ------- +| animation | `object` | [animation](#animation) +| animations | `object` | [animations](#animations) +| transitions | `object` | [transitions](#transitions) + +These keys can be configured in following paths: + +* `` - chart options +* `datasets[type]` - dataset type options +* `overrides[type]` - chart type options + +These paths are valid under `defaults` for global configuration and `options` for instance configuration. + +## animation + +The default configuration is defined here: core.animations.defaults.js + +Namespace: `options.animation` + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| `duration` | `number` | `1000` | The number of milliseconds an animation takes. +| `easing` | `string` | `'easeOutQuart'` | Easing function to use. [more...](#easing) +| `delay` | `number` | `undefined` | Delay before starting the animations. +| `loop` | `boolean` | `undefined` | If set to `true`, the animations loop endlessly. + +These defaults can be overridden in `options.animation` or `dataset.animation` and `tooltip.animation`. These keys are also [Scriptable](../general/options.md#scriptable-options). + +## animations + +Animations options configures which element properties are animated and how. +In addition to the main [animation configuration](#animation-configuration), the following options are available: + +Namespace: `options.animations[animation]` + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| `properties` | `string[]` | `key` | The property names this configuration applies to. Defaults to the key name of this object. +| `type` | `string` | `typeof property` | Type of property, determines the interpolator used. Possible values: `'number'`, `'color'` and `'boolean'`. Only really needed for `'color'`, because `typeof` does not get that right. +| `from` | `number`\|`Color`\|`boolean` | `undefined` | Start value for the animation. Current value is used when `undefined` +| `to` | `number`\|`Color`\|`boolean` | `undefined` | End value for the animation. Updated value is used when `undefined` +| `fn` | <T>(from: T, to: T, factor: number) => T; | `undefined` | Optional custom interpolator, instead of using a predefined interpolator from `type` | + +### Default animations + +| Name | Option | Value +| ---- | ------ | ----- +| `numbers` | `properties` | `['x', 'y', 'borderWidth', 'radius', 'tension']` +| `numbers` | `type` | `'number'` +| `colors` | `properties` | `['color', 'borderColor', 'backgroundColor']` +| `colors` | `type` | `'color'` + +:::tip Note +These default animations are overridden by most of the dataset controllers. +::: + +## transitions + +The core transitions are `'active'`, `'hide'`, `'reset'`, `'resize'`, `'show'`. +A custom transition can be used by passing a custom `mode` to [update](../developers/api.md#updatemode). +Transition extends the main [animation configuration](#animation-configuration) and [animations configuration](#animations-configuration). + +### Default transitions + +Namespace: `options.transitions[mode]` + +| Mode | Option | Value | Description +| -----| ------ | ----- | ----- +| `'active'` | animation.duration | 400 | Override default duration to 400ms for hover animations +| `'resize'` | animation.duration | 0 | Override default duration to 0ms (= no animation) for resize +| `'show'` | animations.colors | `{ type: 'color', properties: ['borderColor', 'backgroundColor'], from: 'transparent' }` | Colors are faded in from transparent when dataset is shown using legend / [api](../developers/api.md#showdatasetIndex). +| `'show'` | animations.visible | `{ type: 'boolean', duration: 0 }` | Dataset visibility is immediately changed to true so the color transition from transparent is visible. +| `'hide'` | animations.colors | `{ type: 'color', properties: ['borderColor', 'backgroundColor'], to: 'transparent' }` | Colors are faded to transparent when dataset id hidden using legend / [api](../developers/api.md#hidedatasetIndex). +| `'hide'` | animations.visible | `{ type: 'boolean', easing: 'easeInExpo' }` | Visibility is changed to false at a very late phase of animation + +## Disabling animation + +To disable an animation configuration, the animation node must be set to `false`, with the exception for animation modes which can be disabled by setting the `duration` to `0`. + +```javascript +chart.options.animation = false; // disables all animations +chart.options.animations.colors = false; // disables animation defined by the collection of 'colors' properties +chart.options.animations.x = false; // disables animation defined by the 'x' property +chart.options.transitions.active.animation.duration = 0; // disables the animation for 'active' mode +``` + +## Easing + +Available options are: + +* `'linear'` +* `'easeInQuad'` +* `'easeOutQuad'` +* `'easeInOutQuad'` +* `'easeInCubic'` +* `'easeOutCubic'` +* `'easeInOutCubic'` +* `'easeInQuart'` +* `'easeOutQuart'` +* `'easeInOutQuart'` +* `'easeInQuint'` +* `'easeOutQuint'` +* `'easeInOutQuint'` +* `'easeInSine'` +* `'easeOutSine'` +* `'easeInOutSine'` +* `'easeInExpo'` +* `'easeOutExpo'` +* `'easeInOutExpo'` +* `'easeInCirc'` +* `'easeOutCirc'` +* `'easeInOutCirc'` +* `'easeInElastic'` +* `'easeOutElastic'` +* `'easeInOutElastic'` +* `'easeInBack'` +* `'easeOutBack'` +* `'easeInOutBack'` +* `'easeInBounce'` +* `'easeOutBounce'` +* `'easeInOutBounce'` + +See [Robert Penner's easing equations](http://robertpenner.com/easing/). + +## Animation Callbacks + +The animation configuration provides callbacks which are useful for synchronizing an external draw to the chart animation. +The callbacks can be set only at main [animation configuration](#animation-configuration). + +Namespace: `options.animation` + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| `onProgress` | `function` | `null` | Callback called on each step of an animation. +| `onComplete` | `function` | `null` | Callback called when all animations are completed. + +The callback is passed the following object: + +```javascript +{ + // Chart object + chart: Chart, + + // Number of animations still in progress + currentStep: number, + + // `true` for the initial animation of the chart + initial: boolean, + + // Total number of animations at the start of current animation + numSteps: number, +} +``` + +The following example fills a progress bar during the chart animation. + +```javascript +const chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + animation: { + onProgress: function(animation) { + progress.value = animation.currentStep / animation.numSteps; + } + } + } +}); +``` + +Another example usage of these callbacks can be found [in this progress bar sample,](../samples/advanced/progress-bar.md) which displays a progress bar showing how far along the animation is. diff --git a/docs/configuration/canvas-background.md b/docs/configuration/canvas-background.md new file mode 100644 index 00000000000..60e6b031625 --- /dev/null +++ b/docs/configuration/canvas-background.md @@ -0,0 +1,131 @@ +# Canvas background + +In some use cases you would want a background image or color over the whole canvas. There is no built-in support for this, the way you can achieve this is by writing a custom plugin. + +In the two example plugins underneath here you can see how you can draw a color or image to the canvas as background. This way of giving the chart a background is only necessary if you want to export the chart with that specific background. +For normal use you can set the background more easily with [CSS](https://www.w3schools.com/cssref/css3_pr_background.asp). + +:::: tabs + +::: tab Color + +```js chart-editor +// +const data = { + labels: [ + 'Red', + 'Blue', + 'Yellow' + ], + datasets: [{ + label: 'My First Dataset', + data: [300, 50, 100], + backgroundColor: [ + 'rgb(255, 99, 132)', + 'rgb(54, 162, 235)', + 'rgb(255, 205, 86)' + ], + hoverOffset: 4 + }] +}; +// + +// +// Note: changes to the plugin code is not reflected to the chart, because the plugin is loaded at chart construction time and editor changes only trigger an chart.update(). +const plugin = { + id: 'customCanvasBackgroundColor', + beforeDraw: (chart, args, options) => { + const {ctx} = chart; + ctx.save(); + ctx.globalCompositeOperation = 'destination-over'; + ctx.fillStyle = options.color || '#99ffff'; + ctx.fillRect(0, 0, chart.width, chart.height); + ctx.restore(); + } +}; +// + +// +const config = { + type: 'doughnut', + data: data, + options: { + plugins: { + customCanvasBackgroundColor: { + color: 'lightGreen', + } + } + }, + plugins: [plugin], +}; +// + +module.exports = { + actions: [], + config: config, +}; +``` + +::: + +::: tab Image + +```js chart-editor +// +const data = { + labels: [ + 'Red', + 'Blue', + 'Yellow' + ], + datasets: [{ + label: 'My First Dataset', + data: [300, 50, 100], + backgroundColor: [ + 'rgb(255, 99, 132)', + 'rgb(54, 162, 235)', + 'rgb(255, 205, 86)' + ], + hoverOffset: 4 + }] +}; +// + +// +// Note: changes to the plugin code is not reflected to the chart, because the plugin is loaded at chart construction time and editor changes only trigger an chart.update(). +const image = new Image(); +image.src = 'https://www.chartjs.org/img/chartjs-logo.svg'; + +const plugin = { + id: 'customCanvasBackgroundImage', + beforeDraw: (chart) => { + if (image.complete) { + const ctx = chart.ctx; + const {top, left, width, height} = chart.chartArea; + const x = left + width / 2 - image.width / 2; + const y = top + height / 2 - image.height / 2; + ctx.drawImage(image, x, y); + } else { + image.onload = () => chart.draw(); + } + } +}; +// + +// +const config = { + type: 'doughnut', + data: data, + plugins: [plugin], +}; +// + +module.exports = { + actions: [], + config: config, +}; +``` + +::: + +:::: diff --git a/docs/configuration/decimation.md b/docs/configuration/decimation.md new file mode 100644 index 00000000000..fe8fa24bc7a --- /dev/null +++ b/docs/configuration/decimation.md @@ -0,0 +1,44 @@ +# Data Decimation + +The decimation plugin can be used with line charts to automatically decimate data at the start of the chart lifecycle. Before enabling this plugin, review the [requirements](#requirements) to ensure that it will work with the chart you want to create. + +## Configuration Options + +Namespace: `options.plugins.decimation`, the global options for the plugin are defined in `Chart.defaults.plugins.decimation`. + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| `enabled` | `boolean` | `false` | Is decimation enabled? +| `algorithm` | `string` | `'min-max'` | Decimation algorithm to use. See the [more...](#decimation-algorithms) +| `samples` | `number` | | If the `'lttb'` algorithm is used, this is the number of samples in the output dataset. Defaults to the canvas width to pick 1 sample per pixel. +| `threshold` | `number` | | If the number of samples in the current axis range is above this value, the decimation will be triggered. Defaults to 4 times the canvas width.
    The number of point after decimation can be higher than the `threshold` value. + +## Decimation Algorithms + +Decimation algorithm to use for data. Options are: + +* `'lttb'` +* `'min-max'` + +### Largest Triangle Three Bucket (LTTB) Decimation + +[LTTB](https://github.com/sveinn-steinarsson/flot-downsample) decimation reduces the number of data points significantly. This is most useful for showing trends in data using only a few data points. + +### Min/Max Decimation + +[Min/max](https://digital.ni.com/public.nsf/allkb/F694FFEEA0ACF282862576020075F784) decimation will preserve peaks in your data but could require up to 4 points for each pixel. This type of decimation would work well for a very noisy signal where you need to see data peaks. + +## Requirements + +To use the decimation plugin, the following requirements must be met: + +1. The dataset must have an [`indexAxis`](../charts/line.md#general) of `'x'` +2. The dataset must be a line +3. The X axis for the dataset must be either a [`'linear'`](../axes/cartesian/linear.md) or [`'time'`](../axes/cartesian/time.md) type axis +4. Data must not need parsing, i.e. [`parsing`](../general/data-structures.md#dataset-configuration) must be `false` +5. The dataset object must be mutable. The plugin stores the original data as `dataset._data` and then defines a new `data` property on the dataset. +6. There must be more points on the chart than the threshold value. Take a look at the Configuration Options for more information. + +## Related Samples + +* [Data Decimation Sample](../samples/advanced/data-decimation) diff --git a/docs/configuration/device-pixel-ratio.md b/docs/configuration/device-pixel-ratio.md new file mode 100644 index 00000000000..4a3418a465b --- /dev/null +++ b/docs/configuration/device-pixel-ratio.md @@ -0,0 +1,15 @@ +# Device Pixel Ratio + +By default, the chart's canvas will use a 1:1 pixel ratio, unless the physical display has a higher pixel ratio (e.g. Retina displays). + +For applications where a chart will be converted to a bitmap, or printed to a higher DPI medium, it can be desirable to render the chart at a higher resolution than the default. + +Setting `devicePixelRatio` to a value other than 1 will force the canvas size to be scaled by that amount, relative to the container size. There should be no visible difference on screen; the difference will only be visible when the image is zoomed or printed. + +## Configuration Options + +Namespace: `options` + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| `devicePixelRatio` | `number` | `window.devicePixelRatio` | Override the window's default devicePixelRatio. diff --git a/docs/configuration/elements.md b/docs/configuration/elements.md new file mode 100644 index 00000000000..6bdbe42f38f --- /dev/null +++ b/docs/configuration/elements.md @@ -0,0 +1,107 @@ +# Elements + +While chart types provide settings to configure the styling of each dataset, you sometimes want to style **all datasets the same way**. A common example would be to stroke all the bars in a bar chart with the same colour but change the fill per dataset. Options can be configured for four different types of elements: **[arc](#arc-configuration)**, **[lines](#line-configuration)**, **[points](#point-configuration)**, and **[bars](#bar-configuration)**. When set, these options apply to all objects of that type unless specifically overridden by the configuration attached to a dataset. + +## Global Configuration + +The element options can be specified per chart or globally. The global options for elements are defined in `Chart.defaults.elements`. For example, to set the border width of all bar charts globally, you would do: + +```javascript +Chart.defaults.elements.bar.borderWidth = 2; +``` + +## Point Configuration + +Point elements are used to represent the points in a line, radar or bubble chart. + +Namespace: `options.elements.point`, global point options: `Chart.defaults.elements.point`. + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| `radius` | `number` | `3` | Point radius. +| [`pointStyle`](#point-styles) | [`pointStyle`](#types) | `'circle'` | Point style. +| `rotation` | `number` | `0` | Point rotation (in degrees). +| `backgroundColor` | [`Color`](../general/colors.md) | `Chart.defaults.backgroundColor` | Point fill color. +| `borderWidth` | `number` | `1` | Point stroke width. +| `borderColor` | [`Color`](../general/colors.md) | `'Chart.defaults.borderColor` | Point stroke color. +| `hitRadius` | `number` | `1` | Extra radius added to point radius for hit detection. +| `hoverRadius` | `number` | `4` | Point radius when hovered. +| `hoverBorderWidth` | `number` | `1` | Stroke width when hovered. + +### Point Styles + +#### Types + +The `pointStyle` argument accepts the following type of inputs: `string`, `Image` and `HTMLCanvasElement` + +#### Info +When a string is provided, the following values are supported: + +- `'circle'` +- `'cross'` +- `'crossRot'` +- `'dash'` +- `'line'` +- `'rect'` +- `'rectRounded'` +- `'rectRot'` +- `'star'` +- `'triangle'` +- `false` + +If the value is an image or a canvas element, that image or canvas element is drawn on the canvas using [drawImage](https://developer.mozilla.org/en/docs/Web/API/CanvasRenderingContext2D/drawImage). + +## Line Configuration + +Line elements are used to represent the line in a line chart. + +Namespace: `options.elements.line`, global line options: `Chart.defaults.elements.line`. + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| `tension` | `number` | `0` | Bézier curve tension (`0` for no Bézier curves). +| `backgroundColor` | [`Color`](/general/colors.md) | `Chart.defaults.backgroundColor` | Line fill color. +| `borderWidth` | `number` | `3` | Line stroke width. +| `borderColor` | [`Color`](/general/colors.md) | `Chart.defaults.borderColor` | Line stroke color. +| `borderCapStyle` | `string` | `'butt'` | Line cap style. See [MDN](https://developer.mozilla.org/en/docs/Web/API/CanvasRenderingContext2D/lineCap). +| `borderDash` | `number[]` | `[]` | Line dash. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash). +| `borderDashOffset` | `number` | `0.0` | Line dash offset. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset). +| `borderJoinStyle` | `'round'`\|`'bevel'`\|`'miter'` | `'miter'` | Line join style. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin). +| `capBezierPoints` | `boolean` | `true` | `true` to keep Bézier control inside the chart, `false` for no restriction. +| `cubicInterpolationMode` | `string` | `'default'` | Interpolation mode to apply. [See more...](/charts/line.md#cubicinterpolationmode) +| `fill` | `boolean`\|`string` | `false` | How to fill the area under the line. See [area charts](/charts/area.md#filling-modes). +| `stepped` | `boolean` | `false` | `true` to show the line as a stepped line (`tension` will be ignored). + +## Bar Configuration + +Bar elements are used to represent the bars in a bar chart. + +Namespace: `options.elements.bar`, global bar options: `Chart.defaults.elements.bar`. + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| `backgroundColor` | [`Color`](/general/colors.md) | `Chart.defaults.backgroundColor` | Bar fill color. +| `borderWidth` | `number` | `0` | Bar stroke width. +| `borderColor` | [`Color`](/general/colors.md) | `Chart.defaults.borderColor` | Bar stroke color. +| `borderSkipped` | `string` | `'start'` | Skipped (excluded) border: `'start'`, `'end'`, `'middle'`, `'bottom'`, `'left'`, `'top'`, `'right'` or `false`. +| `borderRadius` | `number`\|`object` | `0` | The bar border radius (in pixels). +| `inflateAmount` | `number`\|`'auto'` | `'auto'` | The amount of pixels to inflate the bar rectangle(s) when drawing. +| [`pointStyle`](#point-styles) | `string`\|`Image`\|`HTMLCanvasElement` | `'circle'` | Style of the point for legend. + +## Arc Configuration + +Arcs are used in the polar area, doughnut and pie charts. + +Namespace: `options.elements.arc`, global arc options: `Chart.defaults.elements.arc`. + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| `angle` - for polar only | `number` | `circumference / (arc count)` | Arc angle to cover. +| `backgroundColor` | [`Color`](/general/colors.md) | `Chart.defaults.backgroundColor` | Arc fill color. +| `borderAlign` | `'center'`\|`'inner'` | `'center'` | Arc stroke alignment. +| `borderColor` | [`Color`](/general/colors.md) | `'#fff'` | Arc stroke color. +| `borderDash` | `number[]` | `[]` | Arc line dash. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash). +| `borderDashOffset` | `number` | `0.0` | Arc line dash offset. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset). +| `borderJoinStyle` | `'round'`\|`'bevel'`\|`'miter'` | `'bevel'`\|`'round'` | Line join style. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin). The default is `'round'` when `borderAlign` is `'inner'` +| `borderWidth`| `number` | `2` | Arc stroke width. +| `circular` | `boolean` | `true` | By default the Arc is curved. If `circular: false` the Arc will be flat diff --git a/docs/configuration/index.md b/docs/configuration/index.md new file mode 100644 index 00000000000..b4c3aacf342 --- /dev/null +++ b/docs/configuration/index.md @@ -0,0 +1,94 @@ +# Configuration + +The configuration is used to change how the chart behaves. There are properties to control styling, fonts, the legend, etc. + +## Configuration object structure + +The top level structure of Chart.js configuration: + +```javascript +const config = { + type: 'line', + data: {}, + options: {}, + plugins: [] +} +``` + +### type + +Chart type determines the main type of the chart. + +**note** A dataset can override the `type`, this is how mixed charts are constructed. + +### data + +See [Data Structures](../general/data-structures.md) for details. + +### options + +Majority of the documentation talks about these options. + +### plugins + +Inline plugins can be included in this array. It is an alternative way of adding plugins for single chart (vs registering the plugin globally). +More about plugins in the [developers section](../developers/plugins.md). + +## Global Configuration + +This concept was introduced in Chart.js 1.0 to keep configuration [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself), and allow for changing options globally across chart types, avoiding the need to specify options for each instance, or the default for a particular chart type. + +Chart.js merges the `options` object passed to the chart with the global configuration using chart type defaults and scales defaults appropriately. This way you can be as specific as you would like in your individual chart configuration, while still changing the defaults for all chart types where applicable. The global general options are defined in `Chart.defaults`. The defaults for each chart type are discussed in the documentation for that chart type. + +The following example would set the interaction mode to 'nearest' for all charts where this was not overridden by the chart type defaults or the options passed to the constructor on creation. + +```javascript +Chart.defaults.interaction.mode = 'nearest'; + +// Interaction mode is set to nearest because it was not overridden here +const chartInteractionModeNearest = new Chart(ctx, { + type: 'line', + data: data +}); + +// This chart would have the interaction mode that was passed in +const chartDifferentInteractionMode = new Chart(ctx, { + type: 'line', + data: data, + options: { + interaction: { + // Overrides the global setting + mode: 'index' + } + } +}); +``` + +## Dataset Configuration + +Options may be configured directly on the dataset. The dataset options can be changed at multiple different levels. See [options](../general/options.md#dataset-level-options) for details on how the options are resolved. + +The following example would set the `showLine` option to 'false' for all line datasets except for those overridden by options passed to the dataset on creation. + +```javascript +// Do not show lines for all datasets by default +Chart.defaults.datasets.line.showLine = false; + +// This chart would show a line only for the third dataset +const chart = new Chart(ctx, { + type: 'line', + data: { + datasets: [{ + data: [0, 0], + }, { + data: [0, 1] + }, { + data: [1, 0], + showLine: true // overrides the `line` dataset default + }, { + type: 'scatter', // 'line' dataset default does not affect this dataset since it's a 'scatter' + data: [1, 1] + }] + } +}); +``` diff --git a/docs/configuration/interactions.md b/docs/configuration/interactions.md new file mode 100644 index 00000000000..8b4a672fc91 --- /dev/null +++ b/docs/configuration/interactions.md @@ -0,0 +1,278 @@ +# Interactions + +Namespace: `options.interaction`, the global interaction configuration is at `Chart.defaults.interaction`. To configure which events trigger chart interactions, see [events](#events). + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| `mode` | `string` | `'nearest'` | Sets which elements appear in the interaction. See [Interaction Modes](#modes) for details. +| `intersect` | `boolean` | `true` | if true, the interaction mode only applies when the mouse position intersects an item on the chart. +| `axis` | `string` | `'x'` | Can be set to `'x'`, `'y'`, `'xy'` or `'r'` to define which directions are used in calculating distances. Defaults to `'x'` for `'index'` mode and `'xy'` in `dataset` and `'nearest'` modes. +| `includeInvisible` | `boolean` | `false` | if true, the invisible points that are outside of the chart area will also be included when evaluating interactions. + +By default, these options apply to both the hover and tooltip interactions. The same options can be set in the `options.hover` namespace, in which case they will only affect the hover interaction. Similarly, the options can be set in the `options.plugins.tooltip` namespace to independently configure the tooltip interactions. + +## Events + +The following properties define how the chart interacts with events. +Namespace: `options` + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| `events` | `string[]` | `['mousemove', 'mouseout', 'click', 'touchstart', 'touchmove']` | The `events` option defines the browser events that the chart should listen to for. Each of these events trigger hover and are passed to plugins. [more...](#event-option) +| `onHover` | `function` | `null` | Called when any of the events fire over chartArea. Passed the event, an array of active elements (bars, points, etc), and the chart. +| `onClick` | `function` | `null` | Called if the event is of type `'mouseup'`, `'click'` or '`'contextmenu'` over chartArea. Passed the event, an array of active elements, and the chart. + +### Event Option + +For example, to have the chart only respond to click events, you could do: + +```javascript +const chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + // This chart will not respond to mousemove, etc + events: ['click'] + } +}); +``` + +Events for each plugin can be further limited by defining (allowed) events array in plugin options: + +```javascript +const chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + // All of these (default) events trigger a hover and are passed to all plugins, + // unless limited at plugin options + events: ['mousemove', 'mouseout', 'click', 'touchstart', 'touchmove'], + plugins: { + tooltip: { + // Tooltip will only receive click events + events: ['click'] + } + } + } +}); +``` + +Events that do not fire over chartArea, like `mouseout`, can be captured using a simple plugin: + +```javascript +const chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + // these are the default events: + // events: ['mousemove', 'mouseout', 'click', 'touchstart', 'touchmove'], + }, + plugins: [{ + id: 'myEventCatcher', + beforeEvent(chart, args, pluginOptions) { + const event = args.event; + if (event.type === 'mouseout') { + // process the event + } + } + }] +}); +``` + +For more information about plugins, see [Plugins](../developers/plugins.md) + +### Converting Events to Data Values + +A common occurrence is taking an event, such as a click, and finding the data coordinates on the chart where the event occurred. Chart.js provides helpers that make this a straightforward process. + +```javascript +const chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + onClick: (e) => { + const canvasPosition = Chart.helpers.getRelativePosition(e, chart); + + // Substitute the appropriate scale IDs + const dataX = chart.scales.x.getValueForPixel(canvasPosition.x); + const dataY = chart.scales.y.getValueForPixel(canvasPosition.y); + } + } +}); +``` + +When using a bundler, the helper functions have to be imported separately, for a full explanation of this please head over to the [integration](../getting-started/integration.md#helper-functions) page + +## Modes + +When configuring the interaction with the graph via `interaction`, `hover` or `tooltips`, a number of different modes are available. + +`options.hover` and `options.plugins.tooltip` extend from `options.interaction`. So if `mode`, `intersect` or any other common settings are configured only in `options.interaction`, both hover and tooltips obey that. + +The modes are detailed below and how they behave in conjunction with the `intersect` setting. + +See how different modes work with the tooltip in [tooltip interactions sample](../samples/tooltip/interactions.md ) + +### point + +Finds all of the items that intersect the point. + +```javascript +const chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + interaction: { + mode: 'point' + } + } +}); +``` + +### nearest + +Gets the items that are at the nearest distance to the point. The nearest item is determined based on the distance to the center of the chart item (point, bar). You can use the `axis` setting to define which coordinates are considered in distance calculation. If `intersect` is true, this is only triggered when the mouse position intersects an item in the graph. This is very useful for combo charts where points are hidden behind bars. + +```javascript +const chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + interaction: { + mode: 'nearest' + } + } +}); +``` + +### index + +Finds item at the same index. If the `intersect` setting is true, the first intersecting item is used to determine the index in the data. If `intersect` false the nearest item, in the x direction, is used to determine the index. + +```javascript +const chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + interaction: { + mode: 'index' + } + } +}); +``` + +To use index mode in a chart like the horizontal bar chart, where we search along the y direction, you can use the `axis` setting introduced in v2.7.0. By setting this value to `'y'` on the y direction is used. + +```javascript +const chart = new Chart(ctx, { + type: 'bar', + data: data, + options: { + interaction: { + mode: 'index', + axis: 'y' + } + } +}); +``` + +### dataset + +Finds items in the same dataset. If the `intersect` setting is true, the first intersecting item is used to determine the index in the data. If `intersect` false the nearest item is used to determine the index. + +```javascript +const chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + interaction: { + mode: 'dataset' + } + } +}); +``` + +### x + +Returns all items that would intersect based on the `X` coordinate of the position only. Would be useful for a vertical cursor implementation. Note that this only applies to cartesian charts. + +```javascript +const chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + interaction: { + mode: 'x' + } + } +}); +``` + +### y + +Returns all items that would intersect based on the `Y` coordinate of the position. This would be useful for a horizontal cursor implementation. Note that this only applies to cartesian charts. + +```javascript +const chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + interaction: { + mode: 'y' + } + } +}); +``` + +## Custom Interaction Modes + +New modes can be defined by adding functions to the `Chart.Interaction.modes` map. You can use the `Chart.Interaction.evaluateInteractionItems` function to help implement these. + +Example: + +```javascript +import { Interaction } from 'chart.js'; +import { getRelativePosition } from 'chart.js/helpers'; + +/** + * Custom interaction mode + * @function Interaction.modes.myCustomMode + * @param {Chart} chart - the chart we are returning items from + * @param {Event} e - the event we are find things at + * @param {InteractionOptions} options - options to use + * @param {boolean} [useFinalPosition] - use final element position (animation target) + * @return {InteractionItem[]} - items that are found + */ +Interaction.modes.myCustomMode = function(chart, e, options, useFinalPosition) { + const position = getRelativePosition(e, chart); + + const items = []; + Interaction.evaluateInteractionItems(chart, 'x', position, (element, datasetIndex, index) => { + if (element.inXRange(position.x, useFinalPosition) && myCustomLogic(element)) { + items.push({element, datasetIndex, index}); + } + }); + return items; +}; + +// Then, to use it... +new Chart.js(ctx, { + type: 'line', + data: data, + options: { + interaction: { + mode: 'myCustomMode' + } + } +}) +``` + +If you're using TypeScript, you'll also need to register the new mode: + +```typescript +declare module 'chart.js' { + interface InteractionModeMap { + myCustomMode: InteractionModeFunction; + } +} +``` diff --git a/docs/configuration/layout.md b/docs/configuration/layout.md new file mode 100644 index 00000000000..9cce256448f --- /dev/null +++ b/docs/configuration/layout.md @@ -0,0 +1,8 @@ +# Layout + +Namespace: `options.layout`, the global options for the chart layout is defined in `Chart.defaults.layout`. + +| Name | Type | Default | [Scriptable](../general/options.md#scriptable-options) | Description +| ---- | ---- | ------- | :----: | ----------- +| `autoPadding` | `boolean` | `true` | No | Apply automatic padding so visible elements are completely drawn. +| `padding` | [`Padding`](../general/padding.md) | `0` | Yes | The padding to add inside the chart. diff --git a/docs/configuration/legend.md b/docs/configuration/legend.md new file mode 100644 index 00000000000..1621f5a87d1 --- /dev/null +++ b/docs/configuration/legend.md @@ -0,0 +1,223 @@ +# Legend + +The chart legend displays data about the datasets that are appearing on the chart. + +## Configuration options + +Namespace: `options.plugins.legend`, the global options for the chart legend is defined in `Chart.defaults.plugins.legend`. + +:::warning +The doughnut, pie, and polar area charts override the legend defaults. To change the overrides for those chart types, the options are defined in `Chart.overrides[type].plugins.legend`. +::: + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| `display` | `boolean` | `true` | Is the legend shown? +| `position` | `string` | `'top'` | Position of the legend. [more...](#position) +| `align` | `string` | `'center'` | Alignment of the legend. [more...](#align) +| `maxHeight` | `number` | | Maximum height of the legend, in pixels +| `maxWidth` | `number` | | Maximum width of the legend, in pixels +| `fullSize` | `boolean` | `true` | Marks that this box should take the full width/height of the canvas (moving other boxes). This is unlikely to need to be changed in day-to-day use. +| `onClick` | `function` | | A callback that is called when a click event is registered on a label item. Arguments: `[event, legendItem, legend]`. +| `onHover` | `function` | | A callback that is called when a 'mousemove' event is registered on top of a label item. Arguments: `[event, legendItem, legend]`. +| `onLeave` | `function` | | A callback that is called when a 'mousemove' event is registered outside of a previously hovered label item. Arguments: `[event, legendItem, legend]`. +| `reverse` | `boolean` | `false` | Legend will show datasets in reverse order. +| `labels` | `object` | | See the [Legend Label Configuration](#legend-label-configuration) section below. +| `rtl` | `boolean` | | `true` for rendering the legends from right to left. +| `textDirection` | `string` | canvas' default | This will force the text direction `'rtl'` or `'ltr'` on the canvas for rendering the legend, regardless of the css specified on the canvas +| `title` | `object` | | See the [Legend Title Configuration](#legend-title-configuration) section below. + +:::tip Note +If you need more visual customizations, please use an [HTML legend](../samples/legend/html.md). +::: + +## Position + +Position of the legend. Options are: + +* `'top'` +* `'left'` +* `'bottom'` +* `'right'` +* `'chartArea'` + +When using the `'chartArea'` option the legend position is at the moment not configurable, it will always be on the left side of the chart in the middle. + +## Align + +Alignment of the legend. Options are: + +* `'start'` +* `'center'` +* `'end'` + +Defaults to `'center'` for unrecognized values. + +## Legend Label Configuration + +Namespace: `options.plugins.legend.labels` + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| `boxWidth` | `number` | `40` | Width of coloured box. +| `boxHeight` | `number` | `font.size` | Height of the coloured box. +| `color` | [`Color`](../general/colors.md) | `Chart.defaults.color` | Color of label and the strikethrough. +| `font` | `Font` | `Chart.defaults.font` | See [Fonts](../general/fonts.md) +| `padding` | `number` | `10` | Padding between rows of colored boxes. +| `generateLabels` | `function` | | Generates legend items for each thing in the legend. Default implementation returns the text + styling for the color box. See [Legend Item](#legend-item-interface) for details. +| `filter` | `function` | `null` | Filters legend items out of the legend. Receives 2 parameters, a [Legend Item](#legend-item-interface) and the chart data. +| `sort` | `function` | `null` | Sorts legend items. Type is : `sort(a: LegendItem, b: LegendItem, data: ChartData): number;`. Receives 3 parameters, two [Legend Items](#legend-item-interface) and the chart data. The return value of the function is a number that indicates the order of the two legend item parameters. The ordering matches the [return value](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#description) of `Array.prototype.sort()` +| [`pointStyle`](elements.md#point-styles) | [`pointStyle`](elements.md#types) | `'circle'` | If specified, this style of point is used for the legend. Only used if `usePointStyle` is true. +| `textAlign` | `string` | `'center'` | Horizontal alignment of the label text. Options are: `'left'`, `'right'` or `'center'`. +| `usePointStyle` | `boolean` | `false` | Label style will match corresponding point style (size is based on pointStyleWidth or the minimum value between boxWidth and font.size). +| `pointStyleWidth` | `number` | `null` | If `usePointStyle` is true, the width of the point style used for the legend. +| `useBorderRadius` | `boolean` | `false` | Label borderRadius will match corresponding borderRadius. +| `borderRadius` | `number` | `undefined` | Override the borderRadius to use. + +## Legend Title Configuration + +Namespace: `options.plugins.legend.title` + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| `color` | [`Color`](../general/colors.md) | `Chart.defaults.color` | Color of text. +| `display` | `boolean` | `false` | Is the legend title displayed. +| `font` | `Font` | `Chart.defaults.font` | See [Fonts](../general/fonts.md) +| `padding` | [`Padding`](../general/padding.md) | `0` | Padding around the title. +| `text` | `string` | | The string title. + +## Legend Item Interface + +Items passed to the legend `onClick` function are the ones returned from `labels.generateLabels`. These items must implement the following interface. + +```javascript +{ + // Label that will be displayed + text: string, + + // Border radius of the legend item. + // Introduced in 3.1.0 + borderRadius?: number | BorderRadius, + + // Index of the associated dataset + datasetIndex: number, + + // Fill style of the legend box + fillStyle: Color, + + // Text color + fontColor: Color, + + // If true, this item represents a hidden dataset. Label will be rendered with a strike-through effect + hidden: boolean, + + // For box border. See https://developer.mozilla.org/en/docs/Web/API/CanvasRenderingContext2D/lineCap + lineCap: string, + + // For box border. See https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash + lineDash: number[], + + // For box border. See https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset + lineDashOffset: number, + + // For box border. See https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin + lineJoin: string, + + // Width of box border + lineWidth: number, + + // Stroke style of the legend box + strokeStyle: Color, + + // Point style of the legend box (only used if usePointStyle is true) + pointStyle: string | Image | HTMLCanvasElement, + + // Rotation of the point in degrees (only used if usePointStyle is true) + rotation: number +} +``` + +## Example + +The following example will create a chart with the legend enabled and turn all the text red in color. + +```javascript +const chart = new Chart(ctx, { + type: 'bar', + data: data, + options: { + plugins: { + legend: { + display: true, + labels: { + color: 'rgb(255, 99, 132)' + } + } + } + } +}); +``` + +## Custom On Click Actions + +It can be common to want to trigger different behaviour when clicking an item in the legend. This can be easily achieved using a callback in the config object. + +The default legend click handler is: + +```javascript +function(e, legendItem, legend) { + const index = legendItem.datasetIndex; + const ci = legend.chart; + if (ci.isDatasetVisible(index)) { + ci.hide(index); + legendItem.hidden = true; + } else { + ci.show(index); + legendItem.hidden = false; + } +} +``` + +Let's say we wanted instead to link the display of the first two datasets. We could change the click handler accordingly. + +```javascript +const defaultLegendClickHandler = Chart.defaults.plugins.legend.onClick; +const pieDoughnutLegendClickHandler = Chart.controllers.doughnut.overrides.plugins.legend.onClick; +const newLegendClickHandler = function (e, legendItem, legend) { + const index = legendItem.datasetIndex; + const type = legend.chart.config.type; + + if (index > 1) { + // Do the original logic + if (type === 'pie' || type === 'doughnut') { + pieDoughnutLegendClickHandler(e, legendItem, legend) + } else { + defaultLegendClickHandler(e, legendItem, legend); + } + + } else { + let ci = legend.chart; + [ + ci.getDatasetMeta(0), + ci.getDatasetMeta(1) + ].forEach(function(meta) { + meta.hidden = meta.hidden === null ? !ci.data.datasets[index].hidden : null; + }); + ci.update(); + } +}; + +const chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + plugins: { + legend: { + onClick: newLegendClickHandler + } + } + } +}); +``` + +Now when you click the legend in this chart, the visibility of the first two datasets will be linked together. diff --git a/docs/configuration/locale.md b/docs/configuration/locale.md new file mode 100644 index 00000000000..5783b647ef1 --- /dev/null +++ b/docs/configuration/locale.md @@ -0,0 +1,25 @@ +# Locale + +For applications where the numbers of ticks on scales must be formatted accordingly with a language sensitive number formatting, you can enable this kind of formatting by setting the `locale` option. + +The locale is a string that is a [Unicode BCP 47 locale identifier](https://www.unicode.org/reports/tr35/tr35.html#BCP_47_Conformance). + +A Unicode BCP 47 locale identifier consists of + + 1. a language code, + 2. (optionally) a script code, + 3. (optionally) a region (or country) code, + 4. (optionally) one or more variant codes, and + 5. (optionally) one or more extension sequences, + +with all present components separated by hyphens. + +By default, the chart is using the default locale of the platform which is running on. + +## Configuration Options + +Namespace: `options` + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| `locale` | `string` | `undefined` | a string with a BCP 47 language tag, leveraging on [INTL NumberFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat). diff --git a/docs/configuration/responsive.md b/docs/configuration/responsive.md new file mode 100644 index 00000000000..867dcde1459 --- /dev/null +++ b/docs/configuration/responsive.md @@ -0,0 +1,78 @@ +# Responsive Charts + +When it comes to changing the chart size based on the window size, a major limitation is that the canvas *render* size (`canvas.width` and `.height`) can **not** be expressed with relative values, contrary to the *display* size (`canvas.style.width` and `.height`). Furthermore, these sizes are independent of each other and thus the canvas *render* size does not adjust automatically based on the *display* size, making the rendering inaccurate. + +The following examples **do not work**: + +- ``: **invalid** values, the canvas doesn't resize ([example](https://codepen.io/chartjs/pen/oWLZaR)) +- ``: **invalid** behavior, the canvas is resized but becomes blurry ([example](https://codepen.io/chartjs/pen/WjxpmO)) +- ``: **invalid** behavior, the canvas continually shrinks. Chart.js needs a dedicated container for each canvas and this styling should be applied there. + +Chart.js provides a [few options](#configuration-options) to enable responsiveness and control the resize behavior of charts by detecting when the canvas *display* size changes and update the *render* size accordingly. + +## Configuration Options + +Namespace: `options` + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| `responsive` | `boolean` | `true` | Resizes the chart canvas when its container does ([important note...](#important-note)). +| `maintainAspectRatio` | `boolean` | `true` | Maintain the original canvas aspect ratio `(width / height)` when resizing. +| `aspectRatio` | `number` | `1`\|`2` | Canvas aspect ratio (i.e. `width / height`, a value of 1 representing a square canvas). Note that this option is ignored if the height is explicitly defined either as attribute or via the style. The default value varies by chart type; Radial charts (doughnut, pie, polarArea, radar) default to `1` and others default to `2`. +| `onResize` | `function` | `null` | Called when a resize occurs. Gets passed two arguments: the chart instance and the new size. +| `resizeDelay` | `number` | `0` | Delay the resize update by the given amount of milliseconds. This can ease the resize process by debouncing the update of the elements. + +## Important Note + +Detecting when the canvas size changes can not be done directly from the `canvas` element. Chart.js uses its parent container to update the canvas *render* and *display* sizes. However, this method requires the container to be **relatively positioned** and **dedicated to the chart canvas only**. Responsiveness can then be achieved by setting relative values for the container size ([example](https://codepen.io/chartjs/pen/YVWZbz)): + +```html +
    + +
    +``` + +The chart can also be programmatically resized by modifying the container size: + +```javascript +chart.canvas.parentNode.style.height = '128px'; +chart.canvas.parentNode.style.width = '128px'; +``` + +Note that in order for the above code to correctly resize the chart height, the [`maintainAspectRatio`](#configuration-options) option must also be set to `false`. + +## Flexbox / Grid Layout + +To prevent overflow issues when using flexbox / grid layout, you must set the flex / grid child element to have a `min-width` of `0`. +See [issue 4156](https://github.com/chartjs/Chart.js/issues/4156#issuecomment-295180128) for more details. + +```html +
    +
    + +
    +
    +``` + +## Printing Resizable Charts + +CSS media queries allow changing styles when printing a page. The CSS applied from these media queries may cause charts to need to resize. However, the resize won't happen automatically. To support resizing charts when printing, you need to hook the [onbeforeprint](https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeprint) event and manually trigger resizing of each chart. + +```javascript +function beforePrintHandler () { + for (let id in Chart.instances) { + Chart.instances[id].resize(); + } +} +``` + +You may also find that, due to complexities in when the browser lays out the document for printing and when resize events are fired, Chart.js is unable to properly resize for the print layout. To work around this, you can pass an explicit size to `.resize()` then use an [onafterprint](https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onafterprint) event to restore the automatic size when done. + +```javascript +window.addEventListener('beforeprint', () => { + myChart.resize(600, 600); +}); +window.addEventListener('afterprint', () => { + myChart.resize(); +}); +``` \ No newline at end of file diff --git a/docs/configuration/subtitle.md b/docs/configuration/subtitle.md new file mode 100644 index 00000000000..c3822af31ab --- /dev/null +++ b/docs/configuration/subtitle.md @@ -0,0 +1,28 @@ +# Subtitle + +Subtitle is a second title placed under the main title, by default. It has exactly the same configuration options with the main [title](./title.md). + +## Subtitle Configuration + +Namespace: `options.plugins.subtitle`. The global defaults for subtitle are configured in `Chart.defaults.plugins.subtitle`. + +Exactly the same configuration options with [title](./title.md) are available for subtitle, the namespaces only differ. + +## Example Usage + +The example below would enable a title of 'Custom Chart Subtitle' on the chart that is created. + +```javascript +const chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + plugins: { + subtitle: { + display: true, + text: 'Custom Chart Subtitle' + } + } + } +}); +``` diff --git a/docs/configuration/title.md b/docs/configuration/title.md new file mode 100644 index 00000000000..336abd4d7fb --- /dev/null +++ b/docs/configuration/title.md @@ -0,0 +1,79 @@ +# Title + +The chart title defines text to draw at the top of the chart. + +## Title Configuration + +Namespace: `options.plugins.title`, the global options for the chart title is defined in `Chart.defaults.plugins.title`. + +| Name | Type | Default | [Scriptable](../general/options.md#scriptable-options) | Description +| ---- | ---- | ------- | :----: | ----------- +| `align` | `string` | `'center'` | Yes | Alignment of the title. [more...](#align) +| `color` | [`Color`](../general/colors.md) | `Chart.defaults.color` | Yes | Color of text. +| `display` | `boolean` | `false` | Yes | Is the title shown? +| `fullSize` | `boolean` | `true` | Yes | Marks that this box should take the full width/height of the canvas. If `false`, the box is sized and placed above/beside the chart area. +| `position` | `string` | `'top'` | Yes | Position of title. [more...](#position) +| `font` | `Font` | `{weight: 'bold'}` | Yes | See [Fonts](../general/fonts.md) +| `padding` | [`Padding`](../general/padding.md) | `10` | Yes | Padding to apply around the title. Only `top` and `bottom` are implemented. +| `text` | `string`\|`string[]` | `''` | Yes | Title text to display. If specified as an array, text is rendered on multiple lines. + +:::tip Note +If you need more visual customizations, you can implement the title with HTML and CSS. +::: + +### Position + +Possible title position values are: + +* `'top'` +* `'left'` +* `'bottom'` +* `'right'` + +## Align + +Alignment of the title. Options are: + +* `'start'` +* `'center'` +* `'end'` + +## Example Usage + +The example below would enable a title of 'Custom Chart Title' on the chart that is created. + +```javascript +const chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + plugins: { + title: { + display: true, + text: 'Custom Chart Title' + } + } + } +}); +``` + +This example shows how to specify separate top and bottom title text padding: + +```javascript +const chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + plugins: { + title: { + display: true, + text: 'Custom Chart Title', + padding: { + top: 10, + bottom: 30 + } + } + } + } +}); +``` diff --git a/docs/configuration/tooltip.md b/docs/configuration/tooltip.md new file mode 100644 index 00000000000..8e6e0539e60 --- /dev/null +++ b/docs/configuration/tooltip.md @@ -0,0 +1,461 @@ +# Tooltip + +## Tooltip Configuration + +Namespace: `options.plugins.tooltip`, the global options for the chart tooltips is defined in `Chart.defaults.plugins.tooltip`. + +:::warning +The `titleFont`, `bodyFont` and `footerFont` options default to the `Chart.defaults.font` options. To change the overrides for those options, you will need to pass a function that returns a font object. See section about [overriding default fonts](#default-font-overrides) for extra information below. +::: + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| `enabled` | `boolean` | `true` | Are on-canvas tooltips enabled? +| `external` | `function` | `null` | See [external tooltip](#external-custom-tooltips) section. +| `mode` | `string` | `interaction.mode` | Sets which elements appear in the tooltip. [more...](interactions.md#modes). +| `intersect` | `boolean` | `interaction.intersect` | If true, the tooltip mode applies only when the mouse position intersects with an element. If false, the mode will be applied at all times. +| `position` | `string` | `'average'` | The mode for positioning the tooltip. [more...](#position-modes) +| `callbacks` | `object` | | See the [callbacks section](#tooltip-callbacks). +| `itemSort` | `function` | | Sort tooltip items. [more...](#sort-callback) +| `filter` | `function` | | Filter tooltip items. [more...](#filter-callback) +| `backgroundColor` | [`Color`](../general/colors.md) | `'rgba(0, 0, 0, 0.8)'` | Background color of the tooltip. +| `titleColor` | [`Color`](../general/colors.md) | `'#fff'` | Color of title text. +| `titleFont` | `Font` | `{weight: 'bold'}` | See [Fonts](../general/fonts.md). +| `titleAlign` | `string` | `'left'` | Horizontal alignment of the title text lines. [more...](#text-alignment) +| `titleSpacing` | `number` | `2` | Spacing to add to top and bottom of each title line. +| `titleMarginBottom` | `number` | `6` | Margin to add on bottom of title section. +| `bodyColor` | [`Color`](../general/colors.md) | `'#fff'` | Color of body text. +| `bodyFont` | `Font` | `{}` | See [Fonts](../general/fonts.md). +| `bodyAlign` | `string` | `'left'` | Horizontal alignment of the body text lines. [more...](#text-alignment) +| `bodySpacing` | `number` | `2` | Spacing to add to top and bottom of each tooltip item. +| `footerColor` | [`Color`](../general/colors.md) | `'#fff'` | Color of footer text. +| `footerFont` | `Font` | `{weight: 'bold'}` | See [Fonts](../general/fonts.md). +| `footerAlign` | `string` | `'left'` | Horizontal alignment of the footer text lines. [more...](#text-alignment) +| `footerSpacing` | `number` | `2` | Spacing to add to top and bottom of each footer line. +| `footerMarginTop` | `number` | `6` | Margin to add before drawing the footer. +| `padding` | [`Padding`](../general/padding.md) | `6` | Padding inside the tooltip. +| `caretPadding` | `number` | `2` | Extra distance to move the end of the tooltip arrow away from the tooltip point. +| `caretSize` | `number` | `5` | Size, in px, of the tooltip arrow. +| `cornerRadius` | `number`\|`object` | `6` | Radius of tooltip corner curves. +| `multiKeyBackground` | [`Color`](../general/colors.md) | `'#fff'` | Color to draw behind the colored boxes when multiple items are in the tooltip. +| `displayColors` | `boolean` | `true` | If true, color boxes are shown in the tooltip. +| `boxWidth` | `number` | `bodyFont.size` | Width of the color box if displayColors is true. +| `boxHeight` | `number` | `bodyFont.size` | Height of the color box if displayColors is true. +| `boxPadding` | `number` | `1` | Padding between the color box and the text. +| `usePointStyle` | `boolean` | `false` | Use the corresponding point style (from dataset options) instead of color boxes, ex: star, triangle etc. (size is based on the minimum value between boxWidth and boxHeight). +| `borderColor` | [`Color`](../general/colors.md) | `'rgba(0, 0, 0, 0)'` | Color of the border. +| `borderWidth` | `number` | `0` | Size of the border. +| `rtl` | `boolean` | | `true` for rendering the tooltip from right to left. +| `textDirection` | `string` | canvas' default | This will force the text direction `'rtl'` or `'ltr'` on the canvas for rendering the tooltips, regardless of the css specified on the canvas +| `xAlign` | `string` | `undefined` | Position of the tooltip caret in the X direction. [more](#tooltip-alignment) +| `yAlign` | `string` | `undefined` | Position of the tooltip caret in the Y direction. [more](#tooltip-alignment) + +:::tip Note +If you need more visual customizations, please use an [HTML tooltip](../samples/tooltip/html.md). +::: + +### Position Modes + +Possible modes are: + +* `'average'` +* `'nearest'` + +`'average'` mode will place the tooltip at the average position of the items displayed in the tooltip. `'nearest'` will place the tooltip at the position of the element closest to the event position. + +You can also define [custom position modes](#custom-position-modes). + +### Tooltip Alignment + +The `xAlign` and `yAlign` options define the position of the tooltip caret. If these parameters are unset, the optimal caret position is determined. + +The following values for the `xAlign` setting are supported. + +* `'left'` +* `'center'` +* `'right'` + +The following values for the `yAlign` setting are supported. + +* `'top'` +* `'center'` +* `'bottom'` + +### Text Alignment + +The `titleAlign`, `bodyAlign` and `footerAlign` options define the horizontal position of the text lines with respect to the tooltip box. The following values are supported. + +* `'left'` (default) +* `'right'` +* `'center'` + +These options are only applied to text lines. Color boxes are always aligned to the left edge. + +### Sort Callback + +Allows sorting of [tooltip items](#tooltip-item-context). Must implement at minimum a function that can be passed to [Array.prototype.sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort). This function can also accept a third parameter that is the data object passed to the chart. + +### Filter Callback + +Allows filtering of [tooltip items](#tooltip-item-context). Must implement at minimum a function that can be passed to [Array.prototype.filter](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Array/filter). This function can also accept a fourth parameter that is the data object passed to the chart. + +## Tooltip Callbacks + +Namespace: `options.plugins.tooltip.callbacks`, the tooltip has the following callbacks for providing text. For all functions, `this` will be the tooltip object created from the `Tooltip` constructor. If the callback returns `undefined`, then the default callback will be used. To remove things from the tooltip callback should return an empty string. + +Namespace: `data.datasets[].tooltip.callbacks`, items marked with `Yes` in the column `Dataset override` can be overridden per dataset. + +A [tooltip item context](#tooltip-item-context) is generated for each item that appears in the tooltip. This is the primary model that the callback methods interact with. For functions that return text, arrays of strings are treated as multiple lines of text. + +| Name | Arguments | Return Type | Dataset override | Description +| ---- | --------- | ----------- | ---------------- | ----------- +| `beforeTitle` | `TooltipItem[]` | `string | string[] | undefined` | | Returns the text to render before the title. +| `title` | `TooltipItem[]` | `string | string[] | undefined` | | Returns text to render as the title of the tooltip. +| `afterTitle` | `TooltipItem[]` | `string | string[] | undefined` | | Returns text to render after the title. +| `beforeBody` | `TooltipItem[]` | `string | string[] | undefined` | | Returns text to render before the body section. +| `beforeLabel` | `TooltipItem` | `string | string[] | undefined` | Yes | Returns text to render before an individual label. This will be called for each item in the tooltip. +| `label` | `TooltipItem` | `string | string[] | undefined` | Yes | Returns text to render for an individual item in the tooltip. [more...](#label-callback) +| `labelColor` | `TooltipItem` | `object | undefined` | Yes | Returns the colors to render for the tooltip item. [more...](#label-color-callback) +| `labelTextColor` | `TooltipItem` | `Color | undefined` | Yes | Returns the colors for the text of the label for the tooltip item. +| `labelPointStyle` | `TooltipItem` | `object | undefined` | Yes | Returns the point style to use instead of color boxes if usePointStyle is true (object with values `pointStyle` and `rotation`). Default implementation uses the point style from the dataset points. [more...](#label-point-style-callback) +| `afterLabel` | `TooltipItem` | `string | string[] | undefined` | Yes | Returns text to render after an individual label. +| `afterBody` | `TooltipItem[]` | `string | string[] | undefined` | | Returns text to render after the body section. +| `beforeFooter` | `TooltipItem[]` | `string | string[] | undefined` | | Returns text to render before the footer section. +| `footer` | `TooltipItem[]` | `string | string[] | undefined` | | Returns text to render as the footer of the tooltip. +| `afterFooter` | `TooltipItem[]` | `string | string[] | undefined` | | Text to render after the footer section. + +### Label Callback + +The `label` callback can change the text that displays for a given data point. A common example to show a unit. The example below puts a `'$'` before every row. + +```javascript +const chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + plugins: { + tooltip: { + callbacks: { + label: function(context) { + let label = context.dataset.label || ''; + + if (label) { + label += ': '; + } + if (context.parsed.y !== null) { + label += new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(context.parsed.y); + } + return label; + } + } + } + } + } +}); +``` + +### Label Color Callback + +For example, to return a red box with a blue dashed border that has a border radius for each item in the tooltip you could do: + +```javascript +const chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + plugins: { + tooltip: { + callbacks: { + labelColor: function(context) { + return { + borderColor: 'rgb(0, 0, 255)', + backgroundColor: 'rgb(255, 0, 0)', + borderWidth: 2, + borderDash: [2, 2], + borderRadius: 2, + }; + }, + labelTextColor: function(context) { + return '#543453'; + } + } + } + } + } +}); +``` + +### Label Point Style Callback + +For example, to draw triangles instead of the regular color box for each item in the tooltip, you could do: + +```javascript +const chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + plugins: { + tooltip: { + usePointStyle: true, + callbacks: { + labelPointStyle: function(context) { + return { + pointStyle: 'triangle', + rotation: 0 + }; + } + } + } + } + } +}); +``` + +### Tooltip Item Context + +The tooltip items passed to the tooltip callbacks implement the following interface. + +```javascript +{ + // The chart the tooltip is being shown on + chart: Chart + + // Label for the tooltip + label: string, + + // Parsed data values for the given `dataIndex` and `datasetIndex` + parsed: object, + + // Raw data values for the given `dataIndex` and `datasetIndex` + raw: object, + + // Formatted value for the tooltip + formattedValue: string, + + // The dataset the item comes from + dataset: object + + // Index of the dataset the item comes from + datasetIndex: number, + + // Index of this data item in the dataset + dataIndex: number, + + // The chart element (point, arc, bar, etc.) for this tooltip item + element: Element, +} +``` + +## External (Custom) Tooltips + +External tooltips allow you to hook into the tooltip rendering process so that you can render the tooltip in your own custom way. Generally this is used to create an HTML tooltip instead of an on-canvas tooltip. The `external` option takes a function which is passed a context parameter containing the `chart` and `tooltip`. You can enable external tooltips in the global or chart configuration like so: + +```javascript +const myPieChart = new Chart(ctx, { + type: 'pie', + data: data, + options: { + plugins: { + tooltip: { + // Disable the on-canvas tooltip + enabled: false, + + external: function(context) { + // Tooltip Element + let tooltipEl = document.getElementById('chartjs-tooltip'); + + // Create element on first render + if (!tooltipEl) { + tooltipEl = document.createElement('div'); + tooltipEl.id = 'chartjs-tooltip'; + tooltipEl.innerHTML = '
    '; + document.body.appendChild(tooltipEl); + } + + // Hide if no tooltip + const tooltipModel = context.tooltip; + if (tooltipModel.opacity === 0) { + tooltipEl.style.opacity = 0; + return; + } + + // Set caret Position + tooltipEl.classList.remove('above', 'below', 'no-transform'); + if (tooltipModel.yAlign) { + tooltipEl.classList.add(tooltipModel.yAlign); + } else { + tooltipEl.classList.add('no-transform'); + } + + function getBody(bodyItem) { + return bodyItem.lines; + } + + // Set Text + if (tooltipModel.body) { + const titleLines = tooltipModel.title || []; + const bodyLines = tooltipModel.body.map(getBody); + + let innerHtml = ''; + + titleLines.forEach(function(title) { + innerHtml += '' + title + ''; + }); + innerHtml += ''; + + bodyLines.forEach(function(body, i) { + const colors = tooltipModel.labelColors[i]; + let style = 'background:' + colors.backgroundColor; + style += '; border-color:' + colors.borderColor; + style += '; border-width: 2px'; + const span = '' + body + ''; + innerHtml += '' + span + ''; + }); + innerHtml += ''; + + let tableRoot = tooltipEl.querySelector('table'); + tableRoot.innerHTML = innerHtml; + } + + const position = context.chart.canvas.getBoundingClientRect(); + const bodyFont = Chart.helpers.toFont(tooltipModel.options.bodyFont); + + // Display, position, and set styles for font + tooltipEl.style.opacity = 1; + tooltipEl.style.position = 'absolute'; + tooltipEl.style.left = position.left + window.pageXOffset + tooltipModel.caretX + 'px'; + tooltipEl.style.top = position.top + window.pageYOffset + tooltipModel.caretY + 'px'; + tooltipEl.style.font = bodyFont.string; + tooltipEl.style.padding = tooltipModel.padding + 'px ' + tooltipModel.padding + 'px'; + tooltipEl.style.pointerEvents = 'none'; + } + } + } + } +}); +``` + +See [samples](/samples/tooltip/html.md) for examples on how to get started with external tooltips. + +## Tooltip Model + +The tooltip model contains parameters that can be used to render the tooltip. + +```javascript +{ + chart: Chart, + + // The items that we are rendering in the tooltip. See Tooltip Item Interface section + dataPoints: TooltipItem[], + + // Positioning + xAlign: string, + yAlign: string, + + // X and Y properties are the top left of the tooltip + x: number, + y: number, + width: number, + height: number, + // Where the tooltip points to + caretX: number, + caretY: number, + + // Body + // The body lines that need to be rendered + // Each object contains 3 parameters + // before: string[] // lines of text before the line with the color square + // lines: string[], // lines of text to render as the main item with color square + // after: string[], // lines of text to render after the main lines + body: object[], + // lines of text that appear after the title but before the body + beforeBody: string[], + // line of text that appear after the body and before the footer + afterBody: string[], + + // Title + // lines of text that form the title + title: string[], + + // Footer + // lines of text that form the footer + footer: string[], + + // style to render for each item in body[]. This is the style of the squares in the tooltip + labelColors: TooltipLabelStyle[], + labelTextColors: Color[], + labelPointStyles: { pointStyle: PointStyle; rotation: number }[], + + // 0 opacity is a hidden tooltip + opacity: number, + + // tooltip options + options: Object +} +``` + +## Custom Position Modes + +New modes can be defined by adding functions to the `Chart.Tooltip.positioners` map. + +Example: + +```javascript +import { Tooltip } from 'chart.js'; + +/** + * Custom positioner + * @function Tooltip.positioners.myCustomPositioner + * @param elements {Chart.Element[]} the tooltip elements + * @param eventPosition {Point} the position of the event in canvas coordinates + * @returns {TooltipPosition} the tooltip position + */ +Tooltip.positioners.myCustomPositioner = function(elements, eventPosition) { + // A reference to the tooltip model + const tooltip = this; + + /* ... */ + + return { + x: 0, + y: 0 + // You may also include xAlign and yAlign to override those tooltip options. + }; +}; + +// Then, to use it... +new Chart(ctx, { + data, + options: { + plugins: { + tooltip: { + position: 'myCustomPositioner' + } + } + } +}) +``` + +See [samples](/samples/tooltip/position.md) for a more detailed example. + +If you're using TypeScript, you'll also need to register the new mode: + +```typescript +declare module 'chart.js' { + interface TooltipPositionerMap { + myCustomPositioner: TooltipPositionerFunction; + } +} +``` + +## Default font overrides + +By default, the `titleFont`, `bodyFont` and `footerFont` listen to the `Chart.defaults.font` options for setting its values. +Overriding these normally by accessing the object won't work because it is backed by a get function that looks to the default `font` namespace. +So you will need to override this get function with your own function that returns the desired config. + +Example: + +```javascript +Chart.defaults.plugins.tooltip.titleFont = () => ({ size: 20, lineHeight: 1.2, weight: 800 }); +``` \ No newline at end of file diff --git a/docs/developers/api.md b/docs/developers/api.md new file mode 100644 index 00000000000..2662f68f8a5 --- /dev/null +++ b/docs/developers/api.md @@ -0,0 +1,252 @@ +# API + +For each chart, there are a set of global prototype methods on the shared chart type which you may find useful. These are available on all charts created with Chart.js, but for the examples, let's use a line chart we've made. + +```javascript +// For example: +const myLineChart = new Chart(ctx, config); +``` + +## .destroy() + +Use this to destroy any chart instances that are created. This will clean up any references stored to the chart object within Chart.js, along with any associated event listeners attached by Chart.js. +This must be called before the canvas is reused for a new chart. + +```javascript +// Destroys a specific chart instance +myLineChart.destroy(); +``` + +## .update(mode?) + +Triggers an update of the chart. This can be safely called after updating the data object. This will update all scales, legends, and then re-render the chart. + +```javascript +myLineChart.data.datasets[0].data[2] = 50; // Would update the first dataset's value of 'March' to be 50 +myLineChart.update(); // Calling update now animates the position of March from 90 to 50. +``` +A `mode` can be provided to indicate transition configuration should be used. This can be either: + +- **string value**: Core calls this method using any of `'active'`, `'hide'`, `'reset'`, `'resize'`, `'show'` or `undefined`. `'none'` is also supported for skipping animations for single update. Please see [animations](../configuration/animations.md) docs for more details. + +- **function**: that receives a context object `{ datasetIndex: number }` and returns a mode string, allowing different modes per dataset. + +Examples: +```javascript +// Using string mode +myChart.update('active'); + +// Using function mode for dataset-specific animations +myChart.update(ctx => ctx.datasetIndex === 0 ? 'active' : 'none'); +``` + +See [Updating Charts](updates.md) for more details. + +## .reset() + +Reset the chart to its state before the initial animation. A new animation can then be triggered using `update`. + +```javascript +myLineChart.reset(); +``` + +## .render() + +Triggers a redraw of all chart elements. Note, this does not update elements for new data. Use `.update()` in that case. + +## .stop() + +Use this to stop any current animation. This will pause the chart during any current animation frame. Call `.render()` to re-animate. + +```javascript +// Stops the charts animation loop at its current frame +myLineChart.stop(); +// => returns 'this' for chainability +``` + +## .resize(width?, height?) + +Use this to manually resize the canvas element. This is run each time the canvas container is resized, but you can call this method manually if you change the size of the canvas nodes container element. + +You can call `.resize()` with no parameters to have the chart take the size of its container element, or you can pass explicit dimensions (e.g., for [printing](../configuration/responsive.md#printing-resizable-charts)). + +```javascript +// Resizes & redraws to fill its container element +myLineChart.resize(); +// => returns 'this' for chainability + +// With an explicit size: +myLineChart.resize(width, height); +``` + +## .clear() + +Will clear the chart canvas. Used extensively internally between animation frames, but you might find it useful. + +```javascript +// Will clear the canvas that myLineChart is drawn on +myLineChart.clear(); +// => returns 'this' for chainability +``` + +## .toBase64Image(type?, quality?) + +This returns a base 64 encoded string of the chart in its current state. + +```javascript +myLineChart.toBase64Image(); +// => returns png data url of the image on the canvas + +myLineChart.toBase64Image('image/jpeg', 1) +// => returns a jpeg data url in the highest quality of the canvas +``` + +## .getElementsAtEventForMode(e, mode, options, useFinalPosition) + +Calling `getElementsAtEventForMode(e, mode, options, useFinalPosition)` on your Chart instance passing an event and a mode will return the elements that are found. The `options` and `useFinalPosition` arguments are passed through to the handlers. + +To get an item that was clicked on, `getElementsAtEventForMode` can be used. + +```javascript +function clickHandler(evt) { + const points = myChart.getElementsAtEventForMode(evt, 'nearest', { intersect: true }, true); + + if (points.length) { + const firstPoint = points[0]; + const label = myChart.data.labels[firstPoint.index]; + const value = myChart.data.datasets[firstPoint.datasetIndex].data[firstPoint.index]; + } +} +``` + +## .getSortedVisibleDatasetMetas() + +Returns an array of all the dataset meta's in the order that they are drawn on the canvas that are not hidden. + +```javascript +const visibleMetas = chart.getSortedVisibleDatasetMetas(); +``` + +## .getDatasetMeta(index) + +Looks for the dataset that matches the current index and returns that metadata. This returned data has all of the metadata that is used to construct the chart. + +The `data` property of the metadata will contain information about each point, bar, etc. depending on the chart type. + +Extensive examples of usage are available in the [Chart.js tests](https://github.com/chartjs/Chart.js/tree/master/test). + +```javascript +const meta = myChart.getDatasetMeta(0); +const x = meta.data[0].x; +``` + +## getVisibleDatasetCount + +Returns the number of datasets that are currently not hidden. + +```javascript +const numberOfVisibleDatasets = chart.getVisibleDatasetCount(); +``` +## isDatasetVisible(datasetIndex) + +Returns a boolean if a dataset at the given index is currently visible. + +The visibility is determined by first checking the hidden property in the dataset metadata (set via [`setDatasetVisibility()`](#setdatasetvisibility-datasetindex-visibility) and accessible through [`getDatasetMeta()`](#getdatasetmeta-index)). If this is not set, the hidden property of the dataset object itself (`chart.data.datasets[n].hidden`) is returned. + +```javascript +chart.isDatasetVisible(1); +``` + +## setDatasetVisibility(datasetIndex, visibility) + +Sets the visibility for a given dataset. This can be used to build a chart legend in HTML. During click on one of the HTML items, you can call `setDatasetVisibility` to change the appropriate dataset. + +```javascript +chart.setDatasetVisibility(1, false); // hides dataset at index 1 +chart.update(); // chart now renders with dataset hidden +``` + +## toggleDataVisibility(index) + +Toggles the visibility of an item in all datasets. A dataset needs to explicitly support this feature for it to have an effect. From internal chart types, doughnut / pie, polar area, and bar use this. + +```javascript +chart.toggleDataVisibility(2); // toggles the item in all datasets, at index 2 +chart.update(); // chart now renders with item hidden +``` + +## getDataVisibility(index) + +Returns the stored visibility state of a data index for all datasets. Set by [toggleDataVisibility](#toggledatavisibility-index). A dataset controller should use this method to determine if an item should not be visible. + +```javascript +const visible = chart.getDataVisibility(2); +``` + +## hide(datasetIndex, dataIndex?) + +If dataIndex is not specified, sets the visibility for the given dataset to false. Updates the chart and animates the dataset with `'hide'` mode. This animation can be configured under the `hide` key in animation options. Please see [animations](../configuration/animations.md) docs for more details. + +If dataIndex is specified, sets the hidden flag of that element to true and updates the chart. + +```javascript +chart.hide(1); // hides dataset at index 1 and does 'hide' animation. +chart.hide(0, 2); // hides the data element at index 2 of the first dataset. +``` + +## show(datasetIndex, dataIndex?) + +If dataIndex is not specified, sets the visibility for the given dataset to true. Updates the chart and animates the dataset with `'show'` mode. This animation can be configured under the `show` key in animation options. Please see [animations](../configuration/animations.md) docs for more details. + +If dataIndex is specified, sets the hidden flag of that element to false and updates the chart. + +```javascript +chart.show(1); // shows dataset at index 1 and does 'show' animation. +chart.show(0, 2); // shows the data element at index 2 of the first dataset. +``` + +## setActiveElements(activeElements) + +Sets the active (hovered) elements for the chart. See the "Programmatic Events" sample file to see this in action. + +```javascript +chart.setActiveElements([ + {datasetIndex: 0, index: 1}, +]); +``` + +## isPluginEnabled(pluginId) + +Returns a boolean if a plugin with the given ID has been registered to the chart instance. + +```javascript +chart.isPluginEnabled('filler'); +``` + +## Static: getChart(key) + +Finds the chart instance from the given key. If the key is a `string`, it is interpreted as the ID of the Canvas node for the Chart. The key can also be a `CanvasRenderingContext2D` or an `HTMLDOMElement`. This will return `undefined` if no Chart is found. To be found, the chart must have previously been created. + +```javascript +const chart = Chart.getChart("canvas-id"); +``` + +## Static: register(chartComponentLike) + +Used to register plugins, axis types or chart types globally to all your charts. + +```javascript +import { Chart, Tooltip, LinearScale, PointElement, BubbleController } from 'chart.js'; + +Chart.register(Tooltip, LinearScale, PointElement, BubbleController); +``` + +## Static: unregister(chartComponentLike) + +Used to unregister plugins, axis types or chart types globally from all your charts. + +```javascript +import { Chart, Tooltip, LinearScale, PointElement, BubbleController } from 'chart.js'; + +Chart.unregister(Tooltip, LinearScale, PointElement, BubbleController); +``` diff --git a/docs/developers/axes.md b/docs/developers/axes.md new file mode 100644 index 00000000000..6512371270a --- /dev/null +++ b/docs/developers/axes.md @@ -0,0 +1,135 @@ +# New Axes + +Axes in Chart.js can be individually extended. Axes should always derive from `Chart.Scale` but this is not a mandatory requirement. + +```javascript +class MyScale extends Chart.Scale { + /* extensions ... */ +} +MyScale.id = 'myScale'; +MyScale.defaults = defaultConfigObject; + +// MyScale is now derived from Chart.Scale +``` + +Once you have created your scale class, you need to register it with the global chart object so that it can be used. + +```javascript +Chart.register(MyScale); + +// If the new scale is not extending Chart.Scale, the prototype can not be used to detect what +// you are trying to register - so you need to be explicit: + +// Chart.registry.addScales(MyScale); +``` + +To use the new scale, simply pass in the string key to the config when creating a chart. + +```javascript +const lineChart = new Chart(ctx, { + data: data, + type: 'line', + options: { + scales: { + y: { + type: 'myScale' // this is the same id that was set on the scale + } + } + } +}); +``` + +## Scale Properties + +Scale instances are given the following properties during the fitting process. + +```javascript +{ + left: number, // left edge of the scale bounding box + right: number, // right edge of the bounding box + top: number, + bottom: number, + width: number, // the same as right - left + height: number, // the same as bottom - top + + // Margin on each side. Like css, this is outside the bounding box. + margins: { + left: number, + right: number, + top: number, + bottom: number + }, + + // Amount of padding on the inside of the bounding box (like CSS) + paddingLeft: number, + paddingRight: number, + paddingTop: number, + paddingBottom: number +} +``` + +## Scale Interface + +To work with Chart.js, custom scale types must implement the following interface. + +```javascript +{ + // Determines the data limits. Should set this.min and this.max to be the data max/min + determineDataLimits: function() {}, + + // Generate tick marks. this.chart is the chart instance. The data object can be accessed as this.chart.data + // buildTicks() should create a ticks array on the axis instance, if you intend to use any of the implementations from the base class + buildTicks: function() {}, + + // Get the label to show for the given value + getLabelForValue: function(value) {}, + + // Get the pixel (x coordinate for horizontal axis, y coordinate for vertical axis) for a given value + // @param index: index into the ticks array + getPixelForTick: function(index) {}, + + // Get the pixel (x coordinate for horizontal axis, y coordinate for vertical axis) for a given value + // @param value : the value to get the pixel for + // @param [index] : index into the data array of the value + getPixelForValue: function(value, index) {}, + + // Get the value for a given pixel (x coordinate for horizontal axis, y coordinate for vertical axis) + // @param pixel : pixel value + getValueForPixel: function(pixel) {} +} +``` + +Optionally, the following methods may also be overwritten, but an implementation is already provided by the `Chart.Scale` base class. + +```javascript +{ + // Adds labels to objects in the ticks array. The default implementation simply calls this.options.ticks.callback(numericalTick, index, ticks); + generateTickLabels: function() {}, + + // Determine how much the labels will rotate by. The default implementation will only rotate labels if the scale is horizontal. + calculateLabelRotation: function() {}, + + // Fits the scale into the canvas. + // this.maxWidth and this.maxHeight will tell you the maximum dimensions the scale instance can be. Scales should endeavour to be as efficient as possible with canvas space. + // this.margins is the amount of space you have on either side of your scale that you may expand in to. This is used already for calculating the best label rotation + // You must set this.minSize to be the size of your scale. It must be an object containing 2 properties: width and height. + // You must set this.width to be the width and this.height to be the height of the scale + fit: function() {}, + + // Draws the scale onto the canvas. this.(left|right|top|bottom) will have been populated to tell you the area on the canvas to draw in + // @param chartArea : an object containing four properties: left, right, top, bottom. This is the rectangle that lines, bars, etc will be drawn in. It may be used, for example, to draw grid lines. + draw: function(chartArea) {} +} +``` + +The Core.Scale base class also has some utility functions that you may find useful. + +```javascript +{ + // Returns true if the scale instance is horizontal + isHorizontal: function() {}, + + // Returns the scale tick objects ({label, major}) + getTicks: function() {} +} +``` diff --git a/docs/developers/charts.md b/docs/developers/charts.md new file mode 100644 index 00000000000..41cf73f8927 --- /dev/null +++ b/docs/developers/charts.md @@ -0,0 +1,141 @@ +# New Charts + +Chart.js 2.0 introduced the concept of controllers for each dataset. Like scales, new controllers can be written as needed. + +```javascript +class MyType extends Chart.DatasetController { + +} + +Chart.register(MyType); + +// Now we can create a new instance of our chart, using the Chart.js API +new Chart(ctx, { + // this is the string the constructor was registered at, ie Chart.controllers.MyType + type: 'MyType', + data: data, + options: options +}); +``` + +## Dataset Controller Interface + +Dataset controllers must implement the following interface. + +```javascript +{ + // Defaults for charts of this type + defaults: { + // If set to `false` or `null`, no dataset level element is created. + // If set to a string, this is the type of element to create for the dataset. + // For example, a line create needs to create a line element so this is the string 'line' + datasetElementType: string | null | false, + + // If set to `false` or `null`, no elements are created for each data value. + // If set to a string, this is the type of element to create for each data value. + // For example, a line create needs to create a point element so this is the string 'point' + dataElementType: string | null | false, + } + + // ID of the controller + id: string; + + // Update the elements in response to new data + // @param mode : update mode, core calls this method using any of `'active'`, `'hide'`, `'reset'`, `'resize'`, `'show'` or `undefined` + update: function(mode) {} +} +``` + +The following methods may optionally be overridden by derived dataset controllers. + +```javascript +{ + // Draw the representation of the dataset. The base implementation works in most cases, and an example of a derived version + // can be found in the line controller + draw: function() {}, + + // Initializes the controller + initialize: function() {}, + + // Ensures that the dataset represented by this controller is linked to a scale. Overridden to helpers.noop in the polar area and doughnut controllers as these + // chart types using a single scale + linkScales: function() {}, + + // Parse the data into the controller meta data. The default implementation will work for cartesian parsing, but an example of an overridden + // version can be found in the doughnut controller + parse: function(start, count) {}, +} +``` + +## Extending Existing Chart Types + +Extending or replacing an existing controller type is easy. Simply replace the constructor for one of the built-in types with your own. + +The built-in controller types are: + +* `BarController` +* `BubbleController` +* `DoughnutController` +* `LineController` +* `PieController` +* `PolarAreaController` +* `RadarController` +* `ScatterController` + +These controllers are also available in the UMD package, directly under `Chart`. Eg: `Chart.BarController`. + +For example, to derive a new chart type that extends from a bubble chart, you would do the following. + +```javascript +import {BubbleController} from 'chart.js'; +class Custom extends BubbleController { + draw() { + // Call bubble controller method to draw all the points + super.draw(arguments); + + // Now we can do some custom drawing for this dataset. Here we'll draw a red box around the first point in each dataset + const meta = this.getMeta(); + const pt0 = meta.data[0]; + + const {x, y} = pt0.getProps(['x', 'y']); + const {radius} = pt0.options; + + const ctx = this.chart.ctx; + ctx.save(); + ctx.strokeStyle = 'red'; + ctx.lineWidth = 1; + ctx.strokeRect(x - radius, y - radius, 2 * radius, 2 * radius); + ctx.restore(); + } +}; +Custom.id = 'derivedBubble'; +Custom.defaults = BubbleController.defaults; + +// Stores the controller so that the chart initialization routine can look it up +Chart.register(Custom); + +// Now we can create and use our new chart type +new Chart(ctx, { + type: 'derivedBubble', + data: data, + options: options +}); +``` + +## TypeScript Typings + +If you want your new chart type to be statically typed, you must provide a `.d.ts` TypeScript declaration file. Chart.js provides a way to augment built-in types with user-defined ones, by using the concept of "declaration merging". + +When adding a new chart type, `ChartTypeRegistry` must contain the declarations for the new type, either by extending an existing entry in `ChartTypeRegistry` or by creating a new one. + +For example, to provide typings for a new chart type that extends from a bubble chart, you would add a `.d.ts` containing: + +```typescript +import { ChartTypeRegistry } from 'chart.js'; + +declare module 'chart.js' { + interface ChartTypeRegistry { + derivedBubble: ChartTypeRegistry['bubble'] + } +} +``` diff --git a/docs/developers/contributing.md b/docs/developers/contributing.md new file mode 100644 index 00000000000..a5bb44566c0 --- /dev/null +++ b/docs/developers/contributing.md @@ -0,0 +1,79 @@ +# Contributing + +New contributions to the library are welcome, but we ask that you please follow these guidelines: + +- Before opening a PR for major additions or changes, please discuss the expected API and/or implementation by [filing an issue](https://github.com/chartjs/Chart.js/issues) or asking about it in the [Chart.js Discord](https://discord.gg/HxEguTK6av) #dev channel. This will save you development time by getting feedback upfront and make reviews faster by giving the maintainers more context and details. +- Consider whether your changes are useful for all users, or if creating a Chart.js [plugin](plugins.md) would be more appropriate. +- Check that your code will pass tests and `eslint` code standards. `pnpm test` will run both the linter and tests for you. +- Add unit tests and document new functionality (in the `test/` and `docs/` directories respectively). +- Avoid breaking changes unless there is an upcoming major release, which is infrequent. We encourage people to write plugins for the most new advanced features, and care a lot about backward compatibility. +- We strongly prefer new methods to be added as private whenever possible. A method can be made private either by making a top-level `function` outside of a class or by prefixing it with `_` and adding `@private` JSDoc if inside a class. Public APIs take considerable time to review and become locked once implemented as we have limited ability to change them without breaking backward compatibility. Private APIs allow the flexibility to address unforeseen cases. + +## Joining the project + +Active committers and contributors are invited to introduce themselves and request commit access to this project. We have a very active Discord community that you can join [here](https://discord.gg/HxEguTK6av). If you think you can help, we'd love to have you! + +## Building and Testing + +Firstly, we need to ensure development dependencies are installed. With node and pnpm installed, after cloning the Chart.js repo to a local directory, and navigating to that directory in the command line, we can run the following: + +```bash +> pnpm install +``` + +This will install the local development dependencies for Chart.js. + +The following commands are now available from the repository root: + +```bash +> pnpm run build // build dist files in ./dist +> pnpm run autobuild // build and watch for source changes +> pnpm run dev // run tests and watch for source and test changes +> pnpm run lint // perform code linting (ESLint, tsc) +> pnpm test // perform code linting and run unit tests with coverage +``` + +`pnpm run dev` and `pnpm test` can be appended with a string that is used to match the spec filenames. For example: `pnpm run dev plugins` will start karma in watch mode for `test/specs/**/*plugin*.js`. + +### Documentation + +We use [Vuepress](https://vuepress.vuejs.org/) to manage the docs which are contained as Markdown files in the docs directory. You can run the doc server locally using these commands: + +```bash +> pnpm run docs:dev +``` + +### Image-Based Tests + +Some display-related functionality is difficult to test via typical Jasmine units. For this reason, we introduced image-based tests ([#3988](https://github.com/chartjs/Chart.js/pull/3988) and [#5777](https://github.com/chartjs/Chart.js/pull/5777)) to assert that a chart is drawn pixel-for-pixel matching an expected image. + +Generated charts in image-based tests should be **as minimal as possible** and focus only on the tested feature to prevent failure if another feature breaks (e.g. disable the title and legend when testing scales). + +You can create a new image-based test by following the steps below: + +- Create a JS file ([example](https://github.com/chartjs/Chart.js/blob/f7b671006a86201808402c3b6fe2054fe834fd4a/test/fixtures/controller.bubble/radius-scriptable.js)) or JSON file ([example](https://github.com/chartjs/Chart.js/blob/4b421a50bfa17f73ac7aa8db7d077e674dbc148d/test/fixtures/plugin.filler/fill-line-dataset.json)) that defines chart config and generation options. +- Add this file in `test/fixtures/{spec.name}/{feature-name}.json`. +- Add a [describe line](https://github.com/chartjs/Chart.js/blob/4b421a50bfa17f73ac7aa8db7d077e674dbc148d/test/specs/plugin.filler.tests.js#L10) to the beginning of `test/specs/{spec.name}.tests.js` if it doesn't exist yet. +- Run `pnpm run dev`. +- Click the *"Debug"* button (top/right): a test should fail with the associated canvas visible. +- Right-click on the chart and *"Save image as..."* `test/fixtures/{spec.name}/{feature-name}.png` making sure not to activate the tooltip or any hover functionality +- Refresh the browser page (`CTRL+R`): test should now pass +- Verify test relevancy by changing the feature values *slightly* in the JSON file. + +Tests should pass in both browsers. In general, we've hidden all text in image tests since it's quite difficult to get them to pass between different browsers. As a result, it is recommended to hide all scales in image-based tests. It is also recommended to disable animations. If tests still do not pass, adjust [`tolerance` and/or `threshold`](https://github.com/chartjs/Chart.js/blob/1ca0ffb5d5b6c2072176fd36fa85a58c483aa434/test/jasmine.matchers.js) at the beginning of the JSON file keeping them **as low as possible**. + +When a test fails, the expected and actual images are shown. If you'd like to see the images even when the tests pass, set `"debug": true` in the JSON file. + +## Bugs and Issues + +Please report these on the GitHub page - at github.com/chartjs/Chart.js. Please do not use issues for support requests. For help using Chart.js, please take a look at the [`chart.js`](https://stackoverflow.com/questions/tagged/chart.js) tag on Stack Overflow. + +Well-structured, detailed bug reports are hugely valuable for the project. + +Guidelines for reporting bugs: + +- Check the issue search to see if it has already been reported +- Isolate the problem to a simple test case +- Please include a demonstration of the bug on a website such as [JS Bin](https://jsbin.com/), [JS Fiddle](https://jsfiddle.net/), or [Codepen](https://codepen.io/pen/). ([Template](https://codepen.io/pen?template=wvezeOq)). If filing a bug against `master`, you may reference the latest code via (changing the filename to point at the file you need as appropriate). Do not rely on these files for production purposes as they may be removed at any time. + +Please provide any additional details associated with the bug, if it's browser or screen density specific, or only happens with a certain configuration or data. diff --git a/docs/developers/destroy_flowchart.png b/docs/developers/destroy_flowchart.png new file mode 100644 index 00000000000..4ba151b76fa Binary files /dev/null and b/docs/developers/destroy_flowchart.png differ diff --git a/docs/developers/event_flowchart.png b/docs/developers/event_flowchart.png new file mode 100644 index 00000000000..08ee5ed4ca8 Binary files /dev/null and b/docs/developers/event_flowchart.png differ diff --git a/docs/developers/index.md b/docs/developers/index.md new file mode 100644 index 00000000000..b48290f4fbf --- /dev/null +++ b/docs/developers/index.md @@ -0,0 +1,53 @@ +# Developers + +Developer features allow extending and enhancing Chart.js in many different ways. + +## Latest resources + +The latest documentation and samples, including unreleased features, are available at: + +- +- + +## Development releases + +Latest builds are available for testing at: + +- +- + +:::warning Warning + +Development builds **must not** be used for production purposes or as replacement for a CDN. See [available CDNs](../getting-started/installation.md#cdn). + +::: + +## Browser support + +All modern and up-to-date browsers are supported, including, but not limited to: + +* Chrome +* Edge +* Firefox +* Safari + +As of version 3, we have dropped Internet Explorer 11 support. + +Browser support for the canvas element is available in all modern & major mobile browsers. [CanIUse](https://caniuse.com/#feat=canvas) + +Run `npx browserslist` at the root of the [codebase](https://github.com/chartjs/Chart.js) to get a list of supported browsers. + +Thanks to [BrowserStack](https://browserstack.com) for allowing our team to test on thousands of browsers. + +## Previous versions + +To migrate from version 2 to version 3, please see [the v3 migration guide](../getting-started/v3-migration). + +Version 3 has a largely different API than earlier versions. + +Most earlier version options have current equivalents or are the same. + +Please note - documentation for previous versions is available online or in the GitHub repo. + +- [2.9.4 Documentation](https://www.chartjs.org/docs/2.9.4/) +- [1.x Documentation](https://github.com/chartjs/Chart.js/tree/v1.1.1/docs) diff --git a/docs/developers/init_flowchart.png b/docs/developers/init_flowchart.png new file mode 100644 index 00000000000..f52d628e502 Binary files /dev/null and b/docs/developers/init_flowchart.png differ diff --git a/docs/developers/plugin_flowcharts.drawio b/docs/developers/plugin_flowcharts.drawio new file mode 100644 index 00000000000..1e5ed9290a1 --- /dev/null +++ b/docs/developers/plugin_flowcharts.drawio @@ -0,0 +1 @@ +7V3dl5o4FP9rfGyPSQjiY50Z23M6O/sx7enOY5So7CBxEUftX79BkhEICiqSYLcvhUsC8d77y/2E6aC7+eZzSBaz35hL/Q7supsOuu9ACLrI4f/FlG1CwVYvIUxDzxWD9oRn7yeVMwV15bl0mRkYMeZH3iJLHLMgoOMoQyNhyNbZYRPmZ5+6IFOqEJ7HxFepPzw3miVUB3f39C/Um87kk0FXXJkTOVgQljPisnWKhB466C5kLEqO5ps76sfMk3xJ5g0PXH1fWEiDqMqEydz/ffvkTt3hcPBvb7x+GX65/4DE2qKt/MHU5b9fnLIwmrEpC4j/sKcOQrYKXBrftcvP9mMeGVtwIuDEf2gUbYUwySpinDSL5r64Sjde9Hc8/SMWZy+pK/cbcefdyVaeBFG4TU2KT1/S1/bTdmdynsolwbglW4VjeoQ1UttIOKXRkXEwGRfzLfUAIYPPlM0pXw8fEFKfRN5bVq+IUM/p+7i9BPmBEOIJAhX3fSP+SjxpQKdewEl/US61UJW373MsxXJdz7yIPi/IjitrDues1MhykQBs4m1i6R9m7RsNI7o5ygxxFTkCHGJ3kFhZp6AmSLMUyiStdu7h/+FQoualcLCMggNU4PDEIm8SL2BEJyykB1CxFyooR8bE8/075rNwNxe5mDquxenLKGSvNHXFgSNk2zVBx85CBxRg552WBo91LfA4Cq9fuOXWCCiQgtMeXGWAysBpj67aAWVVBFTPKEBJv8z8LfL6kukbJRlLgd8dCcZ8Ysz4obrDzdh8tFqW725XMfOa7XxP4dXzq7c47CSdZA5qYBiGOYZp39wBvH3c9yviHgCjgN8v83HuQ7K+AQ8H6QeBusdqdnG6nXNcnNNihpzwJ86YjsdFwh852MIXBt1Vo26AKmJQKNOH7keBk8qg3N3rUxiSbWrAgnlBtEw96o+YkFLablZpLSuXlSkZD5ySCRY8PoEfJGvOTZc/gE0mS87ZPBTeOXZBrKXmHp7Y7VuNyiorBWeI2QCquIx1GC3dDiOwW6zbGnKnqOo2js3CRO/WjXkDMq09LD7LEMO8Ye3n6yMlE6RlrmqIc+OzdrguGwuqJ9keyYj6uRS+700DfjzmwudRLhrEO7I3Jv4ncWHuuW6i03Tp/SSj3f1itRHc5jfHgw6+L1Sko4hS9v73kp14SiddFTvkweEMx2X16jRFatIlkphJiWsYknksMb7qOfECL5iaZmyB42i2tpbebbgZtxFX3E4t2ywTiQs8IZFuIBO+rdxMRYUbBN0JB9ia+vz5QJAMLY+foFFAkOtOASFJtHUfyZaGy53ecLWJx9k+/2GDEUeFPY2PfiZUMo/1fndtdwOpbxozz3kQYO1JN9iaovwFGIBVMWBYeR0eNgYi90wiwp2r5Y3koG3tJgG1pgDbhnwzrFrrhWZ5YUhN3pkdB0q1rSUOdDC2M7AUdzI3EERaHbnzGmEa6yyrikFklgOI1GrggZywIRCUyYg6IGjbMJuMMR6CsEUNMrbuDAx02uJlXLDv2FVtv1kteFCtRZ0XeO4oRkadoKhD7Gp+9urP4Os2+k6fwqkz+fp6D75j+J66NR8Bil/c3f27DBs9FRvFbDLLJkO1sy+bnbyROBTAAgvRcCBqtwUg58NAepzlrqlZdWy57tK8TEvhkO+P7dm60WBBnWgwO8BDlUtdZjXWIrXUZUzEgK0sAmQXoLaIARn3+o/Z7TSowMO6LOuR6pMAjgh2r9tjg/M9NgAd75nJT+iVNLti5+h4fb2uVmviY4M1u/6c+nlanO9/Ac6JLdi5Cddp/bKK3vMo1EIz0o0SIrWkGy0Eshl/83u/kBqKymJox6Dur7wjAbp93d1fVlv21vP3SOnplrvEVd9zaeglU7XuKHKPbqLbqi43nEq0c/os2341hoataWBp3lewqjaVI7Ny8Jba2JvLM7Y6v5IHEdDfCmmpVQ/Zacr9ifnCpxFVcWbWx210V/VQvy07UZM9QJXTU6btQWoDquE9QDKCqSMi6GGZcZIRgUkdCIW1sl+lolhcKFRBVjiuarardowdW/XhOsq35GOELbXzSlnR6umuuxcVcH9l3MCKuNHWn1qMG71dyg1k/hVZD07yRKrK+rKPXzT7rQuA+1o/dlGXnS74MoIxjtVRtNXTXN2X6mVgZ+cx2JhYplXercW4uRiwWFda825tbTtzDVa4aohY/858kbDL3tW9Me+1VwCuZr3X1pSFrwGSgn7qY1ppCEYOdlOL78xfBoxrqLmtPUgDekt0LXXvZa90a/17dCP+fZFZLFRoQ9x7+eZSLd/QgdIDNNehB61pLb9KprRqykdbt+zRZadQ9U1aUG6pdjdwY8tqWkiGrheS8dP9H3hJ0LH/Mzno4T8=7Zlbd6IwEMc/jY/u4Rarj/XWdtf2dLVb6754ogRJRUJD8PbpN0AQEEqt22r19EkymYTwn/klA5bUxmx5RaFj3hIdWSVF0pcltVlSFFlSq/zHt6xCC9AuQsOEYl04xYYeXqNopLB6WEduypERYjHspI1jYttozFI2SClZpN0MYqXv6sAJyhh6Y2hlrX2sMzO0VoEU268RnpjRnWVJ9Mxg5CwMrgl1skiY1FZJbVBCWHg1WzaQ5YsX6fJIbyvPjy910NJbDz+f5tPhqFYOJ2u/Z8jmESiy2d5T15V+D9eHvzpTZIyacDKd33ejqV22ivRCOpdPNAllJpkQG1qt2FqnxLN15M8q8Vbs0yHE4UaZG58RYyuRC9BjhJtMNrNEL1pi9uQP/wFEa5DoaS7FzEFjFTVsRleJQX5zkOyLhwWtaNyO0kU6EI+OhRALYtZ6tG4icN+/braf3VpnLSSWGKQTxAp0FX6+mIk0FIG5QmSG+CK5A0UWZHiezlUoUn6y8YvDyi9EZPOjXLTqObQ8caeGCSnzqaMIMh7LTBZYFgfUj/bCxAz1HBjIsuB7RDqW0HVCag289HPinYLPEWVoWSiR6FWr4inEPhRRuUhALUxmgufI9j+aTpWhWbu7af/BHdIe12WFyWpZORVy9iegKLOTBOTqczQCiladIOCOMGz4C8C2y6A/w1Y442DJb2NgYMtqEIvQYKyqA1TVNW53GSVTlOipKiO1UvlMTippTuQcUDa2JCnaB5Dirgee8XteGbVbtNcd9EC565TVY5IiJziJqfkqZ0xuripZwnJ1VY5FWO5qtAxhA1725UW+A0e8xkyfIhae2Px6zBVEnJS6n++YF3GXomOGdT1MDOTiNRwF8/naOwTbLHgWUC+BZm40irIyA9amEhU3SRV7ecCVpR+VFHFCiZ1lFzPf+0+ScCGG4fL4b8dls4D9Q1XJ2QxP5TQ7MUbBjozGuQT2yZ5LSuEq4SCoyCaXuI+mpA8JDWy9O7zPn1+EK/jMVC6KQyKVu8h1iO0G0rWzh7pJZiPPfftAP1QZq4LD1bG5GXrxTf5r5Beduh9G/mFO5ywnm/o3OFHRuZS/2hZfmnrs8rd2KoB9MChgR1CqXwoU8Doo0OC16Y2N2bmwsv2qCORjs1LNqO/rjaE/0HN0yLL71Nf/UgVqhzviKw9Dp+24y9Z1o1+TXjztL+18f+R9fefK1WvXj7xHewEvWnXezjVCBqHonLeuDTyH2Lpyv3eeDGT7w1L0nTcJS1F6Hh6WolXnweKy4K+RM+VE+jxOeDP+MzR8wY//UlZb/wA=7V1de6I4FP41XrqPIYD2sq2dzu52OrPTbWc6d1GiMgPExVi1v34DJvKRCDgVidaryoGEcM55k/OVtAWv/eVtiKaTT8TBXsvoOMsW7LcMA3Rgj/2JKCtOsc3umjIOXYfTEsKD+4pFU06duw6eZR6khHjUnWaJQxIEeEgzNBSGZJF9bES87FunaIwlwsMQeTL1m+vQyZraszoJ/SN2xxPxZtDhd3wkHuaE2QQ5ZJEiwZsWvA4Joetf/vIaexH3BF+e/hn5n6f/9nHnqW+5l9R+as/b684+7NJk8wkhDuhvdz1//bX8e/X9H9SfI2vY7X9ZPF61Tf5pdCX4hR3GPn5JQjohYxIg7yahXoVkHjg46rXDrpJn7giZMiJgxJ+Y0hXXBTSnhJEm1Pf4Xbx06feo+R8Wv3pO3ekvec/xxUpcBDRcpRpFl8/pe0mz+Eq0q8g6zuIZmYdDzoj//po/0sHHr5O7Ob15fqIfe9jlLO5QFI4x74+O/Vn38da9/wFfbs0uvvpx3xbPRcxMqSEXzC0mPmaDZA+E2EPUfcnqKuIqP948l4iV/eCSVUu5aNQvyJvzN13hsRsw0uPUQRTLSuB5DJ+RsBcTl+KHKYq5smBzRFaUaDZdg3bkLiOV2JHfLzikeFnIIX4X9vhH8HmoDQVOFylUc9IkBWhBewtTldCxztDZBh0lvxTQUT5nNAWdolGnoHNPqDuKBjDAIxLiLQhKJA3KUTRyPe+aeCSM20LHwj3HZPQZDckvnLrTMwbQtuuEmZ2FGTBklG1oaZiZdcEMSAJ4ZuZEg9ADKeAlMCyDXgZ4CQ63QC+nEKPeEA+HKoUY9CzTqgGsRkWwQq3A2j1PybVI2dZKyoY0I1yjYMgaRlL8IM/EE+IP5rPyWfhQpovRtOUCJQY+/HKnjDLfw1pWFxctI8fFxlemi2OZbfY8a9gVZ42eVrOGLc8aIY61vRN1bHT62MPxJcMAmrEvi2MSbMFlE0s4i0MPYTQwJ5KfaBRin7zE17yVbJpogZ68XQeroseuza4DEqc0hY8mi3WvIuxAY7GHomFvd6BuPOwzDs1OzJGysoAzgQJw5kEdqXOsbzfAgaoRC6CXFwTkmEV/y5ImDL4OXoOQ/WJjQNFadhzrmNmruI5ZtcHqvZqBQuvL4WHpBQ/Z/cmtSHdoReb0ZFaiHGQsu2nHCTQap/m9+F2zK5FVFWp6RWqAyvhTyv4ODbCXSy157jhgv4eMh5iB5SpSeXeIvEt+w3cdZ60aeOa+okHcX8T9KXGZPRl1bl21rL5SHoWKKYFrk57mb2mlM8Aq0LWZwsCelQEe76ky63nnX6KvST1CRqN4Kc/JZjOGN4jLksR1XJE1GzYcWTPOTu2OM1vVYJKhl1cL5HBSLB72qiCKBk29CAvMoCDLKFvViZPtdBKRhih4QZqGicwcoLqNB1kNeEbUbjmdynl2QytEGbLXujHL0YhZAKdlleeR1lMkhQ6MNLtJpB2hVS4AVI40vRxgQ1i55SUVepjlQjP3Y5ZbupvhxnuNKFUHlF6pRTHuFKAuA9dnbyZBZPzhINLP43CheoqA6mFdqJNPUzRe3FU5nKSZiSi8+9NVjX1PqJUFrdmEKgeivuJ1/irAi5a6MGPmE8JmrMgD53Pv23yFQ1n+F43H46FxLLjaNz6q1lRAoBc+SosqeML3xIsqQMdUYOegVRWwUXPlCL1mAaVyzOlVVQHlNUnrXJZQzL04zTawzCzyWpo70ULNjjaXBVRbbw5bJn402yn2PUVVrWyBeqXbYWllCzcLTssqyBvUQLXB4sBWwXsN4cGq+Vyol8cJ5XyuAEmyH0BLb/Iir/xVKyJrU37zvbqTsKo7KVivi/IXuJNx6vU0l408cmDjgRjzRDY7N+pgCunsz8FM/KALUTy8m+NzGYZolXqAO3WyX8TfZJs51RQmzoeqDUBJAysfSck1YD/Wg67TcbPdm+nXTz9fP49+DL7Z9v3ysTvciFn7xUNjdVcyVq81Rwxb3qQyi8fgIzdwg7GWrroF8vBUhCHrctWVsj0XyW0FTREWSjFjNIWZolGXmWknFvSX3HvlBpa63HulIBqtlDuMg1MEh1LYNFbvVjTqrbA5LbhIJzjZqhxZXW6NUgDnip66K3pUSbUjWNDklNqfgUtd5MUjYPIPZc3R/mRCoNo0UZc1qDx8stH42y4Rg99HQdGhm6WHczZWzKEcjQwCbaqxi7RrL3nlLjBhBjyidy0Sy8rPNyVxbSkDONT6pnWE7k04bawApGjUacHHtYhTb74+S5dJKNroN6NRnYGWgQvpEF2gcKUOulQdzylQe8bA3tcgdZzXzJ2atzH8RRdrEPJWiTB3jVhbuQB0Sfg5P6yi4PO+Jm45t7JxAxlkw5PdWdg+ZFpLyXk5vdhwVmuXNXPPuFeUFimf62q19r2Dcpaq0io6JXzPs/muk7Bxof0kLFeofRHmU3KU5igkvu6mlJFLoLYPuG9OvbydN0fthu3uUWEb6I9teVt5ysBi+nQi9hWAWdbWeEQKu0z+vdBaTMl/aYI3/wM=3Vpbc6IwFP41PtrhLvvYqrU72227a2d2uy87ESKkjYQJ8frrN4EgUBC1VdHtgyUnF5LvfN/JIdDSu5PFgILQ/05ciFua4i5aeq+laaqi2/yfsCwTi2l0EoNHkSsbZYYhWsG0p7ROkQujQkNGCGYoLBodEgTQYQUboJTMi83GBBfvGgIPlgxDB+Cy9RdymZ9YbVPJ7HcQeX56Z1WRNROQNpaGyAcumedMer+ldykhLLmaLLoQC/BSXB6JYdz9fMPfQwSfn17bg6+PT+1ksNt9uqyXQGHAPjz0b3IbrOw/9w+vU2fV9WZ/ekRtp0tjyxQv6HL4ZJFQ5hOPBAD3M+sNJdPAhWJUhZeyNveEhNyocuMrZGwpuQCmjHCTzyZY1sIFYr9F9ytTll5yNb2FHDkuLNNCwOgy10kUX/J1Wbe4lPbbEToJcUSm1IE1eKUMBtSDrK6dkTQUaOZ4KD0zgGQC+Sx5AwoxYGhWJCuQnPfW7dZdnwjiC9EUqU8jJadUp5qW0yGSmcpeGTv4RW4amSnmzB78kROeATyVS+jPYDzBn9CBfF1umWAYc+0LIs19xOAwBDHicx5+ijQBUZgEhDFaCLrt6csZpAwuasGXtbpdBDHFcJ6LF9Lk50KFrWz2VgHnGlCH5jPpPz7YULn5+mOo+9po4LXNSxHlocVllNVVCZB2aHHt6q7aaedE8EAYGosZjOCYUCg18c6pmcvU7WIYI4y7BBMa99VdE9quwe0Ro+QN5mpsbaRb1jHVYm0IOTm5rG15vRjH0ks5CL3wjKNBDak5BWV62raxFba1bJc7+MZWJ6mt0tObkt639orCO+S+0M7fv4NQvR4MrPXqzj5Snj592dXLlbgqTXm5btY5fXdB4PCOwou35bjqk8loGm2PqafLJxpOKPQSgMM3FMZJGpcC/dzOdCwUzXepbVVadrR9plIVnUuJNgeOGtaOUcM6q6hhbc7KwJhB+l8nZUZFyDmtWLT/Xyx1W+fWRKoxsdTNOieWOxC43Cl8hp/XyalYrze+RagXk5EemPbmjnvEl7OivVmi/TNFnidSIj6D882NjE6R+Fbj4d5ukvcfe94+1ZNYXa60VS/mWelFLT+KbTpquQcjiN8d6GLkBfza4RByZek3gvDIAfhaVkyQ6ybMgBFagVE8ngA/FOfX8WLMm5bZq3RHHS1Lylq/b5I3aeVf6VQprq1cWXbHLKhOjvTRI/y0CRmPI/jZ0/lqd13M652G1HVeu5Fafk5/IGcsrpReB1FXR9PVy1JX+QGz64PAu5BDqXUKcYJDqTrt5eDrkQCW6X72rwstu2Ekq19A8atrbnzCUw8FQpJA/KLA5UGBiQc7IIwxY/nFSCyQC4WhwBN11IuuHEln8cWG+KHTsns4ZmzzoUggPLpPbKrycXFDKp2tKPEfr3FB5K8T82M5n9+v4Hw9zQnymbdS4f0v+3ufF7OvS5KolX2jo/f/AQ==7VxbU9s4FP41eWTH8t2P5dZOh7I7S7stTx0RK4mKbWUcBRJ+/UqO7NhSCAr4CmEGiI9v8tH3nYvOcUbWWbz6nML57BsJUTQyjXA1ss5HpgkMy2f/uGQtJK7tbSTTFIdCthXc4CeUnyqkSxyiReVASkhE8bwqHJMkQWNakcE0JY/VwyYkqt51DqdIEdyMYaRKf+KQzjZS3zG28i8IT2f5nYEh9sQwP1gIFjMYkseSyLoYWWcpIXTzKV6doYhrL9dLSN2/Z+6P35Of/vLh6ov1359fwcnmYpeHnFI8QooS+upLf326vP7qBEsv+W1cGPG3ZOo/nbimeDa6zhWGQqY/sUlSOiNTksDoYis9TckyCRG/rMG2tsdcETJnQsCEfxClawEGuKSEiWY0jsRetML0Fz/9L0ds3Zb2nK/ElbONtdjQ1IHQ1YIs0zHa8+A5FGE6RXSfgsSBXCslQAkVf0YkRjRdswNSFEGKH6qogwK80+K47QSxD2KODpgvcd0HGC3FnU7RFCdM9GMeQorU2YwixjQ+a48zTNHNHGZqeWRsr84JXMw39JvgFZ/bAxX+gFKKVns1JPZavuCTsCgnpisEjyV+CtGsRM1cVrtSXXsoJGBTka5LJ/HN2/K+7WnZVkPkcbXZ49TNHnHqPwSzJykwZZsSpoApgWUzVHGahJdiHG+AkErMf9EC8SEuMl/E0bWLn1sYgZc52hglXVl9OyhZqLTMSbsxTjqKQm8ydYY4RskCk2QxFF0aXevSMxRVHe3bXvvm6Nq3oBv7JrvCzRM1Zt28TqNEUILPFkz9BlCgCSCvX+GlZylW95rsnPsreMcyxWoEGeFpwj6PmQ5RygTcaGLm/T6JHTEOww000AI/wbvselz7cw7c7Fmc05FzvnM+9gJTMc9FPinuMiqnbLvM9gkDDHC9CstEWPha7uaHkMkkiwTq56U/mOxtaLzMDWxPeJmPu8TLc0ghk0Q4xow6DO9wPOOzeqmGRTMS3y0XHYZEcsZnd5zwec5QeFMz/j3dxM2ze4V/T02vrgnFEz6COzQhKeJ0uBJceEtaMMFRdEYikmbnWqGD/NBm8gVNyT0q7fHNO8t1G2SNLbFmB2laTiO8j8oaW5c1br9YY6teA7HQLMYJX5AIqx6kj8m0zIEe5NLBRyWBq0sCv18kcJ93HXDC2PD+PUf3q3k+UHT7QVjja7KmZwlHPu4Say5Wc5iEfAQwmaIMTQxMvOzLGcF0weYCJ1N+5Qn7kyDEJ28QbqUo8XZHkMGUoGomSD4TLxPE6hVB8nE/n5GcLnEUfsfj+3fjV+TShtV5NOa7H5U2li5taq/Avm3C1AXmjCdMVANVWgN+9wGVPxTg92Tl19ct6QFQe0lGr6bnuRJYGm5Z8NUKezUxev8OzO487gvUMOIWqfo+Vlv3cbug7MvesK16vVcFmg8kADVcsA/Mo3toCEJB7ZURzZYP2T08A6EXLxS03BoXWEcsHhaq6Bapu4Ji2wjKFVLuBYTxPOuqpP3NGmQn4HRewQsGU8HrCRUD3cpfUZhqm4vAkLskmjbnalHxjCScFYKN/B/h42G/UdYlxgGXZkvDfaSpnBS4nScFwDi6zAPDt5x+GkTtyGkCq2WvWejkmfJ/zs2UUPacJBkIO/3u2ekpej3m7IczVt+16nbVbFt5LfYjucnNZn97eUE+xKPRbwBC/epJKUZeMiHZ1LCnvsfzIq3pZTOvlNV4u7IaY4dBLoQNqHMwTVm9oY5uN1f/qKP2c13iiL9wUiwG8O8NgOt+xjN2IC3rgc7jGXCMZ+pglG6nFwAtvRTtuPJSgWyBm85AwNEuN4Yi02wHRUoea3pt57FqL2Kx/IsS/oJfT197yl8zyk19oJr6YIelD5qz9OprNJkmN22bvdal/DLMjmWAVl8hK6z4u1kHlSOToPMmp+JuR/+h6z+AdptTW/5DiUKAbN9eW8hu3xPlb4Qf8aiLxxxlGnisPc/UQ5FpyParcRSZiue4xDT3Ej0tZsvr8MDovJpdQKakyPxb0Iwx4RHiIL8QDYAdawI1BTdsc/uFhRtEb7/30br4Hw==7ZjbctowEIafhst2fMI4lw2QdtrS6QzTNrlU8NpWI1seWQa7T18JSz5gIKQFEjK5wvtLK0u7+7EaD+xxXHxkKI1m1AcysAy/GNiTgWWZhu2JH6mUlTJ0RpUQMuyrSY0wx39Aeyo1xz5knYmcUsJx2hUXNElgwTsaYoyuutMCSrpvTVEIPWG+QKSv/sI+jyrVs0aN/glwGOk3m+5VNRIjPVmdJIuQT1ctyZ4O7DGjlFdPcTEGIoOn41L53ewYrTfGIOGHOMROdmM8/PzxxboNZt7sw+frRfhOrZLxUh8YfHF+ZVLGIxrSBJFpo14zmic+yFUNYTVzvlKaCtEU4m/gvFTJRDmnQop4TNQoFJjfSvf3Q2XdtUYmhVp5bZTaSDgrW07SvGuPNW5rS/tV55OH2hk2HQOaswXsiZWlyg+xEPieeU6dXEEF0BjEfoQfA4I4Xnb3gVR5hvW8JoPiQSXxCQlVm1wikqs3XUOIE0kgZJzRsp9wQgRMMrGrCHOYp2gdhZXguZs2lKUVYQEuZPpVbJfAOBT7o9uPhnKwPUWH+nvQsKxarCkpamGmtaOHz37j4WAe/rfOlet3isWb64pwrM2K2Mh1xZ/y2kh3vY1/rwCnB9A3ynEgT3YPAWUw2cFRUwbm4ywFmJAxJZStfW1/CJ7vCF0u/QCtEc+6t133SLC53dCadp8209qCm3Mq3IaXgtsRsTH1NeaxPuKehC93owgcw+ouUR3gZHy5Pb5qoMQlLk4JcHjZPcoxredtUqNLoeYFNCnvQNhMY3sNnOfW5u1uOijgwF5Pz7GMPjzn7TlXb/Qc3quMQ/HZUQXnwUdvcxs/GRcpegXcjJ6bG31v2RbkPMFJxpFc5OIjbTuni7Qwmw8+1Y2q+WxmT/8C \ No newline at end of file diff --git a/docs/developers/plugins.md b/docs/developers/plugins.md new file mode 100644 index 00000000000..60747416b91 --- /dev/null +++ b/docs/developers/plugins.md @@ -0,0 +1,203 @@ +# Plugins + +Plugins are the most efficient way to customize or change the default behavior of a chart. They have been introduced at [version 2.1.0](https://github.com/chartjs/Chart.js/releases/tag/2.1.0) (global plugins only) and extended at [version 2.5.0](https://github.com/chartjs/Chart.js/releases/tag/v2.5.0) (per chart plugins and options). + +## Using plugins + +Plugins can be shared between chart instances: + +```javascript +const plugin = { /* plugin implementation */ }; + +// chart1 and chart2 use "plugin" +const chart1 = new Chart(ctx, { + plugins: [plugin] +}); + +const chart2 = new Chart(ctx, { + plugins: [plugin] +}); + +// chart3 doesn't use "plugin" +const chart3 = new Chart(ctx, {}); +``` + +Plugins can also be defined directly in the chart `plugins` config (a.k.a. *inline plugins*): + +:::warning +*inline* plugins are not registered. Some plugins require registering, i.e. can't be used *inline*. +::: + +```javascript +const chart = new Chart(ctx, { + plugins: [{ + beforeInit: function(chart, args, options) { + //.. + } + }] +}); +``` + +However, this approach is not ideal when the customization needs to apply to many charts. + +## Global plugins + +Plugins can be registered globally to be applied on all charts (a.k.a. *global plugins*): + +```javascript +Chart.register({ + // plugin implementation +}); +``` + +:::warning +*inline* plugins can't be registered globally. +::: + +## Configuration + +### Plugin ID + +Plugins must define a unique id in order to be configurable. + +This id should follow the [npm package name convention](https://docs.npmjs.com/files/package.json#name): + +- can't start with a dot or an underscore +- can't contain any non-URL-safe characters +- can't contain uppercase letters +- should be something short, but also reasonably descriptive + +If a plugin is intended to be released publicly, you may want to check the [registry](https://www.npmjs.com/search?q=chartjs-plugin-) to see if there's something by that name already. Note that in this case, the package name should be prefixed by `chartjs-plugin-` to appear in Chart.js plugin registry. + +### Plugin options + +Plugin options are located under the `options.plugins` config and are scoped by the plugin ID: `options.plugins.{plugin-id}`. + +```javascript +const chart = new Chart(ctx, { + options: { + foo: { ... }, // chart 'foo' option + plugins: { + p1: { + foo: { ... }, // p1 plugin 'foo' option + bar: { ... } + }, + p2: { + foo: { ... }, // p2 plugin 'foo' option + bla: { ... } + } + } + } +}); +``` + +#### Disable plugins + +To disable a global plugin for a specific chart instance, the plugin options must be set to `false`: + +```javascript +Chart.register({ + id: 'p1', + // ... +}); + +const chart = new Chart(ctx, { + options: { + plugins: { + p1: false // disable plugin 'p1' for this instance + } + } +}); +``` + +To disable all plugins for a specific chart instance, set `options.plugins` to `false`: + +```javascript +const chart = new Chart(ctx, { + options: { + plugins: false // all plugins are disabled for this instance + } +}); +``` + +#### Plugin defaults + +You can set default values for your plugin options in the `defaults` entry of your plugin object. In the example below the canvas will always have a lightgreen backgroundColor unless the user overrides this option in `options.plugins.custom_canvas_background_color.color`. + +```javascript +const plugin = { + id: 'custom_canvas_background_color', + beforeDraw: (chart, args, options) => { + const {ctx} = chart; + ctx.save(); + ctx.globalCompositeOperation = 'destination-over'; + ctx.fillStyle = options.color; + ctx.fillRect(0, 0, chart.width, chart.height); + ctx.restore(); + }, + defaults: { + color: 'lightGreen' + } +} +``` + +## Plugin Core API + +Read more about the [existing plugin extension hooks](../api/interfaces/Plugin). + +### Chart Initialization + +Plugins are notified during the initialization process. These hooks can be used to set up data needed for the plugin to operate. + +![Chart.js init flowchart](./init_flowchart.png) + +### Chart Update + +Plugins are notified throughout the update process. + +![Chart.js update flowchart](./update_flowchart.png) + +### Scale Update + +Plugins are notified throughout the scale update process. + +![Chart.js scale update flowchart](./scale_flowchart.png) + +### Rendering + +Plugins can interact with the chart throughout the render process. The rendering process is documented in the flowchart below. Each of the green processes is a plugin notification. The red lines indicate how cancelling part of the render process can occur when a plugin returns `false` from a hook. Not all hooks are cancelable, however, in general most `before*` hooks can be cancelled. + +![Chart.js render pipeline flowchart](./render_flowchart.png) + +### Event Handling + +Plugins can interact with the chart during the event handling process. The event handling flow is documented in the flowchart below. Each of the green processes is a plugin notification. If a plugin makes changes that require a re-render, the plugin can set `args.changed` to `true` to indicate that a render is needed. The built-in tooltip plugin uses this method to indicate when the tooltip has changed. + +![Chart.js event handling flowchart](./event_flowchart.png) + +### Chart destroy + +Plugins are notified during the destroy process. These hooks can be used to destroy things that the plugin made and used during its life. +The `destroy` hook has been deprecated since Chart.js version 3.7.0, use the `afterDestroy` hook instead. + +![Chart.js destroy flowchart](./destroy_flowchart.png) + +## TypeScript Typings + +If you want your plugin to be statically typed, you must provide a `.d.ts` TypeScript declaration file. Chart.js provides a way to augment built-in types with user-defined ones, by using the concept of "declaration merging". + +When adding a plugin, `PluginOptionsByType` must contain the declarations for the plugin. + +For example, to provide typings for the [`canvas backgroundColor plugin`](../configuration/canvas-background.md), you would add a `.d.ts` containing: + +```ts +import {ChartType, Plugin} from 'chart.js'; + +declare module 'chart.js' { + interface PluginOptionsByType { + customCanvasBackgroundColor?: { + color?: string + } + } +} +``` diff --git a/docs/developers/publishing.md b/docs/developers/publishing.md new file mode 100644 index 00000000000..8090b96b046 --- /dev/null +++ b/docs/developers/publishing.md @@ -0,0 +1,37 @@ +# Publishing an extension + +If you are planning on publishing an extension for Chart.js, here are some pointers. + +## Awesome + +You'd probably want your extension to be listed in the [awesome](https://github.com/chartjs/awesome). + +Note the minimum extension age requirement of 30 days. + +## ESM + +If you are utilizing ESM, you probably still want to publish a UMD bundle of your extension. Because Chart.js v3 is tree shakeable, the interface is a bit different. +UMD package's global `Chart` includes everything, while ESM package exports all the things separately. +Fortunately, most of the exports can be mapped automatically by the bundlers. + +But not the helpers. + +In UMD, helpers are available through `Chart.helpers`. In ESM, they are imported from `chart.js/helpers`. + +For example `import {isNullOrUndef} from 'chart.js/helpers'` is available at `Chart.helpers.isNullOrUndef` for UMD. + +### Rollup + +`output.globals` can be used to convert the helpers. + +```js +module.exports = { + // ... + output: { + globals: { + 'chart.js': 'Chart', + 'chart.js/helpers': 'Chart.helpers' + } + } +}; +``` diff --git a/docs/developers/render_flowchart.png b/docs/developers/render_flowchart.png new file mode 100644 index 00000000000..db03a04a97c Binary files /dev/null and b/docs/developers/render_flowchart.png differ diff --git a/docs/developers/scale_flowchart.png b/docs/developers/scale_flowchart.png new file mode 100644 index 00000000000..29e680ff36b Binary files /dev/null and b/docs/developers/scale_flowchart.png differ diff --git a/docs/developers/update_flowchart.png b/docs/developers/update_flowchart.png new file mode 100644 index 00000000000..f3270c2e33d Binary files /dev/null and b/docs/developers/update_flowchart.png differ diff --git a/docs/developers/updates.md b/docs/developers/updates.md new file mode 100644 index 00000000000..ed22e3315cb --- /dev/null +++ b/docs/developers/updates.md @@ -0,0 +1,106 @@ +# Updating Charts + +It's pretty common to want to update charts after they've been created. When the chart data or options are changed, Chart.js will animate to the new data values and options. + +## Adding or Removing Data + +Adding and removing data is supported by changing the data array. To add data, just add data into the data array as seen in this example, to remove it you can pop it again. + +```javascript +function addData(chart, label, newData) { + chart.data.labels.push(label); + chart.data.datasets.forEach((dataset) => { + dataset.data.push(newData); + }); + chart.update(); +} + +function removeData(chart) { + chart.data.labels.pop(); + chart.data.datasets.forEach((dataset) => { + dataset.data.pop(); + }); + chart.update(); +} +``` + +## Updating Options + +To update the options, mutating the `options` property in place or passing in a new options object are supported. + +- If the options are mutated in place, other option properties would be preserved, including those calculated by Chart.js. +- If created as a new object, it would be like creating a new chart with the options - old options would be discarded. + +```javascript +function updateConfigByMutating(chart) { + chart.options.plugins.title.text = 'new title'; + chart.update(); +} + +function updateConfigAsNewObject(chart) { + chart.options = { + responsive: true, + plugins: { + title: { + display: true, + text: 'Chart.js' + } + }, + scales: { + x: { + display: true + }, + y: { + display: true + } + } + }; + chart.update(); +} +``` + +Scales can be updated separately without changing other options. +To update the scales, pass in an object containing all the customization including those unchanged ones. + +Variables referencing any one from `chart.scales` would be lost after updating scales with a new `id` or the changed `type`. + +```javascript +function updateScales(chart) { + let xScale = chart.scales.x; + let yScale = chart.scales.y; + chart.options.scales = { + newId: { + display: true + }, + y: { + display: true, + type: 'logarithmic' + } + }; + chart.update(); + // need to update the reference + xScale = chart.scales.newId; + yScale = chart.scales.y; +} +``` + +You can update a specific scale by its id as well. + +```javascript +function updateScale(chart) { + chart.options.scales.y = { + type: 'logarithmic' + }; + chart.update(); +} +``` + +Code sample for updating options can be found in [line-datasets.html](https://www.chartjs.org/docs/latest/samples/area/line-datasets.html). + +## Preventing Animations + +Sometimes when a chart updates, you may not want an animation. To achieve this you can call `update` with `'none'` as mode. + +```javascript +myChart.update('none'); +``` diff --git a/docs/general/accessibility.md b/docs/general/accessibility.md new file mode 100644 index 00000000000..687bee148d7 --- /dev/null +++ b/docs/general/accessibility.md @@ -0,0 +1,39 @@ +# Accessibility + +Chart.js charts are rendered on user provided `canvas` elements. Thus, it is up to the user to create the `canvas` element in a way that is accessible. The `canvas` element has support in all browsers and will render on screen but the `canvas` content will not be accessible to screen readers. + +With `canvas`, the accessibility has to be added with ARIA attributes on the `canvas` element or added using internal fallback content placed within the opening and closing canvas tags. + +This [website](http://pauljadam.com/demos/canvas.html) has a more detailed explanation of `canvas` accessibility as well as in depth examples. + +## Examples + +These are some examples of **accessible** `canvas` elements. + +By setting the `role` and `aria-label`, this `canvas` now has an accessible name. + +```html + +``` + +This `canvas` element has a text alternative via fallback content. + +```html + +

    Hello Fallback World

    +
    +``` + +These are some bad examples of **inaccessible** `canvas` elements. + +This `canvas` element does not have an accessible name or role. + +```html + +``` + +This `canvas` element has inaccessible fallback content. + +```html +Your browser does not support the canvas element. +``` diff --git a/docs/general/colors-plugin-palette.png b/docs/general/colors-plugin-palette.png new file mode 100644 index 00000000000..ac6994ee272 Binary files /dev/null and b/docs/general/colors-plugin-palette.png differ diff --git a/docs/general/colors.md b/docs/general/colors.md new file mode 100644 index 00000000000..49f1b2cacf1 --- /dev/null +++ b/docs/general/colors.md @@ -0,0 +1,158 @@ +# Colors + +Charts support three color options: +* for geometric elements, you can change *background* and *border* colors; +* for textual elements, you can change the *font* color. + +Also, you can change the whole [canvas background](../configuration/canvas-background.md). + +## Default colors + +If a color is not specified, a global default color from `Chart.defaults` is used: + +| Name | Type | Description | Default value +| ---- | ---- | ----------- | ------------- +| `backgroundColor` | [`Color`](../api/#color) | Background color | `rgba(0, 0, 0, 0.1)` +| `borderColor` | [`Color`](../api/#color) | Border color | `rgba(0, 0, 0, 0.1)` +| `color` | [`Color`](../api/#color) | Font color | `#666` + +You can reset default colors by updating these properties of `Chart.defaults`: + +```javascript +Chart.defaults.backgroundColor = '#9BD0F5'; +Chart.defaults.borderColor = '#36A2EB'; +Chart.defaults.color = '#000'; +``` + +### Per-dataset color settings + +If your chart has multiple datasets, using default colors would make individual datasets indistinguishable. In that case, you can set `backgroundColor` and `borderColor` for each dataset: + +```javascript +const data = { + labels: ['A', 'B', 'C'], + datasets: [ + { + label: 'Dataset 1', + data: [1, 2, 3], + borderColor: '#36A2EB', + backgroundColor: '#9BD0F5', + }, + { + label: 'Dataset 2', + data: [2, 3, 4], + borderColor: '#FF6384', + backgroundColor: '#FFB1C1', + } + ] +}; +``` + +However, setting colors for each dataset might require additional work that you'd rather not do. In that case, consider using the following plugins with pre-defined or generated palettes. + +### Default color palette + +If you don't have any preference for colors, you can use the built-in `Colors` plugin. It will cycle through a palette of seven Chart.js brand colors: + +
    + +![Colors plugin palette](./colors-plugin-palette.png) + +
    + +All you need is to import and register the plugin: + +```javascript +import { Colors } from 'chart.js'; + +Chart.register(Colors); +``` + +:::tip Note + +If you are using the UMD version of Chart.js, this plugin will be enabled by default. You can disable it by setting the `enabled` option to `false`: + +```js +const options = { + plugins: { + colors: { + enabled: false + } + } +}; +``` + +::: + +### Dynamic datasets at runtime + +By default, the colors plugin only works when you initialize the chart without any colors for the border or background specified. +If you want to force the colors plugin to always color your datasets, for example, when using dynamic datasets at runtime you will need to set the `forceOverride` option to true: + +```js +const options = { + plugins: { + colors: { + forceOverride: true + } + } +}; +``` + + +### Advanced color palettes + +See the [awesome list](https://github.com/chartjs/awesome#plugins) for plugins that would give you more flexibility defining color palettes. + +## Color formats + +You can specify the color as a string in either of the following notations: + +| Notation | Example | Example with transparency +| -------- | ------- | ------------------------- +| [Hexadecimal](https://developer.mozilla.org/en-US/docs/Web/CSS/hex-color) | `#36A2EB` | `#36A2EB80` +| [RGB](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/rgb) or [RGBA](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/rgba) | `rgb(54, 162, 235)` | `rgba(54, 162, 235, 0.5)` +| [HSL](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/hsl) or [HSLA](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/hsla) | `hsl(204, 82%, 57%)` | `hsla(204, 82%, 57%, 0.5)` + +Alternatively, you can pass a [CanvasPattern](https://developer.mozilla.org/en-US/docs/Web/API/CanvasPattern) or [CanvasGradient](https://developer.mozilla.org/en/docs/Web/API/CanvasGradient) object instead of a string color to achieve some interesting effects. + +## Patterns and Gradients + +For example, you can fill a dataset with a pattern from an image. + +```javascript +const img = new Image(); +img.src = 'https://example.com/my_image.png'; +img.onload = () => { + const ctx = document.getElementById('canvas').getContext('2d'); + const fillPattern = ctx.createPattern(img, 'repeat'); + + const chart = new Chart(ctx, { + data: { + labels: ['Item 1', 'Item 2', 'Item 3'], + datasets: [{ + data: [10, 20, 30], + backgroundColor: fillPattern + }] + } + }); +}; +``` +Pattern fills can help viewers with vision deficiencies (e.g., color-blindness or partial sight) [more easily understand your data](http://betweentwobrackets.com/data-graphics-and-colour-vision/). + +You can use the [Patternomaly](https://github.com/ashiguruma/patternomaly) library to generate patterns to fill datasets: + +```javascript +const chartData = { + datasets: [{ + data: [45, 25, 20, 10], + backgroundColor: [ + pattern.draw('square', '#ff6384'), + pattern.draw('circle', '#36a2eb'), + pattern.draw('diamond', '#cc65fe'), + pattern.draw('triangle', '#ffce56') + ] + }], + labels: ['Red', 'Blue', 'Purple', 'Yellow'] +}; +``` diff --git a/docs/general/data-structures.md b/docs/general/data-structures.md new file mode 100644 index 00000000000..39074070ab5 --- /dev/null +++ b/docs/general/data-structures.md @@ -0,0 +1,216 @@ +# Data structures + +The `data` property of a dataset can be passed in various formats. By default, that `data` is parsed using the associated chart type and scales. + +If the `labels` property of the main `data` property is used, it has to contain the same amount of elements as the dataset with the most values. These labels are used to label the index axis (default `x` axis). The values for the labels have to be provided in an array. +The provided labels can be of the type string or number to be rendered correctly. If you want multiline labels, you can provide an array with each line as one entry in the array. + +## Primitive[] + +```javascript +const cfg = { + type: 'bar', + data: { + datasets: [{ + data: [20, 10], + }], + labels: ['a', 'b'] + } +} +``` + +When `data` is an array of numbers, values from the `labels` array at the same index are used for the index axis (`x` for vertical, `y` for horizontal charts). + +## Array[] + +```javascript +const cfg = { + type: 'line', + data: { + datasets: [{ + data: [[10, 20], [15, null], [20, 10]] + }] + } +} +``` + +When `data` is an array of arrays (or what TypeScript would call tuples), the first element of each tuple is the index (`x` for vertical, `y` for horizontal charts) and the second element is the value (`y` by default). + +## Object[] + +```javascript +const cfg = { + type: 'line', + data: { + datasets: [{ + data: [{x: 10, y: 20}, {x: 15, y: null}, {x: 20, y: 10}] + }] + } +} +``` + +```javascript +const cfg = { + type: 'line', + data: { + datasets: [{ + data: [{x: '2016-12-25', y: 20}, {x: '2016-12-26', y: 10}] + }] + } +} +``` + +```javascript +const cfg = { + type: 'bar', + data: { + datasets: [{ + data: [{x: 'Sales', y: 20}, {x: 'Revenue', y: 10}] + }] + } +} +``` + +This is also the internal format used for parsed data. In this mode, parsing can be disabled by specifying `parsing: false` at chart options or dataset. If parsing is disabled, data must be sorted and in the formats the associated chart type and scales use internally. + +The values provided must be parsable by the associated scales or in the internal format of the associated scales. For example, the `category` scale uses integers as an internal format, where each integer represents an index in the labels array; but, if parsing is enabled, it can also parse string labels. + +`null` can be used for skipped values. + +## Object[] using custom properties + +```javascript +const cfg = { + type: 'bar', + data: { + datasets: [{ + data: [{id: 'Sales', nested: {value: 1500}}, {id: 'Purchases', nested: {value: 500}}] + }] + }, + options: { + parsing: { + xAxisKey: 'id', + yAxisKey: 'nested.value' + } + } +} +``` + +When using the pie/doughnut, radar or polarArea chart type, the `parsing` object should have a `key` item that points to the value to look at. In this example, the doughnut chart will show two items with values 1500 and 500. + +```javascript +const cfg = { + type: 'doughnut', + data: { + datasets: [{ + data: [{id: 'Sales', nested: {value: 1500}}, {id: 'Purchases', nested: {value: 500}}] + }] + }, + options: { + parsing: { + key: 'nested.value' + } + } +} +``` + +If the key contains a dot, it needs to be escaped with a double slash: + +```javascript +const cfg = { + type: 'line', + data: { + datasets: [{ + data: [{'data.key': 'one', 'data.value': 20}, {'data.key': 'two', 'data.value': 30}] + }] + }, + options: { + parsing: { + xAxisKey: 'data\\.key', + yAxisKey: 'data\\.value' + } + } +} +``` + +:::warning +When using object notation in a radar chart, you still need a `labels` array with labels for the chart to show correctly. +::: + +## Object + +```javascript +const cfg = { + type: 'line', + data: { + datasets: [{ + data: { + January: 10, + February: 20 + } + }] + } +} +``` + +In this mode, the property name is used for the `index` scale and value for the `value` scale. For vertical charts, the index scale is `x` and value scale is `y`. + +## Dataset Configuration + +| Name | Type | Description +| ---- | ---- | ----------- +| `label` | `string` | The label for the dataset which appears in the legend and tooltips. +| `clip` | `number`\|`object` | How to clip relative to chartArea. Positive value allows overflow, negative value clips that many pixels inside chartArea. 0 = clip at chartArea. Clipping can also be configured per side: clip: {left: 5, top: false, right: -2, bottom: 0} +| `order` | `number` | The drawing order of dataset. Also affects order for stacking, tooltip and legend. +| `stack` | `string` | The ID of the group to which this dataset belongs to (when stacked, each group will be a separate stack). Defaults to dataset `type`. +| `parsing` | `boolean`\|`object` | How to parse the dataset. The parsing can be disabled by specifying parsing: false at chart options or dataset. If parsing is disabled, data must be sorted and in the formats the associated chart type and scales use internally. +| `hidden` | `boolean` | Configure the visibility of the dataset. Using `hidden: true` will hide the dataset from being rendered in the Chart. + +### parsing + +```javascript +const data = [{x: 'Jan', net: 100, cogs: 50, gm: 50}, {x: 'Feb', net: 120, cogs: 55, gm: 75}]; +const cfg = { + type: 'bar', + data: { + labels: ['Jan', 'Feb'], + datasets: [{ + label: 'Net sales', + data: data, + parsing: { + yAxisKey: 'net' + } + }, { + label: 'Cost of goods sold', + data: data, + parsing: { + yAxisKey: 'cogs' + } + }, { + label: 'Gross margin', + data: data, + parsing: { + yAxisKey: 'gm' + } + }] + }, +}; +``` + +## TypeScript + +When using TypeScript, if you want to use a data structure that is not the default data structure, you will need to pass it to the type interface when instantiating the data variable. + +```ts +import {ChartData} from 'chart.js'; + +const datasets: ChartData <'bar', {key: string, value: number} []> = { + datasets: [{ + data: [{key: 'Sales', value: 20}, {key: 'Revenue', value: 10}], + parsing: { + xAxisKey: 'key', + yAxisKey: 'value' + } + }], +}; +``` diff --git a/docs/general/fonts.md b/docs/general/fonts.md new file mode 100644 index 00000000000..df9f4f6cb67 --- /dev/null +++ b/docs/general/fonts.md @@ -0,0 +1,41 @@ +# Fonts + +There are special global settings that can change all the fonts on the chart. These options are in `Chart.defaults.font`. The global font settings only apply when more specific options are not included in the config. + +For example, in this chart, the text will have a font size of 16px except for the labels in the legend. + +```javascript +Chart.defaults.font.size = 16; +let chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + plugins: { + legend: { + labels: { + // This more specific font property overrides the global property + font: { + size: 14 + } + } + } + } + } +}); +``` + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| `family` | `string` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | Default font family for all text, follows CSS font-family options. +| `size` | `number` | `12` | Default font size (in px) for text. Does not apply to radialLinear scale point labels. +| `style` | `string` | `'normal'` | Default font style. Does not apply to tooltip title or footer. Does not apply to chart title. Follows CSS font-style options (i.e. normal, italic, oblique, initial, inherit). +| `weight` | `normal` \| `bold` \| `lighter` \| `bolder` \| `number` | `undefined` | Default font weight (boldness). (see [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight)). +| `lineHeight` | `number`\|`string` | `1.2` | Height of an individual line of text (see [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/line-height)). + +## Missing Fonts + +If a font is specified for a chart that does exist on the system, the browser will not apply the font when it is set. If you notice odd fonts appearing in your charts, check that the font you are applying exists on your system. See [issue 3318](https://github.com/chartjs/Chart.js/issues/3318) for more details. + +## Loading Fonts + +If a font is not cached and needs to be loaded, charts that use the font will need to be updated once the font is loaded. This can be accomplished using the [Font Loading APIs](https://developer.mozilla.org/en-US/docs/Web/API/CSS_Font_Loading_API). See [issue 8020](https://github.com/chartjs/Chart.js/issues/8020) for more details. diff --git a/docs/general/options.md b/docs/general/options.md new file mode 100644 index 00000000000..cea6e971285 --- /dev/null +++ b/docs/general/options.md @@ -0,0 +1,184 @@ +# Options + +## Option resolution + +Options are resolved from top to bottom, using a context dependent route. + +### Chart level options + +* options +* overrides[`config.type`] +* defaults + +### Dataset level options + +`dataset.type` defaults to `config.type`, if not specified. + +* dataset +* options.datasets[`dataset.type`] +* options +* overrides[`config.type`].datasets[`dataset.type`] +* defaults.datasets[`dataset.type`] +* defaults + +### Dataset animation options + +* dataset.animation +* options.datasets[`dataset.type`].animation +* options.animation +* overrides[`config.type`].datasets[`dataset.type`].animation +* defaults.datasets[`dataset.type`].animation +* defaults.animation + +### Dataset element level options + +Each scope is looked up with `elementType` prefix in the option name first, then without the prefix. For example, `radius` for `point` element is looked up using `pointRadius` and if that does not hit, then `radius`. + +* dataset +* options.datasets[`dataset.type`] +* options.datasets[`dataset.type`].elements[`elementType`] +* options.elements[`elementType`] +* options +* overrides[`config.type`].datasets[`dataset.type`] +* overrides[`config.type`].datasets[`dataset.type`].elements[`elementType`] +* defaults.datasets[`dataset.type`] +* defaults.datasets[`dataset.type`].elements[`elementType`] +* defaults.elements[`elementType`] +* defaults + +### Scale options + +* options.scales +* overrides[`config.type`].scales +* defaults.scales +* defaults.scale + +### Plugin options + +A plugin can provide `additionalOptionScopes` array of paths to additionally look for its options in. For root scope, use empty string: `''`. Most core plugins also take options from root scope. + +* options.plugins[`plugin.id`] +* (options.[`...plugin.additionalOptionScopes`]) +* overrides[`config.type`].plugins[`plugin.id`] +* defaults.plugins[`plugin.id`] +* (defaults.[`...plugin.additionalOptionScopes`]) + +## Scriptable Options + +Scriptable options also accept a function which is called for each of the underlying data values and that takes the unique argument `context` representing contextual information (see [option context](options.md#option-context)). +A resolver is passed as second parameter, that can be used to access other options in the same context. + +:::tip Note + +The `context` argument should be validated in the scriptable function, because the function can be invoked in different contexts. The `type` field is a good candidate for this validation. + +::: + +Example: + +```javascript +color: function(context) { + const index = context.dataIndex; + const value = context.dataset.data[index]; + return value < 0 ? 'red' : // draw negative values in red + index % 2 ? 'blue' : // else, alternate values in blue and green + 'green'; +}, +borderColor: function(context, options) { + const color = options.color; // resolve the value of another scriptable option: 'red', 'blue' or 'green' + return Chart.helpers.color(color).lighten(0.2); +} +``` + +## Indexable Options + +Indexable options also accept an array in which each item corresponds to the element at the same index. Note that if there are less items than data, the items are looped over. In many cases, using a [function](#scriptable-options) is more appropriate if supported. + +Example: + +```javascript +color: [ + 'red', // color for data at index 0 + 'blue', // color for data at index 1 + 'green', // color for data at index 2 + 'black', // color for data at index 3 + //... +] +``` + +## Option Context + +The option context is used to give contextual information when resolving options and currently only applies to [scriptable options](#scriptable-options). +The object is preserved, so it can be used to store and pass information between calls. + +There are multiple levels of context objects: + +* `chart` + * `dataset` + * `data` + * `scale` + * `tick` + * `pointLabel` (only used in the radial linear scale) + * `tooltip` + +Each level inherits its parent(s) and any contextual information stored in the parent is available through the child. + +The context object contains the following properties: + +### chart + +* `chart`: the associated chart +* `type`: `'chart'` + +### dataset + +In addition to [chart](#chart) + +* `active`: true if an element is active (hovered) +* `dataset`: dataset at index `datasetIndex` +* `datasetIndex`: index of the current dataset +* `index`: same as `datasetIndex` +* `mode`: the update mode +* `type`: `'dataset'` + +### data + +In addition to [dataset](#dataset) + +* `active`: true if an element is active (hovered) +* `dataIndex`: index of the current data +* `parsed`: the parsed data values for the given `dataIndex` and `datasetIndex` +* `raw`: the raw data values for the given `dataIndex` and `datasetIndex` +* `element`: the element (point, arc, bar, etc.) for this data +* `index`: same as `dataIndex` +* `type`: `'data'` + +### scale + +In addition to [chart](#chart) + +* `scale`: the associated scale +* `type`: `'scale'` + +### tick + +In addition to [scale](#scale) + +* `tick`: the associated tick object +* `index`: tick index +* `type`: `'tick'` + +### pointLabel + +In addition to [scale](#scale) + +* `label`: the associated label value +* `index`: label index +* `type`: `'pointLabel'` + +### tooltip + +In addition to [chart](#chart) + +* `tooltip`: the tooltip object +* `tooltipItems`: the items the tooltip is displaying diff --git a/docs/general/padding.md b/docs/general/padding.md new file mode 100644 index 00000000000..0bea213bb60 --- /dev/null +++ b/docs/general/padding.md @@ -0,0 +1,66 @@ +# Padding + +Padding values in Chart options can be supplied in a couple of different formats. + +## Number + +If this value is a number, it is applied to all sides (left, top, right, bottom). + +For example, defining a 20px padding to all sides of the chart: + +```javascript +let chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + layout: { + padding: 20 + } + } +}); +``` + +## {top, left, bottom, right} object + +If this value is an object, the `left` property defines the left padding. Similarly, the `right`, `top` and `bottom` properties can also be specified. +Omitted properties default to `0`. + +Let's say you wanted to add 50px of padding to the left side of the chart canvas, you would do: + +```javascript +let chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + layout: { + padding: { + left: 50 + } + } + } +}); +``` + +## {x, y} object + +This is a shorthand for defining left/right and top/bottom to the same values. + +For example, 10px left / right and 4px top / bottom padding on a Radial Linear Axis [tick backdropPadding](../axes/radial/linear.md#linear-radial-axis-specific-tick-options): + +```javascript +let chart = new Chart(ctx, { + type: 'radar', + data: data, + options: { + scales: { + r: { + ticks: { + backdropPadding: { + x: 10, + y: 4 + } + } + } + } +}); +``` diff --git a/docs/general/performance.md b/docs/general/performance.md new file mode 100644 index 00000000000..1fedf4d62ba --- /dev/null +++ b/docs/general/performance.md @@ -0,0 +1,194 @@ +# Performance + +Chart.js charts are rendered on `canvas` elements, which makes rendering quite fast. For large datasets or performance sensitive applications, you may wish to consider the tips below. + +## Data structure and format + +### Parsing + +Provide prepared data in the internal format accepted by the dataset and scales, and set `parsing: false`. See [Data structures](data-structures.md) for more information. + +### Data normalization + +Chart.js is fastest if you provide data with indices that are unique, sorted, and consistent across datasets and provide the `normalized: true` option to let Chart.js know that you have done so. Even without this option, it can sometimes still be faster to provide sorted data. + +### Decimation + +Decimating your data will achieve the best results. When there is a lot of data to display on the graph, it doesn't make sense to show tens of thousands of data points on a graph that is only a few hundred pixels wide. + +The [decimation plugin](../configuration/decimation.md) can be used with line charts to decimate data before the chart is rendered. This will provide the best performance since it will reduce the memory needed to render the chart. + +Line charts are able to do [automatic data decimation during draw](#automatic-data-decimation-during-draw), when certain conditions are met. You should still consider decimating data yourself before passing it in for maximum performance since the automatic decimation occurs late in the chart life cycle. + +## Tick Calculation + +### Rotation + +[Specify a rotation value](../axes/cartesian/index.md#tick-configuration) by setting `minRotation` and `maxRotation` to the same value, which avoids the chart from having to automatically determine a value to use. + +### Sampling + +Set the [`ticks.sampleSize`](../axes/cartesian/index.md#tick-configuration) option. This will determine how large your labels are by looking at only a subset of them in order to render axes more quickly. This works best if there is not a large variance in the size of your labels. + +## Disable Animations + +If your charts have long render times, it is a good idea to disable animations. Doing so will mean that the chart needs to only be rendered once during an update instead of multiple times. This will have the effect of reducing CPU usage and improving general page performance. +Line charts use Path2D caching when animations are disabled and Path2D is available. + +To disable animations + +```javascript +new Chart(ctx, { + type: 'line', + data: data, + options: { + animation: false + } +}); +``` + +## Specify `min` and `max` for scales + +If you specify the `min` and `max`, the scale does not have to compute the range from the data. + +```javascript +new Chart(ctx, { + type: 'line', + data: data, + options: { + scales: { + x: { + type: 'time', + min: new Date('2019-01-01').valueOf(), + max: new Date('2019-12-31').valueOf() + }, + y: { + type: 'linear', + min: 0, + max: 100 + } + } + } +}); +``` + +## Parallel rendering with web workers + +As of 2023, modern browser have the ability to [transfer rendering control of a canvas](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/transferControlToOffscreen) to a web worker. Web workers can use the [OffscreenCanvas API](https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas) to render from a web worker onto canvases in the DOM. Chart.js is a canvas-based library and supports rendering in a web worker - just pass an OffscreenCanvas into the Chart constructor instead of a Canvas element. + +By moving all Chart.js calculations onto a separate thread, the main thread can be freed up for other uses. Some tips and tricks when using Chart.js in a web worker: + +* Transferring data between threads can be expensive, so ensure that your config and data objects are as small as possible. Try generating them on the worker side if you can (workers can make HTTP requests!) or passing them to your worker as ArrayBuffers, which can be transferred quickly from one thread to another. +* You can't transfer functions between threads, so if your config object includes functions you'll have to strip them out before transferring and then add them back later. +* You can't access the DOM from worker threads, so Chart.js plugins that use the DOM (including any mouse interactions) will likely not work. +* Ensure that you have a fallback if you support older browsers. +* Resizing the chart must be done manually. See an example in the worker code below. + +Example main thread code: + +```javascript +const config = {}; +const canvas = new HTMLCanvasElement(); +const offscreenCanvas = canvas.transferControlToOffscreen(); + +const worker = new Worker('worker.js'); +worker.postMessage({canvas: offscreenCanvas, config}, [offscreenCanvas]); +``` + +Example worker code, in `worker.js`: + +```javascript +onmessage = function(event) { + const {canvas, config} = event.data; + const chart = new Chart(canvas, config); + + // Resizing the chart must be done manually, since OffscreenCanvas does not include event listeners. + canvas.width = 100; + canvas.height = 100; + chart.resize(); +}; +``` + +## Line Charts + +### Leave Bézier curves disabled + +If you are drawing lines on your chart, disabling Bézier curves will improve render times since drawing a straight line is more performant than a Bézier curve. Bézier curves are disabled by default. + +### Automatic data decimation during draw + +Line element will automatically decimate data, when `tension`, `stepped`, and `borderDash` are left set to their default values (`false`, `0`, and `[]` respectively). This improves rendering speed by skipping drawing of invisible line segments. + +### Enable spanGaps + +If you have a lot of data points, it can be more performant to enable `spanGaps`. This disables segmentation of the line, which can be an unneeded step. + +To enable `spanGaps`: + +```javascript +new Chart(ctx, { + type: 'line', + data: { + datasets: [{ + spanGaps: true // enable for a single dataset + }] + }, + options: { + spanGaps: true // enable for all datasets + } +}); +``` + +### Disable Line Drawing + +If you have a lot of data points, it can be more performant to disable rendering of the line for a dataset and only draw points. Doing this means that there is less to draw on the canvas which will improve render performance. + +To disable lines: + +```javascript +new Chart(ctx, { + type: 'line', + data: { + datasets: [{ + showLine: false // disable for a single dataset + }] + }, + options: { + showLine: false // disable for all datasets + } +}); +``` + +### Disable Point Drawing + +If you have a lot of data points, it can be more performant to disable rendering of the points for a dataset and only draw lines. Doing this means that there is less to draw on the canvas which will improve render performance. + +To disable point drawing: + +```javascript +new Chart(ctx, { + type: 'line', + data: { + datasets: [{ + pointRadius: 0 // disable for a single dataset + }] + }, + options: { + datasets: { + line: { + pointRadius: 0 // disable for all `'line'` datasets + } + }, + elements: { + point: { + radius: 0 // default to disabled in all datasets + } + } + } +}); +``` + +## When transpiling with Babel, consider using `loose` mode + +Babel 7.9 changed the way classes are constructed. It is slow, unless used with `loose` mode. +[More information](https://github.com/babel/babel/issues/11356) diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md new file mode 100644 index 00000000000..6df4d1f57bf --- /dev/null +++ b/docs/getting-started/index.md @@ -0,0 +1,94 @@ +# Getting Started + +Let's get started with Chart.js! + +* **[Follow a step-by-step guide](./usage) to get up to speed with Chart.js** +* [Install Chart.js](./installation) from npm or a CDN +* [Integrate Chart.js](./integration) with bundlers, loaders, and front-end frameworks +* [Use Chart.js from Node.js](./using-from-node-js) + +Alternatively, see the example below or check [samples](../samples). + +## Create a Chart + +In this example, we create a bar chart for a single dataset and render it on an HTML page. Add this code snippet to your page: + +```html +
    + +
    + + + + +``` + +You should get a chart like this: + +![demo](./preview.png) + +Let's break this code down. + +First, we need to have a canvas in our page. It's recommended to give the chart its own container for [responsiveness](../configuration/responsive.md). + +```html +
    + +
    +``` + +Now that we have a canvas, we can include Chart.js from a CDN. + +```html + +``` + +Finally, we can create a chart. We add a script that acquires the `myChart` canvas element and instantiates `new Chart` with desired configuration: `bar` chart type, labels, data points, and options. + +```html + +``` + +You can see all the ways to use Chart.js in the [step-by-step guide](./usage). diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md new file mode 100644 index 00000000000..dbd7cd59159 --- /dev/null +++ b/docs/getting-started/installation.md @@ -0,0 +1,36 @@ +# Installation + +## npm + +[![npm](https://img.shields.io/npm/v/chart.js.svg?style=flat-square&maxAge=600)](https://npmjs.com/package/chart.js) +[![npm](https://img.shields.io/npm/dm/chart.js.svg?style=flat-square&maxAge=600)](https://npmjs.com/package/chart.js) + +```sh +npm install chart.js +``` + +## CDN + +### CDNJS + +[![cdnjs](https://img.shields.io/cdnjs/v/Chart.js.svg?style=flat-square&maxAge=600)](https://cdnjs.com/libraries/Chart.js) + +Chart.js built files are available on [CDNJS](https://cdnjs.com/): + + + +### jsDelivr + +[![jsdelivr](https://img.shields.io/npm/v/chart.js.svg?label=jsdelivr&style=flat-square&maxAge=600)](https://cdn.jsdelivr.net/npm/chart.js@latest/dist/) [![jsdelivr hits](https://data.jsdelivr.com/v1/package/npm/chart.js/badge)](https://www.jsdelivr.com/package/npm/chart.js) + +Chart.js built files are also available through [jsDelivr](https://www.jsdelivr.com/): + + + +## GitHub + +[![github](https://img.shields.io/github/release/chartjs/Chart.js.svg?style=flat-square&maxAge=600)](https://github.com/chartjs/Chart.js/releases/latest) + +You can download the latest version of [Chart.js on GitHub](https://github.com/chartjs/Chart.js/releases/latest). + +If you download or clone the repository, you must [build](../developers/contributing.md#building-and-testing) Chart.js to generate the dist files. Chart.js no longer comes with prebuilt release versions, so an alternative option to downloading the repo is **strongly** advised. diff --git a/docs/getting-started/integration.md b/docs/getting-started/integration.md new file mode 100644 index 00000000000..3d0c92c4530 --- /dev/null +++ b/docs/getting-started/integration.md @@ -0,0 +1,146 @@ +# Integration + +Chart.js can be integrated with plain JavaScript or with different module loaders. The examples below show how to load Chart.js in different systems. + +If you're using a front-end framework (e.g., React, Angular, or Vue), please see [available integrations](https://github.com/chartjs/awesome#integrations). + +## Script Tag + +```html + + +``` + +## Bundlers (Webpack, Rollup, etc.) + +Chart.js is tree-shakeable, so it is necessary to import and register the controllers, elements, scales and plugins you are going to use. + +### Quick start + +If you don't care about the bundle size, you can use the `auto` package ensuring all features are available: + +```javascript +import Chart from 'chart.js/auto'; +``` + +### Bundle optimization + +When optimizing the bundle, you need to import and register the components that are needed in your application. + +The options are categorized into controllers, elements, plugins, scales. You can pick and choose many of these, e.g. if you are not going to use tooltips, don't import and register the `Tooltip` plugin. But each type of chart has its own bare-minimum requirements (typically the type's controller, element(s) used by that controller and scale(s)): + +* Bar chart + * `BarController` + * `BarElement` + * Default scales: `CategoryScale` (x), `LinearScale` (y) +* Bubble chart + * `BubbleController` + * `PointElement` + * Default scales: `LinearScale` (x/y) +* Doughnut chart + * `DoughnutController` + * `ArcElement` + * Not using scales +* Line chart + * `LineController` + * `LineElement` + * `PointElement` + * Default scales: `CategoryScale` (x), `LinearScale` (y) +* Pie chart + * `PieController` + * `ArcElement` + * Not using scales +* PolarArea chart + * `PolarAreaController` + * `ArcElement` + * Default scale: `RadialLinearScale` (r) +* Radar chart + * `RadarController` + * `LineElement` + * `PointElement` + * Default scale: `RadialLinearScale` (r) +* Scatter chart + * `ScatterController` + * `PointElement` + * Default scales: `LinearScale` (x/y) + +Available plugins: + +* [`Decimation`](../configuration/decimation.md) +* `Filler` - used to fill area described by `LineElement`, see [Area charts](../charts/area.md) +* [`Legend`](../configuration/legend.md) +* [`SubTitle`](../configuration/subtitle.md) +* [`Title`](../configuration/title.md) +* [`Tooltip`](../configuration/tooltip.md) + +Available scales: + +* Cartesian scales (x/y) + * [`CategoryScale`](../axes/cartesian/category.md) + * [`LinearScale`](../axes/cartesian/linear.md) + * [`LogarithmicScale`](../axes/cartesian/logarithmic.md) + * [`TimeScale`](../axes/cartesian/time.md) + * [`TimeSeriesScale`](../axes/cartesian/timeseries.md) + +* Radial scales (r) + * [`RadialLinearScale`](../axes/radial/linear.md) + +### Helper functions + +If you want to use the helper functions, you will need to import these separately from the helpers package and use them as stand-alone functions. + +Example of [Converting Events to Data Values](../configuration/interactions.md#converting-events-to-data-values) using bundlers. + +```javascript +import Chart from 'chart.js/auto'; +import { getRelativePosition } from 'chart.js/helpers'; + +const chart = new Chart(ctx, { + type: 'line', + data: data, + options: { + onClick: (e) => { + const canvasPosition = getRelativePosition(e, chart); + + // Substitute the appropriate scale IDs + const dataX = chart.scales.x.getValueForPixel(canvasPosition.x); + const dataY = chart.scales.y.getValueForPixel(canvasPosition.y); + } + } +}); +``` + +## CommonJS + +Because Chart.js is an ESM library, in CommonJS modules you should use a dynamic `import`: + +```javascript +const { Chart } = await import('chart.js'); +``` + +## RequireJS + +**Important:** RequireJS can load only [AMD modules](https://requirejs.org/docs/whyamd.html), so be sure to require one of the UMD builds instead (i.e. `dist/chart.umd.min.js`). + +```javascript +require(['path/to/chartjs/dist/chart.umd.min.js'], function(Chart){ + const myChart = new Chart(ctx, {...}); +}); +``` + +:::tip Note + +In order to use the time scale, you need to make sure [one of the available date adapters](https://github.com/chartjs/awesome#adapters) and corresponding date library are fully loaded **after** requiring Chart.js. For this you can use nested requires: + +```javascript +require(['chartjs'], function(Chart) { + require(['moment'], function() { + require(['chartjs-adapter-moment'], function() { + new Chart(ctx, {...}); + }); + }); +}); +``` +::: \ No newline at end of file diff --git a/docs/getting-started/preview.png b/docs/getting-started/preview.png new file mode 100644 index 00000000000..c6392606c58 Binary files /dev/null and b/docs/getting-started/preview.png differ diff --git a/docs/getting-started/usage-1.png b/docs/getting-started/usage-1.png new file mode 100644 index 00000000000..6b9162ed89f Binary files /dev/null and b/docs/getting-started/usage-1.png differ diff --git a/docs/getting-started/usage-2.png b/docs/getting-started/usage-2.png new file mode 100644 index 00000000000..63abb3eeb50 Binary files /dev/null and b/docs/getting-started/usage-2.png differ diff --git a/docs/getting-started/usage-3.png b/docs/getting-started/usage-3.png new file mode 100644 index 00000000000..54d772c221b Binary files /dev/null and b/docs/getting-started/usage-3.png differ diff --git a/docs/getting-started/usage-4.png b/docs/getting-started/usage-4.png new file mode 100644 index 00000000000..da31d28706b Binary files /dev/null and b/docs/getting-started/usage-4.png differ diff --git a/docs/getting-started/usage-5.png b/docs/getting-started/usage-5.png new file mode 100644 index 00000000000..c7dfc16d45a Binary files /dev/null and b/docs/getting-started/usage-5.png differ diff --git a/docs/getting-started/usage-6.png b/docs/getting-started/usage-6.png new file mode 100644 index 00000000000..4b9cedd64e2 Binary files /dev/null and b/docs/getting-started/usage-6.png differ diff --git a/docs/getting-started/usage-7.png b/docs/getting-started/usage-7.png new file mode 100644 index 00000000000..b5e43c4c373 Binary files /dev/null and b/docs/getting-started/usage-7.png differ diff --git a/docs/getting-started/usage-8.png b/docs/getting-started/usage-8.png new file mode 100644 index 00000000000..e5f9fa39876 Binary files /dev/null and b/docs/getting-started/usage-8.png differ diff --git a/docs/getting-started/usage.md b/docs/getting-started/usage.md new file mode 100644 index 00000000000..d5add3b7ab8 --- /dev/null +++ b/docs/getting-started/usage.md @@ -0,0 +1,591 @@ +# Step-by-step guide + +Follow this guide to get familiar with all major concepts of Chart.js: chart types and elements, datasets, customization, plugins, components, and tree-shaking. Don't hesitate to follow the links in the text. + +We'll build a Chart.js data visualization with a couple of charts from scratch: + +![result](./usage-8.png) + +## Build a new application with Chart.js + +In a new folder, create the `package.json` file with the following contents: + +```json +{ + "name": "chartjs-example", + "version": "1.0.0", + "license": "MIT", + "scripts": { + "dev": "parcel src/index.html", + "build": "parcel build src/index.html" + }, + "devDependencies": { + "parcel": "^2.6.2" + }, + "dependencies": { + "@cubejs-client/core": "^0.31.0", + "chart.js": "^4.0.0" + } +} +``` + +Modern front-end applications often use JavaScript module bundlers, so we’ve picked [Parcel](https://parceljs.org) as a nice zero-configuration build tool. We’re also installing Chart.js v4 and a JavaScript client for [Cube](https://cube.dev/?ref=eco-chartjs), an open-source API for data apps we’ll use to fetch real-world data (more on that later). + +Run `npm install`, `yarn install`, or `pnpm install` to install the dependencies, then create the `src` folder. Inside that folder, we’ll need a very simple `index.html` file: + +```html + + + + Chart.js example + + + +
    + + + + + +``` + +As you can see, Chart.js requires minimal markup: a `canvas` tag with an `id` by which we’ll reference the chart later. By default, Chart.js charts are [responsive](../configuration/responsive.md) and take the whole enclosing container. So, we set the width of the `div` to control chart width. + +Lastly, let’s create the `src/acquisitions.js` file with the following contents: + +```jsx +import Chart from 'chart.js/auto' + +(async function() { + const data = [ + { year: 2010, count: 10 }, + { year: 2011, count: 20 }, + { year: 2012, count: 15 }, + { year: 2013, count: 25 }, + { year: 2014, count: 22 }, + { year: 2015, count: 30 }, + { year: 2016, count: 28 }, + ]; + + new Chart( + document.getElementById('acquisitions'), + { + type: 'bar', + data: { + labels: data.map(row => row.year), + datasets: [ + { + label: 'Acquisitions by year', + data: data.map(row => row.count) + } + ] + } + } + ); +})(); +``` + +Let’s walk through this code: + +- We import `Chart`, the main Chart.js class, from the special `chart.js/auto` path. It loads [all available Chart.js components](./integration) (which is very convenient) but disallows tree-shaking. We’ll address that later. +- We instantiate a new `Chart` instance and provide two arguments: the canvas element where the chart would be rendered and the options object. +- We just need to provide a chart type (`bar`) and provide `data` which consists of `labels` (often, numeric or textual descriptions of data points) and an array of `datasets` (Chart.js supports multiple datasets for most chart types). Each dataset is designated with a `label` and contains an array of data points. +- For now, we only have a few entries of dummy data. So, we extract `year` and `count` properties to produce the arrays of `labels` and data points within the only dataset. + +Time to run the example with `npm run dev`, `yarn dev`, or `pnpm dev` and navigate to [localhost:1234](http://localhost:1234) in your web browser: + +![result](./usage-1.png) + +With just a few lines of code, we’ve got a chart with a lot of features: a [legend](../configuration/legend.md), [grid lines](../samples/scale-options/grid.md), [ticks](../samples/scale-options/ticks.md), and [tooltips](../configuration/tooltip.md) shown on hover. Refresh the web page a few times to see that the chart is also [animated](../configuration/animations.md#animations). Try clicking on the “Acquisitions by year” label to see that you’re also able to toggle datasets visibility (especially useful when you have multiple datasets). + +### Simple customizations + +Let’s see how Chart.js charts can be customized. First, let’s turn off the animations so the chart appears instantly. Second, let’s hide the legend and tooltips since we have only one dataset and pretty trivial data. + +Replace the `new Chart(...);` invocation in `src/acquisitions.js` with the following snippet: + +```jsx + new Chart( + document.getElementById('acquisitions'), + { + type: 'bar', + options: { + animation: false, + plugins: { + legend: { + display: false + }, + tooltip: { + enabled: false + } + } + }, + data: { + labels: data.map(row => row.year), + datasets: [ + { + label: 'Acquisitions by year', + data: data.map(row => row.count) + } + ] + } + } + ); +``` + +As you can see, we’ve added the `options` property to the second argument—that’s how you can specify all kinds of customization options for Chart.js. The [animation is disabled](../configuration/animations.md#disabling-animation) with a boolean flag provided via `animation`. Most chart-wide options (e.g., [responsiveness](../configuration/responsive.md) or [device pixel ratio](../configuration/device-pixel-ratio.md)) are configured like this. + +The legend and tooltips are hidden with boolean flags provided under the respective sections in `plugins`. Note that some of Chart.js features are extracted into plugins: self-contained, separate pieces of code. A few of them are available as a part of [Chart.js distribution](https://github.com/chartjs/Chart.js/tree/master/src/plugins), other plugins are maintained independently and can be located in the [awesome list](https://github.com/chartjs/awesome) of plugins, framework integrations, and additional chart types. + +You should be able to see the updated minimalistic chart in your browser. + +### Real-world data + +With hardcoded, limited-size, unrealistic data, it’s hard to show the full potential of Chart.js. Let’s quickly connect to a data API to make our example application closer to a production use case. + +Let’s create the `src/api.js` file with the following contents: + +```jsx +import { CubejsApi } from '@cubejs-client/core'; + +const apiUrl = 'https://heavy-lansford.gcp-us-central1.cubecloudapp.dev/cubejs-api/v1'; +const cubeToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjEwMDAwMDAwMDAsImV4cCI6NTAwMDAwMDAwMH0.OHZOpOBVKr-sCwn8sbZ5UFsqI3uCs6e4omT7P6WVMFw'; + +const cubeApi = new CubejsApi(cubeToken, { apiUrl }); + +export async function getAquisitionsByYear() { + const acquisitionsByYearQuery = { + dimensions: [ + 'Artworks.yearAcquired', + ], + measures: [ + 'Artworks.count' + ], + filters: [ { + member: 'Artworks.yearAcquired', + operator: 'set' + } ], + order: { + 'Artworks.yearAcquired': 'asc' + } + }; + + const resultSet = await cubeApi.load(acquisitionsByYearQuery); + + return resultSet.tablePivot().map(row => ({ + year: parseInt(row['Artworks.yearAcquired']), + count: parseInt(row['Artworks.count']) + })); +} + +export async function getDimensions() { + const dimensionsQuery = { + dimensions: [ + 'Artworks.widthCm', + 'Artworks.heightCm' + ], + measures: [ + 'Artworks.count' + ], + filters: [ + { + member: 'Artworks.classification', + operator: 'equals', + values: [ 'Painting' ] + }, + { + member: 'Artworks.widthCm', + operator: 'set' + }, + { + member: 'Artworks.widthCm', + operator: 'lt', + values: [ '500' ] + }, + { + member: 'Artworks.heightCm', + operator: 'set' + }, + { + member: 'Artworks.heightCm', + operator: 'lt', + values: [ '500' ] + } + ] + }; + + const resultSet = await cubeApi.load(dimensionsQuery); + + return resultSet.tablePivot().map(row => ({ + width: parseInt(row['Artworks.widthCm']), + height: parseInt(row['Artworks.heightCm']), + count: parseInt(row['Artworks.count']) + })); +} +``` + +Let’s see what’s happening there: + +- We `import` the JavaScript client library for [Cube](https://cube.dev/?ref=eco-chartjs), an open-source API for data apps, configure it with the API URL (`apiUrl`) and the authentication token (`cubeToken`), and finally instantiate the client (`cubeApi`). +- Cube API is hosted in [Cube Cloud](https://cube.dev/cloud/?ref=eco-chartjs) and connected to a database with a [public dataset](https://github.com/MuseumofModernArt/collection) of ~140,000 records representing all of the artworks in the collection of the [Museum of Modern Art](https://www.moma.org) in New York, USA. Certainly, a more real-world dataset than what we’ve got now. +- We define a couple of asynchronous functions to fetch data from the API: `getAquisitionsByYear` and `getDimensions`. The first one returns the number of artworks by the year of acquisition, the other returns the number of artworks for every width-height pair (we’ll need it for another chart). +- Let’s take a look at `getAquisitionsByYear`. First, we create a declarative, JSON-based query in the `acquisitionsByYearQuery` variable. As you can see, we specify that for every `yearAcquired` we’d like to get the `count` of artworks; `yearAcquired` has to be set (i.e., not undefined); the result set would be sorted by `yearAcquired` in the ascending order. +- Second, we fetch the `resultSet` by calling `cubeApi.load` and map it to an array of objects with desired `year` and `count` properties. + +Now, let’s deliver the real-world data to our chart. Please apply a couple of changes to `src/acquisitions.js`: add an import and replace the definition of the `data` variable. + +```jsx +import { getAquisitionsByYear } from './api' + +// ... + +const data = await getAquisitionsByYear(); +``` + +Done! Now, our chart with real-world data looks like this. Looks like something interesting happened in 1964, 1968, and 2008! + +![result](./usage-2.png) + +We’re done with the bar chart. Let’s try another Chart.js chart type. + +### Further customizations + +Chart.js supports many common chart types. + +For instance, [Bubble chart](../charts/bubble.md) allows to display three dimensions of data at the same time: locations on `x` and `y` axes represent two dimensions, and the third dimension is represented by the size of the individual bubbles. + +To create the chart, stop the already running application, then go to `src/index.html`, and uncomment the following two lines: + +```html +

    + + +``` + +Then, create the `src/dimensions.js` file with the following contents: + +```jsx +import Chart from 'chart.js/auto' +import { getDimensions } from './api' + +(async function() { + const data = await getDimensions(); + + new Chart( + document.getElementById('dimensions'), + { + type: 'bubble', + data: { + labels: data.map(x => x.year), + datasets: [ + { + label: 'Dimensions', + data: data.map(row => ({ + x: row.width, + y: row.height, + r: row.count + })) + } + ] + } + } + ); +})(); +``` + +Probably, everything is pretty straightforward there: we get data from the API and render a new chart with the `bubble` type, passing three dimensions of data as `x`, `y`, and `r` (radius) properties. + +Now, reset caches with `rm -rf .parcel-cache` and start the application again with `npm run dev`, `yarn dev`, or `pnpm dev`. We can review the new chart now: + +![result](./usage-3.png) + +Well, it doesn’t look pretty. + +First of all, the chart is not square. Artworks’ width and height are equally important so we’d like to make the chart width equal to its height as well. By default, Chart.js charts have the [aspect ratio](../configuration/responsive.md) of either 1 (for all radial charts, e.g., a doughnut chart) or 2 (for all the rest). Let’s modify the aspect ratio for our chart: + +```jsx +// ... + + new Chart( + document.getElementById('dimensions'), + { + type: 'bubble', + options: { + aspectRatio: 1, + }, + +// ... +``` + +Looks much better now: + +![result](./usage-4.png) + +However, it’s still not ideal. The horizontal axis spans from 0 to 500 while the vertical axis spans from 0 to 450. By default, Chart.js automatically adjusts the range (minimum and maximum values) of the axes to the values provided in the dataset, so the chart “fits” your data. Apparently, MoMa collection doesn’t have artworks in the range of 450 to 500 cm in height. Let’s modify the [axes configuration](../axes/) for our chart to account for that: + +```jsx +// ... + + new Chart( + document.getElementById('dimensions'), + { + type: 'bubble', + options: { + aspectRatio: 1, + scales: { + x: { + max: 500 + }, + y: { + max: 500 + } + } + }, + +// ... +``` + +Great! Behold the updated chart: + +![result](./usage-5.png) + +However, there’s one more nitpick: what are these numbers? It’s not very obvious that the units are centimetres. Let’s apply a [custom tick format](../axes/labelling.md#creating-custom-tick-formats) to both axes to make things clear. We’ll provide a callback function that would be called to format each tick value. Here’s the updated axes configuration: + +```jsx +// ... + + new Chart( + document.getElementById('dimensions'), + { + type: 'bubble', + options: { + aspectRatio: 1, + scales: { + x: { + max: 500, + ticks: { + callback: value => `${value / 100} m` + } + }, + y: { + max: 500, + ticks: { + callback: value => `${value / 100} m` + } + } + } + }, + +// ... +``` + +Perfect, now we have proper units on both axes: + +![result](./usage-6.png) + +### Multiple datasets + +Chart.js plots each dataset independently and allows to apply custom styles to them. + +Take a look at the chart: there’s a visible “line” of bubbles with equal `x` and `y` coordinates representing square artworks. It would be cool to put these bubbles in their own dataset and paint them differently. Also, we can separate “taller” artworks from “wider” ones and paint them differently, too. + +Here’s how we can do that. Replace the `datasets` with the following code: + +```jsx +// ... + + datasets: [ + { + label: 'width = height', + data: data + .filter(row => row.width === row.height) + .map(row => ({ + x: row.width, + y: row.height, + r: row.count + })) + }, + { + label: 'width > height', + data: data + .filter(row => row.width > row.height) + .map(row => ({ + x: row.width, + y: row.height, + r: row.count + })) + }, + { + label: 'width < height', + data: data + .filter(row => row.width < row.height) + .map(row => ({ + x: row.width, + y: row.height, + r: row.count + })) + } + ] + +// .. +``` + +As you can see, we define three datasets with different labels. Each dataset gets its own slice of data extracted with `filter`. Now they are visually distinct and, as you already know, you can toggle their visibility independently. + +![result](./usage-7.png) + +Here we rely on the default color palette. However, keep in mind every chart type supports a lot of [dataset options](../charts/bubble.md#dataset-properties) that you can feel free to customize. + +### Plugins + +Another—and very powerful!—way to customize Chart.js charts is to use plugins. You can find some in the [plugin directory](https://github.com/chartjs/awesome#plugins) or create your own, ad-hoc ones. In Chart.js ecosystem, it’s idiomatic and expected to fine tune charts with plugins. For example, you can customize [canvas background](../configuration/canvas-background.md) or [add a border](../samples/plugins/chart-area-border.md) to it with simple ad-hoc plugins. Let’s try the latter. + +Plugins have an [extensive API](../developers/plugins.md) but, in a nutshell, a plugin is defined as an object with a `name` and one or more callback functions defined in the extension points. Insert the following snippet before and in place of the `new Chart(...);` invocation in `src/dimensions.js`: + +```jsx +// ... + + const chartAreaBorder = { + id: 'chartAreaBorder', + + beforeDraw(chart, args, options) { + const { ctx, chartArea: { left, top, width, height } } = chart; + + ctx.save(); + ctx.strokeStyle = options.borderColor; + ctx.lineWidth = options.borderWidth; + ctx.setLineDash(options.borderDash || []); + ctx.lineDashOffset = options.borderDashOffset; + ctx.strokeRect(left, top, width, height); + ctx.restore(); + } + }; + + new Chart( + document.getElementById('dimensions'), + { + type: 'bubble', + plugins: [ chartAreaBorder ], + options: { + plugins: { + chartAreaBorder: { + borderColor: 'red', + borderWidth: 2, + borderDash: [ 5, 5 ], + borderDashOffset: 2, + } + }, + aspectRatio: 1, + +// ... +``` + +As you can see, in this `chartAreaBorder` plugin, we acquire the canvas context, save its current state, apply styles, draw a rectangular shape around the chart area, and restore the canvas state. We’re also passing the plugin in `plugins` so it’s only applied to this particular chart. We also pass the plugin options in `options.plugins.chartAreaBorder`; we could surely hardcode them in the plugin source code but it’s much more reusable this way. + +Our bubble chart looks fancier now: + +![result](./usage-8.png) + +### Tree-shaking + +In production, we strive to ship as little code as possible, so the end users can load our data applications faster and have better experience. For that, we’ll need to apply [tree-shaking](https://cube.dev/blog/how-to-build-tree-shakeable-javascript-libraries/?ref=eco-chartjs) which is fancy term for removing unused code from the JavaScript bundle. + +Chart.js fully supports tree-shaking with its component design. You can register all Chart.js components at once (which is convenient when you’re prototyping) and get them bundled with your application. Or, you can register only necessary components and get a minimal bundle, much less in size. + +Let’s inspect our example application. What’s the bundle size? You can stop the application and run `npm run build`, or `yarn build`, or `pnpm build`. In a few moments, you’ll get something like this: + +```bash +% yarn build +yarn run v1.22.17 +$ parcel build src/index.html +✨ Built in 88ms + +dist/index.html 381 B 164ms +dist/index.74a47636.js 265.48 KB 1.25s +dist/index.ba0c2e17.js 881 B 63ms +✨ Done in 0.51s. +``` + +We can see that Chart.js and other dependencies were bundled together in a single 265 KB file. + +To reduce the bundle size, we’ll need to apply a couple of changes to `src/acquisitions.js` and `src/dimensions.js`. First, we’ll need to remove the following import statement from both files: `import Chart from 'chart.js/auto'`. + +Instead, let’s load only necessary components and “register” them with Chart.js using `Chart.register(...)`. Here’s what we need in `src/acquisitions.js`: + +```jsx +import { + Chart, + Colors, + BarController, + CategoryScale, + LinearScale, + BarElement, + Legend +} from 'chart.js' + +Chart.register( + Colors, + BarController, + BarElement, + CategoryScale, + LinearScale, + Legend +); +``` + +And here’s the snippet for `src/dimensions.js`: + +```jsx +import { + Chart, + Colors, + BubbleController, + CategoryScale, + LinearScale, + PointElement, + Legend +} from 'chart.js' + +Chart.register( + Colors, + BubbleController, + PointElement, + CategoryScale, + LinearScale, + Legend +); +``` + +You can see that, in addition to the `Chart` class, we’re also loading a controller for the chart type, scales, and other chart elements (e.g., bars or points). You can look all available components up in the [documentation](./integration.md#bundle-optimization). + +Alternatively, you can follow Chart.js advice in the console. For example, if you forget to import `BarController` for your bar chart, you’ll see the following message in the browser console: + +``` +Unhandled Promise Rejection: Error: "bar" is not a registered controller. +``` + +Remember to carefully check for imports from `chart.js/auto` when preparing your application for production. It takes only one import like this to effectively disable tree-shaking. + +Now, let’s inspect our application once again. Run `yarn build` and you’ll get something like this: + +```bash +% yarn build +yarn run v1.22.17 +$ parcel build src/index.html +✨ Built in 88ms + +dist/index.html 381 B 176ms +dist/index.5888047.js 208.66 KB 1.23s +dist/index.dcb2e865.js 932 B 58ms +✨ Done in 0.51s. +``` + +By importing and registering only select components, we’ve removed more than 56 KB of unnecessary code. Given that other dependencies take ~50 KB in the bundle, tree-shaking helps remove ~25% of Chart.js code from the bundle for our example application. + +## Next steps + +Now you’re familiar with all major concepts of Chart.js: chart types and elements, datasets, customization, plugins, components, and tree-shaking. + +Feel free to review many [examples of charts](../samples/information.md) in the documentation and check the [awesome list](https://github.com/chartjs/awesome) of Chart.js plugins and additional chart types as well as [framework integrations](https://github.com/chartjs/awesome#integrations) (e.g., React, Vue, Svelte, etc.). Also, don’t hesitate to join [Chart.js Discord](https://discord.gg/HxEguTK6av) and follow [Chart.js on Twitter](https://twitter.com/chartjs). + +Have fun and good luck building with Chart.js! \ No newline at end of file diff --git a/docs/getting-started/using-from-node-js.md b/docs/getting-started/using-from-node-js.md new file mode 100644 index 00000000000..90d8959a7be --- /dev/null +++ b/docs/getting-started/using-from-node-js.md @@ -0,0 +1,38 @@ +# Using from Node.js + +You can use Chart.js in Node.js for server-side generation of plots with help from an NPM package such as [node-canvas](https://github.com/Automattic/node-canvas) or [skia-canvas](https://skia-canvas.org/). + +Sample usage: + +```js +import {CategoryScale, Chart, LinearScale, LineController, LineElement, PointElement} from 'chart.js'; +import {Canvas} from 'skia-canvas'; +import fsp from 'node:fs/promises'; + +Chart.register([ + CategoryScale, + LineController, + LineElement, + LinearScale, + PointElement +]); + +const canvas = new Canvas(400, 300); +const chart = new Chart( + canvas, // TypeScript needs "as any" here + { + type: 'line', + data: { + labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], + datasets: [{ + label: '# of Votes', + data: [12, 19, 3, 5, 2, 3], + borderColor: 'red' + }] + } + } +); +const pngBuffer = await canvas.toBuffer('png', {matte: 'white'}); +await fsp.writeFile('output.png', pngBuffer); +chart.destroy(); +``` diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000000..437185f67d8 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,47 @@ +# Chart.js + +Welcome to Chart.js! + +* **[Get started with Chart.js](./getting-started/) — best if you're new to Chart.js** +* Migrate from [Chart.js v3](./migration/v4-migration.md) or [Chart.js v2](./migration/v3-migration.md) +* Join the community on [Discord](https://discord.gg/HxEguTK6av) and [Twitter](https://twitter.com/chartjs) +* Post a question tagged with `chart.js` on [Stack Overflow](https://stackoverflow.com/questions/tagged/chart.js) +* [Contribute to Chart.js](./developers/contributing.md) + +## Why Chart.js + +Among [many charting libraries](https://awesome.cube.dev/?tools=charts&ref=eco-chartjs) for JavaScript application developers, Chart.js is currently the most popular one according to [GitHub stars](https://github.com/chartjs/Chart.js) (~60,000) and [npm downloads](https://www.npmjs.com/package/chart.js) (~2,400,000 weekly). + +Chart.js was created and [announced](https://twitter.com/_nnnick/status/313599208387137536) in 2013 but has come a long way since then. It’s open-source, licensed under the very permissive [MIT license](https://github.com/chartjs/Chart.js/blob/master/LICENSE.md), and maintained by an active community. + +### Features + +Chart.js provides a set of frequently used chart types, plugins, and customization options. In addition to a reasonable set of [built-in chart types](./charts/area.md), you can use additional community-maintained [chart types](https://github.com/chartjs/awesome#charts). On top of that, it’s possible to combine several chart types into a [mixed chart](./charts/mixed.md) (essentially, blending multiple chart types into one on the same canvas). + +Chart.js is highly customizable with [custom plugins](https://github.com/chartjs/awesome#plugins) to create annotations, zoom, or drag-and-drop functionalities to name a few things. + +### Defaults + +Chart.js comes with a sound default configuration, making it very easy to start with and get an app that is ready for production. Chances are you will get a very appealing chart even if you don’t specify any options at all. For instance, Chart.js has animations turned on by default, so you can instantly bring attention to the story you’re telling with the data. + +### Integrations + +Chart.js comes with built-in TypeScript typings and is compatible with all popular [JavaScript frameworks](https://github.com/chartjs/awesome#javascript) including [React](https://github.com/reactchartjs/react-chartjs-2), [Vue](https://github.com/apertureless/vue-chartjs/), [Svelte](https://github.com/SauravKanchan/svelte-chartjs), and [Angular](https://github.com/valor-software/ng2-charts). You can use Chart.js directly or leverage well-maintained wrapper packages that allow for a more native integration with your frameworks of choice. + +### Developer experience + +Chart.js has very thorough documentation (yes, you're reading it), [API reference](./api/), and [examples](./samples/information.md). Maintainers and community members eagerly engage in conversations on [Discord](https://discord.gg/HxEguTK6av), [GitHub Discussions](https://github.com/chartjs/Chart.js/discussions), and [Stack Overflow](https://stackoverflow.com/questions/tagged/chart.js) where more than 11,000 questions are tagged with `chart.js`. + +### Canvas rendering + +Chart.js renders chart elements on an HTML5 canvas unlike several others, mostly D3.js-based, charting libraries that render as SVG. Canvas rendering makes Chart.js very performant, especially for large datasets and complex visualizations that would otherwise require thousands of SVG nodes in the DOM tree. At the same time, canvas rendering disallows CSS styling, so you will have to use built-in options for that, or create a custom plugin or chart type to render everything to your liking. + +### Performance + +Chart.js is very well suited for large datasets. Such datasets can be efficiently ingested using the internal format, so you can skip data [parsing](./general/performance.md#parsing) and [normalization](./general/performance.md#data-normalization). Alternatively, [data decimation](./configuration/decimation.md) can be configured to sample the dataset and reduce its size before rendering. + +In the end, the canvas rendering that Chart.js uses reduces the toll on your DOM tree in comparison to SVG rendering. Also, tree-shaking support allows you to include minimal parts of Chart.js code in your bundle, reducing bundle size and page load time. + +### Community + +Chart.js is [actively developed](https://github.com/chartjs/Chart.js/pulls?q=is%3Apr+is%3Aclosed) and maintained by the community. With minor [releases](https://github.com/chartjs/Chart.js/releases) on an approximately bi-monthly basis and major releases with breaking changes every couple of years, Chart.js keeps the balance between adding new features and making it a hassle to keep up with them. diff --git a/docs/migration/v3-migration.md b/docs/migration/v3-migration.md new file mode 100644 index 00000000000..d9d52b2ad8d --- /dev/null +++ b/docs/migration/v3-migration.md @@ -0,0 +1,531 @@ +# 3.x Migration Guide + +Chart.js 3.0 introduces a number of breaking changes. Chart.js 2.0 was released in April 2016. In the years since then, as Chart.js has grown in popularity and feature set, we've learned some lessons about how to better create a charting library. In order to improve performance, offer new features, and improve maintainability, it was necessary to break backwards compatibility, but we aimed to do so only when worth the benefit. Some major highlights of v3 include: + +* Large [performance](../general/performance.md) improvements including the ability to skip data parsing and render charts in parallel via webworkers +* Additional configurability and scriptable options with better defaults +* Completely rewritten animation system +* Rewritten filler plugin with numerous bug fixes +* Documentation migrated from GitBook to Vuepress +* API documentation generated and verified by TypeDoc +* No more CSS injection +* Tons of bug fixes +* Tree shaking + +## End user migration + +### Setup and installation + +* Distributed files are now in lower case. For example: `dist/chart.js`. +* Chart.js is no longer providing the `Chart.bundle.js` and `Chart.bundle.min.js`. Please see the [installation](../getting-started/installation.md) and [integration](../getting-started/integration.md) docs for details on the recommended way to setup Chart.js if you were using these builds. +* `moment` is no longer specified as an npm dependency. If you are using the `time` or `timeseries` scales, you must include one of [the available adapters](https://github.com/chartjs/awesome#adapters) and corresponding date library. You no longer need to exclude moment from your build. +* The `Chart` constructor will throw an error if the canvas/context provided is already in use +* Chart.js 3 is tree-shakeable. So if you are using it as an `npm` module in a project and want to make use of this feature, you need to import and register the controllers, elements, scales and plugins you want to use, for a list of all the available items to import see [integration](../getting-started/integration.md#bundlers-webpack-rollup-etc). You will not have to call `register` if importing Chart.js via a `script` tag or from the [`auto`](../getting-started/integration.md#bundlers-webpack-rollup-etc) register path as an `npm` module, in this case you will not get the tree shaking benefits. Here is an example of registering components: + +```javascript +import { Chart, LineController, LineElement, PointElement, LinearScale, Title } from `chart.js` + +Chart.register(LineController, LineElement, PointElement, LinearScale, Title); + +const chart = new Chart(ctx, { + type: 'line', + // data: ... + options: { + plugins: { + title: { + display: true, + text: 'Chart Title' + } + }, + scales: { + x: { + type: 'linear' + }, + y: { + type: 'linear' + } + } + } +}) +``` + +### Chart types + +* `horizontalBar` chart type was removed. Horizontal bar charts can be configured using the new [`indexAxis`](../charts/bar.md#horizontal-bar-chart) option + +### Options + +A number of changes were made to the configuration options passed to the `Chart` constructor. Those changes are documented below. + +#### Generic changes + +* Indexable options are now looping. `backgroundColor: ['red', 'green']` will result in alternating `'red'` / `'green'` if there are more than 2 data points. +* The input properties of object data can now be freely specified, see [data structures](../general/data-structures.md) for details. +* Most options are resolved utilizing proxies, instead of merging with defaults. In addition to easily enabling different resolution routes for different contexts, it allows using other resolved options in scriptable options. + * Options are by default scriptable and indexable, unless disabled for some reason. + * Scriptable options receive a option resolver as second parameter for accessing other options in same context. + * Resolution falls to upper scopes, if no match is found earlier. See [options](../general/options.md) for details. + +#### Specific changes + +* `elements.rectangle` is now `elements.bar` +* `hover.animationDuration` is now configured in `animation.active.duration` +* `responsiveAnimationDuration` is now configured in `animation.resize.duration` +* Polar area `elements.arc.angle` is now configured in degrees instead of radians. +* Polar area `startAngle` option is now consistent with `Radar`, 0 is at top and value is in degrees. Default is changed from `-½π` to `0`. +* Doughnut `rotation` option is now in degrees and 0 is at top. Default is changed from `-½π` to `0`. +* Doughnut `circumference` option is now in degrees. Default is changed from `2π` to `360`. +* Doughnut `cutoutPercentage` was renamed to `cutout`and accepts pixels as number and percent as string ending with `%`. +* `scale` option was removed in favor of `options.scales.r` (or any other scale id, with `axis: 'r'`) +* `scales.[x/y]Axes` arrays were removed. Scales are now configured directly to `options.scales` object with the object key being the scale Id. +* `scales.[x/y]Axes.barPercentage` was moved to dataset option `barPercentage` +* `scales.[x/y]Axes.barThickness` was moved to dataset option `barThickness` +* `scales.[x/y]Axes.categoryPercentage` was moved to dataset option `categoryPercentage` +* `scales.[x/y]Axes.maxBarThickness` was moved to dataset option `maxBarThickness` +* `scales.[x/y]Axes.minBarLength` was moved to dataset option `minBarLength` +* `scales.[x/y]Axes.scaleLabel` was renamed to `scales[id].title` +* `scales.[x/y]Axes.scaleLabel.labelString` was renamed to `scales[id].title.text` +* `scales.[x/y]Axes.ticks.beginAtZero` was renamed to `scales[id].beginAtZero` +* `scales.[x/y]Axes.ticks.max` was renamed to `scales[id].max` +* `scales.[x/y]Axes.ticks.min` was renamed to `scales[id].min` +* `scales.[x/y]Axes.ticks.reverse` was renamed to `scales[id].reverse` +* `scales.[x/y]Axes.ticks.suggestedMax` was renamed to `scales[id].suggestedMax` +* `scales.[x/y]Axes.ticks.suggestedMin` was renamed to `scales[id].suggestedMin` +* `scales.[x/y]Axes.ticks.unitStepSize` was removed. Use `scales[id].ticks.stepSize` +* `scales.[x/y]Axes.ticks.userCallback` was renamed to `scales[id].ticks.callback` +* `scales.[x/y]Axes.time.format` was renamed to `scales[id].time.parser` +* `scales.[x/y]Axes.time.max` was renamed to `scales[id].max` +* `scales.[x/y]Axes.time.min` was renamed to `scales[id].min` +* `scales.[x/y]Axes.zeroLine*` options of axes were removed. Use scriptable scale options instead. +* The dataset option `steppedLine` was removed. Use `stepped` +* The chart option `showLines` was renamed to `showLine` to match the dataset option. +* The chart option `startAngle` was moved to `radial` scale options. +* To override the platform class used in a chart instance, pass `platform: PlatformClass` in the config object. Note that the class should be passed, not an instance of the class. +* `aspectRatio` defaults to 1 for doughnut, pie, polarArea, and radar charts +* `TimeScale` does not read `t` from object data by default anymore. The default property is `x` or `y`, depending on the orientation. See [data structures](../general/data-structures.md) for details on how to change the default. +* `tooltips` namespace was renamed to `tooltip` to match the plugin name +* `legend`, `title` and `tooltip` namespaces were moved from `options` to `options.plugins`. +* `tooltips.custom` was renamed to `plugins.tooltip.external` + +#### Defaults + +* `global` namespace was removed from `defaults`. So `Chart.defaults.global` is now `Chart.defaults` +* Dataset controller defaults were relocate to `overrides`. For example `Chart.defaults.line` is now `Chart.overrides.line` +* `default` prefix was removed from defaults. For example `Chart.defaults.global.defaultColor` is now `Chart.defaults.color` +* `defaultColor` was split to `color`, `borderColor` and `backgroundColor` +* `defaultFontColor` was renamed to `color` +* `defaultFontFamily` was renamed to `font.family` +* `defaultFontSize` was renamed to `font.size` +* `defaultFontStyle` was renamed to `font.style` +* `defaultLineHeight` was renamed to `font.lineHeight` +* Horizontal Bar default tooltip mode was changed from `'index'` to `'nearest'` to match vertical bar charts +* `legend`, `title` and `tooltip` namespaces were moved from `Chart.defaults` to `Chart.defaults.plugins`. +* `elements.line.fill` default changed from `true` to `false`. +* Line charts no longer override the default `interaction` mode. Default is changed from `'index'` to `'nearest'`. + +#### Scales + +The configuration options for scales is the largest change in v3. The `xAxes` and `yAxes` arrays were removed and axis options are individual scales now keyed by scale ID. + +The v2 configuration below is shown with it's new v3 configuration + +```javascript +options: { + scales: { + xAxes: [{ + id: 'x', + type: 'time', + display: true, + title: { + display: true, + text: 'Date' + }, + ticks: { + major: { + enabled: true + }, + font: function(context) { + if (context.tick && context.tick.major) { + return { + weight: 'bold', + color: '#FF0000' + }; + } + } + } + }], + yAxes: [{ + id: 'y', + display: true, + title: { + display: true, + text: 'value' + } + }] + } +} +``` + +And now, in v3: + +```javascript +options: { + scales: { + x: { + type: 'time', + display: true, + title: { + display: true, + text: 'Date' + }, + ticks: { + major: { + enabled: true + }, + color: (context) => context.tick && context.tick.major && '#FF0000', + font: function(context) { + if (context.tick && context.tick.major) { + return { + weight: 'bold' + }; + } + } + } + }, + y: { + display: true, + title: { + display: true, + text: 'value' + } + } + } +} +``` + +* The time scale option `distribution: 'series'` was removed and a new scale type `timeseries` was introduced in its place +* In the time scale, `autoSkip` is now enabled by default for consistency with the other scales + +#### Animations + +Animation system was completely rewritten in Chart.js v3. Each property can now be animated separately. Please see [animations](../configuration/animations.md) docs for details. + +#### Customizability + +* `custom` attribute of elements was removed. Please use scriptable options +* The `hover` property of scriptable options `context` object was renamed to `active` to align it with the datalabels plugin. + +#### Interactions + +* To allow DRY configuration, a root options scope for common interaction options was added. `options.hover` and `options.plugins.tooltip` now both extend from `options.interaction`. Defaults are defined at `defaults.interaction` level, so by default hover and tooltip interactions share the same mode etc. +* `interactions` are now limited to the chart area + allowed overflow +* `{mode: 'label'}` was replaced with `{mode: 'index'}` +* `{mode: 'single'}` was replaced with `{mode: 'nearest', intersect: true}` +* `modes['X-axis']` was replaced with `{mode: 'index', intersect: false}` +* `options.onClick` is now limited to the chart area +* `options.onClick` and `options.onHover` now receive the `chart` instance as a 3rd argument +* `options.onHover` now receives a wrapped `event` as the first parameter. The previous first parameter value is accessible via `event.native`. +* `options.hover.onHover` was removed, use `options.onHover`. + +#### Ticks + +* `options.gridLines` was renamed to `options.grid` +* `options.gridLines.offsetGridLines` was renamed to `options.grid.offset`. +* `options.gridLines.tickMarkLength` was renamed to `options.grid.tickLength`. +* `options.ticks.fixedStepSize` is no longer used. Use `options.ticks.stepSize`. +* `options.ticks.major` and `options.ticks.minor` were replaced with scriptable options for tick fonts. +* `Chart.Ticks.formatters.linear` was renamed to `Chart.Ticks.formatters.numeric`. +* `options.ticks.backdropPaddingX` and `options.ticks.backdropPaddingY` were replaced with `options.ticks.backdropPadding` in the radial linear scale. + +#### Tooltip + +* `xLabel` and `yLabel` were removed. Please use `label` and `formattedValue` +* The `filter` option will now be passed additional parameters when called and should have the method signature `function(tooltipItem, index, tooltipItems, data)` +* The `custom` callback now takes a context object that has `tooltip` and `chart` properties +* All properties of tooltip model related to the tooltip options have been moved to reside within the `options` property. +* The callbacks no longer are given a `data` parameter. The tooltip item parameter contains the chart and dataset instead +* The tooltip item's `index` parameter was renamed to `dataIndex` and `value` was renamed to `formattedValue` +* The `xPadding` and `yPadding` options were merged into a single `padding` object + +## Developer migration + +While the end-user migration for Chart.js 3 is fairly straight-forward, the developer migration can be more complicated. Please reach out for help in the #dev [Discord](https://discord.gg/HxEguTK6av) channel if tips on migrating would be helpful. + +Some of the biggest things that have changed: + +* There is a completely rewritten and more performant animation system. + * `Element._model` and `Element._view` are no longer used and properties are now set directly on the elements. You will have to use the method `getProps` to access these properties inside most methods such as `inXRange`/`inYRange` and `getCenterPoint`. Please take a look at [the Chart.js-provided elements](https://github.com/chartjs/Chart.js/tree/master/src/elements) for examples. + * When building the elements in a controller, it's now suggested to call `updateElement` to provide the element properties. There are also methods such as `getSharedOptions` and `includeOptions` that have been added to skip redundant computation. Please take a look at [the Chart.js-provided controllers](https://github.com/chartjs/Chart.js/tree/master/src/controllers) for examples. +* Scales introduced a new parsing API. This API takes user data and converts it into a more standard format. E.g. it allows users to provide numeric data as a `string` and converts it to a `number` where necessary. Previously this was done on the fly as charts were rendered. Now it's done up front with the ability to skip it for better performance if users provide data in the correct format. If you're using standard data format like `x`/`y` you may not need to do anything. If you're using a custom data format you will have to override some of the parse methods in `core.datasetController.js`. An example can be found in [chartjs-chart-financial](https://github.com/chartjs/chartjs-chart-financial), which uses an `{o, h, l, c}` data format. + +A few changes were made to controllers that are more straight-forward, but will affect all controllers: + +* Options: + * `global` was removed from the defaults namespace as it was unnecessary and sometimes inconsistent + * Dataset defaults are now under the chart type options instead of vice-versa. This was not able to be done when introduced in 2.x for backwards compatibility. Fixing it removes the biggest stumbling block that new chart developers encountered + * Scale default options need to be updated as described in the end user migration section (e.g. `x` instead of `xAxes` and `y` instead of `yAxes`) +* `updateElement` was changed to `updateElements` and has a new method signature as described below. This provides performance enhancements such as allowing easier reuse of computations that are common to all elements and reducing the number of function calls + +### Removed + +The following properties and methods were removed: + +#### Removed from Chart + +* `Chart.animationService` +* `Chart.active` +* `Chart.borderWidth` +* `Chart.chart.chart` +* `Chart.Bar`. New charts are created via `new Chart` and providing the appropriate `type` parameter +* `Chart.Bubble`. New charts are created via `new Chart` and providing the appropriate `type` parameter +* `Chart.Chart` +* `Chart.Controller` +* `Chart.Doughnut`. New charts are created via `new Chart` and providing the appropriate `type` parameter +* `Chart.innerRadius` now lives on doughnut, pie, and polarArea controllers +* `Chart.lastActive` +* `Chart.Legend` was moved to `Chart.plugins.legend._element` and made private +* `Chart.Line`. New charts are created via `new Chart` and providing the appropriate `type` parameter +* `Chart.LinearScaleBase` now must be imported and cannot be accessed off the `Chart` object +* `Chart.offsetX` +* `Chart.offsetY` +* `Chart.outerRadius` now lives on doughnut, pie, and polarArea controllers +* `Chart.plugins` was replaced with `Chart.registry`. Plugin defaults are now in `Chart.defaults.plugins[id]`. +* `Chart.plugins.register` was replaced by `Chart.register`. +* `Chart.PolarArea`. New charts are created via `new Chart` and providing the appropriate `type` parameter +* `Chart.prototype.generateLegend` +* `Chart.platform`. It only contained `disableCSSInjection`. CSS is never injected in v3. +* `Chart.PluginBase` +* `Chart.Radar`. New charts are created via `new Chart` and providing the appropriate `type` parameter +* `Chart.radiusLength` +* `Chart.scaleService` was replaced with `Chart.registry`. Scale defaults are now in `Chart.defaults.scales[type]`. +* `Chart.Scatter`. New charts are created via `new Chart` and providing the appropriate `type` parameter +* `Chart.types` +* `Chart.Title` was moved to `Chart.plugins.title._element` and made private +* `Chart.Tooltip` is now provided by the tooltip plugin. The positioners can be accessed from `tooltipPlugin.positioners` +* `ILayoutItem.minSize` + +#### Removed from Dataset Controllers + +* `BarController.getDatasetMeta().bar` +* `DatasetController.addElementAndReset` +* `DatasetController.createMetaData` +* `DatasetController.createMetaDataset` +* `DoughnutController.getRingIndex` + +#### Removed from Elements + +* `Element.getArea` +* `Element.height` +* `Element.hidden` was replaced by chart level status, usable with `getDataVisibility(index)` / `toggleDataVisibility(index)` +* `Element.initialize` +* `Element.inLabelRange` +* `Line.calculatePointY` + +#### Removed from Helpers + +* `helpers.addEvent` +* `helpers.aliasPixel` +* `helpers.arrayEquals` +* `helpers.configMerge` +* `helpers.findIndex` +* `helpers.findNextWhere` +* `helpers.findPreviousWhere` +* `helpers.extend`. Use `Object.assign` instead +* `helpers.getValueAtIndexOrDefault`. Use `helpers.resolve` instead. +* `helpers.indexOf` +* `helpers.lineTo` +* `helpers.longestText` was made private +* `helpers.max` +* `helpers.measureText` was made private +* `helpers.min` +* `helpers.nextItem` +* `helpers.niceNum` +* `helpers.numberOfLabelLines` +* `helpers.previousItem` +* `helpers.removeEvent` +* `helpers.roundedRect` +* `helpers.scaleMerge` +* `helpers.where` + +#### Removed from Layout + +* `Layout.defaults` + +#### Removed from Scales + +* `LinearScaleBase.handleDirectionalChanges` +* `LogarithmicScale.minNotZero` +* `Scale.getRightValue` +* `Scale.longestLabelWidth` +* `Scale.longestTextCache` is now private +* `Scale.margins` is now private +* `Scale.mergeTicksOptions` +* `Scale.ticksAsNumbers` +* `Scale.tickValues` is now private +* `TimeScale.getLabelCapacity` is now private +* `TimeScale.tickFormatFunction` is now private + +#### Removed from Plugins (Legend, Title, and Tooltip) + +* `IPlugin.afterScaleUpdate`. Use `afterLayout` instead +* `Legend.margins` is now private +* Legend `onClick`, `onHover`, and `onLeave` options now receive the legend as the 3rd argument in addition to implicitly via `this` +* Legend `onClick`, `onHover`, and `onLeave` options now receive a wrapped `event` as the first parameter. The previous first parameter value is accessible via `event.native`. +* `Title.margins` is now private +* The tooltip item's `x` and `y` attributes were replaced by `element`. You can use `element.x` and `element.y` or `element.tooltipPosition()` instead. + +#### Removal of Public APIs + +The following public APIs were removed. + +* `getElementAtEvent` is replaced with `chart.getElementsAtEventForMode(e, 'nearest', { intersect: true }, false)` +* `getElementsAtEvent` is replaced with `chart.getElementsAtEventForMode(e, 'index', { intersect: true }, false)` +* `getElementsAtXAxis` is replaced with `chart.getElementsAtEventForMode(e, 'index', { intersect: false }, false)` +* `getDatasetAtEvent` is replaced with `chart.getElementsAtEventForMode(e, 'dataset', { intersect: true }, false)` + +#### Removal of private APIs + +The following private APIs were removed. + +* `Chart._bufferedRender` +* `Chart._updating` +* `Chart.data.datasets[datasetIndex]._meta` +* `DatasetController._getIndexScaleId` +* `DatasetController._getIndexScale` +* `DatasetController._getValueScaleId` +* `DatasetController._getValueScale` +* `Element._ctx` +* `Element._model` +* `Element._view` +* `LogarithmicScale._valueOffset` +* `TimeScale.getPixelForOffset` +* `TimeScale.getLabelWidth` +* `Tooltip._lastActive` + +### Renamed + +The following properties were renamed during v3 development: + +* `Chart.Animation.animationObject` was renamed to `Chart.Animation` +* `Chart.Animation.chartInstance` was renamed to `Chart.Animation.chart` +* `Chart.canvasHelpers` was merged with `Chart.helpers` +* `Chart.elements.Arc` was renamed to `Chart.elements.ArcElement` +* `Chart.elements.Line` was renamed to `Chart.elements.LineElement` +* `Chart.elements.Point` was renamed to `Chart.elements.PointElement` +* `Chart.elements.Rectangle` was renamed to `Chart.elements.BarElement` +* `Chart.layoutService` was renamed to `Chart.layouts` +* `Chart.pluginService` was renamed to `Chart.plugins` +* `helpers.callCallback` was renamed to `helpers.callback` +* `helpers.drawRoundedRectangle` was renamed to `helpers.roundedRect` +* `helpers.getValueOrDefault` was renamed to `helpers.valueOrDefault` +* `LayoutItem.fullWidth` was renamed to `LayoutItem.fullSize` +* `Point.controlPointPreviousX` was renamed to `Point.cp1x` +* `Point.controlPointPreviousY` was renamed to `Point.cp1y` +* `Point.controlPointNextX` was renamed to `Point.cp2x` +* `Point.controlPointNextY` was renamed to `Point.cp2y` +* `Scale.calculateTickRotation` was renamed to `Scale.calculateLabelRotation` +* `Tooltip.options.legendColorBackgroupd` was renamed to `Tooltip.options.multiKeyBackground` + +#### Renamed private APIs + +The private APIs listed below were renamed: + +* `BarController.calculateBarIndexPixels` was renamed to `BarController._calculateBarIndexPixels` +* `BarController.calculateBarValuePixels` was renamed to `BarController._calculateBarValuePixels` +* `BarController.getStackCount` was renamed to `BarController._getStackCount` +* `BarController.getStackIndex` was renamed to `BarController._getStackIndex` +* `BarController.getRuler` was renamed to `BarController._getRuler` +* `Chart.destroyDatasetMeta` was renamed to `Chart._destroyDatasetMeta` +* `Chart.drawDataset` was renamed to `Chart._drawDataset` +* `Chart.drawDatasets` was renamed to `Chart._drawDatasets` +* `Chart.eventHandler` was renamed to `Chart._eventHandler` +* `Chart.handleEvent` was renamed to `Chart._handleEvent` +* `Chart.initialize` was renamed to `Chart._initialize` +* `Chart.resetElements` was renamed to `Chart._resetElements` +* `Chart.unbindEvents` was renamed to `Chart._unbindEvents` +* `Chart.updateDataset` was renamed to `Chart._updateDataset` +* `Chart.updateDatasets` was renamed to `Chart._updateDatasets` +* `Chart.updateLayout` was renamed to `Chart._updateLayout` +* `DatasetController.destroy` was renamed to `DatasetController._destroy` +* `DatasetController.insertElements` was renamed to `DatasetController._insertElements` +* `DatasetController.onDataPop` was renamed to `DatasetController._onDataPop` +* `DatasetController.onDataPush` was renamed to `DatasetController._onDataPush` +* `DatasetController.onDataShift` was renamed to `DatasetController._onDataShift` +* `DatasetController.onDataSplice` was renamed to `DatasetController._onDataSplice` +* `DatasetController.onDataUnshift` was renamed to `DatasetController._onDataUnshift` +* `DatasetController.removeElements` was renamed to `DatasetController._removeElements` +* `DatasetController.resyncElements` was renamed to `DatasetController._resyncElements` +* `LayoutItem.isFullWidth` was renamed to `LayoutItem.isFullSize` +* `RadialLinearScale.setReductions` was renamed to `RadialLinearScale._setReductions` +* `RadialLinearScale.pointLabels` was renamed to `RadialLinearScale._pointLabels` +* `Scale.handleMargins` was renamed to `Scale._handleMargins` + +### Changed + +The APIs listed in this section have changed in signature or behaviour from version 2. + +#### Changed in Scales + +* `Scale.getLabelForIndex` was replaced by `scale.getLabelForValue` +* `Scale.getPixelForValue` now only requires one parameter. For the `TimeScale` that parameter must be millis since the epoch. As a performance optimization, it may take an optional second parameter, giving the index of the data point. + +##### Changed in Ticks + +* `Scale.afterBuildTicks` now has no parameters like the other callbacks +* `Scale.buildTicks` is now expected to return tick objects +* `Scale.convertTicksToLabels` was renamed to `generateTickLabels`. It is now expected to set the label property on the ticks given as input +* `Scale.ticks` now contains objects instead of strings +* When the `autoSkip` option is enabled, `Scale.ticks` now contains only the non-skipped ticks instead of all ticks. +* Ticks are now always generated in monotonically increasing order + +##### Changed in Time Scale + +* `getValueForPixel` now returns milliseconds since the epoch + +#### Changed in Controllers + +##### Core Controller + +* The first parameter to `updateHoverStyle` is now an array of objects containing the `element`, `datasetIndex`, and `index` +* The signature or `resize` changed, the first `silent` parameter was removed. + +##### Dataset Controllers + +* `updateElement` was replaced with `updateElements` now taking the elements to update, the `start` index, `count`, and `mode` +* `setHoverStyle` and `removeHoverStyle` now additionally take the `datasetIndex` and `index` + +#### Changed in Interactions + +* Interaction mode methods now return an array of objects containing the `element`, `datasetIndex`, and `index` + +#### Changed in Layout + +* `ILayoutItem.update` no longer has a return value + +#### Changed in Helpers + +All helpers are now exposed in a flat hierarchy, e.g., `Chart.helpers.canvas.clipArea` -> `Chart.helpers.clipArea` + +##### Canvas Helper + +* The second parameter to `drawPoint` is now the full options object, so `style`, `rotation`, and `radius` are no longer passed explicitly +* `helpers.getMaximumHeight` was replaced by `helpers.dom.getMaximumSize` +* `helpers.getMaximumWidth` was replaced by `helpers.dom.getMaximumSize` +* `helpers.clear` was renamed to `helpers.clearCanvas` and now takes `canvas` and optionally `ctx` as parameter(s). +* `helpers.retinaScale` accepts optional third parameter `forceStyle`, which forces overriding current canvas style. `forceRatio` no longer falls back to `window.devicePixelRatio`, instead it defaults to `1`. + +#### Changed in Platform + +* `Chart.platform` is no longer the platform object used by charts. Every chart instance now has a separate platform instance. +* `Chart.platforms` is an object that contains two usable platform classes, `BasicPlatform` and `DomPlatform`. It also contains `BasePlatform`, a class that all platforms must extend from. +* If the canvas passed in is an instance of `OffscreenCanvas`, the `BasicPlatform` is automatically used. +* `isAttached` method was added to platform. + +#### Changed in IPlugin interface + +* All plugin hooks have unified signature with 3 arguments: `chart`, `args` and `options`. This means change in signature for these hooks: `beforeInit`, `afterInit`, `reset`, `beforeLayout`, `afterLayout`, `beforeRender`, `afterRender`, `beforeDraw`, `afterDraw`, `beforeDatasetsDraw`, `afterDatasetsDraw`, `beforeEvent`, `afterEvent`, `resize`, `destroy`. +* `afterDatasetsUpdate`, `afterUpdate`, `beforeDatasetsUpdate`, and `beforeUpdate` now receive `args` object as second argument. `options` argument is always the last and thus was moved from 2nd to 3rd place. +* `afterEvent` and `beforeEvent` now receive a wrapped `event` as the `event` property of the second argument. The native event is available via `args.event.native`. +* Initial `resize` is no longer silent. Meaning that `resize` event can fire between `beforeInit` and `afterInit` +* New hooks: `install`, `start`, `stop`, and `uninstall` +* `afterEvent` should notify about changes that need a render by setting `args.changed` to true. Because the `args` are shared with all plugins, it should only be set to true and not false. diff --git a/docs/migration/v4-migration.md b/docs/migration/v4-migration.md new file mode 100644 index 00000000000..8048c5cae93 --- /dev/null +++ b/docs/migration/v4-migration.md @@ -0,0 +1,49 @@ +# 4.x Migration Guide + +Chart.js 4.0 introduces a number of breaking changes. We tried keeping the amount of breaking changes to a minimum. For some features and bug fixes it was necessary to break backwards compatibility, but we aimed to do so only when worth the benefit. + +## End user migration + +### Charts + +* Charts don't override the default tooltip callbacks, so all chart types have the same-looking tooltips. +* Default scale override has been removed if the configured scale starts with `x`/`y`. Defining `xAxes` in your config will now create a second scale instead of overriding the default `x` axis. + +### Options + +A number of changes were made to the configuration options passed to the `Chart` constructor. Those changes are documented below. + +#### Specific changes + +* The radialLinear grid indexable and scriptable options don't decrease the index of the specified grid line anymore. +* The `destroy` plugin hook has been removed and replaced with `afterDestroy`. +* Ticks callback on time scale now receives timestamp instead of a formatted label. +* `scales[id].grid.drawBorder` has been renamed to `scales[id].border.display`. +* `scales[id].grid.borderWidth` has been renamed to `scales[id].border.width`. +* `scales[id].grid.borderColor` has been renamed to `scales[id].border.color`. +* `scales[id].grid.borderDash` has been renamed to `scales[id].border.dash`. +* `scales[id].grid.borderDashOffset` has been renamed to `scales[id].border.dashOffset`. +* The z index for the border of a scale is now configurable instead of being 1 higher as the grid z index. +* Linear scales now add and subtracts `5%` of the max value to the range if the min and max are the same instead of `1`. +* If the tooltip callback returns `undefined`, then the default callback will be used. +* `maintainAspectRatio` respects container height. +* Time and timeseries scales use `ticks.stepSize` instead of `time.stepSize`, which has been removed. +* `maxTickslimit` won't be used for the ticks in `autoSkip` if the determined max ticks is less then the `maxTicksLimit`. +* `dist/chart.js` has been removed. +* `dist/chart.min.js` has been renamed to `dist/chart.umd.min.js` (and before 4.5.0 `dist/chart.umd.js`). +* `dist/chart.esm.js` has been renamed to `dist/chart.js`. + +#### Type changes +* The order of the `ChartMeta` parameters have been changed from `` to ``. + +### General +* Chart.js becomes an [ESM-only package](https://nodejs.org/api/esm.html) ([the UMD bundle is still available](../getting-started/installation.md#cdn)). To use Chart.js, your project should also be an ES module. Make sure to have this in your `package.json`: + ```json + { + "type": "module" + } + ``` + If you are experiencing problems with [Jest](https://jestjs.io), follow its [documentation](https://jestjs.io/docs/ecmascript-modules) to enable the ESM support. Or, we can recommend you migrating to [Vitest](https://vitest.dev/). Vitest has the ESM support out of the box and [almost the same API as Jest](https://vitest.dev/guide/migration.html#migrating-from-jest). See an [example of migration](https://github.com/reactchartjs/react-chartjs-2/commit/7f3ec96101d21e43cae8cbfe5e09a46a17cff1ef). +* Removed fallback to `fontColor` for the legend text and strikethrough color. +* Removed `config._chart` fallback for `this.chart` in the filler plugin. +* Removed `this._chart` in the filler plugin. diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 00000000000..007ee597fdc --- /dev/null +++ b/docs/package.json @@ -0,0 +1,31 @@ +{ + "name": "docs", + "private": "true", + "version": "4.0.0-dev", + "license": "MIT", + "type": "module", + "scripts": { + "build": "vuepress build --no-cache", + "dev": "vuepress dev --no-cache" + }, + "devDependencies": { + "@simonbrunel/vuepress-plugin-versions": "^0.2.0", + "@vuepress/plugin-google-analytics": "^1.9.7", + "@vuepress/plugin-html-redirect": "^0.1.2", + "markdown-it": "^12.3.2", + "markdown-it-include": "^2.0.0", + "typedoc": "^0.23.10", + "typedoc-plugin-markdown": "^3.13.4", + "typescript": "^4.7.4", + "vue": "^2.6.14", + "vue-tabs-component": "^1.5.0", + "vuepress": "^1.9.7", + "vuepress-plugin-code-copy": "^1.0.6", + "vuepress-plugin-flexsearch": "^0.3.0", + "vuepress-plugin-redirect": "^1.2.5", + "vuepress-plugin-tabs": "^0.3.0", + "vuepress-plugin-typedoc": "^0.11.0", + "vuepress-theme-chartjs": "^0.2.0", + "webpack": "^4.46.0" + } +} diff --git a/docs/samples/.eslintrc.yml b/docs/samples/.eslintrc.yml new file mode 100644 index 00000000000..b4e00d7d230 --- /dev/null +++ b/docs/samples/.eslintrc.yml @@ -0,0 +1,2 @@ +rules: + no-console: "off" diff --git a/docs/samples/advanced/data-decimation.md b/docs/samples/advanced/data-decimation.md new file mode 100644 index 00000000000..ae108239a03 --- /dev/null +++ b/docs/samples/advanced/data-decimation.md @@ -0,0 +1,118 @@ +# Data Decimation + +This example shows how to use the built-in data decimation to reduce the number of points drawn on the graph for improved performance. + +```js chart-editor +// +const actions = [ + { + name: 'No decimation (default)', + handler(chart) { + chart.options.plugins.decimation.enabled = false; + chart.update(); + } + }, + { + name: 'min-max decimation', + handler(chart) { + chart.options.plugins.decimation.algorithm = 'min-max'; + chart.options.plugins.decimation.enabled = true; + chart.update(); + }, + }, + { + name: 'LTTB decimation (50 samples)', + handler(chart) { + chart.options.plugins.decimation.algorithm = 'lttb'; + chart.options.plugins.decimation.enabled = true; + chart.options.plugins.decimation.samples = 50; + chart.update(); + } + }, + { + name: 'LTTB decimation (500 samples)', + handler(chart) { + chart.options.plugins.decimation.algorithm = 'lttb'; + chart.options.plugins.decimation.enabled = true; + chart.options.plugins.decimation.samples = 500; + chart.update(); + } + } +]; +// + +// +const NUM_POINTS = 100000; +Utils.srand(10); + +// parseISODate returns a luxon date object to work with in the samples +// We will create points every 30s starting from this point in time +const start = Utils.parseISODate('2021-04-01T00:00:00Z').toMillis(); +const pointData = []; + +for (let i = 0; i < NUM_POINTS; ++i) { + // Most data will be in the range [0, 20) but some rare data will be in the range [0, 100) + const max = Math.random() < 0.001 ? 100 : 20; + pointData.push({x: start + (i * 30000), y: Utils.rand(0, max)}); +} + +const data = { + datasets: [{ + borderColor: Utils.CHART_COLORS.red, + borderWidth: 1, + data: pointData, + label: 'Large Dataset', + radius: 0, + }] +}; +// + +// +const decimation = { + enabled: false, + algorithm: 'min-max', +}; +// + +// +const config = { + type: 'line', + data: data, + options: { + // Turn off animations and data parsing for performance + animation: false, + parsing: false, + + interaction: { + mode: 'nearest', + axis: 'x', + intersect: false + }, + plugins: { + decimation: decimation, + }, + scales: { + x: { + type: 'time', + ticks: { + source: 'auto', + // Disabled rotation for performance + maxRotation: 0, + autoSkip: true, + } + } + } + } +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` +## Docs +* [Data Decimation](../../configuration/decimation.md) +* [Line](../../charts/line.md) +* [Time Scale](../../axes/cartesian/time.md) + diff --git a/docs/samples/advanced/derived-axis-type.md b/docs/samples/advanced/derived-axis-type.md new file mode 100644 index 00000000000..f9705e2cc31 --- /dev/null +++ b/docs/samples/advanced/derived-axis-type.md @@ -0,0 +1,55 @@ +# Derived Axis Type + +```js chart-editor +// +const DATA_COUNT = 12; +const NUMBER_CFG = {count: DATA_COUNT, min: 0, max: 1000}; +const labels = Utils.months({count: DATA_COUNT}); +const data = { + labels: labels, + datasets: [ + { + label: 'My First dataset', + data: Utils.numbers(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + fill: false, + } + ], +}; +// + +// +const config = { + type: 'line', + data, + options: { + responsive: true, + scales: { + x: { + display: true, + }, + y: { + display: true, + type: 'log2', + } + } + } +}; + +// + +module.exports = { + actions: [], + config: config, +}; +``` + +## Log2 axis implementation + +<<< @/scripts/log2.js + +## Docs +* [Data structures (`labels`)](../../general/data-structures.md) +* [Line](../../charts/line.md) +* [New Axes](../../developers/axes.md) diff --git a/docs/samples/advanced/derived-chart-type.md b/docs/samples/advanced/derived-chart-type.md new file mode 100644 index 00000000000..7f853e3aaa3 --- /dev/null +++ b/docs/samples/advanced/derived-chart-type.md @@ -0,0 +1,50 @@ +# Derived Chart Type + +```js chart-editor +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100, rmin: 1, rmax: 20}; +const data = { + datasets: [ + { + label: 'My First dataset', + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), + borderColor: Utils.CHART_COLORS.blue, + borderWidth: 1, + boxStrokeStyle: 'red', + data: Utils.bubbles(NUMBER_CFG) + } + ], +}; +// + +// +const config = { + type: 'derivedBubble', + data: data, + options: { + responsive: true, + plugins: { + title: { + display: true, + text: 'Derived Chart Type' + }, + } + } +}; + +// + +module.exports = { + actions: [], + config: config, +}; +``` + +## DerivedBubble Implementation + +<<< @/scripts/derived-bubble.js + +## Docs +* [Bubble Chart](../../charts/bubble.md) +* [New Charts](../../developers/charts.md) diff --git a/docs/samples/advanced/linear-gradient.md b/docs/samples/advanced/linear-gradient.md new file mode 100644 index 00000000000..e045f7cd522 --- /dev/null +++ b/docs/samples/advanced/linear-gradient.md @@ -0,0 +1,118 @@ +# Linear Gradient + +```js chart-editor +// +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = Utils.numbers({count: chart.data.labels.length, min: -100, max: 100}); + }); + chart.update(); + } + }, + { + name: 'Add Data', + handler(chart) { + const data = chart.data; + if (data.datasets.length > 0) { + data.labels = Utils.months({count: data.labels.length + 1}); + + for (let index = 0; index < data.datasets.length; ++index) { + data.datasets[index].data.push(Utils.rand(-100, 100)); + } + + chart.update(); + } + } + }, + { + name: 'Remove Data', + handler(chart) { + chart.data.labels.splice(-1, 1); // remove the label first + + chart.data.datasets.forEach(dataset => { + dataset.data.pop(); + }); + + chart.update(); + } + } +]; +// + +// +let width, height, gradient; +function getGradient(ctx, chartArea) { + const chartWidth = chartArea.right - chartArea.left; + const chartHeight = chartArea.bottom - chartArea.top; + if (!gradient || width !== chartWidth || height !== chartHeight) { + // Create the gradient because this is either the first render + // or the size of the chart has changed + width = chartWidth; + height = chartHeight; + gradient = ctx.createLinearGradient(0, chartArea.bottom, 0, chartArea.top); + gradient.addColorStop(0, Utils.CHART_COLORS.blue); + gradient.addColorStop(0.5, Utils.CHART_COLORS.yellow); + gradient.addColorStop(1, Utils.CHART_COLORS.red); + } + + return gradient; +} +// + +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; +const labels = Utils.months({count: 7}); + +const data = { + labels: labels, + datasets: [ + { + label: 'Dataset 1', + data: Utils.numbers(NUMBER_CFG), + borderColor: function(context) { + const chart = context.chart; + const {ctx, chartArea} = chart; + + if (!chartArea) { + // This case happens on initial chart load + return; + } + return getGradient(ctx, chartArea); + }, + }, + ] +}; +// + +// +const config = { + type: 'line', + data: data, + options: { + responsive: true, + plugins: { + legend: { + position: 'top', + }, + } + }, +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` + +## Docs +* [Colors](../../general/colors.md) + * [Patterns and Gradients](../../general/colors.md#patterns-and-gradients) +* [Data structures (`labels`)](../../general/data-structures.md) +* [Options](../../general/options.md) + * [Scriptable Options](../../general/options.md#scriptable-options) +* [Line](../../charts/line.md) diff --git a/docs/samples/advanced/programmatic-events.md b/docs/samples/advanced/programmatic-events.md new file mode 100644 index 00000000000..4157d7ee98b --- /dev/null +++ b/docs/samples/advanced/programmatic-events.md @@ -0,0 +1,115 @@ +# Programmatic Event Triggers + +```js chart-editor +// +function triggerHover(chart) { + if (chart.getActiveElements().length > 0) { + chart.setActiveElements([]); + } else { + chart.setActiveElements([ + { + datasetIndex: 0, + index: 0, + }, { + datasetIndex: 1, + index: 0, + } + ]); + } + chart.update(); +} +// + +// +function triggerTooltip(chart) { + const tooltip = chart.tooltip; + if (tooltip.getActiveElements().length > 0) { + tooltip.setActiveElements([], {x: 0, y: 0}); + } else { + const chartArea = chart.chartArea; + tooltip.setActiveElements([ + { + datasetIndex: 0, + index: 2, + }, { + datasetIndex: 1, + index: 2, + } + ], + { + x: (chartArea.left + chartArea.right) / 2, + y: (chartArea.top + chartArea.bottom) / 2, + }); + } + + chart.update(); +} +// + +// +const actions = [ + { + name: 'Trigger Hover', + handler: triggerHover + }, + { + name: 'Trigger Tooltip', + handler: triggerTooltip + } +]; +// + +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; + +const labels = Utils.months({count: 7}); +const data = { + labels: labels, + datasets: [ + { + label: 'Dataset 1', + data: Utils.numbers(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + hoverBorderWidth: 5, + hoverBorderColor: 'green', + }, + { + label: 'Dataset 2', + data: Utils.numbers(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.blue, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), + hoverBorderWidth: 5, + hoverBorderColor: 'green', + } + ] +}; +// + +// +const config = { + type: 'bar', + data: data, + options: { + }, +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` + +## API +* [Chart](../../api/classes/Chart.md) + * [`setActiveElements`](../../api/classes/Chart.md#setactiveelements) +* [TooltipModel](../../api/interfaces/TooltipModel.md) + * [`setActiveElements`](../../api/interfaces/TooltipModel.md#setactiveelements) + +## Docs +* [Bar](../../charts/bar.md) + * [Interactions (`hoverBorderColor`)](../../charts/bar.md#interactions) +* [Interactions](../../configuration/interactions.md) +* [Tooltip](../../configuration/tooltip.md) diff --git a/docs/samples/advanced/progress-bar.md b/docs/samples/advanced/progress-bar.md new file mode 100644 index 00000000000..016831b22a7 --- /dev/null +++ b/docs/samples/advanced/progress-bar.md @@ -0,0 +1,152 @@ +# Animation Progress Bar + +## Initial animation + + + +## Other animations + + + +```js chart-editor +// +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = Utils.numbers({count: chart.data.labels.length, min: -100, max: 100}); + }); + chart.update(); + } + }, + { + name: 'Add Dataset', + handler(chart) { + const data = chart.data; + const dsColor = Utils.namedColor(chart.data.datasets.length); + const newDataset = { + label: 'Dataset ' + (data.datasets.length + 1), + backgroundColor: Utils.transparentize(dsColor, 0.5), + borderColor: dsColor, + data: Utils.numbers({count: data.labels.length, min: -100, max: 100}), + }; + chart.data.datasets.push(newDataset); + chart.update(); + } + }, + { + name: 'Add Data', + handler(chart) { + const data = chart.data; + if (data.datasets.length > 0) { + data.labels = Utils.months({count: data.labels.length + 1}); + + for (let index = 0; index < data.datasets.length; ++index) { + data.datasets[index].data.push(Utils.rand(-100, 100)); + } + + chart.update(); + } + } + }, + { + name: 'Remove Dataset', + handler(chart) { + chart.data.datasets.pop(); + chart.update(); + } + }, + { + name: 'Remove Data', + handler(chart) { + chart.data.labels.splice(-1, 1); // remove the label first + + chart.data.datasets.forEach(dataset => { + dataset.data.pop(); + }); + + chart.update(); + } + } +]; +// + +// +const initProgress = document.getElementById('initialProgress'); +const progress = document.getElementById('animationProgress'); + +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; + +const labels = Utils.months({count: 7}); +const data = { + labels: labels, + datasets: [ + { + label: 'Dataset 1', + data: Utils.numbers(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + }, + { + label: 'Dataset 2', + data: Utils.numbers(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.blue, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), + } + ] +}; +// + +// +const config = { + type: 'line', + data: data, + options: { + animation: { + duration: 2000, + onProgress: function(context) { + if (context.initial) { + initProgress.value = context.currentStep / context.numSteps; + } else { + progress.value = context.currentStep / context.numSteps; + } + }, + onComplete: function(context) { + if (context.initial) { + console.log('Initial animation finished'); + } else { + console.log('animation finished'); + } + } + }, + interaction: { + mode: 'nearest', + axis: 'x', + intersect: false + }, + plugins: { + title: { + display: true, + text: 'Chart.js Line Chart - Animation Progress Bar' + } + }, + }, +}; +// + +module.exports = { + actions: actions, + config: config, + output: 'console.log output is displayed here' +}; +``` + +## Docs +* [Animations](../../configuration/animations.md) + * [Animation Callbacks](../../configuration/animations.md#animation-callbacks) +* [Data structures (`labels`)](../../general/data-structures.md) +* [Line](../../charts/line.md) +* [Options](../../general/options.md) + * [Scriptable Options](../../general/options.md#scriptable-options) diff --git a/docs/samples/advanced/radial-gradient.md b/docs/samples/advanced/radial-gradient.md new file mode 100644 index 00000000000..0c07502be65 --- /dev/null +++ b/docs/samples/advanced/radial-gradient.md @@ -0,0 +1,122 @@ +# Radial Gradient + +```js chart-editor +// +const DATA_COUNT = 5; +Utils.srand(110); + +const chartColors = Utils.CHART_COLORS; +const colors = [chartColors.red, chartColors.orange, chartColors.yellow, chartColors.green, chartColors.blue]; + +const cache = new Map(); +let width = null; +let height = null; + +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = generateData(); + }); + chart.update(); + } + }, +]; +// + +// +function createRadialGradient3(context, c1, c2, c3) { + const chartArea = context.chart.chartArea; + if (!chartArea) { + // This case happens on initial chart load + return; + } + + const chartWidth = chartArea.right - chartArea.left; + const chartHeight = chartArea.bottom - chartArea.top; + if (width !== chartWidth || height !== chartHeight) { + cache.clear(); + } + let gradient = cache.get(c1 + c2 + c3); + if (!gradient) { + // Create the gradient because this is either the first render + // or the size of the chart has changed + width = chartWidth; + height = chartHeight; + const centerX = (chartArea.left + chartArea.right) / 2; + const centerY = (chartArea.top + chartArea.bottom) / 2; + const r = Math.min( + (chartArea.right - chartArea.left) / 2, + (chartArea.bottom - chartArea.top) / 2 + ); + const ctx = context.chart.ctx; + gradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, r); + gradient.addColorStop(0, c1); + gradient.addColorStop(0.5, c2); + gradient.addColorStop(1, c3); + cache.set(c1 + c2 + c3, gradient); + } + + return gradient; +} +// + +// +function generateData() { + return Utils.numbers({ + count: DATA_COUNT, + min: 0, + max: 100 + }); +} + +const data = { + labels: Utils.months({count: DATA_COUNT}), + datasets: [{ + data: generateData() + }] +}; +// + +// +const config = { + type: 'polarArea', + data: data, + options: { + plugins: { + legend: false, + tooltip: false, + }, + elements: { + arc: { + backgroundColor: function(context) { + let c = colors[context.dataIndex]; + if (!c) { + return; + } + if (context.active) { + c = helpers.getHoverColor(c); + } + const mid = helpers.color(c).desaturate(0.2).darken(0.2).rgbString(); + const start = helpers.color(c).lighten(0.2).rotate(270).rgbString(); + const end = helpers.color(c).lighten(0.1).rgbString(); + return createRadialGradient3(context, start, mid, end); + }, + } + } + } +}; +// + +module.exports = { + actions, + config, +}; +``` + +## Docs +* [Polar Area Chart](../../charts/polar.md) + * [Styling](../../charts/polar.md#styling) +* [Options](../../general/options.md) + * [Scriptable Options](../../general/options.md#scriptable-options) \ No newline at end of file diff --git a/docs/samples/animations/delay.md b/docs/samples/animations/delay.md new file mode 100644 index 00000000000..cec3267991c --- /dev/null +++ b/docs/samples/animations/delay.md @@ -0,0 +1,87 @@ +# Delay + +```js chart-editor +// +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = Utils.numbers({count: chart.data.labels.length, min: -100, max: 100}); + }); + chart.update(); + } + }, +]; +// + +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; + +const labels = Utils.months({count: 7}); +const data = { + labels: labels, + datasets: [ + { + label: 'Dataset 1', + data: Utils.numbers(NUMBER_CFG), + backgroundColor: Utils.CHART_COLORS.red, + }, + { + label: 'Dataset 2', + data: Utils.numbers(NUMBER_CFG), + backgroundColor: Utils.CHART_COLORS.blue, + }, + { + label: 'Dataset 3', + data: Utils.numbers(NUMBER_CFG), + backgroundColor: Utils.CHART_COLORS.green, + }, + ] +}; +// + +// +let delayed; +const config = { + type: 'bar', + data: data, + options: { + animation: { + onComplete: () => { + delayed = true; + }, + delay: (context) => { + let delay = 0; + if (context.type === 'data' && context.mode === 'default' && !delayed) { + delay = context.dataIndex * 300 + context.datasetIndex * 100; + } + return delay; + }, + }, + scales: { + x: { + stacked: true, + }, + y: { + stacked: true + } + } + } +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` +## Docs +* [Animations](../../configuration/animations.md) + * [animation (`delay`)](../../configuration/animations.md#animation) + * [Animation Callbacks](../../configuration/animations.md#animation-callbacks) +* [Bar](../../charts/bar.md) + * [Stacked Bar Chart](../../charts/bar.md#stacked-bar-chart) +* [Options](../../general/options.md) + * [Scriptable Options](../../general/options.md#scriptable-options) diff --git a/docs/samples/animations/drop.md b/docs/samples/animations/drop.md new file mode 100644 index 00000000000..37e1af0cd76 --- /dev/null +++ b/docs/samples/animations/drop.md @@ -0,0 +1,136 @@ +# Drop + +```js chart-editor +// +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = Utils.numbers({count: chart.data.labels.length, min: -100, max: 100}); + }); + chart.update(); + } + }, + { + name: 'Add Dataset', + handler(chart) { + const data = chart.data; + const dsColor = Utils.namedColor(chart.data.datasets.length); + const newDataset = { + label: 'Dataset ' + (data.datasets.length + 1), + backgroundColor: Utils.transparentize(dsColor, 0.5), + borderColor: dsColor, + data: Utils.numbers({count: data.labels.length, min: -100, max: 100}), + }; + chart.data.datasets.push(newDataset); + chart.update(); + } + }, + { + name: 'Add Data', + handler(chart) { + const data = chart.data; + if (data.datasets.length > 0) { + data.labels = Utils.months({count: data.labels.length + 1}); + + for (let index = 0; index < data.datasets.length; ++index) { + data.datasets[index].data.push(Utils.rand(-100, 100)); + } + + chart.update(); + } + } + }, + { + name: 'Remove Dataset', + handler(chart) { + chart.data.datasets.pop(); + chart.update(); + } + }, + { + name: 'Remove Data', + handler(chart) { + chart.data.labels.splice(-1, 1); // remove the label first + + chart.data.datasets.forEach(dataset => { + dataset.data.pop(); + }); + + chart.update(); + } + } +]; +// + +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; + +const labels = Utils.months({count: 7}); +const data = { + labels: labels, + datasets: [ + { + label: 'Dataset 1', + animations: { + y: { + duration: 2000, + delay: 500 + } + }, + data: Utils.numbers(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + fill: 1, + tension: 0.5 + }, + { + label: 'Dataset 2', + data: Utils.numbers(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.blue, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), + } + ] +}; +// + +// +const config = { + type: 'line', + data: data, + options: { + animations: { + y: { + easing: 'easeInOutElastic', + from: (ctx) => { + if (ctx.type === 'data') { + if (ctx.mode === 'default' && !ctx.dropped) { + ctx.dropped = true; + return 0; + } + } + } + } + }, + }, +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` +## Docs +* [Area](../../charts/area.md) +* [Animations](../../configuration/animations.md) + * [animation (`easing`)](../../configuration/animations.md#animation) + * [animations (`from`)](../../configuration/animations.md#animations-2) +* [Line](../../charts/line.md) + * [Line Styling](../../charts/line.md#line-styling) + * `fill` + * `tension` +* [Options](../../general/options.md) + * [Scriptable Options](../../general/options.md#scriptable-options) diff --git a/docs/samples/animations/loop.md b/docs/samples/animations/loop.md new file mode 100644 index 00000000000..efd1fd8e15e --- /dev/null +++ b/docs/samples/animations/loop.md @@ -0,0 +1,141 @@ +# Loop + +```js chart-editor +// +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = Utils.numbers({count: chart.data.labels.length, min: -100, max: 100}); + }); + chart.update(); + } + }, + { + name: 'Add Dataset', + handler(chart) { + const data = chart.data; + const dsColor = Utils.namedColor(chart.data.datasets.length); + const newDataset = { + label: 'Dataset ' + (data.datasets.length + 1), + backgroundColor: Utils.transparentize(dsColor, 0.5), + borderColor: dsColor, + data: Utils.numbers({count: data.labels.length, min: -100, max: 100}), + }; + chart.data.datasets.push(newDataset); + chart.update(); + } + }, + { + name: 'Add Data', + handler(chart) { + const data = chart.data; + if (data.datasets.length > 0) { + data.labels = Utils.months({count: data.labels.length + 1}); + + for (let index = 0; index < data.datasets.length; ++index) { + data.datasets[index].data.push(Utils.rand(-100, 100)); + } + + chart.update(); + } + } + }, + { + name: 'Remove Dataset', + handler(chart) { + chart.data.datasets.pop(); + chart.update(); + } + }, + { + name: 'Remove Data', + handler(chart) { + chart.data.labels.splice(-1, 1); // remove the label first + + chart.data.datasets.forEach(dataset => { + dataset.data.pop(); + }); + + chart.update(); + } + } +]; +// + +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; + +const labels = Utils.months({count: DATA_COUNT}); +const data = { + labels: labels, + datasets: [ + { + label: 'Dataset 1', + data: Utils.numbers(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + tension: 0.4, + }, + { + label: 'Dataset 2', + data: Utils.numbers(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.blue, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), + tension: 0.2, + } + ] +}; +// + +// +const config = { + type: 'line', + data: data, + options: { + animations: { + radius: { + duration: 400, + easing: 'linear', + loop: (context) => context.active + } + }, + hoverRadius: 12, + hoverBackgroundColor: 'yellow', + interaction: { + mode: 'nearest', + intersect: false, + axis: 'x' + }, + plugins: { + tooltip: { + enabled: false + } + } + }, +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` +## Docs +* [Animations](../../configuration/animations.md) + * [animation](../../configuration/animations.md#animation) + * `duration` + * `easing` + * **`loop`** + * [Default animations (`radius`)](../../configuration/animations.md#default-animations) +* [Data structures (`labels`)](../../general/data-structures.md) +* [Elements](../../configuration/elements.md) + * [Point Configuration](../../configuration/elements.md#point-configuration) + * `hoverRadius` + * `hoverBackgroundColor` +* [Line](../../charts/line.md) +* [Options](../../general/options.md) + * [Scriptable Options](../../general/options.md#scriptable-options) +* [Tooltip (`enabled`)](../../configuration/tooltip.md) diff --git a/docs/samples/animations/progressive-line-easing.md b/docs/samples/animations/progressive-line-easing.md new file mode 100644 index 00000000000..d64c2cec01e --- /dev/null +++ b/docs/samples/animations/progressive-line-easing.md @@ -0,0 +1,189 @@ +# Progressive Line With Easing + +```js chart-editor + +// +const data = []; +const data2 = []; +let prev = 100; +let prev2 = 80; +for (let i = 0; i < 1000; i++) { + prev += 5 - Math.random() * 10; + data.push({x: i, y: prev}); + prev2 += 5 - Math.random() * 10; + data2.push({x: i, y: prev2}); +} +// + +// +let easing = helpers.easingEffects.easeOutQuad; +let restart = false; +const totalDuration = 5000; +const duration = (ctx) => easing(ctx.index / data.length) * totalDuration / data.length; +const delay = (ctx) => easing(ctx.index / data.length) * totalDuration; +const previousY = (ctx) => ctx.index === 0 ? ctx.chart.scales.y.getPixelForValue(100) : ctx.chart.getDatasetMeta(ctx.datasetIndex).data[ctx.index - 1].getProps(['y'], true).y; +const animation = { + x: { + type: 'number', + easing: 'linear', + duration: duration, + from: NaN, // the point is initially skipped + delay(ctx) { + if (ctx.type !== 'data' || ctx.xStarted) { + return 0; + } + ctx.xStarted = true; + return delay(ctx); + } + }, + y: { + type: 'number', + easing: 'linear', + duration: duration, + from: previousY, + delay(ctx) { + if (ctx.type !== 'data' || ctx.yStarted) { + return 0; + } + ctx.yStarted = true; + return delay(ctx); + } + } +}; +// + +// +const config = { + type: 'line', + data: { + datasets: [{ + borderColor: Utils.CHART_COLORS.red, + borderWidth: 1, + radius: 0, + data: data, + }, + { + borderColor: Utils.CHART_COLORS.blue, + borderWidth: 1, + radius: 0, + data: data2, + }] + }, + options: { + animation, + interaction: { + intersect: false + }, + plugins: { + legend: false, + title: { + display: true, + text: () => easing.name + } + }, + scales: { + x: { + type: 'linear' + } + } + } +}; +// + +// +function restartAnims(chart) { + chart.stop(); + const meta0 = chart.getDatasetMeta(0); + const meta1 = chart.getDatasetMeta(1); + for (let i = 0; i < data.length; i++) { + const ctx0 = meta0.controller.getContext(i); + const ctx1 = meta1.controller.getContext(i); + ctx0.xStarted = ctx0.yStarted = false; + ctx1.xStarted = ctx1.yStarted = false; + } + chart.update(); +} + +const actions = [ + { + name: 'easeOutQuad', + handler(chart) { + easing = helpers.easingEffects.easeOutQuad; + restartAnims(chart); + } + }, + { + name: 'easeOutCubic', + handler(chart) { + easing = helpers.easingEffects.easeOutCubic; + restartAnims(chart); + } + }, + { + name: 'easeOutQuart', + handler(chart) { + easing = helpers.easingEffects.easeOutQuart; + restartAnims(chart); + } + }, + { + name: 'easeOutQuint', + handler(chart) { + easing = helpers.easingEffects.easeOutQuint; + restartAnims(chart); + } + }, + { + name: 'easeInQuad', + handler(chart) { + easing = helpers.easingEffects.easeInQuad; + restartAnims(chart); + } + }, + { + name: 'easeInCubic', + handler(chart) { + easing = helpers.easingEffects.easeInCubic; + restartAnims(chart); + } + }, + { + name: 'easeInQuart', + handler(chart) { + easing = helpers.easingEffects.easeInQuart; + restartAnims(chart); + } + }, + { + name: 'easeInQuint', + handler(chart) { + easing = helpers.easingEffects.easeInQuint; + restartAnims(chart); + } + }, +]; +// + +module.exports = { + config, + actions +}; + +``` +## Api +* [Chart](../../api/classes/Chart.md) + * [`getDatasetMeta`](../../api/classes/Chart.md#getdatasetmeta) +* [Scale](../../api/classes/Scale.md) + * [`getPixelForValue`](../../api/classes/Scale.md#getpixelforvalue) +## Docs +* [Animations](../../configuration/animations.md) + * [animation](../../configuration/animations.md#animation) + * `delay` + * `duration` + * `easing` + * `loop` + * [Easing](../../configuration/animations.md#easing) +* [Line](../../charts/line.md) +* [Options](../../general/options.md) + * [Scriptable Options](../../general/options.md#scriptable-options) + * [Data Context](../../general/options.md#data) diff --git a/docs/samples/animations/progressive-line.md b/docs/samples/animations/progressive-line.md new file mode 100644 index 00000000000..5db427af736 --- /dev/null +++ b/docs/samples/animations/progressive-line.md @@ -0,0 +1,107 @@ +# Progressive Line + +```js chart-editor + +// +const data = []; +const data2 = []; +let prev = 100; +let prev2 = 80; +for (let i = 0; i < 1000; i++) { + prev += 5 - Math.random() * 10; + data.push({x: i, y: prev}); + prev2 += 5 - Math.random() * 10; + data2.push({x: i, y: prev2}); +} +// + +// +const totalDuration = 10000; +const delayBetweenPoints = totalDuration / data.length; +const previousY = (ctx) => ctx.index === 0 ? ctx.chart.scales.y.getPixelForValue(100) : ctx.chart.getDatasetMeta(ctx.datasetIndex).data[ctx.index - 1].getProps(['y'], true).y; +const animation = { + x: { + type: 'number', + easing: 'linear', + duration: delayBetweenPoints, + from: NaN, // the point is initially skipped + delay(ctx) { + if (ctx.type !== 'data' || ctx.xStarted) { + return 0; + } + ctx.xStarted = true; + return ctx.index * delayBetweenPoints; + } + }, + y: { + type: 'number', + easing: 'linear', + duration: delayBetweenPoints, + from: previousY, + delay(ctx) { + if (ctx.type !== 'data' || ctx.yStarted) { + return 0; + } + ctx.yStarted = true; + return ctx.index * delayBetweenPoints; + } + } +}; +// + +// +const config = { + type: 'line', + data: { + datasets: [{ + borderColor: Utils.CHART_COLORS.red, + borderWidth: 1, + radius: 0, + data: data, + }, + { + borderColor: Utils.CHART_COLORS.blue, + borderWidth: 1, + radius: 0, + data: data2, + }] + }, + options: { + animation, + interaction: { + intersect: false + }, + plugins: { + legend: false + }, + scales: { + x: { + type: 'linear' + } + } + } +}; +// + +module.exports = { + config +}; + +``` + +## Api +* [Chart](../../api/classes/Chart.md) + * [`getDatasetMeta`](../../api/classes/Chart.md#getdatasetmeta) +* [Scale](../../api/classes/Scale.md) + * [`getPixelForValue`](../../api/classes/Scale.md#getpixelforvalue) +## Docs +* [Animations](../../configuration/animations.md) + * [animation](../../configuration/animations.md#animation) + * `delay` + * `duration` + * `easing` + * `loop` +* [Line](../../charts/line.md) +* [Options](../../general/options.md) + * [Scriptable Options](../../general/options.md#scriptable-options) + * [Data Context](../../general/options.md#data) diff --git a/docs/samples/area/line-boundaries.md b/docs/samples/area/line-boundaries.md new file mode 100644 index 00000000000..f013db952ef --- /dev/null +++ b/docs/samples/area/line-boundaries.md @@ -0,0 +1,127 @@ +# Line Chart Boundaries + +```js chart-editor +// +const inputs = { + min: -100, + max: 100, + count: 8, + decimals: 2, + continuity: 1 +}; + +const generateLabels = () => { + return Utils.months({count: inputs.count}); +}; + +const generateData = () => (Utils.numbers(inputs)); +// + +// +const data = { + labels: generateLabels(), + datasets: [ + { + label: 'Dataset', + data: generateData(), + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red), + fill: false + } + ] +}; +// + +// +let smooth = false; + +const actions = [ + { + name: 'Fill: false (default)', + handler: (chart) => { + chart.data.datasets.forEach(dataset => { + dataset.fill = false; + }); + chart.update(); + } + }, + { + name: 'Fill: origin', + handler: (chart) => { + chart.data.datasets.forEach(dataset => { + dataset.fill = 'origin'; + }); + chart.update(); + } + }, + { + name: 'Fill: start', + handler: (chart) => { + chart.data.datasets.forEach(dataset => { + dataset.fill = 'start'; + }); + chart.update(); + } + }, + { + name: 'Fill: end', + handler: (chart) => { + chart.data.datasets.forEach(dataset => { + dataset.fill = 'end'; + }); + chart.update(); + } + }, + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = generateData(); + }); + chart.update(); + } + }, + { + name: 'Smooth', + handler(chart) { + smooth = !smooth; + chart.options.elements.line.tension = smooth ? 0.4 : 0; + chart.update(); + } + } +]; +// + +// +const config = { + type: 'line', + data: data, + options: { + plugins: { + filler: { + propagate: false, + }, + title: { + display: true, + text: (ctx) => 'Fill: ' + ctx.chart.data.datasets[0].fill + } + }, + interaction: { + intersect: false, + } + }, +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` + +## Docs +* [Area](../../charts/area.md) + * [Filling modes](../../charts/area.md#filling-modes) + * Boundary: `'start'`, `'end'`, `'origin'` +* [Line](../../charts/line.md) +* [Data structures (`labels`)](../../general/data-structures.md) diff --git a/docs/samples/area/line-datasets.md b/docs/samples/area/line-datasets.md new file mode 100644 index 00000000000..0b380054b6b --- /dev/null +++ b/docs/samples/area/line-datasets.md @@ -0,0 +1,174 @@ +# Line Chart Datasets + +```js chart-editor +// +const inputs = { + min: 20, + max: 80, + count: 8, + decimals: 2, + continuity: 1 +}; + +const generateLabels = () => { + return Utils.months({count: inputs.count}); +}; + +const generateData = () => (Utils.numbers(inputs)); + +Utils.srand(42); +// + +// +const data = { + labels: generateLabels(), + datasets: [ + { + label: 'D0', + data: generateData(), + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red), + hidden: true + }, + { + label: 'D1', + data: generateData(), + borderColor: Utils.CHART_COLORS.orange, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.orange), + fill: '-1' + }, + { + label: 'D2', + data: generateData(), + borderColor: Utils.CHART_COLORS.yellow, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.yellow), + hidden: true, + fill: 1 + }, + { + label: 'D3', + data: generateData(), + borderColor: Utils.CHART_COLORS.green, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.green), + fill: '-1' + }, + { + label: 'D4', + data: generateData(), + borderColor: Utils.CHART_COLORS.blue, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue), + fill: '-1' + }, + { + label: 'D5', + data: generateData(), + borderColor: Utils.CHART_COLORS.grey, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.grey), + fill: '+2' + }, + { + label: 'D6', + data: generateData(), + borderColor: Utils.CHART_COLORS.purple, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.purple), + fill: false + }, + { + label: 'D7', + data: generateData(), + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red), + fill: 8 + }, + { + label: 'D8', + data: generateData(), + borderColor: Utils.CHART_COLORS.orange, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.orange), + fill: 'end', + hidden: true + }, + { + label: 'D9', + data: generateData(), + borderColor: Utils.CHART_COLORS.yellow, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.yellow), + fill: {above: 'blue', below: 'red', target: {value: 350}} + } + ] +}; +// + +// +let smooth = false; +let propagate = false; + +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = generateData(); + }); + chart.update(); + } + }, + { + name: 'Propagate', + handler(chart) { + propagate = !propagate; + chart.options.plugins.filler.propagate = propagate; + chart.update(); + } + }, + { + name: 'Smooth', + handler(chart) { + smooth = !smooth; + chart.options.elements.line.tension = smooth ? 0.4 : 0; + chart.update(); + } + } +]; +// + +// +const config = { + type: 'line', + data: data, + options: { + scales: { + y: { + stacked: true + } + }, + plugins: { + filler: { + propagate: false + }, + 'samples-filler-analyser': { + target: 'chart-analyser' + } + }, + interaction: { + intersect: false, + }, + }, +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` + +
    + +## Docs +* [Area](../../charts/area.md) + * [Filling modes](../../charts/area.md#filling-modes) +* [Line](../../charts/line.md) +* [Data structures (`labels`)](../../general/data-structures.md) +* [Axes scales](../../axes/) + * [Common options to all axes (`stacked`)](../../axes/#common-options-to-all-axes) diff --git a/docs/samples/area/line-drawtime.md b/docs/samples/area/line-drawtime.md new file mode 100644 index 00000000000..8cf5198fc81 --- /dev/null +++ b/docs/samples/area/line-drawtime.md @@ -0,0 +1,121 @@ +# Line Chart drawTime + +```js chart-editor +// +const inputs = { + min: -100, + max: 100, + count: 8, + decimals: 2, + continuity: 1 +}; + +const generateLabels = () => { + return Utils.months({count: inputs.count}); +}; + +Utils.srand(3); +const generateData = () => (Utils.numbers(inputs)); +// + +// +const data = { + labels: generateLabels(), + datasets: [ + { + label: 'Dataset 1', + data: generateData(), + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.CHART_COLORS.red, + fill: true + }, + { + label: 'Dataset 2', + data: generateData(), + borderColor: Utils.CHART_COLORS.blue, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue), + fill: true + } + ] +}; +// + +// +let smooth = false; + +const actions = [ + { + name: 'drawTime: beforeDatasetDraw (default)', + handler: (chart) => { + chart.options.plugins.filler.drawTime = 'beforeDatasetDraw'; + chart.update(); + } + }, + { + name: 'drawTime: beforeDatasetsDraw', + handler: (chart) => { + chart.options.plugins.filler.drawTime = 'beforeDatasetsDraw'; + chart.update(); + } + }, + { + name: 'drawTime: beforeDraw', + handler: (chart) => { + chart.options.plugins.filler.drawTime = 'beforeDraw'; + chart.update(); + } + }, + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = generateData(); + }); + chart.update(); + } + }, + { + name: 'Smooth', + handler(chart) { + smooth = !smooth; + chart.options.elements.line.tension = smooth ? 0.4 : 0; + chart.update(); + } + } +]; +// + +// +const config = { + type: 'line', + data: data, + options: { + plugins: { + filler: { + propagate: false, + }, + title: { + display: true, + text: (ctx) => 'drawTime: ' + ctx.chart.options.plugins.filler.drawTime + } + }, + pointBackgroundColor: '#fff', + radius: 10, + interaction: { + intersect: false, + } + }, +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` +## Docs +* [Area](../../charts/area.md) + * [Configuration (`drawTime`)](../../charts/area.md#configuration) +* [Line](../../charts/line.md) + * [Line Styling (`tension`)](../../charts/line.md#line-styling) +* [Data structures (`labels`)](../../general/data-structures.md) diff --git a/docs/samples/area/line-stacked.md b/docs/samples/area/line-stacked.md new file mode 100644 index 00000000000..a711d125fb2 --- /dev/null +++ b/docs/samples/area/line-stacked.md @@ -0,0 +1,180 @@ +# Line Chart Stacked + +```js chart-editor +// +const actions = [ + { + name: 'Stacked: true', + handler: (chart) => { + chart.options.scales.y.stacked = true; + chart.update(); + } + }, + { + name: 'Stacked: false (default)', + handler: (chart) => { + chart.options.scales.y.stacked = false; + chart.update(); + } + }, + { + name: 'Stacked Single', + handler: (chart) => { + chart.options.scales.y.stacked = 'single'; + chart.update(); + } + }, + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = Utils.numbers({count: chart.data.labels.length, min: -100, max: 100}); + }); + chart.update(); + } + }, + { + name: 'Add Dataset', + handler(chart) { + const data = chart.data; + const dsColor = Utils.namedColor(chart.data.datasets.length); + const newDataset = { + label: 'Dataset ' + (data.datasets.length + 1), + backgroundColor: dsColor, + borderColor: dsColor, + fill: true, + data: Utils.numbers({count: data.labels.length, min: -100, max: 100}), + }; + chart.data.datasets.push(newDataset); + chart.update(); + } + }, + { + name: 'Add Data', + handler(chart) { + const data = chart.data; + if (data.datasets.length > 0) { + data.labels = Utils.months({count: data.labels.length + 1}); + + for (let index = 0; index < data.datasets.length; ++index) { + data.datasets[index].data.push(Utils.rand(-100, 100)); + } + + chart.update(); + } + } + }, + { + name: 'Remove Dataset', + handler(chart) { + chart.data.datasets.pop(); + chart.update(); + } + }, + { + name: 'Remove Data', + handler(chart) { + chart.data.labels.splice(-1, 1); // remove the label first + + chart.data.datasets.forEach(dataset => { + dataset.data.pop(); + }); + + chart.update(); + } + } +]; +// + +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; + +const labels = Utils.months({count: 7}); +const data = { + labels: labels, + datasets: [ + { + label: 'My First dataset', + data: Utils.numbers(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.CHART_COLORS.red, + fill: true + }, + { + label: 'My Second dataset', + data: Utils.numbers(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.blue, + backgroundColor: Utils.CHART_COLORS.blue, + fill: true + }, + { + label: 'My Third dataset', + data: Utils.numbers(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.green, + backgroundColor: Utils.CHART_COLORS.green, + fill: true + }, + { + label: 'My Fourth dataset', + data: Utils.numbers(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.yellow, + backgroundColor: Utils.CHART_COLORS.yellow, + fill: true + } + ] +}; +// + +// +const config = { + type: 'line', + data: data, + options: { + responsive: true, + plugins: { + title: { + display: true, + text: (ctx) => 'Chart.js Line Chart - stacked=' + ctx.chart.options.scales.y.stacked + }, + tooltip: { + mode: 'index' + }, + }, + interaction: { + mode: 'nearest', + axis: 'x', + intersect: false + }, + scales: { + x: { + title: { + display: true, + text: 'Month' + } + }, + y: { + stacked: true, + title: { + display: true, + text: 'Value' + } + } + } + } +}; +// + +module.exports = { + actions: actions, + config: config +}; +``` + +## Docs +* [Area](../../charts/area.md) + * [Filling modes](../../charts/area.md#filling-modes) +* [Line](../../charts/line.md) +* [Data structures (`labels`)](../../general/data-structures.md) +* [Axes scales](../../axes/) + * [Common options to all axes (`stacked`)](../../axes/#common-options-to-all-axes) diff --git a/docs/samples/area/radar.md b/docs/samples/area/radar.md new file mode 100644 index 00000000000..66da8bf6190 --- /dev/null +++ b/docs/samples/area/radar.md @@ -0,0 +1,148 @@ +# Radar Chart Stacked + +```js chart-editor +// +const inputs = { + min: 8, + max: 16, + count: 8, + decimals: 2, + continuity: 1 +}; + +const generateLabels = () => { + return Utils.months({count: inputs.count}); +}; + +const generateData = () => { + const values = Utils.numbers(inputs); + inputs.from = values; + return values; +}; + +const labels = Utils.months({count: 8}); +const data = { + labels: generateLabels(), + datasets: [ + { + label: 'D0', + data: generateData(), + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red), + }, + { + label: 'D1', + data: generateData(), + borderColor: Utils.CHART_COLORS.orange, + hidden: true, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.orange), + fill: '-1' + }, + { + label: 'D2', + data: generateData(), + borderColor: Utils.CHART_COLORS.yellow, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.yellow), + fill: 1 + }, + { + label: 'D3', + data: generateData(), + borderColor: Utils.CHART_COLORS.green, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.green), + fill: false + }, + { + label: 'D4', + data: generateData(), + borderColor: Utils.CHART_COLORS.blue, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue), + fill: '-1' + }, + { + label: 'D5', + data: generateData(), + borderColor: Utils.CHART_COLORS.purple, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.purple), + fill: '-1' + }, + { + label: 'D6', + data: generateData(), + borderColor: Utils.CHART_COLORS.grey, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.grey), + fill: {value: 85} + } + ] +}; +// + +// +let smooth = false; +let propagate = false; + +const actions = [ + { + name: 'Randomize', + handler(chart) { + inputs.from = []; + chart.data.datasets.forEach(dataset => { + dataset.data = generateData(); + }); + chart.update(); + } + }, + { + name: 'Propagate', + handler(chart) { + propagate = !propagate; + chart.options.plugins.filler.propagate = propagate; + chart.update(); + + } + }, + { + name: 'Smooth', + handler(chart) { + smooth = !smooth; + chart.options.elements.line.tension = smooth ? 0.4 : 0; + chart.update(); + } + } +]; +// + +// +const config = { + type: 'radar', + data: data, + options: { + plugins: { + filler: { + propagate: false + }, + 'samples-filler-analyser': { + target: 'chart-analyser' + } + }, + interaction: { + intersect: false + } + } +}; +// + +module.exports = { + actions: actions, + config: config +}; +``` + +
    + +## Docs +* [Area](../../charts/area.md) + * [Filling modes](../../charts/area.md#filling-modes) + * [`propagate`](../../charts/area.md#propagate) +* [Radar](../../charts/radar.md) +* [Data structures (`labels`)](../../general/data-structures.md) diff --git a/docs/samples/bar/border-radius.md b/docs/samples/bar/border-radius.md new file mode 100644 index 00000000000..6cf5e03494e --- /dev/null +++ b/docs/samples/bar/border-radius.md @@ -0,0 +1,76 @@ +# Bar Chart Border Radius + +```js chart-editor +// +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = Utils.numbers({count: chart.data.labels.length, min: -100, max: 100}); + }); + chart.update(); + } + }, +]; +// + +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; + +const labels = Utils.months({count: 7}); +const data = { + labels: labels, + datasets: [ + { + label: 'Fully Rounded', + data: Utils.numbers(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + borderWidth: 2, + borderRadius: Number.MAX_VALUE, + borderSkipped: false, + }, + { + label: 'Small Radius', + data: Utils.numbers(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.blue, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), + borderWidth: 2, + borderRadius: 5, + borderSkipped: false, + } + ] +}; +// + +// +const config = { + type: 'bar', + data: data, + options: { + responsive: true, + plugins: { + legend: { + position: 'top', + }, + title: { + display: true, + text: 'Chart.js Bar Chart' + } + } + }, +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` + +## Docs +* [Bar](../../charts/bar.md) + * [`borderRadius`](../../charts/bar.md#borderradius) +* [Data structures (`labels`)](../../general/data-structures.md) diff --git a/docs/samples/bar/floating.md b/docs/samples/bar/floating.md new file mode 100644 index 00000000000..88832433d17 --- /dev/null +++ b/docs/samples/bar/floating.md @@ -0,0 +1,74 @@ +# Floating Bars + +Using `[number, number][]` as the type for `data` to define the beginning and end value for each bar. This is instead of having every bar start at 0. + +```js chart-editor +// +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = chart.data.labels.map(() => { + return [Utils.rand(-100, 100), Utils.rand(-100, 100)]; + }); + }); + chart.update(); + } + }, +]; +// + +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; + +const labels = Utils.months({count: 7}); +const data = { + labels: labels, + datasets: [ + { + label: 'Dataset 1', + data: labels.map(() => { + return [Utils.rand(-100, 100), Utils.rand(-100, 100)]; + }), + backgroundColor: Utils.CHART_COLORS.red, + }, + { + label: 'Dataset 2', + data: labels.map(() => { + return [Utils.rand(-100, 100), Utils.rand(-100, 100)]; + }), + backgroundColor: Utils.CHART_COLORS.blue, + }, + ] +}; +// + +// +const config = { + type: 'bar', + data: data, + options: { + responsive: true, + plugins: { + legend: { + position: 'top', + }, + title: { + display: true, + text: 'Chart.js Floating Bar Chart' + } + } + } +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` +## Docs +* [Bar](../../charts/bar.md) +* [Data structures (`labels`)](../../general/data-structures.md) diff --git a/docs/samples/bar/horizontal.md b/docs/samples/bar/horizontal.md new file mode 100644 index 00000000000..85a17a421d5 --- /dev/null +++ b/docs/samples/bar/horizontal.md @@ -0,0 +1,128 @@ +# Horizontal Bar Chart + +```js chart-editor +// +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = Utils.numbers({count: chart.data.labels.length, min: -100, max: 100}); + }); + chart.update(); + } + }, + { + name: 'Add Dataset', + handler(chart) { + const data = chart.data; + const dsColor = Utils.namedColor(chart.data.datasets.length); + const newDataset = { + label: 'Dataset ' + (data.datasets.length + 1), + backgroundColor: Utils.transparentize(dsColor, 0.5), + borderColor: dsColor, + borderWidth: 1, + data: Utils.numbers({count: data.labels.length, min: -100, max: 100}), + }; + chart.data.datasets.push(newDataset); + chart.update(); + } + }, + { + name: 'Add Data', + handler(chart) { + const data = chart.data; + if (data.datasets.length > 0) { + data.labels = Utils.months({count: data.labels.length + 1}); + + for (let index = 0; index < data.datasets.length; ++index) { + data.datasets[index].data.push(Utils.rand(-100, 100)); + } + + chart.update(); + } + } + }, + { + name: 'Remove Dataset', + handler(chart) { + chart.data.datasets.pop(); + chart.update(); + } + }, + { + name: 'Remove Data', + handler(chart) { + chart.data.labels.splice(-1, 1); // remove the label first + + chart.data.datasets.forEach(dataset => { + dataset.data.pop(); + }); + + chart.update(); + } + } +]; +// + +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; + +const labels = Utils.months({count: 7}); +const data = { + labels: labels, + datasets: [ + { + label: 'Dataset 1', + data: Utils.numbers(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + }, + { + label: 'Dataset 2', + data: Utils.numbers(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.blue, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), + } + ] +}; +// + +// +const config = { + type: 'bar', + data: data, + options: { + indexAxis: 'y', + // Elements options apply to all of the options unless overridden in a dataset + // In this case, we are setting the border of each horizontal bar to be 2px wide + elements: { + bar: { + borderWidth: 2, + } + }, + responsive: true, + plugins: { + legend: { + position: 'right', + }, + title: { + display: true, + text: 'Chart.js Horizontal Bar Chart' + } + } + }, +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` + +## Docs +* [Bar](../../charts/bar.md) + * [Horizontal Bar Chart](../../charts/bar.md#horizontal-bar-chart) + diff --git a/docs/samples/bar/stacked-groups.md b/docs/samples/bar/stacked-groups.md new file mode 100644 index 00000000000..f9dac39b4e8 --- /dev/null +++ b/docs/samples/bar/stacked-groups.md @@ -0,0 +1,88 @@ +# Stacked Bar Chart with Groups + +Using the `stack` property to divide datasets into multiple stacks. + +```js chart-editor +// +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = Utils.numbers({count: chart.data.labels.length, min: -100, max: 100}); + }); + chart.update(); + } + }, +]; +// + +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; + +const labels = Utils.months({count: 7}); +const data = { + labels: labels, + datasets: [ + { + label: 'Dataset 1', + data: Utils.numbers(NUMBER_CFG), + backgroundColor: Utils.CHART_COLORS.red, + stack: 'Stack 0', + }, + { + label: 'Dataset 2', + data: Utils.numbers(NUMBER_CFG), + backgroundColor: Utils.CHART_COLORS.blue, + stack: 'Stack 0', + }, + { + label: 'Dataset 3', + data: Utils.numbers(NUMBER_CFG), + backgroundColor: Utils.CHART_COLORS.green, + stack: 'Stack 1', + }, + ] +}; +// + +// +const config = { + type: 'bar', + data: data, + options: { + plugins: { + title: { + display: true, + text: 'Chart.js Bar Chart - Stacked' + }, + }, + responsive: true, + interaction: { + intersect: false, + }, + scales: { + x: { + stacked: true, + }, + y: { + stacked: true + } + } + } +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` + +## Docs +* [Bar](../../charts/bar.md) + * [Stacked Bar Chart](../../charts/bar.md#stacked-bar-chart) +* [Data structures (`labels`)](../../general/data-structures.md) + * [Dataset Configuration (`stack`)](../../general/data-structures.md#dataset-configuration) + diff --git a/docs/samples/bar/stacked.md b/docs/samples/bar/stacked.md new file mode 100644 index 00000000000..6e2639c4b2e --- /dev/null +++ b/docs/samples/bar/stacked.md @@ -0,0 +1,77 @@ +# Stacked Bar Chart + +```js chart-editor +// +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = Utils.numbers({count: chart.data.labels.length, min: -100, max: 100}); + }); + chart.update(); + } + }, +]; +// + +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; + +const labels = Utils.months({count: 7}); +const data = { + labels: labels, + datasets: [ + { + label: 'Dataset 1', + data: Utils.numbers(NUMBER_CFG), + backgroundColor: Utils.CHART_COLORS.red, + }, + { + label: 'Dataset 2', + data: Utils.numbers(NUMBER_CFG), + backgroundColor: Utils.CHART_COLORS.blue, + }, + { + label: 'Dataset 3', + data: Utils.numbers(NUMBER_CFG), + backgroundColor: Utils.CHART_COLORS.green, + }, + ] +}; +// + +// +const config = { + type: 'bar', + data: data, + options: { + plugins: { + title: { + display: true, + text: 'Chart.js Bar Chart - Stacked' + }, + }, + responsive: true, + scales: { + x: { + stacked: true, + }, + y: { + stacked: true + } + } + } +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` +## Docs +* [Bar](../../charts/bar.md) + * [Stacked Bar Chart](../../charts/bar.md#stacked-bar-chart) + diff --git a/docs/samples/bar/vertical.md b/docs/samples/bar/vertical.md new file mode 100644 index 00000000000..e14859cdd02 --- /dev/null +++ b/docs/samples/bar/vertical.md @@ -0,0 +1,119 @@ +# Vertical Bar Chart + +```js chart-editor +// +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = Utils.numbers({count: chart.data.labels.length, min: -100, max: 100}); + }); + chart.update(); + } + }, + { + name: 'Add Dataset', + handler(chart) { + const data = chart.data; + const dsColor = Utils.namedColor(chart.data.datasets.length); + const newDataset = { + label: 'Dataset ' + (data.datasets.length + 1), + backgroundColor: Utils.transparentize(dsColor, 0.5), + borderColor: dsColor, + borderWidth: 1, + data: Utils.numbers({count: data.labels.length, min: -100, max: 100}), + }; + chart.data.datasets.push(newDataset); + chart.update(); + } + }, + { + name: 'Add Data', + handler(chart) { + const data = chart.data; + if (data.datasets.length > 0) { + data.labels = Utils.months({count: data.labels.length + 1}); + + for (let index = 0; index < data.datasets.length; ++index) { + data.datasets[index].data.push(Utils.rand(-100, 100)); + } + + chart.update(); + } + } + }, + { + name: 'Remove Dataset', + handler(chart) { + chart.data.datasets.pop(); + chart.update(); + } + }, + { + name: 'Remove Data', + handler(chart) { + chart.data.labels.splice(-1, 1); // remove the label first + + chart.data.datasets.forEach(dataset => { + dataset.data.pop(); + }); + + chart.update(); + } + } +]; +// + +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; + +const labels = Utils.months({count: 7}); +const data = { + labels: labels, + datasets: [ + { + label: 'Dataset 1', + data: Utils.numbers(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + }, + { + label: 'Dataset 2', + data: Utils.numbers(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.blue, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), + } + ] +}; +// + +// +const config = { + type: 'bar', + data: data, + options: { + responsive: true, + plugins: { + legend: { + position: 'top', + }, + title: { + display: true, + text: 'Chart.js Bar Chart' + } + } + }, +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` + +## Docs +* [Bar](../../charts/bar.md) +* [Data structures (`labels`)](../../general/data-structures.md) diff --git a/docs/samples/information.md b/docs/samples/information.md new file mode 100644 index 00000000000..3cb5eb6f860 --- /dev/null +++ b/docs/samples/information.md @@ -0,0 +1,15 @@ +# Chart.js Samples + +You can navigate through the samples via the sidebar. + +Alternatively, you can run them locally. To do so, clone the [Chart.js repository](https://github.com/chartjs/Chart.js) from GitHub, run `pnpm ci` to install all packages, then run `pnpm run docs:dev` to build the documentation. As soon as the build is done, you can go to [localhost:8080/samples](http://localhost:8080/samples/) to see the samples. + +## Out of the box working samples +These samples are made for demonstration purposes only. They won't work out of the box if you copy paste them into your own website. This is because of how the docs are getting built. Some boilerplate code gets hidden. +For a sample that can be copied and pasted and used directly you can check the [usage page](../getting-started/usage.md). + +## Autogenerated data +The data used in the samples is autogenerated using custom functions. These functions do not ship with the library, for more information about this you can check the [utils page](./utils.md). + +## Actions block +The samples have an `actions` code block. These actions are not part of Chart.js. They are internally transformed to separate buttons together with `onClick` listeners by a plugin we use in the documentation. To implement such actions yourself you can make some buttons and add `onClick` event listeners to them. Then in these event listeners you can call your variable in which you made the chart and do the logic that the button is supposed to do. diff --git a/docs/samples/legend/events.md b/docs/samples/legend/events.md new file mode 100644 index 00000000000..fa715445ad5 --- /dev/null +++ b/docs/samples/legend/events.md @@ -0,0 +1,63 @@ +# Events + +This sample demonstrates how to use the event hooks to highlight chart elements. + +```js chart-editor + +// +const data = { + labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], + datasets: [{ + label: '# of Votes', + data: [12, 19, 3, 5, 2, 3], + borderWidth: 1, + backgroundColor: ['#CB4335', '#1F618D', '#F1C40F', '#27AE60', '#884EA0', '#D35400'], + }] +}; +// + +// +// Append '4d' to the colors (alpha channel), except for the hovered index +function handleHover(evt, item, legend) { + legend.chart.data.datasets[0].backgroundColor.forEach((color, index, colors) => { + colors[index] = index === item.index || color.length === 9 ? color : color + '4D'; + }); + legend.chart.update(); +} +// + +// +// Removes the alpha channel from background colors +function handleLeave(evt, item, legend) { + legend.chart.data.datasets[0].backgroundColor.forEach((color, index, colors) => { + colors[index] = color.length === 9 ? color.slice(0, -2) : color; + }); + legend.chart.update(); +} +// + +// +const config = { + type: 'pie', + data: data, + options: { + plugins: { + legend: { + onHover: handleHover, + onLeave: handleLeave + } + } + } +}; +// + +module.exports = { + config +}; +``` + +## Docs +* [Doughnut and Pie Charts](../../charts/doughnut.md) +* [Legend](../../configuration/legend.md) + * `onHover` + * `onLeave` \ No newline at end of file diff --git a/docs/samples/legend/html.md b/docs/samples/legend/html.md new file mode 100644 index 00000000000..3628059428b --- /dev/null +++ b/docs/samples/legend/html.md @@ -0,0 +1,142 @@ +# HTML Legend + +This example shows how to create a custom HTML legend using a plugin and connect it to the chart in lieu of the default on-canvas legend. +For an html legend to work you need to place an empty div at your web page with the ID you provide in the options to bind to like so: `
    `. + +
    + +```js chart-editor +// +const getOrCreateLegendList = (chart, id) => { + const legendContainer = document.getElementById(id); + let listContainer = legendContainer.querySelector('ul'); + + if (!listContainer) { + listContainer = document.createElement('ul'); + listContainer.style.display = 'flex'; + listContainer.style.flexDirection = 'row'; + listContainer.style.margin = 0; + listContainer.style.padding = 0; + + legendContainer.appendChild(listContainer); + } + + return listContainer; +}; + +const htmlLegendPlugin = { + id: 'htmlLegend', + afterUpdate(chart, args, options) { + const ul = getOrCreateLegendList(chart, options.containerID); + + // Remove old legend items + while (ul.firstChild) { + ul.firstChild.remove(); + } + + // Reuse the built-in legendItems generator + const items = chart.options.plugins.legend.labels.generateLabels(chart); + + items.forEach(item => { + const li = document.createElement('li'); + li.style.alignItems = 'center'; + li.style.cursor = 'pointer'; + li.style.display = 'flex'; + li.style.flexDirection = 'row'; + li.style.marginLeft = '10px'; + + li.onclick = () => { + const {type} = chart.config; + if (type === 'pie' || type === 'doughnut') { + // Pie and doughnut charts only have a single dataset and visibility is per item + chart.toggleDataVisibility(item.index); + } else { + chart.setDatasetVisibility(item.datasetIndex, !chart.isDatasetVisible(item.datasetIndex)); + } + chart.update(); + }; + + // Color box + const boxSpan = document.createElement('span'); + boxSpan.style.background = item.fillStyle; + boxSpan.style.borderColor = item.strokeStyle; + boxSpan.style.borderWidth = item.lineWidth + 'px'; + boxSpan.style.display = 'inline-block'; + boxSpan.style.flexShrink = 0; + boxSpan.style.height = '20px'; + boxSpan.style.marginRight = '10px'; + boxSpan.style.width = '20px'; + + // Text + const textContainer = document.createElement('p'); + textContainer.style.color = item.fontColor; + textContainer.style.margin = 0; + textContainer.style.padding = 0; + textContainer.style.textDecoration = item.hidden ? 'line-through' : ''; + + const text = document.createTextNode(item.text); + textContainer.appendChild(text); + + li.appendChild(boxSpan); + li.appendChild(textContainer); + ul.appendChild(li); + }); + } +}; +// + +// +const NUM_DATA = 7; +const NUM_CFG = {count: NUM_DATA, min: 0, max: 100}; +const data = { + labels: Utils.months({count: NUM_DATA}), + datasets: [ + { + label: 'Dataset: 1', + data: Utils.numbers(NUM_CFG), + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + fill: false, + }, + { + label: 'Dataset: 1', + data: Utils.numbers(NUM_CFG), + borderColor: Utils.CHART_COLORS.blue, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), + fill: false, + }, + ], +}; +// + +// +const config = { + type: 'line', + data: data, + options: { + plugins: { + htmlLegend: { + // ID of the container to put the legend in + containerID: 'legend-container', + }, + legend: { + display: false, + } + } + }, + plugins: [htmlLegendPlugin], +}; +// + +module.exports = { + actions: [], + config: config, +}; +``` + +## Docs +* [Data structures (`labels`)](../../general/data-structures.md) +* [Line](../../charts/line.md) +* [Legend](../../configuration/legend.md) + * `display: false` +* [Plugins](../../developers/plugins.md) diff --git a/docs/samples/legend/point-style.md b/docs/samples/legend/point-style.md new file mode 100644 index 00000000000..355045c676e --- /dev/null +++ b/docs/samples/legend/point-style.md @@ -0,0 +1,69 @@ +# Point Style + +This sample show how to use the dataset point style in the legend instead of a rectangle to identify each dataset.. + +```js chart-editor +// +const actions = [ + { + name: 'Toggle Point Style', + handler(chart) { + chart.options.plugins.legend.labels.usePointStyle = !chart.options.plugins.legend.labels.usePointStyle; + chart.update(); + } + }, +]; +// + +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; +const data = { + labels: Utils.months({count: DATA_COUNT}), + datasets: [ + { + label: 'Dataset 1', + data: Utils.numbers(NUMBER_CFG), + fill: false, + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + borderWidth: 1, + pointStyle: 'rectRot', + pointRadius: 5, + pointBorderColor: 'rgb(0, 0, 0)' + }, + ] +}; +// + +// +const config = { + type: 'line', + data: data, + options: { + plugins: { + legend: { + labels: { + usePointStyle: true, + }, + } + } + } +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` + +## Docs +* [Data structures (`labels`)](../../general/data-structures.md) +* [Line](../../charts/line.md) +* [Legend](../../configuration/legend.md) + * [Legend Label Configuration](../../configuration/legend.md#legend-label-configuration) + * `usePointStyle` +* [Elements](../../configuration/elements.md) + * [Point Configuration](../../configuration/elements.md#point-configuration) + * [Point Styles](../../configuration/elements.md#point-styles) diff --git a/docs/samples/legend/position.md b/docs/samples/legend/position.md new file mode 100644 index 00000000000..8597f4ed15b --- /dev/null +++ b/docs/samples/legend/position.md @@ -0,0 +1,74 @@ +# Position + +This sample show how to change the position of the chart legend. + +```js chart-editor +// +const actions = [ + { + name: 'Position: top', + handler(chart) { + chart.options.plugins.legend.position = 'top'; + chart.update(); + } + }, + { + name: 'Position: right', + handler(chart) { + chart.options.plugins.legend.position = 'right'; + chart.update(); + } + }, + { + name: 'Position: bottom', + handler(chart) { + chart.options.plugins.legend.position = 'bottom'; + chart.update(); + } + }, + { + name: 'Position: left', + handler(chart) { + chart.options.plugins.legend.position = 'left'; + chart.update(); + } + }, +]; +// + + +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; +const data = { + labels: Utils.months({count: DATA_COUNT}), + datasets: [ + { + label: 'Dataset 1', + data: Utils.numbers(NUMBER_CFG), + fill: false, + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + }, + ] +}; +// + +// +const config = { + type: 'line', + data: data, +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` + +## Docs +* [Data structures (`labels`)](../../general/data-structures.md) +* [Line](../../charts/line.md) +* [Legend](../../configuration/legend.md) + * [Position](../../configuration/legend.md#position) diff --git a/docs/samples/legend/title.md b/docs/samples/legend/title.md new file mode 100644 index 00000000000..98ba86f404c --- /dev/null +++ b/docs/samples/legend/title.md @@ -0,0 +1,79 @@ +# Alignment and Title Position + +This sample show how to configure the alignment and title position of the chart legend. + +```js chart-editor +// +const actions = [ + { + name: 'Title Position: start', + handler(chart) { + chart.options.plugins.legend.align = 'start'; + chart.options.plugins.legend.title.position = 'start'; + chart.update(); + } + }, + { + name: 'Title Position: center (default)', + handler(chart) { + chart.options.plugins.legend.align = 'center'; + chart.options.plugins.legend.title.position = 'center'; + chart.update(); + } + }, + { + name: 'Title Position: end', + handler(chart) { + chart.options.plugins.legend.align = 'end'; + chart.options.plugins.legend.title.position = 'end'; + chart.update(); + } + }, +]; +// + + +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; +const data = { + labels: Utils.months({count: DATA_COUNT}), + datasets: [ + { + label: 'Dataset 1', + data: Utils.numbers(NUMBER_CFG), + fill: false, + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + }, + ] +}; +// + +// +const config = { + type: 'line', + data: data, + options: { + plugins: { + legend: { + title: { + display: true, + text: 'Legend Title', + } + } + } + } +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` + +## Docs +* [Data structures (`labels`)](../../general/data-structures.md) +* [Line](../../charts/line.md) +* [Legend](../../configuration/legend.md) \ No newline at end of file diff --git a/docs/samples/line/interpolation.md b/docs/samples/line/interpolation.md new file mode 100644 index 00000000000..9e7bfa151f6 --- /dev/null +++ b/docs/samples/line/interpolation.md @@ -0,0 +1,83 @@ +# Interpolation Modes + +```js chart-editor +// +const DATA_COUNT = 12; +const labels = []; +for (let i = 0; i < DATA_COUNT; ++i) { + labels.push(i.toString()); +} +const datapoints = [0, 20, 20, 60, 60, 120, NaN, 180, 120, 125, 105, 110, 170]; +const data = { + labels: labels, + datasets: [ + { + label: 'Cubic interpolation (monotone)', + data: datapoints, + borderColor: Utils.CHART_COLORS.red, + fill: false, + cubicInterpolationMode: 'monotone', + tension: 0.4 + }, { + label: 'Cubic interpolation', + data: datapoints, + borderColor: Utils.CHART_COLORS.blue, + fill: false, + tension: 0.4 + }, { + label: 'Linear interpolation (default)', + data: datapoints, + borderColor: Utils.CHART_COLORS.green, + fill: false + } + ] +}; +// + +// +const config = { + type: 'line', + data: data, + options: { + responsive: true, + plugins: { + title: { + display: true, + text: 'Chart.js Line Chart - Cubic interpolation mode' + }, + }, + interaction: { + intersect: false, + }, + scales: { + x: { + display: true, + title: { + display: true + } + }, + y: { + display: true, + title: { + display: true, + text: 'Value' + }, + suggestedMin: -10, + suggestedMax: 200 + } + } + }, +}; +// + +module.exports = { + actions: [], + config: config, +}; +``` + +## Docs +* [Line](../../charts/line.md) + * [`cubicInterpolationMode`](../../charts/line.md#cubicinterpolationmode) + * [Line Styling (`tension`)](../../charts/line.md#line-styling) + diff --git a/docs/samples/line/line.md b/docs/samples/line/line.md new file mode 100644 index 00000000000..c3682f69093 --- /dev/null +++ b/docs/samples/line/line.md @@ -0,0 +1,118 @@ +# Line Chart + +```js chart-editor +// +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = Utils.numbers({count: chart.data.labels.length, min: -100, max: 100}); + }); + chart.update(); + } + }, + { + name: 'Add Dataset', + handler(chart) { + const data = chart.data; + const dsColor = Utils.namedColor(chart.data.datasets.length); + const newDataset = { + label: 'Dataset ' + (data.datasets.length + 1), + backgroundColor: Utils.transparentize(dsColor, 0.5), + borderColor: dsColor, + data: Utils.numbers({count: data.labels.length, min: -100, max: 100}), + }; + chart.data.datasets.push(newDataset); + chart.update(); + } + }, + { + name: 'Add Data', + handler(chart) { + const data = chart.data; + if (data.datasets.length > 0) { + data.labels = Utils.months({count: data.labels.length + 1}); + + for (let index = 0; index < data.datasets.length; ++index) { + data.datasets[index].data.push(Utils.rand(-100, 100)); + } + + chart.update(); + } + } + }, + { + name: 'Remove Dataset', + handler(chart) { + chart.data.datasets.pop(); + chart.update(); + } + }, + { + name: 'Remove Data', + handler(chart) { + chart.data.labels.splice(-1, 1); // remove the label first + + chart.data.datasets.forEach(dataset => { + dataset.data.pop(); + }); + + chart.update(); + } + } +]; +// + +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; + +const labels = Utils.months({count: 7}); +const data = { + labels: labels, + datasets: [ + { + label: 'Dataset 1', + data: Utils.numbers(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + }, + { + label: 'Dataset 2', + data: Utils.numbers(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.blue, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), + } + ] +}; +// + +// +const config = { + type: 'line', + data: data, + options: { + responsive: true, + plugins: { + legend: { + position: 'top', + }, + title: { + display: true, + text: 'Chart.js Line Chart' + } + } + }, +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` + +## Docs +* [Line](../../charts/line.md) +* [Data structures (`labels`)](../../general/data-structures.md) diff --git a/docs/samples/line/multi-axis.md b/docs/samples/line/multi-axis.md new file mode 100644 index 00000000000..d1c625517ca --- /dev/null +++ b/docs/samples/line/multi-axis.md @@ -0,0 +1,94 @@ +# Multi Axis Line Chart + +```js chart-editor +// +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = Utils.numbers({count: chart.data.labels.length, min: -100, max: 100}); + }); + chart.update(); + } + }, +]; +// + +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; + +const labels = Utils.months({count: 7}); +const data = { + labels: labels, + datasets: [ + { + label: 'Dataset 1', + data: Utils.numbers(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + yAxisID: 'y', + }, + { + label: 'Dataset 2', + data: Utils.numbers(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.blue, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), + yAxisID: 'y1', + } + ] +}; +// + +// +const config = { + type: 'line', + data: data, + options: { + responsive: true, + interaction: { + mode: 'index', + intersect: false, + }, + stacked: false, + plugins: { + title: { + display: true, + text: 'Chart.js Line Chart - Multi Axis' + } + }, + scales: { + y: { + type: 'linear', + display: true, + position: 'left', + }, + y1: { + type: 'linear', + display: true, + position: 'right', + + // grid line settings + grid: { + drawOnChartArea: false, // only want the grid lines for one axis to show up + }, + }, + } + }, +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` + +## Docs +* [Axes scales](../../axes/) +* [Cartesian Axes](../../axes/cartesian/) + * [Axis Position](../../axes/cartesian/#axis-position) +* [Data structures (`labels`)](../../general/data-structures.md) +* [Line](../../charts/line.md) + diff --git a/docs/samples/line/point-styling.md b/docs/samples/line/point-styling.md new file mode 100644 index 00000000000..dbb31c20eda --- /dev/null +++ b/docs/samples/line/point-styling.md @@ -0,0 +1,150 @@ +# Point Styling + +```js chart-editor +// +const actions = [ + { + name: 'pointStyle: circle (default)', + handler: (chart) => { + chart.data.datasets.forEach(dataset => { + dataset.pointStyle = 'circle'; + }); + chart.update(); + } + }, + { + name: 'pointStyle: cross', + handler: (chart) => { + chart.data.datasets.forEach(dataset => { + dataset.pointStyle = 'cross'; + }); + chart.update(); + } + }, + { + name: 'pointStyle: crossRot', + handler: (chart) => { + chart.data.datasets.forEach(dataset => { + dataset.pointStyle = 'crossRot'; + }); + chart.update(); + } + }, + { + name: 'pointStyle: dash', + handler: (chart) => { + chart.data.datasets.forEach(dataset => { + dataset.pointStyle = 'dash'; + }); + chart.update(); + } + }, + { + name: 'pointStyle: line', + handler: (chart) => { + chart.data.datasets.forEach(dataset => { + dataset.pointStyle = 'line'; + }); + chart.update(); + } + }, + { + name: 'pointStyle: rect', + handler: (chart) => { + chart.data.datasets.forEach(dataset => { + dataset.pointStyle = 'rect'; + }); + chart.update(); + } + }, + { + name: 'pointStyle: rectRounded', + handler: (chart) => { + chart.data.datasets.forEach(dataset => { + dataset.pointStyle = 'rectRounded'; + }); + chart.update(); + } + }, + { + name: 'pointStyle: rectRot', + handler: (chart) => { + chart.data.datasets.forEach(dataset => { + dataset.pointStyle = 'rectRot'; + }); + chart.update(); + } + }, + { + name: 'pointStyle: star', + handler: (chart) => { + chart.data.datasets.forEach(dataset => { + dataset.pointStyle = 'star'; + }); + chart.update(); + } + }, + { + name: 'pointStyle: triangle', + handler: (chart) => { + chart.data.datasets.forEach(dataset => { + dataset.pointStyle = 'triangle'; + }); + chart.update(); + } + }, + { + name: 'pointStyle: false', + handler: (chart) => { + chart.data.datasets.forEach(dataset => { + dataset.pointStyle = false; + }); + chart.update(); + } + } +]; +// + +// +const data = { + labels: ['Day 1', 'Day 2', 'Day 3', 'Day 4', 'Day 5', 'Day 6'], + datasets: [ + { + label: 'Dataset', + data: Utils.numbers({count: 6, min: -100, max: 100}), + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + pointStyle: 'circle', + pointRadius: 10, + pointHoverRadius: 15 + } + ] +}; +// + +// +const config = { + type: 'line', + data: data, + options: { + responsive: true, + plugins: { + title: { + display: true, + text: (ctx) => 'Point Style: ' + ctx.chart.data.datasets[0].pointStyle, + } + } + } +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` + +## Docs +* [Data structures (`labels`)](../../general/data-structures.md) +* [Line](../../charts/line.md) + * [Point Styling](../../charts/line.md#point-styling) diff --git a/docs/samples/line/segments.md b/docs/samples/line/segments.md new file mode 100644 index 00000000000..c4b847d66e1 --- /dev/null +++ b/docs/samples/line/segments.md @@ -0,0 +1,53 @@ +# Line Segment Styling +Using helper functions to style each segment. Gaps in the data ('skipped') are set to dashed lines and segments with values going 'down' are set to a different color. + +```js chart-editor + +// +const skipped = (ctx, value) => ctx.p0.skip || ctx.p1.skip ? value : undefined; +const down = (ctx, value) => ctx.p0.parsed.y > ctx.p1.parsed.y ? value : undefined; +// + +// +const genericOptions = { + fill: false, + interaction: { + intersect: false + }, + radius: 0, +}; +// + +// +const config = { + type: 'line', + data: { + labels: Utils.months({count: 7}), + datasets: [{ + label: 'My First Dataset', + data: [65, 59, NaN, 48, 56, 57, 40], + borderColor: 'rgb(75, 192, 192)', + segment: { + borderColor: ctx => skipped(ctx, 'rgb(0,0,0,0.2)') || down(ctx, 'rgb(192,75,75)'), + borderDash: ctx => skipped(ctx, [6, 6]), + }, + spanGaps: true + }] + }, + options: genericOptions +}; +// + +module.exports = { + actions: [], + config: config, +}; +``` + +## Docs +* [Data structures (`labels`)](../../general/data-structures.md) +* [Line](../../charts/line.md) + * [Line Styling](../../charts/line.md#line-styling) + * [Segment](../../charts/line.md#segment) +* [Options](../../general/options.md) + * [Scriptable Options](../../general/options.md#scriptable-options) \ No newline at end of file diff --git a/docs/samples/line/stepped.md b/docs/samples/line/stepped.md new file mode 100644 index 00000000000..9536d195e14 --- /dev/null +++ b/docs/samples/line/stepped.md @@ -0,0 +1,98 @@ +# Stepped Line Charts + +```js chart-editor +// +const actions = [ + { + name: 'Step: false (default)', + handler: (chart) => { + chart.data.datasets.forEach(dataset => { + dataset.stepped = false; + }); + chart.update(); + } + }, + { + name: 'Step: true', + handler: (chart) => { + chart.data.datasets.forEach(dataset => { + dataset.stepped = true; + }); + chart.update(); + } + }, + { + name: 'Step: before', + handler: (chart) => { + chart.data.datasets.forEach(dataset => { + dataset.stepped = 'before'; + }); + chart.update(); + } + }, + { + name: 'Step: after', + handler: (chart) => { + chart.data.datasets.forEach(dataset => { + dataset.stepped = 'after'; + }); + chart.update(); + } + }, + { + name: 'Step: middle', + handler: (chart) => { + chart.data.datasets.forEach(dataset => { + dataset.stepped = 'middle'; + }); + chart.update(); + } + } +]; +// + +// +const data = { + labels: ['Day 1', 'Day 2', 'Day 3', 'Day 4', 'Day 5', 'Day 6'], + datasets: [ + { + label: 'Dataset', + data: Utils.numbers({count: 6, min: -100, max: 100}), + borderColor: Utils.CHART_COLORS.red, + fill: false, + stepped: true, + } + ] +}; +// + +// +const config = { + type: 'line', + data: data, + options: { + responsive: true, + interaction: { + intersect: false, + axis: 'x' + }, + plugins: { + title: { + display: true, + text: (ctx) => 'Step ' + ctx.chart.data.datasets[0].stepped + ' Interpolation', + } + } + } +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` + +## Docs +* [Data structures (`labels`)](../../general/data-structures.md) +* [Line](../../charts/line.md) + * [Stepped](../../charts/line.md#stepped) diff --git a/docs/samples/line/styling.md b/docs/samples/line/styling.md new file mode 100644 index 00000000000..18778d0a7d2 --- /dev/null +++ b/docs/samples/line/styling.md @@ -0,0 +1,81 @@ +# Line Styling + +```js chart-editor +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; + +const labels = Utils.months({count: DATA_COUNT}); +const data = { + labels: labels, + datasets: [ + { + label: 'Unfilled', + fill: false, + backgroundColor: Utils.CHART_COLORS.blue, + borderColor: Utils.CHART_COLORS.blue, + data: Utils.numbers(NUMBER_CFG), + }, { + label: 'Dashed', + fill: false, + backgroundColor: Utils.CHART_COLORS.green, + borderColor: Utils.CHART_COLORS.green, + borderDash: [5, 5], + data: Utils.numbers(NUMBER_CFG), + }, { + label: 'Filled', + backgroundColor: Utils.CHART_COLORS.red, + borderColor: Utils.CHART_COLORS.red, + data: Utils.numbers(NUMBER_CFG), + fill: true, + } + ] +}; +// + +// +const config = { + type: 'line', + data: data, + options: { + responsive: true, + plugins: { + title: { + display: true, + text: 'Chart.js Line Chart' + }, + }, + interaction: { + mode: 'index', + intersect: false + }, + scales: { + x: { + display: true, + title: { + display: true, + text: 'Month' + } + }, + y: { + display: true, + title: { + display: true, + text: 'Value' + } + } + } + }, +}; +// + +module.exports = { + actions: [], + config: config, +}; +``` + +## Docs +* [Data structures (`labels`)](../../general/data-structures.md) +* [Line](../../charts/line.md) + * [Line Styling](../../charts/line.md#line-styling) diff --git a/docs/samples/other-charts/bubble.md b/docs/samples/other-charts/bubble.md new file mode 100644 index 00000000000..26f4d40fd92 --- /dev/null +++ b/docs/samples/other-charts/bubble.md @@ -0,0 +1,112 @@ +# Bubble + +```js chart-editor +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, rmin: 5, rmax: 15, min: 0, max: 100}; + +const data = { + datasets: [ + { + label: 'Dataset 1', + data: Utils.bubbles(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + }, + { + label: 'Dataset 2', + data: Utils.bubbles(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.orange, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.orange, 0.5), + } + ] +}; +// + +// +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = Utils.bubbles({count: DATA_COUNT, rmin: 5, rmax: 15, min: 0, max: 100}); + }); + chart.update(); + } + }, + { + name: 'Add Dataset', + handler(chart) { + const chartData = chart.data; + const dsColor = Utils.namedColor(chartData.datasets.length); + const newDataset = { + label: 'Dataset ' + (chartData.datasets.length + 1), + backgroundColor: Utils.transparentize(dsColor, 0.5), + borderColor: dsColor, + data: Utils.bubbles({count: DATA_COUNT, rmin: 5, rmax: 15, min: 0, max: 100}), + }; + chart.data.datasets.push(newDataset); + chart.update(); + } + }, + { + name: 'Add Data', + handler(chart) { + const chartData = chart.data; + if (chartData.datasets.length > 0) { + + for (let index = 0; index < chartData.datasets.length; ++index) { + chartData.datasets[index].data.push(Utils.bubbles({count: 1, rmin: 5, rmax: 15, min: 0, max: 100})[0]); + } + + chart.update(); + } + } + }, + { + name: 'Remove Dataset', + handler(chart) { + chart.data.datasets.pop(); + chart.update(); + } + }, + { + name: 'Remove Data', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data.pop(); + }); + + chart.update(); + } + } +]; +// + +// +const config = { + type: 'bubble', + data: data, + options: { + responsive: true, + plugins: { + legend: { + position: 'top', + }, + title: { + display: true, + text: 'Chart.js Bubble Chart' + } + } + }, +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` + +## Docs +* [Bubble](../../charts/bubble.md) diff --git a/docs/samples/other-charts/combo-bar-line.md b/docs/samples/other-charts/combo-bar-line.md new file mode 100644 index 00000000000..d6451b740d7 --- /dev/null +++ b/docs/samples/other-charts/combo-bar-line.md @@ -0,0 +1,123 @@ +# Combo bar/line + +```js chart-editor +// +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = Utils.numbers({count: chart.data.labels.length, min: -100, max: 100}); + }); + chart.update(); + } + }, + { + name: 'Add Dataset', + handler(chart) { + const data = chart.data; + const dsColor = Utils.namedColor(chart.data.datasets.length); + const newDataset = { + label: 'Dataset ' + (data.datasets.length + 1), + backgroundColor: Utils.transparentize(dsColor, 0.5), + borderColor: dsColor, + borderWidth: 1, + data: Utils.numbers({count: data.labels.length, min: -100, max: 100}), + }; + chart.data.datasets.push(newDataset); + chart.update(); + } + }, + { + name: 'Add Data', + handler(chart) { + const data = chart.data; + if (data.datasets.length > 0) { + data.labels = Utils.months({count: data.labels.length + 1}); + + for (let index = 0; index < data.datasets.length; ++index) { + data.datasets[index].data.push(Utils.rand(-100, 100)); + } + + chart.update(); + } + } + }, + { + name: 'Remove Dataset', + handler(chart) { + chart.data.datasets.pop(); + chart.update(); + } + }, + { + name: 'Remove Data', + handler(chart) { + chart.data.labels.splice(-1, 1); // remove the label first + + chart.data.datasets.forEach(dataset => { + dataset.data.pop(); + }); + + chart.update(); + } + } +]; +// + +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; + +const labels = Utils.months({count: 7}); +const data = { + labels: labels, + datasets: [ + { + label: 'Dataset 1', + data: Utils.numbers(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + order: 1 + }, + { + label: 'Dataset 2', + data: Utils.numbers(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.blue, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), + type: 'line', + order: 0 + } + ] +}; +// + +// +const config = { + type: 'bar', + data: data, + options: { + responsive: true, + plugins: { + legend: { + position: 'top', + }, + title: { + display: true, + text: 'Chart.js Combined Line/Bar Chart' + } + } + }, +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` + +## Docs +* [Bar](../../charts/bar.md) +* [Line](../../charts/line.md) +* [Data structures (`labels`)](../../general/data-structures.md) diff --git a/docs/samples/other-charts/doughnut.md b/docs/samples/other-charts/doughnut.md new file mode 100644 index 00000000000..6d67b16955d --- /dev/null +++ b/docs/samples/other-charts/doughnut.md @@ -0,0 +1,139 @@ +# Doughnut + +```js chart-editor +// +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = Utils.numbers({count: chart.data.labels.length, min: 0, max: 100}); + }); + chart.update(); + } + }, + { + name: 'Add Dataset', + handler(chart) { + const data = chart.data; + const newDataset = { + label: 'Dataset ' + (data.datasets.length + 1), + backgroundColor: [], + data: [], + }; + + for (let i = 0; i < data.labels.length; i++) { + newDataset.data.push(Utils.numbers({count: 1, min: 0, max: 100})); + + const colorIndex = i % Object.keys(Utils.CHART_COLORS).length; + newDataset.backgroundColor.push(Object.values(Utils.CHART_COLORS)[colorIndex]); + } + + chart.data.datasets.push(newDataset); + chart.update(); + } + }, + { + name: 'Add Data', + handler(chart) { + const data = chart.data; + if (data.datasets.length > 0) { + data.labels.push('data #' + (data.labels.length + 1)); + + for (let index = 0; index < data.datasets.length; ++index) { + data.datasets[index].data.push(Utils.rand(0, 100)); + } + + chart.update(); + } + } + }, + { + name: 'Hide(0)', + handler(chart) { + chart.hide(0); + } + }, + { + name: 'Show(0)', + handler(chart) { + chart.show(0); + } + }, + { + name: 'Hide (0, 1)', + handler(chart) { + chart.hide(0, 1); + } + }, + { + name: 'Show (0, 1)', + handler(chart) { + chart.show(0, 1); + } + }, + { + name: 'Remove Dataset', + handler(chart) { + chart.data.datasets.pop(); + chart.update(); + } + }, + { + name: 'Remove Data', + handler(chart) { + chart.data.labels.splice(-1, 1); // remove the label first + + chart.data.datasets.forEach(dataset => { + dataset.data.pop(); + }); + + chart.update(); + } + } +]; +// + +// +const DATA_COUNT = 5; +const NUMBER_CFG = {count: DATA_COUNT, min: 0, max: 100}; + +const data = { + labels: ['Red', 'Orange', 'Yellow', 'Green', 'Blue'], + datasets: [ + { + label: 'Dataset 1', + data: Utils.numbers(NUMBER_CFG), + backgroundColor: Object.values(Utils.CHART_COLORS), + } + ] +}; +// + +// +const config = { + type: 'doughnut', + data: data, + options: { + responsive: true, + plugins: { + legend: { + position: 'top', + }, + title: { + display: true, + text: 'Chart.js Doughnut Chart' + } + } + }, +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` + +## Docs +* [Doughnut and Pie Charts](../../charts/doughnut.md) diff --git a/docs/samples/other-charts/multi-series-pie.md b/docs/samples/other-charts/multi-series-pie.md new file mode 100644 index 00000000000..66e8ce4bdde --- /dev/null +++ b/docs/samples/other-charts/multi-series-pie.md @@ -0,0 +1,96 @@ +# Multi Series Pie + +```js chart-editor +// +const DATA_COUNT = 5; +const NUMBER_CFG = {count: DATA_COUNT, min: 0, max: 100}; + +const labels = Utils.months({count: 7}); +const data = { + labels: ['Overall Yay', 'Overall Nay', 'Group A Yay', 'Group A Nay', 'Group B Yay', 'Group B Nay', 'Group C Yay', 'Group C Nay'], + datasets: [ + { + backgroundColor: ['#AAA', '#777'], + data: [21, 79] + }, + { + backgroundColor: ['hsl(0, 100%, 60%)', 'hsl(0, 100%, 35%)'], + data: [33, 67] + }, + { + backgroundColor: ['hsl(100, 100%, 60%)', 'hsl(100, 100%, 35%)'], + data: [20, 80] + }, + { + backgroundColor: ['hsl(180, 100%, 60%)', 'hsl(180, 100%, 35%)'], + data: [10, 90] + } + ] +}; +// + +// +const config = { + type: 'pie', + data: data, + options: { + responsive: true, + plugins: { + legend: { + labels: { + generateLabels: function(chart) { + // Get the default label list + const original = Chart.overrides.pie.plugins.legend.labels.generateLabels; + const labelsOriginal = original.call(this, chart); + + // Build an array of colors used in the datasets of the chart + let datasetColors = chart.data.datasets.map(function(e) { + return e.backgroundColor; + }); + datasetColors = datasetColors.flat(); + + // Modify the color and hide state of each label + labelsOriginal.forEach(label => { + // There are twice as many labels as there are datasets. This converts the label index into the corresponding dataset index + label.datasetIndex = (label.index - label.index % 2) / 2; + + // The hidden state must match the dataset's hidden state + label.hidden = !chart.isDatasetVisible(label.datasetIndex); + + // Change the color to match the dataset + label.fillStyle = datasetColors[label.index]; + }); + + return labelsOriginal; + } + }, + onClick: function(mouseEvent, legendItem, legend) { + // toggle the visibility of the dataset from what it currently is + legend.chart.getDatasetMeta( + legendItem.datasetIndex + ).hidden = legend.chart.isDatasetVisible(legendItem.datasetIndex); + legend.chart.update(); + } + }, + tooltip: { + callbacks: { + title: function(context) { + const labelIndex = (context[0].datasetIndex * 2) + context[0].dataIndex; + return context[0].chart.data.labels[labelIndex] + ': ' + context[0].formattedValue; + } + } + } + } + }, +}; +// + +module.exports = { + config: config, +}; +``` + +## Docs +* [Doughnut and Pie Charts](../../charts/doughnut.md) +* [Options](../../general/options.md) + * [Scriptable Options](../../general/options.md#scriptable-options) \ No newline at end of file diff --git a/docs/samples/other-charts/pie.md b/docs/samples/other-charts/pie.md new file mode 100644 index 00000000000..465869b888a --- /dev/null +++ b/docs/samples/other-charts/pie.md @@ -0,0 +1,114 @@ +# Pie + +```js chart-editor +// +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = Utils.numbers({count: chart.data.labels.length, min: 0, max: 100}); + }); + chart.update(); + } + }, + { + name: 'Add Dataset', + handler(chart) { + const data = chart.data; + const newDataset = { + label: 'Dataset ' + (data.datasets.length + 1), + backgroundColor: [], + data: [], + }; + + for (let i = 0; i < data.labels.length; i++) { + newDataset.data.push(Utils.numbers({count: 1, min: 0, max: 100})); + + const colorIndex = i % Object.keys(Utils.CHART_COLORS).length; + newDataset.backgroundColor.push(Object.values(Utils.CHART_COLORS)[colorIndex]); + } + + chart.data.datasets.push(newDataset); + chart.update(); + } + }, + { + name: 'Add Data', + handler(chart) { + const data = chart.data; + if (data.datasets.length > 0) { + data.labels.push('data #' + (data.labels.length + 1)); + + for (let index = 0; index < data.datasets.length; ++index) { + data.datasets[index].data.push(Utils.rand(0, 100)); + } + + chart.update(); + } + } + }, + { + name: 'Remove Dataset', + handler(chart) { + chart.data.datasets.pop(); + chart.update(); + } + }, + { + name: 'Remove Data', + handler(chart) { + chart.data.labels.splice(-1, 1); // remove the label first + + chart.data.datasets.forEach(dataset => { + dataset.data.pop(); + }); + + chart.update(); + } + } +]; +// + +// +const DATA_COUNT = 5; +const NUMBER_CFG = {count: DATA_COUNT, min: 0, max: 100}; + +const data = { + labels: ['Red', 'Orange', 'Yellow', 'Green', 'Blue'], + datasets: [ + { + label: 'Dataset 1', + data: Utils.numbers(NUMBER_CFG), + backgroundColor: Object.values(Utils.CHART_COLORS), + } + ] +}; +// + +// +const config = { + type: 'pie', + data: data, + options: { + responsive: true, + plugins: { + legend: { + position: 'top', + }, + title: { + display: true, + text: 'Chart.js Pie Chart' + } + } + }, +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` +## Docs +* [Doughnut and Pie Charts](../../charts/doughnut.md) diff --git a/docs/samples/other-charts/polar-area-center-labels.md b/docs/samples/other-charts/polar-area-center-labels.md new file mode 100644 index 00000000000..c25f8421317 --- /dev/null +++ b/docs/samples/other-charts/polar-area-center-labels.md @@ -0,0 +1,107 @@ +# Polar area centered point labels + +```js chart-editor +// +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = Utils.numbers({count: chart.data.labels.length, min: 0, max: 100}); + }); + chart.update(); + } + }, + { + name: 'Add Data', + handler(chart) { + const data = chart.data; + if (data.datasets.length > 0) { + data.labels.push('data #' + (data.labels.length + 1)); + + for (let index = 0; index < data.datasets.length; ++index) { + data.datasets[index].data.push(Utils.rand(0, 100)); + } + + chart.update(); + } + } + }, + { + name: 'Remove Data', + handler(chart) { + chart.data.labels.splice(-1, 1); // remove the label first + + chart.data.datasets.forEach(dataset => { + dataset.data.pop(); + }); + + chart.update(); + } + } +]; +// + +// +const DATA_COUNT = 5; +const NUMBER_CFG = {count: DATA_COUNT, min: 0, max: 100}; + +const labels = ['Red', 'Orange', 'Yellow', 'Green', 'Blue']; +const data = { + labels: labels, + datasets: [ + { + label: 'Dataset 1', + data: Utils.numbers(NUMBER_CFG), + backgroundColor: [ + Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + Utils.transparentize(Utils.CHART_COLORS.orange, 0.5), + Utils.transparentize(Utils.CHART_COLORS.yellow, 0.5), + Utils.transparentize(Utils.CHART_COLORS.green, 0.5), + Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), + ] + } + ] +}; +// + +// +const config = { + type: 'polarArea', + data: data, + options: { + responsive: true, + scales: { + r: { + pointLabels: { + display: true, + centerPointLabels: true, + font: { + size: 18 + } + } + } + }, + plugins: { + legend: { + position: 'top', + }, + title: { + display: true, + text: 'Chart.js Polar Area Chart With Centered Point Labels' + } + } + }, +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` + +## Docs +* [Polar Area Chart](../../charts/polar.md) +* [Linear Radial Axis](../../axes/radial/linear.md) + * [Point Label Options (`centerPointLabels`)](../../axes/radial/linear.md#point-label-options) \ No newline at end of file diff --git a/docs/samples/other-charts/polar-area.md b/docs/samples/other-charts/polar-area.md new file mode 100644 index 00000000000..3c951f9f1ea --- /dev/null +++ b/docs/samples/other-charts/polar-area.md @@ -0,0 +1,95 @@ +# Polar area + +```js chart-editor +// +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = Utils.numbers({count: chart.data.labels.length, min: 0, max: 100}); + }); + chart.update(); + } + }, + { + name: 'Add Data', + handler(chart) { + const data = chart.data; + if (data.datasets.length > 0) { + data.labels.push('data #' + (data.labels.length + 1)); + + for (let index = 0; index < data.datasets.length; ++index) { + data.datasets[index].data.push(Utils.rand(0, 100)); + } + + chart.update(); + } + } + }, + { + name: 'Remove Data', + handler(chart) { + chart.data.labels.splice(-1, 1); // remove the label first + + chart.data.datasets.forEach(dataset => { + dataset.data.pop(); + }); + + chart.update(); + } + } +]; +// + +// +const DATA_COUNT = 5; +const NUMBER_CFG = {count: DATA_COUNT, min: 0, max: 100}; + +const labels = ['Red', 'Orange', 'Yellow', 'Green', 'Blue']; +const data = { + labels: labels, + datasets: [ + { + label: 'Dataset 1', + data: Utils.numbers(NUMBER_CFG), + backgroundColor: [ + Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + Utils.transparentize(Utils.CHART_COLORS.orange, 0.5), + Utils.transparentize(Utils.CHART_COLORS.yellow, 0.5), + Utils.transparentize(Utils.CHART_COLORS.green, 0.5), + Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), + ] + } + ] +}; +// + +// +const config = { + type: 'polarArea', + data: data, + options: { + responsive: true, + plugins: { + legend: { + position: 'top', + }, + title: { + display: true, + text: 'Chart.js Polar Area Chart' + } + } + }, +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` + +## Docs +* [Polar Area Chart](../../charts/polar.md) +* [Radial linear scale](../../axes/radial/linear.md) diff --git a/docs/samples/other-charts/radar-skip-points.md b/docs/samples/other-charts/radar-skip-points.md new file mode 100644 index 00000000000..2dd268fc1af --- /dev/null +++ b/docs/samples/other-charts/radar-skip-points.md @@ -0,0 +1,91 @@ +# Radar skip points + +```js chart-editor +// +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach((dataset, i) => { + const data = Utils.numbers({count: chart.data.labels.length, min: 0, max: 100}); + + if (i === 0) { + data[0] = null; + } else if (i === 1) { + data[Number.parseInt(data.length / 2, 10)] = null; + } else { + data[data.length - 1] = null; + } + + dataset.data = data; + }); + chart.update(); + } + } +]; +// + +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: 0, max: 100}; + +const labels = Utils.months({count: 7}); +const dataFirstSkip = Utils.numbers(NUMBER_CFG); +const dataMiddleSkip = Utils.numbers(NUMBER_CFG); +const dataLastSkip = Utils.numbers(NUMBER_CFG); + +dataFirstSkip[0] = null; +dataMiddleSkip[Number.parseInt(dataMiddleSkip.length / 2, 10)] = null; +dataLastSkip[dataLastSkip.length - 1] = null; + +const data = { + labels: labels, + datasets: [ + { + label: 'Skip first dataset', + data: dataFirstSkip, + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + }, + { + label: 'Skip mid dataset', + data: dataMiddleSkip, + borderColor: Utils.CHART_COLORS.blue, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), + }, + { + label: 'Skip last dataset', + data: dataLastSkip, + borderColor: Utils.CHART_COLORS.green, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.green, 0.5), + } + ] +}; +// + +// +const config = { + type: 'radar', + data: data, + options: { + responsive: true, + plugins: { + title: { + display: true, + text: 'Chart.js Radar Skip Points Chart' + } + } + }, +}; +// + +module.exports = { + actions: actions, + config: config +}; +``` + +## Docs +* [Radar](../../charts/radar.md) +* [Data structures (`labels`)](../../general/data-structures.md) +* [Radial linear scale](../../axes/radial/linear.md) diff --git a/docs/samples/other-charts/radar.md b/docs/samples/other-charts/radar.md new file mode 100644 index 00000000000..e60648caa63 --- /dev/null +++ b/docs/samples/other-charts/radar.md @@ -0,0 +1,116 @@ +# Radar + +```js chart-editor +// +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = Utils.numbers({count: chart.data.labels.length, min: 0, max: 100}); + }); + chart.update(); + } + }, + { + name: 'Add Dataset', + handler(chart) { + const data = chart.data; + const dsColor = Utils.namedColor(chart.data.datasets.length); + const newDataset = { + label: 'Dataset ' + (data.datasets.length + 1), + backgroundColor: Utils.transparentize(dsColor, 0.5), + borderColor: dsColor, + data: Utils.numbers({count: data.labels.length, min: 0, max: 100}), + }; + chart.data.datasets.push(newDataset); + chart.update(); + } + }, + { + name: 'Add Data', + handler(chart) { + const data = chart.data; + if (data.datasets.length > 0) { + data.labels = Utils.months({count: data.labels.length + 1}); + + for (let index = 0; index < data.datasets.length; ++index) { + data.datasets[index].data.push(Utils.rand(0, 100)); + } + + chart.update(); + } + } + }, + { + name: 'Remove Dataset', + handler(chart) { + chart.data.datasets.pop(); + chart.update(); + } + }, + { + name: 'Remove Data', + handler(chart) { + chart.data.labels.splice(-1, 1); // remove the label first + + chart.data.datasets.forEach(dataset => { + dataset.data.pop(); + }); + + chart.update(); + } + } +]; +// + +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: 0, max: 100}; + +const labels = Utils.months({count: 7}); +const data = { + labels: labels, + datasets: [ + { + label: 'Dataset 1', + data: Utils.numbers(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + }, + { + label: 'Dataset 2', + data: Utils.numbers(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.blue, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), + } + ] +}; +// + +// +const config = { + type: 'radar', + data: data, + options: { + responsive: true, + plugins: { + title: { + display: true, + text: 'Chart.js Radar Chart' + } + } + }, +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` + +## Docs +* [Radar](../../charts/radar.md) +* [Data structures (`labels`)](../../general/data-structures.md) +* [Radial linear scale](../../axes/radial/linear.md) diff --git a/docs/samples/other-charts/scatter-multi-axis.md b/docs/samples/other-charts/scatter-multi-axis.md new file mode 100644 index 00000000000..6b3cb8e70d2 --- /dev/null +++ b/docs/samples/other-charts/scatter-multi-axis.md @@ -0,0 +1,136 @@ +# Scatter - Multi axis + +```js chart-editor +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, rmin: 1, rmax: 1, min: -100, max: 100}; + +const data = { + datasets: [ + { + label: 'Dataset 1', + data: Utils.bubbles(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + yAxisID: 'y', + }, + { + label: 'Dataset 2', + data: Utils.bubbles(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.orange, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.orange, 0.5), + yAxisID: 'y2', + } + ] +}; +// + +// +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = Utils.bubbles({count: DATA_COUNT, rmin: 1, rmax: 1, min: -100, max: 100}); + }); + chart.update(); + } + }, + { + name: 'Add Dataset', + handler(chart) { + const chartData = chart.data; + const dsColor = Utils.namedColor(chartData.datasets.length); + const newDataset = { + label: 'Dataset ' + (chartData.datasets.length + 1), + backgroundColor: Utils.transparentize(dsColor, 0.5), + borderColor: dsColor, + data: Utils.bubbles({count: DATA_COUNT, rmin: 1, rmax: 1, min: -100, max: 100}), + }; + chart.data.datasets.push(newDataset); + chart.update(); + } + }, + { + name: 'Add Data', + handler(chart) { + const chartData = chart.data; + if (chartData.datasets.length > 0) { + + for (let index = 0; index < chartData.datasets.length; ++index) { + chartData.datasets[index].data.push(Utils.bubbles({count: 1, rmin: 1, rmax: 1, min: -100, max: 100})[0]); + } + + chart.update(); + } + } + }, + { + name: 'Remove Dataset', + handler(chart) { + chart.data.datasets.pop(); + chart.update(); + } + }, + { + name: 'Remove Data', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data.pop(); + }); + + chart.update(); + } + } +]; +// + +// +const config = { + type: 'scatter', + data: data, + options: { + responsive: true, + plugins: { + legend: { + position: 'top', + }, + title: { + display: true, + text: 'Chart.js Scatter Multi Axis Chart' + } + }, + scales: { + y: { + type: 'linear', // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance + position: 'left', + ticks: { + color: Utils.CHART_COLORS.red + } + }, + y2: { + type: 'linear', // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance + position: 'right', + reverse: true, + ticks: { + color: Utils.CHART_COLORS.blue + }, + grid: { + drawOnChartArea: false // only want the grid lines for one axis to show up + } + } + } + }, +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` + +## Docs +* [Scatter](../../charts/scatter.md) +* [Cartesian Axes](../../axes/cartesian/) + * [Axis Position](../../axes/cartesian/#axis-position) diff --git a/docs/samples/other-charts/scatter.md b/docs/samples/other-charts/scatter.md new file mode 100644 index 00000000000..2ab75fbfbe8 --- /dev/null +++ b/docs/samples/other-charts/scatter.md @@ -0,0 +1,112 @@ +# Scatter + +```js chart-editor +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, rmin: 1, rmax: 1, min: 0, max: 100}; + +const data = { + datasets: [ + { + label: 'Dataset 1', + data: Utils.bubbles(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + }, + { + label: 'Dataset 2', + data: Utils.bubbles(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.orange, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.orange, 0.5), + } + ] +}; +// + +// +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = Utils.bubbles({count: DATA_COUNT, rmin: 1, rmax: 1, min: 0, max: 100}); + }); + chart.update(); + } + }, + { + name: 'Add Dataset', + handler(chart) { + const chartData = chart.data; + const dsColor = Utils.namedColor(chartData.datasets.length); + const newDataset = { + label: 'Dataset ' + (chartData.datasets.length + 1), + backgroundColor: Utils.transparentize(dsColor, 0.5), + borderColor: dsColor, + data: Utils.bubbles({count: DATA_COUNT, rmin: 1, rmax: 1, min: 0, max: 100}), + }; + chart.data.datasets.push(newDataset); + chart.update(); + } + }, + { + name: 'Add Data', + handler(chart) { + const chartData = chart.data; + if (chartData.datasets.length > 0) { + + for (let index = 0; index < chartData.datasets.length; ++index) { + chartData.datasets[index].data.push(Utils.bubbles({count: 1, rmin: 1, rmax: 1, min: 0, max: 100})[0]); + } + + chart.update(); + } + } + }, + { + name: 'Remove Dataset', + handler(chart) { + chart.data.datasets.pop(); + chart.update(); + } + }, + { + name: 'Remove Data', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data.pop(); + }); + + chart.update(); + } + } +]; +// + +// +const config = { + type: 'scatter', + data: data, + options: { + responsive: true, + plugins: { + legend: { + position: 'top', + }, + title: { + display: true, + text: 'Chart.js Scatter Chart' + } + } + }, +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` + +## Docs +* [Scatter](../../charts/scatter.md) diff --git a/docs/samples/other-charts/stacked-bar-line.md b/docs/samples/other-charts/stacked-bar-line.md new file mode 100644 index 00000000000..20651722f3b --- /dev/null +++ b/docs/samples/other-charts/stacked-bar-line.md @@ -0,0 +1,130 @@ +# Stacked bar/line + +```js chart-editor +// +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = Utils.numbers({count: chart.data.labels.length, min: 0, max: 100}); + }); + chart.update(); + } + }, + { + name: 'Add Dataset', + handler(chart) { + const data = chart.data; + const dsColor = Utils.namedColor(chart.data.datasets.length); + const newDataset = { + label: 'Dataset ' + (data.datasets.length + 1), + backgroundColor: Utils.transparentize(dsColor, 0.5), + borderColor: dsColor, + borderWidth: 1, + stack: 'combined', + data: Utils.numbers({count: data.labels.length, min: 0, max: 100}), + }; + chart.data.datasets.push(newDataset); + chart.update(); + } + }, + { + name: 'Add Data', + handler(chart) { + const data = chart.data; + if (data.datasets.length > 0) { + data.labels = Utils.months({count: data.labels.length + 1}); + + for (let index = 0; index < data.datasets.length; ++index) { + data.datasets[index].data.push(Utils.rand(0, 100)); + } + + chart.update(); + } + } + }, + { + name: 'Remove Dataset', + handler(chart) { + chart.data.datasets.pop(); + chart.update(); + } + }, + { + name: 'Remove Data', + handler(chart) { + chart.data.labels.splice(-1, 1); // remove the label first + + chart.data.datasets.forEach(dataset => { + dataset.data.pop(); + }); + + chart.update(); + } + } +]; +// + +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: 0, max: 100}; + +const labels = Utils.months({count: 7}); +const data = { + labels: labels, + datasets: [ + { + label: 'Dataset 1', + data: Utils.numbers(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + stack: 'combined', + type: 'bar' + }, + { + label: 'Dataset 2', + data: Utils.numbers(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.blue, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), + stack: 'combined' + } + ] +}; +// + +// +const config = { + type: 'line', + data: data, + options: { + plugins: { + title: { + display: true, + text: 'Chart.js Stacked Line/Bar Chart' + } + }, + scales: { + y: { + stacked: true + } + } + }, +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` + +## Docs +* [Axes scales](../../axes/) + * [Common options to all axes (`stacked`)](../../axes/#common-options-to-all-axes) + * [Stacking](../../axes/#stacking) +* [Bar](../../charts/bar.md) +* [Line](../../charts/line.md) +* [Data structures (`labels`)](../../general/data-structures.md) + * [Dataset Configuration (`stack`)](../../general/data-structures.md#dataset-configuration) + diff --git a/docs/samples/plugins/chart-area-border.md b/docs/samples/plugins/chart-area-border.md new file mode 100644 index 00000000000..cccfa2473ec --- /dev/null +++ b/docs/samples/plugins/chart-area-border.md @@ -0,0 +1,69 @@ +# Chart Area Border + +```js chart-editor +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; +const labels = Utils.months({count: 7}); +const data = { + labels: labels, + datasets: [ + { + label: 'Dataset 1', + data: Utils.numbers(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + }, + { + label: 'Dataset 2', + data: Utils.numbers(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.blue, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), + } + ] +}; +// + +// +const chartAreaBorder = { + id: 'chartAreaBorder', + beforeDraw(chart, args, options) { + const {ctx, chartArea: {left, top, width, height}} = chart; + ctx.save(); + ctx.strokeStyle = options.borderColor; + ctx.lineWidth = options.borderWidth; + ctx.setLineDash(options.borderDash || []); + ctx.lineDashOffset = options.borderDashOffset; + ctx.strokeRect(left, top, width, height); + ctx.restore(); + } +}; +// + +// +const config = { + type: 'line', + data: data, + options: { + plugins: { + chartAreaBorder: { + borderColor: 'red', + borderWidth: 2, + borderDash: [5, 5], + borderDashOffset: 2, + } + } + }, + plugins: [chartAreaBorder] +}; +// + +module.exports = { + config: config, +}; +``` + +## Docs +* [Line](../../charts/line.md) +* [Data structures (`labels`)](../../general/data-structures.md) +* [Plugins](../../developers/plugins.md) diff --git a/docs/samples/plugins/doughnut-empty-state.md b/docs/samples/plugins/doughnut-empty-state.md new file mode 100644 index 00000000000..1e9267b7920 --- /dev/null +++ b/docs/samples/plugins/doughnut-empty-state.md @@ -0,0 +1,83 @@ +# Doughnut Empty State + +```js chart-editor +// +const data = { + labels: [], + datasets: [ + { + label: 'Dataset 1', + data: [] + } + ] +}; +// + +// +const plugin = { + id: 'emptyDoughnut', + afterDraw(chart, args, options) { + const {datasets} = chart.data; + const {color, width, radiusDecrease} = options; + let hasData = false; + + for (let i = 0; i < datasets.length; i += 1) { + const dataset = datasets[i]; + hasData |= dataset.data.length > 0; + } + + if (!hasData) { + const {chartArea: {left, top, right, bottom}, ctx} = chart; + const centerX = (left + right) / 2; + const centerY = (top + bottom) / 2; + const r = Math.min(right - left, bottom - top) / 2; + + ctx.beginPath(); + ctx.lineWidth = width || 2; + ctx.strokeStyle = color || 'rgba(255, 128, 0, 0.5)'; + ctx.arc(centerX, centerY, (r - radiusDecrease || 0), 0, 2 * Math.PI); + ctx.stroke(); + } + } +}; +// + +// +const config = { + type: 'doughnut', + data: data, + options: { + plugins: { + emptyDoughnut: { + color: 'rgba(255, 128, 0, 0.5)', + width: 2, + radiusDecrease: 20 + } + } + }, + plugins: [plugin] +}; +// + +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = Utils.points(NUMBER_CFG); + }); + chart.update(); + } + }, +]; + +module.exports = { + actions, + config, +}; +``` + +## Docs +* [Data structures (`labels`)](../../general/data-structures.md) +* [Plugins](../../developers/plugins.md) +* [Doughnut and Pie Charts](../../charts/doughnut.md) diff --git a/docs/samples/plugins/quadrants.md b/docs/samples/plugins/quadrants.md new file mode 100644 index 00000000000..3354a6a0290 --- /dev/null +++ b/docs/samples/plugins/quadrants.md @@ -0,0 +1,85 @@ +# Quadrants + +```js chart-editor +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; +const data = { + datasets: [ + { + label: 'Dataset 1', + data: Utils.points(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + }, + { + label: 'Dataset 2', + data: Utils.points(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.blue, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), + } + ] +}; +// + +// +const quadrants = { + id: 'quadrants', + beforeDraw(chart, args, options) { + const {ctx, chartArea: {left, top, right, bottom}, scales: {x, y}} = chart; + const midX = x.getPixelForValue(0); + const midY = y.getPixelForValue(0); + ctx.save(); + ctx.fillStyle = options.topLeft; + ctx.fillRect(left, top, midX - left, midY - top); + ctx.fillStyle = options.topRight; + ctx.fillRect(midX, top, right - midX, midY - top); + ctx.fillStyle = options.bottomRight; + ctx.fillRect(midX, midY, right - midX, bottom - midY); + ctx.fillStyle = options.bottomLeft; + ctx.fillRect(left, midY, midX - left, bottom - midY); + ctx.restore(); + } +}; +// + +// +const config = { + type: 'scatter', + data: data, + options: { + plugins: { + quadrants: { + topLeft: Utils.CHART_COLORS.red, + topRight: Utils.CHART_COLORS.blue, + bottomRight: Utils.CHART_COLORS.green, + bottomLeft: Utils.CHART_COLORS.yellow, + } + } + }, + plugins: [quadrants] +}; +// + +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = Utils.points(NUMBER_CFG); + }); + chart.update(); + } + }, +]; + +module.exports = { + actions, + config, +}; +``` + +## Docs +* [Data structures (`labels`)](../../general/data-structures.md) +* [Plugins](../../developers/plugins.md) +* [Scatter](../../charts/scatter.md) diff --git a/docs/samples/scale-options/center.md b/docs/samples/scale-options/center.md new file mode 100644 index 00000000000..b435c6174f5 --- /dev/null +++ b/docs/samples/scale-options/center.md @@ -0,0 +1,94 @@ +# Center Positioning + +This sample show how to place the axis in the center of the chart area, instead of at the edges. + +```js chart-editor +// +const actions = [ + { + name: 'Default Positions', + handler(chart) { + chart.options.scales.x.position = 'bottom'; + chart.options.scales.y.position = 'left'; + chart.update(); + } + }, + { + name: 'Position: center', + handler(chart) { + chart.options.scales.x.position = 'center'; + chart.options.scales.y.position = 'center'; + chart.update(); + } + }, + { + name: 'Position: Vertical: x=-60, Horizontal: y=30', + handler(chart) { + chart.options.scales.x.position = {y: 30}; + chart.options.scales.y.position = {x: -60}; + chart.update(); + } + }, +]; +// + + +// +const DATA_COUNT = 6; +const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; +const data = { + datasets: [ + { + label: 'Dataset 1', + data: Utils.points(NUMBER_CFG), + fill: false, + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + }, + { + label: 'Dataset 2', + data: Utils.points(NUMBER_CFG), + fill: false, + borderColor: Utils.CHART_COLORS.blue, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), + } + ] +}; +// + +// +const config = { + type: 'scatter', + data: data, + options: { + responsive: true, + plugins: { + title: { + display: true, + text: 'Axis Center Positioning' + } + }, + scales: { + x: { + min: -100, + max: 100, + }, + y: { + min: -100, + max: 100, + } + } + }, +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` + +## Docs +* [Scatter](../../charts/scatter.md) +* [Cartesian Axes](../../axes/cartesian/) + * [Axis Position](../../axes/cartesian/#axis-position) \ No newline at end of file diff --git a/docs/samples/scale-options/grid.md b/docs/samples/scale-options/grid.md new file mode 100644 index 00000000000..fdbea55de7b --- /dev/null +++ b/docs/samples/scale-options/grid.md @@ -0,0 +1,107 @@ +# Grid Configuration + +This sample shows how to use scriptable grid options for an axis to control styling. In this case, the Y axis grid lines are colored based on their value. In addition, booleans are provided to toggle different parts of the X axis grid visibility. + +```js chart-editor +// +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = Utils.numbers({count: chart.data.labels.length, min: -100, max: 100}); + }); + chart.update(); + } + }, +]; +// + +// +const DATA_COUNT = 7; +const data = { + labels: Utils.months({count: DATA_COUNT}), + datasets: [ + { + label: 'Dataset 1', + data: [10, 30, 39, 20, 25, 34, -10], + fill: false, + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + }, + { + label: 'Dataset 2', + data: [18, 33, 22, 19, 11, -39, 30], + fill: false, + borderColor: Utils.CHART_COLORS.blue, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), + } + ] +}; +// + +// +// Change these settings to change the display for different parts of the X axis +// grid configuration +const DISPLAY = true; +const BORDER = true; +const CHART_AREA = true; +const TICKS = true; + +const config = { + type: 'line', + data: data, + options: { + responsive: true, + plugins: { + title: { + display: true, + text: 'Grid Line Settings' + } + }, + scales: { + x: { + border: { + display: BORDER + }, + grid: { + display: DISPLAY, + drawOnChartArea: CHART_AREA, + drawTicks: TICKS, + } + }, + y: { + border: { + display: false + }, + grid: { + color: function(context) { + if (context.tick.value > 0) { + return Utils.CHART_COLORS.green; + } else if (context.tick.value < 0) { + return Utils.CHART_COLORS.red; + } + + return '#000000'; + }, + }, + } + } + }, +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` + +## Docs +* [Line](../../charts/line.md) +* [Options](../../general/options.md) + * [Scriptable Options](../../general/options.md#scriptable-options) + * [Tick Context](../../general/options.md#tick) +* [Data structures (`labels`)](../../general/data-structures.md) +* [Axes Styling](../../axes/styling.md) + * [Grid Line Configuration](../../axes/styling.md#grid-line-configuration) \ No newline at end of file diff --git a/docs/samples/scale-options/ticks.md b/docs/samples/scale-options/ticks.md new file mode 100644 index 00000000000..e6e7bb2f94d --- /dev/null +++ b/docs/samples/scale-options/ticks.md @@ -0,0 +1,103 @@ +# Tick Configuration + +This sample shows how to use different tick features to control how tick labels are shown on the X axis. These features include: + +* Multi-line labels +* Filtering labels +* Changing the tick color +* Changing the tick alignment for the X axis + +```js chart-editor +// +const actions = [ + { + name: 'Alignment: start', + handler(chart) { + chart.options.scales.x.ticks.align = 'start'; + chart.update(); + } + }, + { + name: 'Alignment: center (default)', + handler(chart) { + chart.options.scales.x.ticks.align = 'center'; + chart.update(); + } + }, + { + name: 'Alignment: end', + handler(chart) { + chart.options.scales.x.ticks.align = 'end'; + chart.update(); + } + }, +]; +// + + +// +const DATA_COUNT = 12; +const NUMBER_CFG = {count: DATA_COUNT, min: 0, max: 100}; +const data = { + labels: [['June', '2015'], 'July', 'August', 'September', 'October', 'November', 'December', ['January', '2016'], 'February', 'March', 'April', 'May'], + datasets: [ + { + label: 'Dataset 1', + data: Utils.numbers(NUMBER_CFG), + fill: false, + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + }, + { + label: 'Dataset 2', + data: Utils.numbers(NUMBER_CFG), + fill: false, + borderColor: Utils.CHART_COLORS.blue, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), + } + ] +}; +// + +// +const config = { + type: 'line', + data: data, + options: { + responsive: true, + plugins: { + title: { + display: true, + text: 'Chart with Tick Configuration' + } + }, + scales: { + x: { + ticks: { + // For a category axis, the val is the index so the lookup via getLabelForValue is needed + callback: function(val, index) { + // Hide every 2nd tick label + return index % 2 === 0 ? this.getLabelForValue(val) : ''; + }, + color: 'red', + } + } + } + }, +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` + +## Docs +* [Line](../../charts/line.md) +* [Options](../../general/options.md) + * [Scriptable Options](../../general/options.md#scriptable-options) + * [Tick Context](../../general/options.md#tick) +* [Data structures (`labels`)](../../general/data-structures.md) +* [Axes Styling](../../axes/styling.md) + * [Tick Configuration](../../axes/styling.md#tick-configuration) \ No newline at end of file diff --git a/docs/samples/scale-options/titles.md b/docs/samples/scale-options/titles.md new file mode 100644 index 00000000000..e49316b0671 --- /dev/null +++ b/docs/samples/scale-options/titles.md @@ -0,0 +1,85 @@ +# Title Configuration + +This sample shows how to configure the title of an axis including alignment, font, and color. + +```js chart-editor +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: 0, max: 100}; +const data = { + labels: Utils.months({count: DATA_COUNT}), + datasets: [ + { + label: 'Dataset 1', + data: Utils.numbers(NUMBER_CFG), + fill: false, + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + }, + { + label: 'Dataset 2', + data: Utils.numbers(NUMBER_CFG), + fill: false, + borderColor: Utils.CHART_COLORS.blue, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), + } + ] +}; +// + +// +const config = { + type: 'line', + data: data, + options: { + responsive: true, + scales: { + x: { + display: true, + title: { + display: true, + text: 'Month', + color: '#911', + font: { + family: 'Comic Sans MS', + size: 20, + weight: 'bold', + lineHeight: 1.2, + }, + padding: {top: 20, left: 0, right: 0, bottom: 0} + } + }, + y: { + display: true, + title: { + display: true, + text: 'Value', + color: '#191', + font: { + family: 'Times', + size: 20, + style: 'normal', + lineHeight: 1.2 + }, + padding: {top: 30, left: 0, right: 0, bottom: 0} + } + } + } + }, +}; +// + +module.exports = { + actions: [], + config: config, +}; +``` + +## Docs +* [Line](../../charts/line.md) +* [Data structures (`labels`)](../../general/data-structures.md) +* [Axes Styling](../../axes/styling.md) +* [Cartesian Axes](../../axes/cartesian/) + * [Common options to all cartesian axes](../../axes/cartesian/#common-options-to-all-cartesian-axes) +* [Labeling Axes](../../axes/labelling.md) + * [Scale Title Configuration](../../axes/labelling.md#scale-title-configuration) \ No newline at end of file diff --git a/docs/samples/scales/linear-min-max-suggested.md b/docs/samples/scales/linear-min-max-suggested.md new file mode 100644 index 00000000000..3e6e4fdbd8d --- /dev/null +++ b/docs/samples/scales/linear-min-max-suggested.md @@ -0,0 +1,63 @@ +# Linear Scale - Suggested Min-Max + +```js chart-editor +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: 0, max: 100}; + +const labels = Utils.months({count: 7}); +const data = { + labels: labels, + datasets: [ + { + label: 'Dataset 1', + data: [10, 30, 39, 20, 25, 34, -10], + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.CHART_COLORS.red, + }, + { + label: 'Dataset 2', + data: [18, 33, 22, 19, 11, 39, 30], + borderColor: Utils.CHART_COLORS.blue, + backgroundColor: Utils.CHART_COLORS.blue, + } + ] +}; +// + +// +const config = { + type: 'line', + data: data, + options: { + responsive: true, + plugins: { + title: { + display: true, + text: 'Suggested Min and Max Settings' + } + }, + scales: { + y: { + // the data minimum used for determining the ticks is Math.min(dataMin, suggestedMin) + suggestedMin: 30, + + // the data maximum used for determining the ticks is Math.max(dataMax, suggestedMax) + suggestedMax: 50, + } + } + }, +}; +// + +module.exports = { + config: config, +}; +``` + +## Docs +* [Line](../../charts/line.md) +* [Data structures (`labels`)](../../general/data-structures.md) +* [Axes scales](../../axes/) + * [Common options to all axes](../../axes/#common-options-to-all-axes) + * [Axis Range Settings](../../axes/#axis-range-settings) diff --git a/docs/samples/scales/linear-min-max.md b/docs/samples/scales/linear-min-max.md new file mode 100644 index 00000000000..29b8a2d17eb --- /dev/null +++ b/docs/samples/scales/linear-min-max.md @@ -0,0 +1,60 @@ +# Linear Scale - Min-Max + +```js chart-editor +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: 0, max: 100}; + +const labels = Utils.months({count: 7}); +const data = { + labels: labels, + datasets: [ + { + label: 'Dataset 1', + data: [10, 30, 50, 20, 25, 44, -10], + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.CHART_COLORS.red, + }, + { + label: 'Dataset 2', + data: [100, 33, 22, 19, 11, 49, 30], + borderColor: Utils.CHART_COLORS.blue, + backgroundColor: Utils.CHART_COLORS.blue, + } + ] +}; +// + +// +const config = { + type: 'line', + data: data, + options: { + responsive: true, + plugins: { + title: { + display: true, + text: 'Min and Max Settings' + } + }, + scales: { + y: { + min: 10, + max: 50, + } + } + }, +}; +// + +module.exports = { + config: config, +}; +``` + +## Docs +* [Line](../../charts/line.md) +* [Data structures (`labels`)](../../general/data-structures.md) +* [Axes scales](../../axes/) + * [Common options to all axes (`min`,`max`)](../../axes/#common-options-to-all-axes) + \ No newline at end of file diff --git a/docs/samples/scales/linear-step-size.md b/docs/samples/scales/linear-step-size.md new file mode 100644 index 00000000000..23c3c9e4c5e --- /dev/null +++ b/docs/samples/scales/linear-step-size.md @@ -0,0 +1,148 @@ +# Linear Scale - Step Size + +```js chart-editor +// +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = Utils.numbers({count: chart.data.labels.length, min: 0, max: 100}); + }); + chart.update(); + } + }, + { + name: 'Add Dataset', + handler(chart) { + const data = chart.data; + const dsColor = Utils.namedColor(chart.data.datasets.length); + const newDataset = { + label: 'Dataset ' + (data.datasets.length + 1), + backgroundColor: dsColor, + borderColor: dsColor, + data: Utils.numbers({count: data.labels.length, min: 0, max: 100}), + }; + chart.data.datasets.push(newDataset); + chart.update(); + } + }, + { + name: 'Add Data', + handler(chart) { + const data = chart.data; + if (data.datasets.length > 0) { + data.labels = Utils.months({count: data.labels.length + 1}); + + for (let index = 0; index < data.datasets.length; ++index) { + data.datasets[index].data.push(Utils.rand(0, 100)); + } + + chart.update(); + } + } + }, + { + name: 'Remove Dataset', + handler(chart) { + chart.data.datasets.pop(); + chart.update(); + } + }, + { + name: 'Remove Data', + handler(chart) { + chart.data.labels.splice(-1, 1); // remove the label first + + chart.data.datasets.forEach(dataset => { + dataset.data.pop(); + }); + + chart.update(); + } + } +]; +// + +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: 0, max: 100}; + +const labels = Utils.months({count: 7}); +const data = { + labels: labels, + datasets: [ + { + label: 'Dataset 1', + data: Utils.numbers(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.CHART_COLORS.red, + }, + { + label: 'Dataset 2', + data: Utils.numbers(NUMBER_CFG), + borderColor: Utils.CHART_COLORS.blue, + backgroundColor: Utils.CHART_COLORS.blue, + } + ] +}; +// + +// +const config = { + type: 'line', + data: data, + options: { + responsive: true, + plugins: { + tooltip: { + mode: 'index', + intersect: false + }, + title: { + display: true, + text: 'Chart.js Line Chart' + } + }, + hover: { + mode: 'index', + intersect: false + }, + scales: { + x: { + title: { + display: true, + text: 'Month' + } + }, + y: { + title: { + display: true, + text: 'Value' + }, + min: 0, + max: 100, + ticks: { + // forces step size to be 50 units + stepSize: 50 + } + } + } + }, +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` + +## Docs +* [Line](../../charts/line.md) +* [Data structures (`labels`)](../../general/data-structures.md) +* [Axes scales](../../axes/) + * [Common options to all axes (`min`,`max`)](../../axes/#common-options-to-all-axes) +* [Linear Axis](../../axes/cartesian/linear.md) + * [Linear Axis specific tick options (`stepSize`)](../../axes/cartesian/linear.md#linear-axis-specific-tick-options) + * [Step Size](../../axes/cartesian/linear.md#step-size) diff --git a/docs/samples/scales/log.md b/docs/samples/scales/log.md new file mode 100644 index 00000000000..35ae8e63414 --- /dev/null +++ b/docs/samples/scales/log.md @@ -0,0 +1,82 @@ +# Log Scale + +```js chart-editor +// +const logNumbers = (num) => { + const data = []; + + for (let i = 0; i < num; ++i) { + data.push(Math.ceil(Math.random() * 10.0) * Math.pow(10, Math.ceil(Math.random() * 5))); + } + + return data; +}; + +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = logNumbers(chart.data.labels.length); + }); + chart.update(); + } + }, +]; +// + +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: 0, max: 100}; + +const labels = Utils.months({count: 7}); +const data = { + labels: labels, + datasets: [ + { + label: 'Dataset 1', + data: logNumbers(DATA_COUNT), + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.CHART_COLORS.red, + fill: false, + }, + ] +}; +// + +// +const config = { + type: 'line', + data: data, + options: { + responsive: true, + plugins: { + title: { + display: true, + text: 'Chart.js Line Chart - Logarithmic' + } + }, + scales: { + x: { + display: true, + }, + y: { + display: true, + type: 'logarithmic', + } + } + }, +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` + +## Docs +* [Line](../../charts/line.md) +* [Logarithmic Axis](../../axes/cartesian/logarithmic.md) +* [Data structures (`labels`)](../../general/data-structures.md) + diff --git a/docs/samples/scales/stacked.md b/docs/samples/scales/stacked.md new file mode 100644 index 00000000000..461236dc95b --- /dev/null +++ b/docs/samples/scales/stacked.md @@ -0,0 +1,77 @@ +# Stacked Linear / Category + +```js chart-editor +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: 0, max: 100}; + +const labels = Utils.months({count: 7}); +const data = { + labels: labels, + datasets: [ + { + label: 'Dataset 1', + data: [10, 30, 50, 20, 25, 44, -10], + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.CHART_COLORS.red, + }, + { + label: 'Dataset 2', + data: ['ON', 'ON', 'OFF', 'ON', 'OFF', 'OFF', 'ON'], + borderColor: Utils.CHART_COLORS.blue, + backgroundColor: Utils.CHART_COLORS.blue, + stepped: true, + yAxisID: 'y2', + } + ] +}; +// + +// +const config = { + type: 'line', + data: data, + options: { + responsive: true, + plugins: { + title: { + display: true, + text: 'Stacked scales', + }, + }, + scales: { + y: { + type: 'linear', + position: 'left', + stack: 'demo', + stackWeight: 2, + border: { + color: Utils.CHART_COLORS.red + } + }, + y2: { + type: 'category', + labels: ['ON', 'OFF'], + offset: true, + position: 'left', + stack: 'demo', + stackWeight: 1, + border: { + color: Utils.CHART_COLORS.blue + } + } + } + }, +}; +// + +module.exports = { + config: config, +}; +``` + +## Docs +* [Line](../../charts/line.md) +* [Axes scales](../../axes/) + * [Stacking](../../axes/#stacking) +* [Data structures (`labels`)](../../general/data-structures.md) diff --git a/docs/samples/scales/time-combo.md b/docs/samples/scales/time-combo.md new file mode 100644 index 00000000000..9c7436ca219 --- /dev/null +++ b/docs/samples/scales/time-combo.md @@ -0,0 +1,91 @@ +# Time Scale - Combo Chart + +```js chart-editor +// +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = Utils.numbers({count: chart.data.labels.length, min: 0, max: 100}); + }); + chart.update(); + } + }, +]; +// + +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: 0, max: 100}; + +const labels = []; + +for (let i = 0; i < DATA_COUNT; ++i) { + labels.push(Utils.newDate(i)); +} + +const data = { + labels: labels, + datasets: [{ + type: 'bar', + label: 'Dataset 1', + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + borderColor: Utils.CHART_COLORS.red, + data: Utils.numbers(NUMBER_CFG), + }, { + type: 'bar', + label: 'Dataset 2', + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), + borderColor: Utils.CHART_COLORS.blue, + data: Utils.numbers(NUMBER_CFG), + }, { + type: 'line', + label: 'Dataset 3', + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.green, 0.5), + borderColor: Utils.CHART_COLORS.green, + fill: false, + data: Utils.numbers(NUMBER_CFG), + }] +}; +// + +// +const config = { + type: 'line', + data: data, + options: { + plugins: { + title: { + text: 'Chart.js Combo Time Scale', + display: true + } + }, + scales: { + x: { + type: 'time', + display: true, + offset: true, + ticks: { + source: 'data' + }, + time: { + unit: 'day' + }, + }, + }, + }, +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` + +## Docs +* [Bar](../../charts/bar.md) +* [Line](../../charts/line.md) +* [Data structures (`labels`)](../../general/data-structures.md) +* [Time Scale](../../axes/cartesian/time.md) diff --git a/docs/samples/scales/time-line.md b/docs/samples/scales/time-line.md new file mode 100644 index 00000000000..057fe90ea8f --- /dev/null +++ b/docs/samples/scales/time-line.md @@ -0,0 +1,116 @@ +# Time Scale + +```js chart-editor +// +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data.forEach(function(dataObj, j) { + const newVal = Utils.rand(0, 100); + + if (typeof dataObj === 'object') { + dataObj.y = newVal; + } else { + dataset.data[j] = newVal; + } + }); + }); + chart.update(); + } + }, +]; +// + +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: 0, max: 100}; + +const data = { + labels: [ // Date Objects + Utils.newDate(0), + Utils.newDate(1), + Utils.newDate(2), + Utils.newDate(3), + Utils.newDate(4), + Utils.newDate(5), + Utils.newDate(6) + ], + datasets: [{ + label: 'My First dataset', + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + borderColor: Utils.CHART_COLORS.red, + fill: false, + data: Utils.numbers(NUMBER_CFG), + }, { + label: 'My Second dataset', + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), + borderColor: Utils.CHART_COLORS.blue, + fill: false, + data: Utils.numbers(NUMBER_CFG), + }, { + label: 'Dataset with point data', + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.green, 0.5), + borderColor: Utils.CHART_COLORS.green, + fill: false, + data: [{ + x: Utils.newDateString(0), + y: Utils.rand(0, 100) + }, { + x: Utils.newDateString(5), + y: Utils.rand(0, 100) + }, { + x: Utils.newDateString(7), + y: Utils.rand(0, 100) + }, { + x: Utils.newDateString(15), + y: Utils.rand(0, 100) + }], + }] +}; +// + +// +const config = { + type: 'line', + data: data, + options: { + plugins: { + title: { + text: 'Chart.js Time Scale', + display: true + } + }, + scales: { + x: { + type: 'time', + time: { + // Luxon format string + tooltipFormat: 'DD T' + }, + title: { + display: true, + text: 'Date' + } + }, + y: { + title: { + display: true, + text: 'value' + } + } + }, + }, +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` + +## Docs +* [Line](../../charts/line.md) +* [Time Cartesian Axis](../../axes/cartesian/time.md) \ No newline at end of file diff --git a/docs/samples/scales/time-max-span.md b/docs/samples/scales/time-max-span.md new file mode 100644 index 00000000000..47d771f8773 --- /dev/null +++ b/docs/samples/scales/time-max-span.md @@ -0,0 +1,131 @@ +# Time Scale - Max Span + +```js chart-editor +// +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data.forEach(function(dataObj, j) { + const newVal = Utils.rand(0, 100); + + if (typeof dataObj === 'object') { + dataObj.y = newVal; + } else { + dataset.data[j] = newVal; + } + }); + }); + chart.update(); + } + }, +]; +// + +// +const data = { + datasets: [{ + label: 'Dataset with string point data', + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + borderColor: Utils.CHART_COLORS.red, + fill: false, + data: [{ + x: Utils.newDateString(0), + y: Utils.rand(0, 100) + }, { + x: Utils.newDateString(2), + y: Utils.rand(0, 100) + }, { + x: Utils.newDateString(4), + y: Utils.rand(0, 100) + }, { + x: Utils.newDateString(6), + y: Utils.rand(0, 100) + }], + }, { + label: 'Dataset with date object point data', + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), + borderColor: Utils.CHART_COLORS.blue, + fill: false, + data: [{ + x: Utils.newDate(0), + y: Utils.rand(0, 100) + }, { + x: Utils.newDate(2), + y: Utils.rand(0, 100) + }, { + x: Utils.newDate(5), + y: Utils.rand(0, 100) + }, { + x: Utils.newDate(6), + y: Utils.rand(0, 100) + }] + }] +}; +// + +// +const config = { + type: 'line', + data: data, + options: { + spanGaps: 1000 * 60 * 60 * 24 * 2, // 2 days + responsive: true, + interaction: { + mode: 'nearest', + }, + plugins: { + title: { + display: true, + text: 'Chart.js Time - spanGaps: 172800000 (2 days in ms)' + }, + }, + scales: { + x: { + type: 'time', + display: true, + title: { + display: true, + text: 'Date' + }, + ticks: { + autoSkip: false, + maxRotation: 0, + major: { + enabled: true + }, + // color: function(context) { + // return context.tick && context.tick.major ? '#FF0000' : 'rgba(0,0,0,0.1)'; + // }, + font: function(context) { + if (context.tick && context.tick.major) { + return { + weight: 'bold', + }; + } + } + } + }, + y: { + display: true, + title: { + display: true, + text: 'value' + } + } + } + }, +}; +// + +module.exports = { + actions: [], + config: config, +}; +``` + +## Docs +* [Line](../../charts/line.md) + * [`spanGaps`](../../charts/line.md#line-styling) +* [Time Scale](../../axes/cartesian/time.md) diff --git a/docs/samples/scriptable/bar.md b/docs/samples/scriptable/bar.md new file mode 100644 index 00000000000..74562c881b8 --- /dev/null +++ b/docs/samples/scriptable/bar.md @@ -0,0 +1,81 @@ +# Bar Chart +Demo selecting bar color based on the bar's y value. + +```js chart-editor +// +const DATA_COUNT = 16; +Utils.srand(110); + +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = generateData(); + }); + chart.update(); + } + }, +]; +// + +// +function generateData() { + return Utils.numbers({ + count: DATA_COUNT, + min: -100, + max: 100 + }); +} + +const data = { + labels: Utils.months({count: DATA_COUNT}), + datasets: [{ + data: generateData(), + }] +}; +// + +// +function colorize(opaque) { + return (ctx) => { + const v = ctx.parsed.y; + const c = v < -50 ? '#D60000' + : v < 0 ? '#F46300' + : v < 50 ? '#0358B6' + : '#44DE28'; + + return opaque ? c : Utils.transparentize(c, 1 - Math.abs(v / 150)); + }; +} + +const config = { + type: 'bar', + data: data, + options: { + plugins: { + legend: false, + }, + elements: { + bar: { + backgroundColor: colorize(false), + borderColor: colorize(true), + borderWidth: 2 + } + } + } +}; +// + +module.exports = { + actions, + config, +}; +``` + +## Docs +* [Bar](../../charts/bar.md) +* [Data structures (`labels`)](../../general/data-structures.md) + * [Dataset Configuration (`stack`)](../../general/data-structures.md#dataset-configuration) +* [Options](../../general/options.md) + * [Scriptable Options](../../general/options.md#scriptable-options) diff --git a/docs/samples/scriptable/bubble.md b/docs/samples/scriptable/bubble.md new file mode 100644 index 00000000000..7d2c0286e81 --- /dev/null +++ b/docs/samples/scriptable/bubble.md @@ -0,0 +1,114 @@ +# Bubble Chart + +```js chart-editor +// +const DATA_COUNT = 16; +const MIN_XY = -150; +const MAX_XY = 100; +Utils.srand(110); + +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = generateData(); + }); + chart.update(); + } + }, +]; +// + +// +function generateData() { + const data = []; + let i; + + for (i = 0; i < DATA_COUNT; ++i) { + data.push({ + x: Utils.rand(MIN_XY, MAX_XY), + y: Utils.rand(MIN_XY, MAX_XY), + v: Utils.rand(0, 1000) + }); + } + + return data; +} + +const data = { + datasets: [{ + data: generateData() + }, { + data: generateData() + }] +}; +// + +// +function channelValue(x, y, values) { + return x < 0 && y < 0 ? values[0] : x < 0 ? values[1] : y < 0 ? values[2] : values[3]; +} + +function colorize(opaque, context) { + const value = context.raw; + const x = value.x / 100; + const y = value.y / 100; + const r = channelValue(x, y, [250, 150, 50, 0]); + const g = channelValue(x, y, [0, 50, 150, 250]); + const b = channelValue(x, y, [0, 150, 150, 250]); + const a = opaque ? 1 : 0.5 * value.v / 1000; + + return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')'; +} + +const config = { + type: 'bubble', + data: data, + options: { + aspectRatio: 1, + plugins: { + legend: false, + tooltip: false, + }, + elements: { + point: { + backgroundColor: colorize.bind(null, false), + + borderColor: colorize.bind(null, true), + + borderWidth: function(context) { + return Math.min(Math.max(1, context.datasetIndex + 1), 8); + }, + + hoverBackgroundColor: 'transparent', + + hoverBorderColor: function(context) { + return Utils.color(context.datasetIndex); + }, + + hoverBorderWidth: function(context) { + return Math.round(8 * context.raw.v / 1000); + }, + + radius: function(context) { + const size = context.chart.width; + const base = Math.abs(context.raw.v) / 1000; + return (size / 24) * base; + } + } + } + } +}; +// + +module.exports = { + actions, + config, +}; +``` + +## Docs +* [Bubble](../../charts/bubble.md) +* [Options](../../general/options.md) + * [Scriptable Options](../../general/options.md#scriptable-options) \ No newline at end of file diff --git a/docs/samples/scriptable/line.md b/docs/samples/scriptable/line.md new file mode 100644 index 00000000000..bef78abc737 --- /dev/null +++ b/docs/samples/scriptable/line.md @@ -0,0 +1,99 @@ +# Line Chart + +```js chart-editor +// +const DATA_COUNT = 12; +Utils.srand(110); + +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = generateData(); + }); + chart.update(); + } + }, +]; +// + +// +function generateData() { + return Utils.numbers({ + count: DATA_COUNT, + min: 0, + max: 100 + }); +} + +const data = { + labels: Utils.months({count: DATA_COUNT}), + datasets: [{ + data: generateData() + }] +}; +// + +// +function getLineColor(ctx) { + return Utils.color(ctx.datasetIndex); +} + +function alternatePointStyles(ctx) { + const index = ctx.dataIndex; + return index % 2 === 0 ? 'circle' : 'rect'; +} + +function makeHalfAsOpaque(ctx) { + return Utils.transparentize(getLineColor(ctx)); +} + +function adjustRadiusBasedOnData(ctx) { + const v = ctx.parsed.y; + return v < 10 ? 5 + : v < 25 ? 7 + : v < 50 ? 9 + : v < 75 ? 11 + : 15; +} + +const config = { + type: 'line', + data: data, + options: { + plugins: { + legend: false, + tooltip: true, + }, + elements: { + line: { + fill: false, + backgroundColor: getLineColor, + borderColor: getLineColor, + }, + point: { + backgroundColor: getLineColor, + hoverBackgroundColor: makeHalfAsOpaque, + radius: adjustRadiusBasedOnData, + pointStyle: alternatePointStyles, + hoverRadius: 15, + } + } + } +}; +// + +module.exports = { + actions, + config, +}; +``` + +## Docs +* [Line](../../charts/line.md) + * [Point Styling](../../charts/line.md#point-styling) +* [Options](../../general/options.md) + * [Scriptable Options](../../general/options.md#scriptable-options) +* [Data structures (`labels`)](../../general/data-structures.md) + diff --git a/docs/samples/scriptable/pie.md b/docs/samples/scriptable/pie.md new file mode 100644 index 00000000000..1a633108c42 --- /dev/null +++ b/docs/samples/scriptable/pie.md @@ -0,0 +1,92 @@ +# Pie Chart + +```js chart-editor +// +const DATA_COUNT = 5; +Utils.srand(110); + +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = generateData(); + }); + chart.update(); + } + }, + { + name: 'Toggle Doughnut View', + handler(chart) { + if (chart.options.cutout) { + chart.options.cutout = 0; + } else { + chart.options.cutout = '50%'; + } + chart.update(); + } + } +]; +// + +// +function generateData() { + return Utils.numbers({ + count: DATA_COUNT, + min: -100, + max: 100 + }); +} + +const data = { + datasets: [{ + data: generateData() + }] +}; +// + +// +function colorize(opaque, hover, ctx) { + const v = ctx.parsed; + const c = v < -50 ? '#D60000' + : v < 0 ? '#F46300' + : v < 50 ? '#0358B6' + : '#44DE28'; + + const opacity = hover ? 1 - Math.abs(v / 150) - 0.2 : 1 - Math.abs(v / 150); + + return opaque ? c : Utils.transparentize(c, opacity); +} + +function hoverColorize(ctx) { + return colorize(false, true, ctx); +} + +const config = { + type: 'pie', + data: data, + options: { + plugins: { + legend: false, + tooltip: false, + }, + elements: { + arc: { + backgroundColor: colorize.bind(null, false, false), + hoverBackgroundColor: hoverColorize + } + } + } +}; +// + +module.exports = { + actions, + config, +}; +``` + +## Docs +* [Options](../../general/options.md) + * [Scriptable Options](../../general/options.md#scriptable-options) +* [Doughnut and Pie Charts](../../charts/doughnut.md) \ No newline at end of file diff --git a/docs/samples/scriptable/polar.md b/docs/samples/scriptable/polar.md new file mode 100644 index 00000000000..de2178b98d1 --- /dev/null +++ b/docs/samples/scriptable/polar.md @@ -0,0 +1,82 @@ +# Polar Area Chart + +```js chart-editor +// +const DATA_COUNT = 7; +Utils.srand(110); + +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = generateData(); + }); + chart.update(); + } + }, +]; +// + +// +function generateData() { + return Utils.numbers({ + count: DATA_COUNT, + min: 0, + max: 100 + }); +} + +const data = { + labels: Utils.months({count: DATA_COUNT}), + datasets: [{ + data: generateData() + }] +}; +// + +// +function colorize(opaque, hover, ctx) { + const v = ctx.raw; + const c = v < 35 ? '#D60000' + : v < 55 ? '#F46300' + : v < 75 ? '#0358B6' + : '#44DE28'; + + const opacity = hover ? 1 - Math.abs(v / 150) - 0.2 : 1 - Math.abs(v / 150); + + return opaque ? c : Utils.transparentize(c, opacity); +} + +function hoverColorize(ctx) { + return colorize(false, true, ctx); +} + +const config = { + type: 'polarArea', + data: data, + options: { + plugins: { + legend: false, + tooltip: false, + }, + elements: { + arc: { + backgroundColor: colorize.bind(null, false, false), + hoverBackgroundColor: hoverColorize + } + } + } +}; +// + +module.exports = { + actions, + config, +}; +``` + +## Docs +* [Options](../../general/options.md) + * [Scriptable Options](../../general/options.md#scriptable-options) +* [Polar Area Chart](../../charts/polar.md) diff --git a/docs/samples/scriptable/radar.md b/docs/samples/scriptable/radar.md new file mode 100644 index 00000000000..9ee177de891 --- /dev/null +++ b/docs/samples/scriptable/radar.md @@ -0,0 +1,99 @@ +# Radar Chart + +```js chart-editor +// +const DATA_COUNT = 7; +Utils.srand(110); + +const actions = [ + { + name: 'Randomize', + handler(chart) { + chart.data.datasets.forEach(dataset => { + dataset.data = generateData(); + }); + chart.update(); + } + }, +]; +// + +// +function generateData() { + return Utils.numbers({ + count: DATA_COUNT, + min: 0, + max: 100 + }); +} + +const data = { + labels: [['Eating', 'Dinner'], ['Drinking', 'Water'], 'Sleeping', ['Designing', 'Graphics'], 'Coding', 'Cycling', 'Running'], + datasets: [{ + data: generateData() + }] +}; +// + +// +function getLineColor(ctx) { + return Utils.color(ctx.datasetIndex); +} + +function alternatePointStyles(ctx) { + const index = ctx.dataIndex; + return index % 2 === 0 ? 'circle' : 'rect'; +} + +function makeHalfAsOpaque(ctx) { + return Utils.transparentize(getLineColor(ctx)); +} + +function make20PercentOpaque(ctx) { + return Utils.transparentize(getLineColor(ctx), 0.8); +} + +function adjustRadiusBasedOnData(ctx) { + const v = ctx.parsed.y; + return v < 10 ? 5 + : v < 25 ? 7 + : v < 50 ? 9 + : v < 75 ? 11 + : 15; +} + +const config = { + type: 'radar', + data: data, + options: { + plugins: { + legend: false, + tooltip: false, + }, + elements: { + line: { + backgroundColor: make20PercentOpaque, + borderColor: getLineColor, + }, + point: { + backgroundColor: getLineColor, + hoverBackgroundColor: makeHalfAsOpaque, + radius: adjustRadiusBasedOnData, + pointStyle: alternatePointStyles, + hoverRadius: 15, + } + } + } +}; +// + +module.exports = { + actions, + config, +}; +``` + +## Docs +* [Options](../../general/options.md) + * [Scriptable Options](../../general/options.md#scriptable-options) +* [Radar](../../charts/radar.md) diff --git a/docs/samples/subtitle/basic.md b/docs/samples/subtitle/basic.md new file mode 100644 index 00000000000..285ed2693ea --- /dev/null +++ b/docs/samples/subtitle/basic.md @@ -0,0 +1,61 @@ +# Basic + +This sample shows basic usage of subtitle. + +```js chart-editor +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; +const data = { + labels: Utils.months({count: DATA_COUNT}), + datasets: [ + { + label: 'Dataset 1', + data: Utils.numbers(NUMBER_CFG), + fill: false, + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + }, + ] +}; +// + +// +const config = { + type: 'line', + data: data, + options: { + plugins: { + title: { + display: true, + text: 'Chart Title', + }, + subtitle: { + display: true, + text: 'Chart Subtitle', + color: 'blue', + font: { + size: 12, + family: 'tahoma', + weight: 'normal', + style: 'italic' + }, + padding: { + bottom: 10 + } + } + } + } +}; +// + +module.exports = { + config: config, +}; +``` + +## Docs +* [Data structures (`labels`)](../../general/data-structures.md) +* [Line](../../charts/line.md) +* [Title](../../configuration/title.md) +* [Subtitle](../../configuration/subtitle.md) diff --git a/docs/samples/title/alignment.md b/docs/samples/title/alignment.md new file mode 100644 index 00000000000..5c612e70908 --- /dev/null +++ b/docs/samples/title/alignment.md @@ -0,0 +1,74 @@ +# Alignment + +This sample show how to configure the alignment of the chart title + +```js chart-editor +// +const actions = [ + { + name: 'Title Alignment: start', + handler(chart) { + chart.options.plugins.title.align = 'start'; + chart.update(); + } + }, + { + name: 'Title Alignment: center (default)', + handler(chart) { + chart.options.plugins.title.align = 'center'; + chart.update(); + } + }, + { + name: 'Title Alignment: end', + handler(chart) { + chart.options.plugins.title.align = 'end'; + chart.update(); + } + }, +]; +// + + +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; +const data = { + labels: Utils.months({count: DATA_COUNT}), + datasets: [ + { + label: 'Dataset 1', + data: Utils.numbers(NUMBER_CFG), + fill: false, + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + }, + ] +}; +// + +// +const config = { + type: 'line', + data: data, + options: { + plugins: { + title: { + display: true, + text: 'Chart Title', + } + } + } +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` + +## Docs +* [Data structures (`labels`)](../../general/data-structures.md) +* [Line](../../charts/line.md) +* [Title](../../configuration/title.md) \ No newline at end of file diff --git a/docs/samples/tooltip/content.md b/docs/samples/tooltip/content.md new file mode 100644 index 00000000000..bc5834a4e7c --- /dev/null +++ b/docs/samples/tooltip/content.md @@ -0,0 +1,72 @@ +# Custom Tooltip Content + +This sample shows how to use the tooltip callbacks to add additional content to the tooltip. + +```js chart-editor +// +const footer = (tooltipItems) => { + let sum = 0; + + tooltipItems.forEach(function(tooltipItem) { + sum += tooltipItem.parsed.y; + }); + return 'Sum: ' + sum; +}; + +// + +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100, decimals: 0}; +const data = { + labels: Utils.months({count: DATA_COUNT}), + datasets: [ + { + label: 'Dataset 1', + data: Utils.numbers(NUMBER_CFG), + fill: false, + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + }, + { + label: 'Dataset 2', + data: Utils.numbers(NUMBER_CFG), + fill: false, + borderColor: Utils.CHART_COLORS.blue, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), + }, + ] +}; +// + +// +const config = { + type: 'line', + data: data, + options: { + interaction: { + intersect: false, + mode: 'index', + }, + plugins: { + tooltip: { + callbacks: { + footer: footer, + } + } + } + } +}; +// + +module.exports = { + actions: [], + config: config, +}; +``` + +## Docs +* [Data structures (`labels`)](../../general/data-structures.md) +* [Line](../../charts/line.md) +* [Tooltip](../../configuration/tooltip.md) + * [Tooltip Callbacks](../../configuration/tooltip.md#tooltip-callbacks) diff --git a/docs/samples/tooltip/html.md b/docs/samples/tooltip/html.md new file mode 100644 index 00000000000..267787eaf33 --- /dev/null +++ b/docs/samples/tooltip/html.md @@ -0,0 +1,172 @@ +# External HTML Tooltip + +This sample shows how to use the external tooltip functionality to generate an HTML tooltip. + +```js chart-editor +// +const getOrCreateTooltip = (chart) => { + let tooltipEl = chart.canvas.parentNode.querySelector('div'); + + if (!tooltipEl) { + tooltipEl = document.createElement('div'); + tooltipEl.style.background = 'rgba(0, 0, 0, 0.7)'; + tooltipEl.style.borderRadius = '3px'; + tooltipEl.style.color = 'white'; + tooltipEl.style.opacity = 1; + tooltipEl.style.pointerEvents = 'none'; + tooltipEl.style.position = 'absolute'; + tooltipEl.style.transform = 'translate(-50%, 0)'; + tooltipEl.style.transition = 'all .1s ease'; + + const table = document.createElement('table'); + table.style.margin = '0px'; + + tooltipEl.appendChild(table); + chart.canvas.parentNode.appendChild(tooltipEl); + } + + return tooltipEl; +}; + +const externalTooltipHandler = (context) => { + // Tooltip Element + const {chart, tooltip} = context; + const tooltipEl = getOrCreateTooltip(chart); + + // Hide if no tooltip + if (tooltip.opacity === 0) { + tooltipEl.style.opacity = 0; + return; + } + + // Set Text + if (tooltip.body) { + const titleLines = tooltip.title || []; + const bodyLines = tooltip.body.map(b => b.lines); + + const tableHead = document.createElement('thead'); + + titleLines.forEach(title => { + const tr = document.createElement('tr'); + tr.style.borderWidth = 0; + + const th = document.createElement('th'); + th.style.borderWidth = 0; + const text = document.createTextNode(title); + + th.appendChild(text); + tr.appendChild(th); + tableHead.appendChild(tr); + }); + + const tableBody = document.createElement('tbody'); + bodyLines.forEach((body, i) => { + const colors = tooltip.labelColors[i]; + + const span = document.createElement('span'); + span.style.background = colors.backgroundColor; + span.style.borderColor = colors.borderColor; + span.style.borderWidth = '2px'; + span.style.marginRight = '10px'; + span.style.height = '10px'; + span.style.width = '10px'; + span.style.display = 'inline-block'; + + const tr = document.createElement('tr'); + tr.style.backgroundColor = 'inherit'; + tr.style.borderWidth = 0; + + const td = document.createElement('td'); + td.style.borderWidth = 0; + + const text = document.createTextNode(body); + + td.appendChild(span); + td.appendChild(text); + tr.appendChild(td); + tableBody.appendChild(tr); + }); + + const tableRoot = tooltipEl.querySelector('table'); + + // Remove old children + while (tableRoot.firstChild) { + tableRoot.firstChild.remove(); + } + + // Add new children + tableRoot.appendChild(tableHead); + tableRoot.appendChild(tableBody); + } + + const {offsetLeft: positionX, offsetTop: positionY} = chart.canvas; + + // Display, position, and set styles for font + tooltipEl.style.opacity = 1; + tooltipEl.style.left = positionX + tooltip.caretX + 'px'; + tooltipEl.style.top = positionY + tooltip.caretY + 'px'; + tooltipEl.style.font = tooltip.options.bodyFont.string; + tooltipEl.style.padding = tooltip.options.padding + 'px ' + tooltip.options.padding + 'px'; +}; +// + +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100, decimals: 0}; +const data = { + labels: Utils.months({count: DATA_COUNT}), + datasets: [ + { + label: 'Dataset 1', + data: Utils.numbers(NUMBER_CFG), + fill: false, + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + }, + { + label: 'Dataset 2', + data: Utils.numbers(NUMBER_CFG), + fill: false, + borderColor: Utils.CHART_COLORS.blue, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), + }, + ] +}; +// + +// +const config = { + type: 'line', + data: data, + options: { + interaction: { + mode: 'index', + intersect: false, + }, + plugins: { + title: { + display: true, + text: 'Chart.js Line Chart - External Tooltips' + }, + tooltip: { + enabled: false, + position: 'nearest', + external: externalTooltipHandler + } + } + } +}; +// + +module.exports = { + actions: [], + config: config, +}; +``` + +## Docs +* [Data structures (`labels`)](../../general/data-structures.md) +* [Line](../../charts/line.md) +* [Tooltip](../../configuration/tooltip.md) + * [External (Custom) Tooltips](../../configuration/tooltip.md#external-custom-tooltips) + \ No newline at end of file diff --git a/docs/samples/tooltip/interactions.md b/docs/samples/tooltip/interactions.md new file mode 100644 index 00000000000..1b4e3937eeb --- /dev/null +++ b/docs/samples/tooltip/interactions.md @@ -0,0 +1,136 @@ +# Interaction Modes + +This sample shows how to use the tooltip position mode setting. + +```js chart-editor +// +const actions = [ + { + name: 'Mode: index', + handler(chart) { + chart.options.interaction.axis = 'xy'; + chart.options.interaction.mode = 'index'; + chart.update(); + } + }, + { + name: 'Mode: dataset', + handler(chart) { + chart.options.interaction.axis = 'xy'; + chart.options.interaction.mode = 'dataset'; + chart.update(); + } + }, + { + name: 'Mode: point', + handler(chart) { + chart.options.interaction.axis = 'xy'; + chart.options.interaction.mode = 'point'; + chart.update(); + } + }, + { + name: 'Mode: nearest, axis: xy', + handler(chart) { + chart.options.interaction.axis = 'xy'; + chart.options.interaction.mode = 'nearest'; + chart.update(); + } + }, + { + name: 'Mode: nearest, axis: x', + handler(chart) { + chart.options.interaction.axis = 'x'; + chart.options.interaction.mode = 'nearest'; + chart.update(); + } + }, + { + name: 'Mode: nearest, axis: y', + handler(chart) { + chart.options.interaction.axis = 'y'; + chart.options.interaction.mode = 'nearest'; + chart.update(); + } + }, + { + name: 'Mode: x', + handler(chart) { + chart.options.interaction.mode = 'x'; + chart.update(); + } + }, + { + name: 'Mode: y', + handler(chart) { + chart.options.interaction.mode = 'y'; + chart.update(); + } + }, + { + name: 'Toggle Intersect', + handler(chart) { + chart.options.interaction.intersect = !chart.options.interaction.intersect; + chart.update(); + } + }, +]; +// + +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; +const data = { + labels: Utils.months({count: DATA_COUNT}), + datasets: [ + { + label: 'Dataset 1', + data: Utils.numbers(NUMBER_CFG), + fill: false, + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + }, + { + label: 'Dataset 2', + data: Utils.numbers(NUMBER_CFG), + fill: false, + borderColor: Utils.CHART_COLORS.blue, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), + }, + ] +}; +// + +// +const config = { + type: 'line', + data: data, + options: { + interaction: { + intersect: false, + mode: 'index', + }, + plugins: { + title: { + display: true, + text: (ctx) => { + const {axis = 'xy', intersect, mode} = ctx.chart.options.interaction; + return 'Mode: ' + mode + ', axis: ' + axis + ', intersect: ' + intersect; + } + }, + } + } +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` + +## Docs +* [Data structures (`labels`)](../../general/data-structures.md) +* [Line](../../charts/line.md) +* [Tooltip](../../configuration/tooltip.md) +* [Interactions](../../configuration/interactions.md) diff --git a/docs/samples/tooltip/point-style.md b/docs/samples/tooltip/point-style.md new file mode 100644 index 00000000000..d6dcfda5b20 --- /dev/null +++ b/docs/samples/tooltip/point-style.md @@ -0,0 +1,89 @@ +# Point Style + +This sample shows how to use the dataset point style in the tooltip instead of a rectangle to identify each dataset. + +```js chart-editor +// +const actions = [ + { + name: 'Toggle Tooltip Point Style', + handler(chart) { + chart.options.plugins.tooltip.usePointStyle = !chart.options.plugins.tooltip.usePointStyle; + chart.update(); + } + }, +]; +// + +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; +const data = { + labels: Utils.months({count: DATA_COUNT}), + datasets: [ + { + label: 'Triangles', + data: Utils.numbers(NUMBER_CFG), + fill: false, + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + pointStyle: 'triangle', + pointRadius: 6, + }, + { + label: 'Circles', + data: Utils.numbers(NUMBER_CFG), + fill: false, + borderColor: Utils.CHART_COLORS.blue, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), + pointStyle: 'circle', + pointRadius: 6, + }, + { + label: 'Stars', + data: Utils.numbers(NUMBER_CFG), + fill: false, + borderColor: Utils.CHART_COLORS.green, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.green, 0.5), + pointStyle: 'star', + pointRadius: 6, + } + ] +}; +// + +// +const config = { + type: 'line', + data: data, + options: { + interaction: { + mode: 'index', + }, + plugins: { + title: { + display: true, + text: (ctx) => 'Tooltip point style: ' + ctx.chart.options.plugins.tooltip.usePointStyle, + }, + tooltip: { + usePointStyle: true, + } + } + } +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` + +## Docs +* [Data structures (`labels`)](../../general/data-structures.md) +* [Line](../../charts/line.md) +* [Tooltip](../../configuration/tooltip.md) + * `usePointStyle` +* [Elements](../../configuration/elements.md) + * [Point Styles](../../configuration/elements.md#point-styles) + diff --git a/docs/samples/tooltip/position.md b/docs/samples/tooltip/position.md new file mode 100644 index 00000000000..4209de5a6f7 --- /dev/null +++ b/docs/samples/tooltip/position.md @@ -0,0 +1,108 @@ +# Position + +This sample shows how to use the tooltip position mode setting. + +```js chart-editor +// +const actions = [ + { + name: 'Position: average', + handler(chart) { + chart.options.plugins.tooltip.position = 'average'; + chart.update(); + } + }, + { + name: 'Position: nearest', + handler(chart) { + chart.options.plugins.tooltip.position = 'nearest'; + chart.update(); + } + }, + { + name: 'Position: bottom (custom)', + handler(chart) { + chart.options.plugins.tooltip.position = 'bottom'; + chart.update(); + } + }, +]; +// + +// +const DATA_COUNT = 7; +const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; +const data = { + labels: Utils.months({count: DATA_COUNT}), + datasets: [ + { + label: 'Dataset 1', + data: Utils.numbers(NUMBER_CFG), + fill: false, + borderColor: Utils.CHART_COLORS.red, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), + }, + { + label: 'Dataset 2', + data: Utils.numbers(NUMBER_CFG), + fill: false, + borderColor: Utils.CHART_COLORS.blue, + backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), + }, + ] +}; +// + +// +// Create a custom tooltip positioner to put at the bottom of the chart area +components.Tooltip.positioners.bottom = function(items) { + const pos = components.Tooltip.positioners.average(items); + + // Happens when nothing is found + if (pos === false) { + return false; + } + + const chart = this.chart; + + return { + x: pos.x, + y: chart.chartArea.bottom, + xAlign: 'center', + yAlign: 'bottom', + }; +}; + +// + +// +const config = { + type: 'line', + data: data, + options: { + interaction: { + intersect: false, + mode: 'index', + }, + plugins: { + title: { + display: true, + text: (ctx) => 'Tooltip position mode: ' + ctx.chart.options.plugins.tooltip.position, + }, + } + } +}; +// + +module.exports = { + actions: actions, + config: config, +}; +``` + +## Docs +* [Data structures (`labels`)](../../general/data-structures.md) +* [Line](../../charts/line.md) +* [Tooltip](../../configuration/tooltip.md) + * [Position Modes](../../configuration/tooltip.md#position-modes) + * [Custom Position Modes](../../configuration/tooltip.md#custom-position-modes) \ No newline at end of file diff --git a/docs/samples/utils.md b/docs/samples/utils.md new file mode 100644 index 00000000000..ae8c1e5f83c --- /dev/null +++ b/docs/samples/utils.md @@ -0,0 +1,21 @@ +# Utils + +## Disclaimer +The Utils file contains multiple helper functions that the chart.js sample pages use to generate charts. +These functions are subject to change, including but not limited to breaking changes without prior notice. + +Because of this please don't rely on this file in production environments. + +## Functions + +<<< @/scripts/utils.js + +[File on github](https://github.com/chartjs/Chart.js/blob/master/docs/scripts/utils.js) + +## Components + +Some of the samples make reference to a `components` object. This is an artifact of using a module bundler to build the samples. The creation of that components object is shown below. If chart.js is included as a browser script, these items are accessible via the `Chart` object, i.e `Chart.Tooltip`. + +<<< @/scripts/components.js + +[File on github](https://github.com/chartjs/Chart.js/blob/master/docs/scripts/components.js) diff --git a/docs/scripts/analyzer.js b/docs/scripts/analyzer.js new file mode 100644 index 00000000000..8d71e8a0604 --- /dev/null +++ b/docs/scripts/analyzer.js @@ -0,0 +1,60 @@ +export default { + id: 'samples-filler-analyser', + + beforeInit: function(chart, args, options) { + this.element = document.getElementById(options.target); + }, + + afterUpdate: function(chart) { + var datasets = chart.data.datasets; + var element = this.element; + var stats = []; + var meta, i, ilen, dataset; + + if (!element) { + return; + } + + for (i = 0, ilen = datasets.length; i < ilen; ++i) { + meta = chart.getDatasetMeta(i).$filler; + if (meta) { + dataset = datasets[i]; + stats.push({ + fill: dataset.fill, + target: meta.fill, + visible: meta.visible, + index: i + }); + } + } + + this.element.innerHTML = '' + + '' + + '' + + '' + + '' + + '' + + stats.map(function(stat) { + var target = stat.target; + var row = + '' + + ''; + + if (target === false) { + target = 'none'; + } else if (isFinite(target)) { + target = 'dataset ' + target; + } else { + target = 'boundary "' + target + '"'; + } + + if (stat.visible) { + row += ''; + } else { + row += ''; + } + + return '' + row + ''; + }).join('') + '
    DatasetFillTarget (visibility)
    ' + stat.index + '' + JSON.stringify(stat.fill) + '' + target + '(hidden)
    '; + } +}; diff --git a/docs/scripts/components.js b/docs/scripts/components.js new file mode 100644 index 00000000000..79f7841f3e7 --- /dev/null +++ b/docs/scripts/components.js @@ -0,0 +1,3 @@ +// Add Chart components needed in samples here. +// Usable through `components[name]`. +export {Tooltip} from '../../dist/chart.js'; diff --git a/docs/scripts/derived-bubble.js b/docs/scripts/derived-bubble.js new file mode 100644 index 00000000000..2111ba9df30 --- /dev/null +++ b/docs/scripts/derived-bubble.js @@ -0,0 +1,34 @@ +import {Chart, BubbleController} from 'chart.js'; + +class Custom extends BubbleController { + draw() { + // Call bubble controller method to draw all the points + super.draw(arguments); + + // Now we can do some custom drawing for this dataset. + // Here we'll draw a box around the first point in each dataset, + // using `boxStrokeStyle` dataset option for color + var meta = this.getMeta(); + var pt0 = meta.data[0]; + + const {x, y} = pt0.getProps(['x', 'y']); + const {radius} = pt0.options; + + var ctx = this.chart.ctx; + ctx.save(); + ctx.strokeStyle = this.options.boxStrokeStyle; + ctx.lineWidth = 1; + ctx.strokeRect(x - radius, y - radius, 2 * radius, 2 * radius); + ctx.restore(); + } +} +Custom.id = 'derivedBubble'; +Custom.defaults = { + // Custom defaults. Bubble defaults are inherited. + boxStrokeStyle: 'red' +}; +// Overrides are only inherited, but not merged if defined +// Custom.overrides = Chart.overrides.bubble; + +// Stores the controller so that the chart initialization routine can look it up +Chart.register(Custom); diff --git a/docs/scripts/helpers.js b/docs/scripts/helpers.js new file mode 100644 index 00000000000..dc989488c92 --- /dev/null +++ b/docs/scripts/helpers.js @@ -0,0 +1,3 @@ +// Add helpers needed in samples here. +// Usable through `helpers[name]`. +export {color, getHoverColor, easingEffects} from '../../dist/helpers.js'; diff --git a/docs/scripts/log2.js b/docs/scripts/log2.js new file mode 100644 index 00000000000..7a51a5315f3 --- /dev/null +++ b/docs/scripts/log2.js @@ -0,0 +1,67 @@ +import {Scale, LinearScale} from 'chart.js'; + +export default class Log2Axis extends Scale { + constructor(cfg) { + super(cfg); + this._startValue = undefined; + this._valueRange = 0; + } + + parse(raw, index) { + const value = LinearScale.prototype.parse.apply(this, [raw, index]); + return isFinite(value) && value > 0 ? value : null; + } + + determineDataLimits() { + const {min, max} = this.getMinMax(true); + this.min = isFinite(min) ? Math.max(0, min) : null; + this.max = isFinite(max) ? Math.max(0, max) : null; + } + + buildTicks() { + const ticks = []; + + let power = Math.floor(Math.log2(this.min || 1)); + let maxPower = Math.ceil(Math.log2(this.max || 2)); + while (power <= maxPower) { + ticks.push({value: Math.pow(2, power)}); + power += 1; + } + + this.min = ticks[0].value; + this.max = ticks[ticks.length - 1].value; + return ticks; + } + + /** + * @protected + */ + configure() { + const start = this.min; + + super.configure(); + + this._startValue = Math.log2(start); + this._valueRange = Math.log2(this.max) - Math.log2(start); + } + + getPixelForValue(value) { + if (value === undefined || value === 0) { + value = this.min; + } + + return this.getPixelForDecimal(value === this.min ? 0 + : (Math.log2(value) - this._startValue) / this._valueRange); + } + + getValueForPixel(pixel) { + const decimal = this.getDecimalForPixel(pixel); + return Math.pow(2, this._startValue + decimal * this._valueRange); + } +} + +Log2Axis.id = 'log2'; +Log2Axis.defaults = {}; + +// The derived axis is registered like this: +// Chart.register(Log2Axis); diff --git a/docs/scripts/register.js b/docs/scripts/register.js new file mode 100644 index 00000000000..e9b6a9f893f --- /dev/null +++ b/docs/scripts/register.js @@ -0,0 +1,8 @@ +import {Chart, registerables} from '../../dist/chart.js'; +import Log2Axis from './log2'; +import './derived-bubble'; +import analyzer from './analyzer'; + +Chart.register(...registerables); +Chart.register(Log2Axis); +Chart.register(analyzer); diff --git a/docs/scripts/utils.js b/docs/scripts/utils.js new file mode 100644 index 00000000000..9cd3cfcf815 --- /dev/null +++ b/docs/scripts/utils.js @@ -0,0 +1,161 @@ +import colorLib from '@kurkle/color'; +import {DateTime} from 'luxon'; +import 'chartjs-adapter-luxon'; +import {valueOrDefault} from '../../dist/helpers.js'; + +// Adapted from http://indiegamr.com/generate-repeatable-random-numbers-in-js/ +var _seed = Date.now(); + +export function srand(seed) { + _seed = seed; +} + +export function rand(min, max) { + min = valueOrDefault(min, 0); + max = valueOrDefault(max, 0); + _seed = (_seed * 9301 + 49297) % 233280; + return min + (_seed / 233280) * (max - min); +} + +export function numbers(config) { + var cfg = config || {}; + var min = valueOrDefault(cfg.min, 0); + var max = valueOrDefault(cfg.max, 100); + var from = valueOrDefault(cfg.from, []); + var count = valueOrDefault(cfg.count, 8); + var decimals = valueOrDefault(cfg.decimals, 8); + var continuity = valueOrDefault(cfg.continuity, 1); + var dfactor = Math.pow(10, decimals) || 0; + var data = []; + var i, value; + + for (i = 0; i < count; ++i) { + value = (from[i] || 0) + this.rand(min, max); + if (this.rand() <= continuity) { + data.push(Math.round(dfactor * value) / dfactor); + } else { + data.push(null); + } + } + + return data; +} + +export function points(config) { + const xs = this.numbers(config); + const ys = this.numbers(config); + return xs.map((x, i) => ({x, y: ys[i]})); +} + +export function bubbles(config) { + return this.points(config).map(pt => { + pt.r = this.rand(config.rmin, config.rmax); + return pt; + }); +} + +export function labels(config) { + var cfg = config || {}; + var min = cfg.min || 0; + var max = cfg.max || 100; + var count = cfg.count || 8; + var step = (max - min) / count; + var decimals = cfg.decimals || 8; + var dfactor = Math.pow(10, decimals) || 0; + var prefix = cfg.prefix || ''; + var values = []; + var i; + + for (i = min; i < max; i += step) { + values.push(prefix + Math.round(dfactor * i) / dfactor); + } + + return values; +} + +const MONTHS = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December' +]; + +export function months(config) { + var cfg = config || {}; + var count = cfg.count || 12; + var section = cfg.section; + var values = []; + var i, value; + + for (i = 0; i < count; ++i) { + value = MONTHS[Math.ceil(i) % 12]; + values.push(value.substring(0, section)); + } + + return values; +} + +const COLORS = [ + '#4dc9f6', + '#f67019', + '#f53794', + '#537bc4', + '#acc236', + '#166a8f', + '#00a950', + '#58595b', + '#8549ba' +]; + +export function color(index) { + return COLORS[index % COLORS.length]; +} + +export function transparentize(value, opacity) { + var alpha = opacity === undefined ? 0.5 : 1 - opacity; + return colorLib(value).alpha(alpha).rgbString(); +} + +export const CHART_COLORS = { + red: 'rgb(255, 99, 132)', + orange: 'rgb(255, 159, 64)', + yellow: 'rgb(255, 205, 86)', + green: 'rgb(75, 192, 192)', + blue: 'rgb(54, 162, 235)', + purple: 'rgb(153, 102, 255)', + grey: 'rgb(201, 203, 207)' +}; + +const NAMED_COLORS = [ + CHART_COLORS.red, + CHART_COLORS.orange, + CHART_COLORS.yellow, + CHART_COLORS.green, + CHART_COLORS.blue, + CHART_COLORS.purple, + CHART_COLORS.grey, +]; + +export function namedColor(index) { + return NAMED_COLORS[index % NAMED_COLORS.length]; +} + +export function newDate(days) { + return DateTime.now().plus({days}).toJSDate(); +} + +export function newDateString(days) { + return DateTime.now().plus({days}).toISO(); +} + +export function parseISODate(str) { + return DateTime.fromISO(str); +} diff --git a/gulpfile.js b/gulpfile.js deleted file mode 100644 index 29fa13bf08b..00000000000 --- a/gulpfile.js +++ /dev/null @@ -1,137 +0,0 @@ -var gulp = require('gulp'), - concat = require('gulp-concat'), - uglify = require('gulp-uglify'), - util = require('gulp-util'), - jshint = require('gulp-jshint'), - size = require('gulp-size'), - connect = require('gulp-connect'), - replace = require('gulp-replace'), - htmlv = require('gulp-html-validator'), - inquirer = require('inquirer'), - semver = require('semver'), - exec = require('child_process').exec, - fs = require('fs'), - package = require('./package.json'), - bower = require('./bower.json'); - -var srcDir = './src/'; -/* - * Usage : gulp build --types=Bar,Line,Doughnut - * Output: - A built Chart.js file with Core and types Bar, Line and Doughnut concatenated together - * - A minified version of this code, in Chart.min.js - */ - -gulp.task('build', function(){ - - // Default to all of the chart types, with Chart.Core first - var srcFiles = [FileName('Core')], - isCustom = !!(util.env.types), - outputDir = (isCustom) ? 'custom' : '.'; - if (isCustom){ - util.env.types.split(',').forEach(function(type){ return srcFiles.push(FileName(type));}); - } - else{ - // Seems gulp-concat remove duplicates - nice! - // So we can use this to sort out dependency order - aka include Core first! - srcFiles.push(srcDir+'*'); - } - - return gulp.src(srcFiles) - .pipe(concat('Chart.js')) - .pipe(replace('{{ version }}', package.version)) - .pipe(gulp.dest(outputDir)) - .pipe(uglify({preserveComments:'some'})) - .pipe(concat('Chart.min.js')) - .pipe(gulp.dest(outputDir)); - - function FileName(moduleName){ - return srcDir+'Chart.'+moduleName+'.js'; - } -}); - -/* - * Usage : gulp bump - * Prompts: Version increment to bump - * Output: - New version number written into package.json & bower.json - */ - -gulp.task('bump', function(complete){ - util.log('Current version:', util.colors.cyan(package.version)); - var choices = ['major', 'premajor', 'minor', 'preminor', 'patch', 'prepatch', 'prerelease'].map(function(versionType){ - return versionType + ' (v' + semver.inc(package.version, versionType) + ')'; - }); - inquirer.prompt({ - type: 'list', - name: 'version', - message: 'What version update would you like?', - choices: choices - }, function(res){ - var increment = res.version.split(' ')[0], - newVersion = semver.inc(package.version, increment); - - // Set the new versions into the bower/package object - package.version = newVersion; - bower.version = newVersion; - - // Write these to their own files, then build the output - fs.writeFileSync('package.json', JSON.stringify(package, null, 2)); - fs.writeFileSync('bower.json', JSON.stringify(bower, null, 2)); - - complete(); - }); -}); - -gulp.task('release', ['build'], function(){ - exec('git tag -a v' + package.version); -}); - -gulp.task('jshint', function(){ - return gulp.src(srcDir + '*.js') - .pipe(jshint()) - .pipe(jshint.reporter('default')); -}); - -gulp.task('valid', function(){ - return gulp.src('samples/*.html') - .pipe(htmlv()); -}); - -gulp.task('library-size', function(){ - return gulp.src('Chart.min.js') - .pipe(size({ - gzip: true - })); -}); - -gulp.task('module-sizes', function(){ - return gulp.src(srcDir + '*.js') - .pipe(uglify({preserveComments:'some'})) - .pipe(size({ - showFiles: true, - gzip: true - })); -}); - -gulp.task('watch', function(){ - gulp.watch('./src/*', ['build']); -}); - -gulp.task('test', ['jshint', 'valid']); - -gulp.task('size', ['library-size', 'module-sizes']); - -gulp.task('default', ['build', 'watch']); - -gulp.task('server', function(){ - connect.server({ - port: 8000 - }); -}); - -// Convenience task for opening the project straight from the command line -gulp.task('_open', function(){ - exec('open http://localhost:8000'); - exec('subl .'); -}); - -gulp.task('dev', ['server', 'default']); diff --git a/helpers/helpers.cjs b/helpers/helpers.cjs new file mode 100644 index 00000000000..d476848184a --- /dev/null +++ b/helpers/helpers.cjs @@ -0,0 +1 @@ +module.exports = require('../dist/helpers.cjs'); diff --git a/helpers/helpers.d.ts b/helpers/helpers.d.ts new file mode 100644 index 00000000000..3870461f29b --- /dev/null +++ b/helpers/helpers.d.ts @@ -0,0 +1 @@ +export * from '../dist/helpers/index.js'; diff --git a/helpers/helpers.js b/helpers/helpers.js new file mode 100644 index 00000000000..451fa58f739 --- /dev/null +++ b/helpers/helpers.js @@ -0,0 +1 @@ +export * from '../dist/helpers.js'; diff --git a/helpers/package.json b/helpers/package.json new file mode 100644 index 00000000000..b856155d409 --- /dev/null +++ b/helpers/package.json @@ -0,0 +1,14 @@ +{ + "name": "chart.js-helpers", + "private": true, + "description": "Helpers package. Exists to support bundlers without exports support such as webpack 4.", + "type": "module", + "main": "./helpers.cjs", + "module": "./helpers.js", + "exports": { + "types": "./helpers.d.ts", + "import": "./helpers.js", + "require": "./helpers.cjs" + }, + "types": "./helpers.d.ts" +} diff --git a/karma.conf.cjs b/karma.conf.cjs new file mode 100644 index 00000000000..1306c412e24 --- /dev/null +++ b/karma.conf.cjs @@ -0,0 +1,156 @@ +/* eslint-disable global-require */ +const jasmineSeedReporter = require('./test/seed-reporter.cjs'); +const commonjs = require('@rollup/plugin-commonjs'); +const istanbul = require('rollup-plugin-istanbul'); +const json = require('@rollup/plugin-json'); +const resolve = require('@rollup/plugin-node-resolve').default; +const yargs = require('yargs'); + +module.exports = async function(karma) { + const builds = (await import('./rollup.config.js')).default; + + const args = yargs + .option('verbose', {default: false}) + .argv; + + const grep = (args.grep === true || args.grep === undefined) ? '' : args.grep; + const specPattern = 'test/specs/**/*' + grep + '*.js'; + + // Use the same rollup config as our dist files: when debugging (npm run dev), + // we will prefer the unminified build which is easier to browse and works + // better with source mapping. In other cases, pick the minified build to + // make sure that the minification process (terser) doesn't break anything. + const regex = /chart\.umd(\.min)?\.js$/; + const build = builds.filter(v => v.output.file && v.output.file.match(regex))[0]; + + if (karma.autoWatch) { + build.plugins.pop(); + } + + if (args.coverage) { + build.plugins.push( + istanbul({exclude: ['node_modules/**/*.js', 'package.json']}) + ); + } + + // workaround a karma bug where it doesn't resolve dependencies correctly in + // the same way that Node does + // https://github.com/pnpm/pnpm/issues/720#issuecomment-954120387 + const plugins = Object.keys(require('./package').devDependencies).flatMap( + (packageName) => { + if (!packageName.startsWith('karma-')) { + return []; + } + return [require(packageName)]; + } + ); + + plugins.push(jasmineSeedReporter); + + karma.set({ + frameworks: ['jasmine'], + plugins, + reporters: ['spec', 'kjhtml', 'jasmine-seed'], + browsers: (args.browsers || 'chrome,firefox').split(','), + logLevel: karma.LOG_INFO, + + client: { + jasmine: { + stopOnSpecFailure: !!karma.autoWatch + } + }, + + specReporter: { + // maxLogLines: 5, // limit number of lines logged per test + suppressErrorSummary: true, // do not print error summary + suppressFailed: false, // do not print information about failed tests + suppressPassed: true, // do not print information about passed tests + suppressSkipped: false, // do not print information about skipped tests + showSpecTiming: false, // print the time elapsed for each spec + failFast: false // test would finish with error when a first fail occurs. + }, + + // Explicitly disable hardware acceleration to make image + // diff more stable when ran on Travis and dev machine. + // https://github.com/chartjs/Chart.js/pull/5629 + // Since FF 110 https://github.com/chartjs/Chart.js/issues/11164 + customLaunchers: { + chrome: { + base: 'Chrome', + flags: [ + '--disable-accelerated-2d-canvas', + '--disable-background-timer-throttling', + '--disable-backgrounding-occluded-windows', + '--disable-renderer-backgrounding' + ] + }, + firefox: { + base: 'Firefox', + prefs: { + 'layers.acceleration.disabled': true, + 'gfx.canvas.accelerated': false + } + }, + safari: { + base: 'SafariPrivate' + }, + edge: { + base: 'Edge' + } + }, + + files: [ + {pattern: 'test/fixtures/**/*.js', included: false}, + {pattern: 'test/fixtures/**/*.json', included: false}, + {pattern: 'test/fixtures/**/*.png', included: false}, + 'node_modules/moment/min/moment.min.js', + 'node_modules/moment-timezone/builds/moment-timezone-with-data.min.js', + {pattern: 'test/index.js', watched: false}, + {pattern: 'test/BasicChartWebWorker.js', included: false}, + {pattern: 'src/index.umd.ts', watched: false}, + 'node_modules/chartjs-adapter-moment/dist/chartjs-adapter-moment.js', + {pattern: specPattern} + ], + + preprocessors: { + 'test/index.js': ['rollup'], + 'src/index.umd.ts': ['sources'] + }, + + rollupPreprocessor: { + plugins: [ + json(), + resolve(), + commonjs({exclude: ['src/**', 'test/**']}), + ], + output: { + name: 'test', + format: 'umd', + sourcemap: karma.autoWatch ? 'inline' : false + } + }, + + customPreprocessors: { + sources: { + base: 'rollup', + options: build + } + }, + + // These settings deal with browser disconnects. We had seen test flakiness from Firefox + // [Firefox 56.0.0 (Linux 0.0.0)]: Disconnected (1 times), because no message in 10000 ms. + // https://github.com/jasmine/jasmine/issues/1327#issuecomment-332939551 + browserDisconnectTolerance: 3 + }); + + if (args.coverage) { + karma.reporters.push('coverage'); + karma.coverageReporter = { + dir: 'coverage/', + reporters: [ + {type: 'html', subdir: 'html'}, + {type: 'lcovonly', subdir: (browser) => browser.toLowerCase().split(/[ /-]/)[0]} + ] + }; + } +}; diff --git a/package.json b/package.json index 2a3ac22e036..706e759d0ba 100644 --- a/package.json +++ b/package.json @@ -1,29 +1,139 @@ { - "name": "chart.js", - "homepage": "http://www.chartjs.org", - "description": "Simple HTML5 charts using the canvas element.", - "version": "1.0.2", - "main": "Chart.js", - "repository": { - "type": "git", - "url": "https://github.com/nnnick/Chart.js.git" - }, - "license" : "MIT", - "dependences": {}, - "devDependencies": { - "gulp": "3.5.x", - "gulp-concat": "~2.1.x", - "gulp-connect": "~2.0.5", - "gulp-jshint": "~1.5.1", - "gulp-replace": "^0.4.0", - "gulp-size": "~0.4.0", - "gulp-uglify": "~0.2.x", - "gulp-util": "~2.2.x", - "gulp-html-validator": "^0.0.2", - "inquirer": "^0.5.1", - "semver": "^3.0.1" - }, - "spm": { - "main": "Chart.js" - } + "name": "chart.js", + "homepage": "https://www.chartjs.org", + "description": "Simple HTML5 charts using the canvas element.", + "version": "4.5.1", + "license": "MIT", + "type": "module", + "sideEffects": [ + "./auto/auto.js", + "./auto/auto.cjs", + "./dist/chart.umd.min.js", + "./dist/chart.umd.js" + ], + "jsdelivr": "./dist/chart.umd.min.js", + "unpkg": "./dist/chart.umd.min.js", + "main": "./dist/chart.cjs", + "module": "./dist/chart.js", + "exports": { + ".": { + "types": "./dist/types.d.ts", + "import": "./dist/chart.js", + "require": "./dist/chart.cjs" + }, + "./auto": { + "types": "./auto/auto.d.ts", + "import": "./auto/auto.js", + "require": "./auto/auto.cjs" + }, + "./helpers": { + "types": "./helpers/helpers.d.ts", + "import": "./helpers/helpers.js", + "require": "./helpers/helpers.cjs" + } + }, + "types": "./dist/types.d.ts", + "keywords": [ + "canvas", + "charts", + "data", + "graphs", + "html5", + "responsive" + ], + "repository": { + "type": "git", + "url": "https://github.com/chartjs/Chart.js.git" + }, + "bugs": { + "url": "https://github.com/chartjs/Chart.js/issues" + }, + "files": [ + "auto/**", + "dist/**", + "!dist/docs/**", + "helpers/**" + ], + "scripts": { + "autobuild": "rollup -c -w", + "copyDeclarations": "node -e \"fs.cpSync('./src/types/', './dist/types/', {recursive:true})\"", + "emitDeclarations": "tsc --emitDeclarationOnly && pnpm copyDeclarations", + "build": "rollup -c && pnpm emitDeclarations", + "dev": "karma start ./karma.conf.cjs --auto-watch --no-single-run --browsers chrome --grep", + "dev:ff": "karma start ./karma.conf.cjs --auto-watch --no-single-run --browsers firefox --grep", + "docs": "pnpm run build && pnpm --filter \"./docs/**\" build", + "docs:dev": "pnpm run build && pnpm --filter \"./docs/**\" dev", + "lint-js": "eslint \"src/**/*.{js,ts}\" \"test/**/*.js\" \"docs/**/*.js\" --cache", + "lint-md": "eslint \"**/*.md\" --cache", + "lint-types": "pnpm build && node test/types/autogen.js && tsc -p test/types", + "lint": "concurrently \"pnpm:lint-*\"", + "test": "pnpm lint && pnpm test-ci", + "test-ci": "concurrently \"pnpm:test-ci-*\"", + "test-ci-karma": "cross-env NODE_ENV=test karma start ./karma.conf.cjs --auto-watch --single-run --coverage --grep", + "test-ci-integration": "pnpm --filter \"./test/integration/**\" test" + }, + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^23.0.2", + "@rollup/plugin-inject": "^5.0.2", + "@rollup/plugin-json": "^5.0.1", + "@rollup/plugin-node-resolve": "^15.0.1", + "@swc/core": "^1.3.18", + "@types/estree": "^1.0.0", + "@types/offscreencanvas": "^2019.7.0", + "@typescript-eslint/eslint-plugin": "^5.32.0", + "@typescript-eslint/parser": "^5.32.0", + "chartjs-adapter-luxon": "^1.2.0", + "chartjs-adapter-moment": "^1.0.0", + "chartjs-test-utils": "^0.4.0", + "concurrently": "^7.3.0", + "coveralls": "^3.1.1", + "cross-env": "^7.0.3", + "eslint": "^8.21.0", + "eslint-config-chartjs": "^0.3.0", + "eslint-plugin-es": "^4.1.0", + "eslint-plugin-html": "^7.1.0", + "eslint-plugin-markdown": "^3.0.0", + "esm": "^3.2.25", + "glob": "^8.0.3", + "jasmine": "^3.7.0", + "jasmine-core": "^3.7.1", + "karma": "^6.3.2", + "karma-chrome-launcher": "^3.1.0", + "karma-coverage": "^2.0.3", + "karma-edge-launcher": "^0.4.2", + "karma-firefox-launcher": "^2.1.0", + "karma-jasmine": "^4.0.1", + "karma-jasmine-html-reporter": "^1.5.4", + "karma-rollup-preprocessor": "7.0.7", + "karma-safari-private-launcher": "^1.0.0", + "karma-spec-reporter": "0.0.32", + "luxon": "^3.0.1", + "moment": "^2.29.4", + "moment-timezone": "^0.5.34", + "pixelmatch": "^5.3.0", + "rollup": "^3.3.0", + "rollup-plugin-cleanup": "^3.2.1", + "rollup-plugin-istanbul": "^4.0.0", + "rollup-plugin-swc3": "^0.7.0", + "rollup-plugin-terser": "^7.0.2", + "typescript": "^4.7.4", + "yargs": "^17.5.1" + }, + "engines": { + "pnpm": ">=8" + }, + "packageManager": "pnpm@8.13.0", + "pnpm": { + "overrides": { + "html-entities": "1.4.0" + }, + "peerDependencyRules": { + "ignoreMissing": [ + "chart.js" + ] + } + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 00000000000..47133c232fd --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,17243 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + html-entities: 1.4.0 + +importers: + + .: + dependencies: + '@kurkle/color': + specifier: ^0.3.0 + version: 0.3.2 + devDependencies: + '@rollup/plugin-commonjs': + specifier: ^23.0.2 + version: 23.0.7(rollup@3.20.2) + '@rollup/plugin-inject': + specifier: ^5.0.2 + version: 5.0.3(rollup@3.20.2) + '@rollup/plugin-json': + specifier: ^5.0.1 + version: 5.0.2(rollup@3.20.2) + '@rollup/plugin-node-resolve': + specifier: ^15.0.1 + version: 15.0.1(rollup@3.20.2) + '@swc/core': + specifier: ^1.3.18 + version: 1.3.42 + '@types/estree': + specifier: ^1.0.0 + version: 1.0.0 + '@types/offscreencanvas': + specifier: ^2019.7.0 + version: 2019.7.0 + '@typescript-eslint/eslint-plugin': + specifier: ^5.32.0 + version: 5.57.0(@typescript-eslint/parser@5.57.0)(eslint@8.37.0)(typescript@4.9.5) + '@typescript-eslint/parser': + specifier: ^5.32.0 + version: 5.57.0(eslint@8.37.0)(typescript@4.9.5) + chartjs-adapter-luxon: + specifier: ^1.2.0 + version: 1.3.1(luxon@3.3.0) + chartjs-adapter-moment: + specifier: ^1.0.0 + version: 1.0.1(moment@2.29.4) + chartjs-test-utils: + specifier: ^0.4.0 + version: 0.4.0(jasmine@3.99.0)(karma-jasmine@4.0.2)(karma@6.4.1) + concurrently: + specifier: ^7.3.0 + version: 7.6.0 + coveralls: + specifier: ^3.1.1 + version: 3.1.1 + cross-env: + specifier: ^7.0.3 + version: 7.0.3 + eslint: + specifier: ^8.21.0 + version: 8.37.0 + eslint-config-chartjs: + specifier: ^0.3.0 + version: 0.3.0 + eslint-plugin-es: + specifier: ^4.1.0 + version: 4.1.0(eslint@8.37.0) + eslint-plugin-html: + specifier: ^7.1.0 + version: 7.1.0 + eslint-plugin-markdown: + specifier: ^3.0.0 + version: 3.0.0(eslint@8.37.0) + esm: + specifier: ^3.2.25 + version: 3.2.25 + glob: + specifier: ^8.0.3 + version: 8.1.0 + jasmine: + specifier: ^3.7.0 + version: 3.99.0 + jasmine-core: + specifier: ^3.7.1 + version: 3.99.1 + karma: + specifier: ^6.3.2 + version: 6.4.1 + karma-chrome-launcher: + specifier: ^3.1.0 + version: 3.1.1 + karma-coverage: + specifier: ^2.0.3 + version: 2.2.0 + karma-edge-launcher: + specifier: ^0.4.2 + version: 0.4.2(karma@6.4.1) + karma-firefox-launcher: + specifier: ^2.1.0 + version: 2.1.2 + karma-jasmine: + specifier: ^4.0.1 + version: 4.0.2(karma@6.4.1) + karma-jasmine-html-reporter: + specifier: ^1.5.4 + version: 1.7.0(jasmine-core@3.99.1)(karma-jasmine@4.0.2)(karma@6.4.1) + karma-rollup-preprocessor: + specifier: 7.0.7 + version: 7.0.7(rollup@3.20.2) + karma-safari-private-launcher: + specifier: ^1.0.0 + version: 1.0.0 + karma-spec-reporter: + specifier: 0.0.32 + version: 0.0.32(karma@6.4.1) + luxon: + specifier: ^3.0.1 + version: 3.3.0 + moment: + specifier: ^2.29.4 + version: 2.29.4 + moment-timezone: + specifier: ^0.5.34 + version: 0.5.42 + pixelmatch: + specifier: ^5.3.0 + version: 5.3.0 + rollup: + specifier: ^3.3.0 + version: 3.20.2 + rollup-plugin-cleanup: + specifier: ^3.2.1 + version: 3.2.1(rollup@3.20.2) + rollup-plugin-istanbul: + specifier: ^4.0.0 + version: 4.0.0(rollup@3.20.2) + rollup-plugin-swc3: + specifier: ^0.7.0 + version: 0.7.0(@swc/core@1.3.42)(rollup@3.20.2) + rollup-plugin-terser: + specifier: ^7.0.2 + version: 7.0.2(rollup@3.20.2) + typescript: + specifier: ^4.7.4 + version: 4.9.5 + yargs: + specifier: ^17.5.1 + version: 17.7.1 + + docs: + devDependencies: + '@simonbrunel/vuepress-plugin-versions': + specifier: ^0.2.0 + version: 0.2.0 + '@vuepress/plugin-google-analytics': + specifier: ^1.9.7 + version: 1.9.9 + '@vuepress/plugin-html-redirect': + specifier: ^0.1.2 + version: 0.1.4 + markdown-it: + specifier: ^12.3.2 + version: 12.3.2 + markdown-it-include: + specifier: ^2.0.0 + version: 2.0.0(markdown-it@12.3.2) + typedoc: + specifier: ^0.23.10 + version: 0.23.28(typescript@4.9.5) + typedoc-plugin-markdown: + specifier: ^3.13.4 + version: 3.14.0(typedoc@0.23.28) + typescript: + specifier: ^4.7.4 + version: 4.9.5 + vue: + specifier: ^2.6.14 + version: 2.7.14 + vue-tabs-component: + specifier: ^1.5.0 + version: 1.5.0(vue@2.7.14) + vuepress: + specifier: ^1.9.7 + version: 1.9.9 + vuepress-plugin-code-copy: + specifier: ^1.0.6 + version: 1.0.6 + vuepress-plugin-flexsearch: + specifier: ^0.3.0 + version: 0.3.0 + vuepress-plugin-redirect: + specifier: ^1.2.5 + version: 1.2.5 + vuepress-plugin-tabs: + specifier: ^0.3.0 + version: 0.3.0 + vuepress-plugin-typedoc: + specifier: ^0.11.0 + version: 0.11.2(typedoc-plugin-markdown@3.14.0)(typedoc@0.23.28) + vuepress-theme-chartjs: + specifier: ^0.2.0 + version: 0.2.0(postcss@8.5.6)(vue@2.7.14) + webpack: + specifier: ^4.46.0 + version: 4.46.0 + + test/integration/node: + dependencies: + chart.js: + specifier: workspace:* + version: link:../../.. + + test/integration/node-commonjs: + dependencies: + chart.js: + specifier: workspace:* + version: link:../../.. + + test/integration/react-browser: + dependencies: + '@babel/core': + specifier: ^7.0.0 + version: 7.21.3 + '@babel/plugin-syntax-flow': + specifier: ^7.14.5 + version: 7.18.6(@babel/core@7.21.3) + '@babel/plugin-transform-react-jsx': + specifier: ^7.14.9 + version: 7.21.0(@babel/core@7.21.3) + '@types/node': + specifier: ^18.7.6 + version: 18.15.11 + '@types/react': + specifier: ^18.0.17 + version: 18.0.31 + '@types/react-dom': + specifier: ^18.0.6 + version: 18.0.11 + chart.js: + specifier: workspace:* + version: link:../../.. + react: + specifier: ^18.2.0 + version: 18.2.0 + react-dom: + specifier: ^18.2.0 + version: 18.2.0(react@18.2.0) + react-scripts: + specifier: 5.0.1 + version: 5.0.1(@babel/plugin-syntax-flow@7.18.6)(@babel/plugin-transform-react-jsx@7.21.0)(@swc/core@1.3.42)(eslint@8.57.1)(react@18.2.0)(typescript@4.9.5) + typescript: + specifier: ^4.7.4 + version: 4.9.5 + web-vitals: + specifier: ^2.1.4 + version: 2.1.4 + + test/integration/typescript-node: + dependencies: + chart.js: + specifier: workspace:* + version: link:../../.. + typescript: + specifier: ^4.7.4 + version: 4.9.5 + devDependencies: + ts-expect: + specifier: ^1.3.0 + version: 1.3.0 + + test/integration/typescript-node-next: + dependencies: + chart.js: + specifier: workspace:* + version: link:../../.. + typescript: + specifier: ^4.7.4 + version: 4.9.5 + devDependencies: + ts-expect: + specifier: ^1.3.0 + version: 1.3.0 + +packages: + + /@ampproject/remapping@2.2.0: + resolution: {integrity: sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/gen-mapping': 0.1.1 + '@jridgewell/trace-mapping': 0.3.17 + + /@apideck/better-ajv-errors@0.3.6(ajv@8.12.0): + resolution: {integrity: sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA==} + engines: {node: '>=10'} + peerDependencies: + ajv: '>=8' + dependencies: + ajv: 8.12.0 + json-schema: 0.4.0 + jsonpointer: 5.0.1 + leven: 3.1.0 + dev: false + + /@babel/code-frame@7.18.6: + resolution: {integrity: sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.18.6 + + /@babel/compat-data@7.21.0: + resolution: {integrity: sha512-gMuZsmsgxk/ENC3O/fRw5QY8A9/uxQbbCEypnLIiYYc/qVJtEV7ouxC3EllIIwNzMqAQee5tanFabWsUOutS7g==} + engines: {node: '>=6.9.0'} + + /@babel/core@7.21.3: + resolution: {integrity: sha512-qIJONzoa/qiHghnm0l1n4i/6IIziDpzqc36FBs4pzMhDUraHqponwJLiAKm1hGLP3OSB/TVNz6rMwVGpwxxySw==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.2.0 + '@babel/code-frame': 7.18.6 + '@babel/generator': 7.21.3 + '@babel/helper-compilation-targets': 7.20.7(@babel/core@7.21.3) + '@babel/helper-module-transforms': 7.21.2 + '@babel/helpers': 7.21.0 + '@babel/parser': 7.21.3 + '@babel/template': 7.20.7 + '@babel/traverse': 7.21.3 + '@babel/types': 7.21.3 + convert-source-map: 1.9.0 + debug: 4.3.4(supports-color@6.1.0) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.0 + transitivePeerDependencies: + - supports-color + + /@babel/eslint-parser@7.21.3(@babel/core@7.21.3)(eslint@8.57.1): + resolution: {integrity: sha512-kfhmPimwo6k4P8zxNs8+T7yR44q1LdpsZdE1NkCsVlfiuTPRfnGgjaF8Qgug9q9Pou17u6wneYF0lDCZJATMFg==} + engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0} + peerDependencies: + '@babel/core': '>=7.11.0' + eslint: ^7.5.0 || ^8.0.0 + dependencies: + '@babel/core': 7.21.3 + '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1 + eslint: 8.57.1 + eslint-visitor-keys: 2.1.0 + semver: 6.3.0 + dev: false + + /@babel/generator@7.21.3: + resolution: {integrity: sha512-QS3iR1GYC/YGUnW7IdggFeN5c1poPUurnGttOV/bZgPGV+izC/D8HnD6DLwod0fsatNyVn1G3EVWMYIF0nHbeA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.21.3 + '@jridgewell/gen-mapping': 0.3.2 + '@jridgewell/trace-mapping': 0.3.17 + jsesc: 2.5.2 + + /@babel/helper-annotate-as-pure@7.18.6: + resolution: {integrity: sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.21.3 + + /@babel/helper-builder-binary-assignment-operator-visitor@7.18.9: + resolution: {integrity: sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-explode-assignable-expression': 7.18.6 + '@babel/types': 7.21.3 + + /@babel/helper-compilation-targets@7.20.7(@babel/core@7.21.3): + resolution: {integrity: sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/compat-data': 7.21.0 + '@babel/core': 7.21.3 + '@babel/helper-validator-option': 7.21.0 + browserslist: 4.21.5 + lru-cache: 5.1.1 + semver: 6.3.0 + + /@babel/helper-create-class-features-plugin@7.21.0(@babel/core@7.21.3): + resolution: {integrity: sha512-Q8wNiMIdwsv5la5SPxNYzzkPnjgC0Sy0i7jLkVOCdllu/xcVNkr3TeZzbHBJrj+XXRqzX5uCyCoV9eu6xUG7KQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-annotate-as-pure': 7.18.6 + '@babel/helper-environment-visitor': 7.18.9 + '@babel/helper-function-name': 7.21.0 + '@babel/helper-member-expression-to-functions': 7.21.0 + '@babel/helper-optimise-call-expression': 7.18.6 + '@babel/helper-replace-supers': 7.20.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.20.0 + '@babel/helper-split-export-declaration': 7.18.6 + transitivePeerDependencies: + - supports-color + + /@babel/helper-create-regexp-features-plugin@7.21.0(@babel/core@7.21.3): + resolution: {integrity: sha512-N+LaFW/auRSWdx7SHD/HiARwXQju1vXTW4fKr4u5SgBUTm51OKEjKgj+cs00ggW3kEvNqwErnlwuq7Y3xBe4eg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-annotate-as-pure': 7.18.6 + regexpu-core: 5.3.2 + + /@babel/helper-define-polyfill-provider@0.3.3(@babel/core@7.21.3): + resolution: {integrity: sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==} + peerDependencies: + '@babel/core': ^7.4.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-compilation-targets': 7.20.7(@babel/core@7.21.3) + '@babel/helper-plugin-utils': 7.20.2 + debug: 4.3.4(supports-color@6.1.0) + lodash.debounce: 4.0.8 + resolve: 1.22.1 + semver: 6.3.0 + transitivePeerDependencies: + - supports-color + + /@babel/helper-environment-visitor@7.18.9: + resolution: {integrity: sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==} + engines: {node: '>=6.9.0'} + + /@babel/helper-explode-assignable-expression@7.18.6: + resolution: {integrity: sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.21.3 + + /@babel/helper-function-name@7.21.0: + resolution: {integrity: sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.20.7 + '@babel/types': 7.21.3 + + /@babel/helper-hoist-variables@7.18.6: + resolution: {integrity: sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.21.3 + + /@babel/helper-member-expression-to-functions@7.21.0: + resolution: {integrity: sha512-Muu8cdZwNN6mRRNG6lAYErJ5X3bRevgYR2O8wN0yn7jJSnGDu6eG59RfT29JHxGUovyfrh6Pj0XzmR7drNVL3Q==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.21.3 + + /@babel/helper-module-imports@7.18.6: + resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.21.3 + + /@babel/helper-module-transforms@7.21.2: + resolution: {integrity: sha512-79yj2AR4U/Oqq/WOV7Lx6hUjau1Zfo4cI+JLAVYeMV5XIlbOhmjEk5ulbTc9fMpmlojzZHkUUxAiK+UKn+hNQQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-environment-visitor': 7.18.9 + '@babel/helper-module-imports': 7.18.6 + '@babel/helper-simple-access': 7.20.2 + '@babel/helper-split-export-declaration': 7.18.6 + '@babel/helper-validator-identifier': 7.19.1 + '@babel/template': 7.20.7 + '@babel/traverse': 7.21.3 + '@babel/types': 7.21.3 + transitivePeerDependencies: + - supports-color + + /@babel/helper-optimise-call-expression@7.18.6: + resolution: {integrity: sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.21.3 + + /@babel/helper-plugin-utils@7.20.2: + resolution: {integrity: sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==} + engines: {node: '>=6.9.0'} + + /@babel/helper-remap-async-to-generator@7.18.9(@babel/core@7.21.3): + resolution: {integrity: sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-annotate-as-pure': 7.18.6 + '@babel/helper-environment-visitor': 7.18.9 + '@babel/helper-wrap-function': 7.20.5 + '@babel/types': 7.21.3 + transitivePeerDependencies: + - supports-color + + /@babel/helper-replace-supers@7.20.7: + resolution: {integrity: sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-environment-visitor': 7.18.9 + '@babel/helper-member-expression-to-functions': 7.21.0 + '@babel/helper-optimise-call-expression': 7.18.6 + '@babel/template': 7.20.7 + '@babel/traverse': 7.21.3 + '@babel/types': 7.21.3 + transitivePeerDependencies: + - supports-color + + /@babel/helper-simple-access@7.20.2: + resolution: {integrity: sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.21.3 + + /@babel/helper-skip-transparent-expression-wrappers@7.20.0: + resolution: {integrity: sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.21.3 + + /@babel/helper-split-export-declaration@7.18.6: + resolution: {integrity: sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.21.3 + + /@babel/helper-string-parser@7.19.4: + resolution: {integrity: sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==} + engines: {node: '>=6.9.0'} + + /@babel/helper-validator-identifier@7.19.1: + resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==} + engines: {node: '>=6.9.0'} + + /@babel/helper-validator-option@7.21.0: + resolution: {integrity: sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==} + engines: {node: '>=6.9.0'} + + /@babel/helper-wrap-function@7.20.5: + resolution: {integrity: sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-function-name': 7.21.0 + '@babel/template': 7.20.7 + '@babel/traverse': 7.21.3 + '@babel/types': 7.21.3 + transitivePeerDependencies: + - supports-color + + /@babel/helpers@7.21.0: + resolution: {integrity: sha512-XXve0CBtOW0pd7MRzzmoyuSj0e3SEzj8pgyFxnTT1NJZL38BD1MK7yYrm8yefRPIDvNNe14xR4FdbHwpInD4rA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.20.7 + '@babel/traverse': 7.21.3 + '@babel/types': 7.21.3 + transitivePeerDependencies: + - supports-color + + /@babel/highlight@7.18.6: + resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.19.1 + chalk: 2.4.2 + js-tokens: 4.0.0 + + /@babel/parser@7.21.3: + resolution: {integrity: sha512-lobG0d7aOfQRXh8AyklEAgZGvA4FShxo6xQbUrrT/cNBPUdIDojlokwJsQyCC/eKia7ifqM0yP+2DRZ4WKw2RQ==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.21.3 + + /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.18.6(@babel/core@7.21.3): + resolution: {integrity: sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.20.7(@babel/core@7.21.3): + resolution: {integrity: sha512-sbr9+wNE5aXMBBFBICk01tt7sBf2Oc9ikRFEcem/ZORup9IMUdNhW7/wVLEbbtlWOsEubJet46mHAL2C8+2jKQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-skip-transparent-expression-wrappers': 7.20.0 + '@babel/plugin-proposal-optional-chaining': 7.21.0(@babel/core@7.21.3) + + /@babel/plugin-proposal-async-generator-functions@7.20.7(@babel/core@7.21.3): + resolution: {integrity: sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-environment-visitor': 7.18.9 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-remap-async-to-generator': 7.18.9(@babel/core@7.21.3) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.21.3) + transitivePeerDependencies: + - supports-color + + /@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.21.3): + resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-create-class-features-plugin': 7.21.0(@babel/core@7.21.3) + '@babel/helper-plugin-utils': 7.20.2 + transitivePeerDependencies: + - supports-color + + /@babel/plugin-proposal-class-static-block@7.21.0(@babel/core@7.21.3): + resolution: {integrity: sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-create-class-features-plugin': 7.21.0(@babel/core@7.21.3) + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.21.3) + transitivePeerDependencies: + - supports-color + + /@babel/plugin-proposal-decorators@7.21.0(@babel/core@7.21.3): + resolution: {integrity: sha512-MfgX49uRrFUTL/HvWtmx3zmpyzMMr4MTj3d527MLlr/4RTT9G/ytFFP7qet2uM2Ve03b+BkpWUpK+lRXnQ+v9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-create-class-features-plugin': 7.21.0(@babel/core@7.21.3) + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-replace-supers': 7.20.7 + '@babel/helper-split-export-declaration': 7.18.6 + '@babel/plugin-syntax-decorators': 7.21.0(@babel/core@7.21.3) + transitivePeerDependencies: + - supports-color + + /@babel/plugin-proposal-dynamic-import@7.18.6(@babel/core@7.21.3): + resolution: {integrity: sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.21.3) + + /@babel/plugin-proposal-export-namespace-from@7.18.9(@babel/core@7.21.3): + resolution: {integrity: sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.21.3) + + /@babel/plugin-proposal-json-strings@7.18.6(@babel/core@7.21.3): + resolution: {integrity: sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.21.3) + + /@babel/plugin-proposal-logical-assignment-operators@7.20.7(@babel/core@7.21.3): + resolution: {integrity: sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.21.3) + + /@babel/plugin-proposal-nullish-coalescing-operator@7.18.6(@babel/core@7.21.3): + resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.21.3) + + /@babel/plugin-proposal-numeric-separator@7.18.6(@babel/core@7.21.3): + resolution: {integrity: sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.21.3) + + /@babel/plugin-proposal-object-rest-spread@7.20.7(@babel/core@7.21.3): + resolution: {integrity: sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.21.0 + '@babel/core': 7.21.3 + '@babel/helper-compilation-targets': 7.20.7(@babel/core@7.21.3) + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.21.3) + '@babel/plugin-transform-parameters': 7.21.3(@babel/core@7.21.3) + + /@babel/plugin-proposal-optional-catch-binding@7.18.6(@babel/core@7.21.3): + resolution: {integrity: sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.21.3) + + /@babel/plugin-proposal-optional-chaining@7.21.0(@babel/core@7.21.3): + resolution: {integrity: sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-skip-transparent-expression-wrappers': 7.20.0 + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.21.3) + + /@babel/plugin-proposal-private-methods@7.18.6(@babel/core@7.21.3): + resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-create-class-features-plugin': 7.21.0(@babel/core@7.21.3) + '@babel/helper-plugin-utils': 7.20.2 + transitivePeerDependencies: + - supports-color + + /@babel/plugin-proposal-private-property-in-object@7.21.0(@babel/core@7.21.3): + resolution: {integrity: sha512-ha4zfehbJjc5MmXBlHec1igel5TJXXLDDRbuJ4+XT2TJcyD9/V1919BA8gMvsdHcNMBy4WBUBiRb3nw/EQUtBw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-annotate-as-pure': 7.18.6 + '@babel/helper-create-class-features-plugin': 7.21.0(@babel/core@7.21.3) + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.21.3) + transitivePeerDependencies: + - supports-color + + /@babel/plugin-proposal-unicode-property-regex@7.18.6(@babel/core@7.21.3): + resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==} + engines: {node: '>=4'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-create-regexp-features-plugin': 7.21.0(@babel/core@7.21.3) + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.21.3): + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.21.3): + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + dev: false + + /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.21.3): + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.21.3): + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/plugin-syntax-decorators@7.21.0(@babel/core@7.21.3): + resolution: {integrity: sha512-tIoPpGBR8UuM4++ccWN3gifhVvQu7ZizuR1fklhRJrd5ewgbkUS+0KVFeWWxELtn18NTLoW32XV7zyOgIAiz+w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.21.3): + resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.21.3): + resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/plugin-syntax-flow@7.18.6(@babel/core@7.21.3): + resolution: {integrity: sha512-LUbR+KNTBWCUAqRG9ex5Gnzu2IOkt8jRJbHHXFT9q+L9zm7M/QQbEqXyw1n1pohYvOyWC8CjeyjrSaIwiYjK7A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + dev: false + + /@babel/plugin-syntax-import-assertions@7.20.0(@babel/core@7.21.3): + resolution: {integrity: sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.21.3): + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + dev: false + + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.21.3): + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/plugin-syntax-jsx@7.18.6(@babel/core@7.21.3): + resolution: {integrity: sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.21.3): + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.21.3): + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.21.3): + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.21.3): + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.21.3): + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.21.3): + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.21.3): + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.21.3): + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/plugin-syntax-typescript@7.20.0(@babel/core@7.21.3): + resolution: {integrity: sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + dev: false + + /@babel/plugin-transform-arrow-functions@7.20.7(@babel/core@7.21.3): + resolution: {integrity: sha512-3poA5E7dzDomxj9WXWwuD6A5F3kc7VXwIJO+E+J8qtDtS+pXPAhrgEyh+9GBwBgPq1Z+bB+/JD60lp5jsN7JPQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/plugin-transform-async-to-generator@7.20.7(@babel/core@7.21.3): + resolution: {integrity: sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-module-imports': 7.18.6 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-remap-async-to-generator': 7.18.9(@babel/core@7.21.3) + transitivePeerDependencies: + - supports-color + + /@babel/plugin-transform-block-scoped-functions@7.18.6(@babel/core@7.21.3): + resolution: {integrity: sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/plugin-transform-block-scoping@7.21.0(@babel/core@7.21.3): + resolution: {integrity: sha512-Mdrbunoh9SxwFZapeHVrwFmri16+oYotcZysSzhNIVDwIAb1UV+kvnxULSYq9J3/q5MDG+4X6w8QVgD1zhBXNQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/plugin-transform-classes@7.21.0(@babel/core@7.21.3): + resolution: {integrity: sha512-RZhbYTCEUAe6ntPehC4hlslPWosNHDox+vAs4On/mCLRLfoDVHf6hVEd7kuxr1RnHwJmxFfUM3cZiZRmPxJPXQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-annotate-as-pure': 7.18.6 + '@babel/helper-compilation-targets': 7.20.7(@babel/core@7.21.3) + '@babel/helper-environment-visitor': 7.18.9 + '@babel/helper-function-name': 7.21.0 + '@babel/helper-optimise-call-expression': 7.18.6 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-replace-supers': 7.20.7 + '@babel/helper-split-export-declaration': 7.18.6 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + /@babel/plugin-transform-computed-properties@7.20.7(@babel/core@7.21.3): + resolution: {integrity: sha512-Lz7MvBK6DTjElHAmfu6bfANzKcxpyNPeYBGEafyA6E5HtRpjpZwU+u7Qrgz/2OR0z+5TvKYbPdphfSaAcZBrYQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/template': 7.20.7 + + /@babel/plugin-transform-destructuring@7.21.3(@babel/core@7.21.3): + resolution: {integrity: sha512-bp6hwMFzuiE4HqYEyoGJ/V2LeIWn+hLVKc4pnj++E5XQptwhtcGmSayM029d/j2X1bPKGTlsyPwAubuU22KhMA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/plugin-transform-dotall-regex@7.18.6(@babel/core@7.21.3): + resolution: {integrity: sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-create-regexp-features-plugin': 7.21.0(@babel/core@7.21.3) + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/plugin-transform-duplicate-keys@7.18.9(@babel/core@7.21.3): + resolution: {integrity: sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/plugin-transform-exponentiation-operator@7.18.6(@babel/core@7.21.3): + resolution: {integrity: sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-builder-binary-assignment-operator-visitor': 7.18.9 + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/plugin-transform-flow-strip-types@7.21.0(@babel/core@7.21.3): + resolution: {integrity: sha512-FlFA2Mj87a6sDkW4gfGrQQqwY/dLlBAyJa2dJEZ+FHXUVHBflO2wyKvg+OOEzXfrKYIa4HWl0mgmbCzt0cMb7w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-flow': 7.18.6(@babel/core@7.21.3) + dev: false + + /@babel/plugin-transform-for-of@7.21.0(@babel/core@7.21.3): + resolution: {integrity: sha512-LlUYlydgDkKpIY7mcBWvyPPmMcOphEyYA27Ef4xpbh1IiDNLr0kZsos2nf92vz3IccvJI25QUwp86Eo5s6HmBQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/plugin-transform-function-name@7.18.9(@babel/core@7.21.3): + resolution: {integrity: sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-compilation-targets': 7.20.7(@babel/core@7.21.3) + '@babel/helper-function-name': 7.21.0 + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/plugin-transform-literals@7.18.9(@babel/core@7.21.3): + resolution: {integrity: sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/plugin-transform-member-expression-literals@7.18.6(@babel/core@7.21.3): + resolution: {integrity: sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/plugin-transform-modules-amd@7.20.11(@babel/core@7.21.3): + resolution: {integrity: sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-module-transforms': 7.21.2 + '@babel/helper-plugin-utils': 7.20.2 + transitivePeerDependencies: + - supports-color + + /@babel/plugin-transform-modules-commonjs@7.21.2(@babel/core@7.21.3): + resolution: {integrity: sha512-Cln+Yy04Gxua7iPdj6nOV96smLGjpElir5YwzF0LBPKoPlLDNJePNlrGGaybAJkd0zKRnOVXOgizSqPYMNYkzA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-module-transforms': 7.21.2 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-simple-access': 7.20.2 + transitivePeerDependencies: + - supports-color + + /@babel/plugin-transform-modules-systemjs@7.20.11(@babel/core@7.21.3): + resolution: {integrity: sha512-vVu5g9BPQKSFEmvt2TA4Da5N+QVS66EX21d8uoOihC+OCpUoGvzVsXeqFdtAEfVa5BILAeFt+U7yVmLbQnAJmw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-hoist-variables': 7.18.6 + '@babel/helper-module-transforms': 7.21.2 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-validator-identifier': 7.19.1 + transitivePeerDependencies: + - supports-color + + /@babel/plugin-transform-modules-umd@7.18.6(@babel/core@7.21.3): + resolution: {integrity: sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-module-transforms': 7.21.2 + '@babel/helper-plugin-utils': 7.20.2 + transitivePeerDependencies: + - supports-color + + /@babel/plugin-transform-named-capturing-groups-regex@7.20.5(@babel/core@7.21.3): + resolution: {integrity: sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-create-regexp-features-plugin': 7.21.0(@babel/core@7.21.3) + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/plugin-transform-new-target@7.18.6(@babel/core@7.21.3): + resolution: {integrity: sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/plugin-transform-object-super@7.18.6(@babel/core@7.21.3): + resolution: {integrity: sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-replace-supers': 7.20.7 + transitivePeerDependencies: + - supports-color + + /@babel/plugin-transform-parameters@7.21.3(@babel/core@7.21.3): + resolution: {integrity: sha512-Wxc+TvppQG9xWFYatvCGPvZ6+SIUxQ2ZdiBP+PHYMIjnPXD+uThCshaz4NZOnODAtBjjcVQQ/3OKs9LW28purQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/plugin-transform-property-literals@7.18.6(@babel/core@7.21.3): + resolution: {integrity: sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/plugin-transform-react-constant-elements@7.21.3(@babel/core@7.21.3): + resolution: {integrity: sha512-4DVcFeWe/yDYBLp0kBmOGFJ6N2UYg7coGid1gdxb4co62dy/xISDMaYBXBVXEDhfgMk7qkbcYiGtwd5Q/hwDDQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + dev: false + + /@babel/plugin-transform-react-display-name@7.18.6(@babel/core@7.21.3): + resolution: {integrity: sha512-TV4sQ+T013n61uMoygyMRm+xf04Bd5oqFpv2jAEQwSZ8NwQA7zeRPg1LMVg2PWi3zWBz+CLKD+v5bcpZ/BS0aA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + dev: false + + /@babel/plugin-transform-react-jsx-development@7.18.6(@babel/core@7.21.3): + resolution: {integrity: sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/plugin-transform-react-jsx': 7.21.0(@babel/core@7.21.3) + dev: false + + /@babel/plugin-transform-react-jsx@7.21.0(@babel/core@7.21.3): + resolution: {integrity: sha512-6OAWljMvQrZjR2DaNhVfRz6dkCAVV+ymcLUmaf8bccGOHn2v5rHJK3tTpij0BuhdYWP4LLaqj5lwcdlpAAPuvg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-annotate-as-pure': 7.18.6 + '@babel/helper-module-imports': 7.18.6 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-jsx': 7.18.6(@babel/core@7.21.3) + '@babel/types': 7.21.3 + dev: false + + /@babel/plugin-transform-react-pure-annotations@7.18.6(@babel/core@7.21.3): + resolution: {integrity: sha512-I8VfEPg9r2TRDdvnHgPepTKvuRomzA8+u+nhY7qSI1fR2hRNebasZEETLyM5mAUr0Ku56OkXJ0I7NHJnO6cJiQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-annotate-as-pure': 7.18.6 + '@babel/helper-plugin-utils': 7.20.2 + dev: false + + /@babel/plugin-transform-regenerator@7.20.5(@babel/core@7.21.3): + resolution: {integrity: sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + regenerator-transform: 0.15.1 + + /@babel/plugin-transform-reserved-words@7.18.6(@babel/core@7.21.3): + resolution: {integrity: sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/plugin-transform-runtime@7.21.0(@babel/core@7.21.3): + resolution: {integrity: sha512-ReY6pxwSzEU0b3r2/T/VhqMKg/AkceBT19X0UptA3/tYi5Pe2eXgEUH+NNMC5nok6c6XQz5tyVTUpuezRfSMSg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-module-imports': 7.18.6 + '@babel/helper-plugin-utils': 7.20.2 + babel-plugin-polyfill-corejs2: 0.3.3(@babel/core@7.21.3) + babel-plugin-polyfill-corejs3: 0.6.0(@babel/core@7.21.3) + babel-plugin-polyfill-regenerator: 0.4.1(@babel/core@7.21.3) + semver: 6.3.0 + transitivePeerDependencies: + - supports-color + + /@babel/plugin-transform-shorthand-properties@7.18.6(@babel/core@7.21.3): + resolution: {integrity: sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/plugin-transform-spread@7.20.7(@babel/core@7.21.3): + resolution: {integrity: sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-skip-transparent-expression-wrappers': 7.20.0 + + /@babel/plugin-transform-sticky-regex@7.18.6(@babel/core@7.21.3): + resolution: {integrity: sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/plugin-transform-template-literals@7.18.9(@babel/core@7.21.3): + resolution: {integrity: sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/plugin-transform-typeof-symbol@7.18.9(@babel/core@7.21.3): + resolution: {integrity: sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/plugin-transform-typescript@7.21.3(@babel/core@7.21.3): + resolution: {integrity: sha512-RQxPz6Iqt8T0uw/WsJNReuBpWpBqs/n7mNo18sKLoTbMp+UrEekhH+pKSVC7gWz+DNjo9gryfV8YzCiT45RgMw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-annotate-as-pure': 7.18.6 + '@babel/helper-create-class-features-plugin': 7.21.0(@babel/core@7.21.3) + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-syntax-typescript': 7.20.0(@babel/core@7.21.3) + transitivePeerDependencies: + - supports-color + dev: false + + /@babel/plugin-transform-unicode-escapes@7.18.10(@babel/core@7.21.3): + resolution: {integrity: sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/plugin-transform-unicode-regex@7.18.6(@babel/core@7.21.3): + resolution: {integrity: sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-create-regexp-features-plugin': 7.21.0(@babel/core@7.21.3) + '@babel/helper-plugin-utils': 7.20.2 + + /@babel/preset-env@7.20.2(@babel/core@7.21.3): + resolution: {integrity: sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.21.0 + '@babel/core': 7.21.3 + '@babel/helper-compilation-targets': 7.20.7(@babel/core@7.21.3) + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-validator-option': 7.21.0 + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.18.6(@babel/core@7.21.3) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.20.7(@babel/core@7.21.3) + '@babel/plugin-proposal-async-generator-functions': 7.20.7(@babel/core@7.21.3) + '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.21.3) + '@babel/plugin-proposal-class-static-block': 7.21.0(@babel/core@7.21.3) + '@babel/plugin-proposal-dynamic-import': 7.18.6(@babel/core@7.21.3) + '@babel/plugin-proposal-export-namespace-from': 7.18.9(@babel/core@7.21.3) + '@babel/plugin-proposal-json-strings': 7.18.6(@babel/core@7.21.3) + '@babel/plugin-proposal-logical-assignment-operators': 7.20.7(@babel/core@7.21.3) + '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.21.3) + '@babel/plugin-proposal-numeric-separator': 7.18.6(@babel/core@7.21.3) + '@babel/plugin-proposal-object-rest-spread': 7.20.7(@babel/core@7.21.3) + '@babel/plugin-proposal-optional-catch-binding': 7.18.6(@babel/core@7.21.3) + '@babel/plugin-proposal-optional-chaining': 7.21.0(@babel/core@7.21.3) + '@babel/plugin-proposal-private-methods': 7.18.6(@babel/core@7.21.3) + '@babel/plugin-proposal-private-property-in-object': 7.21.0(@babel/core@7.21.3) + '@babel/plugin-proposal-unicode-property-regex': 7.18.6(@babel/core@7.21.3) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.21.3) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.21.3) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.21.3) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.21.3) + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.21.3) + '@babel/plugin-syntax-import-assertions': 7.20.0(@babel/core@7.21.3) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.21.3) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.21.3) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.21.3) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.21.3) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.21.3) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.21.3) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.21.3) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.21.3) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.21.3) + '@babel/plugin-transform-arrow-functions': 7.20.7(@babel/core@7.21.3) + '@babel/plugin-transform-async-to-generator': 7.20.7(@babel/core@7.21.3) + '@babel/plugin-transform-block-scoped-functions': 7.18.6(@babel/core@7.21.3) + '@babel/plugin-transform-block-scoping': 7.21.0(@babel/core@7.21.3) + '@babel/plugin-transform-classes': 7.21.0(@babel/core@7.21.3) + '@babel/plugin-transform-computed-properties': 7.20.7(@babel/core@7.21.3) + '@babel/plugin-transform-destructuring': 7.21.3(@babel/core@7.21.3) + '@babel/plugin-transform-dotall-regex': 7.18.6(@babel/core@7.21.3) + '@babel/plugin-transform-duplicate-keys': 7.18.9(@babel/core@7.21.3) + '@babel/plugin-transform-exponentiation-operator': 7.18.6(@babel/core@7.21.3) + '@babel/plugin-transform-for-of': 7.21.0(@babel/core@7.21.3) + '@babel/plugin-transform-function-name': 7.18.9(@babel/core@7.21.3) + '@babel/plugin-transform-literals': 7.18.9(@babel/core@7.21.3) + '@babel/plugin-transform-member-expression-literals': 7.18.6(@babel/core@7.21.3) + '@babel/plugin-transform-modules-amd': 7.20.11(@babel/core@7.21.3) + '@babel/plugin-transform-modules-commonjs': 7.21.2(@babel/core@7.21.3) + '@babel/plugin-transform-modules-systemjs': 7.20.11(@babel/core@7.21.3) + '@babel/plugin-transform-modules-umd': 7.18.6(@babel/core@7.21.3) + '@babel/plugin-transform-named-capturing-groups-regex': 7.20.5(@babel/core@7.21.3) + '@babel/plugin-transform-new-target': 7.18.6(@babel/core@7.21.3) + '@babel/plugin-transform-object-super': 7.18.6(@babel/core@7.21.3) + '@babel/plugin-transform-parameters': 7.21.3(@babel/core@7.21.3) + '@babel/plugin-transform-property-literals': 7.18.6(@babel/core@7.21.3) + '@babel/plugin-transform-regenerator': 7.20.5(@babel/core@7.21.3) + '@babel/plugin-transform-reserved-words': 7.18.6(@babel/core@7.21.3) + '@babel/plugin-transform-shorthand-properties': 7.18.6(@babel/core@7.21.3) + '@babel/plugin-transform-spread': 7.20.7(@babel/core@7.21.3) + '@babel/plugin-transform-sticky-regex': 7.18.6(@babel/core@7.21.3) + '@babel/plugin-transform-template-literals': 7.18.9(@babel/core@7.21.3) + '@babel/plugin-transform-typeof-symbol': 7.18.9(@babel/core@7.21.3) + '@babel/plugin-transform-unicode-escapes': 7.18.10(@babel/core@7.21.3) + '@babel/plugin-transform-unicode-regex': 7.18.6(@babel/core@7.21.3) + '@babel/preset-modules': 0.1.5(@babel/core@7.21.3) + '@babel/types': 7.21.3 + babel-plugin-polyfill-corejs2: 0.3.3(@babel/core@7.21.3) + babel-plugin-polyfill-corejs3: 0.6.0(@babel/core@7.21.3) + babel-plugin-polyfill-regenerator: 0.4.1(@babel/core@7.21.3) + core-js-compat: 3.29.1 + semver: 6.3.0 + transitivePeerDependencies: + - supports-color + + /@babel/preset-modules@0.1.5(@babel/core@7.21.3): + resolution: {integrity: sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/plugin-proposal-unicode-property-regex': 7.18.6(@babel/core@7.21.3) + '@babel/plugin-transform-dotall-regex': 7.18.6(@babel/core@7.21.3) + '@babel/types': 7.21.3 + esutils: 2.0.3 + + /@babel/preset-react@7.18.6(@babel/core@7.21.3): + resolution: {integrity: sha512-zXr6atUmyYdiWRVLOZahakYmOBHtWc2WGCkP8PYTgZi0iJXDY2CN180TdrIW4OGOAdLc7TifzDIvtx6izaRIzg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-validator-option': 7.21.0 + '@babel/plugin-transform-react-display-name': 7.18.6(@babel/core@7.21.3) + '@babel/plugin-transform-react-jsx': 7.21.0(@babel/core@7.21.3) + '@babel/plugin-transform-react-jsx-development': 7.18.6(@babel/core@7.21.3) + '@babel/plugin-transform-react-pure-annotations': 7.18.6(@babel/core@7.21.3) + dev: false + + /@babel/preset-typescript@7.21.0(@babel/core@7.21.3): + resolution: {integrity: sha512-myc9mpoVA5m1rF8K8DgLEatOYFDpwC+RkMkjZ0Du6uI62YvDe8uxIEYVs/VCdSJ097nlALiU/yBC7//3nI+hNg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-validator-option': 7.21.0 + '@babel/plugin-transform-typescript': 7.21.3(@babel/core@7.21.3) + transitivePeerDependencies: + - supports-color + dev: false + + /@babel/regjsgen@0.8.0: + resolution: {integrity: sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==} + + /@babel/runtime@7.21.0: + resolution: {integrity: sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.13.11 + + /@babel/template@7.20.7: + resolution: {integrity: sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.18.6 + '@babel/parser': 7.21.3 + '@babel/types': 7.21.3 + + /@babel/traverse@7.21.3: + resolution: {integrity: sha512-XLyopNeaTancVitYZe2MlUEvgKb6YVVPXzofHgqHijCImG33b/uTurMS488ht/Hbsb2XK3U2BnSTxKVNGV3nGQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.18.6 + '@babel/generator': 7.21.3 + '@babel/helper-environment-visitor': 7.18.9 + '@babel/helper-function-name': 7.21.0 + '@babel/helper-hoist-variables': 7.18.6 + '@babel/helper-split-export-declaration': 7.18.6 + '@babel/parser': 7.21.3 + '@babel/types': 7.21.3 + debug: 4.3.4(supports-color@6.1.0) + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + /@babel/types@7.21.3: + resolution: {integrity: sha512-sBGdETxC+/M4o/zKC0sl6sjWv62WFR/uzxrJ6uYyMLZOUlPnwzw0tKgVHOXxaAd5l2g8pEDM5RZ495GPQI77kg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.19.4 + '@babel/helper-validator-identifier': 7.19.1 + to-fast-properties: 2.0.0 + + /@bcoe/v8-coverage@0.2.3: + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + dev: false + + /@colors/colors@1.5.0: + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + dev: true + + /@csstools/normalize.css@12.0.0: + resolution: {integrity: sha512-M0qqxAcwCsIVfpFQSlGN5XjXWu8l5JDZN+fPt1LeW5SZexQTgnaEvgXAY+CeygRw0EeppWHi12JxESWiWrB0Sg==} + dev: false + + /@csstools/postcss-cascade-layers@1.1.1(postcss@8.4.21): + resolution: {integrity: sha512-+KdYrpKC5TgomQr2DlZF4lDEpHcoxnj5IGddYYfBWJAKfj1JtuHUIqMa+E1pJJ+z3kvDViWMqyqPlG4Ja7amQA==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + dependencies: + '@csstools/selector-specificity': 2.2.0(postcss-selector-parser@6.0.11) + postcss: 8.4.21 + postcss-selector-parser: 6.0.11 + dev: false + + /@csstools/postcss-color-function@1.1.1(postcss@8.4.21): + resolution: {integrity: sha512-Bc0f62WmHdtRDjf5f3e2STwRAl89N2CLb+9iAwzrv4L2hncrbDwnQD9PCq0gtAt7pOI2leIV08HIBUd4jxD8cw==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + dependencies: + '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.4.21) + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: false + + /@csstools/postcss-font-format-keywords@1.0.1(postcss@8.4.21): + resolution: {integrity: sha512-ZgrlzuUAjXIOc2JueK0X5sZDjCtgimVp/O5CEqTcs5ShWBa6smhWYbS0x5cVc/+rycTDbjjzoP0KTDnUneZGOg==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + dependencies: + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: false + + /@csstools/postcss-hwb-function@1.0.2(postcss@8.4.21): + resolution: {integrity: sha512-YHdEru4o3Rsbjmu6vHy4UKOXZD+Rn2zmkAmLRfPet6+Jz4Ojw8cbWxe1n42VaXQhD3CQUXXTooIy8OkVbUcL+w==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + dependencies: + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: false + + /@csstools/postcss-ic-unit@1.0.1(postcss@8.4.21): + resolution: {integrity: sha512-Ot1rcwRAaRHNKC9tAqoqNZhjdYBzKk1POgWfhN4uCOE47ebGcLRqXjKkApVDpjifL6u2/55ekkpnFcp+s/OZUw==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + dependencies: + '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.4.21) + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: false + + /@csstools/postcss-is-pseudo-class@2.0.7(postcss@8.4.21): + resolution: {integrity: sha512-7JPeVVZHd+jxYdULl87lvjgvWldYu+Bc62s9vD/ED6/QTGjy0jy0US/f6BG53sVMTBJ1lzKZFpYmofBN9eaRiA==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + dependencies: + '@csstools/selector-specificity': 2.2.0(postcss-selector-parser@6.0.11) + postcss: 8.4.21 + postcss-selector-parser: 6.0.11 + dev: false + + /@csstools/postcss-nested-calc@1.0.0(postcss@8.4.21): + resolution: {integrity: sha512-JCsQsw1wjYwv1bJmgjKSoZNvf7R6+wuHDAbi5f/7MbFhl2d/+v+TvBTU4BJH3G1X1H87dHl0mh6TfYogbT/dJQ==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + dependencies: + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: false + + /@csstools/postcss-normalize-display-values@1.0.1(postcss@8.4.21): + resolution: {integrity: sha512-jcOanIbv55OFKQ3sYeFD/T0Ti7AMXc9nM1hZWu8m/2722gOTxFg7xYu4RDLJLeZmPUVQlGzo4jhzvTUq3x4ZUw==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + dependencies: + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: false + + /@csstools/postcss-oklab-function@1.1.1(postcss@8.4.21): + resolution: {integrity: sha512-nJpJgsdA3dA9y5pgyb/UfEzE7W5Ka7u0CX0/HIMVBNWzWemdcTH3XwANECU6anWv/ao4vVNLTMxhiPNZsTK6iA==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + dependencies: + '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.4.21) + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: false + + /@csstools/postcss-progressive-custom-properties@1.3.0(postcss@8.4.21): + resolution: {integrity: sha512-ASA9W1aIy5ygskZYuWams4BzafD12ULvSypmaLJT2jvQ8G0M3I8PRQhC0h7mG0Z3LI05+agZjqSR9+K9yaQQjA==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.3 + dependencies: + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: false + + /@csstools/postcss-stepped-value-functions@1.0.1(postcss@8.4.21): + resolution: {integrity: sha512-dz0LNoo3ijpTOQqEJLY8nyaapl6umbmDcgj4AD0lgVQ572b2eqA1iGZYTTWhrcrHztWDDRAX2DGYyw2VBjvCvQ==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + dependencies: + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: false + + /@csstools/postcss-text-decoration-shorthand@1.0.0(postcss@8.4.21): + resolution: {integrity: sha512-c1XwKJ2eMIWrzQenN0XbcfzckOLLJiczqy+YvfGmzoVXd7pT9FfObiSEfzs84bpE/VqfpEuAZ9tCRbZkZxxbdw==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + dependencies: + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: false + + /@csstools/postcss-trigonometric-functions@1.0.2(postcss@8.4.21): + resolution: {integrity: sha512-woKaLO///4bb+zZC2s80l+7cm07M7268MsyG3M0ActXXEFi6SuhvriQYcb58iiKGbjwwIU7n45iRLEHypB47Og==} + engines: {node: ^14 || >=16} + peerDependencies: + postcss: ^8.2 + dependencies: + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: false + + /@csstools/postcss-unset-value@1.0.2(postcss@8.4.21): + resolution: {integrity: sha512-c8J4roPBILnelAsdLr4XOAR/GsTm0GJi4XpcfvoWk3U6KiTCqiFYc63KhRMQQX35jYMp4Ao8Ij9+IZRgMfJp1g==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + dependencies: + postcss: 8.4.21 + dev: false + + /@csstools/selector-specificity@2.2.0(postcss-selector-parser@6.0.11): + resolution: {integrity: sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==} + engines: {node: ^14 || ^16 || >=18} + peerDependencies: + postcss-selector-parser: ^6.0.10 + dependencies: + postcss-selector-parser: 6.0.11 + dev: false + + /@eslint-community/eslint-utils@4.4.0(eslint@8.37.0): + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.37.0 + eslint-visitor-keys: 3.4.0 + dev: true + + /@eslint-community/eslint-utils@4.4.0(eslint@8.57.1): + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.57.1 + eslint-visitor-keys: 3.4.0 + dev: false + + /@eslint-community/eslint-utils@4.9.0(eslint@8.57.1): + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.57.1 + eslint-visitor-keys: 3.4.3 + dev: false + + /@eslint-community/regexpp@4.12.2: + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + dev: false + + /@eslint-community/regexpp@4.5.0: + resolution: {integrity: sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + /@eslint/eslintrc@2.0.2: + resolution: {integrity: sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + ajv: 6.12.6 + debug: 4.3.4(supports-color@6.1.0) + espree: 9.5.1 + globals: 13.20.0 + ignore: 5.2.4 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@eslint/eslintrc@2.1.4: + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: false + + /@eslint/js@8.37.0: + resolution: {integrity: sha512-x5vzdtOOGgFVDCUs81QRB2+liax8rFg3+7hqM+QhBG0/G3F1ZsoYl97UrqgHgQ9KKT7G6c4V+aTUCgu/n22v1A==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /@eslint/js@8.57.1: + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: false + + /@fastify/deepmerge@1.3.0: + resolution: {integrity: sha512-J8TOSBq3SoZbDhM9+R/u77hP93gz/rajSA+K2kGyijPpORPWUXHUpTaleoj+92As0S9uPRP7Oi8IqMf0u+ro6A==} + dev: true + + /@humanwhocodes/config-array@0.11.8: + resolution: {integrity: sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==} + engines: {node: '>=10.10.0'} + dependencies: + '@humanwhocodes/object-schema': 1.2.1 + debug: 4.3.4(supports-color@6.1.0) + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@humanwhocodes/config-array@0.13.0: + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: false + + /@humanwhocodes/module-importer@1.0.1: + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + /@humanwhocodes/object-schema@1.2.1: + resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} + dev: true + + /@humanwhocodes/object-schema@2.0.3: + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + dev: false + + /@istanbuljs/load-nyc-config@1.1.0: + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.1 + resolve-from: 5.0.0 + dev: false + + /@istanbuljs/schema@0.1.3: + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + + /@jest/console@27.5.1: + resolution: {integrity: sha512-kZ/tNpS3NXn0mlXXXPNuDZnb4c0oZ20r4K5eemM2k30ZC3G0T02nXUvyhf5YdbXWHPEJLc9qGLxEZ216MdL+Zg==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + '@jest/types': 27.5.1 + '@types/node': 18.15.11 + chalk: 4.1.2 + jest-message-util: 27.5.1 + jest-util: 27.5.1 + slash: 3.0.0 + dev: false + + /@jest/console@28.1.3: + resolution: {integrity: sha512-QPAkP5EwKdK/bxIr6C1I4Vs0rm2nHiANzj/Z5X2JQkrZo6IqvC4ldZ9K95tF0HdidhA8Bo6egxSzUFPYKcEXLw==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@jest/types': 28.1.3 + '@types/node': 18.15.11 + chalk: 4.1.2 + jest-message-util: 28.1.3 + jest-util: 28.1.3 + slash: 3.0.0 + dev: false + + /@jest/core@27.5.1: + resolution: {integrity: sha512-AK6/UTrvQD0Cd24NSqmIA6rKsu0tKIxfiCducZvqxYdmMisOYAsdItspT+fQDQYARPf8XgjAFZi0ogW2agH5nQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/console': 27.5.1 + '@jest/reporters': 27.5.1 + '@jest/test-result': 27.5.1 + '@jest/transform': 27.5.1 + '@jest/types': 27.5.1 + '@types/node': 18.15.11 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.8.1 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 27.5.1 + jest-config: 27.5.1 + jest-haste-map: 27.5.1 + jest-message-util: 27.5.1 + jest-regex-util: 27.5.1 + jest-resolve: 27.5.1 + jest-resolve-dependencies: 27.5.1 + jest-runner: 27.5.1 + jest-runtime: 27.5.1 + jest-snapshot: 27.5.1 + jest-util: 27.5.1 + jest-validate: 27.5.1 + jest-watcher: 27.5.1 + micromatch: 4.0.5 + rimraf: 3.0.2 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - ts-node + - utf-8-validate + dev: false + + /@jest/environment@27.5.1: + resolution: {integrity: sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + '@jest/fake-timers': 27.5.1 + '@jest/types': 27.5.1 + '@types/node': 18.15.11 + jest-mock: 27.5.1 + dev: false + + /@jest/fake-timers@27.5.1: + resolution: {integrity: sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + '@jest/types': 27.5.1 + '@sinonjs/fake-timers': 8.1.0 + '@types/node': 18.15.11 + jest-message-util: 27.5.1 + jest-mock: 27.5.1 + jest-util: 27.5.1 + dev: false + + /@jest/globals@27.5.1: + resolution: {integrity: sha512-ZEJNB41OBQQgGzgyInAv0UUfDDj3upmHydjieSxFvTRuZElrx7tXg/uVQ5hYVEwiXs3+aMsAeEc9X7xiSKCm4Q==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + '@jest/environment': 27.5.1 + '@jest/types': 27.5.1 + expect: 27.5.1 + dev: false + + /@jest/reporters@27.5.1: + resolution: {integrity: sha512-cPXh9hWIlVJMQkVk84aIvXuBB4uQQmFqZiacloFuGiP3ah1sbCxCosidXFDfqG8+6fO1oR2dTJTlsOy4VFmUfw==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 27.5.1 + '@jest/test-result': 27.5.1 + '@jest/transform': 27.5.1 + '@jest/types': 27.5.1 + '@types/node': 18.15.11 + chalk: 4.1.2 + collect-v8-coverage: 1.0.1 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.0 + istanbul-lib-instrument: 5.2.1 + istanbul-lib-report: 3.0.0 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.1.5 + jest-haste-map: 27.5.1 + jest-resolve: 27.5.1 + jest-util: 27.5.1 + jest-worker: 27.5.1 + slash: 3.0.0 + source-map: 0.6.1 + string-length: 4.0.2 + terminal-link: 2.1.1 + v8-to-istanbul: 8.1.1 + transitivePeerDependencies: + - supports-color + dev: false + + /@jest/schemas@28.1.3: + resolution: {integrity: sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@sinclair/typebox': 0.24.51 + dev: false + + /@jest/source-map@27.5.1: + resolution: {integrity: sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + callsites: 3.1.0 + graceful-fs: 4.2.11 + source-map: 0.6.1 + dev: false + + /@jest/test-result@27.5.1: + resolution: {integrity: sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + '@jest/console': 27.5.1 + '@jest/types': 27.5.1 + '@types/istanbul-lib-coverage': 2.0.4 + collect-v8-coverage: 1.0.1 + dev: false + + /@jest/test-result@28.1.3: + resolution: {integrity: sha512-kZAkxnSE+FqE8YjW8gNuoVkkC9I7S1qmenl8sGcDOLropASP+BkcGKwhXoyqQuGOGeYY0y/ixjrd/iERpEXHNg==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@jest/console': 28.1.3 + '@jest/types': 28.1.3 + '@types/istanbul-lib-coverage': 2.0.4 + collect-v8-coverage: 1.0.1 + dev: false + + /@jest/test-sequencer@27.5.1: + resolution: {integrity: sha512-LCheJF7WB2+9JuCS7VB/EmGIdQuhtqjRNI9A43idHv3E4KltCTsPsLxvdaubFHSYwY/fNjMWjl6vNRhDiN7vpQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + '@jest/test-result': 27.5.1 + graceful-fs: 4.2.11 + jest-haste-map: 27.5.1 + jest-runtime: 27.5.1 + transitivePeerDependencies: + - supports-color + dev: false + + /@jest/transform@27.5.1: + resolution: {integrity: sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + '@babel/core': 7.21.3 + '@jest/types': 27.5.1 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 1.9.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 27.5.1 + jest-regex-util: 27.5.1 + jest-util: 27.5.1 + micromatch: 4.0.5 + pirates: 4.0.5 + slash: 3.0.0 + source-map: 0.6.1 + write-file-atomic: 3.0.3 + transitivePeerDependencies: + - supports-color + dev: false + + /@jest/types@27.5.1: + resolution: {integrity: sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + '@types/istanbul-lib-coverage': 2.0.4 + '@types/istanbul-reports': 3.0.1 + '@types/node': 18.15.11 + '@types/yargs': 16.0.5 + chalk: 4.1.2 + dev: false + + /@jest/types@28.1.3: + resolution: {integrity: sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@jest/schemas': 28.1.3 + '@types/istanbul-lib-coverage': 2.0.4 + '@types/istanbul-reports': 3.0.1 + '@types/node': 18.15.11 + '@types/yargs': 17.0.24 + chalk: 4.1.2 + dev: false + + /@jridgewell/gen-mapping@0.1.1: + resolution: {integrity: sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.1.2 + '@jridgewell/sourcemap-codec': 1.4.14 + + /@jridgewell/gen-mapping@0.3.2: + resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.1.2 + '@jridgewell/sourcemap-codec': 1.4.14 + '@jridgewell/trace-mapping': 0.3.17 + + /@jridgewell/resolve-uri@3.1.0: + resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} + engines: {node: '>=6.0.0'} + + /@jridgewell/set-array@1.1.2: + resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} + engines: {node: '>=6.0.0'} + + /@jridgewell/source-map@0.3.2: + resolution: {integrity: sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==} + dependencies: + '@jridgewell/gen-mapping': 0.3.2 + '@jridgewell/trace-mapping': 0.3.17 + + /@jridgewell/sourcemap-codec@1.4.14: + resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} + + /@jridgewell/trace-mapping@0.3.17: + resolution: {integrity: sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==} + dependencies: + '@jridgewell/resolve-uri': 3.1.0 + '@jridgewell/sourcemap-codec': 1.4.14 + + /@kurkle/color@0.3.2: + resolution: {integrity: sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==} + dev: false + + /@leichtgewicht/ip-codec@2.0.4: + resolution: {integrity: sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==} + dev: false + + /@mrmlnc/readdir-enhanced@2.2.1: + resolution: {integrity: sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==} + engines: {node: '>=4'} + dependencies: + call-me-maybe: 1.0.2 + glob-to-regexp: 0.3.0 + dev: true + + /@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1: + resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==} + dependencies: + eslint-scope: 5.1.1 + dev: false + + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + /@nodelib/fs.stat@1.1.3: + resolution: {integrity: sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==} + engines: {node: '>= 6'} + dev: true + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.15.0 + + /@pmmmwh/react-refresh-webpack-plugin@0.5.10(react-refresh@0.11.0)(webpack-dev-server@4.13.1)(webpack@5.76.3): + resolution: {integrity: sha512-j0Ya0hCFZPd4x40qLzbhGsh9TMtdb+CJQiso+WxLOPNasohq9cc5SNUcwsZaRH6++Xh91Xkm/xHCkuIiIu0LUA==} + engines: {node: '>= 10.13'} + peerDependencies: + '@types/webpack': 4.x || 5.x + react-refresh: '>=0.10.0 <1.0.0' + sockjs-client: ^1.4.0 + type-fest: '>=0.17.0 <4.0.0' + webpack: '>=4.43.0 <6.0.0' + webpack-dev-server: 3.x || 4.x + webpack-hot-middleware: 2.x + webpack-plugin-serve: 0.x || 1.x + peerDependenciesMeta: + '@types/webpack': + optional: true + sockjs-client: + optional: true + type-fest: + optional: true + webpack-dev-server: + optional: true + webpack-hot-middleware: + optional: true + webpack-plugin-serve: + optional: true + dependencies: + ansi-html-community: 0.0.8 + common-path-prefix: 3.0.0 + core-js-pure: 3.29.1 + error-stack-parser: 2.1.4 + find-up: 5.0.0 + html-entities: 1.4.0 + loader-utils: 2.0.4 + react-refresh: 0.11.0 + schema-utils: 3.1.1 + source-map: 0.7.4 + webpack: 5.76.3(@swc/core@1.3.42) + webpack-dev-server: 4.13.1(webpack@5.76.3) + dev: false + + /@rollup/plugin-babel@5.3.1(@babel/core@7.21.3)(rollup@2.79.1): + resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==} + engines: {node: '>= 10.0.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@types/babel__core': ^7.1.9 + rollup: ^1.20.0||^2.0.0 + peerDependenciesMeta: + '@types/babel__core': + optional: true + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-module-imports': 7.18.6 + '@rollup/pluginutils': 3.1.0(rollup@2.79.1) + rollup: 2.79.1 + dev: false + + /@rollup/plugin-commonjs@23.0.7(rollup@3.20.2): + resolution: {integrity: sha512-hsSD5Qzyuat/swzrExGG5l7EuIlPhwTsT7KwKbSCQzIcJWjRxiimi/0tyMYY2bByitNb3i1p+6JWEDGa0NvT0Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.68.0||^3.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@rollup/pluginutils': 5.0.2(rollup@3.20.2) + commondir: 1.0.1 + estree-walker: 2.0.2 + glob: 8.1.0 + is-reference: 1.2.1 + magic-string: 0.27.0 + rollup: 3.20.2 + dev: true + + /@rollup/plugin-inject@5.0.3(rollup@3.20.2): + resolution: {integrity: sha512-411QlbL+z2yXpRWFXSmw/teQRMkXcAAC8aYTemc15gwJRpvEVDQwoe+N/HTFD8RFG8+88Bme9DK2V9CVm7hJdA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@rollup/pluginutils': 5.0.2(rollup@3.20.2) + estree-walker: 2.0.2 + magic-string: 0.27.0 + rollup: 3.20.2 + dev: true + + /@rollup/plugin-json@5.0.2(rollup@3.20.2): + resolution: {integrity: sha512-D1CoOT2wPvadWLhVcmpkDnesTzjhNIQRWLsc3fA49IFOP2Y84cFOOJ+nKGYedvXHKUsPeq07HR4hXpBBr+CHlA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@rollup/pluginutils': 5.0.2(rollup@3.20.2) + rollup: 3.20.2 + dev: true + + /@rollup/plugin-node-resolve@11.2.1(rollup@2.79.1): + resolution: {integrity: sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==} + engines: {node: '>= 10.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0 + dependencies: + '@rollup/pluginutils': 3.1.0(rollup@2.79.1) + '@types/resolve': 1.17.1 + builtin-modules: 3.3.0 + deepmerge: 4.3.1 + is-module: 1.0.0 + resolve: 1.22.1 + rollup: 2.79.1 + dev: false + + /@rollup/plugin-node-resolve@15.0.1(rollup@3.20.2): + resolution: {integrity: sha512-ReY88T7JhJjeRVbfCyNj+NXAG3IIsVMsX9b5/9jC98dRP8/yxlZdz7mHZbHk5zHr24wZZICS5AcXsFZAXYUQEg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.78.0||^3.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@rollup/pluginutils': 5.0.2(rollup@3.20.2) + '@types/resolve': 1.20.2 + deepmerge: 4.3.1 + is-builtin-module: 3.2.1 + is-module: 1.0.0 + resolve: 1.22.1 + rollup: 3.20.2 + dev: true + + /@rollup/plugin-replace@2.4.2(rollup@2.79.1): + resolution: {integrity: sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==} + peerDependencies: + rollup: ^1.20.0 || ^2.0.0 + dependencies: + '@rollup/pluginutils': 3.1.0(rollup@2.79.1) + magic-string: 0.25.9 + rollup: 2.79.1 + dev: false + + /@rollup/pluginutils@3.1.0(rollup@2.79.1): + resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==} + engines: {node: '>= 8.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0 + dependencies: + '@types/estree': 0.0.39 + estree-walker: 1.0.1 + picomatch: 2.3.1 + rollup: 2.79.1 + dev: false + + /@rollup/pluginutils@4.2.1: + resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} + engines: {node: '>= 8.0.0'} + dependencies: + estree-walker: 2.0.2 + picomatch: 2.3.1 + dev: true + + /@rollup/pluginutils@5.0.2(rollup@3.20.2): + resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@types/estree': 1.0.0 + estree-walker: 2.0.2 + picomatch: 2.3.1 + rollup: 3.20.2 + dev: true + + /@rushstack/eslint-patch@1.2.0: + resolution: {integrity: sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==} + dev: false + + /@shigma/stringify-object@3.3.0: + resolution: {integrity: sha512-tO5pn6RJp8m1ldYtqY3GEQA6+Nqp1cIZVrVx7iFVPx0YfhMqfplwrvyrQPP1cCwuyRoAyDr/BxVZYt+USm8LXQ==} + engines: {node: '>=6'} + dependencies: + get-own-enumerable-property-symbols: 3.0.2 + is-obj: 1.0.1 + is-regexp: 2.1.0 + dev: true + + /@simonbrunel/vuepress-plugin-versions@0.2.0: + resolution: {integrity: sha512-6qgrbxCVG5mIHQDqTvWfpSxGMpqcDAHKIlxScZ0MfJjUWW40Kt4xcZ3OTx4NvlsNZUDNLZVWngIPYsMah4C/mQ==} + dependencies: + node-fetch: 2.6.9 + semiver: 1.1.0 + stringify-object: 3.3.0 + transitivePeerDependencies: + - encoding + dev: true + + /@sinclair/typebox@0.24.51: + resolution: {integrity: sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==} + dev: false + + /@sindresorhus/is@0.14.0: + resolution: {integrity: sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==} + engines: {node: '>=6'} + dev: true + + /@sinonjs/commons@1.8.6: + resolution: {integrity: sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==} + dependencies: + type-detect: 4.0.8 + dev: false + + /@sinonjs/fake-timers@8.1.0: + resolution: {integrity: sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==} + dependencies: + '@sinonjs/commons': 1.8.6 + dev: false + + /@socket.io/component-emitter@3.1.0: + resolution: {integrity: sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==} + dev: true + + /@surma/rollup-plugin-off-main-thread@2.2.3: + resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==} + dependencies: + ejs: 3.1.9 + json5: 2.2.3 + magic-string: 0.25.9 + string.prototype.matchall: 4.0.8 + dev: false + + /@svgr/babel-plugin-add-jsx-attribute@5.4.0: + resolution: {integrity: sha512-ZFf2gs/8/6B8PnSofI0inYXr2SDNTDScPXhN7k5EqD4aZ3gi6u+rbmZHVB8IM3wDyx8ntKACZbtXSm7oZGRqVg==} + engines: {node: '>=10'} + dev: false + + /@svgr/babel-plugin-remove-jsx-attribute@5.4.0: + resolution: {integrity: sha512-yaS4o2PgUtwLFGTKbsiAy6D0o3ugcUhWK0Z45umJ66EPWunAz9fuFw2gJuje6wqQvQWOTJvIahUwndOXb7QCPg==} + engines: {node: '>=10'} + dev: false + + /@svgr/babel-plugin-remove-jsx-empty-expression@5.0.1: + resolution: {integrity: sha512-LA72+88A11ND/yFIMzyuLRSMJ+tRKeYKeQ+mR3DcAZ5I4h5CPWN9AHyUzJbWSYp/u2u0xhmgOe0+E41+GjEueA==} + engines: {node: '>=10'} + dev: false + + /@svgr/babel-plugin-replace-jsx-attribute-value@5.0.1: + resolution: {integrity: sha512-PoiE6ZD2Eiy5mK+fjHqwGOS+IXX0wq/YDtNyIgOrc6ejFnxN4b13pRpiIPbtPwHEc+NT2KCjteAcq33/F1Y9KQ==} + engines: {node: '>=10'} + dev: false + + /@svgr/babel-plugin-svg-dynamic-title@5.4.0: + resolution: {integrity: sha512-zSOZH8PdZOpuG1ZVx/cLVePB2ibo3WPpqo7gFIjLV9a0QsuQAzJiwwqmuEdTaW2pegyBE17Uu15mOgOcgabQZg==} + engines: {node: '>=10'} + dev: false + + /@svgr/babel-plugin-svg-em-dimensions@5.4.0: + resolution: {integrity: sha512-cPzDbDA5oT/sPXDCUYoVXEmm3VIoAWAPT6mSPTJNbQaBNUuEKVKyGH93oDY4e42PYHRW67N5alJx/eEol20abw==} + engines: {node: '>=10'} + dev: false + + /@svgr/babel-plugin-transform-react-native-svg@5.4.0: + resolution: {integrity: sha512-3eYP/SaopZ41GHwXma7Rmxcv9uRslRDTY1estspeB1w1ueZWd/tPlMfEOoccYpEMZU3jD4OU7YitnXcF5hLW2Q==} + engines: {node: '>=10'} + dev: false + + /@svgr/babel-plugin-transform-svg-component@5.5.0: + resolution: {integrity: sha512-q4jSH1UUvbrsOtlo/tKcgSeiCHRSBdXoIoqX1pgcKK/aU3JD27wmMKwGtpB8qRYUYoyXvfGxUVKchLuR5pB3rQ==} + engines: {node: '>=10'} + dev: false + + /@svgr/babel-preset@5.5.0: + resolution: {integrity: sha512-4FiXBjvQ+z2j7yASeGPEi8VD/5rrGQk4Xrq3EdJmoZgz/tpqChpo5hgXDvmEauwtvOc52q8ghhZK4Oy7qph4ig==} + engines: {node: '>=10'} + dependencies: + '@svgr/babel-plugin-add-jsx-attribute': 5.4.0 + '@svgr/babel-plugin-remove-jsx-attribute': 5.4.0 + '@svgr/babel-plugin-remove-jsx-empty-expression': 5.0.1 + '@svgr/babel-plugin-replace-jsx-attribute-value': 5.0.1 + '@svgr/babel-plugin-svg-dynamic-title': 5.4.0 + '@svgr/babel-plugin-svg-em-dimensions': 5.4.0 + '@svgr/babel-plugin-transform-react-native-svg': 5.4.0 + '@svgr/babel-plugin-transform-svg-component': 5.5.0 + dev: false + + /@svgr/core@5.5.0: + resolution: {integrity: sha512-q52VOcsJPvV3jO1wkPtzTuKlvX7Y3xIcWRpCMtBF3MrteZJtBfQw/+u0B1BHy5ColpQc1/YVTrPEtSYIMNZlrQ==} + engines: {node: '>=10'} + dependencies: + '@svgr/plugin-jsx': 5.5.0 + camelcase: 6.3.0 + cosmiconfig: 7.1.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@svgr/hast-util-to-babel-ast@5.5.0: + resolution: {integrity: sha512-cAaR/CAiZRB8GP32N+1jocovUtvlj0+e65TB50/6Lcime+EA49m/8l+P2ko+XPJ4dw3xaPS3jOL4F2X4KWxoeQ==} + engines: {node: '>=10'} + dependencies: + '@babel/types': 7.21.3 + dev: false + + /@svgr/plugin-jsx@5.5.0: + resolution: {integrity: sha512-V/wVh33j12hGh05IDg8GpIUXbjAPnTdPTKuP4VNLggnwaHMPNQNae2pRnyTAILWCQdz5GyMqtO488g7CKM8CBA==} + engines: {node: '>=10'} + dependencies: + '@babel/core': 7.21.3 + '@svgr/babel-preset': 5.5.0 + '@svgr/hast-util-to-babel-ast': 5.5.0 + svg-parser: 2.0.4 + transitivePeerDependencies: + - supports-color + dev: false + + /@svgr/plugin-svgo@5.5.0: + resolution: {integrity: sha512-r5swKk46GuQl4RrVejVwpeeJaydoxkdwkM1mBKOgJLBUJPGaLci6ylg/IjhrRsREKDkr4kbMWdgOtbXEh0fyLQ==} + engines: {node: '>=10'} + dependencies: + cosmiconfig: 7.1.0 + deepmerge: 4.3.1 + svgo: 1.3.2 + dev: false + + /@svgr/webpack@5.5.0: + resolution: {integrity: sha512-DOBOK255wfQxguUta2INKkzPj6AIS6iafZYiYmHn6W3pHlycSRRlvWKCfLDG10fXfLWqE3DJHgRUOyJYmARa7g==} + engines: {node: '>=10'} + dependencies: + '@babel/core': 7.21.3 + '@babel/plugin-transform-react-constant-elements': 7.21.3(@babel/core@7.21.3) + '@babel/preset-env': 7.20.2(@babel/core@7.21.3) + '@babel/preset-react': 7.18.6(@babel/core@7.21.3) + '@svgr/core': 5.5.0 + '@svgr/plugin-jsx': 5.5.0 + '@svgr/plugin-svgo': 5.5.0 + loader-utils: 2.0.4 + transitivePeerDependencies: + - supports-color + dev: false + + /@swc/core-darwin-arm64@1.3.42: + resolution: {integrity: sha512-hM6RrZFyoCM9mX3cj/zM5oXwhAqjUdOCLXJx7KTQps7NIkv/Qjvobgvyf2gAb89j3ARNo9NdIoLjTjJ6oALtiA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + optional: true + + /@swc/core-darwin-x64@1.3.42: + resolution: {integrity: sha512-bjsWtHMb6wJK1+RGlBs2USvgZ0txlMk11y0qBLKo32gLKTqzUwRw0Fmfzuf6Ue2a/w//7eqMlPFEre4LvJajGw==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + requiresBuild: true + optional: true + + /@swc/core-linux-arm-gnueabihf@1.3.42: + resolution: {integrity: sha512-Oe0ggMz3MyqXNfeVmY+bBTL0hFSNY3bx8dhcqsh4vXk/ZVGse94QoC4dd92LuPHmKT0x6nsUzB86x2jU9QHW5g==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + requiresBuild: true + optional: true + + /@swc/core-linux-arm64-gnu@1.3.42: + resolution: {integrity: sha512-ZJsa8NIW1RLmmHGTJCbM7OPSbBZ9rOMrLqDtUOGrT0uoJXZnnQqolflamB5wviW0X6h3Z3/PSTNGNDCJ3u3Lqg==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + optional: true + + /@swc/core-linux-arm64-musl@1.3.42: + resolution: {integrity: sha512-YpZwlFAfOp5vkm/uVUJX1O7N3yJDO1fDQRWqsOPPNyIJkI2ydlRQtgN6ZylC159Qv+TimfXnGTlNr7o3iBAqjg==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + optional: true + + /@swc/core-linux-x64-gnu@1.3.42: + resolution: {integrity: sha512-0ccpKnsZbyHBzaQFdP8U9i29nvOfKitm6oJfdJzlqsY/jCqwvD8kv2CAKSK8WhJz//ExI2LqNrDI0yazx5j7+A==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + requiresBuild: true + optional: true + + /@swc/core-linux-x64-musl@1.3.42: + resolution: {integrity: sha512-7eckRRuTZ6+3K21uyfXXgc2ZCg0mSWRRNwNT3wap2bYkKPeqTgb8pm8xYSZNEiMuDonHEat6XCCV36lFY6kOdQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + requiresBuild: true + optional: true + + /@swc/core-win32-arm64-msvc@1.3.42: + resolution: {integrity: sha512-t27dJkdw0GWANdN4TV0lY/V5vTYSx5SRjyzzZolep358ueCGuN1XFf1R0JcCbd1ojosnkQg2L7A7991UjXingg==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + requiresBuild: true + optional: true + + /@swc/core-win32-ia32-msvc@1.3.42: + resolution: {integrity: sha512-xfpc/Zt/aMILX4IX0e3loZaFyrae37u3MJCv1gJxgqrpeLi7efIQr3AmERkTK3mxTO6R5urSliWw2W3FyZ7D3Q==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + requiresBuild: true + optional: true + + /@swc/core-win32-x64-msvc@1.3.42: + resolution: {integrity: sha512-ra2K4Tu++EJLPhzZ6L8hWUsk94TdK/2UKhL9dzCBhtzKUixsGCEqhtqH1zISXNvW8qaVLFIMUP37ULe80/IJaA==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + requiresBuild: true + optional: true + + /@swc/core@1.3.42: + resolution: {integrity: sha512-nVFUd5+7tGniM2cT3LXaqnu3735Cu4az8A9gAKK+8sdpASI52SWuqfDBmjFCK9xG90MiVDVp2PTZr0BWqCIzpw==} + engines: {node: '>=10'} + requiresBuild: true + optionalDependencies: + '@swc/core-darwin-arm64': 1.3.42 + '@swc/core-darwin-x64': 1.3.42 + '@swc/core-linux-arm-gnueabihf': 1.3.42 + '@swc/core-linux-arm64-gnu': 1.3.42 + '@swc/core-linux-arm64-musl': 1.3.42 + '@swc/core-linux-x64-gnu': 1.3.42 + '@swc/core-linux-x64-musl': 1.3.42 + '@swc/core-win32-arm64-msvc': 1.3.42 + '@swc/core-win32-ia32-msvc': 1.3.42 + '@swc/core-win32-x64-msvc': 1.3.42 + + /@szmarczak/http-timer@1.1.2: + resolution: {integrity: sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==} + engines: {node: '>=6'} + dependencies: + defer-to-connect: 1.1.3 + dev: true + + /@tootallnate/once@1.1.2: + resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==} + engines: {node: '>= 6'} + dev: false + + /@trysound/sax@0.2.0: + resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} + engines: {node: '>=10.13.0'} + + /@types/babel__core@7.20.0: + resolution: {integrity: sha512-+n8dL/9GWblDO0iU6eZAwEIJVr5DWigtle+Q6HLOrh/pdbXOhOtqzq8VPPE2zvNJzSKY4vH/z3iT3tn0A3ypiQ==} + dependencies: + '@babel/parser': 7.21.3 + '@babel/types': 7.21.3 + '@types/babel__generator': 7.6.4 + '@types/babel__template': 7.4.1 + '@types/babel__traverse': 7.18.3 + dev: false + + /@types/babel__generator@7.6.4: + resolution: {integrity: sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==} + dependencies: + '@babel/types': 7.21.3 + dev: false + + /@types/babel__template@7.4.1: + resolution: {integrity: sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==} + dependencies: + '@babel/parser': 7.21.3 + '@babel/types': 7.21.3 + dev: false + + /@types/babel__traverse@7.18.3: + resolution: {integrity: sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w==} + dependencies: + '@babel/types': 7.21.3 + dev: false + + /@types/body-parser@1.19.2: + resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} + dependencies: + '@types/connect': 3.4.35 + '@types/node': 18.15.11 + + /@types/bonjour@3.5.10: + resolution: {integrity: sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==} + dependencies: + '@types/node': 18.15.11 + dev: false + + /@types/connect-history-api-fallback@1.3.5: + resolution: {integrity: sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==} + dependencies: + '@types/express-serve-static-core': 4.17.33 + '@types/node': 18.15.11 + + /@types/connect@3.4.35: + resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} + dependencies: + '@types/node': 18.15.11 + + /@types/cookie@0.4.1: + resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} + dev: true + + /@types/cors@2.8.13: + resolution: {integrity: sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==} + dependencies: + '@types/node': 18.15.11 + dev: true + + /@types/eslint-scope@3.7.4: + resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==} + dependencies: + '@types/eslint': 8.21.3 + '@types/estree': 0.0.51 + dev: false + + /@types/eslint@8.21.3: + resolution: {integrity: sha512-fa7GkppZVEByMWGbTtE5MbmXWJTVbrjjaS8K6uQj+XtuuUv1fsuPAxhygfqLmsb/Ufb3CV8deFCpiMfAgi00Sw==} + dependencies: + '@types/estree': 1.0.0 + '@types/json-schema': 7.0.11 + dev: false + + /@types/estree@0.0.39: + resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} + dev: false + + /@types/estree@0.0.51: + resolution: {integrity: sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==} + dev: false + + /@types/estree@1.0.0: + resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==} + + /@types/express-serve-static-core@4.17.33: + resolution: {integrity: sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==} + dependencies: + '@types/node': 18.15.11 + '@types/qs': 6.9.7 + '@types/range-parser': 1.2.4 + + /@types/express@4.17.17: + resolution: {integrity: sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==} + dependencies: + '@types/body-parser': 1.19.2 + '@types/express-serve-static-core': 4.17.33 + '@types/qs': 6.9.7 + '@types/serve-static': 1.15.1 + + /@types/glob@7.2.0: + resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} + dependencies: + '@types/minimatch': 5.1.2 + '@types/node': 18.15.11 + dev: true + + /@types/graceful-fs@4.1.6: + resolution: {integrity: sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==} + dependencies: + '@types/node': 18.15.11 + dev: false + + /@types/highlight.js@9.12.4: + resolution: {integrity: sha512-t2szdkwmg2JJyuCM20e8kR2X59WCE5Zkl4bzm1u1Oukjm79zpbiAv+QjnwLnuuV0WHEcX2NgUItu0pAMKuOPww==} + dev: true + + /@types/html-minifier-terser@6.1.0: + resolution: {integrity: sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==} + dev: false + + /@types/http-proxy@1.17.10: + resolution: {integrity: sha512-Qs5aULi+zV1bwKAg5z1PWnDXWmsn+LxIvUGv6E2+OOMYhclZMO+OXd9pYVf2gLykf2I7IV2u7oTHwChPNsvJ7g==} + dependencies: + '@types/node': 18.15.11 + + /@types/istanbul-lib-coverage@2.0.4: + resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==} + dev: false + + /@types/istanbul-lib-report@3.0.0: + resolution: {integrity: sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==} + dependencies: + '@types/istanbul-lib-coverage': 2.0.4 + dev: false + + /@types/istanbul-reports@3.0.1: + resolution: {integrity: sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==} + dependencies: + '@types/istanbul-lib-report': 3.0.0 + dev: false + + /@types/json-schema@7.0.11: + resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==} + + /@types/json5@0.0.29: + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + dev: false + + /@types/keyv@3.1.4: + resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + dependencies: + '@types/node': 18.15.11 + dev: true + + /@types/linkify-it@3.0.2: + resolution: {integrity: sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==} + dev: true + + /@types/markdown-it@10.0.3: + resolution: {integrity: sha512-daHJk22isOUvNssVGF2zDnnSyxHhFYhtjeX4oQaKD6QzL3ZR1QSgiD1g+Q6/WSWYVogNXYDXODtbgW/WiFCtyw==} + dependencies: + '@types/highlight.js': 9.12.4 + '@types/linkify-it': 3.0.2 + '@types/mdurl': 1.0.2 + highlight.js: 9.18.5 + dev: true + + /@types/mdast@3.0.11: + resolution: {integrity: sha512-Y/uImid8aAwrEA24/1tcRZwpxX3pIFTSilcNDKSPn+Y2iDywSEachzRuvgAYYLR3wpGXAsMbv5lvKLDZLeYPAw==} + dependencies: + '@types/unist': 2.0.6 + dev: true + + /@types/mdurl@1.0.2: + resolution: {integrity: sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==} + dev: true + + /@types/mime@3.0.1: + resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==} + + /@types/minimatch@5.1.2: + resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} + dev: true + + /@types/node@18.15.11: + resolution: {integrity: sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==} + + /@types/offscreencanvas@2019.7.0: + resolution: {integrity: sha512-PGcyveRIpL1XIqK8eBsmRBt76eFgtzuPiSTyKHZxnGemp2yzGzWpjYKAfK3wIMiU7eH+851yEpiuP8JZerTmWg==} + dev: true + + /@types/parse-json@4.0.0: + resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==} + dev: false + + /@types/prettier@2.7.2: + resolution: {integrity: sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==} + dev: false + + /@types/prop-types@15.7.5: + resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} + dev: false + + /@types/q@1.5.5: + resolution: {integrity: sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ==} + + /@types/qs@6.9.7: + resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==} + + /@types/range-parser@1.2.4: + resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==} + + /@types/react-dom@18.0.11: + resolution: {integrity: sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw==} + dependencies: + '@types/react': 18.0.31 + dev: false + + /@types/react@18.0.31: + resolution: {integrity: sha512-EEG67of7DsvRDU6BLLI0p+k1GojDLz9+lZsnCpCRTa/lOokvyPBvp8S5x+A24hME3yyQuIipcP70KJ6H7Qupww==} + dependencies: + '@types/prop-types': 15.7.5 + '@types/scheduler': 0.16.3 + csstype: 3.1.1 + dev: false + + /@types/resolve@1.17.1: + resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} + dependencies: + '@types/node': 18.15.11 + dev: false + + /@types/resolve@1.20.2: + resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + dev: true + + /@types/responselike@1.0.0: + resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==} + dependencies: + '@types/node': 18.15.11 + dev: true + + /@types/retry@0.12.0: + resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} + dev: false + + /@types/scheduler@0.16.3: + resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==} + dev: false + + /@types/semver@7.3.13: + resolution: {integrity: sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==} + + /@types/serve-index@1.9.1: + resolution: {integrity: sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==} + dependencies: + '@types/express': 4.17.17 + dev: false + + /@types/serve-static@1.15.1: + resolution: {integrity: sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==} + dependencies: + '@types/mime': 3.0.1 + '@types/node': 18.15.11 + + /@types/sockjs@0.3.33: + resolution: {integrity: sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==} + dependencies: + '@types/node': 18.15.11 + dev: false + + /@types/source-list-map@0.1.2: + resolution: {integrity: sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==} + dev: true + + /@types/stack-utils@2.0.1: + resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==} + dev: false + + /@types/tapable@1.0.8: + resolution: {integrity: sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==} + dev: true + + /@types/trusted-types@2.0.3: + resolution: {integrity: sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==} + dev: false + + /@types/uglify-js@3.17.1: + resolution: {integrity: sha512-GkewRA4i5oXacU/n4MA9+bLgt5/L3F1mKrYvFGm7r2ouLXhRKjuWwo9XHNnbx6WF3vlGW21S3fCvgqxvxXXc5g==} + dependencies: + source-map: 0.6.1 + dev: true + + /@types/unist@2.0.6: + resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==} + dev: true + + /@types/webpack-dev-server@3.11.6: + resolution: {integrity: sha512-XCph0RiiqFGetukCTC3KVnY1jwLcZ84illFRMbyFzCcWl90B/76ew0tSqF46oBhnLC4obNDG7dMO0JfTN0MgMQ==} + dependencies: + '@types/connect-history-api-fallback': 1.3.5 + '@types/express': 4.17.17 + '@types/serve-static': 1.15.1 + '@types/webpack': 4.41.33 + http-proxy-middleware: 1.3.1 + transitivePeerDependencies: + - debug + dev: true + + /@types/webpack-sources@3.2.0: + resolution: {integrity: sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg==} + dependencies: + '@types/node': 18.15.11 + '@types/source-list-map': 0.1.2 + source-map: 0.7.4 + dev: true + + /@types/webpack@4.41.33: + resolution: {integrity: sha512-PPajH64Ft2vWevkerISMtnZ8rTs4YmRbs+23c402J0INmxDKCrhZNvwZYtzx96gY2wAtXdrK1BS2fiC8MlLr3g==} + dependencies: + '@types/node': 18.15.11 + '@types/tapable': 1.0.8 + '@types/uglify-js': 3.17.1 + '@types/webpack-sources': 3.2.0 + anymatch: 3.1.3 + source-map: 0.6.1 + dev: true + + /@types/ws@8.5.4: + resolution: {integrity: sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==} + dependencies: + '@types/node': 18.15.11 + dev: false + + /@types/yargs-parser@21.0.0: + resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} + dev: false + + /@types/yargs@16.0.5: + resolution: {integrity: sha512-AxO/ADJOBFJScHbWhq2xAhlWP24rY4aCEG/NFaMvbT3X2MgRsLjhjQwsn0Zi5zn0LG9jUhCCZMeX9Dkuw6k+vQ==} + dependencies: + '@types/yargs-parser': 21.0.0 + dev: false + + /@types/yargs@17.0.24: + resolution: {integrity: sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==} + dependencies: + '@types/yargs-parser': 21.0.0 + dev: false + + /@typescript-eslint/eslint-plugin@5.57.0(@typescript-eslint/parser@5.57.0)(eslint@8.37.0)(typescript@4.9.5): + resolution: {integrity: sha512-itag0qpN6q2UMM6Xgk6xoHa0D0/P+M17THnr4SVgqn9Rgam5k/He33MA7/D7QoJcdMxHFyX7U9imaBonAX/6qA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + '@typescript-eslint/parser': ^5.0.0 + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.5.0 + '@typescript-eslint/parser': 5.57.0(eslint@8.37.0)(typescript@4.9.5) + '@typescript-eslint/scope-manager': 5.57.0 + '@typescript-eslint/type-utils': 5.57.0(eslint@8.37.0)(typescript@4.9.5) + '@typescript-eslint/utils': 5.57.0(eslint@8.37.0)(typescript@4.9.5) + debug: 4.3.4(supports-color@6.1.0) + eslint: 8.37.0 + grapheme-splitter: 1.0.4 + ignore: 5.2.4 + natural-compare-lite: 1.4.0 + semver: 7.3.8 + tsutils: 3.21.0(typescript@4.9.5) + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/eslint-plugin@5.57.0(@typescript-eslint/parser@5.57.0)(eslint@8.57.1)(typescript@4.9.5): + resolution: {integrity: sha512-itag0qpN6q2UMM6Xgk6xoHa0D0/P+M17THnr4SVgqn9Rgam5k/He33MA7/D7QoJcdMxHFyX7U9imaBonAX/6qA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + '@typescript-eslint/parser': ^5.0.0 + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.5.0 + '@typescript-eslint/parser': 5.57.0(eslint@8.57.1)(typescript@4.9.5) + '@typescript-eslint/scope-manager': 5.57.0 + '@typescript-eslint/type-utils': 5.57.0(eslint@8.57.1)(typescript@4.9.5) + '@typescript-eslint/utils': 5.57.0(eslint@8.57.1)(typescript@4.9.5) + debug: 4.3.4(supports-color@6.1.0) + eslint: 8.57.1 + grapheme-splitter: 1.0.4 + ignore: 5.2.4 + natural-compare-lite: 1.4.0 + semver: 7.3.8 + tsutils: 3.21.0(typescript@4.9.5) + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + dev: false + + /@typescript-eslint/experimental-utils@5.57.0(eslint@8.57.1)(typescript@4.9.5): + resolution: {integrity: sha512-0RnrwGQ7MmgtOSnzB/rSGYr2iXENi6L+CtPzX3g5ovo0HlruLukSEKcc4s+q0IEc+DLTDc7Edan0Y4WSQ/bFhw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + '@typescript-eslint/utils': 5.57.0(eslint@8.57.1)(typescript@4.9.5) + eslint: 8.57.1 + transitivePeerDependencies: + - supports-color + - typescript + dev: false + + /@typescript-eslint/parser@5.57.0(eslint@8.37.0)(typescript@4.9.5): + resolution: {integrity: sha512-orrduvpWYkgLCyAdNtR1QIWovcNZlEm6yL8nwH/eTxWLd8gsP+25pdLHYzL2QdkqrieaDwLpytHqycncv0woUQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 5.57.0 + '@typescript-eslint/types': 5.57.0 + '@typescript-eslint/typescript-estree': 5.57.0(typescript@4.9.5) + debug: 4.3.4(supports-color@6.1.0) + eslint: 8.37.0 + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/parser@5.57.0(eslint@8.57.1)(typescript@4.9.5): + resolution: {integrity: sha512-orrduvpWYkgLCyAdNtR1QIWovcNZlEm6yL8nwH/eTxWLd8gsP+25pdLHYzL2QdkqrieaDwLpytHqycncv0woUQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 5.57.0 + '@typescript-eslint/types': 5.57.0 + '@typescript-eslint/typescript-estree': 5.57.0(typescript@4.9.5) + debug: 4.3.4(supports-color@6.1.0) + eslint: 8.57.1 + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + dev: false + + /@typescript-eslint/scope-manager@5.57.0: + resolution: {integrity: sha512-NANBNOQvllPlizl9LatX8+MHi7bx7WGIWYjPHDmQe5Si/0YEYfxSljJpoTyTWFTgRy3X8gLYSE4xQ2U+aCozSw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.57.0 + '@typescript-eslint/visitor-keys': 5.57.0 + + /@typescript-eslint/type-utils@5.57.0(eslint@8.37.0)(typescript@4.9.5): + resolution: {integrity: sha512-kxXoq9zOTbvqzLbdNKy1yFrxLC6GDJFE2Yuo3KqSwTmDOFjUGeWSakgoXT864WcK5/NAJkkONCiKb1ddsqhLXQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '*' + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 5.57.0(typescript@4.9.5) + '@typescript-eslint/utils': 5.57.0(eslint@8.37.0)(typescript@4.9.5) + debug: 4.3.4(supports-color@6.1.0) + eslint: 8.37.0 + tsutils: 3.21.0(typescript@4.9.5) + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/type-utils@5.57.0(eslint@8.57.1)(typescript@4.9.5): + resolution: {integrity: sha512-kxXoq9zOTbvqzLbdNKy1yFrxLC6GDJFE2Yuo3KqSwTmDOFjUGeWSakgoXT864WcK5/NAJkkONCiKb1ddsqhLXQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '*' + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 5.57.0(typescript@4.9.5) + '@typescript-eslint/utils': 5.57.0(eslint@8.57.1)(typescript@4.9.5) + debug: 4.3.4(supports-color@6.1.0) + eslint: 8.57.1 + tsutils: 3.21.0(typescript@4.9.5) + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + dev: false + + /@typescript-eslint/types@5.57.0: + resolution: {integrity: sha512-mxsod+aZRSyLT+jiqHw1KK6xrANm19/+VFALVFP5qa/aiJnlP38qpyaTd0fEKhWvQk6YeNZ5LGwI1pDpBRBhtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + /@typescript-eslint/typescript-estree@5.57.0(typescript@4.9.5): + resolution: {integrity: sha512-LTzQ23TV82KpO8HPnWuxM2V7ieXW8O142I7hQTxWIHDcCEIjtkat6H96PFkYBQqGFLW/G/eVVOB9Z8rcvdY/Vw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 5.57.0 + '@typescript-eslint/visitor-keys': 5.57.0 + debug: 4.3.4(supports-color@6.1.0) + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.3.8 + tsutils: 3.21.0(typescript@4.9.5) + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + + /@typescript-eslint/utils@5.57.0(eslint@8.37.0)(typescript@4.9.5): + resolution: {integrity: sha512-ps/4WohXV7C+LTSgAL5CApxvxbMkl9B9AUZRtnEFonpIxZDIT7wC1xfvuJONMidrkB9scs4zhtRyIwHh4+18kw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.37.0) + '@types/json-schema': 7.0.11 + '@types/semver': 7.3.13 + '@typescript-eslint/scope-manager': 5.57.0 + '@typescript-eslint/types': 5.57.0 + '@typescript-eslint/typescript-estree': 5.57.0(typescript@4.9.5) + eslint: 8.37.0 + eslint-scope: 5.1.1 + semver: 7.3.8 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@typescript-eslint/utils@5.57.0(eslint@8.57.1)(typescript@4.9.5): + resolution: {integrity: sha512-ps/4WohXV7C+LTSgAL5CApxvxbMkl9B9AUZRtnEFonpIxZDIT7wC1xfvuJONMidrkB9scs4zhtRyIwHh4+18kw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.1) + '@types/json-schema': 7.0.11 + '@types/semver': 7.3.13 + '@typescript-eslint/scope-manager': 5.57.0 + '@typescript-eslint/types': 5.57.0 + '@typescript-eslint/typescript-estree': 5.57.0(typescript@4.9.5) + eslint: 8.57.1 + eslint-scope: 5.1.1 + semver: 7.3.8 + transitivePeerDependencies: + - supports-color + - typescript + dev: false + + /@typescript-eslint/visitor-keys@5.57.0: + resolution: {integrity: sha512-ery2g3k0hv5BLiKpPuwYt9KBkAp2ugT6VvyShXdLOkax895EC55sP0Tx5L0fZaQueiK3fBLvHVvEl3jFS5ia+g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.57.0 + eslint-visitor-keys: 3.4.0 + + /@ungap/structured-clone@1.3.0: + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + dev: false + + /@vue/babel-helper-vue-jsx-merge-props@1.4.0: + resolution: {integrity: sha512-JkqXfCkUDp4PIlFdDQ0TdXoIejMtTHP67/pvxlgeY+u5k3LEdKuWZ3LK6xkxo52uDoABIVyRwqVkfLQJhk7VBA==} + dev: true + + /@vue/babel-helper-vue-transform-on@1.0.2: + resolution: {integrity: sha512-hz4R8tS5jMn8lDq6iD+yWL6XNB699pGIVLk7WSJnn1dbpjaazsjZQkieJoRX6gW5zpYSCFqQ7jUquPNY65tQYA==} + dev: true + + /@vue/babel-plugin-jsx@1.1.1(@babel/core@7.21.3): + resolution: {integrity: sha512-j2uVfZjnB5+zkcbc/zsOc0fSNGCMMjaEXP52wdwdIfn0qjFfEYpYZBFKFg+HHnQeJCVrjOeO0YxgaL7DMrym9w==} + dependencies: + '@babel/helper-module-imports': 7.18.6 + '@babel/plugin-syntax-jsx': 7.18.6(@babel/core@7.21.3) + '@babel/template': 7.20.7 + '@babel/traverse': 7.21.3 + '@babel/types': 7.21.3 + '@vue/babel-helper-vue-transform-on': 1.0.2 + camelcase: 6.3.0 + html-tags: 3.2.0 + svg-tags: 1.0.0 + transitivePeerDependencies: + - '@babel/core' + - supports-color + dev: true + + /@vue/babel-plugin-transform-vue-jsx@1.4.0(@babel/core@7.21.3): + resolution: {integrity: sha512-Fmastxw4MMx0vlgLS4XBX0XiBbUFzoMGeVXuMV08wyOfXdikAFqBTuYPR0tlk+XskL19EzHc39SgjrPGY23JnA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-module-imports': 7.18.6 + '@babel/plugin-syntax-jsx': 7.18.6(@babel/core@7.21.3) + '@vue/babel-helper-vue-jsx-merge-props': 1.4.0 + html-tags: 2.0.0 + lodash.kebabcase: 4.1.1 + svg-tags: 1.0.0 + dev: true + + /@vue/babel-preset-app@4.5.19(@babel/core@7.21.3)(core-js@3.29.1)(vue@2.7.14): + resolution: {integrity: sha512-VCNRiAt2P/bLo09rYt3DLe6xXUMlhJwrvU18Ddd/lYJgC7s8+wvhgYs+MTx4OiAXdu58drGwSBO9SPx7C6J82Q==} + peerDependencies: + '@babel/core': '*' + core-js: ^3 + vue: ^2 || ^3.0.0-0 + peerDependenciesMeta: + core-js: + optional: true + vue: + optional: true + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-compilation-targets': 7.20.7(@babel/core@7.21.3) + '@babel/helper-module-imports': 7.18.6 + '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.21.3) + '@babel/plugin-proposal-decorators': 7.21.0(@babel/core@7.21.3) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.21.3) + '@babel/plugin-syntax-jsx': 7.18.6(@babel/core@7.21.3) + '@babel/plugin-transform-runtime': 7.21.0(@babel/core@7.21.3) + '@babel/preset-env': 7.20.2(@babel/core@7.21.3) + '@babel/runtime': 7.21.0 + '@vue/babel-plugin-jsx': 1.1.1(@babel/core@7.21.3) + '@vue/babel-preset-jsx': 1.4.0(@babel/core@7.21.3)(vue@2.7.14) + babel-plugin-dynamic-import-node: 2.3.3 + core-js: 3.29.1 + core-js-compat: 3.29.1 + semver: 6.3.0 + vue: 2.7.14 + transitivePeerDependencies: + - supports-color + dev: true + + /@vue/babel-preset-jsx@1.4.0(@babel/core@7.21.3)(vue@2.7.14): + resolution: {integrity: sha512-QmfRpssBOPZWL5xw7fOuHNifCQcNQC1PrOo/4fu6xlhlKJJKSA3HqX92Nvgyx8fqHZTUGMPHmFA+IDqwXlqkSA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + vue: '*' + peerDependenciesMeta: + vue: + optional: true + dependencies: + '@babel/core': 7.21.3 + '@vue/babel-helper-vue-jsx-merge-props': 1.4.0 + '@vue/babel-plugin-transform-vue-jsx': 1.4.0(@babel/core@7.21.3) + '@vue/babel-sugar-composition-api-inject-h': 1.4.0(@babel/core@7.21.3) + '@vue/babel-sugar-composition-api-render-instance': 1.4.0(@babel/core@7.21.3) + '@vue/babel-sugar-functional-vue': 1.4.0(@babel/core@7.21.3) + '@vue/babel-sugar-inject-h': 1.4.0(@babel/core@7.21.3) + '@vue/babel-sugar-v-model': 1.4.0(@babel/core@7.21.3) + '@vue/babel-sugar-v-on': 1.4.0(@babel/core@7.21.3) + vue: 2.7.14 + dev: true + + /@vue/babel-sugar-composition-api-inject-h@1.4.0(@babel/core@7.21.3): + resolution: {integrity: sha512-VQq6zEddJHctnG4w3TfmlVp5FzDavUSut/DwR0xVoe/mJKXyMcsIibL42wPntozITEoY90aBV0/1d2KjxHU52g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/plugin-syntax-jsx': 7.18.6(@babel/core@7.21.3) + dev: true + + /@vue/babel-sugar-composition-api-render-instance@1.4.0(@babel/core@7.21.3): + resolution: {integrity: sha512-6ZDAzcxvy7VcnCjNdHJ59mwK02ZFuP5CnucloidqlZwVQv5CQLijc3lGpR7MD3TWFi78J7+a8J56YxbCtHgT9Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/plugin-syntax-jsx': 7.18.6(@babel/core@7.21.3) + dev: true + + /@vue/babel-sugar-functional-vue@1.4.0(@babel/core@7.21.3): + resolution: {integrity: sha512-lTEB4WUFNzYt2In6JsoF9sAYVTo84wC4e+PoZWSgM6FUtqRJz7wMylaEhSRgG71YF+wfLD6cc9nqVeXN2rwBvw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/plugin-syntax-jsx': 7.18.6(@babel/core@7.21.3) + dev: true + + /@vue/babel-sugar-inject-h@1.4.0(@babel/core@7.21.3): + resolution: {integrity: sha512-muwWrPKli77uO2fFM7eA3G1lAGnERuSz2NgAxuOLzrsTlQl8W4G+wwbM4nB6iewlKbwKRae3nL03UaF5ffAPMA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/plugin-syntax-jsx': 7.18.6(@babel/core@7.21.3) + dev: true + + /@vue/babel-sugar-v-model@1.4.0(@babel/core@7.21.3): + resolution: {integrity: sha512-0t4HGgXb7WHYLBciZzN5s0Hzqan4Ue+p/3FdQdcaHAb7s5D9WZFGoSxEZHrR1TFVZlAPu1bejTKGeAzaaG3NCQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/plugin-syntax-jsx': 7.18.6(@babel/core@7.21.3) + '@vue/babel-helper-vue-jsx-merge-props': 1.4.0 + '@vue/babel-plugin-transform-vue-jsx': 1.4.0(@babel/core@7.21.3) + camelcase: 5.3.1 + html-tags: 2.0.0 + svg-tags: 1.0.0 + dev: true + + /@vue/babel-sugar-v-on@1.4.0(@babel/core@7.21.3): + resolution: {integrity: sha512-m+zud4wKLzSKgQrWwhqRObWzmTuyzl6vOP7024lrpeJM4x2UhQtRDLgYjXAw9xBXjCwS0pP9kXjg91F9ZNo9JA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/plugin-syntax-jsx': 7.18.6(@babel/core@7.21.3) + '@vue/babel-plugin-transform-vue-jsx': 1.4.0(@babel/core@7.21.3) + camelcase: 5.3.1 + dev: true + + /@vue/compiler-sfc@2.7.14: + resolution: {integrity: sha512-aNmNHyLPsw+sVvlQFQ2/8sjNuLtK54TC6cuKnVzAY93ks4ZBrvwQSnkkIh7bsbNhum5hJBS00wSDipQ937f5DA==} + dependencies: + '@babel/parser': 7.21.3 + postcss: 8.4.21 + source-map: 0.6.1 + dev: true + + /@vue/component-compiler-utils@3.3.0: + resolution: {integrity: sha512-97sfH2mYNU+2PzGrmK2haqffDpVASuib9/w2/noxiFi31Z54hW+q3izKQXXQZSNhtiUpAI36uSuYepeBe4wpHQ==} + dependencies: + consolidate: 0.15.1 + hash-sum: 1.0.2 + lru-cache: 4.1.5 + merge-source-map: 1.1.0 + postcss: 7.0.39 + postcss-selector-parser: 6.0.11 + source-map: 0.6.1 + vue-template-es2015-compiler: 1.9.1 + optionalDependencies: + prettier: 2.8.8 + transitivePeerDependencies: + - arc-templates + - atpl + - babel-core + - bracket-template + - coffee-script + - dot + - dust + - dustjs-helpers + - dustjs-linkedin + - eco + - ect + - ejs + - haml-coffee + - hamlet + - hamljs + - handlebars + - hogan.js + - htmling + - jade + - jazz + - jqtpl + - just + - liquid-node + - liquor + - lodash + - marko + - mote + - mustache + - nunjucks + - plates + - pug + - qejs + - ractive + - razor-tmpl + - react + - react-dom + - slm + - squirrelly + - swig + - swig-templates + - teacup + - templayed + - then-jade + - then-pug + - tinyliquid + - toffee + - twig + - twing + - underscore + - vash + - velocityjs + - walrus + - whiskers + dev: true + + /@vuepress/core@1.9.9: + resolution: {integrity: sha512-Ekgu409ZSgvAV9n14F3DaEWtgkwrEicg1nWs0gbxGgUCdREeX/7rwxSfKwWwBjCwfCUKR2L3+6pXGjzxex0t+g==} + engines: {node: '>=8.6'} + dependencies: + '@babel/core': 7.21.3 + '@vue/babel-preset-app': 4.5.19(@babel/core@7.21.3)(core-js@3.29.1)(vue@2.7.14) + '@vuepress/markdown': 1.9.9 + '@vuepress/markdown-loader': 1.9.9 + '@vuepress/plugin-last-updated': 1.9.9 + '@vuepress/plugin-register-components': 1.9.9 + '@vuepress/shared-utils': 1.9.9 + '@vuepress/types': 1.9.9 + autoprefixer: 9.8.8 + babel-loader: 8.3.0(@babel/core@7.21.3)(webpack@4.46.0) + bundle-require: 2.1.8(esbuild@0.14.7) + cache-loader: 3.0.1(webpack@4.46.0) + chokidar: 2.1.8(supports-color@6.1.0) + connect-history-api-fallback: 1.6.0 + copy-webpack-plugin: 5.1.2(webpack@4.46.0) + core-js: 3.29.1 + cross-spawn: 6.0.5 + css-loader: 2.1.1(webpack@4.46.0) + esbuild: 0.14.7 + file-loader: 3.0.1(webpack@4.46.0) + js-yaml: 3.14.1 + lru-cache: 5.1.1 + mini-css-extract-plugin: 0.6.0(webpack@4.46.0) + optimize-css-assets-webpack-plugin: 5.0.8(webpack@4.46.0) + portfinder: 1.0.32(supports-color@6.1.0) + postcss-loader: 3.0.0 + postcss-safe-parser: 4.0.2 + toml: 3.0.0 + url-loader: 1.1.2(webpack@4.46.0) + vue: 2.7.14 + vue-loader: 15.10.1(cache-loader@3.0.1)(css-loader@2.1.1)(vue-template-compiler@2.7.14)(webpack@4.46.0) + vue-router: 3.6.5(vue@2.7.14) + vue-server-renderer: 2.7.14 + vue-template-compiler: 2.7.14 + vuepress-html-webpack-plugin: 3.2.0(webpack@4.46.0) + vuepress-plugin-container: 2.1.5 + webpack: 4.46.0 + webpack-chain: 6.5.1 + webpack-dev-server: 3.11.3(webpack@4.46.0) + webpack-merge: 4.2.2 + webpackbar: 3.2.0(webpack@4.46.0) + transitivePeerDependencies: + - '@vue/compiler-sfc' + - arc-templates + - atpl + - babel-core + - bracket-template + - bufferutil + - coffee-script + - debug + - dot + - dust + - dustjs-helpers + - dustjs-linkedin + - eco + - ect + - ejs + - haml-coffee + - hamlet + - hamljs + - handlebars + - hogan.js + - htmling + - jade + - jazz + - jqtpl + - just + - liquid-node + - liquor + - lodash + - marko + - mote + - mustache + - nunjucks + - plates + - pug + - qejs + - ractive + - razor-tmpl + - react + - react-dom + - slm + - squirrelly + - supports-color + - swig + - swig-templates + - teacup + - templayed + - then-jade + - then-pug + - tinyliquid + - toffee + - twig + - twing + - underscore + - utf-8-validate + - vash + - velocityjs + - walrus + - webpack-cli + - webpack-command + - whiskers + dev: true + + /@vuepress/markdown-loader@1.9.9: + resolution: {integrity: sha512-nyY+sytuQaDLEIk6Yj9JFUfSQpe9/sz30xQFkGCYqi0lQTRGQM6IcRDgfcTS7b25A0qRlwpDGBfKQiGGMZKSfg==} + dependencies: + '@vuepress/markdown': 1.9.9 + loader-utils: 1.4.2 + lru-cache: 5.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@vuepress/markdown@1.9.9: + resolution: {integrity: sha512-JzFdBdGe5aoiKSaEgF+h3JLDXNVfWPI5DJWXrIt7rhhkMJesF6HowIznPLdXqukzHfXHcPvo9oQ4o6eT0YmVGA==} + dependencies: + '@vuepress/shared-utils': 1.9.9 + markdown-it: 8.4.2 + markdown-it-anchor: 5.3.0(markdown-it@8.4.2) + markdown-it-chain: 1.3.0(markdown-it@8.4.2) + markdown-it-emoji: 1.4.0 + markdown-it-table-of-contents: 0.4.4 + prismjs: 1.29.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@vuepress/plugin-active-header-links@1.9.9: + resolution: {integrity: sha512-lTnIhbuALjOjFts33jJD8r4ScNBxnZ6MtmePKEwvYlC3J9uvngs1Htpb1JzLEX9QCydt+bhLmZ92bTXn/PdTpg==} + dependencies: + '@vuepress/types': 1.9.9 + lodash.debounce: 4.0.8 + transitivePeerDependencies: + - debug + dev: true + + /@vuepress/plugin-google-analytics@1.9.9: + resolution: {integrity: sha512-GxrM4BopPqTiGAq2ku5HqInha6uQZePxdGpU8etTbM6hhaxZAev4HehrtHISAJm5dVptbFFJl3sNGQBnw2deFQ==} + dependencies: + '@vuepress/types': 1.9.9 + transitivePeerDependencies: + - debug + dev: true + + /@vuepress/plugin-html-redirect@0.1.4: + resolution: {integrity: sha512-tzVquctn7Jwv/nFlsbDxqUeaJzG5H+muoOWl1O3M24XFu3KVsIoqZZt1seawrSCWWfFyLB9nVPJSoXALQ62hdg==} + dev: true + + /@vuepress/plugin-last-updated@1.9.9: + resolution: {integrity: sha512-MV4csmM0/lye83VtkOc+b8fs0roi7mvE7BmCCOE39Z6t8nv/ZmEPOwKeHD0+hXPT+ZfoATYvDcsYU7uxbdw0Pw==} + dependencies: + '@vuepress/types': 1.9.9 + cross-spawn: 6.0.5 + transitivePeerDependencies: + - debug + dev: true + + /@vuepress/plugin-nprogress@1.9.9: + resolution: {integrity: sha512-+3fLxjwTLH8MeU54E7i1ovRu9KzBom2lvSeUsu9B8PuLyrETAqW7Pe1H66awEEALEe0ZnnEU4d7SeVe9ljsLAQ==} + dependencies: + '@vuepress/types': 1.9.9 + nprogress: 0.2.0 + transitivePeerDependencies: + - debug + dev: true + + /@vuepress/plugin-register-components@1.9.9: + resolution: {integrity: sha512-tddnAiSmJsIWWPzE7TcbGU8xzndXf4a8i4BfIev2QzSUnIOQFZDGXUAsCkw4/f9N9UFxQSObjFPzTeUUxb7EvA==} + dependencies: + '@vuepress/shared-utils': 1.9.9 + '@vuepress/types': 1.9.9 + transitivePeerDependencies: + - debug + - supports-color + dev: true + + /@vuepress/plugin-search@1.9.9: + resolution: {integrity: sha512-W/FE+YHoXDD4qk2wu5yRMkti271TA4y+7UBMrmCavvVAGrLIRnaZfswRUgIiDlEthBc+Pn8/As/Dy1jFTLBa9A==} + dependencies: + '@vuepress/types': 1.9.9 + transitivePeerDependencies: + - debug + dev: true + + /@vuepress/shared-utils@1.9.9: + resolution: {integrity: sha512-qhk/7QF5LgMEXhEB1hlqreGFgkz4p2pmaBBNFxnAnYmSwmyO+u/oFOpZLI16QRx9Wg6ekR2ENmByQLxV7y4lJg==} + dependencies: + chalk: 2.4.2 + escape-html: 1.0.3 + fs-extra: 7.0.1 + globby: 9.2.0 + gray-matter: 4.0.3 + hash-sum: 1.0.2 + semver: 6.3.0 + toml: 3.0.0 + upath: 1.2.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@vuepress/theme-default@1.9.9: + resolution: {integrity: sha512-de0FiOwM/h3rFTBSZK0NNBB117lA/e3IHusU7Xm2XeZRiZ/EE3yvbWclZnbbRNt3YjDMmrWXEW/kBTBxfiMuWQ==} + dependencies: + '@vuepress/plugin-active-header-links': 1.9.9 + '@vuepress/plugin-nprogress': 1.9.9 + '@vuepress/plugin-search': 1.9.9 + '@vuepress/types': 1.9.9 + docsearch.js: 2.6.3 + lodash: 4.17.21 + stylus: 0.54.8 + stylus-loader: 3.0.2(stylus@0.54.8) + vuepress-plugin-container: 2.1.5 + vuepress-plugin-smooth-scroll: 0.0.3 + transitivePeerDependencies: + - debug + - supports-color + dev: true + + /@vuepress/types@1.9.9: + resolution: {integrity: sha512-ukGW49ILzLhIc7CltHMr+BeIjWKloJNN1mrvbDz3beycp9b9kgH+DXNdRIK9QCKr4fJsy7x08vNMwZr9Nq/PTQ==} + dependencies: + '@types/markdown-it': 10.0.3 + '@types/webpack-dev-server': 3.11.6 + webpack-chain: 6.5.1 + transitivePeerDependencies: + - debug + dev: true + + /@webassemblyjs/ast@1.11.1: + resolution: {integrity: sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==} + dependencies: + '@webassemblyjs/helper-numbers': 1.11.1 + '@webassemblyjs/helper-wasm-bytecode': 1.11.1 + dev: false + + /@webassemblyjs/ast@1.9.0: + resolution: {integrity: sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA==} + dependencies: + '@webassemblyjs/helper-module-context': 1.9.0 + '@webassemblyjs/helper-wasm-bytecode': 1.9.0 + '@webassemblyjs/wast-parser': 1.9.0 + dev: true + + /@webassemblyjs/floating-point-hex-parser@1.11.1: + resolution: {integrity: sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==} + dev: false + + /@webassemblyjs/floating-point-hex-parser@1.9.0: + resolution: {integrity: sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA==} + dev: true + + /@webassemblyjs/helper-api-error@1.11.1: + resolution: {integrity: sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==} + dev: false + + /@webassemblyjs/helper-api-error@1.9.0: + resolution: {integrity: sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw==} + dev: true + + /@webassemblyjs/helper-buffer@1.11.1: + resolution: {integrity: sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==} + dev: false + + /@webassemblyjs/helper-buffer@1.9.0: + resolution: {integrity: sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA==} + dev: true + + /@webassemblyjs/helper-code-frame@1.9.0: + resolution: {integrity: sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA==} + dependencies: + '@webassemblyjs/wast-printer': 1.9.0 + dev: true + + /@webassemblyjs/helper-fsm@1.9.0: + resolution: {integrity: sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw==} + dev: true + + /@webassemblyjs/helper-module-context@1.9.0: + resolution: {integrity: sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g==} + dependencies: + '@webassemblyjs/ast': 1.9.0 + dev: true + + /@webassemblyjs/helper-numbers@1.11.1: + resolution: {integrity: sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==} + dependencies: + '@webassemblyjs/floating-point-hex-parser': 1.11.1 + '@webassemblyjs/helper-api-error': 1.11.1 + '@xtuc/long': 4.2.2 + dev: false + + /@webassemblyjs/helper-wasm-bytecode@1.11.1: + resolution: {integrity: sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==} + dev: false + + /@webassemblyjs/helper-wasm-bytecode@1.9.0: + resolution: {integrity: sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==} + dev: true + + /@webassemblyjs/helper-wasm-section@1.11.1: + resolution: {integrity: sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==} + dependencies: + '@webassemblyjs/ast': 1.11.1 + '@webassemblyjs/helper-buffer': 1.11.1 + '@webassemblyjs/helper-wasm-bytecode': 1.11.1 + '@webassemblyjs/wasm-gen': 1.11.1 + dev: false + + /@webassemblyjs/helper-wasm-section@1.9.0: + resolution: {integrity: sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw==} + dependencies: + '@webassemblyjs/ast': 1.9.0 + '@webassemblyjs/helper-buffer': 1.9.0 + '@webassemblyjs/helper-wasm-bytecode': 1.9.0 + '@webassemblyjs/wasm-gen': 1.9.0 + dev: true + + /@webassemblyjs/ieee754@1.11.1: + resolution: {integrity: sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==} + dependencies: + '@xtuc/ieee754': 1.2.0 + dev: false + + /@webassemblyjs/ieee754@1.9.0: + resolution: {integrity: sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg==} + dependencies: + '@xtuc/ieee754': 1.2.0 + dev: true + + /@webassemblyjs/leb128@1.11.1: + resolution: {integrity: sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==} + dependencies: + '@xtuc/long': 4.2.2 + dev: false + + /@webassemblyjs/leb128@1.9.0: + resolution: {integrity: sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw==} + dependencies: + '@xtuc/long': 4.2.2 + dev: true + + /@webassemblyjs/utf8@1.11.1: + resolution: {integrity: sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==} + dev: false + + /@webassemblyjs/utf8@1.9.0: + resolution: {integrity: sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w==} + dev: true + + /@webassemblyjs/wasm-edit@1.11.1: + resolution: {integrity: sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==} + dependencies: + '@webassemblyjs/ast': 1.11.1 + '@webassemblyjs/helper-buffer': 1.11.1 + '@webassemblyjs/helper-wasm-bytecode': 1.11.1 + '@webassemblyjs/helper-wasm-section': 1.11.1 + '@webassemblyjs/wasm-gen': 1.11.1 + '@webassemblyjs/wasm-opt': 1.11.1 + '@webassemblyjs/wasm-parser': 1.11.1 + '@webassemblyjs/wast-printer': 1.11.1 + dev: false + + /@webassemblyjs/wasm-edit@1.9.0: + resolution: {integrity: sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw==} + dependencies: + '@webassemblyjs/ast': 1.9.0 + '@webassemblyjs/helper-buffer': 1.9.0 + '@webassemblyjs/helper-wasm-bytecode': 1.9.0 + '@webassemblyjs/helper-wasm-section': 1.9.0 + '@webassemblyjs/wasm-gen': 1.9.0 + '@webassemblyjs/wasm-opt': 1.9.0 + '@webassemblyjs/wasm-parser': 1.9.0 + '@webassemblyjs/wast-printer': 1.9.0 + dev: true + + /@webassemblyjs/wasm-gen@1.11.1: + resolution: {integrity: sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==} + dependencies: + '@webassemblyjs/ast': 1.11.1 + '@webassemblyjs/helper-wasm-bytecode': 1.11.1 + '@webassemblyjs/ieee754': 1.11.1 + '@webassemblyjs/leb128': 1.11.1 + '@webassemblyjs/utf8': 1.11.1 + dev: false + + /@webassemblyjs/wasm-gen@1.9.0: + resolution: {integrity: sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA==} + dependencies: + '@webassemblyjs/ast': 1.9.0 + '@webassemblyjs/helper-wasm-bytecode': 1.9.0 + '@webassemblyjs/ieee754': 1.9.0 + '@webassemblyjs/leb128': 1.9.0 + '@webassemblyjs/utf8': 1.9.0 + dev: true + + /@webassemblyjs/wasm-opt@1.11.1: + resolution: {integrity: sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==} + dependencies: + '@webassemblyjs/ast': 1.11.1 + '@webassemblyjs/helper-buffer': 1.11.1 + '@webassemblyjs/wasm-gen': 1.11.1 + '@webassemblyjs/wasm-parser': 1.11.1 + dev: false + + /@webassemblyjs/wasm-opt@1.9.0: + resolution: {integrity: sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A==} + dependencies: + '@webassemblyjs/ast': 1.9.0 + '@webassemblyjs/helper-buffer': 1.9.0 + '@webassemblyjs/wasm-gen': 1.9.0 + '@webassemblyjs/wasm-parser': 1.9.0 + dev: true + + /@webassemblyjs/wasm-parser@1.11.1: + resolution: {integrity: sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==} + dependencies: + '@webassemblyjs/ast': 1.11.1 + '@webassemblyjs/helper-api-error': 1.11.1 + '@webassemblyjs/helper-wasm-bytecode': 1.11.1 + '@webassemblyjs/ieee754': 1.11.1 + '@webassemblyjs/leb128': 1.11.1 + '@webassemblyjs/utf8': 1.11.1 + dev: false + + /@webassemblyjs/wasm-parser@1.9.0: + resolution: {integrity: sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA==} + dependencies: + '@webassemblyjs/ast': 1.9.0 + '@webassemblyjs/helper-api-error': 1.9.0 + '@webassemblyjs/helper-wasm-bytecode': 1.9.0 + '@webassemblyjs/ieee754': 1.9.0 + '@webassemblyjs/leb128': 1.9.0 + '@webassemblyjs/utf8': 1.9.0 + dev: true + + /@webassemblyjs/wast-parser@1.9.0: + resolution: {integrity: sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw==} + dependencies: + '@webassemblyjs/ast': 1.9.0 + '@webassemblyjs/floating-point-hex-parser': 1.9.0 + '@webassemblyjs/helper-api-error': 1.9.0 + '@webassemblyjs/helper-code-frame': 1.9.0 + '@webassemblyjs/helper-fsm': 1.9.0 + '@xtuc/long': 4.2.2 + dev: true + + /@webassemblyjs/wast-printer@1.11.1: + resolution: {integrity: sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==} + dependencies: + '@webassemblyjs/ast': 1.11.1 + '@xtuc/long': 4.2.2 + dev: false + + /@webassemblyjs/wast-printer@1.9.0: + resolution: {integrity: sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA==} + dependencies: + '@webassemblyjs/ast': 1.9.0 + '@webassemblyjs/wast-parser': 1.9.0 + '@xtuc/long': 4.2.2 + dev: true + + /@xtuc/ieee754@1.2.0: + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} + + /@xtuc/long@4.2.2: + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + + /abab@2.0.6: + resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} + dev: false + + /abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + dev: true + + /accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + /acorn-globals@6.0.0: + resolution: {integrity: sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==} + dependencies: + acorn: 7.4.1 + acorn-walk: 7.2.0 + dev: false + + /acorn-import-assertions@1.8.0(acorn@8.8.2): + resolution: {integrity: sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==} + peerDependencies: + acorn: ^8 + dependencies: + acorn: 8.8.2 + dev: false + + /acorn-jsx@5.3.2(acorn@8.15.0): + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 8.15.0 + dev: false + + /acorn-jsx@5.3.2(acorn@8.8.2): + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 8.8.2 + dev: true + + /acorn-walk@7.2.0: + resolution: {integrity: sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==} + engines: {node: '>=0.4.0'} + dev: false + + /acorn@6.4.2: + resolution: {integrity: sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /acorn@7.4.1: + resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: false + + /acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: false + + /acorn@8.8.2: + resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==} + engines: {node: '>=0.4.0'} + hasBin: true + + /address@1.2.2: + resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==} + engines: {node: '>= 10.0.0'} + dev: false + + /adjust-sourcemap-loader@4.0.0: + resolution: {integrity: sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==} + engines: {node: '>=8.9'} + dependencies: + loader-utils: 2.0.4 + regex-parser: 2.2.11 + dev: false + + /agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + dependencies: + debug: 4.3.4(supports-color@6.1.0) + transitivePeerDependencies: + - supports-color + dev: false + + /agentkeepalive@2.2.0: + resolution: {integrity: sha512-TnB6ziK363p7lR8QpeLC8aMr8EGYBKZTpgzQLfqTs3bR0Oo5VbKdwKf8h0dSzsYrB7lSCgfJnMZKqShvlq5Oyg==} + engines: {node: '>= 0.10.0'} + dev: true + + /ajv-errors@1.0.1(ajv@6.12.6): + resolution: {integrity: sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==} + peerDependencies: + ajv: '>=5.0.0' + dependencies: + ajv: 6.12.6 + dev: true + + /ajv-formats@2.1.1(ajv@8.12.0): + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + dependencies: + ajv: 8.12.0 + dev: false + + /ajv-keywords@3.5.2(ajv@6.12.6): + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + peerDependencies: + ajv: ^6.9.1 + dependencies: + ajv: 6.12.6 + + /ajv-keywords@5.1.0(ajv@8.12.0): + resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} + peerDependencies: + ajv: ^8.8.2 + dependencies: + ajv: 8.12.0 + fast-deep-equal: 3.1.3 + dev: false + + /ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + /ajv@8.12.0: + resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + dev: false + + /algoliasearch@3.35.1: + resolution: {integrity: sha512-K4yKVhaHkXfJ/xcUnil04xiSrB8B8yHZoFEhWNpXg23eiCnqvTZw1tn/SqvdsANlYHLJlKl0qi3I/Q2Sqo7LwQ==} + engines: {node: '>=0.8'} + dependencies: + agentkeepalive: 2.2.0 + debug: 2.6.9(supports-color@6.1.0) + envify: 4.1.0 + es6-promise: 4.2.8 + events: 1.1.1 + foreach: 2.0.6 + global: 4.4.0 + inherits: 2.0.4 + isarray: 2.0.5 + load-script: 1.0.0 + object-keys: 1.1.1 + querystring-es3: 0.2.1 + reduce: 1.0.2 + semver: 5.7.1 + tunnel-agent: 0.6.0 + transitivePeerDependencies: + - supports-color + dev: true + + /alphanum-sort@1.0.2: + resolution: {integrity: sha512-0FcBfdcmaumGPQ0qPn7Q5qTgz/ooXgIyp1rf8ik5bGX8mpE2YHjC0P/eyQvxu1GURYQgq9ozf2mteQ5ZD9YiyQ==} + dev: true + + /ansi-align@3.0.1: + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + dependencies: + string-width: 4.2.3 + dev: true + + /ansi-colors@3.2.4: + resolution: {integrity: sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==} + engines: {node: '>=6'} + dev: true + + /ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.21.3 + + /ansi-html-community@0.0.8: + resolution: {integrity: sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==} + engines: {'0': node >= 0.8.0} + hasBin: true + + /ansi-regex@2.1.1: + resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==} + engines: {node: '>=0.10.0'} + dev: true + + /ansi-regex@4.1.1: + resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} + engines: {node: '>=6'} + dev: true + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + /ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + dev: false + + /ansi-sequence-parser@1.1.0: + resolution: {integrity: sha512-lEm8mt52to2fT8GhciPCGeCXACSz2UwIN4X2e2LJSnZ5uAbn2/dsYdOmUXq0AtWS5cpAupysIneExOgH0Vd2TQ==} + dev: true + + /ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + dependencies: + color-convert: 1.9.3 + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + + /ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + dev: false + + /any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + dev: false + + /anymatch@2.0.0(supports-color@6.1.0): + resolution: {integrity: sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==} + requiresBuild: true + dependencies: + micromatch: 3.1.10(supports-color@6.1.0) + normalize-path: 2.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + /applescript@1.0.0: + resolution: {integrity: sha512-yvtNHdWvtbYEiIazXAdp/NY+BBb65/DAseqlNiJQjOx9DynuzOYDbVLBJvuc0ve0VL9x6B3OHF6eH52y9hCBtQ==} + dev: true + + /aproba@1.2.0: + resolution: {integrity: sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==} + dev: true + + /arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + dev: false + + /argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + dependencies: + sprintf-js: 1.0.3 + + /argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + /aria-query@5.1.3: + resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==} + dependencies: + deep-equal: 2.2.0 + dev: false + + /arr-diff@4.0.0: + resolution: {integrity: sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==} + engines: {node: '>=0.10.0'} + dev: true + + /arr-flatten@1.1.0: + resolution: {integrity: sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==} + engines: {node: '>=0.10.0'} + dev: true + + /arr-union@3.1.0: + resolution: {integrity: sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==} + engines: {node: '>=0.10.0'} + dev: true + + /array-buffer-byte-length@1.0.0: + resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} + dependencies: + call-bind: 1.0.2 + is-array-buffer: 3.0.2 + + /array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + + /array-flatten@2.1.2: + resolution: {integrity: sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==} + + /array-includes@3.1.6: + resolution: {integrity: sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + get-intrinsic: 1.2.0 + is-string: 1.0.7 + dev: false + + /array-union@1.0.2: + resolution: {integrity: sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==} + engines: {node: '>=0.10.0'} + dependencies: + array-uniq: 1.0.3 + dev: true + + /array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + /array-uniq@1.0.3: + resolution: {integrity: sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==} + engines: {node: '>=0.10.0'} + dev: true + + /array-unique@0.3.2: + resolution: {integrity: sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==} + engines: {node: '>=0.10.0'} + dev: true + + /array.prototype.flat@1.3.1: + resolution: {integrity: sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + es-shim-unscopables: 1.0.0 + dev: false + + /array.prototype.flatmap@1.3.1: + resolution: {integrity: sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + es-shim-unscopables: 1.0.0 + dev: false + + /array.prototype.reduce@1.0.5: + resolution: {integrity: sha512-kDdugMl7id9COE8R7MHF5jWk7Dqt/fs4Pv+JXoICnYwqpjjjbUurz6w5fT5IG6brLdJhv6/VoHB0H7oyIBXd+Q==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + es-array-method-boxes-properly: 1.0.0 + is-string: 1.0.7 + + /array.prototype.tosorted@1.1.1: + resolution: {integrity: sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + es-shim-unscopables: 1.0.0 + get-intrinsic: 1.2.0 + dev: false + + /asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + dev: false + + /asn1.js@5.4.1: + resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} + dependencies: + bn.js: 4.12.0 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + safer-buffer: 2.1.2 + dev: true + + /asn1@0.2.6: + resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + dependencies: + safer-buffer: 2.1.2 + dev: true + + /assert-plus@1.0.0: + resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} + engines: {node: '>=0.8'} + dev: true + + /assert@1.5.0: + resolution: {integrity: sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==} + dependencies: + object-assign: 4.1.1 + util: 0.10.3 + dev: true + + /assign-symbols@1.0.0: + resolution: {integrity: sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==} + engines: {node: '>=0.10.0'} + dev: true + + /ast-types-flow@0.0.7: + resolution: {integrity: sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==} + dev: false + + /async-each@1.0.6: + resolution: {integrity: sha512-c646jH1avxr+aVpndVMeAfYw7wAa6idufrlN3LPA4PmKS0QEGp6PIC9nwz0WQkkvBGAMEki3pFdtxaF39J9vvg==} + requiresBuild: true + dev: true + + /async-limiter@1.0.1: + resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==} + dev: true + + /async@2.6.4: + resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==} + dependencies: + lodash: 4.17.21 + dev: true + + /async@3.2.4: + resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==} + dev: false + + /asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + /at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + dev: false + + /atob@2.1.2: + resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==} + engines: {node: '>= 4.5.0'} + hasBin: true + dev: true + + /autocomplete.js@0.36.0: + resolution: {integrity: sha512-jEwUXnVMeCHHutUt10i/8ZiRaCb0Wo+ZyKxeGsYwBDtw6EJHqEeDrq4UwZRD8YBSvp3g6klP678il2eeiVXN2Q==} + dependencies: + immediate: 3.3.0 + dev: true + + /autoprefixer@10.4.14(postcss@8.4.21): + resolution: {integrity: sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + dependencies: + browserslist: 4.21.5 + caniuse-lite: 1.0.30001472 + fraction.js: 4.2.0 + normalize-range: 0.1.2 + picocolors: 1.0.0 + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: false + + /autoprefixer@9.8.8: + resolution: {integrity: sha512-eM9d/swFopRt5gdJ7jrpCwgvEMIayITpojhkkSMRsFHYuH5bkSQ4p/9qTEHtmNudUZh22Tehu7I6CxAW0IXTKA==} + hasBin: true + dependencies: + browserslist: 4.21.5 + caniuse-lite: 1.0.30001472 + normalize-range: 0.1.2 + num2fraction: 1.2.2 + picocolors: 0.2.1 + postcss: 7.0.39 + postcss-value-parser: 4.2.0 + dev: true + + /available-typed-arrays@1.0.5: + resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} + engines: {node: '>= 0.4'} + + /aws-sign2@0.7.0: + resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} + dev: true + + /aws4@1.12.0: + resolution: {integrity: sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==} + dev: true + + /axe-core@4.6.3: + resolution: {integrity: sha512-/BQzOX780JhsxDnPpH4ZiyrJAzcd8AfzFPkv+89veFSr1rcMjuq2JDCwypKaPeB6ljHp9KjXhPpjgCvQlWYuqg==} + engines: {node: '>=4'} + dev: false + + /axobject-query@3.1.1: + resolution: {integrity: sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg==} + dependencies: + deep-equal: 2.2.0 + dev: false + + /babel-jest@27.5.1(@babel/core@7.21.3): + resolution: {integrity: sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + dependencies: + '@babel/core': 7.21.3 + '@jest/transform': 27.5.1 + '@jest/types': 27.5.1 + '@types/babel__core': 7.20.0 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 27.5.1(@babel/core@7.21.3) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /babel-loader@8.3.0(@babel/core@7.21.3)(webpack@4.46.0): + resolution: {integrity: sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==} + engines: {node: '>= 8.9'} + peerDependencies: + '@babel/core': ^7.0.0 + webpack: '>=2' + dependencies: + '@babel/core': 7.21.3 + find-cache-dir: 3.3.2 + loader-utils: 2.0.4 + make-dir: 3.1.0 + schema-utils: 2.7.1 + webpack: 4.46.0 + dev: true + + /babel-loader@8.3.0(@babel/core@7.21.3)(webpack@5.76.3): + resolution: {integrity: sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==} + engines: {node: '>= 8.9'} + peerDependencies: + '@babel/core': ^7.0.0 + webpack: '>=2' + dependencies: + '@babel/core': 7.21.3 + find-cache-dir: 3.3.2 + loader-utils: 2.0.4 + make-dir: 3.1.0 + schema-utils: 2.7.1 + webpack: 5.76.3(@swc/core@1.3.42) + dev: false + + /babel-plugin-dynamic-import-node@2.3.3: + resolution: {integrity: sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==} + dependencies: + object.assign: 4.1.4 + dev: true + + /babel-plugin-istanbul@6.1.1: + resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} + engines: {node: '>=8'} + dependencies: + '@babel/helper-plugin-utils': 7.20.2 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 5.2.1 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /babel-plugin-jest-hoist@27.5.1: + resolution: {integrity: sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + '@babel/template': 7.20.7 + '@babel/types': 7.21.3 + '@types/babel__core': 7.20.0 + '@types/babel__traverse': 7.18.3 + dev: false + + /babel-plugin-macros@3.1.0: + resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} + engines: {node: '>=10', npm: '>=6'} + dependencies: + '@babel/runtime': 7.21.0 + cosmiconfig: 7.1.0 + resolve: 1.22.1 + dev: false + + /babel-plugin-named-asset-import@0.3.8(@babel/core@7.21.3): + resolution: {integrity: sha512-WXiAc++qo7XcJ1ZnTYGtLxmBCVbddAml3CEXgWaBzNzLNoxtQ8AiGEFDMOhot9XjTCQbvP5E77Fj9Gk924f00Q==} + peerDependencies: + '@babel/core': ^7.1.0 + dependencies: + '@babel/core': 7.21.3 + dev: false + + /babel-plugin-polyfill-corejs2@0.3.3(@babel/core@7.21.3): + resolution: {integrity: sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.21.0 + '@babel/core': 7.21.3 + '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.21.3) + semver: 6.3.0 + transitivePeerDependencies: + - supports-color + + /babel-plugin-polyfill-corejs3@0.6.0(@babel/core@7.21.3): + resolution: {integrity: sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.21.3) + core-js-compat: 3.29.1 + transitivePeerDependencies: + - supports-color + + /babel-plugin-polyfill-regenerator@0.4.1(@babel/core@7.21.3): + resolution: {integrity: sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.3 + '@babel/helper-define-polyfill-provider': 0.3.3(@babel/core@7.21.3) + transitivePeerDependencies: + - supports-color + + /babel-plugin-transform-react-remove-prop-types@0.4.24: + resolution: {integrity: sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA==} + dev: false + + /babel-preset-current-node-syntax@1.0.1(@babel/core@7.21.3): + resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.21.3 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.21.3) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.21.3) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.21.3) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.21.3) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.21.3) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.21.3) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.21.3) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.21.3) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.21.3) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.21.3) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.21.3) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.21.3) + dev: false + + /babel-preset-jest@27.5.1(@babel/core@7.21.3): + resolution: {integrity: sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.21.3 + babel-plugin-jest-hoist: 27.5.1 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.21.3) + dev: false + + /babel-preset-react-app@10.0.1: + resolution: {integrity: sha512-b0D9IZ1WhhCWkrTXyFuIIgqGzSkRIH5D5AmB0bXbzYAB1OBAwHcUeyWW2LorutLWF5btNo/N7r/cIdmvvKJlYg==} + dependencies: + '@babel/core': 7.21.3 + '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.21.3) + '@babel/plugin-proposal-decorators': 7.21.0(@babel/core@7.21.3) + '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.21.3) + '@babel/plugin-proposal-numeric-separator': 7.18.6(@babel/core@7.21.3) + '@babel/plugin-proposal-optional-chaining': 7.21.0(@babel/core@7.21.3) + '@babel/plugin-proposal-private-methods': 7.18.6(@babel/core@7.21.3) + '@babel/plugin-proposal-private-property-in-object': 7.21.0(@babel/core@7.21.3) + '@babel/plugin-transform-flow-strip-types': 7.21.0(@babel/core@7.21.3) + '@babel/plugin-transform-react-display-name': 7.18.6(@babel/core@7.21.3) + '@babel/plugin-transform-runtime': 7.21.0(@babel/core@7.21.3) + '@babel/preset-env': 7.20.2(@babel/core@7.21.3) + '@babel/preset-react': 7.18.6(@babel/core@7.21.3) + '@babel/preset-typescript': 7.21.0(@babel/core@7.21.3) + '@babel/runtime': 7.21.0 + babel-plugin-macros: 3.1.0 + babel-plugin-transform-react-remove-prop-types: 0.4.24 + transitivePeerDependencies: + - supports-color + dev: false + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + /base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + dev: true + + /base64id@2.0.0: + resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} + engines: {node: ^4.5.0 || >= 5.9} + dev: true + + /base@0.11.2: + resolution: {integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==} + engines: {node: '>=0.10.0'} + dependencies: + cache-base: 1.0.1 + class-utils: 0.3.6 + component-emitter: 1.3.0 + define-property: 1.0.0 + isobject: 3.0.1 + mixin-deep: 1.3.2 + pascalcase: 0.1.1 + dev: true + + /batch@0.6.1: + resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==} + + /bcrypt-pbkdf@1.0.2: + resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + dependencies: + tweetnacl: 0.14.5 + dev: true + + /bfj@7.0.2: + resolution: {integrity: sha512-+e/UqUzwmzJamNF50tBV6tZPTORow7gQ96iFow+8b562OdMpEK0BcJEq2OSPEDmAbSMBQ7PKZ87ubFkgxpYWgw==} + engines: {node: '>= 8.0.0'} + dependencies: + bluebird: 3.7.2 + check-types: 11.2.2 + hoopy: 0.1.4 + tryer: 1.0.1 + dev: false + + /big.js@3.2.0: + resolution: {integrity: sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==} + dev: true + + /big.js@5.2.2: + resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} + + /binary-extensions@1.13.1: + resolution: {integrity: sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==} + engines: {node: '>=0.10.0'} + requiresBuild: true + dev: true + + /binary-extensions@2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + + /bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + requiresBuild: true + dependencies: + file-uri-to-path: 1.0.0 + dev: true + optional: true + + /bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + + /bn.js@4.12.0: + resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==} + dev: true + + /bn.js@5.2.1: + resolution: {integrity: sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==} + dev: true + + /body-parser@1.20.1(supports-color@6.1.0): + resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9(supports-color@6.1.0) + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.11.0 + raw-body: 2.5.1 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + /body-parser@1.20.2: + resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9(supports-color@6.1.0) + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.11.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /bonjour-service@1.1.1: + resolution: {integrity: sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg==} + dependencies: + array-flatten: 2.1.2 + dns-equal: 1.0.0 + fast-deep-equal: 3.1.3 + multicast-dns: 7.2.5 + dev: false + + /bonjour@3.5.0: + resolution: {integrity: sha512-RaVTblr+OnEli0r/ud8InrU7D+G0y6aJhlxaLa6Pwty4+xoxboF1BsUI45tujvRpbj9dQVoglChqonGAsjEBYg==} + dependencies: + array-flatten: 2.1.2 + deep-equal: 1.1.1 + dns-equal: 1.0.0 + dns-txt: 2.0.2 + multicast-dns: 6.2.3 + multicast-dns-service-types: 1.1.0 + dev: true + + /boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + /boxen@4.2.0: + resolution: {integrity: sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==} + engines: {node: '>=8'} + dependencies: + ansi-align: 3.0.1 + camelcase: 5.3.1 + chalk: 3.0.0 + cli-boxes: 2.2.1 + string-width: 4.2.3 + term-size: 2.2.1 + type-fest: 0.8.1 + widest-line: 3.1.0 + dev: true + + /brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + /brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + + /braces@2.3.2(supports-color@6.1.0): + resolution: {integrity: sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==} + engines: {node: '>=0.10.0'} + dependencies: + arr-flatten: 1.1.0 + array-unique: 0.3.2 + extend-shallow: 2.0.1 + fill-range: 4.0.0 + isobject: 3.0.1 + repeat-element: 1.1.4 + snapdragon: 0.8.2(supports-color@6.1.0) + snapdragon-node: 2.1.1 + split-string: 3.1.0 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + dev: true + + /braces@3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + + /brorand@1.1.0: + resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==} + dev: true + + /browser-process-hrtime@1.0.0: + resolution: {integrity: sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==} + dev: false + + /browserify-aes@1.2.0: + resolution: {integrity: sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==} + dependencies: + buffer-xor: 1.0.3 + cipher-base: 1.0.4 + create-hash: 1.2.0 + evp_bytestokey: 1.0.3 + inherits: 2.0.4 + safe-buffer: 5.2.1 + dev: true + + /browserify-cipher@1.0.1: + resolution: {integrity: sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==} + dependencies: + browserify-aes: 1.2.0 + browserify-des: 1.0.2 + evp_bytestokey: 1.0.3 + dev: true + + /browserify-des@1.0.2: + resolution: {integrity: sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==} + dependencies: + cipher-base: 1.0.4 + des.js: 1.0.1 + inherits: 2.0.4 + safe-buffer: 5.2.1 + dev: true + + /browserify-rsa@4.1.0: + resolution: {integrity: sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==} + dependencies: + bn.js: 5.2.1 + randombytes: 2.1.0 + dev: true + + /browserify-sign@4.2.1: + resolution: {integrity: sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==} + dependencies: + bn.js: 5.2.1 + browserify-rsa: 4.1.0 + create-hash: 1.2.0 + create-hmac: 1.1.7 + elliptic: 6.5.4 + inherits: 2.0.4 + parse-asn1: 5.1.6 + readable-stream: 3.6.2 + safe-buffer: 5.2.1 + dev: true + + /browserify-zlib@0.2.0: + resolution: {integrity: sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==} + dependencies: + pako: 1.0.11 + dev: true + + /browserslist@4.21.5: + resolution: {integrity: sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + dependencies: + caniuse-lite: 1.0.30001472 + electron-to-chromium: 1.4.342 + node-releases: 2.0.10 + update-browserslist-db: 1.0.10(browserslist@4.21.5) + + /bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + dependencies: + node-int64: 0.4.0 + dev: false + + /buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + /buffer-indexof@1.1.1: + resolution: {integrity: sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==} + dev: true + + /buffer-json@2.0.0: + resolution: {integrity: sha512-+jjPFVqyfF1esi9fvfUs3NqM0pH1ziZ36VP4hmA/y/Ssfo/5w5xHKfTw9BwQjoJ1w/oVtpLomqwUHKdefGyuHw==} + dev: true + + /buffer-xor@1.0.3: + resolution: {integrity: sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==} + dev: true + + /buffer@4.9.2: + resolution: {integrity: sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + isarray: 1.0.0 + dev: true + + /builtin-modules@3.3.0: + resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} + engines: {node: '>=6'} + + /builtin-status-codes@3.0.0: + resolution: {integrity: sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==} + dev: true + + /bundle-require@2.1.8(esbuild@0.14.7): + resolution: {integrity: sha512-oOEg3A0hy/YzvNWNowtKD0pmhZKseOFweCbgyMqTIih4gRY1nJWsvrOCT27L9NbIyL5jMjTFrAUpGxxpW68Puw==} + peerDependencies: + esbuild: '>=0.13' + dependencies: + esbuild: 0.14.7 + dev: true + + /bytes@3.0.0: + resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} + engines: {node: '>= 0.8'} + + /bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + /cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + dev: true + + /cacache@12.0.4: + resolution: {integrity: sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==} + dependencies: + bluebird: 3.7.2 + chownr: 1.1.4 + figgy-pudding: 3.5.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + infer-owner: 1.0.4 + lru-cache: 5.1.1 + mississippi: 3.0.0 + mkdirp: 0.5.6 + move-concurrently: 1.0.1 + promise-inflight: 1.0.1(bluebird@3.7.2) + rimraf: 2.7.1 + ssri: 6.0.2 + unique-filename: 1.1.1 + y18n: 4.0.3 + dev: true + + /cache-base@1.0.1: + resolution: {integrity: sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==} + engines: {node: '>=0.10.0'} + dependencies: + collection-visit: 1.0.0 + component-emitter: 1.3.0 + get-value: 2.0.6 + has-value: 1.0.0 + isobject: 3.0.1 + set-value: 2.0.1 + to-object-path: 0.3.0 + union-value: 1.0.1 + unset-value: 1.0.0 + dev: true + + /cache-loader@3.0.1(webpack@4.46.0): + resolution: {integrity: sha512-HzJIvGiGqYsFUrMjAJNDbVZoG7qQA+vy9AIoKs7s9DscNfki0I589mf2w6/tW+kkFH3zyiknoWV5Jdynu6b/zw==} + engines: {node: '>= 6.9.0'} + peerDependencies: + webpack: ^4.0.0 + dependencies: + buffer-json: 2.0.0 + find-cache-dir: 2.1.0 + loader-utils: 1.4.2 + mkdirp: 0.5.6 + neo-async: 2.6.2 + schema-utils: 1.0.0 + webpack: 4.46.0 + dev: true + + /cacheable-request@6.1.0: + resolution: {integrity: sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==} + engines: {node: '>=8'} + dependencies: + clone-response: 1.0.3 + get-stream: 5.2.0 + http-cache-semantics: 4.1.1 + keyv: 3.1.0 + lowercase-keys: 2.0.0 + normalize-url: 4.5.1 + responselike: 1.0.2 + dev: true + + /call-bind@1.0.2: + resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} + dependencies: + function-bind: 1.1.1 + get-intrinsic: 1.2.0 + + /call-me-maybe@1.0.2: + resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==} + dev: true + + /caller-callsite@2.0.0: + resolution: {integrity: sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==} + engines: {node: '>=4'} + dependencies: + callsites: 2.0.0 + dev: true + + /caller-path@2.0.0: + resolution: {integrity: sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==} + engines: {node: '>=4'} + dependencies: + caller-callsite: 2.0.0 + dev: true + + /callsites@2.0.0: + resolution: {integrity: sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==} + engines: {node: '>=4'} + dev: true + + /callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + /camel-case@3.0.0: + resolution: {integrity: sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w==} + dependencies: + no-case: 2.3.2 + upper-case: 1.1.3 + dev: true + + /camel-case@4.1.2: + resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} + dependencies: + pascal-case: 3.1.2 + tslib: 2.5.0 + dev: false + + /camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + dev: false + + /camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + /camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + /caniuse-api@3.0.0: + resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} + dependencies: + browserslist: 4.21.5 + caniuse-lite: 1.0.30001472 + lodash.memoize: 4.1.2 + lodash.uniq: 4.5.0 + + /caniuse-lite@1.0.30001472: + resolution: {integrity: sha512-xWC/0+hHHQgj3/vrKYY0AAzeIUgr7L9wlELIcAvZdDUHlhL/kNxMdnQLOSOQfP8R51ZzPhmHdyMkI0MMpmxCfg==} + + /case-sensitive-paths-webpack-plugin@2.4.0: + resolution: {integrity: sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==} + engines: {node: '>=4'} + dev: false + + /caseless@0.12.0: + resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} + dev: true + + /chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + + /chalk@3.0.0: + resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} + engines: {node: '>=8'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + /char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + dev: false + + /char-regex@2.0.1: + resolution: {integrity: sha512-oSvEeo6ZUD7NepqAat3RqoucZ5SeqLJgOvVIwkafu6IP3V0pO38s/ypdVUmDDK6qIIHNlYHJAKX9E7R7HoKElw==} + engines: {node: '>=12.20'} + dev: false + + /character-entities-legacy@1.1.4: + resolution: {integrity: sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==} + dev: true + + /character-entities@1.2.4: + resolution: {integrity: sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==} + dev: true + + /character-reference-invalid@1.1.4: + resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==} + dev: true + + /chartjs-adapter-luxon@1.3.1(luxon@3.3.0): + resolution: {integrity: sha512-yxHov3X8y+reIibl1o+j18xzrcdddCLqsXhriV2+aQ4hCR66IYFchlRXUvrJVoxglJ380pgytU7YWtoqdIgqhg==} + peerDependencies: + chart.js: '>=3.0.0' + luxon: '>=1.0.0' + peerDependenciesMeta: + chart.js: + optional: true + dependencies: + luxon: 3.3.0 + dev: true + + /chartjs-adapter-moment@1.0.1(moment@2.29.4): + resolution: {integrity: sha512-Uz+nTX/GxocuqXpGylxK19YG4R3OSVf8326D+HwSTsNw1LgzyIGRo+Qujwro1wy6X+soNSnfj5t2vZ+r6EaDmA==} + peerDependencies: + chart.js: '>=3.0.0' + moment: ^2.10.2 + peerDependenciesMeta: + chart.js: + optional: true + dependencies: + moment: 2.29.4 + dev: true + + /chartjs-test-utils@0.4.0(jasmine@3.99.0)(karma-jasmine@4.0.2)(karma@6.4.1): + resolution: {integrity: sha512-hT7weEZeWDVduSflHMpoNYW4arxVNp3+u7iZW91P6+zTYLHqgtv1gB/K0wiMqForXvw7IsDWuMF2iEvh3WT1mg==} + peerDependencies: + jasmine: ^3.6.4 + karma: ^6.1.1 + karma-jasmine: ^4.0.1 + dependencies: + jasmine: 3.99.0 + karma: 6.4.1 + karma-jasmine: 4.0.2(karma@6.4.1) + pixelmatch: 5.3.0 + dev: true + + /check-types@11.2.2: + resolution: {integrity: sha512-HBiYvXvn9Z70Z88XKjz3AEKd4HJhBXsa3j7xFnITAzoS8+q6eIGi8qDB8FKPBAjtuxjI/zFpwuiCb8oDtKOYrA==} + dev: false + + /chokidar@2.1.8(supports-color@6.1.0): + resolution: {integrity: sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==} + deprecated: Chokidar 2 does not receive security updates since 2019. Upgrade to chokidar 3 with 15x fewer dependencies + dependencies: + anymatch: 2.0.0(supports-color@6.1.0) + async-each: 1.0.6 + braces: 2.3.2(supports-color@6.1.0) + glob-parent: 3.1.0 + inherits: 2.0.4 + is-binary-path: 1.0.1 + is-glob: 4.0.3 + normalize-path: 3.0.0 + path-is-absolute: 1.0.1 + readdirp: 2.2.1(supports-color@6.1.0) + upath: 1.2.0 + optionalDependencies: + fsevents: 1.2.13 + transitivePeerDependencies: + - supports-color + dev: true + + /chokidar@3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + /chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + /chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + dev: true + + /chrome-trace-event@1.0.3: + resolution: {integrity: sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==} + engines: {node: '>=6.0'} + + /ci-info@2.0.0: + resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==} + dev: true + + /ci-info@3.8.0: + resolution: {integrity: sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==} + engines: {node: '>=8'} + + /cipher-base@1.0.4: + resolution: {integrity: sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==} + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + dev: true + + /cjs-module-lexer@1.2.2: + resolution: {integrity: sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==} + dev: false + + /class-utils@0.3.6: + resolution: {integrity: sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==} + engines: {node: '>=0.10.0'} + dependencies: + arr-union: 3.1.0 + define-property: 0.2.5 + isobject: 3.0.1 + static-extend: 0.1.2 + dev: true + + /clean-css@4.2.4: + resolution: {integrity: sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==} + engines: {node: '>= 4.0'} + dependencies: + source-map: 0.6.1 + dev: true + + /clean-css@5.3.2: + resolution: {integrity: sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww==} + engines: {node: '>= 10.0'} + dependencies: + source-map: 0.6.1 + dev: false + + /cli-boxes@2.2.1: + resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==} + engines: {node: '>=6'} + dev: true + + /cliui@5.0.0: + resolution: {integrity: sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==} + dependencies: + string-width: 3.1.0 + strip-ansi: 5.2.0 + wrap-ansi: 5.1.0 + dev: true + + /cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + /cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + dev: true + + /clone-response@1.0.3: + resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} + dependencies: + mimic-response: 1.0.1 + dev: true + + /co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + dev: false + + /coa@2.0.2: + resolution: {integrity: sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==} + engines: {node: '>= 4.0'} + dependencies: + '@types/q': 1.5.5 + chalk: 2.4.2 + q: 1.5.1 + + /collect-v8-coverage@1.0.1: + resolution: {integrity: sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==} + dev: false + + /collection-visit@1.0.0: + resolution: {integrity: sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==} + engines: {node: '>=0.10.0'} + dependencies: + map-visit: 1.0.0 + object-visit: 1.0.1 + dev: true + + /color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + dependencies: + color-name: 1.1.3 + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + + /color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + /color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + dev: true + + /color@3.2.1: + resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} + dependencies: + color-convert: 1.9.3 + color-string: 1.9.1 + dev: true + + /colord@2.9.3: + resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + + /colorette@2.0.19: + resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==} + dev: false + + /colors@1.4.0: + resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==} + engines: {node: '>=0.1.90'} + dev: true + + /combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + + /commander@2.17.1: + resolution: {integrity: sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==} + dev: true + + /commander@2.19.0: + resolution: {integrity: sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==} + dev: true + + /commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + /commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + dev: false + + /commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + /commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + dev: false + + /common-path-prefix@3.0.0: + resolution: {integrity: sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==} + dev: false + + /common-tags@1.8.2: + resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} + engines: {node: '>=4.0.0'} + dev: false + + /commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + + /component-emitter@1.3.0: + resolution: {integrity: sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==} + dev: true + + /compressible@2.0.18: + resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + + /compression@1.7.4(supports-color@6.1.0): + resolution: {integrity: sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==} + engines: {node: '>= 0.8.0'} + dependencies: + accepts: 1.3.8 + bytes: 3.0.0 + compressible: 2.0.18 + debug: 2.6.9(supports-color@6.1.0) + on-headers: 1.0.2 + safe-buffer: 5.1.2 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + /concat-stream@1.6.2: + resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} + engines: {'0': node >= 0.8} + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 2.3.8 + typedarray: 0.0.6 + dev: true + + /concurrently@7.6.0: + resolution: {integrity: sha512-BKtRgvcJGeZ4XttiDiNcFiRlxoAeZOseqUvyYRUp/Vtd+9p1ULmeoSqGsDA+2ivdeDFpqrJvGvmI+StKfKl5hw==} + engines: {node: ^12.20.0 || ^14.13.0 || >=16.0.0} + hasBin: true + dependencies: + chalk: 4.1.2 + date-fns: 2.29.3 + lodash: 4.17.21 + rxjs: 7.8.0 + shell-quote: 1.8.0 + spawn-command: 0.0.2-1 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 17.7.1 + dev: true + + /configstore@5.0.1: + resolution: {integrity: sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==} + engines: {node: '>=8'} + dependencies: + dot-prop: 5.3.0 + graceful-fs: 4.2.11 + make-dir: 3.1.0 + unique-string: 2.0.0 + write-file-atomic: 3.0.3 + xdg-basedir: 4.0.0 + dev: true + + /confusing-browser-globals@1.0.11: + resolution: {integrity: sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==} + dev: false + + /connect-history-api-fallback@1.6.0: + resolution: {integrity: sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==} + engines: {node: '>=0.8'} + dev: true + + /connect-history-api-fallback@2.0.0: + resolution: {integrity: sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==} + engines: {node: '>=0.8'} + dev: false + + /connect@3.7.0: + resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==} + engines: {node: '>= 0.10.0'} + dependencies: + debug: 2.6.9(supports-color@6.1.0) + finalhandler: 1.1.2 + parseurl: 1.3.3 + utils-merge: 1.0.1 + transitivePeerDependencies: + - supports-color + dev: true + + /consola@2.15.3: + resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==} + dev: true + + /console-browserify@1.2.0: + resolution: {integrity: sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==} + dev: true + + /consolidate@0.15.1: + resolution: {integrity: sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==} + engines: {node: '>= 0.10.0'} + peerDependencies: + arc-templates: ^0.5.3 + atpl: '>=0.7.6' + babel-core: ^6.26.3 + bracket-template: ^1.1.5 + coffee-script: ^1.12.7 + dot: ^1.1.3 + dust: ^0.3.0 + dustjs-helpers: ^1.7.4 + dustjs-linkedin: ^2.7.5 + eco: ^1.1.0-rc-3 + ect: ^0.5.9 + ejs: ^3.1.5 + haml-coffee: ^1.14.1 + hamlet: ^0.3.3 + hamljs: ^0.6.2 + handlebars: ^4.7.6 + hogan.js: ^3.0.2 + htmling: ^0.0.8 + jade: ^1.11.0 + jazz: ^0.0.18 + jqtpl: ~1.1.0 + just: ^0.1.8 + liquid-node: ^3.0.1 + liquor: ^0.0.5 + lodash: ^4.17.20 + marko: ^3.14.4 + mote: ^0.2.0 + mustache: ^3.0.0 + nunjucks: ^3.2.2 + plates: ~0.4.11 + pug: ^3.0.0 + qejs: ^3.0.5 + ractive: ^1.3.12 + razor-tmpl: ^1.3.1 + react: ^16.13.1 + react-dom: ^16.13.1 + slm: ^2.0.0 + squirrelly: ^5.1.0 + swig: ^1.4.2 + swig-templates: ^2.0.3 + teacup: ^2.0.0 + templayed: '>=0.2.3' + then-jade: '*' + then-pug: '*' + tinyliquid: ^0.2.34 + toffee: ^0.3.6 + twig: ^1.15.2 + twing: ^5.0.2 + underscore: ^1.11.0 + vash: ^0.13.0 + velocityjs: ^2.0.1 + walrus: ^0.10.1 + whiskers: ^0.4.0 + peerDependenciesMeta: + arc-templates: + optional: true + atpl: + optional: true + babel-core: + optional: true + bracket-template: + optional: true + coffee-script: + optional: true + dot: + optional: true + dust: + optional: true + dustjs-helpers: + optional: true + dustjs-linkedin: + optional: true + eco: + optional: true + ect: + optional: true + ejs: + optional: true + haml-coffee: + optional: true + hamlet: + optional: true + hamljs: + optional: true + handlebars: + optional: true + hogan.js: + optional: true + htmling: + optional: true + jade: + optional: true + jazz: + optional: true + jqtpl: + optional: true + just: + optional: true + liquid-node: + optional: true + liquor: + optional: true + lodash: + optional: true + marko: + optional: true + mote: + optional: true + mustache: + optional: true + nunjucks: + optional: true + plates: + optional: true + pug: + optional: true + qejs: + optional: true + ractive: + optional: true + razor-tmpl: + optional: true + react: + optional: true + react-dom: + optional: true + slm: + optional: true + squirrelly: + optional: true + swig: + optional: true + swig-templates: + optional: true + teacup: + optional: true + templayed: + optional: true + then-jade: + optional: true + then-pug: + optional: true + tinyliquid: + optional: true + toffee: + optional: true + twig: + optional: true + twing: + optional: true + underscore: + optional: true + vash: + optional: true + velocityjs: + optional: true + walrus: + optional: true + whiskers: + optional: true + dependencies: + bluebird: 3.7.2 + dev: true + + /constants-browserify@1.0.0: + resolution: {integrity: sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==} + dev: true + + /content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + dependencies: + safe-buffer: 5.2.1 + + /content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + /convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + + /cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + + /cookie@0.4.2: + resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} + engines: {node: '>= 0.6'} + dev: true + + /cookie@0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + engines: {node: '>= 0.6'} + + /copy-concurrently@1.0.5: + resolution: {integrity: sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==} + dependencies: + aproba: 1.2.0 + fs-write-stream-atomic: 1.0.10 + iferr: 0.1.5 + mkdirp: 0.5.6 + rimraf: 2.7.1 + run-queue: 1.0.3 + dev: true + + /copy-descriptor@0.1.1: + resolution: {integrity: sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==} + engines: {node: '>=0.10.0'} + dev: true + + /copy-webpack-plugin@5.1.2(webpack@4.46.0): + resolution: {integrity: sha512-Uh7crJAco3AjBvgAy9Z75CjK8IG+gxaErro71THQ+vv/bl4HaQcpkexAY8KVW/T6D2W2IRr+couF/knIRkZMIQ==} + engines: {node: '>= 6.9.0'} + peerDependencies: + webpack: ^4.0.0 || ^5.0.0 + dependencies: + cacache: 12.0.4 + find-cache-dir: 2.1.0 + glob-parent: 3.1.0 + globby: 7.1.1 + is-glob: 4.0.3 + loader-utils: 1.4.2 + minimatch: 3.1.2 + normalize-path: 3.0.0 + p-limit: 2.3.0 + schema-utils: 1.0.0 + serialize-javascript: 4.0.0 + webpack: 4.46.0 + webpack-log: 2.0.0 + dev: true + + /core-js-compat@3.29.1: + resolution: {integrity: sha512-QmchCua884D8wWskMX8tW5ydINzd8oSJVx38lx/pVkFGqztxt73GYre3pm/hyYq8bPf+MW5In4I/uRShFDsbrA==} + dependencies: + browserslist: 4.21.5 + + /core-js-pure@3.29.1: + resolution: {integrity: sha512-4En6zYVi0i0XlXHVz/bi6l1XDjCqkKRq765NXuX+SnaIatlE96Odt5lMLjdxUiNI1v9OXI5DSLWYPlmTfkTktg==} + requiresBuild: true + dev: false + + /core-js@3.29.1: + resolution: {integrity: sha512-+jwgnhg6cQxKYIIjGtAHq2nwUOolo9eoFZ4sHfUH09BLXBgxnH4gA0zEd+t+BO2cNB8idaBtZFcFTRjQJRJmAw==} + requiresBuild: true + + /core-util-is@1.0.2: + resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} + dev: true + + /core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + /cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + dev: true + + /cosmiconfig@5.2.1: + resolution: {integrity: sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==} + engines: {node: '>=4'} + dependencies: + import-fresh: 2.0.0 + is-directory: 0.3.1 + js-yaml: 3.14.1 + parse-json: 4.0.0 + dev: true + + /cosmiconfig@6.0.0: + resolution: {integrity: sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==} + engines: {node: '>=8'} + dependencies: + '@types/parse-json': 4.0.0 + import-fresh: 3.3.0 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.2 + dev: false + + /cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + dependencies: + '@types/parse-json': 4.0.0 + import-fresh: 3.3.0 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.2 + dev: false + + /coveralls@3.1.1: + resolution: {integrity: sha512-+dxnG2NHncSD1NrqbSM3dn/lE57O6Qf/koe9+I7c+wzkqRmEvcp0kgJdxKInzYzkICKkFMZsX3Vct3++tsF9ww==} + engines: {node: '>=6'} + hasBin: true + dependencies: + js-yaml: 3.14.1 + lcov-parse: 1.0.0 + log-driver: 1.2.7 + minimist: 1.2.8 + request: 2.88.2 + dev: true + + /create-ecdh@4.0.4: + resolution: {integrity: sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==} + dependencies: + bn.js: 4.12.0 + elliptic: 6.5.4 + dev: true + + /create-hash@1.2.0: + resolution: {integrity: sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==} + dependencies: + cipher-base: 1.0.4 + inherits: 2.0.4 + md5.js: 1.3.5 + ripemd160: 2.0.2 + sha.js: 2.4.11 + dev: true + + /create-hmac@1.1.7: + resolution: {integrity: sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==} + dependencies: + cipher-base: 1.0.4 + create-hash: 1.2.0 + inherits: 2.0.4 + ripemd160: 2.0.2 + safe-buffer: 5.2.1 + sha.js: 2.4.11 + dev: true + + /cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + dependencies: + cross-spawn: 7.0.3 + dev: true + + /cross-spawn@6.0.5: + resolution: {integrity: sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==} + engines: {node: '>=4.8'} + dependencies: + nice-try: 1.0.5 + path-key: 2.0.1 + semver: 5.7.1 + shebang-command: 1.2.0 + which: 1.3.1 + dev: true + + /cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + /cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + dev: false + + /crypto-browserify@3.12.0: + resolution: {integrity: sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==} + dependencies: + browserify-cipher: 1.0.1 + browserify-sign: 4.2.1 + create-ecdh: 4.0.4 + create-hash: 1.2.0 + create-hmac: 1.1.7 + diffie-hellman: 5.0.3 + inherits: 2.0.4 + pbkdf2: 3.1.2 + public-encrypt: 4.0.3 + randombytes: 2.1.0 + randomfill: 1.0.4 + dev: true + + /crypto-random-string@2.0.0: + resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} + engines: {node: '>=8'} + + /css-blank-pseudo@3.0.3(postcss@8.4.21): + resolution: {integrity: sha512-VS90XWtsHGqoM0t4KpH053c4ehxZ2E6HtGI7x68YFV0pTo/QmkV/YFA+NnlvK8guxZVNWGQhVNJGC39Q8XF4OQ==} + engines: {node: ^12 || ^14 || >=16} + hasBin: true + peerDependencies: + postcss: ^8.4 + dependencies: + postcss: 8.4.21 + postcss-selector-parser: 6.0.11 + dev: false + + /css-color-names@0.0.4: + resolution: {integrity: sha512-zj5D7X1U2h2zsXOAM8EyUREBnnts6H+Jm+d1M2DbiQQcUtnqgQsMrdo8JW9R80YFUmIdBZeMu5wvYM7hcgWP/Q==} + dev: true + + /css-declaration-sorter@4.0.1: + resolution: {integrity: sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA==} + engines: {node: '>4'} + dependencies: + postcss: 7.0.39 + timsort: 0.3.0 + dev: true + + /css-declaration-sorter@6.4.0(postcss@8.4.21): + resolution: {integrity: sha512-jDfsatwWMWN0MODAFuHszfjphEXfNw9JUAhmY4pLu3TyTU+ohUpsbVtbU+1MZn4a47D9kqh03i4eyOm+74+zew==} + engines: {node: ^10 || ^12 || >=14} + peerDependencies: + postcss: ^8.0.9 + dependencies: + postcss: 8.4.21 + dev: false + + /css-declaration-sorter@6.4.0(postcss@8.5.6): + resolution: {integrity: sha512-jDfsatwWMWN0MODAFuHszfjphEXfNw9JUAhmY4pLu3TyTU+ohUpsbVtbU+1MZn4a47D9kqh03i4eyOm+74+zew==} + engines: {node: ^10 || ^12 || >=14} + peerDependencies: + postcss: ^8.0.9 + dependencies: + postcss: 8.5.6 + dev: true + + /css-has-pseudo@3.0.4(postcss@8.4.21): + resolution: {integrity: sha512-Vse0xpR1K9MNlp2j5w1pgWIJtm1a8qS0JwS9goFYcImjlHEmywP9VUF05aGBXzGpDJF86QXk4L0ypBmwPhGArw==} + engines: {node: ^12 || ^14 || >=16} + hasBin: true + peerDependencies: + postcss: ^8.4 + dependencies: + postcss: 8.4.21 + postcss-selector-parser: 6.0.11 + dev: false + + /css-loader@2.1.1(webpack@4.46.0): + resolution: {integrity: sha512-OcKJU/lt232vl1P9EEDamhoO9iKY3tIjY5GU+XDLblAykTdgs6Ux9P1hTHve8nFKy5KPpOXOsVI/hIwi3841+w==} + engines: {node: '>= 6.9.0'} + peerDependencies: + webpack: ^4.0.0 + dependencies: + camelcase: 5.3.1 + icss-utils: 4.1.1 + loader-utils: 1.4.2 + normalize-path: 3.0.0 + postcss: 7.0.39 + postcss-modules-extract-imports: 2.0.0 + postcss-modules-local-by-default: 2.0.6 + postcss-modules-scope: 2.2.0 + postcss-modules-values: 2.0.0 + postcss-value-parser: 3.3.1 + schema-utils: 1.0.0 + webpack: 4.46.0 + dev: true + + /css-loader@6.7.3(webpack@5.76.3): + resolution: {integrity: sha512-qhOH1KlBMnZP8FzRO6YCH9UHXQhVMcEGLyNdb7Hv2cpcmJbW0YrddO+tG1ab5nT41KpHIYGsbeHqxB9xPu1pKQ==} + engines: {node: '>= 12.13.0'} + peerDependencies: + webpack: ^5.0.0 + dependencies: + icss-utils: 5.1.0(postcss@8.4.21) + postcss: 8.4.21 + postcss-modules-extract-imports: 3.0.0(postcss@8.4.21) + postcss-modules-local-by-default: 4.0.0(postcss@8.4.21) + postcss-modules-scope: 3.0.0(postcss@8.4.21) + postcss-modules-values: 4.0.0(postcss@8.4.21) + postcss-value-parser: 4.2.0 + semver: 7.3.8 + webpack: 5.76.3(@swc/core@1.3.42) + dev: false + + /css-minimizer-webpack-plugin@3.4.1(webpack@5.76.3): + resolution: {integrity: sha512-1u6D71zeIfgngN2XNRJefc/hY7Ybsxd74Jm4qngIXyUEk7fss3VUzuHxLAq/R8NAba4QU9OUSaMZlbpRc7bM4Q==} + engines: {node: '>= 12.13.0'} + peerDependencies: + '@parcel/css': '*' + clean-css: '*' + csso: '*' + esbuild: '*' + webpack: ^5.0.0 + peerDependenciesMeta: + '@parcel/css': + optional: true + clean-css: + optional: true + csso: + optional: true + esbuild: + optional: true + dependencies: + cssnano: 5.1.15(postcss@8.4.21) + jest-worker: 27.5.1 + postcss: 8.4.21 + schema-utils: 4.0.0 + serialize-javascript: 6.0.1 + source-map: 0.6.1 + webpack: 5.76.3(@swc/core@1.3.42) + dev: false + + /css-parse@2.0.0: + resolution: {integrity: sha512-UNIFik2RgSbiTwIW1IsFwXWn6vs+bYdq83LKTSOsx7NJR7WII9dxewkHLltfTLVppoUApHV0118a4RZRI9FLwA==} + dependencies: + css: 2.2.4 + dev: true + + /css-prefers-color-scheme@6.0.3(postcss@8.4.21): + resolution: {integrity: sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA==} + engines: {node: ^12 || ^14 || >=16} + hasBin: true + peerDependencies: + postcss: ^8.4 + dependencies: + postcss: 8.4.21 + dev: false + + /css-select-base-adapter@0.1.1: + resolution: {integrity: sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==} + + /css-select@2.1.0: + resolution: {integrity: sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==} + dependencies: + boolbase: 1.0.0 + css-what: 3.4.2 + domutils: 1.7.0 + nth-check: 1.0.2 + + /css-select@4.3.0: + resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 4.3.1 + domutils: 2.8.0 + nth-check: 2.1.1 + + /css-tree@1.0.0-alpha.37: + resolution: {integrity: sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==} + engines: {node: '>=8.0.0'} + dependencies: + mdn-data: 2.0.4 + source-map: 0.6.1 + + /css-tree@1.1.3: + resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==} + engines: {node: '>=8.0.0'} + dependencies: + mdn-data: 2.0.14 + source-map: 0.6.1 + + /css-what@3.4.2: + resolution: {integrity: sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==} + engines: {node: '>= 6'} + + /css-what@6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + + /css@2.2.4: + resolution: {integrity: sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==} + dependencies: + inherits: 2.0.4 + source-map: 0.6.1 + source-map-resolve: 0.5.3 + urix: 0.1.0 + dev: true + + /cssdb@7.5.2: + resolution: {integrity: sha512-Xpu7Bf5Vlw+G7ikA2Lg/lVCRTSY8D5M5qFUgGNFyS4pa8ufGLyCBxIX/3if3krHlF1SKSfVPI/YsAWLDVEbocw==} + dev: false + + /cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + /cssnano-preset-default@4.0.8: + resolution: {integrity: sha512-LdAyHuq+VRyeVREFmuxUZR1TXjQm8QQU/ktoo/x7bz+SdOge1YKc5eMN6pRW7YWBmyq59CqYba1dJ5cUukEjLQ==} + engines: {node: '>=6.9.0'} + dependencies: + css-declaration-sorter: 4.0.1 + cssnano-util-raw-cache: 4.0.1 + postcss: 7.0.39 + postcss-calc: 7.0.5 + postcss-colormin: 4.0.3 + postcss-convert-values: 4.0.1 + postcss-discard-comments: 4.0.2 + postcss-discard-duplicates: 4.0.2 + postcss-discard-empty: 4.0.1 + postcss-discard-overridden: 4.0.1 + postcss-merge-longhand: 4.0.11 + postcss-merge-rules: 4.0.3 + postcss-minify-font-values: 4.0.2 + postcss-minify-gradients: 4.0.2 + postcss-minify-params: 4.0.2 + postcss-minify-selectors: 4.0.2 + postcss-normalize-charset: 4.0.1 + postcss-normalize-display-values: 4.0.2 + postcss-normalize-positions: 4.0.2 + postcss-normalize-repeat-style: 4.0.2 + postcss-normalize-string: 4.0.2 + postcss-normalize-timing-functions: 4.0.2 + postcss-normalize-unicode: 4.0.1 + postcss-normalize-url: 4.0.1 + postcss-normalize-whitespace: 4.0.2 + postcss-ordered-values: 4.1.2 + postcss-reduce-initial: 4.0.3 + postcss-reduce-transforms: 4.0.2 + postcss-svgo: 4.0.3 + postcss-unique-selectors: 4.0.1 + dev: true + + /cssnano-preset-default@5.2.14(postcss@8.4.21): + resolution: {integrity: sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + css-declaration-sorter: 6.4.0(postcss@8.4.21) + cssnano-utils: 3.1.0(postcss@8.4.21) + postcss: 8.4.21 + postcss-calc: 8.2.4(postcss@8.4.21) + postcss-colormin: 5.3.1(postcss@8.4.21) + postcss-convert-values: 5.1.3(postcss@8.4.21) + postcss-discard-comments: 5.1.2(postcss@8.4.21) + postcss-discard-duplicates: 5.1.0(postcss@8.4.21) + postcss-discard-empty: 5.1.1(postcss@8.4.21) + postcss-discard-overridden: 5.1.0(postcss@8.4.21) + postcss-merge-longhand: 5.1.7(postcss@8.4.21) + postcss-merge-rules: 5.1.4(postcss@8.4.21) + postcss-minify-font-values: 5.1.0(postcss@8.4.21) + postcss-minify-gradients: 5.1.1(postcss@8.4.21) + postcss-minify-params: 5.1.4(postcss@8.4.21) + postcss-minify-selectors: 5.2.1(postcss@8.4.21) + postcss-normalize-charset: 5.1.0(postcss@8.4.21) + postcss-normalize-display-values: 5.1.0(postcss@8.4.21) + postcss-normalize-positions: 5.1.1(postcss@8.4.21) + postcss-normalize-repeat-style: 5.1.1(postcss@8.4.21) + postcss-normalize-string: 5.1.0(postcss@8.4.21) + postcss-normalize-timing-functions: 5.1.0(postcss@8.4.21) + postcss-normalize-unicode: 5.1.1(postcss@8.4.21) + postcss-normalize-url: 5.1.0(postcss@8.4.21) + postcss-normalize-whitespace: 5.1.1(postcss@8.4.21) + postcss-ordered-values: 5.1.3(postcss@8.4.21) + postcss-reduce-initial: 5.1.2(postcss@8.4.21) + postcss-reduce-transforms: 5.1.0(postcss@8.4.21) + postcss-svgo: 5.1.0(postcss@8.4.21) + postcss-unique-selectors: 5.1.1(postcss@8.4.21) + dev: false + + /cssnano-preset-default@5.2.14(postcss@8.5.6): + resolution: {integrity: sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + css-declaration-sorter: 6.4.0(postcss@8.5.6) + cssnano-utils: 3.1.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-calc: 8.2.4(postcss@8.5.6) + postcss-colormin: 5.3.1(postcss@8.5.6) + postcss-convert-values: 5.1.3(postcss@8.5.6) + postcss-discard-comments: 5.1.2(postcss@8.5.6) + postcss-discard-duplicates: 5.1.0(postcss@8.5.6) + postcss-discard-empty: 5.1.1(postcss@8.5.6) + postcss-discard-overridden: 5.1.0(postcss@8.5.6) + postcss-merge-longhand: 5.1.7(postcss@8.5.6) + postcss-merge-rules: 5.1.4(postcss@8.5.6) + postcss-minify-font-values: 5.1.0(postcss@8.5.6) + postcss-minify-gradients: 5.1.1(postcss@8.5.6) + postcss-minify-params: 5.1.4(postcss@8.5.6) + postcss-minify-selectors: 5.2.1(postcss@8.5.6) + postcss-normalize-charset: 5.1.0(postcss@8.5.6) + postcss-normalize-display-values: 5.1.0(postcss@8.5.6) + postcss-normalize-positions: 5.1.1(postcss@8.5.6) + postcss-normalize-repeat-style: 5.1.1(postcss@8.5.6) + postcss-normalize-string: 5.1.0(postcss@8.5.6) + postcss-normalize-timing-functions: 5.1.0(postcss@8.5.6) + postcss-normalize-unicode: 5.1.1(postcss@8.5.6) + postcss-normalize-url: 5.1.0(postcss@8.5.6) + postcss-normalize-whitespace: 5.1.1(postcss@8.5.6) + postcss-ordered-values: 5.1.3(postcss@8.5.6) + postcss-reduce-initial: 5.1.2(postcss@8.5.6) + postcss-reduce-transforms: 5.1.0(postcss@8.5.6) + postcss-svgo: 5.1.0(postcss@8.5.6) + postcss-unique-selectors: 5.1.1(postcss@8.5.6) + dev: true + + /cssnano-util-get-arguments@4.0.0: + resolution: {integrity: sha512-6RIcwmV3/cBMG8Aj5gucQRsJb4vv4I4rn6YjPbVWd5+Pn/fuG+YseGvXGk00XLkoZkaj31QOD7vMUpNPC4FIuw==} + engines: {node: '>=6.9.0'} + dev: true + + /cssnano-util-get-match@4.0.0: + resolution: {integrity: sha512-JPMZ1TSMRUPVIqEalIBNoBtAYbi8okvcFns4O0YIhcdGebeYZK7dMyHJiQ6GqNBA9kE0Hym4Aqym5rPdsV/4Cw==} + engines: {node: '>=6.9.0'} + dev: true + + /cssnano-util-raw-cache@4.0.1: + resolution: {integrity: sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA==} + engines: {node: '>=6.9.0'} + dependencies: + postcss: 7.0.39 + dev: true + + /cssnano-util-same-parent@4.0.1: + resolution: {integrity: sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==} + engines: {node: '>=6.9.0'} + dev: true + + /cssnano-utils@3.1.0(postcss@8.4.21): + resolution: {integrity: sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.21 + dev: false + + /cssnano-utils@3.1.0(postcss@8.5.6): + resolution: {integrity: sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.5.6 + dev: true + + /cssnano@4.1.11: + resolution: {integrity: sha512-6gZm2htn7xIPJOHY824ERgj8cNPgPxyCSnkXc4v7YvNW+TdVfzgngHcEhy/8D11kUWRUMbke+tC+AUcUsnMz2g==} + engines: {node: '>=6.9.0'} + dependencies: + cosmiconfig: 5.2.1 + cssnano-preset-default: 4.0.8 + is-resolvable: 1.1.0 + postcss: 7.0.39 + dev: true + + /cssnano@5.1.15(postcss@8.4.21): + resolution: {integrity: sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + cssnano-preset-default: 5.2.14(postcss@8.4.21) + lilconfig: 2.1.0 + postcss: 8.4.21 + yaml: 1.10.2 + dev: false + + /cssnano@5.1.15(postcss@8.5.6): + resolution: {integrity: sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + cssnano-preset-default: 5.2.14(postcss@8.5.6) + lilconfig: 2.1.0 + postcss: 8.5.6 + yaml: 1.10.2 + dev: true + + /csso@4.2.0: + resolution: {integrity: sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==} + engines: {node: '>=8.0.0'} + dependencies: + css-tree: 1.1.3 + + /cssom@0.3.8: + resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==} + dev: false + + /cssom@0.4.4: + resolution: {integrity: sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==} + dev: false + + /cssstyle@2.3.0: + resolution: {integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==} + engines: {node: '>=8'} + dependencies: + cssom: 0.3.8 + dev: false + + /csstype@3.1.1: + resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==} + + /custom-event@1.0.1: + resolution: {integrity: sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==} + dev: true + + /cyclist@1.0.1: + resolution: {integrity: sha512-NJGVKPS81XejHcLhaLJS7plab0fK3slPh11mESeeDq2W4ZI5kUKK/LRRdVDvjJseojbPB7ZwjnyOybg3Igea/A==} + dev: true + + /damerau-levenshtein@1.0.8: + resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + dev: false + + /dashdash@1.14.1: + resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} + engines: {node: '>=0.10'} + dependencies: + assert-plus: 1.0.0 + dev: true + + /data-urls@2.0.0: + resolution: {integrity: sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==} + engines: {node: '>=10'} + dependencies: + abab: 2.0.6 + whatwg-mimetype: 2.3.0 + whatwg-url: 8.7.0 + dev: false + + /date-fns@2.29.3: + resolution: {integrity: sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==} + engines: {node: '>=0.11'} + dev: true + + /date-format@4.0.14: + resolution: {integrity: sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==} + engines: {node: '>=4.0'} + dev: true + + /de-indent@1.0.2: + resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + dev: true + + /debounce@1.2.1: + resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} + dev: true + + /debug@2.6.9(supports-color@6.1.0): + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.0.0 + supports-color: 6.1.0 + + /debug@3.1.0: + resolution: {integrity: sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.0.0 + dev: true + + /debug@3.2.7(supports-color@6.1.0): + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + supports-color: 6.1.0 + + /debug@4.3.4(supports-color@6.1.0): + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + supports-color: 6.1.0 + + /debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + dev: false + + /decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + dev: true + + /decimal.js@10.4.3: + resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + dev: false + + /decode-uri-component@0.2.2: + resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} + engines: {node: '>=0.10'} + dev: true + + /decompress-response@3.3.0: + resolution: {integrity: sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==} + engines: {node: '>=4'} + dependencies: + mimic-response: 1.0.1 + dev: true + + /dedent@0.7.0: + resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} + dev: false + + /deep-equal@1.1.1: + resolution: {integrity: sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==} + dependencies: + is-arguments: 1.1.1 + is-date-object: 1.0.5 + is-regex: 1.1.4 + object-is: 1.1.5 + object-keys: 1.1.1 + regexp.prototype.flags: 1.4.3 + dev: true + + /deep-equal@2.2.0: + resolution: {integrity: sha512-RdpzE0Hv4lhowpIUKKMJfeH6C1pXdtT1/it80ubgWqwI3qpuxUBpC1S4hnHg+zjnuOoDkzUtUCEEkG+XG5l3Mw==} + dependencies: + call-bind: 1.0.2 + es-get-iterator: 1.1.3 + get-intrinsic: 1.2.0 + is-arguments: 1.1.1 + is-array-buffer: 3.0.2 + is-date-object: 1.0.5 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.2 + isarray: 2.0.5 + object-is: 1.1.5 + object-keys: 1.1.1 + object.assign: 4.1.4 + regexp.prototype.flags: 1.4.3 + side-channel: 1.0.4 + which-boxed-primitive: 1.0.2 + which-collection: 1.0.1 + which-typed-array: 1.1.9 + dev: false + + /deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + dev: true + + /deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + /deepmerge@1.5.2: + resolution: {integrity: sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ==} + engines: {node: '>=0.10.0'} + dev: true + + /deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + /default-gateway@4.2.0: + resolution: {integrity: sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA==} + engines: {node: '>=6'} + dependencies: + execa: 1.0.0 + ip-regex: 2.1.0 + dev: true + + /default-gateway@6.0.3: + resolution: {integrity: sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==} + engines: {node: '>= 10'} + dependencies: + execa: 5.1.1 + dev: false + + /defer-to-connect@1.1.3: + resolution: {integrity: sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==} + dev: true + + /define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + dev: false + + /define-properties@1.2.0: + resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==} + engines: {node: '>= 0.4'} + dependencies: + has-property-descriptors: 1.0.0 + object-keys: 1.1.1 + + /define-property@0.2.5: + resolution: {integrity: sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==} + engines: {node: '>=0.10.0'} + dependencies: + is-descriptor: 0.1.6 + dev: true + + /define-property@1.0.0: + resolution: {integrity: sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==} + engines: {node: '>=0.10.0'} + dependencies: + is-descriptor: 1.0.2 + dev: true + + /define-property@2.0.2: + resolution: {integrity: sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==} + engines: {node: '>=0.10.0'} + dependencies: + is-descriptor: 1.0.2 + isobject: 3.0.1 + dev: true + + /del@4.1.1: + resolution: {integrity: sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==} + engines: {node: '>=6'} + dependencies: + '@types/glob': 7.2.0 + globby: 6.1.0 + is-path-cwd: 2.2.0 + is-path-in-cwd: 2.1.0 + p-map: 2.1.0 + pify: 4.0.1 + rimraf: 2.7.1 + dev: true + + /delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + /depd@1.1.2: + resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} + engines: {node: '>= 0.6'} + + /depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + /des.js@1.0.1: + resolution: {integrity: sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==} + dependencies: + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + dev: true + + /destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + /detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + dev: false + + /detect-node@2.1.0: + resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + + /detect-port-alt@1.1.6: + resolution: {integrity: sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==} + engines: {node: '>= 4.2.1'} + hasBin: true + dependencies: + address: 1.2.2 + debug: 2.6.9(supports-color@6.1.0) + transitivePeerDependencies: + - supports-color + dev: false + + /di@0.0.1: + resolution: {integrity: sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==} + dev: true + + /didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + dev: false + + /diff-sequences@27.5.1: + resolution: {integrity: sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dev: false + + /diffie-hellman@5.0.3: + resolution: {integrity: sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==} + dependencies: + bn.js: 4.12.0 + miller-rabin: 4.0.1 + randombytes: 2.1.0 + dev: true + + /dir-glob@2.2.2: + resolution: {integrity: sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==} + engines: {node: '>=4'} + dependencies: + path-type: 3.0.0 + dev: true + + /dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + dependencies: + path-type: 4.0.0 + + /dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + dev: false + + /dns-equal@1.0.0: + resolution: {integrity: sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==} + + /dns-packet@1.3.4: + resolution: {integrity: sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA==} + dependencies: + ip: 1.1.8 + safe-buffer: 5.2.1 + dev: true + + /dns-packet@5.5.0: + resolution: {integrity: sha512-USawdAUzRkV6xrqTjiAEp6M9YagZEzWcSUaZTcIFAiyQWW1SoI6KyId8y2+/71wbgHKQAKd+iupLv4YvEwYWvA==} + engines: {node: '>=6'} + dependencies: + '@leichtgewicht/ip-codec': 2.0.4 + dev: false + + /dns-txt@2.0.2: + resolution: {integrity: sha512-Ix5PrWjphuSoUXV/Zv5gaFHjnaJtb02F2+Si3Ht9dyJ87+Z/lMmy+dpNHtTGraNK958ndXq2i+GLkWsWHcKaBQ==} + dependencies: + buffer-indexof: 1.1.1 + dev: true + + /docsearch.js@2.6.3: + resolution: {integrity: sha512-GN+MBozuyz664ycpZY0ecdQE0ND/LSgJKhTLA0/v3arIS3S1Rpf2OJz6A35ReMsm91V5apcmzr5/kM84cvUg+A==} + deprecated: This package has been deprecated and is no longer maintained. Please use @docsearch/js. + dependencies: + algoliasearch: 3.35.1 + autocomplete.js: 0.36.0 + hogan.js: 3.0.2 + request: 2.88.2 + stack-utils: 1.0.5 + to-factory: 1.0.0 + zepto: 1.2.0 + transitivePeerDependencies: + - supports-color + dev: true + + /doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + dependencies: + esutils: 2.0.3 + dev: false + + /doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dependencies: + esutils: 2.0.3 + + /dom-converter@0.2.0: + resolution: {integrity: sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==} + dependencies: + utila: 0.4.0 + + /dom-serialize@2.2.1: + resolution: {integrity: sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==} + dependencies: + custom-event: 1.0.1 + ent: 2.2.0 + extend: 3.0.2 + void-elements: 2.0.1 + dev: true + + /dom-serializer@0.2.2: + resolution: {integrity: sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==} + dependencies: + domelementtype: 2.3.0 + entities: 2.2.0 + + /dom-serializer@1.4.1: + resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + entities: 2.2.0 + + /dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.4.0 + dev: true + + /dom-walk@0.1.2: + resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==} + dev: true + + /domain-browser@1.2.0: + resolution: {integrity: sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==} + engines: {node: '>=0.4', npm: '>=1.2'} + dev: true + + /domelementtype@1.3.1: + resolution: {integrity: sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==} + + /domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + /domexception@2.0.1: + resolution: {integrity: sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==} + engines: {node: '>=8'} + dependencies: + webidl-conversions: 5.0.0 + dev: false + + /domhandler@4.3.1: + resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} + engines: {node: '>= 4'} + dependencies: + domelementtype: 2.3.0 + + /domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + dependencies: + domelementtype: 2.3.0 + dev: true + + /domutils@1.7.0: + resolution: {integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==} + dependencies: + dom-serializer: 0.2.2 + domelementtype: 1.3.1 + + /domutils@2.8.0: + resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} + dependencies: + dom-serializer: 1.4.1 + domelementtype: 2.3.0 + domhandler: 4.3.1 + + /domutils@3.0.1: + resolution: {integrity: sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==} + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + dev: true + + /dot-case@3.0.4: + resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} + dependencies: + no-case: 3.0.4 + tslib: 2.5.0 + dev: false + + /dot-prop@5.3.0: + resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} + engines: {node: '>=8'} + dependencies: + is-obj: 2.0.0 + dev: true + + /dotenv-expand@5.1.0: + resolution: {integrity: sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==} + dev: false + + /dotenv@10.0.0: + resolution: {integrity: sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==} + engines: {node: '>=10'} + dev: false + + /duplexer3@0.1.5: + resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==} + dev: true + + /duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + dev: false + + /duplexify@3.7.1: + resolution: {integrity: sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==} + dependencies: + end-of-stream: 1.4.4 + inherits: 2.0.4 + readable-stream: 2.3.8 + stream-shift: 1.0.1 + dev: true + + /ecc-jsbn@0.1.2: + resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} + dependencies: + jsbn: 0.1.1 + safer-buffer: 2.1.2 + dev: true + + /edge-launcher@1.2.2: + resolution: {integrity: sha512-JcD5WBi3BHZXXVSSeEhl6sYO8g5cuynk/hifBzds2Bp4JdzCGLNMHgMCKu5DvrO1yatMgF0goFsxXRGus0yh1g==} + dev: true + + /ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + /ejs@3.1.9: + resolution: {integrity: sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==} + engines: {node: '>=0.10.0'} + hasBin: true + dependencies: + jake: 10.8.5 + dev: false + + /electron-to-chromium@1.4.342: + resolution: {integrity: sha512-dTei3VResi5bINDENswBxhL+N0Mw5YnfWyTqO75KGsVldurEkhC9+CelJVAse8jycWyP8pv3VSj4BSyP8wTWJA==} + + /elliptic@6.5.4: + resolution: {integrity: sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==} + dependencies: + bn.js: 4.12.0 + brorand: 1.1.0 + hash.js: 1.1.7 + hmac-drbg: 1.0.1 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + minimalistic-crypto-utils: 1.0.1 + dev: true + + /emittery@0.10.2: + resolution: {integrity: sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==} + engines: {node: '>=12'} + dev: false + + /emittery@0.8.1: + resolution: {integrity: sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==} + engines: {node: '>=10'} + dev: false + + /emoji-regex@7.0.3: + resolution: {integrity: sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==} + dev: true + + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + /emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + dev: false + + /emojis-list@2.1.0: + resolution: {integrity: sha512-knHEZMgs8BB+MInokmNTg/OyPlAddghe1YBgNwJBc5zsJi/uyIcXoSDsL/W9ymOsBoBGdPIHXYJ9+qKFwRwDng==} + engines: {node: '>= 0.10'} + dev: true + + /emojis-list@3.0.0: + resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==} + engines: {node: '>= 4'} + + /encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + /end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + dependencies: + once: 1.4.0 + dev: true + + /engine.io-parser@5.2.3: + resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} + engines: {node: '>=10.0.0'} + dev: true + + /engine.io@6.5.5: + resolution: {integrity: sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==} + engines: {node: '>=10.2.0'} + dependencies: + '@types/cookie': 0.4.1 + '@types/cors': 2.8.13 + '@types/node': 18.15.11 + accepts: 1.3.8 + base64id: 2.0.0 + cookie: 0.4.2 + cors: 2.8.5 + debug: 4.3.4(supports-color@6.1.0) + engine.io-parser: 5.2.3 + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: true + + /enhanced-resolve@4.5.0: + resolution: {integrity: sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==} + engines: {node: '>=6.9.0'} + dependencies: + graceful-fs: 4.2.11 + memory-fs: 0.5.0 + tapable: 1.1.3 + dev: true + + /enhanced-resolve@5.12.0: + resolution: {integrity: sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==} + engines: {node: '>=10.13.0'} + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.1 + dev: false + + /ent@2.2.0: + resolution: {integrity: sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==} + dev: true + + /entities@1.1.2: + resolution: {integrity: sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==} + dev: true + + /entities@2.1.0: + resolution: {integrity: sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==} + dev: true + + /entities@2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + + /entities@4.4.0: + resolution: {integrity: sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==} + engines: {node: '>=0.12'} + dev: true + + /envify@4.1.0: + resolution: {integrity: sha512-IKRVVoAYr4pIx4yIWNsz9mOsboxlNXiu7TNBnem/K/uTHdkyzXWDzHCK7UTolqBbgaBz0tQHsD3YNls0uIIjiw==} + hasBin: true + dependencies: + esprima: 4.0.1 + through: 2.3.8 + dev: true + + /envinfo@7.8.1: + resolution: {integrity: sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /errno@0.1.8: + resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==} + hasBin: true + dependencies: + prr: 1.0.1 + dev: true + + /error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + dependencies: + is-arrayish: 0.2.1 + + /error-stack-parser@2.1.4: + resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} + dependencies: + stackframe: 1.3.4 + dev: false + + /es-abstract@1.21.2: + resolution: {integrity: sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + es-set-tostringtag: 2.0.1 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.5 + get-intrinsic: 1.2.0 + get-symbol-description: 1.0.0 + globalthis: 1.0.3 + gopd: 1.0.1 + has: 1.0.3 + has-property-descriptors: 1.0.0 + has-proto: 1.0.1 + has-symbols: 1.0.3 + internal-slot: 1.0.5 + is-array-buffer: 3.0.2 + is-callable: 1.2.7 + is-negative-zero: 2.0.2 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.2 + is-string: 1.0.7 + is-typed-array: 1.1.10 + is-weakref: 1.0.2 + object-inspect: 1.12.3 + object-keys: 1.1.1 + object.assign: 4.1.4 + regexp.prototype.flags: 1.4.3 + safe-regex-test: 1.0.0 + string.prototype.trim: 1.2.7 + string.prototype.trimend: 1.0.6 + string.prototype.trimstart: 1.0.6 + typed-array-length: 1.0.4 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.9 + + /es-array-method-boxes-properly@1.0.0: + resolution: {integrity: sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==} + + /es-get-iterator@1.1.3: + resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.0 + has-symbols: 1.0.3 + is-arguments: 1.1.1 + is-map: 2.0.2 + is-set: 2.0.2 + is-string: 1.0.7 + isarray: 2.0.5 + stop-iteration-iterator: 1.0.0 + dev: false + + /es-module-lexer@0.9.3: + resolution: {integrity: sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==} + dev: false + + /es-set-tostringtag@2.0.1: + resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.0 + has: 1.0.3 + has-tostringtag: 1.0.0 + + /es-shim-unscopables@1.0.0: + resolution: {integrity: sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==} + dependencies: + has: 1.0.3 + dev: false + + /es-to-primitive@1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + + /es6-promise@4.2.8: + resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} + dev: true + + /esbuild-android-arm64@0.14.7: + resolution: {integrity: sha512-9/Q1NC4JErvsXzJKti0NHt+vzKjZOgPIjX/e6kkuCzgfT/GcO3FVBcGIv4HeJG7oMznE6KyKhvLrFgt7CdU2/w==} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /esbuild-darwin-64@0.14.7: + resolution: {integrity: sha512-Z9X+3TT/Xj+JiZTVlwHj2P+8GoiSmUnGVz0YZTSt8WTbW3UKw5Pw2ucuJ8VzbD2FPy0jbIKJkko/6CMTQchShQ==} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /esbuild-darwin-arm64@0.14.7: + resolution: {integrity: sha512-68e7COhmwIiLXBEyxUxZSSU0akgv8t3e50e2QOtKdBUE0F6KIRISzFntLe2rYlNqSsjGWsIO6CCc9tQxijjSkw==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /esbuild-freebsd-64@0.14.7: + resolution: {integrity: sha512-76zy5jAjPiXX/S3UvRgG85Bb0wy0zv/J2lel3KtHi4V7GUTBfhNUPt0E5bpSXJ6yMT7iThhnA5rOn+IJiUcslQ==} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-freebsd-arm64@0.14.7: + resolution: {integrity: sha512-lSlYNLiqyzd7qCN5CEOmLxn7MhnGHPcu5KuUYOG1i+t5A6q7LgBmfYC9ZHJBoYyow3u4CNu79AWHbvVLpE/VQQ==} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-32@0.14.7: + resolution: {integrity: sha512-Vk28u409wVOXqTaT6ek0TnfQG4Ty1aWWfiysIaIRERkNLhzLhUf4i+qJBN8mMuGTYOkE40F0Wkbp6m+IidOp2A==} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-64@0.14.7: + resolution: {integrity: sha512-+Lvz6x+8OkRk3K2RtZwO+0a92jy9si9cUea5Zoru4yJ/6EQm9ENX5seZE0X9DTwk1dxJbjmLsJsd3IoowyzgVg==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-arm64@0.14.7: + resolution: {integrity: sha512-kJd5beWSqteSAW086qzCEsH6uwpi7QRIpzYWHzEYwKKu9DiG1TwIBegQJmLpPsLp4v5RAFjea0JAmAtpGtRpqg==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-arm@0.14.7: + resolution: {integrity: sha512-OzpXEBogbYdcBqE4uKynuSn5YSetCvK03Qv1HcOY1VN6HmReuatjJ21dCH+YPHSpMEF0afVCnNfffvsGEkxGJQ==} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-mips64le@0.14.7: + resolution: {integrity: sha512-mFWpnDhZJmj/h7pxqn1GGDsKwRfqtV7fx6kTF5pr4PfXe8pIaTERpwcKkoCwZUkWAOmUEjMIUAvFM72A6hMZnA==} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-ppc64le@0.14.7: + resolution: {integrity: sha512-wM7f4M0bsQXfDL4JbbYD0wsr8cC8KaQ3RPWc/fV27KdErPW7YsqshZZSjDV0kbhzwpNNdhLItfbaRT8OE8OaKA==} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-netbsd-64@0.14.7: + resolution: {integrity: sha512-J/afS7woKyzGgAL5FlgvMyqgt5wQ597lgsT+xc2yJ9/7BIyezeXutXqfh05vszy2k3kSvhLesugsxIA71WsqBw==} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-openbsd-64@0.14.7: + resolution: {integrity: sha512-7CcxgdlCD+zAPyveKoznbgr3i0Wnh0L8BDGRCjE/5UGkm5P/NQko51tuIDaYof8zbmXjjl0OIt9lSo4W7I8mrw==} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-sunos-64@0.14.7: + resolution: {integrity: sha512-GKCafP2j/KUljVC3nesw1wLFSZktb2FGCmoT1+730zIF5O6hNroo0bSEofm6ZK5mNPnLiSaiLyRB9YFgtkd5Xg==} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-32@0.14.7: + resolution: {integrity: sha512-5I1GeL/gZoUUdTPA0ws54bpYdtyeA2t6MNISalsHpY269zK8Jia/AXB3ta/KcDHv2SvNwabpImeIPXC/k0YW6A==} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-64@0.14.7: + resolution: {integrity: sha512-CIGKCFpQOSlYsLMbxt8JjxxvVw9MlF1Rz2ABLVfFyHUF5OeqHD5fPhGrCVNaVrhO8Xrm+yFmtjcZudUGr5/WYQ==} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-arm64@0.14.7: + resolution: {integrity: sha512-eOs1eSivOqN7cFiRIukEruWhaCf75V0N8P0zP7dh44LIhLl8y6/z++vv9qQVbkBm5/D7M7LfCfCTmt1f1wHOCw==} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild@0.14.7: + resolution: {integrity: sha512-+u/msd6iu+HvfysUPkZ9VHm83LImmSNnecYPfFI01pQ7TTcsFR+V0BkybZX7mPtIaI7LCrse6YRj+v3eraJSgw==} + hasBin: true + requiresBuild: true + optionalDependencies: + esbuild-android-arm64: 0.14.7 + esbuild-darwin-64: 0.14.7 + esbuild-darwin-arm64: 0.14.7 + esbuild-freebsd-64: 0.14.7 + esbuild-freebsd-arm64: 0.14.7 + esbuild-linux-32: 0.14.7 + esbuild-linux-64: 0.14.7 + esbuild-linux-arm: 0.14.7 + esbuild-linux-arm64: 0.14.7 + esbuild-linux-mips64le: 0.14.7 + esbuild-linux-ppc64le: 0.14.7 + esbuild-netbsd-64: 0.14.7 + esbuild-openbsd-64: 0.14.7 + esbuild-sunos-64: 0.14.7 + esbuild-windows-32: 0.14.7 + esbuild-windows-64: 0.14.7 + esbuild-windows-arm64: 0.14.7 + dev: true + + /escalade@3.1.1: + resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} + engines: {node: '>=6'} + + /escape-goat@2.1.1: + resolution: {integrity: sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==} + engines: {node: '>=8'} + dev: true + + /escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + /escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + /escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + + /escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + /escodegen@2.0.0: + resolution: {integrity: sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==} + engines: {node: '>=6.0'} + hasBin: true + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionator: 0.8.3 + optionalDependencies: + source-map: 0.6.1 + dev: false + + /eslint-config-chartjs@0.3.0: + resolution: {integrity: sha512-L3AC5VSG8EBwwKkpOrxvBMjYzGF/XrGM+EjXgYO1KFUn3cMUFMKd562lSHdCSr4+ocw80vi+0fZhiFesUpqV3g==} + dev: true + + /eslint-config-react-app@7.0.1(@babel/plugin-syntax-flow@7.18.6)(@babel/plugin-transform-react-jsx@7.21.0)(eslint@8.57.1)(jest@27.5.1)(typescript@4.9.5): + resolution: {integrity: sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA==} + engines: {node: '>=14.0.0'} + peerDependencies: + eslint: ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@babel/core': 7.21.3 + '@babel/eslint-parser': 7.21.3(@babel/core@7.21.3)(eslint@8.57.1) + '@rushstack/eslint-patch': 1.2.0 + '@typescript-eslint/eslint-plugin': 5.57.0(@typescript-eslint/parser@5.57.0)(eslint@8.57.1)(typescript@4.9.5) + '@typescript-eslint/parser': 5.57.0(eslint@8.57.1)(typescript@4.9.5) + babel-preset-react-app: 10.0.1 + confusing-browser-globals: 1.0.11 + eslint: 8.57.1 + eslint-plugin-flowtype: 8.0.3(@babel/plugin-syntax-flow@7.18.6)(@babel/plugin-transform-react-jsx@7.21.0)(eslint@8.57.1) + eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.57.0)(eslint@8.57.1) + eslint-plugin-jest: 25.7.0(@typescript-eslint/eslint-plugin@5.57.0)(eslint@8.57.1)(jest@27.5.1)(typescript@4.9.5) + eslint-plugin-jsx-a11y: 6.7.1(eslint@8.57.1) + eslint-plugin-react: 7.32.2(eslint@8.57.1) + eslint-plugin-react-hooks: 4.6.0(eslint@8.57.1) + eslint-plugin-testing-library: 5.10.2(eslint@8.57.1)(typescript@4.9.5) + typescript: 4.9.5 + transitivePeerDependencies: + - '@babel/plugin-syntax-flow' + - '@babel/plugin-transform-react-jsx' + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - jest + - supports-color + dev: false + + /eslint-import-resolver-node@0.3.7: + resolution: {integrity: sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==} + dependencies: + debug: 3.2.7(supports-color@6.1.0) + is-core-module: 2.11.0 + resolve: 1.22.1 + transitivePeerDependencies: + - supports-color + dev: false + + /eslint-module-utils@2.7.4(@typescript-eslint/parser@5.57.0)(eslint-import-resolver-node@0.3.7)(eslint@8.57.1): + resolution: {integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + dependencies: + '@typescript-eslint/parser': 5.57.0(eslint@8.57.1)(typescript@4.9.5) + debug: 3.2.7(supports-color@6.1.0) + eslint: 8.57.1 + eslint-import-resolver-node: 0.3.7 + transitivePeerDependencies: + - supports-color + dev: false + + /eslint-plugin-es@4.1.0(eslint@8.37.0): + resolution: {integrity: sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==} + engines: {node: '>=8.10.0'} + peerDependencies: + eslint: '>=4.19.1' + dependencies: + eslint: 8.37.0 + eslint-utils: 2.1.0 + regexpp: 3.2.0 + dev: true + + /eslint-plugin-flowtype@8.0.3(@babel/plugin-syntax-flow@7.18.6)(@babel/plugin-transform-react-jsx@7.21.0)(eslint@8.57.1): + resolution: {integrity: sha512-dX8l6qUL6O+fYPtpNRideCFSpmWOUVx5QcaGLVqe/vlDiBSe4vYljDWDETwnyFzpl7By/WVIu6rcrniCgH9BqQ==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@babel/plugin-syntax-flow': ^7.14.5 + '@babel/plugin-transform-react-jsx': ^7.14.9 + eslint: ^8.1.0 + dependencies: + '@babel/plugin-syntax-flow': 7.18.6(@babel/core@7.21.3) + '@babel/plugin-transform-react-jsx': 7.21.0(@babel/core@7.21.3) + eslint: 8.57.1 + lodash: 4.17.21 + string-natural-compare: 3.0.1 + dev: false + + /eslint-plugin-html@7.1.0: + resolution: {integrity: sha512-fNLRraV/e6j8e3XYOC9xgND4j+U7b1Rq+OygMlLcMg+wI/IpVbF+ubQa3R78EjKB9njT6TQOlcK5rFKBVVtdfg==} + dependencies: + htmlparser2: 8.0.2 + dev: true + + /eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.57.0)(eslint@8.57.1): + resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + dependencies: + '@typescript-eslint/parser': 5.57.0(eslint@8.57.1)(typescript@4.9.5) + array-includes: 3.1.6 + array.prototype.flat: 1.3.1 + array.prototype.flatmap: 1.3.1 + debug: 3.2.7(supports-color@6.1.0) + doctrine: 2.1.0 + eslint: 8.57.1 + eslint-import-resolver-node: 0.3.7 + eslint-module-utils: 2.7.4(@typescript-eslint/parser@5.57.0)(eslint-import-resolver-node@0.3.7)(eslint@8.57.1) + has: 1.0.3 + is-core-module: 2.11.0 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.values: 1.1.6 + resolve: 1.22.1 + semver: 6.3.0 + tsconfig-paths: 3.14.2 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + dev: false + + /eslint-plugin-jest@25.7.0(@typescript-eslint/eslint-plugin@5.57.0)(eslint@8.57.1)(jest@27.5.1)(typescript@4.9.5): + resolution: {integrity: sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + peerDependencies: + '@typescript-eslint/eslint-plugin': ^4.0.0 || ^5.0.0 + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + jest: '*' + peerDependenciesMeta: + '@typescript-eslint/eslint-plugin': + optional: true + jest: + optional: true + dependencies: + '@typescript-eslint/eslint-plugin': 5.57.0(@typescript-eslint/parser@5.57.0)(eslint@8.57.1)(typescript@4.9.5) + '@typescript-eslint/experimental-utils': 5.57.0(eslint@8.57.1)(typescript@4.9.5) + eslint: 8.57.1 + jest: 27.5.1 + transitivePeerDependencies: + - supports-color + - typescript + dev: false + + /eslint-plugin-jsx-a11y@6.7.1(eslint@8.57.1): + resolution: {integrity: sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==} + engines: {node: '>=4.0'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + dependencies: + '@babel/runtime': 7.21.0 + aria-query: 5.1.3 + array-includes: 3.1.6 + array.prototype.flatmap: 1.3.1 + ast-types-flow: 0.0.7 + axe-core: 4.6.3 + axobject-query: 3.1.1 + damerau-levenshtein: 1.0.8 + emoji-regex: 9.2.2 + eslint: 8.57.1 + has: 1.0.3 + jsx-ast-utils: 3.3.3 + language-tags: 1.0.5 + minimatch: 3.1.2 + object.entries: 1.1.6 + object.fromentries: 2.0.6 + semver: 6.3.0 + dev: false + + /eslint-plugin-markdown@3.0.0(eslint@8.37.0): + resolution: {integrity: sha512-hRs5RUJGbeHDLfS7ELanT0e29Ocyssf/7kBM+p7KluY5AwngGkDf8Oyu4658/NZSGTTq05FZeWbkxXtbVyHPwg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + eslint: 8.37.0 + mdast-util-from-markdown: 0.8.5 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-plugin-react-hooks@4.6.0(eslint@8.57.1): + resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + dependencies: + eslint: 8.57.1 + dev: false + + /eslint-plugin-react@7.32.2(eslint@8.57.1): + resolution: {integrity: sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + dependencies: + array-includes: 3.1.6 + array.prototype.flatmap: 1.3.1 + array.prototype.tosorted: 1.1.1 + doctrine: 2.1.0 + eslint: 8.57.1 + estraverse: 5.3.0 + jsx-ast-utils: 3.3.3 + minimatch: 3.1.2 + object.entries: 1.1.6 + object.fromentries: 2.0.6 + object.hasown: 1.1.2 + object.values: 1.1.6 + prop-types: 15.8.1 + resolve: 2.0.0-next.4 + semver: 6.3.0 + string.prototype.matchall: 4.0.8 + dev: false + + /eslint-plugin-testing-library@5.10.2(eslint@8.57.1)(typescript@4.9.5): + resolution: {integrity: sha512-f1DmDWcz5SDM+IpCkEX0lbFqrrTs8HRsEElzDEqN/EBI0hpRj8Cns5+IVANXswE8/LeybIJqPAOQIFu2j5Y5sw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0, npm: '>=6'} + peerDependencies: + eslint: ^7.5.0 || ^8.0.0 + dependencies: + '@typescript-eslint/utils': 5.57.0(eslint@8.57.1)(typescript@4.9.5) + eslint: 8.57.1 + transitivePeerDependencies: + - supports-color + - typescript + dev: false + + /eslint-scope@4.0.3: + resolution: {integrity: sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==} + engines: {node: '>=4.0.0'} + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + dev: true + + /eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + + /eslint-scope@7.1.1: + resolution: {integrity: sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + dev: true + + /eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + dev: false + + /eslint-utils@2.1.0: + resolution: {integrity: sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==} + engines: {node: '>=6'} + dependencies: + eslint-visitor-keys: 1.3.0 + dev: true + + /eslint-visitor-keys@1.3.0: + resolution: {integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==} + engines: {node: '>=4'} + dev: true + + /eslint-visitor-keys@2.1.0: + resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} + engines: {node: '>=10'} + dev: false + + /eslint-visitor-keys@3.4.0: + resolution: {integrity: sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + /eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: false + + /eslint-webpack-plugin@3.2.0(eslint@8.57.1)(webpack@5.76.3): + resolution: {integrity: sha512-avrKcGncpPbPSUHX6B3stNGzkKFto3eL+DKM4+VyMrVnhPc3vRczVlCq3uhuFOdRvDHTVXuzwk1ZKUrqDQHQ9w==} + engines: {node: '>= 12.13.0'} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + webpack: ^5.0.0 + dependencies: + '@types/eslint': 8.21.3 + eslint: 8.57.1 + jest-worker: 28.1.3 + micromatch: 4.0.5 + normalize-path: 3.0.0 + schema-utils: 4.0.0 + webpack: 5.76.3(@swc/core@1.3.42) + dev: false + + /eslint@8.37.0: + resolution: {integrity: sha512-NU3Ps9nI05GUoVMxcZx1J8CNR6xOvUT4jAUMH5+z8lpp3aEdPVCImKw6PWG4PY+Vfkpr+jvMpxs/qoE7wq0sPw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.37.0) + '@eslint-community/regexpp': 4.5.0 + '@eslint/eslintrc': 2.0.2 + '@eslint/js': 8.37.0 + '@humanwhocodes/config-array': 0.11.8 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.4(supports-color@6.1.0) + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.1.1 + eslint-visitor-keys: 3.4.0 + espree: 9.5.1 + esquery: 1.5.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.20.0 + grapheme-splitter: 1.0.4 + ignore: 5.2.4 + import-fresh: 3.3.0 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-sdsl: 4.4.0 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.1 + strip-ansi: 6.0.1 + strip-json-comments: 3.1.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint@8.57.1: + resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1) + '@eslint-community/regexpp': 4.12.2 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.1 + '@humanwhocodes/config-array': 0.13.0 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.3.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.1 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + dev: false + + /esm@3.2.25: + resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==} + engines: {node: '>=6'} + dev: true + + /espree@9.5.1: + resolution: {integrity: sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + acorn: 8.8.2 + acorn-jsx: 5.3.2(acorn@8.8.2) + eslint-visitor-keys: 3.4.0 + dev: true + + /espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 3.4.3 + dev: false + + /esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + /esquery@1.5.0: + resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} + engines: {node: '>=0.10'} + dependencies: + estraverse: 5.3.0 + dev: true + + /esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + dependencies: + estraverse: 5.3.0 + dev: false + + /esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + dependencies: + estraverse: 5.3.0 + + /estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + + /estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + /estree-walker@0.6.1: + resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} + dev: true + + /estree-walker@1.0.1: + resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==} + dev: false + + /estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + dev: true + + /esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + /etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + /eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + /events@1.1.1: + resolution: {integrity: sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==} + engines: {node: '>=0.4.x'} + dev: true + + /events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + /eventsource@2.0.2: + resolution: {integrity: sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==} + engines: {node: '>=12.0.0'} + dev: true + + /evp_bytestokey@1.0.3: + resolution: {integrity: sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==} + dependencies: + md5.js: 1.3.5 + safe-buffer: 5.2.1 + dev: true + + /execa@1.0.0: + resolution: {integrity: sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==} + engines: {node: '>=6'} + dependencies: + cross-spawn: 6.0.5 + get-stream: 4.1.0 + is-stream: 1.1.0 + npm-run-path: 2.0.2 + p-finally: 1.0.0 + signal-exit: 3.0.7 + strip-eof: 1.0.0 + dev: true + + /execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + dependencies: + cross-spawn: 7.0.3 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + dev: false + + /exit@0.1.2: + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + engines: {node: '>= 0.8.0'} + dev: false + + /expand-brackets@2.1.4(supports-color@6.1.0): + resolution: {integrity: sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==} + engines: {node: '>=0.10.0'} + dependencies: + debug: 2.6.9(supports-color@6.1.0) + define-property: 0.2.5 + extend-shallow: 2.0.1 + posix-character-classes: 0.1.1 + regex-not: 1.0.2 + snapdragon: 0.8.2(supports-color@6.1.0) + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + dev: true + + /expect@27.5.1: + resolution: {integrity: sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + '@jest/types': 27.5.1 + jest-get-type: 27.5.1 + jest-matcher-utils: 27.5.1 + jest-message-util: 27.5.1 + dev: false + + /express@4.18.2(supports-color@6.1.0): + resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} + engines: {node: '>= 0.10.0'} + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.1(supports-color@6.1.0) + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.5.0 + cookie-signature: 1.0.6 + debug: 2.6.9(supports-color@6.1.0) + depd: 2.0.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.2.0(supports-color@6.1.0) + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.1 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.7 + proxy-addr: 2.0.7 + qs: 6.11.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.18.0(supports-color@6.1.0) + serve-static: 1.15.0(supports-color@6.1.0) + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + /extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + dependencies: + is-extendable: 0.1.1 + dev: true + + /extend-shallow@3.0.2: + resolution: {integrity: sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==} + engines: {node: '>=0.10.0'} + dependencies: + assign-symbols: 1.0.0 + is-extendable: 1.0.1 + dev: true + + /extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + dev: true + + /extglob@2.0.4(supports-color@6.1.0): + resolution: {integrity: sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==} + engines: {node: '>=0.10.0'} + dependencies: + array-unique: 0.3.2 + define-property: 1.0.0 + expand-brackets: 2.1.4(supports-color@6.1.0) + extend-shallow: 2.0.1 + fragment-cache: 0.2.1 + regex-not: 1.0.2 + snapdragon: 0.8.2(supports-color@6.1.0) + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + dev: true + + /extsprintf@1.3.0: + resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==} + engines: {'0': node >=0.6.0} + dev: true + + /fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + /fast-glob@2.2.7: + resolution: {integrity: sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==} + engines: {node: '>=4.0.0'} + dependencies: + '@mrmlnc/readdir-enhanced': 2.2.1 + '@nodelib/fs.stat': 1.1.3 + glob-parent: 3.1.0 + is-glob: 4.0.3 + merge2: 1.4.1 + micromatch: 3.1.10(supports-color@6.1.0) + transitivePeerDependencies: + - supports-color + dev: true + + /fast-glob@3.2.12: + resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + + /fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + /fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + /fastq@1.15.0: + resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} + dependencies: + reusify: 1.0.4 + + /faye-websocket@0.11.4: + resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} + engines: {node: '>=0.8.0'} + dependencies: + websocket-driver: 0.7.4 + + /fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + dependencies: + bser: 2.1.1 + dev: false + + /figgy-pudding@3.5.2: + resolution: {integrity: sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==} + dev: true + + /figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + dependencies: + escape-string-regexp: 1.0.5 + dev: true + + /file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flat-cache: 3.0.4 + + /file-loader@3.0.1(webpack@4.46.0): + resolution: {integrity: sha512-4sNIOXgtH/9WZq4NvlfU3Opn5ynUsqBwSLyM+I7UOwdGigTBYfVVQEwe/msZNX/j4pCJTIM14Fsw66Svo1oVrw==} + engines: {node: '>= 6.9.0'} + peerDependencies: + webpack: ^4.0.0 + dependencies: + loader-utils: 1.4.2 + schema-utils: 1.0.0 + webpack: 4.46.0 + dev: true + + /file-loader@6.2.0(webpack@5.76.3): + resolution: {integrity: sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==} + engines: {node: '>= 10.13.0'} + peerDependencies: + webpack: ^4.0.0 || ^5.0.0 + dependencies: + loader-utils: 2.0.4 + schema-utils: 3.1.1 + webpack: 5.76.3(@swc/core@1.3.42) + dev: false + + /file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + requiresBuild: true + dev: true + optional: true + + /filelist@1.0.4: + resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} + dependencies: + minimatch: 5.1.6 + dev: false + + /filesize@8.0.7: + resolution: {integrity: sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==} + engines: {node: '>= 0.4.0'} + dev: false + + /fill-range@4.0.0: + resolution: {integrity: sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==} + engines: {node: '>=0.10.0'} + dependencies: + extend-shallow: 2.0.1 + is-number: 3.0.0 + repeat-string: 1.6.1 + to-regex-range: 2.1.1 + dev: true + + /fill-range@7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + + /finalhandler@1.1.2: + resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} + engines: {node: '>= 0.8'} + dependencies: + debug: 2.6.9(supports-color@6.1.0) + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.3.0 + parseurl: 1.3.3 + statuses: 1.5.0 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /finalhandler@1.2.0(supports-color@6.1.0): + resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} + engines: {node: '>= 0.8'} + dependencies: + debug: 2.6.9(supports-color@6.1.0) + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + /find-cache-dir@2.1.0: + resolution: {integrity: sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==} + engines: {node: '>=6'} + dependencies: + commondir: 1.0.1 + make-dir: 2.1.0 + pkg-dir: 3.0.0 + dev: true + + /find-cache-dir@3.3.2: + resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} + engines: {node: '>=8'} + dependencies: + commondir: 1.0.1 + make-dir: 3.1.0 + pkg-dir: 4.2.0 + + /find-up@3.0.0: + resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} + engines: {node: '>=6'} + dependencies: + locate-path: 3.0.0 + + /find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + /find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + /flat-cache@3.0.4: + resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flatted: 3.2.7 + rimraf: 3.0.2 + + /flatted@3.2.7: + resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} + + /flexsearch@0.6.32: + resolution: {integrity: sha512-EF1BWkhwoeLtbIlDbY/vDSLBen/E5l/f1Vg7iX5CDymQCamcx1vhlc3tIZxIDplPjgi0jhG37c67idFbjg+v+Q==} + dev: true + + /flush-write-stream@1.1.1: + resolution: {integrity: sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==} + dependencies: + inherits: 2.0.4 + readable-stream: 2.3.8 + dev: true + + /follow-redirects@1.15.6(debug@4.3.4): + resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dependencies: + debug: 4.3.4(supports-color@6.1.0) + + /for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + + /for-in@1.0.2: + resolution: {integrity: sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==} + engines: {node: '>=0.10.0'} + dev: true + + /foreach@2.0.6: + resolution: {integrity: sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==} + dev: true + + /forever-agent@0.6.1: + resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} + dev: true + + /fork-ts-checker-webpack-plugin@6.5.3(eslint@8.57.1)(typescript@4.9.5)(webpack@5.76.3): + resolution: {integrity: sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==} + engines: {node: '>=10', yarn: '>=1.0.0'} + peerDependencies: + eslint: '>= 6' + typescript: '>= 2.7' + vue-template-compiler: '*' + webpack: '>= 4' + peerDependenciesMeta: + eslint: + optional: true + vue-template-compiler: + optional: true + dependencies: + '@babel/code-frame': 7.18.6 + '@types/json-schema': 7.0.11 + chalk: 4.1.2 + chokidar: 3.6.0 + cosmiconfig: 6.0.0 + deepmerge: 4.3.1 + eslint: 8.57.1 + fs-extra: 9.1.0 + glob: 7.2.3 + memfs: 3.4.13 + minimatch: 3.1.2 + schema-utils: 2.7.0 + semver: 7.3.8 + tapable: 1.1.3 + typescript: 4.9.5 + webpack: 5.76.3(@swc/core@1.3.42) + dev: false + + /form-data@2.3.3: + resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==} + engines: {node: '>= 0.12'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: true + + /form-data@3.0.1: + resolution: {integrity: sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: false + + /forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + /fraction.js@4.2.0: + resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==} + dev: false + + /fragment-cache@0.2.1: + resolution: {integrity: sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==} + engines: {node: '>=0.10.0'} + dependencies: + map-cache: 0.2.2 + dev: true + + /fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + /from2@2.3.0: + resolution: {integrity: sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==} + dependencies: + inherits: 2.0.4 + readable-stream: 2.3.8 + dev: true + + /fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.0 + dev: false + + /fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + dev: true + + /fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + dev: true + + /fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.0 + dev: false + + /fs-monkey@1.0.3: + resolution: {integrity: sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==} + dev: false + + /fs-write-stream-atomic@1.0.10: + resolution: {integrity: sha512-gehEzmPn2nAwr39eay+x3X34Ra+M2QlVUTLhkXPjWdeO8RF9kszk116avgBJM3ZyNHgHXBNx+VmPaFC36k0PzA==} + dependencies: + graceful-fs: 4.2.11 + iferr: 0.1.5 + imurmurhash: 0.1.4 + readable-stream: 2.3.8 + dev: true + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + /fsevents@1.2.13: + resolution: {integrity: sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==} + engines: {node: '>= 4.0'} + os: [darwin] + deprecated: Upgrade to fsevents v2 to mitigate potential security issues + requiresBuild: true + dependencies: + bindings: 1.5.0 + nan: 2.23.1 + dev: true + optional: true + + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + optional: true + + /function-bind@1.1.1: + resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + + /function.prototype.name@1.1.5: + resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + functions-have-names: 1.2.3 + + /functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + /gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + /get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + /get-intrinsic@1.2.0: + resolution: {integrity: sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==} + dependencies: + function-bind: 1.1.1 + has: 1.0.3 + has-symbols: 1.0.3 + + /get-own-enumerable-property-symbols@3.0.2: + resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} + + /get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + dev: false + + /get-stream@4.1.0: + resolution: {integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==} + engines: {node: '>=6'} + dependencies: + pump: 3.0.0 + dev: true + + /get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + dependencies: + pump: 3.0.0 + dev: true + + /get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + dev: false + + /get-symbol-description@1.0.0: + resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.0 + + /get-tsconfig@4.5.0: + resolution: {integrity: sha512-MjhiaIWCJ1sAU4pIQ5i5OfOuHHxVo1oYeNsWTON7jxYkod8pHocXeh+SSbmu5OZZZK73B6cbJ2XADzXehLyovQ==} + dev: true + + /get-value@2.0.6: + resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==} + engines: {node: '>=0.10.0'} + dev: true + + /getpass@0.1.7: + resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==} + dependencies: + assert-plus: 1.0.0 + dev: true + + /glob-parent@3.1.0: + resolution: {integrity: sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==} + dependencies: + is-glob: 3.1.0 + path-dirname: 1.0.2 + dev: true + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + + /glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + dependencies: + is-glob: 4.0.3 + + /glob-to-regexp@0.3.0: + resolution: {integrity: sha512-Iozmtbqv0noj0uDDqoL0zNq0VBEfK2YFoMAZoxJe4cwphvLR+JskfF30QhXHOR4m3KrE6NLRYw+U9MRXvifyig==} + dev: true + + /glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + dev: false + + /glob@7.1.6: + resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: false + + /glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + /glob@8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 5.1.6 + once: 1.4.0 + dev: true + + /global-dirs@2.1.0: + resolution: {integrity: sha512-MG6kdOUh/xBnyo9cJFeIKkLEc1AyFq42QTU4XiX51i2NEdxLxLWXIjEjmqKeSuKR7pAZjTqUVoT2b2huxVLgYQ==} + engines: {node: '>=8'} + dependencies: + ini: 1.3.7 + dev: true + + /global-modules@2.0.0: + resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} + engines: {node: '>=6'} + dependencies: + global-prefix: 3.0.0 + dev: false + + /global-prefix@3.0.0: + resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==} + engines: {node: '>=6'} + dependencies: + ini: 1.3.8 + kind-of: 6.0.3 + which: 1.3.1 + dev: false + + /global@4.4.0: + resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==} + dependencies: + min-document: 2.19.0 + process: 0.11.10 + dev: true + + /globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + + /globals@13.20.0: + resolution: {integrity: sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.20.2 + dev: true + + /globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.20.2 + dev: false + + /globalthis@1.0.3: + resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.2.0 + + /globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.2.12 + ignore: 5.2.4 + merge2: 1.4.1 + slash: 3.0.0 + + /globby@6.1.0: + resolution: {integrity: sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==} + engines: {node: '>=0.10.0'} + dependencies: + array-union: 1.0.2 + glob: 7.2.3 + object-assign: 4.1.1 + pify: 2.3.0 + pinkie-promise: 2.0.1 + dev: true + + /globby@7.1.1: + resolution: {integrity: sha512-yANWAN2DUcBtuus5Cpd+SKROzXHs2iVXFZt/Ykrfz6SAXqacLX25NZpltE+39ceMexYF4TtEadjuSTw8+3wX4g==} + engines: {node: '>=4'} + dependencies: + array-union: 1.0.2 + dir-glob: 2.2.2 + glob: 7.2.3 + ignore: 3.3.10 + pify: 3.0.0 + slash: 1.0.0 + dev: true + + /globby@9.2.0: + resolution: {integrity: sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg==} + engines: {node: '>=6'} + dependencies: + '@types/glob': 7.2.0 + array-union: 1.0.2 + dir-glob: 2.2.2 + fast-glob: 2.2.7 + glob: 7.2.3 + ignore: 4.0.6 + pify: 4.0.1 + slash: 2.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.0 + + /got@9.6.0: + resolution: {integrity: sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==} + engines: {node: '>=8.6'} + dependencies: + '@sindresorhus/is': 0.14.0 + '@szmarczak/http-timer': 1.1.2 + '@types/keyv': 3.1.4 + '@types/responselike': 1.0.0 + cacheable-request: 6.1.0 + decompress-response: 3.3.0 + duplexer3: 0.1.5 + get-stream: 4.1.0 + lowercase-keys: 1.0.1 + mimic-response: 1.0.1 + p-cancelable: 1.1.0 + to-readable-stream: 1.0.0 + url-parse-lax: 3.0.0 + dev: true + + /graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + /grapheme-splitter@1.0.4: + resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} + + /graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + dev: false + + /gray-matter@4.0.3: + resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} + engines: {node: '>=6.0'} + dependencies: + js-yaml: 3.14.1 + kind-of: 6.0.3 + section-matter: 1.0.0 + strip-bom-string: 1.0.0 + dev: true + + /gzip-size@6.0.0: + resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} + engines: {node: '>=10'} + dependencies: + duplexer: 0.1.2 + dev: false + + /handle-thing@2.0.1: + resolution: {integrity: sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==} + + /handlebars@4.7.7: + resolution: {integrity: sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==} + engines: {node: '>=0.4.7'} + hasBin: true + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + dev: true + + /har-schema@2.0.0: + resolution: {integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==} + engines: {node: '>=4'} + dev: true + + /har-validator@5.1.5: + resolution: {integrity: sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==} + engines: {node: '>=6'} + deprecated: this library is no longer supported + dependencies: + ajv: 6.12.6 + har-schema: 2.0.0 + dev: true + + /harmony-reflect@1.6.2: + resolution: {integrity: sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==} + dev: false + + /has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + + /has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + /has-property-descriptors@1.0.0: + resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} + dependencies: + get-intrinsic: 1.2.0 + + /has-proto@1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + + /has-tostringtag@1.0.0: + resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + + /has-value@0.3.1: + resolution: {integrity: sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==} + engines: {node: '>=0.10.0'} + dependencies: + get-value: 2.0.6 + has-values: 0.1.4 + isobject: 2.1.0 + dev: true + + /has-value@1.0.0: + resolution: {integrity: sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==} + engines: {node: '>=0.10.0'} + dependencies: + get-value: 2.0.6 + has-values: 1.0.0 + isobject: 3.0.1 + dev: true + + /has-values@0.1.4: + resolution: {integrity: sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==} + engines: {node: '>=0.10.0'} + dev: true + + /has-values@1.0.0: + resolution: {integrity: sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==} + engines: {node: '>=0.10.0'} + dependencies: + is-number: 3.0.0 + kind-of: 4.0.0 + dev: true + + /has-yarn@2.1.0: + resolution: {integrity: sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==} + engines: {node: '>=8'} + dev: true + + /has@1.0.3: + resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} + engines: {node: '>= 0.4.0'} + dependencies: + function-bind: 1.1.1 + + /hash-base@3.1.0: + resolution: {integrity: sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==} + engines: {node: '>=4'} + dependencies: + inherits: 2.0.4 + readable-stream: 3.6.2 + safe-buffer: 5.2.1 + dev: true + + /hash-sum@1.0.2: + resolution: {integrity: sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==} + dev: true + + /hash-sum@2.0.0: + resolution: {integrity: sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==} + dev: true + + /hash.js@1.1.7: + resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} + dependencies: + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + dev: true + + /he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + /hex-color-regex@1.1.0: + resolution: {integrity: sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==} + dev: true + + /highlight.js@9.18.5: + resolution: {integrity: sha512-a5bFyofd/BHCX52/8i8uJkjr9DYwXIPnM/plwI6W7ezItLGqzt7X2G2nXuYSfsIJdkwwj/g9DG1LkcGJI/dDoA==} + deprecated: Support has ended for 9.x series. Upgrade to @latest + requiresBuild: true + dev: true + + /hmac-drbg@1.0.1: + resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==} + dependencies: + hash.js: 1.1.7 + minimalistic-assert: 1.0.1 + minimalistic-crypto-utils: 1.0.1 + dev: true + + /hogan.js@3.0.2: + resolution: {integrity: sha512-RqGs4wavGYJWE07t35JQccByczmNUXQT0E12ZYV1VKYu5UiAU9lsos/yBAcf840+zrUQQxgVduCR5/B8nNtibg==} + hasBin: true + dependencies: + mkdirp: 0.3.0 + nopt: 1.0.10 + dev: true + + /hoopy@0.1.4: + resolution: {integrity: sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==} + engines: {node: '>= 6.0.0'} + dev: false + + /hpack.js@2.1.6: + resolution: {integrity: sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==} + dependencies: + inherits: 2.0.4 + obuf: 1.1.2 + readable-stream: 2.3.8 + wbuf: 1.7.3 + + /hsl-regex@1.0.0: + resolution: {integrity: sha512-M5ezZw4LzXbBKMruP+BNANf0k+19hDQMgpzBIYnya//Al+fjNct9Wf3b1WedLqdEs2hKBvxq/jh+DsHJLj0F9A==} + dev: true + + /hsla-regex@1.0.0: + resolution: {integrity: sha512-7Wn5GMLuHBjZCb2bTmnDOycho0p/7UVaAeqXZGbHrBCl6Yd/xDhQJAXe6Ga9AXJH2I5zY1dEdYw2u1UptnSBJA==} + dev: true + + /html-encoding-sniffer@2.0.1: + resolution: {integrity: sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==} + engines: {node: '>=10'} + dependencies: + whatwg-encoding: 1.0.5 + dev: false + + /html-entities@1.4.0: + resolution: {integrity: sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA==} + + /html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + /html-minifier-terser@6.1.0: + resolution: {integrity: sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==} + engines: {node: '>=12'} + hasBin: true + dependencies: + camel-case: 4.1.2 + clean-css: 5.3.2 + commander: 8.3.0 + he: 1.2.0 + param-case: 3.0.4 + relateurl: 0.2.7 + terser: 5.16.8 + dev: false + + /html-minifier@3.5.21: + resolution: {integrity: sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA==} + engines: {node: '>=4'} + hasBin: true + dependencies: + camel-case: 3.0.0 + clean-css: 4.2.4 + commander: 2.17.1 + he: 1.2.0 + param-case: 2.1.1 + relateurl: 0.2.7 + uglify-js: 3.4.10 + dev: true + + /html-tags@2.0.0: + resolution: {integrity: sha512-+Il6N8cCo2wB/Vd3gqy/8TZhTD3QvcVeQLCnZiGkGCH3JP28IgGAY41giccp2W4R3jfyJPAP318FQTa1yU7K7g==} + engines: {node: '>=4'} + dev: true + + /html-tags@3.2.0: + resolution: {integrity: sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg==} + engines: {node: '>=8'} + dev: true + + /html-webpack-plugin@5.5.0(webpack@5.76.3): + resolution: {integrity: sha512-sy88PC2cRTVxvETRgUHFrL4No3UxvcH8G1NepGhqaTT+GXN2kTamqasot0inS5hXeg1cMbFDt27zzo9p35lZVw==} + engines: {node: '>=10.13.0'} + peerDependencies: + webpack: ^5.20.0 + dependencies: + '@types/html-minifier-terser': 6.1.0 + html-minifier-terser: 6.1.0 + lodash: 4.17.21 + pretty-error: 4.0.0 + tapable: 2.2.1 + webpack: 5.76.3(@swc/core@1.3.42) + dev: false + + /htmlparser2@6.1.0: + resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==} + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + domutils: 2.8.0 + entities: 2.2.0 + + /htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.0.1 + entities: 4.4.0 + dev: true + + /http-cache-semantics@4.1.1: + resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} + dev: true + + /http-deceiver@1.2.7: + resolution: {integrity: sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==} + + /http-errors@1.6.3: + resolution: {integrity: sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==} + engines: {node: '>= 0.6'} + dependencies: + depd: 1.1.2 + inherits: 2.0.3 + setprototypeof: 1.1.0 + statuses: 1.5.0 + + /http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + /http-parser-js@0.5.8: + resolution: {integrity: sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==} + + /http-proxy-agent@4.0.1: + resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==} + engines: {node: '>= 6'} + dependencies: + '@tootallnate/once': 1.1.2 + agent-base: 6.0.2 + debug: 4.3.4(supports-color@6.1.0) + transitivePeerDependencies: + - supports-color + dev: false + + /http-proxy-middleware@0.19.1(debug@4.3.4)(supports-color@6.1.0): + resolution: {integrity: sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==} + engines: {node: '>=4.0.0'} + dependencies: + http-proxy: 1.18.1(debug@4.3.4) + is-glob: 4.0.3 + lodash: 4.17.21 + micromatch: 3.1.10(supports-color@6.1.0) + transitivePeerDependencies: + - debug + - supports-color + dev: true + + /http-proxy-middleware@1.3.1: + resolution: {integrity: sha512-13eVVDYS4z79w7f1+NPllJtOQFx/FdUW4btIvVRMaRlUY9VGstAbo5MOhLEuUgZFRHn3x50ufn25zkj/boZnEg==} + engines: {node: '>=8.0.0'} + dependencies: + '@types/http-proxy': 1.17.10 + http-proxy: 1.18.1(debug@4.3.4) + is-glob: 4.0.3 + is-plain-obj: 3.0.0 + micromatch: 4.0.5 + transitivePeerDependencies: + - debug + dev: true + + /http-proxy-middleware@2.0.6(@types/express@4.17.17): + resolution: {integrity: sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/express': ^4.17.13 + peerDependenciesMeta: + '@types/express': + optional: true + dependencies: + '@types/express': 4.17.17 + '@types/http-proxy': 1.17.10 + http-proxy: 1.18.1(debug@4.3.4) + is-glob: 4.0.3 + is-plain-obj: 3.0.0 + micromatch: 4.0.5 + transitivePeerDependencies: + - debug + dev: false + + /http-proxy@1.18.1(debug@4.3.4): + resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} + engines: {node: '>=8.0.0'} + dependencies: + eventemitter3: 4.0.7 + follow-redirects: 1.15.6(debug@4.3.4) + requires-port: 1.0.0 + transitivePeerDependencies: + - debug + + /http-signature@1.2.0: + resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==} + engines: {node: '>=0.8', npm: '>=1.3.7'} + dependencies: + assert-plus: 1.0.0 + jsprim: 1.4.2 + sshpk: 1.17.0 + dev: true + + /https-browserify@1.0.0: + resolution: {integrity: sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==} + dev: true + + /https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + dependencies: + agent-base: 6.0.2 + debug: 4.3.4(supports-color@6.1.0) + transitivePeerDependencies: + - supports-color + dev: false + + /human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + dev: false + + /iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + + /iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: false + + /icss-replace-symbols@1.1.0: + resolution: {integrity: sha512-chIaY3Vh2mh2Q3RGXttaDIzeiPvaVXJ+C4DAh/w3c37SKZ/U6PGMmuicR2EQQp9bKG8zLMCl7I+PtIoOOPp8Gg==} + dev: true + + /icss-utils@4.1.1: + resolution: {integrity: sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==} + engines: {node: '>= 6'} + dependencies: + postcss: 7.0.39 + dev: true + + /icss-utils@5.1.0(postcss@8.4.21): + resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + dependencies: + postcss: 8.4.21 + dev: false + + /idb@7.1.1: + resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==} + dev: false + + /identity-obj-proxy@3.0.0: + resolution: {integrity: sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==} + engines: {node: '>=4'} + dependencies: + harmony-reflect: 1.6.2 + dev: false + + /ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + dev: true + + /iferr@0.1.5: + resolution: {integrity: sha512-DUNFN5j7Tln0D+TxzloUjKB+CtVu6myn0JEFak6dG18mNt9YkQ6lzGCdafwofISZ1lLF3xRHJ98VKy9ynkcFaA==} + dev: true + + /ignore@3.3.10: + resolution: {integrity: sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==} + dev: true + + /ignore@4.0.6: + resolution: {integrity: sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==} + engines: {node: '>= 4'} + dev: true + + /ignore@5.2.4: + resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} + engines: {node: '>= 4'} + + /ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + dev: false + + /immediate@3.3.0: + resolution: {integrity: sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==} + dev: true + + /immer@9.0.21: + resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==} + dev: false + + /import-cwd@2.1.0: + resolution: {integrity: sha512-Ew5AZzJQFqrOV5BTW3EIoHAnoie1LojZLXKcCQ/yTRyVZosBhK1x1ViYjHGf5pAFOq8ZyChZp6m/fSN7pJyZtg==} + engines: {node: '>=4'} + dependencies: + import-from: 2.1.0 + dev: true + + /import-fresh@2.0.0: + resolution: {integrity: sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==} + engines: {node: '>=4'} + dependencies: + caller-path: 2.0.0 + resolve-from: 3.0.0 + dev: true + + /import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + /import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + dev: false + + /import-from@2.1.0: + resolution: {integrity: sha512-0vdnLL2wSGnhlRmzHJAg5JHjt1l2vYhzJ7tNLGbeVg0fse56tpGaH0uzH+r9Slej+BSXXEHvBKDEnVSLLE9/+w==} + engines: {node: '>=4'} + dependencies: + resolve-from: 3.0.0 + dev: true + + /import-lazy@2.1.0: + resolution: {integrity: sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A==} + engines: {node: '>=4'} + dev: true + + /import-local@2.0.0: + resolution: {integrity: sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==} + engines: {node: '>=6'} + hasBin: true + dependencies: + pkg-dir: 3.0.0 + resolve-cwd: 2.0.0 + dev: true + + /import-local@3.1.0: + resolution: {integrity: sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==} + engines: {node: '>=8'} + hasBin: true + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + dev: false + + /imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + /indexes-of@1.0.1: + resolution: {integrity: sha512-bup+4tap3Hympa+JBJUG7XuOsdNQ6fxt0MHyXMKuLBKn0OqsTfvUxkUrroEX1+B2VsSHvCjiIcZVxRtYa4nllA==} + dev: true + + /infer-owner@1.0.4: + resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==} + dev: true + + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + /inherits@2.0.1: + resolution: {integrity: sha512-8nWq2nLTAwd02jTqJExUYFSD/fKq6VH9Y/oG2accc/kdI0V98Bag8d5a4gi3XHz73rDWa2PvTtvcWYquKqSENA==} + dev: true + + /inherits@2.0.3: + resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==} + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + /ini@1.3.7: + resolution: {integrity: sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==} + dev: true + + /ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + /internal-ip@4.3.0: + resolution: {integrity: sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg==} + engines: {node: '>=6'} + dependencies: + default-gateway: 4.2.0 + ipaddr.js: 1.9.1 + dev: true + + /internal-slot@1.0.5: + resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.0 + has: 1.0.3 + side-channel: 1.0.4 + + /ip-regex@2.1.0: + resolution: {integrity: sha512-58yWmlHpp7VYfcdTwMTvwMmqx/Elfxjd9RXTDyMsbL7lLWmhMylLEqiYVLKuLzOZqVgiWXD9MfR62Vv89VRxkw==} + engines: {node: '>=4'} + dev: true + + /ip@1.1.8: + resolution: {integrity: sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==} + dev: true + + /ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + /ipaddr.js@2.0.1: + resolution: {integrity: sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==} + engines: {node: '>= 10'} + dev: false + + /is-absolute-url@2.1.0: + resolution: {integrity: sha512-vOx7VprsKyllwjSkLV79NIhpyLfr3jAp7VaTCMXOJHu4m0Ew1CZ2fcjASwmV1jI3BWuWHB013M48eyeldk9gYg==} + engines: {node: '>=0.10.0'} + dev: true + + /is-absolute-url@3.0.3: + resolution: {integrity: sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==} + engines: {node: '>=8'} + dev: true + + /is-accessor-descriptor@0.1.6: + resolution: {integrity: sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==} + engines: {node: '>=0.10.0'} + dependencies: + kind-of: 3.2.2 + dev: true + + /is-accessor-descriptor@1.0.0: + resolution: {integrity: sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==} + engines: {node: '>=0.10.0'} + dependencies: + kind-of: 6.0.3 + dev: true + + /is-alphabetical@1.0.4: + resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} + dev: true + + /is-alphanumerical@1.0.4: + resolution: {integrity: sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==} + dependencies: + is-alphabetical: 1.0.4 + is-decimal: 1.0.4 + dev: true + + /is-arguments@1.1.1: + resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + + /is-array-buffer@3.0.2: + resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.0 + is-typed-array: 1.1.10 + + /is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + /is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + dev: true + + /is-bigint@1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + dependencies: + has-bigints: 1.0.2 + + /is-binary-path@1.0.1: + resolution: {integrity: sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==} + engines: {node: '>=0.10.0'} + requiresBuild: true + dependencies: + binary-extensions: 1.13.1 + dev: true + + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 + + /is-boolean-object@1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + + /is-buffer@1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + dev: true + + /is-builtin-module@3.2.1: + resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} + engines: {node: '>=6'} + dependencies: + builtin-modules: 3.3.0 + dev: true + + /is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + /is-ci@2.0.0: + resolution: {integrity: sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==} + hasBin: true + dependencies: + ci-info: 2.0.0 + dev: true + + /is-color-stop@1.1.0: + resolution: {integrity: sha512-H1U8Vz0cfXNujrJzEcvvwMDW9Ra+biSYA3ThdQvAnMLJkEHQXn6bWzLkxHtVYJ+Sdbx0b6finn3jZiaVe7MAHA==} + dependencies: + css-color-names: 0.0.4 + hex-color-regex: 1.1.0 + hsl-regex: 1.0.0 + hsla-regex: 1.0.0 + rgb-regex: 1.0.1 + rgba-regex: 1.0.0 + dev: true + + /is-core-module@2.11.0: + resolution: {integrity: sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==} + dependencies: + has: 1.0.3 + + /is-data-descriptor@0.1.4: + resolution: {integrity: sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==} + engines: {node: '>=0.10.0'} + dependencies: + kind-of: 3.2.2 + dev: true + + /is-data-descriptor@1.0.0: + resolution: {integrity: sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==} + engines: {node: '>=0.10.0'} + dependencies: + kind-of: 6.0.3 + dev: true + + /is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + + /is-decimal@1.0.4: + resolution: {integrity: sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==} + dev: true + + /is-descriptor@0.1.6: + resolution: {integrity: sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==} + engines: {node: '>=0.10.0'} + dependencies: + is-accessor-descriptor: 0.1.6 + is-data-descriptor: 0.1.4 + kind-of: 5.1.0 + dev: true + + /is-descriptor@1.0.2: + resolution: {integrity: sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==} + engines: {node: '>=0.10.0'} + dependencies: + is-accessor-descriptor: 1.0.0 + is-data-descriptor: 1.0.0 + kind-of: 6.0.3 + dev: true + + /is-directory@0.3.1: + resolution: {integrity: sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==} + engines: {node: '>=0.10.0'} + dev: true + + /is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + /is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + dev: true + + /is-extendable@1.0.1: + resolution: {integrity: sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==} + engines: {node: '>=0.10.0'} + dependencies: + is-plain-object: 2.0.4 + dev: true + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + /is-fullwidth-code-point@2.0.0: + resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==} + engines: {node: '>=4'} + dev: true + + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + /is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + dev: false + + /is-glob@3.1.0: + resolution: {integrity: sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==} + engines: {node: '>=0.10.0'} + requiresBuild: true + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + + /is-hexadecimal@1.0.4: + resolution: {integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==} + dev: true + + /is-installed-globally@0.3.2: + resolution: {integrity: sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==} + engines: {node: '>=8'} + dependencies: + global-dirs: 2.1.0 + is-path-inside: 3.0.3 + dev: true + + /is-map@2.0.2: + resolution: {integrity: sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==} + dev: false + + /is-module@1.0.0: + resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + + /is-negative-zero@2.0.2: + resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} + engines: {node: '>= 0.4'} + + /is-npm@4.0.0: + resolution: {integrity: sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==} + engines: {node: '>=8'} + dev: true + + /is-number-object@1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + + /is-number@3.0.0: + resolution: {integrity: sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==} + engines: {node: '>=0.10.0'} + dependencies: + kind-of: 3.2.2 + dev: true + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + /is-obj@1.0.1: + resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==} + engines: {node: '>=0.10.0'} + + /is-obj@2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} + dev: true + + /is-path-cwd@2.2.0: + resolution: {integrity: sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==} + engines: {node: '>=6'} + dev: true + + /is-path-in-cwd@2.1.0: + resolution: {integrity: sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==} + engines: {node: '>=6'} + dependencies: + is-path-inside: 2.1.0 + dev: true + + /is-path-inside@2.1.0: + resolution: {integrity: sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==} + engines: {node: '>=6'} + dependencies: + path-is-inside: 1.0.2 + dev: true + + /is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + /is-plain-obj@1.1.0: + resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} + engines: {node: '>=0.10.0'} + dev: true + + /is-plain-obj@3.0.0: + resolution: {integrity: sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==} + engines: {node: '>=10'} + + /is-plain-object@2.0.4: + resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} + engines: {node: '>=0.10.0'} + dependencies: + isobject: 3.0.1 + dev: true + + /is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + dev: false + + /is-reference@1.2.1: + resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} + dependencies: + '@types/estree': 1.0.0 + dev: true + + /is-regex@1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + + /is-regexp@1.0.0: + resolution: {integrity: sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==} + engines: {node: '>=0.10.0'} + + /is-regexp@2.1.0: + resolution: {integrity: sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA==} + engines: {node: '>=6'} + dev: true + + /is-resolvable@1.1.0: + resolution: {integrity: sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==} + dev: true + + /is-root@2.1.0: + resolution: {integrity: sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==} + engines: {node: '>=6'} + dev: false + + /is-set@2.0.2: + resolution: {integrity: sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==} + dev: false + + /is-shared-array-buffer@1.0.2: + resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} + dependencies: + call-bind: 1.0.2 + + /is-stream@1.1.0: + resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + dev: false + + /is-string@1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + + /is-symbol@1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + + /is-typed-array@1.1.10: + resolution: {integrity: sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + + /is-typedarray@1.0.0: + resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} + + /is-weakmap@2.0.1: + resolution: {integrity: sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==} + dev: false + + /is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + dependencies: + call-bind: 1.0.2 + + /is-weakset@2.0.2: + resolution: {integrity: sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.0 + dev: false + + /is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + dev: true + + /is-wsl@1.1.0: + resolution: {integrity: sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==} + engines: {node: '>=4'} + dev: true + + /is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + dependencies: + is-docker: 2.2.1 + + /is-yarn-global@0.3.0: + resolution: {integrity: sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==} + dev: true + + /isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + /isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + /isbinaryfile@4.0.10: + resolution: {integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==} + engines: {node: '>= 8.0.0'} + dev: true + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + /isobject@2.1.0: + resolution: {integrity: sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==} + engines: {node: '>=0.10.0'} + dependencies: + isarray: 1.0.0 + dev: true + + /isobject@3.0.1: + resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} + engines: {node: '>=0.10.0'} + dev: true + + /isstream@0.1.2: + resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} + dev: true + + /istanbul-lib-coverage@3.2.0: + resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==} + engines: {node: '>=8'} + + /istanbul-lib-instrument@5.2.1: + resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} + engines: {node: '>=8'} + dependencies: + '@babel/core': 7.21.3 + '@babel/parser': 7.21.3 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.0 + semver: 6.3.0 + transitivePeerDependencies: + - supports-color + + /istanbul-lib-report@3.0.0: + resolution: {integrity: sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==} + engines: {node: '>=8'} + dependencies: + istanbul-lib-coverage: 3.2.0 + make-dir: 3.1.0 + supports-color: 7.2.0 + + /istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + dependencies: + debug: 4.3.4(supports-color@6.1.0) + istanbul-lib-coverage: 3.2.0 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + + /istanbul-reports@3.1.5: + resolution: {integrity: sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==} + engines: {node: '>=8'} + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.0 + + /jake@10.8.5: + resolution: {integrity: sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==} + engines: {node: '>=10'} + hasBin: true + dependencies: + async: 3.2.4 + chalk: 4.1.2 + filelist: 1.0.4 + minimatch: 3.1.2 + dev: false + + /jasmine-core@3.99.1: + resolution: {integrity: sha512-Hu1dmuoGcZ7AfyynN3LsfruwMbxMALMka+YtZeGoLuDEySVmVAPaonkNoBRIw/ectu8b9tVQCJNgp4a4knp+tg==} + dev: true + + /jasmine@3.99.0: + resolution: {integrity: sha512-YIThBuHzaIIcjxeuLmPD40SjxkEcc8i//sGMDKCgkRMVgIwRJf5qyExtlJpQeh7pkeoBSOe6lQEdg+/9uKg9mw==} + hasBin: true + dependencies: + glob: 7.2.3 + jasmine-core: 3.99.1 + dev: true + + /javascript-stringify@1.6.0: + resolution: {integrity: sha512-fnjC0up+0SjEJtgmmG+teeel68kutkvzfctO/KxE3qJlbunkJYAshgH3boU++gSBHP8z5/r0ts0qRIrHf0RTQQ==} + dev: true + + /javascript-stringify@2.1.0: + resolution: {integrity: sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==} + dev: true + + /jest-changed-files@27.5.1: + resolution: {integrity: sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + '@jest/types': 27.5.1 + execa: 5.1.1 + throat: 6.0.2 + dev: false + + /jest-circus@27.5.1: + resolution: {integrity: sha512-D95R7x5UtlMA5iBYsOHFFbMD/GVA4R/Kdq15f7xYWUfWHBto9NYRsOvnSauTgdF+ogCpJ4tyKOXhUifxS65gdw==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + '@jest/environment': 27.5.1 + '@jest/test-result': 27.5.1 + '@jest/types': 27.5.1 + '@types/node': 18.15.11 + chalk: 4.1.2 + co: 4.6.0 + dedent: 0.7.0 + expect: 27.5.1 + is-generator-fn: 2.1.0 + jest-each: 27.5.1 + jest-matcher-utils: 27.5.1 + jest-message-util: 27.5.1 + jest-runtime: 27.5.1 + jest-snapshot: 27.5.1 + jest-util: 27.5.1 + pretty-format: 27.5.1 + slash: 3.0.0 + stack-utils: 2.0.6 + throat: 6.0.2 + transitivePeerDependencies: + - supports-color + dev: false + + /jest-cli@27.5.1: + resolution: {integrity: sha512-Hc6HOOwYq4/74/c62dEE3r5elx8wjYqxY0r0G/nFrLDPMFRu6RA/u8qINOIkvhxG7mMQ5EJsOGfRpI8L6eFUVw==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 27.5.1 + '@jest/test-result': 27.5.1 + '@jest/types': 27.5.1 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + import-local: 3.1.0 + jest-config: 27.5.1 + jest-util: 27.5.1 + jest-validate: 27.5.1 + prompts: 2.4.2 + yargs: 16.2.0 + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - ts-node + - utf-8-validate + dev: false + + /jest-config@27.5.1: + resolution: {integrity: sha512-5sAsjm6tGdsVbW9ahcChPAFCk4IlkQUknH5AvKjuLTSlcO/wCZKyFdn7Rg0EkC+OGgWODEy2hDpWB1PgzH0JNA==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + peerDependencies: + ts-node: '>=9.0.0' + peerDependenciesMeta: + ts-node: + optional: true + dependencies: + '@babel/core': 7.21.3 + '@jest/test-sequencer': 27.5.1 + '@jest/types': 27.5.1 + babel-jest: 27.5.1(@babel/core@7.21.3) + chalk: 4.1.2 + ci-info: 3.8.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 27.5.1 + jest-environment-jsdom: 27.5.1 + jest-environment-node: 27.5.1 + jest-get-type: 27.5.1 + jest-jasmine2: 27.5.1 + jest-regex-util: 27.5.1 + jest-resolve: 27.5.1 + jest-runner: 27.5.1 + jest-util: 27.5.1 + jest-validate: 27.5.1 + micromatch: 4.0.5 + parse-json: 5.2.0 + pretty-format: 27.5.1 + slash: 3.0.0 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - utf-8-validate + dev: false + + /jest-diff@27.5.1: + resolution: {integrity: sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + chalk: 4.1.2 + diff-sequences: 27.5.1 + jest-get-type: 27.5.1 + pretty-format: 27.5.1 + dev: false + + /jest-docblock@27.5.1: + resolution: {integrity: sha512-rl7hlABeTsRYxKiUfpHrQrG4e2obOiTQWfMEH3PxPjOtdsfLQO4ReWSZaQ7DETm4xu07rl4q/h4zcKXyU0/OzQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + detect-newline: 3.1.0 + dev: false + + /jest-each@27.5.1: + resolution: {integrity: sha512-1Ff6p+FbhT/bXQnEouYy00bkNSY7OUpfIcmdl8vZ31A1UUaurOLPA8a8BbJOF2RDUElwJhmeaV7LnagI+5UwNQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + '@jest/types': 27.5.1 + chalk: 4.1.2 + jest-get-type: 27.5.1 + jest-util: 27.5.1 + pretty-format: 27.5.1 + dev: false + + /jest-environment-jsdom@27.5.1: + resolution: {integrity: sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + '@jest/environment': 27.5.1 + '@jest/fake-timers': 27.5.1 + '@jest/types': 27.5.1 + '@types/node': 18.15.11 + jest-mock: 27.5.1 + jest-util: 27.5.1 + jsdom: 16.7.0 + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - utf-8-validate + dev: false + + /jest-environment-node@27.5.1: + resolution: {integrity: sha512-Jt4ZUnxdOsTGwSRAfKEnE6BcwsSPNOijjwifq5sDFSA2kesnXTvNqKHYgM0hDq3549Uf/KzdXNYn4wMZJPlFLw==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + '@jest/environment': 27.5.1 + '@jest/fake-timers': 27.5.1 + '@jest/types': 27.5.1 + '@types/node': 18.15.11 + jest-mock: 27.5.1 + jest-util: 27.5.1 + dev: false + + /jest-get-type@27.5.1: + resolution: {integrity: sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dev: false + + /jest-haste-map@27.5.1: + resolution: {integrity: sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + '@jest/types': 27.5.1 + '@types/graceful-fs': 4.1.6 + '@types/node': 18.15.11 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 27.5.1 + jest-serializer: 27.5.1 + jest-util: 27.5.1 + jest-worker: 27.5.1 + micromatch: 4.0.5 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + dev: false + + /jest-jasmine2@27.5.1: + resolution: {integrity: sha512-jtq7VVyG8SqAorDpApwiJJImd0V2wv1xzdheGHRGyuT7gZm6gG47QEskOlzsN1PG/6WNaCo5pmwMHDf3AkG2pQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + '@jest/environment': 27.5.1 + '@jest/source-map': 27.5.1 + '@jest/test-result': 27.5.1 + '@jest/types': 27.5.1 + '@types/node': 18.15.11 + chalk: 4.1.2 + co: 4.6.0 + expect: 27.5.1 + is-generator-fn: 2.1.0 + jest-each: 27.5.1 + jest-matcher-utils: 27.5.1 + jest-message-util: 27.5.1 + jest-runtime: 27.5.1 + jest-snapshot: 27.5.1 + jest-util: 27.5.1 + pretty-format: 27.5.1 + throat: 6.0.2 + transitivePeerDependencies: + - supports-color + dev: false + + /jest-leak-detector@27.5.1: + resolution: {integrity: sha512-POXfWAMvfU6WMUXftV4HolnJfnPOGEu10fscNCA76KBpRRhcMN2c8d3iT2pxQS3HLbA+5X4sOUPzYO2NUyIlHQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + jest-get-type: 27.5.1 + pretty-format: 27.5.1 + dev: false + + /jest-matcher-utils@27.5.1: + resolution: {integrity: sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + chalk: 4.1.2 + jest-diff: 27.5.1 + jest-get-type: 27.5.1 + pretty-format: 27.5.1 + dev: false + + /jest-message-util@27.5.1: + resolution: {integrity: sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + '@babel/code-frame': 7.18.6 + '@jest/types': 27.5.1 + '@types/stack-utils': 2.0.1 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.5 + pretty-format: 27.5.1 + slash: 3.0.0 + stack-utils: 2.0.6 + dev: false + + /jest-message-util@28.1.3: + resolution: {integrity: sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@babel/code-frame': 7.18.6 + '@jest/types': 28.1.3 + '@types/stack-utils': 2.0.1 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.5 + pretty-format: 28.1.3 + slash: 3.0.0 + stack-utils: 2.0.6 + dev: false + + /jest-mock@27.5.1: + resolution: {integrity: sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + '@jest/types': 27.5.1 + '@types/node': 18.15.11 + dev: false + + /jest-pnp-resolver@1.2.3(jest-resolve@27.5.1): + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + dependencies: + jest-resolve: 27.5.1 + dev: false + + /jest-regex-util@27.5.1: + resolution: {integrity: sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dev: false + + /jest-regex-util@28.0.2: + resolution: {integrity: sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dev: false + + /jest-resolve-dependencies@27.5.1: + resolution: {integrity: sha512-QQOOdY4PE39iawDn5rzbIePNigfe5B9Z91GDD1ae/xNDlu9kaat8QQ5EKnNmVWPV54hUdxCVwwj6YMgR2O7IOg==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + '@jest/types': 27.5.1 + jest-regex-util: 27.5.1 + jest-snapshot: 27.5.1 + transitivePeerDependencies: + - supports-color + dev: false + + /jest-resolve@27.5.1: + resolution: {integrity: sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + '@jest/types': 27.5.1 + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 27.5.1 + jest-pnp-resolver: 1.2.3(jest-resolve@27.5.1) + jest-util: 27.5.1 + jest-validate: 27.5.1 + resolve: 1.22.1 + resolve.exports: 1.1.1 + slash: 3.0.0 + dev: false + + /jest-runner@27.5.1: + resolution: {integrity: sha512-g4NPsM4mFCOwFKXO4p/H/kWGdJp9V8kURY2lX8Me2drgXqG7rrZAx5kv+5H7wtt/cdFIjhqYx1HrlqWHaOvDaQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + '@jest/console': 27.5.1 + '@jest/environment': 27.5.1 + '@jest/test-result': 27.5.1 + '@jest/transform': 27.5.1 + '@jest/types': 27.5.1 + '@types/node': 18.15.11 + chalk: 4.1.2 + emittery: 0.8.1 + graceful-fs: 4.2.11 + jest-docblock: 27.5.1 + jest-environment-jsdom: 27.5.1 + jest-environment-node: 27.5.1 + jest-haste-map: 27.5.1 + jest-leak-detector: 27.5.1 + jest-message-util: 27.5.1 + jest-resolve: 27.5.1 + jest-runtime: 27.5.1 + jest-util: 27.5.1 + jest-worker: 27.5.1 + source-map-support: 0.5.21 + throat: 6.0.2 + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - utf-8-validate + dev: false + + /jest-runtime@27.5.1: + resolution: {integrity: sha512-o7gxw3Gf+H2IGt8fv0RiyE1+r83FJBRruoA+FXrlHw6xEyBsU8ugA6IPfTdVyA0w8HClpbK+DGJxH59UrNMx8A==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + '@jest/environment': 27.5.1 + '@jest/fake-timers': 27.5.1 + '@jest/globals': 27.5.1 + '@jest/source-map': 27.5.1 + '@jest/test-result': 27.5.1 + '@jest/transform': 27.5.1 + '@jest/types': 27.5.1 + chalk: 4.1.2 + cjs-module-lexer: 1.2.2 + collect-v8-coverage: 1.0.1 + execa: 5.1.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-haste-map: 27.5.1 + jest-message-util: 27.5.1 + jest-mock: 27.5.1 + jest-regex-util: 27.5.1 + jest-resolve: 27.5.1 + jest-snapshot: 27.5.1 + jest-util: 27.5.1 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /jest-serializer@27.5.1: + resolution: {integrity: sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + '@types/node': 18.15.11 + graceful-fs: 4.2.11 + dev: false + + /jest-snapshot@27.5.1: + resolution: {integrity: sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + '@babel/core': 7.21.3 + '@babel/generator': 7.21.3 + '@babel/plugin-syntax-typescript': 7.20.0(@babel/core@7.21.3) + '@babel/traverse': 7.21.3 + '@babel/types': 7.21.3 + '@jest/transform': 27.5.1 + '@jest/types': 27.5.1 + '@types/babel__traverse': 7.18.3 + '@types/prettier': 2.7.2 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.21.3) + chalk: 4.1.2 + expect: 27.5.1 + graceful-fs: 4.2.11 + jest-diff: 27.5.1 + jest-get-type: 27.5.1 + jest-haste-map: 27.5.1 + jest-matcher-utils: 27.5.1 + jest-message-util: 27.5.1 + jest-util: 27.5.1 + natural-compare: 1.4.0 + pretty-format: 27.5.1 + semver: 7.3.8 + transitivePeerDependencies: + - supports-color + dev: false + + /jest-util@27.5.1: + resolution: {integrity: sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + '@jest/types': 27.5.1 + '@types/node': 18.15.11 + chalk: 4.1.2 + ci-info: 3.8.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + dev: false + + /jest-util@28.1.3: + resolution: {integrity: sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@jest/types': 28.1.3 + '@types/node': 18.15.11 + chalk: 4.1.2 + ci-info: 3.8.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + dev: false + + /jest-validate@27.5.1: + resolution: {integrity: sha512-thkNli0LYTmOI1tDB3FI1S1RTp/Bqyd9pTarJwL87OIBFuqEb5Apv5EaApEudYg4g86e3CT6kM0RowkhtEnCBQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + '@jest/types': 27.5.1 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 27.5.1 + leven: 3.1.0 + pretty-format: 27.5.1 + dev: false + + /jest-watch-typeahead@1.1.0(jest@27.5.1): + resolution: {integrity: sha512-Va5nLSJTN7YFtC2jd+7wsoe1pNe5K4ShLux/E5iHEwlB9AxaxmggY7to9KUqKojhaJw3aXqt5WAb4jGPOolpEw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + jest: ^27.0.0 || ^28.0.0 + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + jest: 27.5.1 + jest-regex-util: 28.0.2 + jest-watcher: 28.1.3 + slash: 4.0.0 + string-length: 5.0.1 + strip-ansi: 7.0.1 + dev: false + + /jest-watcher@27.5.1: + resolution: {integrity: sha512-z676SuD6Z8o8qbmEGhoEUFOM1+jfEiL3DXHK/xgEiG2EyNYfFG60jluWcupY6dATjfEsKQuibReS1djInQnoVw==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + '@jest/test-result': 27.5.1 + '@jest/types': 27.5.1 + '@types/node': 18.15.11 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + jest-util: 27.5.1 + string-length: 4.0.2 + dev: false + + /jest-watcher@28.1.3: + resolution: {integrity: sha512-t4qcqj9hze+jviFPUN3YAtAEeFnr/azITXQEMARf5cMwKY2SMBRnCQTXLixTl20OR6mLh9KLMrgVJgJISym+1g==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@jest/test-result': 28.1.3 + '@jest/types': 28.1.3 + '@types/node': 18.15.11 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.10.2 + jest-util: 28.1.3 + string-length: 4.0.2 + dev: false + + /jest-worker@26.6.2: + resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==} + engines: {node: '>= 10.13.0'} + dependencies: + '@types/node': 18.15.11 + merge-stream: 2.0.0 + supports-color: 7.2.0 + + /jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} + dependencies: + '@types/node': 18.15.11 + merge-stream: 2.0.0 + supports-color: 8.1.1 + dev: false + + /jest-worker@28.1.3: + resolution: {integrity: sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@types/node': 18.15.11 + merge-stream: 2.0.0 + supports-color: 8.1.1 + dev: false + + /jest@27.5.1: + resolution: {integrity: sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 27.5.1 + import-local: 3.1.0 + jest-cli: 27.5.1 + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - ts-node + - utf-8-validate + dev: false + + /jiti@1.18.2: + resolution: {integrity: sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==} + hasBin: true + dev: false + + /js-cleanup@1.2.0: + resolution: {integrity: sha512-JeDD0yiiSt80fXzAVa/crrS0JDPQljyBG/RpOtaSbyDq03VHa9szJWMaWOYU/bcTn412uMN2MxApXq8v79cUiQ==} + engines: {node: ^10.14.2 || >=12.0.0} + dependencies: + magic-string: 0.25.9 + perf-regexes: 1.0.1 + skip-regex: 1.0.2 + dev: true + + /js-sdsl@4.4.0: + resolution: {integrity: sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==} + dev: true + + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + /js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + /js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + dependencies: + argparse: 2.0.1 + dev: true + + /js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + dependencies: + argparse: 2.0.1 + dev: false + + /jsbn@0.1.1: + resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==} + dev: true + + /jsdom@16.7.0: + resolution: {integrity: sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==} + engines: {node: '>=10'} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + dependencies: + abab: 2.0.6 + acorn: 8.8.2 + acorn-globals: 6.0.0 + cssom: 0.4.4 + cssstyle: 2.3.0 + data-urls: 2.0.0 + decimal.js: 10.4.3 + domexception: 2.0.1 + escodegen: 2.0.0 + form-data: 3.0.1 + html-encoding-sniffer: 2.0.1 + http-proxy-agent: 4.0.1 + https-proxy-agent: 5.0.1 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.2 + parse5: 6.0.1 + saxes: 5.0.1 + symbol-tree: 3.2.4 + tough-cookie: 4.1.2 + w3c-hr-time: 1.0.2 + w3c-xmlserializer: 2.0.0 + webidl-conversions: 6.1.0 + whatwg-encoding: 1.0.5 + whatwg-mimetype: 2.3.0 + whatwg-url: 8.7.0 + ws: 7.5.9 + xml-name-validator: 3.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + + /jsesc@0.5.0: + resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} + hasBin: true + + /jsesc@2.5.2: + resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} + engines: {node: '>=4'} + hasBin: true + + /json-buffer@3.0.0: + resolution: {integrity: sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==} + dev: true + + /json-parse-better-errors@1.0.2: + resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} + dev: true + + /json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + dev: false + + /json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + /json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + dev: false + + /json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + + /json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + /json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + dev: true + + /json5@0.5.1: + resolution: {integrity: sha512-4xrs1aW+6N5DalkqSVA8fxh458CXvR99WU8WLKmq4v8eWAL86Xo3BVqyd3SkA9wEVjCMqyvvRRkshAdOnBp5rw==} + hasBin: true + dev: true + + /json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + dependencies: + minimist: 1.2.8 + + /json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + /jsonc-parser@3.2.0: + resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} + dev: true + + /jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + optionalDependencies: + graceful-fs: 4.2.11 + dev: true + + /jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + dependencies: + universalify: 2.0.0 + optionalDependencies: + graceful-fs: 4.2.11 + dev: false + + /jsonpointer@5.0.1: + resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} + engines: {node: '>=0.10.0'} + dev: false + + /jsprim@1.4.2: + resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==} + engines: {node: '>=0.6.0'} + dependencies: + assert-plus: 1.0.0 + extsprintf: 1.3.0 + json-schema: 0.4.0 + verror: 1.10.0 + dev: true + + /jsx-ast-utils@3.3.3: + resolution: {integrity: sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==} + engines: {node: '>=4.0'} + dependencies: + array-includes: 3.1.6 + object.assign: 4.1.4 + dev: false + + /karma-chrome-launcher@3.1.1: + resolution: {integrity: sha512-hsIglcq1vtboGPAN+DGCISCFOxW+ZVnIqhDQcCMqqCp+4dmJ0Qpq5QAjkbA0X2L9Mi6OBkHi2Srrbmm7pUKkzQ==} + dependencies: + which: 1.3.1 + dev: true + + /karma-coverage@2.2.0: + resolution: {integrity: sha512-gPVdoZBNDZ08UCzdMHHhEImKrw1+PAOQOIiffv1YsvxFhBjqvo/SVXNk4tqn1SYqX0BJZT6S/59zgxiBe+9OuA==} + engines: {node: '>=10.0.0'} + dependencies: + istanbul-lib-coverage: 3.2.0 + istanbul-lib-instrument: 5.2.1 + istanbul-lib-report: 3.0.0 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.1.5 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: true + + /karma-edge-launcher@0.4.2(karma@6.4.1): + resolution: {integrity: sha512-YAJZb1fmRcxNhMIWYsjLuxwODBjh2cSHgTW/jkVmdpGguJjLbs9ZgIK/tEJsMQcBLUkO+yO4LBbqYxqgGW2HIw==} + engines: {node: '>=4'} + peerDependencies: + karma: '>=0.9' + dependencies: + edge-launcher: 1.2.2 + karma: 6.4.1 + dev: true + + /karma-firefox-launcher@2.1.2: + resolution: {integrity: sha512-VV9xDQU1QIboTrjtGVD4NCfzIH7n01ZXqy/qpBhnOeGVOkG5JYPEm8kuSd7psHE6WouZaQ9Ool92g8LFweSNMA==} + dependencies: + is-wsl: 2.2.0 + which: 2.0.2 + dev: true + + /karma-jasmine-html-reporter@1.7.0(jasmine-core@3.99.1)(karma-jasmine@4.0.2)(karma@6.4.1): + resolution: {integrity: sha512-pzum1TL7j90DTE86eFt48/s12hqwQuiD+e5aXx2Dc9wDEn2LfGq6RoAxEZZjFiN0RDSCOnosEKRZWxbQ+iMpQQ==} + peerDependencies: + jasmine-core: '>=3.8' + karma: '>=0.9' + karma-jasmine: '>=1.1' + dependencies: + jasmine-core: 3.99.1 + karma: 6.4.1 + karma-jasmine: 4.0.2(karma@6.4.1) + dev: true + + /karma-jasmine@4.0.2(karma@6.4.1): + resolution: {integrity: sha512-ggi84RMNQffSDmWSyyt4zxzh2CQGwsxvYYsprgyR1j8ikzIduEdOlcLvXjZGwXG/0j41KUXOWsUCBfbEHPWP9g==} + engines: {node: '>= 10'} + peerDependencies: + karma: '*' + dependencies: + jasmine-core: 3.99.1 + karma: 6.4.1 + dev: true + + /karma-rollup-preprocessor@7.0.7(rollup@3.20.2): + resolution: {integrity: sha512-Y1QwsTCiCBp8sSALZdqmqry/mWIWIy0V6zonUIpy+0/D/Kpb2XZvR+JZrWfacQvcvKQdZFJvg6EwlnKtjepu3Q==} + engines: {node: '>= 8.0.0'} + peerDependencies: + rollup: '>= 1.0.0' + dependencies: + chokidar: 3.5.3 + debounce: 1.2.1 + rollup: 3.20.2 + dev: true + + /karma-safari-private-launcher@1.0.0: + resolution: {integrity: sha512-kscGowncLO6msIm43AU1CPSR9Xas35t/myoSnfUs9Djsh7y/3ORBURxJPu2tAfzsNeTfWACJYO0bYOB5tihsXg==} + dependencies: + applescript: 1.0.0 + dev: true + + /karma-spec-reporter@0.0.32(karma@6.4.1): + resolution: {integrity: sha512-ZXsYERZJMTNRR2F3QN11OWF5kgnT/K2dzhM+oY3CDyMrDI3TjIWqYGG7c15rR9wjmy9lvdC+CCshqn3YZqnNrA==} + peerDependencies: + karma: '>=0.9' + dependencies: + colors: 1.4.0 + karma: 6.4.1 + dev: true + + /karma@6.4.1: + resolution: {integrity: sha512-Cj57NKOskK7wtFWSlMvZf459iX+kpYIPXmkNUzP2WAFcA7nhr/ALn5R7sw3w+1udFDcpMx/tuB8d5amgm3ijaA==} + engines: {node: '>= 10'} + hasBin: true + dependencies: + '@colors/colors': 1.5.0 + body-parser: 1.20.2 + braces: 3.0.2 + chokidar: 3.5.3 + connect: 3.7.0 + di: 0.0.1 + dom-serialize: 2.2.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + http-proxy: 1.18.1(debug@4.3.4) + isbinaryfile: 4.0.10 + lodash: 4.17.21 + log4js: 6.9.1 + mime: 2.6.0 + minimatch: 3.1.2 + mkdirp: 0.5.6 + qjobs: 1.2.0 + range-parser: 1.2.1 + rimraf: 3.0.2 + socket.io: 4.7.5 + source-map: 0.6.1 + tmp: 0.2.1 + ua-parser-js: 0.7.34 + yargs: 16.2.0 + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - utf-8-validate + dev: true + + /keyv@3.1.0: + resolution: {integrity: sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==} + dependencies: + json-buffer: 3.0.0 + dev: true + + /killable@1.0.1: + resolution: {integrity: sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg==} + dev: true + + /kind-of@3.2.2: + resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==} + engines: {node: '>=0.10.0'} + dependencies: + is-buffer: 1.1.6 + dev: true + + /kind-of@4.0.0: + resolution: {integrity: sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==} + engines: {node: '>=0.10.0'} + dependencies: + is-buffer: 1.1.6 + dev: true + + /kind-of@5.1.0: + resolution: {integrity: sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==} + engines: {node: '>=0.10.0'} + dev: true + + /kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + /kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + dev: false + + /klona@2.0.6: + resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} + engines: {node: '>= 8'} + dev: false + + /language-subtag-registry@0.3.22: + resolution: {integrity: sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==} + dev: false + + /language-tags@1.0.5: + resolution: {integrity: sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ==} + dependencies: + language-subtag-registry: 0.3.22 + dev: false + + /last-call-webpack-plugin@3.0.0: + resolution: {integrity: sha512-7KI2l2GIZa9p2spzPIVZBYyNKkN+e/SQPpnjlTiPhdbDW3F86tdKKELxKpzJ5sgU19wQWsACULZmpTPYHeWO5w==} + dependencies: + lodash: 4.17.21 + webpack-sources: 1.4.3 + dev: true + + /latest-version@5.1.0: + resolution: {integrity: sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==} + engines: {node: '>=8'} + dependencies: + package-json: 6.5.0 + dev: true + + /launch-editor@2.6.0: + resolution: {integrity: sha512-JpDCcQnyAAzZZaZ7vEiSqL690w7dAEyLao+KC96zBplnYbJS7TYNjvM3M7y3dGz+v7aIsJk3hllWuc0kWAjyRQ==} + dependencies: + picocolors: 1.0.0 + shell-quote: 1.8.0 + dev: false + + /lcov-parse@1.0.0: + resolution: {integrity: sha512-aprLII/vPzuQvYZnDRU78Fns9I2Ag3gi4Ipga/hxnVMCZC8DnR2nI7XBqrPoywGfxqIx/DgarGvDJZAD3YBTgQ==} + hasBin: true + dev: true + + /leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + dev: false + + /levn@0.3.0: + resolution: {integrity: sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.1.2 + type-check: 0.3.2 + dev: false + + /levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + /lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + + /lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + dev: false + + /linkify-it@2.2.0: + resolution: {integrity: sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==} + dependencies: + uc.micro: 1.0.6 + dev: true + + /linkify-it@3.0.3: + resolution: {integrity: sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==} + dependencies: + uc.micro: 1.0.6 + dev: true + + /load-script@1.0.0: + resolution: {integrity: sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==} + dev: true + + /loader-runner@2.4.0: + resolution: {integrity: sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==} + engines: {node: '>=4.3.0 <5.0.0 || >=5.10'} + dev: true + + /loader-runner@4.3.0: + resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} + engines: {node: '>=6.11.5'} + dev: false + + /loader-utils@0.2.17: + resolution: {integrity: sha512-tiv66G0SmiOx+pLWMtGEkfSEejxvb6N6uRrQjfWJIT79W9GMpgKeCAmm9aVBKtd4WEgntciI8CsGqjpDoCWJug==} + dependencies: + big.js: 3.2.0 + emojis-list: 2.1.0 + json5: 0.5.1 + object-assign: 4.1.1 + dev: true + + /loader-utils@1.4.2: + resolution: {integrity: sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==} + engines: {node: '>=4.0.0'} + dependencies: + big.js: 5.2.2 + emojis-list: 3.0.0 + json5: 1.0.2 + dev: true + + /loader-utils@2.0.4: + resolution: {integrity: sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==} + engines: {node: '>=8.9.0'} + dependencies: + big.js: 5.2.2 + emojis-list: 3.0.0 + json5: 2.2.3 + + /loader-utils@3.2.1: + resolution: {integrity: sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==} + engines: {node: '>= 12.13.0'} + dev: false + + /locate-path@3.0.0: + resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} + engines: {node: '>=6'} + dependencies: + p-locate: 3.0.0 + path-exists: 3.0.0 + + /locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + dependencies: + p-locate: 4.1.0 + + /locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + dependencies: + p-locate: 5.0.0 + + /lodash._reinterpolate@3.0.0: + resolution: {integrity: sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==} + dev: true + + /lodash.clonedeep@4.5.0: + resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} + dev: true + + /lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + + /lodash.kebabcase@4.1.1: + resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} + dev: true + + /lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + + /lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + /lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + dev: false + + /lodash.template@4.5.0: + resolution: {integrity: sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==} + dependencies: + lodash._reinterpolate: 3.0.0 + lodash.templatesettings: 4.2.0 + dev: true + + /lodash.templatesettings@4.2.0: + resolution: {integrity: sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==} + dependencies: + lodash._reinterpolate: 3.0.0 + dev: true + + /lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + /log-driver@1.2.7: + resolution: {integrity: sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==} + engines: {node: '>=0.8.6'} + dev: true + + /log4js@6.9.1: + resolution: {integrity: sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==} + engines: {node: '>=8.0'} + dependencies: + date-format: 4.0.14 + debug: 4.3.4(supports-color@6.1.0) + flatted: 3.2.7 + rfdc: 1.3.0 + streamroller: 3.1.5 + transitivePeerDependencies: + - supports-color + dev: true + + /loglevel@1.8.1: + resolution: {integrity: sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==} + engines: {node: '>= 0.6.0'} + dev: true + + /loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + dependencies: + js-tokens: 4.0.0 + dev: false + + /lower-case@1.1.4: + resolution: {integrity: sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==} + dev: true + + /lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + dependencies: + tslib: 2.5.0 + dev: false + + /lowercase-keys@1.0.1: + resolution: {integrity: sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==} + engines: {node: '>=0.10.0'} + dev: true + + /lowercase-keys@2.0.0: + resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} + engines: {node: '>=8'} + dev: true + + /lru-cache@4.1.5: + resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} + dependencies: + pseudomap: 1.0.2 + yallist: 2.1.2 + dev: true + + /lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + dependencies: + yallist: 3.1.1 + + /lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + dependencies: + yallist: 4.0.0 + + /lunr@2.3.9: + resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} + dev: true + + /luxon@3.3.0: + resolution: {integrity: sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==} + engines: {node: '>=12'} + dev: true + + /magic-string@0.25.9: + resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} + dependencies: + sourcemap-codec: 1.4.8 + + /magic-string@0.27.0: + resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.14 + dev: true + + /make-dir@2.1.0: + resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} + engines: {node: '>=6'} + dependencies: + pify: 4.0.1 + semver: 5.7.1 + dev: true + + /make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + dependencies: + semver: 6.3.0 + + /makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + dependencies: + tmpl: 1.0.5 + dev: false + + /map-cache@0.2.2: + resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==} + engines: {node: '>=0.10.0'} + dev: true + + /map-visit@1.0.0: + resolution: {integrity: sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==} + engines: {node: '>=0.10.0'} + dependencies: + object-visit: 1.0.1 + dev: true + + /markdown-it-anchor@5.3.0(markdown-it@8.4.2): + resolution: {integrity: sha512-/V1MnLL/rgJ3jkMWo84UR+K+jF1cxNG1a+KwqeXqTIJ+jtA8aWSHuigx8lTzauiIjBDbwF3NcWQMotd0Dm39jA==} + peerDependencies: + markdown-it: '*' + dependencies: + markdown-it: 8.4.2 + dev: true + + /markdown-it-chain@1.3.0(markdown-it@8.4.2): + resolution: {integrity: sha512-XClV8I1TKy8L2qsT9iX3qiV+50ZtcInGXI80CA+DP62sMs7hXlyV/RM3hfwy5O3Ad0sJm9xIwQELgANfESo8mQ==} + engines: {node: '>=6.9'} + peerDependencies: + markdown-it: '>=5.0.0' + dependencies: + markdown-it: 8.4.2 + webpack-chain: 4.12.1 + dev: true + + /markdown-it-container@2.0.0: + resolution: {integrity: sha512-IxPOaq2LzrGuFGyYq80zaorXReh2ZHGFOB1/Hen429EJL1XkPI3FJTpx9TsJeua+j2qTru4h3W1TiCRdeivMmA==} + dev: true + + /markdown-it-emoji@1.4.0: + resolution: {integrity: sha512-QCz3Hkd+r5gDYtS2xsFXmBYrgw6KuWcJZLCEkdfAuwzZbShCmCfta+hwAMq4NX/4xPzkSHduMKgMkkPUJxSXNg==} + dev: true + + /markdown-it-include@2.0.0(markdown-it@12.3.2): + resolution: {integrity: sha512-wfgIX92ZEYahYWiCk6Jx36XmHvAimeHN420csOWgfyZjpf171Y0xREqZWcm/Rwjzyd0RLYryY+cbNmrkYW2MDw==} + engines: {node: '>=10'} + peerDependencies: + markdown-it: '>=8.4.2' + dependencies: + markdown-it: 12.3.2 + dev: true + + /markdown-it-table-of-contents@0.4.4: + resolution: {integrity: sha512-TAIHTHPwa9+ltKvKPWulm/beozQU41Ab+FIefRaQV1NRnpzwcV9QOe6wXQS5WLivm5Q/nlo0rl6laGkMDZE7Gw==} + engines: {node: '>6.4.0'} + dev: true + + /markdown-it@12.3.2: + resolution: {integrity: sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==} + hasBin: true + dependencies: + argparse: 2.0.1 + entities: 2.1.0 + linkify-it: 3.0.3 + mdurl: 1.0.1 + uc.micro: 1.0.6 + dev: true + + /markdown-it@8.4.2: + resolution: {integrity: sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==} + hasBin: true + dependencies: + argparse: 1.0.10 + entities: 1.1.2 + linkify-it: 2.2.0 + mdurl: 1.0.1 + uc.micro: 1.0.6 + dev: true + + /marked@4.3.0: + resolution: {integrity: sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==} + engines: {node: '>= 12'} + hasBin: true + dev: true + + /md5.js@1.3.5: + resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==} + dependencies: + hash-base: 3.1.0 + inherits: 2.0.4 + safe-buffer: 5.2.1 + dev: true + + /mdast-util-from-markdown@0.8.5: + resolution: {integrity: sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ==} + dependencies: + '@types/mdast': 3.0.11 + mdast-util-to-string: 2.0.0 + micromark: 2.11.4 + parse-entities: 2.0.0 + unist-util-stringify-position: 2.0.3 + transitivePeerDependencies: + - supports-color + dev: true + + /mdast-util-to-string@2.0.0: + resolution: {integrity: sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==} + dev: true + + /mdn-data@2.0.14: + resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} + + /mdn-data@2.0.4: + resolution: {integrity: sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==} + + /mdurl@1.0.1: + resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==} + dev: true + + /media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + /memfs@3.4.13: + resolution: {integrity: sha512-omTM41g3Skpvx5dSYeZIbXKcXoAVc/AoMNwn9TKx++L/gaen/+4TTttmu8ZSch5vfVJ8uJvGbroTsIlslRg6lg==} + engines: {node: '>= 4.0.0'} + dependencies: + fs-monkey: 1.0.3 + dev: false + + /memory-fs@0.4.1: + resolution: {integrity: sha512-cda4JKCxReDXFXRqOHPQscuIYg1PvxbE2S2GP45rnwfEK+vZaXC8C1OFvdHIbgw0DLzowXGVoxLaAmlgRy14GQ==} + dependencies: + errno: 0.1.8 + readable-stream: 2.3.8 + dev: true + + /memory-fs@0.5.0: + resolution: {integrity: sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==} + engines: {node: '>=4.3.0 <5.0.0 || >=5.10'} + dependencies: + errno: 0.1.8 + readable-stream: 2.3.8 + dev: true + + /merge-descriptors@1.0.1: + resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} + + /merge-source-map@1.1.0: + resolution: {integrity: sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==} + dependencies: + source-map: 0.6.1 + dev: true + + /merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + /merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + /methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + /micromark@2.11.4: + resolution: {integrity: sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==} + dependencies: + debug: 4.3.4(supports-color@6.1.0) + parse-entities: 2.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /micromatch@3.1.10(supports-color@6.1.0): + resolution: {integrity: sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==} + engines: {node: '>=0.10.0'} + dependencies: + arr-diff: 4.0.0 + array-unique: 0.3.2 + braces: 2.3.2(supports-color@6.1.0) + define-property: 2.0.2 + extend-shallow: 3.0.2 + extglob: 2.0.4(supports-color@6.1.0) + fragment-cache: 0.2.1 + kind-of: 6.0.3 + nanomatch: 1.2.13(supports-color@6.1.0) + object.pick: 1.3.0 + regex-not: 1.0.2 + snapdragon: 0.8.2(supports-color@6.1.0) + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + dev: true + + /micromatch@4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.2 + picomatch: 2.3.1 + + /miller-rabin@4.0.1: + resolution: {integrity: sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==} + hasBin: true + dependencies: + bn.js: 4.12.0 + brorand: 1.1.0 + dev: true + + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + + /mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + /mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + dev: true + + /mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + dev: false + + /mimic-response@1.0.1: + resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} + engines: {node: '>=4'} + dev: true + + /min-document@2.19.0: + resolution: {integrity: sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==} + dependencies: + dom-walk: 0.1.2 + dev: true + + /mini-css-extract-plugin@0.6.0(webpack@4.46.0): + resolution: {integrity: sha512-79q5P7YGI6rdnVyIAV4NXpBQJFWdkzJxCim3Kog4078fM0piAaFlwocqbejdWtLW1cEzCexPrh6EdyFsPgVdAw==} + engines: {node: '>= 6.9.0'} + peerDependencies: + webpack: ^4.4.0 + dependencies: + loader-utils: 1.4.2 + normalize-url: 2.0.1 + schema-utils: 1.0.0 + webpack: 4.46.0 + webpack-sources: 1.4.3 + dev: true + + /mini-css-extract-plugin@2.7.5(webpack@5.76.3): + resolution: {integrity: sha512-9HaR++0mlgom81s95vvNjxkg52n2b5s//3ZTI1EtzFb98awsLSivs2LMsVqnQ3ay0PVhqWcGNyDaTE961FOcjQ==} + engines: {node: '>= 12.13.0'} + peerDependencies: + webpack: ^5.0.0 + dependencies: + schema-utils: 4.0.0 + webpack: 5.76.3(@swc/core@1.3.42) + dev: false + + /minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + + /minimalistic-crypto-utils@1.0.1: + resolution: {integrity: sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==} + dev: true + + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + + /minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + dependencies: + brace-expansion: 2.0.1 + + /minimatch@7.4.3: + resolution: {integrity: sha512-5UB4yYusDtkRPbRiy1cqZ1IpGNcJCGlEMG17RKzPddpyiPKoCdwohbED8g4QXT0ewCt8LTkQXuljsUfQ3FKM4A==} + engines: {node: '>=10'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + /mississippi@3.0.0: + resolution: {integrity: sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==} + engines: {node: '>=4.0.0'} + dependencies: + concat-stream: 1.6.2 + duplexify: 3.7.1 + end-of-stream: 1.4.4 + flush-write-stream: 1.1.1 + from2: 2.3.0 + parallel-transform: 1.2.0 + pump: 3.0.0 + pumpify: 1.5.1 + stream-each: 1.2.3 + through2: 2.0.5 + dev: true + + /mixin-deep@1.3.2: + resolution: {integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==} + engines: {node: '>=0.10.0'} + dependencies: + for-in: 1.0.2 + is-extendable: 1.0.1 + dev: true + + /mkdirp@0.3.0: + resolution: {integrity: sha512-OHsdUcVAQ6pOtg5JYWpCBo9W/GySVuwvP9hueRMW7UqshC0tbfzLv8wjySTPm3tfUZ/21CE9E1pJagOA91Pxew==} + deprecated: Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.) + dev: true + + /mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + dependencies: + minimist: 1.2.8 + + /mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + dev: true + + /moment-timezone@0.5.42: + resolution: {integrity: sha512-tjI9goqwzkflKSTxJo+jC/W8riTFwEjjunssmFvAWlvNVApjbkJM7UHggyKO0q1Fd/kZVKY77H7C9A0XKhhAFw==} + dependencies: + moment: 2.29.4 + dev: true + + /moment@2.29.4: + resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==} + dev: true + + /move-concurrently@1.0.1: + resolution: {integrity: sha512-hdrFxZOycD/g6A6SoI2bB5NA/5NEqD0569+S47WZhPvm46sD50ZHdYaFmnua5lndde9rCHGjmfK7Z8BuCt/PcQ==} + dependencies: + aproba: 1.2.0 + copy-concurrently: 1.0.5 + fs-write-stream-atomic: 1.0.10 + mkdirp: 0.5.6 + rimraf: 2.7.1 + run-queue: 1.0.3 + dev: true + + /ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + /multicast-dns-service-types@1.1.0: + resolution: {integrity: sha512-cnAsSVxIDsYt0v7HmC0hWZFwwXSh+E6PgCrREDuN/EsjgLwA5XRmlMHhSiDPrt6HxY1gTivEa/Zh7GtODoLevQ==} + dev: true + + /multicast-dns@6.2.3: + resolution: {integrity: sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==} + hasBin: true + dependencies: + dns-packet: 1.3.4 + thunky: 1.1.0 + dev: true + + /multicast-dns@7.2.5: + resolution: {integrity: sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==} + hasBin: true + dependencies: + dns-packet: 5.5.0 + thunky: 1.1.0 + dev: false + + /mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + dev: false + + /nan@2.23.1: + resolution: {integrity: sha512-r7bBUGKzlqk8oPBDYxt6Z0aEdF1G1rwlMcLk8LCOMbOzf0mG+JUfUzG4fIMWwHWP0iyaLWEQZJmtB7nOHEm/qw==} + requiresBuild: true + dev: true + optional: true + + /nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + + /nanoid@3.3.6: + resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + /nanomatch@1.2.13(supports-color@6.1.0): + resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==} + engines: {node: '>=0.10.0'} + dependencies: + arr-diff: 4.0.0 + array-unique: 0.3.2 + define-property: 2.0.2 + extend-shallow: 3.0.2 + fragment-cache: 0.2.1 + is-windows: 1.0.2 + kind-of: 6.0.3 + object.pick: 1.3.0 + regex-not: 1.0.2 + snapdragon: 0.8.2(supports-color@6.1.0) + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + dev: true + + /natural-compare-lite@1.4.0: + resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} + + /natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + /negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + /neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + /nice-try@1.0.5: + resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} + dev: true + + /no-case@2.3.2: + resolution: {integrity: sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==} + dependencies: + lower-case: 1.1.4 + dev: true + + /no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + dependencies: + lower-case: 2.0.2 + tslib: 2.5.0 + dev: false + + /node-fetch@2.6.9: + resolution: {integrity: sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + dependencies: + whatwg-url: 5.0.0 + dev: true + + /node-forge@0.10.0: + resolution: {integrity: sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==} + engines: {node: '>= 6.0.0'} + dev: true + + /node-forge@1.3.1: + resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} + engines: {node: '>= 6.13.0'} + dev: false + + /node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + dev: false + + /node-libs-browser@2.2.1: + resolution: {integrity: sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==} + dependencies: + assert: 1.5.0 + browserify-zlib: 0.2.0 + buffer: 4.9.2 + console-browserify: 1.2.0 + constants-browserify: 1.0.0 + crypto-browserify: 3.12.0 + domain-browser: 1.2.0 + events: 3.3.0 + https-browserify: 1.0.0 + os-browserify: 0.3.0 + path-browserify: 0.0.1 + process: 0.11.10 + punycode: 1.4.1 + querystring-es3: 0.2.1 + readable-stream: 2.3.8 + stream-browserify: 2.0.2 + stream-http: 2.8.3 + string_decoder: 1.3.0 + timers-browserify: 2.0.12 + tty-browserify: 0.0.0 + url: 0.11.0 + util: 0.11.1 + vm-browserify: 1.1.2 + dev: true + + /node-releases@2.0.10: + resolution: {integrity: sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==} + + /nopt@1.0.10: + resolution: {integrity: sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==} + hasBin: true + dependencies: + abbrev: 1.1.1 + dev: true + + /normalize-path@2.1.1: + resolution: {integrity: sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==} + engines: {node: '>=0.10.0'} + requiresBuild: true + dependencies: + remove-trailing-separator: 1.1.0 + dev: true + + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + /normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + + /normalize-url@2.0.1: + resolution: {integrity: sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw==} + engines: {node: '>=4'} + dependencies: + prepend-http: 2.0.0 + query-string: 5.1.1 + sort-keys: 2.0.0 + dev: true + + /normalize-url@3.3.0: + resolution: {integrity: sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==} + engines: {node: '>=6'} + dev: true + + /normalize-url@4.5.1: + resolution: {integrity: sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==} + engines: {node: '>=8'} + dev: true + + /normalize-url@6.1.0: + resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} + engines: {node: '>=10'} + + /npm-run-path@2.0.2: + resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==} + engines: {node: '>=4'} + dependencies: + path-key: 2.0.1 + dev: true + + /npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + dependencies: + path-key: 3.1.1 + dev: false + + /nprogress@0.2.0: + resolution: {integrity: sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==} + dev: true + + /nth-check@1.0.2: + resolution: {integrity: sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==} + dependencies: + boolbase: 1.0.0 + + /nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + dependencies: + boolbase: 1.0.0 + + /num2fraction@1.2.2: + resolution: {integrity: sha512-Y1wZESM7VUThYY+4W+X4ySH2maqcA+p7UR+w8VWNWVAd6lwuXXWz/w/Cz43J/dI2I+PS6wD5N+bJUF+gjWvIqg==} + dev: true + + /nwsapi@2.2.2: + resolution: {integrity: sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==} + dev: false + + /oauth-sign@0.9.0: + resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==} + dev: true + + /object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + /object-copy@0.1.0: + resolution: {integrity: sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==} + engines: {node: '>=0.10.0'} + dependencies: + copy-descriptor: 0.1.1 + define-property: 0.2.5 + kind-of: 3.2.2 + dev: true + + /object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + dev: false + + /object-inspect@1.12.3: + resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} + + /object-is@1.1.5: + resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + + /object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + /object-visit@1.0.1: + resolution: {integrity: sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==} + engines: {node: '>=0.10.0'} + dependencies: + isobject: 3.0.1 + dev: true + + /object.assign@4.1.4: + resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + has-symbols: 1.0.3 + object-keys: 1.1.1 + + /object.entries@1.1.6: + resolution: {integrity: sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + dev: false + + /object.fromentries@2.0.6: + resolution: {integrity: sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + dev: false + + /object.getownpropertydescriptors@2.1.5: + resolution: {integrity: sha512-yDNzckpM6ntyQiGTik1fKV1DcVDRS+w8bvpWNCBanvH5LfRX9O8WTHqQzG4RZwRAM4I0oU7TV11Lj5v0g20ibw==} + engines: {node: '>= 0.8'} + dependencies: + array.prototype.reduce: 1.0.5 + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + + /object.hasown@1.1.2: + resolution: {integrity: sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==} + dependencies: + define-properties: 1.2.0 + es-abstract: 1.21.2 + dev: false + + /object.pick@1.3.0: + resolution: {integrity: sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==} + engines: {node: '>=0.10.0'} + dependencies: + isobject: 3.0.1 + dev: true + + /object.values@1.1.6: + resolution: {integrity: sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + + /obuf@1.1.2: + resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} + + /on-finished@2.3.0: + resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} + engines: {node: '>= 0.8'} + dependencies: + ee-first: 1.1.1 + dev: true + + /on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + dependencies: + ee-first: 1.1.1 + + /on-headers@1.0.2: + resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} + engines: {node: '>= 0.8'} + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + + /onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + dependencies: + mimic-fn: 2.1.0 + dev: false + + /open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + dev: false + + /opencollective-postinstall@2.0.3: + resolution: {integrity: sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==} + hasBin: true + dev: true + + /opn@5.5.0: + resolution: {integrity: sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==} + engines: {node: '>=4'} + dependencies: + is-wsl: 1.1.0 + dev: true + + /optimize-css-assets-webpack-plugin@5.0.8(webpack@4.46.0): + resolution: {integrity: sha512-mgFS1JdOtEGzD8l+EuISqL57cKO+We9GcoiQEmdCWRqqck+FGNmYJtx9qfAPzEz+lRrlThWMuGDaRkI/yWNx/Q==} + peerDependencies: + webpack: ^4.0.0 + dependencies: + cssnano: 4.1.11 + last-call-webpack-plugin: 3.0.0 + webpack: 4.46.0 + dev: true + + /optionator@0.8.3: + resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==} + engines: {node: '>= 0.8.0'} + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.3.0 + prelude-ls: 1.1.2 + type-check: 0.3.2 + word-wrap: 1.2.3 + dev: false + + /optionator@0.9.1: + resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==} + engines: {node: '>= 0.8.0'} + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.3 + dev: true + + /optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + dev: false + + /os-browserify@0.3.0: + resolution: {integrity: sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==} + dev: true + + /p-cancelable@1.1.0: + resolution: {integrity: sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==} + engines: {node: '>=6'} + dev: true + + /p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + dev: true + + /p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + dependencies: + p-try: 2.2.0 + + /p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + dependencies: + yocto-queue: 0.1.0 + + /p-locate@3.0.0: + resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} + engines: {node: '>=6'} + dependencies: + p-limit: 2.3.0 + + /p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + dependencies: + p-limit: 2.3.0 + + /p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + dependencies: + p-limit: 3.1.0 + + /p-map@2.1.0: + resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} + engines: {node: '>=6'} + dev: true + + /p-retry@3.0.1: + resolution: {integrity: sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w==} + engines: {node: '>=6'} + dependencies: + retry: 0.12.0 + dev: true + + /p-retry@4.6.2: + resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} + engines: {node: '>=8'} + dependencies: + '@types/retry': 0.12.0 + retry: 0.13.1 + dev: false + + /p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + /package-json@6.5.0: + resolution: {integrity: sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==} + engines: {node: '>=8'} + dependencies: + got: 9.6.0 + registry-auth-token: 4.2.2 + registry-url: 5.1.0 + semver: 6.3.0 + dev: true + + /pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + dev: true + + /parallel-transform@1.2.0: + resolution: {integrity: sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==} + dependencies: + cyclist: 1.0.1 + inherits: 2.0.4 + readable-stream: 2.3.8 + dev: true + + /param-case@2.1.1: + resolution: {integrity: sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==} + dependencies: + no-case: 2.3.2 + dev: true + + /param-case@3.0.4: + resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} + dependencies: + dot-case: 3.0.4 + tslib: 2.5.0 + dev: false + + /parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + dependencies: + callsites: 3.1.0 + + /parse-asn1@5.1.6: + resolution: {integrity: sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==} + dependencies: + asn1.js: 5.4.1 + browserify-aes: 1.2.0 + evp_bytestokey: 1.0.3 + pbkdf2: 3.1.2 + safe-buffer: 5.2.1 + dev: true + + /parse-entities@2.0.0: + resolution: {integrity: sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==} + dependencies: + character-entities: 1.2.4 + character-entities-legacy: 1.1.4 + character-reference-invalid: 1.1.4 + is-alphanumerical: 1.0.4 + is-decimal: 1.0.4 + is-hexadecimal: 1.0.4 + dev: true + + /parse-json@4.0.0: + resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} + engines: {node: '>=4'} + dependencies: + error-ex: 1.3.2 + json-parse-better-errors: 1.0.2 + dev: true + + /parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + dependencies: + '@babel/code-frame': 7.18.6 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + dev: false + + /parse5@6.0.1: + resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + dev: false + + /parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + /pascal-case@3.1.2: + resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} + dependencies: + no-case: 3.0.4 + tslib: 2.5.0 + dev: false + + /pascalcase@0.1.1: + resolution: {integrity: sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==} + engines: {node: '>=0.10.0'} + dev: true + + /path-browserify@0.0.1: + resolution: {integrity: sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==} + dev: true + + /path-dirname@1.0.2: + resolution: {integrity: sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==} + requiresBuild: true + dev: true + + /path-exists@3.0.0: + resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} + engines: {node: '>=4'} + + /path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + /path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + /path-is-inside@1.0.2: + resolution: {integrity: sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==} + dev: true + + /path-key@2.0.1: + resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} + engines: {node: '>=4'} + dev: true + + /path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + /path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + /path-to-regexp@0.1.7: + resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} + + /path-type@3.0.0: + resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} + engines: {node: '>=4'} + dependencies: + pify: 3.0.0 + dev: true + + /path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + /pbkdf2@3.1.2: + resolution: {integrity: sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==} + engines: {node: '>=0.12'} + dependencies: + create-hash: 1.2.0 + create-hmac: 1.1.7 + ripemd160: 2.0.2 + safe-buffer: 5.2.1 + sha.js: 2.4.11 + dev: true + + /perf-regexes@1.0.1: + resolution: {integrity: sha512-L7MXxUDtqr4PUaLFCDCXBfGV/6KLIuSEccizDI7JxT+c9x1G1v04BQ4+4oag84SHaCdrBgQAIs/Cqn+flwFPng==} + engines: {node: '>=6.14'} + dev: true + + /perfect-scrollbar@1.5.5: + resolution: {integrity: sha512-dzalfutyP3e/FOpdlhVryN4AJ5XDVauVWxybSkLZmakFE2sS3y3pc4JnSprw8tGmHvkaG5Edr5T7LBTZ+WWU2g==} + dev: true + + /performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + + /picocolors@0.2.1: + resolution: {integrity: sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==} + + /picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + + /picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + dev: true + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + /pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + /pify@3.0.0: + resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} + engines: {node: '>=4'} + dev: true + + /pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + dev: true + + /pinkie-promise@2.0.1: + resolution: {integrity: sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==} + engines: {node: '>=0.10.0'} + dependencies: + pinkie: 2.0.4 + dev: true + + /pinkie@2.0.4: + resolution: {integrity: sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==} + engines: {node: '>=0.10.0'} + dev: true + + /pirates@4.0.5: + resolution: {integrity: sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==} + engines: {node: '>= 6'} + dev: false + + /pixelmatch@5.3.0: + resolution: {integrity: sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==} + hasBin: true + dependencies: + pngjs: 6.0.0 + dev: true + + /pkg-dir@3.0.0: + resolution: {integrity: sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==} + engines: {node: '>=6'} + dependencies: + find-up: 3.0.0 + dev: true + + /pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + dependencies: + find-up: 4.1.0 + + /pkg-up@3.1.0: + resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==} + engines: {node: '>=8'} + dependencies: + find-up: 3.0.0 + dev: false + + /pngjs@6.0.0: + resolution: {integrity: sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==} + engines: {node: '>=12.13.0'} + dev: true + + /portfinder@1.0.32(supports-color@6.1.0): + resolution: {integrity: sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==} + engines: {node: '>= 0.12.0'} + dependencies: + async: 2.6.4 + debug: 3.2.7(supports-color@6.1.0) + mkdirp: 0.5.6 + transitivePeerDependencies: + - supports-color + dev: true + + /posix-character-classes@0.1.1: + resolution: {integrity: sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==} + engines: {node: '>=0.10.0'} + dev: true + + /postcss-attribute-case-insensitive@5.0.2(postcss@8.4.21): + resolution: {integrity: sha512-XIidXV8fDr0kKt28vqki84fRK8VW8eTuIa4PChv2MqKuT6C9UjmSKzen6KaWhWEoYvwxFCa7n/tC1SZ3tyq4SQ==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + dependencies: + postcss: 8.4.21 + postcss-selector-parser: 6.0.11 + dev: false + + /postcss-browser-comments@4.0.0(browserslist@4.21.5)(postcss@8.4.21): + resolution: {integrity: sha512-X9X9/WN3KIvY9+hNERUqX9gncsgBA25XaeR+jshHz2j8+sYyHktHw1JdKuMjeLpGktXidqDhA7b/qm1mrBDmgg==} + engines: {node: '>=8'} + peerDependencies: + browserslist: '>=4' + postcss: '>=8' + dependencies: + browserslist: 4.21.5 + postcss: 8.4.21 + dev: false + + /postcss-calc@7.0.5: + resolution: {integrity: sha512-1tKHutbGtLtEZF6PT4JSihCHfIVldU72mZ8SdZHIYriIZ9fh9k9aWSppaT8rHsyI3dX+KSR+W+Ix9BMY3AODrg==} + dependencies: + postcss: 7.0.39 + postcss-selector-parser: 6.0.11 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-calc@8.2.4(postcss@8.4.21): + resolution: {integrity: sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==} + peerDependencies: + postcss: ^8.2.2 + dependencies: + postcss: 8.4.21 + postcss-selector-parser: 6.0.11 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-calc@8.2.4(postcss@8.5.6): + resolution: {integrity: sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==} + peerDependencies: + postcss: ^8.2.2 + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.0.11 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-clamp@4.1.0(postcss@8.4.21): + resolution: {integrity: sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==} + engines: {node: '>=7.6.0'} + peerDependencies: + postcss: ^8.4.6 + dependencies: + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-color-functional-notation@4.2.4(postcss@8.4.21): + resolution: {integrity: sha512-2yrTAUZUab9s6CpxkxC4rVgFEVaR6/2Pipvi6qcgvnYiVqZcbDHEoBDhrXzyb7Efh2CCfHQNtcqWcIruDTIUeg==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + dependencies: + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-color-hex-alpha@8.0.4(postcss@8.4.21): + resolution: {integrity: sha512-nLo2DCRC9eE4w2JmuKgVA3fGL3d01kGq752pVALF68qpGLmx2Qrk91QTKkdUqqp45T1K1XV8IhQpcu1hoAQflQ==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.4 + dependencies: + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-color-rebeccapurple@7.1.1(postcss@8.4.21): + resolution: {integrity: sha512-pGxkuVEInwLHgkNxUc4sdg4g3py7zUeCQ9sMfwyHAT+Ezk8a4OaaVZ8lIY5+oNqA/BXXgLyXv0+5wHP68R79hg==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + dependencies: + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-colormin@4.0.3: + resolution: {integrity: sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw==} + engines: {node: '>=6.9.0'} + dependencies: + browserslist: 4.21.5 + color: 3.2.1 + has: 1.0.3 + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + dev: true + + /postcss-colormin@5.3.1(postcss@8.4.21): + resolution: {integrity: sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.21.5 + caniuse-api: 3.0.0 + colord: 2.9.3 + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-colormin@5.3.1(postcss@8.5.6): + resolution: {integrity: sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.21.5 + caniuse-api: 3.0.0 + colord: 2.9.3 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-convert-values@4.0.1: + resolution: {integrity: sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ==} + engines: {node: '>=6.9.0'} + dependencies: + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + dev: true + + /postcss-convert-values@5.1.3(postcss@8.4.21): + resolution: {integrity: sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.21.5 + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-convert-values@5.1.3(postcss@8.5.6): + resolution: {integrity: sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.21.5 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-custom-media@8.0.2(postcss@8.4.21): + resolution: {integrity: sha512-7yi25vDAoHAkbhAzX9dHx2yc6ntS4jQvejrNcC+csQJAXjj15e7VcWfMgLqBNAbOvqi5uIa9huOVwdHbf+sKqg==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.3 + dependencies: + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-custom-properties@12.1.11(postcss@8.4.21): + resolution: {integrity: sha512-0IDJYhgU8xDv1KY6+VgUwuQkVtmYzRwu+dMjnmdMafXYv86SWqfxkc7qdDvWS38vsjaEtv8e0vGOUQrAiMBLpQ==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + dependencies: + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-custom-selectors@6.0.3(postcss@8.4.21): + resolution: {integrity: sha512-fgVkmyiWDwmD3JbpCmB45SvvlCD6z9CG6Ie6Iere22W5aHea6oWa7EM2bpnv2Fj3I94L3VbtvX9KqwSi5aFzSg==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.3 + dependencies: + postcss: 8.4.21 + postcss-selector-parser: 6.0.11 + dev: false + + /postcss-dir-pseudo-class@6.0.5(postcss@8.4.21): + resolution: {integrity: sha512-eqn4m70P031PF7ZQIvSgy9RSJ5uI2171O/OO/zcRNYpJbvaeKFUlar1aJ7rmgiQtbm0FSPsRewjpdS0Oew7MPA==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + dependencies: + postcss: 8.4.21 + postcss-selector-parser: 6.0.11 + dev: false + + /postcss-discard-comments@4.0.2: + resolution: {integrity: sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg==} + engines: {node: '>=6.9.0'} + dependencies: + postcss: 7.0.39 + dev: true + + /postcss-discard-comments@5.1.2(postcss@8.4.21): + resolution: {integrity: sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.21 + dev: false + + /postcss-discard-comments@5.1.2(postcss@8.5.6): + resolution: {integrity: sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.5.6 + dev: true + + /postcss-discard-duplicates@4.0.2: + resolution: {integrity: sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ==} + engines: {node: '>=6.9.0'} + dependencies: + postcss: 7.0.39 + dev: true + + /postcss-discard-duplicates@5.1.0(postcss@8.4.21): + resolution: {integrity: sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.21 + dev: false + + /postcss-discard-duplicates@5.1.0(postcss@8.5.6): + resolution: {integrity: sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.5.6 + dev: true + + /postcss-discard-empty@4.0.1: + resolution: {integrity: sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w==} + engines: {node: '>=6.9.0'} + dependencies: + postcss: 7.0.39 + dev: true + + /postcss-discard-empty@5.1.1(postcss@8.4.21): + resolution: {integrity: sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.21 + dev: false + + /postcss-discard-empty@5.1.1(postcss@8.5.6): + resolution: {integrity: sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.5.6 + dev: true + + /postcss-discard-overridden@4.0.1: + resolution: {integrity: sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg==} + engines: {node: '>=6.9.0'} + dependencies: + postcss: 7.0.39 + dev: true + + /postcss-discard-overridden@5.1.0(postcss@8.4.21): + resolution: {integrity: sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.21 + dev: false + + /postcss-discard-overridden@5.1.0(postcss@8.5.6): + resolution: {integrity: sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.5.6 + dev: true + + /postcss-double-position-gradients@3.1.2(postcss@8.4.21): + resolution: {integrity: sha512-GX+FuE/uBR6eskOK+4vkXgT6pDkexLokPaz/AbJna9s5Kzp/yl488pKPjhy0obB475ovfT1Wv8ho7U/cHNaRgQ==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + dependencies: + '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.4.21) + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-env-function@4.0.6(postcss@8.4.21): + resolution: {integrity: sha512-kpA6FsLra+NqcFnL81TnsU+Z7orGtDTxcOhl6pwXeEq1yFPpRMkCDpHhrz8CFQDr/Wfm0jLiNQ1OsGGPjlqPwA==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.4 + dependencies: + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-flexbugs-fixes@5.0.2(postcss@8.4.21): + resolution: {integrity: sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ==} + peerDependencies: + postcss: ^8.1.4 + dependencies: + postcss: 8.4.21 + dev: false + + /postcss-focus-visible@6.0.4(postcss@8.4.21): + resolution: {integrity: sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.4 + dependencies: + postcss: 8.4.21 + postcss-selector-parser: 6.0.11 + dev: false + + /postcss-focus-within@5.0.4(postcss@8.4.21): + resolution: {integrity: sha512-vvjDN++C0mu8jz4af5d52CB184ogg/sSxAFS+oUJQq2SuCe7T5U2iIsVJtsCp2d6R4j0jr5+q3rPkBVZkXD9fQ==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.4 + dependencies: + postcss: 8.4.21 + postcss-selector-parser: 6.0.11 + dev: false + + /postcss-font-variant@5.0.0(postcss@8.4.21): + resolution: {integrity: sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==} + peerDependencies: + postcss: ^8.1.0 + dependencies: + postcss: 8.4.21 + dev: false + + /postcss-gap-properties@3.0.5(postcss@8.4.21): + resolution: {integrity: sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + dependencies: + postcss: 8.4.21 + dev: false + + /postcss-image-set-function@4.0.7(postcss@8.4.21): + resolution: {integrity: sha512-9T2r9rsvYzm5ndsBE8WgtrMlIT7VbtTfE7b3BQnudUqnBcBo7L758oc+o+pdj/dUV0l5wjwSdjeOH2DZtfv8qw==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + dependencies: + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-import@12.0.1: + resolution: {integrity: sha512-3Gti33dmCjyKBgimqGxL3vcV8w9+bsHwO5UrBawp796+jdardbcFl4RP5w/76BwNL7aGzpKstIfF9I+kdE8pTw==} + engines: {node: '>=6.0.0'} + dependencies: + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + read-cache: 1.0.0 + resolve: 1.22.1 + dev: true + + /postcss-import@14.1.0(postcss@8.4.21): + resolution: {integrity: sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==} + engines: {node: '>=10.0.0'} + peerDependencies: + postcss: ^8.0.0 + dependencies: + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.1 + dev: false + + /postcss-initial@4.0.1(postcss@8.4.21): + resolution: {integrity: sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==} + peerDependencies: + postcss: ^8.0.0 + dependencies: + postcss: 8.4.21 + dev: false + + /postcss-js@4.0.1(postcss@8.4.21): + resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + dependencies: + camelcase-css: 2.0.1 + postcss: 8.4.21 + dev: false + + /postcss-lab-function@4.2.1(postcss@8.4.21): + resolution: {integrity: sha512-xuXll4isR03CrQsmxyz92LJB2xX9n+pZJ5jE9JgcnmsCammLyKdlzrBin+25dy6wIjfhJpKBAN80gsTlCgRk2w==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + dependencies: + '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.4.21) + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-load-config@2.1.2: + resolution: {integrity: sha512-/rDeGV6vMUo3mwJZmeHfEDvwnTKKqQ0S7OHUi/kJvvtx3aWtyWG2/0ZWnzCt2keEclwN6Tf0DST2v9kITdOKYw==} + engines: {node: '>= 4'} + dependencies: + cosmiconfig: 5.2.1 + import-cwd: 2.1.0 + dev: true + + /postcss-load-config@3.1.4(postcss@8.4.21): + resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} + engines: {node: '>= 10'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + dependencies: + lilconfig: 2.1.0 + postcss: 8.4.21 + yaml: 1.10.2 + dev: false + + /postcss-loader@3.0.0: + resolution: {integrity: sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA==} + engines: {node: '>= 6'} + dependencies: + loader-utils: 1.4.2 + postcss: 7.0.39 + postcss-load-config: 2.1.2 + schema-utils: 1.0.0 + dev: true + + /postcss-loader@6.2.1(postcss@8.4.21)(webpack@5.76.3): + resolution: {integrity: sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q==} + engines: {node: '>= 12.13.0'} + peerDependencies: + postcss: ^7.0.0 || ^8.0.1 + webpack: ^5.0.0 + dependencies: + cosmiconfig: 7.1.0 + klona: 2.0.6 + postcss: 8.4.21 + semver: 7.3.8 + webpack: 5.76.3(@swc/core@1.3.42) + dev: false + + /postcss-logical@5.0.4(postcss@8.4.21): + resolution: {integrity: sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.4 + dependencies: + postcss: 8.4.21 + dev: false + + /postcss-media-minmax@5.0.0(postcss@8.4.21): + resolution: {integrity: sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + postcss: ^8.1.0 + dependencies: + postcss: 8.4.21 + dev: false + + /postcss-merge-longhand@4.0.11: + resolution: {integrity: sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw==} + engines: {node: '>=6.9.0'} + dependencies: + css-color-names: 0.0.4 + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + stylehacks: 4.0.3 + dev: true + + /postcss-merge-longhand@5.1.7(postcss@8.4.21): + resolution: {integrity: sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + stylehacks: 5.1.1(postcss@8.4.21) + dev: false + + /postcss-merge-longhand@5.1.7(postcss@8.5.6): + resolution: {integrity: sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + stylehacks: 5.1.1(postcss@8.5.6) + dev: true + + /postcss-merge-rules@4.0.3: + resolution: {integrity: sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ==} + engines: {node: '>=6.9.0'} + dependencies: + browserslist: 4.21.5 + caniuse-api: 3.0.0 + cssnano-util-same-parent: 4.0.1 + postcss: 7.0.39 + postcss-selector-parser: 3.1.2 + vendors: 1.0.4 + dev: true + + /postcss-merge-rules@5.1.4(postcss@8.4.21): + resolution: {integrity: sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.21.5 + caniuse-api: 3.0.0 + cssnano-utils: 3.1.0(postcss@8.4.21) + postcss: 8.4.21 + postcss-selector-parser: 6.0.11 + dev: false + + /postcss-merge-rules@5.1.4(postcss@8.5.6): + resolution: {integrity: sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.21.5 + caniuse-api: 3.0.0 + cssnano-utils: 3.1.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-selector-parser: 6.0.11 + dev: true + + /postcss-minify-font-values@4.0.2: + resolution: {integrity: sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg==} + engines: {node: '>=6.9.0'} + dependencies: + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + dev: true + + /postcss-minify-font-values@5.1.0(postcss@8.4.21): + resolution: {integrity: sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-minify-font-values@5.1.0(postcss@8.5.6): + resolution: {integrity: sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-minify-gradients@4.0.2: + resolution: {integrity: sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q==} + engines: {node: '>=6.9.0'} + dependencies: + cssnano-util-get-arguments: 4.0.0 + is-color-stop: 1.1.0 + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + dev: true + + /postcss-minify-gradients@5.1.1(postcss@8.4.21): + resolution: {integrity: sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + colord: 2.9.3 + cssnano-utils: 3.1.0(postcss@8.4.21) + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-minify-gradients@5.1.1(postcss@8.5.6): + resolution: {integrity: sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + colord: 2.9.3 + cssnano-utils: 3.1.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-minify-params@4.0.2: + resolution: {integrity: sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg==} + engines: {node: '>=6.9.0'} + dependencies: + alphanum-sort: 1.0.2 + browserslist: 4.21.5 + cssnano-util-get-arguments: 4.0.0 + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + uniqs: 2.0.0 + dev: true + + /postcss-minify-params@5.1.4(postcss@8.4.21): + resolution: {integrity: sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.21.5 + cssnano-utils: 3.1.0(postcss@8.4.21) + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-minify-params@5.1.4(postcss@8.5.6): + resolution: {integrity: sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.21.5 + cssnano-utils: 3.1.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-minify-selectors@4.0.2: + resolution: {integrity: sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g==} + engines: {node: '>=6.9.0'} + dependencies: + alphanum-sort: 1.0.2 + has: 1.0.3 + postcss: 7.0.39 + postcss-selector-parser: 3.1.2 + dev: true + + /postcss-minify-selectors@5.2.1(postcss@8.4.21): + resolution: {integrity: sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.21 + postcss-selector-parser: 6.0.11 + dev: false + + /postcss-minify-selectors@5.2.1(postcss@8.5.6): + resolution: {integrity: sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.0.11 + dev: true + + /postcss-modules-extract-imports@2.0.0: + resolution: {integrity: sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==} + engines: {node: '>= 6'} + dependencies: + postcss: 7.0.39 + dev: true + + /postcss-modules-extract-imports@3.0.0(postcss@8.4.21): + resolution: {integrity: sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + dependencies: + postcss: 8.4.21 + dev: false + + /postcss-modules-local-by-default@2.0.6: + resolution: {integrity: sha512-oLUV5YNkeIBa0yQl7EYnxMgy4N6noxmiwZStaEJUSe2xPMcdNc8WmBQuQCx18H5psYbVxz8zoHk0RAAYZXP9gA==} + engines: {node: '>= 6'} + dependencies: + postcss: 7.0.39 + postcss-selector-parser: 6.0.11 + postcss-value-parser: 3.3.1 + dev: true + + /postcss-modules-local-by-default@4.0.0(postcss@8.4.21): + resolution: {integrity: sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + dependencies: + icss-utils: 5.1.0(postcss@8.4.21) + postcss: 8.4.21 + postcss-selector-parser: 6.0.11 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-modules-scope@2.2.0: + resolution: {integrity: sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ==} + engines: {node: '>= 6'} + dependencies: + postcss: 7.0.39 + postcss-selector-parser: 6.0.11 + dev: true + + /postcss-modules-scope@3.0.0(postcss@8.4.21): + resolution: {integrity: sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + dependencies: + postcss: 8.4.21 + postcss-selector-parser: 6.0.11 + dev: false + + /postcss-modules-values@2.0.0: + resolution: {integrity: sha512-Ki7JZa7ff1N3EIMlPnGTZfUMe69FFwiQPnVSXC9mnn3jozCRBYIxiZd44yJOV2AmabOo4qFf8s0dC/+lweG7+w==} + dependencies: + icss-replace-symbols: 1.1.0 + postcss: 7.0.39 + dev: true + + /postcss-modules-values@4.0.0(postcss@8.4.21): + resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + dependencies: + icss-utils: 5.1.0(postcss@8.4.21) + postcss: 8.4.21 + dev: false + + /postcss-nested@6.0.0(postcss@8.4.21): + resolution: {integrity: sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + dependencies: + postcss: 8.4.21 + postcss-selector-parser: 6.0.11 + dev: false + + /postcss-nesting@10.2.0(postcss@8.4.21): + resolution: {integrity: sha512-EwMkYchxiDiKUhlJGzWsD9b2zvq/r2SSubcRrgP+jujMXFzqvANLt16lJANC+5uZ6hjI7lpRmI6O8JIl+8l1KA==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + dependencies: + '@csstools/selector-specificity': 2.2.0(postcss-selector-parser@6.0.11) + postcss: 8.4.21 + postcss-selector-parser: 6.0.11 + dev: false + + /postcss-normalize-charset@4.0.1: + resolution: {integrity: sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g==} + engines: {node: '>=6.9.0'} + dependencies: + postcss: 7.0.39 + dev: true + + /postcss-normalize-charset@5.1.0(postcss@8.4.21): + resolution: {integrity: sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.21 + dev: false + + /postcss-normalize-charset@5.1.0(postcss@8.5.6): + resolution: {integrity: sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.5.6 + dev: true + + /postcss-normalize-display-values@4.0.2: + resolution: {integrity: sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ==} + engines: {node: '>=6.9.0'} + dependencies: + cssnano-util-get-match: 4.0.0 + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + dev: true + + /postcss-normalize-display-values@5.1.0(postcss@8.4.21): + resolution: {integrity: sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-normalize-display-values@5.1.0(postcss@8.5.6): + resolution: {integrity: sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-normalize-positions@4.0.2: + resolution: {integrity: sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA==} + engines: {node: '>=6.9.0'} + dependencies: + cssnano-util-get-arguments: 4.0.0 + has: 1.0.3 + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + dev: true + + /postcss-normalize-positions@5.1.1(postcss@8.4.21): + resolution: {integrity: sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-normalize-positions@5.1.1(postcss@8.5.6): + resolution: {integrity: sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-normalize-repeat-style@4.0.2: + resolution: {integrity: sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q==} + engines: {node: '>=6.9.0'} + dependencies: + cssnano-util-get-arguments: 4.0.0 + cssnano-util-get-match: 4.0.0 + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + dev: true + + /postcss-normalize-repeat-style@5.1.1(postcss@8.4.21): + resolution: {integrity: sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-normalize-repeat-style@5.1.1(postcss@8.5.6): + resolution: {integrity: sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-normalize-string@4.0.2: + resolution: {integrity: sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA==} + engines: {node: '>=6.9.0'} + dependencies: + has: 1.0.3 + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + dev: true + + /postcss-normalize-string@5.1.0(postcss@8.4.21): + resolution: {integrity: sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-normalize-string@5.1.0(postcss@8.5.6): + resolution: {integrity: sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-normalize-timing-functions@4.0.2: + resolution: {integrity: sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A==} + engines: {node: '>=6.9.0'} + dependencies: + cssnano-util-get-match: 4.0.0 + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + dev: true + + /postcss-normalize-timing-functions@5.1.0(postcss@8.4.21): + resolution: {integrity: sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-normalize-timing-functions@5.1.0(postcss@8.5.6): + resolution: {integrity: sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-normalize-unicode@4.0.1: + resolution: {integrity: sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg==} + engines: {node: '>=6.9.0'} + dependencies: + browserslist: 4.21.5 + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + dev: true + + /postcss-normalize-unicode@5.1.1(postcss@8.4.21): + resolution: {integrity: sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.21.5 + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-normalize-unicode@5.1.1(postcss@8.5.6): + resolution: {integrity: sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.21.5 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-normalize-url@4.0.1: + resolution: {integrity: sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA==} + engines: {node: '>=6.9.0'} + dependencies: + is-absolute-url: 2.1.0 + normalize-url: 3.3.0 + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + dev: true + + /postcss-normalize-url@5.1.0(postcss@8.4.21): + resolution: {integrity: sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + normalize-url: 6.1.0 + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-normalize-url@5.1.0(postcss@8.5.6): + resolution: {integrity: sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + normalize-url: 6.1.0 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-normalize-whitespace@4.0.2: + resolution: {integrity: sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA==} + engines: {node: '>=6.9.0'} + dependencies: + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + dev: true + + /postcss-normalize-whitespace@5.1.1(postcss@8.4.21): + resolution: {integrity: sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-normalize-whitespace@5.1.1(postcss@8.5.6): + resolution: {integrity: sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-normalize@10.0.1(browserslist@4.21.5)(postcss@8.4.21): + resolution: {integrity: sha512-+5w18/rDev5mqERcG3W5GZNMJa1eoYYNGo8gB7tEwaos0ajk3ZXAI4mHGcNT47NE+ZnZD1pEpUOFLvltIwmeJA==} + engines: {node: '>= 12'} + peerDependencies: + browserslist: '>= 4' + postcss: '>= 8' + dependencies: + '@csstools/normalize.css': 12.0.0 + browserslist: 4.21.5 + postcss: 8.4.21 + postcss-browser-comments: 4.0.0(browserslist@4.21.5)(postcss@8.4.21) + sanitize.css: 13.0.0 + dev: false + + /postcss-opacity-percentage@1.1.3(postcss@8.4.21): + resolution: {integrity: sha512-An6Ba4pHBiDtyVpSLymUUERMo2cU7s+Obz6BTrS+gxkbnSBNKSuD0AVUc+CpBMrpVPKKfoVz0WQCX+Tnst0i4A==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + dependencies: + postcss: 8.4.21 + dev: false + + /postcss-ordered-values@4.1.2: + resolution: {integrity: sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw==} + engines: {node: '>=6.9.0'} + dependencies: + cssnano-util-get-arguments: 4.0.0 + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + dev: true + + /postcss-ordered-values@5.1.3(postcss@8.4.21): + resolution: {integrity: sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + cssnano-utils: 3.1.0(postcss@8.4.21) + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-ordered-values@5.1.3(postcss@8.5.6): + resolution: {integrity: sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + cssnano-utils: 3.1.0(postcss@8.5.6) + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-overflow-shorthand@3.0.4(postcss@8.4.21): + resolution: {integrity: sha512-otYl/ylHK8Y9bcBnPLo3foYFLL6a6Ak+3EQBPOTR7luMYCOsiVTUk1iLvNf6tVPNGXcoL9Hoz37kpfriRIFb4A==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + dependencies: + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-page-break@3.0.4(postcss@8.4.21): + resolution: {integrity: sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==} + peerDependencies: + postcss: ^8 + dependencies: + postcss: 8.4.21 + dev: false + + /postcss-place@7.0.5(postcss@8.4.21): + resolution: {integrity: sha512-wR8igaZROA6Z4pv0d+bvVrvGY4GVHihBCBQieXFY3kuSuMyOmEnnfFzHl/tQuqHZkfkIVBEbDvYcFfHmpSet9g==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + dependencies: + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-preset-env@7.8.3(postcss@8.4.21): + resolution: {integrity: sha512-T1LgRm5uEVFSEF83vHZJV2z19lHg4yJuZ6gXZZkqVsqv63nlr6zabMH3l4Pc01FQCyfWVrh2GaUeCVy9Po+Aag==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + dependencies: + '@csstools/postcss-cascade-layers': 1.1.1(postcss@8.4.21) + '@csstools/postcss-color-function': 1.1.1(postcss@8.4.21) + '@csstools/postcss-font-format-keywords': 1.0.1(postcss@8.4.21) + '@csstools/postcss-hwb-function': 1.0.2(postcss@8.4.21) + '@csstools/postcss-ic-unit': 1.0.1(postcss@8.4.21) + '@csstools/postcss-is-pseudo-class': 2.0.7(postcss@8.4.21) + '@csstools/postcss-nested-calc': 1.0.0(postcss@8.4.21) + '@csstools/postcss-normalize-display-values': 1.0.1(postcss@8.4.21) + '@csstools/postcss-oklab-function': 1.1.1(postcss@8.4.21) + '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.4.21) + '@csstools/postcss-stepped-value-functions': 1.0.1(postcss@8.4.21) + '@csstools/postcss-text-decoration-shorthand': 1.0.0(postcss@8.4.21) + '@csstools/postcss-trigonometric-functions': 1.0.2(postcss@8.4.21) + '@csstools/postcss-unset-value': 1.0.2(postcss@8.4.21) + autoprefixer: 10.4.14(postcss@8.4.21) + browserslist: 4.21.5 + css-blank-pseudo: 3.0.3(postcss@8.4.21) + css-has-pseudo: 3.0.4(postcss@8.4.21) + css-prefers-color-scheme: 6.0.3(postcss@8.4.21) + cssdb: 7.5.2 + postcss: 8.4.21 + postcss-attribute-case-insensitive: 5.0.2(postcss@8.4.21) + postcss-clamp: 4.1.0(postcss@8.4.21) + postcss-color-functional-notation: 4.2.4(postcss@8.4.21) + postcss-color-hex-alpha: 8.0.4(postcss@8.4.21) + postcss-color-rebeccapurple: 7.1.1(postcss@8.4.21) + postcss-custom-media: 8.0.2(postcss@8.4.21) + postcss-custom-properties: 12.1.11(postcss@8.4.21) + postcss-custom-selectors: 6.0.3(postcss@8.4.21) + postcss-dir-pseudo-class: 6.0.5(postcss@8.4.21) + postcss-double-position-gradients: 3.1.2(postcss@8.4.21) + postcss-env-function: 4.0.6(postcss@8.4.21) + postcss-focus-visible: 6.0.4(postcss@8.4.21) + postcss-focus-within: 5.0.4(postcss@8.4.21) + postcss-font-variant: 5.0.0(postcss@8.4.21) + postcss-gap-properties: 3.0.5(postcss@8.4.21) + postcss-image-set-function: 4.0.7(postcss@8.4.21) + postcss-initial: 4.0.1(postcss@8.4.21) + postcss-lab-function: 4.2.1(postcss@8.4.21) + postcss-logical: 5.0.4(postcss@8.4.21) + postcss-media-minmax: 5.0.0(postcss@8.4.21) + postcss-nesting: 10.2.0(postcss@8.4.21) + postcss-opacity-percentage: 1.1.3(postcss@8.4.21) + postcss-overflow-shorthand: 3.0.4(postcss@8.4.21) + postcss-page-break: 3.0.4(postcss@8.4.21) + postcss-place: 7.0.5(postcss@8.4.21) + postcss-pseudo-class-any-link: 7.1.6(postcss@8.4.21) + postcss-replace-overflow-wrap: 4.0.0(postcss@8.4.21) + postcss-selector-not: 6.0.1(postcss@8.4.21) + postcss-value-parser: 4.2.0 + dev: false + + /postcss-pseudo-class-any-link@7.1.6(postcss@8.4.21): + resolution: {integrity: sha512-9sCtZkO6f/5ML9WcTLcIyV1yz9D1rf0tWc+ulKcvV30s0iZKS/ONyETvoWsr6vnrmW+X+KmuK3gV/w5EWnT37w==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + dependencies: + postcss: 8.4.21 + postcss-selector-parser: 6.0.11 + dev: false + + /postcss-reduce-initial@4.0.3: + resolution: {integrity: sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA==} + engines: {node: '>=6.9.0'} + dependencies: + browserslist: 4.21.5 + caniuse-api: 3.0.0 + has: 1.0.3 + postcss: 7.0.39 + dev: true + + /postcss-reduce-initial@5.1.2(postcss@8.4.21): + resolution: {integrity: sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.21.5 + caniuse-api: 3.0.0 + postcss: 8.4.21 + dev: false + + /postcss-reduce-initial@5.1.2(postcss@8.5.6): + resolution: {integrity: sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.21.5 + caniuse-api: 3.0.0 + postcss: 8.5.6 + dev: true + + /postcss-reduce-transforms@4.0.2: + resolution: {integrity: sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg==} + engines: {node: '>=6.9.0'} + dependencies: + cssnano-util-get-match: 4.0.0 + has: 1.0.3 + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + dev: true + + /postcss-reduce-transforms@5.1.0(postcss@8.4.21): + resolution: {integrity: sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: false + + /postcss-reduce-transforms@5.1.0(postcss@8.5.6): + resolution: {integrity: sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + dev: true + + /postcss-replace-overflow-wrap@4.0.0(postcss@8.4.21): + resolution: {integrity: sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==} + peerDependencies: + postcss: ^8.0.3 + dependencies: + postcss: 8.4.21 + dev: false + + /postcss-safe-parser@4.0.2: + resolution: {integrity: sha512-Uw6ekxSWNLCPesSv/cmqf2bY/77z11O7jZGPax3ycZMFU/oi2DMH9i89AdHc1tRwFg/arFoEwX0IS3LCUxJh1g==} + engines: {node: '>=6.0.0'} + dependencies: + postcss: 7.0.39 + dev: true + + /postcss-selector-not@6.0.1(postcss@8.4.21): + resolution: {integrity: sha512-1i9affjAe9xu/y9uqWH+tD4r6/hDaXJruk8xn2x1vzxC2U3J3LKO3zJW4CyxlNhA56pADJ/djpEwpH1RClI2rQ==} + engines: {node: ^12 || ^14 || >=16} + peerDependencies: + postcss: ^8.2 + dependencies: + postcss: 8.4.21 + postcss-selector-parser: 6.0.11 + dev: false + + /postcss-selector-parser@3.1.2: + resolution: {integrity: sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==} + engines: {node: '>=8'} + dependencies: + dot-prop: 5.3.0 + indexes-of: 1.0.1 + uniq: 1.0.1 + dev: true + + /postcss-selector-parser@6.0.11: + resolution: {integrity: sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==} + engines: {node: '>=4'} + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + /postcss-svgo@4.0.3: + resolution: {integrity: sha512-NoRbrcMWTtUghzuKSoIm6XV+sJdvZ7GZSc3wdBN0W19FTtp2ko8NqLsgoh/m9CzNhU3KLPvQmjIwtaNFkaFTvw==} + engines: {node: '>=6.9.0'} + dependencies: + postcss: 7.0.39 + postcss-value-parser: 3.3.1 + svgo: 1.3.2 + dev: true + + /postcss-svgo@5.1.0(postcss@8.4.21): + resolution: {integrity: sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + svgo: 2.8.0 + dev: false + + /postcss-svgo@5.1.0(postcss@8.5.6): + resolution: {integrity: sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + svgo: 2.8.0 + dev: true + + /postcss-unique-selectors@4.0.1: + resolution: {integrity: sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg==} + engines: {node: '>=6.9.0'} + dependencies: + alphanum-sort: 1.0.2 + postcss: 7.0.39 + uniqs: 2.0.0 + dev: true + + /postcss-unique-selectors@5.1.1(postcss@8.4.21): + resolution: {integrity: sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.4.21 + postcss-selector-parser: 6.0.11 + dev: false + + /postcss-unique-selectors@5.1.1(postcss@8.5.6): + resolution: {integrity: sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.0.11 + dev: true + + /postcss-value-parser@3.3.1: + resolution: {integrity: sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==} + dev: true + + /postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + /postcss@7.0.39: + resolution: {integrity: sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==} + engines: {node: '>=6.0.0'} + dependencies: + picocolors: 0.2.1 + source-map: 0.6.1 + + /postcss@8.4.21: + resolution: {integrity: sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.6 + picocolors: 1.0.0 + source-map-js: 1.0.2 + + /postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + dev: true + + /prelude-ls@1.1.2: + resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} + engines: {node: '>= 0.8.0'} + dev: false + + /prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + /prepend-http@2.0.0: + resolution: {integrity: sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==} + engines: {node: '>=4'} + dev: true + + /prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + requiresBuild: true + dev: true + optional: true + + /pretty-bytes@5.6.0: + resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} + engines: {node: '>=6'} + dev: false + + /pretty-error@2.1.2: + resolution: {integrity: sha512-EY5oDzmsX5wvuynAByrmY0P0hcp+QpnAKbJng2A2MPjVKXCxrDSUkzghVJ4ZGPIv+JC4gX8fPUWscC0RtjsWGw==} + dependencies: + lodash: 4.17.21 + renderkid: 2.0.7 + dev: true + + /pretty-error@4.0.0: + resolution: {integrity: sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==} + dependencies: + lodash: 4.17.21 + renderkid: 3.0.0 + dev: false + + /pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + dev: false + + /pretty-format@28.1.3: + resolution: {integrity: sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q==} + engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} + dependencies: + '@jest/schemas': 28.1.3 + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 18.2.0 + dev: false + + /pretty-time@1.1.0: + resolution: {integrity: sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA==} + engines: {node: '>=4'} + dev: true + + /prismjs@1.29.0: + resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} + engines: {node: '>=6'} + dev: true + + /process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + /process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + dev: true + + /promise-inflight@1.0.1(bluebird@3.7.2): + resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} + peerDependencies: + bluebird: '*' + peerDependenciesMeta: + bluebird: + optional: true + dependencies: + bluebird: 3.7.2 + dev: true + + /promise@8.3.0: + resolution: {integrity: sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==} + dependencies: + asap: 2.0.6 + dev: false + + /prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + dev: false + + /prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + dev: false + + /proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + /prr@1.0.1: + resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==} + dev: true + + /pseudomap@1.0.2: + resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} + dev: true + + /psl@1.9.0: + resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} + + /public-encrypt@4.0.3: + resolution: {integrity: sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==} + dependencies: + bn.js: 4.12.0 + browserify-rsa: 4.1.0 + create-hash: 1.2.0 + parse-asn1: 5.1.6 + randombytes: 2.1.0 + safe-buffer: 5.2.1 + dev: true + + /pump@2.0.1: + resolution: {integrity: sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==} + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + dev: true + + /pump@3.0.0: + resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + dev: true + + /pumpify@1.5.1: + resolution: {integrity: sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==} + dependencies: + duplexify: 3.7.1 + inherits: 2.0.4 + pump: 2.0.1 + dev: true + + /punycode@1.3.2: + resolution: {integrity: sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==} + dev: true + + /punycode@1.4.1: + resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} + dev: true + + /punycode@2.3.0: + resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} + engines: {node: '>=6'} + + /pupa@2.1.1: + resolution: {integrity: sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==} + engines: {node: '>=8'} + dependencies: + escape-goat: 2.1.1 + dev: true + + /q@1.5.1: + resolution: {integrity: sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==} + engines: {node: '>=0.6.0', teleport: '>=0.2.0'} + + /qjobs@1.2.0: + resolution: {integrity: sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==} + engines: {node: '>=0.9'} + dev: true + + /qs@6.11.0: + resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.4 + + /qs@6.5.3: + resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==} + engines: {node: '>=0.6'} + dev: true + + /query-string@5.1.1: + resolution: {integrity: sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==} + engines: {node: '>=0.10.0'} + dependencies: + decode-uri-component: 0.2.2 + object-assign: 4.1.1 + strict-uri-encode: 1.1.0 + dev: true + + /querystring-es3@0.2.1: + resolution: {integrity: sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==} + engines: {node: '>=0.4.x'} + dev: true + + /querystring@0.2.0: + resolution: {integrity: sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==} + engines: {node: '>=0.4.x'} + deprecated: The querystring API is considered Legacy. new code should use the URLSearchParams API instead. + dev: true + + /querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + + /queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + /quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + dev: false + + /raf@3.4.1: + resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} + dependencies: + performance-now: 2.1.0 + dev: false + + /randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + dependencies: + safe-buffer: 5.2.1 + + /randomfill@1.0.4: + resolution: {integrity: sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==} + dependencies: + randombytes: 2.1.0 + safe-buffer: 5.2.1 + dev: true + + /range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + /raw-body@2.5.1: + resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==} + engines: {node: '>= 0.8'} + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + /raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + dev: true + + /rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + dev: true + + /react-app-polyfill@3.0.0: + resolution: {integrity: sha512-sZ41cxiU5llIB003yxxQBYrARBqe0repqPTTYBTmMqTz9szeBbE37BehCE891NZsmdZqqP+xWKdT3eo3vOzN8w==} + engines: {node: '>=14'} + dependencies: + core-js: 3.29.1 + object-assign: 4.1.1 + promise: 8.3.0 + raf: 3.4.1 + regenerator-runtime: 0.13.11 + whatwg-fetch: 3.6.2 + dev: false + + /react-dev-utils@12.0.1(eslint@8.57.1)(typescript@4.9.5)(webpack@5.76.3): + resolution: {integrity: sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=2.7' + webpack: '>=4' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@babel/code-frame': 7.18.6 + address: 1.2.2 + browserslist: 4.21.5 + chalk: 4.1.2 + cross-spawn: 7.0.3 + detect-port-alt: 1.1.6 + escape-string-regexp: 4.0.0 + filesize: 8.0.7 + find-up: 5.0.0 + fork-ts-checker-webpack-plugin: 6.5.3(eslint@8.57.1)(typescript@4.9.5)(webpack@5.76.3) + global-modules: 2.0.0 + globby: 11.1.0 + gzip-size: 6.0.0 + immer: 9.0.21 + is-root: 2.1.0 + loader-utils: 3.2.1 + open: 8.4.2 + pkg-up: 3.1.0 + prompts: 2.4.2 + react-error-overlay: 6.0.11 + recursive-readdir: 2.2.3 + shell-quote: 1.8.0 + strip-ansi: 6.0.1 + text-table: 0.2.0 + typescript: 4.9.5 + webpack: 5.76.3(@swc/core@1.3.42) + transitivePeerDependencies: + - eslint + - supports-color + - vue-template-compiler + dev: false + + /react-dom@18.2.0(react@18.2.0): + resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} + peerDependencies: + react: ^18.2.0 + dependencies: + loose-envify: 1.4.0 + react: 18.2.0 + scheduler: 0.23.0 + dev: false + + /react-error-overlay@6.0.11: + resolution: {integrity: sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==} + dev: false + + /react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + dev: false + + /react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + dev: false + + /react-is@18.2.0: + resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + dev: false + + /react-refresh@0.11.0: + resolution: {integrity: sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==} + engines: {node: '>=0.10.0'} + dev: false + + /react-scripts@5.0.1(@babel/plugin-syntax-flow@7.18.6)(@babel/plugin-transform-react-jsx@7.21.0)(@swc/core@1.3.42)(eslint@8.57.1)(react@18.2.0)(typescript@4.9.5): + resolution: {integrity: sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ==} + engines: {node: '>=14.0.0'} + hasBin: true + peerDependencies: + eslint: '*' + react: '>= 16' + typescript: ^3.2.1 || ^4 + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@babel/core': 7.21.3 + '@pmmmwh/react-refresh-webpack-plugin': 0.5.10(react-refresh@0.11.0)(webpack-dev-server@4.13.1)(webpack@5.76.3) + '@svgr/webpack': 5.5.0 + babel-jest: 27.5.1(@babel/core@7.21.3) + babel-loader: 8.3.0(@babel/core@7.21.3)(webpack@5.76.3) + babel-plugin-named-asset-import: 0.3.8(@babel/core@7.21.3) + babel-preset-react-app: 10.0.1 + bfj: 7.0.2 + browserslist: 4.21.5 + camelcase: 6.3.0 + case-sensitive-paths-webpack-plugin: 2.4.0 + css-loader: 6.7.3(webpack@5.76.3) + css-minimizer-webpack-plugin: 3.4.1(webpack@5.76.3) + dotenv: 10.0.0 + dotenv-expand: 5.1.0 + eslint: 8.57.1 + eslint-config-react-app: 7.0.1(@babel/plugin-syntax-flow@7.18.6)(@babel/plugin-transform-react-jsx@7.21.0)(eslint@8.57.1)(jest@27.5.1)(typescript@4.9.5) + eslint-webpack-plugin: 3.2.0(eslint@8.57.1)(webpack@5.76.3) + file-loader: 6.2.0(webpack@5.76.3) + fs-extra: 10.1.0 + html-webpack-plugin: 5.5.0(webpack@5.76.3) + identity-obj-proxy: 3.0.0 + jest: 27.5.1 + jest-resolve: 27.5.1 + jest-watch-typeahead: 1.1.0(jest@27.5.1) + mini-css-extract-plugin: 2.7.5(webpack@5.76.3) + postcss: 8.4.21 + postcss-flexbugs-fixes: 5.0.2(postcss@8.4.21) + postcss-loader: 6.2.1(postcss@8.4.21)(webpack@5.76.3) + postcss-normalize: 10.0.1(browserslist@4.21.5)(postcss@8.4.21) + postcss-preset-env: 7.8.3(postcss@8.4.21) + prompts: 2.4.2 + react: 18.2.0 + react-app-polyfill: 3.0.0 + react-dev-utils: 12.0.1(eslint@8.57.1)(typescript@4.9.5)(webpack@5.76.3) + react-refresh: 0.11.0 + resolve: 1.22.1 + resolve-url-loader: 4.0.0 + sass-loader: 12.6.0(webpack@5.76.3) + semver: 7.3.8 + source-map-loader: 3.0.2(webpack@5.76.3) + style-loader: 3.3.2(webpack@5.76.3) + tailwindcss: 3.3.0(postcss@8.4.21) + terser-webpack-plugin: 5.3.7(@swc/core@1.3.42)(webpack@5.76.3) + typescript: 4.9.5 + webpack: 5.76.3(@swc/core@1.3.42) + webpack-dev-server: 4.13.1(webpack@5.76.3) + webpack-manifest-plugin: 4.1.1(webpack@5.76.3) + workbox-webpack-plugin: 6.5.4(webpack@5.76.3) + optionalDependencies: + fsevents: 2.3.3 + transitivePeerDependencies: + - '@babel/plugin-syntax-flow' + - '@babel/plugin-transform-react-jsx' + - '@parcel/css' + - '@swc/core' + - '@types/babel__core' + - '@types/webpack' + - bufferutil + - canvas + - clean-css + - csso + - debug + - esbuild + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - fibers + - node-notifier + - node-sass + - rework + - rework-visit + - sass + - sass-embedded + - sockjs-client + - supports-color + - ts-node + - type-fest + - uglify-js + - utf-8-validate + - vue-template-compiler + - webpack-cli + - webpack-hot-middleware + - webpack-plugin-serve + dev: false + + /react@18.2.0: + resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} + engines: {node: '>=0.10.0'} + dependencies: + loose-envify: 1.4.0 + dev: false + + /read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + dependencies: + pify: 2.3.0 + + /readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + /readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + /readdirp@2.2.1(supports-color@6.1.0): + resolution: {integrity: sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==} + engines: {node: '>=0.10'} + requiresBuild: true + dependencies: + graceful-fs: 4.2.11 + micromatch: 3.1.10(supports-color@6.1.0) + readable-stream: 2.3.8 + transitivePeerDependencies: + - supports-color + dev: true + + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + + /recursive-readdir@2.2.3: + resolution: {integrity: sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==} + engines: {node: '>=6.0.0'} + dependencies: + minimatch: 3.1.2 + dev: false + + /reduce@1.0.2: + resolution: {integrity: sha512-xX7Fxke/oHO5IfZSk77lvPa/7bjMh9BuCk4OOoX5XTXrM7s0Z+MkPfSDfz0q7r91BhhGSs8gii/VEN/7zhCPpQ==} + dependencies: + object-keys: 1.1.1 + dev: true + + /regenerate-unicode-properties@10.1.0: + resolution: {integrity: sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==} + engines: {node: '>=4'} + dependencies: + regenerate: 1.4.2 + + /regenerate@1.4.2: + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + + /regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + + /regenerator-transform@0.15.1: + resolution: {integrity: sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==} + dependencies: + '@babel/runtime': 7.21.0 + + /regex-not@1.0.2: + resolution: {integrity: sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==} + engines: {node: '>=0.10.0'} + dependencies: + extend-shallow: 3.0.2 + safe-regex: 1.1.0 + dev: true + + /regex-parser@2.2.11: + resolution: {integrity: sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==} + dev: false + + /regexp.prototype.flags@1.4.3: + resolution: {integrity: sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + functions-have-names: 1.2.3 + + /regexpp@3.2.0: + resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} + engines: {node: '>=8'} + dev: true + + /regexpu-core@5.3.2: + resolution: {integrity: sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==} + engines: {node: '>=4'} + dependencies: + '@babel/regjsgen': 0.8.0 + regenerate: 1.4.2 + regenerate-unicode-properties: 10.1.0 + regjsparser: 0.9.1 + unicode-match-property-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.1.0 + + /registry-auth-token@4.2.2: + resolution: {integrity: sha512-PC5ZysNb42zpFME6D/XlIgtNGdTl8bBOCw90xQLVMpzuuubJKYDWFAEuUNc+Cn8Z8724tg2SDhDRrkVEsqfDMg==} + engines: {node: '>=6.0.0'} + dependencies: + rc: 1.2.8 + dev: true + + /registry-url@5.1.0: + resolution: {integrity: sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==} + engines: {node: '>=8'} + dependencies: + rc: 1.2.8 + dev: true + + /regjsparser@0.9.1: + resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==} + hasBin: true + dependencies: + jsesc: 0.5.0 + + /relateurl@0.2.7: + resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==} + engines: {node: '>= 0.10'} + + /remove-trailing-separator@1.1.0: + resolution: {integrity: sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==} + requiresBuild: true + dev: true + + /renderkid@2.0.7: + resolution: {integrity: sha512-oCcFyxaMrKsKcTY59qnCAtmDVSLfPbrv6A3tVbPdFMMrv5jaK10V6m40cKsoPNhAqN6rmHW9sswW4o3ruSrwUQ==} + dependencies: + css-select: 4.3.0 + dom-converter: 0.2.0 + htmlparser2: 6.1.0 + lodash: 4.17.21 + strip-ansi: 3.0.1 + dev: true + + /renderkid@3.0.0: + resolution: {integrity: sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==} + dependencies: + css-select: 4.3.0 + dom-converter: 0.2.0 + htmlparser2: 6.1.0 + lodash: 4.17.21 + strip-ansi: 6.0.1 + dev: false + + /repeat-element@1.1.4: + resolution: {integrity: sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==} + engines: {node: '>=0.10.0'} + dev: true + + /repeat-string@1.6.1: + resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} + engines: {node: '>=0.10'} + dev: true + + /request@2.88.2: + resolution: {integrity: sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==} + engines: {node: '>= 6'} + deprecated: request has been deprecated, see https://github.com/request/request/issues/3142 + dependencies: + aws-sign2: 0.7.0 + aws4: 1.12.0 + caseless: 0.12.0 + combined-stream: 1.0.8 + extend: 3.0.2 + forever-agent: 0.6.1 + form-data: 2.3.3 + har-validator: 5.1.5 + http-signature: 1.2.0 + is-typedarray: 1.0.0 + isstream: 0.1.2 + json-stringify-safe: 5.0.1 + mime-types: 2.1.35 + oauth-sign: 0.9.0 + performance-now: 2.1.0 + qs: 6.5.3 + safe-buffer: 5.2.1 + tough-cookie: 2.5.0 + tunnel-agent: 0.6.0 + uuid: 3.4.0 + dev: true + + /require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + /require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + dev: false + + /require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + dev: true + + /requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + + /resolve-cwd@2.0.0: + resolution: {integrity: sha512-ccu8zQTrzVr954472aUVPLEcB3YpKSYR3cg/3lo1okzobPBM+1INXBbBZlDbnI/hbEocnf8j0QVo43hQKrbchg==} + engines: {node: '>=4'} + dependencies: + resolve-from: 3.0.0 + dev: true + + /resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + dependencies: + resolve-from: 5.0.0 + dev: false + + /resolve-from@3.0.0: + resolution: {integrity: sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==} + engines: {node: '>=4'} + dev: true + + /resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + /resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + dev: false + + /resolve-url-loader@4.0.0: + resolution: {integrity: sha512-05VEMczVREcbtT7Bz+C+96eUO5HDNvdthIiMB34t7FcF8ehcu4wC0sSgPUubs3XW2Q3CNLJk/BJrCU9wVRymiA==} + engines: {node: '>=8.9'} + peerDependencies: + rework: 1.0.1 + rework-visit: 1.0.0 + peerDependenciesMeta: + rework: + optional: true + rework-visit: + optional: true + dependencies: + adjust-sourcemap-loader: 4.0.0 + convert-source-map: 1.9.0 + loader-utils: 2.0.4 + postcss: 7.0.39 + source-map: 0.6.1 + dev: false + + /resolve-url@0.2.1: + resolution: {integrity: sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==} + deprecated: https://github.com/lydell/resolve-url#deprecated + dev: true + + /resolve.exports@1.1.1: + resolution: {integrity: sha512-/NtpHNDN7jWhAaQ9BvBUYZ6YTXsRBgfqWFWP7BZBaoMJO/I3G5OFzvTuWNlZC3aPjins1F+TNrLKsGbH4rfsRQ==} + engines: {node: '>=10'} + dev: false + + /resolve@1.22.1: + resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==} + hasBin: true + dependencies: + is-core-module: 2.11.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + /resolve@2.0.0-next.4: + resolution: {integrity: sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==} + hasBin: true + dependencies: + is-core-module: 2.11.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: false + + /responselike@1.0.2: + resolution: {integrity: sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==} + dependencies: + lowercase-keys: 1.0.1 + dev: true + + /ret@0.1.15: + resolution: {integrity: sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==} + engines: {node: '>=0.12'} + dev: true + + /retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + dev: true + + /retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + dev: false + + /reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + /rfdc@1.3.0: + resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==} + dev: true + + /rgb-regex@1.0.1: + resolution: {integrity: sha512-gDK5mkALDFER2YLqH6imYvK6g02gpNGM4ILDZ472EwWfXZnC2ZEpoB2ECXTyOVUKuk/bPJZMzwQPBYICzP+D3w==} + dev: true + + /rgba-regex@1.0.0: + resolution: {integrity: sha512-zgn5OjNQXLUTdq8m17KdaicF6w89TZs8ZU8y0AYENIU6wG8GG6LLm0yLSiPY8DmaYmHdgRW8rnApjoT0fQRfMg==} + dev: true + + /rimraf@2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + hasBin: true + dependencies: + glob: 7.2.3 + dev: true + + /rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + hasBin: true + dependencies: + glob: 7.2.3 + + /ripemd160@2.0.2: + resolution: {integrity: sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==} + dependencies: + hash-base: 3.1.0 + inherits: 2.0.4 + dev: true + + /rollup-plugin-cleanup@3.2.1(rollup@3.20.2): + resolution: {integrity: sha512-zuv8EhoO3TpnrU8MX8W7YxSbO4gmOR0ny06Lm3nkFfq0IVKdBUtHwhVzY1OAJyNCIAdLiyPnOrU0KnO0Fri1GQ==} + engines: {node: ^10.14.2 || >=12.0.0} + peerDependencies: + rollup: '>=2.0' + dependencies: + js-cleanup: 1.2.0 + rollup: 3.20.2 + rollup-pluginutils: 2.8.2 + dev: true + + /rollup-plugin-istanbul@4.0.0(rollup@3.20.2): + resolution: {integrity: sha512-AOauxxl4eAHWdvTnY/uwSrwMkbDymTWUhaD6aym8a4YJaO9hxK2U8bcuhZA0iravuOTUulqPWUbYP7mTV7i4oQ==} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@rollup/pluginutils': 5.0.2(rollup@3.20.2) + istanbul-lib-instrument: 5.2.1 + rollup: 3.20.2 + transitivePeerDependencies: + - supports-color + dev: true + + /rollup-plugin-swc3@0.7.0(@swc/core@1.3.42)(rollup@3.20.2): + resolution: {integrity: sha512-aWkbRGjmzSLs8BPQEuGo3PQsBAsYyL9Nk5xZ6ruEnBp+5RN9KavSQV1nM13gSmXZNBhz7Wh5mscyo5lCWQ1Bpg==} + engines: {node: '>=12'} + peerDependencies: + '@swc/core': '>=1.2.165' + rollup: ^2.0.0 || ^3.0.0 + dependencies: + '@fastify/deepmerge': 1.3.0 + '@rollup/pluginutils': 4.2.1 + '@swc/core': 1.3.42 + get-tsconfig: 4.5.0 + rollup: 3.20.2 + dev: true + + /rollup-plugin-terser@7.0.2(rollup@2.79.1): + resolution: {integrity: sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==} + deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser + peerDependencies: + rollup: ^2.0.0 + dependencies: + '@babel/code-frame': 7.18.6 + jest-worker: 26.6.2 + rollup: 2.79.1 + serialize-javascript: 4.0.0 + terser: 5.16.8 + dev: false + + /rollup-plugin-terser@7.0.2(rollup@3.20.2): + resolution: {integrity: sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==} + deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser + peerDependencies: + rollup: ^2.0.0 + dependencies: + '@babel/code-frame': 7.18.6 + jest-worker: 26.6.2 + rollup: 3.20.2 + serialize-javascript: 4.0.0 + terser: 5.16.8 + dev: true + + /rollup-pluginutils@2.8.2: + resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==} + dependencies: + estree-walker: 0.6.1 + dev: true + + /rollup@2.79.1: + resolution: {integrity: sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==} + engines: {node: '>=10.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.3 + dev: false + + /rollup@3.20.2: + resolution: {integrity: sha512-3zwkBQl7Ai7MFYQE0y1MeQ15+9jsi7XxfrqwTb/9EK8D9C9+//EBR4M+CuA1KODRaNbFez/lWxA5vhEGZp4MUg==} + engines: {node: '>=14.18.0', npm: '>=8.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + + /run-queue@1.0.3: + resolution: {integrity: sha512-ntymy489o0/QQplUDnpYAYUsO50K9SBrIVaKCWDOJzYJts0f9WH9RFJkyagebkw5+y1oi00R7ynNW/d12GBumg==} + dependencies: + aproba: 1.2.0 + dev: true + + /rxjs@7.8.0: + resolution: {integrity: sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==} + dependencies: + tslib: 2.5.0 + dev: true + + /safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + /safe-regex-test@1.0.0: + resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.0 + is-regex: 1.1.4 + + /safe-regex@1.1.0: + resolution: {integrity: sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==} + dependencies: + ret: 0.1.15 + dev: true + + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + /sanitize.css@13.0.0: + resolution: {integrity: sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA==} + dev: false + + /sass-loader@12.6.0(webpack@5.76.3): + resolution: {integrity: sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA==} + engines: {node: '>= 12.13.0'} + peerDependencies: + fibers: '>= 3.1.0' + node-sass: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + sass: ^1.3.0 + sass-embedded: '*' + webpack: ^5.0.0 + peerDependenciesMeta: + fibers: + optional: true + node-sass: + optional: true + sass: + optional: true + sass-embedded: + optional: true + dependencies: + klona: 2.0.6 + neo-async: 2.6.2 + webpack: 5.76.3(@swc/core@1.3.42) + dev: false + + /sax@1.2.4: + resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==} + + /saxes@5.0.1: + resolution: {integrity: sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==} + engines: {node: '>=10'} + dependencies: + xmlchars: 2.2.0 + dev: false + + /scheduler@0.23.0: + resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} + dependencies: + loose-envify: 1.4.0 + dev: false + + /schema-utils@1.0.0: + resolution: {integrity: sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==} + engines: {node: '>= 4'} + dependencies: + ajv: 6.12.6 + ajv-errors: 1.0.1(ajv@6.12.6) + ajv-keywords: 3.5.2(ajv@6.12.6) + dev: true + + /schema-utils@2.7.0: + resolution: {integrity: sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==} + engines: {node: '>= 8.9.0'} + dependencies: + '@types/json-schema': 7.0.11 + ajv: 6.12.6 + ajv-keywords: 3.5.2(ajv@6.12.6) + dev: false + + /schema-utils@2.7.1: + resolution: {integrity: sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==} + engines: {node: '>= 8.9.0'} + dependencies: + '@types/json-schema': 7.0.11 + ajv: 6.12.6 + ajv-keywords: 3.5.2(ajv@6.12.6) + + /schema-utils@3.1.1: + resolution: {integrity: sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==} + engines: {node: '>= 10.13.0'} + dependencies: + '@types/json-schema': 7.0.11 + ajv: 6.12.6 + ajv-keywords: 3.5.2(ajv@6.12.6) + dev: false + + /schema-utils@4.0.0: + resolution: {integrity: sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==} + engines: {node: '>= 12.13.0'} + dependencies: + '@types/json-schema': 7.0.11 + ajv: 8.12.0 + ajv-formats: 2.1.1(ajv@8.12.0) + ajv-keywords: 5.1.0(ajv@8.12.0) + dev: false + + /section-matter@1.0.0: + resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} + engines: {node: '>=4'} + dependencies: + extend-shallow: 2.0.1 + kind-of: 6.0.3 + dev: true + + /select-hose@2.0.0: + resolution: {integrity: sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==} + + /selfsigned@1.10.14: + resolution: {integrity: sha512-lkjaiAye+wBZDCBsu5BGi0XiLRxeUlsGod5ZP924CRSEoGuZAw/f7y9RKu28rwTfiHVhdavhB0qH0INV6P1lEA==} + dependencies: + node-forge: 0.10.0 + dev: true + + /selfsigned@2.1.1: + resolution: {integrity: sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==} + engines: {node: '>=10'} + dependencies: + node-forge: 1.3.1 + dev: false + + /semiver@1.1.0: + resolution: {integrity: sha512-QNI2ChmuioGC1/xjyYwyZYADILWyW6AmS1UH6gDj/SFUUUS4MBAWs/7mxnkRPc/F4iHezDP+O8t0dO8WHiEOdg==} + engines: {node: '>=6'} + dev: true + + /semver-diff@3.1.1: + resolution: {integrity: sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==} + engines: {node: '>=8'} + dependencies: + semver: 6.3.0 + dev: true + + /semver@5.7.1: + resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==} + hasBin: true + dev: true + + /semver@6.3.0: + resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} + hasBin: true + + /semver@7.3.8: + resolution: {integrity: sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + + /send@0.18.0(supports-color@6.1.0): + resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} + engines: {node: '>= 0.8.0'} + dependencies: + debug: 2.6.9(supports-color@6.1.0) + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + /serialize-javascript@4.0.0: + resolution: {integrity: sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==} + dependencies: + randombytes: 2.1.0 + + /serialize-javascript@6.0.1: + resolution: {integrity: sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==} + dependencies: + randombytes: 2.1.0 + + /serve-index@1.9.1(supports-color@6.1.0): + resolution: {integrity: sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==} + engines: {node: '>= 0.8.0'} + dependencies: + accepts: 1.3.8 + batch: 0.6.1 + debug: 2.6.9(supports-color@6.1.0) + escape-html: 1.0.3 + http-errors: 1.6.3 + mime-types: 2.1.35 + parseurl: 1.3.3 + transitivePeerDependencies: + - supports-color + + /serve-static@1.15.0(supports-color@6.1.0): + resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} + engines: {node: '>= 0.8.0'} + dependencies: + encodeurl: 1.0.2 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.18.0(supports-color@6.1.0) + transitivePeerDependencies: + - supports-color + + /set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + dev: true + + /set-value@2.0.1: + resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==} + engines: {node: '>=0.10.0'} + dependencies: + extend-shallow: 2.0.1 + is-extendable: 0.1.1 + is-plain-object: 2.0.4 + split-string: 3.1.0 + dev: true + + /setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + dev: true + + /setprototypeof@1.1.0: + resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==} + + /setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + /sha.js@2.4.11: + resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==} + hasBin: true + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + dev: true + + /shebang-command@1.2.0: + resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} + engines: {node: '>=0.10.0'} + dependencies: + shebang-regex: 1.0.0 + dev: true + + /shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + + /shebang-regex@1.0.0: + resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} + engines: {node: '>=0.10.0'} + dev: true + + /shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + /shell-quote@1.8.0: + resolution: {integrity: sha512-QHsz8GgQIGKlRi24yFc6a6lN69Idnx634w49ay6+jA5yFh7a1UY+4Rp6HPx/L/1zcEDPEij8cIsiqR6bQsE5VQ==} + + /shiki@0.14.1: + resolution: {integrity: sha512-+Jz4nBkCBe0mEDqo1eKRcCdjRtrCjozmcbTUjbPTX7OOJfEbTZzlUWlZtGe3Gb5oV1/jnojhG//YZc3rs9zSEw==} + dependencies: + ansi-sequence-parser: 1.1.0 + jsonc-parser: 3.2.0 + vscode-oniguruma: 1.7.0 + vscode-textmate: 8.0.0 + dev: true + + /side-channel@1.0.4: + resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.0 + object-inspect: 1.12.3 + + /signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + /simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + dependencies: + is-arrayish: 0.3.2 + dev: true + + /sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + dev: false + + /skip-regex@1.0.2: + resolution: {integrity: sha512-pEjMUbwJ5Pl/6Vn6FsamXHXItJXSRftcibixDmNCWbWhic0hzHrwkMZo0IZ7fMRH9KxcWDFSkzhccB4285PutA==} + engines: {node: '>=4.2'} + dev: true + + /slash@1.0.0: + resolution: {integrity: sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg==} + engines: {node: '>=0.10.0'} + dev: true + + /slash@2.0.0: + resolution: {integrity: sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==} + engines: {node: '>=6'} + dev: true + + /slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + /slash@4.0.0: + resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} + engines: {node: '>=12'} + dev: false + + /smoothscroll-polyfill@0.4.4: + resolution: {integrity: sha512-TK5ZA9U5RqCwMpfoMq/l1mrH0JAR7y7KRvOBx0n2869aLxch+gT9GhN3yUfjiw+d/DiF1mKo14+hd62JyMmoBg==} + dev: true + + /snapdragon-node@2.1.1: + resolution: {integrity: sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==} + engines: {node: '>=0.10.0'} + dependencies: + define-property: 1.0.0 + isobject: 3.0.1 + snapdragon-util: 3.0.1 + dev: true + + /snapdragon-util@3.0.1: + resolution: {integrity: sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==} + engines: {node: '>=0.10.0'} + dependencies: + kind-of: 3.2.2 + dev: true + + /snapdragon@0.8.2(supports-color@6.1.0): + resolution: {integrity: sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==} + engines: {node: '>=0.10.0'} + dependencies: + base: 0.11.2 + debug: 2.6.9(supports-color@6.1.0) + define-property: 0.2.5 + extend-shallow: 2.0.1 + map-cache: 0.2.2 + source-map: 0.5.7 + source-map-resolve: 0.5.3 + use: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /socket.io-adapter@2.5.2: + resolution: {integrity: sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==} + dependencies: + ws: 8.11.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: true + + /socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} + dependencies: + '@socket.io/component-emitter': 3.1.0 + debug: 4.3.4(supports-color@6.1.0) + transitivePeerDependencies: + - supports-color + dev: true + + /socket.io@4.7.5: + resolution: {integrity: sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==} + engines: {node: '>=10.2.0'} + dependencies: + accepts: 1.3.8 + base64id: 2.0.0 + cors: 2.8.5 + debug: 4.3.4(supports-color@6.1.0) + engine.io: 6.5.5 + socket.io-adapter: 2.5.2 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: true + + /sockjs-client@1.6.1(supports-color@6.1.0): + resolution: {integrity: sha512-2g0tjOR+fRs0amxENLi/q5TiJTqY+WXFOzb5UwXndlK6TO3U/mirZznpx6w34HVMoc3g7cY24yC/ZMIYnDlfkw==} + engines: {node: '>=12'} + dependencies: + debug: 3.2.7(supports-color@6.1.0) + eventsource: 2.0.2 + faye-websocket: 0.11.4 + inherits: 2.0.4 + url-parse: 1.5.10 + transitivePeerDependencies: + - supports-color + dev: true + + /sockjs@0.3.24: + resolution: {integrity: sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==} + dependencies: + faye-websocket: 0.11.4 + uuid: 8.3.2 + websocket-driver: 0.7.4 + + /sort-keys@2.0.0: + resolution: {integrity: sha512-/dPCrG1s3ePpWm6yBbxZq5Be1dXGLyLn9Z791chDC3NFrpkVbWGzkBwPN1knaciexFXgRJ7hzdnwZ4stHSDmjg==} + engines: {node: '>=4'} + dependencies: + is-plain-obj: 1.1.0 + dev: true + + /source-list-map@2.0.1: + resolution: {integrity: sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==} + + /source-map-js@1.0.2: + resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} + engines: {node: '>=0.10.0'} + + /source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + dev: true + + /source-map-loader@3.0.2(webpack@5.76.3): + resolution: {integrity: sha512-BokxPoLjyl3iOrgkWaakaxqnelAJSS+0V+De0kKIq6lyWrXuiPgYTGp6z3iHmqljKAaLXwZa+ctD8GccRJeVvg==} + engines: {node: '>= 12.13.0'} + peerDependencies: + webpack: ^5.0.0 + dependencies: + abab: 2.0.6 + iconv-lite: 0.6.3 + source-map-js: 1.0.2 + webpack: 5.76.3(@swc/core@1.3.42) + dev: false + + /source-map-resolve@0.5.3: + resolution: {integrity: sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==} + deprecated: See https://github.com/lydell/source-map-resolve#deprecated + dependencies: + atob: 2.1.2 + decode-uri-component: 0.2.2 + resolve-url: 0.2.1 + source-map-url: 0.4.1 + urix: 0.1.0 + dev: true + + /source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + /source-map-url@0.4.1: + resolution: {integrity: sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==} + deprecated: See https://github.com/lydell/source-map-url#deprecated + dev: true + + /source-map@0.5.6: + resolution: {integrity: sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==} + engines: {node: '>=0.10.0'} + dev: true + + /source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + dev: true + + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + /source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + + /source-map@0.8.0-beta.0: + resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} + engines: {node: '>= 8'} + dependencies: + whatwg-url: 7.1.0 + dev: false + + /sourcemap-codec@1.4.8: + resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} + deprecated: Please use @jridgewell/sourcemap-codec instead + + /spawn-command@0.0.2-1: + resolution: {integrity: sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg==} + dev: true + + /spdy-transport@3.0.0(supports-color@6.1.0): + resolution: {integrity: sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==} + dependencies: + debug: 4.3.4(supports-color@6.1.0) + detect-node: 2.1.0 + hpack.js: 2.1.6 + obuf: 1.1.2 + readable-stream: 3.6.2 + wbuf: 1.7.3 + transitivePeerDependencies: + - supports-color + + /spdy@4.0.2(supports-color@6.1.0): + resolution: {integrity: sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==} + engines: {node: '>=6.0.0'} + dependencies: + debug: 4.3.4(supports-color@6.1.0) + handle-thing: 2.0.1 + http-deceiver: 1.2.7 + select-hose: 2.0.0 + spdy-transport: 3.0.0(supports-color@6.1.0) + transitivePeerDependencies: + - supports-color + + /split-string@3.1.0: + resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==} + engines: {node: '>=0.10.0'} + dependencies: + extend-shallow: 3.0.2 + dev: true + + /sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + /sshpk@1.17.0: + resolution: {integrity: sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==} + engines: {node: '>=0.10.0'} + hasBin: true + dependencies: + asn1: 0.2.6 + assert-plus: 1.0.0 + bcrypt-pbkdf: 1.0.2 + dashdash: 1.14.1 + ecc-jsbn: 0.1.2 + getpass: 0.1.7 + jsbn: 0.1.1 + safer-buffer: 2.1.2 + tweetnacl: 0.14.5 + dev: true + + /ssri@6.0.2: + resolution: {integrity: sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==} + dependencies: + figgy-pudding: 3.5.2 + dev: true + + /stable@0.1.8: + resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==} + deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility' + + /stack-utils@1.0.5: + resolution: {integrity: sha512-KZiTzuV3CnSnSvgMRrARVCj+Ht7rMbauGDK0LdVFRGyenwdylpajAp4Q0i6SX8rEmbTpMMf6ryq2gb8pPq2WgQ==} + engines: {node: '>=8'} + dependencies: + escape-string-regexp: 2.0.0 + dev: true + + /stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + dependencies: + escape-string-regexp: 2.0.0 + dev: false + + /stackframe@1.3.4: + resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} + dev: false + + /static-extend@0.1.2: + resolution: {integrity: sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==} + engines: {node: '>=0.10.0'} + dependencies: + define-property: 0.2.5 + object-copy: 0.1.0 + dev: true + + /statuses@1.5.0: + resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} + engines: {node: '>= 0.6'} + + /statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + /std-env@2.3.1: + resolution: {integrity: sha512-eOsoKTWnr6C8aWrqJJ2KAReXoa7Vn5Ywyw6uCXgA/xDhxPoaIsBa5aNJmISY04dLwXPBnDHW4diGM7Sn5K4R/g==} + dependencies: + ci-info: 3.8.0 + dev: true + + /stop-iteration-iterator@1.0.0: + resolution: {integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==} + engines: {node: '>= 0.4'} + dependencies: + internal-slot: 1.0.5 + dev: false + + /stream-browserify@2.0.2: + resolution: {integrity: sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==} + dependencies: + inherits: 2.0.4 + readable-stream: 2.3.8 + dev: true + + /stream-each@1.2.3: + resolution: {integrity: sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==} + dependencies: + end-of-stream: 1.4.4 + stream-shift: 1.0.1 + dev: true + + /stream-http@2.8.3: + resolution: {integrity: sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==} + dependencies: + builtin-status-codes: 3.0.0 + inherits: 2.0.4 + readable-stream: 2.3.8 + to-arraybuffer: 1.0.1 + xtend: 4.0.2 + dev: true + + /stream-shift@1.0.1: + resolution: {integrity: sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==} + dev: true + + /streamroller@3.1.5: + resolution: {integrity: sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==} + engines: {node: '>=8.0'} + dependencies: + date-format: 4.0.14 + debug: 4.3.4(supports-color@6.1.0) + fs-extra: 8.1.0 + transitivePeerDependencies: + - supports-color + dev: true + + /strict-uri-encode@1.1.0: + resolution: {integrity: sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==} + engines: {node: '>=0.10.0'} + dev: true + + /string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.1 + dev: false + + /string-length@5.0.1: + resolution: {integrity: sha512-9Ep08KAMUn0OadnVaBuRdE2l615CQ508kr0XMadjClfYpdCyvrbFp6Taebo8yyxokQ4viUd/xPPUA4FGgUa0ow==} + engines: {node: '>=12.20'} + dependencies: + char-regex: 2.0.1 + strip-ansi: 7.0.1 + dev: false + + /string-natural-compare@3.0.1: + resolution: {integrity: sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==} + dev: false + + /string-width@3.1.0: + resolution: {integrity: sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==} + engines: {node: '>=6'} + dependencies: + emoji-regex: 7.0.3 + is-fullwidth-code-point: 2.0.0 + strip-ansi: 5.2.0 + dev: true + + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + /string.prototype.matchall@4.0.8: + resolution: {integrity: sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + get-intrinsic: 1.2.0 + has-symbols: 1.0.3 + internal-slot: 1.0.5 + regexp.prototype.flags: 1.4.3 + side-channel: 1.0.4 + dev: false + + /string.prototype.trim@1.2.7: + resolution: {integrity: sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + + /string.prototype.trimend@1.0.6: + resolution: {integrity: sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + + /string.prototype.trimstart@1.0.6: + resolution: {integrity: sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + + /string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + dependencies: + safe-buffer: 5.1.2 + + /string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + + /stringify-object@3.3.0: + resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==} + engines: {node: '>=4'} + dependencies: + get-own-enumerable-property-symbols: 3.0.2 + is-obj: 1.0.1 + is-regexp: 1.0.0 + + /strip-ansi@3.0.1: + resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==} + engines: {node: '>=0.10.0'} + dependencies: + ansi-regex: 2.1.1 + dev: true + + /strip-ansi@5.2.0: + resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==} + engines: {node: '>=6'} + dependencies: + ansi-regex: 4.1.1 + dev: true + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + + /strip-ansi@7.0.1: + resolution: {integrity: sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.0.1 + dev: false + + /strip-bom-string@1.0.0: + resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} + engines: {node: '>=0.10.0'} + dev: true + + /strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + dev: false + + /strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + dev: false + + /strip-comments@2.0.1: + resolution: {integrity: sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==} + engines: {node: '>=10'} + dev: false + + /strip-eof@1.0.0: + resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==} + engines: {node: '>=0.10.0'} + dev: true + + /strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + dev: false + + /strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + dev: true + + /strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + /style-loader@3.3.2(webpack@5.76.3): + resolution: {integrity: sha512-RHs/vcrKdQK8wZliteNK4NKzxvLBzpuHMqYmUVWeKa6MkaIQ97ZTOS0b+zapZhy6GcrgWnvWYCMHRirC3FsUmw==} + engines: {node: '>= 12.13.0'} + peerDependencies: + webpack: ^5.0.0 + dependencies: + webpack: 5.76.3(@swc/core@1.3.42) + dev: false + + /stylehacks@4.0.3: + resolution: {integrity: sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g==} + engines: {node: '>=6.9.0'} + dependencies: + browserslist: 4.21.5 + postcss: 7.0.39 + postcss-selector-parser: 3.1.2 + dev: true + + /stylehacks@5.1.1(postcss@8.4.21): + resolution: {integrity: sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.21.5 + postcss: 8.4.21 + postcss-selector-parser: 6.0.11 + dev: false + + /stylehacks@5.1.1(postcss@8.5.6): + resolution: {integrity: sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==} + engines: {node: ^10 || ^12 || >=14.0} + peerDependencies: + postcss: ^8.2.15 + dependencies: + browserslist: 4.21.5 + postcss: 8.5.6 + postcss-selector-parser: 6.0.11 + dev: true + + /stylus-loader@3.0.2(stylus@0.54.8): + resolution: {integrity: sha512-+VomPdZ6a0razP+zinir61yZgpw2NfljeSsdUF5kJuEzlo3khXhY19Fn6l8QQz1GRJGtMCo8nG5C04ePyV7SUA==} + peerDependencies: + stylus: '>=0.52.4' + dependencies: + loader-utils: 1.4.2 + lodash.clonedeep: 4.5.0 + stylus: 0.54.8 + when: 3.6.4 + dev: true + + /stylus@0.54.8: + resolution: {integrity: sha512-vr54Or4BZ7pJafo2mpf0ZcwA74rpuYCZbxrHBsH8kbcXOwSfvBFwsRfpGO5OD5fhG5HDCFW737PKaawI7OqEAg==} + hasBin: true + dependencies: + css-parse: 2.0.0 + debug: 3.1.0 + glob: 7.2.3 + mkdirp: 1.0.4 + safer-buffer: 2.1.2 + sax: 1.2.4 + semver: 6.3.0 + source-map: 0.7.4 + transitivePeerDependencies: + - supports-color + dev: true + + /sucrase@3.31.0: + resolution: {integrity: sha512-6QsHnkqyVEzYcaiHsOKkzOtOgdJcb8i54x6AV2hDwyZcY9ZyykGZVw6L/YN98xC0evwTP6utsWWrKRaa8QlfEQ==} + engines: {node: '>=8'} + hasBin: true + dependencies: + commander: 4.1.1 + glob: 7.1.6 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.5 + ts-interface-checker: 0.1.13 + dev: false + + /supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + + /supports-color@6.1.0: + resolution: {integrity: sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==} + engines: {node: '>=6'} + dependencies: + has-flag: 3.0.0 + + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + + /supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + dependencies: + has-flag: 4.0.0 + + /supports-hyperlinks@2.3.0: + resolution: {integrity: sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + supports-color: 7.2.0 + dev: false + + /supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + /svg-parser@2.0.4: + resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==} + dev: false + + /svg-tags@1.0.0: + resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==} + dev: true + + /svgo@1.3.2: + resolution: {integrity: sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==} + engines: {node: '>=4.0.0'} + deprecated: This SVGO version is no longer supported. Upgrade to v2.x.x. + hasBin: true + dependencies: + chalk: 2.4.2 + coa: 2.0.2 + css-select: 2.1.0 + css-select-base-adapter: 0.1.1 + css-tree: 1.0.0-alpha.37 + csso: 4.2.0 + js-yaml: 3.14.1 + mkdirp: 0.5.6 + object.values: 1.1.6 + sax: 1.2.4 + stable: 0.1.8 + unquote: 1.1.1 + util.promisify: 1.0.1 + + /svgo@2.8.0: + resolution: {integrity: sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==} + engines: {node: '>=10.13.0'} + hasBin: true + dependencies: + '@trysound/sax': 0.2.0 + commander: 7.2.0 + css-select: 4.3.0 + css-tree: 1.1.3 + csso: 4.2.0 + picocolors: 1.0.0 + stable: 0.1.8 + + /symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + dev: false + + /tailwindcss@3.3.0(postcss@8.4.21): + resolution: {integrity: sha512-hOXlFx+YcklJ8kXiCAfk/FMyr4Pm9ck477G0m/us2344Vuj355IpoEDB5UmGAsSpTBmr+4ZhjzW04JuFXkb/fw==} + engines: {node: '>=12.13.0'} + hasBin: true + peerDependencies: + postcss: ^8.0.9 + dependencies: + arg: 5.0.2 + chokidar: 3.5.3 + color-name: 1.1.4 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.2.12 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.18.2 + lilconfig: 2.1.0 + micromatch: 4.0.5 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.0.0 + postcss: 8.4.21 + postcss-import: 14.1.0(postcss@8.4.21) + postcss-js: 4.0.1(postcss@8.4.21) + postcss-load-config: 3.1.4(postcss@8.4.21) + postcss-nested: 6.0.0(postcss@8.4.21) + postcss-selector-parser: 6.0.11 + postcss-value-parser: 4.2.0 + quick-lru: 5.1.1 + resolve: 1.22.1 + sucrase: 3.31.0 + transitivePeerDependencies: + - ts-node + dev: false + + /tapable@1.1.3: + resolution: {integrity: sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==} + engines: {node: '>=6'} + + /tapable@2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + engines: {node: '>=6'} + dev: false + + /temp-dir@2.0.0: + resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} + engines: {node: '>=8'} + dev: false + + /tempy@0.6.0: + resolution: {integrity: sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==} + engines: {node: '>=10'} + dependencies: + is-stream: 2.0.1 + temp-dir: 2.0.0 + type-fest: 0.16.0 + unique-string: 2.0.0 + dev: false + + /term-size@2.2.1: + resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} + engines: {node: '>=8'} + dev: true + + /terminal-link@2.1.1: + resolution: {integrity: sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==} + engines: {node: '>=8'} + dependencies: + ansi-escapes: 4.3.2 + supports-hyperlinks: 2.3.0 + dev: false + + /terser-webpack-plugin@1.4.5(webpack@4.46.0): + resolution: {integrity: sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==} + engines: {node: '>= 6.9.0'} + peerDependencies: + webpack: ^4.0.0 + dependencies: + cacache: 12.0.4 + find-cache-dir: 2.1.0 + is-wsl: 1.1.0 + schema-utils: 1.0.0 + serialize-javascript: 4.0.0 + source-map: 0.6.1 + terser: 4.8.1 + webpack: 4.46.0 + webpack-sources: 1.4.3 + worker-farm: 1.7.0 + dev: true + + /terser-webpack-plugin@5.3.7(@swc/core@1.3.42)(webpack@5.76.3): + resolution: {integrity: sha512-AfKwIktyP7Cu50xNjXF/6Qb5lBNzYaWpU6YfoX3uZicTx0zTy0stDDCsvjDapKsSDvOeWo5MEq4TmdBy2cNoHw==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@swc/core': '*' + esbuild: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@swc/core': + optional: true + esbuild: + optional: true + uglify-js: + optional: true + dependencies: + '@jridgewell/trace-mapping': 0.3.17 + '@swc/core': 1.3.42 + jest-worker: 27.5.1 + schema-utils: 3.1.1 + serialize-javascript: 6.0.1 + terser: 5.16.8 + webpack: 5.76.3(@swc/core@1.3.42) + dev: false + + /terser@4.8.1: + resolution: {integrity: sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + acorn: 8.8.2 + commander: 2.20.3 + source-map: 0.6.1 + source-map-support: 0.5.21 + dev: true + + /terser@5.16.8: + resolution: {integrity: sha512-QI5g1E/ef7d+PsDifb+a6nnVgC4F22Bg6T0xrBrz6iloVB4PUkkunp6V8nzoOOZJIzjWVdAGqCdlKlhLq/TbIA==} + engines: {node: '>=10'} + hasBin: true + dependencies: + '@jridgewell/source-map': 0.3.2 + acorn: 8.8.2 + commander: 2.20.3 + source-map-support: 0.5.21 + + /test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + dev: false + + /text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + /thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + dependencies: + thenify: 3.3.1 + dev: false + + /thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + dependencies: + any-promise: 1.3.0 + dev: false + + /throat@6.0.2: + resolution: {integrity: sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ==} + dev: false + + /through2@2.0.5: + resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} + dependencies: + readable-stream: 2.3.8 + xtend: 4.0.2 + dev: true + + /through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + dev: true + + /thunky@1.1.0: + resolution: {integrity: sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==} + + /timers-browserify@2.0.12: + resolution: {integrity: sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==} + engines: {node: '>=0.6.0'} + dependencies: + setimmediate: 1.0.5 + dev: true + + /timsort@0.3.0: + resolution: {integrity: sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==} + dev: true + + /tmp@0.2.1: + resolution: {integrity: sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==} + engines: {node: '>=8.17.0'} + dependencies: + rimraf: 3.0.2 + dev: true + + /tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + dev: false + + /to-arraybuffer@1.0.1: + resolution: {integrity: sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA==} + dev: true + + /to-factory@1.0.0: + resolution: {integrity: sha512-JVYrY42wMG7ddf+wBUQR/uHGbjUHZbLisJ8N62AMm0iTZ0p8YTcZLzdtomU0+H+wa99VbkyvQGB3zxB7NDzgIQ==} + dev: true + + /to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + + /to-object-path@0.3.0: + resolution: {integrity: sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==} + engines: {node: '>=0.10.0'} + dependencies: + kind-of: 3.2.2 + dev: true + + /to-readable-stream@1.0.0: + resolution: {integrity: sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==} + engines: {node: '>=6'} + dev: true + + /to-regex-range@2.1.1: + resolution: {integrity: sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==} + engines: {node: '>=0.10.0'} + dependencies: + is-number: 3.0.0 + repeat-string: 1.6.1 + dev: true + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + + /to-regex@3.0.2: + resolution: {integrity: sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==} + engines: {node: '>=0.10.0'} + dependencies: + define-property: 2.0.2 + extend-shallow: 3.0.2 + regex-not: 1.0.2 + safe-regex: 1.1.0 + dev: true + + /toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + /toml@3.0.0: + resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} + dev: true + + /toposort@1.0.7: + resolution: {integrity: sha512-FclLrw8b9bMWf4QlCJuHBEVhSRsqDj6u3nIjAzPeJvgl//1hBlffdlk0MALceL14+koWEdU4ofRAXofbODxQzg==} + dev: true + + /tough-cookie@2.5.0: + resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==} + engines: {node: '>=0.8'} + dependencies: + psl: 1.9.0 + punycode: 2.3.0 + dev: true + + /tough-cookie@4.1.2: + resolution: {integrity: sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==} + engines: {node: '>=6'} + dependencies: + psl: 1.9.0 + punycode: 2.3.0 + universalify: 0.2.0 + url-parse: 1.5.10 + dev: false + + /tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + dev: true + + /tr46@1.0.1: + resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + dependencies: + punycode: 2.3.0 + dev: false + + /tr46@2.1.0: + resolution: {integrity: sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==} + engines: {node: '>=8'} + dependencies: + punycode: 2.3.0 + dev: false + + /tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + dev: true + + /tryer@1.0.1: + resolution: {integrity: sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==} + dev: false + + /ts-expect@1.3.0: + resolution: {integrity: sha512-e4g0EJtAjk64xgnFPD6kTBUtpnMVzDrMb12N1YZV0VvSlhnVT3SGxiYTLdGy8Q5cYHOIC/FAHmZ10eGrAguicQ==} + dev: true + + /ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + dev: false + + /tsconfig-paths@3.14.2: + resolution: {integrity: sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==} + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + dev: false + + /tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + + /tslib@2.5.0: + resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==} + + /tsutils@3.21.0(typescript@4.9.5): + resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} + engines: {node: '>= 6'} + peerDependencies: + typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + dependencies: + tslib: 1.14.1 + typescript: 4.9.5 + + /tty-browserify@0.0.0: + resolution: {integrity: sha512-JVa5ijo+j/sOoHGjw0sxw734b1LhBkQ3bvUGNdxnVXDCX81Yx7TFgnZygxrIIWn23hbfTaMYLwRmAxFyDuFmIw==} + dev: true + + /tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /tweetnacl@0.14.5: + resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} + dev: true + + /type-check@0.3.2: + resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.1.2 + dev: false + + /type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + + /type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + dev: false + + /type-fest@0.16.0: + resolution: {integrity: sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==} + engines: {node: '>=10'} + dev: false + + /type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + /type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + /type-fest@0.8.1: + resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} + engines: {node: '>=8'} + dev: true + + /type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + /typed-array-length@1.0.4: + resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} + dependencies: + call-bind: 1.0.2 + for-each: 0.3.3 + is-typed-array: 1.1.10 + + /typedarray-to-buffer@3.1.5: + resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} + dependencies: + is-typedarray: 1.0.0 + + /typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + dev: true + + /typedoc-plugin-markdown@3.14.0(typedoc@0.23.28): + resolution: {integrity: sha512-UyQLkLRkfTFhLdhSf3RRpA3nNInGn+k6sll2vRXjflaMNwQAAiB61SYbisNZTg16t4K1dt1bPQMMGLrxS0GZ0Q==} + peerDependencies: + typedoc: '>=0.23.0' + dependencies: + handlebars: 4.7.7 + typedoc: 0.23.28(typescript@4.9.5) + dev: true + + /typedoc@0.23.28(typescript@4.9.5): + resolution: {integrity: sha512-9x1+hZWTHEQcGoP7qFmlo4unUoVJLB0H/8vfO/7wqTnZxg4kPuji9y3uRzEu0ZKez63OJAUmiGhUrtukC6Uj3w==} + engines: {node: '>= 14.14'} + hasBin: true + peerDependencies: + typescript: 4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x + dependencies: + lunr: 2.3.9 + marked: 4.3.0 + minimatch: 7.4.3 + shiki: 0.14.1 + typescript: 4.9.5 + dev: true + + /typescript@4.9.5: + resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} + engines: {node: '>=4.2.0'} + hasBin: true + + /ua-parser-js@0.7.34: + resolution: {integrity: sha512-cJMeh/eOILyGu0ejgTKB95yKT3zOenSe9UGE3vj6WfiOwgGYnmATUsnDixMFvdU+rNMvWih83hrUP8VwhF9yXQ==} + dev: true + + /uc.micro@1.0.6: + resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==} + dev: true + + /uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + requiresBuild: true + dev: true + optional: true + + /uglify-js@3.4.10: + resolution: {integrity: sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw==} + engines: {node: '>=0.8.0'} + hasBin: true + dependencies: + commander: 2.19.0 + source-map: 0.6.1 + dev: true + + /unbox-primitive@1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + dependencies: + call-bind: 1.0.2 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + + /unicode-canonical-property-names-ecmascript@2.0.0: + resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} + engines: {node: '>=4'} + + /unicode-match-property-ecmascript@2.0.0: + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} + dependencies: + unicode-canonical-property-names-ecmascript: 2.0.0 + unicode-property-aliases-ecmascript: 2.1.0 + + /unicode-match-property-value-ecmascript@2.1.0: + resolution: {integrity: sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==} + engines: {node: '>=4'} + + /unicode-property-aliases-ecmascript@2.1.0: + resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} + engines: {node: '>=4'} + + /union-value@1.0.1: + resolution: {integrity: sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==} + engines: {node: '>=0.10.0'} + dependencies: + arr-union: 3.1.0 + get-value: 2.0.6 + is-extendable: 0.1.1 + set-value: 2.0.1 + dev: true + + /uniq@1.0.1: + resolution: {integrity: sha512-Gw+zz50YNKPDKXs+9d+aKAjVwpjNwqzvNpLigIruT4HA9lMZNdMqs9x07kKHB/L9WRzqp4+DlTU5s4wG2esdoA==} + dev: true + + /uniqs@2.0.0: + resolution: {integrity: sha512-mZdDpf3vBV5Efh29kMw5tXoup/buMgxLzOt/XKFKcVmi+15ManNQWr6HfZ2aiZTYlYixbdNJ0KFmIZIv52tHSQ==} + dev: true + + /unique-filename@1.1.1: + resolution: {integrity: sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==} + dependencies: + unique-slug: 2.0.2 + dev: true + + /unique-slug@2.0.2: + resolution: {integrity: sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==} + dependencies: + imurmurhash: 0.1.4 + dev: true + + /unique-string@2.0.0: + resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} + engines: {node: '>=8'} + dependencies: + crypto-random-string: 2.0.0 + + /unist-util-stringify-position@2.0.3: + resolution: {integrity: sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==} + dependencies: + '@types/unist': 2.0.6 + dev: true + + /universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + dev: true + + /universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + dev: false + + /universalify@2.0.0: + resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} + engines: {node: '>= 10.0.0'} + dev: false + + /unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + /unquote@1.1.1: + resolution: {integrity: sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==} + + /unset-value@1.0.0: + resolution: {integrity: sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==} + engines: {node: '>=0.10.0'} + dependencies: + has-value: 0.3.1 + isobject: 3.0.1 + dev: true + + /upath@1.2.0: + resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==} + engines: {node: '>=4'} + + /update-browserslist-db@1.0.10(browserslist@4.21.5): + resolution: {integrity: sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + dependencies: + browserslist: 4.21.5 + escalade: 3.1.1 + picocolors: 1.0.0 + + /update-notifier@4.1.3: + resolution: {integrity: sha512-Yld6Z0RyCYGB6ckIjffGOSOmHXj1gMeE7aROz4MG+XMkmixBX4jUngrGXNYz7wPKBmtoD4MnBa2Anu7RSKht/A==} + engines: {node: '>=8'} + dependencies: + boxen: 4.2.0 + chalk: 3.0.0 + configstore: 5.0.1 + has-yarn: 2.1.0 + import-lazy: 2.1.0 + is-ci: 2.0.0 + is-installed-globally: 0.3.2 + is-npm: 4.0.0 + is-yarn-global: 0.3.0 + latest-version: 5.1.0 + pupa: 2.1.1 + semver-diff: 3.1.1 + xdg-basedir: 4.0.0 + dev: true + + /upper-case@1.1.3: + resolution: {integrity: sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==} + dev: true + + /uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.3.0 + + /urix@0.1.0: + resolution: {integrity: sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==} + deprecated: Please see https://github.com/lydell/urix#deprecated + dev: true + + /url-loader@1.1.2(webpack@4.46.0): + resolution: {integrity: sha512-dXHkKmw8FhPqu8asTc1puBfe3TehOCo2+RmOOev5suNCIYBcT626kxiWg1NBVkwc4rO8BGa7gP70W7VXuqHrjg==} + engines: {node: '>= 6.9.0'} + peerDependencies: + webpack: ^3.0.0 || ^4.0.0 + dependencies: + loader-utils: 1.4.2 + mime: 2.6.0 + schema-utils: 1.0.0 + webpack: 4.46.0 + dev: true + + /url-parse-lax@3.0.0: + resolution: {integrity: sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==} + engines: {node: '>=4'} + dependencies: + prepend-http: 2.0.0 + dev: true + + /url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + + /url@0.11.0: + resolution: {integrity: sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ==} + dependencies: + punycode: 1.3.2 + querystring: 0.2.0 + dev: true + + /use@3.1.1: + resolution: {integrity: sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==} + engines: {node: '>=0.10.0'} + dev: true + + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + /util.promisify@1.0.0: + resolution: {integrity: sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==} + dependencies: + define-properties: 1.2.0 + object.getownpropertydescriptors: 2.1.5 + dev: true + + /util.promisify@1.0.1: + resolution: {integrity: sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==} + dependencies: + define-properties: 1.2.0 + es-abstract: 1.21.2 + has-symbols: 1.0.3 + object.getownpropertydescriptors: 2.1.5 + + /util@0.10.3: + resolution: {integrity: sha512-5KiHfsmkqacuKjkRkdV7SsfDJ2EGiPsK92s2MhNSY0craxjTdKTtqKsJaCWp4LW33ZZ0OPUv1WO/TFvNQRiQxQ==} + dependencies: + inherits: 2.0.1 + dev: true + + /util@0.11.1: + resolution: {integrity: sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==} + dependencies: + inherits: 2.0.3 + dev: true + + /utila@0.4.0: + resolution: {integrity: sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==} + + /utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + /uuid@3.4.0: + resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} + deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. + hasBin: true + dev: true + + /uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + + /v8-to-istanbul@8.1.1: + resolution: {integrity: sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w==} + engines: {node: '>=10.12.0'} + dependencies: + '@types/istanbul-lib-coverage': 2.0.4 + convert-source-map: 1.9.0 + source-map: 0.7.4 + dev: false + + /vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + /vendors@1.0.4: + resolution: {integrity: sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==} + dev: true + + /verror@1.10.0: + resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} + engines: {'0': node >=0.6.0} + dependencies: + assert-plus: 1.0.0 + core-util-is: 1.0.2 + extsprintf: 1.3.0 + dev: true + + /vm-browserify@1.1.2: + resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==} + dev: true + + /void-elements@2.0.1: + resolution: {integrity: sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==} + engines: {node: '>=0.10.0'} + dev: true + + /vscode-oniguruma@1.7.0: + resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==} + dev: true + + /vscode-textmate@8.0.0: + resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==} + dev: true + + /vue-hot-reload-api@2.3.4: + resolution: {integrity: sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==} + dev: true + + /vue-loader@15.10.1(cache-loader@3.0.1)(css-loader@2.1.1)(vue-template-compiler@2.7.14)(webpack@4.46.0): + resolution: {integrity: sha512-SaPHK1A01VrNthlix6h1hq4uJu7S/z0kdLUb6klubo738NeQoLbS6V9/d8Pv19tU0XdQKju3D1HSKuI8wJ5wMA==} + peerDependencies: + '@vue/compiler-sfc': ^3.0.8 + cache-loader: '*' + css-loader: '*' + vue-template-compiler: '*' + webpack: ^3.0.0 || ^4.1.0 || ^5.0.0-0 + peerDependenciesMeta: + '@vue/compiler-sfc': + optional: true + cache-loader: + optional: true + vue-template-compiler: + optional: true + dependencies: + '@vue/component-compiler-utils': 3.3.0 + cache-loader: 3.0.1(webpack@4.46.0) + css-loader: 2.1.1(webpack@4.46.0) + hash-sum: 1.0.2 + loader-utils: 1.4.2 + vue-hot-reload-api: 2.3.4 + vue-style-loader: 4.1.3 + vue-template-compiler: 2.7.14 + webpack: 4.46.0 + transitivePeerDependencies: + - arc-templates + - atpl + - babel-core + - bracket-template + - coffee-script + - dot + - dust + - dustjs-helpers + - dustjs-linkedin + - eco + - ect + - ejs + - haml-coffee + - hamlet + - hamljs + - handlebars + - hogan.js + - htmling + - jade + - jazz + - jqtpl + - just + - liquid-node + - liquor + - lodash + - marko + - mote + - mustache + - nunjucks + - plates + - pug + - qejs + - ractive + - razor-tmpl + - react + - react-dom + - slm + - squirrelly + - swig + - swig-templates + - teacup + - templayed + - then-jade + - then-pug + - tinyliquid + - toffee + - twig + - twing + - underscore + - vash + - velocityjs + - walrus + - whiskers + dev: true + + /vue-prism-editor@1.3.0(vue@2.7.14): + resolution: {integrity: sha512-54RfgtMGRMNr9484zKMOZs1wyLDR6EfFylzE2QrMCD9alCvXyYYcS0vX8oUHh+6pMUu6ts59uSN9cHglpU2NRQ==} + engines: {node: '>=10'} + peerDependencies: + vue: ^2.6.11 + dependencies: + vue: 2.7.14 + dev: true + + /vue-router@3.6.5(vue@2.7.14): + resolution: {integrity: sha512-VYXZQLtjuvKxxcshuRAwjHnciqZVoXAjTjcqBTz4rKc8qih9g9pI3hbDjmqXaHdgL3v8pV6P8Z335XvHzESxLQ==} + peerDependencies: + vue: ^2 + dependencies: + vue: 2.7.14 + dev: true + + /vue-server-renderer@2.7.14: + resolution: {integrity: sha512-NlGFn24tnUrj7Sqb8njhIhWREuCJcM3140aMunLNcx951BHG8j3XOrPP7psSCaFA8z6L4IWEjudztdwTp1CBVw==} + dependencies: + chalk: 4.1.2 + hash-sum: 2.0.0 + he: 1.2.0 + lodash.template: 4.5.0 + lodash.uniq: 4.5.0 + resolve: 1.22.1 + serialize-javascript: 6.0.1 + source-map: 0.5.6 + dev: true + + /vue-style-loader@4.1.3: + resolution: {integrity: sha512-sFuh0xfbtpRlKfm39ss/ikqs9AbKCoXZBpHeVZ8Tx650o0k0q/YCM7FRvigtxpACezfq6af+a7JeqVTWvncqDg==} + dependencies: + hash-sum: 1.0.2 + loader-utils: 1.4.2 + dev: true + + /vue-tabs-component@1.5.0(vue@2.7.14): + resolution: {integrity: sha512-ld4p+hv49Fimw+zv/7GQqMhbjAHjpbWF3UiJtmMaSnvLKbsB1ysfs9dQH0SZ8NvdYpqqKay/VLIqR9yXgse1Sg==} + peerDependencies: + vue: ^2.3.0 + dependencies: + vue: 2.7.14 + dev: true + + /vue-template-compiler@2.7.14: + resolution: {integrity: sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==} + dependencies: + de-indent: 1.0.2 + he: 1.2.0 + dev: true + + /vue-template-es2015-compiler@1.9.1: + resolution: {integrity: sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==} + dev: true + + /vue2-perfect-scrollbar@1.5.56(postcss@8.5.6): + resolution: {integrity: sha512-0ciZFj8kfMnsVkEi9BYf16HoybdN8bju8zj4Okwlrg9+rJp6i/PYXh+ZWsdeQn6jLDMi6CRSNEsaTsLPStIVHQ==} + dependencies: + cssnano: 5.1.15(postcss@8.5.6) + perfect-scrollbar: 1.5.5 + postcss-import: 12.0.1 + transitivePeerDependencies: + - postcss + dev: true + + /vue@2.7.14: + resolution: {integrity: sha512-b2qkFyOM0kwqWFuQmgd4o+uHGU7T+2z3T+WQp8UBjADfEv2n4FEMffzBmCKNP0IGzOEEfYjvtcC62xaSKeQDrQ==} + dependencies: + '@vue/compiler-sfc': 2.7.14 + csstype: 3.1.1 + dev: true + + /vuepress-html-webpack-plugin@3.2.0(webpack@4.46.0): + resolution: {integrity: sha512-BebAEl1BmWlro3+VyDhIOCY6Gef2MCBllEVAP3NUAtMguiyOwo/dClbwJ167WYmcxHJKLl7b0Chr9H7fpn1d0A==} + engines: {node: '>=6.9'} + peerDependencies: + webpack: ^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0 + dependencies: + html-minifier: 3.5.21 + loader-utils: 0.2.17 + lodash: 4.17.21 + pretty-error: 2.1.2 + tapable: 1.1.3 + toposort: 1.0.7 + util.promisify: 1.0.0 + webpack: 4.46.0 + dev: true + + /vuepress-plugin-code-copy@1.0.6: + resolution: {integrity: sha512-FiqwMtlb4rEsOI56O6sSkekcd3SlESxbkR2IaTIQxsMOMoalKfW5R9WlR1Pjm10v6jmU661Ex8MR11k9IzrNUg==} + dev: true + + /vuepress-plugin-container@2.1.5: + resolution: {integrity: sha512-TQrDX/v+WHOihj3jpilVnjXu9RcTm6m8tzljNJwYhxnJUW0WWQ0hFLcDTqTBwgKIFdEiSxVOmYE+bJX/sq46MA==} + dependencies: + '@vuepress/shared-utils': 1.9.9 + markdown-it-container: 2.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /vuepress-plugin-dehydrate@1.1.5: + resolution: {integrity: sha512-9F2x1vLCK4poPUMkLupD4HsgWdbZ68Escvma+DE1Dk6aAJdH5FGwmfOMxj4sMCBwz7S4s6bTMna+QQgD3+bzBA==} + dependencies: + '@vuepress/shared-utils': 1.9.9 + transitivePeerDependencies: + - supports-color + dev: true + + /vuepress-plugin-flexsearch@0.3.0: + resolution: {integrity: sha512-dffrD35hDE6FcpN3JRTy5E9tccq1uB7l+ocdPBObuiuFjHJP/xlU+pOR3Yc6yQlsvP5ResweGOP2kaeGViorBg==} + dependencies: + '@vuepress/plugin-search': 1.9.9 + flexsearch: 0.6.32 + transitivePeerDependencies: + - debug + dev: true + + /vuepress-plugin-redirect@1.2.5: + resolution: {integrity: sha512-4RAWTVite154Tv7rUJEqWZ4fZtVXwKKoFOa2zY0Esn7cLi3Om2A+Pa2U84tBgPd90v2R7KEOy9jLEVphbsPK7g==} + deprecated: Please use latest version + dependencies: + '@shigma/stringify-object': 3.3.0 + vuepress-plugin-dehydrate: 1.1.5 + transitivePeerDependencies: + - supports-color + dev: true + + /vuepress-plugin-smooth-scroll@0.0.3: + resolution: {integrity: sha512-qsQkDftLVFLe8BiviIHaLV0Ea38YLZKKonDGsNQy1IE0wllFpFIEldWD8frWZtDFdx6b/O3KDMgVQ0qp5NjJCg==} + dependencies: + smoothscroll-polyfill: 0.4.4 + dev: true + + /vuepress-plugin-tabs@0.3.0: + resolution: {integrity: sha512-jooDlcMdBqhXgIaF1awFSaOTM56mleP6bbCiGxyQxTZexfvCfDvZhNLGpyXqMQA50ZmNGmvLrK82YYb63k1jfA==} + dev: true + + /vuepress-plugin-typedoc@0.11.2(typedoc-plugin-markdown@3.14.0)(typedoc@0.23.28): + resolution: {integrity: sha512-OSnxx3jsAQBDwwJ6UsQRwSDvyzAYR9+J21x5iwiEfr9j7H/UqmtqY9BKqRTqNIVTLtUatJ0mmeiz+uZBDam9UQ==} + peerDependencies: + typedoc: '>=0.23.0' + typedoc-plugin-markdown: '>=3.13.0' + dependencies: + typedoc: 0.23.28(typescript@4.9.5) + typedoc-plugin-markdown: 3.14.0(typedoc@0.23.28) + dev: true + + /vuepress-theme-chartjs@0.2.0(postcss@8.5.6)(vue@2.7.14): + resolution: {integrity: sha512-OE9fdPN/bV+SM6dGIjM4nUSEzvHHbQlIriJi4bdVvlSDufgXkkfUbbu+aDqx/a7n7wrqWaTQox73KZX5FFY7rw==} + peerDependencies: + chart.js: '>= 2' + peerDependenciesMeta: + chart.js: + optional: true + dependencies: + acorn: 8.8.2 + vue-prism-editor: 1.3.0(vue@2.7.14) + vue2-perfect-scrollbar: 1.5.56(postcss@8.5.6) + transitivePeerDependencies: + - postcss + - vue + dev: true + + /vuepress@1.9.9: + resolution: {integrity: sha512-CU94W3EdWaCavGx2VSvQJMI/hyv+m/YMdrvJJw67EVfmmJJDb1iTGrilDgLd0qsyrXzBy0Ru9Qi6rkf4IwcOTg==} + engines: {node: '>=8.6'} + hasBin: true + requiresBuild: true + dependencies: + '@vuepress/core': 1.9.9 + '@vuepress/theme-default': 1.9.9 + '@vuepress/types': 1.9.9 + cac: 6.7.14 + envinfo: 7.8.1 + opencollective-postinstall: 2.0.3 + update-notifier: 4.1.3 + transitivePeerDependencies: + - '@vue/compiler-sfc' + - arc-templates + - atpl + - babel-core + - bracket-template + - bufferutil + - coffee-script + - debug + - dot + - dust + - dustjs-helpers + - dustjs-linkedin + - eco + - ect + - ejs + - haml-coffee + - hamlet + - hamljs + - handlebars + - hogan.js + - htmling + - jade + - jazz + - jqtpl + - just + - liquid-node + - liquor + - lodash + - marko + - mote + - mustache + - nunjucks + - plates + - pug + - qejs + - ractive + - razor-tmpl + - react + - react-dom + - slm + - squirrelly + - supports-color + - swig + - swig-templates + - teacup + - templayed + - then-jade + - then-pug + - tinyliquid + - toffee + - twig + - twing + - underscore + - utf-8-validate + - vash + - velocityjs + - walrus + - webpack-cli + - webpack-command + - whiskers + dev: true + + /w3c-hr-time@1.0.2: + resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==} + deprecated: Use your platform's native performance.now() and performance.timeOrigin. + dependencies: + browser-process-hrtime: 1.0.0 + dev: false + + /w3c-xmlserializer@2.0.0: + resolution: {integrity: sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==} + engines: {node: '>=10'} + dependencies: + xml-name-validator: 3.0.0 + dev: false + + /walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + dependencies: + makeerror: 1.0.12 + dev: false + + /watchpack-chokidar2@2.0.1: + resolution: {integrity: sha512-nCFfBIPKr5Sh61s4LPpy1Wtfi0HE8isJ3d2Yb5/Ppw2P2B/3eVSEBjKfN0fmHJSK14+31KwMKmcrzs2GM4P0Ww==} + requiresBuild: true + dependencies: + chokidar: 2.1.8(supports-color@6.1.0) + transitivePeerDependencies: + - supports-color + dev: true + optional: true + + /watchpack@1.7.5: + resolution: {integrity: sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ==} + dependencies: + graceful-fs: 4.2.11 + neo-async: 2.6.2 + optionalDependencies: + chokidar: 3.6.0 + watchpack-chokidar2: 2.0.1 + transitivePeerDependencies: + - supports-color + dev: true + + /watchpack@2.4.0: + resolution: {integrity: sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==} + engines: {node: '>=10.13.0'} + dependencies: + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + dev: false + + /wbuf@1.7.3: + resolution: {integrity: sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==} + dependencies: + minimalistic-assert: 1.0.1 + + /web-vitals@2.1.4: + resolution: {integrity: sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg==} + dev: false + + /webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + dev: true + + /webidl-conversions@4.0.2: + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + dev: false + + /webidl-conversions@5.0.0: + resolution: {integrity: sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==} + engines: {node: '>=8'} + dev: false + + /webidl-conversions@6.1.0: + resolution: {integrity: sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==} + engines: {node: '>=10.4'} + dev: false + + /webpack-chain@4.12.1: + resolution: {integrity: sha512-BCfKo2YkDe2ByqkEWe1Rw+zko4LsyS75LVr29C6xIrxAg9JHJ4pl8kaIZ396SUSNp6b4815dRZPSTAS8LlURRQ==} + dependencies: + deepmerge: 1.5.2 + javascript-stringify: 1.6.0 + dev: true + + /webpack-chain@6.5.1: + resolution: {integrity: sha512-7doO/SRtLu8q5WM0s7vPKPWX580qhi0/yBHkOxNkv50f6qB76Zy9o2wRTrrPULqYTvQlVHuvbA8v+G5ayuUDsA==} + engines: {node: '>=8'} + dependencies: + deepmerge: 1.5.2 + javascript-stringify: 2.1.0 + dev: true + + /webpack-dev-middleware@3.7.3(webpack@4.46.0): + resolution: {integrity: sha512-djelc/zGiz9nZj/U7PTBi2ViorGJXEWo/3ltkPbDyxCXhhEXkW0ce99falaok4TPj+AsxLiXJR0EBOb0zh9fKQ==} + engines: {node: '>= 6'} + peerDependencies: + webpack: ^4.0.0 || ^5.0.0 + dependencies: + memory-fs: 0.4.1 + mime: 2.6.0 + mkdirp: 0.5.6 + range-parser: 1.2.1 + webpack: 4.46.0 + webpack-log: 2.0.0 + dev: true + + /webpack-dev-middleware@5.3.3(webpack@5.76.3): + resolution: {integrity: sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==} + engines: {node: '>= 12.13.0'} + peerDependencies: + webpack: ^4.0.0 || ^5.0.0 + dependencies: + colorette: 2.0.19 + memfs: 3.4.13 + mime-types: 2.1.35 + range-parser: 1.2.1 + schema-utils: 4.0.0 + webpack: 5.76.3(@swc/core@1.3.42) + dev: false + + /webpack-dev-server@3.11.3(webpack@4.46.0): + resolution: {integrity: sha512-3x31rjbEQWKMNzacUZRE6wXvUFuGpH7vr0lIEbYpMAG9BOxi0928QU1BBswOAP3kg3H1O4hiS+sq4YyAn6ANnA==} + engines: {node: '>= 6.11.5'} + hasBin: true + peerDependencies: + webpack: ^4.0.0 || ^5.0.0 + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + dependencies: + ansi-html-community: 0.0.8 + bonjour: 3.5.0 + chokidar: 2.1.8(supports-color@6.1.0) + compression: 1.7.4(supports-color@6.1.0) + connect-history-api-fallback: 1.6.0 + debug: 4.3.4(supports-color@6.1.0) + del: 4.1.1 + express: 4.18.2(supports-color@6.1.0) + html-entities: 1.4.0 + http-proxy-middleware: 0.19.1(debug@4.3.4)(supports-color@6.1.0) + import-local: 2.0.0 + internal-ip: 4.3.0 + ip: 1.1.8 + is-absolute-url: 3.0.3 + killable: 1.0.1 + loglevel: 1.8.1 + opn: 5.5.0 + p-retry: 3.0.1 + portfinder: 1.0.32(supports-color@6.1.0) + schema-utils: 1.0.0 + selfsigned: 1.10.14 + semver: 6.3.0 + serve-index: 1.9.1(supports-color@6.1.0) + sockjs: 0.3.24 + sockjs-client: 1.6.1(supports-color@6.1.0) + spdy: 4.0.2(supports-color@6.1.0) + strip-ansi: 3.0.1 + supports-color: 6.1.0 + url: 0.11.0 + webpack: 4.46.0 + webpack-dev-middleware: 3.7.3(webpack@4.46.0) + webpack-log: 2.0.0 + ws: 6.2.2 + yargs: 13.3.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: true + + /webpack-dev-server@4.13.1(webpack@5.76.3): + resolution: {integrity: sha512-5tWg00bnWbYgkN+pd5yISQKDejRBYGEw15RaEEslH+zdbNDxxaZvEAO2WulaSaFKb5n3YG8JXsGaDsut1D0xdA==} + engines: {node: '>= 12.13.0'} + hasBin: true + peerDependencies: + webpack: ^4.37.0 || ^5.0.0 + webpack-cli: '*' + peerDependenciesMeta: + webpack: + optional: true + webpack-cli: + optional: true + dependencies: + '@types/bonjour': 3.5.10 + '@types/connect-history-api-fallback': 1.3.5 + '@types/express': 4.17.17 + '@types/serve-index': 1.9.1 + '@types/serve-static': 1.15.1 + '@types/sockjs': 0.3.33 + '@types/ws': 8.5.4 + ansi-html-community: 0.0.8 + bonjour-service: 1.1.1 + chokidar: 3.5.3 + colorette: 2.0.19 + compression: 1.7.4(supports-color@6.1.0) + connect-history-api-fallback: 2.0.0 + default-gateway: 6.0.3 + express: 4.18.2(supports-color@6.1.0) + graceful-fs: 4.2.11 + html-entities: 1.4.0 + http-proxy-middleware: 2.0.6(@types/express@4.17.17) + ipaddr.js: 2.0.1 + launch-editor: 2.6.0 + open: 8.4.2 + p-retry: 4.6.2 + rimraf: 3.0.2 + schema-utils: 4.0.0 + selfsigned: 2.1.1 + serve-index: 1.9.1(supports-color@6.1.0) + sockjs: 0.3.24 + spdy: 4.0.2(supports-color@6.1.0) + webpack: 5.76.3(@swc/core@1.3.42) + webpack-dev-middleware: 5.3.3(webpack@5.76.3) + ws: 8.13.0 + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - utf-8-validate + dev: false + + /webpack-log@2.0.0: + resolution: {integrity: sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==} + engines: {node: '>= 6'} + dependencies: + ansi-colors: 3.2.4 + uuid: 3.4.0 + dev: true + + /webpack-manifest-plugin@4.1.1(webpack@5.76.3): + resolution: {integrity: sha512-YXUAwxtfKIJIKkhg03MKuiFAD72PlrqCiwdwO4VEXdRO5V0ORCNwaOwAZawPZalCbmH9kBDmXnNeQOw+BIEiow==} + engines: {node: '>=12.22.0'} + peerDependencies: + webpack: ^4.44.2 || ^5.47.0 + dependencies: + tapable: 2.2.1 + webpack: 5.76.3(@swc/core@1.3.42) + webpack-sources: 2.3.1 + dev: false + + /webpack-merge@4.2.2: + resolution: {integrity: sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g==} + dependencies: + lodash: 4.17.21 + dev: true + + /webpack-sources@1.4.3: + resolution: {integrity: sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==} + dependencies: + source-list-map: 2.0.1 + source-map: 0.6.1 + + /webpack-sources@2.3.1: + resolution: {integrity: sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA==} + engines: {node: '>=10.13.0'} + dependencies: + source-list-map: 2.0.1 + source-map: 0.6.1 + dev: false + + /webpack-sources@3.2.3: + resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} + engines: {node: '>=10.13.0'} + dev: false + + /webpack@4.46.0: + resolution: {integrity: sha512-6jJuJjg8znb/xRItk7bkT0+Q7AHCYjjFnvKIWQPkNIOyRqoCGvkOs0ipeQzrqz4l5FtN5ZI/ukEHroeX/o1/5Q==} + engines: {node: '>=6.11.5'} + hasBin: true + peerDependencies: + webpack-cli: '*' + webpack-command: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + webpack-command: + optional: true + dependencies: + '@webassemblyjs/ast': 1.9.0 + '@webassemblyjs/helper-module-context': 1.9.0 + '@webassemblyjs/wasm-edit': 1.9.0 + '@webassemblyjs/wasm-parser': 1.9.0 + acorn: 6.4.2 + ajv: 6.12.6 + ajv-keywords: 3.5.2(ajv@6.12.6) + chrome-trace-event: 1.0.3 + enhanced-resolve: 4.5.0 + eslint-scope: 4.0.3 + json-parse-better-errors: 1.0.2 + loader-runner: 2.4.0 + loader-utils: 1.4.2 + memory-fs: 0.4.1 + micromatch: 3.1.10(supports-color@6.1.0) + mkdirp: 0.5.6 + neo-async: 2.6.2 + node-libs-browser: 2.2.1 + schema-utils: 1.0.0 + tapable: 1.1.3 + terser-webpack-plugin: 1.4.5(webpack@4.46.0) + watchpack: 1.7.5 + webpack-sources: 1.4.3 + transitivePeerDependencies: + - supports-color + dev: true + + /webpack@5.76.3(@swc/core@1.3.42): + resolution: {integrity: sha512-18Qv7uGPU8b2vqGeEEObnfICyw2g39CHlDEK4I7NK13LOur1d0HGmGNKGT58Eluwddpn3oEejwvBPoP4M7/KSA==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + dependencies: + '@types/eslint-scope': 3.7.4 + '@types/estree': 0.0.51 + '@webassemblyjs/ast': 1.11.1 + '@webassemblyjs/wasm-edit': 1.11.1 + '@webassemblyjs/wasm-parser': 1.11.1 + acorn: 8.8.2 + acorn-import-assertions: 1.8.0(acorn@8.8.2) + browserslist: 4.21.5 + chrome-trace-event: 1.0.3 + enhanced-resolve: 5.12.0 + es-module-lexer: 0.9.3 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.0 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 3.1.1 + tapable: 2.2.1 + terser-webpack-plugin: 5.3.7(@swc/core@1.3.42)(webpack@5.76.3) + watchpack: 2.4.0 + webpack-sources: 3.2.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + dev: false + + /webpackbar@3.2.0(webpack@4.46.0): + resolution: {integrity: sha512-PC4o+1c8gWWileUfwabe0gqptlXUDJd5E0zbpr2xHP1VSOVlZVPBZ8j6NCR8zM5zbKdxPhctHXahgpNK1qFDPw==} + engines: {node: '>= 6.9.0'} + peerDependencies: + webpack: ^3.0.0 || ^4.0.0 + dependencies: + ansi-escapes: 4.3.2 + chalk: 2.4.2 + consola: 2.15.3 + figures: 3.2.0 + pretty-time: 1.1.0 + std-env: 2.3.1 + text-table: 0.2.0 + webpack: 4.46.0 + wrap-ansi: 5.1.0 + dev: true + + /websocket-driver@0.7.4: + resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} + engines: {node: '>=0.8.0'} + dependencies: + http-parser-js: 0.5.8 + safe-buffer: 5.2.1 + websocket-extensions: 0.1.4 + + /websocket-extensions@0.1.4: + resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} + engines: {node: '>=0.8.0'} + + /whatwg-encoding@1.0.5: + resolution: {integrity: sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==} + dependencies: + iconv-lite: 0.4.24 + dev: false + + /whatwg-fetch@3.6.2: + resolution: {integrity: sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==} + dev: false + + /whatwg-mimetype@2.3.0: + resolution: {integrity: sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==} + dev: false + + /whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + dev: true + + /whatwg-url@7.1.0: + resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + dependencies: + lodash.sortby: 4.7.0 + tr46: 1.0.1 + webidl-conversions: 4.0.2 + dev: false + + /whatwg-url@8.7.0: + resolution: {integrity: sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==} + engines: {node: '>=10'} + dependencies: + lodash: 4.17.21 + tr46: 2.1.0 + webidl-conversions: 6.1.0 + dev: false + + /when@3.6.4: + resolution: {integrity: sha512-d1VUP9F96w664lKINMGeElWdhhb5sC+thXM+ydZGU3ZnaE09Wv6FaS+mpM9570kcDs/xMfcXJBTLsMdHEFYY9Q==} + dev: true + + /which-boxed-primitive@1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + + /which-collection@1.0.1: + resolution: {integrity: sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==} + dependencies: + is-map: 2.0.2 + is-set: 2.0.2 + is-weakmap: 2.0.1 + is-weakset: 2.0.2 + dev: false + + /which-module@2.0.0: + resolution: {integrity: sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==} + dev: true + + /which-typed-array@1.1.9: + resolution: {integrity: sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + is-typed-array: 1.1.10 + + /which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + dependencies: + isexe: 2.0.0 + + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + + /widest-line@3.1.0: + resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} + engines: {node: '>=8'} + dependencies: + string-width: 4.2.3 + dev: true + + /word-wrap@1.2.3: + resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} + engines: {node: '>=0.10.0'} + + /word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + dev: false + + /wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + dev: true + + /workbox-background-sync@6.5.4: + resolution: {integrity: sha512-0r4INQZMyPky/lj4Ou98qxcThrETucOde+7mRGJl13MPJugQNKeZQOdIJe/1AchOP23cTqHcN/YVpD6r8E6I8g==} + dependencies: + idb: 7.1.1 + workbox-core: 6.5.4 + dev: false + + /workbox-broadcast-update@6.5.4: + resolution: {integrity: sha512-I/lBERoH1u3zyBosnpPEtcAVe5lwykx9Yg1k6f8/BGEPGaMMgZrwVrqL1uA9QZ1NGGFoyE6t9i7lBjOlDhFEEw==} + dependencies: + workbox-core: 6.5.4 + dev: false + + /workbox-build@6.5.4: + resolution: {integrity: sha512-kgRevLXEYvUW9WS4XoziYqZ8Q9j/2ziJYEtTrjdz5/L/cTUa2XfyMP2i7c3p34lgqJ03+mTiz13SdFef2POwbA==} + engines: {node: '>=10.0.0'} + dependencies: + '@apideck/better-ajv-errors': 0.3.6(ajv@8.12.0) + '@babel/core': 7.21.3 + '@babel/preset-env': 7.20.2(@babel/core@7.21.3) + '@babel/runtime': 7.21.0 + '@rollup/plugin-babel': 5.3.1(@babel/core@7.21.3)(rollup@2.79.1) + '@rollup/plugin-node-resolve': 11.2.1(rollup@2.79.1) + '@rollup/plugin-replace': 2.4.2(rollup@2.79.1) + '@surma/rollup-plugin-off-main-thread': 2.2.3 + ajv: 8.12.0 + common-tags: 1.8.2 + fast-json-stable-stringify: 2.1.0 + fs-extra: 9.1.0 + glob: 7.2.3 + lodash: 4.17.21 + pretty-bytes: 5.6.0 + rollup: 2.79.1 + rollup-plugin-terser: 7.0.2(rollup@2.79.1) + source-map: 0.8.0-beta.0 + stringify-object: 3.3.0 + strip-comments: 2.0.1 + tempy: 0.6.0 + upath: 1.2.0 + workbox-background-sync: 6.5.4 + workbox-broadcast-update: 6.5.4 + workbox-cacheable-response: 6.5.4 + workbox-core: 6.5.4 + workbox-expiration: 6.5.4 + workbox-google-analytics: 6.5.4 + workbox-navigation-preload: 6.5.4 + workbox-precaching: 6.5.4 + workbox-range-requests: 6.5.4 + workbox-recipes: 6.5.4 + workbox-routing: 6.5.4 + workbox-strategies: 6.5.4 + workbox-streams: 6.5.4 + workbox-sw: 6.5.4 + workbox-window: 6.5.4 + transitivePeerDependencies: + - '@types/babel__core' + - supports-color + dev: false + + /workbox-cacheable-response@6.5.4: + resolution: {integrity: sha512-DCR9uD0Fqj8oB2TSWQEm1hbFs/85hXXoayVwFKLVuIuxwJaihBsLsp4y7J9bvZbqtPJ1KlCkmYVGQKrBU4KAug==} + dependencies: + workbox-core: 6.5.4 + dev: false + + /workbox-core@6.5.4: + resolution: {integrity: sha512-OXYb+m9wZm8GrORlV2vBbE5EC1FKu71GGp0H4rjmxmF4/HLbMCoTFws87M3dFwgpmg0v00K++PImpNQ6J5NQ6Q==} + dev: false + + /workbox-expiration@6.5.4: + resolution: {integrity: sha512-jUP5qPOpH1nXtjGGh1fRBa1wJL2QlIb5mGpct3NzepjGG2uFFBn4iiEBiI9GUmfAFR2ApuRhDydjcRmYXddiEQ==} + dependencies: + idb: 7.1.1 + workbox-core: 6.5.4 + dev: false + + /workbox-google-analytics@6.5.4: + resolution: {integrity: sha512-8AU1WuaXsD49249Wq0B2zn4a/vvFfHkpcFfqAFHNHwln3jK9QUYmzdkKXGIZl9wyKNP+RRX30vcgcyWMcZ9VAg==} + dependencies: + workbox-background-sync: 6.5.4 + workbox-core: 6.5.4 + workbox-routing: 6.5.4 + workbox-strategies: 6.5.4 + dev: false + + /workbox-navigation-preload@6.5.4: + resolution: {integrity: sha512-IIwf80eO3cr8h6XSQJF+Hxj26rg2RPFVUmJLUlM0+A2GzB4HFbQyKkrgD5y2d84g2IbJzP4B4j5dPBRzamHrng==} + dependencies: + workbox-core: 6.5.4 + dev: false + + /workbox-precaching@6.5.4: + resolution: {integrity: sha512-hSMezMsW6btKnxHB4bFy2Qfwey/8SYdGWvVIKFaUm8vJ4E53JAY+U2JwLTRD8wbLWoP6OVUdFlXsTdKu9yoLTg==} + dependencies: + workbox-core: 6.5.4 + workbox-routing: 6.5.4 + workbox-strategies: 6.5.4 + dev: false + + /workbox-range-requests@6.5.4: + resolution: {integrity: sha512-Je2qR1NXCFC8xVJ/Lux6saH6IrQGhMpDrPXWZWWS8n/RD+WZfKa6dSZwU+/QksfEadJEr/NfY+aP/CXFFK5JFg==} + dependencies: + workbox-core: 6.5.4 + dev: false + + /workbox-recipes@6.5.4: + resolution: {integrity: sha512-QZNO8Ez708NNwzLNEXTG4QYSKQ1ochzEtRLGaq+mr2PyoEIC1xFW7MrWxrONUxBFOByksds9Z4//lKAX8tHyUA==} + dependencies: + workbox-cacheable-response: 6.5.4 + workbox-core: 6.5.4 + workbox-expiration: 6.5.4 + workbox-precaching: 6.5.4 + workbox-routing: 6.5.4 + workbox-strategies: 6.5.4 + dev: false + + /workbox-routing@6.5.4: + resolution: {integrity: sha512-apQswLsbrrOsBUWtr9Lf80F+P1sHnQdYodRo32SjiByYi36IDyL2r7BH1lJtFX8fwNHDa1QOVY74WKLLS6o5Pg==} + dependencies: + workbox-core: 6.5.4 + dev: false + + /workbox-strategies@6.5.4: + resolution: {integrity: sha512-DEtsxhx0LIYWkJBTQolRxG4EI0setTJkqR4m7r4YpBdxtWJH1Mbg01Cj8ZjNOO8etqfA3IZaOPHUxCs8cBsKLw==} + dependencies: + workbox-core: 6.5.4 + dev: false + + /workbox-streams@6.5.4: + resolution: {integrity: sha512-FXKVh87d2RFXkliAIheBojBELIPnWbQdyDvsH3t74Cwhg0fDheL1T8BqSM86hZvC0ZESLsznSYWw+Va+KVbUzg==} + dependencies: + workbox-core: 6.5.4 + workbox-routing: 6.5.4 + dev: false + + /workbox-sw@6.5.4: + resolution: {integrity: sha512-vo2RQo7DILVRoH5LjGqw3nphavEjK4Qk+FenXeUsknKn14eCNedHOXWbmnvP4ipKhlE35pvJ4yl4YYf6YsJArA==} + dev: false + + /workbox-webpack-plugin@6.5.4(webpack@5.76.3): + resolution: {integrity: sha512-LmWm/zoaahe0EGmMTrSLUi+BjyR3cdGEfU3fS6PN1zKFYbqAKuQ+Oy/27e4VSXsyIwAw8+QDfk1XHNGtZu9nQg==} + engines: {node: '>=10.0.0'} + peerDependencies: + webpack: ^4.4.0 || ^5.9.0 + dependencies: + fast-json-stable-stringify: 2.1.0 + pretty-bytes: 5.6.0 + upath: 1.2.0 + webpack: 5.76.3(@swc/core@1.3.42) + webpack-sources: 1.4.3 + workbox-build: 6.5.4 + transitivePeerDependencies: + - '@types/babel__core' + - supports-color + dev: false + + /workbox-window@6.5.4: + resolution: {integrity: sha512-HnLZJDwYBE+hpG25AQBO8RUWBJRaCsI9ksQJEp3aCOFCaG5kqaToAYXFRAHxzRluM2cQbGzdQF5rjKPWPA1fug==} + dependencies: + '@types/trusted-types': 2.0.3 + workbox-core: 6.5.4 + dev: false + + /worker-farm@1.7.0: + resolution: {integrity: sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==} + dependencies: + errno: 0.1.8 + dev: true + + /wrap-ansi@5.1.0: + resolution: {integrity: sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==} + engines: {node: '>=6'} + dependencies: + ansi-styles: 3.2.1 + string-width: 3.1.0 + strip-ansi: 5.2.0 + dev: true + + /wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + /write-file-atomic@3.0.3: + resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} + dependencies: + imurmurhash: 0.1.4 + is-typedarray: 1.0.0 + signal-exit: 3.0.7 + typedarray-to-buffer: 3.1.5 + + /ws@6.2.2: + resolution: {integrity: sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dependencies: + async-limiter: 1.0.1 + dev: true + + /ws@7.5.9: + resolution: {integrity: sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + + /ws@8.11.0: + resolution: {integrity: sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: true + + /ws@8.13.0: + resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + + /ws@8.17.1: + resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: true + + /xdg-basedir@4.0.0: + resolution: {integrity: sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==} + engines: {node: '>=8'} + dev: true + + /xml-name-validator@3.0.0: + resolution: {integrity: sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==} + dev: false + + /xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + dev: false + + /xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + dev: true + + /y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + dev: true + + /y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + /yallist@2.1.2: + resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==} + dev: true + + /yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + /yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + /yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + + /yargs-parser@13.1.2: + resolution: {integrity: sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==} + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + dev: true + + /yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + + /yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + dev: true + + /yargs@13.3.2: + resolution: {integrity: sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==} + dependencies: + cliui: 5.0.0 + find-up: 3.0.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 3.1.0 + which-module: 2.0.0 + y18n: 4.0.3 + yargs-parser: 13.1.2 + dev: true + + /yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + dependencies: + cliui: 7.0.4 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + + /yargs@17.7.1: + resolution: {integrity: sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==} + engines: {node: '>=12'} + dependencies: + cliui: 8.0.1 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + dev: true + + /yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + /zepto@1.2.0: + resolution: {integrity: sha512-C1x6lfvBICFTQIMgbt3JqMOno3VOtkWat/xEakLTOurskYIHPmzJrzd1e8BnmtdDVJlGuk5D+FxyCA8MPmkIyA==} + dev: true diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 00000000000..22016911b45 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - 'docs' + - 'test/integration/*' diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 00000000000..b396488c148 --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,116 @@ +import cleanup from 'rollup-plugin-cleanup'; +import json from '@rollup/plugin-json'; +import resolve from '@rollup/plugin-node-resolve'; +import {swc} from 'rollup-plugin-swc3'; +import {terser} from 'rollup-plugin-terser'; +import {readFileSync} from 'fs'; + +const {version, homepage} = JSON.parse(readFileSync('./package.json')); + +const banner = `/*! + * Chart.js v${version} + * ${homepage} + * (c) ${(new Date(process.env.SOURCE_DATE_EPOCH ? (process.env.SOURCE_DATE_EPOCH * 1000) : new Date().getTime())).getFullYear()} Chart.js Contributors + * Released under the MIT License + */`; +const extensions = ['.js', '.ts']; +const plugins = (minify) => + [ + json(), + resolve({ + extensions + }), + swc({ + jsc: { + parser: { + syntax: 'typescript' + }, + target: 'es2022' + }, + module: { + type: 'es6' + }, + sourceMaps: true + }), + minify + ? terser({ + output: { + preamble: banner + } + }) + : cleanup({ + comments: ['some', /__PURE__/] + }) + ]; + +export default [ + // UMD build + // dist/chart.umd.min.js + { + input: 'src/index.umd.ts', + plugins: plugins(true), + output: { + name: 'Chart', + file: 'dist/chart.umd.min.js', + format: 'umd', + indent: false, + sourcemap: true, + }, + }, + + // UMD build + // dist/chart.umd.js (old filename) + { + input: 'src/index.umd.ts', + plugins: plugins(true), + output: { + name: 'Chart', + file: 'dist/chart.umd.js', + format: 'umd', + indent: false, + sourcemap: true, + }, + }, + + // ES6 builds + // dist/chart.js + // helpers/*.js + { + input: { + 'dist/chart': 'src/index.ts', + 'dist/helpers': 'src/helpers/index.ts' + }, + plugins: plugins(), + external: _ => (/node_modules/).test(_), + output: { + dir: './', + chunkFileNames: 'dist/chunks/[name].js', + entryFileNames: '[name].js', + banner, + format: 'esm', + indent: false, + sourcemap: true, + }, + }, + + // CommonJS builds + // dist/chart.js + // helpers/*.js + { + input: { + 'dist/chart': 'src/index.ts', + 'dist/helpers': 'src/helpers/index.ts' + }, + plugins: plugins(), + external: _ => (/node_modules/).test(_), + output: { + dir: './', + chunkFileNames: 'dist/chunks/[name].cjs', + entryFileNames: '[name].cjs', + banner, + format: 'commonjs', + indent: false, + sourcemap: true, + }, + } +]; diff --git a/samples/bar.html b/samples/bar.html deleted file mode 100644 index 5bf4b5bae36..00000000000 --- a/samples/bar.html +++ /dev/null @@ -1,45 +0,0 @@ - - - - Bar Chart - - - -
    - -
    - - - - - diff --git a/samples/doughnut.color.html b/samples/doughnut.color.html deleted file mode 100644 index 5457fd094fc..00000000000 --- a/samples/doughnut.color.html +++ /dev/null @@ -1,58 +0,0 @@ - - - - Doughnut Chart - - - - - -
    - -
    - - - - - diff --git a/samples/doughnut.html b/samples/doughnut.html deleted file mode 100644 index fdf7539a893..00000000000 --- a/samples/doughnut.html +++ /dev/null @@ -1,67 +0,0 @@ - - - - Doughnut Chart - - - - -
    - -
    - - - - - diff --git a/samples/line-customTooltips.html b/samples/line-customTooltips.html deleted file mode 100644 index 4dc46e1abca..00000000000 --- a/samples/line-customTooltips.html +++ /dev/null @@ -1,129 +0,0 @@ - - - - - Line Chart with Custom Tooltips - - - - - - - -
    - -
    -
    - -
    - -
    - - - - - - diff --git a/samples/line.html b/samples/line.html deleted file mode 100644 index ccd0dad9653..00000000000 --- a/samples/line.html +++ /dev/null @@ -1,54 +0,0 @@ - - - - Line Chart - - - -
    -
    - -
    -
    - - - - - diff --git a/samples/pie-customTooltips.html b/samples/pie-customTooltips.html deleted file mode 100644 index 732317de464..00000000000 --- a/samples/pie-customTooltips.html +++ /dev/null @@ -1,156 +0,0 @@ - - - - - Pie Chart with Custom Tooltips - - - - - - - -
    - -
    -
    - -
    - -
    - - - - - - diff --git a/samples/pie.html b/samples/pie.html deleted file mode 100644 index 255a4997618..00000000000 --- a/samples/pie.html +++ /dev/null @@ -1,58 +0,0 @@ - - - - Pie Chart - - - -
    - -
    - - - - - diff --git a/samples/polar-area.html b/samples/polar-area.html deleted file mode 100644 index d3d3f01b47e..00000000000 --- a/samples/polar-area.html +++ /dev/null @@ -1,60 +0,0 @@ - - - - Polar Area Chart - - - -
    - -
    - - - - - diff --git a/samples/radar.html b/samples/radar.html deleted file mode 100644 index 6a04f879c09..00000000000 --- a/samples/radar.html +++ /dev/null @@ -1,53 +0,0 @@ - - - - Radar Chart - - - - - -
    - -
    - - - - - diff --git a/scripts/deploy-docs.sh b/scripts/deploy-docs.sh new file mode 100755 index 00000000000..b637a0fa654 --- /dev/null +++ b/scripts/deploy-docs.sh @@ -0,0 +1,69 @@ +#!/bin/bash + +set -e + +source ./scripts/utils.sh + +TARGET_DIR='gh-pages' +TARGET_BRANCH='master' +TARGET_REPO_URL="https://$GITHUB_TOKEN@github.com/chartjs/chartjs.github.io.git" + +VERSION=$1 +MODE=$2 +TAG=$(tag_from_version "$VERSION" "$MODE") + +function move_sample_redirect { + local tag=$1 + + cp ../scripts/sample-redirect-template.html samples/$tag/index.html + sed -i -E "s/TAG/$tag/g" samples/$tag/index.html +} + +function deploy_tagged_files { + local tag=$1 + rm -rf "docs/$tag" + cp -r ../dist/docs docs/$tag + rm -rf "samples/$tag" + mkdir "samples/$tag" + + move_sample_redirect $tag + + deploy_versioned_files $tag +} + +function deploy_versioned_files { + local version=$1 + local in_files='../dist/chart*.js' + local out_path='./dist' + rm -rf $out_path/$version + mkdir -p $out_path/$version + cp -r $in_files $out_path/$version +} + +# Clone the repository and checkout the gh-pages branch +git clone $TARGET_REPO_URL $TARGET_DIR +cd $TARGET_DIR +git checkout $TARGET_BRANCH + +# https://www.chartjs.org/dist/$VERSION +if [["$VERSION" != "$TAG"]]; then + deploy_versioned_files $VERSION +fi + +# https://www.chartjs.org/dist/$TAG +# https://www.chartjs.org/docs/$TAG +# https://www.chartjs.org/samples/$TAG +deploy_tagged_files $TAG + +git add --all + +git remote add auth-origin $TARGET_REPO_URL +git config --global user.email "$GH_AUTH_EMAIL" +git config --global user.name "Chart.js" +git commit -m "Deploy $VERSION from $GITHUB_REPOSITORY" -m "Commit: $GITHUB_SHA" +git push -q auth-origin $TARGET_BRANCH +git remote rm auth-origin + +# Cleanup +cd .. +rm -rf $TARGET_DIR diff --git a/scripts/docs-config.sh b/scripts/docs-config.sh new file mode 100755 index 00000000000..4d15c782ae7 --- /dev/null +++ b/scripts/docs-config.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +set -e + +source ./scripts/utils.sh + +VERSION=$1 +MODE=$2 + +TAG=$(tag_from_version "$VERSION" "$MODE") + +sed -i -e "s/VERSION/$TAG/g" "docs/.vuepress/config.ts" diff --git a/scripts/publish.sh b/scripts/publish.sh new file mode 100755 index 00000000000..bf7781337ee --- /dev/null +++ b/scripts/publish.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +set -e + +NPM_TAG="next" + +if [[ "$VERSION" =~ ^[^-]+$ ]]; then + echo "Release tag indicates a full release. Releasing as \"latest\"." + NPM_TAG="latest" +fi + +npm publish --tag "$NPM_TAG" diff --git a/scripts/sample-redirect-template.html b/scripts/sample-redirect-template.html new file mode 100644 index 00000000000..06cf13b5f22 --- /dev/null +++ b/scripts/sample-redirect-template.html @@ -0,0 +1,32 @@ + + + + + Chart.js | Samples + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/scripts/utils.sh b/scripts/utils.sh new file mode 100644 index 00000000000..d3b6d46248b --- /dev/null +++ b/scripts/utils.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# tag is next|latest|master|x.x.x +# https://www.chartjs.org/dist/$tag/ +# https://www.chartjs.org/docs/$tag/ +# https://www.chartjs.org/samples/$tag/ +function tag_from_version { + local version=$1 + local mode=$2 + local tag='' + if [ "$version" == "master" ]; then + tag=master + elif [[ "$version" =~ ^[^-]+$ ]]; then + if [[ "$mode" == "release" ]]; then + tag=$version + else + tag=latest + fi + else + tag=next + fi + echo $tag +} diff --git a/src/Chart.Bar.js b/src/Chart.Bar.js deleted file mode 100644 index 81532b4fd26..00000000000 --- a/src/Chart.Bar.js +++ /dev/null @@ -1,303 +0,0 @@ -(function(){ - "use strict"; - - var root = this, - Chart = root.Chart, - helpers = Chart.helpers; - - - var defaultConfig = { - //Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value - scaleBeginAtZero : true, - - //Boolean - Whether grid lines are shown across the chart - scaleShowGridLines : true, - - //String - Colour of the grid lines - scaleGridLineColor : "rgba(0,0,0,.05)", - - //Number - Width of the grid lines - scaleGridLineWidth : 1, - - //Boolean - Whether to show horizontal lines (except X axis) - scaleShowHorizontalLines: true, - - //Boolean - Whether to show vertical lines (except Y axis) - scaleShowVerticalLines: true, - - //Boolean - If there is a stroke on each bar - barShowStroke : true, - - //Number - Pixel width of the bar stroke - barStrokeWidth : 2, - - //Number - Spacing between each of the X value sets - barValueSpacing : 5, - - //Number - Spacing between data sets within X values - barDatasetSpacing : 1, - - //String - A legend template - legendTemplate : "
      -legend\"><% for (var i=0; i
    • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
    • <%}%>
    " - - }; - - - Chart.Type.extend({ - name: "Bar", - defaults : defaultConfig, - initialize: function(data){ - - //Expose options as a scope variable here so we can access it in the ScaleClass - var options = this.options; - - this.ScaleClass = Chart.Scale.extend({ - offsetGridLines : true, - calculateBarX : function(datasetCount, datasetIndex, barIndex){ - //Reusable method for calculating the xPosition of a given bar based on datasetIndex & width of the bar - var xWidth = this.calculateBaseWidth(), - xAbsolute = this.calculateX(barIndex) - (xWidth/2), - barWidth = this.calculateBarWidth(datasetCount); - - return xAbsolute + (barWidth * datasetIndex) + (datasetIndex * options.barDatasetSpacing) + barWidth/2; - }, - calculateBaseWidth : function(){ - return (this.calculateX(1) - this.calculateX(0)) - (2*options.barValueSpacing); - }, - calculateBarWidth : function(datasetCount){ - //The padding between datasets is to the right of each bar, providing that there are more than 1 dataset - var baseWidth = this.calculateBaseWidth() - ((datasetCount - 1) * options.barDatasetSpacing); - - return (baseWidth / datasetCount); - } - }); - - this.datasets = []; - - //Set up tooltip events on the chart - if (this.options.showTooltips){ - helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ - var activeBars = (evt.type !== 'mouseout') ? this.getBarsAtEvent(evt) : []; - - this.eachBars(function(bar){ - bar.restore(['fillColor', 'strokeColor']); - }); - helpers.each(activeBars, function(activeBar){ - activeBar.fillColor = activeBar.highlightFill; - activeBar.strokeColor = activeBar.highlightStroke; - }); - this.showTooltip(activeBars); - }); - } - - //Declare the extension of the default point, to cater for the options passed in to the constructor - this.BarClass = Chart.Rectangle.extend({ - strokeWidth : this.options.barStrokeWidth, - showStroke : this.options.barShowStroke, - ctx : this.chart.ctx - }); - - //Iterate through each of the datasets, and build this into a property of the chart - helpers.each(data.datasets,function(dataset,datasetIndex){ - - var datasetObject = { - label : dataset.label || null, - fillColor : dataset.fillColor, - strokeColor : dataset.strokeColor, - bars : [] - }; - - this.datasets.push(datasetObject); - - helpers.each(dataset.data,function(dataPoint,index){ - //Add a new point for each piece of data, passing any required data to draw. - datasetObject.bars.push(new this.BarClass({ - value : dataPoint, - label : data.labels[index], - datasetLabel: dataset.label, - strokeColor : dataset.strokeColor, - fillColor : dataset.fillColor, - highlightFill : dataset.highlightFill || dataset.fillColor, - highlightStroke : dataset.highlightStroke || dataset.strokeColor - })); - },this); - - },this); - - this.buildScale(data.labels); - - this.BarClass.prototype.base = this.scale.endPoint; - - this.eachBars(function(bar, index, datasetIndex){ - helpers.extend(bar, { - width : this.scale.calculateBarWidth(this.datasets.length), - x: this.scale.calculateBarX(this.datasets.length, datasetIndex, index), - y: this.scale.endPoint - }); - bar.save(); - }, this); - - this.render(); - }, - update : function(){ - this.scale.update(); - // Reset any highlight colours before updating. - helpers.each(this.activeElements, function(activeElement){ - activeElement.restore(['fillColor', 'strokeColor']); - }); - - this.eachBars(function(bar){ - bar.save(); - }); - this.render(); - }, - eachBars : function(callback){ - helpers.each(this.datasets,function(dataset, datasetIndex){ - helpers.each(dataset.bars, callback, this, datasetIndex); - },this); - }, - getBarsAtEvent : function(e){ - var barsArray = [], - eventPosition = helpers.getRelativePosition(e), - datasetIterator = function(dataset){ - barsArray.push(dataset.bars[barIndex]); - }, - barIndex; - - for (var datasetIndex = 0; datasetIndex < this.datasets.length; datasetIndex++) { - for (barIndex = 0; barIndex < this.datasets[datasetIndex].bars.length; barIndex++) { - if (this.datasets[datasetIndex].bars[barIndex].inRange(eventPosition.x,eventPosition.y)){ - helpers.each(this.datasets, datasetIterator); - return barsArray; - } - } - } - - return barsArray; - }, - buildScale : function(labels){ - var self = this; - - var dataTotal = function(){ - var values = []; - self.eachBars(function(bar){ - values.push(bar.value); - }); - return values; - }; - - var scaleOptions = { - templateString : this.options.scaleLabel, - height : this.chart.height, - width : this.chart.width, - ctx : this.chart.ctx, - textColor : this.options.scaleFontColor, - fontSize : this.options.scaleFontSize, - fontStyle : this.options.scaleFontStyle, - fontFamily : this.options.scaleFontFamily, - valuesCount : labels.length, - beginAtZero : this.options.scaleBeginAtZero, - integersOnly : this.options.scaleIntegersOnly, - calculateYRange: function(currentHeight){ - var updatedRanges = helpers.calculateScaleRange( - dataTotal(), - currentHeight, - this.fontSize, - this.beginAtZero, - this.integersOnly - ); - helpers.extend(this, updatedRanges); - }, - xLabels : labels, - font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily), - lineWidth : this.options.scaleLineWidth, - lineColor : this.options.scaleLineColor, - showHorizontalLines : this.options.scaleShowHorizontalLines, - showVerticalLines : this.options.scaleShowVerticalLines, - gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, - gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", - padding : (this.options.showScale) ? 0 : (this.options.barShowStroke) ? this.options.barStrokeWidth : 0, - showLabels : this.options.scaleShowLabels, - display : this.options.showScale - }; - - if (this.options.scaleOverride){ - helpers.extend(scaleOptions, { - calculateYRange: helpers.noop, - steps: this.options.scaleSteps, - stepValue: this.options.scaleStepWidth, - min: this.options.scaleStartValue, - max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) - }); - } - - this.scale = new this.ScaleClass(scaleOptions); - }, - addData : function(valuesArray,label){ - //Map the values array for each of the datasets - helpers.each(valuesArray,function(value,datasetIndex){ - //Add a new point for each piece of data, passing any required data to draw. - this.datasets[datasetIndex].bars.push(new this.BarClass({ - value : value, - label : label, - datasetLabel: this.datasets[datasetIndex].label, - x: this.scale.calculateBarX(this.datasets.length, datasetIndex, this.scale.valuesCount+1), - y: this.scale.endPoint, - width : this.scale.calculateBarWidth(this.datasets.length), - base : this.scale.endPoint, - strokeColor : this.datasets[datasetIndex].strokeColor, - fillColor : this.datasets[datasetIndex].fillColor - })); - },this); - - this.scale.addXLabel(label); - //Then re-render the chart. - this.update(); - }, - removeData : function(){ - this.scale.removeXLabel(); - //Then re-render the chart. - helpers.each(this.datasets,function(dataset){ - dataset.bars.shift(); - },this); - this.update(); - }, - reflow : function(){ - helpers.extend(this.BarClass.prototype,{ - y: this.scale.endPoint, - base : this.scale.endPoint - }); - var newScaleProps = helpers.extend({ - height : this.chart.height, - width : this.chart.width - }); - this.scale.update(newScaleProps); - }, - draw : function(ease){ - var easingDecimal = ease || 1; - this.clear(); - - var ctx = this.chart.ctx; - - this.scale.draw(easingDecimal); - - //Draw all the bars for each dataset - helpers.each(this.datasets,function(dataset,datasetIndex){ - helpers.each(dataset.bars,function(bar,index){ - if (bar.hasValue()){ - bar.base = this.scale.endPoint; - //Transition then draw - bar.transition({ - x : this.scale.calculateBarX(this.datasets.length, datasetIndex, index), - y : this.scale.calculateY(bar.value), - width : this.scale.calculateBarWidth(this.datasets.length) - }, easingDecimal).draw(); - } - },this); - - },this); - } - }); - - -}).call(this); diff --git a/src/Chart.Core.js b/src/Chart.Core.js deleted file mode 100755 index 5b1beb4e0c1..00000000000 --- a/src/Chart.Core.js +++ /dev/null @@ -1,2233 +0,0 @@ -/*! - * Chart.js - * http://chartjs.org/ - * Version: {{ version }} - * - * Copyright 2015 Nick Downie - * Released under the MIT license - * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md - */ - - -(function(){ - - "use strict"; - - //Declare root variable - window in the browser, global on the server - var root = this, - previous = root.Chart; - - //Occupy the global variable of Chart, and create a simple base class - var Chart = function(context){ - var chart = this; - this.canvas = context.canvas; - - this.ctx = context; - - //Variables global to the chart - var computeDimension = function(element,dimension) - { - if (element['offset'+dimension]) - { - return element['offset'+dimension]; - } - else - { - return document.defaultView.getComputedStyle(element).getPropertyValue(dimension); - } - }; - - var width = this.width = computeDimension(context.canvas,'Width') || context.canvas.width; - var height = this.height = computeDimension(context.canvas,'Height') || context.canvas.height; - - // Firefox requires this to work correctly - context.canvas.width = width; - context.canvas.height = height; - - width = this.width = context.canvas.width; - height = this.height = context.canvas.height; - this.aspectRatio = this.width / this.height; - //High pixel density displays - multiply the size of the canvas height/width by the device pixel ratio, then scale. - helpers.retinaScale(this); - - return this; - }; - //Globally expose the defaults to allow for user updating/changing - Chart.defaults = { - global: { - // Boolean - Whether to animate the chart - animation: true, - - // Number - Number of animation steps - animationSteps: 60, - - // String - Animation easing effect - animationEasing: "easeOutQuart", - - // Boolean - If we should show the scale at all - showScale: true, - - // Boolean - If we want to override with a hard coded scale - scaleOverride: false, - - // ** Required if scaleOverride is true ** - // Number - The number of steps in a hard coded scale - scaleSteps: null, - // Number - The value jump in the hard coded scale - scaleStepWidth: null, - // Number - The scale starting value - scaleStartValue: null, - - // String - Colour of the scale line - scaleLineColor: "rgba(0,0,0,.1)", - - // Number - Pixel width of the scale line - scaleLineWidth: 1, - - // Boolean - Whether to show labels on the scale - scaleShowLabels: true, - - // Interpolated JS string - can access value - scaleLabel: "<%=value%>", - - // Boolean - Whether the scale should stick to integers, and not show any floats even if drawing space is there - scaleIntegersOnly: true, - - // Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value - scaleBeginAtZero: false, - - // String - Scale label font declaration for the scale label - scaleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", - - // Number - Scale label font size in pixels - scaleFontSize: 12, - - // String - Scale label font weight style - scaleFontStyle: "normal", - - // String - Scale label font colour - scaleFontColor: "#666", - - // Boolean - whether or not the chart should be responsive and resize when the browser does. - responsive: false, - - // Boolean - whether to maintain the starting aspect ratio or not when responsive, if set to false, will take up entire container - maintainAspectRatio: true, - - // Boolean - Determines whether to draw tooltips on the canvas or not - attaches events to touchmove & mousemove - showTooltips: true, - - // Boolean - Determines whether to draw built-in tooltip or call custom tooltip function - customTooltips: false, - - // Array - Array of string names to attach tooltip events - tooltipEvents: ["mousemove", "touchstart", "touchmove", "mouseout"], - - // String - Tooltip background colour - tooltipFillColor: "rgba(0,0,0,0.8)", - - // String - Tooltip label font declaration for the scale label - tooltipFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", - - // Number - Tooltip label font size in pixels - tooltipFontSize: 14, - - // String - Tooltip font weight style - tooltipFontStyle: "normal", - - // String - Tooltip label font colour - tooltipFontColor: "#fff", - - // String - Tooltip title font declaration for the scale label - tooltipTitleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", - - // Number - Tooltip title font size in pixels - tooltipTitleFontSize: 14, - - // String - Tooltip title font weight style - tooltipTitleFontStyle: "bold", - - // String - Tooltip title font colour - tooltipTitleFontColor: "#fff", - - // String - Tooltip title template - tooltipTitleTemplate: "<%= label%>", - - // Number - pixel width of padding around tooltip text - tooltipYPadding: 6, - - // Number - pixel width of padding around tooltip text - tooltipXPadding: 6, - - // Number - Size of the caret on the tooltip - tooltipCaretSize: 8, - - // Number - Pixel radius of the tooltip border - tooltipCornerRadius: 6, - - // Number - Pixel offset from point x to tooltip edge - tooltipXOffset: 10, - - // String - Template string for single tooltips - tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= value %>", - - // String - Template string for single tooltips - multiTooltipTemplate: "<%= value %>", - - // String - Colour behind the legend colour block - multiTooltipKeyBackground: '#fff', - - // Function - Will fire on animation progression. - onAnimationProgress: function(){}, - - // Function - Will fire on animation completion. - onAnimationComplete: function(){} - - } - }; - - //Create a dictionary of chart types, to allow for extension of existing types - Chart.types = {}; - - //Global Chart helpers object for utility methods and classes - var helpers = Chart.helpers = {}; - - //-- Basic js utility methods - var each = helpers.each = function(loopable,callback,self){ - var additionalArgs = Array.prototype.slice.call(arguments, 3); - // Check to see if null or undefined firstly. - if (loopable){ - if (loopable.length === +loopable.length){ - var i; - for (i=0; i= 0; i--) { - var currentItem = arrayToSearch[i]; - if (filterCallback(currentItem)){ - return currentItem; - } - } - }, - inherits = helpers.inherits = function(extensions){ - //Basic javascript inheritance based on the model created in Backbone.js - var parent = this; - var ChartElement = (extensions && extensions.hasOwnProperty("constructor")) ? extensions.constructor : function(){ return parent.apply(this, arguments); }; - - var Surrogate = function(){ this.constructor = ChartElement;}; - Surrogate.prototype = parent.prototype; - ChartElement.prototype = new Surrogate(); - - ChartElement.extend = inherits; - - if (extensions) extend(ChartElement.prototype, extensions); - - ChartElement.__super__ = parent.prototype; - - return ChartElement; - }, - noop = helpers.noop = function(){}, - uid = helpers.uid = (function(){ - var id=0; - return function(){ - return "chart-" + id++; - }; - })(), - warn = helpers.warn = function(str){ - //Method for warning of errors - if (window.console && typeof window.console.warn === "function") console.warn(str); - }, - amd = helpers.amd = (typeof define === 'function' && define.amd), - //-- Math methods - isNumber = helpers.isNumber = function(n){ - return !isNaN(parseFloat(n)) && isFinite(n); - }, - max = helpers.max = function(array){ - return Math.max.apply( Math, array ); - }, - min = helpers.min = function(array){ - return Math.min.apply( Math, array ); - }, - cap = helpers.cap = function(valueToCap,maxValue,minValue){ - if(isNumber(maxValue)) { - if( valueToCap > maxValue ) { - return maxValue; - } - } - else if(isNumber(minValue)){ - if ( valueToCap < minValue ){ - return minValue; - } - } - return valueToCap; - }, - getDecimalPlaces = helpers.getDecimalPlaces = function(num){ - if (num%1!==0 && isNumber(num)){ - var s = num.toString(); - if(s.indexOf("e-") < 0){ - // no exponent, e.g. 0.01 - return s.split(".")[1].length; - } - else if(s.indexOf(".") < 0) { - // no decimal point, e.g. 1e-9 - return parseInt(s.split("e-")[1]); - } - else { - // exponent and decimal point, e.g. 1.23e-9 - var parts = s.split(".")[1].split("e-"); - return parts[0].length + parseInt(parts[1]); - } - } - else { - return 0; - } - }, - toRadians = helpers.radians = function(degrees){ - return degrees * (Math.PI/180); - }, - // Gets the angle from vertical upright to the point about a centre. - getAngleFromPoint = helpers.getAngleFromPoint = function(centrePoint, anglePoint){ - var distanceFromXCenter = anglePoint.x - centrePoint.x, - distanceFromYCenter = anglePoint.y - centrePoint.y, - radialDistanceFromCenter = Math.sqrt( distanceFromXCenter * distanceFromXCenter + distanceFromYCenter * distanceFromYCenter); - - - var angle = Math.PI * 2 + Math.atan2(distanceFromYCenter, distanceFromXCenter); - - //If the segment is in the top left quadrant, we need to add another rotation to the angle - if (distanceFromXCenter < 0 && distanceFromYCenter < 0){ - angle += Math.PI*2; - } - - return { - angle: angle, - distance: radialDistanceFromCenter - }; - }, - aliasPixel = helpers.aliasPixel = function(pixelWidth){ - return (pixelWidth % 2 === 0) ? 0 : 0.5; - }, - splineCurve = helpers.splineCurve = function(FirstPoint,MiddlePoint,AfterPoint,t){ - //Props to Rob Spencer at scaled innovation for his post on splining between points - //http://scaledinnovation.com/analytics/splines/aboutSplines.html - var d01=Math.sqrt(Math.pow(MiddlePoint.x-FirstPoint.x,2)+Math.pow(MiddlePoint.y-FirstPoint.y,2)), - d12=Math.sqrt(Math.pow(AfterPoint.x-MiddlePoint.x,2)+Math.pow(AfterPoint.y-MiddlePoint.y,2)), - fa=t*d01/(d01+d12),// scaling factor for triangle Ta - fb=t*d12/(d01+d12); - return { - inner : { - x : MiddlePoint.x-fa*(AfterPoint.x-FirstPoint.x), - y : MiddlePoint.y-fa*(AfterPoint.y-FirstPoint.y) - }, - outer : { - x: MiddlePoint.x+fb*(AfterPoint.x-FirstPoint.x), - y : MiddlePoint.y+fb*(AfterPoint.y-FirstPoint.y) - } - }; - }, - calculateOrderOfMagnitude = helpers.calculateOrderOfMagnitude = function(val){ - return Math.floor(Math.log(val) / Math.LN10); - }, - calculateScaleRange = helpers.calculateScaleRange = function(valuesArray, drawingSize, textSize, startFromZero, integersOnly){ - - //Set a minimum step of two - a point at the top of the graph, and a point at the base - var minSteps = 2, - maxSteps = Math.floor(drawingSize/(textSize * 1.5)), - skipFitting = (minSteps >= maxSteps); - - var maxValue = max(valuesArray), - minValue = min(valuesArray); - - // We need some degree of separation here to calculate the scales if all the values are the same - // Adding/minusing 0.5 will give us a range of 1. - if (maxValue === minValue){ - maxValue += 0.5; - // So we don't end up with a graph with a negative start value if we've said always start from zero - if (minValue >= 0.5 && !startFromZero){ - minValue -= 0.5; - } - else{ - // Make up a whole number above the values - maxValue += 0.5; - } - } - - var valueRange = Math.abs(maxValue - minValue), - rangeOrderOfMagnitude = calculateOrderOfMagnitude(valueRange), - graphMax = Math.ceil(maxValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude), - graphMin = (startFromZero) ? 0 : Math.floor(minValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude), - graphRange = graphMax - graphMin, - stepValue = Math.pow(10, rangeOrderOfMagnitude), - numberOfSteps = Math.round(graphRange / stepValue); - - //If we have more space on the graph we'll use it to give more definition to the data - while((numberOfSteps > maxSteps || (numberOfSteps * 2) < maxSteps) && !skipFitting) { - if(numberOfSteps > maxSteps){ - stepValue *=2; - numberOfSteps = Math.round(graphRange/stepValue); - // Don't ever deal with a decimal number of steps - cancel fitting and just use the minimum number of steps. - if (numberOfSteps % 1 !== 0){ - skipFitting = true; - } - } - //We can fit in double the amount of scale points on the scale - else{ - //If user has declared ints only, and the step value isn't a decimal - if (integersOnly && rangeOrderOfMagnitude >= 0){ - //If the user has said integers only, we need to check that making the scale more granular wouldn't make it a float - if(stepValue/2 % 1 === 0){ - stepValue /=2; - numberOfSteps = Math.round(graphRange/stepValue); - } - //If it would make it a float break out of the loop - else{ - break; - } - } - //If the scale doesn't have to be an int, make the scale more granular anyway. - else{ - stepValue /=2; - numberOfSteps = Math.round(graphRange/stepValue); - } - - } - } - - if (skipFitting){ - numberOfSteps = minSteps; - stepValue = graphRange / numberOfSteps; - } - - return { - steps : numberOfSteps, - stepValue : stepValue, - min : graphMin, - max : graphMin + (numberOfSteps * stepValue) - }; - - }, - /* jshint ignore:start */ - // Blows up jshint errors based on the new Function constructor - //Templating methods - //Javascript micro templating by John Resig - source at http://ejohn.org/blog/javascript-micro-templating/ - template = helpers.template = function(templateString, valuesObject){ - - // If templateString is function rather than string-template - call the function for valuesObject - - if(templateString instanceof Function){ - return templateString(valuesObject); - } - - var cache = {}; - function tmpl(str, data){ - // Figure out if we're getting a template, or if we need to - // load the template - and be sure to cache the result. - var fn = !/\W/.test(str) ? - cache[str] = cache[str] : - - // Generate a reusable function that will serve as a template - // generator (and which will be cached). - new Function("obj", - "var p=[],print=function(){p.push.apply(p,arguments);};" + - - // Introduce the data as local variables using with(){} - "with(obj){p.push('" + - - // Convert the template into pure JavaScript - str - .replace(/[\r\t\n]/g, " ") - .split("<%").join("\t") - .replace(/((^|%>)[^\t]*)'/g, "$1\r") - .replace(/\t=(.*?)%>/g, "',$1,'") - .split("\t").join("');") - .split("%>").join("p.push('") - .split("\r").join("\\'") + - "');}return p.join('');" - ); - - // Provide some basic currying to the user - return data ? fn( data ) : fn; - } - return tmpl(templateString,valuesObject); - }, - /* jshint ignore:end */ - generateLabels = helpers.generateLabels = function(templateString,numberOfSteps,graphMin,stepValue){ - var labelsArray = new Array(numberOfSteps); - if (templateString){ - each(labelsArray,function(val,index){ - labelsArray[index] = template(templateString,{value: (graphMin + (stepValue*(index+1)))}); - }); - } - return labelsArray; - }, - //--Animation methods - //Easing functions adapted from Robert Penner's easing equations - //http://www.robertpenner.com/easing/ - easingEffects = helpers.easingEffects = { - linear: function (t) { - return t; - }, - easeInQuad: function (t) { - return t * t; - }, - easeOutQuad: function (t) { - return -1 * t * (t - 2); - }, - easeInOutQuad: function (t) { - if ((t /= 1 / 2) < 1){ - return 1 / 2 * t * t; - } - return -1 / 2 * ((--t) * (t - 2) - 1); - }, - easeInCubic: function (t) { - return t * t * t; - }, - easeOutCubic: function (t) { - return 1 * ((t = t / 1 - 1) * t * t + 1); - }, - easeInOutCubic: function (t) { - if ((t /= 1 / 2) < 1){ - return 1 / 2 * t * t * t; - } - return 1 / 2 * ((t -= 2) * t * t + 2); - }, - easeInQuart: function (t) { - return t * t * t * t; - }, - easeOutQuart: function (t) { - return -1 * ((t = t / 1 - 1) * t * t * t - 1); - }, - easeInOutQuart: function (t) { - if ((t /= 1 / 2) < 1){ - return 1 / 2 * t * t * t * t; - } - return -1 / 2 * ((t -= 2) * t * t * t - 2); - }, - easeInQuint: function (t) { - return 1 * (t /= 1) * t * t * t * t; - }, - easeOutQuint: function (t) { - return 1 * ((t = t / 1 - 1) * t * t * t * t + 1); - }, - easeInOutQuint: function (t) { - if ((t /= 1 / 2) < 1){ - return 1 / 2 * t * t * t * t * t; - } - return 1 / 2 * ((t -= 2) * t * t * t * t + 2); - }, - easeInSine: function (t) { - return -1 * Math.cos(t / 1 * (Math.PI / 2)) + 1; - }, - easeOutSine: function (t) { - return 1 * Math.sin(t / 1 * (Math.PI / 2)); - }, - easeInOutSine: function (t) { - return -1 / 2 * (Math.cos(Math.PI * t / 1) - 1); - }, - easeInExpo: function (t) { - return (t === 0) ? 1 : 1 * Math.pow(2, 10 * (t / 1 - 1)); - }, - easeOutExpo: function (t) { - return (t === 1) ? 1 : 1 * (-Math.pow(2, -10 * t / 1) + 1); - }, - easeInOutExpo: function (t) { - if (t === 0){ - return 0; - } - if (t === 1){ - return 1; - } - if ((t /= 1 / 2) < 1){ - return 1 / 2 * Math.pow(2, 10 * (t - 1)); - } - return 1 / 2 * (-Math.pow(2, -10 * --t) + 2); - }, - easeInCirc: function (t) { - if (t >= 1){ - return t; - } - return -1 * (Math.sqrt(1 - (t /= 1) * t) - 1); - }, - easeOutCirc: function (t) { - return 1 * Math.sqrt(1 - (t = t / 1 - 1) * t); - }, - easeInOutCirc: function (t) { - if ((t /= 1 / 2) < 1){ - return -1 / 2 * (Math.sqrt(1 - t * t) - 1); - } - return 1 / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1); - }, - easeInElastic: function (t) { - var s = 1.70158; - var p = 0; - var a = 1; - if (t === 0){ - return 0; - } - if ((t /= 1) == 1){ - return 1; - } - if (!p){ - p = 1 * 0.3; - } - if (a < Math.abs(1)) { - a = 1; - s = p / 4; - } else{ - s = p / (2 * Math.PI) * Math.asin(1 / a); - } - return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p)); - }, - easeOutElastic: function (t) { - var s = 1.70158; - var p = 0; - var a = 1; - if (t === 0){ - return 0; - } - if ((t /= 1) == 1){ - return 1; - } - if (!p){ - p = 1 * 0.3; - } - if (a < Math.abs(1)) { - a = 1; - s = p / 4; - } else{ - s = p / (2 * Math.PI) * Math.asin(1 / a); - } - return a * Math.pow(2, -10 * t) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) + 1; - }, - easeInOutElastic: function (t) { - var s = 1.70158; - var p = 0; - var a = 1; - if (t === 0){ - return 0; - } - if ((t /= 1 / 2) == 2){ - return 1; - } - if (!p){ - p = 1 * (0.3 * 1.5); - } - if (a < Math.abs(1)) { - a = 1; - s = p / 4; - } else { - s = p / (2 * Math.PI) * Math.asin(1 / a); - } - if (t < 1){ - return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p));} - return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) * 0.5 + 1; - }, - easeInBack: function (t) { - var s = 1.70158; - return 1 * (t /= 1) * t * ((s + 1) * t - s); - }, - easeOutBack: function (t) { - var s = 1.70158; - return 1 * ((t = t / 1 - 1) * t * ((s + 1) * t + s) + 1); - }, - easeInOutBack: function (t) { - var s = 1.70158; - if ((t /= 1 / 2) < 1){ - return 1 / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)); - } - return 1 / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2); - }, - easeInBounce: function (t) { - return 1 - easingEffects.easeOutBounce(1 - t); - }, - easeOutBounce: function (t) { - if ((t /= 1) < (1 / 2.75)) { - return 1 * (7.5625 * t * t); - } else if (t < (2 / 2.75)) { - return 1 * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75); - } else if (t < (2.5 / 2.75)) { - return 1 * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375); - } else { - return 1 * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375); - } - }, - easeInOutBounce: function (t) { - if (t < 1 / 2){ - return easingEffects.easeInBounce(t * 2) * 0.5; - } - return easingEffects.easeOutBounce(t * 2 - 1) * 0.5 + 1 * 0.5; - } - }, - //Request animation polyfill - http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/ - requestAnimFrame = helpers.requestAnimFrame = (function(){ - return window.requestAnimationFrame || - window.webkitRequestAnimationFrame || - window.mozRequestAnimationFrame || - window.oRequestAnimationFrame || - window.msRequestAnimationFrame || - function(callback) { - return window.setTimeout(callback, 1000 / 60); - }; - })(), - cancelAnimFrame = helpers.cancelAnimFrame = (function(){ - return window.cancelAnimationFrame || - window.webkitCancelAnimationFrame || - window.mozCancelAnimationFrame || - window.oCancelAnimationFrame || - window.msCancelAnimationFrame || - function(callback) { - return window.clearTimeout(callback, 1000 / 60); - }; - })(), - animationLoop = helpers.animationLoop = function(callback,totalSteps,easingString,onProgress,onComplete,chartInstance){ - - var currentStep = 0, - easingFunction = easingEffects[easingString] || easingEffects.linear; - - var animationFrame = function(){ - currentStep++; - var stepDecimal = currentStep/totalSteps; - var easeDecimal = easingFunction(stepDecimal); - - callback.call(chartInstance,easeDecimal,stepDecimal, currentStep); - onProgress.call(chartInstance,easeDecimal,stepDecimal); - if (currentStep < totalSteps){ - chartInstance.animationFrame = requestAnimFrame(animationFrame); - } else{ - onComplete.apply(chartInstance); - } - }; - requestAnimFrame(animationFrame); - }, - //-- DOM methods - getRelativePosition = helpers.getRelativePosition = function(evt){ - var mouseX, mouseY; - var e = evt.originalEvent || evt, - canvas = evt.currentTarget || evt.srcElement, - boundingRect = canvas.getBoundingClientRect(); - - if (e.touches){ - mouseX = e.touches[0].clientX - boundingRect.left; - mouseY = e.touches[0].clientY - boundingRect.top; - - } - else{ - mouseX = e.clientX - boundingRect.left; - mouseY = e.clientY - boundingRect.top; - } - - return { - x : mouseX, - y : mouseY - }; - - }, - addEvent = helpers.addEvent = function(node,eventType,method){ - if (node.addEventListener){ - node.addEventListener(eventType,method); - } else if (node.attachEvent){ - node.attachEvent("on"+eventType, method); - } else { - node["on"+eventType] = method; - } - }, - removeEvent = helpers.removeEvent = function(node, eventType, handler){ - if (node.removeEventListener){ - node.removeEventListener(eventType, handler, false); - } else if (node.detachEvent){ - node.detachEvent("on"+eventType,handler); - } else{ - node["on" + eventType] = noop; - } - }, - bindEvents = helpers.bindEvents = function(chartInstance, arrayOfEvents, handler){ - // Create the events object if it's not already present - if (!chartInstance.events) chartInstance.events = {}; - - each(arrayOfEvents,function(eventName){ - chartInstance.events[eventName] = function(){ - handler.apply(chartInstance, arguments); - }; - addEvent(chartInstance.chart.canvas,eventName,chartInstance.events[eventName]); - }); - }, - unbindEvents = helpers.unbindEvents = function (chartInstance, arrayOfEvents) { - each(arrayOfEvents, function(handler,eventName){ - removeEvent(chartInstance.chart.canvas, eventName, handler); - }); - }, - getMaximumWidth = helpers.getMaximumWidth = function(domNode){ - var container = domNode.parentNode, - padding = parseInt(getStyle(container, 'padding-left')) + parseInt(getStyle(container, 'padding-right')); - // TODO = check cross browser stuff with this. - return container.clientWidth - padding; - }, - getMaximumHeight = helpers.getMaximumHeight = function(domNode){ - var container = domNode.parentNode, - padding = parseInt(getStyle(container, 'padding-bottom')) + parseInt(getStyle(container, 'padding-top')); - // TODO = check cross browser stuff with this. - return container.clientHeight - padding; - }, - getStyle = helpers.getStyle = function (el, property) { - return el.currentStyle ? - el.currentStyle[property] : - document.defaultView.getComputedStyle(el, null).getPropertyValue(property); - }, - getMaximumSize = helpers.getMaximumSize = helpers.getMaximumWidth, // legacy support - retinaScale = helpers.retinaScale = function(chart){ - var ctx = chart.ctx, - width = chart.canvas.width, - height = chart.canvas.height; - - if (window.devicePixelRatio) { - ctx.canvas.style.width = width + "px"; - ctx.canvas.style.height = height + "px"; - ctx.canvas.height = height * window.devicePixelRatio; - ctx.canvas.width = width * window.devicePixelRatio; - ctx.scale(window.devicePixelRatio, window.devicePixelRatio); - } - }, - //-- Canvas methods - clear = helpers.clear = function(chart){ - chart.ctx.clearRect(0,0,chart.width,chart.height); - }, - fontString = helpers.fontString = function(pixelSize,fontStyle,fontFamily){ - return fontStyle + " " + pixelSize+"px " + fontFamily; - }, - longestText = helpers.longestText = function(ctx,font,arrayOfStrings){ - ctx.font = font; - var longest = 0; - each(arrayOfStrings,function(string){ - var textWidth = ctx.measureText(string).width; - longest = (textWidth > longest) ? textWidth : longest; - }); - return longest; - }, - drawRoundedRectangle = helpers.drawRoundedRectangle = function(ctx,x,y,width,height,radius){ - ctx.beginPath(); - ctx.moveTo(x + radius, y); - ctx.lineTo(x + width - radius, y); - ctx.quadraticCurveTo(x + width, y, x + width, y + radius); - ctx.lineTo(x + width, y + height - radius); - ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); - ctx.lineTo(x + radius, y + height); - ctx.quadraticCurveTo(x, y + height, x, y + height - radius); - ctx.lineTo(x, y + radius); - ctx.quadraticCurveTo(x, y, x + radius, y); - ctx.closePath(); - }; - - - //Store a reference to each instance - allowing us to globally resize chart instances on window resize. - //Destroy method on the chart will remove the instance of the chart from this reference. - Chart.instances = {}; - - Chart.Type = function(data,options,chart){ - this.options = options; - this.chart = chart; - this.id = uid(); - //Add the chart instance to the global namespace - Chart.instances[this.id] = this; - - // Initialize is always called when a chart type is created - // By default it is a no op, but it should be extended - if (options.responsive){ - this.resize(); - } - this.initialize.call(this,data); - }; - - //Core methods that'll be a part of every chart type - extend(Chart.Type.prototype,{ - initialize : function(){return this;}, - clear : function(){ - clear(this.chart); - return this; - }, - stop : function(){ - // Stops any current animation loop occuring - Chart.animationService.cancelAnimation(this); - return this; - }, - resize : function(callback){ - this.stop(); - var canvas = this.chart.canvas, - newWidth = getMaximumWidth(this.chart.canvas), - newHeight = this.options.maintainAspectRatio ? newWidth / this.chart.aspectRatio : getMaximumHeight(this.chart.canvas); - - canvas.width = this.chart.width = newWidth; - canvas.height = this.chart.height = newHeight; - - retinaScale(this.chart); - - if (typeof callback === "function"){ - callback.apply(this, Array.prototype.slice.call(arguments, 1)); - } - return this; - }, - reflow : noop, - render : function(reflow){ - if (reflow){ - this.reflow(); - } - - if (this.options.animation && !reflow){ - var animation = new Chart.Animation(); - animation.numSteps = this.options.animationSteps; - animation.easing = this.options.animationEasing; - - // render function - animation.render = function(chartInstance, animationObject) { - var easingFunction = helpers.easingEffects[animationObject.easing]; - var stepDecimal = animationObject.currentStep / animationObject.numSteps; - var easeDecimal = easingFunction(stepDecimal); - - chartInstance.draw(easeDecimal, stepDecimal, animationObject.currentStep); - }; - - // user events - animation.onAnimationProgress = this.options.onAnimationProgress; - animation.onAnimationComplete = this.options.onAnimationComplete; - - Chart.animationService.addAnimation(this, animation); - } - else{ - this.draw(); - this.options.onAnimationComplete.call(this); - } - return this; - }, - generateLegend : function(){ - return template(this.options.legendTemplate,this); - }, - destroy : function(){ - this.clear(); - unbindEvents(this, this.events); - var canvas = this.chart.canvas; - - // Reset canvas height/width attributes starts a fresh with the canvas context - canvas.width = this.chart.width; - canvas.height = this.chart.height; - - // < IE9 doesn't support removeProperty - if (canvas.style.removeProperty) { - canvas.style.removeProperty('width'); - canvas.style.removeProperty('height'); - } else { - canvas.style.removeAttribute('width'); - canvas.style.removeAttribute('height'); - } - - delete Chart.instances[this.id]; - }, - showTooltip : function(ChartElements, forceRedraw){ - // Only redraw the chart if we've actually changed what we're hovering on. - if (typeof this.activeElements === 'undefined') this.activeElements = []; - - var isChanged = (function(Elements){ - var changed = false; - - if (Elements.length !== this.activeElements.length){ - changed = true; - return changed; - } - - each(Elements, function(element, index){ - if (element !== this.activeElements[index]){ - changed = true; - } - }, this); - return changed; - }).call(this, ChartElements); - - if (!isChanged && !forceRedraw){ - return; - } - else{ - this.activeElements = ChartElements; - } - this.draw(); - if(this.options.customTooltips){ - this.options.customTooltips(false); - } - if (ChartElements.length > 0){ - // If we have multiple datasets, show a MultiTooltip for all of the data points at that index - if (this.datasets && this.datasets.length > 1) { - var dataArray, - dataIndex; - - for (var i = this.datasets.length - 1; i >= 0; i--) { - dataArray = this.datasets[i].points || this.datasets[i].bars || this.datasets[i].segments; - dataIndex = indexOf(dataArray, ChartElements[0]); - if (dataIndex !== -1){ - break; - } - } - var tooltipLabels = [], - tooltipColors = [], - medianPosition = (function(index) { - - // Get all the points at that particular index - var Elements = [], - dataCollection, - xPositions = [], - yPositions = [], - xMax, - yMax, - xMin, - yMin; - helpers.each(this.datasets, function(dataset){ - dataCollection = dataset.points || dataset.bars || dataset.segments; - if (dataCollection[dataIndex] && dataCollection[dataIndex].hasValue()){ - Elements.push(dataCollection[dataIndex]); - } - }); - - helpers.each(Elements, function(element) { - xPositions.push(element.x); - yPositions.push(element.y); - - - //Include any colour information about the element - tooltipLabels.push(helpers.template(this.options.multiTooltipTemplate, element)); - tooltipColors.push({ - fill: element._saved.fillColor || element.fillColor, - stroke: element._saved.strokeColor || element.strokeColor - }); - - }, this); - - yMin = min(yPositions); - yMax = max(yPositions); - - xMin = min(xPositions); - xMax = max(xPositions); - - return { - x: (xMin > this.chart.width/2) ? xMin : xMax, - y: (yMin + yMax)/2 - }; - }).call(this, dataIndex); - - new Chart.MultiTooltip({ - x: medianPosition.x, - y: medianPosition.y, - xPadding: this.options.tooltipXPadding, - yPadding: this.options.tooltipYPadding, - xOffset: this.options.tooltipXOffset, - fillColor: this.options.tooltipFillColor, - textColor: this.options.tooltipFontColor, - fontFamily: this.options.tooltipFontFamily, - fontStyle: this.options.tooltipFontStyle, - fontSize: this.options.tooltipFontSize, - titleTextColor: this.options.tooltipTitleFontColor, - titleFontFamily: this.options.tooltipTitleFontFamily, - titleFontStyle: this.options.tooltipTitleFontStyle, - titleFontSize: this.options.tooltipTitleFontSize, - cornerRadius: this.options.tooltipCornerRadius, - labels: tooltipLabels, - legendColors: tooltipColors, - legendColorBackground : this.options.multiTooltipKeyBackground, - title: template(this.options.tooltipTitleTemplate,ChartElements[0]), - chart: this.chart, - ctx: this.chart.ctx, - custom: this.options.customTooltips - }).draw(); - - } else { - each(ChartElements, function(Element) { - var tooltipPosition = Element.tooltipPosition(); - new Chart.Tooltip({ - x: Math.round(tooltipPosition.x), - y: Math.round(tooltipPosition.y), - xPadding: this.options.tooltipXPadding, - yPadding: this.options.tooltipYPadding, - fillColor: this.options.tooltipFillColor, - textColor: this.options.tooltipFontColor, - fontFamily: this.options.tooltipFontFamily, - fontStyle: this.options.tooltipFontStyle, - fontSize: this.options.tooltipFontSize, - caretHeight: this.options.tooltipCaretSize, - cornerRadius: this.options.tooltipCornerRadius, - text: template(this.options.tooltipTemplate, Element), - chart: this.chart, - custom: this.options.customTooltips - }).draw(); - }, this); - } - } - return this; - }, - toBase64Image : function(){ - return this.chart.canvas.toDataURL.apply(this.chart.canvas, arguments); - } - }); - - Chart.Type.extend = function(extensions){ - - var parent = this; - - var ChartType = function(){ - return parent.apply(this,arguments); - }; - - //Copy the prototype object of the this class - ChartType.prototype = clone(parent.prototype); - //Now overwrite some of the properties in the base class with the new extensions - extend(ChartType.prototype, extensions); - - ChartType.extend = Chart.Type.extend; - - if (extensions.name || parent.prototype.name){ - - var chartName = extensions.name || parent.prototype.name; - //Assign any potential default values of the new chart type - - //If none are defined, we'll use a clone of the chart type this is being extended from. - //I.e. if we extend a line chart, we'll use the defaults from the line chart if our new chart - //doesn't define some defaults of their own. - - var baseDefaults = (Chart.defaults[parent.prototype.name]) ? clone(Chart.defaults[parent.prototype.name]) : {}; - - Chart.defaults[chartName] = extend(baseDefaults,extensions.defaults); - - Chart.types[chartName] = ChartType; - - //Register this new chart type in the Chart prototype - Chart.prototype[chartName] = function(data,options){ - var config = merge(Chart.defaults.global, Chart.defaults[chartName], options || {}); - return new ChartType(data,config,this); - }; - } else{ - warn("Name not provided for this chart, so it hasn't been registered"); - } - return parent; - }; - - Chart.Element = function(configuration){ - extend(this,configuration); - this.initialize.apply(this,arguments); - this.save(); - }; - extend(Chart.Element.prototype,{ - initialize : function(){}, - restore : function(props){ - if (!props){ - extend(this,this._saved); - } else { - each(props,function(key){ - this[key] = this._saved[key]; - },this); - } - return this; - }, - save : function(){ - this._saved = clone(this); - delete this._saved._saved; - return this; - }, - update : function(newProps){ - each(newProps,function(value,key){ - this._saved[key] = this[key]; - this[key] = value; - },this); - return this; - }, - transition : function(props,ease){ - each(props,function(value,key){ - this[key] = ((value - this._saved[key]) * ease) + this._saved[key]; - },this); - return this; - }, - tooltipPosition : function(){ - return { - x : this.x, - y : this.y - }; - }, - hasValue: function(){ - return isNumber(this.value); - } - }); - - Chart.Element.extend = inherits; - - - Chart.Point = Chart.Element.extend({ - display: true, - inRange: function(chartX,chartY){ - var hitDetectionRange = this.hitDetectionRadius + this.radius; - return ((Math.pow(chartX-this.x, 2)+Math.pow(chartY-this.y, 2)) < Math.pow(hitDetectionRange,2)); - }, - draw : function(){ - if (this.display){ - var ctx = this.ctx; - ctx.beginPath(); - - ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2); - ctx.closePath(); - - ctx.strokeStyle = this.strokeColor; - ctx.lineWidth = this.strokeWidth; - - ctx.fillStyle = this.fillColor; - - ctx.fill(); - ctx.stroke(); - } - - - //Quick debug for bezier curve splining - //Highlights control points and the line between them. - //Handy for dev - stripped in the min version. - - // ctx.save(); - // ctx.fillStyle = "black"; - // ctx.strokeStyle = "black" - // ctx.beginPath(); - // ctx.arc(this.controlPoints.inner.x,this.controlPoints.inner.y, 2, 0, Math.PI*2); - // ctx.fill(); - - // ctx.beginPath(); - // ctx.arc(this.controlPoints.outer.x,this.controlPoints.outer.y, 2, 0, Math.PI*2); - // ctx.fill(); - - // ctx.moveTo(this.controlPoints.inner.x,this.controlPoints.inner.y); - // ctx.lineTo(this.x, this.y); - // ctx.lineTo(this.controlPoints.outer.x,this.controlPoints.outer.y); - // ctx.stroke(); - - // ctx.restore(); - - - - } - }); - - Chart.Arc = Chart.Element.extend({ - inRange : function(chartX,chartY){ - - var pointRelativePosition = helpers.getAngleFromPoint(this, { - x: chartX, - y: chartY - }); - - //Check if within the range of the open/close angle - var betweenAngles = (pointRelativePosition.angle >= this.startAngle && pointRelativePosition.angle <= this.endAngle), - withinRadius = (pointRelativePosition.distance >= this.innerRadius && pointRelativePosition.distance <= this.outerRadius); - - return (betweenAngles && withinRadius); - //Ensure within the outside of the arc centre, but inside arc outer - }, - tooltipPosition : function(){ - var centreAngle = this.startAngle + ((this.endAngle - this.startAngle) / 2), - rangeFromCentre = (this.outerRadius - this.innerRadius) / 2 + this.innerRadius; - return { - x : this.x + (Math.cos(centreAngle) * rangeFromCentre), - y : this.y + (Math.sin(centreAngle) * rangeFromCentre) - }; - }, - draw : function(animationPercent){ - - var easingDecimal = animationPercent || 1; - - var ctx = this.ctx; - - ctx.beginPath(); - - ctx.arc(this.x, this.y, this.outerRadius < 0 ? 0 : this.outerRadius, this.startAngle, this.endAngle); - - ctx.arc(this.x, this.y, this.innerRadius < 0 ? 0 : this.innerRadius, this.endAngle, this.startAngle, true); - - ctx.closePath(); - ctx.strokeStyle = this.strokeColor; - ctx.lineWidth = this.strokeWidth; - - ctx.fillStyle = this.fillColor; - - ctx.fill(); - ctx.lineJoin = 'bevel'; - - if (this.showStroke){ - ctx.stroke(); - } - } - }); - - Chart.Rectangle = Chart.Element.extend({ - draw : function(){ - var ctx = this.ctx, - halfWidth = this.width/2, - leftX = this.x - halfWidth, - rightX = this.x + halfWidth, - top = this.base - (this.base - this.y), - halfStroke = this.strokeWidth / 2; - - // Canvas doesn't allow us to stroke inside the width so we can - // adjust the sizes to fit if we're setting a stroke on the line - if (this.showStroke){ - leftX += halfStroke; - rightX -= halfStroke; - top += halfStroke; - } - - ctx.beginPath(); - - ctx.fillStyle = this.fillColor; - ctx.strokeStyle = this.strokeColor; - ctx.lineWidth = this.strokeWidth; - - // It'd be nice to keep this class totally generic to any rectangle - // and simply specify which border to miss out. - ctx.moveTo(leftX, this.base); - ctx.lineTo(leftX, top); - ctx.lineTo(rightX, top); - ctx.lineTo(rightX, this.base); - ctx.fill(); - if (this.showStroke){ - ctx.stroke(); - } - }, - height : function(){ - return this.base - this.y; - }, - inRange : function(chartX,chartY){ - return (chartX >= this.x - this.width/2 && chartX <= this.x + this.width/2) && (chartY >= this.y && chartY <= this.base); - } - }); - - Chart.Animation = Chart.Element.extend({ - currentStep: null, // the current animation step - numSteps: 60, // default number of steps - easing: "", // the easing to use for this animation - render: null, // render function used by the animation service - - onAnimationProgress: null, // user specified callback to fire on each step of the animation - onAnimationComplete: null, // user specified callback to fire when the animation finishes - }); - - Chart.Tooltip = Chart.Element.extend({ - draw : function(){ - - var ctx = this.chart.ctx; - - ctx.font = fontString(this.fontSize,this.fontStyle,this.fontFamily); - - this.xAlign = "center"; - this.yAlign = "above"; - - //Distance between the actual element.y position and the start of the tooltip caret - var caretPadding = this.caretPadding = 2; - - var tooltipWidth = ctx.measureText(this.text).width + 2*this.xPadding, - tooltipRectHeight = this.fontSize + 2*this.yPadding, - tooltipHeight = tooltipRectHeight + this.caretHeight + caretPadding; - - if (this.x + tooltipWidth/2 >this.chart.width){ - this.xAlign = "left"; - } else if (this.x - tooltipWidth/2 < 0){ - this.xAlign = "right"; - } - - if (this.y - tooltipHeight < 0){ - this.yAlign = "below"; - } - - - var tooltipX = this.x - tooltipWidth/2, - tooltipY = this.y - tooltipHeight; - - ctx.fillStyle = this.fillColor; - - // Custom Tooltips - if(this.custom){ - this.custom(this); - } - else{ - switch(this.yAlign) - { - case "above": - //Draw a caret above the x/y - ctx.beginPath(); - ctx.moveTo(this.x,this.y - caretPadding); - ctx.lineTo(this.x + this.caretHeight, this.y - (caretPadding + this.caretHeight)); - ctx.lineTo(this.x - this.caretHeight, this.y - (caretPadding + this.caretHeight)); - ctx.closePath(); - ctx.fill(); - break; - case "below": - tooltipY = this.y + caretPadding + this.caretHeight; - //Draw a caret below the x/y - ctx.beginPath(); - ctx.moveTo(this.x, this.y + caretPadding); - ctx.lineTo(this.x + this.caretHeight, this.y + caretPadding + this.caretHeight); - ctx.lineTo(this.x - this.caretHeight, this.y + caretPadding + this.caretHeight); - ctx.closePath(); - ctx.fill(); - break; - } - - switch(this.xAlign) - { - case "left": - tooltipX = this.x - tooltipWidth + (this.cornerRadius + this.caretHeight); - break; - case "right": - tooltipX = this.x - (this.cornerRadius + this.caretHeight); - break; - } - - drawRoundedRectangle(ctx,tooltipX,tooltipY,tooltipWidth,tooltipRectHeight,this.cornerRadius); - - ctx.fill(); - - ctx.fillStyle = this.textColor; - ctx.textAlign = "center"; - ctx.textBaseline = "middle"; - ctx.fillText(this.text, tooltipX + tooltipWidth/2, tooltipY + tooltipRectHeight/2); - } - } - }); - - Chart.MultiTooltip = Chart.Element.extend({ - initialize : function(){ - this.font = fontString(this.fontSize,this.fontStyle,this.fontFamily); - - this.titleFont = fontString(this.titleFontSize,this.titleFontStyle,this.titleFontFamily); - - this.height = (this.labels.length * this.fontSize) + ((this.labels.length-1) * (this.fontSize/2)) + (this.yPadding*2) + this.titleFontSize *1.5; - - this.ctx.font = this.titleFont; - - var titleWidth = this.ctx.measureText(this.title).width, - //Label has a legend square as well so account for this. - labelWidth = longestText(this.ctx,this.font,this.labels) + this.fontSize + 3, - longestTextWidth = max([labelWidth,titleWidth]); - - this.width = longestTextWidth + (this.xPadding*2); - - - var halfHeight = this.height/2; - - //Check to ensure the height will fit on the canvas - if (this.y - halfHeight < 0 ){ - this.y = halfHeight; - } else if (this.y + halfHeight > this.chart.height){ - this.y = this.chart.height - halfHeight; - } - - //Decide whether to align left or right based on position on canvas - if (this.x > this.chart.width/2){ - this.x -= this.xOffset + this.width; - } else { - this.x += this.xOffset; - } - - - }, - getLineHeight : function(index){ - var baseLineHeight = this.y - (this.height/2) + this.yPadding, - afterTitleIndex = index-1; - - //If the index is zero, we're getting the title - if (index === 0){ - return baseLineHeight + this.titleFontSize/2; - } else{ - return baseLineHeight + ((this.fontSize*1.5*afterTitleIndex) + this.fontSize/2) + this.titleFontSize * 1.5; - } - - }, - draw : function(){ - // Custom Tooltips - if(this.custom){ - this.custom(this); - } - else{ - drawRoundedRectangle(this.ctx,this.x,this.y - this.height/2,this.width,this.height,this.cornerRadius); - var ctx = this.ctx; - ctx.fillStyle = this.fillColor; - ctx.fill(); - ctx.closePath(); - - ctx.textAlign = "left"; - ctx.textBaseline = "middle"; - ctx.fillStyle = this.titleTextColor; - ctx.font = this.titleFont; - - ctx.fillText(this.title,this.x + this.xPadding, this.getLineHeight(0)); - - ctx.font = this.font; - helpers.each(this.labels,function(label,index){ - ctx.fillStyle = this.textColor; - ctx.fillText(label,this.x + this.xPadding + this.fontSize + 3, this.getLineHeight(index + 1)); - - //A bit gnarly, but clearing this rectangle breaks when using explorercanvas (clears whole canvas) - //ctx.clearRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); - //Instead we'll make a white filled block to put the legendColour palette over. - - ctx.fillStyle = this.legendColorBackground; - ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); - - ctx.fillStyle = this.legendColors[index].fill; - ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); - - - },this); - } - } - }); - - Chart.Scale = Chart.Element.extend({ - initialize : function(){ - this.fit(); - }, - buildYLabels : function(){ - this.yLabels = []; - - var stepDecimalPlaces = getDecimalPlaces(this.stepValue); - - for (var i=0; i<=this.steps; i++){ - this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)})); - } - this.yLabelWidth = (this.display && this.showLabels) ? longestText(this.ctx,this.font,this.yLabels) + 10 : 0; - }, - addXLabel : function(label){ - this.xLabels.push(label); - this.valuesCount++; - this.fit(); - }, - removeXLabel : function(){ - this.xLabels.shift(); - this.valuesCount--; - this.fit(); - }, - // Fitting loop to rotate x Labels and figure out what fits there, and also calculate how many Y steps to use - fit: function(){ - // First we need the width of the yLabels, assuming the xLabels aren't rotated - - // To do that we need the base line at the top and base of the chart, assuming there is no x label rotation - this.startPoint = (this.display) ? this.fontSize : 0; - this.endPoint = (this.display) ? this.height - (this.fontSize * 1.5) - 5 : this.height; // -5 to pad labels - - // Apply padding settings to the start and end point. - this.startPoint += this.padding; - this.endPoint -= this.padding; - - // Cache the starting endpoint, excluding the space for x labels - var cachedEndPoint = this.endPoint; - - // Cache the starting height, so can determine if we need to recalculate the scale yAxis - var cachedHeight = this.endPoint - this.startPoint, - cachedYLabelWidth; - - // Build the current yLabels so we have an idea of what size they'll be to start - /* - * This sets what is returned from calculateScaleRange as static properties of this class: - * - this.steps; - this.stepValue; - this.min; - this.max; - * - */ - this.calculateYRange(cachedHeight); - - // With these properties set we can now build the array of yLabels - // and also the width of the largest yLabel - this.buildYLabels(); - - this.calculateXLabelRotation(); - - while((cachedHeight > this.endPoint - this.startPoint)){ - cachedHeight = this.endPoint - this.startPoint; - cachedYLabelWidth = this.yLabelWidth; - - this.calculateYRange(cachedHeight); - this.buildYLabels(); - - // Only go through the xLabel loop again if the yLabel width has changed - if (cachedYLabelWidth < this.yLabelWidth){ - this.endPoint = cachedEndPoint; - this.calculateXLabelRotation(); - } - } - - }, - calculateXLabelRotation : function(){ - //Get the width of each grid by calculating the difference - //between x offsets between 0 and 1. - - this.ctx.font = this.font; - - var firstWidth = this.ctx.measureText(this.xLabels[0]).width, - lastWidth = this.ctx.measureText(this.xLabels[this.xLabels.length - 1]).width, - firstRotated, - lastRotated; - - - this.xScalePaddingRight = lastWidth/2 + 3; - this.xScalePaddingLeft = (firstWidth/2 > this.yLabelWidth) ? firstWidth/2 : this.yLabelWidth; - - this.xLabelRotation = 0; - if (this.display){ - var originalLabelWidth = longestText(this.ctx,this.font,this.xLabels), - cosRotation, - firstRotatedWidth; - this.xLabelWidth = originalLabelWidth; - //Allow 3 pixels x2 padding either side for label readability - var xGridWidth = Math.floor(this.calculateX(1) - this.calculateX(0)) - 6; - - //Max label rotate should be 90 - also act as a loop counter - while ((this.xLabelWidth > xGridWidth && this.xLabelRotation === 0) || (this.xLabelWidth > xGridWidth && this.xLabelRotation <= 90 && this.xLabelRotation > 0)){ - cosRotation = Math.cos(toRadians(this.xLabelRotation)); - - firstRotated = cosRotation * firstWidth; - lastRotated = cosRotation * lastWidth; - - // We're right aligning the text now. - if (firstRotated + this.fontSize / 2 > this.yLabelWidth){ - this.xScalePaddingLeft = firstRotated + this.fontSize / 2; - } - this.xScalePaddingRight = this.fontSize/2; - - - this.xLabelRotation++; - this.xLabelWidth = cosRotation * originalLabelWidth; - - } - if (this.xLabelRotation > 0){ - this.endPoint -= Math.sin(toRadians(this.xLabelRotation))*originalLabelWidth + 3; - } - } - else{ - this.xLabelWidth = 0; - this.xScalePaddingRight = this.padding; - this.xScalePaddingLeft = this.padding; - } - - }, - // Needs to be overidden in each Chart type - // Otherwise we need to pass all the data into the scale class - calculateYRange: noop, - drawingArea: function(){ - return this.startPoint - this.endPoint; - }, - calculateY : function(value){ - var scalingFactor = this.drawingArea() / (this.min - this.max); - return this.endPoint - (scalingFactor * (value - this.min)); - }, - calculateX : function(index){ - var isRotated = (this.xLabelRotation > 0), - // innerWidth = (this.offsetGridLines) ? this.width - offsetLeft - this.padding : this.width - (offsetLeft + halfLabelWidth * 2) - this.padding, - innerWidth = this.width - (this.xScalePaddingLeft + this.xScalePaddingRight), - valueWidth = innerWidth/Math.max((this.valuesCount - ((this.offsetGridLines) ? 0 : 1)), 1), - valueOffset = (valueWidth * index) + this.xScalePaddingLeft; - - if (this.offsetGridLines){ - valueOffset += (valueWidth/2); - } - - return Math.round(valueOffset); - }, - update : function(newProps){ - helpers.extend(this, newProps); - this.fit(); - }, - draw : function(){ - var ctx = this.ctx, - yLabelGap = (this.endPoint - this.startPoint) / this.steps, - xStart = Math.round(this.xScalePaddingLeft); - if (this.display){ - ctx.fillStyle = this.textColor; - ctx.font = this.font; - each(this.yLabels,function(labelString,index){ - var yLabelCenter = this.endPoint - (yLabelGap * index), - linePositionY = Math.round(yLabelCenter), - drawHorizontalLine = this.showHorizontalLines; - - ctx.textAlign = "right"; - ctx.textBaseline = "middle"; - if (this.showLabels){ - ctx.fillText(labelString,xStart - 10,yLabelCenter); - } - - // This is X axis, so draw it - if (index === 0 && !drawHorizontalLine){ - drawHorizontalLine = true; - } - - if (drawHorizontalLine){ - ctx.beginPath(); - } - - if (index > 0){ - // This is a grid line in the centre, so drop that - ctx.lineWidth = this.gridLineWidth; - ctx.strokeStyle = this.gridLineColor; - } else { - // This is the first line on the scale - ctx.lineWidth = this.lineWidth; - ctx.strokeStyle = this.lineColor; - } - - linePositionY += helpers.aliasPixel(ctx.lineWidth); - - if(drawHorizontalLine){ - ctx.moveTo(xStart, linePositionY); - ctx.lineTo(this.width, linePositionY); - ctx.stroke(); - ctx.closePath(); - } - - ctx.lineWidth = this.lineWidth; - ctx.strokeStyle = this.lineColor; - ctx.beginPath(); - ctx.moveTo(xStart - 5, linePositionY); - ctx.lineTo(xStart, linePositionY); - ctx.stroke(); - ctx.closePath(); - - },this); - - each(this.xLabels,function(label,index){ - var xPos = this.calculateX(index) + aliasPixel(this.lineWidth), - // Check to see if line/bar here and decide where to place the line - linePos = this.calculateX(index - (this.offsetGridLines ? 0.5 : 0)) + aliasPixel(this.lineWidth), - isRotated = (this.xLabelRotation > 0), - drawVerticalLine = this.showVerticalLines; - - // This is Y axis, so draw it - if (index === 0 && !drawVerticalLine){ - drawVerticalLine = true; - } - - if (drawVerticalLine){ - ctx.beginPath(); - } - - if (index > 0){ - // This is a grid line in the centre, so drop that - ctx.lineWidth = this.gridLineWidth; - ctx.strokeStyle = this.gridLineColor; - } else { - // This is the first line on the scale - ctx.lineWidth = this.lineWidth; - ctx.strokeStyle = this.lineColor; - } - - if (drawVerticalLine){ - ctx.moveTo(linePos,this.endPoint); - ctx.lineTo(linePos,this.startPoint - 3); - ctx.stroke(); - ctx.closePath(); - } - - - ctx.lineWidth = this.lineWidth; - ctx.strokeStyle = this.lineColor; - - - // Small lines at the bottom of the base grid line - ctx.beginPath(); - ctx.moveTo(linePos,this.endPoint); - ctx.lineTo(linePos,this.endPoint + 5); - ctx.stroke(); - ctx.closePath(); - - ctx.save(); - ctx.translate(xPos,(isRotated) ? this.endPoint + 12 : this.endPoint + 8); - ctx.rotate(toRadians(this.xLabelRotation)*-1); - ctx.font = this.font; - ctx.textAlign = (isRotated) ? "right" : "center"; - ctx.textBaseline = (isRotated) ? "middle" : "top"; - ctx.fillText(label, 0, 0); - ctx.restore(); - },this); - - } - } - - }); - - Chart.RadialScale = Chart.Element.extend({ - initialize: function(){ - this.size = min([this.height, this.width]); - this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2); - }, - calculateCenterOffset: function(value){ - // Take into account half font size + the yPadding of the top value - var scalingFactor = this.drawingArea / (this.max - this.min); - - return (value - this.min) * scalingFactor; - }, - update : function(){ - if (!this.lineArc){ - this.setScaleSize(); - } else { - this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2); - } - this.buildYLabels(); - }, - buildYLabels: function(){ - this.yLabels = []; - - var stepDecimalPlaces = getDecimalPlaces(this.stepValue); - - for (var i=0; i<=this.steps; i++){ - this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)})); - } - }, - getCircumference : function(){ - return ((Math.PI*2) / this.valuesCount); - }, - setScaleSize: function(){ - /* - * Right, this is really confusing and there is a lot of maths going on here - * The gist of the problem is here: https://gist.github.com/nnnick/696cc9c55f4b0beb8fe9 - * - * Reaction: https://dl.dropboxusercontent.com/u/34601363/toomuchscience.gif - * - * Solution: - * - * We assume the radius of the polygon is half the size of the canvas at first - * at each index we check if the text overlaps. - * - * Where it does, we store that angle and that index. - * - * After finding the largest index and angle we calculate how much we need to remove - * from the shape radius to move the point inwards by that x. - * - * We average the left and right distances to get the maximum shape radius that can fit in the box - * along with labels. - * - * Once we have that, we can find the centre point for the chart, by taking the x text protrusion - * on each side, removing that from the size, halving it and adding the left x protrusion width. - * - * This will mean we have a shape fitted to the canvas, as large as it can be with the labels - * and position it in the most space efficient manner - * - * https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif - */ - - - // Get maximum radius of the polygon. Either half the height (minus the text width) or half the width. - // Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points - var largestPossibleRadius = min([(this.height/2 - this.pointLabelFontSize - 5), this.width/2]), - pointPosition, - i, - textWidth, - halfTextWidth, - furthestRight = this.width, - furthestRightIndex, - furthestRightAngle, - furthestLeft = 0, - furthestLeftIndex, - furthestLeftAngle, - xProtrusionLeft, - xProtrusionRight, - radiusReductionRight, - radiusReductionLeft, - maxWidthRadius; - this.ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily); - for (i=0;i furthestRight) { - furthestRight = pointPosition.x + halfTextWidth; - furthestRightIndex = i; - } - if (pointPosition.x - halfTextWidth < furthestLeft) { - furthestLeft = pointPosition.x - halfTextWidth; - furthestLeftIndex = i; - } - } - else if (i < this.valuesCount/2) { - // Less than half the values means we'll left align the text - if (pointPosition.x + textWidth > furthestRight) { - furthestRight = pointPosition.x + textWidth; - furthestRightIndex = i; - } - } - else if (i > this.valuesCount/2){ - // More than half the values means we'll right align the text - if (pointPosition.x - textWidth < furthestLeft) { - furthestLeft = pointPosition.x - textWidth; - furthestLeftIndex = i; - } - } - } - - xProtrusionLeft = furthestLeft; - - xProtrusionRight = Math.ceil(furthestRight - this.width); - - furthestRightAngle = this.getIndexAngle(furthestRightIndex); - - furthestLeftAngle = this.getIndexAngle(furthestLeftIndex); - - radiusReductionRight = xProtrusionRight / Math.sin(furthestRightAngle + Math.PI/2); - - radiusReductionLeft = xProtrusionLeft / Math.sin(furthestLeftAngle + Math.PI/2); - - // Ensure we actually need to reduce the size of the chart - radiusReductionRight = (isNumber(radiusReductionRight)) ? radiusReductionRight : 0; - radiusReductionLeft = (isNumber(radiusReductionLeft)) ? radiusReductionLeft : 0; - - this.drawingArea = largestPossibleRadius - (radiusReductionLeft + radiusReductionRight)/2; - - //this.drawingArea = min([maxWidthRadius, (this.height - (2 * (this.pointLabelFontSize + 5)))/2]) - this.setCenterPoint(radiusReductionLeft, radiusReductionRight); - - }, - setCenterPoint: function(leftMovement, rightMovement){ - - var maxRight = this.width - rightMovement - this.drawingArea, - maxLeft = leftMovement + this.drawingArea; - - this.xCenter = (maxLeft + maxRight)/2; - // Always vertically in the centre as the text height doesn't change - this.yCenter = (this.height/2); - }, - - getIndexAngle : function(index){ - var angleMultiplier = (Math.PI * 2) / this.valuesCount; - // Start from the top instead of right, so remove a quarter of the circle - - return index * angleMultiplier - (Math.PI/2); - }, - getPointPosition : function(index, distanceFromCenter){ - var thisAngle = this.getIndexAngle(index); - return { - x : (Math.cos(thisAngle) * distanceFromCenter) + this.xCenter, - y : (Math.sin(thisAngle) * distanceFromCenter) + this.yCenter - }; - }, - draw: function(){ - if (this.display){ - var ctx = this.ctx; - each(this.yLabels, function(label, index){ - // Don't draw a centre value - if (index > 0){ - var yCenterOffset = index * (this.drawingArea/this.steps), - yHeight = this.yCenter - yCenterOffset, - pointPosition; - - // Draw circular lines around the scale - if (this.lineWidth > 0){ - ctx.strokeStyle = this.lineColor; - ctx.lineWidth = this.lineWidth; - - if(this.lineArc){ - ctx.beginPath(); - ctx.arc(this.xCenter, this.yCenter, yCenterOffset, 0, Math.PI*2); - ctx.closePath(); - ctx.stroke(); - } else{ - ctx.beginPath(); - for (var i=0;i= 0; i--) { - var centerOffset = null, outerPosition = null; - - if (this.angleLineWidth > 0){ - centerOffset = this.calculateCenterOffset(this.max); - outerPosition = this.getPointPosition(i, centerOffset); - ctx.beginPath(); - ctx.moveTo(this.xCenter, this.yCenter); - ctx.lineTo(outerPosition.x, outerPosition.y); - ctx.stroke(); - ctx.closePath(); - } - - if (this.backgroundColors && this.backgroundColors.length == this.valuesCount) { - if (centerOffset == null) - centerOffset = this.calculateCenterOffset(this.max); - - if (outerPosition == null) - outerPosition = this.getPointPosition(i, centerOffset); - - var previousOuterPosition = this.getPointPosition(i === 0 ? this.valuesCount - 1 : i - 1, centerOffset); - var nextOuterPosition = this.getPointPosition(i === this.valuesCount - 1 ? 0 : i + 1, centerOffset); - - var previousOuterHalfway = { x: (previousOuterPosition.x + outerPosition.x) / 2, y: (previousOuterPosition.y + outerPosition.y) / 2 }; - var nextOuterHalfway = { x: (outerPosition.x + nextOuterPosition.x) / 2, y: (outerPosition.y + nextOuterPosition.y) / 2 }; - - ctx.beginPath(); - ctx.moveTo(this.xCenter, this.yCenter); - ctx.lineTo(previousOuterHalfway.x, previousOuterHalfway.y); - ctx.lineTo(outerPosition.x, outerPosition.y); - ctx.lineTo(nextOuterHalfway.x, nextOuterHalfway.y); - ctx.fillStyle = this.backgroundColors[i]; - ctx.fill(); - ctx.closePath(); - } - // Extra 3px out for some label spacing - var pointLabelPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max) + 5); - ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily); - ctx.fillStyle = this.pointLabelFontColor; - - var labelsCount = this.labels.length, - halfLabelsCount = this.labels.length/2, - quarterLabelsCount = halfLabelsCount/2, - upperHalf = (i < quarterLabelsCount || i > labelsCount - quarterLabelsCount), - exactQuarter = (i === quarterLabelsCount || i === labelsCount - quarterLabelsCount); - if (i === 0){ - ctx.textAlign = 'center'; - } else if(i === halfLabelsCount){ - ctx.textAlign = 'center'; - } else if (i < halfLabelsCount){ - ctx.textAlign = 'left'; - } else { - ctx.textAlign = 'right'; - } - - // Set the correct text baseline based on outer positioning - if (exactQuarter){ - ctx.textBaseline = 'middle'; - } else if (upperHalf){ - ctx.textBaseline = 'bottom'; - } else { - ctx.textBaseline = 'top'; - } - - ctx.fillText(this.labels[i], pointLabelPosition.x, pointLabelPosition.y); - } - } - } - } - }); - - Chart.animationService = { - frameDuration: 17, - animations: [], - dropFrames: 0, - addAnimation: function(chartInstance, animationObject) { - for (var index = 0; index < this.animations.length; ++ index){ - if (this.animations[index].chartInstance === chartInstance){ - // replacing an in progress animation - this.animations[index].animationObject = animationObject; - return; - } - } - - this.animations.push({ - chartInstance: chartInstance, - animationObject: animationObject - }); - - // If there are no animations queued, manually kickstart a digest, for lack of a better word - if (this.animations.length == 1) { - helpers.requestAnimFrame.call(window, this.digestWrapper); - } - }, - // Cancel the animation for a given chart instance - cancelAnimation: function(chartInstance) { - var index = helpers.findNextWhere(this.animations, function(animationWrapper) { - return animationWrapper.chartInstance === chartInstance; - }); - - if (index) - { - this.animations.splice(index, 1); - } - }, - // calls startDigest with the proper context - digestWrapper: function() { - Chart.animationService.startDigest.call(Chart.animationService); - }, - startDigest: function() { - - var startTime = Date.now(); - var framesToDrop = 0; - - if(this.dropFrames > 1){ - framesToDrop = Math.floor(this.dropFrames); - this.dropFrames -= framesToDrop; - } - - for (var i = 0; i < this.animations.length; i++) { - - if (this.animations[i].animationObject.currentStep === null){ - this.animations[i].animationObject.currentStep = 0; - } - - this.animations[i].animationObject.currentStep += 1 + framesToDrop; - if(this.animations[i].animationObject.currentStep > this.animations[i].animationObject.numSteps){ - this.animations[i].animationObject.currentStep = this.animations[i].animationObject.numSteps; - } - - this.animations[i].animationObject.render(this.animations[i].chartInstance, this.animations[i].animationObject); - - // Check if executed the last frame. - if (this.animations[i].animationObject.currentStep == this.animations[i].animationObject.numSteps){ - // Call onAnimationComplete - this.animations[i].animationObject.onAnimationComplete.call(this.animations[i].chartInstance); - // Remove the animation. - this.animations.splice(i, 1); - // Keep the index in place to offset the splice - i--; - } - } - - var endTime = Date.now(); - var delay = endTime - startTime - this.frameDuration; - var frameDelay = delay / this.frameDuration; - - if(frameDelay > 1){ - this.dropFrames += frameDelay; - } - - // Do we have more stuff to animate? - if (this.animations.length > 0){ - helpers.requestAnimFrame.call(window, this.digestWrapper); - } - } - }; - - // Attach global event to resize each chart instance when the browser resizes - helpers.addEvent(window, "resize", (function(){ - // Basic debounce of resize function so it doesn't hurt performance when resizing browser. - var timeout; - return function(){ - clearTimeout(timeout); - timeout = setTimeout(function(){ - each(Chart.instances,function(instance){ - // If the responsive flag is set in the chart instance config - // Cascade the resize event down to the chart. - if (instance.options.responsive){ - instance.resize(instance.render, true); - } - }); - }, 50); - }; - })()); - - - if (amd) { - define(function(){ - return Chart; - }); - } else if (typeof module === 'object' && module.exports) { - module.exports = Chart; - } - - root.Chart = Chart; - - Chart.noConflict = function(){ - root.Chart = previous; - return Chart; - }; - -}).call(this); diff --git a/src/Chart.Doughnut.js b/src/Chart.Doughnut.js deleted file mode 100644 index e63bf9e502d..00000000000 --- a/src/Chart.Doughnut.js +++ /dev/null @@ -1,190 +0,0 @@ -(function(){ - "use strict"; - - var root = this, - Chart = root.Chart, - //Cache a local reference to Chart.helpers - helpers = Chart.helpers; - - var defaultConfig = { - //Boolean - Whether we should show a stroke on each segment - segmentShowStroke : true, - - //String - The colour of each segment stroke - segmentStrokeColor : "#fff", - - //Number - The width of each segment stroke - segmentStrokeWidth : 2, - - //The percentage of the chart that we cut out of the middle. - percentageInnerCutout : 50, - - //Number - Amount of animation steps - animationSteps : 100, - - //String - Animation easing effect - animationEasing : "easeOutBounce", - - //Boolean - Whether we animate the rotation of the Doughnut - animateRotate : true, - - //Boolean - Whether we animate scaling the Doughnut from the centre - animateScale : false, - - //String - A legend template - legendTemplate : "
      -legend\"><% for (var i=0; i
    • \"><%if(segments[i].label){%><%=segments[i].label%><%}%>
    • <%}%>
    " - - }; - - Chart.Type.extend({ - //Passing in a name registers this chart in the Chart namespace - name: "Doughnut", - //Providing a defaults will also register the deafults in the chart namespace - defaults : defaultConfig, - //Initialize is fired when the chart is initialized - Data is passed in as a parameter - //Config is automatically merged by the core of Chart.js, and is available at this.options - initialize: function(data){ - - //Declare segments as a static property to prevent inheriting across the Chart type prototype - this.segments = []; - this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2; - - this.SegmentArc = Chart.Arc.extend({ - ctx : this.chart.ctx, - x : this.chart.width/2, - y : this.chart.height/2 - }); - - //Set up tooltip events on the chart - if (this.options.showTooltips){ - helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ - var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : []; - - helpers.each(this.segments,function(segment){ - segment.restore(["fillColor"]); - }); - helpers.each(activeSegments,function(activeSegment){ - activeSegment.fillColor = activeSegment.highlightColor; - }); - this.showTooltip(activeSegments); - }); - } - this.calculateTotal(data); - - helpers.each(data,function(datapoint, index){ - if (!datapoint.color) { - datapoint.color = 'hsl(' + (360 * index / data.length) + ', 100%, 50%)'; - } - this.addData(datapoint, index, true); - },this); - - this.render(); - }, - getSegmentsAtEvent : function(e){ - var segmentsArray = []; - - var location = helpers.getRelativePosition(e); - - helpers.each(this.segments,function(segment){ - if (segment.inRange(location.x,location.y)) segmentsArray.push(segment); - },this); - return segmentsArray; - }, - addData : function(segment, atIndex, silent){ - var index = atIndex !== undefined ? atIndex : this.segments.length; - this.segments.splice(index, 0, new this.SegmentArc({ - value : segment.value, - outerRadius : (this.options.animateScale) ? 0 : this.outerRadius, - innerRadius : (this.options.animateScale) ? 0 : (this.outerRadius/100) * this.options.percentageInnerCutout, - fillColor : segment.color, - highlightColor : segment.highlight || segment.color, - showStroke : this.options.segmentShowStroke, - strokeWidth : this.options.segmentStrokeWidth, - strokeColor : this.options.segmentStrokeColor, - startAngle : Math.PI * 1.5, - circumference : (this.options.animateRotate) ? 0 : this.calculateCircumference(segment.value), - label : segment.label - })); - if (!silent){ - this.reflow(); - this.update(); - } - }, - calculateCircumference : function(value) { - if ( this.total > 0 ) { - return (Math.PI*2)*(value / this.total); - } else { - return 0; - } - }, - calculateTotal : function(data){ - this.total = 0; - helpers.each(data,function(segment){ - this.total += Math.abs(segment.value); - },this); - }, - update : function(){ - this.calculateTotal(this.segments); - - // Reset any highlight colours before updating. - helpers.each(this.activeElements, function(activeElement){ - activeElement.restore(['fillColor']); - }); - - helpers.each(this.segments,function(segment){ - segment.save(); - }); - this.render(); - }, - - removeData: function(atIndex){ - var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1; - this.segments.splice(indexToDelete, 1); - this.reflow(); - this.update(); - }, - - reflow : function(){ - helpers.extend(this.SegmentArc.prototype,{ - x : this.chart.width/2, - y : this.chart.height/2 - }); - this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2; - helpers.each(this.segments, function(segment){ - segment.update({ - outerRadius : this.outerRadius, - innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout - }); - }, this); - }, - draw : function(easeDecimal){ - var animDecimal = (easeDecimal) ? easeDecimal : 1; - this.clear(); - helpers.each(this.segments,function(segment,index){ - segment.transition({ - circumference : this.calculateCircumference(segment.value), - outerRadius : this.outerRadius, - innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout - },animDecimal); - - segment.endAngle = segment.startAngle + segment.circumference; - - segment.draw(); - if (index === 0){ - segment.startAngle = Math.PI * 1.5; - } - //Check to see if it's the last segment, if not get the next and update the start angle - if (index < this.segments.length-1){ - this.segments[index+1].startAngle = segment.endAngle; - } - },this); - - } - }); - - Chart.types.Doughnut.extend({ - name : "Pie", - defaults : helpers.merge(defaultConfig,{percentageInnerCutout : 0}) - }); - -}).call(this); diff --git a/src/Chart.Line.js b/src/Chart.Line.js deleted file mode 100644 index fdf1b0169b7..00000000000 --- a/src/Chart.Line.js +++ /dev/null @@ -1,383 +0,0 @@ -(function(){ - "use strict"; - - var root = this, - Chart = root.Chart, - helpers = Chart.helpers; - - var defaultConfig = { - - ///Boolean - Whether grid lines are shown across the chart - scaleShowGridLines : true, - - //String - Colour of the grid lines - scaleGridLineColor : "rgba(0,0,0,.05)", - - //Number - Width of the grid lines - scaleGridLineWidth : 1, - - //Boolean - Whether to show horizontal lines (except X axis) - scaleShowHorizontalLines: true, - - //Boolean - Whether to show vertical lines (except Y axis) - scaleShowVerticalLines: true, - - //Boolean - Whether the line is curved between points - bezierCurve : true, - - //Number - Tension of the bezier curve between points - bezierCurveTension : 0.4, - - //Boolean - Whether to show a dot for each point - pointDot : true, - - //Number - Radius of each point dot in pixels - pointDotRadius : 4, - - //Number - Pixel width of point dot stroke - pointDotStrokeWidth : 1, - - //Number - amount extra to add to the radius to cater for hit detection outside the drawn point - pointHitDetectionRadius : 20, - - //Boolean - Whether to show a stroke for datasets - datasetStroke : true, - - //Number - Pixel width of dataset stroke - datasetStrokeWidth : 2, - - //Boolean - Whether to fill the dataset with a colour - datasetFill : true, - - //String - A legend template - legendTemplate : "
      -legend\"><% for (var i=0; i
    • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
    • <%}%>
    ", - - //Boolean - Whether to horizontally center the label and point dot inside the grid - offsetGridLines : false - - }; - - - Chart.Type.extend({ - name: "Line", - defaults : defaultConfig, - initialize: function(data){ - //Declare the extension of the default point, to cater for the options passed in to the constructor - this.PointClass = Chart.Point.extend({ - offsetGridLines : this.options.offsetGridLines, - strokeWidth : this.options.pointDotStrokeWidth, - radius : this.options.pointDotRadius, - display: this.options.pointDot, - hitDetectionRadius : this.options.pointHitDetectionRadius, - ctx : this.chart.ctx, - inRange : function(mouseX){ - return (Math.pow(mouseX-this.x, 2) < Math.pow(this.radius + this.hitDetectionRadius,2)); - } - }); - - this.datasets = []; - - //Set up tooltip events on the chart - if (this.options.showTooltips){ - helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ - var activePoints = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : []; - this.eachPoints(function(point){ - point.restore(['fillColor', 'strokeColor']); - }); - helpers.each(activePoints, function(activePoint){ - activePoint.fillColor = activePoint.highlightFill; - activePoint.strokeColor = activePoint.highlightStroke; - }); - this.showTooltip(activePoints); - }); - } - - //Iterate through each of the datasets, and build this into a property of the chart - helpers.each(data.datasets,function(dataset){ - - var datasetObject = { - label : dataset.label || null, - fillColor : dataset.fillColor, - strokeColor : dataset.strokeColor, - pointColor : dataset.pointColor, - pointStrokeColor : dataset.pointStrokeColor, - points : [] - }; - - this.datasets.push(datasetObject); - - - helpers.each(dataset.data,function(dataPoint,index){ - //Add a new point for each piece of data, passing any required data to draw. - datasetObject.points.push(new this.PointClass({ - value : dataPoint, - label : data.labels[index], - datasetLabel: dataset.label, - strokeColor : dataset.pointStrokeColor, - fillColor : dataset.pointColor, - highlightFill : dataset.pointHighlightFill || dataset.pointColor, - highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor - })); - },this); - - this.buildScale(data.labels); - - - this.eachPoints(function(point, index){ - helpers.extend(point, { - x: this.scale.calculateX(index), - y: this.scale.endPoint - }); - point.save(); - }, this); - - },this); - - - this.render(); - }, - update : function(){ - this.scale.update(); - // Reset any highlight colours before updating. - helpers.each(this.activeElements, function(activeElement){ - activeElement.restore(['fillColor', 'strokeColor']); - }); - this.eachPoints(function(point){ - point.save(); - }); - this.render(); - }, - eachPoints : function(callback){ - helpers.each(this.datasets,function(dataset){ - helpers.each(dataset.points,callback,this); - },this); - }, - getPointsAtEvent : function(e){ - var pointsArray = [], - eventPosition = helpers.getRelativePosition(e); - helpers.each(this.datasets,function(dataset){ - helpers.each(dataset.points,function(point){ - if (point.inRange(eventPosition.x,eventPosition.y)) pointsArray.push(point); - }); - },this); - return pointsArray; - }, - buildScale : function(labels){ - var self = this; - - var dataTotal = function(){ - var values = []; - self.eachPoints(function(point){ - values.push(point.value); - }); - - return values; - }; - - var scaleOptions = { - templateString : this.options.scaleLabel, - height : this.chart.height, - width : this.chart.width, - ctx : this.chart.ctx, - textColor : this.options.scaleFontColor, - offsetGridLines : this.options.offsetGridLines, - fontSize : this.options.scaleFontSize, - fontStyle : this.options.scaleFontStyle, - fontFamily : this.options.scaleFontFamily, - valuesCount : labels.length, - beginAtZero : this.options.scaleBeginAtZero, - integersOnly : this.options.scaleIntegersOnly, - calculateYRange : function(currentHeight){ - var updatedRanges = helpers.calculateScaleRange( - dataTotal(), - currentHeight, - this.fontSize, - this.beginAtZero, - this.integersOnly - ); - helpers.extend(this, updatedRanges); - }, - xLabels : labels, - font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily), - lineWidth : this.options.scaleLineWidth, - lineColor : this.options.scaleLineColor, - showHorizontalLines : this.options.scaleShowHorizontalLines, - showVerticalLines : this.options.scaleShowVerticalLines, - gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, - gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", - padding: (this.options.showScale) ? 0 : this.options.pointDotRadius + this.options.pointDotStrokeWidth, - showLabels : this.options.scaleShowLabels, - display : this.options.showScale - }; - - if (this.options.scaleOverride){ - helpers.extend(scaleOptions, { - calculateYRange: helpers.noop, - steps: this.options.scaleSteps, - stepValue: this.options.scaleStepWidth, - min: this.options.scaleStartValue, - max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) - }); - } - - - this.scale = new Chart.Scale(scaleOptions); - }, - addData : function(valuesArray,label){ - //Map the values array for each of the datasets - - helpers.each(valuesArray,function(value,datasetIndex){ - //Add a new point for each piece of data, passing any required data to draw. - this.datasets[datasetIndex].points.push(new this.PointClass({ - value : value, - label : label, - datasetLabel: this.datasets[datasetIndex].label, - x: this.scale.calculateX(this.scale.valuesCount+1), - y: this.scale.endPoint, - strokeColor : this.datasets[datasetIndex].pointStrokeColor, - fillColor : this.datasets[datasetIndex].pointColor - })); - },this); - - this.scale.addXLabel(label); - //Then re-render the chart. - this.update(); - }, - removeData : function(){ - this.scale.removeXLabel(); - //Then re-render the chart. - helpers.each(this.datasets,function(dataset){ - dataset.points.shift(); - },this); - this.update(); - }, - reflow : function(){ - var newScaleProps = helpers.extend({ - height : this.chart.height, - width : this.chart.width - }); - this.scale.update(newScaleProps); - }, - draw : function(ease){ - var easingDecimal = ease || 1; - this.clear(); - - var ctx = this.chart.ctx; - - // Some helper methods for getting the next/prev points - var hasValue = function(item){ - return item.value !== null; - }, - nextPoint = function(point, collection, index){ - return helpers.findNextWhere(collection, hasValue, index) || point; - }, - previousPoint = function(point, collection, index){ - return helpers.findPreviousWhere(collection, hasValue, index) || point; - }; - - if (!this.scale) return; - this.scale.draw(easingDecimal); - - - helpers.each(this.datasets,function(dataset){ - var pointsWithValues = helpers.where(dataset.points, hasValue); - - //Transition each point first so that the line and point drawing isn't out of sync - //We can use this extra loop to calculate the control points of this dataset also in this loop - - helpers.each(dataset.points, function(point, index){ - if (point.hasValue()){ - point.transition({ - y : this.scale.calculateY(point.value), - x : this.scale.calculateX(index) - }, easingDecimal); - } - },this); - - - // Control points need to be calculated in a separate loop, because we need to know the current x/y of the point - // This would cause issues when there is no animation, because the y of the next point would be 0, so beziers would be skewed - if (this.options.bezierCurve){ - helpers.each(pointsWithValues, function(point, index){ - var tension = (index > 0 && index < pointsWithValues.length - 1) ? this.options.bezierCurveTension : 0; - point.controlPoints = helpers.splineCurve( - previousPoint(point, pointsWithValues, index), - point, - nextPoint(point, pointsWithValues, index), - tension - ); - - // Prevent the bezier going outside of the bounds of the graph - - // Cap puter bezier handles to the upper/lower scale bounds - if (point.controlPoints.outer.y > this.scale.endPoint){ - point.controlPoints.outer.y = this.scale.endPoint; - } - else if (point.controlPoints.outer.y < this.scale.startPoint){ - point.controlPoints.outer.y = this.scale.startPoint; - } - - // Cap inner bezier handles to the upper/lower scale bounds - if (point.controlPoints.inner.y > this.scale.endPoint){ - point.controlPoints.inner.y = this.scale.endPoint; - } - else if (point.controlPoints.inner.y < this.scale.startPoint){ - point.controlPoints.inner.y = this.scale.startPoint; - } - },this); - } - - - //Draw the line between all the points - ctx.lineWidth = this.options.datasetStrokeWidth; - ctx.strokeStyle = dataset.strokeColor; - ctx.beginPath(); - - helpers.each(pointsWithValues, function(point, index){ - if (index === 0){ - ctx.moveTo(point.x, point.y); - } - else{ - if(this.options.bezierCurve){ - var previous = previousPoint(point, pointsWithValues, index); - - ctx.bezierCurveTo( - previous.controlPoints.outer.x, - previous.controlPoints.outer.y, - point.controlPoints.inner.x, - point.controlPoints.inner.y, - point.x, - point.y - ); - } - else{ - ctx.lineTo(point.x,point.y); - } - } - }, this); - - if (this.options.datasetStroke) { - ctx.stroke(); - } - - if (this.options.datasetFill && pointsWithValues.length > 0){ - //Round off the line by going to the base of the chart, back to the start, then fill. - ctx.lineTo(pointsWithValues[pointsWithValues.length - 1].x, this.scale.endPoint); - ctx.lineTo(pointsWithValues[0].x, this.scale.endPoint); - ctx.fillStyle = dataset.fillColor; - ctx.closePath(); - ctx.fill(); - } - - //Now draw the points over the line - //A little inefficient double looping, but better than the line - //lagging behind the point positions - helpers.each(pointsWithValues,function(point){ - point.draw(); - }); - },this); - } - }); - - -}).call(this); diff --git a/src/Chart.PolarArea.js b/src/Chart.PolarArea.js deleted file mode 100644 index d1802f5d617..00000000000 --- a/src/Chart.PolarArea.js +++ /dev/null @@ -1,250 +0,0 @@ -(function(){ - "use strict"; - - var root = this, - Chart = root.Chart, - //Cache a local reference to Chart.helpers - helpers = Chart.helpers; - - var defaultConfig = { - //Boolean - Show a backdrop to the scale label - scaleShowLabelBackdrop : true, - - //String - The colour of the label backdrop - scaleBackdropColor : "rgba(255,255,255,0.75)", - - // Boolean - Whether the scale should begin at zero - scaleBeginAtZero : true, - - //Number - The backdrop padding above & below the label in pixels - scaleBackdropPaddingY : 2, - - //Number - The backdrop padding to the side of the label in pixels - scaleBackdropPaddingX : 2, - - //Boolean - Show line for each value in the scale - scaleShowLine : true, - - //Boolean - Stroke a line around each segment in the chart - segmentShowStroke : true, - - //String - The colour of the stroke on each segment. - segmentStrokeColor : "#fff", - - //Number - The width of the stroke value in pixels - segmentStrokeWidth : 2, - - //Number - Amount of animation steps - animationSteps : 100, - - //String - Animation easing effect. - animationEasing : "easeOutBounce", - - //Boolean - Whether to animate the rotation of the chart - animateRotate : true, - - //Boolean - Whether to animate scaling the chart from the centre - animateScale : false, - - //String - A legend template - legendTemplate : "
      -legend\"><% for (var i=0; i
    • \"><%if(segments[i].label){%><%=segments[i].label%><%}%>
    • <%}%>
    " - }; - - - Chart.Type.extend({ - //Passing in a name registers this chart in the Chart namespace - name: "PolarArea", - //Providing a defaults will also register the deafults in the chart namespace - defaults : defaultConfig, - //Initialize is fired when the chart is initialized - Data is passed in as a parameter - //Config is automatically merged by the core of Chart.js, and is available at this.options - initialize: function(data){ - this.segments = []; - //Declare segment class as a chart instance specific class, so it can share props for this instance - this.SegmentArc = Chart.Arc.extend({ - showStroke : this.options.segmentShowStroke, - strokeWidth : this.options.segmentStrokeWidth, - strokeColor : this.options.segmentStrokeColor, - ctx : this.chart.ctx, - innerRadius : 0, - x : this.chart.width/2, - y : this.chart.height/2 - }); - this.scale = new Chart.RadialScale({ - display: this.options.showScale, - fontStyle: this.options.scaleFontStyle, - fontSize: this.options.scaleFontSize, - fontFamily: this.options.scaleFontFamily, - fontColor: this.options.scaleFontColor, - showLabels: this.options.scaleShowLabels, - showLabelBackdrop: this.options.scaleShowLabelBackdrop, - backdropColor: this.options.scaleBackdropColor, - backdropPaddingY : this.options.scaleBackdropPaddingY, - backdropPaddingX: this.options.scaleBackdropPaddingX, - lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0, - lineColor: this.options.scaleLineColor, - lineArc: true, - width: this.chart.width, - height: this.chart.height, - xCenter: this.chart.width/2, - yCenter: this.chart.height/2, - ctx : this.chart.ctx, - templateString: this.options.scaleLabel, - valuesCount: data.length - }); - - this.updateScaleRange(data); - - this.scale.update(); - - helpers.each(data,function(segment,index){ - this.addData(segment,index,true); - },this); - - //Set up tooltip events on the chart - if (this.options.showTooltips){ - helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ - var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : []; - helpers.each(this.segments,function(segment){ - segment.restore(["fillColor"]); - }); - helpers.each(activeSegments,function(activeSegment){ - activeSegment.fillColor = activeSegment.highlightColor; - }); - this.showTooltip(activeSegments); - }); - } - - this.render(); - }, - getSegmentsAtEvent : function(e){ - var segmentsArray = []; - - var location = helpers.getRelativePosition(e); - - helpers.each(this.segments,function(segment){ - if (segment.inRange(location.x,location.y)) segmentsArray.push(segment); - },this); - return segmentsArray; - }, - addData : function(segment, atIndex, silent){ - var index = atIndex || this.segments.length; - - this.segments.splice(index, 0, new this.SegmentArc({ - fillColor: segment.color, - highlightColor: segment.highlight || segment.color, - label: segment.label, - value: segment.value, - outerRadius: (this.options.animateScale) ? 0 : this.scale.calculateCenterOffset(segment.value), - circumference: (this.options.animateRotate) ? 0 : this.scale.getCircumference(), - startAngle: Math.PI * 1.5 - })); - if (!silent){ - this.reflow(); - this.update(); - } - }, - removeData: function(atIndex){ - var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1; - this.segments.splice(indexToDelete, 1); - this.reflow(); - this.update(); - }, - calculateTotal: function(data){ - this.total = 0; - helpers.each(data,function(segment){ - this.total += segment.value; - },this); - this.scale.valuesCount = this.segments.length; - }, - updateScaleRange: function(datapoints){ - var valuesArray = []; - helpers.each(datapoints,function(segment){ - valuesArray.push(segment.value); - }); - - var scaleSizes = (this.options.scaleOverride) ? - { - steps: this.options.scaleSteps, - stepValue: this.options.scaleStepWidth, - min: this.options.scaleStartValue, - max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) - } : - helpers.calculateScaleRange( - valuesArray, - helpers.min([this.chart.width, this.chart.height])/2, - this.options.scaleFontSize, - this.options.scaleBeginAtZero, - this.options.scaleIntegersOnly - ); - - helpers.extend( - this.scale, - scaleSizes, - { - size: helpers.min([this.chart.width, this.chart.height]), - xCenter: this.chart.width/2, - yCenter: this.chart.height/2 - } - ); - - }, - update : function(){ - this.calculateTotal(this.segments); - - helpers.each(this.segments,function(segment){ - segment.save(); - }); - - this.reflow(); - this.render(); - }, - reflow : function(){ - helpers.extend(this.SegmentArc.prototype,{ - x : this.chart.width/2, - y : this.chart.height/2 - }); - this.updateScaleRange(this.segments); - this.scale.update(); - - helpers.extend(this.scale,{ - xCenter: this.chart.width/2, - yCenter: this.chart.height/2 - }); - - helpers.each(this.segments, function(segment){ - segment.update({ - outerRadius : this.scale.calculateCenterOffset(segment.value) - }); - }, this); - - }, - draw : function(ease){ - var easingDecimal = ease || 1; - //Clear & draw the canvas - this.clear(); - helpers.each(this.segments,function(segment, index){ - segment.transition({ - circumference : this.scale.getCircumference(), - outerRadius : this.scale.calculateCenterOffset(segment.value) - },easingDecimal); - - segment.endAngle = segment.startAngle + segment.circumference; - - // If we've removed the first segment we need to set the first one to - // start at the top. - if (index === 0){ - segment.startAngle = Math.PI * 1.5; - } - - //Check to see if it's the last segment, if not get the next and update the start angle - if (index < this.segments.length - 1){ - this.segments[index+1].startAngle = segment.endAngle; - } - segment.draw(); - }, this); - this.scale.draw(); - } - }); - -}).call(this); diff --git a/src/Chart.Radar.js b/src/Chart.Radar.js deleted file mode 100644 index 93d7352fd98..00000000000 --- a/src/Chart.Radar.js +++ /dev/null @@ -1,346 +0,0 @@ -(function(){ - "use strict"; - - var root = this, - Chart = root.Chart, - helpers = Chart.helpers; - - - - Chart.Type.extend({ - name: "Radar", - defaults:{ - //Boolean - Whether to show lines for each scale point - scaleShowLine : true, - - //Boolean - Whether we show the angle lines out of the radar - angleShowLineOut : true, - - //Boolean - Whether to show labels on the scale - scaleShowLabels : false, - - // Boolean - Whether the scale should begin at zero - scaleBeginAtZero : true, - - //String - Colour of the angle line - angleLineColor : "rgba(0,0,0,.1)", - - //Number - Pixel width of the angle line - angleLineWidth : 1, - - //String - Point label font declaration - pointLabelFontFamily : "'Arial'", - - //String - Point label font weight - pointLabelFontStyle : "normal", - - //Number - Point label font size in pixels - pointLabelFontSize : 10, - - //String - Point label font colour - pointLabelFontColor : "#666", - - //Boolean - Whether to show a dot for each point - pointDot : true, - - //Number - Radius of each point dot in pixels - pointDotRadius : 3, - - //Number - Pixel width of point dot stroke - pointDotStrokeWidth : 1, - - //Number - amount extra to add to the radius to cater for hit detection outside the drawn point - pointHitDetectionRadius : 20, - - //Boolean - Whether to show a stroke for datasets - datasetStroke : true, - - //Number - Pixel width of dataset stroke - datasetStrokeWidth : 2, - - //Boolean - Whether to fill the dataset with a colour - datasetFill : true, - - //String - A legend template - legendTemplate : "
      -legend\"><% for (var i=0; i
    • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
    • <%}%>
    " - - }, - - initialize: function(data){ - this.PointClass = Chart.Point.extend({ - strokeWidth : this.options.pointDotStrokeWidth, - radius : this.options.pointDotRadius, - display: this.options.pointDot, - hitDetectionRadius : this.options.pointHitDetectionRadius, - ctx : this.chart.ctx - }); - - this.datasets = []; - - this.buildScale(data); - - //Set up tooltip events on the chart - if (this.options.showTooltips){ - helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ - var activePointsCollection = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : []; - - this.eachPoints(function(point){ - point.restore(['fillColor', 'strokeColor']); - }); - helpers.each(activePointsCollection, function(activePoint){ - activePoint.fillColor = activePoint.highlightFill; - activePoint.strokeColor = activePoint.highlightStroke; - }); - - this.showTooltip(activePointsCollection); - }); - } - - //Iterate through each of the datasets, and build this into a property of the chart - helpers.each(data.datasets,function(dataset){ - - var datasetObject = { - label: dataset.label || null, - fillColor : dataset.fillColor, - strokeColor : dataset.strokeColor, - pointColor : dataset.pointColor, - pointStrokeColor : dataset.pointStrokeColor, - points : [] - }; - - this.datasets.push(datasetObject); - - helpers.each(dataset.data,function(dataPoint,index){ - //Add a new point for each piece of data, passing any required data to draw. - var pointPosition; - if (!this.scale.animation){ - pointPosition = this.scale.getPointPosition(index, this.scale.calculateCenterOffset(dataPoint)); - } - datasetObject.points.push(new this.PointClass({ - value : dataPoint, - label : data.labels[index], - datasetLabel: dataset.label, - x: (this.options.animation) ? this.scale.xCenter : pointPosition.x, - y: (this.options.animation) ? this.scale.yCenter : pointPosition.y, - strokeColor : dataset.pointStrokeColor, - fillColor : dataset.pointColor, - highlightFill : dataset.pointHighlightFill || dataset.pointColor, - highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor - })); - },this); - - },this); - - this.render(); - }, - eachPoints : function(callback){ - helpers.each(this.datasets,function(dataset){ - helpers.each(dataset.points,callback,this); - },this); - }, - - getPointsAtEvent : function(evt){ - var mousePosition = helpers.getRelativePosition(evt), - fromCenter = helpers.getAngleFromPoint({ - x: this.scale.xCenter, - y: this.scale.yCenter - }, mousePosition); - - var anglePerIndex = (Math.PI * 2) /this.scale.valuesCount, - pointIndex = Math.round((fromCenter.angle - Math.PI * 1.5) / anglePerIndex), - activePointsCollection = []; - - // If we're at the top, make the pointIndex 0 to get the first of the array. - if (pointIndex >= this.scale.valuesCount || pointIndex < 0){ - pointIndex = 0; - } - - if (fromCenter.distance <= this.scale.drawingArea){ - helpers.each(this.datasets, function(dataset){ - activePointsCollection.push(dataset.points[pointIndex]); - }); - } - - return activePointsCollection; - }, - - buildScale : function(data){ - this.scale = new Chart.RadialScale({ - display: this.options.showScale, - fontStyle: this.options.scaleFontStyle, - fontSize: this.options.scaleFontSize, - fontFamily: this.options.scaleFontFamily, - fontColor: this.options.scaleFontColor, - showLabels: this.options.scaleShowLabels, - showLabelBackdrop: this.options.scaleShowLabelBackdrop, - backdropColor: this.options.scaleBackdropColor, - backgroundColors: this.options.scaleBackgroundColors, - backdropPaddingY : this.options.scaleBackdropPaddingY, - backdropPaddingX: this.options.scaleBackdropPaddingX, - lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0, - lineColor: this.options.scaleLineColor, - angleLineColor : this.options.angleLineColor, - angleLineWidth : (this.options.angleShowLineOut) ? this.options.angleLineWidth : 0, - // Point labels at the edge of each line - pointLabelFontColor : this.options.pointLabelFontColor, - pointLabelFontSize : this.options.pointLabelFontSize, - pointLabelFontFamily : this.options.pointLabelFontFamily, - pointLabelFontStyle : this.options.pointLabelFontStyle, - height : this.chart.height, - width: this.chart.width, - xCenter: this.chart.width/2, - yCenter: this.chart.height/2, - ctx : this.chart.ctx, - templateString: this.options.scaleLabel, - labels: data.labels, - valuesCount: data.datasets[0].data.length - }); - - this.scale.setScaleSize(); - this.updateScaleRange(data.datasets); - this.scale.buildYLabels(); - }, - updateScaleRange: function(datasets){ - var valuesArray = (function(){ - var totalDataArray = []; - helpers.each(datasets,function(dataset){ - if (dataset.data){ - totalDataArray = totalDataArray.concat(dataset.data); - } - else { - helpers.each(dataset.points, function(point){ - totalDataArray.push(point.value); - }); - } - }); - return totalDataArray; - })(); - - - var scaleSizes = (this.options.scaleOverride) ? - { - steps: this.options.scaleSteps, - stepValue: this.options.scaleStepWidth, - min: this.options.scaleStartValue, - max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) - } : - helpers.calculateScaleRange( - valuesArray, - helpers.min([this.chart.width, this.chart.height])/2, - this.options.scaleFontSize, - this.options.scaleBeginAtZero, - this.options.scaleIntegersOnly - ); - - helpers.extend( - this.scale, - scaleSizes - ); - - }, - addData : function(valuesArray,label){ - //Map the values array for each of the datasets - this.scale.valuesCount++; - helpers.each(valuesArray,function(value,datasetIndex){ - var pointPosition = this.scale.getPointPosition(this.scale.valuesCount, this.scale.calculateCenterOffset(value)); - this.datasets[datasetIndex].points.push(new this.PointClass({ - value : value, - label : label, - datasetLabel: this.datasets[datasetIndex].label, - x: pointPosition.x, - y: pointPosition.y, - strokeColor : this.datasets[datasetIndex].pointStrokeColor, - fillColor : this.datasets[datasetIndex].pointColor - })); - },this); - - this.scale.labels.push(label); - - this.reflow(); - - this.update(); - }, - removeData : function(){ - this.scale.valuesCount--; - this.scale.labels.shift(); - helpers.each(this.datasets,function(dataset){ - dataset.points.shift(); - },this); - this.reflow(); - this.update(); - }, - update : function(){ - this.eachPoints(function(point){ - point.save(); - }); - this.reflow(); - this.render(); - }, - reflow: function(){ - helpers.extend(this.scale, { - width : this.chart.width, - height: this.chart.height, - size : helpers.min([this.chart.width, this.chart.height]), - xCenter: this.chart.width/2, - yCenter: this.chart.height/2 - }); - this.updateScaleRange(this.datasets); - this.scale.setScaleSize(); - this.scale.buildYLabels(); - }, - draw : function(ease){ - var easeDecimal = ease || 1, - ctx = this.chart.ctx; - this.clear(); - this.scale.draw(); - - helpers.each(this.datasets,function(dataset){ - - //Transition each point first so that the line and point drawing isn't out of sync - helpers.each(dataset.points,function(point,index){ - if (point.hasValue()){ - point.transition(this.scale.getPointPosition(index, this.scale.calculateCenterOffset(point.value)), easeDecimal); - } - },this); - - - - //Draw the line between all the points - ctx.lineWidth = this.options.datasetStrokeWidth; - ctx.strokeStyle = dataset.strokeColor; - ctx.beginPath(); - helpers.each(dataset.points,function(point,index){ - if (index === 0){ - ctx.moveTo(point.x,point.y); - } - else{ - ctx.lineTo(point.x,point.y); - } - },this); - ctx.closePath(); - ctx.stroke(); - - ctx.fillStyle = dataset.fillColor; - if(this.options.datasetFill){ - ctx.fill(); - } - //Now draw the points over the line - //A little inefficient double looping, but better than the line - //lagging behind the point positions - helpers.each(dataset.points,function(point){ - if (point.hasValue()){ - point.draw(); - } - }); - - },this); - - } - - }); - - - - - -}).call(this); diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js new file mode 100644 index 00000000000..554497b3053 --- /dev/null +++ b/src/controllers/controller.bar.js @@ -0,0 +1,682 @@ +import DatasetController from '../core/core.datasetController.js'; +import { + _arrayUnique, isArray, isNullOrUndef, + valueOrDefault, resolveObjectKey, sign, defined +} from '../helpers/index.js'; + +function getAllScaleValues(scale, type) { + if (!scale._cache.$bar) { + const visibleMetas = scale.getMatchingVisibleMetas(type); + let values = []; + + for (let i = 0, ilen = visibleMetas.length; i < ilen; i++) { + values = values.concat(visibleMetas[i].controller.getAllParsedValues(scale)); + } + scale._cache.$bar = _arrayUnique(values.sort((a, b) => a - b)); + } + return scale._cache.$bar; +} + +/** + * Computes the "optimal" sample size to maintain bars equally sized while preventing overlap. + * @private + */ +function computeMinSampleSize(meta) { + const scale = meta.iScale; + const values = getAllScaleValues(scale, meta.type); + let min = scale._length; + let i, ilen, curr, prev; + const updateMinAndPrev = () => { + if (curr === 32767 || curr === -32768) { + // Ignore truncated pixels + return; + } + if (defined(prev)) { + // curr - prev === 0 is ignored + min = Math.min(min, Math.abs(curr - prev) || min); + } + prev = curr; + }; + + for (i = 0, ilen = values.length; i < ilen; ++i) { + curr = scale.getPixelForValue(values[i]); + updateMinAndPrev(); + } + + prev = undefined; + for (i = 0, ilen = scale.ticks.length; i < ilen; ++i) { + curr = scale.getPixelForTick(i); + updateMinAndPrev(); + } + + return min; +} + +/** + * Computes an "ideal" category based on the absolute bar thickness or, if undefined or null, + * uses the smallest interval (see computeMinSampleSize) that prevents bar overlapping. This + * mode currently always generates bars equally sized (until we introduce scriptable options?). + * @private + */ +function computeFitCategoryTraits(index, ruler, options, stackCount) { + const thickness = options.barThickness; + let size, ratio; + + if (isNullOrUndef(thickness)) { + size = ruler.min * options.categoryPercentage; + ratio = options.barPercentage; + } else { + // When bar thickness is enforced, category and bar percentages are ignored. + // Note(SB): we could add support for relative bar thickness (e.g. barThickness: '50%') + // and deprecate barPercentage since this value is ignored when thickness is absolute. + size = thickness * stackCount; + ratio = 1; + } + + return { + chunk: size / stackCount, + ratio, + start: ruler.pixels[index] - (size / 2) + }; +} + +/** + * Computes an "optimal" category that globally arranges bars side by side (no gap when + * percentage options are 1), based on the previous and following categories. This mode + * generates bars with different widths when data are not evenly spaced. + * @private + */ +function computeFlexCategoryTraits(index, ruler, options, stackCount) { + const pixels = ruler.pixels; + const curr = pixels[index]; + let prev = index > 0 ? pixels[index - 1] : null; + let next = index < pixels.length - 1 ? pixels[index + 1] : null; + const percent = options.categoryPercentage; + + if (prev === null) { + // first data: its size is double based on the next point or, + // if it's also the last data, we use the scale size. + prev = curr - (next === null ? ruler.end - ruler.start : next - curr); + } + + if (next === null) { + // last data: its size is also double based on the previous point. + next = curr + curr - prev; + } + + const start = curr - (curr - Math.min(prev, next)) / 2 * percent; + const size = Math.abs(next - prev) / 2 * percent; + + return { + chunk: size / stackCount, + ratio: options.barPercentage, + start + }; +} + +function parseFloatBar(entry, item, vScale, i) { + const startValue = vScale.parse(entry[0], i); + const endValue = vScale.parse(entry[1], i); + const min = Math.min(startValue, endValue); + const max = Math.max(startValue, endValue); + let barStart = min; + let barEnd = max; + + if (Math.abs(min) > Math.abs(max)) { + barStart = max; + barEnd = min; + } + + // Store `barEnd` (furthest away from origin) as parsed value, + // to make stacking straight forward + item[vScale.axis] = barEnd; + + item._custom = { + barStart, + barEnd, + start: startValue, + end: endValue, + min, + max + }; +} + +function parseValue(entry, item, vScale, i) { + if (isArray(entry)) { + parseFloatBar(entry, item, vScale, i); + } else { + item[vScale.axis] = vScale.parse(entry, i); + } + return item; +} + +function parseArrayOrPrimitive(meta, data, start, count) { + const iScale = meta.iScale; + const vScale = meta.vScale; + const labels = iScale.getLabels(); + const singleScale = iScale === vScale; + const parsed = []; + let i, ilen, item, entry; + + for (i = start, ilen = start + count; i < ilen; ++i) { + entry = data[i]; + item = {}; + item[iScale.axis] = singleScale || iScale.parse(labels[i], i); + parsed.push(parseValue(entry, item, vScale, i)); + } + return parsed; +} + +function isFloatBar(custom) { + return custom && custom.barStart !== undefined && custom.barEnd !== undefined; +} + +function barSign(size, vScale, actualBase) { + if (size !== 0) { + return sign(size); + } + return (vScale.isHorizontal() ? 1 : -1) * (vScale.min >= actualBase ? 1 : -1); +} + +function borderProps(properties) { + let reverse, start, end, top, bottom; + if (properties.horizontal) { + reverse = properties.base > properties.x; + start = 'left'; + end = 'right'; + } else { + reverse = properties.base < properties.y; + start = 'bottom'; + end = 'top'; + } + if (reverse) { + top = 'end'; + bottom = 'start'; + } else { + top = 'start'; + bottom = 'end'; + } + return {start, end, reverse, top, bottom}; +} + +function setBorderSkipped(properties, options, stack, index) { + let edge = options.borderSkipped; + const res = {}; + + if (!edge) { + properties.borderSkipped = res; + return; + } + + if (edge === true) { + properties.borderSkipped = {top: true, right: true, bottom: true, left: true}; + return; + } + + const {start, end, reverse, top, bottom} = borderProps(properties); + + if (edge === 'middle' && stack) { + properties.enableBorderRadius = true; + if ((stack._top || 0) === index) { + edge = top; + } else if ((stack._bottom || 0) === index) { + edge = bottom; + } else { + res[parseEdge(bottom, start, end, reverse)] = true; + edge = top; + } + } + + res[parseEdge(edge, start, end, reverse)] = true; + properties.borderSkipped = res; +} + +function parseEdge(edge, a, b, reverse) { + if (reverse) { + edge = swap(edge, a, b); + edge = startEnd(edge, b, a); + } else { + edge = startEnd(edge, a, b); + } + return edge; +} + +function swap(orig, v1, v2) { + return orig === v1 ? v2 : orig === v2 ? v1 : orig; +} + +function startEnd(v, start, end) { + return v === 'start' ? start : v === 'end' ? end : v; +} + +function setInflateAmount(properties, {inflateAmount}, ratio) { + properties.inflateAmount = inflateAmount === 'auto' + ? ratio === 1 ? 0.33 : 0 + : inflateAmount; +} + +export default class BarController extends DatasetController { + + static id = 'bar'; + + /** + * @type {any} + */ + static defaults = { + datasetElementType: false, + dataElementType: 'bar', + + categoryPercentage: 0.8, + barPercentage: 0.9, + grouped: true, + + animations: { + numbers: { + type: 'number', + properties: ['x', 'y', 'base', 'width', 'height'] + } + } + }; + + /** + * @type {any} + */ + static overrides = { + scales: { + _index_: { + type: 'category', + offset: true, + grid: { + offset: true + } + }, + _value_: { + type: 'linear', + beginAtZero: true, + } + } + }; + + + /** + * Overriding primitive data parsing since we support mixed primitive/array + * data for float bars + * @protected + */ + parsePrimitiveData(meta, data, start, count) { + return parseArrayOrPrimitive(meta, data, start, count); + } + + /** + * Overriding array data parsing since we support mixed primitive/array + * data for float bars + * @protected + */ + parseArrayData(meta, data, start, count) { + return parseArrayOrPrimitive(meta, data, start, count); + } + + /** + * Overriding object data parsing since we support mixed primitive/array + * value-scale data for float bars + * @protected + */ + parseObjectData(meta, data, start, count) { + const {iScale, vScale} = meta; + const {xAxisKey = 'x', yAxisKey = 'y'} = this._parsing; + const iAxisKey = iScale.axis === 'x' ? xAxisKey : yAxisKey; + const vAxisKey = vScale.axis === 'x' ? xAxisKey : yAxisKey; + const parsed = []; + let i, ilen, item, obj; + for (i = start, ilen = start + count; i < ilen; ++i) { + obj = data[i]; + item = {}; + item[iScale.axis] = iScale.parse(resolveObjectKey(obj, iAxisKey), i); + parsed.push(parseValue(resolveObjectKey(obj, vAxisKey), item, vScale, i)); + } + return parsed; + } + + /** + * @protected + */ + updateRangeFromParsed(range, scale, parsed, stack) { + super.updateRangeFromParsed(range, scale, parsed, stack); + const custom = parsed._custom; + if (custom && scale === this._cachedMeta.vScale) { + // float bar: only one end of the bar is considered by `super` + range.min = Math.min(range.min, custom.min); + range.max = Math.max(range.max, custom.max); + } + } + + /** + * @return {number|boolean} + * @protected + */ + getMaxOverflow() { + return 0; + } + + /** + * @protected + */ + getLabelAndValue(index) { + const meta = this._cachedMeta; + const {iScale, vScale} = meta; + const parsed = this.getParsed(index); + const custom = parsed._custom; + const value = isFloatBar(custom) + ? '[' + custom.start + ', ' + custom.end + ']' + : '' + vScale.getLabelForValue(parsed[vScale.axis]); + + return { + label: '' + iScale.getLabelForValue(parsed[iScale.axis]), + value + }; + } + + initialize() { + this.enableOptionSharing = true; + + super.initialize(); + + const meta = this._cachedMeta; + meta.stack = this.getDataset().stack; + } + + update(mode) { + const meta = this._cachedMeta; + this.updateElements(meta.data, 0, meta.data.length, mode); + } + + updateElements(bars, start, count, mode) { + const reset = mode === 'reset'; + const {index, _cachedMeta: {vScale}} = this; + const base = vScale.getBasePixel(); + const horizontal = vScale.isHorizontal(); + const ruler = this._getRuler(); + const {sharedOptions, includeOptions} = this._getSharedOptions(start, mode); + + for (let i = start; i < start + count; i++) { + const parsed = this.getParsed(i); + const vpixels = reset || isNullOrUndef(parsed[vScale.axis]) ? {base, head: base} : this._calculateBarValuePixels(i); + const ipixels = this._calculateBarIndexPixels(i, ruler); + const stack = (parsed._stacks || {})[vScale.axis]; + + const properties = { + horizontal, + base: vpixels.base, + enableBorderRadius: !stack || isFloatBar(parsed._custom) || (index === stack._top || index === stack._bottom), + x: horizontal ? vpixels.head : ipixels.center, + y: horizontal ? ipixels.center : vpixels.head, + height: horizontal ? ipixels.size : Math.abs(vpixels.size), + width: horizontal ? Math.abs(vpixels.size) : ipixels.size + }; + + if (includeOptions) { + properties.options = sharedOptions || this.resolveDataElementOptions(i, bars[i].active ? 'active' : mode); + } + const options = properties.options || bars[i].options; + setBorderSkipped(properties, options, stack, index); + setInflateAmount(properties, options, ruler.ratio); + this.updateElement(bars[i], i, properties, mode); + } + } + + /** + * Returns the stacks based on groups and bar visibility. + * @param {number} [last] - The dataset index + * @param {number} [dataIndex] - The data index of the ruler + * @returns {string[]} The list of stack IDs + * @private + */ + _getStacks(last, dataIndex) { + const {iScale} = this._cachedMeta; + const metasets = iScale.getMatchingVisibleMetas(this._type) + .filter(meta => meta.controller.options.grouped); + const stacked = iScale.options.stacked; + const stacks = []; + const currentParsed = this._cachedMeta.controller.getParsed(dataIndex); + const iScaleValue = currentParsed && currentParsed[iScale.axis]; + + const skipNull = (meta) => { + const parsed = meta._parsed.find(item => item[iScale.axis] === iScaleValue); + const val = parsed && parsed[meta.vScale.axis]; + + if (isNullOrUndef(val) || isNaN(val)) { + return true; + } + }; + + for (const meta of metasets) { + if (dataIndex !== undefined && skipNull(meta)) { + continue; + } + + // stacked | meta.stack + // | found | not found | undefined + // false | x | x | x + // true | | x | + // undefined | | x | x + if (stacked === false || stacks.indexOf(meta.stack) === -1 || + (stacked === undefined && meta.stack === undefined)) { + stacks.push(meta.stack); + } + if (meta.index === last) { + break; + } + } + + // No stacks? that means there is no visible data. Let's still initialize an `undefined` + // stack where possible invisible bars will be located. + // https://github.com/chartjs/Chart.js/issues/6368 + if (!stacks.length) { + stacks.push(undefined); + } + + return stacks; + } + + /** + * Returns the effective number of stacks based on groups and bar visibility. + * @private + */ + _getStackCount(index) { + return this._getStacks(undefined, index).length; + } + + _getAxisCount() { + return this._getAxis().length; + } + + getFirstScaleIdForIndexAxis() { + const scales = this.chart.scales; + const indexScaleId = this.chart.options.indexAxis; + return Object.keys(scales).filter(key => scales[key].axis === indexScaleId).shift(); + } + + _getAxis() { + const axis = {}; + const firstScaleAxisId = this.getFirstScaleIdForIndexAxis(); + for (const dataset of this.chart.data.datasets) { + axis[valueOrDefault( + this.chart.options.indexAxis === 'x' ? dataset.xAxisID : dataset.yAxisID, firstScaleAxisId + )] = true; + } + return Object.keys(axis); + } + + /** + * Returns the stack index for the given dataset based on groups and bar visibility. + * @param {number} [datasetIndex] - The dataset index + * @param {string} [name] - The stack name to find + * @param {number} [dataIndex] + * @returns {number} The stack index + * @private + */ + _getStackIndex(datasetIndex, name, dataIndex) { + const stacks = this._getStacks(datasetIndex, dataIndex); + const index = (name !== undefined) + ? stacks.indexOf(name) + : -1; // indexOf returns -1 if element is not present + + return (index === -1) + ? stacks.length - 1 + : index; + } + + /** + * @private + */ + _getRuler() { + const opts = this.options; + const meta = this._cachedMeta; + const iScale = meta.iScale; + const pixels = []; + let i, ilen; + + for (i = 0, ilen = meta.data.length; i < ilen; ++i) { + pixels.push(iScale.getPixelForValue(this.getParsed(i)[iScale.axis], i)); + } + + const barThickness = opts.barThickness; + const min = barThickness || computeMinSampleSize(meta); + + return { + min, + pixels, + start: iScale._startPixel, + end: iScale._endPixel, + stackCount: this._getStackCount(), + scale: iScale, + grouped: opts.grouped, + // bar thickness ratio used for non-grouped bars + ratio: barThickness ? 1 : opts.categoryPercentage * opts.barPercentage + }; + } + + /** + * Note: pixel values are not clamped to the scale area. + * @private + */ + _calculateBarValuePixels(index) { + const {_cachedMeta: {vScale, _stacked, index: datasetIndex}, options: {base: baseValue, minBarLength}} = this; + const actualBase = baseValue || 0; + const parsed = this.getParsed(index); + const custom = parsed._custom; + const floating = isFloatBar(custom); + let value = parsed[vScale.axis]; + let start = 0; + let length = _stacked ? this.applyStack(vScale, parsed, _stacked) : value; + let head, size; + + if (length !== value) { + start = length - value; + length = value; + } + + if (floating) { + value = custom.barStart; + length = custom.barEnd - custom.barStart; + // bars crossing origin are not stacked + if (value !== 0 && sign(value) !== sign(custom.barEnd)) { + start = 0; + } + start += value; + } + + const startValue = !isNullOrUndef(baseValue) && !floating ? baseValue : start; + let base = vScale.getPixelForValue(startValue); + + if (this.chart.getDataVisibility(index)) { + head = vScale.getPixelForValue(start + length); + } else { + // When not visible, no height + head = base; + } + + size = head - base; + + if (Math.abs(size) < minBarLength) { + size = barSign(size, vScale, actualBase) * minBarLength; + if (value === actualBase) { + base -= size / 2; + } + const startPixel = vScale.getPixelForDecimal(0); + const endPixel = vScale.getPixelForDecimal(1); + const min = Math.min(startPixel, endPixel); + const max = Math.max(startPixel, endPixel); + base = Math.max(Math.min(base, max), min); + head = base + size; + + if (_stacked && !floating) { + // visual data coordinates after applying minBarLength + parsed._stacks[vScale.axis]._visualValues[datasetIndex] = vScale.getValueForPixel(head) - vScale.getValueForPixel(base); + } + } + + if (base === vScale.getPixelForValue(actualBase)) { + const halfGrid = sign(size) * vScale.getLineWidthForValue(actualBase) / 2; + base += halfGrid; + size -= halfGrid; + } + + return { + size, + base, + head, + center: head + size / 2 + }; + } + + /** + * @private + */ + _calculateBarIndexPixels(index, ruler) { + const scale = ruler.scale; + const options = this.options; + const skipNull = options.skipNull; + const maxBarThickness = valueOrDefault(options.maxBarThickness, Infinity); + let center, size; + const axisCount = this._getAxisCount(); + if (ruler.grouped) { + const stackCount = skipNull ? this._getStackCount(index) : ruler.stackCount; + const range = options.barThickness === 'flex' + ? computeFlexCategoryTraits(index, ruler, options, stackCount * axisCount) + : computeFitCategoryTraits(index, ruler, options, stackCount * axisCount); + const axisID = this.chart.options.indexAxis === 'x' ? this.getDataset().xAxisID : this.getDataset().yAxisID; + const axisNumber = this._getAxis().indexOf(valueOrDefault(axisID, this.getFirstScaleIdForIndexAxis())); + const stackIndex = this._getStackIndex(this.index, this._cachedMeta.stack, skipNull ? index : undefined) + axisNumber; + center = range.start + (range.chunk * stackIndex) + (range.chunk / 2); + size = Math.min(maxBarThickness, range.chunk * range.ratio); + } else { + // For non-grouped bar charts, exact pixel values are used + center = scale.getPixelForValue(this.getParsed(index)[scale.axis], index); + size = Math.min(maxBarThickness, ruler.min * ruler.ratio); + } + + + return { + base: center - size / 2, + head: center + size / 2, + center, + size + }; + } + + draw() { + const meta = this._cachedMeta; + const vScale = meta.vScale; + const rects = meta.data; + const ilen = rects.length; + let i = 0; + + for (; i < ilen; ++i) { + if (this.getParsed(i)[vScale.axis] !== null && !rects[i].hidden) { + rects[i].draw(this._ctx); + } + } + } + +} diff --git a/src/controllers/controller.bubble.js b/src/controllers/controller.bubble.js new file mode 100644 index 00000000000..81bf516356a --- /dev/null +++ b/src/controllers/controller.bubble.js @@ -0,0 +1,169 @@ +import DatasetController from '../core/core.datasetController.js'; +import {valueOrDefault} from '../helpers/helpers.core.js'; + +export default class BubbleController extends DatasetController { + + static id = 'bubble'; + + /** + * @type {any} + */ + static defaults = { + datasetElementType: false, + dataElementType: 'point', + + animations: { + numbers: { + type: 'number', + properties: ['x', 'y', 'borderWidth', 'radius'] + } + } + }; + + /** + * @type {any} + */ + static overrides = { + scales: { + x: { + type: 'linear' + }, + y: { + type: 'linear' + } + } + }; + + initialize() { + this.enableOptionSharing = true; + super.initialize(); + } + + /** + * Parse array of primitive values + * @protected + */ + parsePrimitiveData(meta, data, start, count) { + const parsed = super.parsePrimitiveData(meta, data, start, count); + for (let i = 0; i < parsed.length; i++) { + parsed[i]._custom = this.resolveDataElementOptions(i + start).radius; + } + return parsed; + } + + /** + * Parse array of arrays + * @protected + */ + parseArrayData(meta, data, start, count) { + const parsed = super.parseArrayData(meta, data, start, count); + for (let i = 0; i < parsed.length; i++) { + const item = data[start + i]; + parsed[i]._custom = valueOrDefault(item[2], this.resolveDataElementOptions(i + start).radius); + } + return parsed; + } + + /** + * Parse array of objects + * @protected + */ + parseObjectData(meta, data, start, count) { + const parsed = super.parseObjectData(meta, data, start, count); + for (let i = 0; i < parsed.length; i++) { + const item = data[start + i]; + parsed[i]._custom = valueOrDefault(item && item.r && +item.r, this.resolveDataElementOptions(i + start).radius); + } + return parsed; + } + + /** + * @protected + */ + getMaxOverflow() { + const data = this._cachedMeta.data; + + let max = 0; + for (let i = data.length - 1; i >= 0; --i) { + max = Math.max(max, data[i].size(this.resolveDataElementOptions(i)) / 2); + } + return max > 0 && max; + } + + /** + * @protected + */ + getLabelAndValue(index) { + const meta = this._cachedMeta; + const labels = this.chart.data.labels || []; + const {xScale, yScale} = meta; + const parsed = this.getParsed(index); + const x = xScale.getLabelForValue(parsed.x); + const y = yScale.getLabelForValue(parsed.y); + const r = parsed._custom; + + return { + label: labels[index] || '', + value: '(' + x + ', ' + y + (r ? ', ' + r : '') + ')' + }; + } + + update(mode) { + const points = this._cachedMeta.data; + + // Update Points + this.updateElements(points, 0, points.length, mode); + } + + updateElements(points, start, count, mode) { + const reset = mode === 'reset'; + const {iScale, vScale} = this._cachedMeta; + const {sharedOptions, includeOptions} = this._getSharedOptions(start, mode); + const iAxis = iScale.axis; + const vAxis = vScale.axis; + + for (let i = start; i < start + count; i++) { + const point = points[i]; + const parsed = !reset && this.getParsed(i); + const properties = {}; + const iPixel = properties[iAxis] = reset ? iScale.getPixelForDecimal(0.5) : iScale.getPixelForValue(parsed[iAxis]); + const vPixel = properties[vAxis] = reset ? vScale.getBasePixel() : vScale.getPixelForValue(parsed[vAxis]); + + properties.skip = isNaN(iPixel) || isNaN(vPixel); + + if (includeOptions) { + properties.options = sharedOptions || this.resolveDataElementOptions(i, point.active ? 'active' : mode); + + if (reset) { + properties.options.radius = 0; + } + } + + this.updateElement(point, i, properties, mode); + } + } + + /** + * @param {number} index + * @param {string} [mode] + * @protected + */ + resolveDataElementOptions(index, mode) { + const parsed = this.getParsed(index); + let values = super.resolveDataElementOptions(index, mode); + + // In case values were cached (and thus frozen), we need to clone the values + if (values.$shared) { + values = Object.assign({}, values, {$shared: false}); + } + + // Custom radius resolution + const radius = values.radius; + if (mode !== 'active') { + values.radius = 0; + } + values.radius += valueOrDefault(parsed && parsed._custom, radius); + + return values; + } +} diff --git a/src/controllers/controller.doughnut.js b/src/controllers/controller.doughnut.js new file mode 100644 index 00000000000..904dd8eb1c3 --- /dev/null +++ b/src/controllers/controller.doughnut.js @@ -0,0 +1,399 @@ +import DatasetController from '../core/core.datasetController.js'; +import {isObject, resolveObjectKey, toPercentage, toDimension, valueOrDefault} from '../helpers/helpers.core.js'; +import {formatNumber} from '../helpers/helpers.intl.js'; +import {toRadians, PI, TAU, HALF_PI, _angleBetween} from '../helpers/helpers.math.js'; + +/** + * @typedef { import('../core/core.controller.js').default } Chart + */ + +function getRatioAndOffset(rotation, circumference, cutout) { + let ratioX = 1; + let ratioY = 1; + let offsetX = 0; + let offsetY = 0; + // If the chart's circumference isn't a full circle, calculate size as a ratio of the width/height of the arc + if (circumference < TAU) { + const startAngle = rotation; + const endAngle = startAngle + circumference; + const startX = Math.cos(startAngle); + const startY = Math.sin(startAngle); + const endX = Math.cos(endAngle); + const endY = Math.sin(endAngle); + const calcMax = (angle, a, b) => _angleBetween(angle, startAngle, endAngle, true) ? 1 : Math.max(a, a * cutout, b, b * cutout); + const calcMin = (angle, a, b) => _angleBetween(angle, startAngle, endAngle, true) ? -1 : Math.min(a, a * cutout, b, b * cutout); + const maxX = calcMax(0, startX, endX); + const maxY = calcMax(HALF_PI, startY, endY); + const minX = calcMin(PI, startX, endX); + const minY = calcMin(PI + HALF_PI, startY, endY); + ratioX = (maxX - minX) / 2; + ratioY = (maxY - minY) / 2; + offsetX = -(maxX + minX) / 2; + offsetY = -(maxY + minY) / 2; + } + return {ratioX, ratioY, offsetX, offsetY}; +} + +export default class DoughnutController extends DatasetController { + + static id = 'doughnut'; + + /** + * @type {any} + */ + static defaults = { + datasetElementType: false, + dataElementType: 'arc', + animation: { + // Boolean - Whether we animate the rotation of the Doughnut + animateRotate: true, + // Boolean - Whether we animate scaling the Doughnut from the centre + animateScale: false + }, + animations: { + numbers: { + type: 'number', + properties: ['circumference', 'endAngle', 'innerRadius', 'outerRadius', 'startAngle', 'x', 'y', 'offset', 'borderWidth', 'spacing'] + }, + }, + // The percentage of the chart that we cut out of the middle. + cutout: '50%', + + // The rotation of the chart, where the first data arc begins. + rotation: 0, + + // The total circumference of the chart. + circumference: 360, + + // The outer radius of the chart + radius: '100%', + + // Spacing between arcs + spacing: 0, + + indexAxis: 'r', + }; + + static descriptors = { + _scriptable: (name) => name !== 'spacing', + _indexable: (name) => name !== 'spacing' && !name.startsWith('borderDash') && !name.startsWith('hoverBorderDash'), + }; + + /** + * @type {any} + */ + static overrides = { + aspectRatio: 1, + + // Need to override these to give a nice default + plugins: { + legend: { + labels: { + generateLabels(chart) { + const data = chart.data; + const {labels: {pointStyle, textAlign, color, useBorderRadius, borderRadius}} = chart.legend.options; + if (data.labels.length && data.datasets.length) { + return data.labels.map((label, i) => { + const meta = chart.getDatasetMeta(0); + const style = meta.controller.getStyle(i); + + return { + text: label, + fillStyle: style.backgroundColor, + fontColor: color, + hidden: !chart.getDataVisibility(i), + lineDash: style.borderDash, + lineDashOffset: style.borderDashOffset, + lineJoin: style.borderJoinStyle, + lineWidth: style.borderWidth, + strokeStyle: style.borderColor, + textAlign: textAlign, + pointStyle: pointStyle, + borderRadius: useBorderRadius && (borderRadius || style.borderRadius), + // Extra data used for toggling the correct item + index: i + }; + }); + } + return []; + } + }, + + onClick(e, legendItem, legend) { + legend.chart.toggleDataVisibility(legendItem.index); + legend.chart.update(); + } + } + } + }; + + constructor(chart, datasetIndex) { + super(chart, datasetIndex); + + this.enableOptionSharing = true; + this.innerRadius = undefined; + this.outerRadius = undefined; + this.offsetX = undefined; + this.offsetY = undefined; + } + + linkScales() {} + + /** + * Override data parsing, since we are not using scales + */ + parse(start, count) { + const data = this.getDataset().data; + const meta = this._cachedMeta; + + if (this._parsing === false) { + meta._parsed = data; + } else { + let getter = (i) => +data[i]; + + if (isObject(data[start])) { + const {key = 'value'} = this._parsing; + getter = (i) => +resolveObjectKey(data[i], key); + } + + let i, ilen; + for (i = start, ilen = start + count; i < ilen; ++i) { + meta._parsed[i] = getter(i); + } + } + } + + /** + * @private + */ + _getRotation() { + return toRadians(this.options.rotation - 90); + } + + /** + * @private + */ + _getCircumference() { + return toRadians(this.options.circumference); + } + + /** + * Get the maximal rotation & circumference extents + * across all visible datasets. + */ + _getRotationExtents() { + let min = TAU; + let max = -TAU; + + for (let i = 0; i < this.chart.data.datasets.length; ++i) { + if (this.chart.isDatasetVisible(i) && this.chart.getDatasetMeta(i).type === this._type) { + const controller = this.chart.getDatasetMeta(i).controller; + const rotation = controller._getRotation(); + const circumference = controller._getCircumference(); + + min = Math.min(min, rotation); + max = Math.max(max, rotation + circumference); + } + } + + return { + rotation: min, + circumference: max - min, + }; + } + + /** + * @param {string} mode + */ + update(mode) { + const chart = this.chart; + const {chartArea} = chart; + const meta = this._cachedMeta; + const arcs = meta.data; + const spacing = this.getMaxBorderWidth() + this.getMaxOffset(arcs) + this.options.spacing; + const maxSize = Math.max((Math.min(chartArea.width, chartArea.height) - spacing) / 2, 0); + const cutout = Math.min(toPercentage(this.options.cutout, maxSize), 1); + const chartWeight = this._getRingWeight(this.index); + + // Compute the maximal rotation & circumference limits. + // If we only consider our dataset, this can cause problems when two datasets + // are both less than a circle with different rotations (starting angles) + const {circumference, rotation} = this._getRotationExtents(); + const {ratioX, ratioY, offsetX, offsetY} = getRatioAndOffset(rotation, circumference, cutout); + const maxWidth = (chartArea.width - spacing) / ratioX; + const maxHeight = (chartArea.height - spacing) / ratioY; + const maxRadius = Math.max(Math.min(maxWidth, maxHeight) / 2, 0); + const outerRadius = toDimension(this.options.radius, maxRadius); + const innerRadius = Math.max(outerRadius * cutout, 0); + const radiusLength = (outerRadius - innerRadius) / this._getVisibleDatasetWeightTotal(); + this.offsetX = offsetX * outerRadius; + this.offsetY = offsetY * outerRadius; + + meta.total = this.calculateTotal(); + + this.outerRadius = outerRadius - radiusLength * this._getRingWeightOffset(this.index); + this.innerRadius = Math.max(this.outerRadius - radiusLength * chartWeight, 0); + + this.updateElements(arcs, 0, arcs.length, mode); + } + + /** + * @private + */ + _circumference(i, reset) { + const opts = this.options; + const meta = this._cachedMeta; + const circumference = this._getCircumference(); + if ((reset && opts.animation.animateRotate) || !this.chart.getDataVisibility(i) || meta._parsed[i] === null || meta.data[i].hidden) { + return 0; + } + return this.calculateCircumference(meta._parsed[i] * circumference / TAU); + } + + updateElements(arcs, start, count, mode) { + const reset = mode === 'reset'; + const chart = this.chart; + const chartArea = chart.chartArea; + const opts = chart.options; + const animationOpts = opts.animation; + const centerX = (chartArea.left + chartArea.right) / 2; + const centerY = (chartArea.top + chartArea.bottom) / 2; + const animateScale = reset && animationOpts.animateScale; + const innerRadius = animateScale ? 0 : this.innerRadius; + const outerRadius = animateScale ? 0 : this.outerRadius; + const {sharedOptions, includeOptions} = this._getSharedOptions(start, mode); + let startAngle = this._getRotation(); + let i; + + for (i = 0; i < start; ++i) { + startAngle += this._circumference(i, reset); + } + + for (i = start; i < start + count; ++i) { + const circumference = this._circumference(i, reset); + const arc = arcs[i]; + const properties = { + x: centerX + this.offsetX, + y: centerY + this.offsetY, + startAngle, + endAngle: startAngle + circumference, + circumference, + outerRadius, + innerRadius + }; + if (includeOptions) { + properties.options = sharedOptions || this.resolveDataElementOptions(i, arc.active ? 'active' : mode); + } + startAngle += circumference; + + this.updateElement(arc, i, properties, mode); + } + } + + calculateTotal() { + const meta = this._cachedMeta; + const metaData = meta.data; + let total = 0; + let i; + + for (i = 0; i < metaData.length; i++) { + const value = meta._parsed[i]; + if (value !== null && !isNaN(value) && this.chart.getDataVisibility(i) && !metaData[i].hidden) { + total += Math.abs(value); + } + } + + return total; + } + + calculateCircumference(value) { + const total = this._cachedMeta.total; + if (total > 0 && !isNaN(value)) { + return TAU * (Math.abs(value) / total); + } + return 0; + } + + getLabelAndValue(index) { + const meta = this._cachedMeta; + const chart = this.chart; + const labels = chart.data.labels || []; + const value = formatNumber(meta._parsed[index], chart.options.locale); + + return { + label: labels[index] || '', + value, + }; + } + + getMaxBorderWidth(arcs) { + let max = 0; + const chart = this.chart; + let i, ilen, meta, controller, options; + + if (!arcs) { + // Find the outmost visible dataset + for (i = 0, ilen = chart.data.datasets.length; i < ilen; ++i) { + if (chart.isDatasetVisible(i)) { + meta = chart.getDatasetMeta(i); + arcs = meta.data; + controller = meta.controller; + break; + } + } + } + + if (!arcs) { + return 0; + } + + for (i = 0, ilen = arcs.length; i < ilen; ++i) { + options = controller.resolveDataElementOptions(i); + if (options.borderAlign !== 'inner') { + max = Math.max(max, options.borderWidth || 0, options.hoverBorderWidth || 0); + } + } + return max; + } + + getMaxOffset(arcs) { + let max = 0; + + for (let i = 0, ilen = arcs.length; i < ilen; ++i) { + const options = this.resolveDataElementOptions(i); + max = Math.max(max, options.offset || 0, options.hoverOffset || 0); + } + return max; + } + + /** + * Get radius length offset of the dataset in relation to the visible datasets weights. This allows determining the inner and outer radius correctly + * @private + */ + _getRingWeightOffset(datasetIndex) { + let ringWeightOffset = 0; + + for (let i = 0; i < datasetIndex; ++i) { + if (this.chart.isDatasetVisible(i)) { + ringWeightOffset += this._getRingWeight(i); + } + } + + return ringWeightOffset; + } + + /** + * @private + */ + _getRingWeight(datasetIndex) { + return Math.max(valueOrDefault(this.chart.data.datasets[datasetIndex].weight, 1), 0); + } + + /** + * Returns the sum of all visible data set weights. + * @private + */ + _getVisibleDatasetWeightTotal() { + return this._getRingWeightOffset(this.chart.data.datasets.length) || 1; + } +} diff --git a/src/controllers/controller.line.js b/src/controllers/controller.line.js new file mode 100644 index 00000000000..fddd5ce9889 --- /dev/null +++ b/src/controllers/controller.line.js @@ -0,0 +1,143 @@ +import DatasetController from '../core/core.datasetController.js'; +import {isNullOrUndef} from '../helpers/index.js'; +import {isNumber} from '../helpers/helpers.math.js'; +import {_getStartAndCountOfVisiblePoints, _scaleRangesChanged} from '../helpers/helpers.extras.js'; + +export default class LineController extends DatasetController { + + static id = 'line'; + + /** + * @type {any} + */ + static defaults = { + datasetElementType: 'line', + dataElementType: 'point', + + showLine: true, + spanGaps: false, + }; + + /** + * @type {any} + */ + static overrides = { + scales: { + _index_: { + type: 'category', + }, + _value_: { + type: 'linear', + }, + } + }; + + initialize() { + this.enableOptionSharing = true; + this.supportsDecimation = true; + super.initialize(); + } + + update(mode) { + const meta = this._cachedMeta; + const {dataset: line, data: points = [], _dataset} = meta; + // @ts-ignore + const animationsDisabled = this.chart._animationsDisabled; + let {start, count} = _getStartAndCountOfVisiblePoints(meta, points, animationsDisabled); + + this._drawStart = start; + this._drawCount = count; + + if (_scaleRangesChanged(meta)) { + start = 0; + count = points.length; + } + + // Update Line + line._chart = this.chart; + line._datasetIndex = this.index; + line._decimated = !!_dataset._decimated; + line.points = points; + + const options = this.resolveDatasetElementOptions(mode); + if (!this.options.showLine) { + options.borderWidth = 0; + } + options.segment = this.options.segment; + this.updateElement(line, undefined, { + animated: !animationsDisabled, + options + }, mode); + + // Update Points + this.updateElements(points, start, count, mode); + } + + updateElements(points, start, count, mode) { + const reset = mode === 'reset'; + const {iScale, vScale, _stacked, _dataset} = this._cachedMeta; + const {sharedOptions, includeOptions} = this._getSharedOptions(start, mode); + const iAxis = iScale.axis; + const vAxis = vScale.axis; + const {spanGaps, segment} = this.options; + const maxGapLength = isNumber(spanGaps) ? spanGaps : Number.POSITIVE_INFINITY; + const directUpdate = this.chart._animationsDisabled || reset || mode === 'none'; + const end = start + count; + const pointsCount = points.length; + let prevParsed = start > 0 && this.getParsed(start - 1); + + for (let i = 0; i < pointsCount; ++i) { + const point = points[i]; + const properties = directUpdate ? point : {}; + + if (i < start || i >= end) { + properties.skip = true; + continue; + } + + const parsed = this.getParsed(i); + const nullData = isNullOrUndef(parsed[vAxis]); + const iPixel = properties[iAxis] = iScale.getPixelForValue(parsed[iAxis], i); + const vPixel = properties[vAxis] = reset || nullData ? vScale.getBasePixel() : vScale.getPixelForValue(_stacked ? this.applyStack(vScale, parsed, _stacked) : parsed[vAxis], i); + + properties.skip = isNaN(iPixel) || isNaN(vPixel) || nullData; + properties.stop = i > 0 && (Math.abs(parsed[iAxis] - prevParsed[iAxis])) > maxGapLength; + if (segment) { + properties.parsed = parsed; + properties.raw = _dataset.data[i]; + } + + if (includeOptions) { + properties.options = sharedOptions || this.resolveDataElementOptions(i, point.active ? 'active' : mode); + } + + if (!directUpdate) { + this.updateElement(point, i, properties, mode); + } + + prevParsed = parsed; + } + } + + /** + * @protected + */ + getMaxOverflow() { + const meta = this._cachedMeta; + const dataset = meta.dataset; + const border = dataset.options && dataset.options.borderWidth || 0; + const data = meta.data || []; + if (!data.length) { + return border; + } + const firstPoint = data[0].size(this.resolveDataElementOptions(0)); + const lastPoint = data[data.length - 1].size(this.resolveDataElementOptions(data.length - 1)); + return Math.max(border, firstPoint, lastPoint) / 2; + } + + draw() { + const meta = this._cachedMeta; + meta.dataset.updateControlPoints(this.chart.chartArea, meta.iScale.axis); + super.draw(); + } +} diff --git a/src/controllers/controller.pie.js b/src/controllers/controller.pie.js new file mode 100644 index 00000000000..9390c4a6ca8 --- /dev/null +++ b/src/controllers/controller.pie.js @@ -0,0 +1,24 @@ +import DoughnutController from './controller.doughnut.js'; + +// Pie charts are Doughnut chart with different defaults +export default class PieController extends DoughnutController { + + static id = 'pie'; + + /** + * @type {any} + */ + static defaults = { + // The percentage of the chart that we cut out of the middle. + cutout: 0, + + // The rotation of the chart, where the first data arc begins. + rotation: 0, + + // The total circumference of the chart. + circumference: 360, + + // The outer radius of the chart + radius: '100%' + }; +} diff --git a/src/controllers/controller.polarArea.js b/src/controllers/controller.polarArea.js new file mode 100644 index 00000000000..9514cf7c7c7 --- /dev/null +++ b/src/controllers/controller.polarArea.js @@ -0,0 +1,227 @@ +import DatasetController from '../core/core.datasetController.js'; +import {toRadians, PI, formatNumber, _parseObjectDataRadialScale} from '../helpers/index.js'; + +export default class PolarAreaController extends DatasetController { + + static id = 'polarArea'; + + /** + * @type {any} + */ + static defaults = { + dataElementType: 'arc', + animation: { + animateRotate: true, + animateScale: true + }, + animations: { + numbers: { + type: 'number', + properties: ['x', 'y', 'startAngle', 'endAngle', 'innerRadius', 'outerRadius'] + }, + }, + indexAxis: 'r', + startAngle: 0, + }; + + /** + * @type {any} + */ + static overrides = { + aspectRatio: 1, + + plugins: { + legend: { + labels: { + generateLabels(chart) { + const data = chart.data; + if (data.labels.length && data.datasets.length) { + const {labels: {pointStyle, color}} = chart.legend.options; + + return data.labels.map((label, i) => { + const meta = chart.getDatasetMeta(0); + const style = meta.controller.getStyle(i); + + return { + text: label, + fillStyle: style.backgroundColor, + strokeStyle: style.borderColor, + fontColor: color, + lineWidth: style.borderWidth, + pointStyle: pointStyle, + hidden: !chart.getDataVisibility(i), + + // Extra data used for toggling the correct item + index: i + }; + }); + } + return []; + } + }, + + onClick(e, legendItem, legend) { + legend.chart.toggleDataVisibility(legendItem.index); + legend.chart.update(); + } + } + }, + + scales: { + r: { + type: 'radialLinear', + angleLines: { + display: false + }, + beginAtZero: true, + grid: { + circular: true + }, + pointLabels: { + display: false + }, + startAngle: 0 + } + } + }; + + constructor(chart, datasetIndex) { + super(chart, datasetIndex); + + this.innerRadius = undefined; + this.outerRadius = undefined; + } + + getLabelAndValue(index) { + const meta = this._cachedMeta; + const chart = this.chart; + const labels = chart.data.labels || []; + const value = formatNumber(meta._parsed[index].r, chart.options.locale); + + return { + label: labels[index] || '', + value, + }; + } + + parseObjectData(meta, data, start, count) { + return _parseObjectDataRadialScale.bind(this)(meta, data, start, count); + } + + update(mode) { + const arcs = this._cachedMeta.data; + + this._updateRadius(); + this.updateElements(arcs, 0, arcs.length, mode); + } + + /** + * @protected + */ + getMinMax() { + const meta = this._cachedMeta; + const range = {min: Number.POSITIVE_INFINITY, max: Number.NEGATIVE_INFINITY}; + + meta.data.forEach((element, index) => { + const parsed = this.getParsed(index).r; + + if (!isNaN(parsed) && this.chart.getDataVisibility(index)) { + if (parsed < range.min) { + range.min = parsed; + } + + if (parsed > range.max) { + range.max = parsed; + } + } + }); + + return range; + } + + /** + * @private + */ + _updateRadius() { + const chart = this.chart; + const chartArea = chart.chartArea; + const opts = chart.options; + const minSize = Math.min(chartArea.right - chartArea.left, chartArea.bottom - chartArea.top); + + const outerRadius = Math.max(minSize / 2, 0); + const innerRadius = Math.max(opts.cutoutPercentage ? (outerRadius / 100) * (opts.cutoutPercentage) : 1, 0); + const radiusLength = (outerRadius - innerRadius) / chart.getVisibleDatasetCount(); + + this.outerRadius = outerRadius - (radiusLength * this.index); + this.innerRadius = this.outerRadius - radiusLength; + } + + updateElements(arcs, start, count, mode) { + const reset = mode === 'reset'; + const chart = this.chart; + const opts = chart.options; + const animationOpts = opts.animation; + const scale = this._cachedMeta.rScale; + const centerX = scale.xCenter; + const centerY = scale.yCenter; + const datasetStartAngle = scale.getIndexAngle(0) - 0.5 * PI; + let angle = datasetStartAngle; + let i; + + const defaultAngle = 360 / this.countVisibleElements(); + + for (i = 0; i < start; ++i) { + angle += this._computeAngle(i, mode, defaultAngle); + } + for (i = start; i < start + count; i++) { + const arc = arcs[i]; + let startAngle = angle; + let endAngle = angle + this._computeAngle(i, mode, defaultAngle); + let outerRadius = chart.getDataVisibility(i) ? scale.getDistanceFromCenterForValue(this.getParsed(i).r) : 0; + angle = endAngle; + + if (reset) { + if (animationOpts.animateScale) { + outerRadius = 0; + } + if (animationOpts.animateRotate) { + startAngle = endAngle = datasetStartAngle; + } + } + + const properties = { + x: centerX, + y: centerY, + innerRadius: 0, + outerRadius, + startAngle, + endAngle, + options: this.resolveDataElementOptions(i, arc.active ? 'active' : mode) + }; + + this.updateElement(arc, i, properties, mode); + } + } + + countVisibleElements() { + const meta = this._cachedMeta; + let count = 0; + + meta.data.forEach((element, index) => { + if (!isNaN(this.getParsed(index).r) && this.chart.getDataVisibility(index)) { + count++; + } + }); + + return count; + } + + /** + * @private + */ + _computeAngle(index, mode, defaultAngle) { + return this.chart.getDataVisibility(index) + ? toRadians(this.resolveDataElementOptions(index, mode).angle || defaultAngle) + : 0; + } +} diff --git a/src/controllers/controller.radar.js b/src/controllers/controller.radar.js new file mode 100644 index 00000000000..d702a3c4457 --- /dev/null +++ b/src/controllers/controller.radar.js @@ -0,0 +1,104 @@ +import DatasetController from '../core/core.datasetController.js'; +import {_parseObjectDataRadialScale} from '../helpers/index.js'; + +export default class RadarController extends DatasetController { + + static id = 'radar'; + + /** + * @type {any} + */ + static defaults = { + datasetElementType: 'line', + dataElementType: 'point', + indexAxis: 'r', + showLine: true, + elements: { + line: { + fill: 'start' + } + }, + }; + + /** + * @type {any} + */ + static overrides = { + aspectRatio: 1, + + scales: { + r: { + type: 'radialLinear', + } + } + }; + + /** + * @protected + */ + getLabelAndValue(index) { + const vScale = this._cachedMeta.vScale; + const parsed = this.getParsed(index); + + return { + label: vScale.getLabels()[index], + value: '' + vScale.getLabelForValue(parsed[vScale.axis]) + }; + } + + parseObjectData(meta, data, start, count) { + return _parseObjectDataRadialScale.bind(this)(meta, data, start, count); + } + + update(mode) { + const meta = this._cachedMeta; + const line = meta.dataset; + const points = meta.data || []; + const labels = meta.iScale.getLabels(); + + // Update Line + line.points = points; + // In resize mode only point locations change, so no need to set the points or options. + if (mode !== 'resize') { + const options = this.resolveDatasetElementOptions(mode); + if (!this.options.showLine) { + options.borderWidth = 0; + } + + const properties = { + _loop: true, + _fullLoop: labels.length === points.length, + options + }; + + this.updateElement(line, undefined, properties, mode); + } + + // Update Points + this.updateElements(points, 0, points.length, mode); + } + + updateElements(points, start, count, mode) { + const scale = this._cachedMeta.rScale; + const reset = mode === 'reset'; + + for (let i = start; i < start + count; i++) { + const point = points[i]; + const options = this.resolveDataElementOptions(i, point.active ? 'active' : mode); + const pointPosition = scale.getPointPositionForValue(i, this.getParsed(i).r); + + const x = reset ? scale.xCenter : pointPosition.x; + const y = reset ? scale.yCenter : pointPosition.y; + + const properties = { + x, + y, + angle: pointPosition.angle, + skip: isNaN(x) || isNaN(y), + options + }; + + this.updateElement(point, i, properties, mode); + } + } +} diff --git a/src/controllers/controller.scatter.js b/src/controllers/controller.scatter.js new file mode 100644 index 00000000000..15445ea8ae3 --- /dev/null +++ b/src/controllers/controller.scatter.js @@ -0,0 +1,179 @@ +import DatasetController from '../core/core.datasetController.js'; +import {isNullOrUndef} from '../helpers/index.js'; +import {isNumber} from '../helpers/helpers.math.js'; +import {_getStartAndCountOfVisiblePoints, _scaleRangesChanged} from '../helpers/helpers.extras.js'; + +export default class ScatterController extends DatasetController { + + static id = 'scatter'; + + /** + * @type {any} + */ + static defaults = { + datasetElementType: false, + dataElementType: 'point', + showLine: false, + fill: false + }; + + /** + * @type {any} + */ + static overrides = { + + interaction: { + mode: 'point' + }, + + scales: { + x: { + type: 'linear' + }, + y: { + type: 'linear' + } + } + }; + + /** + * @protected + */ + getLabelAndValue(index) { + const meta = this._cachedMeta; + const labels = this.chart.data.labels || []; + const {xScale, yScale} = meta; + const parsed = this.getParsed(index); + const x = xScale.getLabelForValue(parsed.x); + const y = yScale.getLabelForValue(parsed.y); + + return { + label: labels[index] || '', + value: '(' + x + ', ' + y + ')' + }; + } + + update(mode) { + const meta = this._cachedMeta; + const {data: points = []} = meta; + // @ts-ignore + const animationsDisabled = this.chart._animationsDisabled; + let {start, count} = _getStartAndCountOfVisiblePoints(meta, points, animationsDisabled); + + this._drawStart = start; + this._drawCount = count; + + if (_scaleRangesChanged(meta)) { + start = 0; + count = points.length; + } + + if (this.options.showLine) { + + // https://github.com/chartjs/Chart.js/issues/11333 + if (!this.datasetElementType) { + this.addElements(); + } + const {dataset: line, _dataset} = meta; + + // Update Line + line._chart = this.chart; + line._datasetIndex = this.index; + line._decimated = !!_dataset._decimated; + line.points = points; + + const options = this.resolveDatasetElementOptions(mode); + options.segment = this.options.segment; + this.updateElement(line, undefined, { + animated: !animationsDisabled, + options + }, mode); + } else if (this.datasetElementType) { + // https://github.com/chartjs/Chart.js/issues/11333 + delete meta.dataset; + this.datasetElementType = false; + } + + // Update Points + this.updateElements(points, start, count, mode); + } + + addElements() { + const {showLine} = this.options; + + if (!this.datasetElementType && showLine) { + this.datasetElementType = this.chart.registry.getElement('line'); + } + + super.addElements(); + } + + updateElements(points, start, count, mode) { + const reset = mode === 'reset'; + const {iScale, vScale, _stacked, _dataset} = this._cachedMeta; + const firstOpts = this.resolveDataElementOptions(start, mode); + const sharedOptions = this.getSharedOptions(firstOpts); + const includeOptions = this.includeOptions(mode, sharedOptions); + const iAxis = iScale.axis; + const vAxis = vScale.axis; + const {spanGaps, segment} = this.options; + const maxGapLength = isNumber(spanGaps) ? spanGaps : Number.POSITIVE_INFINITY; + const directUpdate = this.chart._animationsDisabled || reset || mode === 'none'; + let prevParsed = start > 0 && this.getParsed(start - 1); + + for (let i = start; i < start + count; ++i) { + const point = points[i]; + const parsed = this.getParsed(i); + const properties = directUpdate ? point : {}; + const nullData = isNullOrUndef(parsed[vAxis]); + const iPixel = properties[iAxis] = iScale.getPixelForValue(parsed[iAxis], i); + const vPixel = properties[vAxis] = reset || nullData ? vScale.getBasePixel() : vScale.getPixelForValue(_stacked ? this.applyStack(vScale, parsed, _stacked) : parsed[vAxis], i); + + properties.skip = isNaN(iPixel) || isNaN(vPixel) || nullData; + properties.stop = i > 0 && (Math.abs(parsed[iAxis] - prevParsed[iAxis])) > maxGapLength; + if (segment) { + properties.parsed = parsed; + properties.raw = _dataset.data[i]; + } + + if (includeOptions) { + properties.options = sharedOptions || this.resolveDataElementOptions(i, point.active ? 'active' : mode); + } + + if (!directUpdate) { + this.updateElement(point, i, properties, mode); + } + + prevParsed = parsed; + } + + this.updateSharedOptions(sharedOptions, mode, firstOpts); + } + + /** + * @protected + */ + getMaxOverflow() { + const meta = this._cachedMeta; + const data = meta.data || []; + + if (!this.options.showLine) { + let max = 0; + for (let i = data.length - 1; i >= 0; --i) { + max = Math.max(max, data[i].size(this.resolveDataElementOptions(i)) / 2); + } + return max > 0 && max; + } + + const dataset = meta.dataset; + const border = dataset.options && dataset.options.borderWidth || 0; + + if (!data.length) { + return border; + } + + const firstPoint = data[0].size(this.resolveDataElementOptions(0)); + const lastPoint = data[data.length - 1].size(this.resolveDataElementOptions(data.length - 1)); + return Math.max(border, firstPoint, lastPoint) / 2; + } +} diff --git a/src/controllers/index.js b/src/controllers/index.js new file mode 100644 index 00000000000..9d265dc9dc6 --- /dev/null +++ b/src/controllers/index.js @@ -0,0 +1,8 @@ +export {default as BarController} from './controller.bar.js'; +export {default as BubbleController} from './controller.bubble.js'; +export {default as DoughnutController} from './controller.doughnut.js'; +export {default as LineController} from './controller.line.js'; +export {default as PolarAreaController} from './controller.polarArea.js'; +export {default as PieController} from './controller.pie.js'; +export {default as RadarController} from './controller.radar.js'; +export {default as ScatterController} from './controller.scatter.js'; diff --git a/src/core/core.adapters.ts b/src/core/core.adapters.ts new file mode 100644 index 00000000000..282d13699f1 --- /dev/null +++ b/src/core/core.adapters.ts @@ -0,0 +1,138 @@ +/** + * @namespace Chart._adapters + * @since 2.8.0 + * @private + */ + +import type {AnyObject} from '../types/basic.js'; +import type {ChartOptions} from '../types/index.js'; + +export type TimeUnit = 'millisecond' | 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year'; + +export interface DateAdapter { + readonly options: T; + /** + * Will called with chart options after adapter creation. + */ + init(this: DateAdapter, chartOptions: ChartOptions): void; + /** + * Returns a map of time formats for the supported formatting units defined + * in Unit as well as 'datetime' representing a detailed date/time string. + */ + formats(this: DateAdapter): Record; + /** + * Parses the given `value` and return the associated timestamp. + * @param value - the value to parse (usually comes from the data) + * @param [format] - the expected data format + */ + parse(this: DateAdapter, value: unknown, format?: string): number | null; + /** + * Returns the formatted date in the specified `format` for a given `timestamp`. + * @param timestamp - the timestamp to format + * @param format - the date/time token + */ + format(this: DateAdapter, timestamp: number, format: string): string; + /** + * Adds the specified `amount` of `unit` to the given `timestamp`. + * @param timestamp - the input timestamp + * @param amount - the amount to add + * @param unit - the unit as string + */ + add(this: DateAdapter, timestamp: number, amount: number, unit: TimeUnit): number; + /** + * Returns the number of `unit` between the given timestamps. + * @param a - the input timestamp (reference) + * @param b - the timestamp to subtract + * @param unit - the unit as string + */ + diff(this: DateAdapter, a: number, b: number, unit: TimeUnit): number; + /** + * Returns start of `unit` for the given `timestamp`. + * @param timestamp - the input timestamp + * @param unit - the unit as string + * @param [weekday] - the ISO day of the week with 1 being Monday + * and 7 being Sunday (only needed if param *unit* is `isoWeek`). + */ + startOf(this: DateAdapter, timestamp: number, unit: TimeUnit | 'isoWeek', weekday?: number | boolean): number; + /** + * Returns end of `unit` for the given `timestamp`. + * @param timestamp - the input timestamp + * @param unit - the unit as string + */ + endOf(this: DateAdapter, timestamp: number, unit: TimeUnit): number; +} + +function abstract(): T { + throw new Error('This method is not implemented: Check that a complete date adapter is provided.'); +} + +/** + * Date adapter (current used by the time scale) + * @namespace Chart._adapters._date + * @memberof Chart._adapters + * @private + */ +class DateAdapterBase implements DateAdapter { + + /** + * Override default date adapter methods. + * Accepts type parameter to define options type. + * @example + * Chart._adapters._date.override<{myAdapterOption: string}>({ + * init() { + * console.log(this.options.myAdapterOption); + * } + * }) + */ + static override( + members: Partial, 'options'>> + ) { + Object.assign(DateAdapterBase.prototype, members); + } + + readonly options: AnyObject; + + constructor(options?: AnyObject) { + this.options = options || {}; + } + + // eslint-disable-next-line @typescript-eslint/no-empty-function + init() {} + + formats(): Record { + return abstract(); + } + + parse(): number | null { + return abstract(); + } + + format(): string { + return abstract(); + } + + add(): number { + return abstract(); + } + + diff(): number { + return abstract(); + } + + startOf(): number { + return abstract(); + } + + endOf(): number { + return abstract(); + } +} + +export default { + _date: DateAdapterBase as { + new (options?: AnyObject): DateAdapter; + override( + members: Partial, 'options'>> + ): void; + } +}; diff --git a/src/core/core.animation.js b/src/core/core.animation.js new file mode 100644 index 00000000000..eca21f63584 --- /dev/null +++ b/src/core/core.animation.js @@ -0,0 +1,119 @@ +import effects from '../helpers/helpers.easing.js'; +import {resolve} from '../helpers/helpers.options.js'; +import {color as helpersColor} from '../helpers/helpers.color.js'; + +const transparent = 'transparent'; +const interpolators = { + boolean(from, to, factor) { + return factor > 0.5 ? to : from; + }, + /** + * @param {string} from + * @param {string} to + * @param {number} factor + */ + color(from, to, factor) { + const c0 = helpersColor(from || transparent); + const c1 = c0.valid && helpersColor(to || transparent); + return c1 && c1.valid + ? c1.mix(c0, factor).hexString() + : to; + }, + number(from, to, factor) { + return from + (to - from) * factor; + } +}; + +export default class Animation { + constructor(cfg, target, prop, to) { + const currentValue = target[prop]; + + to = resolve([cfg.to, to, currentValue, cfg.from]); + const from = resolve([cfg.from, currentValue, to]); + + this._active = true; + this._fn = cfg.fn || interpolators[cfg.type || typeof from]; + this._easing = effects[cfg.easing] || effects.linear; + this._start = Math.floor(Date.now() + (cfg.delay || 0)); + this._duration = this._total = Math.floor(cfg.duration); + this._loop = !!cfg.loop; + this._target = target; + this._prop = prop; + this._from = from; + this._to = to; + this._promises = undefined; + } + + active() { + return this._active; + } + + update(cfg, to, date) { + if (this._active) { + this._notify(false); + + const currentValue = this._target[this._prop]; + const elapsed = date - this._start; + const remain = this._duration - elapsed; + this._start = date; + this._duration = Math.floor(Math.max(remain, cfg.duration)); + this._total += elapsed; + this._loop = !!cfg.loop; + this._to = resolve([cfg.to, to, currentValue, cfg.from]); + this._from = resolve([cfg.from, currentValue, to]); + } + } + + cancel() { + if (this._active) { + // update current evaluated value, for smoother animations + this.tick(Date.now()); + this._active = false; + this._notify(false); + } + } + + tick(date) { + const elapsed = date - this._start; + const duration = this._duration; + const prop = this._prop; + const from = this._from; + const loop = this._loop; + const to = this._to; + let factor; + + this._active = from !== to && (loop || (elapsed < duration)); + + if (!this._active) { + this._target[prop] = to; + this._notify(true); + return; + } + + if (elapsed < 0) { + this._target[prop] = from; + return; + } + + factor = (elapsed / duration) % 2; + factor = loop && factor > 1 ? 2 - factor : factor; + factor = this._easing(Math.min(1, Math.max(0, factor))); + + this._target[prop] = this._fn(from, to, factor); + } + + wait() { + const promises = this._promises || (this._promises = []); + return new Promise((res, rej) => { + promises.push({res, rej}); + }); + } + + _notify(resolved) { + const method = resolved ? 'res' : 'rej'; + const promises = this._promises || []; + for (let i = 0; i < promises.length; i++) { + promises[i][method](); + } + } +} diff --git a/src/core/core.animations.defaults.js b/src/core/core.animations.defaults.js new file mode 100644 index 00000000000..43501aba2c6 --- /dev/null +++ b/src/core/core.animations.defaults.js @@ -0,0 +1,72 @@ +const numbers = ['x', 'y', 'borderWidth', 'radius', 'tension']; +const colors = ['color', 'borderColor', 'backgroundColor']; + +export function applyAnimationsDefaults(defaults) { + defaults.set('animation', { + delay: undefined, + duration: 1000, + easing: 'easeOutQuart', + fn: undefined, + from: undefined, + loop: undefined, + to: undefined, + type: undefined, + }); + + defaults.describe('animation', { + _fallback: false, + _indexable: false, + _scriptable: (name) => name !== 'onProgress' && name !== 'onComplete' && name !== 'fn', + }); + + defaults.set('animations', { + colors: { + type: 'color', + properties: colors + }, + numbers: { + type: 'number', + properties: numbers + }, + }); + + defaults.describe('animations', { + _fallback: 'animation', + }); + + defaults.set('transitions', { + active: { + animation: { + duration: 400 + } + }, + resize: { + animation: { + duration: 0 + } + }, + show: { + animations: { + colors: { + from: 'transparent' + }, + visible: { + type: 'boolean', + duration: 0 // show immediately + }, + } + }, + hide: { + animations: { + colors: { + to: 'transparent' + }, + visible: { + type: 'boolean', + easing: 'linear', + fn: v => v | 0 // for keeping the dataset visible all the way through the animation + }, + } + } + }); +} diff --git a/src/core/core.animations.js b/src/core/core.animations.js new file mode 100644 index 00000000000..4ee61b84b0d --- /dev/null +++ b/src/core/core.animations.js @@ -0,0 +1,162 @@ +import animator from './core.animator.js'; +import Animation from './core.animation.js'; +import defaults from './core.defaults.js'; +import {isArray, isObject} from '../helpers/helpers.core.js'; + +export default class Animations { + constructor(chart, config) { + this._chart = chart; + this._properties = new Map(); + this.configure(config); + } + + configure(config) { + if (!isObject(config)) { + return; + } + + const animationOptions = Object.keys(defaults.animation); + const animatedProps = this._properties; + + Object.getOwnPropertyNames(config).forEach(key => { + const cfg = config[key]; + if (!isObject(cfg)) { + return; + } + const resolved = {}; + for (const option of animationOptions) { + resolved[option] = cfg[option]; + } + + (isArray(cfg.properties) && cfg.properties || [key]).forEach((prop) => { + if (prop === key || !animatedProps.has(prop)) { + animatedProps.set(prop, resolved); + } + }); + }); + } + + /** + * Utility to handle animation of `options`. + * @private + */ + _animateOptions(target, values) { + const newOptions = values.options; + const options = resolveTargetOptions(target, newOptions); + if (!options) { + return []; + } + + const animations = this._createAnimations(options, newOptions); + if (newOptions.$shared) { + // Going to shared options: + // After all animations are done, assign the shared options object to the element + // So any new updates to the shared options are observed + awaitAll(target.options.$animations, newOptions).then(() => { + target.options = newOptions; + }, () => { + // rejected, noop + }); + } + + return animations; + } + + /** + * @private + */ + _createAnimations(target, values) { + const animatedProps = this._properties; + const animations = []; + const running = target.$animations || (target.$animations = {}); + const props = Object.keys(values); + const date = Date.now(); + let i; + + for (i = props.length - 1; i >= 0; --i) { + const prop = props[i]; + if (prop.charAt(0) === '$') { + continue; + } + + if (prop === 'options') { + animations.push(...this._animateOptions(target, values)); + continue; + } + const value = values[prop]; + let animation = running[prop]; + const cfg = animatedProps.get(prop); + + if (animation) { + if (cfg && animation.active()) { + // There is an existing active animation, let's update that + animation.update(cfg, value, date); + continue; + } else { + animation.cancel(); + } + } + if (!cfg || !cfg.duration) { + // not animated, set directly to new value + target[prop] = value; + continue; + } + + running[prop] = animation = new Animation(cfg, target, prop, value); + animations.push(animation); + } + return animations; + } + + + /** + * Update `target` properties to new values, using configured animations + * @param {object} target - object to update + * @param {object} values - new target properties + * @returns {boolean|undefined} - `true` if animations were started + **/ + update(target, values) { + if (this._properties.size === 0) { + // Nothing is animated, just apply the new values. + Object.assign(target, values); + return; + } + + const animations = this._createAnimations(target, values); + + if (animations.length) { + animator.add(this._chart, animations); + return true; + } + } +} + +function awaitAll(animations, properties) { + const running = []; + const keys = Object.keys(properties); + for (let i = 0; i < keys.length; i++) { + const anim = animations[keys[i]]; + if (anim && anim.active()) { + running.push(anim.wait()); + } + } + // @ts-ignore + return Promise.all(running); +} + +function resolveTargetOptions(target, newOptions) { + if (!newOptions) { + return; + } + let options = target.options; + if (!options) { + target.options = newOptions; + return; + } + if (options.$shared) { + // Going from shared options to distinct one: + // Create new options object containing the old shared values and start updating that. + target.options = options = Object.assign({}, options, {$shared: false, $animations: {}}); + } + return options; +} diff --git a/src/core/core.animator.js b/src/core/core.animator.js new file mode 100644 index 00000000000..1a93e83e71d --- /dev/null +++ b/src/core/core.animator.js @@ -0,0 +1,214 @@ +import {requestAnimFrame} from '../helpers/helpers.extras.js'; + +/** + * @typedef { import('./core.animation.js').default } Animation + * @typedef { import('./core.controller.js').default } Chart + */ + +/** + * Please use the module's default export which provides a singleton instance + * Note: class is export for typedoc + */ +export class Animator { + constructor() { + this._request = null; + this._charts = new Map(); + this._running = false; + this._lastDate = undefined; + } + + /** + * @private + */ + _notify(chart, anims, date, type) { + const callbacks = anims.listeners[type]; + const numSteps = anims.duration; + + callbacks.forEach(fn => fn({ + chart, + initial: anims.initial, + numSteps, + currentStep: Math.min(date - anims.start, numSteps) + })); + } + + /** + * @private + */ + _refresh() { + if (this._request) { + return; + } + this._running = true; + + this._request = requestAnimFrame.call(window, () => { + this._update(); + this._request = null; + + if (this._running) { + this._refresh(); + } + }); + } + + /** + * @private + */ + _update(date = Date.now()) { + let remaining = 0; + + this._charts.forEach((anims, chart) => { + if (!anims.running || !anims.items.length) { + return; + } + const items = anims.items; + let i = items.length - 1; + let draw = false; + let item; + + for (; i >= 0; --i) { + item = items[i]; + + if (item._active) { + if (item._total > anims.duration) { + // if the animation has been updated and its duration prolonged, + // update to total duration of current animations run (for progress event) + anims.duration = item._total; + } + item.tick(date); + draw = true; + } else { + // Remove the item by replacing it with last item and removing the last + // A lot faster than splice. + items[i] = items[items.length - 1]; + items.pop(); + } + } + + if (draw) { + chart.draw(); + this._notify(chart, anims, date, 'progress'); + } + + if (!items.length) { + anims.running = false; + this._notify(chart, anims, date, 'complete'); + anims.initial = false; + } + + remaining += items.length; + }); + + this._lastDate = date; + + if (remaining === 0) { + this._running = false; + } + } + + /** + * @private + */ + _getAnims(chart) { + const charts = this._charts; + let anims = charts.get(chart); + if (!anims) { + anims = { + running: false, + initial: true, + items: [], + listeners: { + complete: [], + progress: [] + } + }; + charts.set(chart, anims); + } + return anims; + } + + /** + * @param {Chart} chart + * @param {string} event - event name + * @param {Function} cb - callback + */ + listen(chart, event, cb) { + this._getAnims(chart).listeners[event].push(cb); + } + + /** + * Add animations + * @param {Chart} chart + * @param {Animation[]} items - animations + */ + add(chart, items) { + if (!items || !items.length) { + return; + } + this._getAnims(chart).items.push(...items); + } + + /** + * Counts number of active animations for the chart + * @param {Chart} chart + */ + has(chart) { + return this._getAnims(chart).items.length > 0; + } + + /** + * Start animating (all charts) + * @param {Chart} chart + */ + start(chart) { + const anims = this._charts.get(chart); + if (!anims) { + return; + } + anims.running = true; + anims.start = Date.now(); + anims.duration = anims.items.reduce((acc, cur) => Math.max(acc, cur._duration), 0); + this._refresh(); + } + + running(chart) { + if (!this._running) { + return false; + } + const anims = this._charts.get(chart); + if (!anims || !anims.running || !anims.items.length) { + return false; + } + return true; + } + + /** + * Stop all animations for the chart + * @param {Chart} chart + */ + stop(chart) { + const anims = this._charts.get(chart); + if (!anims || !anims.items.length) { + return; + } + const items = anims.items; + let i = items.length - 1; + + for (; i >= 0; --i) { + items[i].cancel(); + } + anims.items = []; + this._notify(chart, anims, Date.now(), 'complete'); + } + + /** + * Remove chart from Animator + * @param {Chart} chart + */ + remove(chart) { + return this._charts.delete(chart); + } +} + +// singleton instance +export default /* #__PURE__ */ new Animator(); diff --git a/src/core/core.config.js b/src/core/core.config.js new file mode 100644 index 00000000000..6e83d5de327 --- /dev/null +++ b/src/core/core.config.js @@ -0,0 +1,418 @@ +import defaults, {overrides, descriptors} from './core.defaults.js'; +import {mergeIf, resolveObjectKey, isArray, isFunction, valueOrDefault, isObject} from '../helpers/helpers.core.js'; +import {_attachContext, _createResolver, _descriptors} from '../helpers/helpers.config.js'; + +export function getIndexAxis(type, options) { + const datasetDefaults = defaults.datasets[type] || {}; + const datasetOptions = (options.datasets || {})[type] || {}; + return datasetOptions.indexAxis || options.indexAxis || datasetDefaults.indexAxis || 'x'; +} + +function getAxisFromDefaultScaleID(id, indexAxis) { + let axis = id; + if (id === '_index_') { + axis = indexAxis; + } else if (id === '_value_') { + axis = indexAxis === 'x' ? 'y' : 'x'; + } + return axis; +} + +function getDefaultScaleIDFromAxis(axis, indexAxis) { + return axis === indexAxis ? '_index_' : '_value_'; +} + +function idMatchesAxis(id) { + if (id === 'x' || id === 'y' || id === 'r') { + return id; + } +} + +function axisFromPosition(position) { + if (position === 'top' || position === 'bottom') { + return 'x'; + } + if (position === 'left' || position === 'right') { + return 'y'; + } +} + +export function determineAxis(id, ...scaleOptions) { + if (idMatchesAxis(id)) { + return id; + } + for (const opts of scaleOptions) { + const axis = opts.axis + || axisFromPosition(opts.position) + || id.length > 1 && idMatchesAxis(id[0].toLowerCase()); + if (axis) { + return axis; + } + } + throw new Error(`Cannot determine type of '${id}' axis. Please provide 'axis' or 'position' option.`); +} + +function getAxisFromDataset(id, axis, dataset) { + if (dataset[axis + 'AxisID'] === id) { + return {axis}; + } +} + +function retrieveAxisFromDatasets(id, config) { + if (config.data && config.data.datasets) { + const boundDs = config.data.datasets.filter((d) => d.xAxisID === id || d.yAxisID === id); + if (boundDs.length) { + return getAxisFromDataset(id, 'x', boundDs[0]) || getAxisFromDataset(id, 'y', boundDs[0]); + } + } + return {}; +} + +function mergeScaleConfig(config, options) { + const chartDefaults = overrides[config.type] || {scales: {}}; + const configScales = options.scales || {}; + const chartIndexAxis = getIndexAxis(config.type, options); + const scales = Object.create(null); + + // First figure out first scale id's per axis. + Object.keys(configScales).forEach(id => { + const scaleConf = configScales[id]; + if (!isObject(scaleConf)) { + return console.error(`Invalid scale configuration for scale: ${id}`); + } + if (scaleConf._proxy) { + return console.warn(`Ignoring resolver passed as options for scale: ${id}`); + } + const axis = determineAxis(id, scaleConf, retrieveAxisFromDatasets(id, config), defaults.scales[scaleConf.type]); + const defaultId = getDefaultScaleIDFromAxis(axis, chartIndexAxis); + const defaultScaleOptions = chartDefaults.scales || {}; + scales[id] = mergeIf(Object.create(null), [{axis}, scaleConf, defaultScaleOptions[axis], defaultScaleOptions[defaultId]]); + }); + + // Then merge dataset defaults to scale configs + config.data.datasets.forEach(dataset => { + const type = dataset.type || config.type; + const indexAxis = dataset.indexAxis || getIndexAxis(type, options); + const datasetDefaults = overrides[type] || {}; + const defaultScaleOptions = datasetDefaults.scales || {}; + Object.keys(defaultScaleOptions).forEach(defaultID => { + const axis = getAxisFromDefaultScaleID(defaultID, indexAxis); + const id = dataset[axis + 'AxisID'] || axis; + scales[id] = scales[id] || Object.create(null); + mergeIf(scales[id], [{axis}, configScales[id], defaultScaleOptions[defaultID]]); + }); + }); + + // apply scale defaults, if not overridden by dataset defaults + Object.keys(scales).forEach(key => { + const scale = scales[key]; + mergeIf(scale, [defaults.scales[scale.type], defaults.scale]); + }); + + return scales; +} + +function initOptions(config) { + const options = config.options || (config.options = {}); + + options.plugins = valueOrDefault(options.plugins, {}); + options.scales = mergeScaleConfig(config, options); +} + +function initData(data) { + data = data || {}; + data.datasets = data.datasets || []; + data.labels = data.labels || []; + return data; +} + +function initConfig(config) { + config = config || {}; + config.data = initData(config.data); + + initOptions(config); + + return config; +} + +const keyCache = new Map(); +const keysCached = new Set(); + +function cachedKeys(cacheKey, generate) { + let keys = keyCache.get(cacheKey); + if (!keys) { + keys = generate(); + keyCache.set(cacheKey, keys); + keysCached.add(keys); + } + return keys; +} + +const addIfFound = (set, obj, key) => { + const opts = resolveObjectKey(obj, key); + if (opts !== undefined) { + set.add(opts); + } +}; + +export default class Config { + constructor(config) { + this._config = initConfig(config); + this._scopeCache = new Map(); + this._resolverCache = new Map(); + } + + get platform() { + return this._config.platform; + } + + get type() { + return this._config.type; + } + + set type(type) { + this._config.type = type; + } + + get data() { + return this._config.data; + } + + set data(data) { + this._config.data = initData(data); + } + + get options() { + return this._config.options; + } + + set options(options) { + this._config.options = options; + } + + get plugins() { + return this._config.plugins; + } + + update() { + const config = this._config; + this.clearCache(); + initOptions(config); + } + + clearCache() { + this._scopeCache.clear(); + this._resolverCache.clear(); + } + + /** + * Returns the option scope keys for resolving dataset options. + * These keys do not include the dataset itself, because it is not under options. + * @param {string} datasetType + * @return {string[][]} + */ + datasetScopeKeys(datasetType) { + return cachedKeys(datasetType, + () => [[ + `datasets.${datasetType}`, + '' + ]]); + } + + /** + * Returns the option scope keys for resolving dataset animation options. + * These keys do not include the dataset itself, because it is not under options. + * @param {string} datasetType + * @param {string} transition + * @return {string[][]} + */ + datasetAnimationScopeKeys(datasetType, transition) { + return cachedKeys(`${datasetType}.transition.${transition}`, + () => [ + [ + `datasets.${datasetType}.transitions.${transition}`, + `transitions.${transition}`, + ], + // The following are used for looking up the `animations` and `animation` keys + [ + `datasets.${datasetType}`, + '' + ] + ]); + } + + /** + * Returns the options scope keys for resolving element options that belong + * to an dataset. These keys do not include the dataset itself, because it + * is not under options. + * @param {string} datasetType + * @param {string} elementType + * @return {string[][]} + */ + datasetElementScopeKeys(datasetType, elementType) { + return cachedKeys(`${datasetType}-${elementType}`, + () => [[ + `datasets.${datasetType}.elements.${elementType}`, + `datasets.${datasetType}`, + `elements.${elementType}`, + '' + ]]); + } + + /** + * Returns the options scope keys for resolving plugin options. + * @param {{id: string, additionalOptionScopes?: string[]}} plugin + * @return {string[][]} + */ + pluginScopeKeys(plugin) { + const id = plugin.id; + const type = this.type; + return cachedKeys(`${type}-plugin-${id}`, + () => [[ + `plugins.${id}`, + ...plugin.additionalOptionScopes || [], + ]]); + } + + /** + * @private + */ + _cachedScopes(mainScope, resetCache) { + const _scopeCache = this._scopeCache; + let cache = _scopeCache.get(mainScope); + if (!cache || resetCache) { + cache = new Map(); + _scopeCache.set(mainScope, cache); + } + return cache; + } + + /** + * Resolves the objects from options and defaults for option value resolution. + * @param {object} mainScope - The main scope object for options + * @param {string[][]} keyLists - The arrays of keys in resolution order + * @param {boolean} [resetCache] - reset the cache for this mainScope + */ + getOptionScopes(mainScope, keyLists, resetCache) { + const {options, type} = this; + const cache = this._cachedScopes(mainScope, resetCache); + const cached = cache.get(keyLists); + if (cached) { + return cached; + } + + const scopes = new Set(); + + keyLists.forEach(keys => { + if (mainScope) { + scopes.add(mainScope); + keys.forEach(key => addIfFound(scopes, mainScope, key)); + } + keys.forEach(key => addIfFound(scopes, options, key)); + keys.forEach(key => addIfFound(scopes, overrides[type] || {}, key)); + keys.forEach(key => addIfFound(scopes, defaults, key)); + keys.forEach(key => addIfFound(scopes, descriptors, key)); + }); + + const array = Array.from(scopes); + if (array.length === 0) { + array.push(Object.create(null)); + } + if (keysCached.has(keyLists)) { + cache.set(keyLists, array); + } + return array; + } + + /** + * Returns the option scopes for resolving chart options + * @return {object[]} + */ + chartOptionScopes() { + const {options, type} = this; + + return [ + options, + overrides[type] || {}, + defaults.datasets[type] || {}, // https://github.com/chartjs/Chart.js/issues/8531 + {type}, + defaults, + descriptors + ]; + } + + /** + * @param {object[]} scopes + * @param {string[]} names + * @param {function|object} context + * @param {string[]} [prefixes] + * @return {object} + */ + resolveNamedOptions(scopes, names, context, prefixes = ['']) { + const result = {$shared: true}; + const {resolver, subPrefixes} = getResolver(this._resolverCache, scopes, prefixes); + let options = resolver; + if (needContext(resolver, names)) { + result.$shared = false; + context = isFunction(context) ? context() : context; + // subResolver is passed to scriptable options. It should not resolve to hover options. + const subResolver = this.createResolver(scopes, context, subPrefixes); + options = _attachContext(resolver, context, subResolver); + } + + for (const prop of names) { + result[prop] = options[prop]; + } + return result; + } + + /** + * @param {object[]} scopes + * @param {object} [context] + * @param {string[]} [prefixes] + * @param {{scriptable: boolean, indexable: boolean, allKeys?: boolean}} [descriptorDefaults] + */ + createResolver(scopes, context, prefixes = [''], descriptorDefaults) { + const {resolver} = getResolver(this._resolverCache, scopes, prefixes); + return isObject(context) + ? _attachContext(resolver, context, undefined, descriptorDefaults) + : resolver; + } +} + +function getResolver(resolverCache, scopes, prefixes) { + let cache = resolverCache.get(scopes); + if (!cache) { + cache = new Map(); + resolverCache.set(scopes, cache); + } + const cacheKey = prefixes.join(); + let cached = cache.get(cacheKey); + if (!cached) { + const resolver = _createResolver(scopes, prefixes); + cached = { + resolver, + subPrefixes: prefixes.filter(p => !p.toLowerCase().includes('hover')) + }; + cache.set(cacheKey, cached); + } + return cached; +} + +const hasFunction = value => isObject(value) + && Object.getOwnPropertyNames(value).some((key) => isFunction(value[key])); + +function needContext(proxy, names) { + const {isScriptable, isIndexable} = _descriptors(proxy); + + for (const prop of names) { + const scriptable = isScriptable(prop); + const indexable = isIndexable(prop); + const value = (indexable || scriptable) && proxy[prop]; + if ((scriptable && (isFunction(value) || hasFunction(value))) + || (indexable && isArray(value))) { + return true; + } + } + return false; +} diff --git a/src/core/core.controller.js b/src/core/core.controller.js new file mode 100644 index 00000000000..e0408ae212a --- /dev/null +++ b/src/core/core.controller.js @@ -0,0 +1,1269 @@ +import animator from './core.animator.js'; +import defaults, {overrides} from './core.defaults.js'; +import Interaction from './core.interaction.js'; +import layouts from './core.layouts.js'; +import {_detectPlatform} from '../platform/index.js'; +import PluginService from './core.plugins.js'; +import registry from './core.registry.js'; +import Config, {determineAxis, getIndexAxis} from './core.config.js'; +import {each, callback as callCallback, uid, valueOrDefault, _elementsEqual, isNullOrUndef, setsEqual, defined, isFunction, _isClickEvent} from '../helpers/helpers.core.js'; +import {clearCanvas, clipArea, createContext, unclipArea, _isPointInArea, _isDomSupported, retinaScale, getDatasetClipArea} from '../helpers/index.js'; +// @ts-ignore +import {version} from '../../package.json'; +import {debounce} from '../helpers/helpers.extras.js'; + +/** + * @typedef { import('../types/index.js').ChartEvent } ChartEvent + * @typedef { import('../types/index.js').Point } Point + */ + +const KNOWN_POSITIONS = ['top', 'bottom', 'left', 'right', 'chartArea']; +function positionIsHorizontal(position, axis) { + return position === 'top' || position === 'bottom' || (KNOWN_POSITIONS.indexOf(position) === -1 && axis === 'x'); +} + +function compare2Level(l1, l2) { + return function(a, b) { + return a[l1] === b[l1] + ? a[l2] - b[l2] + : a[l1] - b[l1]; + }; +} + +function onAnimationsComplete(context) { + const chart = context.chart; + const animationOptions = chart.options.animation; + + chart.notifyPlugins('afterRender'); + callCallback(animationOptions && animationOptions.onComplete, [context], chart); +} + +function onAnimationProgress(context) { + const chart = context.chart; + const animationOptions = chart.options.animation; + callCallback(animationOptions && animationOptions.onProgress, [context], chart); +} + +/** + * Chart.js can take a string id of a canvas element, a 2d context, or a canvas element itself. + * Attempt to unwrap the item passed into the chart constructor so that it is a canvas element (if possible). + */ +function getCanvas(item) { + if (_isDomSupported() && typeof item === 'string') { + item = document.getElementById(item); + } else if (item && item.length) { + // Support for array based queries (such as jQuery) + item = item[0]; + } + + if (item && item.canvas) { + // Support for any object associated to a canvas (including a context2d) + item = item.canvas; + } + return item; +} + +const instances = {}; +const getChart = (key) => { + const canvas = getCanvas(key); + return Object.values(instances).filter((c) => c.canvas === canvas).pop(); +}; + +function moveNumericKeys(obj, start, move) { + const keys = Object.keys(obj); + for (const key of keys) { + const intKey = +key; + if (intKey >= start) { + const value = obj[key]; + delete obj[key]; + if (move > 0 || intKey > start) { + obj[intKey + move] = value; + } + } + } +} + +/** + * @param {ChartEvent} e + * @param {ChartEvent|null} lastEvent + * @param {boolean} inChartArea + * @param {boolean} isClick + * @returns {ChartEvent|null} + */ +function determineLastEvent(e, lastEvent, inChartArea, isClick) { + if (!inChartArea || e.type === 'mouseout') { + return null; + } + if (isClick) { + return lastEvent; + } + return e; +} + +class Chart { + + static defaults = defaults; + static instances = instances; + static overrides = overrides; + static registry = registry; + static version = version; + static getChart = getChart; + + static register(...items) { + registry.add(...items); + invalidatePlugins(); + } + + static unregister(...items) { + registry.remove(...items); + invalidatePlugins(); + } + + // eslint-disable-next-line max-statements + constructor(item, userConfig) { + const config = this.config = new Config(userConfig); + const initialCanvas = getCanvas(item); + const existingChart = getChart(initialCanvas); + if (existingChart) { + throw new Error( + 'Canvas is already in use. Chart with ID \'' + existingChart.id + '\'' + + ' must be destroyed before the canvas with ID \'' + existingChart.canvas.id + '\' can be reused.' + ); + } + + const options = config.createResolver(config.chartOptionScopes(), this.getContext()); + + this.platform = new (config.platform || _detectPlatform(initialCanvas))(); + this.platform.updateConfig(config); + + const context = this.platform.acquireContext(initialCanvas, options.aspectRatio); + const canvas = context && context.canvas; + const height = canvas && canvas.height; + const width = canvas && canvas.width; + + this.id = uid(); + this.ctx = context; + this.canvas = canvas; + this.width = width; + this.height = height; + this._options = options; + // Store the previously used aspect ratio to determine if a resize + // is needed during updates. Do this after _options is set since + // aspectRatio uses a getter + this._aspectRatio = this.aspectRatio; + this._layers = []; + this._metasets = []; + this._stacks = undefined; + this.boxes = []; + this.currentDevicePixelRatio = undefined; + this.chartArea = undefined; + this._active = []; + this._lastEvent = undefined; + this._listeners = {}; + /** @type {?{attach?: function, detach?: function, resize?: function}} */ + this._responsiveListeners = undefined; + this._sortedMetasets = []; + this.scales = {}; + this._plugins = new PluginService(); + this.$proxies = {}; + this._hiddenIndices = {}; + this.attached = false; + this._animationsDisabled = undefined; + this.$context = undefined; + this._doResize = debounce(mode => this.update(mode), options.resizeDelay || 0); + this._dataChanges = []; + + // Add the chart instance to the global namespace + instances[this.id] = this; + + if (!context || !canvas) { + // The given item is not a compatible context2d element, let's return before finalizing + // the chart initialization but after setting basic chart / controller properties that + // can help to figure out that the chart is not valid (e.g chart.canvas !== null); + // https://github.com/chartjs/Chart.js/issues/2807 + console.error("Failed to create chart: can't acquire context from the given item"); + return; + } + + animator.listen(this, 'complete', onAnimationsComplete); + animator.listen(this, 'progress', onAnimationProgress); + + this._initialize(); + if (this.attached) { + this.update(); + } + } + + get aspectRatio() { + const {options: {aspectRatio, maintainAspectRatio}, width, height, _aspectRatio} = this; + if (!isNullOrUndef(aspectRatio)) { + // If aspectRatio is defined in options, use that. + return aspectRatio; + } + + if (maintainAspectRatio && _aspectRatio) { + // If maintainAspectRatio is truthly and we had previously determined _aspectRatio, use that + return _aspectRatio; + } + + // Calculate + return height ? width / height : null; + } + + get data() { + return this.config.data; + } + + set data(data) { + this.config.data = data; + } + + get options() { + return this._options; + } + + set options(options) { + this.config.options = options; + } + + get registry() { + return registry; + } + + /** + * @private + */ + _initialize() { + // Before init plugin notification + this.notifyPlugins('beforeInit'); + + if (this.options.responsive) { + this.resize(); + } else { + retinaScale(this, this.options.devicePixelRatio); + } + + this.bindEvents(); + + // After init plugin notification + this.notifyPlugins('afterInit'); + + return this; + } + + clear() { + clearCanvas(this.canvas, this.ctx); + return this; + } + + stop() { + animator.stop(this); + return this; + } + + /** + * Resize the chart to its container or to explicit dimensions. + * @param {number} [width] + * @param {number} [height] + */ + resize(width, height) { + if (!animator.running(this)) { + this._resize(width, height); + } else { + this._resizeBeforeDraw = {width, height}; + } + } + + _resize(width, height) { + const options = this.options; + const canvas = this.canvas; + const aspectRatio = options.maintainAspectRatio && this.aspectRatio; + const newSize = this.platform.getMaximumSize(canvas, width, height, aspectRatio); + const newRatio = options.devicePixelRatio || this.platform.getDevicePixelRatio(); + const mode = this.width ? 'resize' : 'attach'; + + this.width = newSize.width; + this.height = newSize.height; + this._aspectRatio = this.aspectRatio; + if (!retinaScale(this, newRatio, true)) { + return; + } + + this.notifyPlugins('resize', {size: newSize}); + + callCallback(options.onResize, [this, newSize], this); + + if (this.attached) { + if (this._doResize(mode)) { + // The resize update is delayed, only draw without updating. + this.render(); + } + } + } + + ensureScalesHaveIDs() { + const options = this.options; + const scalesOptions = options.scales || {}; + + each(scalesOptions, (axisOptions, axisID) => { + axisOptions.id = axisID; + }); + } + + /** + * Builds a map of scale ID to scale object for future lookup. + */ + buildOrUpdateScales() { + const options = this.options; + const scaleOpts = options.scales; + const scales = this.scales; + const updated = Object.keys(scales).reduce((obj, id) => { + obj[id] = false; + return obj; + }, {}); + let items = []; + + if (scaleOpts) { + items = items.concat( + Object.keys(scaleOpts).map((id) => { + const scaleOptions = scaleOpts[id]; + const axis = determineAxis(id, scaleOptions); + const isRadial = axis === 'r'; + const isHorizontal = axis === 'x'; + return { + options: scaleOptions, + dposition: isRadial ? 'chartArea' : isHorizontal ? 'bottom' : 'left', + dtype: isRadial ? 'radialLinear' : isHorizontal ? 'category' : 'linear' + }; + }) + ); + } + + each(items, (item) => { + const scaleOptions = item.options; + const id = scaleOptions.id; + const axis = determineAxis(id, scaleOptions); + const scaleType = valueOrDefault(scaleOptions.type, item.dtype); + + if (scaleOptions.position === undefined || positionIsHorizontal(scaleOptions.position, axis) !== positionIsHorizontal(item.dposition)) { + scaleOptions.position = item.dposition; + } + + updated[id] = true; + let scale = null; + if (id in scales && scales[id].type === scaleType) { + scale = scales[id]; + } else { + const scaleClass = registry.getScale(scaleType); + scale = new scaleClass({ + id, + type: scaleType, + ctx: this.ctx, + chart: this + }); + scales[scale.id] = scale; + } + + scale.init(scaleOptions, options); + }); + // clear up discarded scales + each(updated, (hasUpdated, id) => { + if (!hasUpdated) { + delete scales[id]; + } + }); + + each(scales, (scale) => { + layouts.configure(this, scale, scale.options); + layouts.addBox(this, scale); + }); + } + + /** + * @private + */ + _updateMetasets() { + const metasets = this._metasets; + const numData = this.data.datasets.length; + const numMeta = metasets.length; + + metasets.sort((a, b) => a.index - b.index); + if (numMeta > numData) { + for (let i = numData; i < numMeta; ++i) { + this._destroyDatasetMeta(i); + } + metasets.splice(numData, numMeta - numData); + } + this._sortedMetasets = metasets.slice(0).sort(compare2Level('order', 'index')); + } + + /** + * @private + */ + _removeUnreferencedMetasets() { + const {_metasets: metasets, data: {datasets}} = this; + if (metasets.length > datasets.length) { + delete this._stacks; + } + metasets.forEach((meta, index) => { + if (datasets.filter(x => x === meta._dataset).length === 0) { + this._destroyDatasetMeta(index); + } + }); + } + + buildOrUpdateControllers() { + const newControllers = []; + const datasets = this.data.datasets; + let i, ilen; + + this._removeUnreferencedMetasets(); + + for (i = 0, ilen = datasets.length; i < ilen; i++) { + const dataset = datasets[i]; + let meta = this.getDatasetMeta(i); + const type = dataset.type || this.config.type; + + if (meta.type && meta.type !== type) { + this._destroyDatasetMeta(i); + meta = this.getDatasetMeta(i); + } + meta.type = type; + meta.indexAxis = dataset.indexAxis || getIndexAxis(type, this.options); + meta.order = dataset.order || 0; + meta.index = i; + meta.label = '' + dataset.label; + meta.visible = this.isDatasetVisible(i); + + if (meta.controller) { + meta.controller.updateIndex(i); + meta.controller.linkScales(); + } else { + const ControllerClass = registry.getController(type); + const {datasetElementType, dataElementType} = defaults.datasets[type]; + Object.assign(ControllerClass, { + dataElementType: registry.getElement(dataElementType), + datasetElementType: datasetElementType && registry.getElement(datasetElementType) + }); + meta.controller = new ControllerClass(this, i); + newControllers.push(meta.controller); + } + } + + this._updateMetasets(); + return newControllers; + } + + /** + * Reset the elements of all datasets + * @private + */ + _resetElements() { + each(this.data.datasets, (dataset, datasetIndex) => { + this.getDatasetMeta(datasetIndex).controller.reset(); + }, this); + } + + /** + * Resets the chart back to its state before the initial animation + */ + reset() { + this._resetElements(); + this.notifyPlugins('reset'); + } + + update(mode) { + const config = this.config; + + config.update(); + const options = this._options = config.createResolver(config.chartOptionScopes(), this.getContext()); + const animsDisabled = this._animationsDisabled = !options.animation; + + this._updateScales(); + this._checkEventBindings(); + this._updateHiddenIndices(); + + // plugins options references might have change, let's invalidate the cache + // https://github.com/chartjs/Chart.js/issues/5111#issuecomment-355934167 + this._plugins.invalidate(); + + if (this.notifyPlugins('beforeUpdate', {mode, cancelable: true}) === false) { + return; + } + + // Make sure dataset controllers are updated and new controllers are reset + const newControllers = this.buildOrUpdateControllers(); + + this.notifyPlugins('beforeElementsUpdate'); + + // Make sure all dataset controllers have correct meta data counts + let minPadding = 0; + for (let i = 0, ilen = this.data.datasets.length; i < ilen; i++) { + const {controller} = this.getDatasetMeta(i); + const reset = !animsDisabled && newControllers.indexOf(controller) === -1; + // New controllers will be reset after the layout pass, so we only want to modify + // elements added to new datasets + controller.buildOrUpdateElements(reset); + minPadding = Math.max(+controller.getMaxOverflow(), minPadding); + } + minPadding = this._minPadding = options.layout.autoPadding ? minPadding : 0; + this._updateLayout(minPadding); + + // Only reset the controllers if we have animations + if (!animsDisabled) { + // Can only reset the new controllers after the scales have been updated + // Reset is done to get the starting point for the initial animation + each(newControllers, (controller) => { + controller.reset(); + }); + } + + this._updateDatasets(mode); + + // Do this before render so that any plugins that need final scale updates can use it + this.notifyPlugins('afterUpdate', {mode}); + + this._layers.sort(compare2Level('z', '_idx')); + + // Replay last event from before update, or set hover styles on active elements + const {_active, _lastEvent} = this; + if (_lastEvent) { + this._eventHandler(_lastEvent, true); + } else if (_active.length) { + this._updateHoverStyles(_active, _active, true); + } + + this.render(); + } + + /** + * @private + */ + _updateScales() { + each(this.scales, (scale) => { + layouts.removeBox(this, scale); + }); + + this.ensureScalesHaveIDs(); + this.buildOrUpdateScales(); + } + + /** + * @private + */ + _checkEventBindings() { + const options = this.options; + const existingEvents = new Set(Object.keys(this._listeners)); + const newEvents = new Set(options.events); + + if (!setsEqual(existingEvents, newEvents) || !!this._responsiveListeners !== options.responsive) { + // The configured events have changed. Rebind. + this.unbindEvents(); + this.bindEvents(); + } + } + + /** + * @private + */ + _updateHiddenIndices() { + const {_hiddenIndices} = this; + const changes = this._getUniformDataChanges() || []; + for (const {method, start, count} of changes) { + const move = method === '_removeElements' ? -count : count; + moveNumericKeys(_hiddenIndices, start, move); + } + } + + /** + * @private + */ + _getUniformDataChanges() { + const _dataChanges = this._dataChanges; + if (!_dataChanges || !_dataChanges.length) { + return; + } + + this._dataChanges = []; + const datasetCount = this.data.datasets.length; + const makeSet = (idx) => new Set( + _dataChanges + .filter(c => c[0] === idx) + .map((c, i) => i + ',' + c.splice(1).join(',')) + ); + + const changeSet = makeSet(0); + for (let i = 1; i < datasetCount; i++) { + if (!setsEqual(changeSet, makeSet(i))) { + return; + } + } + return Array.from(changeSet) + .map(c => c.split(',')) + .map(a => ({method: a[1], start: +a[2], count: +a[3]})); + } + + /** + * Updates the chart layout unless a plugin returns `false` to the `beforeLayout` + * hook, in which case, plugins will not be called on `afterLayout`. + * @private + */ + _updateLayout(minPadding) { + if (this.notifyPlugins('beforeLayout', {cancelable: true}) === false) { + return; + } + + layouts.update(this, this.width, this.height, minPadding); + + const area = this.chartArea; + const noArea = area.width <= 0 || area.height <= 0; + + this._layers = []; + each(this.boxes, (box) => { + if (noArea && box.position === 'chartArea') { + // Skip drawing and configuring chartArea boxes when chartArea is zero or negative + return; + } + + // configure is called twice, once in core.scale.update and once here. + // Here the boxes are fully updated and at their final positions. + if (box.configure) { + box.configure(); + } + this._layers.push(...box._layers()); + }, this); + + this._layers.forEach((item, index) => { + item._idx = index; + }); + + this.notifyPlugins('afterLayout'); + } + + /** + * Updates all datasets unless a plugin returns `false` to the `beforeDatasetsUpdate` + * hook, in which case, plugins will not be called on `afterDatasetsUpdate`. + * @private + */ + _updateDatasets(mode) { + if (this.notifyPlugins('beforeDatasetsUpdate', {mode, cancelable: true}) === false) { + return; + } + + for (let i = 0, ilen = this.data.datasets.length; i < ilen; ++i) { + this.getDatasetMeta(i).controller.configure(); + } + + for (let i = 0, ilen = this.data.datasets.length; i < ilen; ++i) { + this._updateDataset(i, isFunction(mode) ? mode({datasetIndex: i}) : mode); + } + + this.notifyPlugins('afterDatasetsUpdate', {mode}); + } + + /** + * Updates dataset at index unless a plugin returns `false` to the `beforeDatasetUpdate` + * hook, in which case, plugins will not be called on `afterDatasetUpdate`. + * @private + */ + _updateDataset(index, mode) { + const meta = this.getDatasetMeta(index); + const args = {meta, index, mode, cancelable: true}; + + if (this.notifyPlugins('beforeDatasetUpdate', args) === false) { + return; + } + + meta.controller._update(mode); + + args.cancelable = false; + this.notifyPlugins('afterDatasetUpdate', args); + } + + render() { + if (this.notifyPlugins('beforeRender', {cancelable: true}) === false) { + return; + } + + if (animator.has(this)) { + if (this.attached && !animator.running(this)) { + animator.start(this); + } + } else { + this.draw(); + onAnimationsComplete({chart: this}); + } + } + + draw() { + let i; + if (this._resizeBeforeDraw) { + const {width, height} = this._resizeBeforeDraw; + // Unset pending resize request now to avoid possible recursion within _resize + this._resizeBeforeDraw = null; + this._resize(width, height); + } + this.clear(); + + if (this.width <= 0 || this.height <= 0) { + return; + } + + if (this.notifyPlugins('beforeDraw', {cancelable: true}) === false) { + return; + } + + // Because of plugin hooks (before/afterDatasetsDraw), datasets can't + // currently be part of layers. Instead, we draw + // layers <= 0 before(default, backward compat), and the rest after + const layers = this._layers; + for (i = 0; i < layers.length && layers[i].z <= 0; ++i) { + layers[i].draw(this.chartArea); + } + + this._drawDatasets(); + + // Rest of layers + for (; i < layers.length; ++i) { + layers[i].draw(this.chartArea); + } + + this.notifyPlugins('afterDraw'); + } + + /** + * @private + */ + _getSortedDatasetMetas(filterVisible) { + const metasets = this._sortedMetasets; + const result = []; + let i, ilen; + + for (i = 0, ilen = metasets.length; i < ilen; ++i) { + const meta = metasets[i]; + if (!filterVisible || meta.visible) { + result.push(meta); + } + } + + return result; + } + + /** + * Gets the visible dataset metas in drawing order + * @return {object[]} + */ + getSortedVisibleDatasetMetas() { + return this._getSortedDatasetMetas(true); + } + + /** + * Draws all datasets unless a plugin returns `false` to the `beforeDatasetsDraw` + * hook, in which case, plugins will not be called on `afterDatasetsDraw`. + * @private + */ + _drawDatasets() { + if (this.notifyPlugins('beforeDatasetsDraw', {cancelable: true}) === false) { + return; + } + + const metasets = this.getSortedVisibleDatasetMetas(); + for (let i = metasets.length - 1; i >= 0; --i) { + this._drawDataset(metasets[i]); + } + + this.notifyPlugins('afterDatasetsDraw'); + } + + /** + * Draws dataset at index unless a plugin returns `false` to the `beforeDatasetDraw` + * hook, in which case, plugins will not be called on `afterDatasetDraw`. + * @private + */ + _drawDataset(meta) { + const ctx = this.ctx; + const args = { + meta, + index: meta.index, + cancelable: true + }; + // @ts-expect-error + const clip = getDatasetClipArea(this, meta); + + if (this.notifyPlugins('beforeDatasetDraw', args) === false) { + return; + } + + if (clip) { + clipArea(ctx, clip); + } + + meta.controller.draw(); + + if (clip) { + unclipArea(ctx); + } + + args.cancelable = false; + this.notifyPlugins('afterDatasetDraw', args); + } + + /** + * Checks whether the given point is in the chart area. + * @param {Point} point - in relative coordinates (see, e.g., getRelativePosition) + * @returns {boolean} + */ + isPointInArea(point) { + return _isPointInArea(point, this.chartArea, this._minPadding); + } + + getElementsAtEventForMode(e, mode, options, useFinalPosition) { + const method = Interaction.modes[mode]; + if (typeof method === 'function') { + return method(this, e, options, useFinalPosition); + } + + return []; + } + + getDatasetMeta(datasetIndex) { + const dataset = this.data.datasets[datasetIndex]; + const metasets = this._metasets; + let meta = metasets.filter(x => x && x._dataset === dataset).pop(); + + if (!meta) { + meta = { + type: null, + data: [], + dataset: null, + controller: null, + hidden: null, // See isDatasetVisible() comment + xAxisID: null, + yAxisID: null, + order: dataset && dataset.order || 0, + index: datasetIndex, + _dataset: dataset, + _parsed: [], + _sorted: false + }; + metasets.push(meta); + } + + return meta; + } + + getContext() { + return this.$context || (this.$context = createContext(null, {chart: this, type: 'chart'})); + } + + getVisibleDatasetCount() { + return this.getSortedVisibleDatasetMetas().length; + } + + isDatasetVisible(datasetIndex) { + const dataset = this.data.datasets[datasetIndex]; + if (!dataset) { + return false; + } + + const meta = this.getDatasetMeta(datasetIndex); + + // meta.hidden is a per chart dataset hidden flag override with 3 states: if true or false, + // the dataset.hidden value is ignored, else if null, the dataset hidden state is returned. + return typeof meta.hidden === 'boolean' ? !meta.hidden : !dataset.hidden; + } + + setDatasetVisibility(datasetIndex, visible) { + const meta = this.getDatasetMeta(datasetIndex); + meta.hidden = !visible; + } + + toggleDataVisibility(index) { + this._hiddenIndices[index] = !this._hiddenIndices[index]; + } + + getDataVisibility(index) { + return !this._hiddenIndices[index]; + } + + /** + * @private + */ + _updateVisibility(datasetIndex, dataIndex, visible) { + const mode = visible ? 'show' : 'hide'; + const meta = this.getDatasetMeta(datasetIndex); + const anims = meta.controller._resolveAnimations(undefined, mode); + + if (defined(dataIndex)) { + meta.data[dataIndex].hidden = !visible; + this.update(); + } else { + this.setDatasetVisibility(datasetIndex, visible); + // Animate visible state, so hide animation can be seen. This could be handled better if update / updateDataset returned a Promise. + anims.update(meta, {visible}); + this.update((ctx) => ctx.datasetIndex === datasetIndex ? mode : undefined); + } + } + + hide(datasetIndex, dataIndex) { + this._updateVisibility(datasetIndex, dataIndex, false); + } + + show(datasetIndex, dataIndex) { + this._updateVisibility(datasetIndex, dataIndex, true); + } + + /** + * @private + */ + _destroyDatasetMeta(datasetIndex) { + const meta = this._metasets[datasetIndex]; + if (meta && meta.controller) { + meta.controller._destroy(); + } + delete this._metasets[datasetIndex]; + } + + _stop() { + let i, ilen; + this.stop(); + animator.remove(this); + + for (i = 0, ilen = this.data.datasets.length; i < ilen; ++i) { + this._destroyDatasetMeta(i); + } + } + + destroy() { + this.notifyPlugins('beforeDestroy'); + const {canvas, ctx} = this; + + this._stop(); + this.config.clearCache(); + + if (canvas) { + this.unbindEvents(); + clearCanvas(canvas, ctx); + this.platform.releaseContext(ctx); + this.canvas = null; + this.ctx = null; + } + + delete instances[this.id]; + + this.notifyPlugins('afterDestroy'); + } + + toBase64Image(...args) { + return this.canvas.toDataURL(...args); + } + + /** + * @private + */ + bindEvents() { + this.bindUserEvents(); + if (this.options.responsive) { + this.bindResponsiveEvents(); + } else { + this.attached = true; + } + } + + /** + * @private + */ + bindUserEvents() { + const listeners = this._listeners; + const platform = this.platform; + + const _add = (type, listener) => { + platform.addEventListener(this, type, listener); + listeners[type] = listener; + }; + + const listener = (e, x, y) => { + e.offsetX = x; + e.offsetY = y; + this._eventHandler(e); + }; + + each(this.options.events, (type) => _add(type, listener)); + } + + /** + * @private + */ + bindResponsiveEvents() { + if (!this._responsiveListeners) { + this._responsiveListeners = {}; + } + const listeners = this._responsiveListeners; + const platform = this.platform; + + const _add = (type, listener) => { + platform.addEventListener(this, type, listener); + listeners[type] = listener; + }; + const _remove = (type, listener) => { + if (listeners[type]) { + platform.removeEventListener(this, type, listener); + delete listeners[type]; + } + }; + + const listener = (width, height) => { + if (this.canvas) { + this.resize(width, height); + } + }; + + let detached; // eslint-disable-line prefer-const + const attached = () => { + _remove('attach', attached); + + this.attached = true; + this.resize(); + + _add('resize', listener); + _add('detach', detached); + }; + + detached = () => { + this.attached = false; + + _remove('resize', listener); + + // Stop animating and remove metasets, so when re-attached, the animations start from beginning. + this._stop(); + this._resize(0, 0); + + _add('attach', attached); + }; + + if (platform.isAttached(this.canvas)) { + attached(); + } else { + detached(); + } + } + + /** + * @private + */ + unbindEvents() { + each(this._listeners, (listener, type) => { + this.platform.removeEventListener(this, type, listener); + }); + this._listeners = {}; + + each(this._responsiveListeners, (listener, type) => { + this.platform.removeEventListener(this, type, listener); + }); + this._responsiveListeners = undefined; + } + + updateHoverStyle(items, mode, enabled) { + const prefix = enabled ? 'set' : 'remove'; + let meta, item, i, ilen; + + if (mode === 'dataset') { + meta = this.getDatasetMeta(items[0].datasetIndex); + meta.controller['_' + prefix + 'DatasetHoverStyle'](); + } + + for (i = 0, ilen = items.length; i < ilen; ++i) { + item = items[i]; + const controller = item && this.getDatasetMeta(item.datasetIndex).controller; + if (controller) { + controller[prefix + 'HoverStyle'](item.element, item.datasetIndex, item.index); + } + } + } + + /** + * Get active (hovered) elements + * @returns array + */ + getActiveElements() { + return this._active || []; + } + + /** + * Set active (hovered) elements + * @param {array} activeElements New active data points + */ + setActiveElements(activeElements) { + const lastActive = this._active || []; + const active = activeElements.map(({datasetIndex, index}) => { + const meta = this.getDatasetMeta(datasetIndex); + if (!meta) { + throw new Error('No dataset found at index ' + datasetIndex); + } + + return { + datasetIndex, + element: meta.data[index], + index, + }; + }); + const changed = !_elementsEqual(active, lastActive); + + if (changed) { + this._active = active; + // Make sure we don't use the previous mouse event to override the active elements in update. + this._lastEvent = null; + this._updateHoverStyles(active, lastActive); + } + } + + /** + * Calls enabled plugins on the specified hook and with the given args. + * This method immediately returns as soon as a plugin explicitly returns false. The + * returned value can be used, for instance, to interrupt the current action. + * @param {string} hook - The name of the plugin method to call (e.g. 'beforeUpdate'). + * @param {Object} [args] - Extra arguments to apply to the hook call. + * @param {import('./core.plugins.js').filterCallback} [filter] - Filtering function for limiting which plugins are notified + * @returns {boolean} false if any of the plugins return false, else returns true. + */ + notifyPlugins(hook, args, filter) { + return this._plugins.notify(this, hook, args, filter); + } + + /** + * Check if a plugin with the specific ID is registered and enabled + * @param {string} pluginId - The ID of the plugin of which to check if it is enabled + * @returns {boolean} + */ + isPluginEnabled(pluginId) { + return this._plugins._cache.filter(p => p.plugin.id === pluginId).length === 1; + } + + /** + * @private + */ + _updateHoverStyles(active, lastActive, replay) { + const hoverOptions = this.options.hover; + const diff = (a, b) => a.filter(x => !b.some(y => x.datasetIndex === y.datasetIndex && x.index === y.index)); + const deactivated = diff(lastActive, active); + const activated = replay ? active : diff(active, lastActive); + + if (deactivated.length) { + this.updateHoverStyle(deactivated, hoverOptions.mode, false); + } + + if (activated.length && hoverOptions.mode) { + this.updateHoverStyle(activated, hoverOptions.mode, true); + } + } + + /** + * @private + */ + _eventHandler(e, replay) { + const args = { + event: e, + replay, + cancelable: true, + inChartArea: this.isPointInArea(e) + }; + const eventFilter = (plugin) => (plugin.options.events || this.options.events).includes(e.native.type); + + if (this.notifyPlugins('beforeEvent', args, eventFilter) === false) { + return; + } + + const changed = this._handleEvent(e, replay, args.inChartArea); + + args.cancelable = false; + this.notifyPlugins('afterEvent', args, eventFilter); + + if (changed || args.changed) { + this.render(); + } + + return this; + } + + /** + * Handle an event + * @param {ChartEvent} e the event to handle + * @param {boolean} [replay] - true if the event was replayed by `update` + * @param {boolean} [inChartArea] - true if the event is inside chartArea + * @return {boolean} true if the chart needs to re-render + * @private + */ + _handleEvent(e, replay, inChartArea) { + const {_active: lastActive = [], options} = this; + + // If the event is replayed from `update`, we should evaluate with the final positions. + // + // The `replay`: + // It's the last event (excluding click) that has occurred before `update`. + // So mouse has not moved. It's also over the chart, because there is a `replay`. + // + // The why: + // If animations are active, the elements haven't moved yet compared to state before update. + // But if they will, we are activating the elements that would be active, if this check + // was done after the animations have completed. => "final positions". + // If there is no animations, the "final" and "current" positions are equal. + // This is done so we do not have to evaluate the active elements each animation frame + // - it would be expensive. + const useFinalPosition = replay; + const active = this._getActiveElements(e, lastActive, inChartArea, useFinalPosition); + const isClick = _isClickEvent(e); + const lastEvent = determineLastEvent(e, this._lastEvent, inChartArea, isClick); + + if (inChartArea) { + // Set _lastEvent to null while we are processing the event handlers. + // This prevents recursion if the handler calls chart.update() + this._lastEvent = null; + + // Invoke onHover hook + callCallback(options.onHover, [e, active, this], this); + + if (isClick) { + callCallback(options.onClick, [e, active, this], this); + } + } + + const changed = !_elementsEqual(active, lastActive); + if (changed || replay) { + this._active = active; + this._updateHoverStyles(active, lastActive, replay); + } + + this._lastEvent = lastEvent; + + return changed; + } + + /** + * @param {ChartEvent} e - The event + * @param {import('../types/index.js').ActiveElement[]} lastActive - Previously active elements + * @param {boolean} inChartArea - Is the event inside chartArea + * @param {boolean} useFinalPosition - Should the evaluation be done with current or final (after animation) element positions + * @returns {import('../types/index.js').ActiveElement[]} - The active elements + * @pravate + */ + _getActiveElements(e, lastActive, inChartArea, useFinalPosition) { + if (e.type === 'mouseout') { + return []; + } + + if (!inChartArea) { + // Let user control the active elements outside chartArea. Eg. using Legend. + return lastActive; + } + + const hoverOptions = this.options.hover; + return this.getElementsAtEventForMode(e, hoverOptions.mode, hoverOptions, useFinalPosition); + } +} + +// @ts-ignore +function invalidatePlugins() { + return each(Chart.instances, (chart) => chart._plugins.invalidate()); +} + +export default Chart; diff --git a/src/core/core.datasetController.js b/src/core/core.datasetController.js new file mode 100644 index 00000000000..9b7126a93fd --- /dev/null +++ b/src/core/core.datasetController.js @@ -0,0 +1,1077 @@ +import Animations from './core.animations.js'; +import defaults from './core.defaults.js'; +import {isArray, isFinite, isObject, valueOrDefault, resolveObjectKey, defined} from '../helpers/helpers.core.js'; +import {listenArrayEvents, unlistenArrayEvents} from '../helpers/helpers.collection.js'; +import {createContext, sign} from '../helpers/index.js'; + +/** + * @typedef { import('./core.controller.js').default } Chart + * @typedef { import('./core.scale.js').default } Scale + */ + +function scaleClip(scale, allowedOverflow) { + const opts = scale && scale.options || {}; + const reverse = opts.reverse; + const min = opts.min === undefined ? allowedOverflow : 0; + const max = opts.max === undefined ? allowedOverflow : 0; + return { + start: reverse ? max : min, + end: reverse ? min : max + }; +} + +function defaultClip(xScale, yScale, allowedOverflow) { + if (allowedOverflow === false) { + return false; + } + const x = scaleClip(xScale, allowedOverflow); + const y = scaleClip(yScale, allowedOverflow); + + return { + top: y.end, + right: x.end, + bottom: y.start, + left: x.start + }; +} + +function toClip(value) { + let t, r, b, l; + + if (isObject(value)) { + t = value.top; + r = value.right; + b = value.bottom; + l = value.left; + } else { + t = r = b = l = value; + } + + return { + top: t, + right: r, + bottom: b, + left: l, + disabled: value === false + }; +} + +function getSortedDatasetIndices(chart, filterVisible) { + const keys = []; + const metasets = chart._getSortedDatasetMetas(filterVisible); + let i, ilen; + + for (i = 0, ilen = metasets.length; i < ilen; ++i) { + keys.push(metasets[i].index); + } + return keys; +} + +function applyStack(stack, value, dsIndex, options = {}) { + const keys = stack.keys; + const singleMode = options.mode === 'single'; + let i, ilen, datasetIndex, otherValue; + + if (value === null) { + return; + } + + let found = false; + for (i = 0, ilen = keys.length; i < ilen; ++i) { + datasetIndex = +keys[i]; + if (datasetIndex === dsIndex) { + found = true; + if (options.all) { + continue; + } + break; + } + otherValue = stack.values[datasetIndex]; + if (isFinite(otherValue) && (singleMode || (value === 0 || sign(value) === sign(otherValue)))) { + value += otherValue; + } + } + + if (!found && !options.all) { + return 0; + } + + return value; +} + +function convertObjectDataToArray(data, meta) { + const {iScale, vScale} = meta; + const iAxisKey = iScale.axis === 'x' ? 'x' : 'y'; + const vAxisKey = vScale.axis === 'x' ? 'x' : 'y'; + const keys = Object.keys(data); + const adata = new Array(keys.length); + let i, ilen, key; + for (i = 0, ilen = keys.length; i < ilen; ++i) { + key = keys[i]; + adata[i] = { + [iAxisKey]: key, + [vAxisKey]: data[key] + }; + } + return adata; +} + +function isStacked(scale, meta) { + const stacked = scale && scale.options.stacked; + return stacked || (stacked === undefined && meta.stack !== undefined); +} + +function getStackKey(indexScale, valueScale, meta) { + return `${indexScale.id}.${valueScale.id}.${meta.stack || meta.type}`; +} + +function getUserBounds(scale) { + const {min, max, minDefined, maxDefined} = scale.getUserBounds(); + return { + min: minDefined ? min : Number.NEGATIVE_INFINITY, + max: maxDefined ? max : Number.POSITIVE_INFINITY + }; +} + +function getOrCreateStack(stacks, stackKey, indexValue) { + const subStack = stacks[stackKey] || (stacks[stackKey] = {}); + return subStack[indexValue] || (subStack[indexValue] = {}); +} + +function getLastIndexInStack(stack, vScale, positive, type) { + for (const meta of vScale.getMatchingVisibleMetas(type).reverse()) { + const value = stack[meta.index]; + if ((positive && value > 0) || (!positive && value < 0)) { + return meta.index; + } + } + + return null; +} + +function updateStacks(controller, parsed) { + const {chart, _cachedMeta: meta} = controller; + const stacks = chart._stacks || (chart._stacks = {}); // map structure is {stackKey: {datasetIndex: value}} + const {iScale, vScale, index: datasetIndex} = meta; + const iAxis = iScale.axis; + const vAxis = vScale.axis; + const key = getStackKey(iScale, vScale, meta); + const ilen = parsed.length; + let stack; + + for (let i = 0; i < ilen; ++i) { + const item = parsed[i]; + const {[iAxis]: index, [vAxis]: value} = item; + const itemStacks = item._stacks || (item._stacks = {}); + stack = itemStacks[vAxis] = getOrCreateStack(stacks, key, index); + stack[datasetIndex] = value; + + stack._top = getLastIndexInStack(stack, vScale, true, meta.type); + stack._bottom = getLastIndexInStack(stack, vScale, false, meta.type); + + const visualValues = stack._visualValues || (stack._visualValues = {}); + visualValues[datasetIndex] = value; + } +} + +function getFirstScaleId(chart, axis) { + const scales = chart.scales; + return Object.keys(scales).filter(key => scales[key].axis === axis).shift(); +} + +function createDatasetContext(parent, index) { + return createContext(parent, + { + active: false, + dataset: undefined, + datasetIndex: index, + index, + mode: 'default', + type: 'dataset' + } + ); +} + +function createDataContext(parent, index, element) { + return createContext(parent, { + active: false, + dataIndex: index, + parsed: undefined, + raw: undefined, + element, + index, + mode: 'default', + type: 'data' + }); +} + +function clearStacks(meta, items) { + // Not using meta.index here, because it might be already updated if the dataset changed location + const datasetIndex = meta.controller.index; + const axis = meta.vScale && meta.vScale.axis; + if (!axis) { + return; + } + + items = items || meta._parsed; + for (const parsed of items) { + const stacks = parsed._stacks; + if (!stacks || stacks[axis] === undefined || stacks[axis][datasetIndex] === undefined) { + return; + } + delete stacks[axis][datasetIndex]; + if (stacks[axis]._visualValues !== undefined && stacks[axis]._visualValues[datasetIndex] !== undefined) { + delete stacks[axis]._visualValues[datasetIndex]; + } + } +} + +const isDirectUpdateMode = (mode) => mode === 'reset' || mode === 'none'; +const cloneIfNotShared = (cached, shared) => shared ? cached : Object.assign({}, cached); +const createStack = (canStack, meta, chart) => canStack && !meta.hidden && meta._stacked + && {keys: getSortedDatasetIndices(chart, true), values: null}; + +export default class DatasetController { + + /** + * @type {any} + */ + static defaults = {}; + + /** + * Element type used to generate a meta dataset (e.g. Chart.element.LineElement). + */ + static datasetElementType = null; + + /** + * Element type used to generate a meta data (e.g. Chart.element.PointElement). + */ + static dataElementType = null; + + /** + * @param {Chart} chart + * @param {number} datasetIndex + */ + constructor(chart, datasetIndex) { + this.chart = chart; + this._ctx = chart.ctx; + this.index = datasetIndex; + this._cachedDataOpts = {}; + this._cachedMeta = this.getMeta(); + this._type = this._cachedMeta.type; + this.options = undefined; + /** @type {boolean | object} */ + this._parsing = false; + this._data = undefined; + this._objectData = undefined; + this._sharedOptions = undefined; + this._drawStart = undefined; + this._drawCount = undefined; + this.enableOptionSharing = false; + this.supportsDecimation = false; + this.$context = undefined; + this._syncList = []; + this.datasetElementType = new.target.datasetElementType; + this.dataElementType = new.target.dataElementType; + + this.initialize(); + } + + initialize() { + const meta = this._cachedMeta; + this.configure(); + this.linkScales(); + meta._stacked = isStacked(meta.vScale, meta); + this.addElements(); + + if (this.options.fill && !this.chart.isPluginEnabled('filler')) { + console.warn("Tried to use the 'fill' option without the 'Filler' plugin enabled. Please import and register the 'Filler' plugin and make sure it is not disabled in the options"); + } + } + + updateIndex(datasetIndex) { + if (this.index !== datasetIndex) { + clearStacks(this._cachedMeta); + } + this.index = datasetIndex; + } + + linkScales() { + const chart = this.chart; + const meta = this._cachedMeta; + const dataset = this.getDataset(); + + const chooseId = (axis, x, y, r) => axis === 'x' ? x : axis === 'r' ? r : y; + + const xid = meta.xAxisID = valueOrDefault(dataset.xAxisID, getFirstScaleId(chart, 'x')); + const yid = meta.yAxisID = valueOrDefault(dataset.yAxisID, getFirstScaleId(chart, 'y')); + const rid = meta.rAxisID = valueOrDefault(dataset.rAxisID, getFirstScaleId(chart, 'r')); + const indexAxis = meta.indexAxis; + const iid = meta.iAxisID = chooseId(indexAxis, xid, yid, rid); + const vid = meta.vAxisID = chooseId(indexAxis, yid, xid, rid); + meta.xScale = this.getScaleForId(xid); + meta.yScale = this.getScaleForId(yid); + meta.rScale = this.getScaleForId(rid); + meta.iScale = this.getScaleForId(iid); + meta.vScale = this.getScaleForId(vid); + } + + getDataset() { + return this.chart.data.datasets[this.index]; + } + + getMeta() { + return this.chart.getDatasetMeta(this.index); + } + + /** + * @param {string} scaleID + * @return {Scale} + */ + getScaleForId(scaleID) { + return this.chart.scales[scaleID]; + } + + /** + * @private + */ + _getOtherScale(scale) { + const meta = this._cachedMeta; + return scale === meta.iScale + ? meta.vScale + : meta.iScale; + } + + reset() { + this._update('reset'); + } + + /** + * @private + */ + _destroy() { + const meta = this._cachedMeta; + if (this._data) { + unlistenArrayEvents(this._data, this); + } + if (meta._stacked) { + clearStacks(meta); + } + } + + /** + * @private + */ + _dataCheck() { + const dataset = this.getDataset(); + const data = dataset.data || (dataset.data = []); + const _data = this._data; + + // In order to correctly handle data addition/deletion animation (and thus simulate + // real-time charts), we need to monitor these data modifications and synchronize + // the internal metadata accordingly. + + if (isObject(data)) { + const meta = this._cachedMeta; + this._data = convertObjectDataToArray(data, meta); + } else if (_data !== data) { + if (_data) { + // This case happens when the user replaced the data array instance. + unlistenArrayEvents(_data, this); + // Discard old parsed data and stacks + const meta = this._cachedMeta; + clearStacks(meta); + meta._parsed = []; + } + if (data && Object.isExtensible(data)) { + listenArrayEvents(data, this); + } + this._syncList = []; + this._data = data; + } + } + + addElements() { + const meta = this._cachedMeta; + + this._dataCheck(); + + if (this.datasetElementType) { + meta.dataset = new this.datasetElementType(); + } + } + + buildOrUpdateElements(resetNewElements) { + const meta = this._cachedMeta; + const dataset = this.getDataset(); + let stackChanged = false; + + this._dataCheck(); + + // make sure cached _stacked status is current + const oldStacked = meta._stacked; + meta._stacked = isStacked(meta.vScale, meta); + + // detect change in stack option + if (meta.stack !== dataset.stack) { + stackChanged = true; + // remove values from old stack + clearStacks(meta); + meta.stack = dataset.stack; + } + + // Re-sync meta data in case the user replaced the data array or if we missed + // any updates and so make sure that we handle number of datapoints changing. + this._resyncElements(resetNewElements); + + // if stack changed, update stack values for the whole dataset + if (stackChanged || oldStacked !== meta._stacked) { + updateStacks(this, meta._parsed); + meta._stacked = isStacked(meta.vScale, meta); + } + } + + /** + * Merges user-supplied and default dataset-level options + * @private + */ + configure() { + const config = this.chart.config; + const scopeKeys = config.datasetScopeKeys(this._type); + const scopes = config.getOptionScopes(this.getDataset(), scopeKeys, true); + this.options = config.createResolver(scopes, this.getContext()); + this._parsing = this.options.parsing; + this._cachedDataOpts = {}; + } + + /** + * @param {number} start + * @param {number} count + */ + parse(start, count) { + const {_cachedMeta: meta, _data: data} = this; + const {iScale, _stacked} = meta; + const iAxis = iScale.axis; + + let sorted = start === 0 && count === data.length ? true : meta._sorted; + let prev = start > 0 && meta._parsed[start - 1]; + let i, cur, parsed; + + if (this._parsing === false) { + meta._parsed = data; + meta._sorted = true; + parsed = data; + } else { + if (isArray(data[start])) { + parsed = this.parseArrayData(meta, data, start, count); + } else if (isObject(data[start])) { + parsed = this.parseObjectData(meta, data, start, count); + } else { + parsed = this.parsePrimitiveData(meta, data, start, count); + } + + const isNotInOrderComparedToPrev = () => cur[iAxis] === null || (prev && cur[iAxis] < prev[iAxis]); + for (i = 0; i < count; ++i) { + meta._parsed[i + start] = cur = parsed[i]; + if (sorted) { + if (isNotInOrderComparedToPrev()) { + sorted = false; + } + prev = cur; + } + } + meta._sorted = sorted; + } + + if (_stacked) { + updateStacks(this, parsed); + } + } + + /** + * Parse array of primitive values + * @param {object} meta - dataset meta + * @param {array} data - data array. Example [1,3,4] + * @param {number} start - start index + * @param {number} count - number of items to parse + * @returns {object} parsed item - item containing index and a parsed value + * for each scale id. + * Example: {xScale0: 0, yScale0: 1} + * @protected + */ + parsePrimitiveData(meta, data, start, count) { + const {iScale, vScale} = meta; + const iAxis = iScale.axis; + const vAxis = vScale.axis; + const labels = iScale.getLabels(); + const singleScale = iScale === vScale; + const parsed = new Array(count); + let i, ilen, index; + + for (i = 0, ilen = count; i < ilen; ++i) { + index = i + start; + parsed[i] = { + [iAxis]: singleScale || iScale.parse(labels[index], index), + [vAxis]: vScale.parse(data[index], index) + }; + } + return parsed; + } + + /** + * Parse array of arrays + * @param {object} meta - dataset meta + * @param {array} data - data array. Example [[1,2],[3,4]] + * @param {number} start - start index + * @param {number} count - number of items to parse + * @returns {object} parsed item - item containing index and a parsed value + * for each scale id. + * Example: {x: 0, y: 1} + * @protected + */ + parseArrayData(meta, data, start, count) { + const {xScale, yScale} = meta; + const parsed = new Array(count); + let i, ilen, index, item; + + for (i = 0, ilen = count; i < ilen; ++i) { + index = i + start; + item = data[index]; + parsed[i] = { + x: xScale.parse(item[0], index), + y: yScale.parse(item[1], index) + }; + } + return parsed; + } + + /** + * Parse array of objects + * @param {object} meta - dataset meta + * @param {array} data - data array. Example [{x:1, y:5}, {x:2, y:10}] + * @param {number} start - start index + * @param {number} count - number of items to parse + * @returns {object} parsed item - item containing index and a parsed value + * for each scale id. _custom is optional + * Example: {xScale0: 0, yScale0: 1, _custom: {r: 10, foo: 'bar'}} + * @protected + */ + parseObjectData(meta, data, start, count) { + const {xScale, yScale} = meta; + const {xAxisKey = 'x', yAxisKey = 'y'} = this._parsing; + const parsed = new Array(count); + let i, ilen, index, item; + + for (i = 0, ilen = count; i < ilen; ++i) { + index = i + start; + item = data[index]; + parsed[i] = { + x: xScale.parse(resolveObjectKey(item, xAxisKey), index), + y: yScale.parse(resolveObjectKey(item, yAxisKey), index) + }; + } + return parsed; + } + + /** + * @protected + */ + getParsed(index) { + return this._cachedMeta._parsed[index]; + } + + /** + * @protected + */ + getDataElement(index) { + return this._cachedMeta.data[index]; + } + + /** + * @protected + */ + applyStack(scale, parsed, mode) { + const chart = this.chart; + const meta = this._cachedMeta; + const value = parsed[scale.axis]; + const stack = { + keys: getSortedDatasetIndices(chart, true), + values: parsed._stacks[scale.axis]._visualValues + }; + return applyStack(stack, value, meta.index, {mode}); + } + + /** + * @protected + */ + updateRangeFromParsed(range, scale, parsed, stack) { + const parsedValue = parsed[scale.axis]; + let value = parsedValue === null ? NaN : parsedValue; + const values = stack && parsed._stacks[scale.axis]; + if (stack && values) { + stack.values = values; + value = applyStack(stack, parsedValue, this._cachedMeta.index); + } + range.min = Math.min(range.min, value); + range.max = Math.max(range.max, value); + } + + /** + * @protected + */ + getMinMax(scale, canStack) { + const meta = this._cachedMeta; + const _parsed = meta._parsed; + const sorted = meta._sorted && scale === meta.iScale; + const ilen = _parsed.length; + const otherScale = this._getOtherScale(scale); + const stack = createStack(canStack, meta, this.chart); + const range = {min: Number.POSITIVE_INFINITY, max: Number.NEGATIVE_INFINITY}; + const {min: otherMin, max: otherMax} = getUserBounds(otherScale); + let i, parsed; + + function _skip() { + parsed = _parsed[i]; + const otherValue = parsed[otherScale.axis]; + return !isFinite(parsed[scale.axis]) || otherMin > otherValue || otherMax < otherValue; + } + + for (i = 0; i < ilen; ++i) { + if (_skip()) { + continue; + } + this.updateRangeFromParsed(range, scale, parsed, stack); + if (sorted) { + // if the data is sorted, we don't need to check further from this end of array + break; + } + } + if (sorted) { + // in the sorted case, find first non-skipped value from other end of array + for (i = ilen - 1; i >= 0; --i) { + if (_skip()) { + continue; + } + this.updateRangeFromParsed(range, scale, parsed, stack); + break; + } + } + return range; + } + + getAllParsedValues(scale) { + const parsed = this._cachedMeta._parsed; + const values = []; + let i, ilen, value; + + for (i = 0, ilen = parsed.length; i < ilen; ++i) { + value = parsed[i][scale.axis]; + if (isFinite(value)) { + values.push(value); + } + } + return values; + } + + /** + * @return {number|boolean} + * @protected + */ + getMaxOverflow() { + return false; + } + + /** + * @protected + */ + getLabelAndValue(index) { + const meta = this._cachedMeta; + const iScale = meta.iScale; + const vScale = meta.vScale; + const parsed = this.getParsed(index); + return { + label: iScale ? '' + iScale.getLabelForValue(parsed[iScale.axis]) : '', + value: vScale ? '' + vScale.getLabelForValue(parsed[vScale.axis]) : '' + }; + } + + /** + * @private + */ + _update(mode) { + const meta = this._cachedMeta; + this.update(mode || 'default'); + meta._clip = toClip(valueOrDefault(this.options.clip, defaultClip(meta.xScale, meta.yScale, this.getMaxOverflow()))); + } + + /** + * @param {string} mode + */ + update(mode) {} // eslint-disable-line no-unused-vars + + draw() { + const ctx = this._ctx; + const chart = this.chart; + const meta = this._cachedMeta; + const elements = meta.data || []; + const area = chart.chartArea; + const active = []; + const start = this._drawStart || 0; + const count = this._drawCount || (elements.length - start); + const drawActiveElementsOnTop = this.options.drawActiveElementsOnTop; + let i; + + if (meta.dataset) { + meta.dataset.draw(ctx, area, start, count); + } + + for (i = start; i < start + count; ++i) { + const element = elements[i]; + if (element.hidden) { + continue; + } + if (element.active && drawActiveElementsOnTop) { + active.push(element); + } else { + element.draw(ctx, area); + } + } + + for (i = 0; i < active.length; ++i) { + active[i].draw(ctx, area); + } + } + + /** + * Returns a set of predefined style properties that should be used to represent the dataset + * or the data if the index is specified + * @param {number} index - data index + * @param {boolean} [active] - true if hover + * @return {object} style object + */ + getStyle(index, active) { + const mode = active ? 'active' : 'default'; + return index === undefined && this._cachedMeta.dataset + ? this.resolveDatasetElementOptions(mode) + : this.resolveDataElementOptions(index || 0, mode); + } + + /** + * @protected + */ + getContext(index, active, mode) { + const dataset = this.getDataset(); + let context; + if (index >= 0 && index < this._cachedMeta.data.length) { + const element = this._cachedMeta.data[index]; + context = element.$context || + (element.$context = createDataContext(this.getContext(), index, element)); + context.parsed = this.getParsed(index); + context.raw = dataset.data[index]; + context.index = context.dataIndex = index; + } else { + context = this.$context || + (this.$context = createDatasetContext(this.chart.getContext(), this.index)); + context.dataset = dataset; + context.index = context.datasetIndex = this.index; + } + + context.active = !!active; + context.mode = mode; + return context; + } + + /** + * @param {string} [mode] + * @protected + */ + resolveDatasetElementOptions(mode) { + return this._resolveElementOptions(this.datasetElementType.id, mode); + } + + /** + * @param {number} index + * @param {string} [mode] + * @protected + */ + resolveDataElementOptions(index, mode) { + return this._resolveElementOptions(this.dataElementType.id, mode, index); + } + + /** + * @private + */ + _resolveElementOptions(elementType, mode = 'default', index) { + const active = mode === 'active'; + const cache = this._cachedDataOpts; + const cacheKey = elementType + '-' + mode; + const cached = cache[cacheKey]; + const sharing = this.enableOptionSharing && defined(index); + if (cached) { + return cloneIfNotShared(cached, sharing); + } + const config = this.chart.config; + const scopeKeys = config.datasetElementScopeKeys(this._type, elementType); + const prefixes = active ? [`${elementType}Hover`, 'hover', elementType, ''] : [elementType, '']; + const scopes = config.getOptionScopes(this.getDataset(), scopeKeys); + const names = Object.keys(defaults.elements[elementType]); + // context is provided as a function, and is called only if needed, + // so we don't create a context for each element if not needed. + const context = () => this.getContext(index, active, mode); + const values = config.resolveNamedOptions(scopes, names, context, prefixes); + + if (values.$shared) { + // `$shared` indicates this set of options can be shared between multiple elements. + // Sharing is used to reduce number of properties to change during animation. + values.$shared = sharing; + + // We cache options by `mode`, which can be 'active' for example. This enables us + // to have the 'active' element options and 'default' options to switch between + // when interacting. + cache[cacheKey] = Object.freeze(cloneIfNotShared(values, sharing)); + } + + return values; + } + + + /** + * @private + */ + _resolveAnimations(index, transition, active) { + const chart = this.chart; + const cache = this._cachedDataOpts; + const cacheKey = `animation-${transition}`; + const cached = cache[cacheKey]; + if (cached) { + return cached; + } + let options; + if (chart.options.animation !== false) { + const config = this.chart.config; + const scopeKeys = config.datasetAnimationScopeKeys(this._type, transition); + const scopes = config.getOptionScopes(this.getDataset(), scopeKeys); + options = config.createResolver(scopes, this.getContext(index, active, transition)); + } + const animations = new Animations(chart, options && options.animations); + if (options && options._cacheable) { + cache[cacheKey] = Object.freeze(animations); + } + return animations; + } + + /** + * Utility for getting the options object shared between elements + * @protected + */ + getSharedOptions(options) { + if (!options.$shared) { + return; + } + return this._sharedOptions || (this._sharedOptions = Object.assign({}, options)); + } + + /** + * Utility for determining if `options` should be included in the updated properties + * @protected + */ + includeOptions(mode, sharedOptions) { + return !sharedOptions || isDirectUpdateMode(mode) || this.chart._animationsDisabled; + } + + /** + * @todo v4, rename to getSharedOptions and remove excess functions + */ + _getSharedOptions(start, mode) { + const firstOpts = this.resolveDataElementOptions(start, mode); + const previouslySharedOptions = this._sharedOptions; + const sharedOptions = this.getSharedOptions(firstOpts); + const includeOptions = this.includeOptions(mode, sharedOptions) || (sharedOptions !== previouslySharedOptions); + this.updateSharedOptions(sharedOptions, mode, firstOpts); + return {sharedOptions, includeOptions}; + } + + /** + * Utility for updating an element with new properties, using animations when appropriate. + * @protected + */ + updateElement(element, index, properties, mode) { + if (isDirectUpdateMode(mode)) { + Object.assign(element, properties); + } else { + this._resolveAnimations(index, mode).update(element, properties); + } + } + + /** + * Utility to animate the shared options, that are potentially affecting multiple elements. + * @protected + */ + updateSharedOptions(sharedOptions, mode, newOptions) { + if (sharedOptions && !isDirectUpdateMode(mode)) { + this._resolveAnimations(undefined, mode).update(sharedOptions, newOptions); + } + } + + /** + * @private + */ + _setStyle(element, index, mode, active) { + element.active = active; + const options = this.getStyle(index, active); + this._resolveAnimations(index, mode, active).update(element, { + // When going from active to inactive, we need to update to the shared options. + // This way the once hovered element will end up with the same original shared options instance, after animation. + options: (!active && this.getSharedOptions(options)) || options + }); + } + + removeHoverStyle(element, datasetIndex, index) { + this._setStyle(element, index, 'active', false); + } + + setHoverStyle(element, datasetIndex, index) { + this._setStyle(element, index, 'active', true); + } + + /** + * @private + */ + _removeDatasetHoverStyle() { + const element = this._cachedMeta.dataset; + + if (element) { + this._setStyle(element, undefined, 'active', false); + } + } + + /** + * @private + */ + _setDatasetHoverStyle() { + const element = this._cachedMeta.dataset; + + if (element) { + this._setStyle(element, undefined, 'active', true); + } + } + + /** + * @private + */ + _resyncElements(resetNewElements) { + const data = this._data; + const elements = this._cachedMeta.data; + + // Apply changes detected through array listeners + for (const [method, arg1, arg2] of this._syncList) { + this[method](arg1, arg2); + } + this._syncList = []; + + const numMeta = elements.length; + const numData = data.length; + const count = Math.min(numData, numMeta); + + if (count) { + // TODO: It is not optimal to always parse the old data + // This is done because we are not detecting direct assignments: + // chart.data.datasets[0].data[5] = 10; + // chart.data.datasets[0].data[5].y = 10; + this.parse(0, count); + } + + if (numData > numMeta) { + this._insertElements(numMeta, numData - numMeta, resetNewElements); + } else if (numData < numMeta) { + this._removeElements(numData, numMeta - numData); + } + } + + /** + * @private + */ + _insertElements(start, count, resetNewElements = true) { + const meta = this._cachedMeta; + const data = meta.data; + const end = start + count; + let i; + + const move = (arr) => { + arr.length += count; + for (i = arr.length - 1; i >= end; i--) { + arr[i] = arr[i - count]; + } + }; + move(data); + + for (i = start; i < end; ++i) { + data[i] = new this.dataElementType(); + } + + if (this._parsing) { + move(meta._parsed); + } + this.parse(start, count); + + if (resetNewElements) { + this.updateElements(data, start, count, 'reset'); + } + } + + updateElements(element, start, count, mode) {} // eslint-disable-line no-unused-vars + + /** + * @private + */ + _removeElements(start, count) { + const meta = this._cachedMeta; + if (this._parsing) { + const removed = meta._parsed.splice(start, count); + if (meta._stacked) { + clearStacks(meta, removed); + } + } + meta.data.splice(start, count); + } + + /** + * @private + */ + _sync(args) { + if (this._parsing) { + this._syncList.push(args); + } else { + const [method, arg1, arg2] = args; + this[method](arg1, arg2); + } + this.chart._dataChanges.push([this.index, ...args]); + } + + _onDataPush() { + const count = arguments.length; + this._sync(['_insertElements', this.getDataset().data.length - count, count]); + } + + _onDataPop() { + this._sync(['_removeElements', this._cachedMeta.data.length - 1, 1]); + } + + _onDataShift() { + this._sync(['_removeElements', 0, 1]); + } + + _onDataSplice(start, count) { + if (count) { + this._sync(['_removeElements', start, count]); + } + const newCount = arguments.length - 2; + if (newCount) { + this._sync(['_insertElements', start, newCount]); + } + } + + _onDataUnshift() { + this._sync(['_insertElements', 0, arguments.length]); + } +} diff --git a/src/core/core.defaults.js b/src/core/core.defaults.js new file mode 100644 index 00000000000..67a1c8e5b84 --- /dev/null +++ b/src/core/core.defaults.js @@ -0,0 +1,175 @@ +import {getHoverColor} from '../helpers/helpers.color.js'; +import {isObject, merge, valueOrDefault} from '../helpers/helpers.core.js'; +import {applyAnimationsDefaults} from './core.animations.defaults.js'; +import {applyLayoutsDefaults} from './core.layouts.defaults.js'; +import {applyScaleDefaults} from './core.scale.defaults.js'; + +export const overrides = Object.create(null); +export const descriptors = Object.create(null); + +/** + * @param {object} node + * @param {string} key + * @return {object} + */ +function getScope(node, key) { + if (!key) { + return node; + } + const keys = key.split('.'); + for (let i = 0, n = keys.length; i < n; ++i) { + const k = keys[i]; + node = node[k] || (node[k] = Object.create(null)); + } + return node; +} + +function set(root, scope, values) { + if (typeof scope === 'string') { + return merge(getScope(root, scope), values); + } + return merge(getScope(root, ''), scope); +} + +/** + * Please use the module's default export which provides a singleton instance + * Note: class is exported for typedoc + */ +export class Defaults { + constructor(_descriptors, _appliers) { + this.animation = undefined; + this.backgroundColor = 'rgba(0,0,0,0.1)'; + this.borderColor = 'rgba(0,0,0,0.1)'; + this.color = '#666'; + this.datasets = {}; + this.devicePixelRatio = (context) => context.chart.platform.getDevicePixelRatio(); + this.elements = {}; + this.events = [ + 'mousemove', + 'mouseout', + 'click', + 'touchstart', + 'touchmove' + ]; + this.font = { + family: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + size: 12, + style: 'normal', + lineHeight: 1.2, + weight: null + }; + this.hover = {}; + this.hoverBackgroundColor = (ctx, options) => getHoverColor(options.backgroundColor); + this.hoverBorderColor = (ctx, options) => getHoverColor(options.borderColor); + this.hoverColor = (ctx, options) => getHoverColor(options.color); + this.indexAxis = 'x'; + this.interaction = { + mode: 'nearest', + intersect: true, + includeInvisible: false + }; + this.maintainAspectRatio = true; + this.onHover = null; + this.onClick = null; + this.parsing = true; + this.plugins = {}; + this.responsive = true; + this.scale = undefined; + this.scales = {}; + this.showLine = true; + this.drawActiveElementsOnTop = true; + + this.describe(_descriptors); + this.apply(_appliers); + } + + /** + * @param {string|object} scope + * @param {object} [values] + */ + set(scope, values) { + return set(this, scope, values); + } + + /** + * @param {string} scope + */ + get(scope) { + return getScope(this, scope); + } + + /** + * @param {string|object} scope + * @param {object} [values] + */ + describe(scope, values) { + return set(descriptors, scope, values); + } + + override(scope, values) { + return set(overrides, scope, values); + } + + /** + * Routes the named defaults to fallback to another scope/name. + * This routing is useful when those target values, like defaults.color, are changed runtime. + * If the values would be copied, the runtime change would not take effect. By routing, the + * fallback is evaluated at each access, so its always up to date. + * + * Example: + * + * defaults.route('elements.arc', 'backgroundColor', '', 'color') + * - reads the backgroundColor from defaults.color when undefined locally + * + * @param {string} scope Scope this route applies to. + * @param {string} name Property name that should be routed to different namespace when not defined here. + * @param {string} targetScope The namespace where those properties should be routed to. + * Empty string ('') is the root of defaults. + * @param {string} targetName The target name in the target scope the property should be routed to. + */ + route(scope, name, targetScope, targetName) { + const scopeObject = getScope(this, scope); + const targetScopeObject = getScope(this, targetScope); + const privateName = '_' + name; + + Object.defineProperties(scopeObject, { + // A private property is defined to hold the actual value, when this property is set in its scope (set in the setter) + [privateName]: { + value: scopeObject[name], + writable: true + }, + // The actual property is defined as getter/setter so we can do the routing when value is not locally set. + [name]: { + enumerable: true, + get() { + const local = this[privateName]; + const target = targetScopeObject[targetName]; + if (isObject(local)) { + return Object.assign({}, target, local); + } + return valueOrDefault(local, target); + }, + set(value) { + this[privateName] = value; + } + } + }); + } + + apply(appliers) { + appliers.forEach((apply) => apply(this)); + } +} + +// singleton instance +export default /* #__PURE__ */ new Defaults({ + _scriptable: (name) => !name.startsWith('on'), + _indexable: (name) => name !== 'events', + hover: { + _fallback: 'interaction' + }, + interaction: { + _scriptable: false, + _indexable: false, + } +}, [applyAnimationsDefaults, applyLayoutsDefaults, applyScaleDefaults]); diff --git a/src/core/core.element.ts b/src/core/core.element.ts new file mode 100644 index 00000000000..9c1e76172c9 --- /dev/null +++ b/src/core/core.element.ts @@ -0,0 +1,45 @@ +import type {AnyObject} from '../types/basic.js'; +import type {Point} from '../types/geometric.js'; +import type {Animation} from '../types/animation.js'; +import {isNumber} from '../helpers/helpers.math.js'; + +export default class Element { + + static defaults = {}; + static defaultRoutes = undefined; + + x: number; + y: number; + active = false; + options: O; + $animations: Record; + + tooltipPosition(useFinalPosition: boolean): Point { + const {x, y} = this.getProps(['x', 'y'], useFinalPosition); + return {x, y} as Point; + } + + hasValue() { + return isNumber(this.x) && isNumber(this.y); + } + + /** + * Gets the current or final value of each prop. Can return extra properties (whole object). + * @param props - properties to get + * @param [final] - get the final value (animation target) + */ + getProps

    (props: P, final?: boolean): Pick; + getProps

    (props: P[], final?: boolean): Partial>; + getProps(props: string[], final?: boolean): Partial> { + const anims = this.$animations; + if (!final || !anims) { + // let's not create an object, if not needed + return this as Record; + } + const ret: Record = {}; + props.forEach((prop) => { + ret[prop] = anims[prop] && anims[prop].active() ? anims[prop]._to : this[prop as string]; + }); + return ret; + } +} diff --git a/src/core/core.interaction.js b/src/core/core.interaction.js new file mode 100644 index 00000000000..d84318b34d5 --- /dev/null +++ b/src/core/core.interaction.js @@ -0,0 +1,387 @@ +import {_lookupByKey, _rlookupByKey} from '../helpers/helpers.collection.js'; +import {getRelativePosition} from '../helpers/helpers.dom.js'; +import {_angleBetween, getAngleFromPoint} from '../helpers/helpers.math.js'; +import {_isPointInArea, isNullOrUndef} from '../helpers/index.js'; + +/** + * @typedef { import('./core.controller.js').default } Chart + * @typedef { import('../types/index.js').ChartEvent } ChartEvent + * @typedef {{axis?: string, intersect?: boolean, includeInvisible?: boolean}} InteractionOptions + * @typedef {{datasetIndex: number, index: number, element: import('./core.element.js').default}} InteractionItem + * @typedef { import('../types/index.js').Point } Point + */ + +/** + * Helper function to do binary search when possible + * @param {object} metaset - the dataset meta + * @param {string} axis - the axis mode. x|y|xy|r + * @param {number} value - the value to find + * @param {boolean} [intersect] - should the element intersect + * @returns {{lo:number, hi:number}} indices to search data array between + */ +function binarySearch(metaset, axis, value, intersect) { + const {controller, data, _sorted} = metaset; + const iScale = controller._cachedMeta.iScale; + const spanGaps = metaset.dataset ? metaset.dataset.options ? metaset.dataset.options.spanGaps : null : null; + + if (iScale && axis === iScale.axis && axis !== 'r' && _sorted && data.length) { + const lookupMethod = iScale._reversePixels ? _rlookupByKey : _lookupByKey; + if (!intersect) { + const result = lookupMethod(data, axis, value); + if (spanGaps) { + const {vScale} = controller._cachedMeta; + const {_parsed} = metaset; + + const distanceToDefinedLo = (_parsed + .slice(0, result.lo + 1) + .reverse() + .findIndex( + point => !isNullOrUndef(point[vScale.axis]))); + result.lo -= Math.max(0, distanceToDefinedLo); + + const distanceToDefinedHi = (_parsed + .slice(result.hi) + .findIndex( + point => !isNullOrUndef(point[vScale.axis]))); + result.hi += Math.max(0, distanceToDefinedHi); + } + return result; + } else if (controller._sharedOptions) { + // _sharedOptions indicates that each element has equal options -> equal proportions + // So we can do a ranged binary search based on the range of first element and + // be confident to get the full range of indices that can intersect with the value. + const el = data[0]; + const range = typeof el.getRange === 'function' && el.getRange(axis); + if (range) { + const start = lookupMethod(data, axis, value - range); + const end = lookupMethod(data, axis, value + range); + return {lo: start.lo, hi: end.hi}; + } + } + } + // Default to all elements, when binary search can not be used. + return {lo: 0, hi: data.length - 1}; +} + +/** + * Helper function to select candidate elements for interaction + * @param {Chart} chart - the chart + * @param {string} axis - the axis mode. x|y|xy|r + * @param {Point} position - the point to be nearest to, in relative coordinates + * @param {function} handler - the callback to execute for each visible item + * @param {boolean} [intersect] - consider intersecting items + */ +function evaluateInteractionItems(chart, axis, position, handler, intersect) { + const metasets = chart.getSortedVisibleDatasetMetas(); + const value = position[axis]; + for (let i = 0, ilen = metasets.length; i < ilen; ++i) { + const {index, data} = metasets[i]; + const {lo, hi} = binarySearch(metasets[i], axis, value, intersect); + for (let j = lo; j <= hi; ++j) { + const element = data[j]; + if (!element.skip) { + handler(element, index, j); + } + } + } +} + +/** + * Get a distance metric function for two points based on the + * axis mode setting + * @param {string} axis - the axis mode. x|y|xy|r + */ +function getDistanceMetricForAxis(axis) { + const useX = axis.indexOf('x') !== -1; + const useY = axis.indexOf('y') !== -1; + + return function(pt1, pt2) { + const deltaX = useX ? Math.abs(pt1.x - pt2.x) : 0; + const deltaY = useY ? Math.abs(pt1.y - pt2.y) : 0; + return Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2)); + }; +} + +/** + * Helper function to get the items that intersect the event position + * @param {Chart} chart - the chart + * @param {Point} position - the point to be nearest to, in relative coordinates + * @param {string} axis - the axis mode. x|y|xy|r + * @param {boolean} [useFinalPosition] - use the element's animation target instead of current position + * @param {boolean} [includeInvisible] - include invisible points that are outside of the chart area + * @return {InteractionItem[]} the nearest items + */ +function getIntersectItems(chart, position, axis, useFinalPosition, includeInvisible) { + const items = []; + + if (!includeInvisible && !chart.isPointInArea(position)) { + return items; + } + + const evaluationFunc = function(element, datasetIndex, index) { + if (!includeInvisible && !_isPointInArea(element, chart.chartArea, 0)) { + return; + } + if (element.inRange(position.x, position.y, useFinalPosition)) { + items.push({element, datasetIndex, index}); + } + }; + + evaluateInteractionItems(chart, axis, position, evaluationFunc, true); + return items; +} + +/** + * Helper function to get the items nearest to the event position for a radial chart + * @param {Chart} chart - the chart to look at elements from + * @param {Point} position - the point to be nearest to, in relative coordinates + * @param {string} axis - the axes along which to measure distance + * @param {boolean} [useFinalPosition] - use the element's animation target instead of current position + * @return {InteractionItem[]} the nearest items + */ +function getNearestRadialItems(chart, position, axis, useFinalPosition) { + let items = []; + + function evaluationFunc(element, datasetIndex, index) { + const {startAngle, endAngle} = element.getProps(['startAngle', 'endAngle'], useFinalPosition); + const {angle} = getAngleFromPoint(element, {x: position.x, y: position.y}); + + if (_angleBetween(angle, startAngle, endAngle)) { + items.push({element, datasetIndex, index}); + } + } + + evaluateInteractionItems(chart, axis, position, evaluationFunc); + return items; +} + +/** + * Helper function to get the items nearest to the event position for a cartesian chart + * @param {Chart} chart - the chart to look at elements from + * @param {Point} position - the point to be nearest to, in relative coordinates + * @param {string} axis - the axes along which to measure distance + * @param {boolean} [intersect] - if true, only consider items that intersect the position + * @param {boolean} [useFinalPosition] - use the element's animation target instead of current position + * @param {boolean} [includeInvisible] - include invisible points that are outside of the chart area + * @return {InteractionItem[]} the nearest items + */ +function getNearestCartesianItems(chart, position, axis, intersect, useFinalPosition, includeInvisible) { + let items = []; + const distanceMetric = getDistanceMetricForAxis(axis); + let minDistance = Number.POSITIVE_INFINITY; + + function evaluationFunc(element, datasetIndex, index) { + const inRange = element.inRange(position.x, position.y, useFinalPosition); + if (intersect && !inRange) { + return; + } + + const center = element.getCenterPoint(useFinalPosition); + const pointInArea = !!includeInvisible || chart.isPointInArea(center); + if (!pointInArea && !inRange) { + return; + } + + const distance = distanceMetric(position, center); + if (distance < minDistance) { + items = [{element, datasetIndex, index}]; + minDistance = distance; + } else if (distance === minDistance) { + // Can have multiple items at the same distance in which case we sort by size + items.push({element, datasetIndex, index}); + } + } + + evaluateInteractionItems(chart, axis, position, evaluationFunc); + return items; +} + +/** + * Helper function to get the items nearest to the event position considering all visible items in the chart + * @param {Chart} chart - the chart to look at elements from + * @param {Point} position - the point to be nearest to, in relative coordinates + * @param {string} axis - the axes along which to measure distance + * @param {boolean} [intersect] - if true, only consider items that intersect the position + * @param {boolean} [useFinalPosition] - use the element's animation target instead of current position + * @param {boolean} [includeInvisible] - include invisible points that are outside of the chart area + * @return {InteractionItem[]} the nearest items + */ +function getNearestItems(chart, position, axis, intersect, useFinalPosition, includeInvisible) { + if (!includeInvisible && !chart.isPointInArea(position)) { + return []; + } + + return axis === 'r' && !intersect + ? getNearestRadialItems(chart, position, axis, useFinalPosition) + : getNearestCartesianItems(chart, position, axis, intersect, useFinalPosition, includeInvisible); +} + +/** + * Helper function to get the items matching along the given X or Y axis + * @param {Chart} chart - the chart to look at elements from + * @param {Point} position - the point to be nearest to, in relative coordinates + * @param {string} axis - the axis to match + * @param {boolean} [intersect] - if true, only consider items that intersect the position + * @param {boolean} [useFinalPosition] - use the element's animation target instead of current position + * @return {InteractionItem[]} the nearest items + */ +function getAxisItems(chart, position, axis, intersect, useFinalPosition) { + const items = []; + const rangeMethod = axis === 'x' ? 'inXRange' : 'inYRange'; + let intersectsItem = false; + + evaluateInteractionItems(chart, axis, position, (element, datasetIndex, index) => { + if (element[rangeMethod] && element[rangeMethod](position[axis], useFinalPosition)) { + items.push({element, datasetIndex, index}); + intersectsItem = intersectsItem || element.inRange(position.x, position.y, useFinalPosition); + } + }); + + // If we want to trigger on an intersect and we don't have any items + // that intersect the position, return nothing + if (intersect && !intersectsItem) { + return []; + } + return items; +} + +/** + * Contains interaction related functions + * @namespace Chart.Interaction + */ +export default { + // Part of the public API to facilitate developers creating their own modes + evaluateInteractionItems, + + // Helper function for different modes + modes: { + /** + * Returns items at the same index. If the options.intersect parameter is true, we only return items if we intersect something + * If the options.intersect mode is false, we find the nearest item and return the items at the same index as that item + * @function Chart.Interaction.modes.index + * @since v2.4.0 + * @param {Chart} chart - the chart we are returning items from + * @param {Event} e - the event we are find things at + * @param {InteractionOptions} options - options to use + * @param {boolean} [useFinalPosition] - use final element position (animation target) + * @return {InteractionItem[]} - items that are found + */ + index(chart, e, options, useFinalPosition) { + const position = getRelativePosition(e, chart); + // Default axis for index mode is 'x' to match old behaviour + const axis = options.axis || 'x'; + const includeInvisible = options.includeInvisible || false; + const items = options.intersect + ? getIntersectItems(chart, position, axis, useFinalPosition, includeInvisible) + : getNearestItems(chart, position, axis, false, useFinalPosition, includeInvisible); + const elements = []; + + if (!items.length) { + return []; + } + + chart.getSortedVisibleDatasetMetas().forEach((meta) => { + const index = items[0].index; + const element = meta.data[index]; + + // don't count items that are skipped (null data) + if (element && !element.skip) { + elements.push({element, datasetIndex: meta.index, index}); + } + }); + + return elements; + }, + + /** + * Returns items in the same dataset. If the options.intersect parameter is true, we only return items if we intersect something + * If the options.intersect is false, we find the nearest item and return the items in that dataset + * @function Chart.Interaction.modes.dataset + * @param {Chart} chart - the chart we are returning items from + * @param {Event} e - the event we are find things at + * @param {InteractionOptions} options - options to use + * @param {boolean} [useFinalPosition] - use final element position (animation target) + * @return {InteractionItem[]} - items that are found + */ + dataset(chart, e, options, useFinalPosition) { + const position = getRelativePosition(e, chart); + const axis = options.axis || 'xy'; + const includeInvisible = options.includeInvisible || false; + let items = options.intersect + ? getIntersectItems(chart, position, axis, useFinalPosition, includeInvisible) : + getNearestItems(chart, position, axis, false, useFinalPosition, includeInvisible); + + if (items.length > 0) { + const datasetIndex = items[0].datasetIndex; + const data = chart.getDatasetMeta(datasetIndex).data; + items = []; + for (let i = 0; i < data.length; ++i) { + items.push({element: data[i], datasetIndex, index: i}); + } + } + + return items; + }, + + /** + * Point mode returns all elements that hit test based on the event position + * of the event + * @function Chart.Interaction.modes.intersect + * @param {Chart} chart - the chart we are returning items from + * @param {Event} e - the event we are find things at + * @param {InteractionOptions} options - options to use + * @param {boolean} [useFinalPosition] - use final element position (animation target) + * @return {InteractionItem[]} - items that are found + */ + point(chart, e, options, useFinalPosition) { + const position = getRelativePosition(e, chart); + const axis = options.axis || 'xy'; + const includeInvisible = options.includeInvisible || false; + return getIntersectItems(chart, position, axis, useFinalPosition, includeInvisible); + }, + + /** + * nearest mode returns the element closest to the point + * @function Chart.Interaction.modes.intersect + * @param {Chart} chart - the chart we are returning items from + * @param {Event} e - the event we are find things at + * @param {InteractionOptions} options - options to use + * @param {boolean} [useFinalPosition] - use final element position (animation target) + * @return {InteractionItem[]} - items that are found + */ + nearest(chart, e, options, useFinalPosition) { + const position = getRelativePosition(e, chart); + const axis = options.axis || 'xy'; + const includeInvisible = options.includeInvisible || false; + return getNearestItems(chart, position, axis, options.intersect, useFinalPosition, includeInvisible); + }, + + /** + * x mode returns the elements that hit-test at the current x coordinate + * @function Chart.Interaction.modes.x + * @param {Chart} chart - the chart we are returning items from + * @param {Event} e - the event we are find things at + * @param {InteractionOptions} options - options to use + * @param {boolean} [useFinalPosition] - use final element position (animation target) + * @return {InteractionItem[]} - items that are found + */ + x(chart, e, options, useFinalPosition) { + const position = getRelativePosition(e, chart); + return getAxisItems(chart, position, 'x', options.intersect, useFinalPosition); + }, + + /** + * y mode returns the elements that hit-test at the current y coordinate + * @function Chart.Interaction.modes.y + * @param {Chart} chart - the chart we are returning items from + * @param {Event} e - the event we are find things at + * @param {InteractionOptions} options - options to use + * @param {boolean} [useFinalPosition] - use final element position (animation target) + * @return {InteractionItem[]} - items that are found + */ + y(chart, e, options, useFinalPosition) { + const position = getRelativePosition(e, chart); + return getAxisItems(chart, position, 'y', options.intersect, useFinalPosition); + } + } +}; diff --git a/src/core/core.layouts.defaults.js b/src/core/core.layouts.defaults.js new file mode 100644 index 00000000000..cac2a7bca2d --- /dev/null +++ b/src/core/core.layouts.defaults.js @@ -0,0 +1,11 @@ +export function applyLayoutsDefaults(defaults) { + defaults.set('layout', { + autoPadding: true, + padding: { + top: 0, + right: 0, + bottom: 0, + left: 0 + } + }); +} diff --git a/src/core/core.layouts.js b/src/core/core.layouts.js new file mode 100644 index 00000000000..301a1241733 --- /dev/null +++ b/src/core/core.layouts.js @@ -0,0 +1,455 @@ +import {defined, each, isObject} from '../helpers/helpers.core.js'; +import {toPadding} from '../helpers/helpers.options.js'; + +/** + * @typedef { import('./core.controller.js').default } Chart + */ + +const STATIC_POSITIONS = ['left', 'top', 'right', 'bottom']; + +function filterByPosition(array, position) { + return array.filter(v => v.pos === position); +} + +function filterDynamicPositionByAxis(array, axis) { + return array.filter(v => STATIC_POSITIONS.indexOf(v.pos) === -1 && v.box.axis === axis); +} + +function sortByWeight(array, reverse) { + return array.sort((a, b) => { + const v0 = reverse ? b : a; + const v1 = reverse ? a : b; + return v0.weight === v1.weight ? + v0.index - v1.index : + v0.weight - v1.weight; + }); +} + +function wrapBoxes(boxes) { + const layoutBoxes = []; + let i, ilen, box, pos, stack, stackWeight; + + for (i = 0, ilen = (boxes || []).length; i < ilen; ++i) { + box = boxes[i]; + ({position: pos, options: {stack, stackWeight = 1}} = box); + layoutBoxes.push({ + index: i, + box, + pos, + horizontal: box.isHorizontal(), + weight: box.weight, + stack: stack && (pos + stack), + stackWeight + }); + } + return layoutBoxes; +} + +function buildStacks(layouts) { + const stacks = {}; + for (const wrap of layouts) { + const {stack, pos, stackWeight} = wrap; + if (!stack || !STATIC_POSITIONS.includes(pos)) { + continue; + } + const _stack = stacks[stack] || (stacks[stack] = {count: 0, placed: 0, weight: 0, size: 0}); + _stack.count++; + _stack.weight += stackWeight; + } + return stacks; +} + +/** + * store dimensions used instead of available chartArea in fitBoxes + **/ +function setLayoutDims(layouts, params) { + const stacks = buildStacks(layouts); + const {vBoxMaxWidth, hBoxMaxHeight} = params; + let i, ilen, layout; + for (i = 0, ilen = layouts.length; i < ilen; ++i) { + layout = layouts[i]; + const {fullSize} = layout.box; + const stack = stacks[layout.stack]; + const factor = stack && layout.stackWeight / stack.weight; + if (layout.horizontal) { + layout.width = factor ? factor * vBoxMaxWidth : fullSize && params.availableWidth; + layout.height = hBoxMaxHeight; + } else { + layout.width = vBoxMaxWidth; + layout.height = factor ? factor * hBoxMaxHeight : fullSize && params.availableHeight; + } + } + return stacks; +} + +function buildLayoutBoxes(boxes) { + const layoutBoxes = wrapBoxes(boxes); + const fullSize = sortByWeight(layoutBoxes.filter(wrap => wrap.box.fullSize), true); + const left = sortByWeight(filterByPosition(layoutBoxes, 'left'), true); + const right = sortByWeight(filterByPosition(layoutBoxes, 'right')); + const top = sortByWeight(filterByPosition(layoutBoxes, 'top'), true); + const bottom = sortByWeight(filterByPosition(layoutBoxes, 'bottom')); + const centerHorizontal = filterDynamicPositionByAxis(layoutBoxes, 'x'); + const centerVertical = filterDynamicPositionByAxis(layoutBoxes, 'y'); + + return { + fullSize, + leftAndTop: left.concat(top), + rightAndBottom: right.concat(centerVertical).concat(bottom).concat(centerHorizontal), + chartArea: filterByPosition(layoutBoxes, 'chartArea'), + vertical: left.concat(right).concat(centerVertical), + horizontal: top.concat(bottom).concat(centerHorizontal) + }; +} + +function getCombinedMax(maxPadding, chartArea, a, b) { + return Math.max(maxPadding[a], chartArea[a]) + Math.max(maxPadding[b], chartArea[b]); +} + +function updateMaxPadding(maxPadding, boxPadding) { + maxPadding.top = Math.max(maxPadding.top, boxPadding.top); + maxPadding.left = Math.max(maxPadding.left, boxPadding.left); + maxPadding.bottom = Math.max(maxPadding.bottom, boxPadding.bottom); + maxPadding.right = Math.max(maxPadding.right, boxPadding.right); +} + +function updateDims(chartArea, params, layout, stacks) { + const {pos, box} = layout; + const maxPadding = chartArea.maxPadding; + + // dynamically placed boxes size is not considered + if (!isObject(pos)) { + if (layout.size) { + // this layout was already counted for, lets first reduce old size + chartArea[pos] -= layout.size; + } + const stack = stacks[layout.stack] || {size: 0, count: 1}; + stack.size = Math.max(stack.size, layout.horizontal ? box.height : box.width); + layout.size = stack.size / stack.count; + chartArea[pos] += layout.size; + } + + if (box.getPadding) { + updateMaxPadding(maxPadding, box.getPadding()); + } + + const newWidth = Math.max(0, params.outerWidth - getCombinedMax(maxPadding, chartArea, 'left', 'right')); + const newHeight = Math.max(0, params.outerHeight - getCombinedMax(maxPadding, chartArea, 'top', 'bottom')); + const widthChanged = newWidth !== chartArea.w; + const heightChanged = newHeight !== chartArea.h; + chartArea.w = newWidth; + chartArea.h = newHeight; + + // return booleans on the changes per direction + return layout.horizontal + ? {same: widthChanged, other: heightChanged} + : {same: heightChanged, other: widthChanged}; +} + +function handleMaxPadding(chartArea) { + const maxPadding = chartArea.maxPadding; + + function updatePos(pos) { + const change = Math.max(maxPadding[pos] - chartArea[pos], 0); + chartArea[pos] += change; + return change; + } + chartArea.y += updatePos('top'); + chartArea.x += updatePos('left'); + updatePos('right'); + updatePos('bottom'); +} + +function getMargins(horizontal, chartArea) { + const maxPadding = chartArea.maxPadding; + + function marginForPositions(positions) { + const margin = {left: 0, top: 0, right: 0, bottom: 0}; + positions.forEach((pos) => { + margin[pos] = Math.max(chartArea[pos], maxPadding[pos]); + }); + return margin; + } + + return horizontal + ? marginForPositions(['left', 'right']) + : marginForPositions(['top', 'bottom']); +} + +function fitBoxes(boxes, chartArea, params, stacks) { + const refitBoxes = []; + let i, ilen, layout, box, refit, changed; + + for (i = 0, ilen = boxes.length, refit = 0; i < ilen; ++i) { + layout = boxes[i]; + box = layout.box; + + box.update( + layout.width || chartArea.w, + layout.height || chartArea.h, + getMargins(layout.horizontal, chartArea) + ); + const {same, other} = updateDims(chartArea, params, layout, stacks); + + // Dimensions changed and there were non full width boxes before this + // -> we have to refit those + refit |= same && refitBoxes.length; + + // Chart area changed in the opposite direction + changed = changed || other; + + if (!box.fullSize) { // fullSize boxes don't need to be re-fitted in any case + refitBoxes.push(layout); + } + } + + return refit && fitBoxes(refitBoxes, chartArea, params, stacks) || changed; +} + +function setBoxDims(box, left, top, width, height) { + box.top = top; + box.left = left; + box.right = left + width; + box.bottom = top + height; + box.width = width; + box.height = height; +} + +function placeBoxes(boxes, chartArea, params, stacks) { + const userPadding = params.padding; + let {x, y} = chartArea; + + for (const layout of boxes) { + const box = layout.box; + const stack = stacks[layout.stack] || {count: 1, placed: 0, weight: 1}; + const weight = (layout.stackWeight / stack.weight) || 1; + if (layout.horizontal) { + const width = chartArea.w * weight; + const height = stack.size || box.height; + if (defined(stack.start)) { + y = stack.start; + } + if (box.fullSize) { + setBoxDims(box, userPadding.left, y, params.outerWidth - userPadding.right - userPadding.left, height); + } else { + setBoxDims(box, chartArea.left + stack.placed, y, width, height); + } + stack.start = y; + stack.placed += width; + y = box.bottom; + } else { + const height = chartArea.h * weight; + const width = stack.size || box.width; + if (defined(stack.start)) { + x = stack.start; + } + if (box.fullSize) { + setBoxDims(box, x, userPadding.top, width, params.outerHeight - userPadding.bottom - userPadding.top); + } else { + setBoxDims(box, x, chartArea.top + stack.placed, width, height); + } + stack.start = x; + stack.placed += height; + x = box.right; + } + } + + chartArea.x = x; + chartArea.y = y; +} + +/** + * @interface LayoutItem + * @typedef {object} LayoutItem + * @prop {string} position - The position of the item in the chart layout. Possible values are + * 'left', 'top', 'right', 'bottom', and 'chartArea' + * @prop {number} weight - The weight used to sort the item. Higher weights are further away from the chart area + * @prop {boolean} fullSize - if true, and the item is horizontal, then push vertical boxes down + * @prop {function} isHorizontal - returns true if the layout item is horizontal (ie. top or bottom) + * @prop {function} update - Takes two parameters: width and height. Returns size of item + * @prop {function} draw - Draws the element + * @prop {function} [getPadding] - Returns an object with padding on the edges + * @prop {number} width - Width of item. Must be valid after update() + * @prop {number} height - Height of item. Must be valid after update() + * @prop {number} left - Left edge of the item. Set by layout system and cannot be used in update + * @prop {number} top - Top edge of the item. Set by layout system and cannot be used in update + * @prop {number} right - Right edge of the item. Set by layout system and cannot be used in update + * @prop {number} bottom - Bottom edge of the item. Set by layout system and cannot be used in update + */ + +// The layout service is very self explanatory. It's responsible for the layout within a chart. +// Scales, Legends and Plugins all rely on the layout service and can easily register to be placed anywhere they need +// It is this service's responsibility of carrying out that layout. +export default { + + /** + * Register a box to a chart. + * A box is simply a reference to an object that requires layout. eg. Scales, Legend, Title. + * @param {Chart} chart - the chart to use + * @param {LayoutItem} item - the item to add to be laid out + */ + addBox(chart, item) { + if (!chart.boxes) { + chart.boxes = []; + } + + // initialize item with default values + item.fullSize = item.fullSize || false; + item.position = item.position || 'top'; + item.weight = item.weight || 0; + // @ts-ignore + item._layers = item._layers || function() { + return [{ + z: 0, + draw(chartArea) { + item.draw(chartArea); + } + }]; + }; + + chart.boxes.push(item); + }, + + /** + * Remove a layoutItem from a chart + * @param {Chart} chart - the chart to remove the box from + * @param {LayoutItem} layoutItem - the item to remove from the layout + */ + removeBox(chart, layoutItem) { + const index = chart.boxes ? chart.boxes.indexOf(layoutItem) : -1; + if (index !== -1) { + chart.boxes.splice(index, 1); + } + }, + + /** + * Sets (or updates) options on the given `item`. + * @param {Chart} chart - the chart in which the item lives (or will be added to) + * @param {LayoutItem} item - the item to configure with the given options + * @param {object} options - the new item options. + */ + configure(chart, item, options) { + item.fullSize = options.fullSize; + item.position = options.position; + item.weight = options.weight; + }, + + /** + * Fits boxes of the given chart into the given size by having each box measure itself + * then running a fitting algorithm + * @param {Chart} chart - the chart + * @param {number} width - the width to fit into + * @param {number} height - the height to fit into + * @param {number} minPadding - minimum padding required for each side of chart area + */ + update(chart, width, height, minPadding) { + if (!chart) { + return; + } + + const padding = toPadding(chart.options.layout.padding); + const availableWidth = Math.max(width - padding.width, 0); + const availableHeight = Math.max(height - padding.height, 0); + const boxes = buildLayoutBoxes(chart.boxes); + const verticalBoxes = boxes.vertical; + const horizontalBoxes = boxes.horizontal; + + // Before any changes are made, notify boxes that an update is about to being + // This is used to clear any cached data (e.g. scale limits) + each(chart.boxes, box => { + if (typeof box.beforeLayout === 'function') { + box.beforeLayout(); + } + }); + + // Essentially we now have any number of boxes on each of the 4 sides. + // Our canvas looks like the following. + // The areas L1 and L2 are the left axes. R1 is the right axis, T1 is the top axis and + // B1 is the bottom axis + // There are also 4 quadrant-like locations (left to right instead of clockwise) reserved for chart overlays + // These locations are single-box locations only, when trying to register a chartArea location that is already taken, + // an error will be thrown. + // + // |----------------------------------------------------| + // | T1 (Full Width) | + // |----------------------------------------------------| + // | | | T2 | | + // | |----|-------------------------------------|----| + // | | | C1 | | C2 | | + // | | |----| |----| | + // | | | | | + // | L1 | L2 | ChartArea (C0) | R1 | + // | | | | | + // | | |----| |----| | + // | | | C3 | | C4 | | + // | |----|-------------------------------------|----| + // | | | B1 | | + // |----------------------------------------------------| + // | B2 (Full Width) | + // |----------------------------------------------------| + // + + const visibleVerticalBoxCount = verticalBoxes.reduce((total, wrap) => + wrap.box.options && wrap.box.options.display === false ? total : total + 1, 0) || 1; + + const params = Object.freeze({ + outerWidth: width, + outerHeight: height, + padding, + availableWidth, + availableHeight, + vBoxMaxWidth: availableWidth / 2 / visibleVerticalBoxCount, + hBoxMaxHeight: availableHeight / 2 + }); + const maxPadding = Object.assign({}, padding); + updateMaxPadding(maxPadding, toPadding(minPadding)); + const chartArea = Object.assign({ + maxPadding, + w: availableWidth, + h: availableHeight, + x: padding.left, + y: padding.top + }, padding); + + const stacks = setLayoutDims(verticalBoxes.concat(horizontalBoxes), params); + + // First fit the fullSize boxes, to reduce probability of re-fitting. + fitBoxes(boxes.fullSize, chartArea, params, stacks); + + // Then fit vertical boxes + fitBoxes(verticalBoxes, chartArea, params, stacks); + + // Then fit horizontal boxes + if (fitBoxes(horizontalBoxes, chartArea, params, stacks)) { + // if the area changed, re-fit vertical boxes + fitBoxes(verticalBoxes, chartArea, params, stacks); + } + + handleMaxPadding(chartArea); + + // Finally place the boxes to correct coordinates + placeBoxes(boxes.leftAndTop, chartArea, params, stacks); + + // Move to opposite side of chart + chartArea.x += chartArea.w; + chartArea.y += chartArea.h; + + placeBoxes(boxes.rightAndBottom, chartArea, params, stacks); + + chart.chartArea = { + left: chartArea.left, + top: chartArea.top, + right: chartArea.left + chartArea.w, + bottom: chartArea.top + chartArea.h, + height: chartArea.h, + width: chartArea.w, + }; + + // Finally update boxes in chartArea (radial scale for example) + each(boxes.chartArea, (layout) => { + const box = layout.box; + Object.assign(box, chart.chartArea); + box.update(chartArea.w, chartArea.h, {left: 0, top: 0, right: 0, bottom: 0}); + }); + } +}; diff --git a/src/core/core.plugins.js b/src/core/core.plugins.js new file mode 100644 index 00000000000..c73a2a1c2a2 --- /dev/null +++ b/src/core/core.plugins.js @@ -0,0 +1,188 @@ +import registry from './core.registry.js'; +import {callback as callCallback, isNullOrUndef, valueOrDefault} from '../helpers/helpers.core.js'; + +/** + * @typedef { import('./core.controller.js').default } Chart + * @typedef { import('../types/index.js').ChartEvent } ChartEvent + * @typedef { import('../plugins/plugin.tooltip.js').default } Tooltip + */ + +/** + * @callback filterCallback + * @param {{plugin: object, options: object}} value + * @param {number} [index] + * @param {array} [array] + * @param {object} [thisArg] + * @return {boolean} + */ + + +export default class PluginService { + constructor() { + this._init = undefined; + } + + /** + * Calls enabled plugins for `chart` on the specified hook and with the given args. + * This method immediately returns as soon as a plugin explicitly returns false. The + * returned value can be used, for instance, to interrupt the current action. + * @param {Chart} chart - The chart instance for which plugins should be called. + * @param {string} hook - The name of the plugin method to call (e.g. 'beforeUpdate'). + * @param {object} [args] - Extra arguments to apply to the hook call. + * @param {filterCallback} [filter] - Filtering function for limiting which plugins are notified + * @returns {boolean} false if any of the plugins return false, else returns true. + */ + notify(chart, hook, args, filter) { + if (hook === 'beforeInit') { + this._init = this._createDescriptors(chart, true); + this._notify(this._init, chart, 'install'); + } + + if (this._init === undefined) { // Do not trigger events before install + return; + } + + const descriptors = filter ? this._descriptors(chart).filter(filter) : this._descriptors(chart); + const result = this._notify(descriptors, chart, hook, args); + + if (hook === 'afterDestroy') { + this._notify(descriptors, chart, 'stop'); + this._notify(this._init, chart, 'uninstall'); + this._init = undefined; // Do not trigger events after uninstall + } + return result; + } + + /** + * @private + */ + _notify(descriptors, chart, hook, args) { + args = args || {}; + for (const descriptor of descriptors) { + const plugin = descriptor.plugin; + const method = plugin[hook]; + const params = [chart, args, descriptor.options]; + if (callCallback(method, params, plugin) === false && args.cancelable) { + return false; + } + } + + return true; + } + + invalidate() { + // When plugins are registered, there is the possibility of a double + // invalidate situation. In this case, we only want to invalidate once. + // If we invalidate multiple times, the `_oldCache` is lost and all of the + // plugins are restarted without being correctly stopped. + // See https://github.com/chartjs/Chart.js/issues/8147 + if (!isNullOrUndef(this._cache)) { + this._oldCache = this._cache; + this._cache = undefined; + } + } + + /** + * @param {Chart} chart + * @private + */ + _descriptors(chart) { + if (this._cache) { + return this._cache; + } + + const descriptors = this._cache = this._createDescriptors(chart); + + this._notifyStateChanges(chart); + + return descriptors; + } + + _createDescriptors(chart, all) { + const config = chart && chart.config; + const options = valueOrDefault(config.options && config.options.plugins, {}); + const plugins = allPlugins(config); + // options === false => all plugins are disabled + return options === false && !all ? [] : createDescriptors(chart, plugins, options, all); + } + + /** + * @param {Chart} chart + * @private + */ + _notifyStateChanges(chart) { + const previousDescriptors = this._oldCache || []; + const descriptors = this._cache; + const diff = (a, b) => a.filter(x => !b.some(y => x.plugin.id === y.plugin.id)); + this._notify(diff(previousDescriptors, descriptors), chart, 'stop'); + this._notify(diff(descriptors, previousDescriptors), chart, 'start'); + } +} + +/** + * @param {import('./core.config.js').default} config + */ +function allPlugins(config) { + const localIds = {}; + const plugins = []; + const keys = Object.keys(registry.plugins.items); + for (let i = 0; i < keys.length; i++) { + plugins.push(registry.getPlugin(keys[i])); + } + + const local = config.plugins || []; + for (let i = 0; i < local.length; i++) { + const plugin = local[i]; + + if (plugins.indexOf(plugin) === -1) { + plugins.push(plugin); + localIds[plugin.id] = true; + } + } + + return {plugins, localIds}; +} + +function getOpts(options, all) { + if (!all && options === false) { + return null; + } + if (options === true) { + return {}; + } + return options; +} + +function createDescriptors(chart, {plugins, localIds}, options, all) { + const result = []; + const context = chart.getContext(); + + for (const plugin of plugins) { + const id = plugin.id; + const opts = getOpts(options[id], all); + if (opts === null) { + continue; + } + result.push({ + plugin, + options: pluginOpts(chart.config, {plugin, local: localIds[id]}, opts, context) + }); + } + + return result; +} + +function pluginOpts(config, {plugin, local}, opts, context) { + const keys = config.pluginScopeKeys(plugin); + const scopes = config.getOptionScopes(opts, keys); + if (local && plugin.defaults) { + // make sure plugin defaults are in scopes for local (not registered) plugins + scopes.push(plugin.defaults); + } + return config.createResolver(scopes, context, [''], { + // These are just defaults that plugins can override + scriptable: false, + indexable: false, + allKeys: true + }); +} diff --git a/src/core/core.registry.js b/src/core/core.registry.js new file mode 100644 index 00000000000..09222bd3788 --- /dev/null +++ b/src/core/core.registry.js @@ -0,0 +1,186 @@ +import DatasetController from './core.datasetController.js'; +import Element from './core.element.js'; +import Scale from './core.scale.js'; +import TypedRegistry from './core.typedRegistry.js'; +import {each, callback as call, _capitalize} from '../helpers/helpers.core.js'; + +/** + * Please use the module's default export which provides a singleton instance + * Note: class is exported for typedoc + */ +export class Registry { + constructor() { + this.controllers = new TypedRegistry(DatasetController, 'datasets', true); + this.elements = new TypedRegistry(Element, 'elements'); + this.plugins = new TypedRegistry(Object, 'plugins'); + this.scales = new TypedRegistry(Scale, 'scales'); + // Order is important, Scale has Element in prototype chain, + // so Scales must be before Elements. Plugins are a fallback, so not listed here. + this._typedRegistries = [this.controllers, this.scales, this.elements]; + } + + /** + * @param {...any} args + */ + add(...args) { + this._each('register', args); + } + + remove(...args) { + this._each('unregister', args); + } + + /** + * @param {...typeof DatasetController} args + */ + addControllers(...args) { + this._each('register', args, this.controllers); + } + + /** + * @param {...typeof Element} args + */ + addElements(...args) { + this._each('register', args, this.elements); + } + + /** + * @param {...any} args + */ + addPlugins(...args) { + this._each('register', args, this.plugins); + } + + /** + * @param {...typeof Scale} args + */ + addScales(...args) { + this._each('register', args, this.scales); + } + + /** + * @param {string} id + * @returns {typeof DatasetController} + */ + getController(id) { + return this._get(id, this.controllers, 'controller'); + } + + /** + * @param {string} id + * @returns {typeof Element} + */ + getElement(id) { + return this._get(id, this.elements, 'element'); + } + + /** + * @param {string} id + * @returns {object} + */ + getPlugin(id) { + return this._get(id, this.plugins, 'plugin'); + } + + /** + * @param {string} id + * @returns {typeof Scale} + */ + getScale(id) { + return this._get(id, this.scales, 'scale'); + } + + /** + * @param {...typeof DatasetController} args + */ + removeControllers(...args) { + this._each('unregister', args, this.controllers); + } + + /** + * @param {...typeof Element} args + */ + removeElements(...args) { + this._each('unregister', args, this.elements); + } + + /** + * @param {...any} args + */ + removePlugins(...args) { + this._each('unregister', args, this.plugins); + } + + /** + * @param {...typeof Scale} args + */ + removeScales(...args) { + this._each('unregister', args, this.scales); + } + + /** + * @private + */ + _each(method, args, typedRegistry) { + [...args].forEach(arg => { + const reg = typedRegistry || this._getRegistryForType(arg); + if (typedRegistry || reg.isForType(arg) || (reg === this.plugins && arg.id)) { + this._exec(method, reg, arg); + } else { + // Handle loopable args + // Use case: + // import * as plugins from './plugins.js'; + // Chart.register(plugins); + each(arg, item => { + // If there are mixed types in the loopable, make sure those are + // registered in correct registry + // Use case: (treemap exporting controller, elements etc) + // import * as treemap from 'chartjs-chart-treemap.js'; + // Chart.register(treemap); + + const itemReg = typedRegistry || this._getRegistryForType(item); + this._exec(method, itemReg, item); + }); + } + }); + } + + /** + * @private + */ + _exec(method, registry, component) { + const camelMethod = _capitalize(method); + call(component['before' + camelMethod], [], component); // beforeRegister / beforeUnregister + registry[method](component); + call(component['after' + camelMethod], [], component); // afterRegister / afterUnregister + } + + /** + * @private + */ + _getRegistryForType(type) { + for (let i = 0; i < this._typedRegistries.length; i++) { + const reg = this._typedRegistries[i]; + if (reg.isForType(type)) { + return reg; + } + } + // plugins is the fallback registry + return this.plugins; + } + + /** + * @private + */ + _get(id, typedRegistry, type) { + const item = typedRegistry.get(id); + if (item === undefined) { + throw new Error('"' + id + '" is not a registered ' + type + '.'); + } + return item; + } + +} + +// singleton instance +export default /* #__PURE__ */ new Registry(); diff --git a/src/core/core.scale.autoskip.js b/src/core/core.scale.autoskip.js new file mode 100644 index 00000000000..b703bda85a9 --- /dev/null +++ b/src/core/core.scale.autoskip.js @@ -0,0 +1,170 @@ +import {isNullOrUndef, valueOrDefault} from '../helpers/helpers.core.js'; +import {_factorize} from '../helpers/helpers.math.js'; + + +/** + * @typedef { import('./core.controller.js').default } Chart + * @typedef {{value:number | string, label?:string, major?:boolean, $context?:any}} Tick + */ + +/** + * Returns a subset of ticks to be plotted to avoid overlapping labels. + * @param {import('./core.scale.js').default} scale + * @param {Tick[]} ticks + * @return {Tick[]} + * @private + */ +export function autoSkip(scale, ticks) { + const tickOpts = scale.options.ticks; + const determinedMaxTicks = determineMaxTicks(scale); + const ticksLimit = Math.min(tickOpts.maxTicksLimit || determinedMaxTicks, determinedMaxTicks); + const majorIndices = tickOpts.major.enabled ? getMajorIndices(ticks) : []; + const numMajorIndices = majorIndices.length; + const first = majorIndices[0]; + const last = majorIndices[numMajorIndices - 1]; + const newTicks = []; + + // If there are too many major ticks to display them all + if (numMajorIndices > ticksLimit) { + skipMajors(ticks, newTicks, majorIndices, numMajorIndices / ticksLimit); + return newTicks; + } + + const spacing = calculateSpacing(majorIndices, ticks, ticksLimit); + + if (numMajorIndices > 0) { + let i, ilen; + const avgMajorSpacing = numMajorIndices > 1 ? Math.round((last - first) / (numMajorIndices - 1)) : null; + skip(ticks, newTicks, spacing, isNullOrUndef(avgMajorSpacing) ? 0 : first - avgMajorSpacing, first); + for (i = 0, ilen = numMajorIndices - 1; i < ilen; i++) { + skip(ticks, newTicks, spacing, majorIndices[i], majorIndices[i + 1]); + } + skip(ticks, newTicks, spacing, last, isNullOrUndef(avgMajorSpacing) ? ticks.length : last + avgMajorSpacing); + return newTicks; + } + skip(ticks, newTicks, spacing); + return newTicks; +} + +function determineMaxTicks(scale) { + const offset = scale.options.offset; + const tickLength = scale._tickSize(); + const maxScale = scale._length / tickLength + (offset ? 0 : 1); + const maxChart = scale._maxLength / tickLength; + return Math.floor(Math.min(maxScale, maxChart)); +} + +/** + * @param {number[]} majorIndices + * @param {Tick[]} ticks + * @param {number} ticksLimit + */ +function calculateSpacing(majorIndices, ticks, ticksLimit) { + const evenMajorSpacing = getEvenSpacing(majorIndices); + const spacing = ticks.length / ticksLimit; + + // If the major ticks are evenly spaced apart, place the minor ticks + // so that they divide the major ticks into even chunks + if (!evenMajorSpacing) { + return Math.max(spacing, 1); + } + + const factors = _factorize(evenMajorSpacing); + for (let i = 0, ilen = factors.length - 1; i < ilen; i++) { + const factor = factors[i]; + if (factor > spacing) { + return factor; + } + } + return Math.max(spacing, 1); +} + +/** + * @param {Tick[]} ticks + */ +function getMajorIndices(ticks) { + const result = []; + let i, ilen; + for (i = 0, ilen = ticks.length; i < ilen; i++) { + if (ticks[i].major) { + result.push(i); + } + } + return result; +} + +/** + * @param {Tick[]} ticks + * @param {Tick[]} newTicks + * @param {number[]} majorIndices + * @param {number} spacing + */ +function skipMajors(ticks, newTicks, majorIndices, spacing) { + let count = 0; + let next = majorIndices[0]; + let i; + + spacing = Math.ceil(spacing); + for (i = 0; i < ticks.length; i++) { + if (i === next) { + newTicks.push(ticks[i]); + count++; + next = majorIndices[count * spacing]; + } + } +} + +/** + * @param {Tick[]} ticks + * @param {Tick[]} newTicks + * @param {number} spacing + * @param {number} [majorStart] + * @param {number} [majorEnd] + */ +function skip(ticks, newTicks, spacing, majorStart, majorEnd) { + const start = valueOrDefault(majorStart, 0); + const end = Math.min(valueOrDefault(majorEnd, ticks.length), ticks.length); + let count = 0; + let length, i, next; + + spacing = Math.ceil(spacing); + if (majorEnd) { + length = majorEnd - majorStart; + spacing = length / Math.floor(length / spacing); + } + + next = start; + + while (next < 0) { + count++; + next = Math.round(start + count * spacing); + } + + for (i = Math.max(start, 0); i < end; i++) { + if (i === next) { + newTicks.push(ticks[i]); + count++; + next = Math.round(start + count * spacing); + } + } +} + + +/** + * @param {number[]} arr + */ +function getEvenSpacing(arr) { + const len = arr.length; + let i, diff; + + if (len < 2) { + return false; + } + + for (diff = arr[0], i = 1; i < len; ++i) { + if (arr[i] - arr[i - 1] !== diff) { + return false; + } + } + return diff; +} diff --git a/src/core/core.scale.defaults.js b/src/core/core.scale.defaults.js new file mode 100644 index 00000000000..b6798e094b6 --- /dev/null +++ b/src/core/core.scale.defaults.js @@ -0,0 +1,105 @@ +import Ticks from './core.ticks.js'; + +export function applyScaleDefaults(defaults) { + defaults.set('scale', { + display: true, + offset: false, + reverse: false, + beginAtZero: false, + + /** + * Scale boundary strategy (bypassed by min/max time options) + * - `data`: make sure data are fully visible, ticks outside are removed + * - `ticks`: make sure ticks are fully visible, data outside are truncated + * @see https://github.com/chartjs/Chart.js/pull/4556 + * @since 3.0.0 + */ + bounds: 'ticks', + + clip: true, + + /** + * Addition grace added to max and reduced from min data value. + * @since 3.0.0 + */ + grace: 0, + + // grid line settings + grid: { + display: true, + lineWidth: 1, + drawOnChartArea: true, + drawTicks: true, + tickLength: 8, + tickWidth: (_ctx, options) => options.lineWidth, + tickColor: (_ctx, options) => options.color, + offset: false, + }, + + border: { + display: true, + dash: [], + dashOffset: 0.0, + width: 1 + }, + + // scale title + title: { + // display property + display: false, + + // actual label + text: '', + + // top/bottom padding + padding: { + top: 4, + bottom: 4 + } + }, + + // label settings + ticks: { + minRotation: 0, + maxRotation: 50, + mirror: false, + textStrokeWidth: 0, + textStrokeColor: '', + padding: 3, + display: true, + autoSkip: true, + autoSkipPadding: 3, + labelOffset: 0, + // We pass through arrays to be rendered as multiline labels, we convert Others to strings here. + callback: Ticks.formatters.values, + minor: {}, + major: {}, + align: 'center', + crossAlign: 'near', + + showLabelBackdrop: false, + backdropColor: 'rgba(255, 255, 255, 0.75)', + backdropPadding: 2, + } + }); + + defaults.route('scale.ticks', 'color', '', 'color'); + defaults.route('scale.grid', 'color', '', 'borderColor'); + defaults.route('scale.border', 'color', '', 'borderColor'); + defaults.route('scale.title', 'color', '', 'color'); + + defaults.describe('scale', { + _fallback: false, + _scriptable: (name) => !name.startsWith('before') && !name.startsWith('after') && name !== 'callback' && name !== 'parser', + _indexable: (name) => name !== 'borderDash' && name !== 'tickBorderDash' && name !== 'dash', + }); + + defaults.describe('scales', { + _fallback: 'scale', + }); + + defaults.describe('scale.ticks', { + _scriptable: (name) => name !== 'backdropPadding' && name !== 'callback', + _indexable: (name) => name !== 'backdropPadding', + }); +} diff --git a/src/core/core.scale.js b/src/core/core.scale.js new file mode 100644 index 00000000000..e81b6b933bf --- /dev/null +++ b/src/core/core.scale.js @@ -0,0 +1,1712 @@ +import Element from './core.element.js'; +import {_alignPixel, _measureText, renderText, clipArea, unclipArea} from '../helpers/helpers.canvas.js'; +import {callback as call, each, finiteOrDefault, isArray, isFinite, isNullOrUndef, isObject, valueOrDefault} from '../helpers/helpers.core.js'; +import {toDegrees, toRadians, _int16Range, _limitValue, HALF_PI} from '../helpers/helpers.math.js'; +import {_alignStartEnd, _toLeftRightCenter} from '../helpers/helpers.extras.js'; +import {createContext, toFont, toPadding, _addGrace} from '../helpers/helpers.options.js'; +import {autoSkip} from './core.scale.autoskip.js'; + +const reverseAlign = (align) => align === 'left' ? 'right' : align === 'right' ? 'left' : align; +const offsetFromEdge = (scale, edge, offset) => edge === 'top' || edge === 'left' ? scale[edge] + offset : scale[edge] - offset; +const getTicksLimit = (ticksLength, maxTicksLimit) => Math.min(maxTicksLimit || ticksLength, ticksLength); + +/** + * @typedef { import('../types/index.js').Chart } Chart + * @typedef {{value:number | string, label?:string, major?:boolean, $context?:any}} Tick + */ + +/** + * Returns a new array containing numItems from arr + * @param {any[]} arr + * @param {number} numItems + */ +function sample(arr, numItems) { + const result = []; + const increment = arr.length / numItems; + const len = arr.length; + let i = 0; + + for (; i < len; i += increment) { + result.push(arr[Math.floor(i)]); + } + return result; +} + +/** + * @param {Scale} scale + * @param {number} index + * @param {boolean} offsetGridLines + */ +function getPixelForGridLine(scale, index, offsetGridLines) { + const length = scale.ticks.length; + const validIndex = Math.min(index, length - 1); + const start = scale._startPixel; + const end = scale._endPixel; + const epsilon = 1e-6; // 1e-6 is margin in pixels for accumulated error. + let lineValue = scale.getPixelForTick(validIndex); + let offset; + + if (offsetGridLines) { + if (length === 1) { + offset = Math.max(lineValue - start, end - lineValue); + } else if (index === 0) { + offset = (scale.getPixelForTick(1) - lineValue) / 2; + } else { + offset = (lineValue - scale.getPixelForTick(validIndex - 1)) / 2; + } + lineValue += validIndex < index ? offset : -offset; + + // Return undefined if the pixel is out of the range + if (lineValue < start - epsilon || lineValue > end + epsilon) { + return; + } + } + return lineValue; +} + +/** + * @param {object} caches + * @param {number} length + */ +function garbageCollect(caches, length) { + each(caches, (cache) => { + const gc = cache.gc; + const gcLen = gc.length / 2; + let i; + if (gcLen > length) { + for (i = 0; i < gcLen; ++i) { + delete cache.data[gc[i]]; + } + gc.splice(0, gcLen); + } + }); +} + +/** + * @param {object} options + */ +function getTickMarkLength(options) { + return options.drawTicks ? options.tickLength : 0; +} + +/** + * @param {object} options + */ +function getTitleHeight(options, fallback) { + if (!options.display) { + return 0; + } + + const font = toFont(options.font, fallback); + const padding = toPadding(options.padding); + const lines = isArray(options.text) ? options.text.length : 1; + + return (lines * font.lineHeight) + padding.height; +} + +function createScaleContext(parent, scale) { + return createContext(parent, { + scale, + type: 'scale' + }); +} + +function createTickContext(parent, index, tick) { + return createContext(parent, { + tick, + index, + type: 'tick' + }); +} + +function titleAlign(align, position, reverse) { + /** @type {CanvasTextAlign} */ + let ret = _toLeftRightCenter(align); + if ((reverse && position !== 'right') || (!reverse && position === 'right')) { + ret = reverseAlign(ret); + } + return ret; +} + +function titleArgs(scale, offset, position, align) { + const {top, left, bottom, right, chart} = scale; + const {chartArea, scales} = chart; + let rotation = 0; + let maxWidth, titleX, titleY; + const height = bottom - top; + const width = right - left; + + if (scale.isHorizontal()) { + titleX = _alignStartEnd(align, left, right); + + if (isObject(position)) { + const positionAxisID = Object.keys(position)[0]; + const value = position[positionAxisID]; + titleY = scales[positionAxisID].getPixelForValue(value) + height - offset; + } else if (position === 'center') { + titleY = (chartArea.bottom + chartArea.top) / 2 + height - offset; + } else { + titleY = offsetFromEdge(scale, position, offset); + } + maxWidth = right - left; + } else { + if (isObject(position)) { + const positionAxisID = Object.keys(position)[0]; + const value = position[positionAxisID]; + titleX = scales[positionAxisID].getPixelForValue(value) - width + offset; + } else if (position === 'center') { + titleX = (chartArea.left + chartArea.right) / 2 - width + offset; + } else { + titleX = offsetFromEdge(scale, position, offset); + } + titleY = _alignStartEnd(align, bottom, top); + rotation = position === 'left' ? -HALF_PI : HALF_PI; + } + return {titleX, titleY, maxWidth, rotation}; +} + +export default class Scale extends Element { + + // eslint-disable-next-line max-statements + constructor(cfg) { + super(); + + /** @type {string} */ + this.id = cfg.id; + /** @type {string} */ + this.type = cfg.type; + /** @type {any} */ + this.options = undefined; + /** @type {CanvasRenderingContext2D} */ + this.ctx = cfg.ctx; + /** @type {Chart} */ + this.chart = cfg.chart; + + // implements box + /** @type {number} */ + this.top = undefined; + /** @type {number} */ + this.bottom = undefined; + /** @type {number} */ + this.left = undefined; + /** @type {number} */ + this.right = undefined; + /** @type {number} */ + this.width = undefined; + /** @type {number} */ + this.height = undefined; + this._margins = { + left: 0, + right: 0, + top: 0, + bottom: 0 + }; + /** @type {number} */ + this.maxWidth = undefined; + /** @type {number} */ + this.maxHeight = undefined; + /** @type {number} */ + this.paddingTop = undefined; + /** @type {number} */ + this.paddingBottom = undefined; + /** @type {number} */ + this.paddingLeft = undefined; + /** @type {number} */ + this.paddingRight = undefined; + + // scale-specific properties + /** @type {string=} */ + this.axis = undefined; + /** @type {number=} */ + this.labelRotation = undefined; + this.min = undefined; + this.max = undefined; + this._range = undefined; + /** @type {Tick[]} */ + this.ticks = []; + /** @type {object[]|null} */ + this._gridLineItems = null; + /** @type {object[]|null} */ + this._labelItems = null; + /** @type {object|null} */ + this._labelSizes = null; + this._length = 0; + this._maxLength = 0; + this._longestTextCache = {}; + /** @type {number} */ + this._startPixel = undefined; + /** @type {number} */ + this._endPixel = undefined; + this._reversePixels = false; + this._userMax = undefined; + this._userMin = undefined; + this._suggestedMax = undefined; + this._suggestedMin = undefined; + this._ticksLength = 0; + this._borderValue = 0; + this._cache = {}; + this._dataLimitsCached = false; + this.$context = undefined; + } + + /** + * @param {any} options + * @since 3.0 + */ + init(options) { + this.options = options.setContext(this.getContext()); + + this.axis = options.axis; + + // parse min/max value, so we can properly determine min/max for other scales + this._userMin = this.parse(options.min); + this._userMax = this.parse(options.max); + this._suggestedMin = this.parse(options.suggestedMin); + this._suggestedMax = this.parse(options.suggestedMax); + } + + /** + * Parse a supported input value to internal representation. + * @param {*} raw + * @param {number} [index] + * @since 3.0 + */ + parse(raw, index) { // eslint-disable-line no-unused-vars + return raw; + } + + /** + * @return {{min: number, max: number, minDefined: boolean, maxDefined: boolean}} + * @protected + * @since 3.0 + */ + getUserBounds() { + let {_userMin, _userMax, _suggestedMin, _suggestedMax} = this; + _userMin = finiteOrDefault(_userMin, Number.POSITIVE_INFINITY); + _userMax = finiteOrDefault(_userMax, Number.NEGATIVE_INFINITY); + _suggestedMin = finiteOrDefault(_suggestedMin, Number.POSITIVE_INFINITY); + _suggestedMax = finiteOrDefault(_suggestedMax, Number.NEGATIVE_INFINITY); + return { + min: finiteOrDefault(_userMin, _suggestedMin), + max: finiteOrDefault(_userMax, _suggestedMax), + minDefined: isFinite(_userMin), + maxDefined: isFinite(_userMax) + }; + } + + /** + * @param {boolean} canStack + * @return {{min: number, max: number}} + * @protected + * @since 3.0 + */ + getMinMax(canStack) { + let {min, max, minDefined, maxDefined} = this.getUserBounds(); + let range; + + if (minDefined && maxDefined) { + return {min, max}; + } + + const metas = this.getMatchingVisibleMetas(); + for (let i = 0, ilen = metas.length; i < ilen; ++i) { + range = metas[i].controller.getMinMax(this, canStack); + if (!minDefined) { + min = Math.min(min, range.min); + } + if (!maxDefined) { + max = Math.max(max, range.max); + } + } + + // Make sure min <= max when only min or max is defined by user and the data is outside that range + min = maxDefined && min > max ? max : min; + max = minDefined && min > max ? min : max; + + return { + min: finiteOrDefault(min, finiteOrDefault(max, min)), + max: finiteOrDefault(max, finiteOrDefault(min, max)) + }; + } + + /** + * Get the padding needed for the scale + * @return {{top: number, left: number, bottom: number, right: number}} the necessary padding + * @private + */ + getPadding() { + return { + left: this.paddingLeft || 0, + top: this.paddingTop || 0, + right: this.paddingRight || 0, + bottom: this.paddingBottom || 0 + }; + } + + /** + * Returns the scale tick objects + * @return {Tick[]} + * @since 2.7 + */ + getTicks() { + return this.ticks; + } + + /** + * @return {string[]} + */ + getLabels() { + const data = this.chart.data; + return this.options.labels || (this.isHorizontal() ? data.xLabels : data.yLabels) || data.labels || []; + } + + /** + * @return {import('../types.js').LabelItem[]} + */ + getLabelItems(chartArea = this.chart.chartArea) { + const items = this._labelItems || (this._labelItems = this._computeLabelItems(chartArea)); + return items; + } + + // When a new layout is created, reset the data limits cache + beforeLayout() { + this._cache = {}; + this._dataLimitsCached = false; + } + + // These methods are ordered by lifecycle. Utilities then follow. + // Any function defined here is inherited by all scale types. + // Any function can be extended by the scale type + + beforeUpdate() { + call(this.options.beforeUpdate, [this]); + } + + /** + * @param {number} maxWidth - the max width in pixels + * @param {number} maxHeight - the max height in pixels + * @param {{top: number, left: number, bottom: number, right: number}} margins - the space between the edge of the other scales and edge of the chart + * This space comes from two sources: + * - padding - space that's required to show the labels at the edges of the scale + * - thickness of scales or legends in another orientation + */ + update(maxWidth, maxHeight, margins) { + const {beginAtZero, grace, ticks: tickOpts} = this.options; + const sampleSize = tickOpts.sampleSize; + + // Update Lifecycle - Probably don't want to ever extend or overwrite this function ;) + this.beforeUpdate(); + + // Absorb the master measurements + this.maxWidth = maxWidth; + this.maxHeight = maxHeight; + this._margins = margins = Object.assign({ + left: 0, + right: 0, + top: 0, + bottom: 0 + }, margins); + + this.ticks = null; + this._labelSizes = null; + this._gridLineItems = null; + this._labelItems = null; + + // Dimensions + this.beforeSetDimensions(); + this.setDimensions(); + this.afterSetDimensions(); + + this._maxLength = this.isHorizontal() + ? this.width + margins.left + margins.right + : this.height + margins.top + margins.bottom; + + // Data min/max + if (!this._dataLimitsCached) { + this.beforeDataLimits(); + this.determineDataLimits(); + this.afterDataLimits(); + this._range = _addGrace(this, grace, beginAtZero); + this._dataLimitsCached = true; + } + + this.beforeBuildTicks(); + + this.ticks = this.buildTicks() || []; + + // Allow modification of ticks in callback. + this.afterBuildTicks(); + + // Compute tick rotation and fit using a sampled subset of labels + // We generally don't need to compute the size of every single label for determining scale size + const samplingEnabled = sampleSize < this.ticks.length; + this._convertTicksToLabels(samplingEnabled ? sample(this.ticks, sampleSize) : this.ticks); + + // configure is called twice, once here, once from core.controller.updateLayout. + // Here we haven't been positioned yet, but dimensions are correct. + // Variables set in configure are needed for calculateLabelRotation, and + // it's ok that coordinates are not correct there, only dimensions matter. + this.configure(); + + // Tick Rotation + this.beforeCalculateLabelRotation(); + this.calculateLabelRotation(); // Preconditions: number of ticks and sizes of largest labels must be calculated beforehand + this.afterCalculateLabelRotation(); + + // Auto-skip + if (tickOpts.display && (tickOpts.autoSkip || tickOpts.source === 'auto')) { + this.ticks = autoSkip(this, this.ticks); + this._labelSizes = null; + this.afterAutoSkip(); + } + + if (samplingEnabled) { + // Generate labels using all non-skipped ticks + this._convertTicksToLabels(this.ticks); + } + + this.beforeFit(); + this.fit(); // Preconditions: label rotation and label sizes must be calculated beforehand + this.afterFit(); + + // IMPORTANT: after this point, we consider that `this.ticks` will NEVER change! + + this.afterUpdate(); + } + + /** + * @protected + */ + configure() { + let reversePixels = this.options.reverse; + let startPixel, endPixel; + + if (this.isHorizontal()) { + startPixel = this.left; + endPixel = this.right; + } else { + startPixel = this.top; + endPixel = this.bottom; + // by default vertical scales are from bottom to top, so pixels are reversed + reversePixels = !reversePixels; + } + this._startPixel = startPixel; + this._endPixel = endPixel; + this._reversePixels = reversePixels; + this._length = endPixel - startPixel; + this._alignToPixels = this.options.alignToPixels; + } + + afterUpdate() { + call(this.options.afterUpdate, [this]); + } + + // + + beforeSetDimensions() { + call(this.options.beforeSetDimensions, [this]); + } + setDimensions() { + // Set the unconstrained dimension before label rotation + if (this.isHorizontal()) { + // Reset position before calculating rotation + this.width = this.maxWidth; + this.left = 0; + this.right = this.width; + } else { + this.height = this.maxHeight; + + // Reset position before calculating rotation + this.top = 0; + this.bottom = this.height; + } + + // Reset padding + this.paddingLeft = 0; + this.paddingTop = 0; + this.paddingRight = 0; + this.paddingBottom = 0; + } + afterSetDimensions() { + call(this.options.afterSetDimensions, [this]); + } + + _callHooks(name) { + this.chart.notifyPlugins(name, this.getContext()); + call(this.options[name], [this]); + } + + // Data limits + beforeDataLimits() { + this._callHooks('beforeDataLimits'); + } + determineDataLimits() {} + afterDataLimits() { + this._callHooks('afterDataLimits'); + } + + // + beforeBuildTicks() { + this._callHooks('beforeBuildTicks'); + } + /** + * @return {object[]} the ticks + */ + buildTicks() { + return []; + } + afterBuildTicks() { + this._callHooks('afterBuildTicks'); + } + + beforeTickToLabelConversion() { + call(this.options.beforeTickToLabelConversion, [this]); + } + /** + * Convert ticks to label strings + * @param {Tick[]} ticks + */ + generateTickLabels(ticks) { + const tickOpts = this.options.ticks; + let i, ilen, tick; + for (i = 0, ilen = ticks.length; i < ilen; i++) { + tick = ticks[i]; + tick.label = call(tickOpts.callback, [tick.value, i, ticks], this); + } + } + afterTickToLabelConversion() { + call(this.options.afterTickToLabelConversion, [this]); + } + + // + + beforeCalculateLabelRotation() { + call(this.options.beforeCalculateLabelRotation, [this]); + } + calculateLabelRotation() { + const options = this.options; + const tickOpts = options.ticks; + const numTicks = getTicksLimit(this.ticks.length, options.ticks.maxTicksLimit); + const minRotation = tickOpts.minRotation || 0; + const maxRotation = tickOpts.maxRotation; + let labelRotation = minRotation; + let tickWidth, maxHeight, maxLabelDiagonal; + + if (!this._isVisible() || !tickOpts.display || minRotation >= maxRotation || numTicks <= 1 || !this.isHorizontal()) { + this.labelRotation = minRotation; + return; + } + + const labelSizes = this._getLabelSizes(); + const maxLabelWidth = labelSizes.widest.width; + const maxLabelHeight = labelSizes.highest.height; + + // Estimate the width of each grid based on the canvas width, the maximum + // label width and the number of tick intervals + const maxWidth = _limitValue(this.chart.width - maxLabelWidth, 0, this.maxWidth); + tickWidth = options.offset ? this.maxWidth / numTicks : maxWidth / (numTicks - 1); + + // Allow 3 pixels x2 padding either side for label readability + if (maxLabelWidth + 6 > tickWidth) { + tickWidth = maxWidth / (numTicks - (options.offset ? 0.5 : 1)); + maxHeight = this.maxHeight - getTickMarkLength(options.grid) + - tickOpts.padding - getTitleHeight(options.title, this.chart.options.font); + maxLabelDiagonal = Math.sqrt(maxLabelWidth * maxLabelWidth + maxLabelHeight * maxLabelHeight); + labelRotation = toDegrees(Math.min( + Math.asin(_limitValue((labelSizes.highest.height + 6) / tickWidth, -1, 1)), + Math.asin(_limitValue(maxHeight / maxLabelDiagonal, -1, 1)) - Math.asin(_limitValue(maxLabelHeight / maxLabelDiagonal, -1, 1)) + )); + labelRotation = Math.max(minRotation, Math.min(maxRotation, labelRotation)); + } + + this.labelRotation = labelRotation; + } + afterCalculateLabelRotation() { + call(this.options.afterCalculateLabelRotation, [this]); + } + afterAutoSkip() {} + + // + + beforeFit() { + call(this.options.beforeFit, [this]); + } + fit() { + // Reset + const minSize = { + width: 0, + height: 0 + }; + + const {chart, options: {ticks: tickOpts, title: titleOpts, grid: gridOpts}} = this; + const display = this._isVisible(); + const isHorizontal = this.isHorizontal(); + + if (display) { + const titleHeight = getTitleHeight(titleOpts, chart.options.font); + if (isHorizontal) { + minSize.width = this.maxWidth; + minSize.height = getTickMarkLength(gridOpts) + titleHeight; + } else { + minSize.height = this.maxHeight; // fill all the height + minSize.width = getTickMarkLength(gridOpts) + titleHeight; + } + + // Don't bother fitting the ticks if we are not showing the labels + if (tickOpts.display && this.ticks.length) { + const {first, last, widest, highest} = this._getLabelSizes(); + const tickPadding = tickOpts.padding * 2; + const angleRadians = toRadians(this.labelRotation); + const cos = Math.cos(angleRadians); + const sin = Math.sin(angleRadians); + + if (isHorizontal) { + // A horizontal axis is more constrained by the height. + const labelHeight = tickOpts.mirror ? 0 : sin * widest.width + cos * highest.height; + minSize.height = Math.min(this.maxHeight, minSize.height + labelHeight + tickPadding); + } else { + // A vertical axis is more constrained by the width. Labels are the + // dominant factor here, so get that length first and account for padding + const labelWidth = tickOpts.mirror ? 0 : cos * widest.width + sin * highest.height; + + minSize.width = Math.min(this.maxWidth, minSize.width + labelWidth + tickPadding); + } + this._calculatePadding(first, last, sin, cos); + } + } + + this._handleMargins(); + + if (isHorizontal) { + this.width = this._length = chart.width - this._margins.left - this._margins.right; + this.height = minSize.height; + } else { + this.width = minSize.width; + this.height = this._length = chart.height - this._margins.top - this._margins.bottom; + } + } + + _calculatePadding(first, last, sin, cos) { + const {ticks: {align, padding}, position} = this.options; + const isRotated = this.labelRotation !== 0; + const labelsBelowTicks = position !== 'top' && this.axis === 'x'; + + if (this.isHorizontal()) { + const offsetLeft = this.getPixelForTick(0) - this.left; + const offsetRight = this.right - this.getPixelForTick(this.ticks.length - 1); + let paddingLeft = 0; + let paddingRight = 0; + + // Ensure that our ticks are always inside the canvas. When rotated, ticks are right aligned + // which means that the right padding is dominated by the font height + if (isRotated) { + if (labelsBelowTicks) { + paddingLeft = cos * first.width; + paddingRight = sin * last.height; + } else { + paddingLeft = sin * first.height; + paddingRight = cos * last.width; + } + } else if (align === 'start') { + paddingRight = last.width; + } else if (align === 'end') { + paddingLeft = first.width; + } else if (align !== 'inner') { + paddingLeft = first.width / 2; + paddingRight = last.width / 2; + } + + // Adjust padding taking into account changes in offsets + this.paddingLeft = Math.max((paddingLeft - offsetLeft + padding) * this.width / (this.width - offsetLeft), 0); + this.paddingRight = Math.max((paddingRight - offsetRight + padding) * this.width / (this.width - offsetRight), 0); + } else { + let paddingTop = last.height / 2; + let paddingBottom = first.height / 2; + + if (align === 'start') { + paddingTop = 0; + paddingBottom = first.height; + } else if (align === 'end') { + paddingTop = last.height; + paddingBottom = 0; + } + + this.paddingTop = paddingTop + padding; + this.paddingBottom = paddingBottom + padding; + } + } + + /** + * Handle margins and padding interactions + * @private + */ + _handleMargins() { + if (this._margins) { + this._margins.left = Math.max(this.paddingLeft, this._margins.left); + this._margins.top = Math.max(this.paddingTop, this._margins.top); + this._margins.right = Math.max(this.paddingRight, this._margins.right); + this._margins.bottom = Math.max(this.paddingBottom, this._margins.bottom); + } + } + + afterFit() { + call(this.options.afterFit, [this]); + } + + // Shared Methods + /** + * @return {boolean} + */ + isHorizontal() { + const {axis, position} = this.options; + return position === 'top' || position === 'bottom' || axis === 'x'; + } + /** + * @return {boolean} + */ + isFullSize() { + return this.options.fullSize; + } + + /** + * @param {Tick[]} ticks + * @private + */ + _convertTicksToLabels(ticks) { + this.beforeTickToLabelConversion(); + + this.generateTickLabels(ticks); + + // Ticks should be skipped when callback returns null or undef, so lets remove those. + let i, ilen; + for (i = 0, ilen = ticks.length; i < ilen; i++) { + if (isNullOrUndef(ticks[i].label)) { + ticks.splice(i, 1); + ilen--; + i--; + } + } + + this.afterTickToLabelConversion(); + } + + /** + * @return {{ first: object, last: object, widest: object, highest: object, widths: Array, heights: array }} + * @private + */ + _getLabelSizes() { + let labelSizes = this._labelSizes; + + if (!labelSizes) { + const sampleSize = this.options.ticks.sampleSize; + let ticks = this.ticks; + if (sampleSize < ticks.length) { + ticks = sample(ticks, sampleSize); + } + + this._labelSizes = labelSizes = this._computeLabelSizes(ticks, ticks.length, this.options.ticks.maxTicksLimit); + } + + return labelSizes; + } + + /** + * Returns {width, height, offset} objects for the first, last, widest, highest tick + * labels where offset indicates the anchor point offset from the top in pixels. + * @return {{ first: object, last: object, widest: object, highest: object, widths: Array, heights: array }} + * @private + */ + _computeLabelSizes(ticks, length, maxTicksLimit) { + const {ctx, _longestTextCache: caches} = this; + const widths = []; + const heights = []; + const increment = Math.floor(length / getTicksLimit(length, maxTicksLimit)); + let widestLabelSize = 0; + let highestLabelSize = 0; + let i, j, jlen, label, tickFont, fontString, cache, lineHeight, width, height, nestedLabel; + + for (i = 0; i < length; i += increment) { + label = ticks[i].label; + tickFont = this._resolveTickFontOptions(i); + ctx.font = fontString = tickFont.string; + cache = caches[fontString] = caches[fontString] || {data: {}, gc: []}; + lineHeight = tickFont.lineHeight; + width = height = 0; + // Undefined labels and arrays should not be measured + if (!isNullOrUndef(label) && !isArray(label)) { + width = _measureText(ctx, cache.data, cache.gc, width, label); + height = lineHeight; + } else if (isArray(label)) { + // if it is an array let's measure each element + for (j = 0, jlen = label.length; j < jlen; ++j) { + nestedLabel = /** @type {string} */ (label[j]); + // Undefined labels and arrays should not be measured + if (!isNullOrUndef(nestedLabel) && !isArray(nestedLabel)) { + width = _measureText(ctx, cache.data, cache.gc, width, nestedLabel); + height += lineHeight; + } + } + } + widths.push(width); + heights.push(height); + widestLabelSize = Math.max(width, widestLabelSize); + highestLabelSize = Math.max(height, highestLabelSize); + } + garbageCollect(caches, length); + + const widest = widths.indexOf(widestLabelSize); + const highest = heights.indexOf(highestLabelSize); + + const valueAt = (idx) => ({width: widths[idx] || 0, height: heights[idx] || 0}); + + return { + first: valueAt(0), + last: valueAt(length - 1), + widest: valueAt(widest), + highest: valueAt(highest), + widths, + heights, + }; + } + + /** + * Used to get the label to display in the tooltip for the given value + * @param {*} value + * @return {string} + */ + getLabelForValue(value) { + return value; + } + + /** + * Returns the location of the given data point. Value can either be an index or a numerical value + * The coordinate (0, 0) is at the upper-left corner of the canvas + * @param {*} value + * @param {number} [index] + * @return {number} + */ + getPixelForValue(value, index) { // eslint-disable-line no-unused-vars + return NaN; + } + + /** + * Used to get the data value from a given pixel. This is the inverse of getPixelForValue + * The coordinate (0, 0) is at the upper-left corner of the canvas + * @param {number} pixel + * @return {*} + */ + getValueForPixel(pixel) {} // eslint-disable-line no-unused-vars + + /** + * Returns the location of the tick at the given index + * The coordinate (0, 0) is at the upper-left corner of the canvas + * @param {number} index + * @return {number} + */ + getPixelForTick(index) { + const ticks = this.ticks; + if (index < 0 || index > ticks.length - 1) { + return null; + } + return this.getPixelForValue(ticks[index].value); + } + + /** + * Utility for getting the pixel location of a percentage of scale + * The coordinate (0, 0) is at the upper-left corner of the canvas + * @param {number} decimal + * @return {number} + */ + getPixelForDecimal(decimal) { + if (this._reversePixels) { + decimal = 1 - decimal; + } + + const pixel = this._startPixel + decimal * this._length; + return _int16Range(this._alignToPixels ? _alignPixel(this.chart, pixel, 0) : pixel); + } + + /** + * @param {number} pixel + * @return {number} + */ + getDecimalForPixel(pixel) { + const decimal = (pixel - this._startPixel) / this._length; + return this._reversePixels ? 1 - decimal : decimal; + } + + /** + * Returns the pixel for the minimum chart value + * The coordinate (0, 0) is at the upper-left corner of the canvas + * @return {number} + */ + getBasePixel() { + return this.getPixelForValue(this.getBaseValue()); + } + + /** + * @return {number} + */ + getBaseValue() { + const {min, max} = this; + + return min < 0 && max < 0 ? max : + min > 0 && max > 0 ? min : + 0; + } + + /** + * @protected + */ + getContext(index) { + const ticks = this.ticks || []; + + if (index >= 0 && index < ticks.length) { + const tick = ticks[index]; + return tick.$context || + (tick.$context = createTickContext(this.getContext(), index, tick)); + } + return this.$context || + (this.$context = createScaleContext(this.chart.getContext(), this)); + } + + /** + * @return {number} + * @private + */ + _tickSize() { + const optionTicks = this.options.ticks; + + // Calculate space needed by label in axis direction. + const rot = toRadians(this.labelRotation); + const cos = Math.abs(Math.cos(rot)); + const sin = Math.abs(Math.sin(rot)); + + const labelSizes = this._getLabelSizes(); + const padding = optionTicks.autoSkipPadding || 0; + const w = labelSizes ? labelSizes.widest.width + padding : 0; + const h = labelSizes ? labelSizes.highest.height + padding : 0; + + // Calculate space needed for 1 tick in axis direction. + return this.isHorizontal() + ? h * cos > w * sin ? w / cos : h / sin + : h * sin < w * cos ? h / cos : w / sin; + } + + /** + * @return {boolean} + * @private + */ + _isVisible() { + const display = this.options.display; + + if (display !== 'auto') { + return !!display; + } + + return this.getMatchingVisibleMetas().length > 0; + } + + /** + * @private + */ + _computeGridLineItems(chartArea) { + const axis = this.axis; + const chart = this.chart; + const options = this.options; + const {grid, position, border} = options; + const offset = grid.offset; + const isHorizontal = this.isHorizontal(); + const ticks = this.ticks; + const ticksLength = ticks.length + (offset ? 1 : 0); + const tl = getTickMarkLength(grid); + const items = []; + + const borderOpts = border.setContext(this.getContext()); + const axisWidth = borderOpts.display ? borderOpts.width : 0; + const axisHalfWidth = axisWidth / 2; + const alignBorderValue = function(pixel) { + return _alignPixel(chart, pixel, axisWidth); + }; + let borderValue, i, lineValue, alignedLineValue; + let tx1, ty1, tx2, ty2, x1, y1, x2, y2; + + if (position === 'top') { + borderValue = alignBorderValue(this.bottom); + ty1 = this.bottom - tl; + ty2 = borderValue - axisHalfWidth; + y1 = alignBorderValue(chartArea.top) + axisHalfWidth; + y2 = chartArea.bottom; + } else if (position === 'bottom') { + borderValue = alignBorderValue(this.top); + y1 = chartArea.top; + y2 = alignBorderValue(chartArea.bottom) - axisHalfWidth; + ty1 = borderValue + axisHalfWidth; + ty2 = this.top + tl; + } else if (position === 'left') { + borderValue = alignBorderValue(this.right); + tx1 = this.right - tl; + tx2 = borderValue - axisHalfWidth; + x1 = alignBorderValue(chartArea.left) + axisHalfWidth; + x2 = chartArea.right; + } else if (position === 'right') { + borderValue = alignBorderValue(this.left); + x1 = chartArea.left; + x2 = alignBorderValue(chartArea.right) - axisHalfWidth; + tx1 = borderValue + axisHalfWidth; + tx2 = this.left + tl; + } else if (axis === 'x') { + if (position === 'center') { + borderValue = alignBorderValue((chartArea.top + chartArea.bottom) / 2 + 0.5); + } else if (isObject(position)) { + const positionAxisID = Object.keys(position)[0]; + const value = position[positionAxisID]; + borderValue = alignBorderValue(this.chart.scales[positionAxisID].getPixelForValue(value)); + } + + y1 = chartArea.top; + y2 = chartArea.bottom; + ty1 = borderValue + axisHalfWidth; + ty2 = ty1 + tl; + } else if (axis === 'y') { + if (position === 'center') { + borderValue = alignBorderValue((chartArea.left + chartArea.right) / 2); + } else if (isObject(position)) { + const positionAxisID = Object.keys(position)[0]; + const value = position[positionAxisID]; + borderValue = alignBorderValue(this.chart.scales[positionAxisID].getPixelForValue(value)); + } + + tx1 = borderValue - axisHalfWidth; + tx2 = tx1 - tl; + x1 = chartArea.left; + x2 = chartArea.right; + } + + const limit = valueOrDefault(options.ticks.maxTicksLimit, ticksLength); + const step = Math.max(1, Math.ceil(ticksLength / limit)); + for (i = 0; i < ticksLength; i += step) { + const context = this.getContext(i); + const optsAtIndex = grid.setContext(context); + const optsAtIndexBorder = border.setContext(context); + + const lineWidth = optsAtIndex.lineWidth; + const lineColor = optsAtIndex.color; + const borderDash = optsAtIndexBorder.dash || []; + const borderDashOffset = optsAtIndexBorder.dashOffset; + + const tickWidth = optsAtIndex.tickWidth; + const tickColor = optsAtIndex.tickColor; + const tickBorderDash = optsAtIndex.tickBorderDash || []; + const tickBorderDashOffset = optsAtIndex.tickBorderDashOffset; + + lineValue = getPixelForGridLine(this, i, offset); + + // Skip if the pixel is out of the range + if (lineValue === undefined) { + continue; + } + + alignedLineValue = _alignPixel(chart, lineValue, lineWidth); + + if (isHorizontal) { + tx1 = tx2 = x1 = x2 = alignedLineValue; + } else { + ty1 = ty2 = y1 = y2 = alignedLineValue; + } + + items.push({ + tx1, + ty1, + tx2, + ty2, + x1, + y1, + x2, + y2, + width: lineWidth, + color: lineColor, + borderDash, + borderDashOffset, + tickWidth, + tickColor, + tickBorderDash, + tickBorderDashOffset, + }); + } + + this._ticksLength = ticksLength; + this._borderValue = borderValue; + + return items; + } + + /** + * @private + */ + _computeLabelItems(chartArea) { + const axis = this.axis; + const options = this.options; + const {position, ticks: optionTicks} = options; + const isHorizontal = this.isHorizontal(); + const ticks = this.ticks; + const {align, crossAlign, padding, mirror} = optionTicks; + const tl = getTickMarkLength(options.grid); + const tickAndPadding = tl + padding; + const hTickAndPadding = mirror ? -padding : tickAndPadding; + const rotation = -toRadians(this.labelRotation); + const items = []; + let i, ilen, tick, label, x, y, textAlign, pixel, font, lineHeight, lineCount, textOffset; + let textBaseline = 'middle'; + + if (position === 'top') { + y = this.bottom - hTickAndPadding; + textAlign = this._getXAxisLabelAlignment(); + } else if (position === 'bottom') { + y = this.top + hTickAndPadding; + textAlign = this._getXAxisLabelAlignment(); + } else if (position === 'left') { + const ret = this._getYAxisLabelAlignment(tl); + textAlign = ret.textAlign; + x = ret.x; + } else if (position === 'right') { + const ret = this._getYAxisLabelAlignment(tl); + textAlign = ret.textAlign; + x = ret.x; + } else if (axis === 'x') { + if (position === 'center') { + y = ((chartArea.top + chartArea.bottom) / 2) + tickAndPadding; + } else if (isObject(position)) { + const positionAxisID = Object.keys(position)[0]; + const value = position[positionAxisID]; + y = this.chart.scales[positionAxisID].getPixelForValue(value) + tickAndPadding; + } + textAlign = this._getXAxisLabelAlignment(); + } else if (axis === 'y') { + if (position === 'center') { + x = ((chartArea.left + chartArea.right) / 2) - tickAndPadding; + } else if (isObject(position)) { + const positionAxisID = Object.keys(position)[0]; + const value = position[positionAxisID]; + x = this.chart.scales[positionAxisID].getPixelForValue(value); + } + textAlign = this._getYAxisLabelAlignment(tl).textAlign; + } + + if (axis === 'y') { + if (align === 'start') { + textBaseline = 'top'; + } else if (align === 'end') { + textBaseline = 'bottom'; + } + } + + const labelSizes = this._getLabelSizes(); + for (i = 0, ilen = ticks.length; i < ilen; ++i) { + tick = ticks[i]; + label = tick.label; + + const optsAtIndex = optionTicks.setContext(this.getContext(i)); + pixel = this.getPixelForTick(i) + optionTicks.labelOffset; + font = this._resolveTickFontOptions(i); + lineHeight = font.lineHeight; + lineCount = isArray(label) ? label.length : 1; + const halfCount = lineCount / 2; + const color = optsAtIndex.color; + const strokeColor = optsAtIndex.textStrokeColor; + const strokeWidth = optsAtIndex.textStrokeWidth; + let tickTextAlign = textAlign; + + if (isHorizontal) { + x = pixel; + + if (textAlign === 'inner') { + if (i === ilen - 1) { + tickTextAlign = !this.options.reverse ? 'right' : 'left'; + } else if (i === 0) { + tickTextAlign = !this.options.reverse ? 'left' : 'right'; + } else { + tickTextAlign = 'center'; + } + } + + if (position === 'top') { + if (crossAlign === 'near' || rotation !== 0) { + textOffset = -lineCount * lineHeight + lineHeight / 2; + } else if (crossAlign === 'center') { + textOffset = -labelSizes.highest.height / 2 - halfCount * lineHeight + lineHeight; + } else { + textOffset = -labelSizes.highest.height + lineHeight / 2; + } + } else { + // eslint-disable-next-line no-lonely-if + if (crossAlign === 'near' || rotation !== 0) { + textOffset = lineHeight / 2; + } else if (crossAlign === 'center') { + textOffset = labelSizes.highest.height / 2 - halfCount * lineHeight; + } else { + textOffset = labelSizes.highest.height - lineCount * lineHeight; + } + } + if (mirror) { + textOffset *= -1; + } + if (rotation !== 0 && !optsAtIndex.showLabelBackdrop) { + x += (lineHeight / 2) * Math.sin(rotation); + } + } else { + y = pixel; + textOffset = (1 - lineCount) * lineHeight / 2; + } + + let backdrop; + + if (optsAtIndex.showLabelBackdrop) { + const labelPadding = toPadding(optsAtIndex.backdropPadding); + const height = labelSizes.heights[i]; + const width = labelSizes.widths[i]; + + let top = textOffset - labelPadding.top; + let left = 0 - labelPadding.left; + + switch (textBaseline) { + case 'middle': + top -= height / 2; + break; + case 'bottom': + top -= height; + break; + default: + break; + } + + switch (textAlign) { + case 'center': + left -= width / 2; + break; + case 'right': + left -= width; + break; + case 'inner': + if (i === ilen - 1) { + left -= width; + } else if (i > 0) { + left -= width / 2; + } + break; + default: + break; + } + + backdrop = { + left, + top, + width: width + labelPadding.width, + height: height + labelPadding.height, + + color: optsAtIndex.backdropColor, + }; + } + + items.push({ + label, + font, + textOffset, + options: { + rotation, + color, + strokeColor, + strokeWidth, + textAlign: tickTextAlign, + textBaseline, + translation: [x, y], + backdrop, + } + }); + } + + return items; + } + + _getXAxisLabelAlignment() { + const {position, ticks} = this.options; + const rotation = -toRadians(this.labelRotation); + + if (rotation) { + return position === 'top' ? 'left' : 'right'; + } + + let align = 'center'; + + if (ticks.align === 'start') { + align = 'left'; + } else if (ticks.align === 'end') { + align = 'right'; + } else if (ticks.align === 'inner') { + align = 'inner'; + } + + return align; + } + + _getYAxisLabelAlignment(tl) { + const {position, ticks: {crossAlign, mirror, padding}} = this.options; + const labelSizes = this._getLabelSizes(); + const tickAndPadding = tl + padding; + const widest = labelSizes.widest.width; + + let textAlign; + let x; + + if (position === 'left') { + if (mirror) { + x = this.right + padding; + + if (crossAlign === 'near') { + textAlign = 'left'; + } else if (crossAlign === 'center') { + textAlign = 'center'; + x += (widest / 2); + } else { + textAlign = 'right'; + x += widest; + } + } else { + x = this.right - tickAndPadding; + + if (crossAlign === 'near') { + textAlign = 'right'; + } else if (crossAlign === 'center') { + textAlign = 'center'; + x -= (widest / 2); + } else { + textAlign = 'left'; + x = this.left; + } + } + } else if (position === 'right') { + if (mirror) { + x = this.left + padding; + + if (crossAlign === 'near') { + textAlign = 'right'; + } else if (crossAlign === 'center') { + textAlign = 'center'; + x -= (widest / 2); + } else { + textAlign = 'left'; + x -= widest; + } + } else { + x = this.left + tickAndPadding; + + if (crossAlign === 'near') { + textAlign = 'left'; + } else if (crossAlign === 'center') { + textAlign = 'center'; + x += widest / 2; + } else { + textAlign = 'right'; + x = this.right; + } + } + } else { + textAlign = 'right'; + } + + return {textAlign, x}; + } + + /** + * @private + */ + _computeLabelArea() { + if (this.options.ticks.mirror) { + return; + } + + const chart = this.chart; + const position = this.options.position; + + if (position === 'left' || position === 'right') { + return {top: 0, left: this.left, bottom: chart.height, right: this.right}; + } if (position === 'top' || position === 'bottom') { + return {top: this.top, left: 0, bottom: this.bottom, right: chart.width}; + } + } + + /** + * @protected + */ + drawBackground() { + const {ctx, options: {backgroundColor}, left, top, width, height} = this; + if (backgroundColor) { + ctx.save(); + ctx.fillStyle = backgroundColor; + ctx.fillRect(left, top, width, height); + ctx.restore(); + } + } + + getLineWidthForValue(value) { + const grid = this.options.grid; + if (!this._isVisible() || !grid.display) { + return 0; + } + const ticks = this.ticks; + const index = ticks.findIndex(t => t.value === value); + if (index >= 0) { + const opts = grid.setContext(this.getContext(index)); + return opts.lineWidth; + } + return 0; + } + + /** + * @protected + */ + drawGrid(chartArea) { + const grid = this.options.grid; + const ctx = this.ctx; + const items = this._gridLineItems || (this._gridLineItems = this._computeGridLineItems(chartArea)); + let i, ilen; + + const drawLine = (p1, p2, style) => { + if (!style.width || !style.color) { + return; + } + ctx.save(); + ctx.lineWidth = style.width; + ctx.strokeStyle = style.color; + ctx.setLineDash(style.borderDash || []); + ctx.lineDashOffset = style.borderDashOffset; + + ctx.beginPath(); + ctx.moveTo(p1.x, p1.y); + ctx.lineTo(p2.x, p2.y); + ctx.stroke(); + ctx.restore(); + }; + + if (grid.display) { + for (i = 0, ilen = items.length; i < ilen; ++i) { + const item = items[i]; + + if (grid.drawOnChartArea) { + drawLine( + {x: item.x1, y: item.y1}, + {x: item.x2, y: item.y2}, + item + ); + } + + if (grid.drawTicks) { + drawLine( + {x: item.tx1, y: item.ty1}, + {x: item.tx2, y: item.ty2}, + { + color: item.tickColor, + width: item.tickWidth, + borderDash: item.tickBorderDash, + borderDashOffset: item.tickBorderDashOffset + } + ); + } + } + } + } + + /** + * @protected + */ + drawBorder() { + const {chart, ctx, options: {border, grid}} = this; + const borderOpts = border.setContext(this.getContext()); + const axisWidth = border.display ? borderOpts.width : 0; + if (!axisWidth) { + return; + } + const lastLineWidth = grid.setContext(this.getContext(0)).lineWidth; + const borderValue = this._borderValue; + let x1, x2, y1, y2; + + if (this.isHorizontal()) { + x1 = _alignPixel(chart, this.left, axisWidth) - axisWidth / 2; + x2 = _alignPixel(chart, this.right, lastLineWidth) + lastLineWidth / 2; + y1 = y2 = borderValue; + } else { + y1 = _alignPixel(chart, this.top, axisWidth) - axisWidth / 2; + y2 = _alignPixel(chart, this.bottom, lastLineWidth) + lastLineWidth / 2; + x1 = x2 = borderValue; + } + ctx.save(); + ctx.lineWidth = borderOpts.width; + ctx.strokeStyle = borderOpts.color; + + ctx.beginPath(); + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + ctx.stroke(); + + ctx.restore(); + } + + /** + * @protected + */ + drawLabels(chartArea) { + const optionTicks = this.options.ticks; + + if (!optionTicks.display) { + return; + } + + const ctx = this.ctx; + + const area = this._computeLabelArea(); + if (area) { + clipArea(ctx, area); + } + + const items = this.getLabelItems(chartArea); + for (const item of items) { + const renderTextOptions = item.options; + const tickFont = item.font; + const label = item.label; + const y = item.textOffset; + renderText(ctx, label, 0, y, tickFont, renderTextOptions); + } + + if (area) { + unclipArea(ctx); + } + } + + /** + * @protected + */ + drawTitle() { + const {ctx, options: {position, title, reverse}} = this; + + if (!title.display) { + return; + } + + const font = toFont(title.font); + const padding = toPadding(title.padding); + const align = title.align; + let offset = font.lineHeight / 2; + + if (position === 'bottom' || position === 'center' || isObject(position)) { + offset += padding.bottom; + if (isArray(title.text)) { + offset += font.lineHeight * (title.text.length - 1); + } + } else { + offset += padding.top; + } + + const {titleX, titleY, maxWidth, rotation} = titleArgs(this, offset, position, align); + + renderText(ctx, title.text, 0, 0, font, { + color: title.color, + maxWidth, + rotation, + textAlign: titleAlign(align, position, reverse), + textBaseline: 'middle', + translation: [titleX, titleY], + strokeColor: title.strokeColor, + strokeWidth: title.strokeWidth + }); + } + + draw(chartArea) { + if (!this._isVisible()) { + return; + } + + this.drawBackground(); + this.drawGrid(chartArea); + this.drawBorder(); + this.drawTitle(); + this.drawLabels(chartArea); + } + + /** + * @return {object[]} + * @private + */ + _layers() { + const opts = this.options; + const tz = opts.ticks && opts.ticks.z || 0; + const gz = valueOrDefault(opts.grid && opts.grid.z, -1); + const bz = valueOrDefault(opts.border && opts.border.z, 0); + + if (!this._isVisible() || this.draw !== Scale.prototype.draw) { + // backward compatibility: draw has been overridden by custom scale + return [{ + z: tz, + draw: (chartArea) => { + this.draw(chartArea); + } + }]; + } + + return [{ + z: gz, + draw: (chartArea) => { + this.drawBackground(); + this.drawGrid(chartArea); + this.drawTitle(); + } + }, { + z: bz, + draw: () => { + this.drawBorder(); + } + }, { + z: tz, + draw: (chartArea) => { + this.drawLabels(chartArea); + } + }]; + } + + /** + * Returns visible dataset metas that are attached to this scale + * @param {string} [type] - if specified, also filter by dataset type + * @return {object[]} + */ + getMatchingVisibleMetas(type) { + const metas = this.chart.getSortedVisibleDatasetMetas(); + const axisID = this.axis + 'AxisID'; + const result = []; + let i, ilen; + + for (i = 0, ilen = metas.length; i < ilen; ++i) { + const meta = metas[i]; + if (meta[axisID] === this.id && (!type || meta.type === type)) { + result.push(meta); + } + } + return result; + } + + /** + * @param {number} index + * @return {object} + * @protected + */ + _resolveTickFontOptions(index) { + const opts = this.options.ticks.setContext(this.getContext(index)); + return toFont(opts.font); + } + + /** + * @protected + */ + _maxDigits() { + const fontSize = this._resolveTickFontOptions(0).lineHeight; + return (this.isHorizontal() ? this.width : this.height) / fontSize; + } +} diff --git a/src/core/core.ticks.js b/src/core/core.ticks.js new file mode 100644 index 00000000000..c0e34b11eda --- /dev/null +++ b/src/core/core.ticks.js @@ -0,0 +1,102 @@ +import {isArray} from '../helpers/helpers.core.js'; +import {formatNumber} from '../helpers/helpers.intl.js'; +import {log10} from '../helpers/helpers.math.js'; + +/** + * Namespace to hold formatters for different types of ticks + * @namespace Chart.Ticks.formatters + */ +const formatters = { + /** + * Formatter for value labels + * @method Chart.Ticks.formatters.values + * @param value the value to display + * @return {string|string[]} the label to display + */ + values(value) { + return isArray(value) ? /** @type {string[]} */ (value) : '' + value; + }, + + /** + * Formatter for numeric ticks + * @method Chart.Ticks.formatters.numeric + * @param tickValue {number} the value to be formatted + * @param index {number} the position of the tickValue parameter in the ticks array + * @param ticks {object[]} the list of ticks being converted + * @return {string} string representation of the tickValue parameter + */ + numeric(tickValue, index, ticks) { + if (tickValue === 0) { + return '0'; // never show decimal places for 0 + } + + const locale = this.chart.options.locale; + let notation; + let delta = tickValue; // This is used when there are less than 2 ticks as the tick interval. + + if (ticks.length > 1) { + // all ticks are small or there huge numbers; use scientific notation + const maxTick = Math.max(Math.abs(ticks[0].value), Math.abs(ticks[ticks.length - 1].value)); + if (maxTick < 1e-4 || maxTick > 1e+15) { + notation = 'scientific'; + } + + delta = calculateDelta(tickValue, ticks); + } + + const logDelta = log10(Math.abs(delta)); + + // When datasets have values approaching Number.MAX_VALUE, the tick calculations might result in + // infinity and eventually NaN. Passing NaN for minimumFractionDigits or maximumFractionDigits + // will make the number formatter throw. So instead we check for isNaN and use a fallback value. + // + // toFixed has a max of 20 decimal places + const numDecimal = isNaN(logDelta) ? 1 : Math.max(Math.min(-1 * Math.floor(logDelta), 20), 0); + + const options = {notation, minimumFractionDigits: numDecimal, maximumFractionDigits: numDecimal}; + Object.assign(options, this.options.ticks.format); + + return formatNumber(tickValue, locale, options); + }, + + + /** + * Formatter for logarithmic ticks + * @method Chart.Ticks.formatters.logarithmic + * @param tickValue {number} the value to be formatted + * @param index {number} the position of the tickValue parameter in the ticks array + * @param ticks {object[]} the list of ticks being converted + * @return {string} string representation of the tickValue parameter + */ + logarithmic(tickValue, index, ticks) { + if (tickValue === 0) { + return '0'; + } + const remain = ticks[index].significand || (tickValue / (Math.pow(10, Math.floor(log10(tickValue))))); + if ([1, 2, 3, 5, 10, 15].includes(remain) || index > 0.8 * ticks.length) { + return formatters.numeric.call(this, tickValue, index, ticks); + } + return ''; + } + +}; + + +function calculateDelta(tickValue, ticks) { + // Figure out how many digits to show + // The space between the first two ticks might be smaller than normal spacing + let delta = ticks.length > 3 ? ticks[2].value - ticks[1].value : ticks[1].value - ticks[0].value; + + // If we have a number like 2.5 as the delta, figure out how many decimal places we need + if (Math.abs(delta) >= 1 && tickValue !== Math.floor(tickValue)) { + // not an integer + delta = tickValue - Math.floor(tickValue); + } + return delta; +} + +/** + * Namespace to hold static tick generation functions + * @namespace Chart.Ticks + */ +export default {formatters}; diff --git a/src/core/core.typedRegistry.js b/src/core/core.typedRegistry.js new file mode 100644 index 00000000000..bc921f6903f --- /dev/null +++ b/src/core/core.typedRegistry.js @@ -0,0 +1,117 @@ +import {merge} from '../helpers/index.js'; +import defaults, {overrides} from './core.defaults.js'; + +/** + * @typedef {{id: string, defaults: any, overrides?: any, defaultRoutes: any}} IChartComponent + */ + +export default class TypedRegistry { + constructor(type, scope, override) { + this.type = type; + this.scope = scope; + this.override = override; + this.items = Object.create(null); + } + + isForType(type) { + return Object.prototype.isPrototypeOf.call(this.type.prototype, type.prototype); + } + + /** + * @param {IChartComponent} item + * @returns {string} The scope where items defaults were registered to. + */ + register(item) { + const proto = Object.getPrototypeOf(item); + let parentScope; + + if (isIChartComponent(proto)) { + // Make sure the parent is registered and note the scope where its defaults are. + parentScope = this.register(proto); + } + + const items = this.items; + const id = item.id; + const scope = this.scope + '.' + id; + + if (!id) { + throw new Error('class does not have id: ' + item); + } + + if (id in items) { + // already registered + return scope; + } + + items[id] = item; + registerDefaults(item, scope, parentScope); + if (this.override) { + defaults.override(item.id, item.overrides); + } + + return scope; + } + + /** + * @param {string} id + * @returns {object?} + */ + get(id) { + return this.items[id]; + } + + /** + * @param {IChartComponent} item + */ + unregister(item) { + const items = this.items; + const id = item.id; + const scope = this.scope; + + if (id in items) { + delete items[id]; + } + + if (scope && id in defaults[scope]) { + delete defaults[scope][id]; + if (this.override) { + delete overrides[id]; + } + } + } +} + +function registerDefaults(item, scope, parentScope) { + // Inherit the parent's defaults and keep existing defaults + const itemDefaults = merge(Object.create(null), [ + parentScope ? defaults.get(parentScope) : {}, + defaults.get(scope), + item.defaults + ]); + + defaults.set(scope, itemDefaults); + + if (item.defaultRoutes) { + routeDefaults(scope, item.defaultRoutes); + } + + if (item.descriptors) { + defaults.describe(scope, item.descriptors); + } +} + +function routeDefaults(scope, routes) { + Object.keys(routes).forEach(property => { + const propertyParts = property.split('.'); + const sourceName = propertyParts.pop(); + const sourceScope = [scope].concat(propertyParts).join('.'); + const parts = routes[property].split('.'); + const targetName = parts.pop(); + const targetScope = parts.join('.'); + defaults.route(sourceScope, sourceName, targetScope, targetName); + }); +} + +function isIChartComponent(proto) { + return 'id' in proto && 'defaults' in proto; +} diff --git a/src/core/index.ts b/src/core/index.ts new file mode 100644 index 00000000000..81a06149da6 --- /dev/null +++ b/src/core/index.ts @@ -0,0 +1,15 @@ +export type {DateAdapter, TimeUnit} from './core.adapters.js'; +export {default as _adapters} from './core.adapters.js'; +export {default as Animation} from './core.animation.js'; +export {default as Animations} from './core.animations.js'; +export {default as animator} from './core.animator.js'; +export {default as Chart} from './core.controller.js'; +export {default as DatasetController} from './core.datasetController.js'; +export {default as defaults} from './core.defaults.js'; +export {default as Element} from './core.element.js'; +export {default as Interaction} from './core.interaction.js'; +export {default as layouts} from './core.layouts.js'; +export {default as plugins} from './core.plugins.js'; +export {default as registry} from './core.registry.js'; +export {default as Scale} from './core.scale.js'; +export {default as Ticks} from './core.ticks.js'; diff --git a/src/elements/element.arc.ts b/src/elements/element.arc.ts new file mode 100644 index 00000000000..42f41f045b0 --- /dev/null +++ b/src/elements/element.arc.ts @@ -0,0 +1,421 @@ +import Element from '../core/core.element.js'; +import {_angleBetween, getAngleFromPoint, TAU, HALF_PI, valueOrDefault} from '../helpers/index.js'; +import {PI, _angleDiff, _normalizeAngle, _isBetween, _limitValue} from '../helpers/helpers.math.js'; +import {_readValueToProps} from '../helpers/helpers.options.js'; +import type {ArcOptions, Point} from '../types/index.js'; + +function clipSelf(ctx: CanvasRenderingContext2D, element: ArcElement, endAngle: number) { + const {startAngle, x, y, outerRadius, innerRadius, options} = element; + const {borderWidth, borderJoinStyle} = options; + const outerAngleClip = Math.min(borderWidth / outerRadius, _normalizeAngle(startAngle - endAngle)); + ctx.beginPath(); + ctx.arc(x, y, outerRadius - borderWidth / 2, startAngle + outerAngleClip / 2, endAngle - outerAngleClip / 2); + + if (innerRadius > 0) { + const innerAngleClip = Math.min(borderWidth / innerRadius, _normalizeAngle(startAngle - endAngle)); + ctx.arc(x, y, innerRadius + borderWidth / 2, endAngle - innerAngleClip / 2, startAngle + innerAngleClip / 2, true); + } else { + const clipWidth = Math.min(borderWidth / 2, outerRadius * _normalizeAngle(startAngle - endAngle)); + + if (borderJoinStyle === 'round') { + ctx.arc(x, y, clipWidth, endAngle - PI / 2, startAngle + PI / 2, true); + } else if (borderJoinStyle === 'bevel') { + const r = 2 * clipWidth * clipWidth; + const endX = -r * Math.cos(endAngle + PI / 2) + x; + const endY = -r * Math.sin(endAngle + PI / 2) + y; + const startX = r * Math.cos(startAngle + PI / 2) + x; + const startY = r * Math.sin(startAngle + PI / 2) + y; + ctx.lineTo(endX, endY); + ctx.lineTo(startX, startY); + } + } + ctx.closePath(); + + ctx.moveTo(0, 0); + ctx.rect(0, 0, ctx.canvas.width, ctx.canvas.height); + + ctx.clip('evenodd'); +} + + +function clipArc(ctx: CanvasRenderingContext2D, element: ArcElement, endAngle: number) { + const {startAngle, pixelMargin, x, y, outerRadius, innerRadius} = element; + let angleMargin = pixelMargin / outerRadius; + + // Draw an inner border by clipping the arc and drawing a double-width border + // Enlarge the clipping arc by 0.33 pixels to eliminate glitches between borders + ctx.beginPath(); + ctx.arc(x, y, outerRadius, startAngle - angleMargin, endAngle + angleMargin); + if (innerRadius > pixelMargin) { + angleMargin = pixelMargin / innerRadius; + ctx.arc(x, y, innerRadius, endAngle + angleMargin, startAngle - angleMargin, true); + } else { + ctx.arc(x, y, pixelMargin, endAngle + HALF_PI, startAngle - HALF_PI); + } + ctx.closePath(); + ctx.clip(); +} + +function toRadiusCorners(value) { + return _readValueToProps(value, ['outerStart', 'outerEnd', 'innerStart', 'innerEnd']); +} + +/** + * Parse border radius from the provided options + */ +function parseBorderRadius(arc: ArcElement, innerRadius: number, outerRadius: number, angleDelta: number) { + const o = toRadiusCorners(arc.options.borderRadius); + const halfThickness = (outerRadius - innerRadius) / 2; + const innerLimit = Math.min(halfThickness, angleDelta * innerRadius / 2); + + // Outer limits are complicated. We want to compute the available angular distance at + // a radius of outerRadius - borderRadius because for small angular distances, this term limits. + // We compute at r = outerRadius - borderRadius because this circle defines the center of the border corners. + // + // If the borderRadius is large, that value can become negative. + // This causes the outer borders to lose their radius entirely, which is rather unexpected. To solve that, if borderRadius > outerRadius + // we know that the thickness term will dominate and compute the limits at that point + const computeOuterLimit = (val) => { + const outerArcLimit = (outerRadius - Math.min(halfThickness, val)) * angleDelta / 2; + return _limitValue(val, 0, Math.min(halfThickness, outerArcLimit)); + }; + + return { + outerStart: computeOuterLimit(o.outerStart), + outerEnd: computeOuterLimit(o.outerEnd), + innerStart: _limitValue(o.innerStart, 0, innerLimit), + innerEnd: _limitValue(o.innerEnd, 0, innerLimit), + }; +} + +/** + * Convert (r, 𝜃) to (x, y) + */ +function rThetaToXY(r: number, theta: number, x: number, y: number) { + return { + x: x + r * Math.cos(theta), + y: y + r * Math.sin(theta), + }; +} + + +/** + * Path the arc, respecting border radius by separating into left and right halves. + * + * Start End + * + * 1--->a--->2 Outer + * / \ + * 8 3 + * | | + * | | + * 7 4 + * \ / + * 6<---b<---5 Inner + */ +function pathArc( + ctx: CanvasRenderingContext2D, + element: ArcElement, + offset: number, + spacing: number, + end: number, + circular: boolean, +) { + const {x, y, startAngle: start, pixelMargin, innerRadius: innerR} = element; + + const outerRadius = Math.max(element.outerRadius + spacing + offset - pixelMargin, 0); + const innerRadius = innerR > 0 ? innerR + spacing + offset + pixelMargin : 0; + + let spacingOffset = 0; + const alpha = end - start; + + if (spacing) { + // When spacing is present, it is the same for all items + // So we adjust the start and end angle of the arc such that + // the distance is the same as it would be without the spacing + const noSpacingInnerRadius = innerR > 0 ? innerR - spacing : 0; + const noSpacingOuterRadius = outerRadius > 0 ? outerRadius - spacing : 0; + const avNogSpacingRadius = (noSpacingInnerRadius + noSpacingOuterRadius) / 2; + const adjustedAngle = avNogSpacingRadius !== 0 ? (alpha * avNogSpacingRadius) / (avNogSpacingRadius + spacing) : alpha; + spacingOffset = (alpha - adjustedAngle) / 2; + } + + const beta = Math.max(0.001, alpha * outerRadius - offset / PI) / outerRadius; + const angleOffset = (alpha - beta) / 2; + const startAngle = start + angleOffset + spacingOffset; + const endAngle = end - angleOffset - spacingOffset; + const {outerStart, outerEnd, innerStart, innerEnd} = parseBorderRadius(element, innerRadius, outerRadius, endAngle - startAngle); + + const outerStartAdjustedRadius = outerRadius - outerStart; + const outerEndAdjustedRadius = outerRadius - outerEnd; + const outerStartAdjustedAngle = startAngle + outerStart / outerStartAdjustedRadius; + const outerEndAdjustedAngle = endAngle - outerEnd / outerEndAdjustedRadius; + + const innerStartAdjustedRadius = innerRadius + innerStart; + const innerEndAdjustedRadius = innerRadius + innerEnd; + const innerStartAdjustedAngle = startAngle + innerStart / innerStartAdjustedRadius; + const innerEndAdjustedAngle = endAngle - innerEnd / innerEndAdjustedRadius; + + ctx.beginPath(); + + if (circular) { + // The first arc segments from point 1 to point a to point 2 + const outerMidAdjustedAngle = (outerStartAdjustedAngle + outerEndAdjustedAngle) / 2; + ctx.arc(x, y, outerRadius, outerStartAdjustedAngle, outerMidAdjustedAngle); + ctx.arc(x, y, outerRadius, outerMidAdjustedAngle, outerEndAdjustedAngle); + + // The corner segment from point 2 to point 3 + if (outerEnd > 0) { + const pCenter = rThetaToXY(outerEndAdjustedRadius, outerEndAdjustedAngle, x, y); + ctx.arc(pCenter.x, pCenter.y, outerEnd, outerEndAdjustedAngle, endAngle + HALF_PI); + } + + // The line from point 3 to point 4 + const p4 = rThetaToXY(innerEndAdjustedRadius, endAngle, x, y); + ctx.lineTo(p4.x, p4.y); + + // The corner segment from point 4 to point 5 + if (innerEnd > 0) { + const pCenter = rThetaToXY(innerEndAdjustedRadius, innerEndAdjustedAngle, x, y); + ctx.arc(pCenter.x, pCenter.y, innerEnd, endAngle + HALF_PI, innerEndAdjustedAngle + Math.PI); + } + + // The inner arc from point 5 to point b to point 6 + const innerMidAdjustedAngle = ((endAngle - (innerEnd / innerRadius)) + (startAngle + (innerStart / innerRadius))) / 2; + ctx.arc(x, y, innerRadius, endAngle - (innerEnd / innerRadius), innerMidAdjustedAngle, true); + ctx.arc(x, y, innerRadius, innerMidAdjustedAngle, startAngle + (innerStart / innerRadius), true); + + // The corner segment from point 6 to point 7 + if (innerStart > 0) { + const pCenter = rThetaToXY(innerStartAdjustedRadius, innerStartAdjustedAngle, x, y); + ctx.arc(pCenter.x, pCenter.y, innerStart, innerStartAdjustedAngle + Math.PI, startAngle - HALF_PI); + } + + // The line from point 7 to point 8 + const p8 = rThetaToXY(outerStartAdjustedRadius, startAngle, x, y); + ctx.lineTo(p8.x, p8.y); + + // The corner segment from point 8 to point 1 + if (outerStart > 0) { + const pCenter = rThetaToXY(outerStartAdjustedRadius, outerStartAdjustedAngle, x, y); + ctx.arc(pCenter.x, pCenter.y, outerStart, startAngle - HALF_PI, outerStartAdjustedAngle); + } + } else { + ctx.moveTo(x, y); + + const outerStartX = Math.cos(outerStartAdjustedAngle) * outerRadius + x; + const outerStartY = Math.sin(outerStartAdjustedAngle) * outerRadius + y; + ctx.lineTo(outerStartX, outerStartY); + + const outerEndX = Math.cos(outerEndAdjustedAngle) * outerRadius + x; + const outerEndY = Math.sin(outerEndAdjustedAngle) * outerRadius + y; + ctx.lineTo(outerEndX, outerEndY); + } + + ctx.closePath(); +} + +function drawArc( + ctx: CanvasRenderingContext2D, + element: ArcElement, + offset: number, + spacing: number, + circular: boolean, +) { + const {fullCircles, startAngle, circumference} = element; + let endAngle = element.endAngle; + if (fullCircles) { + pathArc(ctx, element, offset, spacing, endAngle, circular); + for (let i = 0; i < fullCircles; ++i) { + ctx.fill(); + } + if (!isNaN(circumference)) { + endAngle = startAngle + (circumference % TAU || TAU); + } + } + pathArc(ctx, element, offset, spacing, endAngle, circular); + ctx.fill(); + return endAngle; +} + +function drawBorder( + ctx: CanvasRenderingContext2D, + element: ArcElement, + offset: number, + spacing: number, + circular: boolean, +) { + const {fullCircles, startAngle, circumference, options} = element; + const {borderWidth, borderJoinStyle, borderDash, borderDashOffset, borderRadius} = options; + const inner = options.borderAlign === 'inner'; + + if (!borderWidth) { + return; + } + + ctx.setLineDash(borderDash || []); + ctx.lineDashOffset = borderDashOffset; + + if (inner) { + ctx.lineWidth = borderWidth * 2; + ctx.lineJoin = borderJoinStyle || 'round'; + } else { + ctx.lineWidth = borderWidth; + ctx.lineJoin = borderJoinStyle || 'bevel'; + } + + let endAngle = element.endAngle; + if (fullCircles) { + pathArc(ctx, element, offset, spacing, endAngle, circular); + for (let i = 0; i < fullCircles; ++i) { + ctx.stroke(); + } + if (!isNaN(circumference)) { + endAngle = startAngle + (circumference % TAU || TAU); + } + } + + if (inner) { + clipArc(ctx, element, endAngle); + } + + if (options.selfJoin && endAngle - startAngle >= PI && borderRadius === 0 && borderJoinStyle !== 'miter') { + clipSelf(ctx, element, endAngle); + } + + if (!fullCircles) { + pathArc(ctx, element, offset, spacing, endAngle, circular); + ctx.stroke(); + } +} + +export interface ArcProps extends Point { + startAngle: number; + endAngle: number; + innerRadius: number; + outerRadius: number; + circumference: number; +} + +export default class ArcElement extends Element { + + static id = 'arc'; + + static defaults = { + borderAlign: 'center', + borderColor: '#fff', + borderDash: [], + borderDashOffset: 0, + borderJoinStyle: undefined, + borderRadius: 0, + borderWidth: 2, + offset: 0, + spacing: 0, + angle: undefined, + circular: true, + selfJoin: false, + }; + + static defaultRoutes = { + backgroundColor: 'backgroundColor' + }; + + static descriptors = { + _scriptable: true, + _indexable: (name) => name !== 'borderDash' + }; + + circumference: number; + endAngle: number; + fullCircles: number; + innerRadius: number; + outerRadius: number; + pixelMargin: number; + startAngle: number; + + constructor(cfg) { + super(); + + this.options = undefined; + this.circumference = undefined; + this.startAngle = undefined; + this.endAngle = undefined; + this.innerRadius = undefined; + this.outerRadius = undefined; + this.pixelMargin = 0; + this.fullCircles = 0; + + if (cfg) { + Object.assign(this, cfg); + } + } + + inRange(chartX: number, chartY: number, useFinalPosition: boolean) { + const point = this.getProps(['x', 'y'], useFinalPosition); + const {angle, distance} = getAngleFromPoint(point, {x: chartX, y: chartY}); + const {startAngle, endAngle, innerRadius, outerRadius, circumference} = this.getProps([ + 'startAngle', + 'endAngle', + 'innerRadius', + 'outerRadius', + 'circumference' + ], useFinalPosition); + const rAdjust = (this.options.spacing + this.options.borderWidth) / 2; + const _circumference = valueOrDefault(circumference, endAngle - startAngle); + const nonZeroBetween = _angleBetween(angle, startAngle, endAngle) && startAngle !== endAngle; + const betweenAngles = _circumference >= TAU || nonZeroBetween; + const withinRadius = _isBetween(distance, innerRadius + rAdjust, outerRadius + rAdjust); + + return (betweenAngles && withinRadius); + } + + getCenterPoint(useFinalPosition: boolean) { + const {x, y, startAngle, endAngle, innerRadius, outerRadius} = this.getProps([ + 'x', + 'y', + 'startAngle', + 'endAngle', + 'innerRadius', + 'outerRadius' + ], useFinalPosition); + const {offset, spacing} = this.options; + const halfAngle = (startAngle + endAngle) / 2; + const halfRadius = (innerRadius + outerRadius + spacing + offset) / 2; + return { + x: x + Math.cos(halfAngle) * halfRadius, + y: y + Math.sin(halfAngle) * halfRadius + }; + } + + tooltipPosition(useFinalPosition: boolean) { + return this.getCenterPoint(useFinalPosition); + } + + draw(ctx: CanvasRenderingContext2D) { + const {options, circumference} = this; + const offset = (options.offset || 0) / 4; + const spacing = (options.spacing || 0) / 2; + const circular = options.circular; + this.pixelMargin = (options.borderAlign === 'inner') ? 0.33 : 0; + this.fullCircles = circumference > TAU ? Math.floor(circumference / TAU) : 0; + + if (circumference === 0 || this.innerRadius < 0 || this.outerRadius < 0) { + return; + } + + ctx.save(); + + const halfAngle = (this.startAngle + this.endAngle) / 2; + ctx.translate(Math.cos(halfAngle) * offset, Math.sin(halfAngle) * offset); + const fix = 1 - Math.sin(Math.min(PI, circumference || 0)); + const radiusOffset = offset * fix; + + ctx.fillStyle = options.backgroundColor; + ctx.strokeStyle = options.borderColor; + + drawArc(ctx, this, radiusOffset, spacing, circular); + drawBorder(ctx, this, radiusOffset, spacing, circular); + + ctx.restore(); + } +} diff --git a/src/elements/element.bar.js b/src/elements/element.bar.js new file mode 100644 index 00000000000..6b0cfc70bc1 --- /dev/null +++ b/src/elements/element.bar.js @@ -0,0 +1,226 @@ +import Element from '../core/core.element.js'; +import {isObject, _isBetween, _limitValue} from '../helpers/index.js'; +import {addRoundedRectPath} from '../helpers/helpers.canvas.js'; +import {toTRBL, toTRBLCorners} from '../helpers/helpers.options.js'; + +/** @typedef {{ x: number, y: number, base: number, horizontal: boolean, width: number, height: number }} BarProps */ + +/** + * Helper function to get the bounds of the bar regardless of the orientation + * @param {BarElement} bar the bar + * @param {boolean} [useFinalPosition] + * @return {object} bounds of the bar + * @private + */ +function getBarBounds(bar, useFinalPosition) { + const {x, y, base, width, height} = /** @type {BarProps} */ (bar.getProps(['x', 'y', 'base', 'width', 'height'], useFinalPosition)); + + let left, right, top, bottom, half; + + if (bar.horizontal) { + half = height / 2; + left = Math.min(x, base); + right = Math.max(x, base); + top = y - half; + bottom = y + half; + } else { + half = width / 2; + left = x - half; + right = x + half; + top = Math.min(y, base); + bottom = Math.max(y, base); + } + + return {left, top, right, bottom}; +} + +function skipOrLimit(skip, value, min, max) { + return skip ? 0 : _limitValue(value, min, max); +} + +function parseBorderWidth(bar, maxW, maxH) { + const value = bar.options.borderWidth; + const skip = bar.borderSkipped; + const o = toTRBL(value); + + return { + t: skipOrLimit(skip.top, o.top, 0, maxH), + r: skipOrLimit(skip.right, o.right, 0, maxW), + b: skipOrLimit(skip.bottom, o.bottom, 0, maxH), + l: skipOrLimit(skip.left, o.left, 0, maxW) + }; +} + +function parseBorderRadius(bar, maxW, maxH) { + const {enableBorderRadius} = bar.getProps(['enableBorderRadius']); + const value = bar.options.borderRadius; + const o = toTRBLCorners(value); + const maxR = Math.min(maxW, maxH); + const skip = bar.borderSkipped; + + // If the value is an object, assume the user knows what they are doing + // and apply as directed. + const enableBorder = enableBorderRadius || isObject(value); + + return { + topLeft: skipOrLimit(!enableBorder || skip.top || skip.left, o.topLeft, 0, maxR), + topRight: skipOrLimit(!enableBorder || skip.top || skip.right, o.topRight, 0, maxR), + bottomLeft: skipOrLimit(!enableBorder || skip.bottom || skip.left, o.bottomLeft, 0, maxR), + bottomRight: skipOrLimit(!enableBorder || skip.bottom || skip.right, o.bottomRight, 0, maxR) + }; +} + +function boundingRects(bar) { + const bounds = getBarBounds(bar); + const width = bounds.right - bounds.left; + const height = bounds.bottom - bounds.top; + const border = parseBorderWidth(bar, width / 2, height / 2); + const radius = parseBorderRadius(bar, width / 2, height / 2); + + return { + outer: { + x: bounds.left, + y: bounds.top, + w: width, + h: height, + radius + }, + inner: { + x: bounds.left + border.l, + y: bounds.top + border.t, + w: width - border.l - border.r, + h: height - border.t - border.b, + radius: { + topLeft: Math.max(0, radius.topLeft - Math.max(border.t, border.l)), + topRight: Math.max(0, radius.topRight - Math.max(border.t, border.r)), + bottomLeft: Math.max(0, radius.bottomLeft - Math.max(border.b, border.l)), + bottomRight: Math.max(0, radius.bottomRight - Math.max(border.b, border.r)), + } + } + }; +} + +function inRange(bar, x, y, useFinalPosition) { + const skipX = x === null; + const skipY = y === null; + const skipBoth = skipX && skipY; + const bounds = bar && !skipBoth && getBarBounds(bar, useFinalPosition); + + return bounds + && (skipX || _isBetween(x, bounds.left, bounds.right)) + && (skipY || _isBetween(y, bounds.top, bounds.bottom)); +} + +function hasRadius(radius) { + return radius.topLeft || radius.topRight || radius.bottomLeft || radius.bottomRight; +} + +/** + * Add a path of a rectangle to the current sub-path + * @param {CanvasRenderingContext2D} ctx Context + * @param {*} rect Bounding rect + */ +function addNormalRectPath(ctx, rect) { + ctx.rect(rect.x, rect.y, rect.w, rect.h); +} + +function inflateRect(rect, amount, refRect = {}) { + const x = rect.x !== refRect.x ? -amount : 0; + const y = rect.y !== refRect.y ? -amount : 0; + const w = (rect.x + rect.w !== refRect.x + refRect.w ? amount : 0) - x; + const h = (rect.y + rect.h !== refRect.y + refRect.h ? amount : 0) - y; + return { + x: rect.x + x, + y: rect.y + y, + w: rect.w + w, + h: rect.h + h, + radius: rect.radius + }; +} + +export default class BarElement extends Element { + + static id = 'bar'; + + /** + * @type {any} + */ + static defaults = { + borderSkipped: 'start', + borderWidth: 0, + borderRadius: 0, + inflateAmount: 'auto', + pointStyle: undefined + }; + + /** + * @type {any} + */ + static defaultRoutes = { + backgroundColor: 'backgroundColor', + borderColor: 'borderColor' + }; + + constructor(cfg) { + super(); + + this.options = undefined; + this.horizontal = undefined; + this.base = undefined; + this.width = undefined; + this.height = undefined; + this.inflateAmount = undefined; + + if (cfg) { + Object.assign(this, cfg); + } + } + + draw(ctx) { + const {inflateAmount, options: {borderColor, backgroundColor}} = this; + const {inner, outer} = boundingRects(this); + const addRectPath = hasRadius(outer.radius) ? addRoundedRectPath : addNormalRectPath; + + ctx.save(); + + if (outer.w !== inner.w || outer.h !== inner.h) { + ctx.beginPath(); + addRectPath(ctx, inflateRect(outer, inflateAmount, inner)); + ctx.clip(); + addRectPath(ctx, inflateRect(inner, -inflateAmount, outer)); + ctx.fillStyle = borderColor; + ctx.fill('evenodd'); + } + + ctx.beginPath(); + addRectPath(ctx, inflateRect(inner, inflateAmount)); + ctx.fillStyle = backgroundColor; + ctx.fill(); + + ctx.restore(); + } + + inRange(mouseX, mouseY, useFinalPosition) { + return inRange(this, mouseX, mouseY, useFinalPosition); + } + + inXRange(mouseX, useFinalPosition) { + return inRange(this, mouseX, null, useFinalPosition); + } + + inYRange(mouseY, useFinalPosition) { + return inRange(this, null, mouseY, useFinalPosition); + } + + getCenterPoint(useFinalPosition) { + const {x, y, base, horizontal} = /** @type {BarProps} */ (this.getProps(['x', 'y', 'base', 'horizontal'], useFinalPosition)); + return { + x: horizontal ? (x + base) / 2 : x, + y: horizontal ? y : (y + base) / 2 + }; + } + + getRange(axis) { + return axis === 'x' ? this.width / 2 : this.height / 2; + } +} diff --git a/src/elements/element.line.js b/src/elements/element.line.js new file mode 100644 index 00000000000..4384e4d7b84 --- /dev/null +++ b/src/elements/element.line.js @@ -0,0 +1,445 @@ +import Element from '../core/core.element.js'; +import {_bezierInterpolation, _pointInLine, _steppedInterpolation} from '../helpers/helpers.interpolation.js'; +import {_computeSegments, _boundSegments} from '../helpers/helpers.segment.js'; +import {_steppedLineTo, _bezierCurveTo} from '../helpers/helpers.canvas.js'; +import {_updateBezierControlPoints} from '../helpers/helpers.curve.js'; +import {valueOrDefault} from '../helpers/index.js'; + +/** + * @typedef { import('./element.point.js').default } PointElement + */ + +function setStyle(ctx, options, style = options) { + ctx.lineCap = valueOrDefault(style.borderCapStyle, options.borderCapStyle); + ctx.setLineDash(valueOrDefault(style.borderDash, options.borderDash)); + ctx.lineDashOffset = valueOrDefault(style.borderDashOffset, options.borderDashOffset); + ctx.lineJoin = valueOrDefault(style.borderJoinStyle, options.borderJoinStyle); + ctx.lineWidth = valueOrDefault(style.borderWidth, options.borderWidth); + ctx.strokeStyle = valueOrDefault(style.borderColor, options.borderColor); +} + +function lineTo(ctx, previous, target) { + ctx.lineTo(target.x, target.y); +} + +/** + * @returns {any} + */ +function getLineMethod(options) { + if (options.stepped) { + return _steppedLineTo; + } + + if (options.tension || options.cubicInterpolationMode === 'monotone') { + return _bezierCurveTo; + } + + return lineTo; +} + +function pathVars(points, segment, params = {}) { + const count = points.length; + const {start: paramsStart = 0, end: paramsEnd = count - 1} = params; + const {start: segmentStart, end: segmentEnd} = segment; + const start = Math.max(paramsStart, segmentStart); + const end = Math.min(paramsEnd, segmentEnd); + const outside = paramsStart < segmentStart && paramsEnd < segmentStart || paramsStart > segmentEnd && paramsEnd > segmentEnd; + + return { + count, + start, + loop: segment.loop, + ilen: end < start && !outside ? count + end - start : end - start + }; +} + +/** + * Create path from points, grouping by truncated x-coordinate + * Points need to be in order by x-coordinate for this to work efficiently + * @param {CanvasRenderingContext2D|Path2D} ctx - Context + * @param {LineElement} line + * @param {object} segment + * @param {number} segment.start - start index of the segment, referring the points array + * @param {number} segment.end - end index of the segment, referring the points array + * @param {boolean} segment.loop - indicates that the segment is a loop + * @param {object} params + * @param {boolean} params.move - move to starting point (vs line to it) + * @param {boolean} params.reverse - path the segment from end to start + * @param {number} params.start - limit segment to points starting from `start` index + * @param {number} params.end - limit segment to points ending at `start` + `count` index + */ +function pathSegment(ctx, line, segment, params) { + const {points, options} = line; + const {count, start, loop, ilen} = pathVars(points, segment, params); + const lineMethod = getLineMethod(options); + // eslint-disable-next-line prefer-const + let {move = true, reverse} = params || {}; + let i, point, prev; + + for (i = 0; i <= ilen; ++i) { + point = points[(start + (reverse ? ilen - i : i)) % count]; + + if (point.skip) { + // If there is a skipped point inside a segment, spanGaps must be true + continue; + } else if (move) { + ctx.moveTo(point.x, point.y); + move = false; + } else { + lineMethod(ctx, prev, point, reverse, options.stepped); + } + + prev = point; + } + + if (loop) { + point = points[(start + (reverse ? ilen : 0)) % count]; + lineMethod(ctx, prev, point, reverse, options.stepped); + } + + return !!loop; +} + +/** + * Create path from points, grouping by truncated x-coordinate + * Points need to be in order by x-coordinate for this to work efficiently + * @param {CanvasRenderingContext2D|Path2D} ctx - Context + * @param {LineElement} line + * @param {object} segment + * @param {number} segment.start - start index of the segment, referring the points array + * @param {number} segment.end - end index of the segment, referring the points array + * @param {boolean} segment.loop - indicates that the segment is a loop + * @param {object} params + * @param {boolean} params.move - move to starting point (vs line to it) + * @param {boolean} params.reverse - path the segment from end to start + * @param {number} params.start - limit segment to points starting from `start` index + * @param {number} params.end - limit segment to points ending at `start` + `count` index + */ +function fastPathSegment(ctx, line, segment, params) { + const points = line.points; + const {count, start, ilen} = pathVars(points, segment, params); + const {move = true, reverse} = params || {}; + let avgX = 0; + let countX = 0; + let i, point, prevX, minY, maxY, lastY; + + const pointIndex = (index) => (start + (reverse ? ilen - index : index)) % count; + const drawX = () => { + if (minY !== maxY) { + // Draw line to maxY and minY, using the average x-coordinate + ctx.lineTo(avgX, maxY); + ctx.lineTo(avgX, minY); + // Line to y-value of last point in group. So the line continues + // from correct position. Not using move, to have solid path. + ctx.lineTo(avgX, lastY); + } + }; + + if (move) { + point = points[pointIndex(0)]; + ctx.moveTo(point.x, point.y); + } + + for (i = 0; i <= ilen; ++i) { + point = points[pointIndex(i)]; + + if (point.skip) { + // If there is a skipped point inside a segment, spanGaps must be true + continue; + } + + const x = point.x; + const y = point.y; + const truncX = x | 0; // truncated x-coordinate + + if (truncX === prevX) { + // Determine `minY` / `maxY` and `avgX` while we stay within same x-position + if (y < minY) { + minY = y; + } else if (y > maxY) { + maxY = y; + } + // For first point in group, countX is `0`, so average will be `x` / 1. + avgX = (countX * avgX + x) / ++countX; + } else { + drawX(); + // Draw line to next x-position, using the first (or only) + // y-value in that group + ctx.lineTo(x, y); + + prevX = truncX; + countX = 0; + minY = maxY = y; + } + // Keep track of the last y-value in group + lastY = y; + } + drawX(); +} + +/** + * @param {LineElement} line - the line + * @returns {function} + * @private + */ +function _getSegmentMethod(line) { + const opts = line.options; + const borderDash = opts.borderDash && opts.borderDash.length; + const useFastPath = !line._decimated && !line._loop && !opts.tension && opts.cubicInterpolationMode !== 'monotone' && !opts.stepped && !borderDash; + return useFastPath ? fastPathSegment : pathSegment; +} + +/** + * @private + */ +function _getInterpolationMethod(options) { + if (options.stepped) { + return _steppedInterpolation; + } + + if (options.tension || options.cubicInterpolationMode === 'monotone') { + return _bezierInterpolation; + } + + return _pointInLine; +} + +function strokePathWithCache(ctx, line, start, count) { + let path = line._path; + if (!path) { + path = line._path = new Path2D(); + if (line.path(path, start, count)) { + path.closePath(); + } + } + setStyle(ctx, line.options); + ctx.stroke(path); +} + +function strokePathDirect(ctx, line, start, count) { + const {segments, options} = line; + const segmentMethod = _getSegmentMethod(line); + + for (const segment of segments) { + setStyle(ctx, options, segment.style); + ctx.beginPath(); + if (segmentMethod(ctx, line, segment, {start, end: start + count - 1})) { + ctx.closePath(); + } + ctx.stroke(); + } +} + +const usePath2D = typeof Path2D === 'function'; + +function draw(ctx, line, start, count) { + if (usePath2D && !line.options.segment) { + strokePathWithCache(ctx, line, start, count); + } else { + strokePathDirect(ctx, line, start, count); + } +} + +export default class LineElement extends Element { + + static id = 'line'; + + /** + * @type {any} + */ + static defaults = { + borderCapStyle: 'butt', + borderDash: [], + borderDashOffset: 0, + borderJoinStyle: 'miter', + borderWidth: 3, + capBezierPoints: true, + cubicInterpolationMode: 'default', + fill: false, + spanGaps: false, + stepped: false, + tension: 0, + }; + + /** + * @type {any} + */ + static defaultRoutes = { + backgroundColor: 'backgroundColor', + borderColor: 'borderColor' + }; + + + static descriptors = { + _scriptable: true, + _indexable: (name) => name !== 'borderDash' && name !== 'fill', + }; + + + constructor(cfg) { + super(); + + this.animated = true; + this.options = undefined; + this._chart = undefined; + this._loop = undefined; + this._fullLoop = undefined; + this._path = undefined; + this._points = undefined; + this._segments = undefined; + this._decimated = false; + this._pointsUpdated = false; + this._datasetIndex = undefined; + + if (cfg) { + Object.assign(this, cfg); + } + } + + updateControlPoints(chartArea, indexAxis) { + const options = this.options; + if ((options.tension || options.cubicInterpolationMode === 'monotone') && !options.stepped && !this._pointsUpdated) { + const loop = options.spanGaps ? this._loop : this._fullLoop; + _updateBezierControlPoints(this._points, options, chartArea, loop, indexAxis); + this._pointsUpdated = true; + } + } + + set points(points) { + this._points = points; + delete this._segments; + delete this._path; + this._pointsUpdated = false; + } + + get points() { + return this._points; + } + + get segments() { + return this._segments || (this._segments = _computeSegments(this, this.options.segment)); + } + + /** + * First non-skipped point on this line + * @returns {PointElement|undefined} + */ + first() { + const segments = this.segments; + const points = this.points; + return segments.length && points[segments[0].start]; + } + + /** + * Last non-skipped point on this line + * @returns {PointElement|undefined} + */ + last() { + const segments = this.segments; + const points = this.points; + const count = segments.length; + return count && points[segments[count - 1].end]; + } + + /** + * Interpolate a point in this line at the same value on `property` as + * the reference `point` provided + * @param {PointElement} point - the reference point + * @param {string} property - the property to match on + * @returns {PointElement|undefined} + */ + interpolate(point, property) { + const options = this.options; + const value = point[property]; + const points = this.points; + const segments = _boundSegments(this, {property, start: value, end: value}); + + if (!segments.length) { + return; + } + + const result = []; + const _interpolate = _getInterpolationMethod(options); + let i, ilen; + for (i = 0, ilen = segments.length; i < ilen; ++i) { + const {start, end} = segments[i]; + const p1 = points[start]; + const p2 = points[end]; + if (p1 === p2) { + result.push(p1); + continue; + } + const t = Math.abs((value - p1[property]) / (p2[property] - p1[property])); + const interpolated = _interpolate(p1, p2, t, options.stepped); + interpolated[property] = point[property]; + result.push(interpolated); + } + return result.length === 1 ? result[0] : result; + } + + /** + * Append a segment of this line to current path. + * @param {CanvasRenderingContext2D} ctx + * @param {object} segment + * @param {number} segment.start - start index of the segment, referring the points array + * @param {number} segment.end - end index of the segment, referring the points array + * @param {boolean} segment.loop - indicates that the segment is a loop + * @param {object} params + * @param {boolean} params.move - move to starting point (vs line to it) + * @param {boolean} params.reverse - path the segment from end to start + * @param {number} params.start - limit segment to points starting from `start` index + * @param {number} params.end - limit segment to points ending at `start` + `count` index + * @returns {undefined|boolean} - true if the segment is a full loop (path should be closed) + */ + pathSegment(ctx, segment, params) { + const segmentMethod = _getSegmentMethod(this); + return segmentMethod(ctx, this, segment, params); + } + + /** + * Append all segments of this line to current path. + * @param {CanvasRenderingContext2D|Path2D} ctx + * @param {number} [start] + * @param {number} [count] + * @returns {undefined|boolean} - true if line is a full loop (path should be closed) + */ + path(ctx, start, count) { + const segments = this.segments; + const segmentMethod = _getSegmentMethod(this); + let loop = this._loop; + + start = start || 0; + count = count || (this.points.length - start); + + for (const segment of segments) { + loop &= segmentMethod(ctx, this, segment, {start, end: start + count - 1}); + } + return !!loop; + } + + /** + * Draw + * @param {CanvasRenderingContext2D} ctx + * @param {object} chartArea + * @param {number} [start] + * @param {number} [count] + */ + draw(ctx, chartArea, start, count) { + const options = this.options || {}; + const points = this.points || []; + + if (points.length && options.borderWidth) { + ctx.save(); + + draw(ctx, this, start, count); + + ctx.restore(); + } + + if (this.animated) { + // When line is animated, the control points and path are not cached. + this._pointsUpdated = false; + this._path = undefined; + } + } +} diff --git a/src/elements/element.point.ts b/src/elements/element.point.ts new file mode 100644 index 00000000000..dbe6b131ef4 --- /dev/null +++ b/src/elements/element.point.ts @@ -0,0 +1,107 @@ +import Element from '../core/core.element.js'; +import {drawPoint, _isPointInArea} from '../helpers/helpers.canvas.js'; +import type { + CartesianParsedData, + ChartArea, + Point, + PointHoverOptions, + PointOptions, +} from '../types/index.js'; + +function inRange(el: PointElement, pos: number, axis: 'x' | 'y', useFinalPosition?: boolean) { + const options = el.options; + const {[axis]: value} = el.getProps([axis], useFinalPosition); + + return (Math.abs(pos - value) < options.radius + options.hitRadius); +} + +export type PointProps = Point + +export default class PointElement extends Element { + + static id = 'point'; + + parsed: CartesianParsedData; + skip?: boolean; + stop?: boolean; + + /** + * @type {any} + */ + static defaults = { + borderWidth: 1, + hitRadius: 1, + hoverBorderWidth: 1, + hoverRadius: 4, + pointStyle: 'circle', + radius: 3, + rotation: 0 + }; + + /** + * @type {any} + */ + static defaultRoutes = { + backgroundColor: 'backgroundColor', + borderColor: 'borderColor' + }; + + constructor(cfg) { + super(); + + this.options = undefined; + this.parsed = undefined; + this.skip = undefined; + this.stop = undefined; + + if (cfg) { + Object.assign(this, cfg); + } + } + + inRange(mouseX: number, mouseY: number, useFinalPosition?: boolean) { + const options = this.options; + const {x, y} = this.getProps(['x', 'y'], useFinalPosition); + return ((Math.pow(mouseX - x, 2) + Math.pow(mouseY - y, 2)) < Math.pow(options.hitRadius + options.radius, 2)); + } + + inXRange(mouseX: number, useFinalPosition?: boolean) { + return inRange(this, mouseX, 'x', useFinalPosition); + } + + inYRange(mouseY: number, useFinalPosition?: boolean) { + return inRange(this, mouseY, 'y', useFinalPosition); + } + + getCenterPoint(useFinalPosition?: boolean) { + const {x, y} = this.getProps(['x', 'y'], useFinalPosition); + return {x, y}; + } + + size(options?: Partial) { + options = options || this.options || {}; + let radius = options.radius || 0; + radius = Math.max(radius, radius && options.hoverRadius || 0); + const borderWidth = radius && options.borderWidth || 0; + return (radius + borderWidth) * 2; + } + + draw(ctx: CanvasRenderingContext2D, area: ChartArea) { + const options = this.options; + + if (this.skip || options.radius < 0.1 || !_isPointInArea(this, area, this.size(options) / 2)) { + return; + } + + ctx.strokeStyle = options.borderColor; + ctx.lineWidth = options.borderWidth; + ctx.fillStyle = options.backgroundColor; + drawPoint(ctx, options, this.x, this.y); + } + + getRange() { + const options = this.options || {}; + // @ts-expect-error Fallbacks should never be hit in practice + return options.radius + options.hitRadius; + } +} diff --git a/src/elements/index.js b/src/elements/index.js new file mode 100644 index 00000000000..baa2a6dbd8d --- /dev/null +++ b/src/elements/index.js @@ -0,0 +1,4 @@ +export {default as ArcElement} from './element.arc.js'; +export {default as LineElement} from './element.line.js'; +export {default as PointElement} from './element.point.js'; +export {default as BarElement} from './element.bar.js'; diff --git a/src/helpers/helpers.canvas.ts b/src/helpers/helpers.canvas.ts new file mode 100644 index 00000000000..f37504c0097 --- /dev/null +++ b/src/helpers/helpers.canvas.ts @@ -0,0 +1,527 @@ +import type { + Chart, + Point, + FontSpec, + CanvasFontSpec, + PointStyle, + RenderTextOpts, + BackdropOptions +} from '../types/index.js'; +import type { + TRBL, + SplinePoint, + RoundedRect, + TRBLCorners +} from '../types/geometric.js'; +import {isArray, isNullOrUndef} from './helpers.core.js'; +import {PI, TAU, HALF_PI, QUARTER_PI, TWO_THIRDS_PI, RAD_PER_DEG} from './helpers.math.js'; + +/** + * Converts the given font object into a CSS font string. + * @param font - A font object. + * @return The CSS font string. See https://developer.mozilla.org/en-US/docs/Web/CSS/font + * @private + */ +export function toFontString(font: FontSpec) { + if (!font || isNullOrUndef(font.size) || isNullOrUndef(font.family)) { + return null; + } + + return (font.style ? font.style + ' ' : '') + + (font.weight ? font.weight + ' ' : '') + + font.size + 'px ' + + font.family; +} + +/** + * @private + */ +export function _measureText( + ctx: CanvasRenderingContext2D, + data: Record, + gc: string[], + longest: number, + string: string +) { + let textWidth = data[string]; + if (!textWidth) { + textWidth = data[string] = ctx.measureText(string).width; + gc.push(string); + } + if (textWidth > longest) { + longest = textWidth; + } + return longest; +} + +type Thing = string | undefined | null +type Things = (Thing | Thing[])[] + +/** + * @private + */ +// eslint-disable-next-line complexity +export function _longestText( + ctx: CanvasRenderingContext2D, + font: string, + arrayOfThings: Things, + cache?: {data?: Record, garbageCollect?: string[], font?: string} +) { + cache = cache || {}; + let data = cache.data = cache.data || {}; + let gc = cache.garbageCollect = cache.garbageCollect || []; + + if (cache.font !== font) { + data = cache.data = {}; + gc = cache.garbageCollect = []; + cache.font = font; + } + + ctx.save(); + + ctx.font = font; + let longest = 0; + const ilen = arrayOfThings.length; + let i: number, j: number, jlen: number, thing: Thing | Thing[], nestedThing: Thing | Thing[]; + for (i = 0; i < ilen; i++) { + thing = arrayOfThings[i]; + + // Undefined strings and arrays should not be measured + if (thing !== undefined && thing !== null && !isArray(thing)) { + longest = _measureText(ctx, data, gc, longest, thing); + } else if (isArray(thing)) { + // if it is an array lets measure each element + // to do maybe simplify this function a bit so we can do this more recursively? + for (j = 0, jlen = thing.length; j < jlen; j++) { + nestedThing = thing[j]; + // Undefined strings and arrays should not be measured + if (nestedThing !== undefined && nestedThing !== null && !isArray(nestedThing)) { + longest = _measureText(ctx, data, gc, longest, nestedThing); + } + } + } + } + + ctx.restore(); + + const gcLen = gc.length / 2; + if (gcLen > arrayOfThings.length) { + for (i = 0; i < gcLen; i++) { + delete data[gc[i]]; + } + gc.splice(0, gcLen); + } + return longest; +} + +/** + * Returns the aligned pixel value to avoid anti-aliasing blur + * @param chart - The chart instance. + * @param pixel - A pixel value. + * @param width - The width of the element. + * @returns The aligned pixel value. + * @private + */ +export function _alignPixel(chart: Chart, pixel: number, width: number) { + const devicePixelRatio = chart.currentDevicePixelRatio; + const halfWidth = width !== 0 ? Math.max(width / 2, 0.5) : 0; + return Math.round((pixel - halfWidth) * devicePixelRatio) / devicePixelRatio + halfWidth; +} + +/** + * Clears the entire canvas. + */ +export function clearCanvas(canvas?: HTMLCanvasElement, ctx?: CanvasRenderingContext2D) { + if (!ctx && !canvas) { + return; + } + + ctx = ctx || canvas.getContext('2d'); + + ctx.save(); + // canvas.width and canvas.height do not consider the canvas transform, + // while clearRect does + ctx.resetTransform(); + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.restore(); +} + +export interface DrawPointOptions { + pointStyle: PointStyle; + rotation?: number; + radius: number; + borderWidth: number; +} + +export function drawPoint( + ctx: CanvasRenderingContext2D, + options: DrawPointOptions, + x: number, + y: number +) { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + drawPointLegend(ctx, options, x, y, null); +} + +// eslint-disable-next-line complexity +export function drawPointLegend( + ctx: CanvasRenderingContext2D, + options: DrawPointOptions, + x: number, + y: number, + w: number +) { + let type: string, xOffset: number, yOffset: number, size: number, cornerRadius: number, width: number, xOffsetW: number, yOffsetW: number; + const style = options.pointStyle; + const rotation = options.rotation; + const radius = options.radius; + let rad = (rotation || 0) * RAD_PER_DEG; + + if (style && typeof style === 'object') { + type = style.toString(); + if (type === '[object HTMLImageElement]' || type === '[object HTMLCanvasElement]') { + ctx.save(); + ctx.translate(x, y); + ctx.rotate(rad); + ctx.drawImage(style, -style.width / 2, -style.height / 2, style.width, style.height); + ctx.restore(); + return; + } + } + + if (isNaN(radius) || radius <= 0) { + return; + } + + ctx.beginPath(); + + switch (style) { + // Default includes circle + default: + if (w) { + ctx.ellipse(x, y, w / 2, radius, 0, 0, TAU); + } else { + ctx.arc(x, y, radius, 0, TAU); + } + ctx.closePath(); + break; + case 'triangle': + width = w ? w / 2 : radius; + ctx.moveTo(x + Math.sin(rad) * width, y - Math.cos(rad) * radius); + rad += TWO_THIRDS_PI; + ctx.lineTo(x + Math.sin(rad) * width, y - Math.cos(rad) * radius); + rad += TWO_THIRDS_PI; + ctx.lineTo(x + Math.sin(rad) * width, y - Math.cos(rad) * radius); + ctx.closePath(); + break; + case 'rectRounded': + // NOTE: the rounded rect implementation changed to use `arc` instead of + // `quadraticCurveTo` since it generates better results when rect is + // almost a circle. 0.516 (instead of 0.5) produces results with visually + // closer proportion to the previous impl and it is inscribed in the + // circle with `radius`. For more details, see the following PRs: + // https://github.com/chartjs/Chart.js/issues/5597 + // https://github.com/chartjs/Chart.js/issues/5858 + cornerRadius = radius * 0.516; + size = radius - cornerRadius; + xOffset = Math.cos(rad + QUARTER_PI) * size; + xOffsetW = Math.cos(rad + QUARTER_PI) * (w ? w / 2 - cornerRadius : size); + yOffset = Math.sin(rad + QUARTER_PI) * size; + yOffsetW = Math.sin(rad + QUARTER_PI) * (w ? w / 2 - cornerRadius : size); + ctx.arc(x - xOffsetW, y - yOffset, cornerRadius, rad - PI, rad - HALF_PI); + ctx.arc(x + yOffsetW, y - xOffset, cornerRadius, rad - HALF_PI, rad); + ctx.arc(x + xOffsetW, y + yOffset, cornerRadius, rad, rad + HALF_PI); + ctx.arc(x - yOffsetW, y + xOffset, cornerRadius, rad + HALF_PI, rad + PI); + ctx.closePath(); + break; + case 'rect': + if (!rotation) { + size = Math.SQRT1_2 * radius; + width = w ? w / 2 : size; + ctx.rect(x - width, y - size, 2 * width, 2 * size); + break; + } + rad += QUARTER_PI; + /* falls through */ + case 'rectRot': + xOffsetW = Math.cos(rad) * (w ? w / 2 : radius); + xOffset = Math.cos(rad) * radius; + yOffset = Math.sin(rad) * radius; + yOffsetW = Math.sin(rad) * (w ? w / 2 : radius); + ctx.moveTo(x - xOffsetW, y - yOffset); + ctx.lineTo(x + yOffsetW, y - xOffset); + ctx.lineTo(x + xOffsetW, y + yOffset); + ctx.lineTo(x - yOffsetW, y + xOffset); + ctx.closePath(); + break; + case 'crossRot': + rad += QUARTER_PI; + /* falls through */ + case 'cross': + xOffsetW = Math.cos(rad) * (w ? w / 2 : radius); + xOffset = Math.cos(rad) * radius; + yOffset = Math.sin(rad) * radius; + yOffsetW = Math.sin(rad) * (w ? w / 2 : radius); + ctx.moveTo(x - xOffsetW, y - yOffset); + ctx.lineTo(x + xOffsetW, y + yOffset); + ctx.moveTo(x + yOffsetW, y - xOffset); + ctx.lineTo(x - yOffsetW, y + xOffset); + break; + case 'star': + xOffsetW = Math.cos(rad) * (w ? w / 2 : radius); + xOffset = Math.cos(rad) * radius; + yOffset = Math.sin(rad) * radius; + yOffsetW = Math.sin(rad) * (w ? w / 2 : radius); + ctx.moveTo(x - xOffsetW, y - yOffset); + ctx.lineTo(x + xOffsetW, y + yOffset); + ctx.moveTo(x + yOffsetW, y - xOffset); + ctx.lineTo(x - yOffsetW, y + xOffset); + rad += QUARTER_PI; + xOffsetW = Math.cos(rad) * (w ? w / 2 : radius); + xOffset = Math.cos(rad) * radius; + yOffset = Math.sin(rad) * radius; + yOffsetW = Math.sin(rad) * (w ? w / 2 : radius); + ctx.moveTo(x - xOffsetW, y - yOffset); + ctx.lineTo(x + xOffsetW, y + yOffset); + ctx.moveTo(x + yOffsetW, y - xOffset); + ctx.lineTo(x - yOffsetW, y + xOffset); + break; + case 'line': + xOffset = w ? w / 2 : Math.cos(rad) * radius; + yOffset = Math.sin(rad) * radius; + ctx.moveTo(x - xOffset, y - yOffset); + ctx.lineTo(x + xOffset, y + yOffset); + break; + case 'dash': + ctx.moveTo(x, y); + ctx.lineTo(x + Math.cos(rad) * (w ? w / 2 : radius), y + Math.sin(rad) * radius); + break; + case false: + ctx.closePath(); + break; + } + + ctx.fill(); + if (options.borderWidth > 0) { + ctx.stroke(); + } +} + +/** + * Returns true if the point is inside the rectangle + * @param point - The point to test + * @param area - The rectangle + * @param margin - allowed margin + * @private + */ +export function _isPointInArea( + point: Point, + area: TRBL, + margin?: number +) { + margin = margin || 0.5; // margin - default is to match rounded decimals + + return !area || (point && point.x > area.left - margin && point.x < area.right + margin && + point.y > area.top - margin && point.y < area.bottom + margin); +} + +export function clipArea(ctx: CanvasRenderingContext2D, area: TRBL) { + ctx.save(); + ctx.beginPath(); + ctx.rect(area.left, area.top, area.right - area.left, area.bottom - area.top); + ctx.clip(); +} + +export function unclipArea(ctx: CanvasRenderingContext2D) { + ctx.restore(); +} + +/** + * @private + */ +export function _steppedLineTo( + ctx: CanvasRenderingContext2D, + previous: Point, + target: Point, + flip?: boolean, + mode?: string +) { + if (!previous) { + return ctx.lineTo(target.x, target.y); + } + if (mode === 'middle') { + const midpoint = (previous.x + target.x) / 2.0; + ctx.lineTo(midpoint, previous.y); + ctx.lineTo(midpoint, target.y); + } else if (mode === 'after' !== !!flip) { + ctx.lineTo(previous.x, target.y); + } else { + ctx.lineTo(target.x, previous.y); + } + ctx.lineTo(target.x, target.y); +} + +/** + * @private + */ +export function _bezierCurveTo( + ctx: CanvasRenderingContext2D, + previous: SplinePoint, + target: SplinePoint, + flip?: boolean +) { + if (!previous) { + return ctx.lineTo(target.x, target.y); + } + ctx.bezierCurveTo( + flip ? previous.cp1x : previous.cp2x, + flip ? previous.cp1y : previous.cp2y, + flip ? target.cp2x : target.cp1x, + flip ? target.cp2y : target.cp1y, + target.x, + target.y); +} + +function setRenderOpts(ctx: CanvasRenderingContext2D, opts: RenderTextOpts) { + if (opts.translation) { + ctx.translate(opts.translation[0], opts.translation[1]); + } + + if (!isNullOrUndef(opts.rotation)) { + ctx.rotate(opts.rotation); + } + + if (opts.color) { + ctx.fillStyle = opts.color; + } + + if (opts.textAlign) { + ctx.textAlign = opts.textAlign; + } + + if (opts.textBaseline) { + ctx.textBaseline = opts.textBaseline; + } +} + +function decorateText( + ctx: CanvasRenderingContext2D, + x: number, + y: number, + line: string, + opts: RenderTextOpts +) { + if (opts.strikethrough || opts.underline) { + /** + * Now that IE11 support has been dropped, we can use more + * of the TextMetrics object. The actual bounding boxes + * are unflagged in Chrome, Firefox, Edge, and Safari so they + * can be safely used. + * See https://developer.mozilla.org/en-US/docs/Web/API/TextMetrics#Browser_compatibility + */ + const metrics = ctx.measureText(line); + const left = x - metrics.actualBoundingBoxLeft; + const right = x + metrics.actualBoundingBoxRight; + const top = y - metrics.actualBoundingBoxAscent; + const bottom = y + metrics.actualBoundingBoxDescent; + const yDecoration = opts.strikethrough ? (top + bottom) / 2 : bottom; + + ctx.strokeStyle = ctx.fillStyle; + ctx.beginPath(); + ctx.lineWidth = opts.decorationWidth || 2; + ctx.moveTo(left, yDecoration); + ctx.lineTo(right, yDecoration); + ctx.stroke(); + } +} + +function drawBackdrop(ctx: CanvasRenderingContext2D, opts: BackdropOptions) { + const oldColor = ctx.fillStyle; + + ctx.fillStyle = opts.color as string; + ctx.fillRect(opts.left, opts.top, opts.width, opts.height); + ctx.fillStyle = oldColor; +} + +/** + * Render text onto the canvas + */ +export function renderText( + ctx: CanvasRenderingContext2D, + text: string | string[], + x: number, + y: number, + font: CanvasFontSpec, + opts: RenderTextOpts = {} +) { + const lines = isArray(text) ? text : [text]; + const stroke = opts.strokeWidth > 0 && opts.strokeColor !== ''; + let i: number, line: string; + + ctx.save(); + ctx.font = font.string; + setRenderOpts(ctx, opts); + + for (i = 0; i < lines.length; ++i) { + line = lines[i]; + + if (opts.backdrop) { + drawBackdrop(ctx, opts.backdrop); + } + + if (stroke) { + if (opts.strokeColor) { + ctx.strokeStyle = opts.strokeColor; + } + + if (!isNullOrUndef(opts.strokeWidth)) { + ctx.lineWidth = opts.strokeWidth; + } + + ctx.strokeText(line, x, y, opts.maxWidth); + } + + ctx.fillText(line, x, y, opts.maxWidth); + decorateText(ctx, x, y, line, opts); + + y += Number(font.lineHeight); + } + + ctx.restore(); +} + +/** + * Add a path of a rectangle with rounded corners to the current sub-path + * @param ctx - Context + * @param rect - Bounding rect + */ +export function addRoundedRectPath( + ctx: CanvasRenderingContext2D, + rect: RoundedRect & { radius: TRBLCorners } +) { + const {x, y, w, h, radius} = rect; + + // top left arc + ctx.arc(x + radius.topLeft, y + radius.topLeft, radius.topLeft, 1.5 * PI, PI, true); + + // line from top left to bottom left + ctx.lineTo(x, y + h - radius.bottomLeft); + + // bottom left arc + ctx.arc(x + radius.bottomLeft, y + h - radius.bottomLeft, radius.bottomLeft, PI, HALF_PI, true); + + // line from bottom left to bottom right + ctx.lineTo(x + w - radius.bottomRight, y + h); + + // bottom right arc + ctx.arc(x + w - radius.bottomRight, y + h - radius.bottomRight, radius.bottomRight, HALF_PI, 0, true); + + // line from bottom right to top right + ctx.lineTo(x + w, y + radius.topRight); + + // top right arc + ctx.arc(x + w - radius.topRight, y + radius.topRight, radius.topRight, 0, -HALF_PI, true); + + // line from top right to top left + ctx.lineTo(x + radius.topLeft, y); +} diff --git a/src/helpers/helpers.collection.ts b/src/helpers/helpers.collection.ts new file mode 100644 index 00000000000..458b79b3a2b --- /dev/null +++ b/src/helpers/helpers.collection.ts @@ -0,0 +1,192 @@ +import {_capitalize} from './helpers.core.js'; + +/** + * Binary search + * @param table - the table search. must be sorted! + * @param value - value to find + * @param cmp + * @private + */ +export function _lookup( + table: number[], + value: number, + cmp?: (value: number) => boolean +): {lo: number, hi: number}; +export function _lookup( + table: T[], + value: number, + cmp: (value: number) => boolean +): {lo: number, hi: number}; +export function _lookup( + table: unknown[], + value: number, + cmp?: (value: number) => boolean +) { + cmp = cmp || ((index) => table[index] < value); + let hi = table.length - 1; + let lo = 0; + let mid: number; + + while (hi - lo > 1) { + mid = (lo + hi) >> 1; + if (cmp(mid)) { + lo = mid; + } else { + hi = mid; + } + } + + return {lo, hi}; +} + +/** + * Binary search + * @param table - the table search. must be sorted! + * @param key - property name for the value in each entry + * @param value - value to find + * @param last - lookup last index + * @private + */ +export const _lookupByKey = ( + table: Record[], + key: string, + value: number, + last?: boolean +) => + _lookup(table, value, last + ? index => { + const ti = table[index][key]; + return ti < value || ti === value && table[index + 1][key] === value; + } + : index => table[index][key] < value); + +/** + * Reverse binary search + * @param table - the table search. must be sorted! + * @param key - property name for the value in each entry + * @param value - value to find + * @private + */ +export const _rlookupByKey = ( + table: Record[], + key: string, + value: number +) => + _lookup(table, value, index => table[index][key] >= value); + +/** + * Return subset of `values` between `min` and `max` inclusive. + * Values are assumed to be in sorted order. + * @param values - sorted array of values + * @param min - min value + * @param max - max value + */ +export function _filterBetween(values: number[], min: number, max: number) { + let start = 0; + let end = values.length; + + while (start < end && values[start] < min) { + start++; + } + while (end > start && values[end - 1] > max) { + end--; + } + + return start > 0 || end < values.length + ? values.slice(start, end) + : values; +} + +const arrayEvents = ['push', 'pop', 'shift', 'splice', 'unshift'] as const; + +export interface ArrayListener { + _onDataPush?(...item: T[]): void; + _onDataPop?(): void; + _onDataShift?(): void; + _onDataSplice?(index: number, deleteCount: number, ...items: T[]): void; + _onDataUnshift?(...item: T[]): void; +} + +/** + * Hooks the array methods that add or remove values ('push', pop', 'shift', 'splice', + * 'unshift') and notify the listener AFTER the array has been altered. Listeners are + * called on the '_onData*' callbacks (e.g. _onDataPush, etc.) with same arguments. + */ +export function listenArrayEvents(array: T[], listener: ArrayListener): void; +export function listenArrayEvents(array, listener) { + if (array._chartjs) { + array._chartjs.listeners.push(listener); + return; + } + + Object.defineProperty(array, '_chartjs', { + configurable: true, + enumerable: false, + value: { + listeners: [listener] + } + }); + + arrayEvents.forEach((key) => { + const method = '_onData' + _capitalize(key); + const base = array[key]; + + Object.defineProperty(array, key, { + configurable: true, + enumerable: false, + value(...args) { + const res = base.apply(this, args); + + array._chartjs.listeners.forEach((object) => { + if (typeof object[method] === 'function') { + object[method](...args); + } + }); + + return res; + } + }); + }); +} + + +/** + * Removes the given array event listener and cleanup extra attached properties (such as + * the _chartjs stub and overridden methods) if array doesn't have any more listeners. + */ +export function unlistenArrayEvents(array: T[], listener: ArrayListener): void; +export function unlistenArrayEvents(array, listener) { + const stub = array._chartjs; + if (!stub) { + return; + } + + const listeners = stub.listeners; + const index = listeners.indexOf(listener); + if (index !== -1) { + listeners.splice(index, 1); + } + + if (listeners.length > 0) { + return; + } + + arrayEvents.forEach((key) => { + delete array[key]; + }); + + delete array._chartjs; +} + +/** + * @param items + */ +export function _arrayUnique(items: T[]) { + const set = new Set(items); + + if (set.size === items.length) { + return items; + } + + return Array.from(set); +} diff --git a/src/helpers/helpers.color.ts b/src/helpers/helpers.color.ts new file mode 100644 index 00000000000..0ee3ef6b40e --- /dev/null +++ b/src/helpers/helpers.color.ts @@ -0,0 +1,32 @@ +import {Color} from '@kurkle/color'; + +export function isPatternOrGradient(value: unknown): value is CanvasPattern | CanvasGradient { + if (value && typeof value === 'object') { + const type = value.toString(); + return type === '[object CanvasPattern]' || type === '[object CanvasGradient]'; + } + + return false; +} + +export function color(value: CanvasGradient): CanvasGradient; +export function color(value: CanvasPattern): CanvasPattern; +export function color( + value: + | string + | { r: number; g: number; b: number; a: number } + | [number, number, number] + | [number, number, number, number] +): Color; +export function color(value) { + return isPatternOrGradient(value) ? value : new Color(value); +} + +export function getHoverColor(value: CanvasGradient): CanvasGradient; +export function getHoverColor(value: CanvasPattern): CanvasPattern; +export function getHoverColor(value: string): string; +export function getHoverColor(value) { + return isPatternOrGradient(value) + ? value + : new Color(value).saturate(0.5).darken(0.1).hexString(); +} diff --git a/src/helpers/helpers.config.ts b/src/helpers/helpers.config.ts new file mode 100644 index 00000000000..3b1dd5b695a --- /dev/null +++ b/src/helpers/helpers.config.ts @@ -0,0 +1,456 @@ +/* eslint-disable @typescript-eslint/no-use-before-define */ +import type {AnyObject} from '../types/basic.js'; +import type {ChartMeta} from '../types/index.js'; +import type { + ResolverObjectKey, + ResolverCache, + ResolverProxy, + DescriptorDefaults, + Descriptor, + ContextCache, + ContextProxy +} from './helpers.config.types.js'; +import {isArray, isFunction, isObject, resolveObjectKey, _capitalize} from './helpers.core.js'; + +export * from './helpers.config.types.js'; + +/** + * Creates a Proxy for resolving raw values for options. + * @param scopes - The option scopes to look for values, in resolution order + * @param prefixes - The prefixes for values, in resolution order. + * @param rootScopes - The root option scopes + * @param fallback - Parent scopes fallback + * @param getTarget - callback for getting the target for changed values + * @returns Proxy + * @private + */ +export function _createResolver< + T extends AnyObject[] = AnyObject[], + R extends AnyObject[] = T +>( + scopes: T, + prefixes = [''], + rootScopes?: R, + fallback?: ResolverObjectKey, + getTarget = () => scopes[0] +) { + const finalRootScopes = rootScopes || scopes; + if (typeof fallback === 'undefined') { + fallback = _resolve('_fallback', scopes); + } + const cache: ResolverCache = { + [Symbol.toStringTag]: 'Object', + _cacheable: true, + _scopes: scopes, + _rootScopes: finalRootScopes, + _fallback: fallback, + _getTarget: getTarget, + override: (scope: AnyObject) => _createResolver([scope, ...scopes], prefixes, finalRootScopes, fallback), + }; + return new Proxy(cache, { + /** + * A trap for the delete operator. + */ + deleteProperty(target, prop: string) { + delete target[prop]; // remove from cache + delete target._keys; // remove cached keys + delete scopes[0][prop]; // remove from top level scope + return true; + }, + + /** + * A trap for getting property values. + */ + get(target, prop: string) { + return _cached(target, prop, + () => _resolveWithPrefixes(prop, prefixes, scopes, target)); + }, + + /** + * A trap for Object.getOwnPropertyDescriptor. + * Also used by Object.hasOwnProperty. + */ + getOwnPropertyDescriptor(target, prop) { + return Reflect.getOwnPropertyDescriptor(target._scopes[0], prop); + }, + + /** + * A trap for Object.getPrototypeOf. + */ + getPrototypeOf() { + return Reflect.getPrototypeOf(scopes[0]); + }, + + /** + * A trap for the in operator. + */ + has(target, prop: string) { + return getKeysFromAllScopes(target).includes(prop); + }, + + /** + * A trap for Object.getOwnPropertyNames and Object.getOwnPropertySymbols. + */ + ownKeys(target) { + return getKeysFromAllScopes(target); + }, + + /** + * A trap for setting property values. + */ + set(target, prop: string, value) { + const storage = target._storage || (target._storage = getTarget()); + target[prop] = storage[prop] = value; // set to top level scope + cache + delete target._keys; // remove cached keys + return true; + } + }) as ResolverProxy; +} + +/** + * Returns an Proxy for resolving option values with context. + * @param proxy - The Proxy returned by `_createResolver` + * @param context - Context object for scriptable/indexable options + * @param subProxy - The proxy provided for scriptable options + * @param descriptorDefaults - Defaults for descriptors + * @private + */ +export function _attachContext< + T extends AnyObject[] = AnyObject[], + R extends AnyObject[] = T +>( + proxy: ResolverProxy, + context: AnyObject, + subProxy?: ResolverProxy, + descriptorDefaults?: DescriptorDefaults +) { + const cache: ContextCache = { + _cacheable: false, + _proxy: proxy, + _context: context, + _subProxy: subProxy, + _stack: new Set(), + _descriptors: _descriptors(proxy, descriptorDefaults), + setContext: (ctx: AnyObject) => _attachContext(proxy, ctx, subProxy, descriptorDefaults), + override: (scope: AnyObject) => _attachContext(proxy.override(scope), context, subProxy, descriptorDefaults) + }; + return new Proxy(cache, { + /** + * A trap for the delete operator. + */ + deleteProperty(target, prop) { + delete target[prop]; // remove from cache + delete proxy[prop]; // remove from proxy + return true; + }, + + /** + * A trap for getting property values. + */ + get(target, prop: string, receiver) { + return _cached(target, prop, + () => _resolveWithContext(target, prop, receiver)); + }, + + /** + * A trap for Object.getOwnPropertyDescriptor. + * Also used by Object.hasOwnProperty. + */ + getOwnPropertyDescriptor(target, prop) { + return target._descriptors.allKeys + ? Reflect.has(proxy, prop) ? {enumerable: true, configurable: true} : undefined + : Reflect.getOwnPropertyDescriptor(proxy, prop); + }, + + /** + * A trap for Object.getPrototypeOf. + */ + getPrototypeOf() { + return Reflect.getPrototypeOf(proxy); + }, + + /** + * A trap for the in operator. + */ + has(target, prop) { + return Reflect.has(proxy, prop); + }, + + /** + * A trap for Object.getOwnPropertyNames and Object.getOwnPropertySymbols. + */ + ownKeys() { + return Reflect.ownKeys(proxy); + }, + + /** + * A trap for setting property values. + */ + set(target, prop, value) { + proxy[prop] = value; // set to proxy + delete target[prop]; // remove from cache + return true; + } + }) as ContextProxy; +} + +/** + * @private + */ +export function _descriptors( + proxy: ResolverCache, + defaults: DescriptorDefaults = {scriptable: true, indexable: true} +): Descriptor { + const {_scriptable = defaults.scriptable, _indexable = defaults.indexable, _allKeys = defaults.allKeys} = proxy; + return { + allKeys: _allKeys, + scriptable: _scriptable, + indexable: _indexable, + isScriptable: isFunction(_scriptable) ? _scriptable : () => _scriptable, + isIndexable: isFunction(_indexable) ? _indexable : () => _indexable + }; +} + +const readKey = (prefix: string, name: string) => prefix ? prefix + _capitalize(name) : name; +const needsSubResolver = (prop: string, value: unknown) => isObject(value) && prop !== 'adapters' && + (Object.getPrototypeOf(value) === null || value.constructor === Object); + +function _cached( + target: AnyObject, + prop: string, + resolve: () => unknown +) { + if (Object.prototype.hasOwnProperty.call(target, prop) || prop === 'constructor') { + return target[prop]; + } + + const value = resolve(); + // cache the resolved value + target[prop] = value; + return value; +} + +function _resolveWithContext( + target: ContextCache, + prop: string, + receiver: AnyObject +) { + const {_proxy, _context, _subProxy, _descriptors: descriptors} = target; + let value = _proxy[prop]; // resolve from proxy + + // resolve with context + if (isFunction(value) && descriptors.isScriptable(prop)) { + value = _resolveScriptable(prop, value, target, receiver); + } + if (isArray(value) && value.length) { + value = _resolveArray(prop, value, target, descriptors.isIndexable); + } + if (needsSubResolver(prop, value)) { + // if the resolved value is an object, create a sub resolver for it + value = _attachContext(value, _context, _subProxy && _subProxy[prop], descriptors); + } + return value; +} + +function _resolveScriptable( + prop: string, + getValue: (ctx: AnyObject, sub: AnyObject) => unknown, + target: ContextCache, + receiver: AnyObject +) { + const {_proxy, _context, _subProxy, _stack} = target; + if (_stack.has(prop)) { + throw new Error('Recursion detected: ' + Array.from(_stack).join('->') + '->' + prop); + } + _stack.add(prop); + let value = getValue(_context, _subProxy || receiver); + _stack.delete(prop); + if (needsSubResolver(prop, value)) { + // When scriptable option returns an object, create a resolver on that. + value = createSubResolver(_proxy._scopes, _proxy, prop, value); + } + return value; +} + +function _resolveArray( + prop: string, + value: unknown[], + target: ContextCache, + isIndexable: (key: string) => boolean +) { + const {_proxy, _context, _subProxy, _descriptors: descriptors} = target; + + if (typeof _context.index !== 'undefined' && isIndexable(prop)) { + return value[_context.index % value.length]; + } else if (isObject(value[0])) { + // Array of objects, return array or resolvers + const arr = value; + const scopes = _proxy._scopes.filter(s => s !== arr); + value = []; + for (const item of arr) { + const resolver = createSubResolver(scopes, _proxy, prop, item); + value.push(_attachContext(resolver, _context, _subProxy && _subProxy[prop], descriptors)); + } + } + return value; +} + +function resolveFallback( + fallback: ResolverObjectKey | ((prop: ResolverObjectKey, value: unknown) => ResolverObjectKey), + prop: ResolverObjectKey, + value: unknown +) { + return isFunction(fallback) ? fallback(prop, value) : fallback; +} + +const getScope = (key: ResolverObjectKey, parent: AnyObject) => key === true ? parent + : typeof key === 'string' ? resolveObjectKey(parent, key) : undefined; + +function addScopes( + set: Set, + parentScopes: AnyObject[], + key: ResolverObjectKey, + parentFallback: ResolverObjectKey, + value: unknown +) { + for (const parent of parentScopes) { + const scope = getScope(key, parent); + if (scope) { + set.add(scope); + const fallback = resolveFallback(scope._fallback, key, value); + if (typeof fallback !== 'undefined' && fallback !== key && fallback !== parentFallback) { + // When we reach the descriptor that defines a new _fallback, return that. + // The fallback will resume to that new scope. + return fallback; + } + } else if (scope === false && typeof parentFallback !== 'undefined' && key !== parentFallback) { + // Fallback to `false` results to `false`, when falling back to different key. + // For example `interaction` from `hover` or `plugins.tooltip` and `animation` from `animations` + return null; + } + } + return false; +} + +function createSubResolver( + parentScopes: AnyObject[], + resolver: ResolverCache, + prop: ResolverObjectKey, + value: unknown +) { + const rootScopes = resolver._rootScopes; + const fallback = resolveFallback(resolver._fallback, prop, value); + const allScopes = [...parentScopes, ...rootScopes]; + const set = new Set(); + set.add(value); + let key = addScopesFromKey(set, allScopes, prop, fallback || prop, value); + if (key === null) { + return false; + } + if (typeof fallback !== 'undefined' && fallback !== prop) { + key = addScopesFromKey(set, allScopes, fallback, key, value); + if (key === null) { + return false; + } + } + return _createResolver(Array.from(set), [''], rootScopes, fallback, + () => subGetTarget(resolver, prop as string, value)); +} + +function addScopesFromKey( + set: Set, + allScopes: AnyObject[], + key: ResolverObjectKey, + fallback: ResolverObjectKey, + item: unknown +) { + while (key) { + key = addScopes(set, allScopes, key, fallback, item); + } + return key; +} + +function subGetTarget( + resolver: ResolverCache, + prop: string, + value: unknown +) { + const parent = resolver._getTarget(); + if (!(prop in parent)) { + parent[prop] = {}; + } + const target = parent[prop]; + if (isArray(target) && isObject(value)) { + // For array of objects, the object is used to store updated values + return value; + } + return target || {}; +} + +function _resolveWithPrefixes( + prop: string, + prefixes: string[], + scopes: AnyObject[], + proxy: ResolverProxy +) { + let value: unknown; + for (const prefix of prefixes) { + value = _resolve(readKey(prefix, prop), scopes); + if (typeof value !== 'undefined') { + return needsSubResolver(prop, value) + ? createSubResolver(scopes, proxy, prop, value) + : value; + } + } +} + +function _resolve(key: string, scopes: AnyObject[]) { + for (const scope of scopes) { + if (!scope) { + continue; + } + const value = scope[key]; + if (typeof value !== 'undefined') { + return value; + } + } +} + +function getKeysFromAllScopes(target: ResolverCache) { + let keys = target._keys; + if (!keys) { + keys = target._keys = resolveKeysFromAllScopes(target._scopes); + } + return keys; +} + +function resolveKeysFromAllScopes(scopes: AnyObject[]) { + const set = new Set(); + for (const scope of scopes) { + for (const key of Object.keys(scope).filter(k => !k.startsWith('_'))) { + set.add(key); + } + } + return Array.from(set); +} + +export function _parseObjectDataRadialScale( + meta: ChartMeta<'line' | 'scatter'>, + data: AnyObject[], + start: number, + count: number +) { + const {iScale} = meta; + const {key = 'r'} = this._parsing; + const parsed = new Array<{r: unknown}>(count); + let i: number, ilen: number, index: number, item: AnyObject; + + for (i = 0, ilen = count; i < ilen; ++i) { + index = i + start; + item = data[index]; + parsed[i] = { + r: iScale.parse(resolveObjectKey(item, key), index) + }; + } + return parsed; +} diff --git a/src/helpers/helpers.config.types.ts b/src/helpers/helpers.config.types.ts new file mode 100644 index 00000000000..64f0e329b54 --- /dev/null +++ b/src/helpers/helpers.config.types.ts @@ -0,0 +1,60 @@ +import type {AnyObject} from '../types/basic.js'; +import type {Merge} from '../types/utils.js'; + +export type ResolverObjectKey = string | boolean; + +export interface ResolverCache< + T extends AnyObject[] = AnyObject[], + R extends AnyObject[] = T +> { + [Symbol.toStringTag]: 'Object'; + _cacheable: boolean; + _scopes: T; + _rootScopes: T | R; + _fallback: ResolverObjectKey; + _keys?: string[]; + _scriptable?: boolean; + _indexable?: boolean; + _allKeys?: boolean; + _storage?: T[number]; + _getTarget(): T[number]; + override(scope: S): ResolverProxy<(T[number] | S)[], T | R> +} + +export type ResolverProxy< + T extends AnyObject[] = AnyObject[], + R extends AnyObject[] = T +> = Merge & ResolverCache + +export interface DescriptorDefaults { + scriptable: boolean; + indexable: boolean; + allKeys?: boolean +} + +export interface Descriptor { + allKeys: boolean; + scriptable: boolean; + indexable: boolean; + isScriptable(key: string): boolean; + isIndexable(key: string): boolean; +} + +export interface ContextCache< + T extends AnyObject[] = AnyObject[], + R extends AnyObject[] = T +> { + _cacheable: boolean; + _proxy: ResolverProxy; + _context: AnyObject; + _subProxy: ResolverProxy; + _stack: Set; + _descriptors: Descriptor + setContext(ctx: AnyObject): ContextProxy + override(scope: S): ContextProxy<(T[number] | S)[], T | R> +} + +export type ContextProxy< + T extends AnyObject[] = AnyObject[], + R extends AnyObject[] = T +> = Merge & ContextCache; diff --git a/src/helpers/helpers.core.ts b/src/helpers/helpers.core.ts new file mode 100644 index 00000000000..7203a92c2ce --- /dev/null +++ b/src/helpers/helpers.core.ts @@ -0,0 +1,416 @@ +/** + * @namespace Chart.helpers + */ + +import type {AnyObject} from '../types/basic.js'; +import type {ActiveDataPoint, ChartEvent} from '../types/index.js'; + +/** + * An empty function that can be used, for example, for optional callback. + */ +export function noop() { + /* noop */ +} + +/** + * Returns a unique id, sequentially generated from a global variable. + */ +export const uid = (() => { + let id = 0; + return () => id++; +})(); + +/** + * Returns true if `value` is neither null nor undefined, else returns false. + * @param value - The value to test. + * @since 2.7.0 + */ +export function isNullOrUndef(value: unknown): value is null | undefined { + return value === null || value === undefined; +} + +/** + * Returns true if `value` is an array (including typed arrays), else returns false. + * @param value - The value to test. + * @function + */ +export function isArray(value: unknown): value is T[] { + if (Array.isArray && Array.isArray(value)) { + return true; + } + const type = Object.prototype.toString.call(value); + if (type.slice(0, 7) === '[object' && type.slice(-6) === 'Array]') { + return true; + } + return false; +} + +/** + * Returns true if `value` is an object (excluding null), else returns false. + * @param value - The value to test. + * @since 2.7.0 + */ +export function isObject(value: unknown): value is AnyObject { + return value !== null && Object.prototype.toString.call(value) === '[object Object]'; +} + +/** + * Returns true if `value` is a finite number, else returns false + * @param value - The value to test. + */ +function isNumberFinite(value: unknown): value is number { + return (typeof value === 'number' || value instanceof Number) && isFinite(+value); +} +export { + isNumberFinite as isFinite, +}; + +/** + * Returns `value` if finite, else returns `defaultValue`. + * @param value - The value to return if defined. + * @param defaultValue - The value to return if `value` is not finite. + */ +export function finiteOrDefault(value: unknown, defaultValue: number) { + return isNumberFinite(value) ? value : defaultValue; +} + +/** + * Returns `value` if defined, else returns `defaultValue`. + * @param value - The value to return if defined. + * @param defaultValue - The value to return if `value` is undefined. + */ +export function valueOrDefault(value: T | undefined, defaultValue: T) { + return typeof value === 'undefined' ? defaultValue : value; +} + +export const toPercentage = (value: number | string, dimension: number) => + typeof value === 'string' && value.endsWith('%') ? + parseFloat(value) / 100 + : +value / dimension; + +export const toDimension = (value: number | string, dimension: number) => + typeof value === 'string' && value.endsWith('%') ? + parseFloat(value) / 100 * dimension + : +value; + +/** + * Calls `fn` with the given `args` in the scope defined by `thisArg` and returns the + * value returned by `fn`. If `fn` is not a function, this method returns undefined. + * @param fn - The function to call. + * @param args - The arguments with which `fn` should be called. + * @param [thisArg] - The value of `this` provided for the call to `fn`. + */ +export function callback R, TA, R>( + fn: T | undefined, + args: unknown[], + thisArg?: TA +): R | undefined { + if (fn && typeof fn.call === 'function') { + return fn.apply(thisArg, args); + } +} + +/** + * Note(SB) for performance sake, this method should only be used when loopable type + * is unknown or in none intensive code (not called often and small loopable). Else + * it's preferable to use a regular for() loop and save extra function calls. + * @param loopable - The object or array to be iterated. + * @param fn - The function to call for each item. + * @param [thisArg] - The value of `this` provided for the call to `fn`. + * @param [reverse] - If true, iterates backward on the loopable. + */ +export function each( + loopable: Record, + fn: (this: TA, v: T, i: string) => void, + thisArg?: TA, + reverse?: boolean +): void; +export function each( + loopable: T[], + fn: (this: TA, v: T, i: number) => void, + thisArg?: TA, + reverse?: boolean +): void; +export function each( + loopable: T[] | Record, + fn: (this: TA, v: T, i: any) => void, + thisArg?: TA, + reverse?: boolean +) { + let i: number, len: number, keys: string[]; + if (isArray(loopable)) { + len = loopable.length; + if (reverse) { + for (i = len - 1; i >= 0; i--) { + fn.call(thisArg, loopable[i], i); + } + } else { + for (i = 0; i < len; i++) { + fn.call(thisArg, loopable[i], i); + } + } + } else if (isObject(loopable)) { + keys = Object.keys(loopable); + len = keys.length; + for (i = 0; i < len; i++) { + fn.call(thisArg, loopable[keys[i]], keys[i]); + } + } +} + +/** + * Returns true if the `a0` and `a1` arrays have the same content, else returns false. + * @param a0 - The array to compare + * @param a1 - The array to compare + * @private + */ +export function _elementsEqual(a0: ActiveDataPoint[], a1: ActiveDataPoint[]) { + let i: number, ilen: number, v0: ActiveDataPoint, v1: ActiveDataPoint; + + if (!a0 || !a1 || a0.length !== a1.length) { + return false; + } + + for (i = 0, ilen = a0.length; i < ilen; ++i) { + v0 = a0[i]; + v1 = a1[i]; + + if (v0.datasetIndex !== v1.datasetIndex || v0.index !== v1.index) { + return false; + } + } + + return true; +} + +/** + * Returns a deep copy of `source` without keeping references on objects and arrays. + * @param source - The value to clone. + */ +export function clone(source: T): T { + if (isArray(source)) { + return source.map(clone) as unknown as T; + } + + if (isObject(source)) { + const target = Object.create(null); + const keys = Object.keys(source); + const klen = keys.length; + let k = 0; + + for (; k < klen; ++k) { + target[keys[k]] = clone(source[keys[k]]); + } + + return target; + } + + return source; +} + +function isValidKey(key: string) { + return ['__proto__', 'prototype', 'constructor'].indexOf(key) === -1; +} + +/** + * The default merger when Chart.helpers.merge is called without merger option. + * Note(SB): also used by mergeConfig and mergeScaleConfig as fallback. + * @private + */ +export function _merger(key: string, target: AnyObject, source: AnyObject, options: AnyObject) { + if (!isValidKey(key)) { + return; + } + + const tval = target[key]; + const sval = source[key]; + + if (isObject(tval) && isObject(sval)) { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + merge(tval, sval, options); + } else { + target[key] = clone(sval); + } +} + +export interface MergeOptions { + merger?: (key: string, target: AnyObject, source: AnyObject, options?: AnyObject) => void; +} + +/** + * Recursively deep copies `source` properties into `target` with the given `options`. + * IMPORTANT: `target` is not cloned and will be updated with `source` properties. + * @param target - The target object in which all sources are merged into. + * @param source - Object(s) to merge into `target`. + * @param [options] - Merging options: + * @param [options.merger] - The merge method (key, target, source, options) + * @returns The `target` object. + */ +export function merge(target: T, source: [], options?: MergeOptions): T; +export function merge(target: T, source: S1, options?: MergeOptions): T & S1; +export function merge(target: T, source: [S1], options?: MergeOptions): T & S1; +export function merge(target: T, source: [S1, S2], options?: MergeOptions): T & S1 & S2; +export function merge(target: T, source: [S1, S2, S3], options?: MergeOptions): T & S1 & S2 & S3; +export function merge( + target: T, + source: [S1, S2, S3, S4], + options?: MergeOptions +): T & S1 & S2 & S3 & S4; +export function merge(target: T, source: AnyObject[], options?: MergeOptions): AnyObject; +export function merge(target: T, source: AnyObject[], options?: MergeOptions): AnyObject { + const sources = isArray(source) ? source : [source]; + const ilen = sources.length; + + if (!isObject(target)) { + return target as AnyObject; + } + + options = options || {}; + const merger = options.merger || _merger; + let current: AnyObject; + + for (let i = 0; i < ilen; ++i) { + current = sources[i]; + if (!isObject(current)) { + continue; + } + + const keys = Object.keys(current); + for (let k = 0, klen = keys.length; k < klen; ++k) { + merger(keys[k], target, current, options as AnyObject); + } + } + + return target; +} + +/** + * Recursively deep copies `source` properties into `target` *only* if not defined in target. + * IMPORTANT: `target` is not cloned and will be updated with `source` properties. + * @param target - The target object in which all sources are merged into. + * @param source - Object(s) to merge into `target`. + * @returns The `target` object. + */ +export function mergeIf(target: T, source: []): T; +export function mergeIf(target: T, source: S1): T & S1; +export function mergeIf(target: T, source: [S1]): T & S1; +export function mergeIf(target: T, source: [S1, S2]): T & S1 & S2; +export function mergeIf(target: T, source: [S1, S2, S3]): T & S1 & S2 & S3; +export function mergeIf(target: T, source: [S1, S2, S3, S4]): T & S1 & S2 & S3 & S4; +export function mergeIf(target: T, source: AnyObject[]): AnyObject; +export function mergeIf(target: T, source: AnyObject[]): AnyObject { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + return merge(target, source, {merger: _mergerIf}); +} + +/** + * Merges source[key] in target[key] only if target[key] is undefined. + * @private + */ +export function _mergerIf(key: string, target: AnyObject, source: AnyObject) { + if (!isValidKey(key)) { + return; + } + + const tval = target[key]; + const sval = source[key]; + + if (isObject(tval) && isObject(sval)) { + mergeIf(tval, sval); + } else if (!Object.prototype.hasOwnProperty.call(target, key)) { + target[key] = clone(sval); + } +} + +/** + * @private + */ +export function _deprecated(scope: string, value: unknown, previous: string, current: string) { + if (value !== undefined) { + console.warn(scope + ': "' + previous + + '" is deprecated. Please use "' + current + '" instead'); + } +} + +// resolveObjectKey resolver cache +const keyResolvers = { + // Chart.helpers.core resolveObjectKey should resolve empty key to root object + '': v => v, + // default resolvers + x: o => o.x, + y: o => o.y +}; + +/** + * @private + */ +export function _splitKey(key: string) { + const parts = key.split('.'); + const keys: string[] = []; + let tmp = ''; + for (const part of parts) { + tmp += part; + if (tmp.endsWith('\\')) { + tmp = tmp.slice(0, -1) + '.'; + } else { + keys.push(tmp); + tmp = ''; + } + } + return keys; +} + +function _getKeyResolver(key: string) { + const keys = _splitKey(key); + return obj => { + for (const k of keys) { + if (k === '') { + // For backward compatibility: + // Chart.helpers.core resolveObjectKey should break at empty key + break; + } + obj = obj && obj[k]; + } + return obj; + }; +} + +export function resolveObjectKey(obj: AnyObject, key: string): any { + const resolver = keyResolvers[key] || (keyResolvers[key] = _getKeyResolver(key)); + return resolver(obj); +} + +/** + * @private + */ +export function _capitalize(str: string) { + return str.charAt(0).toUpperCase() + str.slice(1); +} + + +export const defined = (value: unknown) => typeof value !== 'undefined'; + +export const isFunction = (value: unknown): value is (...args: any[]) => any => typeof value === 'function'; + +// Adapted from https://stackoverflow.com/questions/31128855/comparing-ecma6-sets-for-equality#31129384 +export const setsEqual = (a: Set, b: Set) => { + if (a.size !== b.size) { + return false; + } + + for (const item of a) { + if (!b.has(item)) { + return false; + } + } + + return true; +}; + +/** + * @param e - The event + * @private + */ +export function _isClickEvent(e: ChartEvent) { + return e.type === 'mouseup' || e.type === 'click' || e.type === 'contextmenu'; +} diff --git a/src/helpers/helpers.curve.ts b/src/helpers/helpers.curve.ts new file mode 100644 index 00000000000..7dd0b7c3465 --- /dev/null +++ b/src/helpers/helpers.curve.ts @@ -0,0 +1,223 @@ +import {almostEquals, distanceBetweenPoints, sign} from './helpers.math.js'; +import {_isPointInArea} from './helpers.canvas.js'; +import type {ChartArea} from '../types/index.js'; +import type {SplinePoint} from '../types/geometric.js'; + +const EPSILON = Number.EPSILON || 1e-14; + +type OptionalSplinePoint = SplinePoint | false +const getPoint = (points: SplinePoint[], i: number): OptionalSplinePoint => i < points.length && !points[i].skip && points[i]; +const getValueAxis = (indexAxis: 'x' | 'y') => indexAxis === 'x' ? 'y' : 'x'; + +export function splineCurve( + firstPoint: SplinePoint, + middlePoint: SplinePoint, + afterPoint: SplinePoint, + t: number +): { + previous: SplinePoint + next: SplinePoint + } { + // Props to Rob Spencer at scaled innovation for his post on splining between points + // http://scaledinnovation.com/analytics/splines/aboutSplines.html + + // This function must also respect "skipped" points + + const previous = firstPoint.skip ? middlePoint : firstPoint; + const current = middlePoint; + const next = afterPoint.skip ? middlePoint : afterPoint; + const d01 = distanceBetweenPoints(current, previous); + const d12 = distanceBetweenPoints(next, current); + + let s01 = d01 / (d01 + d12); + let s12 = d12 / (d01 + d12); + + // If all points are the same, s01 & s02 will be inf + s01 = isNaN(s01) ? 0 : s01; + s12 = isNaN(s12) ? 0 : s12; + + const fa = t * s01; // scaling factor for triangle Ta + const fb = t * s12; + + return { + previous: { + x: current.x - fa * (next.x - previous.x), + y: current.y - fa * (next.y - previous.y) + }, + next: { + x: current.x + fb * (next.x - previous.x), + y: current.y + fb * (next.y - previous.y) + } + }; +} + +/** + * Adjust tangents to ensure monotonic properties + */ +function monotoneAdjust(points: SplinePoint[], deltaK: number[], mK: number[]) { + const pointsLen = points.length; + + let alphaK: number, betaK: number, tauK: number, squaredMagnitude: number, pointCurrent: OptionalSplinePoint; + let pointAfter = getPoint(points, 0); + for (let i = 0; i < pointsLen - 1; ++i) { + pointCurrent = pointAfter; + pointAfter = getPoint(points, i + 1); + if (!pointCurrent || !pointAfter) { + continue; + } + + if (almostEquals(deltaK[i], 0, EPSILON)) { + mK[i] = mK[i + 1] = 0; + continue; + } + + alphaK = mK[i] / deltaK[i]; + betaK = mK[i + 1] / deltaK[i]; + squaredMagnitude = Math.pow(alphaK, 2) + Math.pow(betaK, 2); + if (squaredMagnitude <= 9) { + continue; + } + + tauK = 3 / Math.sqrt(squaredMagnitude); + mK[i] = alphaK * tauK * deltaK[i]; + mK[i + 1] = betaK * tauK * deltaK[i]; + } +} + +function monotoneCompute(points: SplinePoint[], mK: number[], indexAxis: 'x' | 'y' = 'x') { + const valueAxis = getValueAxis(indexAxis); + const pointsLen = points.length; + let delta: number, pointBefore: OptionalSplinePoint, pointCurrent: OptionalSplinePoint; + let pointAfter = getPoint(points, 0); + + for (let i = 0; i < pointsLen; ++i) { + pointBefore = pointCurrent; + pointCurrent = pointAfter; + pointAfter = getPoint(points, i + 1); + if (!pointCurrent) { + continue; + } + + const iPixel = pointCurrent[indexAxis]; + const vPixel = pointCurrent[valueAxis]; + if (pointBefore) { + delta = (iPixel - pointBefore[indexAxis]) / 3; + pointCurrent[`cp1${indexAxis}`] = iPixel - delta; + pointCurrent[`cp1${valueAxis}`] = vPixel - delta * mK[i]; + } + if (pointAfter) { + delta = (pointAfter[indexAxis] - iPixel) / 3; + pointCurrent[`cp2${indexAxis}`] = iPixel + delta; + pointCurrent[`cp2${valueAxis}`] = vPixel + delta * mK[i]; + } + } +} + +/** + * This function calculates Bézier control points in a similar way than |splineCurve|, + * but preserves monotonicity of the provided data and ensures no local extremums are added + * between the dataset discrete points due to the interpolation. + * See : https://en.wikipedia.org/wiki/Monotone_cubic_interpolation + */ +export function splineCurveMonotone(points: SplinePoint[], indexAxis: 'x' | 'y' = 'x') { + const valueAxis = getValueAxis(indexAxis); + const pointsLen = points.length; + const deltaK: number[] = Array(pointsLen).fill(0); + const mK: number[] = Array(pointsLen); + + // Calculate slopes (deltaK) and initialize tangents (mK) + let i, pointBefore: OptionalSplinePoint, pointCurrent: OptionalSplinePoint; + let pointAfter = getPoint(points, 0); + + for (i = 0; i < pointsLen; ++i) { + pointBefore = pointCurrent; + pointCurrent = pointAfter; + pointAfter = getPoint(points, i + 1); + if (!pointCurrent) { + continue; + } + + if (pointAfter) { + const slopeDelta = pointAfter[indexAxis] - pointCurrent[indexAxis]; + + // In the case of two points that appear at the same x pixel, slopeDeltaX is 0 + deltaK[i] = slopeDelta !== 0 ? (pointAfter[valueAxis] - pointCurrent[valueAxis]) / slopeDelta : 0; + } + mK[i] = !pointBefore ? deltaK[i] + : !pointAfter ? deltaK[i - 1] + : (sign(deltaK[i - 1]) !== sign(deltaK[i])) ? 0 + : (deltaK[i - 1] + deltaK[i]) / 2; + } + + monotoneAdjust(points, deltaK, mK); + + monotoneCompute(points, mK, indexAxis); +} + +function capControlPoint(pt: number, min: number, max: number) { + return Math.max(Math.min(pt, max), min); +} + +function capBezierPoints(points: SplinePoint[], area: ChartArea) { + let i, ilen, point, inArea, inAreaPrev; + let inAreaNext = _isPointInArea(points[0], area); + for (i = 0, ilen = points.length; i < ilen; ++i) { + inAreaPrev = inArea; + inArea = inAreaNext; + inAreaNext = i < ilen - 1 && _isPointInArea(points[i + 1], area); + if (!inArea) { + continue; + } + point = points[i]; + if (inAreaPrev) { + point.cp1x = capControlPoint(point.cp1x, area.left, area.right); + point.cp1y = capControlPoint(point.cp1y, area.top, area.bottom); + } + if (inAreaNext) { + point.cp2x = capControlPoint(point.cp2x, area.left, area.right); + point.cp2y = capControlPoint(point.cp2y, area.top, area.bottom); + } + } +} + +/** + * @private + */ +export function _updateBezierControlPoints( + points: SplinePoint[], + options, + area: ChartArea, + loop: boolean, + indexAxis: 'x' | 'y' +) { + let i: number, ilen: number, point: SplinePoint, controlPoints: ReturnType; + + // Only consider points that are drawn in case the spanGaps option is used + if (options.spanGaps) { + points = points.filter((pt) => !pt.skip); + } + + if (options.cubicInterpolationMode === 'monotone') { + splineCurveMonotone(points, indexAxis); + } else { + let prev = loop ? points[points.length - 1] : points[0]; + for (i = 0, ilen = points.length; i < ilen; ++i) { + point = points[i]; + controlPoints = splineCurve( + prev, + point, + points[Math.min(i + 1, ilen - (loop ? 0 : 1)) % ilen], + options.tension + ); + point.cp1x = controlPoints.previous.x; + point.cp1y = controlPoints.previous.y; + point.cp2x = controlPoints.next.x; + point.cp2y = controlPoints.next.y; + prev = point; + } + } + + if (options.capBezierPoints) { + capBezierPoints(points, area); + } +} diff --git a/src/helpers/helpers.dataset.ts b/src/helpers/helpers.dataset.ts new file mode 100644 index 00000000000..000dcfe1977 --- /dev/null +++ b/src/helpers/helpers.dataset.ts @@ -0,0 +1,33 @@ +import type {Chart, ChartArea, ChartMeta, Scale, TRBL} from '../types/index.js'; + +function getSizeForArea(scale: Scale, chartArea: ChartArea, field: keyof ChartArea) { + return scale.options.clip ? scale[field] : chartArea[field]; +} + +function getDatasetArea(meta: ChartMeta, chartArea: ChartArea): TRBL { + const {xScale, yScale} = meta; + if (xScale && yScale) { + return { + left: getSizeForArea(xScale, chartArea, 'left'), + right: getSizeForArea(xScale, chartArea, 'right'), + top: getSizeForArea(yScale, chartArea, 'top'), + bottom: getSizeForArea(yScale, chartArea, 'bottom') + }; + } + return chartArea; +} + +export function getDatasetClipArea(chart: Chart, meta: ChartMeta): TRBL | false { + const clip = meta._clip; + if (clip.disabled) { + return false; + } + const area = getDatasetArea(meta, chart.chartArea); + + return { + left: clip.left === false ? 0 : area.left - (clip.left === true ? 0 : clip.left), + right: clip.right === false ? chart.width : area.right + (clip.right === true ? 0 : clip.right), + top: clip.top === false ? 0 : area.top - (clip.top === true ? 0 : clip.top), + bottom: clip.bottom === false ? chart.height : area.bottom + (clip.bottom === true ? 0 : clip.bottom) + }; +} diff --git a/src/helpers/helpers.dom.ts b/src/helpers/helpers.dom.ts new file mode 100644 index 00000000000..b0c41eec0c0 --- /dev/null +++ b/src/helpers/helpers.dom.ts @@ -0,0 +1,286 @@ +import type {ChartArea, Scale} from '../types/index.js'; +import type PrivateChart from '../core/core.controller.js'; +import type {Chart, ChartEvent} from '../types.js'; +import {INFINITY} from './helpers.math.js'; + +/** + * @private + */ +export function _isDomSupported(): boolean { + return typeof window !== 'undefined' && typeof document !== 'undefined'; +} + +/** + * @private + */ +export function _getParentNode(domNode: HTMLCanvasElement): HTMLCanvasElement { + let parent = domNode.parentNode; + if (parent && parent.toString() === '[object ShadowRoot]') { + parent = (parent as ShadowRoot).host; + } + return parent as HTMLCanvasElement; +} + +/** + * convert max-width/max-height values that may be percentages into a number + * @private + */ + +function parseMaxStyle(styleValue: string | number, node: HTMLElement, parentProperty: string) { + let valueInPixels: number; + if (typeof styleValue === 'string') { + valueInPixels = parseInt(styleValue, 10); + + if (styleValue.indexOf('%') !== -1) { + // percentage * size in dimension + valueInPixels = (valueInPixels / 100) * node.parentNode[parentProperty]; + } + } else { + valueInPixels = styleValue; + } + + return valueInPixels; +} + +const getComputedStyle = (element: HTMLElement): CSSStyleDeclaration => + element.ownerDocument.defaultView.getComputedStyle(element, null); + +export function getStyle(el: HTMLElement, property: string): string { + return getComputedStyle(el).getPropertyValue(property); +} + +const positions = ['top', 'right', 'bottom', 'left']; +function getPositionedStyle(styles: CSSStyleDeclaration, style: string, suffix?: string): ChartArea { + const result = {} as ChartArea; + suffix = suffix ? '-' + suffix : ''; + for (let i = 0; i < 4; i++) { + const pos = positions[i]; + result[pos] = parseFloat(styles[style + '-' + pos + suffix]) || 0; + } + result.width = result.left + result.right; + result.height = result.top + result.bottom; + return result; +} + +const useOffsetPos = (x: number, y: number, target: HTMLElement | EventTarget) => + (x > 0 || y > 0) && (!target || !(target as HTMLElement).shadowRoot); + +/** + * @param e + * @param canvas + * @returns Canvas position + */ +function getCanvasPosition( + e: Event | TouchEvent | MouseEvent, + canvas: HTMLCanvasElement +): { + x: number; + y: number; + box: boolean; + } { + const touches = (e as TouchEvent).touches; + const source = (touches && touches.length ? touches[0] : e) as MouseEvent; + const {offsetX, offsetY} = source as MouseEvent; + let box = false; + let x, y; + if (useOffsetPos(offsetX, offsetY, e.target)) { + x = offsetX; + y = offsetY; + } else { + const rect = canvas.getBoundingClientRect(); + x = source.clientX - rect.left; + y = source.clientY - rect.top; + box = true; + } + return {x, y, box}; +} + +/** + * Gets an event's x, y coordinates, relative to the chart area + * @param event + * @param chart + * @returns x and y coordinates of the event + */ + +export function getRelativePosition( + event: Event | ChartEvent | TouchEvent | MouseEvent, + chart: Chart | PrivateChart +): { x: number; y: number } { + if ('native' in event) { + return event; + } + + const {canvas, currentDevicePixelRatio} = chart; + const style = getComputedStyle(canvas); + const borderBox = style.boxSizing === 'border-box'; + const paddings = getPositionedStyle(style, 'padding'); + const borders = getPositionedStyle(style, 'border', 'width'); + const {x, y, box} = getCanvasPosition(event, canvas); + const xOffset = paddings.left + (box && borders.left); + const yOffset = paddings.top + (box && borders.top); + + let {width, height} = chart; + if (borderBox) { + width -= paddings.width + borders.width; + height -= paddings.height + borders.height; + } + return { + x: Math.round((x - xOffset) / width * canvas.width / currentDevicePixelRatio), + y: Math.round((y - yOffset) / height * canvas.height / currentDevicePixelRatio) + }; +} + +function getContainerSize(canvas: HTMLCanvasElement, width: number, height: number): Partial { + let maxWidth: number, maxHeight: number; + + if (width === undefined || height === undefined) { + const container = canvas && _getParentNode(canvas); + if (!container) { + width = canvas.clientWidth; + height = canvas.clientHeight; + } else { + const rect = container.getBoundingClientRect(); // this is the border box of the container + const containerStyle = getComputedStyle(container); + const containerBorder = getPositionedStyle(containerStyle, 'border', 'width'); + const containerPadding = getPositionedStyle(containerStyle, 'padding'); + width = rect.width - containerPadding.width - containerBorder.width; + height = rect.height - containerPadding.height - containerBorder.height; + maxWidth = parseMaxStyle(containerStyle.maxWidth, container, 'clientWidth'); + maxHeight = parseMaxStyle(containerStyle.maxHeight, container, 'clientHeight'); + } + } + return { + width, + height, + maxWidth: maxWidth || INFINITY, + maxHeight: maxHeight || INFINITY + }; +} + +const round1 = (v: number) => Math.round(v * 10) / 10; + +// eslint-disable-next-line complexity +export function getMaximumSize( + canvas: HTMLCanvasElement, + bbWidth?: number, + bbHeight?: number, + aspectRatio?: number +): { width: number; height: number } { + const style = getComputedStyle(canvas); + const margins = getPositionedStyle(style, 'margin'); + const maxWidth = parseMaxStyle(style.maxWidth, canvas, 'clientWidth') || INFINITY; + const maxHeight = parseMaxStyle(style.maxHeight, canvas, 'clientHeight') || INFINITY; + const containerSize = getContainerSize(canvas, bbWidth, bbHeight); + let {width, height} = containerSize; + + if (style.boxSizing === 'content-box') { + const borders = getPositionedStyle(style, 'border', 'width'); + const paddings = getPositionedStyle(style, 'padding'); + width -= paddings.width + borders.width; + height -= paddings.height + borders.height; + } + width = Math.max(0, width - margins.width); + height = Math.max(0, aspectRatio ? width / aspectRatio : height - margins.height); + width = round1(Math.min(width, maxWidth, containerSize.maxWidth)); + height = round1(Math.min(height, maxHeight, containerSize.maxHeight)); + if (width && !height) { + // https://github.com/chartjs/Chart.js/issues/4659 + // If the canvas has width, but no height, default to aspectRatio of 2 (canvas default) + height = round1(width / 2); + } + + const maintainHeight = bbWidth !== undefined || bbHeight !== undefined; + + if (maintainHeight && aspectRatio && containerSize.height && height > containerSize.height) { + height = containerSize.height; + width = round1(Math.floor(height * aspectRatio)); + } + + return {width, height}; +} + +/** + * @param chart + * @param forceRatio + * @param forceStyle + * @returns True if the canvas context size or transformation has changed. + */ +export function retinaScale( + chart: Chart | PrivateChart, + forceRatio: number, + forceStyle?: boolean +): boolean | void { + const pixelRatio = forceRatio || 1; + const deviceHeight = round1(chart.height * pixelRatio); + const deviceWidth = round1(chart.width * pixelRatio); + + (chart as PrivateChart).height = round1(chart.height); + (chart as PrivateChart).width = round1(chart.width); + + const canvas = chart.canvas; + + // If no style has been set on the canvas, the render size is used as display size, + // making the chart visually bigger, so let's enforce it to the "correct" values. + // See https://github.com/chartjs/Chart.js/issues/3575 + if (canvas.style && (forceStyle || (!canvas.style.height && !canvas.style.width))) { + canvas.style.height = `${chart.height}px`; + canvas.style.width = `${chart.width}px`; + } + + const canvasHeight = Math.floor(deviceHeight); + const canvasWidth = Math.floor(deviceWidth); + if (chart.currentDevicePixelRatio !== pixelRatio + || canvas.height !== canvasHeight + || canvas.width !== canvasWidth) { + (chart as PrivateChart).currentDevicePixelRatio = pixelRatio; + canvas.height = canvasHeight; + canvas.width = canvasWidth; + chart.ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0); + return true; + } + return false; +} + +/** + * Detects support for options object argument in addEventListener. + * https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Safely_detecting_option_support + * @private + */ +export const supportsEventListenerOptions = (function() { + let passiveSupported = false; + try { + const options = { + get passive() { // This function will be called when the browser attempts to access the passive property. + passiveSupported = true; + return false; + } + } as EventListenerOptions; + + if (_isDomSupported()) { + window.addEventListener('test', null, options); + window.removeEventListener('test', null, options); + } + } catch (e) { + // continue regardless of error + } + return passiveSupported; +}()); + +/** + * The "used" size is the final value of a dimension property after all calculations have + * been performed. This method uses the computed style of `element` but returns undefined + * if the computed style is not expressed in pixels. That can happen in some cases where + * `element` has a size relative to its parent and this last one is not yet displayed, + * for example because of `display: none` on a parent node. + * @see https://developer.mozilla.org/en-US/docs/Web/CSS/used_value + * @returns Size in pixels or undefined if unknown. + */ + +export function readUsedSize( + element: HTMLElement, + property: 'width' | 'height' +): number | undefined { + const value = getStyle(element, property); + const matches = value && value.match(/^(\d+)(\.\d+)?px$/); + return matches ? +matches[1] : undefined; +} diff --git a/src/helpers/helpers.easing.ts b/src/helpers/helpers.easing.ts new file mode 100644 index 00000000000..caa9c17bf51 --- /dev/null +++ b/src/helpers/helpers.easing.ts @@ -0,0 +1,124 @@ +import {PI, TAU, HALF_PI} from './helpers.math.js'; + +const atEdge = (t: number) => t === 0 || t === 1; +const elasticIn = (t: number, s: number, p: number) => -(Math.pow(2, 10 * (t -= 1)) * Math.sin((t - s) * TAU / p)); +const elasticOut = (t: number, s: number, p: number) => Math.pow(2, -10 * t) * Math.sin((t - s) * TAU / p) + 1; + +/** + * Easing functions adapted from Robert Penner's easing equations. + * @namespace Chart.helpers.easing.effects + * @see http://www.robertpenner.com/easing/ + */ +const effects = { + linear: (t: number) => t, + + easeInQuad: (t: number) => t * t, + + easeOutQuad: (t: number) => -t * (t - 2), + + easeInOutQuad: (t: number) => ((t /= 0.5) < 1) + ? 0.5 * t * t + : -0.5 * ((--t) * (t - 2) - 1), + + easeInCubic: (t: number) => t * t * t, + + easeOutCubic: (t: number) => (t -= 1) * t * t + 1, + + easeInOutCubic: (t: number) => ((t /= 0.5) < 1) + ? 0.5 * t * t * t + : 0.5 * ((t -= 2) * t * t + 2), + + easeInQuart: (t: number) => t * t * t * t, + + easeOutQuart: (t: number) => -((t -= 1) * t * t * t - 1), + + easeInOutQuart: (t: number) => ((t /= 0.5) < 1) + ? 0.5 * t * t * t * t + : -0.5 * ((t -= 2) * t * t * t - 2), + + easeInQuint: (t: number) => t * t * t * t * t, + + easeOutQuint: (t: number) => (t -= 1) * t * t * t * t + 1, + + easeInOutQuint: (t: number) => ((t /= 0.5) < 1) + ? 0.5 * t * t * t * t * t + : 0.5 * ((t -= 2) * t * t * t * t + 2), + + easeInSine: (t: number) => -Math.cos(t * HALF_PI) + 1, + + easeOutSine: (t: number) => Math.sin(t * HALF_PI), + + easeInOutSine: (t: number) => -0.5 * (Math.cos(PI * t) - 1), + + easeInExpo: (t: number) => (t === 0) ? 0 : Math.pow(2, 10 * (t - 1)), + + easeOutExpo: (t: number) => (t === 1) ? 1 : -Math.pow(2, -10 * t) + 1, + + easeInOutExpo: (t: number) => atEdge(t) ? t : t < 0.5 + ? 0.5 * Math.pow(2, 10 * (t * 2 - 1)) + : 0.5 * (-Math.pow(2, -10 * (t * 2 - 1)) + 2), + + easeInCirc: (t: number) => (t >= 1) ? t : -(Math.sqrt(1 - t * t) - 1), + + easeOutCirc: (t: number) => Math.sqrt(1 - (t -= 1) * t), + + easeInOutCirc: (t: number) => ((t /= 0.5) < 1) + ? -0.5 * (Math.sqrt(1 - t * t) - 1) + : 0.5 * (Math.sqrt(1 - (t -= 2) * t) + 1), + + easeInElastic: (t: number) => atEdge(t) ? t : elasticIn(t, 0.075, 0.3), + + easeOutElastic: (t: number) => atEdge(t) ? t : elasticOut(t, 0.075, 0.3), + + easeInOutElastic(t: number) { + const s = 0.1125; + const p = 0.45; + return atEdge(t) ? t : + t < 0.5 + ? 0.5 * elasticIn(t * 2, s, p) + : 0.5 + 0.5 * elasticOut(t * 2 - 1, s, p); + }, + + easeInBack(t: number) { + const s = 1.70158; + return t * t * ((s + 1) * t - s); + }, + + easeOutBack(t: number) { + const s = 1.70158; + return (t -= 1) * t * ((s + 1) * t + s) + 1; + }, + + easeInOutBack(t: number) { + let s = 1.70158; + if ((t /= 0.5) < 1) { + return 0.5 * (t * t * (((s *= (1.525)) + 1) * t - s)); + } + return 0.5 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2); + }, + + easeInBounce: (t: number) => 1 - effects.easeOutBounce(1 - t), + + easeOutBounce(t: number) { + const m = 7.5625; + const d = 2.75; + if (t < (1 / d)) { + return m * t * t; + } + if (t < (2 / d)) { + return m * (t -= (1.5 / d)) * t + 0.75; + } + if (t < (2.5 / d)) { + return m * (t -= (2.25 / d)) * t + 0.9375; + } + return m * (t -= (2.625 / d)) * t + 0.984375; + }, + + easeInOutBounce: (t: number) => (t < 0.5) + ? effects.easeInBounce(t * 2) * 0.5 + : effects.easeOutBounce(t * 2 - 1) * 0.5 + 0.5, +} as const; + +export type EasingFunction = keyof typeof effects + +export default effects; diff --git a/src/helpers/helpers.extras.ts b/src/helpers/helpers.extras.ts new file mode 100644 index 00000000000..798e10c1e86 --- /dev/null +++ b/src/helpers/helpers.extras.ts @@ -0,0 +1,163 @@ +import type {ChartMeta, PointElement} from '../types/index.js'; + +import {_limitValue} from './helpers.math.js'; +import {_lookupByKey} from './helpers.collection.js'; +import {isNullOrUndef} from './helpers.core.js'; + +export function fontString(pixelSize: number, fontStyle: string, fontFamily: string) { + return fontStyle + ' ' + pixelSize + 'px ' + fontFamily; +} + +/** +* Request animation polyfill +*/ +export const requestAnimFrame = (function() { + if (typeof window === 'undefined') { + return function(callback) { + return callback(); + }; + } + return window.requestAnimationFrame; +}()); + +/** + * Throttles calling `fn` once per animation frame + * Latest arguments are used on the actual call + */ +export function throttled>( + fn: (...args: TArgs) => void, + thisArg: any, +) { + let argsToUse = [] as TArgs; + let ticking = false; + + return function(...args: TArgs) { + // Save the args for use later + argsToUse = args; + if (!ticking) { + ticking = true; + requestAnimFrame.call(window, () => { + ticking = false; + fn.apply(thisArg, argsToUse); + }); + } + }; +} + +/** + * Debounces calling `fn` for `delay` ms + */ +export function debounce>(fn: (...args: TArgs) => void, delay: number) { + let timeout; + return function(...args: TArgs) { + if (delay) { + clearTimeout(timeout); + timeout = setTimeout(fn, delay, args); + } else { + fn.apply(this, args); + } + return delay; + }; +} + +/** + * Converts 'start' to 'left', 'end' to 'right' and others to 'center' + * @private + */ +export const _toLeftRightCenter = (align: 'start' | 'end' | 'center') => align === 'start' ? 'left' : align === 'end' ? 'right' : 'center'; + +/** + * Returns `start`, `end` or `(start + end) / 2` depending on `align`. Defaults to `center` + * @private + */ +export const _alignStartEnd = (align: 'start' | 'end' | 'center', start: number, end: number) => align === 'start' ? start : align === 'end' ? end : (start + end) / 2; + +/** + * Returns `left`, `right` or `(left + right) / 2` depending on `align`. Defaults to `left` + * @private + */ +export const _textX = (align: 'left' | 'right' | 'center', left: number, right: number, rtl: boolean) => { + const check = rtl ? 'left' : 'right'; + return align === check ? right : align === 'center' ? (left + right) / 2 : left; +}; + +/** + * Return start and count of visible points. + * @private + */ +export function _getStartAndCountOfVisiblePoints(meta: ChartMeta<'line' | 'scatter'>, points: PointElement[], animationsDisabled: boolean) { + const pointCount = points.length; + + let start = 0; + let count = pointCount; + + if (meta._sorted) { + const {iScale, vScale, _parsed} = meta; + const spanGaps = meta.dataset ? meta.dataset.options ? meta.dataset.options.spanGaps : null : null; + const axis = iScale.axis; + const {min, max, minDefined, maxDefined} = iScale.getUserBounds(); + + if (minDefined) { + start = Math.min( + // @ts-expect-error Need to type _parsed + _lookupByKey(_parsed, axis, min).lo, + // @ts-expect-error Need to fix types on _lookupByKey + animationsDisabled ? pointCount : _lookupByKey(points, axis, iScale.getPixelForValue(min)).lo); + if (spanGaps) { + const distanceToDefinedLo = (_parsed + .slice(0, start + 1) + .reverse() + .findIndex( + point => !isNullOrUndef(point[vScale.axis]))); + start -= Math.max(0, distanceToDefinedLo); + } + start = _limitValue(start, 0, pointCount - 1); + } + if (maxDefined) { + let end = Math.max( + // @ts-expect-error Need to type _parsed + _lookupByKey(_parsed, iScale.axis, max, true).hi + 1, + // @ts-expect-error Need to fix types on _lookupByKey + animationsDisabled ? 0 : _lookupByKey(points, axis, iScale.getPixelForValue(max), true).hi + 1); + if (spanGaps) { + const distanceToDefinedHi = (_parsed + .slice(end - 1) + .findIndex( + point => !isNullOrUndef(point[vScale.axis]))); + end += Math.max(0, distanceToDefinedHi); + } + count = _limitValue(end, start, pointCount) - start; + } else { + count = pointCount - start; + } + } + + return {start, count}; +} + +/** + * Checks if the scale ranges have changed. + * @param {object} meta - dataset meta. + * @returns {boolean} + * @private + */ +export function _scaleRangesChanged(meta) { + const {xScale, yScale, _scaleRanges} = meta; + const newRanges = { + xmin: xScale.min, + xmax: xScale.max, + ymin: yScale.min, + ymax: yScale.max + }; + if (!_scaleRanges) { + meta._scaleRanges = newRanges; + return true; + } + const changed = _scaleRanges.xmin !== xScale.min + || _scaleRanges.xmax !== xScale.max + || _scaleRanges.ymin !== yScale.min + || _scaleRanges.ymax !== yScale.max; + + Object.assign(_scaleRanges, newRanges); + return changed; +} diff --git a/src/helpers/helpers.interpolation.ts b/src/helpers/helpers.interpolation.ts new file mode 100644 index 00000000000..d3427b5592d --- /dev/null +++ b/src/helpers/helpers.interpolation.ts @@ -0,0 +1,41 @@ +import type {Point, SplinePoint} from '../types/geometric.js'; + +/** + * @private + */ +export function _pointInLine(p1: Point, p2: Point, t: number, mode?) { // eslint-disable-line @typescript-eslint/no-unused-vars + return { + x: p1.x + t * (p2.x - p1.x), + y: p1.y + t * (p2.y - p1.y) + }; +} + +/** + * @private + */ +export function _steppedInterpolation( + p1: Point, + p2: Point, + t: number, mode: 'middle' | 'after' | unknown +) { + return { + x: p1.x + t * (p2.x - p1.x), + y: mode === 'middle' ? t < 0.5 ? p1.y : p2.y + : mode === 'after' ? t < 1 ? p1.y : p2.y + : t > 0 ? p2.y : p1.y + }; +} + +/** + * @private + */ +export function _bezierInterpolation(p1: SplinePoint, p2: SplinePoint, t: number, mode?) { // eslint-disable-line @typescript-eslint/no-unused-vars + const cp1 = {x: p1.cp2x, y: p1.cp2y}; + const cp2 = {x: p2.cp1x, y: p2.cp1y}; + const a = _pointInLine(p1, cp1, t); + const b = _pointInLine(cp1, cp2, t); + const c = _pointInLine(cp2, p2, t); + const d = _pointInLine(a, b, t); + const e = _pointInLine(b, c, t); + return _pointInLine(d, e, t); +} diff --git a/src/helpers/helpers.intl.ts b/src/helpers/helpers.intl.ts new file mode 100644 index 00000000000..24bd4eec359 --- /dev/null +++ b/src/helpers/helpers.intl.ts @@ -0,0 +1,17 @@ + +const intlCache = new Map(); + +function getNumberFormat(locale: string, options?: Intl.NumberFormatOptions) { + options = options || {}; + const cacheKey = locale + JSON.stringify(options); + let formatter = intlCache.get(cacheKey); + if (!formatter) { + formatter = new Intl.NumberFormat(locale, options); + intlCache.set(cacheKey, formatter); + } + return formatter; +} + +export function formatNumber(num: number, locale: string, options?: Intl.NumberFormatOptions) { + return getNumberFormat(locale, options).format(num); +} diff --git a/src/helpers/helpers.math.ts b/src/helpers/helpers.math.ts new file mode 100644 index 00000000000..0fd2a95132c --- /dev/null +++ b/src/helpers/helpers.math.ts @@ -0,0 +1,207 @@ +import type {Point} from '../types/geometric.js'; +import {isFinite as isFiniteNumber} from './helpers.core.js'; + +/** + * @alias Chart.helpers.math + * @namespace + */ + +export const PI = Math.PI; +export const TAU = 2 * PI; +export const PITAU = TAU + PI; +export const INFINITY = Number.POSITIVE_INFINITY; +export const RAD_PER_DEG = PI / 180; +export const HALF_PI = PI / 2; +export const QUARTER_PI = PI / 4; +export const TWO_THIRDS_PI = PI * 2 / 3; + +export const log10 = Math.log10; +export const sign = Math.sign; + +export function almostEquals(x: number, y: number, epsilon: number) { + return Math.abs(x - y) < epsilon; +} + +/** + * Implementation of the nice number algorithm used in determining where axis labels will go + */ +export function niceNum(range: number) { + const roundedRange = Math.round(range); + range = almostEquals(range, roundedRange, range / 1000) ? roundedRange : range; + const niceRange = Math.pow(10, Math.floor(log10(range))); + const fraction = range / niceRange; + const niceFraction = fraction <= 1 ? 1 : fraction <= 2 ? 2 : fraction <= 5 ? 5 : 10; + return niceFraction * niceRange; +} + +/** + * Returns an array of factors sorted from 1 to sqrt(value) + * @private + */ +export function _factorize(value: number) { + const result: number[] = []; + const sqrt = Math.sqrt(value); + let i: number; + + for (i = 1; i < sqrt; i++) { + if (value % i === 0) { + result.push(i); + result.push(value / i); + } + } + if (sqrt === (sqrt | 0)) { // if value is a square number + result.push(sqrt); + } + + result.sort((a, b) => a - b).pop(); + return result; +} + +/** + * Verifies that attempting to coerce n to string or number won't throw a TypeError. + */ +function isNonPrimitive(n: unknown) { + return typeof n === 'symbol' || (typeof n === 'object' && n !== null && !(Symbol.toPrimitive in n || 'toString' in n || 'valueOf' in n)); +} + +export function isNumber(n: unknown): n is number { + return !isNonPrimitive(n) && !isNaN(parseFloat(n as string)) && isFinite(n as number); +} + +export function almostWhole(x: number, epsilon: number) { + const rounded = Math.round(x); + return ((rounded - epsilon) <= x) && ((rounded + epsilon) >= x); +} + +/** + * @private + */ +export function _setMinAndMaxByKey( + array: Record[], + target: { min: number, max: number }, + property: string +) { + let i: number, ilen: number, value: number; + + for (i = 0, ilen = array.length; i < ilen; i++) { + value = array[i][property]; + if (!isNaN(value)) { + target.min = Math.min(target.min, value); + target.max = Math.max(target.max, value); + } + } +} + +export function toRadians(degrees: number) { + return degrees * (PI / 180); +} + +export function toDegrees(radians: number) { + return radians * (180 / PI); +} + +/** + * Returns the number of decimal places + * i.e. the number of digits after the decimal point, of the value of this Number. + * @param x - A number. + * @returns The number of decimal places. + * @private + */ +export function _decimalPlaces(x: number) { + if (!isFiniteNumber(x)) { + return; + } + let e = 1; + let p = 0; + while (Math.round(x * e) / e !== x) { + e *= 10; + p++; + } + return p; +} + +// Gets the angle from vertical upright to the point about a centre. +export function getAngleFromPoint( + centrePoint: Point, + anglePoint: Point +) { + const distanceFromXCenter = anglePoint.x - centrePoint.x; + const distanceFromYCenter = anglePoint.y - centrePoint.y; + const radialDistanceFromCenter = Math.sqrt(distanceFromXCenter * distanceFromXCenter + distanceFromYCenter * distanceFromYCenter); + + let angle = Math.atan2(distanceFromYCenter, distanceFromXCenter); + + if (angle < (-0.5 * PI)) { + angle += TAU; // make sure the returned angle is in the range of (-PI/2, 3PI/2] + } + + return { + angle, + distance: radialDistanceFromCenter + }; +} + +export function distanceBetweenPoints(pt1: Point, pt2: Point) { + return Math.sqrt(Math.pow(pt2.x - pt1.x, 2) + Math.pow(pt2.y - pt1.y, 2)); +} + +/** + * Shortest distance between angles, in either direction. + * @private + */ +export function _angleDiff(a: number, b: number) { + return (a - b + PITAU) % TAU - PI; +} + +/** + * Normalize angle to be between 0 and 2*PI + * @private + */ +export function _normalizeAngle(a: number) { + return (a % TAU + TAU) % TAU; +} + +/** + * @private + */ +export function _angleBetween(angle: number, start: number, end: number, sameAngleIsFullCircle?: boolean) { + const a = _normalizeAngle(angle); + const s = _normalizeAngle(start); + const e = _normalizeAngle(end); + const angleToStart = _normalizeAngle(s - a); + const angleToEnd = _normalizeAngle(e - a); + const startToAngle = _normalizeAngle(a - s); + const endToAngle = _normalizeAngle(a - e); + return a === s || a === e || (sameAngleIsFullCircle && s === e) + || (angleToStart > angleToEnd && startToAngle < endToAngle); +} + +/** + * Limit `value` between `min` and `max` + * @param value + * @param min + * @param max + * @private + */ +export function _limitValue(value: number, min: number, max: number) { + return Math.max(min, Math.min(max, value)); +} + +/** + * @param {number} value + * @private + */ +export function _int16Range(value: number) { + return _limitValue(value, -32768, 32767); +} + +/** + * @param value + * @param start + * @param end + * @param [epsilon] + * @private + */ +export function _isBetween(value: number, start: number, end: number, epsilon = 1e-6) { + return value >= Math.min(start, end) - epsilon && value <= Math.max(start, end) + epsilon; +} diff --git a/src/helpers/helpers.options.ts b/src/helpers/helpers.options.ts new file mode 100644 index 00000000000..a69d52f0632 --- /dev/null +++ b/src/helpers/helpers.options.ts @@ -0,0 +1,206 @@ +import defaults from '../core/core.defaults.js'; +import {isArray, isObject, toDimension, valueOrDefault} from './helpers.core.js'; +import {toFontString} from './helpers.canvas.js'; +import type {ChartArea, FontSpec, Point} from '../types/index.js'; +import type {TRBL, TRBLCorners} from '../types/geometric.js'; + +const LINE_HEIGHT = /^(normal|(\d+(?:\.\d+)?)(px|em|%)?)$/; +const FONT_STYLE = /^(normal|italic|initial|inherit|unset|(oblique( -?[0-9]?[0-9]deg)?))$/; + +/** + * @alias Chart.helpers.options + * @namespace + */ +/** + * Converts the given line height `value` in pixels for a specific font `size`. + * @param value - The lineHeight to parse (eg. 1.6, '14px', '75%', '1.6em'). + * @param size - The font size (in pixels) used to resolve relative `value`. + * @returns The effective line height in pixels (size * 1.2 if value is invalid). + * @see https://developer.mozilla.org/en-US/docs/Web/CSS/line-height + * @since 2.7.0 + */ +export function toLineHeight(value: number | string, size: number): number { + const matches = ('' + value).match(LINE_HEIGHT); + if (!matches || matches[1] === 'normal') { + return size * 1.2; + } + + value = +matches[2]; + + switch (matches[3]) { + case 'px': + return value; + case '%': + value /= 100; + break; + default: + break; + } + + return size * value; +} + +const numberOrZero = (v: unknown) => +v || 0; + +/** + * @param value + * @param props + */ +export function _readValueToProps(value: number | Record, props: K[]): Record; +export function _readValueToProps(value: number | Record, props: Record): Record; +export function _readValueToProps(value: number | Record, props: string[] | Record) { + const ret = {}; + const objProps = isObject(props); + const keys = objProps ? Object.keys(props) : props; + const read = isObject(value) + ? objProps + ? prop => valueOrDefault(value[prop], value[props[prop]]) + : prop => value[prop] + : () => value; + + for (const prop of keys) { + ret[prop] = numberOrZero(read(prop)); + } + return ret; +} + +/** + * Converts the given value into a TRBL object. + * @param value - If a number, set the value to all TRBL component, + * else, if an object, use defined properties and sets undefined ones to 0. + * x / y are shorthands for same value for left/right and top/bottom. + * @returns The padding values (top, right, bottom, left) + * @since 3.0.0 + */ +export function toTRBL(value: number | TRBL | Point) { + return _readValueToProps(value, {top: 'y', right: 'x', bottom: 'y', left: 'x'}); +} + +/** + * Converts the given value into a TRBL corners object (similar with css border-radius). + * @param value - If a number, set the value to all TRBL corner components, + * else, if an object, use defined properties and sets undefined ones to 0. + * @returns The TRBL corner values (topLeft, topRight, bottomLeft, bottomRight) + * @since 3.0.0 + */ +export function toTRBLCorners(value: number | TRBLCorners) { + return _readValueToProps(value, ['topLeft', 'topRight', 'bottomLeft', 'bottomRight']); +} + +/** + * Converts the given value into a padding object with pre-computed width/height. + * @param value - If a number, set the value to all TRBL component, + * else, if an object, use defined properties and sets undefined ones to 0. + * x / y are shorthands for same value for left/right and top/bottom. + * @returns The padding values (top, right, bottom, left, width, height) + * @since 2.7.0 + */ +export function toPadding(value?: number | TRBL): ChartArea { + const obj = toTRBL(value) as ChartArea; + + obj.width = obj.left + obj.right; + obj.height = obj.top + obj.bottom; + + return obj; +} + +/** + * Parses font options and returns the font object. + * @param options - A object that contains font options to be parsed. + * @param fallback - A object that contains fallback font options. + * @return The font object. + * @private + */ + +export function toFont(options: Partial, fallback?: Partial) { + options = options || {}; + fallback = fallback || defaults.font as FontSpec; + + let size = valueOrDefault(options.size, fallback.size); + + if (typeof size === 'string') { + size = parseInt(size, 10); + } + let style = valueOrDefault(options.style, fallback.style); + if (style && !('' + style).match(FONT_STYLE)) { + console.warn('Invalid font style specified: "' + style + '"'); + style = undefined; + } + + const font = { + family: valueOrDefault(options.family, fallback.family), + lineHeight: toLineHeight(valueOrDefault(options.lineHeight, fallback.lineHeight), size), + size, + style, + weight: valueOrDefault(options.weight, fallback.weight), + string: '' + }; + + font.string = toFontString(font); + return font; +} + +/** + * Evaluates the given `inputs` sequentially and returns the first defined value. + * @param inputs - An array of values, falling back to the last value. + * @param context - If defined and the current value is a function, the value + * is called with `context` as first argument and the result becomes the new input. + * @param index - If defined and the current value is an array, the value + * at `index` become the new input. + * @param info - object to return information about resolution in + * @param info.cacheable - Will be set to `false` if option is not cacheable. + * @since 2.7.0 + */ +export function resolve(inputs: Array, context?: object, index?: number, info?: { cacheable: boolean }) { + let cacheable = true; + let i: number, ilen: number, value: unknown; + + for (i = 0, ilen = inputs.length; i < ilen; ++i) { + value = inputs[i]; + if (value === undefined) { + continue; + } + if (context !== undefined && typeof value === 'function') { + value = value(context); + cacheable = false; + } + if (index !== undefined && isArray(value)) { + value = value[index % value.length]; + cacheable = false; + } + if (value !== undefined) { + if (info && !cacheable) { + info.cacheable = false; + } + return value; + } + } +} + +/** + * @param minmax + * @param grace + * @param beginAtZero + * @private + */ +export function _addGrace(minmax: { min: number; max: number; }, grace: number | string, beginAtZero: boolean) { + const {min, max} = minmax; + const change = toDimension(grace, (max - min) / 2); + const keepZero = (value: number, add: number) => beginAtZero && value === 0 ? 0 : value + add; + return { + min: keepZero(min, -Math.abs(change)), + max: keepZero(max, change) + }; +} + +/** + * Create a context inheriting parentContext + * @param parentContext + * @param context + * @returns + */ +export function createContext(parentContext: null, context: T): T; +export function createContext(parentContext: P, context: T): P & T; +export function createContext(parentContext: object, context: object) { + return Object.assign(Object.create(parentContext), context); +} diff --git a/src/helpers/helpers.rtl.ts b/src/helpers/helpers.rtl.ts new file mode 100644 index 00000000000..121c6e7f540 --- /dev/null +++ b/src/helpers/helpers.rtl.ts @@ -0,0 +1,74 @@ +export interface RTLAdapter { + x(x: number): number; + setWidth(w: number): void; + textAlign(align: 'center' | 'left' | 'right'): 'center' | 'left' | 'right'; + xPlus(x: number, value: number): number; + leftForLtr(x: number, itemWidth: number): number; +} + +const getRightToLeftAdapter = function(rectX: number, width: number): RTLAdapter { + return { + x(x) { + return rectX + rectX + width - x; + }, + setWidth(w) { + width = w; + }, + textAlign(align) { + if (align === 'center') { + return align; + } + return align === 'right' ? 'left' : 'right'; + }, + xPlus(x, value) { + return x - value; + }, + leftForLtr(x, itemWidth) { + return x - itemWidth; + }, + }; +}; + +const getLeftToRightAdapter = function(): RTLAdapter { + return { + x(x) { + return x; + }, + setWidth(w) { // eslint-disable-line no-unused-vars + }, + textAlign(align) { + return align; + }, + xPlus(x, value) { + return x + value; + }, + leftForLtr(x, _itemWidth) { // eslint-disable-line @typescript-eslint/no-unused-vars + return x; + }, + }; +}; + +export function getRtlAdapter(rtl: boolean, rectX: number, width: number) { + return rtl ? getRightToLeftAdapter(rectX, width) : getLeftToRightAdapter(); +} + +export function overrideTextDirection(ctx: CanvasRenderingContext2D, direction: 'ltr' | 'rtl') { + let style: CSSStyleDeclaration, original: [string, string]; + if (direction === 'ltr' || direction === 'rtl') { + style = ctx.canvas.style; + original = [ + style.getPropertyValue('direction'), + style.getPropertyPriority('direction'), + ]; + + style.setProperty('direction', direction, 'important'); + (ctx as { prevTextDirection?: [string, string] }).prevTextDirection = original; + } +} + +export function restoreTextDirection(ctx: CanvasRenderingContext2D, original?: [string, string]) { + if (original !== undefined) { + delete (ctx as { prevTextDirection?: [string, string] }).prevTextDirection; + ctx.canvas.style.setProperty('direction', original[0], original[1]); + } +} diff --git a/src/helpers/helpers.segment.js b/src/helpers/helpers.segment.js new file mode 100644 index 00000000000..50ea8da5bc8 --- /dev/null +++ b/src/helpers/helpers.segment.js @@ -0,0 +1,364 @@ +import {_angleBetween, _angleDiff, _isBetween, _normalizeAngle} from './helpers.math.js'; +import {createContext} from './helpers.options.js'; +import {isPatternOrGradient} from './helpers.color.js'; + +/** + * @typedef { import('../elements/element.line.js').default } LineElement + * @typedef { import('../elements/element.point.js').default } PointElement + * @typedef {{start: number, end: number, loop: boolean, style?: any}} Segment + */ + +function propertyFn(property) { + if (property === 'angle') { + return { + between: _angleBetween, + compare: _angleDiff, + normalize: _normalizeAngle, + }; + } + return { + between: _isBetween, + compare: (a, b) => a - b, + normalize: x => x + }; +} + +function normalizeSegment({start, end, count, loop, style}) { + return { + start: start % count, + end: end % count, + loop: loop && (end - start + 1) % count === 0, + style + }; +} + +function getSegment(segment, points, bounds) { + const {property, start: startBound, end: endBound} = bounds; + const {between, normalize} = propertyFn(property); + const count = points.length; + // eslint-disable-next-line prefer-const + let {start, end, loop} = segment; + let i, ilen; + + if (loop) { + start += count; + end += count; + for (i = 0, ilen = count; i < ilen; ++i) { + if (!between(normalize(points[start % count][property]), startBound, endBound)) { + break; + } + start--; + end--; + } + start %= count; + end %= count; + } + + if (end < start) { + end += count; + } + return {start, end, loop, style: segment.style}; +} + +/** + * Returns the sub-segment(s) of a line segment that fall in the given bounds + * @param {object} segment + * @param {number} segment.start - start index of the segment, referring the points array + * @param {number} segment.end - end index of the segment, referring the points array + * @param {boolean} segment.loop - indicates that the segment is a loop + * @param {object} [segment.style] - segment style + * @param {PointElement[]} points - the points that this segment refers to + * @param {object} [bounds] + * @param {string} bounds.property - the property of a `PointElement` we are bounding. `x`, `y` or `angle`. + * @param {number} bounds.start - start value of the property + * @param {number} bounds.end - end value of the property + * @private + **/ +export function _boundSegment(segment, points, bounds) { + if (!bounds) { + return [segment]; + } + + const {property, start: startBound, end: endBound} = bounds; + const count = points.length; + const {compare, between, normalize} = propertyFn(property); + const {start, end, loop, style} = getSegment(segment, points, bounds); + + const result = []; + let inside = false; + let subStart = null; + let value, point, prevValue; + + const startIsBefore = () => between(startBound, prevValue, value) && compare(startBound, prevValue) !== 0; + const endIsBefore = () => compare(endBound, value) === 0 || between(endBound, prevValue, value); + const shouldStart = () => inside || startIsBefore(); + const shouldStop = () => !inside || endIsBefore(); + + for (let i = start, prev = start; i <= end; ++i) { + point = points[i % count]; + + if (point.skip) { + continue; + } + + value = normalize(point[property]); + + if (value === prevValue) { + continue; + } + + inside = between(value, startBound, endBound); + + if (subStart === null && shouldStart()) { + subStart = compare(value, startBound) === 0 ? i : prev; + } + + if (subStart !== null && shouldStop()) { + result.push(normalizeSegment({start: subStart, end: i, loop, count, style})); + subStart = null; + } + prev = i; + prevValue = value; + } + + if (subStart !== null) { + result.push(normalizeSegment({start: subStart, end, loop, count, style})); + } + + return result; +} + + +/** + * Returns the segments of the line that are inside given bounds + * @param {LineElement} line + * @param {object} [bounds] + * @param {string} bounds.property - the property we are bounding with. `x`, `y` or `angle`. + * @param {number} bounds.start - start value of the `property` + * @param {number} bounds.end - end value of the `property` + * @private + */ +export function _boundSegments(line, bounds) { + const result = []; + const segments = line.segments; + + for (let i = 0; i < segments.length; i++) { + const sub = _boundSegment(segments[i], line.points, bounds); + if (sub.length) { + result.push(...sub); + } + } + return result; +} + +/** + * Find start and end index of a line. + */ +function findStartAndEnd(points, count, loop, spanGaps) { + let start = 0; + let end = count - 1; + + if (loop && !spanGaps) { + // loop and not spanning gaps, first find a gap to start from + while (start < count && !points[start].skip) { + start++; + } + } + + // find first non skipped point (after the first gap possibly) + while (start < count && points[start].skip) { + start++; + } + + // if we looped to count, start needs to be 0 + start %= count; + + if (loop) { + // loop will go past count, if start > 0 + end += start; + } + + while (end > start && points[end % count].skip) { + end--; + } + + // end could be more than count, normalize + end %= count; + + return {start, end}; +} + +/** + * Compute solid segments from Points, when spanGaps === false + * @param {PointElement[]} points - the points + * @param {number} start - start index + * @param {number} max - max index (can go past count on a loop) + * @param {boolean} loop - boolean indicating that this would be a loop if no gaps are found + */ +function solidSegments(points, start, max, loop) { + const count = points.length; + const result = []; + let last = start; + let prev = points[start]; + let end; + + for (end = start + 1; end <= max; ++end) { + const cur = points[end % count]; + if (cur.skip || cur.stop) { + if (!prev.skip) { + loop = false; + result.push({start: start % count, end: (end - 1) % count, loop}); + // @ts-ignore + start = last = cur.stop ? end : null; + } + } else { + last = end; + if (prev.skip) { + start = end; + } + } + prev = cur; + } + + if (last !== null) { + result.push({start: start % count, end: last % count, loop}); + } + + return result; +} + +/** + * Compute the continuous segments that define the whole line + * There can be skipped points within a segment, if spanGaps is true. + * @param {LineElement} line + * @param {object} [segmentOptions] + * @return {Segment[]} + * @private + */ +export function _computeSegments(line, segmentOptions) { + const points = line.points; + const spanGaps = line.options.spanGaps; + const count = points.length; + + if (!count) { + return []; + } + + const loop = !!line._loop; + const {start, end} = findStartAndEnd(points, count, loop, spanGaps); + + if (spanGaps === true) { + return splitByStyles(line, [{start, end, loop}], points, segmentOptions); + } + + const max = end < start ? end + count : end; + const completeLoop = !!line._fullLoop && start === 0 && end === count - 1; + return splitByStyles(line, solidSegments(points, start, max, completeLoop), points, segmentOptions); +} + +/** + * @param {Segment[]} segments + * @param {PointElement[]} points + * @param {object} [segmentOptions] + * @return {Segment[]} + */ +function splitByStyles(line, segments, points, segmentOptions) { + if (!segmentOptions || !segmentOptions.setContext || !points) { + return segments; + } + return doSplitByStyles(line, segments, points, segmentOptions); +} + +/** + * @param {LineElement} line + * @param {Segment[]} segments + * @param {PointElement[]} points + * @param {object} [segmentOptions] + * @return {Segment[]} + */ +function doSplitByStyles(line, segments, points, segmentOptions) { + const chartContext = line._chart.getContext(); + const baseStyle = readStyle(line.options); + const {_datasetIndex: datasetIndex, options: {spanGaps}} = line; + const count = points.length; + const result = []; + let prevStyle = baseStyle; + let start = segments[0].start; + let i = start; + + function addStyle(s, e, l, st) { + const dir = spanGaps ? -1 : 1; + if (s === e) { + return; + } + // Style can not start/end on a skipped point, adjust indices accordingly + s += count; + while (points[s % count].skip) { + s -= dir; + } + while (points[e % count].skip) { + e += dir; + } + if (s % count !== e % count) { + result.push({start: s % count, end: e % count, loop: l, style: st}); + prevStyle = st; + start = e % count; + } + } + + for (const segment of segments) { + start = spanGaps ? start : segment.start; + let prev = points[start % count]; + let style; + for (i = start + 1; i <= segment.end; i++) { + const pt = points[i % count]; + style = readStyle(segmentOptions.setContext(createContext(chartContext, { + type: 'segment', + p0: prev, + p1: pt, + p0DataIndex: (i - 1) % count, + p1DataIndex: i % count, + datasetIndex + }))); + if (styleChanged(style, prevStyle)) { + addStyle(start, i - 1, segment.loop, prevStyle); + } + prev = pt; + prevStyle = style; + } + if (start < i - 1) { + addStyle(start, i - 1, segment.loop, prevStyle); + } + } + + return result; +} + +function readStyle(options) { + return { + backgroundColor: options.backgroundColor, + borderCapStyle: options.borderCapStyle, + borderDash: options.borderDash, + borderDashOffset: options.borderDashOffset, + borderJoinStyle: options.borderJoinStyle, + borderWidth: options.borderWidth, + borderColor: options.borderColor + }; +} + +function styleChanged(style, prevStyle) { + if (!prevStyle) { + return false; + } + const cache = []; + const replacer = function(key, value) { + if (!isPatternOrGradient(value)) { + return value; + } + if (!cache.includes(value)) { + cache.push(value); + } + return cache.indexOf(value); + }; + return JSON.stringify(style, replacer) !== JSON.stringify(prevStyle, replacer); +} diff --git a/src/helpers/index.ts b/src/helpers/index.ts new file mode 100644 index 00000000000..9fde7b85951 --- /dev/null +++ b/src/helpers/index.ts @@ -0,0 +1,16 @@ +export * from './helpers.color.js'; +export * from './helpers.core.js'; +export * from './helpers.canvas.js'; +export * from './helpers.collection.js'; +export * from './helpers.config.js'; +export * from './helpers.curve.js'; +export * from './helpers.dom.js'; +export {default as easingEffects} from './helpers.easing.js'; +export * from './helpers.extras.js'; +export * from './helpers.interpolation.js'; +export * from './helpers.intl.js'; +export * from './helpers.options.js'; +export * from './helpers.math.js'; +export * from './helpers.rtl.js'; +export * from './helpers.segment.js'; +export * from './helpers.dataset.js'; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 00000000000..940af6f86e3 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,25 @@ +export * from './controllers/index.js'; +export * from './core/index.js'; +export * from './elements/index.js'; +export * from './platform/index.js'; +export * from './plugins/index.js'; +export * from './scales/index.js'; + +import * as controllers from './controllers/index.js'; +import * as elements from './elements/index.js'; +import * as plugins from './plugins/index.js'; +import * as scales from './scales/index.js'; + +export { + controllers, + elements, + plugins, + scales, +}; + +export const registerables = [ + controllers, + elements, + plugins, + scales, +]; diff --git a/src/index.umd.ts b/src/index.umd.ts new file mode 100644 index 00000000000..f830a013072 --- /dev/null +++ b/src/index.umd.ts @@ -0,0 +1,54 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck + +/** + * @namespace Chart + */ +import Chart from './core/core.controller.js'; + +import * as helpers from './helpers/index.js'; +import _adapters from './core/core.adapters.js'; +import Animation from './core/core.animation.js'; +import animator from './core/core.animator.js'; +import Animations from './core/core.animations.js'; +import * as controllers from './controllers/index.js'; +import DatasetController from './core/core.datasetController.js'; +import Element from './core/core.element.js'; +import * as elements from './elements/index.js'; +import Interaction from './core/core.interaction.js'; +import layouts from './core/core.layouts.js'; +import * as platforms from './platform/index.js'; +import * as plugins from './plugins/index.js'; +import registry from './core/core.registry.js'; +import Scale from './core/core.scale.js'; +import * as scales from './scales/index.js'; +import Ticks from './core/core.ticks.js'; + +// Register built-ins +Chart.register(controllers, scales, elements, plugins); + +Chart.helpers = {...helpers}; +Chart._adapters = _adapters; +Chart.Animation = Animation; +Chart.Animations = Animations; +Chart.animator = animator; +Chart.controllers = registry.controllers.items; +Chart.DatasetController = DatasetController; +Chart.Element = Element; +Chart.elements = elements; +Chart.Interaction = Interaction; +Chart.layouts = layouts; +Chart.platforms = platforms; +Chart.Scale = Scale; +Chart.Ticks = Ticks; + +// Compatibility with ESM extensions +Object.assign(Chart, controllers, scales, elements, plugins, platforms); +Chart.Chart = Chart; + +if (typeof window !== 'undefined') { + window.Chart = Chart; +} + +export default Chart; + diff --git a/src/platform/index.js b/src/platform/index.js new file mode 100644 index 00000000000..1c0fd9d66ee --- /dev/null +++ b/src/platform/index.js @@ -0,0 +1,13 @@ +import {_isDomSupported} from '../helpers/index.js'; +import BasePlatform from './platform.base.js'; +import BasicPlatform from './platform.basic.js'; +import DomPlatform from './platform.dom.js'; + +export function _detectPlatform(canvas) { + if (!_isDomSupported() || (typeof OffscreenCanvas !== 'undefined' && canvas instanceof OffscreenCanvas)) { + return BasicPlatform; + } + return DomPlatform; +} + +export {BasePlatform, BasicPlatform, DomPlatform}; diff --git a/src/platform/platform.base.js b/src/platform/platform.base.js new file mode 100644 index 00000000000..298e4088342 --- /dev/null +++ b/src/platform/platform.base.js @@ -0,0 +1,83 @@ + +/** + * @typedef { import('../core/core.controller.js').default } Chart + */ + +/** + * Abstract class that allows abstracting platform dependencies away from the chart. + */ +export default class BasePlatform { + /** + * Called at chart construction time, returns a context2d instance implementing + * the [W3C Canvas 2D Context API standard]{@link https://www.w3.org/TR/2dcontext/}. + * @param {HTMLCanvasElement} canvas - The canvas from which to acquire context (platform specific) + * @param {number} [aspectRatio] - The chart options + */ + acquireContext(canvas, aspectRatio) {} // eslint-disable-line no-unused-vars + + /** + * Called at chart destruction time, releases any resources associated to the context + * previously returned by the acquireContext() method. + * @param {CanvasRenderingContext2D} context - The context2d instance + * @returns {boolean} true if the method succeeded, else false + */ + releaseContext(context) { // eslint-disable-line no-unused-vars + return false; + } + + /** + * Registers the specified listener on the given chart. + * @param {Chart} chart - Chart from which to listen for event + * @param {string} type - The ({@link ChartEvent}) type to listen for + * @param {function} listener - Receives a notification (an object that implements + * the {@link ChartEvent} interface) when an event of the specified type occurs. + */ + addEventListener(chart, type, listener) {} // eslint-disable-line no-unused-vars + + /** + * Removes the specified listener previously registered with addEventListener. + * @param {Chart} chart - Chart from which to remove the listener + * @param {string} type - The ({@link ChartEvent}) type to remove + * @param {function} listener - The listener function to remove from the event target. + */ + removeEventListener(chart, type, listener) {} // eslint-disable-line no-unused-vars + + /** + * @returns {number} the current devicePixelRatio of the device this platform is connected to. + */ + getDevicePixelRatio() { + return 1; + } + + /** + * Returns the maximum size in pixels of given canvas element. + * @param {HTMLCanvasElement} element + * @param {number} [width] - content width of parent element + * @param {number} [height] - content height of parent element + * @param {number} [aspectRatio] - aspect ratio to maintain + */ + getMaximumSize(element, width, height, aspectRatio) { + width = Math.max(0, width || element.width); + height = height || element.height; + return { + width, + height: Math.max(0, aspectRatio ? Math.floor(width / aspectRatio) : height) + }; + } + + /** + * @param {HTMLCanvasElement} canvas + * @returns {boolean} true if the canvas is attached to the platform, false if not. + */ + isAttached(canvas) { // eslint-disable-line no-unused-vars + return true; + } + + /** + * Updates config with platform specific requirements + * @param {import('../core/core.config.js').default} config + */ + updateConfig(config) { // eslint-disable-line no-unused-vars + // no-op + } +} diff --git a/src/platform/platform.basic.js b/src/platform/platform.basic.js new file mode 100644 index 00000000000..04e0bee943a --- /dev/null +++ b/src/platform/platform.basic.js @@ -0,0 +1,23 @@ +/** + * Platform fallback implementation (minimal). + * @see https://github.com/chartjs/Chart.js/pull/4591#issuecomment-319575939 + */ + +import BasePlatform from './platform.base.js'; + +/** + * Platform class for charts without access to the DOM or to many element properties + * This platform is used by default for any chart passed an OffscreenCanvas. + * @extends BasePlatform + */ +export default class BasicPlatform extends BasePlatform { + acquireContext(item) { + // To prevent canvas fingerprinting, some add-ons undefine the getContext + // method, for example: https://github.com/kkapsner/CanvasBlocker + // https://github.com/chartjs/Chart.js/issues/2807 + return item && item.getContext && item.getContext('2d') || null; + } + updateConfig(config) { + config.options.animation = false; + } +} diff --git a/src/platform/platform.dom.js b/src/platform/platform.dom.js new file mode 100644 index 00000000000..4c6f72e425e --- /dev/null +++ b/src/platform/platform.dom.js @@ -0,0 +1,389 @@ +/** + * Chart.Platform implementation for targeting a web browser + */ + +import BasePlatform from './platform.base.js'; +import {_getParentNode, getRelativePosition, supportsEventListenerOptions, readUsedSize, getMaximumSize} from '../helpers/helpers.dom.js'; +import {throttled} from '../helpers/helpers.extras.js'; +import {isNullOrUndef} from '../helpers/helpers.core.js'; + +/** + * @typedef { import('../core/core.controller.js').default } Chart + */ + +const EXPANDO_KEY = '$chartjs'; + +/** + * DOM event types -> Chart.js event types. + * Note: only events with different types are mapped. + * @see https://developer.mozilla.org/en-US/docs/Web/Events + */ +const EVENT_TYPES = { + touchstart: 'mousedown', + touchmove: 'mousemove', + touchend: 'mouseup', + pointerenter: 'mouseenter', + pointerdown: 'mousedown', + pointermove: 'mousemove', + pointerup: 'mouseup', + pointerleave: 'mouseout', + pointerout: 'mouseout' +}; + +const isNullOrEmpty = value => value === null || value === ''; +/** + * Initializes the canvas style and render size without modifying the canvas display size, + * since responsiveness is handled by the controller.resize() method. The config is used + * to determine the aspect ratio to apply in case no explicit height has been specified. + * @param {HTMLCanvasElement} canvas + * @param {number} [aspectRatio] + */ +function initCanvas(canvas, aspectRatio) { + const style = canvas.style; + + // NOTE(SB) canvas.getAttribute('width') !== canvas.width: in the first case it + // returns null or '' if no explicit value has been set to the canvas attribute. + const renderHeight = canvas.getAttribute('height'); + const renderWidth = canvas.getAttribute('width'); + + // Chart.js modifies some canvas values that we want to restore on destroy + canvas[EXPANDO_KEY] = { + initial: { + height: renderHeight, + width: renderWidth, + style: { + display: style.display, + height: style.height, + width: style.width + } + } + }; + + // Force canvas to display as block to avoid extra space caused by inline + // elements, which would interfere with the responsive resize process. + // https://github.com/chartjs/Chart.js/issues/2538 + style.display = style.display || 'block'; + // Include possible borders in the size + style.boxSizing = style.boxSizing || 'border-box'; + + if (isNullOrEmpty(renderWidth)) { + const displayWidth = readUsedSize(canvas, 'width'); + if (displayWidth !== undefined) { + canvas.width = displayWidth; + } + } + + if (isNullOrEmpty(renderHeight)) { + if (canvas.style.height === '') { + // If no explicit render height and style height, let's apply the aspect ratio, + // which one can be specified by the user but also by charts as default option + // (i.e. options.aspectRatio). If not specified, use canvas aspect ratio of 2. + canvas.height = canvas.width / (aspectRatio || 2); + } else { + const displayHeight = readUsedSize(canvas, 'height'); + if (displayHeight !== undefined) { + canvas.height = displayHeight; + } + } + } + + return canvas; +} + +// Default passive to true as expected by Chrome for 'touchstart' and 'touchend' events. +// https://github.com/chartjs/Chart.js/issues/4287 +const eventListenerOptions = supportsEventListenerOptions ? {passive: true} : false; + +function addListener(node, type, listener) { + if (node) { + node.addEventListener(type, listener, eventListenerOptions); + } +} + +function removeListener(chart, type, listener) { + if (chart && chart.canvas) { + chart.canvas.removeEventListener(type, listener, eventListenerOptions); + } +} + +function fromNativeEvent(event, chart) { + const type = EVENT_TYPES[event.type] || event.type; + const {x, y} = getRelativePosition(event, chart); + return { + type, + chart, + native: event, + x: x !== undefined ? x : null, + y: y !== undefined ? y : null, + }; +} + +function nodeListContains(nodeList, canvas) { + for (const node of nodeList) { + if (node === canvas || node.contains(canvas)) { + return true; + } + } +} + +function createAttachObserver(chart, type, listener) { + const canvas = chart.canvas; + const observer = new MutationObserver(entries => { + let trigger = false; + for (const entry of entries) { + trigger = trigger || nodeListContains(entry.addedNodes, canvas); + trigger = trigger && !nodeListContains(entry.removedNodes, canvas); + } + if (trigger) { + listener(); + } + }); + observer.observe(document, {childList: true, subtree: true}); + return observer; +} + +function createDetachObserver(chart, type, listener) { + const canvas = chart.canvas; + const observer = new MutationObserver(entries => { + let trigger = false; + for (const entry of entries) { + trigger = trigger || nodeListContains(entry.removedNodes, canvas); + trigger = trigger && !nodeListContains(entry.addedNodes, canvas); + } + if (trigger) { + listener(); + } + }); + observer.observe(document, {childList: true, subtree: true}); + return observer; +} + +const drpListeningCharts = new Map(); +let oldDevicePixelRatio = 0; + +function onWindowResize() { + const dpr = window.devicePixelRatio; + if (dpr === oldDevicePixelRatio) { + return; + } + oldDevicePixelRatio = dpr; + drpListeningCharts.forEach((resize, chart) => { + if (chart.currentDevicePixelRatio !== dpr) { + resize(); + } + }); +} + +function listenDevicePixelRatioChanges(chart, resize) { + if (!drpListeningCharts.size) { + window.addEventListener('resize', onWindowResize); + } + drpListeningCharts.set(chart, resize); +} + +function unlistenDevicePixelRatioChanges(chart) { + drpListeningCharts.delete(chart); + if (!drpListeningCharts.size) { + window.removeEventListener('resize', onWindowResize); + } +} + +function createResizeObserver(chart, type, listener) { + const canvas = chart.canvas; + const container = canvas && _getParentNode(canvas); + if (!container) { + return; + } + const resize = throttled((width, height) => { + const w = container.clientWidth; + listener(width, height); + if (w < container.clientWidth) { + // If the container size shrank during chart resize, let's assume + // scrollbar appeared. So we resize again with the scrollbar visible - + // effectively making chart smaller and the scrollbar hidden again. + // Because we are inside `throttled`, and currently `ticking`, scroll + // events are ignored during this whole 2 resize process. + // If we assumed wrong and something else happened, we are resizing + // twice in a frame (potential performance issue) + listener(); + } + }, window); + + // @ts-ignore until https://github.com/microsoft/TypeScript/issues/37861 implemented + const observer = new ResizeObserver(entries => { + const entry = entries[0]; + const width = entry.contentRect.width; + const height = entry.contentRect.height; + // When its container's display is set to 'none' the callback will be called with a + // size of (0, 0), which will cause the chart to lose its original height, so skip + // resizing in such case. + if (width === 0 && height === 0) { + return; + } + resize(width, height); + }); + observer.observe(container); + listenDevicePixelRatioChanges(chart, resize); + + return observer; +} + +function releaseObserver(chart, type, observer) { + if (observer) { + observer.disconnect(); + } + if (type === 'resize') { + unlistenDevicePixelRatioChanges(chart); + } +} + +function createProxyAndListen(chart, type, listener) { + const canvas = chart.canvas; + const proxy = throttled((event) => { + // This case can occur if the chart is destroyed while waiting + // for the throttled function to occur. We prevent crashes by checking + // for a destroyed chart + if (chart.ctx !== null) { + listener(fromNativeEvent(event, chart)); + } + }, chart); + + addListener(canvas, type, proxy); + + return proxy; +} + +/** + * Platform class for charts that can access the DOM and global window/document properties + * @extends BasePlatform + */ +export default class DomPlatform extends BasePlatform { + + /** + * @param {HTMLCanvasElement} canvas + * @param {number} [aspectRatio] + * @return {CanvasRenderingContext2D|null} + */ + acquireContext(canvas, aspectRatio) { + // To prevent canvas fingerprinting, some add-ons undefine the getContext + // method, for example: https://github.com/kkapsner/CanvasBlocker + // https://github.com/chartjs/Chart.js/issues/2807 + const context = canvas && canvas.getContext && canvas.getContext('2d'); + + // `instanceof HTMLCanvasElement/CanvasRenderingContext2D` fails when the canvas is + // inside an iframe or when running in a protected environment. We could guess the + // types from their toString() value but let's keep things flexible and assume it's + // a sufficient condition if the canvas has a context2D which has canvas as `canvas`. + // https://github.com/chartjs/Chart.js/issues/3887 + // https://github.com/chartjs/Chart.js/issues/4102 + // https://github.com/chartjs/Chart.js/issues/4152 + if (context && context.canvas === canvas) { + // Load platform resources on first chart creation, to make it possible to + // import the library before setting platform options. + initCanvas(canvas, aspectRatio); + return context; + } + + return null; + } + + /** + * @param {CanvasRenderingContext2D} context + */ + releaseContext(context) { + const canvas = context.canvas; + if (!canvas[EXPANDO_KEY]) { + return false; + } + + const initial = canvas[EXPANDO_KEY].initial; + ['height', 'width'].forEach((prop) => { + const value = initial[prop]; + if (isNullOrUndef(value)) { + canvas.removeAttribute(prop); + } else { + canvas.setAttribute(prop, value); + } + }); + + const style = initial.style || {}; + Object.keys(style).forEach((key) => { + canvas.style[key] = style[key]; + }); + + // The canvas render size might have been changed (and thus the state stack discarded), + // we can't use save() and restore() to restore the initial state. So make sure that at + // least the canvas context is reset to the default state by setting the canvas width. + // https://www.w3.org/TR/2011/WD-html5-20110525/the-canvas-element.html + // eslint-disable-next-line no-self-assign + canvas.width = canvas.width; + + delete canvas[EXPANDO_KEY]; + return true; + } + + /** + * + * @param {Chart} chart + * @param {string} type + * @param {function} listener + */ + addEventListener(chart, type, listener) { + // Can have only one listener per type, so make sure previous is removed + this.removeEventListener(chart, type); + + const proxies = chart.$proxies || (chart.$proxies = {}); + const handlers = { + attach: createAttachObserver, + detach: createDetachObserver, + resize: createResizeObserver + }; + const handler = handlers[type] || createProxyAndListen; + proxies[type] = handler(chart, type, listener); + } + + + /** + * @param {Chart} chart + * @param {string} type + */ + removeEventListener(chart, type) { + const proxies = chart.$proxies || (chart.$proxies = {}); + const proxy = proxies[type]; + + if (!proxy) { + return; + } + + const handlers = { + attach: releaseObserver, + detach: releaseObserver, + resize: releaseObserver + }; + const handler = handlers[type] || removeListener; + handler(chart, type, proxy); + proxies[type] = undefined; + } + + getDevicePixelRatio() { + return window.devicePixelRatio; + } + + /** + * @param {HTMLCanvasElement} canvas + * @param {number} [width] - content width of parent element + * @param {number} [height] - content height of parent element + * @param {number} [aspectRatio] - aspect ratio to maintain + */ + getMaximumSize(canvas, width, height, aspectRatio) { + return getMaximumSize(canvas, width, height, aspectRatio); + } + + /** + * @param {HTMLCanvasElement} canvas + */ + isAttached(canvas) { + const container = canvas && _getParentNode(canvas); + return !!(container && container.isConnected); + } +} diff --git a/src/plugins/index.js b/src/plugins/index.js new file mode 100644 index 00000000000..f6233705257 --- /dev/null +++ b/src/plugins/index.js @@ -0,0 +1,7 @@ +export {default as Colors} from './plugin.colors.js'; +export {default as Decimation} from './plugin.decimation.js'; +export {default as Filler} from './plugin.filler/index.js'; +export {default as Legend} from './plugin.legend.js'; +export {default as SubTitle} from './plugin.subtitle.js'; +export {default as Title} from './plugin.title.js'; +export {default as Tooltip} from './plugin.tooltip.js'; diff --git a/src/plugins/plugin.colors.ts b/src/plugins/plugin.colors.ts new file mode 100644 index 00000000000..a32998a95dc --- /dev/null +++ b/src/plugins/plugin.colors.ts @@ -0,0 +1,127 @@ +import {DoughnutController, PolarAreaController, defaults} from '../index.js'; +import type {Chart, ChartDataset} from '../types.js'; + +export interface ColorsPluginOptions { + enabled?: boolean; + forceOverride?: boolean; +} + +interface ColorsDescriptor { + backgroundColor?: unknown; + borderColor?: unknown; +} + +const BORDER_COLORS = [ + 'rgb(54, 162, 235)', // blue + 'rgb(255, 99, 132)', // red + 'rgb(255, 159, 64)', // orange + 'rgb(255, 205, 86)', // yellow + 'rgb(75, 192, 192)', // green + 'rgb(153, 102, 255)', // purple + 'rgb(201, 203, 207)' // grey +]; + +// Border colors with 50% transparency +const BACKGROUND_COLORS = /* #__PURE__ */ BORDER_COLORS.map(color => color.replace('rgb(', 'rgba(').replace(')', ', 0.5)')); + +function getBorderColor(i: number) { + return BORDER_COLORS[i % BORDER_COLORS.length]; +} + +function getBackgroundColor(i: number) { + return BACKGROUND_COLORS[i % BACKGROUND_COLORS.length]; +} + +function colorizeDefaultDataset(dataset: ChartDataset, i: number) { + dataset.borderColor = getBorderColor(i); + dataset.backgroundColor = getBackgroundColor(i); + + return ++i; +} + +function colorizeDoughnutDataset(dataset: ChartDataset, i: number) { + dataset.backgroundColor = dataset.data.map(() => getBorderColor(i++)); + + return i; +} + +function colorizePolarAreaDataset(dataset: ChartDataset, i: number) { + dataset.backgroundColor = dataset.data.map(() => getBackgroundColor(i++)); + + return i; +} + +function getColorizer(chart: Chart) { + let i = 0; + + return (dataset: ChartDataset, datasetIndex: number) => { + const controller = chart.getDatasetMeta(datasetIndex).controller; + + if (controller instanceof DoughnutController) { + i = colorizeDoughnutDataset(dataset, i); + } else if (controller instanceof PolarAreaController) { + i = colorizePolarAreaDataset(dataset, i); + } else if (controller) { + i = colorizeDefaultDataset(dataset, i); + } + }; +} + +function containsColorsDefinitions( + descriptors: ColorsDescriptor[] | Record +) { + let k: number | string; + + for (k in descriptors) { + if (descriptors[k].borderColor || descriptors[k].backgroundColor) { + return true; + } + } + + return false; +} + +function containsColorsDefinition( + descriptor: ColorsDescriptor +) { + return descriptor && (descriptor.borderColor || descriptor.backgroundColor); +} + +function containsDefaultColorsDefenitions() { + return defaults.borderColor !== 'rgba(0,0,0,0.1)' || defaults.backgroundColor !== 'rgba(0,0,0,0.1)'; +} + +export default { + id: 'colors', + + defaults: { + enabled: true, + forceOverride: false + } as ColorsPluginOptions, + + beforeLayout(chart: Chart, _args, options: ColorsPluginOptions) { + if (!options.enabled) { + return; + } + + const { + data: {datasets}, + options: chartOptions + } = chart.config; + const {elements} = chartOptions; + + const containsColorDefenition = ( + containsColorsDefinitions(datasets) || + containsColorsDefinition(chartOptions) || + (elements && containsColorsDefinitions(elements)) || + containsDefaultColorsDefenitions()); + + if (!options.forceOverride && containsColorDefenition) { + return; + } + + const colorizer = getColorizer(chart); + + datasets.forEach(colorizer); + } +}; diff --git a/src/plugins/plugin.decimation.js b/src/plugins/plugin.decimation.js new file mode 100644 index 00000000000..2c2a19a375a --- /dev/null +++ b/src/plugins/plugin.decimation.js @@ -0,0 +1,287 @@ +import {_limitValue, _lookupByKey, isNullOrUndef, resolve} from '../helpers/index.js'; + +function lttbDecimation(data, start, count, availableWidth, options) { + /** + * Implementation of the Largest Triangle Three Buckets algorithm. + * + * This implementation is based on the original implementation by Sveinn Steinarsson + * in https://github.com/sveinn-steinarsson/flot-downsample/blob/master/jquery.flot.downsample.js + * + * The original implementation is MIT licensed. + */ + const samples = options.samples || availableWidth; + // There are less points than the threshold, returning the whole array + if (samples >= count) { + return data.slice(start, start + count); + } + + const decimated = []; + + const bucketWidth = (count - 2) / (samples - 2); + let sampledIndex = 0; + const endIndex = start + count - 1; + // Starting from offset + let a = start; + let i, maxAreaPoint, maxArea, area, nextA; + + decimated[sampledIndex++] = data[a]; + + for (i = 0; i < samples - 2; i++) { + let avgX = 0; + let avgY = 0; + let j; + + // Adding offset + const avgRangeStart = Math.floor((i + 1) * bucketWidth) + 1 + start; + const avgRangeEnd = Math.min(Math.floor((i + 2) * bucketWidth) + 1, count) + start; + const avgRangeLength = avgRangeEnd - avgRangeStart; + + for (j = avgRangeStart; j < avgRangeEnd; j++) { + avgX += data[j].x; + avgY += data[j].y; + } + + avgX /= avgRangeLength; + avgY /= avgRangeLength; + + // Adding offset + const rangeOffs = Math.floor(i * bucketWidth) + 1 + start; + const rangeTo = Math.min(Math.floor((i + 1) * bucketWidth) + 1, count) + start; + const {x: pointAx, y: pointAy} = data[a]; + + // Note that this is changed from the original algorithm which initializes these + // values to 1. The reason for this change is that if the area is small, nextA + // would never be set and thus a crash would occur in the next loop as `a` would become + // `undefined`. Since the area is always positive, but could be 0 in the case of a flat trace, + // initializing with a negative number is the correct solution. + maxArea = area = -1; + + for (j = rangeOffs; j < rangeTo; j++) { + area = 0.5 * Math.abs( + (pointAx - avgX) * (data[j].y - pointAy) - + (pointAx - data[j].x) * (avgY - pointAy) + ); + + if (area > maxArea) { + maxArea = area; + maxAreaPoint = data[j]; + nextA = j; + } + } + + decimated[sampledIndex++] = maxAreaPoint; + a = nextA; + } + + // Include the last point + decimated[sampledIndex++] = data[endIndex]; + + return decimated; +} + +function minMaxDecimation(data, start, count, availableWidth) { + let avgX = 0; + let countX = 0; + let i, point, x, y, prevX, minIndex, maxIndex, startIndex, minY, maxY; + const decimated = []; + const endIndex = start + count - 1; + + const xMin = data[start].x; + const xMax = data[endIndex].x; + const dx = xMax - xMin; + + for (i = start; i < start + count; ++i) { + point = data[i]; + x = (point.x - xMin) / dx * availableWidth; + y = point.y; + const truncX = x | 0; + + if (truncX === prevX) { + // Determine `minY` / `maxY` and `avgX` while we stay within same x-position + if (y < minY) { + minY = y; + minIndex = i; + } else if (y > maxY) { + maxY = y; + maxIndex = i; + } + // For first point in group, countX is `0`, so average will be `x` / 1. + // Use point.x here because we're computing the average data `x` value + avgX = (countX * avgX + point.x) / ++countX; + } else { + // Push up to 4 points, 3 for the last interval and the first point for this interval + const lastIndex = i - 1; + + if (!isNullOrUndef(minIndex) && !isNullOrUndef(maxIndex)) { + // The interval is defined by 4 points: start, min, max, end. + // The starting point is already considered at this point, so we need to determine which + // of the other points to add. We need to sort these points to ensure the decimated data + // is still sorted and then ensure there are no duplicates. + const intermediateIndex1 = Math.min(minIndex, maxIndex); + const intermediateIndex2 = Math.max(minIndex, maxIndex); + + if (intermediateIndex1 !== startIndex && intermediateIndex1 !== lastIndex) { + decimated.push({ + ...data[intermediateIndex1], + x: avgX, + }); + } + if (intermediateIndex2 !== startIndex && intermediateIndex2 !== lastIndex) { + decimated.push({ + ...data[intermediateIndex2], + x: avgX + }); + } + } + + // lastIndex === startIndex will occur when a range has only 1 point which could + // happen with very uneven data + if (i > 0 && lastIndex !== startIndex) { + // Last point in the previous interval + decimated.push(data[lastIndex]); + } + + // Start of the new interval + decimated.push(point); + prevX = truncX; + countX = 0; + minY = maxY = y; + minIndex = maxIndex = startIndex = i; + } + } + + return decimated; +} + +function cleanDecimatedDataset(dataset) { + if (dataset._decimated) { + const data = dataset._data; + delete dataset._decimated; + delete dataset._data; + Object.defineProperty(dataset, 'data', { + configurable: true, + enumerable: true, + writable: true, + value: data, + }); + } +} + +function cleanDecimatedData(chart) { + chart.data.datasets.forEach((dataset) => { + cleanDecimatedDataset(dataset); + }); +} + +function getStartAndCountOfVisiblePointsSimplified(meta, points) { + const pointCount = points.length; + + let start = 0; + let count; + + const {iScale} = meta; + const {min, max, minDefined, maxDefined} = iScale.getUserBounds(); + + if (minDefined) { + start = _limitValue(_lookupByKey(points, iScale.axis, min).lo, 0, pointCount - 1); + } + if (maxDefined) { + count = _limitValue(_lookupByKey(points, iScale.axis, max).hi + 1, start, pointCount) - start; + } else { + count = pointCount - start; + } + + return {start, count}; +} + +export default { + id: 'decimation', + + defaults: { + algorithm: 'min-max', + enabled: false, + }, + + beforeElementsUpdate: (chart, args, options) => { + if (!options.enabled) { + // The decimation plugin may have been previously enabled. Need to remove old `dataset._data` handlers + cleanDecimatedData(chart); + return; + } + + // Assume the entire chart is available to show a few more points than needed + const availableWidth = chart.width; + + chart.data.datasets.forEach((dataset, datasetIndex) => { + const {_data, indexAxis} = dataset; + const meta = chart.getDatasetMeta(datasetIndex); + const data = _data || dataset.data; + + if (resolve([indexAxis, chart.options.indexAxis]) === 'y') { + // Decimation is only supported for lines that have an X indexAxis + return; + } + + if (!meta.controller.supportsDecimation) { + // Only line datasets are supported + return; + } + + const xAxis = chart.scales[meta.xAxisID]; + if (xAxis.type !== 'linear' && xAxis.type !== 'time') { + // Only linear interpolation is supported + return; + } + + if (chart.options.parsing) { + // Plugin only supports data that does not need parsing + return; + } + + let {start, count} = getStartAndCountOfVisiblePointsSimplified(meta, data); + const threshold = options.threshold || 4 * availableWidth; + if (count <= threshold) { + // No decimation is required until we are above this threshold + cleanDecimatedDataset(dataset); + return; + } + + if (isNullOrUndef(_data)) { + // First time we are seeing this dataset + // We override the 'data' property with a setter that stores the + // raw data in _data, but reads the decimated data from _decimated + dataset._data = data; + delete dataset.data; + Object.defineProperty(dataset, 'data', { + configurable: true, + enumerable: true, + get: function() { + return this._decimated; + }, + set: function(d) { + this._data = d; + } + }); + } + + // Point the chart to the decimated data + let decimated; + switch (options.algorithm) { + case 'lttb': + decimated = lttbDecimation(data, start, count, availableWidth, options); + break; + case 'min-max': + decimated = minMaxDecimation(data, start, count, availableWidth); + break; + default: + throw new Error(`Unsupported decimation algorithm '${options.algorithm}'`); + } + + dataset._decimated = decimated; + }); + }, + + destroy(chart) { + cleanDecimatedData(chart); + } +}; diff --git a/src/plugins/plugin.filler/filler.drawing.js b/src/plugins/plugin.filler/filler.drawing.js new file mode 100644 index 00000000000..0a718355da8 --- /dev/null +++ b/src/plugins/plugin.filler/filler.drawing.js @@ -0,0 +1,187 @@ +import {clipArea, unclipArea, getDatasetClipArea} from '../../helpers/index.js'; +import {_findSegmentEnd, _getBounds, _segments} from './filler.segment.js'; +import {_getTarget} from './filler.target.js'; + +export function _drawfill(ctx, source, area) { + const target = _getTarget(source); + const {chart, index, line, scale, axis} = source; + const lineOpts = line.options; + const fillOption = lineOpts.fill; + const color = lineOpts.backgroundColor; + const {above = color, below = color} = fillOption || {}; + const meta = chart.getDatasetMeta(index); + const clip = getDatasetClipArea(chart, meta); + if (target && line.points.length) { + clipArea(ctx, area); + doFill(ctx, {line, target, above, below, area, scale, axis, clip}); + unclipArea(ctx); + } +} + +function doFill(ctx, cfg) { + const {line, target, above, below, area, scale, clip} = cfg; + const property = line._loop ? 'angle' : cfg.axis; + + ctx.save(); + + let fillColor = below; + if (below !== above) { + if (property === 'x') { + clipVertical(ctx, target, area.top); + fill(ctx, {line, target, color: above, scale, property, clip}); + ctx.restore(); + ctx.save(); + clipVertical(ctx, target, area.bottom); + } else if (property === 'y') { + clipHorizontal(ctx, target, area.left); + fill(ctx, {line, target, color: below, scale, property, clip}); + ctx.restore(); + ctx.save(); + clipHorizontal(ctx, target, area.right); + fillColor = above; + } + } + fill(ctx, {line, target, color: fillColor, scale, property, clip}); + + ctx.restore(); +} + +function clipVertical(ctx, target, clipY) { + const {segments, points} = target; + let first = true; + let lineLoop = false; + + ctx.beginPath(); + for (const segment of segments) { + const {start, end} = segment; + const firstPoint = points[start]; + const lastPoint = points[_findSegmentEnd(start, end, points)]; + if (first) { + ctx.moveTo(firstPoint.x, firstPoint.y); + first = false; + } else { + ctx.lineTo(firstPoint.x, clipY); + ctx.lineTo(firstPoint.x, firstPoint.y); + } + lineLoop = !!target.pathSegment(ctx, segment, {move: lineLoop}); + if (lineLoop) { + ctx.closePath(); + } else { + ctx.lineTo(lastPoint.x, clipY); + } + } + + ctx.lineTo(target.first().x, clipY); + ctx.closePath(); + ctx.clip(); +} + +function clipHorizontal(ctx, target, clipX) { + const {segments, points} = target; + let first = true; + let lineLoop = false; + + ctx.beginPath(); + for (const segment of segments) { + const {start, end} = segment; + const firstPoint = points[start]; + const lastPoint = points[_findSegmentEnd(start, end, points)]; + if (first) { + ctx.moveTo(firstPoint.x, firstPoint.y); + first = false; + } else { + ctx.lineTo(clipX, firstPoint.y); + ctx.lineTo(firstPoint.x, firstPoint.y); + } + lineLoop = !!target.pathSegment(ctx, segment, {move: lineLoop}); + if (lineLoop) { + ctx.closePath(); + } else { + ctx.lineTo(clipX, lastPoint.y); + } + } + + ctx.lineTo(clipX, target.first().y); + ctx.closePath(); + ctx.clip(); +} + +function fill(ctx, cfg) { + const {line, target, property, color, scale, clip} = cfg; + const segments = _segments(line, target, property); + + for (const {source: src, target: tgt, start, end} of segments) { + const {style: {backgroundColor = color} = {}} = src; + const notShape = target !== true; + + ctx.save(); + ctx.fillStyle = backgroundColor; + + clipBounds(ctx, scale, clip, notShape && _getBounds(property, start, end)); + + ctx.beginPath(); + + const lineLoop = !!line.pathSegment(ctx, src); + + let loop; + if (notShape) { + if (lineLoop) { + ctx.closePath(); + } else { + interpolatedLineTo(ctx, target, end, property); + } + + const targetLoop = !!target.pathSegment(ctx, tgt, {move: lineLoop, reverse: true}); + loop = lineLoop && targetLoop; + if (!loop) { + interpolatedLineTo(ctx, target, start, property); + } + } + + ctx.closePath(); + ctx.fill(loop ? 'evenodd' : 'nonzero'); + + ctx.restore(); + } +} + +function clipBounds(ctx, scale, clip, bounds) { + const chartArea = scale.chart.chartArea; + const {property, start, end} = bounds || {}; + + if (property === 'x' || property === 'y') { + let left, top, right, bottom; + + if (property === 'x') { + left = start; + top = chartArea.top; + right = end; + bottom = chartArea.bottom; + } else { + left = chartArea.left; + top = start; + right = chartArea.right; + bottom = end; + } + + ctx.beginPath(); + + if (clip) { + left = Math.max(left, clip.left); + right = Math.min(right, clip.right); + top = Math.max(top, clip.top); + bottom = Math.min(bottom, clip.bottom); + } + + ctx.rect(left, top, right - left, bottom - top); + ctx.clip(); + } +} + +function interpolatedLineTo(ctx, target, point, property) { + const interpolatedPoint = target.interpolate(point, property); + if (interpolatedPoint) { + ctx.lineTo(interpolatedPoint.x, interpolatedPoint.y); + } +} + diff --git a/src/plugins/plugin.filler/filler.helper.js b/src/plugins/plugin.filler/filler.helper.js new file mode 100644 index 00000000000..8d9c4037fd1 --- /dev/null +++ b/src/plugins/plugin.filler/filler.helper.js @@ -0,0 +1,38 @@ +/** + * @typedef { import('../../core/core.controller.js').default } Chart + * @typedef { import('../../core/core.scale.js').default } Scale + * @typedef { import('../../elements/element.point.js').default } PointElement + */ + +import {LineElement} from '../../elements/index.js'; +import {isArray} from '../../helpers/index.js'; +import {_pointsFromSegments} from './filler.segment.js'; + +/** + * @param {PointElement[] | { x: number; y: number; }} boundary + * @param {LineElement} line + * @return {LineElement?} + */ +export function _createBoundaryLine(boundary, line) { + let points = []; + let _loop = false; + + if (isArray(boundary)) { + _loop = true; + // @ts-ignore + points = boundary; + } else { + points = _pointsFromSegments(boundary, line); + } + + return points.length ? new LineElement({ + points, + options: {tension: 0}, + _loop, + _fullLoop: _loop + }) : null; +} + +export function _shouldApplyFill(source) { + return source && source.fill !== false; +} diff --git a/src/plugins/plugin.filler/filler.options.js b/src/plugins/plugin.filler/filler.options.js new file mode 100644 index 00000000000..33b965bc798 --- /dev/null +++ b/src/plugins/plugin.filler/filler.options.js @@ -0,0 +1,137 @@ +import {isObject, isFinite, valueOrDefault} from '../../helpers/helpers.core.js'; + +/** + * @typedef { import('../../core/core.scale.js').default } Scale + * @typedef { import('../../elements/element.line.js').default } LineElement + * @typedef { import('../../types/index.js').FillTarget } FillTarget + * @typedef { import('../../types/index.js').ComplexFillTarget } ComplexFillTarget + */ + +export function _resolveTarget(sources, index, propagate) { + const source = sources[index]; + let fill = source.fill; + const visited = [index]; + let target; + + if (!propagate) { + return fill; + } + + while (fill !== false && visited.indexOf(fill) === -1) { + if (!isFinite(fill)) { + return fill; + } + + target = sources[fill]; + if (!target) { + return false; + } + + if (target.visible) { + return fill; + } + + visited.push(fill); + fill = target.fill; + } + + return false; +} + +/** + * @param {LineElement} line + * @param {number} index + * @param {number} count + */ +export function _decodeFill(line, index, count) { + /** @type {string | {value: number}} */ + const fill = parseFillOption(line); + + if (isObject(fill)) { + return isNaN(fill.value) ? false : fill; + } + + let target = parseFloat(fill); + + if (isFinite(target) && Math.floor(target) === target) { + return decodeTargetIndex(fill[0], index, target, count); + } + + return ['origin', 'start', 'end', 'stack', 'shape'].indexOf(fill) >= 0 && fill; +} + +function decodeTargetIndex(firstCh, index, target, count) { + if (firstCh === '-' || firstCh === '+') { + target = index + target; + } + + if (target === index || target < 0 || target >= count) { + return false; + } + + return target; +} + +/** + * @param {FillTarget | ComplexFillTarget} fill + * @param {Scale} scale + * @returns {number | null} + */ +export function _getTargetPixel(fill, scale) { + let pixel = null; + if (fill === 'start') { + pixel = scale.bottom; + } else if (fill === 'end') { + pixel = scale.top; + } else if (isObject(fill)) { + // @ts-ignore + pixel = scale.getPixelForValue(fill.value); + } else if (scale.getBasePixel) { + pixel = scale.getBasePixel(); + } + return pixel; +} + +/** + * @param {FillTarget | ComplexFillTarget} fill + * @param {Scale} scale + * @param {number} startValue + * @returns {number | undefined} + */ +export function _getTargetValue(fill, scale, startValue) { + let value; + + if (fill === 'start') { + value = startValue; + } else if (fill === 'end') { + value = scale.options.reverse ? scale.min : scale.max; + } else if (isObject(fill)) { + // @ts-ignore + value = fill.value; + } else { + value = scale.getBaseValue(); + } + return value; +} + +/** + * @param {LineElement} line + */ +function parseFillOption(line) { + const options = line.options; + const fillOption = options.fill; + let fill = valueOrDefault(fillOption && fillOption.target, fillOption); + + if (fill === undefined) { + fill = !!options.backgroundColor; + } + + if (fill === false || fill === null) { + return false; + } + + if (fill === true) { + return 'origin'; + } + return fill; +} diff --git a/src/plugins/plugin.filler/filler.segment.js b/src/plugins/plugin.filler/filler.segment.js new file mode 100644 index 00000000000..c0e4e8d81a1 --- /dev/null +++ b/src/plugins/plugin.filler/filler.segment.js @@ -0,0 +1,99 @@ +import {_boundSegment, _boundSegments, _normalizeAngle} from '../../helpers/index.js'; + +export function _segments(line, target, property) { + const segments = line.segments; + const points = line.points; + const tpoints = target.points; + const parts = []; + + for (const segment of segments) { + let {start, end} = segment; + end = _findSegmentEnd(start, end, points); + + const bounds = _getBounds(property, points[start], points[end], segment.loop); + + if (!target.segments) { + // Special case for boundary not supporting `segments` (simpleArc) + // Bounds are provided as `target` for partial circle, or undefined for full circle + parts.push({ + source: segment, + target: bounds, + start: points[start], + end: points[end] + }); + continue; + } + + // Get all segments from `target` that intersect the bounds of current segment of `line` + const targetSegments = _boundSegments(target, bounds); + + for (const tgt of targetSegments) { + const subBounds = _getBounds(property, tpoints[tgt.start], tpoints[tgt.end], tgt.loop); + const fillSources = _boundSegment(segment, points, subBounds); + + for (const fillSource of fillSources) { + parts.push({ + source: fillSource, + target: tgt, + start: { + [property]: _getEdge(bounds, subBounds, 'start', Math.max) + }, + end: { + [property]: _getEdge(bounds, subBounds, 'end', Math.min) + } + }); + } + } + } + return parts; +} + +export function _getBounds(property, first, last, loop) { + if (loop) { + return; + } + let start = first[property]; + let end = last[property]; + + if (property === 'angle') { + start = _normalizeAngle(start); + end = _normalizeAngle(end); + } + return {property, start, end}; +} + +export function _pointsFromSegments(boundary, line) { + const {x = null, y = null} = boundary || {}; + const linePoints = line.points; + const points = []; + line.segments.forEach(({start, end}) => { + end = _findSegmentEnd(start, end, linePoints); + const first = linePoints[start]; + const last = linePoints[end]; + if (y !== null) { + points.push({x: first.x, y}); + points.push({x: last.x, y}); + } else if (x !== null) { + points.push({x, y: first.y}); + points.push({x, y: last.y}); + } + }); + return points; +} + +export function _findSegmentEnd(start, end, points) { + for (;end > start; end--) { + const point = points[end]; + if (!isNaN(point.x) && !isNaN(point.y)) { + break; + } + } + return end; +} + +function _getEdge(a, b, prop, fn) { + if (a && b) { + return fn(a[prop], b[prop]); + } + return a ? a[prop] : b ? b[prop] : 0; +} diff --git a/src/plugins/plugin.filler/filler.target.js b/src/plugins/plugin.filler/filler.target.js new file mode 100644 index 00000000000..fa9048e70c0 --- /dev/null +++ b/src/plugins/plugin.filler/filler.target.js @@ -0,0 +1,95 @@ +import {isFinite} from '../../helpers/index.js'; +import {_createBoundaryLine} from './filler.helper.js'; +import {_getTargetPixel, _getTargetValue} from './filler.options.js'; +import {_buildStackLine} from './filler.target.stack.js'; +import {simpleArc} from './simpleArc.js'; + +/** + * @typedef { import('../../core/core.controller.js').default } Chart + * @typedef { import('../../core/core.scale.js').default } Scale + * @typedef { import('../../elements/element.point.js').default } PointElement + */ + +export function _getTarget(source) { + const {chart, fill, line} = source; + + if (isFinite(fill)) { + return getLineByIndex(chart, fill); + } + + if (fill === 'stack') { + return _buildStackLine(source); + } + + if (fill === 'shape') { + return true; + } + + const boundary = computeBoundary(source); + + if (boundary instanceof simpleArc) { + return boundary; + } + + return _createBoundaryLine(boundary, line); +} + +/** + * @param {Chart} chart + * @param {number} index + */ +function getLineByIndex(chart, index) { + const meta = chart.getDatasetMeta(index); + const visible = meta && chart.isDatasetVisible(index); + return visible ? meta.dataset : null; +} + +function computeBoundary(source) { + const scale = source.scale || {}; + + if (scale.getPointPositionForValue) { + return computeCircularBoundary(source); + } + return computeLinearBoundary(source); +} + + +function computeLinearBoundary(source) { + const {scale = {}, fill} = source; + const pixel = _getTargetPixel(fill, scale); + + if (isFinite(pixel)) { + const horizontal = scale.isHorizontal(); + + return { + x: horizontal ? pixel : null, + y: horizontal ? null : pixel + }; + } + + return null; +} + +function computeCircularBoundary(source) { + const {scale, fill} = source; + const options = scale.options; + const length = scale.getLabels().length; + const start = options.reverse ? scale.max : scale.min; + const value = _getTargetValue(fill, scale, start); + const target = []; + + if (options.grid.circular) { + const center = scale.getPointPositionForValue(0, start); + return new simpleArc({ + x: center.x, + y: center.y, + radius: scale.getDistanceFromCenterForValue(value) + }); + } + + for (let i = 0; i < length; ++i) { + target.push(scale.getPointPositionForValue(i, value)); + } + return target; +} + diff --git a/src/plugins/plugin.filler/filler.target.stack.js b/src/plugins/plugin.filler/filler.target.stack.js new file mode 100644 index 00000000000..8c6d0532c12 --- /dev/null +++ b/src/plugins/plugin.filler/filler.target.stack.js @@ -0,0 +1,109 @@ +/** + * @typedef { import('../../core/core.controller.js').default } Chart + * @typedef { import('../../core/core.scale.js').default } Scale + * @typedef { import('../../elements/element.point.js').default } PointElement + */ + +import {LineElement} from '../../elements/index.js'; +import {_isBetween} from '../../helpers/index.js'; +import {_createBoundaryLine} from './filler.helper.js'; + +/** + * @param {{ chart: Chart; scale: Scale; index: number; line: LineElement; }} source + * @return {LineElement} + */ +export function _buildStackLine(source) { + const {scale, index, line} = source; + const points = []; + const segments = line.segments; + const sourcePoints = line.points; + const linesBelow = getLinesBelow(scale, index); + linesBelow.push(_createBoundaryLine({x: null, y: scale.bottom}, line)); + + for (let i = 0; i < segments.length; i++) { + const segment = segments[i]; + for (let j = segment.start; j <= segment.end; j++) { + addPointsBelow(points, sourcePoints[j], linesBelow); + } + } + return new LineElement({points, options: {}}); +} + +/** + * @param {Scale} scale + * @param {number} index + * @return {LineElement[]} + */ +function getLinesBelow(scale, index) { + const below = []; + const metas = scale.getMatchingVisibleMetas('line'); + + for (let i = 0; i < metas.length; i++) { + const meta = metas[i]; + if (meta.index === index) { + break; + } + if (!meta.hidden) { + below.unshift(meta.dataset); + } + } + return below; +} + +/** + * @param {PointElement[]} points + * @param {PointElement} sourcePoint + * @param {LineElement[]} linesBelow + */ +function addPointsBelow(points, sourcePoint, linesBelow) { + const postponed = []; + for (let j = 0; j < linesBelow.length; j++) { + const line = linesBelow[j]; + const {first, last, point} = findPoint(line, sourcePoint, 'x'); + + if (!point || (first && last)) { + continue; + } + if (first) { + // First point of a segment -> need to add another point before this, + postponed.unshift(point); + } else { + points.push(point); + if (!last) { + // In the middle of a segment, no need to add more points. + break; + } + } + } + points.push(...postponed); +} + +/** + * @param {LineElement} line + * @param {PointElement} sourcePoint + * @param {string} property + * @returns {{point?: PointElement, first?: boolean, last?: boolean}} + */ +function findPoint(line, sourcePoint, property) { + const point = line.interpolate(sourcePoint, property); + if (!point) { + return {}; + } + + const pointValue = point[property]; + const segments = line.segments; + const linePoints = line.points; + let first = false; + let last = false; + for (let i = 0; i < segments.length; i++) { + const segment = segments[i]; + const firstValue = linePoints[segment.start][property]; + const lastValue = linePoints[segment.end][property]; + if (_isBetween(pointValue, firstValue, lastValue)) { + first = pointValue === firstValue; + last = pointValue === lastValue; + break; + } + } + return {first, last, point}; +} diff --git a/src/plugins/plugin.filler/index.js b/src/plugins/plugin.filler/index.js new file mode 100644 index 00000000000..07e0b968b17 --- /dev/null +++ b/src/plugins/plugin.filler/index.js @@ -0,0 +1,97 @@ +/** + * Plugin based on discussion from the following Chart.js issues: + * @see https://github.com/chartjs/Chart.js/issues/2380#issuecomment-279961569 + * @see https://github.com/chartjs/Chart.js/issues/2440#issuecomment-256461897 + */ + +import LineElement from '../../elements/element.line.js'; +import {_drawfill} from './filler.drawing.js'; +import {_shouldApplyFill} from './filler.helper.js'; +import {_decodeFill, _resolveTarget} from './filler.options.js'; + +export default { + id: 'filler', + + afterDatasetsUpdate(chart, _args, options) { + const count = (chart.data.datasets || []).length; + const sources = []; + let meta, i, line, source; + + for (i = 0; i < count; ++i) { + meta = chart.getDatasetMeta(i); + line = meta.dataset; + source = null; + + if (line && line.options && line instanceof LineElement) { + source = { + visible: chart.isDatasetVisible(i), + index: i, + fill: _decodeFill(line, i, count), + chart, + axis: meta.controller.options.indexAxis, + scale: meta.vScale, + line, + }; + } + + meta.$filler = source; + sources.push(source); + } + + for (i = 0; i < count; ++i) { + source = sources[i]; + if (!source || source.fill === false) { + continue; + } + + source.fill = _resolveTarget(sources, i, options.propagate); + } + }, + + beforeDraw(chart, _args, options) { + const draw = options.drawTime === 'beforeDraw'; + const metasets = chart.getSortedVisibleDatasetMetas(); + const area = chart.chartArea; + for (let i = metasets.length - 1; i >= 0; --i) { + const source = metasets[i].$filler; + if (!source) { + continue; + } + + source.line.updateControlPoints(area, source.axis); + if (draw && source.fill) { + _drawfill(chart.ctx, source, area); + } + } + }, + + beforeDatasetsDraw(chart, _args, options) { + if (options.drawTime !== 'beforeDatasetsDraw') { + return; + } + + const metasets = chart.getSortedVisibleDatasetMetas(); + for (let i = metasets.length - 1; i >= 0; --i) { + const source = metasets[i].$filler; + + if (_shouldApplyFill(source)) { + _drawfill(chart.ctx, source, chart.chartArea); + } + } + }, + + beforeDatasetDraw(chart, args, options) { + const source = args.meta.$filler; + + if (!_shouldApplyFill(source) || options.drawTime !== 'beforeDatasetDraw') { + return; + } + + _drawfill(chart.ctx, source, chart.chartArea); + }, + + defaults: { + propagate: true, + drawTime: 'beforeDatasetDraw' + } +}; diff --git a/src/plugins/plugin.filler/simpleArc.js b/src/plugins/plugin.filler/simpleArc.js new file mode 100644 index 00000000000..7304b3e393b --- /dev/null +++ b/src/plugins/plugin.filler/simpleArc.js @@ -0,0 +1,27 @@ +import {TAU} from '../../helpers/index.js'; + +// TODO: use elements.ArcElement instead +export class simpleArc { + constructor(opts) { + this.x = opts.x; + this.y = opts.y; + this.radius = opts.radius; + } + + pathSegment(ctx, bounds, opts) { + const {x, y, radius} = this; + bounds = bounds || {start: 0, end: TAU}; + ctx.arc(x, y, radius, bounds.end, bounds.start, true); + return !opts.bounds; + } + + interpolate(point) { + const {x, y, radius} = this; + const angle = point.angle; + return { + x: x + Math.cos(angle) * radius, + y: y + Math.sin(angle) * radius, + angle + }; + } +} diff --git a/src/plugins/plugin.legend.js b/src/plugins/plugin.legend.js new file mode 100644 index 00000000000..6ed99413536 --- /dev/null +++ b/src/plugins/plugin.legend.js @@ -0,0 +1,720 @@ +import defaults from '../core/core.defaults.js'; +import Element from '../core/core.element.js'; +import layouts from '../core/core.layouts.js'; +import {addRoundedRectPath, drawPointLegend, renderText} from '../helpers/helpers.canvas.js'; +import { + _isBetween, + callback as call, + clipArea, + getRtlAdapter, + overrideTextDirection, + restoreTextDirection, + toFont, + toPadding, + unclipArea, + valueOrDefault, +} from '../helpers/index.js'; +import {_alignStartEnd, _textX, _toLeftRightCenter} from '../helpers/helpers.extras.js'; +import {toTRBLCorners} from '../helpers/helpers.options.js'; + +/** + * @typedef { import('../types/index.js').ChartEvent } ChartEvent + */ + +const getBoxSize = (labelOpts, fontSize) => { + let {boxHeight = fontSize, boxWidth = fontSize} = labelOpts; + + if (labelOpts.usePointStyle) { + boxHeight = Math.min(boxHeight, fontSize); + boxWidth = labelOpts.pointStyleWidth || Math.min(boxWidth, fontSize); + } + + return { + boxWidth, + boxHeight, + itemHeight: Math.max(fontSize, boxHeight) + }; +}; + +const itemsEqual = (a, b) => a !== null && b !== null && a.datasetIndex === b.datasetIndex && a.index === b.index; + +export class Legend extends Element { + + /** + * @param {{ ctx: any; options: any; chart: any; }} config + */ + constructor(config) { + super(); + + this._added = false; + + // Contains hit boxes for each dataset (in dataset order) + this.legendHitBoxes = []; + + /** + * @private + */ + this._hoveredItem = null; + + // Are we in doughnut mode which has a different data type + this.doughnutMode = false; + + this.chart = config.chart; + this.options = config.options; + this.ctx = config.ctx; + this.legendItems = undefined; + this.columnSizes = undefined; + this.lineWidths = undefined; + this.maxHeight = undefined; + this.maxWidth = undefined; + this.top = undefined; + this.bottom = undefined; + this.left = undefined; + this.right = undefined; + this.height = undefined; + this.width = undefined; + this._margins = undefined; + this.position = undefined; + this.weight = undefined; + this.fullSize = undefined; + } + + update(maxWidth, maxHeight, margins) { + this.maxWidth = maxWidth; + this.maxHeight = maxHeight; + this._margins = margins; + + this.setDimensions(); + this.buildLabels(); + this.fit(); + } + + setDimensions() { + if (this.isHorizontal()) { + this.width = this.maxWidth; + this.left = this._margins.left; + this.right = this.width; + } else { + this.height = this.maxHeight; + this.top = this._margins.top; + this.bottom = this.height; + } + } + + buildLabels() { + const labelOpts = this.options.labels || {}; + let legendItems = call(labelOpts.generateLabels, [this.chart], this) || []; + + if (labelOpts.filter) { + legendItems = legendItems.filter((item) => labelOpts.filter(item, this.chart.data)); + } + + if (labelOpts.sort) { + legendItems = legendItems.sort((a, b) => labelOpts.sort(a, b, this.chart.data)); + } + + if (this.options.reverse) { + legendItems.reverse(); + } + + this.legendItems = legendItems; + } + + fit() { + const {options, ctx} = this; + + // The legend may not be displayed for a variety of reasons including + // the fact that the defaults got set to `false`. + // When the legend is not displayed, there are no guarantees that the options + // are correctly formatted so we need to bail out as early as possible. + if (!options.display) { + this.width = this.height = 0; + return; + } + + const labelOpts = options.labels; + const labelFont = toFont(labelOpts.font); + const fontSize = labelFont.size; + const titleHeight = this._computeTitleHeight(); + const {boxWidth, itemHeight} = getBoxSize(labelOpts, fontSize); + + let width, height; + + ctx.font = labelFont.string; + + if (this.isHorizontal()) { + width = this.maxWidth; // fill all the width + height = this._fitRows(titleHeight, fontSize, boxWidth, itemHeight) + 10; + } else { + height = this.maxHeight; // fill all the height + width = this._fitCols(titleHeight, labelFont, boxWidth, itemHeight) + 10; + } + + this.width = Math.min(width, options.maxWidth || this.maxWidth); + this.height = Math.min(height, options.maxHeight || this.maxHeight); + } + + /** + * @private + */ + _fitRows(titleHeight, fontSize, boxWidth, itemHeight) { + const {ctx, maxWidth, options: {labels: {padding}}} = this; + const hitboxes = this.legendHitBoxes = []; + // Width of each line of legend boxes. Labels wrap onto multiple lines when there are too many to fit on one + const lineWidths = this.lineWidths = [0]; + const lineHeight = itemHeight + padding; + let totalHeight = titleHeight; + + ctx.textAlign = 'left'; + ctx.textBaseline = 'middle'; + + let row = -1; + let top = -lineHeight; + this.legendItems.forEach((legendItem, i) => { + const itemWidth = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; + + if (i === 0 || lineWidths[lineWidths.length - 1] + itemWidth + 2 * padding > maxWidth) { + totalHeight += lineHeight; + lineWidths[lineWidths.length - (i > 0 ? 0 : 1)] = 0; + top += lineHeight; + row++; + } + + hitboxes[i] = {left: 0, top, row, width: itemWidth, height: itemHeight}; + + lineWidths[lineWidths.length - 1] += itemWidth + padding; + }); + + return totalHeight; + } + + _fitCols(titleHeight, labelFont, boxWidth, _itemHeight) { + const {ctx, maxHeight, options: {labels: {padding}}} = this; + const hitboxes = this.legendHitBoxes = []; + const columnSizes = this.columnSizes = []; + const heightLimit = maxHeight - titleHeight; + + let totalWidth = padding; + let currentColWidth = 0; + let currentColHeight = 0; + + let left = 0; + let col = 0; + + this.legendItems.forEach((legendItem, i) => { + const {itemWidth, itemHeight} = calculateItemSize(boxWidth, labelFont, ctx, legendItem, _itemHeight); + + // If too tall, go to new column + if (i > 0 && currentColHeight + itemHeight + 2 * padding > heightLimit) { + totalWidth += currentColWidth + padding; + columnSizes.push({width: currentColWidth, height: currentColHeight}); // previous column size + left += currentColWidth + padding; + col++; + currentColWidth = currentColHeight = 0; + } + + // Store the hitbox width and height here. Final position will be updated in `draw` + hitboxes[i] = {left, top: currentColHeight, col, width: itemWidth, height: itemHeight}; + + // Get max width + currentColWidth = Math.max(currentColWidth, itemWidth); + currentColHeight += itemHeight + padding; + }); + + totalWidth += currentColWidth; + columnSizes.push({width: currentColWidth, height: currentColHeight}); // previous column size + + return totalWidth; + } + + adjustHitBoxes() { + if (!this.options.display) { + return; + } + const titleHeight = this._computeTitleHeight(); + const {legendHitBoxes: hitboxes, options: {align, labels: {padding}, rtl}} = this; + const rtlHelper = getRtlAdapter(rtl, this.left, this.width); + if (this.isHorizontal()) { + let row = 0; + let left = _alignStartEnd(align, this.left + padding, this.right - this.lineWidths[row]); + for (const hitbox of hitboxes) { + if (row !== hitbox.row) { + row = hitbox.row; + left = _alignStartEnd(align, this.left + padding, this.right - this.lineWidths[row]); + } + hitbox.top += this.top + titleHeight + padding; + hitbox.left = rtlHelper.leftForLtr(rtlHelper.x(left), hitbox.width); + left += hitbox.width + padding; + } + } else { + let col = 0; + let top = _alignStartEnd(align, this.top + titleHeight + padding, this.bottom - this.columnSizes[col].height); + for (const hitbox of hitboxes) { + if (hitbox.col !== col) { + col = hitbox.col; + top = _alignStartEnd(align, this.top + titleHeight + padding, this.bottom - this.columnSizes[col].height); + } + hitbox.top = top; + hitbox.left += this.left + padding; + hitbox.left = rtlHelper.leftForLtr(rtlHelper.x(hitbox.left), hitbox.width); + top += hitbox.height + padding; + } + } + } + + isHorizontal() { + return this.options.position === 'top' || this.options.position === 'bottom'; + } + + draw() { + if (this.options.display) { + const ctx = this.ctx; + clipArea(ctx, this); + + this._draw(); + + unclipArea(ctx); + } + } + + /** + * @private + */ + _draw() { + const {options: opts, columnSizes, lineWidths, ctx} = this; + const {align, labels: labelOpts} = opts; + const defaultColor = defaults.color; + const rtlHelper = getRtlAdapter(opts.rtl, this.left, this.width); + const labelFont = toFont(labelOpts.font); + const {padding} = labelOpts; + const fontSize = labelFont.size; + const halfFontSize = fontSize / 2; + let cursor; + + this.drawTitle(); + + // Canvas setup + ctx.textAlign = rtlHelper.textAlign('left'); + ctx.textBaseline = 'middle'; + ctx.lineWidth = 0.5; + ctx.font = labelFont.string; + + const {boxWidth, boxHeight, itemHeight} = getBoxSize(labelOpts, fontSize); + + // current position + const drawLegendBox = function(x, y, legendItem) { + if (isNaN(boxWidth) || boxWidth <= 0 || isNaN(boxHeight) || boxHeight < 0) { + return; + } + + // Set the ctx for the box + ctx.save(); + + const lineWidth = valueOrDefault(legendItem.lineWidth, 1); + ctx.fillStyle = valueOrDefault(legendItem.fillStyle, defaultColor); + ctx.lineCap = valueOrDefault(legendItem.lineCap, 'butt'); + ctx.lineDashOffset = valueOrDefault(legendItem.lineDashOffset, 0); + ctx.lineJoin = valueOrDefault(legendItem.lineJoin, 'miter'); + ctx.lineWidth = lineWidth; + ctx.strokeStyle = valueOrDefault(legendItem.strokeStyle, defaultColor); + + ctx.setLineDash(valueOrDefault(legendItem.lineDash, [])); + + if (labelOpts.usePointStyle) { + // Recalculate x and y for drawPoint() because its expecting + // x and y to be center of figure (instead of top left) + const drawOptions = { + radius: boxHeight * Math.SQRT2 / 2, + pointStyle: legendItem.pointStyle, + rotation: legendItem.rotation, + borderWidth: lineWidth + }; + const centerX = rtlHelper.xPlus(x, boxWidth / 2); + const centerY = y + halfFontSize; + + // Draw pointStyle as legend symbol + drawPointLegend(ctx, drawOptions, centerX, centerY, labelOpts.pointStyleWidth && boxWidth); + } else { + // Draw box as legend symbol + // Adjust position when boxHeight < fontSize (want it centered) + const yBoxTop = y + Math.max((fontSize - boxHeight) / 2, 0); + const xBoxLeft = rtlHelper.leftForLtr(x, boxWidth); + const borderRadius = toTRBLCorners(legendItem.borderRadius); + + ctx.beginPath(); + + if (Object.values(borderRadius).some(v => v !== 0)) { + addRoundedRectPath(ctx, { + x: xBoxLeft, + y: yBoxTop, + w: boxWidth, + h: boxHeight, + radius: borderRadius, + }); + } else { + ctx.rect(xBoxLeft, yBoxTop, boxWidth, boxHeight); + } + + ctx.fill(); + if (lineWidth !== 0) { + ctx.stroke(); + } + } + + ctx.restore(); + }; + + const fillText = function(x, y, legendItem) { + renderText(ctx, legendItem.text, x, y + (itemHeight / 2), labelFont, { + strikethrough: legendItem.hidden, + textAlign: rtlHelper.textAlign(legendItem.textAlign) + }); + }; + + // Horizontal + const isHorizontal = this.isHorizontal(); + const titleHeight = this._computeTitleHeight(); + if (isHorizontal) { + cursor = { + x: _alignStartEnd(align, this.left + padding, this.right - lineWidths[0]), + y: this.top + padding + titleHeight, + line: 0 + }; + } else { + cursor = { + x: this.left + padding, + y: _alignStartEnd(align, this.top + titleHeight + padding, this.bottom - columnSizes[0].height), + line: 0 + }; + } + + overrideTextDirection(this.ctx, opts.textDirection); + + const lineHeight = itemHeight + padding; + this.legendItems.forEach((legendItem, i) => { + ctx.strokeStyle = legendItem.fontColor; // for strikethrough effect + ctx.fillStyle = legendItem.fontColor; // render in correct colour + + const textWidth = ctx.measureText(legendItem.text).width; + const textAlign = rtlHelper.textAlign(legendItem.textAlign || (legendItem.textAlign = labelOpts.textAlign)); + const width = boxWidth + halfFontSize + textWidth; + let x = cursor.x; + let y = cursor.y; + + rtlHelper.setWidth(this.width); + + if (isHorizontal) { + if (i > 0 && x + width + padding > this.right) { + y = cursor.y += lineHeight; + cursor.line++; + x = cursor.x = _alignStartEnd(align, this.left + padding, this.right - lineWidths[cursor.line]); + } + } else if (i > 0 && y + lineHeight > this.bottom) { + x = cursor.x = x + columnSizes[cursor.line].width + padding; + cursor.line++; + y = cursor.y = _alignStartEnd(align, this.top + titleHeight + padding, this.bottom - columnSizes[cursor.line].height); + } + + const realX = rtlHelper.x(x); + + drawLegendBox(realX, y, legendItem); + + x = _textX(textAlign, x + boxWidth + halfFontSize, isHorizontal ? x + width : this.right, opts.rtl); + + // Fill the actual label + fillText(rtlHelper.x(x), y, legendItem); + + if (isHorizontal) { + cursor.x += width + padding; + } else if (typeof legendItem.text !== 'string') { + const fontLineHeight = labelFont.lineHeight; + cursor.y += calculateLegendItemHeight(legendItem, fontLineHeight) + padding; + } else { + cursor.y += lineHeight; + } + }); + + restoreTextDirection(this.ctx, opts.textDirection); + } + + /** + * @protected + */ + drawTitle() { + const opts = this.options; + const titleOpts = opts.title; + const titleFont = toFont(titleOpts.font); + const titlePadding = toPadding(titleOpts.padding); + + if (!titleOpts.display) { + return; + } + + const rtlHelper = getRtlAdapter(opts.rtl, this.left, this.width); + const ctx = this.ctx; + const position = titleOpts.position; + const halfFontSize = titleFont.size / 2; + const topPaddingPlusHalfFontSize = titlePadding.top + halfFontSize; + let y; + + // These defaults are used when the legend is vertical. + // When horizontal, they are computed below. + let left = this.left; + let maxWidth = this.width; + + if (this.isHorizontal()) { + // Move left / right so that the title is above the legend lines + maxWidth = Math.max(...this.lineWidths); + y = this.top + topPaddingPlusHalfFontSize; + left = _alignStartEnd(opts.align, left, this.right - maxWidth); + } else { + // Move down so that the title is above the legend stack in every alignment + const maxHeight = this.columnSizes.reduce((acc, size) => Math.max(acc, size.height), 0); + y = topPaddingPlusHalfFontSize + _alignStartEnd(opts.align, this.top, this.bottom - maxHeight - opts.labels.padding - this._computeTitleHeight()); + } + + // Now that we know the left edge of the inner legend box, compute the correct + // X coordinate from the title alignment + const x = _alignStartEnd(position, left, left + maxWidth); + + // Canvas setup + ctx.textAlign = rtlHelper.textAlign(_toLeftRightCenter(position)); + ctx.textBaseline = 'middle'; + ctx.strokeStyle = titleOpts.color; + ctx.fillStyle = titleOpts.color; + ctx.font = titleFont.string; + + renderText(ctx, titleOpts.text, x, y, titleFont); + } + + /** + * @private + */ + _computeTitleHeight() { + const titleOpts = this.options.title; + const titleFont = toFont(titleOpts.font); + const titlePadding = toPadding(titleOpts.padding); + return titleOpts.display ? titleFont.lineHeight + titlePadding.height : 0; + } + + /** + * @private + */ + _getLegendItemAt(x, y) { + let i, hitBox, lh; + + if (_isBetween(x, this.left, this.right) + && _isBetween(y, this.top, this.bottom)) { + // See if we are touching one of the dataset boxes + lh = this.legendHitBoxes; + for (i = 0; i < lh.length; ++i) { + hitBox = lh[i]; + + if (_isBetween(x, hitBox.left, hitBox.left + hitBox.width) + && _isBetween(y, hitBox.top, hitBox.top + hitBox.height)) { + // Touching an element + return this.legendItems[i]; + } + } + } + + return null; + } + + /** + * Handle an event + * @param {ChartEvent} e - The event to handle + */ + handleEvent(e) { + const opts = this.options; + if (!isListened(e.type, opts)) { + return; + } + + // Chart event already has relative position in it + const hoveredItem = this._getLegendItemAt(e.x, e.y); + + if (e.type === 'mousemove' || e.type === 'mouseout') { + const previous = this._hoveredItem; + const sameItem = itemsEqual(previous, hoveredItem); + if (previous && !sameItem) { + call(opts.onLeave, [e, previous, this], this); + } + + this._hoveredItem = hoveredItem; + + if (hoveredItem && !sameItem) { + call(opts.onHover, [e, hoveredItem, this], this); + } + } else if (hoveredItem) { + call(opts.onClick, [e, hoveredItem, this], this); + } + } +} + +function calculateItemSize(boxWidth, labelFont, ctx, legendItem, _itemHeight) { + const itemWidth = calculateItemWidth(legendItem, boxWidth, labelFont, ctx); + const itemHeight = calculateItemHeight(_itemHeight, legendItem, labelFont.lineHeight); + return {itemWidth, itemHeight}; +} + +function calculateItemWidth(legendItem, boxWidth, labelFont, ctx) { + let legendItemText = legendItem.text; + if (legendItemText && typeof legendItemText !== 'string') { + legendItemText = legendItemText.reduce((a, b) => a.length > b.length ? a : b); + } + return boxWidth + (labelFont.size / 2) + ctx.measureText(legendItemText).width; +} + +function calculateItemHeight(_itemHeight, legendItem, fontLineHeight) { + let itemHeight = _itemHeight; + if (typeof legendItem.text !== 'string') { + itemHeight = calculateLegendItemHeight(legendItem, fontLineHeight); + } + return itemHeight; +} + +function calculateLegendItemHeight(legendItem, fontLineHeight) { + const labelHeight = legendItem.text ? legendItem.text.length : 0; + return fontLineHeight * labelHeight; +} + +function isListened(type, opts) { + if ((type === 'mousemove' || type === 'mouseout') && (opts.onHover || opts.onLeave)) { + return true; + } + if (opts.onClick && (type === 'click' || type === 'mouseup')) { + return true; + } + return false; +} + +export default { + id: 'legend', + + /** + * For tests + * @private + */ + _element: Legend, + + start(chart, _args, options) { + const legend = chart.legend = new Legend({ctx: chart.ctx, options, chart}); + layouts.configure(chart, legend, options); + layouts.addBox(chart, legend); + }, + + stop(chart) { + layouts.removeBox(chart, chart.legend); + delete chart.legend; + }, + + // During the beforeUpdate step, the layout configuration needs to run + // This ensures that if the legend position changes (via an option update) + // the layout system respects the change. See https://github.com/chartjs/Chart.js/issues/7527 + beforeUpdate(chart, _args, options) { + const legend = chart.legend; + layouts.configure(chart, legend, options); + legend.options = options; + }, + + // The labels need to be built after datasets are updated to ensure that colors + // and other styling are correct. See https://github.com/chartjs/Chart.js/issues/6968 + afterUpdate(chart) { + const legend = chart.legend; + legend.buildLabels(); + legend.adjustHitBoxes(); + }, + + + afterEvent(chart, args) { + if (!args.replay) { + chart.legend.handleEvent(args.event); + } + }, + + defaults: { + display: true, + position: 'top', + align: 'center', + fullSize: true, + reverse: false, + weight: 1000, + + // a callback that will handle + onClick(e, legendItem, legend) { + const index = legendItem.datasetIndex; + const ci = legend.chart; + if (ci.isDatasetVisible(index)) { + ci.hide(index); + legendItem.hidden = true; + } else { + ci.show(index); + legendItem.hidden = false; + } + }, + + onHover: null, + onLeave: null, + + labels: { + color: (ctx) => ctx.chart.options.color, + boxWidth: 40, + padding: 10, + // Generates labels shown in the legend + // Valid properties to return: + // text : text to display + // fillStyle : fill of coloured box + // strokeStyle: stroke of coloured box + // hidden : if this legend item refers to a hidden item + // lineCap : cap style for line + // lineDash + // lineDashOffset : + // lineJoin : + // lineWidth : + generateLabels(chart) { + const datasets = chart.data.datasets; + const {labels: {usePointStyle, pointStyle, textAlign, color, useBorderRadius, borderRadius}} = chart.legend.options; + + return chart._getSortedDatasetMetas().map((meta) => { + const style = meta.controller.getStyle(usePointStyle ? 0 : undefined); + const borderWidth = toPadding(style.borderWidth); + + return { + text: datasets[meta.index].label, + fillStyle: style.backgroundColor, + fontColor: color, + hidden: !meta.visible, + lineCap: style.borderCapStyle, + lineDash: style.borderDash, + lineDashOffset: style.borderDashOffset, + lineJoin: style.borderJoinStyle, + lineWidth: (borderWidth.width + borderWidth.height) / 4, + strokeStyle: style.borderColor, + pointStyle: pointStyle || style.pointStyle, + rotation: style.rotation, + textAlign: textAlign || style.textAlign, + borderRadius: useBorderRadius && (borderRadius || style.borderRadius), + + // Below is extra data used for toggling the datasets + datasetIndex: meta.index + }; + }, this); + } + }, + + title: { + color: (ctx) => ctx.chart.options.color, + display: false, + position: 'center', + text: '', + } + }, + + descriptors: { + _scriptable: (name) => !name.startsWith('on'), + labels: { + _scriptable: (name) => !['generateLabels', 'filter', 'sort'].includes(name), + } + }, +}; diff --git a/src/plugins/plugin.subtitle.js b/src/plugins/plugin.subtitle.js new file mode 100644 index 00000000000..6f8be223c1e --- /dev/null +++ b/src/plugins/plugin.subtitle.js @@ -0,0 +1,53 @@ +import {Title} from './plugin.title.js'; +import layouts from '../core/core.layouts.js'; + +const map = new WeakMap(); + +export default { + id: 'subtitle', + + start(chart, _args, options) { + const title = new Title({ + ctx: chart.ctx, + options, + chart + }); + + layouts.configure(chart, title, options); + layouts.addBox(chart, title); + map.set(chart, title); + }, + + stop(chart) { + layouts.removeBox(chart, map.get(chart)); + map.delete(chart); + }, + + beforeUpdate(chart, _args, options) { + const title = map.get(chart); + layouts.configure(chart, title, options); + title.options = options; + }, + + defaults: { + align: 'center', + display: false, + font: { + weight: 'normal', + }, + fullSize: true, + padding: 0, + position: 'top', + text: '', + weight: 1500 // by default greater than legend (1000) and smaller than title (2000) + }, + + defaultRoutes: { + color: 'color' + }, + + descriptors: { + _scriptable: true, + _indexable: false, + }, +}; diff --git a/src/plugins/plugin.title.js b/src/plugins/plugin.title.js new file mode 100644 index 00000000000..ecf625040e6 --- /dev/null +++ b/src/plugins/plugin.title.js @@ -0,0 +1,166 @@ +import Element from '../core/core.element.js'; +import layouts from '../core/core.layouts.js'; +import {PI, isArray, toPadding, toFont} from '../helpers/index.js'; +import {_toLeftRightCenter, _alignStartEnd} from '../helpers/helpers.extras.js'; +import {renderText} from '../helpers/helpers.canvas.js'; + +export class Title extends Element { + /** + * @param {{ ctx: any; options: any; chart: any; }} config + */ + constructor(config) { + super(); + + this.chart = config.chart; + this.options = config.options; + this.ctx = config.ctx; + this._padding = undefined; + this.top = undefined; + this.bottom = undefined; + this.left = undefined; + this.right = undefined; + this.width = undefined; + this.height = undefined; + this.position = undefined; + this.weight = undefined; + this.fullSize = undefined; + } + + update(maxWidth, maxHeight) { + const opts = this.options; + + this.left = 0; + this.top = 0; + + if (!opts.display) { + this.width = this.height = this.right = this.bottom = 0; + return; + } + + this.width = this.right = maxWidth; + this.height = this.bottom = maxHeight; + + const lineCount = isArray(opts.text) ? opts.text.length : 1; + this._padding = toPadding(opts.padding); + const textSize = lineCount * toFont(opts.font).lineHeight + this._padding.height; + + if (this.isHorizontal()) { + this.height = textSize; + } else { + this.width = textSize; + } + } + + isHorizontal() { + const pos = this.options.position; + return pos === 'top' || pos === 'bottom'; + } + + _drawArgs(offset) { + const {top, left, bottom, right, options} = this; + const align = options.align; + let rotation = 0; + let maxWidth, titleX, titleY; + + if (this.isHorizontal()) { + titleX = _alignStartEnd(align, left, right); + titleY = top + offset; + maxWidth = right - left; + } else { + if (options.position === 'left') { + titleX = left + offset; + titleY = _alignStartEnd(align, bottom, top); + rotation = PI * -0.5; + } else { + titleX = right - offset; + titleY = _alignStartEnd(align, top, bottom); + rotation = PI * 0.5; + } + maxWidth = bottom - top; + } + return {titleX, titleY, maxWidth, rotation}; + } + + draw() { + const ctx = this.ctx; + const opts = this.options; + + if (!opts.display) { + return; + } + + const fontOpts = toFont(opts.font); + const lineHeight = fontOpts.lineHeight; + const offset = lineHeight / 2 + this._padding.top; + const {titleX, titleY, maxWidth, rotation} = this._drawArgs(offset); + + renderText(ctx, opts.text, 0, 0, fontOpts, { + color: opts.color, + maxWidth, + rotation, + textAlign: _toLeftRightCenter(opts.align), + textBaseline: 'middle', + translation: [titleX, titleY], + }); + } +} + +function createTitle(chart, titleOpts) { + const title = new Title({ + ctx: chart.ctx, + options: titleOpts, + chart + }); + + layouts.configure(chart, title, titleOpts); + layouts.addBox(chart, title); + chart.titleBlock = title; +} + +export default { + id: 'title', + + /** + * For tests + * @private + */ + _element: Title, + + start(chart, _args, options) { + createTitle(chart, options); + }, + + stop(chart) { + const titleBlock = chart.titleBlock; + layouts.removeBox(chart, titleBlock); + delete chart.titleBlock; + }, + + beforeUpdate(chart, _args, options) { + const title = chart.titleBlock; + layouts.configure(chart, title, options); + title.options = options; + }, + + defaults: { + align: 'center', + display: false, + font: { + weight: 'bold', + }, + fullSize: true, + padding: 10, + position: 'top', + text: '', + weight: 2000 // by default greater than legend (1000) to be above + }, + + defaultRoutes: { + color: 'color' + }, + + descriptors: { + _scriptable: true, + _indexable: false, + }, +}; diff --git a/src/plugins/plugin.tooltip.js b/src/plugins/plugin.tooltip.js new file mode 100644 index 00000000000..b39681ce2ca --- /dev/null +++ b/src/plugins/plugin.tooltip.js @@ -0,0 +1,1350 @@ +import Animations from '../core/core.animations.js'; +import Element from '../core/core.element.js'; +import {addRoundedRectPath} from '../helpers/helpers.canvas.js'; +import {each, noop, isNullOrUndef, isArray, _elementsEqual, isObject} from '../helpers/helpers.core.js'; +import {toFont, toPadding, toTRBLCorners} from '../helpers/helpers.options.js'; +import {getRtlAdapter, overrideTextDirection, restoreTextDirection} from '../helpers/helpers.rtl.js'; +import {distanceBetweenPoints, _limitValue} from '../helpers/helpers.math.js'; +import {createContext, drawPoint} from '../helpers/index.js'; + +/** + * @typedef { import('../platform/platform.base.js').Chart } Chart + * @typedef { import('../types/index.js').ChartEvent } ChartEvent + * @typedef { import('../types/index.js').ActiveElement } ActiveElement + * @typedef { import('../core/core.interaction.js').InteractionItem } InteractionItem + */ + +const positioners = { + /** + * Average mode places the tooltip at the average position of the elements shown + */ + average(items) { + if (!items.length) { + return false; + } + + let i, len; + let xSet = new Set(); + let y = 0; + let count = 0; + + for (i = 0, len = items.length; i < len; ++i) { + const el = items[i].element; + if (el && el.hasValue()) { + const pos = el.tooltipPosition(); + xSet.add(pos.x); + y += pos.y; + ++count; + } + } + + // No visible items where found, return false so we don't have to divide by 0 which reduces in NaN + if (count === 0 || xSet.size === 0) { + return false; + } + + const xAverage = [...xSet].reduce((a, b) => a + b) / xSet.size; + + return { + x: xAverage, + y: y / count + }; + }, + + /** + * Gets the tooltip position nearest of the item nearest to the event position + */ + nearest(items, eventPosition) { + if (!items.length) { + return false; + } + + let x = eventPosition.x; + let y = eventPosition.y; + let minDistance = Number.POSITIVE_INFINITY; + let i, len, nearestElement; + + for (i = 0, len = items.length; i < len; ++i) { + const el = items[i].element; + if (el && el.hasValue()) { + const center = el.getCenterPoint(); + const d = distanceBetweenPoints(eventPosition, center); + + if (d < minDistance) { + minDistance = d; + nearestElement = el; + } + } + } + + if (nearestElement) { + const tp = nearestElement.tooltipPosition(); + x = tp.x; + y = tp.y; + } + + return { + x, + y + }; + } +}; + +// Helper to push or concat based on if the 2nd parameter is an array or not +function pushOrConcat(base, toPush) { + if (toPush) { + if (isArray(toPush)) { + // base = base.concat(toPush); + Array.prototype.push.apply(base, toPush); + } else { + base.push(toPush); + } + } + + return base; +} + +/** + * Returns array of strings split by newline + * @param {*} str - The value to split by newline. + * @returns {string|string[]} value if newline present - Returned from String split() method + * @function + */ +function splitNewlines(str) { + if ((typeof str === 'string' || str instanceof String) && str.indexOf('\n') > -1) { + return str.split('\n'); + } + return str; +} + + +/** + * Private helper to create a tooltip item model + * @param {Chart} chart + * @param {ActiveElement} item - {element, index, datasetIndex} to create the tooltip item for + * @return new tooltip item + */ +function createTooltipItem(chart, item) { + const {element, datasetIndex, index} = item; + const controller = chart.getDatasetMeta(datasetIndex).controller; + const {label, value} = controller.getLabelAndValue(index); + + return { + chart, + label, + parsed: controller.getParsed(index), + raw: chart.data.datasets[datasetIndex].data[index], + formattedValue: value, + dataset: controller.getDataset(), + dataIndex: index, + datasetIndex, + element + }; +} + +/** + * Get the size of the tooltip + */ +function getTooltipSize(tooltip, options) { + const ctx = tooltip.chart.ctx; + const {body, footer, title} = tooltip; + const {boxWidth, boxHeight} = options; + const bodyFont = toFont(options.bodyFont); + const titleFont = toFont(options.titleFont); + const footerFont = toFont(options.footerFont); + const titleLineCount = title.length; + const footerLineCount = footer.length; + const bodyLineItemCount = body.length; + + const padding = toPadding(options.padding); + let height = padding.height; + let width = 0; + + // Count of all lines in the body + let combinedBodyLength = body.reduce((count, bodyItem) => count + bodyItem.before.length + bodyItem.lines.length + bodyItem.after.length, 0); + combinedBodyLength += tooltip.beforeBody.length + tooltip.afterBody.length; + + if (titleLineCount) { + height += titleLineCount * titleFont.lineHeight + + (titleLineCount - 1) * options.titleSpacing + + options.titleMarginBottom; + } + if (combinedBodyLength) { + // Body lines may include some extra height depending on boxHeight + const bodyLineHeight = options.displayColors ? Math.max(boxHeight, bodyFont.lineHeight) : bodyFont.lineHeight; + height += bodyLineItemCount * bodyLineHeight + + (combinedBodyLength - bodyLineItemCount) * bodyFont.lineHeight + + (combinedBodyLength - 1) * options.bodySpacing; + } + if (footerLineCount) { + height += options.footerMarginTop + + footerLineCount * footerFont.lineHeight + + (footerLineCount - 1) * options.footerSpacing; + } + + // Title width + let widthPadding = 0; + const maxLineWidth = function(line) { + width = Math.max(width, ctx.measureText(line).width + widthPadding); + }; + + ctx.save(); + + ctx.font = titleFont.string; + each(tooltip.title, maxLineWidth); + + // Body width + ctx.font = bodyFont.string; + each(tooltip.beforeBody.concat(tooltip.afterBody), maxLineWidth); + + // Body lines may include some extra width due to the color box + widthPadding = options.displayColors ? (boxWidth + 2 + options.boxPadding) : 0; + each(body, (bodyItem) => { + each(bodyItem.before, maxLineWidth); + each(bodyItem.lines, maxLineWidth); + each(bodyItem.after, maxLineWidth); + }); + + // Reset back to 0 + widthPadding = 0; + + // Footer width + ctx.font = footerFont.string; + each(tooltip.footer, maxLineWidth); + + ctx.restore(); + + // Add padding + width += padding.width; + + return {width, height}; +} + +function determineYAlign(chart, size) { + const {y, height} = size; + + if (y < height / 2) { + return 'top'; + } else if (y > (chart.height - height / 2)) { + return 'bottom'; + } + return 'center'; +} + +function doesNotFitWithAlign(xAlign, chart, options, size) { + const {x, width} = size; + const caret = options.caretSize + options.caretPadding; + if (xAlign === 'left' && x + width + caret > chart.width) { + return true; + } + + if (xAlign === 'right' && x - width - caret < 0) { + return true; + } +} + +function determineXAlign(chart, options, size, yAlign) { + const {x, width} = size; + const {width: chartWidth, chartArea: {left, right}} = chart; + let xAlign = 'center'; + + if (yAlign === 'center') { + xAlign = x <= (left + right) / 2 ? 'left' : 'right'; + } else if (x <= width / 2) { + xAlign = 'left'; + } else if (x >= chartWidth - width / 2) { + xAlign = 'right'; + } + + if (doesNotFitWithAlign(xAlign, chart, options, size)) { + xAlign = 'center'; + } + + return xAlign; +} + +/** + * Helper to get the alignment of a tooltip given the size + */ +function determineAlignment(chart, options, size) { + const yAlign = size.yAlign || options.yAlign || determineYAlign(chart, size); + + return { + xAlign: size.xAlign || options.xAlign || determineXAlign(chart, options, size, yAlign), + yAlign + }; +} + +function alignX(size, xAlign) { + let {x, width} = size; + if (xAlign === 'right') { + x -= width; + } else if (xAlign === 'center') { + x -= (width / 2); + } + return x; +} + +function alignY(size, yAlign, paddingAndSize) { + // eslint-disable-next-line prefer-const + let {y, height} = size; + if (yAlign === 'top') { + y += paddingAndSize; + } else if (yAlign === 'bottom') { + y -= height + paddingAndSize; + } else { + y -= (height / 2); + } + return y; +} + +/** + * Helper to get the location a tooltip needs to be placed at given the initial position (via the vm) and the size and alignment + */ +function getBackgroundPoint(options, size, alignment, chart) { + const {caretSize, caretPadding, cornerRadius} = options; + const {xAlign, yAlign} = alignment; + const paddingAndSize = caretSize + caretPadding; + const {topLeft, topRight, bottomLeft, bottomRight} = toTRBLCorners(cornerRadius); + + let x = alignX(size, xAlign); + const y = alignY(size, yAlign, paddingAndSize); + + if (yAlign === 'center') { + if (xAlign === 'left') { + x += paddingAndSize; + } else if (xAlign === 'right') { + x -= paddingAndSize; + } + } else if (xAlign === 'left') { + x -= Math.max(topLeft, bottomLeft) + caretSize; + } else if (xAlign === 'right') { + x += Math.max(topRight, bottomRight) + caretSize; + } + + return { + x: _limitValue(x, 0, chart.width - size.width), + y: _limitValue(y, 0, chart.height - size.height) + }; +} + +function getAlignedX(tooltip, align, options) { + const padding = toPadding(options.padding); + + return align === 'center' + ? tooltip.x + tooltip.width / 2 + : align === 'right' + ? tooltip.x + tooltip.width - padding.right + : tooltip.x + padding.left; +} + +/** + * Helper to build before and after body lines + */ +function getBeforeAfterBodyLines(callback) { + return pushOrConcat([], splitNewlines(callback)); +} + +function createTooltipContext(parent, tooltip, tooltipItems) { + return createContext(parent, { + tooltip, + tooltipItems, + type: 'tooltip' + }); +} + +function overrideCallbacks(callbacks, context) { + const override = context && context.dataset && context.dataset.tooltip && context.dataset.tooltip.callbacks; + return override ? callbacks.override(override) : callbacks; +} + +const defaultCallbacks = { + // Args are: (tooltipItems, data) + beforeTitle: noop, + title(tooltipItems) { + if (tooltipItems.length > 0) { + const item = tooltipItems[0]; + const labels = item.chart.data.labels; + const labelCount = labels ? labels.length : 0; + + if (this && this.options && this.options.mode === 'dataset') { + return item.dataset.label || ''; + } else if (item.label) { + return item.label; + } else if (labelCount > 0 && item.dataIndex < labelCount) { + return labels[item.dataIndex]; + } + } + + return ''; + }, + afterTitle: noop, + + // Args are: (tooltipItems, data) + beforeBody: noop, + + // Args are: (tooltipItem, data) + beforeLabel: noop, + label(tooltipItem) { + if (this && this.options && this.options.mode === 'dataset') { + return tooltipItem.label + ': ' + tooltipItem.formattedValue || tooltipItem.formattedValue; + } + + let label = tooltipItem.dataset.label || ''; + + if (label) { + label += ': '; + } + const value = tooltipItem.formattedValue; + if (!isNullOrUndef(value)) { + label += value; + } + return label; + }, + labelColor(tooltipItem) { + const meta = tooltipItem.chart.getDatasetMeta(tooltipItem.datasetIndex); + const options = meta.controller.getStyle(tooltipItem.dataIndex); + return { + borderColor: options.borderColor, + backgroundColor: options.backgroundColor, + borderWidth: options.borderWidth, + borderDash: options.borderDash, + borderDashOffset: options.borderDashOffset, + borderRadius: 0, + }; + }, + labelTextColor() { + return this.options.bodyColor; + }, + labelPointStyle(tooltipItem) { + const meta = tooltipItem.chart.getDatasetMeta(tooltipItem.datasetIndex); + const options = meta.controller.getStyle(tooltipItem.dataIndex); + return { + pointStyle: options.pointStyle, + rotation: options.rotation, + }; + }, + afterLabel: noop, + + // Args are: (tooltipItems, data) + afterBody: noop, + + // Args are: (tooltipItems, data) + beforeFooter: noop, + footer: noop, + afterFooter: noop +}; + +/** + * Invoke callback from object with context and arguments. + * If callback returns `undefined`, then will be invoked default callback. + * @param {Record} callbacks + * @param {keyof typeof defaultCallbacks} name + * @param {*} ctx + * @param {*} arg + * @returns {any} + */ +function invokeCallbackWithFallback(callbacks, name, ctx, arg) { + const result = callbacks[name].call(ctx, arg); + + if (typeof result === 'undefined') { + return defaultCallbacks[name].call(ctx, arg); + } + + return result; +} + +export class Tooltip extends Element { + + /** + * @namespace Chart.Tooltip.positioners + */ + static positioners = positioners; + + constructor(config) { + super(); + + this.opacity = 0; + this._active = []; + this._eventPosition = undefined; + this._size = undefined; + this._cachedAnimations = undefined; + this._tooltipItems = []; + this.$animations = undefined; + this.$context = undefined; + this.chart = config.chart; + this.options = config.options; + this.dataPoints = undefined; + this.title = undefined; + this.beforeBody = undefined; + this.body = undefined; + this.afterBody = undefined; + this.footer = undefined; + this.xAlign = undefined; + this.yAlign = undefined; + this.x = undefined; + this.y = undefined; + this.height = undefined; + this.width = undefined; + this.caretX = undefined; + this.caretY = undefined; + // TODO: V4, make this private, rename to `_labelStyles`, and combine with `labelPointStyles` + // and `labelTextColors` to create a single variable + this.labelColors = undefined; + this.labelPointStyles = undefined; + this.labelTextColors = undefined; + } + + initialize(options) { + this.options = options; + this._cachedAnimations = undefined; + this.$context = undefined; + } + + /** + * @private + */ + _resolveAnimations() { + const cached = this._cachedAnimations; + + if (cached) { + return cached; + } + + const chart = this.chart; + const options = this.options.setContext(this.getContext()); + const opts = options.enabled && chart.options.animation && options.animations; + const animations = new Animations(this.chart, opts); + if (opts._cacheable) { + this._cachedAnimations = Object.freeze(animations); + } + + return animations; + } + + /** + * @protected + */ + getContext() { + return this.$context || + (this.$context = createTooltipContext(this.chart.getContext(), this, this._tooltipItems)); + } + + getTitle(context, options) { + const {callbacks} = options; + + const beforeTitle = invokeCallbackWithFallback(callbacks, 'beforeTitle', this, context); + const title = invokeCallbackWithFallback(callbacks, 'title', this, context); + const afterTitle = invokeCallbackWithFallback(callbacks, 'afterTitle', this, context); + + let lines = []; + lines = pushOrConcat(lines, splitNewlines(beforeTitle)); + lines = pushOrConcat(lines, splitNewlines(title)); + lines = pushOrConcat(lines, splitNewlines(afterTitle)); + + return lines; + } + + getBeforeBody(tooltipItems, options) { + return getBeforeAfterBodyLines( + invokeCallbackWithFallback(options.callbacks, 'beforeBody', this, tooltipItems) + ); + } + + getBody(tooltipItems, options) { + const {callbacks} = options; + const bodyItems = []; + + each(tooltipItems, (context) => { + const bodyItem = { + before: [], + lines: [], + after: [] + }; + const scoped = overrideCallbacks(callbacks, context); + pushOrConcat(bodyItem.before, splitNewlines(invokeCallbackWithFallback(scoped, 'beforeLabel', this, context))); + pushOrConcat(bodyItem.lines, invokeCallbackWithFallback(scoped, 'label', this, context)); + pushOrConcat(bodyItem.after, splitNewlines(invokeCallbackWithFallback(scoped, 'afterLabel', this, context))); + + bodyItems.push(bodyItem); + }); + + return bodyItems; + } + + getAfterBody(tooltipItems, options) { + return getBeforeAfterBodyLines( + invokeCallbackWithFallback(options.callbacks, 'afterBody', this, tooltipItems) + ); + } + + // Get the footer and beforeFooter and afterFooter lines + getFooter(tooltipItems, options) { + const {callbacks} = options; + + const beforeFooter = invokeCallbackWithFallback(callbacks, 'beforeFooter', this, tooltipItems); + const footer = invokeCallbackWithFallback(callbacks, 'footer', this, tooltipItems); + const afterFooter = invokeCallbackWithFallback(callbacks, 'afterFooter', this, tooltipItems); + + let lines = []; + lines = pushOrConcat(lines, splitNewlines(beforeFooter)); + lines = pushOrConcat(lines, splitNewlines(footer)); + lines = pushOrConcat(lines, splitNewlines(afterFooter)); + + return lines; + } + + /** + * @private + */ + _createItems(options) { + const active = this._active; + const data = this.chart.data; + const labelColors = []; + const labelPointStyles = []; + const labelTextColors = []; + let tooltipItems = []; + let i, len; + + for (i = 0, len = active.length; i < len; ++i) { + tooltipItems.push(createTooltipItem(this.chart, active[i])); + } + + // If the user provided a filter function, use it to modify the tooltip items + if (options.filter) { + tooltipItems = tooltipItems.filter((element, index, array) => options.filter(element, index, array, data)); + } + + // If the user provided a sorting function, use it to modify the tooltip items + if (options.itemSort) { + tooltipItems = tooltipItems.sort((a, b) => options.itemSort(a, b, data)); + } + + // Determine colors for boxes + each(tooltipItems, (context) => { + const scoped = overrideCallbacks(options.callbacks, context); + labelColors.push(invokeCallbackWithFallback(scoped, 'labelColor', this, context)); + labelPointStyles.push(invokeCallbackWithFallback(scoped, 'labelPointStyle', this, context)); + labelTextColors.push(invokeCallbackWithFallback(scoped, 'labelTextColor', this, context)); + }); + + this.labelColors = labelColors; + this.labelPointStyles = labelPointStyles; + this.labelTextColors = labelTextColors; + this.dataPoints = tooltipItems; + return tooltipItems; + } + + update(changed, replay) { + const options = this.options.setContext(this.getContext()); + const active = this._active; + let properties; + let tooltipItems = []; + + if (!active.length) { + if (this.opacity !== 0) { + properties = { + opacity: 0 + }; + } + } else { + const position = positioners[options.position].call(this, active, this._eventPosition); + tooltipItems = this._createItems(options); + + this.title = this.getTitle(tooltipItems, options); + this.beforeBody = this.getBeforeBody(tooltipItems, options); + this.body = this.getBody(tooltipItems, options); + this.afterBody = this.getAfterBody(tooltipItems, options); + this.footer = this.getFooter(tooltipItems, options); + + const size = this._size = getTooltipSize(this, options); + const positionAndSize = Object.assign({}, position, size); + const alignment = determineAlignment(this.chart, options, positionAndSize); + const backgroundPoint = getBackgroundPoint(options, positionAndSize, alignment, this.chart); + + this.xAlign = alignment.xAlign; + this.yAlign = alignment.yAlign; + + properties = { + opacity: 1, + x: backgroundPoint.x, + y: backgroundPoint.y, + width: size.width, + height: size.height, + caretX: position.x, + caretY: position.y + }; + } + + this._tooltipItems = tooltipItems; + this.$context = undefined; + + if (properties) { + this._resolveAnimations().update(this, properties); + } + + if (changed && options.external) { + options.external.call(this, {chart: this.chart, tooltip: this, replay}); + } + } + + drawCaret(tooltipPoint, ctx, size, options) { + const caretPosition = this.getCaretPosition(tooltipPoint, size, options); + + ctx.lineTo(caretPosition.x1, caretPosition.y1); + ctx.lineTo(caretPosition.x2, caretPosition.y2); + ctx.lineTo(caretPosition.x3, caretPosition.y3); + } + + getCaretPosition(tooltipPoint, size, options) { + const {xAlign, yAlign} = this; + const {caretSize, cornerRadius} = options; + const {topLeft, topRight, bottomLeft, bottomRight} = toTRBLCorners(cornerRadius); + const {x: ptX, y: ptY} = tooltipPoint; + const {width, height} = size; + let x1, x2, x3, y1, y2, y3; + + if (yAlign === 'center') { + y2 = ptY + (height / 2); + + if (xAlign === 'left') { + x1 = ptX; + x2 = x1 - caretSize; + + // Left draws bottom -> top, this y1 is on the bottom + y1 = y2 + caretSize; + y3 = y2 - caretSize; + } else { + x1 = ptX + width; + x2 = x1 + caretSize; + + // Right draws top -> bottom, thus y1 is on the top + y1 = y2 - caretSize; + y3 = y2 + caretSize; + } + + x3 = x1; + } else { + if (xAlign === 'left') { + x2 = ptX + Math.max(topLeft, bottomLeft) + (caretSize); + } else if (xAlign === 'right') { + x2 = ptX + width - Math.max(topRight, bottomRight) - caretSize; + } else { + x2 = this.caretX; + } + + if (yAlign === 'top') { + y1 = ptY; + y2 = y1 - caretSize; + + // Top draws left -> right, thus x1 is on the left + x1 = x2 - caretSize; + x3 = x2 + caretSize; + } else { + y1 = ptY + height; + y2 = y1 + caretSize; + + // Bottom draws right -> left, thus x1 is on the right + x1 = x2 + caretSize; + x3 = x2 - caretSize; + } + y3 = y1; + } + return {x1, x2, x3, y1, y2, y3}; + } + + drawTitle(pt, ctx, options) { + const title = this.title; + const length = title.length; + let titleFont, titleSpacing, i; + + if (length) { + const rtlHelper = getRtlAdapter(options.rtl, this.x, this.width); + + pt.x = getAlignedX(this, options.titleAlign, options); + + ctx.textAlign = rtlHelper.textAlign(options.titleAlign); + ctx.textBaseline = 'middle'; + + titleFont = toFont(options.titleFont); + titleSpacing = options.titleSpacing; + + ctx.fillStyle = options.titleColor; + ctx.font = titleFont.string; + + for (i = 0; i < length; ++i) { + ctx.fillText(title[i], rtlHelper.x(pt.x), pt.y + titleFont.lineHeight / 2); + pt.y += titleFont.lineHeight + titleSpacing; // Line Height and spacing + + if (i + 1 === length) { + pt.y += options.titleMarginBottom - titleSpacing; // If Last, add margin, remove spacing + } + } + } + } + + /** + * @private + */ + _drawColorBox(ctx, pt, i, rtlHelper, options) { + const labelColor = this.labelColors[i]; + const labelPointStyle = this.labelPointStyles[i]; + const {boxHeight, boxWidth} = options; + const bodyFont = toFont(options.bodyFont); + const colorX = getAlignedX(this, 'left', options); + const rtlColorX = rtlHelper.x(colorX); + const yOffSet = boxHeight < bodyFont.lineHeight ? (bodyFont.lineHeight - boxHeight) / 2 : 0; + const colorY = pt.y + yOffSet; + + if (options.usePointStyle) { + const drawOptions = { + radius: Math.min(boxWidth, boxHeight) / 2, // fit the circle in the box + pointStyle: labelPointStyle.pointStyle, + rotation: labelPointStyle.rotation, + borderWidth: 1 + }; + // Recalculate x and y for drawPoint() because its expecting + // x and y to be center of figure (instead of top left) + const centerX = rtlHelper.leftForLtr(rtlColorX, boxWidth) + boxWidth / 2; + const centerY = colorY + boxHeight / 2; + + // Fill the point with white so that colours merge nicely if the opacity is < 1 + ctx.strokeStyle = options.multiKeyBackground; + ctx.fillStyle = options.multiKeyBackground; + drawPoint(ctx, drawOptions, centerX, centerY); + + // Draw the point + ctx.strokeStyle = labelColor.borderColor; + ctx.fillStyle = labelColor.backgroundColor; + drawPoint(ctx, drawOptions, centerX, centerY); + } else { + // Border + ctx.lineWidth = isObject(labelColor.borderWidth) ? Math.max(...Object.values(labelColor.borderWidth)) : (labelColor.borderWidth || 1); // TODO, v4 remove fallback + ctx.strokeStyle = labelColor.borderColor; + ctx.setLineDash(labelColor.borderDash || []); + ctx.lineDashOffset = labelColor.borderDashOffset || 0; + + // Fill a white rect so that colours merge nicely if the opacity is < 1 + const outerX = rtlHelper.leftForLtr(rtlColorX, boxWidth); + const innerX = rtlHelper.leftForLtr(rtlHelper.xPlus(rtlColorX, 1), boxWidth - 2); + const borderRadius = toTRBLCorners(labelColor.borderRadius); + + if (Object.values(borderRadius).some(v => v !== 0)) { + ctx.beginPath(); + ctx.fillStyle = options.multiKeyBackground; + addRoundedRectPath(ctx, { + x: outerX, + y: colorY, + w: boxWidth, + h: boxHeight, + radius: borderRadius, + }); + ctx.fill(); + ctx.stroke(); + + // Inner square + ctx.fillStyle = labelColor.backgroundColor; + ctx.beginPath(); + addRoundedRectPath(ctx, { + x: innerX, + y: colorY + 1, + w: boxWidth - 2, + h: boxHeight - 2, + radius: borderRadius, + }); + ctx.fill(); + } else { + // Normal rect + ctx.fillStyle = options.multiKeyBackground; + ctx.fillRect(outerX, colorY, boxWidth, boxHeight); + ctx.strokeRect(outerX, colorY, boxWidth, boxHeight); + // Inner square + ctx.fillStyle = labelColor.backgroundColor; + ctx.fillRect(innerX, colorY + 1, boxWidth - 2, boxHeight - 2); + } + } + + // restore fillStyle + ctx.fillStyle = this.labelTextColors[i]; + } + + drawBody(pt, ctx, options) { + const {body} = this; + const {bodySpacing, bodyAlign, displayColors, boxHeight, boxWidth, boxPadding} = options; + const bodyFont = toFont(options.bodyFont); + let bodyLineHeight = bodyFont.lineHeight; + let xLinePadding = 0; + + const rtlHelper = getRtlAdapter(options.rtl, this.x, this.width); + + const fillLineOfText = function(line) { + ctx.fillText(line, rtlHelper.x(pt.x + xLinePadding), pt.y + bodyLineHeight / 2); + pt.y += bodyLineHeight + bodySpacing; + }; + + const bodyAlignForCalculation = rtlHelper.textAlign(bodyAlign); + let bodyItem, textColor, lines, i, j, ilen, jlen; + + ctx.textAlign = bodyAlign; + ctx.textBaseline = 'middle'; + ctx.font = bodyFont.string; + + pt.x = getAlignedX(this, bodyAlignForCalculation, options); + + // Before body lines + ctx.fillStyle = options.bodyColor; + each(this.beforeBody, fillLineOfText); + + xLinePadding = displayColors && bodyAlignForCalculation !== 'right' + ? bodyAlign === 'center' ? (boxWidth / 2 + boxPadding) : (boxWidth + 2 + boxPadding) + : 0; + + // Draw body lines now + for (i = 0, ilen = body.length; i < ilen; ++i) { + bodyItem = body[i]; + textColor = this.labelTextColors[i]; + + ctx.fillStyle = textColor; + each(bodyItem.before, fillLineOfText); + + lines = bodyItem.lines; + // Draw Legend-like boxes if needed + if (displayColors && lines.length) { + this._drawColorBox(ctx, pt, i, rtlHelper, options); + bodyLineHeight = Math.max(bodyFont.lineHeight, boxHeight); + } + + for (j = 0, jlen = lines.length; j < jlen; ++j) { + fillLineOfText(lines[j]); + // Reset for any lines that don't include colorbox + bodyLineHeight = bodyFont.lineHeight; + } + + each(bodyItem.after, fillLineOfText); + } + + // Reset back to 0 for after body + xLinePadding = 0; + bodyLineHeight = bodyFont.lineHeight; + + // After body lines + each(this.afterBody, fillLineOfText); + pt.y -= bodySpacing; // Remove last body spacing + } + + drawFooter(pt, ctx, options) { + const footer = this.footer; + const length = footer.length; + let footerFont, i; + + if (length) { + const rtlHelper = getRtlAdapter(options.rtl, this.x, this.width); + + pt.x = getAlignedX(this, options.footerAlign, options); + pt.y += options.footerMarginTop; + + ctx.textAlign = rtlHelper.textAlign(options.footerAlign); + ctx.textBaseline = 'middle'; + + footerFont = toFont(options.footerFont); + + ctx.fillStyle = options.footerColor; + ctx.font = footerFont.string; + + for (i = 0; i < length; ++i) { + ctx.fillText(footer[i], rtlHelper.x(pt.x), pt.y + footerFont.lineHeight / 2); + pt.y += footerFont.lineHeight + options.footerSpacing; + } + } + } + + drawBackground(pt, ctx, tooltipSize, options) { + const {xAlign, yAlign} = this; + const {x, y} = pt; + const {width, height} = tooltipSize; + const {topLeft, topRight, bottomLeft, bottomRight} = toTRBLCorners(options.cornerRadius); + + ctx.fillStyle = options.backgroundColor; + ctx.strokeStyle = options.borderColor; + ctx.lineWidth = options.borderWidth; + + ctx.beginPath(); + ctx.moveTo(x + topLeft, y); + if (yAlign === 'top') { + this.drawCaret(pt, ctx, tooltipSize, options); + } + ctx.lineTo(x + width - topRight, y); + ctx.quadraticCurveTo(x + width, y, x + width, y + topRight); + if (yAlign === 'center' && xAlign === 'right') { + this.drawCaret(pt, ctx, tooltipSize, options); + } + ctx.lineTo(x + width, y + height - bottomRight); + ctx.quadraticCurveTo(x + width, y + height, x + width - bottomRight, y + height); + if (yAlign === 'bottom') { + this.drawCaret(pt, ctx, tooltipSize, options); + } + ctx.lineTo(x + bottomLeft, y + height); + ctx.quadraticCurveTo(x, y + height, x, y + height - bottomLeft); + if (yAlign === 'center' && xAlign === 'left') { + this.drawCaret(pt, ctx, tooltipSize, options); + } + ctx.lineTo(x, y + topLeft); + ctx.quadraticCurveTo(x, y, x + topLeft, y); + ctx.closePath(); + + ctx.fill(); + + if (options.borderWidth > 0) { + ctx.stroke(); + } + } + + /** + * Update x/y animation targets when _active elements are animating too + * @private + */ + _updateAnimationTarget(options) { + const chart = this.chart; + const anims = this.$animations; + const animX = anims && anims.x; + const animY = anims && anims.y; + if (animX || animY) { + const position = positioners[options.position].call(this, this._active, this._eventPosition); + if (!position) { + return; + } + const size = this._size = getTooltipSize(this, options); + const positionAndSize = Object.assign({}, position, this._size); + const alignment = determineAlignment(chart, options, positionAndSize); + const point = getBackgroundPoint(options, positionAndSize, alignment, chart); + if (animX._to !== point.x || animY._to !== point.y) { + this.xAlign = alignment.xAlign; + this.yAlign = alignment.yAlign; + this.width = size.width; + this.height = size.height; + this.caretX = position.x; + this.caretY = position.y; + this._resolveAnimations().update(this, point); + } + } + } + + /** + * Determine if the tooltip will draw anything + * @returns {boolean} True if the tooltip will render + */ + _willRender() { + return !!this.opacity; + } + + draw(ctx) { + const options = this.options.setContext(this.getContext()); + let opacity = this.opacity; + + if (!opacity) { + return; + } + + this._updateAnimationTarget(options); + + const tooltipSize = { + width: this.width, + height: this.height + }; + const pt = { + x: this.x, + y: this.y + }; + + // IE11/Edge does not like very small opacities, so snap to 0 + opacity = Math.abs(opacity) < 1e-3 ? 0 : opacity; + + const padding = toPadding(options.padding); + + // Truthy/falsey value for empty tooltip + const hasTooltipContent = this.title.length || this.beforeBody.length || this.body.length || this.afterBody.length || this.footer.length; + + if (options.enabled && hasTooltipContent) { + ctx.save(); + ctx.globalAlpha = opacity; + + // Draw Background + this.drawBackground(pt, ctx, tooltipSize, options); + + overrideTextDirection(ctx, options.textDirection); + + pt.y += padding.top; + + // Titles + this.drawTitle(pt, ctx, options); + + // Body + this.drawBody(pt, ctx, options); + + // Footer + this.drawFooter(pt, ctx, options); + + restoreTextDirection(ctx, options.textDirection); + + ctx.restore(); + } + } + + /** + * Get active elements in the tooltip + * @returns {Array} Array of elements that are active in the tooltip + */ + getActiveElements() { + return this._active || []; + } + + /** + * Set active elements in the tooltip + * @param {array} activeElements Array of active datasetIndex/index pairs. + * @param {object} eventPosition Synthetic event position used in positioning + */ + setActiveElements(activeElements, eventPosition) { + const lastActive = this._active; + const active = activeElements.map(({datasetIndex, index}) => { + const meta = this.chart.getDatasetMeta(datasetIndex); + + if (!meta) { + throw new Error('Cannot find a dataset at index ' + datasetIndex); + } + + return { + datasetIndex, + element: meta.data[index], + index, + }; + }); + const changed = !_elementsEqual(lastActive, active); + const positionChanged = this._positionChanged(active, eventPosition); + + if (changed || positionChanged) { + this._active = active; + this._eventPosition = eventPosition; + this._ignoreReplayEvents = true; + this.update(true); + } + } + + /** + * Handle an event + * @param {ChartEvent} e - The event to handle + * @param {boolean} [replay] - This is a replayed event (from update) + * @param {boolean} [inChartArea] - The event is inside chartArea + * @returns {boolean} true if the tooltip changed + */ + handleEvent(e, replay, inChartArea = true) { + if (replay && this._ignoreReplayEvents) { + return false; + } + this._ignoreReplayEvents = false; + + const options = this.options; + const lastActive = this._active || []; + const active = this._getActiveElements(e, lastActive, replay, inChartArea); + + // When there are multiple items shown, but the tooltip position is nearest mode + // an update may need to be made because our position may have changed even though + // the items are the same as before. + const positionChanged = this._positionChanged(active, e); + + // Remember Last Actives + const changed = replay || !_elementsEqual(active, lastActive) || positionChanged; + + // Only handle target event on tooltip change + if (changed) { + this._active = active; + + if (options.enabled || options.external) { + this._eventPosition = { + x: e.x, + y: e.y + }; + + this.update(true, replay); + } + } + + return changed; + } + + /** + * Helper for determining the active elements for event + * @param {ChartEvent} e - The event to handle + * @param {InteractionItem[]} lastActive - Previously active elements + * @param {boolean} [replay] - This is a replayed event (from update) + * @param {boolean} [inChartArea] - The event is inside chartArea + * @returns {InteractionItem[]} - Active elements + * @private + */ + _getActiveElements(e, lastActive, replay, inChartArea) { + const options = this.options; + + if (e.type === 'mouseout') { + return []; + } + + if (!inChartArea) { + // Let user control the active elements outside chartArea. Eg. using Legend. + // But make sure that active elements are still valid. + return lastActive.filter(i => + this.chart.data.datasets[i.datasetIndex] && + this.chart.getDatasetMeta(i.datasetIndex).controller.getParsed(i.index) !== undefined + ); + } + + // Find Active Elements for tooltips + const active = this.chart.getElementsAtEventForMode(e, options.mode, options, replay); + + if (options.reverse) { + active.reverse(); + } + + return active; + } + + /** + * Determine if the active elements + event combination changes the + * tooltip position + * @param {array} active - Active elements + * @param {ChartEvent} e - Event that triggered the position change + * @returns {boolean} True if the position has changed + */ + _positionChanged(active, e) { + const {caretX, caretY, options} = this; + const position = positioners[options.position].call(this, active, e); + return position !== false && (caretX !== position.x || caretY !== position.y); + } +} + +export default { + id: 'tooltip', + _element: Tooltip, + positioners, + + afterInit(chart, _args, options) { + if (options) { + chart.tooltip = new Tooltip({chart, options}); + } + }, + + beforeUpdate(chart, _args, options) { + if (chart.tooltip) { + chart.tooltip.initialize(options); + } + }, + + reset(chart, _args, options) { + if (chart.tooltip) { + chart.tooltip.initialize(options); + } + }, + + afterDraw(chart) { + const tooltip = chart.tooltip; + + if (tooltip && tooltip._willRender()) { + const args = { + tooltip + }; + + if (chart.notifyPlugins('beforeTooltipDraw', {...args, cancelable: true}) === false) { + return; + } + + tooltip.draw(chart.ctx); + + chart.notifyPlugins('afterTooltipDraw', args); + } + }, + + afterEvent(chart, args) { + if (chart.tooltip) { + // If the event is replayed from `update`, we should evaluate with the final positions. + const useFinalPosition = args.replay; + if (chart.tooltip.handleEvent(args.event, useFinalPosition, args.inChartArea)) { + // notify chart about the change, so it will render + args.changed = true; + } + } + }, + + defaults: { + enabled: true, + external: null, + position: 'average', + backgroundColor: 'rgba(0,0,0,0.8)', + titleColor: '#fff', + titleFont: { + weight: 'bold', + }, + titleSpacing: 2, + titleMarginBottom: 6, + titleAlign: 'left', + bodyColor: '#fff', + bodySpacing: 2, + bodyFont: { + }, + bodyAlign: 'left', + footerColor: '#fff', + footerSpacing: 2, + footerMarginTop: 6, + footerFont: { + weight: 'bold', + }, + footerAlign: 'left', + padding: 6, + caretPadding: 2, + caretSize: 5, + cornerRadius: 6, + boxHeight: (ctx, opts) => opts.bodyFont.size, + boxWidth: (ctx, opts) => opts.bodyFont.size, + multiKeyBackground: '#fff', + displayColors: true, + boxPadding: 0, + borderColor: 'rgba(0,0,0,0)', + borderWidth: 0, + animation: { + duration: 400, + easing: 'easeOutQuart', + }, + animations: { + numbers: { + type: 'number', + properties: ['x', 'y', 'width', 'height', 'caretX', 'caretY'], + }, + opacity: { + easing: 'linear', + duration: 200 + } + }, + callbacks: defaultCallbacks + }, + + defaultRoutes: { + bodyFont: 'font', + footerFont: 'font', + titleFont: 'font' + }, + + descriptors: { + _scriptable: (name) => name !== 'filter' && name !== 'itemSort' && name !== 'external', + _indexable: false, + callbacks: { + _scriptable: false, + _indexable: false, + }, + animation: { + _fallback: false + }, + animations: { + _fallback: 'animation' + } + }, + + // Resolve additionally from `interaction` options and defaults. + additionalOptionScopes: ['interaction'] +}; diff --git a/src/scales/index.js b/src/scales/index.js new file mode 100644 index 00000000000..02ed3c10a2a --- /dev/null +++ b/src/scales/index.js @@ -0,0 +1,6 @@ +export {default as CategoryScale} from './scale.category.js'; +export {default as LinearScale} from './scale.linear.js'; +export {default as LogarithmicScale} from './scale.logarithmic.js'; +export {default as RadialLinearScale} from './scale.radialLinear.js'; +export {default as TimeScale} from './scale.time.js'; +export {default as TimeSeriesScale} from './scale.timeseries.js'; diff --git a/src/scales/scale.category.js b/src/scales/scale.category.js new file mode 100644 index 00000000000..3d773a8a8c2 --- /dev/null +++ b/src/scales/scale.category.js @@ -0,0 +1,158 @@ +import Scale from '../core/core.scale.js'; +import {isNullOrUndef, valueOrDefault, _limitValue} from '../helpers/index.js'; + +const addIfString = (labels, raw, index, addedLabels) => { + if (typeof raw === 'string') { + index = labels.push(raw) - 1; + addedLabels.unshift({index, label: raw}); + } else if (isNaN(raw)) { + index = null; + } + return index; +}; + +function findOrAddLabel(labels, raw, index, addedLabels) { + const first = labels.indexOf(raw); + if (first === -1) { + return addIfString(labels, raw, index, addedLabels); + } + const last = labels.lastIndexOf(raw); + return first !== last ? index : first; +} + +const validIndex = (index, max) => index === null ? null : _limitValue(Math.round(index), 0, max); + +function _getLabelForValue(value) { + const labels = this.getLabels(); + + if (value >= 0 && value < labels.length) { + return labels[value]; + } + return value; +} + +export default class CategoryScale extends Scale { + + static id = 'category'; + + /** + * @type {any} + */ + static defaults = { + ticks: { + callback: _getLabelForValue + } + }; + + constructor(cfg) { + super(cfg); + + /** @type {number} */ + this._startValue = undefined; + this._valueRange = 0; + this._addedLabels = []; + } + + init(scaleOptions) { + const added = this._addedLabels; + if (added.length) { + const labels = this.getLabels(); + for (const {index, label} of added) { + if (labels[index] === label) { + labels.splice(index, 1); + } + } + this._addedLabels = []; + } + super.init(scaleOptions); + } + + parse(raw, index) { + if (isNullOrUndef(raw)) { + return null; + } + const labels = this.getLabels(); + index = isFinite(index) && labels[index] === raw ? index + : findOrAddLabel(labels, raw, valueOrDefault(index, raw), this._addedLabels); + return validIndex(index, labels.length - 1); + } + + determineDataLimits() { + const {minDefined, maxDefined} = this.getUserBounds(); + let {min, max} = this.getMinMax(true); + + if (this.options.bounds === 'ticks') { + if (!minDefined) { + min = 0; + } + if (!maxDefined) { + max = this.getLabels().length - 1; + } + } + + this.min = min; + this.max = max; + } + + buildTicks() { + const min = this.min; + const max = this.max; + const offset = this.options.offset; + const ticks = []; + let labels = this.getLabels(); + + // If we are viewing some subset of labels, slice the original array + labels = (min === 0 && max === labels.length - 1) ? labels : labels.slice(min, max + 1); + + this._valueRange = Math.max(labels.length - (offset ? 0 : 1), 1); + this._startValue = this.min - (offset ? 0.5 : 0); + + for (let value = min; value <= max; value++) { + ticks.push({value}); + } + return ticks; + } + + getLabelForValue(value) { + return _getLabelForValue.call(this, value); + } + + /** + * @protected + */ + configure() { + super.configure(); + + if (!this.isHorizontal()) { + // For backward compatibility, vertical category scale reverse is inverted. + this._reversePixels = !this._reversePixels; + } + } + + // Used to get data value locations. Value can either be an index or a numerical value + getPixelForValue(value) { + if (typeof value !== 'number') { + value = this.parse(value); + } + + return value === null ? NaN : this.getPixelForDecimal((value - this._startValue) / this._valueRange); + } + + // Must override base implementation because it calls getPixelForValue + // and category scale can have duplicate values + getPixelForTick(index) { + const ticks = this.ticks; + if (index < 0 || index > ticks.length - 1) { + return null; + } + return this.getPixelForValue(ticks[index].value); + } + + getValueForPixel(pixel) { + return Math.round(this._startValue + this.getDecimalForPixel(pixel) * this._valueRange); + } + + getBasePixel() { + return this.bottom; + } +} diff --git a/src/scales/scale.linear.js b/src/scales/scale.linear.js new file mode 100644 index 00000000000..9fde052a6b0 --- /dev/null +++ b/src/scales/scale.linear.js @@ -0,0 +1,51 @@ +import {isFinite} from '../helpers/helpers.core.js'; +import LinearScaleBase from './scale.linearbase.js'; +import Ticks from '../core/core.ticks.js'; +import {toRadians} from '../helpers/index.js'; + +export default class LinearScale extends LinearScaleBase { + + static id = 'linear'; + + /** + * @type {any} + */ + static defaults = { + ticks: { + callback: Ticks.formatters.numeric + } + }; + + + determineDataLimits() { + const {min, max} = this.getMinMax(true); + + this.min = isFinite(min) ? min : 0; + this.max = isFinite(max) ? max : 1; + + // Common base implementation to handle min, max, beginAtZero + this.handleTickRangeOptions(); + } + + /** + * Returns the maximum number of ticks based on the scale dimension + * @protected + */ + computeTickLimit() { + const horizontal = this.isHorizontal(); + const length = horizontal ? this.width : this.height; + const minRotation = toRadians(this.options.ticks.minRotation); + const ratio = (horizontal ? Math.sin(minRotation) : Math.cos(minRotation)) || 0.001; + const tickFont = this._resolveTickFontOptions(0); + return Math.ceil(length / Math.min(40, tickFont.lineHeight / ratio)); + } + + // Utils + getPixelForValue(value) { + return value === null ? NaN : this.getPixelForDecimal((value - this._startValue) / this._valueRange); + } + + getValueForPixel(pixel) { + return this._startValue + this.getDecimalForPixel(pixel) * this._valueRange; + } +} diff --git a/src/scales/scale.linearbase.js b/src/scales/scale.linearbase.js new file mode 100644 index 00000000000..d2da5501eb7 --- /dev/null +++ b/src/scales/scale.linearbase.js @@ -0,0 +1,313 @@ +import {isNullOrUndef} from '../helpers/helpers.core.js'; +import {almostEquals, almostWhole, niceNum, _decimalPlaces, _setMinAndMaxByKey, sign, toRadians} from '../helpers/helpers.math.js'; +import Scale from '../core/core.scale.js'; +import {formatNumber} from '../helpers/helpers.intl.js'; + +/** + * Generate a set of linear ticks for an axis + * 1. If generationOptions.min, generationOptions.max, and generationOptions.step are defined: + * if (max - min) / step is an integer, ticks are generated as [min, min + step, ..., max] + * Note that the generationOptions.maxCount setting is respected in this scenario + * + * 2. If generationOptions.min, generationOptions.max, and generationOptions.count is defined + * spacing = (max - min) / count + * Ticks are generated as [min, min + spacing, ..., max] + * + * 3. If generationOptions.count is defined + * spacing = (niceMax - niceMin) / count + * + * 4. Compute optimal spacing of ticks using niceNum algorithm + * + * @param generationOptions the options used to generate the ticks + * @param dataRange the range of the data + * @returns {object[]} array of tick objects + */ +function generateTicks(generationOptions, dataRange) { + const ticks = []; + // To get a "nice" value for the tick spacing, we will use the appropriately named + // "nice number" algorithm. See https://stackoverflow.com/questions/8506881/nice-label-algorithm-for-charts-with-minimum-ticks + // for details. + + const MIN_SPACING = 1e-14; + const {bounds, step, min, max, precision, count, maxTicks, maxDigits, includeBounds} = generationOptions; + const unit = step || 1; + const maxSpaces = maxTicks - 1; + const {min: rmin, max: rmax} = dataRange; + const minDefined = !isNullOrUndef(min); + const maxDefined = !isNullOrUndef(max); + const countDefined = !isNullOrUndef(count); + const minSpacing = (rmax - rmin) / (maxDigits + 1); + let spacing = niceNum((rmax - rmin) / maxSpaces / unit) * unit; + let factor, niceMin, niceMax, numSpaces; + + // Beyond MIN_SPACING floating point numbers being to lose precision + // such that we can't do the math necessary to generate ticks + if (spacing < MIN_SPACING && !minDefined && !maxDefined) { + return [{value: rmin}, {value: rmax}]; + } + + numSpaces = Math.ceil(rmax / spacing) - Math.floor(rmin / spacing); + if (numSpaces > maxSpaces) { + // If the calculated num of spaces exceeds maxNumSpaces, recalculate it + spacing = niceNum(numSpaces * spacing / maxSpaces / unit) * unit; + } + + if (!isNullOrUndef(precision)) { + // If the user specified a precision, round to that number of decimal places + factor = Math.pow(10, precision); + spacing = Math.ceil(spacing * factor) / factor; + } + + if (bounds === 'ticks') { + niceMin = Math.floor(rmin / spacing) * spacing; + niceMax = Math.ceil(rmax / spacing) * spacing; + } else { + niceMin = rmin; + niceMax = rmax; + } + + if (minDefined && maxDefined && step && almostWhole((max - min) / step, spacing / 1000)) { + // Case 1: If min, max and stepSize are set and they make an evenly spaced scale use it. + // spacing = step; + // numSpaces = (max - min) / spacing; + // Note that we round here to handle the case where almostWhole translated an FP error + numSpaces = Math.round(Math.min((max - min) / spacing, maxTicks)); + spacing = (max - min) / numSpaces; + niceMin = min; + niceMax = max; + } else if (countDefined) { + // Cases 2 & 3, we have a count specified. Handle optional user defined edges to the range. + // Sometimes these are no-ops, but it makes the code a lot clearer + // and when a user defined range is specified, we want the correct ticks + niceMin = minDefined ? min : niceMin; + niceMax = maxDefined ? max : niceMax; + numSpaces = count - 1; + spacing = (niceMax - niceMin) / numSpaces; + } else { + // Case 4 + numSpaces = (niceMax - niceMin) / spacing; + + // If very close to our rounded value, use it. + if (almostEquals(numSpaces, Math.round(numSpaces), spacing / 1000)) { + numSpaces = Math.round(numSpaces); + } else { + numSpaces = Math.ceil(numSpaces); + } + } + + // The spacing will have changed in cases 1, 2, and 3 so the factor cannot be computed + // until this point + const decimalPlaces = Math.max( + _decimalPlaces(spacing), + _decimalPlaces(niceMin) + ); + factor = Math.pow(10, isNullOrUndef(precision) ? decimalPlaces : precision); + niceMin = Math.round(niceMin * factor) / factor; + niceMax = Math.round(niceMax * factor) / factor; + + let j = 0; + if (minDefined) { + if (includeBounds && niceMin !== min) { + ticks.push({value: min}); + + if (niceMin < min) { + j++; // Skip niceMin + } + // If the next nice tick is close to min, skip it + if (almostEquals(Math.round((niceMin + j * spacing) * factor) / factor, min, relativeLabelSize(min, minSpacing, generationOptions))) { + j++; + } + } else if (niceMin < min) { + j++; + } + } + + for (; j < numSpaces; ++j) { + const tickValue = Math.round((niceMin + j * spacing) * factor) / factor; + if (maxDefined && tickValue > max) { + break; + } + ticks.push({value: tickValue}); + } + + if (maxDefined && includeBounds && niceMax !== max) { + // If the previous tick is too close to max, replace it with max, else add max + if (ticks.length && almostEquals(ticks[ticks.length - 1].value, max, relativeLabelSize(max, minSpacing, generationOptions))) { + ticks[ticks.length - 1].value = max; + } else { + ticks.push({value: max}); + } + } else if (!maxDefined || niceMax === max) { + ticks.push({value: niceMax}); + } + + return ticks; +} + +function relativeLabelSize(value, minSpacing, {horizontal, minRotation}) { + const rad = toRadians(minRotation); + const ratio = (horizontal ? Math.sin(rad) : Math.cos(rad)) || 0.001; + const length = 0.75 * minSpacing * ('' + value).length; + return Math.min(minSpacing / ratio, length); +} + +export default class LinearScaleBase extends Scale { + + constructor(cfg) { + super(cfg); + + /** @type {number} */ + this.start = undefined; + /** @type {number} */ + this.end = undefined; + /** @type {number} */ + this._startValue = undefined; + /** @type {number} */ + this._endValue = undefined; + this._valueRange = 0; + } + + parse(raw, index) { // eslint-disable-line no-unused-vars + if (isNullOrUndef(raw)) { + return null; + } + if ((typeof raw === 'number' || raw instanceof Number) && !isFinite(+raw)) { + return null; + } + + return +raw; + } + + handleTickRangeOptions() { + const {beginAtZero} = this.options; + const {minDefined, maxDefined} = this.getUserBounds(); + let {min, max} = this; + + const setMin = v => (min = minDefined ? min : v); + const setMax = v => (max = maxDefined ? max : v); + + if (beginAtZero) { + const minSign = sign(min); + const maxSign = sign(max); + + if (minSign < 0 && maxSign < 0) { + setMax(0); + } else if (minSign > 0 && maxSign > 0) { + setMin(0); + } + } + + if (min === max) { + let offset = max === 0 ? 1 : Math.abs(max * 0.05); + + setMax(max + offset); + + if (!beginAtZero) { + setMin(min - offset); + } + } + this.min = min; + this.max = max; + } + + getTickLimit() { + const tickOpts = this.options.ticks; + // eslint-disable-next-line prefer-const + let {maxTicksLimit, stepSize} = tickOpts; + let maxTicks; + + if (stepSize) { + maxTicks = Math.ceil(this.max / stepSize) - Math.floor(this.min / stepSize) + 1; + if (maxTicks > 1000) { + console.warn(`scales.${this.id}.ticks.stepSize: ${stepSize} would result generating up to ${maxTicks} ticks. Limiting to 1000.`); + maxTicks = 1000; + } + } else { + maxTicks = this.computeTickLimit(); + maxTicksLimit = maxTicksLimit || 11; + } + + if (maxTicksLimit) { + maxTicks = Math.min(maxTicksLimit, maxTicks); + } + + return maxTicks; + } + + /** + * @protected + */ + computeTickLimit() { + return Number.POSITIVE_INFINITY; + } + + buildTicks() { + const opts = this.options; + const tickOpts = opts.ticks; + + // Figure out what the max number of ticks we can support it is based on the size of + // the axis area. For now, we say that the minimum tick spacing in pixels must be 40 + // We also limit the maximum number of ticks to 11 which gives a nice 10 squares on + // the graph. Make sure we always have at least 2 ticks + let maxTicks = this.getTickLimit(); + maxTicks = Math.max(2, maxTicks); + + const numericGeneratorOptions = { + maxTicks, + bounds: opts.bounds, + min: opts.min, + max: opts.max, + precision: tickOpts.precision, + step: tickOpts.stepSize, + count: tickOpts.count, + maxDigits: this._maxDigits(), + horizontal: this.isHorizontal(), + minRotation: tickOpts.minRotation || 0, + includeBounds: tickOpts.includeBounds !== false + }; + const dataRange = this._range || this; + const ticks = generateTicks(numericGeneratorOptions, dataRange); + + // At this point, we need to update our max and min given the tick values, + // since we probably have expanded the range of the scale + if (opts.bounds === 'ticks') { + _setMinAndMaxByKey(ticks, this, 'value'); + } + + if (opts.reverse) { + ticks.reverse(); + + this.start = this.max; + this.end = this.min; + } else { + this.start = this.min; + this.end = this.max; + } + + return ticks; + } + + /** + * @protected + */ + configure() { + const ticks = this.ticks; + let start = this.min; + let end = this.max; + + super.configure(); + + if (this.options.offset && ticks.length) { + const offset = (end - start) / Math.max(ticks.length - 1, 1) / 2; + start -= offset; + end += offset; + } + this._startValue = start; + this._endValue = end; + this._valueRange = end - start; + } + + getLabelForValue(value) { + return formatNumber(value, this.chart.options.locale, this.options.ticks.format); + } +} diff --git a/src/scales/scale.logarithmic.js b/src/scales/scale.logarithmic.js new file mode 100644 index 00000000000..a9a06cd10cf --- /dev/null +++ b/src/scales/scale.logarithmic.js @@ -0,0 +1,226 @@ +import {finiteOrDefault, isFinite} from '../helpers/helpers.core.js'; +import {formatNumber} from '../helpers/helpers.intl.js'; +import {_setMinAndMaxByKey, log10} from '../helpers/helpers.math.js'; +import Scale from '../core/core.scale.js'; +import LinearScaleBase from './scale.linearbase.js'; +import Ticks from '../core/core.ticks.js'; + +const log10Floor = v => Math.floor(log10(v)); +const changeExponent = (v, m) => Math.pow(10, log10Floor(v) + m); + +function isMajor(tickVal) { + const remain = tickVal / (Math.pow(10, log10Floor(tickVal))); + return remain === 1; +} + +function steps(min, max, rangeExp) { + const rangeStep = Math.pow(10, rangeExp); + const start = Math.floor(min / rangeStep); + const end = Math.ceil(max / rangeStep); + return end - start; +} + +function startExp(min, max) { + const range = max - min; + let rangeExp = log10Floor(range); + while (steps(min, max, rangeExp) > 10) { + rangeExp++; + } + while (steps(min, max, rangeExp) < 10) { + rangeExp--; + } + return Math.min(rangeExp, log10Floor(min)); +} + + +/** + * Generate a set of logarithmic ticks + * @param generationOptions the options used to generate the ticks + * @param dataRange the range of the data + * @returns {object[]} array of tick objects + */ +function generateTicks(generationOptions, {min, max}) { + min = finiteOrDefault(generationOptions.min, min); + const ticks = []; + const minExp = log10Floor(min); + let exp = startExp(min, max); + let precision = exp < 0 ? Math.pow(10, Math.abs(exp)) : 1; + const stepSize = Math.pow(10, exp); + const base = minExp > exp ? Math.pow(10, minExp) : 0; + const start = Math.round((min - base) * precision) / precision; + const offset = Math.floor((min - base) / stepSize / 10) * stepSize * 10; + let significand = Math.floor((start - offset) / Math.pow(10, exp)); + let value = finiteOrDefault(generationOptions.min, Math.round((base + offset + significand * Math.pow(10, exp)) * precision) / precision); + while (value < max) { + ticks.push({value, major: isMajor(value), significand}); + if (significand >= 10) { + significand = significand < 15 ? 15 : 20; + } else { + significand++; + } + if (significand >= 20) { + exp++; + significand = 2; + precision = exp >= 0 ? 1 : precision; + } + value = Math.round((base + offset + significand * Math.pow(10, exp)) * precision) / precision; + } + const lastTick = finiteOrDefault(generationOptions.max, value); + ticks.push({value: lastTick, major: isMajor(lastTick), significand}); + + return ticks; +} + +export default class LogarithmicScale extends Scale { + + static id = 'logarithmic'; + + /** + * @type {any} + */ + static defaults = { + ticks: { + callback: Ticks.formatters.logarithmic, + major: { + enabled: true + } + } + }; + + + constructor(cfg) { + super(cfg); + + /** @type {number} */ + this.start = undefined; + /** @type {number} */ + this.end = undefined; + /** @type {number} */ + this._startValue = undefined; + this._valueRange = 0; + } + + parse(raw, index) { + const value = LinearScaleBase.prototype.parse.apply(this, [raw, index]); + if (value === 0) { + this._zero = true; + return undefined; + } + return isFinite(value) && value > 0 ? value : null; + } + + determineDataLimits() { + const {min, max} = this.getMinMax(true); + + this.min = isFinite(min) ? Math.max(0, min) : null; + this.max = isFinite(max) ? Math.max(0, max) : null; + + if (this.options.beginAtZero) { + this._zero = true; + } + + // if data has `0` in it or `beginAtZero` is true, min (non zero) value is at bottom + // of scale, and it does not equal suggestedMin, lower the min bound by one exp. + if (this._zero && this.min !== this._suggestedMin && !isFinite(this._userMin)) { + this.min = min === changeExponent(this.min, 0) ? changeExponent(this.min, -1) : changeExponent(this.min, 0); + } + + this.handleTickRangeOptions(); + } + + handleTickRangeOptions() { + const {minDefined, maxDefined} = this.getUserBounds(); + let min = this.min; + let max = this.max; + + const setMin = v => (min = minDefined ? min : v); + const setMax = v => (max = maxDefined ? max : v); + + if (min === max) { + if (min <= 0) { // includes null + setMin(1); + setMax(10); + } else { + setMin(changeExponent(min, -1)); + setMax(changeExponent(max, +1)); + } + } + if (min <= 0) { + setMin(changeExponent(max, -1)); + } + if (max <= 0) { + + setMax(changeExponent(min, +1)); + } + + this.min = min; + this.max = max; + } + + buildTicks() { + const opts = this.options; + + const generationOptions = { + min: this._userMin, + max: this._userMax + }; + const ticks = generateTicks(generationOptions, this); + + // At this point, we need to update our max and min given the tick values, + // since we probably have expanded the range of the scale + if (opts.bounds === 'ticks') { + _setMinAndMaxByKey(ticks, this, 'value'); + } + + if (opts.reverse) { + ticks.reverse(); + + this.start = this.max; + this.end = this.min; + } else { + this.start = this.min; + this.end = this.max; + } + + return ticks; + } + + /** + * @param {number} value + * @return {string} + */ + getLabelForValue(value) { + return value === undefined + ? '0' + : formatNumber(value, this.chart.options.locale, this.options.ticks.format); + } + + /** + * @protected + */ + configure() { + const start = this.min; + + super.configure(); + + this._startValue = log10(start); + this._valueRange = log10(this.max) - log10(start); + } + + getPixelForValue(value) { + if (value === undefined || value === 0) { + value = this.min; + } + if (value === null || isNaN(value)) { + return NaN; + } + return this.getPixelForDecimal(value === this.min + ? 0 + : (log10(value) - this._startValue) / this._valueRange); + } + + getValueForPixel(pixel) { + const decimal = this.getDecimalForPixel(pixel); + return Math.pow(10, this._startValue + decimal * this._valueRange); + } +} diff --git a/src/scales/scale.radialLinear.js b/src/scales/scale.radialLinear.js new file mode 100644 index 00000000000..0a2c61f722b --- /dev/null +++ b/src/scales/scale.radialLinear.js @@ -0,0 +1,684 @@ +import defaults from '../core/core.defaults.js'; +import {_longestText, addRoundedRectPath, renderText, _isPointInArea} from '../helpers/helpers.canvas.js'; +import {HALF_PI, TAU, toDegrees, toRadians, _normalizeAngle, PI} from '../helpers/helpers.math.js'; +import LinearScaleBase from './scale.linearbase.js'; +import Ticks from '../core/core.ticks.js'; +import {valueOrDefault, isArray, isFinite, callback as callCallback, isNullOrUndef} from '../helpers/helpers.core.js'; +import {createContext, toFont, toPadding, toTRBLCorners} from '../helpers/helpers.options.js'; + +function getTickBackdropHeight(opts) { + const tickOpts = opts.ticks; + + if (tickOpts.display && opts.display) { + const padding = toPadding(tickOpts.backdropPadding); + return valueOrDefault(tickOpts.font && tickOpts.font.size, defaults.font.size) + padding.height; + } + return 0; +} + +function measureLabelSize(ctx, font, label) { + label = isArray(label) ? label : [label]; + return { + w: _longestText(ctx, font.string, label), + h: label.length * font.lineHeight + }; +} + +function determineLimits(angle, pos, size, min, max) { + if (angle === min || angle === max) { + return { + start: pos - (size / 2), + end: pos + (size / 2) + }; + } else if (angle < min || angle > max) { + return { + start: pos - size, + end: pos + }; + } + + return { + start: pos, + end: pos + size + }; +} + +/** + * Helper function to fit a radial linear scale with point labels + */ +function fitWithPointLabels(scale) { + + // Right, this is really confusing and there is a lot of maths going on here + // The gist of the problem is here: https://gist.github.com/nnnick/696cc9c55f4b0beb8fe9 + // + // Reaction: https://dl.dropboxusercontent.com/u/34601363/toomuchscience.gif + // + // Solution: + // + // We assume the radius of the polygon is half the size of the canvas at first + // at each index we check if the text overlaps. + // + // Where it does, we store that angle and that index. + // + // After finding the largest index and angle we calculate how much we need to remove + // from the shape radius to move the point inwards by that x. + // + // We average the left and right distances to get the maximum shape radius that can fit in the box + // along with labels. + // + // Once we have that, we can find the centre point for the chart, by taking the x text protrusion + // on each side, removing that from the size, halving it and adding the left x protrusion width. + // + // This will mean we have a shape fitted to the canvas, as large as it can be with the labels + // and position it in the most space efficient manner + // + // https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif + + // Get maximum radius of the polygon. Either half the height (minus the text width) or half the width. + // Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points + const orig = { + l: scale.left + scale._padding.left, + r: scale.right - scale._padding.right, + t: scale.top + scale._padding.top, + b: scale.bottom - scale._padding.bottom + }; + const limits = Object.assign({}, orig); + const labelSizes = []; + const padding = []; + const valueCount = scale._pointLabels.length; + const pointLabelOpts = scale.options.pointLabels; + const additionalAngle = pointLabelOpts.centerPointLabels ? PI / valueCount : 0; + + for (let i = 0; i < valueCount; i++) { + const opts = pointLabelOpts.setContext(scale.getPointLabelContext(i)); + padding[i] = opts.padding; + const pointPosition = scale.getPointPosition(i, scale.drawingArea + padding[i], additionalAngle); + const plFont = toFont(opts.font); + const textSize = measureLabelSize(scale.ctx, plFont, scale._pointLabels[i]); + labelSizes[i] = textSize; + + const angleRadians = _normalizeAngle(scale.getIndexAngle(i) + additionalAngle); + const angle = Math.round(toDegrees(angleRadians)); + const hLimits = determineLimits(angle, pointPosition.x, textSize.w, 0, 180); + const vLimits = determineLimits(angle, pointPosition.y, textSize.h, 90, 270); + updateLimits(limits, orig, angleRadians, hLimits, vLimits); + } + + scale.setCenterPoint( + orig.l - limits.l, + limits.r - orig.r, + orig.t - limits.t, + limits.b - orig.b + ); + + // Now that text size is determined, compute the full positions + scale._pointLabelItems = buildPointLabelItems(scale, labelSizes, padding); +} + +function updateLimits(limits, orig, angle, hLimits, vLimits) { + const sin = Math.abs(Math.sin(angle)); + const cos = Math.abs(Math.cos(angle)); + let x = 0; + let y = 0; + if (hLimits.start < orig.l) { + x = (orig.l - hLimits.start) / sin; + limits.l = Math.min(limits.l, orig.l - x); + } else if (hLimits.end > orig.r) { + x = (hLimits.end - orig.r) / sin; + limits.r = Math.max(limits.r, orig.r + x); + } + if (vLimits.start < orig.t) { + y = (orig.t - vLimits.start) / cos; + limits.t = Math.min(limits.t, orig.t - y); + } else if (vLimits.end > orig.b) { + y = (vLimits.end - orig.b) / cos; + limits.b = Math.max(limits.b, orig.b + y); + } +} + +function createPointLabelItem(scale, index, itemOpts) { + const outerDistance = scale.drawingArea; + const {extra, additionalAngle, padding, size} = itemOpts; + const pointLabelPosition = scale.getPointPosition(index, outerDistance + extra + padding, additionalAngle); + const angle = Math.round(toDegrees(_normalizeAngle(pointLabelPosition.angle + HALF_PI))); + const y = yForAngle(pointLabelPosition.y, size.h, angle); + const textAlign = getTextAlignForAngle(angle); + const left = leftForTextAlign(pointLabelPosition.x, size.w, textAlign); + return { + // if to draw or overlapped + visible: true, + + // Text position + x: pointLabelPosition.x, + y, + + // Text rendering data + textAlign, + + // Bounding box + left, + top: y, + right: left + size.w, + bottom: y + size.h + }; +} + +function isNotOverlapped(item, area) { + if (!area) { + return true; + } + const {left, top, right, bottom} = item; + const apexesInArea = _isPointInArea({x: left, y: top}, area) || _isPointInArea({x: left, y: bottom}, area) || + _isPointInArea({x: right, y: top}, area) || _isPointInArea({x: right, y: bottom}, area); + return !apexesInArea; +} + +function buildPointLabelItems(scale, labelSizes, padding) { + const items = []; + const valueCount = scale._pointLabels.length; + const opts = scale.options; + const {centerPointLabels, display} = opts.pointLabels; + const itemOpts = { + extra: getTickBackdropHeight(opts) / 2, + additionalAngle: centerPointLabels ? PI / valueCount : 0 + }; + let area; + + for (let i = 0; i < valueCount; i++) { + itemOpts.padding = padding[i]; + itemOpts.size = labelSizes[i]; + + const item = createPointLabelItem(scale, i, itemOpts); + items.push(item); + if (display === 'auto') { + item.visible = isNotOverlapped(item, area); + if (item.visible) { + area = item; + } + } + } + return items; +} + +function getTextAlignForAngle(angle) { + if (angle === 0 || angle === 180) { + return 'center'; + } else if (angle < 180) { + return 'left'; + } + + return 'right'; +} + +function leftForTextAlign(x, w, align) { + if (align === 'right') { + x -= w; + } else if (align === 'center') { + x -= (w / 2); + } + return x; +} + +function yForAngle(y, h, angle) { + if (angle === 90 || angle === 270) { + y -= (h / 2); + } else if (angle > 270 || angle < 90) { + y -= h; + } + return y; +} + +function drawPointLabelBox(ctx, opts, item) { + const {left, top, right, bottom} = item; + const {backdropColor} = opts; + + if (!isNullOrUndef(backdropColor)) { + const borderRadius = toTRBLCorners(opts.borderRadius); + const padding = toPadding(opts.backdropPadding); + ctx.fillStyle = backdropColor; + + const backdropLeft = left - padding.left; + const backdropTop = top - padding.top; + const backdropWidth = right - left + padding.width; + const backdropHeight = bottom - top + padding.height; + + if (Object.values(borderRadius).some(v => v !== 0)) { + ctx.beginPath(); + addRoundedRectPath(ctx, { + x: backdropLeft, + y: backdropTop, + w: backdropWidth, + h: backdropHeight, + radius: borderRadius, + }); + ctx.fill(); + } else { + ctx.fillRect(backdropLeft, backdropTop, backdropWidth, backdropHeight); + } + } +} + +function drawPointLabels(scale, labelCount) { + const {ctx, options: {pointLabels}} = scale; + + for (let i = labelCount - 1; i >= 0; i--) { + const item = scale._pointLabelItems[i]; + if (!item.visible) { + // overlapping + continue; + } + const optsAtIndex = pointLabels.setContext(scale.getPointLabelContext(i)); + drawPointLabelBox(ctx, optsAtIndex, item); + const plFont = toFont(optsAtIndex.font); + const {x, y, textAlign} = item; + + renderText( + ctx, + scale._pointLabels[i], + x, + y + (plFont.lineHeight / 2), + plFont, + { + color: optsAtIndex.color, + textAlign: textAlign, + textBaseline: 'middle' + } + ); + } +} + +function pathRadiusLine(scale, radius, circular, labelCount) { + const {ctx} = scale; + if (circular) { + // Draw circular arcs between the points + ctx.arc(scale.xCenter, scale.yCenter, radius, 0, TAU); + } else { + // Draw straight lines connecting each index + let pointPosition = scale.getPointPosition(0, radius); + ctx.moveTo(pointPosition.x, pointPosition.y); + + for (let i = 1; i < labelCount; i++) { + pointPosition = scale.getPointPosition(i, radius); + ctx.lineTo(pointPosition.x, pointPosition.y); + } + } +} + +function drawRadiusLine(scale, gridLineOpts, radius, labelCount, borderOpts) { + const ctx = scale.ctx; + const circular = gridLineOpts.circular; + + const {color, lineWidth} = gridLineOpts; + + if ((!circular && !labelCount) || !color || !lineWidth || radius < 0) { + return; + } + + ctx.save(); + ctx.strokeStyle = color; + ctx.lineWidth = lineWidth; + ctx.setLineDash(borderOpts.dash || []); + ctx.lineDashOffset = borderOpts.dashOffset; + + ctx.beginPath(); + pathRadiusLine(scale, radius, circular, labelCount); + ctx.closePath(); + ctx.stroke(); + ctx.restore(); +} + +function createPointLabelContext(parent, index, label) { + return createContext(parent, { + label, + index, + type: 'pointLabel' + }); +} + +export default class RadialLinearScale extends LinearScaleBase { + + static id = 'radialLinear'; + + /** + * @type {any} + */ + static defaults = { + display: true, + + // Boolean - Whether to animate scaling the chart from the centre + animate: true, + position: 'chartArea', + + angleLines: { + display: true, + lineWidth: 1, + borderDash: [], + borderDashOffset: 0.0 + }, + + grid: { + circular: false + }, + + startAngle: 0, + + // label settings + ticks: { + // Boolean - Show a backdrop to the scale label + showLabelBackdrop: true, + + callback: Ticks.formatters.numeric + }, + + pointLabels: { + backdropColor: undefined, + + // Number - The backdrop padding above & below the label in pixels + backdropPadding: 2, + + // Boolean - if true, show point labels + display: true, + + // Number - Point label font size in pixels + font: { + size: 10 + }, + + // Function - Used to convert point labels + callback(label) { + return label; + }, + + // Number - Additionl padding between scale and pointLabel + padding: 5, + + // Boolean - if true, center point labels to slices in polar chart + centerPointLabels: false + } + }; + + static defaultRoutes = { + 'angleLines.color': 'borderColor', + 'pointLabels.color': 'color', + 'ticks.color': 'color' + }; + + static descriptors = { + angleLines: { + _fallback: 'grid' + } + }; + + constructor(cfg) { + super(cfg); + + /** @type {number} */ + this.xCenter = undefined; + /** @type {number} */ + this.yCenter = undefined; + /** @type {number} */ + this.drawingArea = undefined; + /** @type {string[]} */ + this._pointLabels = []; + this._pointLabelItems = []; + } + + setDimensions() { + // Set the unconstrained dimension before label rotation + const padding = this._padding = toPadding(getTickBackdropHeight(this.options) / 2); + const w = this.width = this.maxWidth - padding.width; + const h = this.height = this.maxHeight - padding.height; + this.xCenter = Math.floor(this.left + w / 2 + padding.left); + this.yCenter = Math.floor(this.top + h / 2 + padding.top); + this.drawingArea = Math.floor(Math.min(w, h) / 2); + } + + determineDataLimits() { + const {min, max} = this.getMinMax(false); + + this.min = isFinite(min) && !isNaN(min) ? min : 0; + this.max = isFinite(max) && !isNaN(max) ? max : 0; + + // Common base implementation to handle min, max, beginAtZero + this.handleTickRangeOptions(); + } + + /** + * Returns the maximum number of ticks based on the scale dimension + * @protected + */ + computeTickLimit() { + return Math.ceil(this.drawingArea / getTickBackdropHeight(this.options)); + } + + generateTickLabels(ticks) { + LinearScaleBase.prototype.generateTickLabels.call(this, ticks); + + // Point labels + this._pointLabels = this.getLabels() + .map((value, index) => { + const label = callCallback(this.options.pointLabels.callback, [value, index], this); + return label || label === 0 ? label : ''; + }) + .filter((v, i) => this.chart.getDataVisibility(i)); + } + + fit() { + const opts = this.options; + + if (opts.display && opts.pointLabels.display) { + fitWithPointLabels(this); + } else { + this.setCenterPoint(0, 0, 0, 0); + } + } + + setCenterPoint(leftMovement, rightMovement, topMovement, bottomMovement) { + this.xCenter += Math.floor((leftMovement - rightMovement) / 2); + this.yCenter += Math.floor((topMovement - bottomMovement) / 2); + this.drawingArea -= Math.min(this.drawingArea / 2, Math.max(leftMovement, rightMovement, topMovement, bottomMovement)); + } + + getIndexAngle(index) { + const angleMultiplier = TAU / (this._pointLabels.length || 1); + const startAngle = this.options.startAngle || 0; + + return _normalizeAngle(index * angleMultiplier + toRadians(startAngle)); + } + + getDistanceFromCenterForValue(value) { + if (isNullOrUndef(value)) { + return NaN; + } + + // Take into account half font size + the yPadding of the top value + const scalingFactor = this.drawingArea / (this.max - this.min); + if (this.options.reverse) { + return (this.max - value) * scalingFactor; + } + return (value - this.min) * scalingFactor; + } + + getValueForDistanceFromCenter(distance) { + if (isNullOrUndef(distance)) { + return NaN; + } + + const scaledDistance = distance / (this.drawingArea / (this.max - this.min)); + return this.options.reverse ? this.max - scaledDistance : this.min + scaledDistance; + } + + getPointLabelContext(index) { + const pointLabels = this._pointLabels || []; + + if (index >= 0 && index < pointLabels.length) { + const pointLabel = pointLabels[index]; + return createPointLabelContext(this.getContext(), index, pointLabel); + } + } + + getPointPosition(index, distanceFromCenter, additionalAngle = 0) { + const angle = this.getIndexAngle(index) - HALF_PI + additionalAngle; + return { + x: Math.cos(angle) * distanceFromCenter + this.xCenter, + y: Math.sin(angle) * distanceFromCenter + this.yCenter, + angle + }; + } + + getPointPositionForValue(index, value) { + return this.getPointPosition(index, this.getDistanceFromCenterForValue(value)); + } + + getBasePosition(index) { + return this.getPointPositionForValue(index || 0, this.getBaseValue()); + } + + getPointLabelPosition(index) { + const {left, top, right, bottom} = this._pointLabelItems[index]; + return { + left, + top, + right, + bottom, + }; + } + + /** + * @protected + */ + drawBackground() { + const {backgroundColor, grid: {circular}} = this.options; + if (backgroundColor) { + const ctx = this.ctx; + ctx.save(); + ctx.beginPath(); + pathRadiusLine(this, this.getDistanceFromCenterForValue(this._endValue), circular, this._pointLabels.length); + ctx.closePath(); + ctx.fillStyle = backgroundColor; + ctx.fill(); + ctx.restore(); + } + } + + /** + * @protected + */ + drawGrid() { + const ctx = this.ctx; + const opts = this.options; + const {angleLines, grid, border} = opts; + const labelCount = this._pointLabels.length; + + let i, offset, position; + + if (opts.pointLabels.display) { + drawPointLabels(this, labelCount); + } + + if (grid.display) { + this.ticks.forEach((tick, index) => { + if (index !== 0 || (index === 0 && this.min < 0)) { + offset = this.getDistanceFromCenterForValue(tick.value); + const context = this.getContext(index); + const optsAtIndex = grid.setContext(context); + const optsAtIndexBorder = border.setContext(context); + + drawRadiusLine(this, optsAtIndex, offset, labelCount, optsAtIndexBorder); + } + }); + } + + if (angleLines.display) { + ctx.save(); + + for (i = labelCount - 1; i >= 0; i--) { + const optsAtIndex = angleLines.setContext(this.getPointLabelContext(i)); + const {color, lineWidth} = optsAtIndex; + + if (!lineWidth || !color) { + continue; + } + + ctx.lineWidth = lineWidth; + ctx.strokeStyle = color; + + ctx.setLineDash(optsAtIndex.borderDash); + ctx.lineDashOffset = optsAtIndex.borderDashOffset; + + offset = this.getDistanceFromCenterForValue(opts.reverse ? this.min : this.max); + position = this.getPointPosition(i, offset); + ctx.beginPath(); + ctx.moveTo(this.xCenter, this.yCenter); + ctx.lineTo(position.x, position.y); + ctx.stroke(); + } + + ctx.restore(); + } + } + + /** + * @protected + */ + drawBorder() {} + + /** + * @protected + */ + drawLabels() { + const ctx = this.ctx; + const opts = this.options; + const tickOpts = opts.ticks; + + if (!tickOpts.display) { + return; + } + + const startAngle = this.getIndexAngle(0); + let offset, width; + + ctx.save(); + ctx.translate(this.xCenter, this.yCenter); + ctx.rotate(startAngle); + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + + this.ticks.forEach((tick, index) => { + if ((index === 0 && this.min >= 0) && !opts.reverse) { + return; + } + + const optsAtIndex = tickOpts.setContext(this.getContext(index)); + const tickFont = toFont(optsAtIndex.font); + offset = this.getDistanceFromCenterForValue(this.ticks[index].value); + + if (optsAtIndex.showLabelBackdrop) { + ctx.font = tickFont.string; + width = ctx.measureText(tick.label).width; + ctx.fillStyle = optsAtIndex.backdropColor; + + const padding = toPadding(optsAtIndex.backdropPadding); + ctx.fillRect( + -width / 2 - padding.left, + -offset - tickFont.size / 2 - padding.top, + width + padding.width, + tickFont.size + padding.height + ); + } + + renderText(ctx, tick.label, 0, -offset, tickFont, { + color: optsAtIndex.color, + strokeColor: optsAtIndex.textStrokeColor, + strokeWidth: optsAtIndex.textStrokeWidth, + }); + }); + + ctx.restore(); + } + + /** + * @protected + */ + drawTitle() {} +} diff --git a/src/scales/scale.time.js b/src/scales/scale.time.js new file mode 100644 index 00000000000..f82d43ad72d --- /dev/null +++ b/src/scales/scale.time.js @@ -0,0 +1,675 @@ +import adapters from '../core/core.adapters.js'; +import {callback as call, isFinite, isNullOrUndef, mergeIf, valueOrDefault} from '../helpers/helpers.core.js'; +import {toRadians, isNumber, _limitValue} from '../helpers/helpers.math.js'; +import Scale from '../core/core.scale.js'; +import {_arrayUnique, _filterBetween, _lookup} from '../helpers/helpers.collection.js'; + +/** + * @typedef { import('../core/core.adapters.js').TimeUnit } Unit + * @typedef {{common: boolean, size: number, steps?: number}} Interval + * @typedef { import('../core/core.adapters.js').DateAdapter } DateAdapter + */ + +/** + * @type {Object} + */ +const INTERVALS = { + millisecond: {common: true, size: 1, steps: 1000}, + second: {common: true, size: 1000, steps: 60}, + minute: {common: true, size: 60000, steps: 60}, + hour: {common: true, size: 3600000, steps: 24}, + day: {common: true, size: 86400000, steps: 30}, + week: {common: false, size: 604800000, steps: 4}, + month: {common: true, size: 2.628e9, steps: 12}, + quarter: {common: false, size: 7.884e9, steps: 4}, + year: {common: true, size: 3.154e10} +}; + +/** + * @type {Unit[]} + */ +const UNITS = /** @type Unit[] */ /* #__PURE__ */ (Object.keys(INTERVALS)); + +/** + * @param {number} a + * @param {number} b + */ +function sorter(a, b) { + return a - b; +} + +/** + * @param {TimeScale} scale + * @param {*} input + * @return {number} + */ +function parse(scale, input) { + if (isNullOrUndef(input)) { + return null; + } + + const adapter = scale._adapter; + const {parser, round, isoWeekday} = scale._parseOpts; + let value = input; + + if (typeof parser === 'function') { + value = parser(value); + } + + // Only parse if it's not a timestamp already + if (!isFinite(value)) { + value = typeof parser === 'string' + ? adapter.parse(value, parser) + : adapter.parse(value); + } + + if (value === null) { + return null; + } + + if (round) { + value = round === 'week' && (isNumber(isoWeekday) || isoWeekday === true) + ? adapter.startOf(value, 'isoWeek', isoWeekday) + : adapter.startOf(value, round); + } + + return +value; +} + +/** + * Figures out what unit results in an appropriate number of auto-generated ticks + * @param {Unit} minUnit + * @param {number} min + * @param {number} max + * @param {number} capacity + * @return {object} + */ +function determineUnitForAutoTicks(minUnit, min, max, capacity) { + const ilen = UNITS.length; + + for (let i = UNITS.indexOf(minUnit); i < ilen - 1; ++i) { + const interval = INTERVALS[UNITS[i]]; + const factor = interval.steps ? interval.steps : Number.MAX_SAFE_INTEGER; + + if (interval.common && Math.ceil((max - min) / (factor * interval.size)) <= capacity) { + return UNITS[i]; + } + } + + return UNITS[ilen - 1]; +} + +/** + * Figures out what unit to format a set of ticks with + * @param {TimeScale} scale + * @param {number} numTicks + * @param {Unit} minUnit + * @param {number} min + * @param {number} max + * @return {Unit} + */ +function determineUnitForFormatting(scale, numTicks, minUnit, min, max) { + for (let i = UNITS.length - 1; i >= UNITS.indexOf(minUnit); i--) { + const unit = UNITS[i]; + if (INTERVALS[unit].common && scale._adapter.diff(max, min, unit) >= numTicks - 1) { + return unit; + } + } + + return UNITS[minUnit ? UNITS.indexOf(minUnit) : 0]; +} + +/** + * @param {Unit} unit + * @return {object} + */ +function determineMajorUnit(unit) { + for (let i = UNITS.indexOf(unit) + 1, ilen = UNITS.length; i < ilen; ++i) { + if (INTERVALS[UNITS[i]].common) { + return UNITS[i]; + } + } +} + +/** + * @param {object} ticks + * @param {number} time + * @param {number[]} [timestamps] - if defined, snap to these timestamps + */ +function addTick(ticks, time, timestamps) { + if (!timestamps) { + ticks[time] = true; + } else if (timestamps.length) { + const {lo, hi} = _lookup(timestamps, time); + const timestamp = timestamps[lo] >= time ? timestamps[lo] : timestamps[hi]; + ticks[timestamp] = true; + } +} + +/** + * @param {TimeScale} scale + * @param {object[]} ticks + * @param {object} map + * @param {Unit} majorUnit + * @return {object[]} + */ +function setMajorTicks(scale, ticks, map, majorUnit) { + const adapter = scale._adapter; + const first = +adapter.startOf(ticks[0].value, majorUnit); + const last = ticks[ticks.length - 1].value; + let major, index; + + for (major = first; major <= last; major = +adapter.add(major, 1, majorUnit)) { + index = map[major]; + if (index >= 0) { + ticks[index].major = true; + } + } + return ticks; +} + +/** + * @param {TimeScale} scale + * @param {number[]} values + * @param {Unit|undefined} [majorUnit] + * @return {object[]} + */ +function ticksFromTimestamps(scale, values, majorUnit) { + const ticks = []; + /** @type {Object} */ + const map = {}; + const ilen = values.length; + let i, value; + + for (i = 0; i < ilen; ++i) { + value = values[i]; + map[value] = i; + + ticks.push({ + value, + major: false + }); + } + + // We set the major ticks separately from the above loop because calling startOf for every tick + // is expensive when there is a large number of ticks + return (ilen === 0 || !majorUnit) ? ticks : setMajorTicks(scale, ticks, map, majorUnit); +} + +export default class TimeScale extends Scale { + + static id = 'time'; + + /** + * @type {any} + */ + static defaults = { + /** + * Scale boundary strategy (bypassed by min/max time options) + * - `data`: make sure data are fully visible, ticks outside are removed + * - `ticks`: make sure ticks are fully visible, data outside are truncated + * @see https://github.com/chartjs/Chart.js/pull/4556 + * @since 2.7.0 + */ + bounds: 'data', + + adapters: {}, + time: { + parser: false, // false == a pattern string from or a custom callback that converts its argument to a timestamp + unit: false, // false == automatic or override with week, month, year, etc. + round: false, // none, or override with week, month, year, etc. + isoWeekday: false, // override week start day + minUnit: 'millisecond', + displayFormats: {} + }, + ticks: { + /** + * Ticks generation input values: + * - 'auto': generates "optimal" ticks based on scale size and time options. + * - 'data': generates ticks from data (including labels from data {t|x|y} objects). + * - 'labels': generates ticks from user given `data.labels` values ONLY. + * @see https://github.com/chartjs/Chart.js/pull/4507 + * @since 2.7.0 + */ + source: 'auto', + + callback: false, + + major: { + enabled: false + } + } + }; + + /** + * @param {object} props + */ + constructor(props) { + super(props); + + /** @type {{data: number[], labels: number[], all: number[]}} */ + this._cache = { + data: [], + labels: [], + all: [] + }; + + /** @type {Unit} */ + this._unit = 'day'; + /** @type {Unit=} */ + this._majorUnit = undefined; + this._offsets = {}; + this._normalized = false; + this._parseOpts = undefined; + } + + init(scaleOpts, opts = {}) { + const time = scaleOpts.time || (scaleOpts.time = {}); + /** @type {DateAdapter} */ + const adapter = this._adapter = new adapters._date(scaleOpts.adapters.date); + + adapter.init(opts); + + // Backward compatibility: before introducing adapter, `displayFormats` was + // supposed to contain *all* unit/string pairs but this can't be resolved + // when loading the scale (adapters are loaded afterward), so let's populate + // missing formats on update + mergeIf(time.displayFormats, adapter.formats()); + + this._parseOpts = { + parser: time.parser, + round: time.round, + isoWeekday: time.isoWeekday + }; + + super.init(scaleOpts); + + this._normalized = opts.normalized; + } + + /** + * @param {*} raw + * @param {number?} [index] + * @return {number} + */ + parse(raw, index) { // eslint-disable-line no-unused-vars + if (raw === undefined) { + return null; + } + return parse(this, raw); + } + + beforeLayout() { + super.beforeLayout(); + this._cache = { + data: [], + labels: [], + all: [] + }; + } + + determineDataLimits() { + const options = this.options; + const adapter = this._adapter; + const unit = options.time.unit || 'day'; + // eslint-disable-next-line prefer-const + let {min, max, minDefined, maxDefined} = this.getUserBounds(); + + /** + * @param {object} bounds + */ + function _applyBounds(bounds) { + if (!minDefined && !isNaN(bounds.min)) { + min = Math.min(min, bounds.min); + } + if (!maxDefined && !isNaN(bounds.max)) { + max = Math.max(max, bounds.max); + } + } + + // If we have user provided `min` and `max` labels / data bounds can be ignored + if (!minDefined || !maxDefined) { + // Labels are always considered, when user did not force bounds + _applyBounds(this._getLabelBounds()); + + // If `bounds` is `'ticks'` and `ticks.source` is `'labels'`, + // data bounds are ignored (and don't need to be determined) + if (options.bounds !== 'ticks' || options.ticks.source !== 'labels') { + _applyBounds(this.getMinMax(false)); + } + } + + min = isFinite(min) && !isNaN(min) ? min : +adapter.startOf(Date.now(), unit); + max = isFinite(max) && !isNaN(max) ? max : +adapter.endOf(Date.now(), unit) + 1; + + // Make sure that max is strictly higher than min (required by the timeseries lookup table) + this.min = Math.min(min, max - 1); + this.max = Math.max(min + 1, max); + } + + /** + * @private + */ + _getLabelBounds() { + const arr = this.getLabelTimestamps(); + let min = Number.POSITIVE_INFINITY; + let max = Number.NEGATIVE_INFINITY; + + if (arr.length) { + min = arr[0]; + max = arr[arr.length - 1]; + } + return {min, max}; + } + + /** + * @return {object[]} + */ + buildTicks() { + const options = this.options; + const timeOpts = options.time; + const tickOpts = options.ticks; + const timestamps = tickOpts.source === 'labels' ? this.getLabelTimestamps() : this._generate(); + + if (options.bounds === 'ticks' && timestamps.length) { + this.min = this._userMin || timestamps[0]; + this.max = this._userMax || timestamps[timestamps.length - 1]; + } + + const min = this.min; + const max = this.max; + + const ticks = _filterBetween(timestamps, min, max); + + // PRIVATE + // determineUnitForFormatting relies on the number of ticks so we don't use it when + // autoSkip is enabled because we don't yet know what the final number of ticks will be + this._unit = timeOpts.unit || (tickOpts.autoSkip + ? determineUnitForAutoTicks(timeOpts.minUnit, this.min, this.max, this._getLabelCapacity(min)) + : determineUnitForFormatting(this, ticks.length, timeOpts.minUnit, this.min, this.max)); + this._majorUnit = !tickOpts.major.enabled || this._unit === 'year' ? undefined + : determineMajorUnit(this._unit); + this.initOffsets(timestamps); + + if (options.reverse) { + ticks.reverse(); + } + + return ticksFromTimestamps(this, ticks, this._majorUnit); + } + + afterAutoSkip() { + // Offsets for bar charts need to be handled with the auto skipped + // ticks. Once ticks have been skipped, we re-compute the offsets. + if (this.options.offsetAfterAutoskip) { + this.initOffsets(this.ticks.map(tick => +tick.value)); + } + } + + /** + * Returns the start and end offsets from edges in the form of {start, end} + * where each value is a relative width to the scale and ranges between 0 and 1. + * They add extra margins on the both sides by scaling down the original scale. + * Offsets are added when the `offset` option is true. + * @param {number[]} timestamps + * @protected + */ + initOffsets(timestamps = []) { + let start = 0; + let end = 0; + let first, last; + + if (this.options.offset && timestamps.length) { + first = this.getDecimalForValue(timestamps[0]); + if (timestamps.length === 1) { + start = 1 - first; + } else { + start = (this.getDecimalForValue(timestamps[1]) - first) / 2; + } + last = this.getDecimalForValue(timestamps[timestamps.length - 1]); + if (timestamps.length === 1) { + end = last; + } else { + end = (last - this.getDecimalForValue(timestamps[timestamps.length - 2])) / 2; + } + } + const limit = timestamps.length < 3 ? 0.5 : 0.25; + start = _limitValue(start, 0, limit); + end = _limitValue(end, 0, limit); + + this._offsets = {start, end, factor: 1 / (start + 1 + end)}; + } + + /** + * Generates a maximum of `capacity` timestamps between min and max, rounded to the + * `minor` unit using the given scale time `options`. + * Important: this method can return ticks outside the min and max range, it's the + * responsibility of the calling code to clamp values if needed. + * @protected + */ + _generate() { + const adapter = this._adapter; + const min = this.min; + const max = this.max; + const options = this.options; + const timeOpts = options.time; + // @ts-ignore + const minor = timeOpts.unit || determineUnitForAutoTicks(timeOpts.minUnit, min, max, this._getLabelCapacity(min)); + const stepSize = valueOrDefault(options.ticks.stepSize, 1); + const weekday = minor === 'week' ? timeOpts.isoWeekday : false; + const hasWeekday = isNumber(weekday) || weekday === true; + const ticks = {}; + let first = min; + let time, count; + + // For 'week' unit, handle the first day of week option + if (hasWeekday) { + first = +adapter.startOf(first, 'isoWeek', weekday); + } + + // Align first ticks on unit + first = +adapter.startOf(first, hasWeekday ? 'day' : minor); + + // Prevent browser from freezing in case user options request millions of milliseconds + if (adapter.diff(max, min, minor) > 100000 * stepSize) { + throw new Error(min + ' and ' + max + ' are too far apart with stepSize of ' + stepSize + ' ' + minor); + } + + const timestamps = options.ticks.source === 'data' && this.getDataTimestamps(); + for (time = first, count = 0; time < max; time = +adapter.add(time, stepSize, minor), count++) { + addTick(ticks, time, timestamps); + } + + if (time === max || options.bounds === 'ticks' || count === 1) { + addTick(ticks, time, timestamps); + } + + // @ts-ignore + return Object.keys(ticks).sort(sorter).map(x => +x); + } + + /** + * @param {number} value + * @return {string} + */ + getLabelForValue(value) { + const adapter = this._adapter; + const timeOpts = this.options.time; + + if (timeOpts.tooltipFormat) { + return adapter.format(value, timeOpts.tooltipFormat); + } + return adapter.format(value, timeOpts.displayFormats.datetime); + } + + /** + * @param {number} value + * @param {string|undefined} format + * @return {string} + */ + format(value, format) { + const options = this.options; + const formats = options.time.displayFormats; + const unit = this._unit; + const fmt = format || formats[unit]; + return this._adapter.format(value, fmt); + } + + /** + * Function to format an individual tick mark + * @param {number} time + * @param {number} index + * @param {object[]} ticks + * @param {string|undefined} [format] + * @return {string} + * @private + */ + _tickFormatFunction(time, index, ticks, format) { + const options = this.options; + const formatter = options.ticks.callback; + + if (formatter) { + return call(formatter, [time, index, ticks], this); + } + + const formats = options.time.displayFormats; + const unit = this._unit; + const majorUnit = this._majorUnit; + const minorFormat = unit && formats[unit]; + const majorFormat = majorUnit && formats[majorUnit]; + const tick = ticks[index]; + const major = majorUnit && majorFormat && tick && tick.major; + + return this._adapter.format(time, format || (major ? majorFormat : minorFormat)); + } + + /** + * @param {object[]} ticks + */ + generateTickLabels(ticks) { + let i, ilen, tick; + + for (i = 0, ilen = ticks.length; i < ilen; ++i) { + tick = ticks[i]; + tick.label = this._tickFormatFunction(tick.value, i, ticks); + } + } + + /** + * @param {number} value - Milliseconds since epoch (1 January 1970 00:00:00 UTC) + * @return {number} + */ + getDecimalForValue(value) { + return value === null ? NaN : (value - this.min) / (this.max - this.min); + } + + /** + * @param {number} value - Milliseconds since epoch (1 January 1970 00:00:00 UTC) + * @return {number} + */ + getPixelForValue(value) { + const offsets = this._offsets; + const pos = this.getDecimalForValue(value); + return this.getPixelForDecimal((offsets.start + pos) * offsets.factor); + } + + /** + * @param {number} pixel + * @return {number} + */ + getValueForPixel(pixel) { + const offsets = this._offsets; + const pos = this.getDecimalForPixel(pixel) / offsets.factor - offsets.end; + return this.min + pos * (this.max - this.min); + } + + /** + * @param {string} label + * @return {{w:number, h:number}} + * @private + */ + _getLabelSize(label) { + const ticksOpts = this.options.ticks; + const tickLabelWidth = this.ctx.measureText(label).width; + const angle = toRadians(this.isHorizontal() ? ticksOpts.maxRotation : ticksOpts.minRotation); + const cosRotation = Math.cos(angle); + const sinRotation = Math.sin(angle); + const tickFontSize = this._resolveTickFontOptions(0).size; + + return { + w: (tickLabelWidth * cosRotation) + (tickFontSize * sinRotation), + h: (tickLabelWidth * sinRotation) + (tickFontSize * cosRotation) + }; + } + + /** + * @param {number} exampleTime + * @return {number} + * @private + */ + _getLabelCapacity(exampleTime) { + const timeOpts = this.options.time; + const displayFormats = timeOpts.displayFormats; + + // pick the longest format (milliseconds) for guesstimation + const format = displayFormats[timeOpts.unit] || displayFormats.millisecond; + const exampleLabel = this._tickFormatFunction(exampleTime, 0, ticksFromTimestamps(this, [exampleTime], this._majorUnit), format); + const size = this._getLabelSize(exampleLabel); + // subtract 1 - if offset then there's one less label than tick + // if not offset then one half label padding is added to each end leaving room for one less label + const capacity = Math.floor(this.isHorizontal() ? this.width / size.w : this.height / size.h) - 1; + return capacity > 0 ? capacity : 1; + } + + /** + * @protected + */ + getDataTimestamps() { + let timestamps = this._cache.data || []; + let i, ilen; + + if (timestamps.length) { + return timestamps; + } + + const metas = this.getMatchingVisibleMetas(); + + if (this._normalized && metas.length) { + return (this._cache.data = metas[0].controller.getAllParsedValues(this)); + } + + for (i = 0, ilen = metas.length; i < ilen; ++i) { + timestamps = timestamps.concat(metas[i].controller.getAllParsedValues(this)); + } + + return (this._cache.data = this.normalize(timestamps)); + } + + /** + * @protected + */ + getLabelTimestamps() { + const timestamps = this._cache.labels || []; + let i, ilen; + + if (timestamps.length) { + return timestamps; + } + + const labels = this.getLabels(); + for (i = 0, ilen = labels.length; i < ilen; ++i) { + timestamps.push(parse(this, labels[i])); + } + + return (this._cache.labels = this._normalized ? timestamps : this.normalize(timestamps)); + } + + /** + * @param {number[]} values + * @protected + */ + normalize(values) { + // It seems to be somewhat faster to do sorting first + return _arrayUnique(values.sort(sorter)); + } +} diff --git a/src/scales/scale.timeseries.js b/src/scales/scale.timeseries.js new file mode 100644 index 00000000000..2be73493407 --- /dev/null +++ b/src/scales/scale.timeseries.js @@ -0,0 +1,177 @@ +import TimeScale from './scale.time.js'; +import {_lookupByKey} from '../helpers/helpers.collection.js'; + +/** + * Linearly interpolates the given source `val` using the table. If value is out of bounds, values + * at edges are used for the interpolation. + * @param {object} table + * @param {number} val + * @param {boolean} [reverse] lookup time based on position instead of vice versa + * @return {object} + */ +function interpolate(table, val, reverse) { + let lo = 0; + let hi = table.length - 1; + let prevSource, nextSource, prevTarget, nextTarget; + if (reverse) { + if (val >= table[lo].pos && val <= table[hi].pos) { + ({lo, hi} = _lookupByKey(table, 'pos', val)); + } + ({pos: prevSource, time: prevTarget} = table[lo]); + ({pos: nextSource, time: nextTarget} = table[hi]); + } else { + if (val >= table[lo].time && val <= table[hi].time) { + ({lo, hi} = _lookupByKey(table, 'time', val)); + } + ({time: prevSource, pos: prevTarget} = table[lo]); + ({time: nextSource, pos: nextTarget} = table[hi]); + } + + const span = nextSource - prevSource; + return span ? prevTarget + (nextTarget - prevTarget) * (val - prevSource) / span : prevTarget; +} + +class TimeSeriesScale extends TimeScale { + + static id = 'timeseries'; + + /** + * @type {any} + */ + static defaults = TimeScale.defaults; + + /** + * @param {object} props + */ + constructor(props) { + super(props); + + /** @type {object[]} */ + this._table = []; + /** @type {number} */ + this._minPos = undefined; + /** @type {number} */ + this._tableRange = undefined; + } + + /** + * @protected + */ + initOffsets() { + const timestamps = this._getTimestampsForTable(); + const table = this._table = this.buildLookupTable(timestamps); + this._minPos = interpolate(table, this.min); + this._tableRange = interpolate(table, this.max) - this._minPos; + super.initOffsets(timestamps); + } + + /** + * Returns an array of {time, pos} objects used to interpolate a specific `time` or position + * (`pos`) on the scale, by searching entries before and after the requested value. `pos` is + * a decimal between 0 and 1: 0 being the start of the scale (left or top) and 1 the other + * extremity (left + width or top + height). Note that it would be more optimized to directly + * store pre-computed pixels, but the scale dimensions are not guaranteed at the time we need + * to create the lookup table. The table ALWAYS contains at least two items: min and max. + * @param {number[]} timestamps + * @return {object[]} + * @protected + */ + buildLookupTable(timestamps) { + const {min, max} = this; + const items = []; + const table = []; + let i, ilen, prev, curr, next; + + for (i = 0, ilen = timestamps.length; i < ilen; ++i) { + curr = timestamps[i]; + if (curr >= min && curr <= max) { + items.push(curr); + } + } + + if (items.length < 2) { + // In case there is less that 2 timestamps between min and max, the scale is defined by min and max + return [ + {time: min, pos: 0}, + {time: max, pos: 1} + ]; + } + + for (i = 0, ilen = items.length; i < ilen; ++i) { + next = items[i + 1]; + prev = items[i - 1]; + curr = items[i]; + + // only add points that breaks the scale linearity + if (Math.round((next + prev) / 2) !== curr) { + table.push({time: curr, pos: i / (ilen - 1)}); + } + } + return table; + } + + /** + * Generates all timestamps defined in the data. + * Important: this method can return ticks outside the min and max range, it's the + * responsibility of the calling code to clamp values if needed. + * @protected + */ + _generate() { + const min = this.min; + const max = this.max; + let timestamps = super.getDataTimestamps(); + if (!timestamps.includes(min) || !timestamps.length) { + timestamps.splice(0, 0, min); + } + if (!timestamps.includes(max) || timestamps.length === 1) { + timestamps.push(max); + } + return timestamps.sort((a, b) => a - b); + } + + /** + * Returns all timestamps + * @return {number[]} + * @private + */ + _getTimestampsForTable() { + let timestamps = this._cache.all || []; + + if (timestamps.length) { + return timestamps; + } + + const data = this.getDataTimestamps(); + const label = this.getLabelTimestamps(); + if (data.length && label.length) { + // If combining labels and data (data might not contain all labels), + // we need to recheck uniqueness and sort + timestamps = this.normalize(data.concat(label)); + } else { + timestamps = data.length ? data : label; + } + timestamps = this._cache.all = timestamps; + + return timestamps; + } + + /** + * @param {number} value - Milliseconds since epoch (1 January 1970 00:00:00 UTC) + * @return {number} + */ + getDecimalForValue(value) { + return (interpolate(this._table, value) - this._minPos) / this._tableRange; + } + + /** + * @param {number} pixel + * @return {number} + */ + getValueForPixel(pixel) { + const offsets = this._offsets; + const decimal = this.getDecimalForPixel(pixel) / offsets.factor - offsets.end; + return interpolate(this._table, decimal * this._tableRange + this._minPos, true); + } +} + +export default TimeSeriesScale; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 00000000000..ff16b8be546 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,54 @@ +/** + * Temporary entry point of the types at the time of the transition. + * After transition done need to remove it in favor of index.ts + */ + +export * from './index.js'; +/** + * Explicitly re-exporting to resolve the ambiguity. + */ +export { + BarController, + BubbleController, + DoughnutController, + LineController, + PieController, + PolarAreaController, + RadarController, + ScatterController, + Animation, + Animations, + Chart, + DatasetController, + Interaction, + Scale, + Ticks, + defaults, + layouts, + registry, + ArcElement, + BarElement, + LineElement, + PointElement, + BasePlatform, + BasicPlatform, + DomPlatform, + Decimation, + Filler, + Legend, + SubTitle, + Title, + Tooltip, + CategoryScale, + LinearScale, + LogarithmicScale, + RadialLinearScale, + TimeScale, + TimeSeriesScale, + PluginOptionsByType, + ElementOptionsByType, + ChartDatasetProperties, + UpdateModeEnum, + registerables +} from './types/index.js'; +export * from './types/index.js'; diff --git a/src/types/animation.d.ts b/src/types/animation.d.ts new file mode 100644 index 00000000000..41895125e99 --- /dev/null +++ b/src/types/animation.d.ts @@ -0,0 +1,34 @@ +import {Chart} from './index.js'; +import {AnyObject} from './basic.js'; + +export declare class Animation { + constructor(cfg: AnyObject, target: AnyObject, prop: string, to?: unknown); + active(): boolean; + update(cfg: AnyObject, to: unknown, date: number): void; + cancel(): void; + tick(date: number): void; + readonly _to: unknown; +} + +export interface AnimationEvent { + chart: Chart; + numSteps: number; + initial: boolean; + currentStep: number; +} + +export declare class Animator { + listen(chart: Chart, event: 'complete' | 'progress', cb: (event: AnimationEvent) => void): void; + add(chart: Chart, items: readonly Animation[]): void; + has(chart: Chart): boolean; + start(chart: Chart): void; + running(chart: Chart): boolean; + stop(chart: Chart): void; + remove(chart: Chart): boolean; +} + +export declare class Animations { + constructor(chart: Chart, animations: AnyObject); + configure(animations: AnyObject): void; + update(target: AnyObject, values: AnyObject): undefined | boolean; +} diff --git a/src/types/basic.d.ts b/src/types/basic.d.ts new file mode 100644 index 00000000000..2f48ee23dcb --- /dev/null +++ b/src/types/basic.d.ts @@ -0,0 +1,3 @@ + +export type AnyObject = Record; +export type EmptyObject = Record; diff --git a/src/types/color.d.ts b/src/types/color.d.ts new file mode 100644 index 00000000000..4a68f98bbd6 --- /dev/null +++ b/src/types/color.d.ts @@ -0,0 +1 @@ +export type Color = string | CanvasGradient | CanvasPattern; diff --git a/src/types/geometric.d.ts b/src/types/geometric.d.ts new file mode 100644 index 00000000000..e48ce1c23d3 --- /dev/null +++ b/src/types/geometric.d.ts @@ -0,0 +1,52 @@ +export interface ChartArea { + top: number; + left: number; + right: number; + bottom: number; + width: number; + height: number; +} + +export interface Point { + x: number | null; + y: number | null; +} + +export type TRBL = { + top: number; + right: number; + bottom: number; + left: number; +} + +export type TRBLCorners = { + topLeft: number; + topRight: number; + bottomLeft: number; + bottomRight: number; +}; + +export type CornerRadius = number | Partial; + +export type RoundedRect = { + x: number; + y: number; + w: number; + h: number; + radius?: CornerRadius +} + +export type Padding = Partial | number | Point; + +export interface SplinePoint { + x: number; + y: number; + skip?: boolean; + + // Both Bezier and monotone interpolations have these fields + // but they are added in different spots + cp1x?: number; + cp1y?: number; + cp2x?: number; + cp2y?: number; +} diff --git a/src/types/index.d.ts b/src/types/index.d.ts new file mode 100644 index 00000000000..911b4cb2bc8 --- /dev/null +++ b/src/types/index.d.ts @@ -0,0 +1,3889 @@ +/* eslint-disable @typescript-eslint/ban-types */ +import {DeepPartial, DistributiveArray, UnionToIntersection} from './utils.js'; + +import {TimeUnit} from '../core/core.adapters.js'; +import PointElement from '../elements/element.point.js'; +import {EasingFunction} from '../helpers/helpers.easing.js'; +import {AnimationEvent} from './animation.js'; +import {AnyObject, EmptyObject} from './basic.js'; +import {Color} from './color.js'; +import Element from '../core/core.element.js'; +import {ChartArea, Padding, Point} from './geometric.js'; +import {LayoutItem, LayoutPosition} from './layout.js'; +import {ColorsPluginOptions} from '../plugins/plugin.colors.js'; + +export {EasingFunction} from '../helpers/helpers.easing.js'; +export {default as ArcElement, ArcProps} from '../elements/element.arc.js'; +export {default as PointElement, PointProps} from '../elements/element.point.js'; +export {Animation, Animations, Animator, AnimationEvent} from './animation.js'; +export {Color} from './color.js'; +export {ChartArea, Point, TRBL} from './geometric.js'; +export {LayoutItem, LayoutPosition} from './layout.js'; + +export interface ScriptableContext { + active: boolean; + chart: Chart; + dataIndex: number; + dataset: UnionToIntersection>; + datasetIndex: number; + type: string; + mode: string; + parsed: UnionToIntersection>; + raw: unknown; +} + +export interface ScriptableLineSegmentContext { + type: 'segment', + p0: PointElement, + p1: PointElement, + p0DataIndex: number, + p1DataIndex: number, + datasetIndex: number +} + +export type Scriptable = T | ((ctx: TContext, options: AnyObject) => T | undefined); +export type ScriptableOptions = { [P in keyof T]: Scriptable }; +export type ScriptableAndScriptableOptions = Scriptable | ScriptableOptions; +export type ScriptableAndArray = readonly T[] | Scriptable; +export type ScriptableAndArrayOptions = { [P in keyof T]: ScriptableAndArray }; + +export interface ParsingOptions { + /** + * How to parse the dataset. The parsing can be disabled by specifying parsing: false at chart options or dataset. If parsing is disabled, data must be sorted and in the formats the associated chart type and scales use internally. + */ + parsing: + { + [key: string]: string; + } + | false; + + /** + * Chart.js is fastest if you provide data with indices that are unique, sorted, and consistent across datasets and provide the normalized: true option to let Chart.js know that you have done so. + */ + normalized: boolean; +} + +export interface ControllerDatasetOptions extends ParsingOptions { + /** + * The base axis of the chart. 'x' for vertical charts and 'y' for horizontal charts. + * @default 'x' + */ + indexAxis: 'x' | 'y'; + /** + * How to clip relative to chartArea. Positive value allows overflow, negative value clips that many pixels inside chartArea. 0 = clip at chartArea. Clipping can also be configured per side: `clip: {left: 5, top: false, right: -2, bottom: 0}` + */ + clip: number | ChartArea | false; + /** + * The label for the dataset which appears in the legend and tooltips. + */ + label: string; + /** + * The drawing order of dataset. Also affects order for stacking, tooltip and legend. + */ + order: number; + + /** + * The ID of the group to which this dataset belongs to (when stacked, each group will be a separate stack). + */ + stack: string; + /** + * Configures the visibility state of the dataset. Set it to true, to hide the dataset from the chart. + * @default false + */ + hidden: boolean; +} + +export interface BarControllerDatasetOptions + extends ControllerDatasetOptions, + ScriptableAndArrayOptions>, + ScriptableAndArrayOptions>, + AnimationOptions<'bar'> { + /** + * The ID of the x axis to plot this dataset on. + */ + xAxisID: string; + /** + * The ID of the y axis to plot this dataset on. + */ + yAxisID: string; + + /** + * Percent (0-1) of the available width each bar should be within the category width. 1.0 will take the whole category width and put the bars right next to each other. + * @default 0.9 + */ + barPercentage: number; + /** + * Percent (0-1) of the available width each category should be within the sample width. + * @default 0.8 + */ + categoryPercentage: number; + + /** + * Manually set width of each bar in pixels. If set to 'flex', it computes "optimal" sample widths that globally arrange bars side by side. If not set (default), bars are equally sized based on the smallest interval. + */ + barThickness: number | 'flex'; + + /** + * Set this to ensure that bars are not sized thicker than this. + */ + maxBarThickness: number; + + /** + * Set this to ensure that bars have a minimum length in pixels. + */ + minBarLength: number; + + /** + * Point style for the legend + * @default 'circle; + */ + pointStyle: PointStyle; + + /** + * Should the bars be grouped on index axis + * @default true + */ + grouped: boolean; +} + +export interface BarControllerChartOptions { + /** + * Should null or undefined values be omitted from drawing + */ + skipNull?: boolean; +} + +export type BarController = DatasetController +export declare const BarController: ChartComponent & { + prototype: BarController; + new (chart: Chart, datasetIndex: number): BarController; +}; + +export interface BubbleControllerDatasetOptions + extends ControllerDatasetOptions, + ScriptableAndArrayOptions>, + ScriptableAndArrayOptions> { + /** + * The ID of the x axis to plot this dataset on. + */ + xAxisID: string; + /** + * The ID of the y axis to plot this dataset on. + */ + yAxisID: string; +} + +export interface BubbleDataPoint extends Point { + /** + * Bubble radius in pixels (not scaled). + */ + r?: number; +} + +export type BubbleController = DatasetController +export declare const BubbleController: ChartComponent & { + prototype: BubbleController; + new (chart: Chart, datasetIndex: number): BubbleController; +}; + +export interface LineControllerDatasetOptions + extends ControllerDatasetOptions, + ScriptableAndArrayOptions>, + ScriptableAndArrayOptions>, + ScriptableOptions, ScriptableContext<'line'>>, + ScriptableAndArrayOptions>, + ScriptableOptions, ScriptableContext<'line'>>, + ScriptableAndArrayOptions>, + AnimationOptions<'line'> { + /** + * The ID of the x axis to plot this dataset on. + */ + xAxisID: string; + /** + * The ID of the y axis to plot this dataset on. + */ + yAxisID: string; + + /** + * If true, lines will be drawn between points with no or null data. If false, points with NaN data will create a break in the line. Can also be a number specifying the maximum gap length to span. The unit of the value depends on the scale used. + * @default false + */ + spanGaps: boolean | number; + + showLine: boolean; +} + +export interface LineControllerChartOptions { + /** + * If true, lines will be drawn between points with no or null data. If false, points with NaN data will create a break in the line. Can also be a number specifying the maximum gap length to span. The unit of the value depends on the scale used. + * @default false + */ + spanGaps: boolean | number; + /** + * If false, the lines between points are not drawn. + * @default true + */ + showLine: boolean; +} + +export type LineController = DatasetController +export declare const LineController: ChartComponent & { + prototype: LineController; + new (chart: Chart, datasetIndex: number): LineController; +}; + +export type ScatterControllerDatasetOptions = LineControllerDatasetOptions; + +export type ScatterDataPoint = Point + +export type ScatterControllerChartOptions = LineControllerChartOptions; + +export type ScatterController = LineController +export declare const ScatterController: ChartComponent & { + prototype: ScatterController; + new (chart: Chart, datasetIndex: number): ScatterController; +}; + +export interface DoughnutControllerDatasetOptions + extends ControllerDatasetOptions, + ScriptableAndArrayOptions>, + ScriptableAndArrayOptions>, + AnimationOptions<'doughnut'> { + + /** + * Sweep to allow arcs to cover. + * @default 360 + */ + circumference: number; + + /** + * Arc offset (in pixels). + */ + offset: number | number[]; + + /** + * Starting angle to draw this dataset from. + * @default 0 + */ + rotation: number; + + /** + * The relative thickness of the dataset. Providing a value for weight will cause the pie or doughnut dataset to be drawn with a thickness relative to the sum of all the dataset weight values. + * @default 1 + */ + weight: number; + + /** + * Similar to the `offset` option, but applies to all arcs. This can be used to to add spaces + * between arcs + * @default 0 + */ + spacing: number; +} + +export interface DoughnutAnimationOptions extends AnimationSpec<'doughnut'> { + /** + * If true, the chart will animate in with a rotation animation. This property is in the options.animation object. + * @default true + */ + animateRotate: boolean; + + /** + * If true, will animate scaling the chart from the center outwards. + * @default false + */ + animateScale: boolean; +} + +export interface DoughnutControllerChartOptions { + /** + * Sweep to allow arcs to cover. + * @default 360 + */ + circumference: number; + + /** + * The portion of the chart that is cut out of the middle. ('50%' - for doughnut, 0 - for pie) + * String ending with '%' means percentage, number means pixels. + * @default 50 + */ + cutout: Scriptable>; + + /** + * Arc offset (in pixels). + */ + offset: number | number[]; + + /** + * The outer radius of the chart. String ending with '%' means percentage of maximum radius, number means pixels. + * @default '100%' + */ + radius: Scriptable>; + + /** + * Starting angle to draw arcs from. + * @default 0 + */ + rotation: number; + + /** + * Spacing between the arcs + * @default 0 + */ + spacing: number; + + animation: false | DoughnutAnimationOptions; +} + +export type DoughnutDataPoint = number; + +export interface DoughnutController extends DatasetController { + readonly innerRadius: number; + readonly outerRadius: number; + readonly offsetX: number; + readonly offsetY: number; + + calculateTotal(): number; + calculateCircumference(value: number): number; +} + +export declare const DoughnutController: ChartComponent & { + prototype: DoughnutController; + new (chart: Chart, datasetIndex: number): DoughnutController; +}; + +export interface DoughnutMetaExtensions { + total: number; +} + +export type PieControllerDatasetOptions = DoughnutControllerDatasetOptions; +export type PieControllerChartOptions = DoughnutControllerChartOptions; +export type PieAnimationOptions = DoughnutAnimationOptions; + +export type PieDataPoint = DoughnutDataPoint; +export type PieMetaExtensions = DoughnutMetaExtensions; + +export type PieController = DoughnutController +export declare const PieController: ChartComponent & { + prototype: PieController; + new (chart: Chart, datasetIndex: number): PieController; +}; + +export interface PolarAreaControllerDatasetOptions extends DoughnutControllerDatasetOptions { + /** + * Arc angle to cover. - for polar only + * @default circumference / (arc count) + */ + angle: number; +} + +export type PolarAreaAnimationOptions = DoughnutAnimationOptions; + +export interface PolarAreaControllerChartOptions { + /** + * Starting angle to draw arcs for the first item in a dataset. In degrees, 0 is at top. + * @default 0 + */ + startAngle: number; + + animation: false | PolarAreaAnimationOptions; +} + +export interface PolarAreaController extends DoughnutController { + countVisibleElements(): number; +} +export declare const PolarAreaController: ChartComponent & { + prototype: PolarAreaController; + new (chart: Chart, datasetIndex: number): PolarAreaController; +}; + +export interface RadarControllerDatasetOptions + extends ControllerDatasetOptions, + ScriptableAndArrayOptions>, + ScriptableAndArrayOptions>, + AnimationOptions<'radar'> { + /** + * The ID of the x axis to plot this dataset on. + */ + xAxisID: string; + /** + * The ID of the y axis to plot this dataset on. + */ + yAxisID: string; + + /** + * If true, lines will be drawn between points with no or null data. If false, points with NaN data will create a break in the line. Can also be a number specifying the maximum gap length to span. The unit of the value depends on the scale used. + */ + spanGaps: boolean | number; + + /** + * If false, the line is not drawn for this dataset. + */ + showLine: boolean; +} + +export type RadarControllerChartOptions = LineControllerChartOptions; + +export type RadarController = DatasetController +export declare const RadarController: ChartComponent & { + prototype: RadarController; + new (chart: Chart, datasetIndex: number): RadarController; +}; + +interface ChartMetaClip { + left: number | boolean; + top: number | boolean; + right: number | boolean; + bottom: number | boolean; + disabled: boolean; +} + +interface ChartMetaCommon { + type: string; + controller: DatasetController; + order: number; + + label: string; + index: number; + visible: boolean; + + stack: number; + + indexAxis: 'x' | 'y'; + + data: TElement[]; + dataset?: TDatasetElement; + + hidden: boolean; + + xAxisID?: string; + yAxisID?: string; + rAxisID?: string; + iAxisID: string; + vAxisID: string; + + xScale?: Scale; + yScale?: Scale; + rScale?: Scale; + iScale?: Scale; + vScale?: Scale; + + _sorted: boolean; + _stacked: boolean | 'single'; + _parsed: unknown[]; + _clip: ChartMetaClip; +} + +export type ChartMeta< + TType extends ChartType = ChartType, + TElement extends Element = Element, + TDatasetElement extends Element = Element, +> = DeepPartial< +{ [key in ChartType]: ChartTypeRegistry[key]['metaExtensions'] }[TType] +> & ChartMetaCommon; + +export interface ActiveDataPoint { + datasetIndex: number; + index: number; +} + +export interface ActiveElement extends ActiveDataPoint { + element: Element; +} + +export declare class Chart< + TType extends ChartType = ChartType, + TData = DefaultDataPoint, + TLabel = unknown +> { + readonly platform: BasePlatform; + readonly id: string; + readonly canvas: HTMLCanvasElement; + readonly ctx: CanvasRenderingContext2D; + readonly config: ChartConfiguration | ChartConfigurationCustomTypesPerDataset; + readonly width: number; + readonly height: number; + readonly aspectRatio: number; + readonly boxes: LayoutItem[]; + readonly currentDevicePixelRatio: number; + readonly chartArea: ChartArea; + readonly scales: { [key: string]: Scale }; + readonly attached: boolean; + + readonly legend?: LegendElement; // Only available if legend plugin is registered and enabled + readonly tooltip?: TooltipModel; // Only available if tooltip plugin is registered and enabled + + data: ChartData; + options: ChartOptions; + + constructor(item: ChartItem, config: ChartConfiguration | ChartConfigurationCustomTypesPerDataset); + + clear(): this; + stop(): this; + + resize(width?: number, height?: number): void; + ensureScalesHaveIDs(): void; + buildOrUpdateScales(): void; + buildOrUpdateControllers(): void; + reset(): void; + update(mode?: UpdateMode | ((ctx: { datasetIndex: number }) => UpdateMode)): void; + render(): void; + draw(): void; + + isPointInArea(point: Point): boolean; + getElementsAtEventForMode(e: Event, mode: string, options: InteractionOptions, useFinalPosition: boolean): InteractionItem[]; + + getSortedVisibleDatasetMetas(): ChartMeta[]; + getDatasetMeta(datasetIndex: number): ChartMeta; + getVisibleDatasetCount(): number; + isDatasetVisible(datasetIndex: number): boolean; + setDatasetVisibility(datasetIndex: number, visible: boolean): void; + toggleDataVisibility(index: number): void; + getDataVisibility(index: number): boolean; + hide(datasetIndex: number, dataIndex?: number): void; + show(datasetIndex: number, dataIndex?: number): void; + + getActiveElements(): ActiveElement[]; + setActiveElements(active: ActiveDataPoint[]): void; + + destroy(): void; + toBase64Image(type?: string, quality?: unknown): string; + bindEvents(): void; + unbindEvents(): void; + updateHoverStyle(items: InteractionItem[], mode: 'dataset', enabled: boolean): void; + + notifyPlugins(hook: string, args?: AnyObject): boolean | void; + + isPluginEnabled(pluginId: string): boolean; + + getContext(): { chart: Chart, type: string }; + + static readonly defaults: Defaults; + static readonly overrides: Overrides; + static readonly version: string; + static readonly instances: { [key: string]: Chart }; + static readonly registry: Registry; + static getChart(key: string | CanvasRenderingContext2D | HTMLCanvasElement): Chart | undefined; + static register(...items: ChartComponentLike[]): void; + static unregister(...items: ChartComponentLike[]): void; +} + +export declare const registerables: readonly ChartComponentLike[]; + +export declare type ChartItem = + | string + | CanvasRenderingContext2D + | HTMLCanvasElement + | { canvas: HTMLCanvasElement } + | ArrayLike; + +export declare enum UpdateModeEnum { + resize = 'resize', + reset = 'reset', + none = 'none', + hide = 'hide', + show = 'show', + default = 'default', + active = 'active' +} + +export type UpdateMode = keyof typeof UpdateModeEnum; + +export declare class DatasetController< + TType extends ChartType = ChartType, + TElement extends Element = Element, + TDatasetElement extends Element = Element, + TParsedData = ParsedDataType, +> { + constructor(chart: Chart, datasetIndex: number); + + readonly chart: Chart; + readonly index: number; + readonly _cachedMeta: ChartMeta; + enableOptionSharing: boolean; + // If true, the controller supports the decimation + // plugin. Defaults to `false` for all controllers + // except the LineController + supportsDecimation: boolean; + + linkScales(): void; + getAllParsedValues(scale: Scale): number[]; + protected getLabelAndValue(index: number): { label: string; value: string }; + updateElements(elements: TElement[], start: number, count: number, mode: UpdateMode): void; + update(mode: UpdateMode): void; + updateIndex(datasetIndex: number): void; + protected getMaxOverflow(): boolean | number; + draw(): void; + reset(): void; + getDataset(): ChartDataset; + getMeta(): ChartMeta; + getScaleForId(scaleID: string): Scale | undefined; + configure(): void; + initialize(): void; + addElements(): void; + buildOrUpdateElements(resetNewElements?: boolean): void; + + getStyle(index: number, active: boolean): AnyObject; + protected resolveDatasetElementOptions(mode: UpdateMode): AnyObject; + protected resolveDataElementOptions(index: number, mode: UpdateMode): AnyObject; + /** + * Utility for checking if the options are shared and should be animated separately. + * @protected + */ + protected getSharedOptions(options: AnyObject): undefined | AnyObject; + /** + * Utility for determining if `options` should be included in the updated properties + * @protected + */ + protected includeOptions(mode: UpdateMode, sharedOptions: AnyObject): boolean; + /** + * Utility for updating an element with new properties, using animations when appropriate. + * @protected + */ + + protected updateElement(element: TElement | TDatasetElement, index: number | undefined, properties: AnyObject, mode: UpdateMode): void; + /** + * Utility to animate the shared options, that are potentially affecting multiple elements. + * @protected + */ + + protected updateSharedOptions(sharedOptions: AnyObject, mode: UpdateMode, newOptions: AnyObject): void; + removeHoverStyle(element: TElement, datasetIndex: number, index: number): void; + setHoverStyle(element: TElement, datasetIndex: number, index: number): void; + + parse(start: number, count: number): void; + protected parsePrimitiveData(meta: ChartMeta, data: AnyObject[], start: number, count: number): AnyObject[]; + protected parseArrayData(meta: ChartMeta, data: AnyObject[], start: number, count: number): AnyObject[]; + protected parseObjectData(meta: ChartMeta, data: AnyObject[], start: number, count: number): AnyObject[]; + protected getParsed(index: number): TParsedData; + protected applyStack(scale: Scale, parsed: unknown[]): number; + protected updateRangeFromParsed( + range: { min: number; max: number }, + scale: Scale, + parsed: unknown[], + stack: boolean | string + ): void; + protected getMinMax(scale: Scale, canStack?: boolean): { min: number; max: number }; +} + +export interface DatasetControllerChartComponent extends ChartComponent { + defaults: { + datasetElementType?: string | null | false; + dataElementType?: string | null | false; + }; +} + +export interface Defaults extends CoreChartOptions, ElementChartOptions, PluginChartOptions { + + scale: ScaleOptionsByType; + scales: { + [key in ScaleType]: ScaleOptionsByType; + }; + + set(values: AnyObject): AnyObject; + set(scope: string, values: AnyObject): AnyObject; + get(scope: string): AnyObject; + + describe(scope: string, values: AnyObject): AnyObject; + override(scope: string, values: AnyObject): AnyObject; + + /** + * Routes the named defaults to fallback to another scope/name. + * This routing is useful when those target values, like defaults.color, are changed runtime. + * If the values would be copied, the runtime change would not take effect. By routing, the + * fallback is evaluated at each access, so its always up to date. + * + * Example: + * + * defaults.route('elements.arc', 'backgroundColor', '', 'color') + * - reads the backgroundColor from defaults.color when undefined locally + * + * @param scope Scope this route applies to. + * @param name Property name that should be routed to different namespace when not defined here. + * @param targetScope The namespace where those properties should be routed to. + * Empty string ('') is the root of defaults. + * @param targetName The target name in the target scope the property should be routed to. + */ + route(scope: string, name: string, targetScope: string, targetName: string): void; +} + +export type Overrides = { + [key in ChartType]: + CoreChartOptions & + ElementChartOptions & + PluginChartOptions & + DatasetChartOptions & + ScaleChartOptions & + ChartTypeRegistry[key]['chartOptions']; +} + +export declare const defaults: Defaults; +export interface InteractionOptions { + axis?: string; + intersect?: boolean; + includeInvisible?: boolean; +} + +export interface InteractionItem { + element: Element; + datasetIndex: number; + index: number; +} + +export type InteractionModeFunction = ( + chart: Chart, + e: ChartEvent, + options: InteractionOptions, + useFinalPosition?: boolean +) => InteractionItem[]; + +export interface InteractionModeMap { + /** + * Returns items at the same index. If the options.intersect parameter is true, we only return items if we intersect something + * If the options.intersect mode is false, we find the nearest item and return the items at the same index as that item + */ + index: InteractionModeFunction; + + /** + * Returns items in the same dataset. If the options.intersect parameter is true, we only return items if we intersect something + * If the options.intersect is false, we find the nearest item and return the items in that dataset + */ + dataset: InteractionModeFunction; + /** + * Point mode returns all elements that hit test based on the event position + * of the event + */ + point: InteractionModeFunction; + /** + * nearest mode returns the element closest to the point + */ + nearest: InteractionModeFunction; + /** + * x mode returns the elements that hit-test at the current x coordinate + */ + x: InteractionModeFunction; + /** + * y mode returns the elements that hit-test at the current y coordinate + */ + y: InteractionModeFunction; +} + +export type InteractionMode = keyof InteractionModeMap; + +export declare const Interaction: { + modes: InteractionModeMap; + + /** + * Helper function to select candidate elements for interaction + */ + evaluateInteractionItems( + chart: Chart, + axis: InteractionAxis, + position: Point, + handler: (element: Element & VisualElement, datasetIndex: number, index: number) => void, + intersect?: boolean + ): InteractionItem[]; +}; + +export declare const layouts: { + /** + * Register a box to a chart. + * A box is simply a reference to an object that requires layout. eg. Scales, Legend, Title. + * @param {Chart} chart - the chart to use + * @param {LayoutItem} item - the item to add to be laid out + */ + addBox(chart: Chart, item: LayoutItem): void; + + /** + * Remove a layoutItem from a chart + * @param {Chart} chart - the chart to remove the box from + * @param {LayoutItem} layoutItem - the item to remove from the layout + */ + removeBox(chart: Chart, layoutItem: LayoutItem): void; + + /** + * Sets (or updates) options on the given `item`. + * @param {Chart} chart - the chart in which the item lives (or will be added to) + * @param {LayoutItem} item - the item to configure with the given options + * @param options - the new item options. + */ + configure( + chart: Chart, + item: LayoutItem, + options: { fullSize?: number; position?: LayoutPosition; weight?: number } + ): void; + + /** + * Fits boxes of the given chart into the given size by having each box measure itself + * then running a fitting algorithm + * @param {Chart} chart - the chart + * @param {number} width - the width to fit into + * @param {number} height - the height to fit into + */ + update(chart: Chart, width: number, height: number): void; +}; + +export interface Plugin extends ExtendedPlugin { + id: string; + + /** + * The events option defines the browser events that the plugin should listen. + * @default ['mousemove', 'mouseout', 'click', 'touchstart', 'touchmove'] + */ + events?: (keyof HTMLElementEventMap)[] + + /** + * @desc Called when plugin is installed for this chart instance. This hook is also invoked for disabled plugins (options === false). + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {object} options - The plugin options. + * @since 3.0.0 + */ + install?(chart: Chart, args: EmptyObject, options: O): void; + /** + * @desc Called when a plugin is starting. This happens when chart is created or plugin is enabled. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {object} options - The plugin options. + * @since 3.0.0 + */ + start?(chart: Chart, args: EmptyObject, options: O): void; + /** + * @desc Called when a plugin stopping. This happens when chart is destroyed or plugin is disabled. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {object} options - The plugin options. + * @since 3.0.0 + */ + stop?(chart: Chart, args: EmptyObject, options: O): void; + /** + * @desc Called before initializing `chart`. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {object} options - The plugin options. + */ + beforeInit?(chart: Chart, args: EmptyObject, options: O): void; + /** + * @desc Called after `chart` has been initialized and before the first update. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {object} options - The plugin options. + */ + afterInit?(chart: Chart, args: EmptyObject, options: O): void; + /** + * @desc Called before updating `chart`. If any plugin returns `false`, the update + * is cancelled (and thus subsequent render(s)) until another `update` is triggered. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {UpdateMode} args.mode - The update mode + * @param {object} options - The plugin options. + * @returns {boolean} `false` to cancel the chart update. + */ + beforeUpdate?(chart: Chart, args: { mode: UpdateMode, cancelable: true }, options: O): boolean | void; + /** + * @desc Called after `chart` has been updated and before rendering. Note that this + * hook will not be called if the chart update has been previously cancelled. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {UpdateMode} args.mode - The update mode + * @param {object} options - The plugin options. + */ + afterUpdate?(chart: Chart, args: { mode: UpdateMode }, options: O): void; + /** + * @desc Called during the update process, before any chart elements have been created. + * This can be used for data decimation by changing the data array inside a dataset. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {object} options - The plugin options. + */ + beforeElementsUpdate?(chart: Chart, args: EmptyObject, options: O): void; + /** + * @desc Called during chart reset + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {object} options - The plugin options. + * @since version 3.0.0 + */ + reset?(chart: Chart, args: EmptyObject, options: O): void; + /** + * @desc Called before updating the `chart` datasets. If any plugin returns `false`, + * the datasets update is cancelled until another `update` is triggered. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {UpdateMode} args.mode - The update mode. + * @param {object} options - The plugin options. + * @returns {boolean} false to cancel the datasets update. + * @since version 2.1.5 + */ + beforeDatasetsUpdate?(chart: Chart, args: { mode: UpdateMode }, options: O): boolean | void; + /** + * @desc Called after the `chart` datasets have been updated. Note that this hook + * will not be called if the datasets update has been previously cancelled. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {UpdateMode} args.mode - The update mode. + * @param {object} options - The plugin options. + * @since version 2.1.5 + */ + afterDatasetsUpdate?(chart: Chart, args: { mode: UpdateMode, cancelable: true }, options: O): void; + /** + * @desc Called before updating the `chart` dataset at the given `args.index`. If any plugin + * returns `false`, the datasets update is cancelled until another `update` is triggered. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {number} args.index - The dataset index. + * @param {object} args.meta - The dataset metadata. + * @param {UpdateMode} args.mode - The update mode. + * @param {object} options - The plugin options. + * @returns {boolean} `false` to cancel the chart datasets drawing. + */ + beforeDatasetUpdate?(chart: Chart, args: { index: number; meta: ChartMeta, mode: UpdateMode, cancelable: true }, options: O): boolean | void; + /** + * @desc Called after the `chart` datasets at the given `args.index` has been updated. Note + * that this hook will not be called if the datasets update has been previously cancelled. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {number} args.index - The dataset index. + * @param {object} args.meta - The dataset metadata. + * @param {UpdateMode} args.mode - The update mode. + * @param {object} options - The plugin options. + */ + afterDatasetUpdate?(chart: Chart, args: { index: number; meta: ChartMeta, mode: UpdateMode, cancelable: false }, options: O): void; + /** + * @desc Called before laying out `chart`. If any plugin returns `false`, + * the layout update is cancelled until another `update` is triggered. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {object} options - The plugin options. + * @returns {boolean} `false` to cancel the chart layout. + */ + beforeLayout?(chart: Chart, args: { cancelable: true }, options: O): boolean | void; + /** + * @desc Called before scale data limits are calculated. This hook is called separately for each scale in the chart. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {Scale} args.scale - The scale. + * @param {object} options - The plugin options. + */ + beforeDataLimits?(chart: Chart, args: { scale: Scale }, options: O): void; + /** + * @desc Called after scale data limits are calculated. This hook is called separately for each scale in the chart. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {Scale} args.scale - The scale. + * @param {object} options - The plugin options. + */ + afterDataLimits?(chart: Chart, args: { scale: Scale }, options: O): void; + /** + * @desc Called before scale builds its ticks. This hook is called separately for each scale in the chart. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {Scale} args.scale - The scale. + * @param {object} options - The plugin options. + */ + beforeBuildTicks?(chart: Chart, args: { scale: Scale }, options: O): void; + /** + * @desc Called after scale has build its ticks. This hook is called separately for each scale in the chart. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {Scale} args.scale - The scale. + * @param {object} options - The plugin options. + */ + afterBuildTicks?(chart: Chart, args: { scale: Scale }, options: O): void; + /** + * @desc Called after the `chart` has been laid out. Note that this hook will not + * be called if the layout update has been previously cancelled. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {object} options - The plugin options. + */ + afterLayout?(chart: Chart, args: EmptyObject, options: O): void; + /** + * @desc Called before rendering `chart`. If any plugin returns `false`, + * the rendering is cancelled until another `render` is triggered. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {object} options - The plugin options. + * @returns {boolean} `false` to cancel the chart rendering. + */ + beforeRender?(chart: Chart, args: { cancelable: true }, options: O): boolean | void; + /** + * @desc Called after the `chart` has been fully rendered (and animation completed). Note + * that this hook will not be called if the rendering has been previously cancelled. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {object} options - The plugin options. + */ + afterRender?(chart: Chart, args: EmptyObject, options: O): void; + /** + * @desc Called before drawing `chart` at every animation frame. If any plugin returns `false`, + * the frame drawing is cancelled untilanother `render` is triggered. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {object} options - The plugin options. + * @returns {boolean} `false` to cancel the chart drawing. + */ + beforeDraw?(chart: Chart, args: { cancelable: true }, options: O): boolean | void; + /** + * @desc Called after the `chart` has been drawn. Note that this hook will not be called + * if the drawing has been previously cancelled. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {object} options - The plugin options. + */ + afterDraw?(chart: Chart, args: EmptyObject, options: O): void; + /** + * @desc Called before drawing the `chart` datasets. If any plugin returns `false`, + * the datasets drawing is cancelled until another `render` is triggered. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {object} options - The plugin options. + * @returns {boolean} `false` to cancel the chart datasets drawing. + */ + beforeDatasetsDraw?(chart: Chart, args: { cancelable: true }, options: O): boolean | void; + /** + * @desc Called after the `chart` datasets have been drawn. Note that this hook + * will not be called if the datasets drawing has been previously cancelled. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {object} options - The plugin options. + */ + afterDatasetsDraw?(chart: Chart, args: EmptyObject, options: O, cancelable: false): void; + /** + * @desc Called before drawing the `chart` dataset at the given `args.index` (datasets + * are drawn in the reverse order). If any plugin returns `false`, the datasets drawing + * is cancelled until another `render` is triggered. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {number} args.index - The dataset index. + * @param {object} args.meta - The dataset metadata. + * @param {object} options - The plugin options. + * @returns {boolean} `false` to cancel the chart datasets drawing. + */ + beforeDatasetDraw?(chart: Chart, args: { index: number; meta: ChartMeta }, options: O): boolean | void; + /** + * @desc Called after the `chart` datasets at the given `args.index` have been drawn + * (datasets are drawn in the reverse order). Note that this hook will not be called + * if the datasets drawing has been previously cancelled. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {number} args.index - The dataset index. + * @param {object} args.meta - The dataset metadata. + * @param {object} options - The plugin options. + */ + afterDatasetDraw?(chart: Chart, args: { index: number; meta: ChartMeta }, options: O): void; + /** + * @desc Called before processing the specified `event`. If any plugin returns `false`, + * the event will be discarded. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {ChartEvent} args.event - The event object. + * @param {boolean} args.replay - True if this event is replayed from `Chart.update` + * @param {boolean} args.inChartArea - The event position is inside chartArea + * @param {boolean} [args.changed] - Set to true if the plugin needs a render. Should only be changed to true, because this args object is passed through all plugins. + * @param {object} options - The plugin options. + */ + beforeEvent?(chart: Chart, args: { event: ChartEvent, replay: boolean, changed?: boolean; cancelable: true, inChartArea: boolean }, options: O): boolean | void; + /** + * @desc Called after the `event` has been consumed. Note that this hook + * will not be called if the `event` has been previously discarded. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {ChartEvent} args.event - The event object. + * @param {boolean} args.replay - True if this event is replayed from `Chart.update` + * @param {boolean} args.inChartArea - The event position is inside chartArea + * @param {boolean} [args.changed] - Set to true if the plugin needs a render. Should only be changed to true, because this args object is passed through all plugins. + * @param {object} options - The plugin options. + */ + afterEvent?(chart: Chart, args: { event: ChartEvent, replay: boolean, changed?: boolean, cancelable: false, inChartArea: boolean }, options: O): void; + /** + * @desc Called after the chart as been resized. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {number} args.size - The new canvas display size (eq. canvas.style width & height). + * @param {object} options - The plugin options. + */ + resize?(chart: Chart, args: { size: { width: number, height: number } }, options: O): void; + /** + * Called before the chart is being destroyed. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {object} options - The plugin options. + */ + beforeDestroy?(chart: Chart, args: EmptyObject, options: O): void; + /** + * Called after the chart has been destroyed. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {object} options - The plugin options. + */ + afterDestroy?(chart: Chart, args: EmptyObject, options: O): void; + /** + * Called after chart is destroyed on all plugins that were installed for that chart. This hook is also invoked for disabled plugins (options === false). + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {object} options - The plugin options. + * @since 3.0.0 + */ + uninstall?(chart: Chart, args: EmptyObject, options: O): void; + + /** + * Default options used in the plugin + */ + defaults?: Partial; +} + +export declare type ChartComponentLike = ChartComponent | ChartComponent[] | { [key: string]: ChartComponent } | Plugin | Plugin[]; + +/** + * Please use the module's default export which provides a singleton instance + * Note: class is exported for typedoc + */ +export interface Registry { + readonly controllers: TypedRegistry; + readonly elements: TypedRegistry; + readonly plugins: TypedRegistry; + readonly scales: TypedRegistry; + + add(...args: ChartComponentLike[]): void; + remove(...args: ChartComponentLike[]): void; + + addControllers(...args: ChartComponentLike[]): void; + addElements(...args: ChartComponentLike[]): void; + addPlugins(...args: ChartComponentLike[]): void; + addScales(...args: ChartComponentLike[]): void; + + getController(id: string): DatasetController | undefined; + getElement(id: string): Element | undefined; + getPlugin(id: string): Plugin | undefined; + getScale(id: string): Scale | undefined; +} + +export declare const registry: Registry; + +export interface Tick { + value: number; + label?: string | string[]; + major?: boolean; +} + +export interface CoreScaleOptions { + /** + * Controls the axis global visibility (visible when true, hidden when false). When display: 'auto', the axis is visible only if at least one associated dataset is visible. + * @default true + */ + display: boolean | 'auto'; + /** + * Align pixel values to device pixels + */ + alignToPixels: boolean; + /** + * Background color of the scale area. + */ + backgroundColor: Color; + /** + * Reverse the scale. + * @default false + */ + reverse: boolean; + /** + * Clip the dataset drawing against the size of the scale instead of chart area. + * @default true + */ + clip: boolean; + /** + * The weight used to sort the axis. Higher weights are further away from the chart area. + * @default true + */ + weight: number; + /** + * User defined minimum value for the scale, overrides minimum value from data. + */ + min: unknown; + /** + * User defined maximum value for the scale, overrides maximum value from data. + */ + max: unknown; + /** + * Adjustment used when calculating the maximum data value. + */ + suggestedMin: unknown; + /** + * Adjustment used when calculating the minimum data value. + */ + suggestedMax: unknown; + /** + * Callback called before the update process starts. + */ + beforeUpdate(axis: Scale): void; + /** + * Callback that runs before dimensions are set. + */ + beforeSetDimensions(axis: Scale): void; + /** + * Callback that runs after dimensions are set. + */ + afterSetDimensions(axis: Scale): void; + /** + * Callback that runs before data limits are determined. + */ + beforeDataLimits(axis: Scale): void; + /** + * Callback that runs after data limits are determined. + */ + afterDataLimits(axis: Scale): void; + /** + * Callback that runs before ticks are created. + */ + beforeBuildTicks(axis: Scale): void; + /** + * Callback that runs after ticks are created. Useful for filtering ticks. + */ + afterBuildTicks(axis: Scale): void; + /** + * Callback that runs before ticks are converted into strings. + */ + beforeTickToLabelConversion(axis: Scale): void; + /** + * Callback that runs after ticks are converted into strings. + */ + afterTickToLabelConversion(axis: Scale): void; + /** + * Callback that runs before tick rotation is determined. + */ + beforeCalculateLabelRotation(axis: Scale): void; + /** + * Callback that runs after tick rotation is determined. + */ + afterCalculateLabelRotation(axis: Scale): void; + /** + * Callback that runs before the scale fits to the canvas. + */ + beforeFit(axis: Scale): void; + /** + * Callback that runs after the scale fits to the canvas. + */ + afterFit(axis: Scale): void; + /** + * Callback that runs at the end of the update process. + */ + afterUpdate(axis: Scale): void; +} + +export interface Scale extends Element, LayoutItem { + readonly id: string; + readonly type: string; + readonly ctx: CanvasRenderingContext2D; + readonly chart: Chart; + + maxWidth: number; + maxHeight: number; + + paddingTop: number; + paddingBottom: number; + paddingLeft: number; + paddingRight: number; + + axis: string; + labelRotation: number; + min: number; + max: number; + ticks: Tick[]; + getMatchingVisibleMetas(type?: string): ChartMeta[]; + + drawTitle(chartArea: ChartArea): void; + drawLabels(chartArea: ChartArea): void; + drawGrid(chartArea: ChartArea): void; + + /** + * @param {number} pixel + * @return {number} + */ + getDecimalForPixel(pixel: number): number; + /** + * Utility for getting the pixel location of a percentage of scale + * The coordinate (0, 0) is at the upper-left corner of the canvas + * @param {number} decimal + * @return {number} + */ + getPixelForDecimal(decimal: number): number; + /** + * Returns the location of the tick at the given index + * The coordinate (0, 0) is at the upper-left corner of the canvas + * @param {number} index + * @return {number} + */ + getPixelForTick(index: number): number; + /** + * Used to get the label to display in the tooltip for the given value + * @param {*} value + * @return {string} + */ + getLabelForValue(value: number): string; + + /** + * Returns the grid line width at given value + */ + getLineWidthForValue(value: number): number; + + /** + * Returns the location of the given data point. Value can either be an index or a numerical value + * The coordinate (0, 0) is at the upper-left corner of the canvas + * @param {*} value + * @param {number} [index] + * @return {number} + */ + getPixelForValue(value: number, index?: number): number; + + /** + * Used to get the data value from a given pixel. This is the inverse of getPixelForValue + * The coordinate (0, 0) is at the upper-left corner of the canvas + * @param {number} pixel + * @return {*} + */ + getValueForPixel(pixel: number): number | undefined; + + getBaseValue(): number; + /** + * Returns the pixel for the minimum chart value + * The coordinate (0, 0) is at the upper-left corner of the canvas + * @return {number} + */ + getBasePixel(): number; + + init(options: O): void; + parse(raw: unknown, index?: number): unknown; + getUserBounds(): { min: number; max: number; minDefined: boolean; maxDefined: boolean }; + getMinMax(canStack: boolean): { min: number; max: number }; + getTicks(): Tick[]; + getLabels(): string[]; + getLabelItems(chartArea?: ChartArea): LabelItem[]; + beforeUpdate(): void; + configure(): void; + afterUpdate(): void; + beforeSetDimensions(): void; + setDimensions(): void; + afterSetDimensions(): void; + beforeDataLimits(): void; + determineDataLimits(): void; + afterDataLimits(): void; + beforeBuildTicks(): void; + buildTicks(): Tick[]; + afterBuildTicks(): void; + beforeTickToLabelConversion(): void; + generateTickLabels(ticks: Tick[]): void; + afterTickToLabelConversion(): void; + beforeCalculateLabelRotation(): void; + calculateLabelRotation(): void; + afterCalculateLabelRotation(): void; + beforeFit(): void; + fit(): void; + afterFit(): void; + + isFullSize(): boolean; +} +export declare class Scale { + constructor(cfg: {id: string, type: string, ctx: CanvasRenderingContext2D, chart: Chart}); +} + +export interface ScriptableScaleContext { + chart: Chart; + scale: Scale; + index: number; + tick: Tick; +} + +export interface ScriptableScalePointLabelContext { + chart: Chart; + scale: Scale; + index: number; + label: string; + type: string; +} + +export interface RenderTextOpts { + /** + * The fill color of the text. If unset, the existing + * fillStyle property of the canvas is unchanged. + */ + color?: Color; + + /** + * The width of the strikethrough / underline + * @default 2 + */ + decorationWidth?: number; + + /** + * The max width of the text in pixels + */ + maxWidth?: number; + + /** + * A rotation to be applied to the canvas + * This is applied after the translation is applied + */ + rotation?: number; + + /** + * Apply a strikethrough effect to the text + */ + strikethrough?: boolean; + + /** + * The color of the text stroke. If unset, the existing + * strokeStyle property of the context is unchanged + */ + strokeColor?: Color; + + /** + * The text stroke width. If unset, the existing + * lineWidth property of the context is unchanged + */ + strokeWidth?: number; + + /** + * The text alignment to use. If unset, the existing + * textAlign property of the context is unchanged + */ + textAlign?: CanvasTextAlign; + + /** + * The text baseline to use. If unset, the existing + * textBaseline property of the context is unchanged + */ + textBaseline?: CanvasTextBaseline; + + /** + * If specified, a translation to apply to the context + */ + translation?: [number, number]; + + /** + * Underline the text + */ + underline?: boolean; + + /** + * Dimensions for drawing the label backdrop + */ + backdrop?: BackdropOptions; +} + +export interface BackdropOptions { + /** + * Left position of backdrop as pixel + */ + left: number; + + /** + * Top position of backdrop as pixel + */ + top: number; + + /** + * Width of backdrop in pixels + */ + width: number; + + /** + * Height of backdrop in pixels + */ + height: number; + + /** + * Color of label backdrops. + */ + color: Scriptable; +} + +export interface LabelItem { + label: string | string[]; + font: CanvasFontSpec; + textOffset: number; + options: RenderTextOpts; +} + +export declare const Ticks: { + formatters: { + /** + * Formatter for value labels + * @param value the value to display + * @return {string|string[]} the label to display + */ + values(value: unknown): string | string[]; + /** + * Formatter for numeric ticks + * @param tickValue the value to be formatted + * @param index the position of the tickValue parameter in the ticks array + * @param ticks the list of ticks being converted + * @return string representation of the tickValue parameter + */ + numeric(this: Scale, tickValue: number, index: number, ticks: { value: number }[]): string; + /** + * Formatter for logarithmic ticks + * @param tickValue the value to be formatted + * @param index the position of the tickValue parameter in the ticks array + * @param ticks the list of ticks being converted + * @return string representation of the tickValue parameter + */ + logarithmic(this: Scale, tickValue: number, index: number, ticks: { value: number }[]): string; + }; +}; + +export interface TypedRegistry { + /** + * @param {ChartComponent} item + * @returns {string} The scope where items defaults were registered to. + */ + register(item: ChartComponent): string; + get(id: string): T | undefined; + unregister(item: ChartComponent): void; +} + +export interface ChartEvent { + type: + | 'contextmenu' + | 'mouseenter' + | 'mousedown' + | 'mousemove' + | 'mouseup' + | 'mouseout' + | 'click' + | 'dblclick' + | 'keydown' + | 'keypress' + | 'keyup' + | 'resize'; + native: Event | null; + x: number | null; + y: number | null; +} +export interface ChartComponent { + id: string; + defaults?: AnyObject; + defaultRoutes?: { [property: string]: string }; + + beforeRegister?(): void; + afterRegister?(): void; + beforeUnregister?(): void; + afterUnregister?(): void; +} + +export type InteractionAxis = 'x' | 'y' | 'xy' | 'r'; + +export interface CoreInteractionOptions { + /** + * Sets which elements appear in the tooltip. See Interaction Modes for details. + * @default 'nearest' + */ + mode: InteractionMode; + /** + * if true, the hover mode only applies when the mouse position intersects an item on the chart. + * @default true + */ + intersect: boolean; + + /** + * Defines which directions are used in calculating distances. Defaults to 'x' for 'index' mode and 'xy' in dataset and 'nearest' modes. + */ + axis: InteractionAxis; + + /** + * if true, the invisible points that are outside of the chart area will also be included when evaluating interactions. + * @default false + */ + includeInvisible: boolean; +} + +export interface CoreChartOptions extends ParsingOptions, AnimationOptions { + + datasets: { + [key in ChartType]: ChartTypeRegistry[key]['datasetOptions'] + } + + /** + * The base axis of the chart. 'x' for vertical charts and 'y' for horizontal charts. + * @default 'x' + */ + indexAxis: 'x' | 'y'; + + /** + * How to clip relative to chartArea. Positive value allows overflow, negative value clips that many pixels inside chartArea. 0 = clip at chartArea. Clipping can also be configured per side: `clip: {left: 5, top: false, right: -2, bottom: 0}` + */ + clip: number | ChartArea | false; + + /** + * base color + * @see Defaults.color + */ + color: Scriptable>; + /** + * base background color + * @see Defaults.backgroundColor + */ + backgroundColor: ScriptableAndArray>; + /** + * base hover background color + * @see Defaults.hoverBackgroundColor + */ + hoverBackgroundColor: ScriptableAndArray>; + /** + * base border color + * @see Defaults.borderColor + */ + borderColor: ScriptableAndArray>; + /** + * base hover border color + * @see Defaults.hoverBorderColor + */ + hoverBorderColor: ScriptableAndArray>; + /** + * base font + * @see Defaults.font + */ + font: Partial; + /** + * Resizes the chart canvas when its container does (important note...). + * @default true + */ + responsive: boolean; + /** + * Maintain the original canvas aspect ratio (width / height) when resizing. For this option to work properly the chart must be in its own dedicated container. + * @default true + */ + maintainAspectRatio: boolean; + /** + * Delay the resize update by give amount of milliseconds. This can ease the resize process by debouncing update of the elements. + * @default 0 + */ + resizeDelay: number; + + /** + * Canvas aspect ratio (i.e. width / height, a value of 1 representing a square canvas). Note that this option is ignored if the height is explicitly defined either as attribute or via the style. + * @default 2 + */ + aspectRatio: number; + + /** + * Locale used for number formatting (using `Intl.NumberFormat`). + * @default user's browser setting + */ + locale: string; + + /** + * Called when a resize occurs. Gets passed two arguments: the chart instance and the new size. + */ + onResize(chart: Chart, size: { width: number; height: number }): void; + + /** + * Override the window's default devicePixelRatio. + * @default window.devicePixelRatio + */ + devicePixelRatio: number; + + interaction: CoreInteractionOptions; + + hover: CoreInteractionOptions; + + /** + * The events option defines the browser events that the chart should listen to for tooltips and hovering. + * @default ['mousemove', 'mouseout', 'click', 'touchstart', 'touchmove'] + */ + events: (keyof HTMLElementEventMap)[] + + /** + * Called when any of the events fire. Passed the event, an array of active elements (bars, points, etc), and the chart. + */ + onHover(event: ChartEvent, elements: ActiveElement[], chart: Chart): void; + + /** + * Called if the event is of type 'mouseup' or 'click'. Passed the event, an array of active elements, and the chart. + */ + onClick(event: ChartEvent, elements: ActiveElement[], chart: Chart): void; + + layout: Partial<{ + autoPadding: boolean; + padding: Scriptable>; + }>; +} + +export type AnimationSpec = { + /** + * The number of milliseconds an animation takes. + * @default 1000 + */ + duration?: Scriptable>; + /** + * Easing function to use + * @default 'easeOutQuart' + */ + easing?: Scriptable>; + + /** + * Delay before starting the animations. + * @default 0 + */ + delay?: Scriptable>; + + /** + * If set to true, the animations loop endlessly. + * @default false + */ + loop?: Scriptable>; +} + +export type AnimationsSpec = { + [name: string]: false | AnimationSpec & { + properties: string[]; + + /** + * Type of property, determines the interpolator used. Possible values: 'number', 'color' and 'boolean'. Only really needed for 'color', because typeof does not get that right. + */ + type: 'color' | 'number' | 'boolean'; + + fn: (from: T, to: T, factor: number) => T; + + /** + * Start value for the animation. Current value is used when undefined + */ + from: Scriptable>; + /** + * + */ + to: Scriptable>; + } +} + +export type TransitionSpec = { + animation: AnimationSpec; + animations: AnimationsSpec; +} + +export type TransitionsSpec = { + [mode: string]: TransitionSpec +} + +export type AnimationOptions = { + animation: false | AnimationSpec & { + /** + * Callback called on each step of an animation. + */ + onProgress?: (this: Chart, event: AnimationEvent) => void; + /** + * Callback called when all animations are completed. + */ + onComplete?: (this: Chart, event: AnimationEvent) => void; + }; + animations: AnimationsSpec; + transitions: TransitionsSpec; +}; + +export interface FontSpec { + /** + * Default font family for all text, follows CSS font-family options. + * @default "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif" + */ + family: string; + /** + * Default font size (in px) for text. Does not apply to radialLinear scale point labels. + * @default 12 + */ + size: number; + /** + * Default font style. Does not apply to tooltip title or footer. Does not apply to chart title. Follows CSS font-style options (i.e. normal, italic, oblique, initial, inherit) + * @default 'normal' + */ + style: 'normal' | 'italic' | 'oblique' | 'initial' | 'inherit'; + /** + * Default font weight (boldness). (see MDN). + */ + weight: 'normal' | 'bold' | 'lighter' | 'bolder' | number | null; + /** + * Height of an individual line of text (see MDN). + * @default 1.2 + */ + lineHeight: number | string; +} + +export interface CanvasFontSpec extends FontSpec { + string: string; +} + +export type TextAlign = 'left' | 'center' | 'right'; +export type Align = 'start' | 'center' | 'end'; + +export interface VisualElement { + draw(ctx: CanvasRenderingContext2D, area?: ChartArea): void; + inRange(mouseX: number, mouseY: number, useFinalPosition?: boolean): boolean; + inXRange(mouseX: number, useFinalPosition?: boolean): boolean; + inYRange(mouseY: number, useFinalPosition?: boolean): boolean; + getCenterPoint(useFinalPosition?: boolean): Point; + getRange?(axis: 'x' | 'y'): number; +} + +export interface CommonElementOptions { + borderWidth: number; + borderColor: Color; + backgroundColor: Color; +} + +export interface CommonHoverOptions { + hoverBorderWidth: number; + hoverBorderColor: Color; + hoverBackgroundColor: Color; +} + +export interface Segment { + start: number; + end: number; + loop: boolean; +} + +export interface ArcBorderRadius { + outerStart: number; + outerEnd: number; + innerStart: number; + innerEnd: number; +} + +export interface ArcOptions extends CommonElementOptions { + /** + * If true, Arc can take up 100% of a circular graph without any visual split or cut. This option doesn't support borderRadius and borderJoinStyle miter + * @default true + */ + selfJoin: boolean; + + /** + * Arc stroke alignment. + */ + borderAlign: 'center' | 'inner'; + /** + * Line dash. See MDN. + * @default [] + */ + borderDash: number[]; + /** + * Line dash offset. See MDN. + * @default 0.0 + */ + borderDashOffset: number; + /** + * Line join style. See MDN. Default is 'round' when `borderAlign` is 'inner', else 'bevel'. + */ + borderJoinStyle: CanvasLineJoin; + + /** + * Sets the border radius for arcs + * @default 0 + */ + borderRadius: number | ArcBorderRadius; + + /** + * Arc offset (in pixels). + */ + offset: number; + + /** + * If false, Arc will be flat. + * @default true + */ + circular: boolean; + + /** + * Spacing between arcs + */ + spacing: number +} + +export interface ArcHoverOptions extends CommonHoverOptions { + hoverBorderDash: number[]; + hoverBorderDashOffset: number; + hoverOffset: number; +} + +export interface LineProps { + points: Point[] +} + +export interface LineOptions extends CommonElementOptions { + /** + * Line cap style. See MDN. + * @default 'butt' + */ + borderCapStyle: CanvasLineCap; + /** + * Line dash. See MDN. + * @default [] + */ + borderDash: number[]; + /** + * Line dash offset. See MDN. + * @default 0.0 + */ + borderDashOffset: number; + /** + * Line join style. See MDN. + * @default 'miter' + */ + borderJoinStyle: CanvasLineJoin; + /** + * true to keep Bézier control inside the chart, false for no restriction. + * @default true + */ + capBezierPoints: boolean; + /** + * Interpolation mode to apply. + * @default 'default' + */ + cubicInterpolationMode: 'default' | 'monotone'; + /** + * Bézier curve tension (0 for no Bézier curves). + * @default 0 + */ + tension: number; + /** + * true to show the line as a stepped line (tension will be ignored). + * @default false + */ + stepped: 'before' | 'after' | 'middle' | boolean; + /** + * Both line and radar charts support a fill option on the dataset object which can be used to create area between two datasets or a dataset and a boundary, i.e. the scale origin, start or end + */ + fill: FillTarget | ComplexFillTarget; + /** + * If true, lines will be drawn between points with no or null data. If false, points with NaN data will create a break in the line. Can also be a number specifying the maximum gap length to span. The unit of the value depends on the scale used. + */ + spanGaps: boolean | number; + + segment: { + backgroundColor: Scriptable, + borderColor: Scriptable, + borderCapStyle: Scriptable; + borderDash: Scriptable; + borderDashOffset: Scriptable; + borderJoinStyle: Scriptable; + borderWidth: Scriptable; + }; +} + +export interface LineHoverOptions extends CommonHoverOptions { + hoverBorderCapStyle: CanvasLineCap; + hoverBorderDash: number[]; + hoverBorderDashOffset: number; + hoverBorderJoinStyle: CanvasLineJoin; +} + +export interface LineElement + extends Element, + VisualElement { + updateControlPoints(chartArea: ChartArea, indexAxis?: 'x' | 'y'): void; + points: Point[]; + readonly segments: Segment[]; + first(): Point | false; + last(): Point | false; + interpolate(point: Point, property: 'x' | 'y'): undefined | Point | Point[]; + pathSegment(ctx: CanvasRenderingContext2D, segment: Segment, params: AnyObject): undefined | boolean; + path(ctx: CanvasRenderingContext2D): boolean; +} + +export declare const LineElement: ChartComponent & { + prototype: LineElement; + new (cfg: AnyObject): LineElement; +}; + +export type PointStyle = + | 'circle' + | 'cross' + | 'crossRot' + | 'dash' + | 'line' + | 'rect' + | 'rectRounded' + | 'rectRot' + | 'star' + | 'triangle' + | false + | HTMLImageElement + | HTMLCanvasElement; + +export interface PointOptions extends CommonElementOptions { + /** + * Point radius + * @default 3 + */ + radius: number; + /** + * Extra radius added to point radius for hit detection. + * @default 1 + */ + hitRadius: number; + /** + * Point style + * @default 'circle; + */ + pointStyle: PointStyle; + /** + * Point rotation (in degrees). + * @default 0 + */ + rotation: number; + /** + * Draw the active elements over the other elements of the dataset, + * @default true + */ + drawActiveElementsOnTop: boolean; +} + +export interface PointHoverOptions extends CommonHoverOptions { + /** + * Point radius when hovered. + * @default 4 + */ + hoverRadius: number; +} + +export interface PointPrefixedOptions { + /** + * The fill color for points. + */ + pointBackgroundColor: Color; + /** + * The border color for points. + */ + pointBorderColor: Color; + /** + * The width of the point border in pixels. + */ + pointBorderWidth: number; + /** + * The pixel size of the non-displayed point that reacts to mouse events. + */ + pointHitRadius: number; + /** + * The radius of the point shape. If set to 0, the point is not rendered. + */ + pointRadius: number; + /** + * The rotation of the point in degrees. + */ + pointRotation: number; + /** + * Style of the point. + */ + pointStyle: PointStyle; +} + +export interface PointPrefixedHoverOptions { + /** + * Point background color when hovered. + */ + pointHoverBackgroundColor: Color; + /** + * Point border color when hovered. + */ + pointHoverBorderColor: Color; + /** + * Border width of point when hovered. + */ + pointHoverBorderWidth: number; + /** + * The radius of the point when hovered. + */ + pointHoverRadius: number; +} + +export interface BarProps extends Point { + base: number; + horizontal: boolean; + width: number; + height: number; +} + +export interface BarOptions extends Omit { + /** + * The base value for the bar in data units along the value axis. + */ + base: number; + + /** + * Skipped (excluded) border: 'start', 'end', 'left', 'right', 'bottom', 'top', 'middle', false (none) or true (all). + * @default 'start' + */ + borderSkipped: 'start' | 'end' | 'left' | 'right' | 'bottom' | 'top' | 'middle' | boolean; + + /** + * Border radius + * @default 0 + */ + borderRadius: number | BorderRadius; + + /** + * Amount to inflate the rectangle(s). This can be used to hide artifacts between bars. + * Unit is pixels. 'auto' translates to 0.33 pixels when barPercentage * categoryPercentage is 1, else 0. + * @default 'auto' + */ + inflateAmount: number | 'auto'; + + /** + * Width of the border, number for all sides, object to specify width for each side specifically + * @default 0 + */ + borderWidth: number | { top?: number, right?: number, bottom?: number, left?: number }; +} + +export interface BorderRadius { + topLeft: number; + topRight: number; + bottomLeft: number; + bottomRight: number; +} + +export interface BarHoverOptions extends CommonHoverOptions { + hoverBorderRadius: number | BorderRadius; +} + +export interface BarElement< + T extends BarProps = BarProps, + O extends BarOptions = BarOptions +> extends Element, VisualElement {} + +export declare const BarElement: ChartComponent & { + prototype: BarElement; + new (cfg: AnyObject): BarElement; +}; + +export interface ElementOptionsByType { + arc: ScriptableAndArrayOptions>; + bar: ScriptableAndArrayOptions>; + line: ScriptableAndArrayOptions>; + point: ScriptableAndArrayOptions>; +} + +export type ElementChartOptions = { + elements: ElementOptionsByType +}; + +export declare class BasePlatform { + /** + * Called at chart construction time, returns a context2d instance implementing + * the [W3C Canvas 2D Context API standard]{@link https://www.w3.org/TR/2dcontext/}. + * @param {HTMLCanvasElement} canvas - The canvas from which to acquire context (platform specific) + * @param options - The chart options + */ + acquireContext( + canvas: HTMLCanvasElement, + options?: CanvasRenderingContext2DSettings + ): CanvasRenderingContext2D | null; + /** + * Called at chart destruction time, releases any resources associated to the context + * previously returned by the acquireContext() method. + * @param {CanvasRenderingContext2D} context - The context2d instance + * @returns {boolean} true if the method succeeded, else false + */ + releaseContext(context: CanvasRenderingContext2D): boolean; + /** + * Registers the specified listener on the given chart. + * @param {Chart} chart - Chart from which to listen for event + * @param {string} type - The ({@link ChartEvent}) type to listen for + * @param listener - Receives a notification (an object that implements + * the {@link ChartEvent} interface) when an event of the specified type occurs. + */ + addEventListener(chart: Chart, type: string, listener: (e: ChartEvent) => void): void; + /** + * Removes the specified listener previously registered with addEventListener. + * @param {Chart} chart - Chart from which to remove the listener + * @param {string} type - The ({@link ChartEvent}) type to remove + * @param listener - The listener function to remove from the event target. + */ + removeEventListener(chart: Chart, type: string, listener: (e: ChartEvent) => void): void; + /** + * @returns {number} the current devicePixelRatio of the device this platform is connected to. + */ + getDevicePixelRatio(): number; + /** + * @param {HTMLCanvasElement} canvas - The canvas for which to calculate the maximum size + * @param {number} [width] - Parent element's content width + * @param {number} [height] - Parent element's content height + * @param {number} [aspectRatio] - The aspect ratio to maintain + * @returns { width: number, height: number } the maximum size available. + */ + getMaximumSize(canvas: HTMLCanvasElement, width?: number, height?: number, aspectRatio?: number): { width: number, height: number }; + /** + * @param {HTMLCanvasElement} canvas + * @returns {boolean} true if the canvas is attached to the platform, false if not. + */ + isAttached(canvas: HTMLCanvasElement): boolean; + /** + * Updates config with platform specific requirements + * @param {ChartConfiguration | ChartConfigurationCustomTypes} config + */ + updateConfig(config: ChartConfiguration | ChartConfigurationCustomTypesPerDataset): void; +} + +export declare class BasicPlatform extends BasePlatform {} +export declare class DomPlatform extends BasePlatform {} + +export declare const Decimation: Plugin; + +export declare const enum DecimationAlgorithm { + lttb = 'lttb', + minmax = 'min-max', +} +interface BaseDecimationOptions { + enabled: boolean; + threshold?: number; +} + +interface LttbDecimationOptions extends BaseDecimationOptions { + algorithm: DecimationAlgorithm.lttb | 'lttb'; + samples?: number; +} + +interface MinMaxDecimationOptions extends BaseDecimationOptions { + algorithm: DecimationAlgorithm.minmax | 'min-max'; +} + +export type DecimationOptions = LttbDecimationOptions | MinMaxDecimationOptions; + +export declare const Filler: Plugin; +export interface FillerOptions { + drawTime: 'beforeDraw' | 'beforeDatasetDraw' | 'beforeDatasetsDraw'; + propagate: boolean; +} + +export type FillTarget = number | string | { value: number } | 'start' | 'end' | 'origin' | 'stack' | 'shape' | boolean; + +export interface ComplexFillTarget { + /** + * The accepted values are the same as the filling mode values, so you may use absolute and relative dataset indexes and/or boundaries. + */ + target: FillTarget; + /** + * If no color is set, the default color will be the background color of the chart. + */ + above: Color; + /** + * Same as the above. + */ + below: Color; +} + +export interface FillerControllerDatasetOptions { + /** + * Both line and radar charts support a fill option on the dataset object which can be used to create area between two datasets or a dataset and a boundary, i.e. the scale origin, start or end + */ + fill: FillTarget | ComplexFillTarget; +} + +export declare const Legend: Plugin; + +export interface LegendItem { + /** + * Label that will be displayed + */ + text: string; + + /** + * Border radius of the legend box + * @since 3.1.0 + */ + borderRadius?: number | BorderRadius; + + /** + * Index of the associated dataset + */ + datasetIndex?: number; + + /** + * Index the associated label in the labels array + */ + index?: number + + /** + * Fill style of the legend box + */ + fillStyle?: Color; + + /** + * Font color for the text + * Defaults to LegendOptions.labels.color + */ + fontColor?: Color; + + /** + * If true, this item represents a hidden dataset. Label will be rendered with a strike-through effect + */ + hidden?: boolean; + + /** + * For box border. + * @see https://developer.mozilla.org/en/docs/Web/API/CanvasRenderingContext2D/lineCap + */ + lineCap?: CanvasLineCap; + + /** + * For box border. + * @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash + */ + lineDash?: number[]; + + /** + * For box border. + * @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset + */ + lineDashOffset?: number; + + /** + * For box border. + * @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin + */ + lineJoin?: CanvasLineJoin; + + /** + * Width of box border + */ + lineWidth?: number; + + /** + * Stroke style of the legend box + */ + strokeStyle?: Color; + + /** + * Point style of the legend box (only used if usePointStyle is true) + */ + pointStyle?: PointStyle; + + /** + * Rotation of the point in degrees (only used if usePointStyle is true) + */ + rotation?: number; + + /** + * Text alignment + */ + textAlign?: TextAlign; +} + +export interface LegendElement extends Element>, LayoutItem { + chart: Chart; + ctx: CanvasRenderingContext2D; + legendItems?: LegendItem[]; + options: LegendOptions; + fit(): void; +} + +export interface LegendOptions { + /** + * Is the legend shown? + * @default true + */ + display: boolean; + /** + * Position of the legend. + * @default 'top' + */ + position: LayoutPosition; + /** + * Alignment of the legend. + * @default 'center' + */ + align: Align; + /** + * Maximum height of the legend, in pixels + */ + maxHeight: number; + /** + * Maximum width of the legend, in pixels + */ + maxWidth: number; + /** + * Marks that this box should take the full width/height of the canvas (moving other boxes). This is unlikely to need to be changed in day-to-day use. + * @default true + */ + fullSize: boolean; + /** + * Legend will show datasets in reverse order. + * @default false + */ + reverse: boolean; + /** + * A callback that is called when a click event is registered on a label item. + */ + onClick(this: LegendElement, e: ChartEvent, legendItem: LegendItem, legend: LegendElement): void; + /** + * A callback that is called when a 'mousemove' event is registered on top of a label item + */ + onHover(this: LegendElement, e: ChartEvent, legendItem: LegendItem, legend: LegendElement): void; + /** + * A callback that is called when a 'mousemove' event is registered outside of a previously hovered label item. + */ + onLeave(this: LegendElement, e: ChartEvent, legendItem: LegendItem, legend: LegendElement): void; + + labels: { + /** + * Width of colored box. + * @default 40 + */ + boxWidth: number; + /** + * Height of the coloured box. + * @default fontSize + */ + boxHeight: number; + /** + * Color of label + * @see Defaults.color + */ + color: Color; + /** + * Font of label + * @see Defaults.font + */ + font: ScriptableAndScriptableOptions, ScriptableChartContext>; + /** + * Padding between rows of colored boxes. + * @default 10 + */ + padding: number; + /** + * If usePointStyle is true, the width of the point style used for the legend. + */ + pointStyleWidth: number; + /** + * Generates legend items for each thing in the legend. Default implementation returns the text + styling for the color box. See Legend Item for details. + */ + generateLabels(chart: Chart): LegendItem[]; + + /** + * Filters legend items out of the legend. Receives 2 parameters, a Legend Item and the chart data + */ + filter(item: LegendItem, data: ChartData): boolean; + + /** + * Sorts the legend items + */ + sort(a: LegendItem, b: LegendItem, data: ChartData): number; + + /** + * Override point style for the legend. Only applies if usePointStyle is true + */ + pointStyle: PointStyle; + + /** + * Text alignment + */ + textAlign?: TextAlign; + + /** + * Label style will match corresponding point style (size is based on the minimum value between boxWidth and font.size). + * @default false + */ + usePointStyle: boolean; + + /** + * Label borderRadius will match corresponding borderRadius. + * @default false + */ + useBorderRadius: boolean; + + /** + * Override the borderRadius to use. + * @default undefined + */ + borderRadius: number; + }; + /** + * true for rendering the legends from right to left. + */ + rtl: boolean; + /** + * This will force the text direction 'rtl' or 'ltr' on the canvas for rendering the legend, regardless of the css specified on the canvas + * @default canvas's default + */ + textDirection: string; + + title: { + /** + * Is the legend title displayed. + * @default false + */ + display: boolean; + /** + * Color of title + * @see Defaults.color + */ + color: Color; + /** + * see Fonts + */ + font: ScriptableAndScriptableOptions, ScriptableChartContext>; + position: 'center' | 'start' | 'end'; + padding?: number | ChartArea; + /** + * The string title. + */ + text: string; + }; +} + +export declare const SubTitle: Plugin; +export declare const Title: Plugin; + +export interface TitleOptions { + /** + * Alignment of the title. + * @default 'center' + */ + align: Align; + /** + * Is the title shown? + * @default false + */ + display: boolean; + /** + * Position of title + * @default 'top' + */ + position: 'top' | 'left' | 'bottom' | 'right'; + /** + * Color of text + * @see Defaults.color + */ + color: Color; + font: ScriptableAndScriptableOptions, ScriptableChartContext>; + + /** + * Marks that this box should take the full width/height of the canvas (moving other boxes). If set to `false`, places the box above/beside the + * chart area + * @default true + */ + fullSize: boolean; + /** + * Adds padding above and below the title text if a single number is specified. It is also possible to change top and bottom padding separately. + */ + padding: number | { top: number; bottom: number }; + /** + * Title text to display. If specified as an array, text is rendered on multiple lines. + */ + text: string | string[]; +} + +export type TooltipXAlignment = 'left' | 'center' | 'right'; +export type TooltipYAlignment = 'top' | 'center' | 'bottom'; +export interface TooltipLabelStyle { + borderColor: Color; + backgroundColor: Color; + + /** + * Width of border line + * @since 3.1.0 + */ + borderWidth?: number; + + /** + * Border dash + * @since 3.1.0 + */ + borderDash?: [number, number]; + + /** + * Border dash offset + * @since 3.1.0 + */ + borderDashOffset?: number; + + /** + * borderRadius + * @since 3.1.0 + */ + borderRadius?: number | BorderRadius; +} +export interface TooltipModel extends Element> { + readonly chart: Chart; + + // The items that we are rendering in the tooltip. See Tooltip Item Interface section + dataPoints: TooltipItem[]; + + // Positioning + xAlign: TooltipXAlignment; + yAlign: TooltipYAlignment; + + // X and Y properties are the top left of the tooltip + x: number; + y: number; + width: number; + height: number; + // Where the tooltip points to + caretX: number; + caretY: number; + + // Body + // The body lines that need to be rendered + // Each object contains 3 parameters + // before: string[] // lines of text before the line with the color square + // lines: string[]; // lines of text to render as the main item with color square + // after: string[]; // lines of text to render after the main lines + body: { before: string[]; lines: string[]; after: string[] }[]; + // lines of text that appear after the title but before the body + beforeBody: string[]; + // line of text that appear after the body and before the footer + afterBody: string[]; + + // Title + // lines of text that form the title + title: string[]; + + // Footer + // lines of text that form the footer + footer: string[]; + + // Styles to render for each item in body[]. This is the styling of the squares in the tooltip + labelColors: TooltipLabelStyle[]; + labelTextColors: Color[]; + labelPointStyles: { pointStyle: PointStyle; rotation: number }[]; + + // 0 opacity is a hidden tooltip + opacity: number; + + // tooltip options + options: TooltipOptions; + + getActiveElements(): ActiveElement[]; + setActiveElements(active: ActiveDataPoint[], eventPosition: Point): void; +} + +export interface TooltipPosition extends Point { + xAlign?: TooltipXAlignment; + yAlign?: TooltipYAlignment; +} + +export type TooltipPositionerFunction = ( + this: TooltipModel, + items: readonly ActiveElement[], + eventPosition: Point +) => TooltipPosition | false; + +export interface TooltipPositionerMap { + average: TooltipPositionerFunction; + nearest: TooltipPositionerFunction; +} + +export type TooltipPositioner = keyof TooltipPositionerMap; + +export interface Tooltip extends Plugin { + readonly positioners: TooltipPositionerMap; +} + +export declare const Tooltip: Tooltip; + +export interface TooltipDatasetCallbacks< + TType extends ChartType, + Model = TooltipModel, + Item = TooltipItem> { + beforeLabel(this: Model, tooltipItem: Item): string | string[] | void; + label(this: Model, tooltipItem: Item): string | string[] | void; + afterLabel(this: Model, tooltipItem: Item): string | string[] | void; + + labelColor(this: Model, tooltipItem: Item): TooltipLabelStyle | void; + labelTextColor(this: Model, tooltipItem: Item): Color | void; + labelPointStyle(this: Model, tooltipItem: Item): { pointStyle: PointStyle; rotation: number } | void; +} + +export interface TooltipCallbacks< + TType extends ChartType, + Model = TooltipModel, + Item = TooltipItem> extends TooltipDatasetCallbacks { + + beforeTitle(this: Model, tooltipItems: Item[]): string | string[] | void; + title(this: Model, tooltipItems: Item[]): string | string[] | void; + afterTitle(this: Model, tooltipItems: Item[]): string | string[] | void; + + beforeBody(this: Model, tooltipItems: Item[]): string | string[] | void; + afterBody(this: Model, tooltipItems: Item[]): string | string[] | void; + + beforeLabel(this: Model, tooltipItem: Item): string | string[] | void; + label(this: Model, tooltipItem: Item): string | string[] | void; + afterLabel(this: Model, tooltipItem: Item): string | string[] | void; + + labelColor(this: Model, tooltipItem: Item): TooltipLabelStyle | void; + labelTextColor(this: Model, tooltipItem: Item): Color | void; + labelPointStyle(this: Model, tooltipItem: Item): { pointStyle: PointStyle; rotation: number } | void; + + beforeFooter(this: Model, tooltipItems: Item[]): string | string[] | void; + footer(this: Model, tooltipItems: Item[]): string | string[] | void; + afterFooter(this: Model, tooltipItems: Item[]): string | string[] | void; +} + +export interface ExtendedPlugin< + TType extends ChartType, + O = AnyObject, + Model = TooltipModel> { + /** + * @desc Called before drawing the `tooltip`. If any plugin returns `false`, + * the tooltip drawing is cancelled until another `render` is triggered. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {Tooltip} args.tooltip - The tooltip. + * @param {object} options - The plugin options. + * @returns {boolean} `false` to cancel the chart tooltip drawing. + */ + beforeTooltipDraw?(chart: Chart, args: { tooltip: Model, cancelable: true }, options: O): boolean | void; + /** + * @desc Called after drawing the `tooltip`. Note that this hook will not + * be called if the tooltip drawing has been previously cancelled. + * @param {Chart} chart - The chart instance. + * @param {object} args - The call arguments. + * @param {Tooltip} args.tooltip - The tooltip. + * @param {object} options - The plugin options. + */ + afterTooltipDraw?(chart: Chart, args: { tooltip: Model }, options: O): void; +} + +export interface ScriptableTooltipContext { + chart: UnionToIntersection>; + tooltip: UnionToIntersection>; + tooltipItems: TooltipItem[]; +} + +export interface TooltipOptions extends CoreInteractionOptions { + /** + * Are on-canvas tooltips enabled? + * @default true + */ + enabled: Scriptable>; + /** + * See external tooltip section. + */ + external(this: TooltipModel, args: { chart: Chart; tooltip: TooltipModel }): void; + /** + * The mode for positioning the tooltip + */ + position: Scriptable> + + /** + * Override the tooltip alignment calculations + */ + xAlign: Scriptable>; + yAlign: Scriptable>; + + /** + * Sort tooltip items. + */ + itemSort: (a: TooltipItem, b: TooltipItem, data: ChartData) => number; + + filter: (e: TooltipItem, index: number, array: TooltipItem[], data: ChartData) => boolean; + + /** + * Background color of the tooltip. + * @default 'rgba(0, 0, 0, 0.8)' + */ + backgroundColor: Scriptable>; + /** + * Padding between the color box and the text. + * @default 1 + */ + boxPadding: number; + /** + * Color of title + * @default '#fff' + */ + titleColor: Scriptable>; + /** + * See Fonts + * @default {weight: 'bold'} + */ + titleFont: ScriptableAndScriptableOptions, ScriptableTooltipContext>; + /** + * Spacing to add to top and bottom of each title line. + * @default 2 + */ + titleSpacing: Scriptable>; + /** + * Margin to add on bottom of title section. + * @default 6 + */ + titleMarginBottom: Scriptable>; + /** + * Horizontal alignment of the title text lines. + * @default 'left' + */ + titleAlign: Scriptable>; + /** + * Spacing to add to top and bottom of each tooltip item. + * @default 2 + */ + bodySpacing: Scriptable>; + /** + * Color of body + * @default '#fff' + */ + bodyColor: Scriptable>; + /** + * See Fonts. + * @default {} + */ + bodyFont: ScriptableAndScriptableOptions, ScriptableTooltipContext>; + /** + * Horizontal alignment of the body text lines. + * @default 'left' + */ + bodyAlign: Scriptable>; + /** + * Spacing to add to top and bottom of each footer line. + * @default 2 + */ + footerSpacing: Scriptable>; + /** + * Margin to add before drawing the footer. + * @default 6 + */ + footerMarginTop: Scriptable>; + /** + * Color of footer + * @default '#fff' + */ + footerColor: Scriptable>; + /** + * See Fonts + * @default {weight: 'bold'} + */ + footerFont: ScriptableAndScriptableOptions, ScriptableTooltipContext>; + /** + * Horizontal alignment of the footer text lines. + * @default 'left' + */ + footerAlign: Scriptable>; + /** + * Padding to add to the tooltip + * @default 6 + */ + padding: Scriptable>; + /** + * Extra distance to move the end of the tooltip arrow away from the tooltip point. + * @default 2 + */ + caretPadding: Scriptable>; + /** + * Size, in px, of the tooltip arrow. + * @default 5 + */ + caretSize: Scriptable>; + /** + * Radius of tooltip corner curves. + * @default 6 + */ + cornerRadius: Scriptable>; + /** + * Color to draw behind the colored boxes when multiple items are in the tooltip. + * @default '#fff' + */ + multiKeyBackground: Scriptable>; + /** + * If true, color boxes are shown in the tooltip. + * @default true + */ + displayColors: Scriptable>; + /** + * Width of the color box if displayColors is true. + * @default bodyFont.size + */ + boxWidth: Scriptable>; + /** + * Height of the color box if displayColors is true. + * @default bodyFont.size + */ + boxHeight: Scriptable>; + /** + * Use the corresponding point style (from dataset options) instead of color boxes, ex: star, triangle etc. (size is based on the minimum value between boxWidth and boxHeight) + * @default false + */ + usePointStyle: Scriptable>; + /** + * Color of the border. + * @default 'rgba(0, 0, 0, 0)' + */ + borderColor: Scriptable>; + /** + * Size of the border. + * @default 0 + */ + borderWidth: Scriptable>; + /** + * true for rendering the legends from right to left. + */ + rtl: Scriptable>; + + /** + * This will force the text direction 'rtl' or 'ltr on the canvas for rendering the tooltips, regardless of the css specified on the canvas + * @default canvas's default + */ + textDirection: Scriptable>; + + animation: AnimationSpec | false; + animations: AnimationsSpec | false; + callbacks: TooltipCallbacks; +} + +export interface TooltipDatasetOptions { + callbacks: TooltipDatasetCallbacks; +} + +export interface TooltipItem { + /** + * The chart the tooltip is being shown on + */ + chart: Chart; + + /** + * Label for the tooltip + */ + label: string; + + /** + * Parsed data values for the given `dataIndex` and `datasetIndex` + */ + parsed: UnionToIntersection>; + + /** + * Raw data values for the given `dataIndex` and `datasetIndex` + */ + raw: unknown; + + /** + * Formatted value for the tooltip + */ + formattedValue: string; + + /** + * The dataset the item comes from + */ + dataset: UnionToIntersection>; + + /** + * Index of the dataset the item comes from + */ + datasetIndex: number; + + /** + * Index of this data item in the dataset + */ + dataIndex: number; + + /** + * The chart element (point, arc, bar, etc.) for this tooltip item + */ + element: Element; +} + +export interface PluginDatasetOptionsByType { + tooltip: TooltipDatasetOptions; +} + +export interface PluginOptionsByType { + colors: ColorsPluginOptions; + decimation: DecimationOptions; + filler: FillerOptions; + legend: LegendOptions; + subtitle: TitleOptions; + title: TitleOptions; + tooltip: TooltipOptions; +} +export interface PluginChartOptions { + plugins: PluginOptionsByType; +} + +export interface BorderOptions { + /** + * @default true + */ + display: boolean + /** + * @default [] + */ + dash: Scriptable; + /** + * @default 0 + */ + dashOffset: Scriptable; + color: Color; + width: number; + z: number; +} + +export interface GridLineOptions { + /** + * @default true + */ + display: boolean; + /** + * @default false + */ + circular: boolean; + /** + * @default 'rgba(0, 0, 0, 0.1)' + */ + color: ScriptableAndArray; + /** + * @default 1 + */ + lineWidth: ScriptableAndArray; + /** + * @default true + */ + drawOnChartArea: boolean; + /** + * @default true + */ + drawTicks: boolean; + /** + * @default [] + */ + tickBorderDash: Scriptable; + /** + * @default 0 + */ + tickBorderDashOffset: Scriptable; + /** + * @default 'rgba(0, 0, 0, 0.1)' + */ + tickColor: ScriptableAndArray; + /** + * @default 10 + */ + tickLength: number; + /** + * @default 1 + */ + tickWidth: number; + /** + * @default false + */ + offset: boolean; + /** + * @default 0 + */ + z: number; +} + +export interface TickOptions { + /** + * Color of label backdrops. + * @default 'rgba(255, 255, 255, 0.75)' + */ + backdropColor: Scriptable; + /** + * Padding of tick backdrop. + * @default 2 + */ + backdropPadding: number | ChartArea; + + /** + * Returns the string representation of the tick value as it should be displayed on the chart. See callback. + */ + callback: (this: Scale, tickValue: number | string, index: number, ticks: Tick[]) => string | string[] | number | number[] | null | undefined; + /** + * If true, show tick labels. + * @default true + */ + display: boolean; + /** + * Color of tick + * @see Defaults.color + */ + color: ScriptableAndArray; + /** + * see Fonts + */ + font: ScriptableAndScriptableOptions, ScriptableScaleContext>; + /** + * Sets the offset of the tick labels from the axis + */ + padding: number; + /** + * If true, draw a background behind the tick labels. + * @default false + */ + showLabelBackdrop: Scriptable; + /** + * The color of the stroke around the text. + * @default undefined + */ + textStrokeColor: Scriptable; + /** + * Stroke width around the text. + * @default 0 + */ + textStrokeWidth: Scriptable; + /** + * z-index of tick layer. Useful when ticks are drawn on chart area. Values <= 0 are drawn under datasets, > 0 on top. + * @default 0 + */ + z: number; + + major: { + /** + * If true, major ticks are generated. A major tick will affect autoskipping and major will be defined on ticks in the scriptable options context. + * @default false + */ + enabled: boolean; + }; +} + +export type CartesianTickOptions = TickOptions & { + /** + * The number of ticks to examine when deciding how many labels will fit. Setting a smaller value will be faster, but may be less accurate when there is large variability in label length. + * @default ticks.length + */ + sampleSize: number; + /** + * The label alignment + * @default 'center' + */ + align: Align | 'inner'; + /** + * If true, automatically calculates how many labels can be shown and hides labels accordingly. Labels will be rotated up to maxRotation before skipping any. Turn autoSkip off to show all labels no matter what. + * @default true + */ + autoSkip: boolean; + /** + * Padding between the ticks on the horizontal axis when autoSkip is enabled. + * @default 0 + */ + autoSkipPadding: number; + + /** + * How is the label positioned perpendicular to the axis direction. + * This only applies when the rotation is 0 and the axis position is one of "top", "left", "right", or "bottom" + * @default 'near' + */ + crossAlign: 'near' | 'center' | 'far'; + + /** + * Should the defined `min` and `max` values be presented as ticks even if they are not "nice". + * @default: true + */ + includeBounds: boolean; + + /** + * Distance in pixels to offset the label from the centre point of the tick (in the x direction for the x axis, and the y direction for the y axis). Note: this can cause labels at the edges to be cropped by the edge of the canvas + * @default 0 + */ + labelOffset: number; + + /** + * Minimum rotation for tick labels. Note: Only applicable to horizontal scales. + * @default 0 + */ + minRotation: number; + /** + * Maximum rotation for tick labels when rotating to condense labels. Note: Rotation doesn't occur until necessary. Note: Only applicable to horizontal scales. + * @default 50 + */ + maxRotation: number; + /** + * Flips tick labels around axis, displaying the labels inside the chart instead of outside. Note: Only applicable to vertical scales. + * @default false + */ + mirror: boolean; + /** + * Padding between the tick label and the axis. When set on a vertical axis, this applies in the horizontal (X) direction. When set on a horizontal axis, this applies in the vertical (Y) direction. + * @default 0 + */ + padding: number; + /** + * Maximum number of ticks and gridlines to show. + * @default 11 + */ + maxTicksLimit: number; +} + +export interface ScriptableCartesianScaleContext { + scale: keyof CartesianScaleTypeRegistry; + type: string; +} + +export interface ScriptableChartContext { + chart: Chart; + type: string; +} + +export interface CartesianScaleOptions extends CoreScaleOptions { + /** + * Scale boundary strategy (bypassed by min/max time options) + * - `data`: make sure data are fully visible, ticks outside are removed + * - `ticks`: make sure ticks are fully visible, data outside are truncated + * @since 2.7.0 + * @default 'ticks' + */ + bounds: 'ticks' | 'data'; + + /** + * Position of the axis. + */ + position: 'left' | 'top' | 'right' | 'bottom' | 'center' | { [scale: string]: number }; + + /** + * Stack group. Axes at the same `position` with same `stack` are stacked. + */ + stack?: string; + + /** + * Weight of the scale in stack group. Used to determine the amount of allocated space for the scale within the group. + * @default 1 + */ + stackWeight?: number; + + /** + * Which type of axis this is. Possible values are: 'x', 'y', 'r'. If not set, this is inferred from the first character of the ID which should be 'x', 'y' or 'r'. + */ + axis: 'x' | 'y' | 'r'; + + /** + * User defined minimum value for the scale, overrides minimum value from data. + */ + min: number; + + /** + * User defined maximum value for the scale, overrides maximum value from data. + */ + max: number; + + /** + * If true, extra space is added to the both edges and the axis is scaled to fit into the chart area. This is set to true for a bar chart by default. + * @default false + */ + offset: boolean; + + grid: Partial; + + border: BorderOptions; + + /** Options for the scale title. */ + title: { + /** If true, displays the axis title. */ + display: boolean; + /** Alignment of the axis title. */ + align: Align; + /** The text for the title, e.g. "# of People" or "Response Choices". */ + text: string | string[]; + /** Color of the axis label. */ + color: Color; + /** The color of the text stroke for the axis label.*/ + strokeColor?: Color; + /** The text stroke width for the axis label.*/ + strokeWidth?: number; + /** Information about the axis title font. */ + font: ScriptableAndScriptableOptions, ScriptableCartesianScaleContext>; + /** Padding to apply around scale labels. */ + padding: number | { + /** Padding on the (relative) top side of this axis label. */ + top: number; + /** Padding on the (relative) bottom side of this axis label. */ + bottom: number; + /** This is a shorthand for defining top/bottom to the same values. */ + y: number; + }; + }; + + /** + * If true, data will be comprised between datasets of data + * @default false + */ + stacked?: boolean | 'single'; + + ticks: CartesianTickOptions; +} + +export type CategoryScaleOptions = Omit & { + min: string | number; + max: string | number; + labels: string[] | string[][]; +}; + +export type CategoryScale = Scale +export declare const CategoryScale: ChartComponent & { + prototype: CategoryScale; + new (cfg: AnyObject): CategoryScale; +}; + +export type LinearScaleOptions = CartesianScaleOptions & { + + /** + * if true, scale will include 0 if it is not already included. + * @default true + */ + beginAtZero: boolean; + /** + * Adjustment used when calculating the minimum data value. + */ + suggestedMin?: number; + /** + * Adjustment used when calculating the maximum data value. + */ + suggestedMax?: number; + /** + * Percentage (string ending with %) or amount (number) for added room in the scale range above and below data. + */ + grace?: string | number; + + ticks: { + /** + * The Intl.NumberFormat options used by the default label formatter + */ + format: Intl.NumberFormatOptions; + + /** + * if defined and stepSize is not specified, the step size will be rounded to this many decimal places. + */ + precision: number; + + /** + * User defined fixed step size for the scale + */ + stepSize: number; + + /** + * User defined count of ticks + */ + count: number; + }; +}; + +export type LinearScale = Scale +export declare const LinearScale: ChartComponent & { + prototype: LinearScale; + new (cfg: AnyObject): LinearScale; +}; + +export type LogarithmicScaleOptions = CartesianScaleOptions & { + /** + * Adjustment used when calculating the maximum data value. + */ + suggestedMin?: number; + /** + * Adjustment used when calculating the minimum data value. + */ + suggestedMax?: number; + + ticks: { + /** + * The Intl.NumberFormat options used by the default label formatter + */ + format: Intl.NumberFormatOptions; + }; +}; + +export type LogarithmicScale = Scale +export declare const LogarithmicScale: ChartComponent & { + prototype: LogarithmicScale; + new (cfg: AnyObject): LogarithmicScale; +}; + +export type TimeScaleTimeOptions = { + /** + * Custom parser for dates. + */ + parser: string | ((v: unknown) => number); + /** + * If defined, dates will be rounded to the start of this unit. See Time Units below for the allowed units. + */ + round: false | TimeUnit; + /** + * If boolean and true and the unit is set to 'week', then the first day of the week will be Monday. Otherwise, it will be Sunday. + * If `number`, the index of the first day of the week (0 - Sunday, 6 - Saturday). + * @default false + */ + isoWeekday: boolean | number; + /** + * Sets how different time units are displayed. + */ + displayFormats: { + [key: string]: string; + }; + /** + * The format string to use for the tooltip. + */ + tooltipFormat: string; + /** + * If defined, will force the unit to be a certain type. See Time Units section below for details. + * @default false + */ + unit: false | TimeUnit; + /** + * The minimum display format to be used for a time unit. + * @default 'millisecond' + */ + minUnit: TimeUnit; +}; + +export type TimeScaleTickOptions = { + /** + * Ticks generation input values: + * - 'auto': generates "optimal" ticks based on scale size and time options. + * - 'data': generates ticks from data (including labels from data `{t|x|y}` objects). + * - 'labels': generates ticks from user given `data.labels` values ONLY. + * @see https://github.com/chartjs/Chart.js/pull/4507 + * @since 2.7.0 + * @default 'auto' + */ + source: 'labels' | 'auto' | 'data'; + /** + * The number of units between grid lines. + * @default 1 + */ + stepSize: number; +}; + +export type TimeScaleOptions = Omit & { + min: string | number; + max: string | number; + suggestedMin: string | number; + suggestedMax: string | number; + /** + * Scale boundary strategy (bypassed by min/max time options) + * - `data`: make sure data are fully visible, ticks outside are removed + * - `ticks`: make sure ticks are fully visible, data outside are truncated + * @since 2.7.0 + * @default 'data' + */ + bounds: 'ticks' | 'data'; + + /** + * If true, bar chart offsets are computed with skipped tick sizes + * @since 3.8.0 + * @default false + */ + offsetAfterAutoskip: boolean; + + /** + * options for creating a new adapter instance + */ + adapters: { + date: unknown; + }; + + time: TimeScaleTimeOptions; + + ticks: TimeScaleTickOptions; +}; + +export interface TimeScale extends Scale { + format(value: number, format?: string): string; + getDataTimestamps(): number[]; + getLabelTimestamps(): string[]; + normalize(values: number[]): number[]; +} + +export declare const TimeScale: ChartComponent & { + prototype: TimeScale; + new (cfg: AnyObject): TimeScale; +}; + +export type TimeSeriesScale = TimeScale +export declare const TimeSeriesScale: ChartComponent & { + prototype: TimeSeriesScale; + new (cfg: AnyObject): TimeSeriesScale; +}; + +export type RadialTickOptions = TickOptions & { + /** + * The Intl.NumberFormat options used by the default label formatter + */ + format: Intl.NumberFormatOptions; + + /** + * Maximum number of ticks and gridlines to show. + * @default 11 + */ + maxTicksLimit: number; + + /** + * if defined and stepSize is not specified, the step size will be rounded to this many decimal places. + */ + precision: number; + + /** + * User defined fixed step size for the scale. + */ + stepSize: number; + + /** + * User defined number of ticks + */ + count: number; +} + +export type RadialLinearScaleOptions = CoreScaleOptions & { + animate: boolean; + + startAngle: number; + + angleLines: { + /** + * if true, angle lines are shown. + * @default true + */ + display: boolean; + /** + * Color of angled lines. + * @default 'rgba(0, 0, 0, 0.1)' + */ + color: Scriptable; + /** + * Width of angled lines. + * @default 1 + */ + lineWidth: Scriptable; + /** + * Length and spacing of dashes on angled lines. See MDN. + * @default [] + */ + borderDash: Scriptable; + /** + * Offset for line dashes. See MDN. + * @default 0 + */ + borderDashOffset: Scriptable; + }; + + /** + * if true, scale will include 0 if it is not already included. + * @default false + */ + beginAtZero: boolean; + + grid: Partial; + + /** + * User defined minimum number for the scale, overrides minimum value from data. + */ + min: number; + /** + * User defined maximum number for the scale, overrides maximum value from data. + */ + max: number; + + pointLabels: { + /** + * Background color of the point label. + * @default undefined + */ + backdropColor: Scriptable; + /** + * Padding of label backdrop. + * @default 2 + */ + backdropPadding: Scriptable; + + /** + * Border radius + * @default 0 + * @since 3.8.0 + */ + borderRadius: Scriptable; + + /** + * if true, point labels are shown. When `display: 'auto'`, the label is hidden if it overlaps with another label. + * @default true + */ + display: boolean | 'auto'; + /** + * Color of label + * @see Defaults.color + */ + color: Scriptable; + /** + */ + font: ScriptableAndScriptableOptions, ScriptableScalePointLabelContext>; + + /** + * Callback function to transform data labels to point labels. The default implementation simply returns the current string. + */ + callback: (label: string, index: number) => string | string[] | number | number[]; + + /** + * Padding around the pointLabels + * @default 5 + */ + padding: Scriptable; + + /** + * if true, point labels are centered. + * @default false + */ + centerPointLabels: boolean; + }; + + /** + * Adjustment used when calculating the maximum data value. + */ + suggestedMax: number; + /** + * Adjustment used when calculating the minimum data value. + */ + suggestedMin: number; + + ticks: RadialTickOptions; +}; + +export interface RadialLinearScale extends Scale { + xCenter: number; + yCenter: number; + readonly drawingArea: number; + setCenterPoint(leftMovement: number, rightMovement: number, topMovement: number, bottomMovement: number): void; + getIndexAngle(index: number): number; + getDistanceFromCenterForValue(value: number): number; + getValueForDistanceFromCenter(distance: number): number; + getPointPosition(index: number, distanceFromCenter: number): { x: number; y: number; angle: number }; + getPointPositionForValue(index: number, value: number): { x: number; y: number; angle: number }; + getPointLabelPosition(index: number): ChartArea; + getBasePosition(index: number): { x: number; y: number; angle: number }; +} +export declare const RadialLinearScale: ChartComponent & { + prototype: RadialLinearScale; + new (cfg: AnyObject): RadialLinearScale; +}; + +export interface CartesianScaleTypeRegistry { + linear: { + options: LinearScaleOptions; + }; + logarithmic: { + options: LogarithmicScaleOptions; + }; + category: { + options: CategoryScaleOptions; + }; + time: { + options: TimeScaleOptions; + }; + timeseries: { + options: TimeScaleOptions; + }; +} + +export interface RadialScaleTypeRegistry { + radialLinear: { + options: RadialLinearScaleOptions; + }; +} + +export interface ScaleTypeRegistry extends CartesianScaleTypeRegistry, RadialScaleTypeRegistry { +} + +export type ScaleType = keyof ScaleTypeRegistry; + +export interface CartesianParsedData extends Point { + // Only specified when stacked bars are enabled + _stacks?: { + // Key is the stack ID which is generally the axis ID + [key: string]: { + // Inner key is the datasetIndex + [key: number]: number; + } + } +} + +export interface BarParsedData extends CartesianParsedData { + // Only specified if floating bars are show + _custom?: { + barStart: number; + barEnd: number; + start: number; + end: number; + min: number; + max: number; + } +} + +export interface BubbleParsedData extends CartesianParsedData { + // The bubble radius value + _custom: number; +} + +export interface RadialParsedData { + r: number; +} + +export interface ChartTypeRegistry { + bar: { + chartOptions: BarControllerChartOptions; + datasetOptions: BarControllerDatasetOptions; + defaultDataPoint: number | [number, number] | null; + metaExtensions: {}; + parsedDataType: BarParsedData, + scales: keyof CartesianScaleTypeRegistry; + }; + line: { + chartOptions: LineControllerChartOptions; + datasetOptions: LineControllerDatasetOptions & FillerControllerDatasetOptions; + defaultDataPoint: ScatterDataPoint | number | null; + metaExtensions: {}; + parsedDataType: CartesianParsedData; + scales: keyof CartesianScaleTypeRegistry; + }; + scatter: { + chartOptions: ScatterControllerChartOptions; + datasetOptions: ScatterControllerDatasetOptions; + defaultDataPoint: ScatterDataPoint | number | null; + metaExtensions: {}; + parsedDataType: CartesianParsedData; + scales: keyof CartesianScaleTypeRegistry; + }; + bubble: { + chartOptions: unknown; + datasetOptions: BubbleControllerDatasetOptions; + defaultDataPoint: BubbleDataPoint; + metaExtensions: {}; + parsedDataType: BubbleParsedData; + scales: keyof CartesianScaleTypeRegistry; + }; + pie: { + chartOptions: PieControllerChartOptions; + datasetOptions: PieControllerDatasetOptions; + defaultDataPoint: PieDataPoint; + metaExtensions: PieMetaExtensions; + parsedDataType: number; + scales: keyof CartesianScaleTypeRegistry; + }; + doughnut: { + chartOptions: DoughnutControllerChartOptions; + datasetOptions: DoughnutControllerDatasetOptions; + defaultDataPoint: DoughnutDataPoint; + metaExtensions: DoughnutMetaExtensions; + parsedDataType: number; + scales: keyof CartesianScaleTypeRegistry; + }; + polarArea: { + chartOptions: PolarAreaControllerChartOptions; + datasetOptions: PolarAreaControllerDatasetOptions; + defaultDataPoint: number; + metaExtensions: {}; + parsedDataType: RadialParsedData; + scales: keyof RadialScaleTypeRegistry; + }; + radar: { + chartOptions: RadarControllerChartOptions; + datasetOptions: RadarControllerDatasetOptions & FillerControllerDatasetOptions; + defaultDataPoint: number | null; + metaExtensions: {}; + parsedDataType: RadialParsedData; + scales: keyof RadialScaleTypeRegistry; + }; +} + +export type ChartType = keyof ChartTypeRegistry; + +export type ScaleOptionsByType = + { [key in ScaleType]: { type: key } & ScaleTypeRegistry[key]['options'] }[TScale] +; + +// Convenience alias for creating and manipulating scale options in user code +export type ScaleOptions = DeepPartial>; + +export type DatasetChartOptions = { + [key in TType]: { + datasets: ChartTypeRegistry[key]['datasetOptions']; + }; +}; + +export type ScaleChartOptions = { + scales: { + [key: string]: ScaleOptionsByType; + }; +}; + +export type ChartOptions = Exclude< +DeepPartial< +CoreChartOptions & +ElementChartOptions & +PluginChartOptions & +DatasetChartOptions & +ScaleChartOptions & +ChartTypeRegistry[TType]['chartOptions'] +>, +DeepPartial +>; + +export type DefaultDataPoint = DistributiveArray; + +export type ParsedDataType = ChartTypeRegistry[TType]['parsedDataType']; + +export interface ChartDatasetProperties { + type?: TType; + data: TData; +} + +export interface ChartDatasetPropertiesCustomTypesPerDataset { + type: TType; + data: TData; +} + +export type ChartDataset< + TType extends ChartType = ChartType, + TData = DefaultDataPoint +> = DeepPartial< +{ [key in ChartType]: { type: key } & ChartTypeRegistry[key]['datasetOptions'] }[TType] +> & DeepPartial< +PluginDatasetOptionsByType +> & ChartDatasetProperties; + +export type ChartDatasetCustomTypesPerDataset< + TType extends ChartType = ChartType, + TData = DefaultDataPoint +> = DeepPartial< +{ [key in ChartType]: { type: key } & ChartTypeRegistry[key]['datasetOptions'] }[TType] +> & DeepPartial< +PluginDatasetOptionsByType +> & ChartDatasetPropertiesCustomTypesPerDataset; + +/** + * TData represents the data point type. If unspecified, a default is provided + * based on the chart type. + * TLabel represents the label type + */ +export interface ChartData< + TType extends ChartType = ChartType, + TData = DefaultDataPoint, + TLabel = unknown +> { + labels?: TLabel[]; + xLabels?: TLabel[]; + yLabels?: TLabel[]; + datasets: ChartDataset[]; +} + +export interface ChartDataCustomTypesPerDataset< + TType extends ChartType = ChartType, + TData = DefaultDataPoint, + TLabel = unknown +> { + labels?: TLabel[]; + xLabels?: TLabel[]; + yLabels?: TLabel[]; + datasets: ChartDatasetCustomTypesPerDataset[]; +} + +export interface ChartConfiguration< + TType extends ChartType = ChartType, + TData = DefaultDataPoint, + TLabel = unknown +> { + type: TType; + data: ChartData; + options?: ChartOptions | undefined; + plugins?: Plugin[]; + platform?: typeof BasePlatform; +} + +export interface ChartConfigurationCustomTypesPerDataset< + TType extends ChartType = ChartType, + TData = DefaultDataPoint, + TLabel = unknown +> { + data: ChartDataCustomTypesPerDataset; + options?: ChartOptions | undefined; + plugins?: Plugin[]; +} diff --git a/src/types/layout.d.ts b/src/types/layout.d.ts new file mode 100644 index 00000000000..39ddc1394b7 --- /dev/null +++ b/src/types/layout.d.ts @@ -0,0 +1,65 @@ +import {ChartArea} from './geometric.js'; + +export type LayoutPosition = 'left' | 'top' | 'right' | 'bottom' | 'center' | 'chartArea' | {[scaleId: string]: number}; + +export interface LayoutItem { + /** + * The position of the item in the chart layout. Possible values are + */ + position: LayoutPosition; + /** + * The weight used to sort the item. Higher weights are further away from the chart area + */ + weight: number; + /** + * if true, and the item is horizontal, then push vertical boxes down + */ + fullSize: boolean; + /** + * Width of item. Must be valid after update() + */ + width: number; + /** + * Height of item. Must be valid after update() + */ + height: number; + /** + * Left edge of the item. Set by layout system and cannot be used in update + */ + left: number; + /** + * Top edge of the item. Set by layout system and cannot be used in update + */ + top: number; + /** + * Right edge of the item. Set by layout system and cannot be used in update + */ + right: number; + /** + * Bottom edge of the item. Set by layout system and cannot be used in update + */ + bottom: number; + + /** + * Called before the layout process starts + */ + beforeLayout?(): void; + /** + * Draws the element + */ + draw(chartArea: ChartArea): void; + /** + * Returns an object with padding on the edges + */ + getPadding?(): ChartArea; + /** + * returns true if the layout item is horizontal (ie. top or bottom) + */ + isHorizontal(): boolean; + /** + * Takes two parameters: width and height. + * @param width + * @param height + */ + update(width: number, height: number, margins?: ChartArea): void; +} diff --git a/src/types/utils.d.ts b/src/types/utils.d.ts new file mode 100644 index 00000000000..17b1cbd9f77 --- /dev/null +++ b/src/types/utils.d.ts @@ -0,0 +1,30 @@ +/* eslint-disable @typescript-eslint/ban-types */ + +// DeepPartial implementation taken from the utility-types NPM package, which is +// Copyright (c) 2016 Piotr Witek (http://piotrwitek.github.io) +// and used under the terms of the MIT license +export type DeepPartial = T extends Function + ? T + : T extends Array + ? _DeepPartialArray + : T extends object + ? _DeepPartialObject + : T | undefined; + +type _DeepPartialArray = Array> +type _DeepPartialObject = { [P in keyof T]?: DeepPartial }; + +export type DistributiveArray = [T] extends [unknown] ? Array : never + +// https://stackoverflow.com/a/50375286 +export type UnionToIntersection = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never; + +export type AllKeys = T extends any ? keyof T : never; + +export type PickType> = T extends { [k in K]?: any } + ? T[K] + : undefined; + +export type Merge = { + [k in AllKeys]: PickType; +}; diff --git a/test/.eslintrc.yml b/test/.eslintrc.yml new file mode 100644 index 00000000000..a35c54a0ecc --- /dev/null +++ b/test/.eslintrc.yml @@ -0,0 +1,12 @@ +env: + jasmine: true + +globals: + acquireChart: true + afterEvent: true + Chart: true + moment: true + waitForResize: true + +rules: + max-statements: ["warn", 50] diff --git a/test/BasicChartWebWorker.js b/test/BasicChartWebWorker.js new file mode 100644 index 00000000000..7a7bd1dc4da --- /dev/null +++ b/test/BasicChartWebWorker.js @@ -0,0 +1,27 @@ +// This file is a basic example of using a chart inside a web worker. +// All it creates a new chart from a transferred OffscreenCanvas and then assert that the correct platform type was +// used. + +// Receives messages with data of type: { type: 'initialize', canvas: OffscreenCanvas } +// Sends messages with data of types: { type: 'success' } | { type: 'error', errorMessage: string } + +// eslint-disable-next-line no-undef +importScripts('../src/chart.umd.min.js'); + +onmessage = function(event) { + try { + const {type, canvas} = event.data; + if (type !== 'initialize') { + throw new Error('invalid message type received by worker: ' + type); + } + + const chart = new Chart(canvas); + if (!(chart.platform instanceof Chart.platforms.BasicPlatform)) { + throw new Error('did not use basic platform for chart in web worker'); + } + + postMessage({type: 'success'}); + } catch (error) { + postMessage({type: 'error', errorMessage: error.stack}); + } +}; diff --git a/test/fixtures/controller.bar/aligned-pixels.js b/test/fixtures/controller.bar/aligned-pixels.js new file mode 100644 index 00000000000..e0ddd37bc89 --- /dev/null +++ b/test/fixtures/controller.bar/aligned-pixels.js @@ -0,0 +1,29 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: ['a'], + datasets: [{ + data: [-1] + }, { + data: [1] + }] + }, + options: { + indexAxis: 'y', + events: [], + backgroundColor: 'navy', + devicePixelRatio: 1.25, + scales: { + x: {display: false, alignToPixels: true}, + y: {display: false, stacked: true} + } + } + }, + options: { + canvas: { + width: 100, + height: 500 + } + } +}; diff --git a/test/fixtures/controller.bar/aligned-pixels.png b/test/fixtures/controller.bar/aligned-pixels.png new file mode 100644 index 00000000000..d261d2f22fc Binary files /dev/null and b/test/fixtures/controller.bar/aligned-pixels.png differ diff --git a/test/fixtures/controller.bar/backgroundColor/indexable.js b/test/fixtures/controller.bar/backgroundColor/indexable.js new file mode 100644 index 00000000000..ca02427e5b5 --- /dev/null +++ b/test/fixtures/controller.bar/backgroundColor/indexable.js @@ -0,0 +1,50 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + backgroundColor: [ + '#ff0000', + '#00ff00', + '#0000ff', + '#ffff00', + '#ff00ff', + '#000000' + ] + }, + { + // option in element (fallback) + data: [0, 5, 10, null, -10, -5], + } + ] + }, + options: { + elements: { + bar: { + backgroundColor: [ + '#ff88ff', + '#888888', + '#ff8800', + '#00ff88', + '#8800ff', + '#ffff88' + ] + } + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/backgroundColor/indexable.png b/test/fixtures/controller.bar/backgroundColor/indexable.png new file mode 100644 index 00000000000..eecc4ac6df2 Binary files /dev/null and b/test/fixtures/controller.bar/backgroundColor/indexable.png differ diff --git a/test/fixtures/controller.bar/backgroundColor/loopable.js b/test/fixtures/controller.bar/backgroundColor/loopable.js new file mode 100644 index 00000000000..345b85038b4 --- /dev/null +++ b/test/fixtures/controller.bar/backgroundColor/loopable.js @@ -0,0 +1,43 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 2, 3, 4, 5, 6], + backgroundColor: [ + '#ff0000', + '#00ff00', + '#0000ff' + ] + }, + { + // option in element (fallback) + data: [6, 5, 4, 3, 2, 1], + } + ] + }, + options: { + elements: { + bar: { + backgroundColor: [ + '#000000', + '#888888' + ] + } + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/backgroundColor/loopable.png b/test/fixtures/controller.bar/backgroundColor/loopable.png new file mode 100644 index 00000000000..ea13b696d53 Binary files /dev/null and b/test/fixtures/controller.bar/backgroundColor/loopable.png differ diff --git a/test/fixtures/controller.bar/backgroundColor/scriptable.js b/test/fixtures/controller.bar/backgroundColor/scriptable.js new file mode 100644 index 00000000000..97b0829df28 --- /dev/null +++ b/test/fixtures/controller.bar/backgroundColor/scriptable.js @@ -0,0 +1,51 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + backgroundColor: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 8 ? '#ff0000' + : value > 0 ? '#00ff00' + : value > -8 ? '#0000ff' + : '#ff00ff'; + } + }, + { + // option in element (fallback) + data: [0, 5, 10, null, -10, -5] + } + ] + }, + options: { + elements: { + bar: { + backgroundColor: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 8 ? '#ff00ff' + : value > 0 ? '#0000ff' + : value > -8 ? '#ff0000' + : '#00ff00'; + } + } + }, + scales: { + x: {display: false}, + y: { + display: false, + beginAtZero: true + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/backgroundColor/scriptable.png b/test/fixtures/controller.bar/backgroundColor/scriptable.png new file mode 100644 index 00000000000..e78839af1d8 Binary files /dev/null and b/test/fixtures/controller.bar/backgroundColor/scriptable.png differ diff --git a/test/fixtures/controller.bar/backgroundColor/value.js b/test/fixtures/controller.bar/backgroundColor/value.js new file mode 100644 index 00000000000..0979ae32b34 --- /dev/null +++ b/test/fixtures/controller.bar/backgroundColor/value.js @@ -0,0 +1,36 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + backgroundColor: '#ff0000' + }, + { + // option in element (fallback) + data: [0, 5, 10, null, -10, -5], + } + ] + }, + options: { + elements: { + bar: { + backgroundColor: '#00ff00' + } + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/backgroundColor/value.png b/test/fixtures/controller.bar/backgroundColor/value.png new file mode 100644 index 00000000000..4885e108607 Binary files /dev/null and b/test/fixtures/controller.bar/backgroundColor/value.png differ diff --git a/test/fixtures/controller.bar/bar-animation-hide-show.js b/test/fixtures/controller.bar/bar-animation-hide-show.js new file mode 100644 index 00000000000..d559e8ae46d --- /dev/null +++ b/test/fixtures/controller.bar/bar-animation-hide-show.js @@ -0,0 +1,84 @@ +const canvas = document.createElement('canvas'); +canvas.width = 512; +canvas.height = 512; +const ctx = canvas.getContext('2d'); + +module.exports = { + config: { + type: 'bar', + data: { + labels: [0], + datasets: [ + { + data: [1], + backgroundColor: 'rgba(255,0,0,0.5)' + }, + { + data: [2], + backgroundColor: 'rgba(0,0,255,0.5)' + }, + { + data: [3], + backgroundColor: 'rgba(0,255,0,0.5)' + } + ] + }, + options: { + animation: { + duration: 14000, + easing: 'linear' + }, + events: [], + scales: { + x: {display: false}, + y: {display: false, max: 4} + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + }, + run: function(chart) { + const animator = Chart.animator; + const anims = animator._getAnims(chart); + // disable animator + const backup = animator._refresh; + animator._refresh = function() { }; + + return new Promise((resolve) => { + window.requestAnimationFrame(() => { + // make sure previous animation is finished + animator._update(Date.now() * 2); + + chart.hide(1); + let start = anims.items[0]._start; + for (let i = 0; i < 8; i++) { + animator._update(start + i * 2000); + let x = i % 4 * 128; + let y = Math.floor(i / 4) * 128; + ctx.drawImage(chart.canvas, x, y, 128, 128); + } + + // make sure previous animation is finished + animator._update(Date.now() * 2); + + chart.show(1); + start = anims.items[0]._start; + for (let i = 0; i < 8; i++) { + animator._update(start + i * 2000); + let x = i % 4 * 128; + let y = Math.floor(2 + i / 4) * 128; + ctx.drawImage(chart.canvas, x, y, 128, 128); + } + Chart.helpers.clearCanvas(chart.canvas); + chart.ctx.drawImage(canvas, 0, 0); + + animator._refresh = backup; + resolve(); + }); + }); + } + } +}; diff --git a/test/fixtures/controller.bar/bar-animation-hide-show.png b/test/fixtures/controller.bar/bar-animation-hide-show.png new file mode 100644 index 00000000000..924e1d239fb Binary files /dev/null and b/test/fixtures/controller.bar/bar-animation-hide-show.png differ diff --git a/test/fixtures/controller.bar/bar-base-value.js b/test/fixtures/controller.bar/bar-base-value.js new file mode 100644 index 00000000000..3f576f1a7c4 --- /dev/null +++ b/test/fixtures/controller.bar/bar-base-value.js @@ -0,0 +1,29 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 3, 4], + datasets: [ + { + data: [5, 20, 10, 11], + base: 10, + backgroundColor: '#00ff00', + borderColor: '#ff0000', + borderWidth: 2, + } + ] + }, + options: { + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/bar-base-value.png b/test/fixtures/controller.bar/bar-base-value.png new file mode 100644 index 00000000000..98c6797511e Binary files /dev/null and b/test/fixtures/controller.bar/bar-base-value.png differ diff --git a/test/fixtures/controller.bar/bar-default-begin-at-zero.js b/test/fixtures/controller.bar/bar-default-begin-at-zero.js new file mode 100644 index 00000000000..1f2b4b8afea --- /dev/null +++ b/test/fixtures/controller.bar/bar-default-begin-at-zero.js @@ -0,0 +1,27 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 3, 4], + datasets: [ + { + data: [5, 20, 1, 10], + backgroundColor: '#00ff00', + borderColor: '#ff0000' + } + ] + }, + options: { + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/bar-default-begin-at-zero.png b/test/fixtures/controller.bar/bar-default-begin-at-zero.png new file mode 100644 index 00000000000..4ec05955cb6 Binary files /dev/null and b/test/fixtures/controller.bar/bar-default-begin-at-zero.png differ diff --git a/test/fixtures/controller.bar/bar-thickness-absolute.json b/test/fixtures/controller.bar/bar-thickness-absolute.json new file mode 100644 index 00000000000..c53d976418a --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-absolute.json @@ -0,0 +1,41 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2017", "2018", "2019", "2024", "2025"], + "datasets": [{ + "backgroundColor": "rgba(255, 99, 132, 0.5)", + "barPercentage": 1, + "categoryPercentage": 1, + "barThickness": 128, + "data": [1, null, 3, 4, 5] + }] + }, + "options": { + "responsive": false, + "scales": { + "x": { + "type": "time", + "offset": true, + "display": false, + "time": { + "parser": "YYYY" + }, + "ticks": { + "source": "labels" + } + }, + "y": { + "display": false, + "beginAtZero": true + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-absolute.png b/test/fixtures/controller.bar/bar-thickness-absolute.png new file mode 100644 index 00000000000..40172b39241 Binary files /dev/null and b/test/fixtures/controller.bar/bar-thickness-absolute.png differ diff --git a/test/fixtures/controller.bar/bar-thickness-flex-offset.json b/test/fixtures/controller.bar/bar-thickness-flex-offset.json new file mode 100644 index 00000000000..c4c9e894cba --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-flex-offset.json @@ -0,0 +1,41 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2017", "2018", "2020", "2024", "2038"], + "datasets": [{ + "backgroundColor": "#FF6384", + "barPercentage": 1, + "categoryPercentage": 1, + "barThickness": "flex", + "data": [1, null, 3, 4, 5] + }] + }, + "options": { + "responsive": false, + "scales": { + "x": { + "type": "time", + "offset": true, + "display": false, + "time": { + "parser": "YYYY" + }, + "ticks": { + "source": "labels" + } + }, + "y": { + "display": false, + "beginAtZero": true + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-flex-offset.png b/test/fixtures/controller.bar/bar-thickness-flex-offset.png new file mode 100644 index 00000000000..14491751df7 Binary files /dev/null and b/test/fixtures/controller.bar/bar-thickness-flex-offset.png differ diff --git a/test/fixtures/controller.bar/bar-thickness-flex-single-reverse.json b/test/fixtures/controller.bar/bar-thickness-flex-single-reverse.json new file mode 100644 index 00000000000..a463037f3b1 --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-flex-single-reverse.json @@ -0,0 +1,42 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2016", "2018", "2020", "2024", "2030"], + "datasets": [{ + "backgroundColor": "#FF6384", + "barThickness": "flex", + "barPercentage": 1, + "categoryPercentage": 1, + "data": [1] + }] + }, + "options": { + "responsive": false, + "scales": { + "x": { + "type": "time", + "display": false, + "offset": false, + "time": { + "parser": "YYYY" + }, + "reverse": true, + "ticks": { + "source": "labels" + } + }, + "y": { + "display": false, + "beginAtZero": true + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-flex-single-reverse.png b/test/fixtures/controller.bar/bar-thickness-flex-single-reverse.png new file mode 100644 index 00000000000..25a19957b0a Binary files /dev/null and b/test/fixtures/controller.bar/bar-thickness-flex-single-reverse.png differ diff --git a/test/fixtures/controller.bar/bar-thickness-flex-single.json b/test/fixtures/controller.bar/bar-thickness-flex-single.json new file mode 100644 index 00000000000..8c3be0fd123 --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-flex-single.json @@ -0,0 +1,41 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2016", "2018", "2020", "2024", "2030"], + "datasets": [{ + "backgroundColor": "#FF6384", + "barThickness": "flex", + "barPercentage": 1, + "categoryPercentage": 1, + "data": [1] + }] + }, + "options": { + "responsive": false, + "scales": { + "x": { + "type": "time", + "display": false, + "offset": false, + "time": { + "parser": "YYYY" + }, + "ticks": { + "source": "labels" + } + }, + "y": { + "display": false, + "beginAtZero": true + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-flex-single.png b/test/fixtures/controller.bar/bar-thickness-flex-single.png new file mode 100644 index 00000000000..520accf0469 Binary files /dev/null and b/test/fixtures/controller.bar/bar-thickness-flex-single.png differ diff --git a/test/fixtures/controller.bar/bar-thickness-flex.json b/test/fixtures/controller.bar/bar-thickness-flex.json new file mode 100644 index 00000000000..d81cb1b35ea --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-flex.json @@ -0,0 +1,41 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2017", "2018", "2020", "2024", "2038"], + "datasets": [{ + "backgroundColor": "#FF6384", + "barPercentage": 1, + "categoryPercentage": 1, + "barThickness": "flex", + "data": [1, null, 3, 4, 5] + }] + }, + "options": { + "responsive": false, + "scales": { + "x": { + "type": "time", + "display": false, + "offset": false, + "time": { + "parser": "YYYY" + }, + "ticks": { + "source": "labels" + } + }, + "y": { + "display": false, + "beginAtZero": true + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-flex.png b/test/fixtures/controller.bar/bar-thickness-flex.png new file mode 100644 index 00000000000..62fb2307db9 Binary files /dev/null and b/test/fixtures/controller.bar/bar-thickness-flex.png differ diff --git a/test/fixtures/controller.bar/bar-thickness-max.json b/test/fixtures/controller.bar/bar-thickness-max.json new file mode 100644 index 00000000000..65575a0cc34 --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-max.json @@ -0,0 +1,41 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2016", "2018", "2020", "2024", "2030"], + "datasets": [{ + "backgroundColor": "#FF6384", + "barPercentage": 1, + "categoryPercentage": 1, + "maxBarThickness": 8, + "data": [1, null, 3, 4, 5] + }] + }, + "options": { + "responsive": false, + "scales": { + "x": { + "type": "time", + "display": false, + "offset": false, + "time": { + "parser": "YYYY" + }, + "ticks": { + "source": "labels" + } + }, + "y": { + "display": false, + "beginAtZero": true + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-max.png b/test/fixtures/controller.bar/bar-thickness-max.png new file mode 100644 index 00000000000..e22867dbd01 Binary files /dev/null and b/test/fixtures/controller.bar/bar-thickness-max.png differ diff --git a/test/fixtures/controller.bar/bar-thickness-min-interval-multi.json b/test/fixtures/controller.bar/bar-thickness-min-interval-multi.json new file mode 100644 index 00000000000..7d59b72e857 --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-min-interval-multi.json @@ -0,0 +1,42 @@ +{ + "config": { + "type": "bar", + "data": { + "datasets": [{ + "backgroundColor": "#FF6384", + "barPercentage": 1, + "categoryPercentage": 1, + "data": [{"x": "2001", "y": 1}, {"x": "2099", "y": 5}] + }, { + "backgroundColor": "#8463FF", + "barPercentage": 1, + "categoryPercentage": 1, + "data": [{"x": "2019", "y": 2}, {"x": "2020", "y": 3}] + }] + }, + "options": { + "responsive": false, + "scales": { + "x": { + "type": "time", + "display": false, + "min": "2000", + "max": "2100", + "time": { + "parser": "YYYY" + } + }, + "y": { + "display": false, + "beginAtZero": true + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-min-interval-multi.png b/test/fixtures/controller.bar/bar-thickness-min-interval-multi.png new file mode 100644 index 00000000000..01ad1fa1614 Binary files /dev/null and b/test/fixtures/controller.bar/bar-thickness-min-interval-multi.png differ diff --git a/test/fixtures/controller.bar/bar-thickness-min-interval.json b/test/fixtures/controller.bar/bar-thickness-min-interval.json new file mode 100644 index 00000000000..887ddc1c237 --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-min-interval.json @@ -0,0 +1,40 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2016", "2018", "2020", "2024", "2030"], + "datasets": [{ + "backgroundColor": "#FF6384", + "barPercentage": 1, + "categoryPercentage": 1, + "data": [1, null, 3, 4, 5] + }] + }, + "options": { + "responsive": false, + "scales": { + "x": { + "type": "time", + "display": false, + "offset": false, + "time": { + "parser": "YYYY" + }, + "ticks": { + "source": "labels" + } + }, + "y": { + "display": false, + "beginAtZero": true + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-min-interval.png b/test/fixtures/controller.bar/bar-thickness-min-interval.png new file mode 100644 index 00000000000..ae01a9b945c Binary files /dev/null and b/test/fixtures/controller.bar/bar-thickness-min-interval.png differ diff --git a/test/fixtures/controller.bar/bar-thickness-multiple.json b/test/fixtures/controller.bar/bar-thickness-multiple.json new file mode 100644 index 00000000000..d9e77a28df9 --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-multiple.json @@ -0,0 +1,50 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2016", "2018", "2020", "2024", "2030"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [1, null, 3, 4, 5] + }, { + "backgroundColor": "#36A2EB", + "data": [5, 4, 3, null, 1] + }, { + "backgroundColor": "#FFCE56", + "data": [3, 5, 2, null, 4] + }] + }, + "options": { + "responsive": false, + "datasets": { + "bar": { + "barPercentage": 1, + "categoryPercentage": 1 + } + }, + "scales": { + "x": { + "type": "time", + "display": false, + "offset": false, + "time": { + "parser": "YYYY" + }, + "ticks": { + "source": "labels" + } + }, + "y": { + "display": false, + "beginAtZero": true + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-multiple.png b/test/fixtures/controller.bar/bar-thickness-multiple.png new file mode 100644 index 00000000000..d38405292c3 Binary files /dev/null and b/test/fixtures/controller.bar/bar-thickness-multiple.png differ diff --git a/test/fixtures/controller.bar/bar-thickness-no-overlap.json b/test/fixtures/controller.bar/bar-thickness-no-overlap.json new file mode 100644 index 00000000000..c3e21c04dca --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-no-overlap.json @@ -0,0 +1,50 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2016", "2018", "2020", "2024", "2030"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [ + {"y": "1", "x": "2016"}, + {"y": "2", "x": "2017"}, + {"y": "3", "x": "2017-08"}, + {"y": "4", "x": "2024"}, + {"y": "5", "x": "2030"} + ] + }] + }, + "options": { + "responsive": false, + "datasets": { + "bar": { + "barPercentage": 1, + "categoryPercentage": 1 + } + }, + "scales": { + "x": { + "type": "time", + "display": false, + "offset": false, + "time": { + "parser": "YYYY-MM" + }, + "ticks": { + "source": "labels" + } + }, + "y": { + "display": false, + "beginAtZero": true + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-no-overlap.png b/test/fixtures/controller.bar/bar-thickness-no-overlap.png new file mode 100644 index 00000000000..1385a0853e8 Binary files /dev/null and b/test/fixtures/controller.bar/bar-thickness-no-overlap.png differ diff --git a/test/fixtures/controller.bar/bar-thickness-offset.json b/test/fixtures/controller.bar/bar-thickness-offset.json new file mode 100644 index 00000000000..7fee6650984 --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-offset.json @@ -0,0 +1,50 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2016", "2018", "2020", "2024", "2030"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [1, null, 3, 4, 5] + }, { + "backgroundColor": "#36A2EB", + "data": [5, 4, 3, null, 1] + }, { + "backgroundColor": "#FFCE56", + "data": [3, 5, 2, null, 4] + }] + }, + "options": { + "responsive": false, + "datasets": { + "bar": { + "barPercentage": 1, + "categoryPercentage": 1 + } + }, + "scales": { + "x": { + "type": "time", + "offset": true, + "display": false, + "time": { + "parser": "YYYY" + }, + "ticks": { + "source": "labels" + } + }, + "y": { + "display": false, + "beginAtZero": true + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-offset.png b/test/fixtures/controller.bar/bar-thickness-offset.png new file mode 100644 index 00000000000..6b35e925708 Binary files /dev/null and b/test/fixtures/controller.bar/bar-thickness-offset.png differ diff --git a/test/fixtures/controller.bar/bar-thickness-per-dataset-stacked.json b/test/fixtures/controller.bar/bar-thickness-per-dataset-stacked.json new file mode 100644 index 00000000000..64bd90964a3 --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-per-dataset-stacked.json @@ -0,0 +1,51 @@ +{ + "config": { + "data": { + "labels": ["2016", "2018", "2020", "2024", "2030"], + "datasets": [{ + "type": "bar", + "barThickness": 16, + "backgroundColor": "#FF6384", + "data": [1, null, 3, 4, 5] + }, { + "type": "bar", + "barThickness": 8, + "backgroundColor": "#36A2EB", + "data": [5, 4, 3, null, 1] + }, { + "type": "bar", + "barThickness": 4, + "backgroundColor": "#FFCE56", + "data": [3, 5, 2, null, 4] + }] + }, + "options": { + "responsive": false, + "scales": { + "x": { + "type": "time", + "offset": true, + "stacked": true, + "display": false, + "time": { + "parser": "YYYY" + }, + "ticks": { + "source": "labels" + } + }, + "y": { + "display": false, + "stacked": true, + "beginAtZero": true + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-per-dataset-stacked.png b/test/fixtures/controller.bar/bar-thickness-per-dataset-stacked.png new file mode 100644 index 00000000000..5e6f1538ee0 Binary files /dev/null and b/test/fixtures/controller.bar/bar-thickness-per-dataset-stacked.png differ diff --git a/test/fixtures/controller.bar/bar-thickness-per-dataset.json b/test/fixtures/controller.bar/bar-thickness-per-dataset.json new file mode 100644 index 00000000000..cc26a65bfa2 --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-per-dataset.json @@ -0,0 +1,44 @@ +{ + "config": { + "data": { + "labels": ["2016", "2018", "2020", "2024", "2030"], + "datasets": [{ + "type": "bar", + "barThickness": 16, + "backgroundColor": "#FF6384", + "data": [1, null, 3, 4, 5] + }, { + "type": "bar", + "barThickness": 8, + "backgroundColor": "#36A2EB", + "data": [5, 4, 3, null, 1] + }] + }, + "options": { + "responsive": false, + "scales": { + "x": { + "type": "time", + "offset": true, + "display": false, + "time": { + "parser": "YYYY" + }, + "ticks": { + "source": "labels" + } + }, + "y": { + "display": false, + "beginAtZero": true + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-per-dataset.png b/test/fixtures/controller.bar/bar-thickness-per-dataset.png new file mode 100644 index 00000000000..1db187bdb95 Binary files /dev/null and b/test/fixtures/controller.bar/bar-thickness-per-dataset.png differ diff --git a/test/fixtures/controller.bar/bar-thickness-reverse.json b/test/fixtures/controller.bar/bar-thickness-reverse.json new file mode 100644 index 00000000000..eb004a27f2c --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-reverse.json @@ -0,0 +1,51 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2016", "2018", "2020", "2024", "2030"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [1, null, 3, 4, 5] + }, { + "backgroundColor": "#36A2EB", + "data": [5, 4, 3, null, 1] + }, { + "backgroundColor": "#FFCE56", + "data": [3, 5, 2, null, 4] + }] + }, + "options": { + "responsive": false, + "datasets": { + "bar": { + "barPercentage": 1, + "categoryPercentage": 1 + } + }, + "scales": { + "x": { + "type": "time", + "display": false, + "offset": false, + "time": { + "parser": "YYYY" + }, + "reverse": true, + "ticks": { + "source": "labels" + } + }, + "y": { + "display": false, + "beginAtZero": true + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-reverse.png b/test/fixtures/controller.bar/bar-thickness-reverse.png new file mode 100644 index 00000000000..0913be22e00 Binary files /dev/null and b/test/fixtures/controller.bar/bar-thickness-reverse.png differ diff --git a/test/fixtures/controller.bar/bar-thickness-single-xy.json b/test/fixtures/controller.bar/bar-thickness-single-xy.json new file mode 100644 index 00000000000..4e94cbc7063 --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-single-xy.json @@ -0,0 +1,40 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2016", "2018", "2020", "2024", "2030"], + "datasets": [{ + "barPercentage": 1, + "categoryPercentage": 1, + "backgroundColor": "#FF6384", + "data": [{"x": "2022", "y": 42}] + }] + }, + "options": { + "responsive": false, + "scales": { + "x": { + "type": "time", + "display": false, + "offset": false, + "time": { + "parser": "YYYY" + }, + "ticks": { + "source": "labels" + } + }, + "y": { + "display": false, + "beginAtZero": true + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-single-xy.png b/test/fixtures/controller.bar/bar-thickness-single-xy.png new file mode 100644 index 00000000000..26171e531a2 Binary files /dev/null and b/test/fixtures/controller.bar/bar-thickness-single-xy.png differ diff --git a/test/fixtures/controller.bar/bar-thickness-single.json b/test/fixtures/controller.bar/bar-thickness-single.json new file mode 100644 index 00000000000..0d9d01e7cdb --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-single.json @@ -0,0 +1,41 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2016", "2018", "2020", "2024", "2030"], + "datasets": [{ + "barPercentage": 1, + "categoryPercentage": 1, + "backgroundColor": "#FF6384", + "data": [1] + }] + }, + "options": { + "responsive": false, + "scales": { + "x": { + "type": "time", + "display": false, + "offset": false, + "time": { + "parser": "YYYY" + }, + "min": "2013", + "ticks": { + "source": "labels" + } + }, + "y": { + "display": false, + "beginAtZero": true + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-single.png b/test/fixtures/controller.bar/bar-thickness-single.png new file mode 100644 index 00000000000..22314319e14 Binary files /dev/null and b/test/fixtures/controller.bar/bar-thickness-single.png differ diff --git a/test/fixtures/controller.bar/bar-thickness-stacked.json b/test/fixtures/controller.bar/bar-thickness-stacked.json new file mode 100644 index 00000000000..807e964a41d --- /dev/null +++ b/test/fixtures/controller.bar/bar-thickness-stacked.json @@ -0,0 +1,52 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2016", "2018", "2020", "2024", "2030"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [1, null, 3, 4, 5] + }, { + "backgroundColor": "#36A2EB", + "data": [5, 4, 3, null, 1] + }, { + "backgroundColor": "#FFCE56", + "data": [3, 5, 2, null, 4] + }] + }, + "options": { + "responsive": false, + "datasets": { + "bar": { + "barPercentage": 1, + "categoryPercentage": 1 + } + }, + "scales": { + "x": { + "type": "time", + "stacked": true, + "display": false, + "offset": false, + "time": { + "parser": "YYYY" + }, + "ticks": { + "source": "labels" + } + }, + "y": { + "display": false, + "stacked": true, + "beginAtZero": true + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/bar-thickness-stacked.png b/test/fixtures/controller.bar/bar-thickness-stacked.png new file mode 100644 index 00000000000..7392dd57c6a Binary files /dev/null and b/test/fixtures/controller.bar/bar-thickness-stacked.png differ diff --git a/test/fixtures/controller.bar/baseLine/bottom.js b/test/fixtures/controller.bar/baseLine/bottom.js new file mode 100644 index 00000000000..272b8e99b3b --- /dev/null +++ b/test/fixtures/controller.bar/baseLine/bottom.js @@ -0,0 +1,40 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: ['a', 'b'], + datasets: [{ + backgroundColor: '#AAFFCC', + borderColor: '#0000FF', + borderWidth: 1, + data: [1, 2] + }] + }, + options: { + scales: { + x: { + display: false + }, + y: { + ticks: { + display: false + }, + grid: { + color: function(context) { + return context.tick.value === 0 ? 'red' : 'transparent'; + }, + lineWidth: 5, + tickLength: 0 + }, + } + }, + maintainAspectRatio: false + } + }, + options: { + canvas: { + width: 128, + height: 128 + } + } +}; diff --git a/test/fixtures/controller.bar/baseLine/bottom.png b/test/fixtures/controller.bar/baseLine/bottom.png new file mode 100644 index 00000000000..c689dd3c689 Binary files /dev/null and b/test/fixtures/controller.bar/baseLine/bottom.png differ diff --git a/test/fixtures/controller.bar/baseLine/left.js b/test/fixtures/controller.bar/baseLine/left.js new file mode 100644 index 00000000000..56a68ce5479 --- /dev/null +++ b/test/fixtures/controller.bar/baseLine/left.js @@ -0,0 +1,41 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: ['a', 'b'], + datasets: [{ + backgroundColor: '#AAFFCC', + borderColor: '#0000FF', + borderWidth: 1, + data: [1, 2] + }] + }, + options: { + indexAxis: 'y', + scales: { + y: { + display: false + }, + x: { + ticks: { + display: false + }, + grid: { + color: function(context) { + return context.tick.value === 0 ? 'red' : 'transparent'; + }, + lineWidth: 5, + tickLength: 0 + }, + } + }, + maintainAspectRatio: false + } + }, + options: { + canvas: { + width: 128, + height: 128 + } + } +}; diff --git a/test/fixtures/controller.bar/baseLine/left.png b/test/fixtures/controller.bar/baseLine/left.png new file mode 100644 index 00000000000..340f71e7e86 Binary files /dev/null and b/test/fixtures/controller.bar/baseLine/left.png differ diff --git a/test/fixtures/controller.bar/baseLine/mid-x.js b/test/fixtures/controller.bar/baseLine/mid-x.js new file mode 100644 index 00000000000..ef6beb5c97b --- /dev/null +++ b/test/fixtures/controller.bar/baseLine/mid-x.js @@ -0,0 +1,41 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: ['a', 'b'], + datasets: [{ + backgroundColor: '#AAFFCC', + borderColor: '#0000FF', + borderWidth: 1, + data: [1, -1] + }] + }, + options: { + indexAxis: 'y', + scales: { + y: { + display: false + }, + x: { + ticks: { + display: false + }, + grid: { + color: function(context) { + return context.tick.value === 0 ? 'red' : 'transparent'; + }, + lineWidth: 5, + tickLength: 0 + }, + } + }, + maintainAspectRatio: false + } + }, + options: { + canvas: { + width: 128, + height: 128 + } + } +}; diff --git a/test/fixtures/controller.bar/baseLine/mid-x.png b/test/fixtures/controller.bar/baseLine/mid-x.png new file mode 100644 index 00000000000..e12c967d25a Binary files /dev/null and b/test/fixtures/controller.bar/baseLine/mid-x.png differ diff --git a/test/fixtures/controller.bar/baseLine/mid-y.js b/test/fixtures/controller.bar/baseLine/mid-y.js new file mode 100644 index 00000000000..d01c00a12d8 --- /dev/null +++ b/test/fixtures/controller.bar/baseLine/mid-y.js @@ -0,0 +1,40 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: ['a', 'b'], + datasets: [{ + backgroundColor: '#AAFFCC', + borderColor: '#0000FF', + borderWidth: 1, + data: [1, -1] + }] + }, + options: { + scales: { + x: { + display: false + }, + y: { + ticks: { + display: false + }, + grid: { + color: function(context) { + return context.tick.value === 0 ? 'red' : 'transparent'; + }, + lineWidth: 5, + tickLength: 0 + }, + } + }, + maintainAspectRatio: false + } + }, + options: { + canvas: { + width: 128, + height: 128 + } + } +}; diff --git a/test/fixtures/controller.bar/baseLine/mid-y.png b/test/fixtures/controller.bar/baseLine/mid-y.png new file mode 100644 index 00000000000..eca057a6366 Binary files /dev/null and b/test/fixtures/controller.bar/baseLine/mid-y.png differ diff --git a/test/fixtures/controller.bar/baseLine/right.js b/test/fixtures/controller.bar/baseLine/right.js new file mode 100644 index 00000000000..1fc069f1f68 --- /dev/null +++ b/test/fixtures/controller.bar/baseLine/right.js @@ -0,0 +1,42 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: ['a', 'b'], + datasets: [{ + backgroundColor: '#AAFFCC', + borderColor: '#0000FF', + borderWidth: 1, + data: [-1, -2] + }] + }, + options: { + indexAxis: 'y', + scales: { + y: { + display: false + }, + x: { + ticks: { + display: false + }, + grid: { + color: function(context) { + return context.tick.value === 0 ? 'red' : 'transparent'; + }, + lineWidth: 5, + tickLength: 0, + borderWidth: 0 + }, + } + }, + maintainAspectRatio: false + } + }, + options: { + canvas: { + width: 128, + height: 128 + } + } +}; diff --git a/test/fixtures/controller.bar/baseLine/right.png b/test/fixtures/controller.bar/baseLine/right.png new file mode 100644 index 00000000000..9de9a9e58ba Binary files /dev/null and b/test/fixtures/controller.bar/baseLine/right.png differ diff --git a/test/fixtures/controller.bar/baseLine/top.js b/test/fixtures/controller.bar/baseLine/top.js new file mode 100644 index 00000000000..86a7a378d1e --- /dev/null +++ b/test/fixtures/controller.bar/baseLine/top.js @@ -0,0 +1,41 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: ['a', 'b'], + datasets: [{ + backgroundColor: '#AAFFCC', + borderColor: '#0000FF', + borderWidth: 1, + data: [-1, -2] + }] + }, + options: { + scales: { + x: { + display: false + }, + y: { + ticks: { + display: false + }, + grid: { + color: function(context) { + return context.tick.value === 0 ? 'red' : 'transparent'; + }, + borderWidth: 0, + lineWidth: 5, + tickLength: 0 + }, + } + }, + maintainAspectRatio: false + } + }, + options: { + canvas: { + width: 128, + height: 128 + } + } +}; diff --git a/test/fixtures/controller.bar/baseLine/top.png b/test/fixtures/controller.bar/baseLine/top.png new file mode 100644 index 00000000000..efe34e9186f Binary files /dev/null and b/test/fixtures/controller.bar/baseLine/top.png differ diff --git a/test/fixtures/controller.bar/baseLine/value-x.js b/test/fixtures/controller.bar/baseLine/value-x.js new file mode 100644 index 00000000000..557a8986385 --- /dev/null +++ b/test/fixtures/controller.bar/baseLine/value-x.js @@ -0,0 +1,42 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: ['a', 'b'], + datasets: [{ + backgroundColor: '#AAFFCC', + borderColor: '#0000FF', + borderWidth: 1, + data: [1, 3] + }] + }, + options: { + base: 2, + indexAxis: 'y', + scales: { + y: { + display: false + }, + x: { + ticks: { + display: false + }, + grid: { + color: function(context) { + return context.tick.value === 2 ? 'red' : 'transparent'; + }, + lineWidth: 5, + tickLength: 0 + }, + } + }, + maintainAspectRatio: false + } + }, + options: { + canvas: { + width: 128, + height: 128 + } + } +}; diff --git a/test/fixtures/controller.bar/baseLine/value-x.png b/test/fixtures/controller.bar/baseLine/value-x.png new file mode 100644 index 00000000000..bb8407e9396 Binary files /dev/null and b/test/fixtures/controller.bar/baseLine/value-x.png differ diff --git a/test/fixtures/controller.bar/baseLine/value-y.js b/test/fixtures/controller.bar/baseLine/value-y.js new file mode 100644 index 00000000000..caee084df6b --- /dev/null +++ b/test/fixtures/controller.bar/baseLine/value-y.js @@ -0,0 +1,41 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: ['a', 'b'], + datasets: [{ + backgroundColor: '#AAFFCC', + borderColor: '#0000FF', + borderWidth: 1, + data: [1, 3] + }] + }, + options: { + base: 2, + scales: { + x: { + display: false + }, + y: { + ticks: { + display: false + }, + grid: { + color: function(context) { + return context.tick.value === 2 ? 'red' : 'transparent'; + }, + lineWidth: 5, + tickLength: 0 + }, + } + }, + maintainAspectRatio: false + } + }, + options: { + canvas: { + width: 128, + height: 128 + } + } +}; diff --git a/test/fixtures/controller.bar/baseLine/value-y.png b/test/fixtures/controller.bar/baseLine/value-y.png new file mode 100644 index 00000000000..f0de922650f Binary files /dev/null and b/test/fixtures/controller.bar/baseLine/value-y.png differ diff --git a/test/fixtures/controller.bar/borderColor/border+dpr.js b/test/fixtures/controller.bar/borderColor/border+dpr.js new file mode 100644 index 00000000000..3fa0b53c017 --- /dev/null +++ b/test/fixtures/controller.bar/borderColor/border+dpr.js @@ -0,0 +1,35 @@ +module.exports = { + threshold: 0, + tolerance: 0, + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5, 6], + datasets: [ + { + // option in dataset + data: [5, 4, 3, 2, 3, 4, 5], + }, + ] + }, + options: { + events: [], + devicePixelRatio: 1.5, + barPercentage: 1, + categoryPercentage: 1, + backgroundColor: 'black', + borderColor: 'black', + borderWidth: 8, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 501 + } + } +}; diff --git a/test/fixtures/controller.bar/borderColor/border+dpr.png b/test/fixtures/controller.bar/borderColor/border+dpr.png new file mode 100644 index 00000000000..0fae394e8f9 Binary files /dev/null and b/test/fixtures/controller.bar/borderColor/border+dpr.png differ diff --git a/test/fixtures/controller.bar/borderColor/indexable.js b/test/fixtures/controller.bar/borderColor/indexable.js new file mode 100644 index 00000000000..9a0e315c382 --- /dev/null +++ b/test/fixtures/controller.bar/borderColor/indexable.js @@ -0,0 +1,52 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + borderColor: [ + '#ff0000', + '#00ff00', + '#0000ff', + '#ffff00', + '#ff00ff', + '#000000' + ] + }, + { + // option in element (fallback) + data: [0, 5, 10, null, -10, -5], + } + ] + }, + options: { + elements: { + bar: { + backgroundColor: 'transparent', + borderColor: [ + '#ff88ff', + '#888888', + '#ff8800', + '#00ff88', + '#8800ff', + '#ffff88' + ], + borderWidth: 8 + } + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/borderColor/indexable.png b/test/fixtures/controller.bar/borderColor/indexable.png new file mode 100644 index 00000000000..1a3a45cdd21 Binary files /dev/null and b/test/fixtures/controller.bar/borderColor/indexable.png differ diff --git a/test/fixtures/controller.bar/borderColor/scriptable.js b/test/fixtures/controller.bar/borderColor/scriptable.js new file mode 100644 index 00000000000..4b541ac6df2 --- /dev/null +++ b/test/fixtures/controller.bar/borderColor/scriptable.js @@ -0,0 +1,53 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + borderColor: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 8 ? '#ff0000' + : value > 0 ? '#00ff00' + : value > -8 ? '#0000ff' + : '#ff00ff'; + } + }, + { + // option in element (fallback) + data: [0, 5, 10, null, -10, -5] + } + ] + }, + options: { + elements: { + bar: { + backgroundColor: 'transparent', + borderColor: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 8 ? '#ff00ff' + : value > 0 ? '#0000ff' + : value > -8 ? '#ff0000' + : '#00ff00'; + }, + borderWidth: 8 + } + }, + scales: { + x: {display: false}, + y: { + display: false, + beginAtZero: true + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/borderColor/scriptable.png b/test/fixtures/controller.bar/borderColor/scriptable.png new file mode 100644 index 00000000000..c24a2beab8e Binary files /dev/null and b/test/fixtures/controller.bar/borderColor/scriptable.png differ diff --git a/test/fixtures/controller.bar/borderColor/value.js b/test/fixtures/controller.bar/borderColor/value.js new file mode 100644 index 00000000000..12cab8bab46 --- /dev/null +++ b/test/fixtures/controller.bar/borderColor/value.js @@ -0,0 +1,38 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + borderColor: '#ff0000' + }, + { + // option in element (fallback) + data: [0, 5, 10, null, -10, -5], + } + ] + }, + options: { + elements: { + bar: { + backgroundColor: 'transparent', + borderColor: '#00ff00', + borderWidth: 8 + } + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/borderColor/value.png b/test/fixtures/controller.bar/borderColor/value.png new file mode 100644 index 00000000000..6e170412c30 Binary files /dev/null and b/test/fixtures/controller.bar/borderColor/value.png differ diff --git a/test/fixtures/controller.bar/borderRadius/border-radius-stacked-number-mixed-chart.js b/test/fixtures/controller.bar/borderRadius/border-radius-stacked-number-mixed-chart.js new file mode 100644 index 00000000000..897037932e9 --- /dev/null +++ b/test/fixtures/controller.bar/borderRadius/border-radius-stacked-number-mixed-chart.js @@ -0,0 +1,42 @@ +module.exports = { + threshold: 0.01, + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + backgroundColor: 'red', + data: [12, 19, 12, 5, 4, 12], + }, + { + backgroundColor: 'green', + data: [12, 19, -4, 5, 8, 3], + type: 'line' + }, + { + backgroundColor: 'blue', + data: [7, 11, -12, 12, 0, -7], + } + ] + }, + options: { + elements: { + bar: { + borderRadius: Number.MAX_VALUE, + borderWidth: 2, + } + }, + scales: { + x: {display: false, stacked: true}, + y: {display: false, stacked: true} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/borderRadius/border-radius-stacked-number-mixed-chart.png b/test/fixtures/controller.bar/borderRadius/border-radius-stacked-number-mixed-chart.png new file mode 100644 index 00000000000..7449d7fa4a1 Binary files /dev/null and b/test/fixtures/controller.bar/borderRadius/border-radius-stacked-number-mixed-chart.png differ diff --git a/test/fixtures/controller.bar/borderRadius/border-radius-stacked-number-with-order.js b/test/fixtures/controller.bar/borderRadius/border-radius-stacked-number-with-order.js new file mode 100644 index 00000000000..886e9c46318 --- /dev/null +++ b/test/fixtures/controller.bar/borderRadius/border-radius-stacked-number-with-order.js @@ -0,0 +1,44 @@ +module.exports = { + threshold: 0.01, + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + backgroundColor: 'red', + data: [12, 19, 12, 5, 4, 12], + order: 2, + }, + { + backgroundColor: 'green', + data: [12, 19, -4, 5, 8, 3], + order: 1, + }, + { + backgroundColor: 'blue', + data: [7, 11, -12, 12, 0, -7], + order: 0, + } + ] + }, + options: { + elements: { + bar: { + borderRadius: Number.MAX_VALUE, + borderWidth: 2, + } + }, + scales: { + x: {display: false, stacked: true}, + y: {display: false, stacked: true} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/borderRadius/border-radius-stacked-number-with-order.png b/test/fixtures/controller.bar/borderRadius/border-radius-stacked-number-with-order.png new file mode 100644 index 00000000000..fa769073c5c Binary files /dev/null and b/test/fixtures/controller.bar/borderRadius/border-radius-stacked-number-with-order.png differ diff --git a/test/fixtures/controller.bar/borderRadius/border-radius-stacked-number.js b/test/fixtures/controller.bar/borderRadius/border-radius-stacked-number.js new file mode 100644 index 00000000000..1aeac88cb59 --- /dev/null +++ b/test/fixtures/controller.bar/borderRadius/border-radius-stacked-number.js @@ -0,0 +1,41 @@ +module.exports = { + threshold: 0.01, + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + backgroundColor: 'red', + data: [12, 19, 12, 5, 4, 12], + }, + { + backgroundColor: 'green', + data: [12, 19, -4, 5, 8, 3], + }, + { + backgroundColor: 'blue', + data: [7, 11, -12, 12, 0, -7], + } + ] + }, + options: { + elements: { + bar: { + borderRadius: Number.MAX_VALUE, + borderWidth: 2, + } + }, + scales: { + x: {display: false, stacked: true}, + y: {display: false, stacked: true} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/borderRadius/border-radius-stacked-number.png b/test/fixtures/controller.bar/borderRadius/border-radius-stacked-number.png new file mode 100644 index 00000000000..951b2eb634a Binary files /dev/null and b/test/fixtures/controller.bar/borderRadius/border-radius-stacked-number.png differ diff --git a/test/fixtures/controller.bar/borderRadius/border-radius.js b/test/fixtures/controller.bar/borderRadius/border-radius.js new file mode 100644 index 00000000000..cf9f96e998b --- /dev/null +++ b/test/fixtures/controller.bar/borderRadius/border-radius.js @@ -0,0 +1,43 @@ +module.exports = { + threshold: 0.01, + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + borderWidth: 2, + borderRadius: 5 + }, + { + // option in element (fallback) + data: [0, 5, 10, null, -10, -5], + borderSkipped: false, + borderRadius: Number.MAX_VALUE + } + ] + }, + options: { + indexAxis: 'y', + elements: { + bar: { + backgroundColor: '#AAAAAA80', + borderColor: '#80808080', + borderWidth: {bottom: 6, left: 15, top: 6, right: 15} + } + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/borderRadius/border-radius.png b/test/fixtures/controller.bar/borderRadius/border-radius.png new file mode 100644 index 00000000000..196b00db5f5 Binary files /dev/null and b/test/fixtures/controller.bar/borderRadius/border-radius.png differ diff --git a/test/fixtures/controller.bar/borderRadius/no-spacing.js b/test/fixtures/controller.bar/borderRadius/no-spacing.js new file mode 100644 index 00000000000..53a0fc47fbe --- /dev/null +++ b/test/fixtures/controller.bar/borderRadius/no-spacing.js @@ -0,0 +1,33 @@ +module.exports = { + threshold: 0.01, + config: { + type: 'bar', + data: { + labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], + datasets: [ + { + data: [9, 25, 13, 17, 12, 21, 20, 19, 6, 12, 14, 20], + categoryPercentage: 1, + barPercentage: 1, + backgroundColor: '#2E5C76', + borderWidth: 2, + borderColor: '#377395', + borderRadius: 5, + }, + ] + }, + options: { + devicePixelRatio: 1.25, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/borderRadius/no-spacing.png b/test/fixtures/controller.bar/borderRadius/no-spacing.png new file mode 100644 index 00000000000..b630cf5ca8a Binary files /dev/null and b/test/fixtures/controller.bar/borderRadius/no-spacing.png differ diff --git a/test/fixtures/controller.bar/borderSkipped/indexable.js b/test/fixtures/controller.bar/borderSkipped/indexable.js new file mode 100644 index 00000000000..66757821cc0 --- /dev/null +++ b/test/fixtures/controller.bar/borderSkipped/indexable.js @@ -0,0 +1,53 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + borderSkipped: [ + 'top', + 'top', + 'right', + 'right', + 'bottom', + 'left' + ] + }, + { + // option in element (fallback) + data: [0, 5, 10, null, -10, -5], + } + ] + }, + options: { + elements: { + bar: { + backgroundColor: 'transparent', + borderColor: '#888', + borderWidth: 8, + borderSkipped: [ + 'bottom', + 'bottom', + 'left', + 'left', + 'top', + 'right' + ] + } + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/borderSkipped/indexable.png b/test/fixtures/controller.bar/borderSkipped/indexable.png new file mode 100644 index 00000000000..64bd848cf62 Binary files /dev/null and b/test/fixtures/controller.bar/borderSkipped/indexable.png differ diff --git a/test/fixtures/controller.bar/borderSkipped/middle.js b/test/fixtures/controller.bar/borderSkipped/middle.js new file mode 100644 index 00000000000..f93c73a4e90 --- /dev/null +++ b/test/fixtures/controller.bar/borderSkipped/middle.js @@ -0,0 +1,38 @@ +module.exports = { + threshold: 0.01, + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + backgroundColor: 'red', + data: [12, 19, 12, 5, 4, 12], + }, + { + backgroundColor: 'green', + data: [12, 19, -4, 5, 8, 3], + }, + { + backgroundColor: 'blue', + data: [7, 11, -12, 12, 0, -7], + } + ] + }, + options: { + borderRadius: Number.MAX_VALUE, + borderSkipped: 'middle', + borderWidth: 2, + scales: { + x: {display: false, stacked: true}, + y: {display: false, stacked: true} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/borderSkipped/middle.png b/test/fixtures/controller.bar/borderSkipped/middle.png new file mode 100644 index 00000000000..41fd2019597 Binary files /dev/null and b/test/fixtures/controller.bar/borderSkipped/middle.png differ diff --git a/test/fixtures/controller.bar/borderSkipped/scriptable.js b/test/fixtures/controller.bar/borderSkipped/scriptable.js new file mode 100644 index 00000000000..a4a71041ec2 --- /dev/null +++ b/test/fixtures/controller.bar/borderSkipped/scriptable.js @@ -0,0 +1,54 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + borderSkipped: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 8 ? 'left' + : value > 0 ? 'right' + : value > -8 ? 'top' + : 'bottom'; + } + }, + { + // option in element (fallback) + data: [0, 5, 10, null, -10, -5] + } + ] + }, + options: { + elements: { + bar: { + backgroundColor: 'transparent', + borderColor: '#888', + borderSkipped: function(ctx) { + var index = ctx.dataIndex; + return index > 4 ? 'left' + : index > 3 ? 'right' + : index > 1 ? 'top' + : 'bottom'; + }, + borderWidth: 8 + } + }, + scales: { + x: {display: false}, + y: { + display: false, + beginAtZero: true + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/borderSkipped/scriptable.png b/test/fixtures/controller.bar/borderSkipped/scriptable.png new file mode 100644 index 00000000000..54708387e1e Binary files /dev/null and b/test/fixtures/controller.bar/borderSkipped/scriptable.png differ diff --git a/test/fixtures/controller.bar/borderSkipped/value.js b/test/fixtures/controller.bar/borderSkipped/value.js new file mode 100644 index 00000000000..4227ab7aa0c --- /dev/null +++ b/test/fixtures/controller.bar/borderSkipped/value.js @@ -0,0 +1,54 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3], + datasets: [ + { + // option in dataset + data: [0, 5, -10, null], + borderSkipped: 'top' + }, + { + // option in dataset + data: [0, 5, -10, null], + borderSkipped: 'right' + }, + { + // option in dataset + data: [0, 5, -10, null], + borderSkipped: 'bottom' + }, + { + // option in element (fallback) + data: [0, 5, -10, null], + }, + { + // option in dataset + data: [0, 5, -10, null], + borderSkipped: false + } + ] + }, + options: { + elements: { + bar: { + backgroundColor: 'transparent', + borderColor: '#888', + borderSkipped: 'left', + borderWidth: 8 + } + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/borderSkipped/value.png b/test/fixtures/controller.bar/borderSkipped/value.png new file mode 100644 index 00000000000..7f4179c87b1 Binary files /dev/null and b/test/fixtures/controller.bar/borderSkipped/value.png differ diff --git a/test/fixtures/controller.bar/borderWidth/indexable-object.js b/test/fixtures/controller.bar/borderWidth/indexable-object.js new file mode 100644 index 00000000000..1c1346f26f4 --- /dev/null +++ b/test/fixtures/controller.bar/borderWidth/indexable-object.js @@ -0,0 +1,54 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + borderSkipped: false, + borderWidth: [ + {}, + {bottom: 1, left: 1, top: 1, right: 1}, + {bottom: 1, left: 2, top: 1, right: 2}, + {bottom: 1, left: 3, top: 1, right: 3}, + {bottom: 1, left: 4, top: 1, right: 4}, + {bottom: 1, left: 5, top: 1, right: 5} + ] + }, + { + // option in element (fallback) + data: [0, 5, 10, null, -10, -5], + } + ] + }, + options: { + elements: { + bar: { + backgroundColor: 'transparent', + borderColor: '#80808080', + borderSkipped: false, + borderWidth: [ + {bottom: 1, left: 5, top: 1, right: 5}, + {bottom: 1, left: 4, top: 1, right: 4}, + {bottom: 1, left: 3, top: 1, right: 3}, + {bottom: 1, left: 2, top: 1, right: 2}, + {bottom: 1, left: 1, top: 1, right: 1}, + {} + ] + } + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/borderWidth/indexable-object.png b/test/fixtures/controller.bar/borderWidth/indexable-object.png new file mode 100644 index 00000000000..75471f52679 Binary files /dev/null and b/test/fixtures/controller.bar/borderWidth/indexable-object.png differ diff --git a/test/fixtures/controller.bar/borderWidth/indexable.js b/test/fixtures/controller.bar/borderWidth/indexable.js new file mode 100644 index 00000000000..feb4ec34696 --- /dev/null +++ b/test/fixtures/controller.bar/borderWidth/indexable.js @@ -0,0 +1,52 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + borderWidth: [ + 0, + 1, + 2, + 3, + 4, + 5 + ] + }, + { + // option in element (fallback) + data: [0, 5, 10, null, -10, -5], + } + ] + }, + options: { + elements: { + bar: { + backgroundColor: 'transparent', + borderColor: '#888', + borderWidth: [ + 5, + 4, + 3, + 2, + 1, + 0 + ] + } + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/borderWidth/indexable.png b/test/fixtures/controller.bar/borderWidth/indexable.png new file mode 100644 index 00000000000..0929ef0e61f Binary files /dev/null and b/test/fixtures/controller.bar/borderWidth/indexable.png differ diff --git a/test/fixtures/controller.bar/borderWidth/negative.js b/test/fixtures/controller.bar/borderWidth/negative.js new file mode 100644 index 00000000000..84c2145df29 --- /dev/null +++ b/test/fixtures/controller.bar/borderWidth/negative.js @@ -0,0 +1,47 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + borderWidth: -2 + }, + { + // option in element (fallback) + data: [0, 5, 10, null, -10, -5], + }, + { + data: [0, 5, 10, null, -10, -5], + borderWidth: {left: -5, top: -5, bottom: -5, right: -5}, + borderSkipped: false + }, + { + data: [0, 5, 10, null, -10, -5], + borderWidth: {} + }, + ] + }, + options: { + elements: { + bar: { + backgroundColor: '#888', + borderColor: '#f00', + borderWidth: -4 + } + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/borderWidth/negative.png b/test/fixtures/controller.bar/borderWidth/negative.png new file mode 100644 index 00000000000..ca2a445d992 Binary files /dev/null and b/test/fixtures/controller.bar/borderWidth/negative.png differ diff --git a/test/fixtures/controller.bar/borderWidth/object.js b/test/fixtures/controller.bar/borderWidth/object.js new file mode 100644 index 00000000000..0c24e0dfb24 --- /dev/null +++ b/test/fixtures/controller.bar/borderWidth/object.js @@ -0,0 +1,40 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + borderSkipped: false, + borderWidth: {bottom: 1, left: 2, top: 3, right: 4} + }, + { + // option in element (fallback) + data: [0, 5, 10, null, -10, -5], + } + ] + }, + options: { + elements: { + bar: { + backgroundColor: 'transparent', + borderColor: '#888', + borderSkipped: false, + borderWidth: {bottom: 4, left: 3, top: 2, right: 1} + } + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/borderWidth/object.png b/test/fixtures/controller.bar/borderWidth/object.png new file mode 100644 index 00000000000..ed251dfa77d Binary files /dev/null and b/test/fixtures/controller.bar/borderWidth/object.png differ diff --git a/test/fixtures/controller.bar/borderWidth/scriptable-object.js b/test/fixtures/controller.bar/borderWidth/scriptable-object.js new file mode 100644 index 00000000000..3db3e2b57b2 --- /dev/null +++ b/test/fixtures/controller.bar/borderWidth/scriptable-object.js @@ -0,0 +1,48 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + borderSkipped: false, + borderWidth: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return {top: Math.abs(value)}; + } + }, + { + // option in element (fallback) + data: [0, 5, 10, null, -10, -5] + } + ] + }, + options: { + elements: { + bar: { + backgroundColor: 'transparent', + borderColor: '#80808080', + borderSkipped: false, + borderWidth: function(ctx) { + return {left: ctx.dataIndex * 2}; + } + } + }, + scales: { + x: {display: false}, + y: { + display: false, + beginAtZero: true + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/borderWidth/scriptable-object.png b/test/fixtures/controller.bar/borderWidth/scriptable-object.png new file mode 100644 index 00000000000..10e65f5ced5 Binary files /dev/null and b/test/fixtures/controller.bar/borderWidth/scriptable-object.png differ diff --git a/test/fixtures/controller.bar/borderWidth/scriptable.js b/test/fixtures/controller.bar/borderWidth/scriptable.js new file mode 100644 index 00000000000..ed9e62dd1c8 --- /dev/null +++ b/test/fixtures/controller.bar/borderWidth/scriptable.js @@ -0,0 +1,46 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + borderWidth: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return Math.abs(value); + } + }, + { + // option in element (fallback) + data: [0, 5, 10, null, -10, -5] + } + ] + }, + options: { + elements: { + bar: { + backgroundColor: 'transparent', + borderColor: '#888', + borderWidth: function(ctx) { + return ctx.dataIndex * 2; + } + } + }, + scales: { + x: {display: false}, + y: { + display: false, + beginAtZero: true + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/borderWidth/scriptable.png b/test/fixtures/controller.bar/borderWidth/scriptable.png new file mode 100644 index 00000000000..fd8b193460f Binary files /dev/null and b/test/fixtures/controller.bar/borderWidth/scriptable.png differ diff --git a/test/fixtures/controller.bar/borderWidth/value.js b/test/fixtures/controller.bar/borderWidth/value.js new file mode 100644 index 00000000000..d5e5dedf022 --- /dev/null +++ b/test/fixtures/controller.bar/borderWidth/value.js @@ -0,0 +1,38 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + borderWidth: 2 + }, + { + // option in element (fallback) + data: [0, 5, 10, null, -10, -5], + } + ] + }, + options: { + elements: { + bar: { + backgroundColor: 'transparent', + borderColor: '#888', + borderWidth: 4 + } + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/borderWidth/value.png b/test/fixtures/controller.bar/borderWidth/value.png new file mode 100644 index 00000000000..4f6aca6142f Binary files /dev/null and b/test/fixtures/controller.bar/borderWidth/value.png differ diff --git a/test/fixtures/controller.bar/chart-area-clip.js b/test/fixtures/controller.bar/chart-area-clip.js new file mode 100644 index 00000000000..78b85ed0650 --- /dev/null +++ b/test/fixtures/controller.bar/chart-area-clip.js @@ -0,0 +1,40 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 3, 4], + datasets: [ + { + data: [5, 20, -5, -20], + borderColor: '#ff0000' + } + ] + }, + options: { + layout: { + padding: { + left: 0, + right: 0, + top: 50, + bottom: 50 + } + }, + elements: { + bar: { + backgroundColor: '#00ff00', + borderWidth: 8 + } + }, + scales: { + x: {display: false}, + y: {display: false, min: -10, max: 10} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/chart-area-clip.png b/test/fixtures/controller.bar/chart-area-clip.png new file mode 100644 index 00000000000..8c59a70128b Binary files /dev/null and b/test/fixtures/controller.bar/chart-area-clip.png differ diff --git a/test/fixtures/controller.bar/data/object-index-axis-y.js b/test/fixtures/controller.bar/data/object-index-axis-y.js new file mode 100644 index 00000000000..0023d08632e --- /dev/null +++ b/test/fixtures/controller.bar/data/object-index-axis-y.js @@ -0,0 +1,21 @@ +module.exports = { + config: { + type: 'bar', + data: { + datasets: [{ + label: '# of Votes', + data: {a: 1, b: 3, c: 2} + }] + }, + options: { + indexAxis: 'y' + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/data/object-index-axis-y.png b/test/fixtures/controller.bar/data/object-index-axis-y.png new file mode 100644 index 00000000000..ace6956afca Binary files /dev/null and b/test/fixtures/controller.bar/data/object-index-axis-y.png differ diff --git a/test/fixtures/controller.bar/data/object.js b/test/fixtures/controller.bar/data/object.js new file mode 100644 index 00000000000..ae1cd0a58c3 --- /dev/null +++ b/test/fixtures/controller.bar/data/object.js @@ -0,0 +1,30 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: ['a', 'b', 'c'], + datasets: [ + { + data: {a: 10, b: 2, c: -5}, + backgroundColor: '#ff0000' + }, + { + data: {a: 8, b: 12, c: 5}, + backgroundColor: '#00ff00' + } + ] + }, + options: { + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/data/object.png b/test/fixtures/controller.bar/data/object.png new file mode 100644 index 00000000000..c705448f0d1 Binary files /dev/null and b/test/fixtures/controller.bar/data/object.png differ diff --git a/test/fixtures/controller.bar/data/parsing.js b/test/fixtures/controller.bar/data/parsing.js new file mode 100644 index 00000000000..d4bdbb31402 --- /dev/null +++ b/test/fixtures/controller.bar/data/parsing.js @@ -0,0 +1,44 @@ +const data = [{x: 'Jan', net: 100, cogs: 50, gm: 50}, {x: 'Feb', net: 120, cogs: 55, gm: 75}]; + +module.exports = { + config: { + type: 'bar', + data: { + labels: ['Jan', 'Feb'], + datasets: [{ + label: 'Net sales', + backgroundColor: 'blue', + data: data, + parsing: { + yAxisKey: 'net' + } + }, { + label: 'Cost of goods sold', + backgroundColor: 'red', + data: data, + parsing: { + yAxisKey: 'cogs' + } + }, { + label: 'Gross margin', + backgroundColor: 'green', + data: data, + parsing: { + yAxisKey: 'gm' + } + }] + }, + options: { + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/data/parsing.png b/test/fixtures/controller.bar/data/parsing.png new file mode 100644 index 00000000000..f0a63fd4d52 Binary files /dev/null and b/test/fixtures/controller.bar/data/parsing.png differ diff --git a/test/fixtures/controller.bar/floatBar/data-as-objects-horizontal.js b/test/fixtures/controller.bar/floatBar/data-as-objects-horizontal.js new file mode 100644 index 00000000000..8d6f373183d --- /dev/null +++ b/test/fixtures/controller.bar/floatBar/data-as-objects-horizontal.js @@ -0,0 +1,31 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: ['a', 'b', 'c'], + datasets: [ + { + data: [{y: 'b', x: [2, 8]}, {y: 'c', x: [2, 5]}], + backgroundColor: '#ff0000' + }, + { + data: [{y: 'a', x: 10}, {y: 'c', x: [6, 10]}], + backgroundColor: '#00ff00' + } + ] + }, + options: { + indexAxis: 'y', + scales: { + x: {display: false, min: 0}, + y: {display: false, stacked: true} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/floatBar/data-as-objects-horizontal.png b/test/fixtures/controller.bar/floatBar/data-as-objects-horizontal.png new file mode 100644 index 00000000000..39b4496f5a2 Binary files /dev/null and b/test/fixtures/controller.bar/floatBar/data-as-objects-horizontal.png differ diff --git a/test/fixtures/controller.bar/floatBar/data-as-objects.js b/test/fixtures/controller.bar/floatBar/data-as-objects.js new file mode 100644 index 00000000000..42654bca24e --- /dev/null +++ b/test/fixtures/controller.bar/floatBar/data-as-objects.js @@ -0,0 +1,30 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: ['a', 'b', 'c'], + datasets: [ + { + data: [{x: 'b', y: [2, 8]}, {x: 'c', y: [2, 5]}], + backgroundColor: '#ff0000' + }, + { + data: [{x: 'a', y: 10}, {x: 'c', y: [6, 10]}], + backgroundColor: '#00ff00' + } + ] + }, + options: { + scales: { + x: {display: false, stacked: true}, + y: {display: false, min: 0} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/floatBar/data-as-objects.png b/test/fixtures/controller.bar/floatBar/data-as-objects.png new file mode 100644 index 00000000000..70c0adabab1 Binary files /dev/null and b/test/fixtures/controller.bar/floatBar/data-as-objects.png differ diff --git a/test/fixtures/controller.bar/floatBar/float-bar-horizontal.json b/test/fixtures/controller.bar/floatBar/float-bar-horizontal.json new file mode 100644 index 00000000000..8f5aafe70f0 --- /dev/null +++ b/test/fixtures/controller.bar/floatBar/float-bar-horizontal.json @@ -0,0 +1,36 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2030", "2034", "2038", "2042"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [11, [6,2], [-4,-7], -2] + }, { + "backgroundColor": "#36A2EB", + "data": [[1,2], [3,4], [-2,-3], [1,4]] + }, { + "backgroundColor": "#FFCE56", + "data": [[0,1], [1,2], [-2,-1], [1,-7]] + }] + }, + "options": { + "indexAxis": "y", + "scales": { + "x": { + "display": false + }, + "y": { + "display": false + } + } + } + }, + "debug": false, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/floatBar/float-bar-horizontal.png b/test/fixtures/controller.bar/floatBar/float-bar-horizontal.png new file mode 100644 index 00000000000..3293dc8877f Binary files /dev/null and b/test/fixtures/controller.bar/floatBar/float-bar-horizontal.png differ diff --git a/test/fixtures/controller.bar/floatBar/float-bar-stacked-horizontal.json b/test/fixtures/controller.bar/floatBar/float-bar-stacked-horizontal.json new file mode 100644 index 00000000000..8640be64a7c --- /dev/null +++ b/test/fixtures/controller.bar/floatBar/float-bar-stacked-horizontal.json @@ -0,0 +1,38 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2030", "2034", "2038", "2042"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [11, [6,2], [-4,-7], -2] + }, { + "backgroundColor": "#36A2EB", + "data": [[1,2], [3,4], [-2,-3], [1,4]] + }, { + "backgroundColor": "#FFCE56", + "data": [[0,1], [1,2], [-2,-1], [1,-7]] + }] + }, + "options": { + "indexAxis": "y", + "scales": { + "x": { + "display": false, + "stacked": true + }, + "y": { + "display": false, + "stacked": true + } + } + } + }, + "debug": false, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/floatBar/float-bar-stacked-horizontal.png b/test/fixtures/controller.bar/floatBar/float-bar-stacked-horizontal.png new file mode 100644 index 00000000000..c3066fd6812 Binary files /dev/null and b/test/fixtures/controller.bar/floatBar/float-bar-stacked-horizontal.png differ diff --git a/test/fixtures/controller.bar/floatBar/float-bar-stacked.json b/test/fixtures/controller.bar/floatBar/float-bar-stacked.json new file mode 100644 index 00000000000..eb0ccb1ec91 --- /dev/null +++ b/test/fixtures/controller.bar/floatBar/float-bar-stacked.json @@ -0,0 +1,37 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2030", "2034", "2038", "2042"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [11, [6,2], [-4,-7], -2] + }, { + "backgroundColor": "#36A2EB", + "data": [[1,2], [3,4], [-2,-3], [1,4]] + }, { + "backgroundColor": "#FFCE56", + "data": [[0,1], [1,2], [-2,-1], [1,-7]] + }] + }, + "options": { + "scales": { + "x": { + "display": false, + "stacked": true + }, + "y": { + "display": false, + "stacked": true + } + } + } + }, + "debug": false, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/floatBar/float-bar-stacked.png b/test/fixtures/controller.bar/floatBar/float-bar-stacked.png new file mode 100644 index 00000000000..5da257bcd43 Binary files /dev/null and b/test/fixtures/controller.bar/floatBar/float-bar-stacked.png differ diff --git a/test/fixtures/controller.bar/floatBar/float-bar.json b/test/fixtures/controller.bar/floatBar/float-bar.json new file mode 100644 index 00000000000..e97bfcecbf8 --- /dev/null +++ b/test/fixtures/controller.bar/floatBar/float-bar.json @@ -0,0 +1,35 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2030", "2034", "2038", "2042"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [11, [6,2], [-4,-7], -2] + }, { + "backgroundColor": "#36A2EB", + "data": [[1,2], [3,4], [-2,-3], [1,4]] + }, { + "backgroundColor": "#FFCE56", + "data": [[0,1], [1,2], [-2,-1], [1,-7]] + }] + }, + "options": { + "scales": { + "x": { + "display": false + }, + "y": { + "display": false + } + } + } + }, + "debug": false, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/floatBar/float-bar.png b/test/fixtures/controller.bar/floatBar/float-bar.png new file mode 100644 index 00000000000..eaff55a63ed Binary files /dev/null and b/test/fixtures/controller.bar/floatBar/float-bar.png differ diff --git a/test/fixtures/controller.bar/horizontal-borders.js b/test/fixtures/controller.bar/horizontal-borders.js new file mode 100644 index 00000000000..bcba2d89afb --- /dev/null +++ b/test/fixtures/controller.bar/horizontal-borders.js @@ -0,0 +1,41 @@ +module.exports = { + threshold: 0.01, + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + borderWidth: 2 + }, + { + // option in element (fallback) + data: [0, 5, 10, null, -10, -5], + borderSkipped: false + } + ] + }, + options: { + indexAxis: 'y', + elements: { + bar: { + backgroundColor: '#AAAAAA80', + borderColor: '#80808080', + borderWidth: {bottom: 6, left: 15, top: 6, right: 15} + } + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/horizontal-borders.png b/test/fixtures/controller.bar/horizontal-borders.png new file mode 100644 index 00000000000..8398645351f Binary files /dev/null and b/test/fixtures/controller.bar/horizontal-borders.png differ diff --git a/test/fixtures/controller.bar/minBarLength/horizontal-neg.js b/test/fixtures/controller.bar/minBarLength/horizontal-neg.js new file mode 100644 index 00000000000..c625aa64e4f --- /dev/null +++ b/test/fixtures/controller.bar/minBarLength/horizontal-neg.js @@ -0,0 +1,34 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 2], + datasets: [ + { + data: [0, -0.01, -30], + backgroundColor: '#00ff00', + borderColor: '#000', + borderWidth: 4, + minBarLength: 20 + } + ] + }, + options: { + indexAxis: 'y', + scales: { + x: { + ticks: { + display: false + } + }, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/minBarLength/horizontal-neg.png b/test/fixtures/controller.bar/minBarLength/horizontal-neg.png new file mode 100644 index 00000000000..16c6cabd522 Binary files /dev/null and b/test/fixtures/controller.bar/minBarLength/horizontal-neg.png differ diff --git a/test/fixtures/controller.bar/minBarLength/horizontal-pos.js b/test/fixtures/controller.bar/minBarLength/horizontal-pos.js new file mode 100644 index 00000000000..397e54adc16 --- /dev/null +++ b/test/fixtures/controller.bar/minBarLength/horizontal-pos.js @@ -0,0 +1,34 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 2], + datasets: [ + { + data: [0, 0.01, 30], + backgroundColor: '#00ff00', + borderColor: '#000', + borderWidth: 4, + minBarLength: 20 + } + ] + }, + options: { + indexAxis: 'y', + scales: { + x: { + ticks: { + display: false + } + }, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/minBarLength/horizontal-pos.png b/test/fixtures/controller.bar/minBarLength/horizontal-pos.png new file mode 100644 index 00000000000..8d8b4724e9f Binary files /dev/null and b/test/fixtures/controller.bar/minBarLength/horizontal-pos.png differ diff --git a/test/fixtures/controller.bar/minBarLength/horizontal-stacked-no-overlap.js b/test/fixtures/controller.bar/minBarLength/horizontal-stacked-no-overlap.js new file mode 100644 index 00000000000..57b8314564f --- /dev/null +++ b/test/fixtures/controller.bar/minBarLength/horizontal-stacked-no-overlap.js @@ -0,0 +1,55 @@ +const minBarLength = 50; + +module.exports = { + config: { + type: 'bar', + data: { + labels: [1, 2, 3, 4], + datasets: [ + { + data: [1, -1, 1, 20], + backgroundColor: '#bb000066', + minBarLength + }, + { + data: [1, -1, -1, -20], + backgroundColor: '#00bb0066', + minBarLength + }, + { + data: [1, -1, 1, 40], + backgroundColor: '#0000bb66', + minBarLength + }, + { + data: [1, -1, -1, -40], + backgroundColor: '#00000066', + minBarLength + } + ] + }, + options: { + indexAxis: 'y', + scales: { + x: { + display: false, + stacked: true + }, + y: { + type: 'linear', + position: 'left', + stacked: true, + ticks: { + display: false + } + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/minBarLength/horizontal-stacked-no-overlap.png b/test/fixtures/controller.bar/minBarLength/horizontal-stacked-no-overlap.png new file mode 100644 index 00000000000..dfa3f87b4f3 Binary files /dev/null and b/test/fixtures/controller.bar/minBarLength/horizontal-stacked-no-overlap.png differ diff --git a/test/fixtures/controller.bar/minBarLength/horizontal-stacked.js b/test/fixtures/controller.bar/minBarLength/horizontal-stacked.js new file mode 100644 index 00000000000..8452ed6c8d6 --- /dev/null +++ b/test/fixtures/controller.bar/minBarLength/horizontal-stacked.js @@ -0,0 +1,43 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4], + datasets: [{ + data: [0, 0.01, 30], + backgroundColor: '#00ff00', + borderColor: '#000', + borderWidth: 4, + minBarLength: 20, + xAxisID: 'x2', + }] + }, + options: { + indexAxis: 'y', + scales: { + x: { + stack: 'demo', + ticks: { + display: false + } + }, + x2: { + type: 'linear', + position: 'bottom', + stack: 'demo', + stackWeight: 1, + ticks: { + display: false + } + }, + y: {display: false}, + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/minBarLength/horizontal-stacked.png b/test/fixtures/controller.bar/minBarLength/horizontal-stacked.png new file mode 100644 index 00000000000..87da74832c7 Binary files /dev/null and b/test/fixtures/controller.bar/minBarLength/horizontal-stacked.png differ diff --git a/test/fixtures/controller.bar/minBarLength/horizontal.js b/test/fixtures/controller.bar/minBarLength/horizontal.js new file mode 100644 index 00000000000..3c52f3cf543 --- /dev/null +++ b/test/fixtures/controller.bar/minBarLength/horizontal.js @@ -0,0 +1,35 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4], + datasets: [ + { + data: [0, -0.01, 0.01, 30, -30], + backgroundColor: '#00ff00', + borderColor: '#000', + borderSkipped: ctx => ctx.raw === 0 ? false : 'start', + borderWidth: 4, + minBarLength: 20 + } + ] + }, + options: { + indexAxis: 'y', + scales: { + x: { + ticks: { + display: false + } + }, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/minBarLength/horizontal.png b/test/fixtures/controller.bar/minBarLength/horizontal.png new file mode 100644 index 00000000000..11aefdb1d8d Binary files /dev/null and b/test/fixtures/controller.bar/minBarLength/horizontal.png differ diff --git a/test/fixtures/controller.bar/minBarLength/vertical-neg.js b/test/fixtures/controller.bar/minBarLength/vertical-neg.js new file mode 100644 index 00000000000..7cee3a8a1e2 --- /dev/null +++ b/test/fixtures/controller.bar/minBarLength/vertical-neg.js @@ -0,0 +1,33 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 2], + datasets: [ + { + data: [0, -0.01, -30], + backgroundColor: '#00ff00', + borderColor: '#000', + borderWidth: 4, + minBarLength: 20 + } + ] + }, + options: { + scales: { + x: {display: false}, + y: { + ticks: { + display: false + } + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/minBarLength/vertical-neg.png b/test/fixtures/controller.bar/minBarLength/vertical-neg.png new file mode 100644 index 00000000000..debb97ee7d1 Binary files /dev/null and b/test/fixtures/controller.bar/minBarLength/vertical-neg.png differ diff --git a/test/fixtures/controller.bar/minBarLength/vertical-pos.js b/test/fixtures/controller.bar/minBarLength/vertical-pos.js new file mode 100644 index 00000000000..96e2756c33a --- /dev/null +++ b/test/fixtures/controller.bar/minBarLength/vertical-pos.js @@ -0,0 +1,33 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 2], + datasets: [ + { + data: [0, 0.01, 30], + backgroundColor: '#00ff00', + borderColor: '#000', + borderWidth: 4, + minBarLength: 20 + } + ] + }, + options: { + scales: { + x: {display: false}, + y: { + ticks: { + display: false + } + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/minBarLength/vertical-pos.png b/test/fixtures/controller.bar/minBarLength/vertical-pos.png new file mode 100644 index 00000000000..9a7b49e9cd9 Binary files /dev/null and b/test/fixtures/controller.bar/minBarLength/vertical-pos.png differ diff --git a/test/fixtures/controller.bar/minBarLength/vertical-stacked-no-overlap.js b/test/fixtures/controller.bar/minBarLength/vertical-stacked-no-overlap.js new file mode 100644 index 00000000000..454e027357b --- /dev/null +++ b/test/fixtures/controller.bar/minBarLength/vertical-stacked-no-overlap.js @@ -0,0 +1,54 @@ +const minBarLength = 50; + +module.exports = { + config: { + type: 'bar', + data: { + labels: [1, 2, 3, 4], + datasets: [ + { + data: [1, -1, 1, 20], + backgroundColor: '#bb000066', + minBarLength + }, + { + data: [1, -1, -1, -20], + backgroundColor: '#00bb0066', + minBarLength + }, + { + data: [1, -1, 1, 40], + backgroundColor: '#0000bb66', + minBarLength + }, + { + data: [1, -1, -1, -40], + backgroundColor: '#00000066', + minBarLength + } + ] + }, + options: { + scales: { + x: { + display: false, + stacked: true + }, + y: { + type: 'linear', + position: 'left', + stacked: true, + ticks: { + display: false + } + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/minBarLength/vertical-stacked-no-overlap.png b/test/fixtures/controller.bar/minBarLength/vertical-stacked-no-overlap.png new file mode 100644 index 00000000000..f6b917af38a Binary files /dev/null and b/test/fixtures/controller.bar/minBarLength/vertical-stacked-no-overlap.png differ diff --git a/test/fixtures/controller.bar/minBarLength/vertical-stacked.js b/test/fixtures/controller.bar/minBarLength/vertical-stacked.js new file mode 100644 index 00000000000..e09f12cd6ab --- /dev/null +++ b/test/fixtures/controller.bar/minBarLength/vertical-stacked.js @@ -0,0 +1,42 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4], + datasets: [{ + data: [0, 0.01, 30], + backgroundColor: '#00ff00', + borderColor: '#000', + borderWidth: 4, + minBarLength: 20, + yAxisID: 'y2', + }] + }, + options: { + scales: { + x: {display: false}, + y: { + stack: 'demo', + ticks: { + display: false + } + }, + y2: { + type: 'linear', + position: 'left', + stack: 'demo', + stackWeight: 1, + ticks: { + display: false + } + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/minBarLength/vertical-stacked.png b/test/fixtures/controller.bar/minBarLength/vertical-stacked.png new file mode 100644 index 00000000000..ecef74b4430 Binary files /dev/null and b/test/fixtures/controller.bar/minBarLength/vertical-stacked.png differ diff --git a/test/fixtures/controller.bar/minBarLength/vertical.js b/test/fixtures/controller.bar/minBarLength/vertical.js new file mode 100644 index 00000000000..d7313be9be2 --- /dev/null +++ b/test/fixtures/controller.bar/minBarLength/vertical.js @@ -0,0 +1,34 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4], + datasets: [ + { + data: [0, -0.01, 0.01, 30, -30], + backgroundColor: '#00ff00', + borderColor: '#000', + borderSkipped: ctx => ctx.raw === 0 ? false : 'start', + borderWidth: 4, + minBarLength: 20 + } + ] + }, + options: { + scales: { + x: {display: false}, + y: { + ticks: { + display: false + } + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/minBarLength/vertical.png b/test/fixtures/controller.bar/minBarLength/vertical.png new file mode 100644 index 00000000000..0595425bcfc Binary files /dev/null and b/test/fixtures/controller.bar/minBarLength/vertical.png differ diff --git a/test/fixtures/controller.bar/not-grouped/mixed.js b/test/fixtures/controller.bar/not-grouped/mixed.js new file mode 100644 index 00000000000..750595312fd --- /dev/null +++ b/test/fixtures/controller.bar/not-grouped/mixed.js @@ -0,0 +1,35 @@ +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/9281', + config: { + type: 'bar', + data: { + labels: [0, 1, 2], + datasets: [ + { + label: 'data 1', + data: [1, 2, 2], + backgroundColor: 'rgb(255,0,0,0.7)', + grouped: true + }, + { + label: 'data 2', + data: [4, 4, 1], + backgroundColor: 'rgb(0,255,0,0.7)', + grouped: true + }, + { + label: 'data 3', + data: [2, 1, 3], + backgroundColor: 'rgb(0,0,255,0.7)', + grouped: false + } + ] + }, + options: { + scales: { + x: {display: false}, + y: {display: false} + } + } + }, +}; diff --git a/test/fixtures/controller.bar/not-grouped/mixed.png b/test/fixtures/controller.bar/not-grouped/mixed.png new file mode 100644 index 00000000000..af2c9fda2fc Binary files /dev/null and b/test/fixtures/controller.bar/not-grouped/mixed.png differ diff --git a/test/fixtures/controller.bar/not-grouped/on-time.js b/test/fixtures/controller.bar/not-grouped/on-time.js new file mode 100644 index 00000000000..6c2d44cbb08 --- /dev/null +++ b/test/fixtures/controller.bar/not-grouped/on-time.js @@ -0,0 +1,134 @@ +const data1 = [ + { + x: '2017-11-02T20:30:00', + y: 27 + }, + { + x: '2017-11-03T20:53:00', + y: 30 + }, + { + x: '2017-11-06T05:46:00', + y: 19 + }, + { + x: '2017-11-06T21:03:00', + y: 28 + }, + { + x: '2017-11-07T20:49:00', + y: 29 + }, + { + x: '2017-11-08T21:52:00', + y: 33 + } +]; + +const data2 = [ + { + x: '2017-11-03T13:07:00', + y: 45 + }, + { + x: '2017-11-04T04:50:00', + y: 40 + }, + { + x: '2017-11-06T12:48:00', + y: 38 + }, + { + x: '2017-11-07T12:28:00', + y: 42 + }, + { + x: '2017-11-08T12:45:00', + y: 51 + }, + { + x: '2017-11-09T05:23:00', + y: 57 + } +]; + +const data3 = [ + { + x: '2017-11-03T16:30:00', + y: 32 + }, + { + x: '2017-11-04T11:50:00', + y: 34 + }, + { + x: '2017-11-06T18:30:00', + y: 28 + }, + { + x: '2017-11-07T15:51:00', + y: 31 + }, + { + x: '2017-11-08T17:27:00', + y: 36 + }, + { + x: '2017-11-09T06:53:00', + y: 31 + } +]; + +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/5139', + config: { + type: 'bar', + data: { + datasets: [ + { + data: data1, + backgroundColor: 'rgb(0,0,255)', + }, + { + data: data2, + backgroundColor: 'rgb(255,0,0)', + }, + { + data: data3, + backgroundColor: 'rgb(0,255,0)', + }, + ] + }, + options: { + barThickness: 10, + grouped: false, + scales: { + x: { + bounds: 'ticks', + type: 'time', + offset: false, + position: 'bottom', + display: true, + time: { + isoWeekday: true, + unit: 'day' + }, + grid: { + offset: false + } + }, + y: { + beginAtZero: true, + display: false + } + }, + } + }, + options: { + spriteText: true, + canvas: { + width: 1000, + height: 300 + } + } +}; diff --git a/test/fixtures/controller.bar/not-grouped/on-time.png b/test/fixtures/controller.bar/not-grouped/on-time.png new file mode 100644 index 00000000000..9ed7d0ca200 Binary files /dev/null and b/test/fixtures/controller.bar/not-grouped/on-time.png differ diff --git a/test/fixtures/controller.bar/skipNull/bar-skip-null-object-data.js b/test/fixtures/controller.bar/skipNull/bar-skip-null-object-data.js new file mode 100644 index 00000000000..b21ec623c5d --- /dev/null +++ b/test/fixtures/controller.bar/skipNull/bar-skip-null-object-data.js @@ -0,0 +1,32 @@ +module.exports = { + config: { + type: 'bar', + data: { + datasets: [ + { + data: {0: 5, 1: 20, 2: 1, 3: 10}, + backgroundColor: '#00ff00', + borderColor: '#ff0000' + }, + { + data: {0: 10, 1: null, 2: 1, 3: NaN}, + backgroundColor: '#ff0000', + borderColor: '#ff0000' + } + ] + }, + options: { + skipNull: true, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/skipNull/bar-skip-null-object-data.png b/test/fixtures/controller.bar/skipNull/bar-skip-null-object-data.png new file mode 100644 index 00000000000..0406b55a074 Binary files /dev/null and b/test/fixtures/controller.bar/skipNull/bar-skip-null-object-data.png differ diff --git a/test/fixtures/controller.bar/skipNull/bar-skip-null.js b/test/fixtures/controller.bar/skipNull/bar-skip-null.js new file mode 100644 index 00000000000..fbbdaf0ce66 --- /dev/null +++ b/test/fixtures/controller.bar/skipNull/bar-skip-null.js @@ -0,0 +1,33 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 3, 4], + datasets: [ + { + data: [5, 20, 1, 10], + backgroundColor: '#00ff00', + borderColor: '#ff0000' + }, + { + data: [10, null, 1, undefined], + backgroundColor: '#ff0000', + borderColor: '#ff0000' + } + ] + }, + options: { + skipNull: true, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/skipNull/bar-skip-null.png b/test/fixtures/controller.bar/skipNull/bar-skip-null.png new file mode 100644 index 00000000000..0406b55a074 Binary files /dev/null and b/test/fixtures/controller.bar/skipNull/bar-skip-null.png differ diff --git a/test/fixtures/controller.bar/skipNull/combinations.js b/test/fixtures/controller.bar/skipNull/combinations.js new file mode 100644 index 00000000000..8dd38761a42 --- /dev/null +++ b/test/fixtures/controller.bar/skipNull/combinations.js @@ -0,0 +1,38 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: ['0', '1', '2', '3', '4', '5', '6', '7'], + datasets: [ + { + data: [null, 1000, null, 1000, null, 1000, null, 1000], + backgroundColor: '#00ff00', + borderColor: '#ff0000' + }, + { + data: [null, null, 1000, 1000, null, null, 1000, 1000], + backgroundColor: '#ff0000', + borderColor: '#ff0000' + }, + { + data: [null, null, null, null, 1000, 1000, 1000, 1000], + backgroundColor: '#0000ff', + borderColor: '#0000ff' + } + ] + }, + options: { + skipNull: true, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/skipNull/combinations.png b/test/fixtures/controller.bar/skipNull/combinations.png new file mode 100644 index 00000000000..6d26ea3d4c3 Binary files /dev/null and b/test/fixtures/controller.bar/skipNull/combinations.png differ diff --git a/test/fixtures/controller.bar/stacking/issue-9105.js b/test/fixtures/controller.bar/stacking/issue-9105.js new file mode 100644 index 00000000000..992a245ac19 --- /dev/null +++ b/test/fixtures/controller.bar/stacking/issue-9105.js @@ -0,0 +1,49 @@ +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/9105', + config: { + type: 'bar', + data: { + labels: ['January', 'February', 'March', 'April', 'May', 'June'], + datasets: [ + { + backgroundColor: 'rgba(255,99,132,0.8)', + label: 'Dataset 1', + data: [12, 19, 3, 5, 2, 3], + stack: '0', + yAxisID: 'y' + }, + { + backgroundColor: 'rgba(54,162,235,0.8)', + label: 'Dataset 2', + data: [13, 19, 3, 5, 8, 3], + stack: '0', + yAxisID: 'y' + }, + { + backgroundColor: 'rgba(75,192,192,0.8)', + label: 'Dataset 3', + data: [13, 19, 3, 5, 8, 3], + stack: '0', + yAxisID: 'y' + } + ] + }, + options: { + plugins: false, + scales: { + x: { + display: false, + }, + y: { + display: false + } + } + } + }, + options: { + run(chart) { + chart.data.datasets[1].stack = '1'; + chart.update(); + } + } +}; diff --git a/test/fixtures/controller.bar/stacking/issue-9105.png b/test/fixtures/controller.bar/stacking/issue-9105.png new file mode 100644 index 00000000000..14a21682680 Binary files /dev/null and b/test/fixtures/controller.bar/stacking/issue-9105.png differ diff --git a/test/fixtures/controller.bar/stacking/logarithmic-strings.js b/test/fixtures/controller.bar/stacking/logarithmic-strings.js new file mode 100644 index 00000000000..96d9e4ba387 --- /dev/null +++ b/test/fixtures/controller.bar/stacking/logarithmic-strings.js @@ -0,0 +1,34 @@ +module.exports = { + config: { + type: 'bar', + data: { + datasets: [{ + data: ['10', '100', '10', '100'], + backgroundColor: '#ff0000' + }, { + data: ['100', '10', '0', '100'], + backgroundColor: '#00ff00' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + datasets: { + bar: { + barPercentage: 1, + } + }, + scales: { + x: { + type: 'category', + display: false, + stacked: true, + }, + y: { + type: 'logarithmic', + display: false, + stacked: true + } + } + } + } +}; diff --git a/test/fixtures/controller.bar/stacking/logarithmic-strings.png b/test/fixtures/controller.bar/stacking/logarithmic-strings.png new file mode 100644 index 00000000000..377b6c59ff1 Binary files /dev/null and b/test/fixtures/controller.bar/stacking/logarithmic-strings.png differ diff --git a/test/fixtures/controller.bar/stacking/logarithmic.js b/test/fixtures/controller.bar/stacking/logarithmic.js new file mode 100644 index 00000000000..ca56ff5f88d --- /dev/null +++ b/test/fixtures/controller.bar/stacking/logarithmic.js @@ -0,0 +1,34 @@ +module.exports = { + config: { + type: 'bar', + data: { + datasets: [{ + data: [10, 100, 10, 100], + backgroundColor: '#ff0000' + }, { + data: [100, 10, 0, 100], + backgroundColor: '#00ff00' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + datasets: { + bar: { + barPercentage: 1, + } + }, + scales: { + x: { + type: 'category', + display: false, + stacked: true, + }, + y: { + type: 'logarithmic', + display: false, + stacked: true + } + } + } + } +}; diff --git a/test/fixtures/controller.bar/stacking/logarithmic.png b/test/fixtures/controller.bar/stacking/logarithmic.png new file mode 100644 index 00000000000..377b6c59ff1 Binary files /dev/null and b/test/fixtures/controller.bar/stacking/logarithmic.png differ diff --git a/test/fixtures/controller.bar/stacking/order-default.json b/test/fixtures/controller.bar/stacking/order-default.json new file mode 100644 index 00000000000..18fbe9cf653 --- /dev/null +++ b/test/fixtures/controller.bar/stacking/order-default.json @@ -0,0 +1,38 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2016", "2018", "2020", "2024", "2030"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [1, null, 3, 4, 5] + }, { + "backgroundColor": "#36A2EB", + "data": [5, 4, 3, null, 1] + }, { + "backgroundColor": "#FFCE56", + "data": [3, 5, 2, null, 4] + }] + }, + "options": { + "responsive": false, + "scales": { + "x": { + "display": false, + "stacked": true + }, + "y": { + "display": false, + "stacked": true, + "beginAtZero": true + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/stacking/order-default.png b/test/fixtures/controller.bar/stacking/order-default.png new file mode 100644 index 00000000000..59e10242bb8 Binary files /dev/null and b/test/fixtures/controller.bar/stacking/order-default.png differ diff --git a/test/fixtures/controller.bar/stacking/order-specified.json b/test/fixtures/controller.bar/stacking/order-specified.json new file mode 100644 index 00000000000..6daf0eab8f0 --- /dev/null +++ b/test/fixtures/controller.bar/stacking/order-specified.json @@ -0,0 +1,41 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["2016", "2018", "2020", "2024", "2030"], + "datasets": [{ + "backgroundColor": "#FF6384", + "data": [1, null, 3, 4, 5], + "order": 20 + }, { + "backgroundColor": "#36A2EB", + "data": [5, 4, 3, null, 1], + "order": 25 + }, { + "backgroundColor": "#FFCE56", + "data": [3, 5, 2, null, 4], + "order": 10 + }] + }, + "options": { + "responsive": false, + "scales": { + "x": { + "display": false, + "stacked": true + }, + "y": { + "display": false, + "stacked": true, + "beginAtZero": true + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bar/stacking/order-specified.png b/test/fixtures/controller.bar/stacking/order-specified.png new file mode 100644 index 00000000000..b927f337ac9 Binary files /dev/null and b/test/fixtures/controller.bar/stacking/order-specified.png differ diff --git a/test/fixtures/controller.bar/stacking/remove-dataset.js b/test/fixtures/controller.bar/stacking/remove-dataset.js new file mode 100644 index 00000000000..a0ca8db1c70 --- /dev/null +++ b/test/fixtures/controller.bar/stacking/remove-dataset.js @@ -0,0 +1,102 @@ +var barChartData = { + labels: [0, 1, 2, 3, 4, 5, 6], + datasets: [ + { + backgroundColor: 'red', + data: [ + // { x: 0, y: 0 }, + {x: 1, y: 5}, + {x: 2, y: 5}, + {x: 3, y: 5}, + {x: 4, y: 5}, + {x: 5, y: 5}, + {x: 6, y: 5} + ] + }, + { + backgroundColor: 'blue', + data: [ + {x: 0, y: 5}, + // { x: 1, y: 0 }, + {x: 2, y: 5}, + {x: 3, y: 5}, + {x: 4, y: 5}, + {x: 5, y: 5}, + {x: 6, y: 5} + ] + }, + { + backgroundColor: 'green', + data: [ + {x: 0, y: 5}, + {x: 1, y: 5}, + // { x: 2, y: 0 }, + {x: 3, y: 5}, + {x: 4, y: 5}, + {x: 5, y: 5}, + {x: 6, y: 5} + ] + }, + { + backgroundColor: 'yellow', + data: [ + {x: 0, y: 5}, + {x: 1, y: 5}, + {x: 2, y: 5}, + // {x: 3, y: 0 }, + {x: 4, y: 5}, + {x: 5, y: 5}, + {x: 6, y: 5} + ] + }, + { + backgroundColor: 'purple', + data: [ + {x: 0, y: 5}, + {x: 1, y: 5}, + {x: 2, y: 5}, + {x: 3, y: 5}, + // { x: 4, y: 0 }, + {x: 5, y: 5}, + {x: 6, y: 5} + ] + }, + { + backgroundColor: 'grey', + data: [ + {x: 0, y: 5}, + {x: 1, y: 5}, + {x: 2, y: 5}, + {x: 3, y: 5}, + {x: 4, y: 5}, + // { x: 5, y: 0 }, + {x: 6, y: 5} + ] + } + ] +}; + +module.exports = { + config: { + type: 'bar', + data: barChartData, + options: { + scales: { + x: { + display: false, + stacked: true + }, + y: { + display: false, + stacked: true + } + } + } + }, + options: { + run(chart) { + chart.data.datasets.splice(0, 1); + chart.update(); + } + } +}; diff --git a/test/fixtures/controller.bar/stacking/remove-dataset.png b/test/fixtures/controller.bar/stacking/remove-dataset.png new file mode 100644 index 00000000000..12c6767c80b Binary files /dev/null and b/test/fixtures/controller.bar/stacking/remove-dataset.png differ diff --git a/test/fixtures/controller.bar/stacking/replace-data.js b/test/fixtures/controller.bar/stacking/replace-data.js new file mode 100644 index 00000000000..2fbea1318dc --- /dev/null +++ b/test/fixtures/controller.bar/stacking/replace-data.js @@ -0,0 +1,50 @@ +var barChartData = { + labels: ['January', 'February', 'March'], + datasets: [ + { + label: 'Dataset 1', + backgroundColor: 'red', + data: [5, 5, 5] + }, + { + label: 'Dataset 2', + backgroundColor: 'blue', + data: [5, 5, 5] + }, + { + label: 'Dataset 3', + backgroundColor: 'green', + data: [5, 5, 5] + } + ] +}; + +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/8614', + config: { + type: 'bar', + data: barChartData, + options: { + scales: { + x: { + display: false, + stacked: true + }, + y: { + display: false, + stacked: true + } + } + } + }, + options: { + run(chart) { + chart.data.datasets[1].data = [ + {x: 'January', y: 5}, + // Februay missing + {x: 'March', y: 5} + ]; + chart.update(); + } + } +}; diff --git a/test/fixtures/controller.bar/stacking/replace-data.png b/test/fixtures/controller.bar/stacking/replace-data.png new file mode 100644 index 00000000000..49a442bd01d Binary files /dev/null and b/test/fixtures/controller.bar/stacking/replace-data.png differ diff --git a/test/fixtures/controller.bar/stacking/stacked-and-multiple-axis.js b/test/fixtures/controller.bar/stacking/stacked-and-multiple-axis.js new file mode 100644 index 00000000000..ca5490a9291 --- /dev/null +++ b/test/fixtures/controller.bar/stacking/stacked-and-multiple-axis.js @@ -0,0 +1,64 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], + datasets: [ + { + label: 'Dataset 1', + data: [100, 90, 100, 50, 99, 87, 34], + backgroundColor: 'rgba(255,99,132,0.8)', + stack: 'a', + xAxisID: 'x' + }, + { + label: 'Dataset 2', + data: [20, 25, 30, 32, 58, 14, 12], + backgroundColor: 'rgba(54,162,235,0.8)', + stack: 'b', + xAxisID: 'x2' + }, + { + label: 'Dataset 3', + data: [80, 30, 40, 60, 70, 80, 47], + backgroundColor: 'rgba(75,192,192,0.8)', + stack: 'a', + xAxisID: 'x3' + }, + { + label: 'Dataset 4', + data: [80, 30, 40, 60, 70, 80, 47], + backgroundColor: 'rgba(54,162,235,0.8)', + stack: 'a', + xAxisID: 'x3' + }, + ] + }, + options: { + plugins: false, + barThickness: 'flex', + scales: { + x: { + stacked: true, + display: false, + }, + x2: { + labels: ['January 2024', 'February 2024', 'March 2024', 'April 2024', 'May 2024', 'June 2024', 'July 2024'], + stacked: true, + display: false, + }, + x3: { + labels: ['January 2025', 'February 2025', 'March 2025', 'April 2025', 'May 2025', 'June 2025', 'July 2025'], + stacked: true, + display: false, + }, + y: { + stacked: true, + display: false, + } + } + } + }, + options: { + } +}; diff --git a/test/fixtures/controller.bar/stacking/stacked-and-multiple-axis.png b/test/fixtures/controller.bar/stacking/stacked-and-multiple-axis.png new file mode 100644 index 00000000000..ea571109e90 Binary files /dev/null and b/test/fixtures/controller.bar/stacking/stacked-and-multiple-axis.png differ diff --git a/test/fixtures/controller.bubble/autoPadding-disabled.js b/test/fixtures/controller.bubble/autoPadding-disabled.js new file mode 100644 index 00000000000..bc0d166cc24 --- /dev/null +++ b/test/fixtures/controller.bubble/autoPadding-disabled.js @@ -0,0 +1,26 @@ +module.exports = { + config: { + type: 'bubble', + data: { + datasets: [{ + backgroundColor: 'red', + data: [{x: 12, y: 54, r: 22.4}] + }, { + backgroundColor: 'blue', + data: [{x: 18, y: 38, r: 25}] + }] + }, + options: { + layout: { + autoPadding: false, + } + } + }, + options: { + spriteText: true, + canvas: { + width: 256, + height: 256 + } + } +}; diff --git a/test/fixtures/controller.bubble/autoPadding-disabled.png b/test/fixtures/controller.bubble/autoPadding-disabled.png new file mode 100644 index 00000000000..deed2ff4f72 Binary files /dev/null and b/test/fixtures/controller.bubble/autoPadding-disabled.png differ diff --git a/test/fixtures/controller.bubble/clip.js b/test/fixtures/controller.bubble/clip.js new file mode 100644 index 00000000000..cf403c972cf --- /dev/null +++ b/test/fixtures/controller.bubble/clip.js @@ -0,0 +1,34 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 5, 10, 15, 20, 25, 30, 50, 55, 60], + datasets: [{ + data: [6, 11, 10, 10, 3, 22, 7, 24], + type: 'bubble', + label: 'test', + borderColor: '#3e95cd', + fill: false + }] + }, + options: { + scales: { + x: {ticks: {display: false}}, + y: { + min: 8, + max: 25, + beginAtZero: true, + ticks: { + display: false + } + } + } + } + }, + options: { + canvas: { + height: 256, + width: 256 + } + } +}; diff --git a/test/fixtures/controller.bubble/clip.png b/test/fixtures/controller.bubble/clip.png new file mode 100644 index 00000000000..7214e4e209c Binary files /dev/null and b/test/fixtures/controller.bubble/clip.png differ diff --git a/test/fixtures/controller.bubble/hover-radius-zero.js b/test/fixtures/controller.bubble/hover-radius-zero.js new file mode 100644 index 00000000000..3628577710b --- /dev/null +++ b/test/fixtures/controller.bubble/hover-radius-zero.js @@ -0,0 +1,48 @@ +module.exports = { + config: { + type: 'bubble', + data: { + labels: [2, 2, 2, 2], + datasets: [{ + data: [ + [1, 1], + [1, 2], + [1, 3, 20], + [1, 4, 20] + ] + }, { + data: [1, 2, 3, 4] + }, { + data: [{x: 3, y: 1}, {x: 3, y: 2}, {x: 3, y: 3, r: 15}, {x: 3, y: 4, r: 15}] + }] + }, + options: { + events: [], + radius: 10, + hoverRadius: 0, + backgroundColor: 'blue', + hoverBackgroundColor: 'red', + scales: { + x: {display: false, bounds: 'data'}, + y: {display: false} + }, + layout: { + padding: 24 + } + } + }, + options: { + canvas: { + height: 256, + width: 256 + }, + run(chart) { + chart.setActiveElements([ + {datasetIndex: 0, index: 1}, {datasetIndex: 0, index: 2}, + {datasetIndex: 1, index: 1}, {datasetIndex: 1, index: 2}, + {datasetIndex: 2, index: 1}, {datasetIndex: 2, index: 2}, + ]); + chart.update(); + } + } +}; diff --git a/test/fixtures/controller.bubble/hover-radius-zero.png b/test/fixtures/controller.bubble/hover-radius-zero.png new file mode 100644 index 00000000000..d86d7ddcaee Binary files /dev/null and b/test/fixtures/controller.bubble/hover-radius-zero.png differ diff --git a/test/fixtures/controller.bubble/padding-update.js b/test/fixtures/controller.bubble/padding-update.js new file mode 100644 index 00000000000..0fd616c45ac --- /dev/null +++ b/test/fixtures/controller.bubble/padding-update.js @@ -0,0 +1,24 @@ +module.exports = { + config: { + type: 'bubble', + data: { + datasets: [{ + backgroundColor: 'red', + data: [{x: 12, y: 54, r: 22.4}] + }, { + backgroundColor: 'blue', + data: [{x: 18, y: 38, r: 25}] + }] + } + }, + options: { + spriteText: true, + canvas: { + width: 256, + height: 256 + }, + run(chart) { + chart.update(); + } + } +}; diff --git a/test/fixtures/controller.bubble/padding-update.png b/test/fixtures/controller.bubble/padding-update.png new file mode 100644 index 00000000000..72a0d6b9c9d Binary files /dev/null and b/test/fixtures/controller.bubble/padding-update.png differ diff --git a/test/fixtures/controller.bubble/padding.js b/test/fixtures/controller.bubble/padding.js new file mode 100644 index 00000000000..4acb79201b5 --- /dev/null +++ b/test/fixtures/controller.bubble/padding.js @@ -0,0 +1,21 @@ +module.exports = { + config: { + type: 'bubble', + data: { + datasets: [{ + backgroundColor: 'red', + data: [{x: 12, y: 54, r: 22.4}] + }, { + backgroundColor: 'blue', + data: [{x: 18, y: 38, r: 25}] + }] + } + }, + options: { + spriteText: true, + canvas: { + width: 256, + height: 256 + } + } +}; diff --git a/test/fixtures/controller.bubble/padding.png b/test/fixtures/controller.bubble/padding.png new file mode 100644 index 00000000000..583120e4819 Binary files /dev/null and b/test/fixtures/controller.bubble/padding.png differ diff --git a/test/fixtures/controller.bubble/point-style.json b/test/fixtures/controller.bubble/point-style.json new file mode 100644 index 00000000000..d341eb11a7e --- /dev/null +++ b/test/fixtures/controller.bubble/point-style.json @@ -0,0 +1,125 @@ +{ + "config": { + "type": "bubble", + "data": { + "datasets": [{ + "data": [ + {"x": 0, "y": 3}, + {"x": 1, "y": 3}, + {"x": 2, "y": 3}, + {"x": 3, "y": 3}, + {"x": 4, "y": 3}, + {"x": 5, "y": 3}, + {"x": 6, "y": 3}, + {"x": 7, "y": 3}, + {"x": 8, "y": 3}, + {"x": 9, "y": 3} + ], + "backgroundColor": "#00ff00", + "borderColor": "transparent", + "borderWidth": 0, + "pointStyle": [ + "circle", + "cross", + "crossRot", + "dash", + "line", + "rect", + "rectRounded", + "rectRot", + "star", + "triangle" + ] + }, { + "data": [ + {"x": 0, "y": 2}, + {"x": 1, "y": 2}, + {"x": 2, "y": 2}, + {"x": 3, "y": 2}, + {"x": 4, "y": 2}, + {"x": 5, "y": 2}, + {"x": 6, "y": 2}, + {"x": 7, "y": 2}, + {"x": 8, "y": 2}, + {"x": 9, "y": 2} + ], + "backgroundColor": "transparent", + "borderColor": "#0000ff", + "borderWidth": 1, + "pointStyle": [ + "circle", + "cross", + "crossRot", + "dash", + "line", + "rect", + "rectRounded", + "rectRot", + "star", + "triangle" + ] + }, { + "data": [ + {"x": 0, "y": 1}, + {"x": 1, "y": 1}, + {"x": 2, "y": 1}, + {"x": 3, "y": 1}, + {"x": 4, "y": 1}, + {"x": 5, "y": 1}, + {"x": 6, "y": 1}, + {"x": 7, "y": 1}, + {"x": 8, "y": 1}, + {"x": 9, "y": 1} + ], + "backgroundColor": "#00ff00", + "borderColor": "#0000ff", + "borderWidth": 1, + "pointStyle": [ + "circle", + "cross", + "crossRot", + "dash", + "line", + "rect", + "rectRounded", + "rectRot", + "star", + "triangle" + ] + }] + }, + "options": { + "responsive": false, + "scales": { + "x": {"display": false}, + "y": { + "display": false, + "min": 0, + "max": 4 + } + }, + "elements": { + "line": { + "borderColor": "transparent", + "borderWidth": 1, + "fill": false + }, + "point": { + "radius": 16 + } + }, + "layout": { + "padding": { + "left": 24, + "right": 24 + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.bubble/point-style.png b/test/fixtures/controller.bubble/point-style.png new file mode 100644 index 00000000000..1957aba0956 Binary files /dev/null and b/test/fixtures/controller.bubble/point-style.png differ diff --git a/test/fixtures/controller.bubble/radius-data.js b/test/fixtures/controller.bubble/radius-data.js new file mode 100644 index 00000000000..bacd6a10f95 --- /dev/null +++ b/test/fixtures/controller.bubble/radius-data.js @@ -0,0 +1,45 @@ +module.exports = { + config: { + type: 'bubble', + data: { + datasets: [{ + data: [ + {x: 0, y: 5, r: 1}, + {x: 1, y: 4, r: 2}, + {x: 2, y: 3, r: 6}, + {x: 3, y: 2}, + {x: 4, y: 1, r: 2}, + {x: 5, y: 0, r: NaN}, + {x: 6, y: -1, r: undefined}, + {x: 7, y: -2, r: null}, + {x: 8, y: -3, r: '4'}, + {x: 9, y: -4, r: '4px'}, + ] + }] + }, + options: { + scales: { + x: {display: false}, + y: {display: false} + }, + elements: { + point: { + backgroundColor: '#444', + radius: 10 + } + }, + layout: { + padding: { + left: 24, + right: 24 + } + } + } + }, + options: { + canvas: { + height: 128, + width: 256 + } + } +}; diff --git a/test/fixtures/controller.bubble/radius-data.png b/test/fixtures/controller.bubble/radius-data.png new file mode 100644 index 00000000000..d565dbdcffa Binary files /dev/null and b/test/fixtures/controller.bubble/radius-data.png differ diff --git a/test/fixtures/controller.bubble/radius-scriptable.js b/test/fixtures/controller.bubble/radius-scriptable.js new file mode 100644 index 00000000000..4ceeac63fe6 --- /dev/null +++ b/test/fixtures/controller.bubble/radius-scriptable.js @@ -0,0 +1,43 @@ +module.exports = { + config: { + type: 'bubble', + data: { + datasets: [{ + data: [ + {x: 0, y: 0}, + {x: 1, y: 0}, + {x: 2, y: 0}, + {x: 3, y: 0}, + {x: 4, y: 0}, + {x: 5, y: 0} + ], + radius: function(ctx) { + return ctx.dataset.data[ctx.dataIndex].x * 4; + } + }] + }, + options: { + scales: { + x: {display: false}, + y: {display: false} + }, + elements: { + point: { + backgroundColor: '#444' + } + }, + layout: { + padding: { + left: 24, + right: 24 + } + } + } + }, + options: { + canvas: { + height: 128, + width: 256 + } + } +}; diff --git a/test/fixtures/controller.bubble/radius-scriptable.png b/test/fixtures/controller.bubble/radius-scriptable.png new file mode 100644 index 00000000000..546466f7192 Binary files /dev/null and b/test/fixtures/controller.bubble/radius-scriptable.png differ diff --git a/test/fixtures/controller.doughnut/backgroundColor/indexable.js b/test/fixtures/controller.doughnut/backgroundColor/indexable.js new file mode 100644 index 00000000000..46a6de396c2 --- /dev/null +++ b/test/fixtures/controller.doughnut/backgroundColor/indexable.js @@ -0,0 +1,46 @@ +module.exports = { + config: { + type: 'doughnut', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 2, 4, null, 6, 8], + backgroundColor: [ + '#ff0000', + '#00ff00', + '#0000ff', + '#ffff00', + '#ff00ff', + '#000000' + ] + }, + { + // option in element (fallback) + data: [0, 2, 4, null, 6, 8], + } + ] + }, + options: { + elements: { + arc: { + backgroundColor: [ + '#ff88ff', + '#888888', + '#ff8800', + '#00ff88', + '#8800ff', + '#ffff88' + ] + } + }, + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.doughnut/backgroundColor/indexable.png b/test/fixtures/controller.doughnut/backgroundColor/indexable.png new file mode 100644 index 00000000000..8dd67b0412f Binary files /dev/null and b/test/fixtures/controller.doughnut/backgroundColor/indexable.png differ diff --git a/test/fixtures/controller.doughnut/backgroundColor/scriptable.js b/test/fixtures/controller.doughnut/backgroundColor/scriptable.js new file mode 100644 index 00000000000..c431349d8e7 --- /dev/null +++ b/test/fixtures/controller.doughnut/backgroundColor/scriptable.js @@ -0,0 +1,44 @@ +module.exports = { + config: { + type: 'doughnut', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 2, 4, null, 6, 8], + backgroundColor: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 8 ? '#ff0000' + : value > 6 ? '#00ff00' + : value > 2 ? '#0000ff' + : '#ff00ff'; + } + }, + { + // option in element (fallback) + data: [0, 2, 4, null, 6, 8], + } + ] + }, + options: { + elements: { + arc: { + backgroundColor: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 8 ? '#ff0000' + : value > 6 ? '#00ff00' + : value > 2 ? '#0000ff' + : '#ff00ff'; + } + } + }, + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.doughnut/backgroundColor/scriptable.png b/test/fixtures/controller.doughnut/backgroundColor/scriptable.png new file mode 100644 index 00000000000..801367ba0d7 Binary files /dev/null and b/test/fixtures/controller.doughnut/backgroundColor/scriptable.png differ diff --git a/test/fixtures/controller.doughnut/backgroundColor/value.js b/test/fixtures/controller.doughnut/backgroundColor/value.js new file mode 100644 index 00000000000..5e887f24417 --- /dev/null +++ b/test/fixtures/controller.doughnut/backgroundColor/value.js @@ -0,0 +1,32 @@ +module.exports = { + config: { + type: 'doughnut', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 2, 4, null, 6, 8], + backgroundColor: '#ff0000' + }, + { + // option in element (fallback) + data: [0, 2, 4, null, 6, 8], + } + ] + }, + options: { + elements: { + arc: { + backgroundColor: '#00ff00' + } + }, + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.doughnut/backgroundColor/value.png b/test/fixtures/controller.doughnut/backgroundColor/value.png new file mode 100644 index 00000000000..25e2c73a72f Binary files /dev/null and b/test/fixtures/controller.doughnut/backgroundColor/value.png differ diff --git a/test/fixtures/controller.doughnut/borderAlign/indexable.js b/test/fixtures/controller.doughnut/borderAlign/indexable.js new file mode 100644 index 00000000000..3b8a79ba505 --- /dev/null +++ b/test/fixtures/controller.doughnut/borderAlign/indexable.js @@ -0,0 +1,50 @@ +module.exports = { + config: { + type: 'doughnut', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 2, 4, null, 6, 8], + borderAlign: [ + 'center', + 'inner', + 'center', + 'inner', + 'center', + 'inner', + ], + borderColor: '#00ff00' + }, + { + // option in element (fallback) + data: [0, 2, 4, null, 6, 8], + } + ] + }, + options: { + elements: { + arc: { + backgroundColor: 'transparent', + borderColor: '#ff0000', + borderWidth: 5, + borderAlign: [ + 'center', + 'inner', + 'center', + 'inner', + 'center', + 'inner', + ] + } + }, + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.doughnut/borderAlign/indexable.png b/test/fixtures/controller.doughnut/borderAlign/indexable.png new file mode 100644 index 00000000000..4043b051e76 Binary files /dev/null and b/test/fixtures/controller.doughnut/borderAlign/indexable.png differ diff --git a/test/fixtures/controller.doughnut/borderAlign/scriptable.js b/test/fixtures/controller.doughnut/borderAlign/scriptable.js new file mode 100644 index 00000000000..27ba4bc43bf --- /dev/null +++ b/test/fixtures/controller.doughnut/borderAlign/scriptable.js @@ -0,0 +1,42 @@ +module.exports = { + config: { + type: 'doughnut', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 2, 4, null, 6, 8], + borderAlign: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 4 ? 'inner' : 'center'; + }, + borderColor: '#0000ff', + }, + { + // option in element (fallback) + data: [0, 2, 4, null, 6, 8], + } + ] + }, + options: { + elements: { + arc: { + backgroundColor: 'transparent', + borderColor: '#ff00ff', + borderWidth: 8, + borderAlign: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 4 ? 'center' : 'inner'; + } + } + }, + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.doughnut/borderAlign/scriptable.png b/test/fixtures/controller.doughnut/borderAlign/scriptable.png new file mode 100644 index 00000000000..ed0030928e5 Binary files /dev/null and b/test/fixtures/controller.doughnut/borderAlign/scriptable.png differ diff --git a/test/fixtures/controller.doughnut/borderAlign/value.js b/test/fixtures/controller.doughnut/borderAlign/value.js new file mode 100644 index 00000000000..31f71190ffb --- /dev/null +++ b/test/fixtures/controller.doughnut/borderAlign/value.js @@ -0,0 +1,36 @@ +module.exports = { + config: { + type: 'doughnut', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 2, 4, null, 6, 8], + borderAlign: 'inner', + borderColor: '#00ff00', + }, + { + // option in element (fallback) + data: [0, 2, 4, null, 6, 8], + } + ] + }, + options: { + elements: { + arc: { + backgroundColor: 'transparent', + borderAlign: 'center', + borderColor: '#0000ff', + borderWidth: 4, + } + }, + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.doughnut/borderAlign/value.png b/test/fixtures/controller.doughnut/borderAlign/value.png new file mode 100644 index 00000000000..0d7b25355cf Binary files /dev/null and b/test/fixtures/controller.doughnut/borderAlign/value.png differ diff --git a/test/fixtures/controller.doughnut/borderColor/indexable.js b/test/fixtures/controller.doughnut/borderColor/indexable.js new file mode 100644 index 00000000000..8035df7363d --- /dev/null +++ b/test/fixtures/controller.doughnut/borderColor/indexable.js @@ -0,0 +1,48 @@ +module.exports = { + config: { + type: 'doughnut', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 2, 4, null, 6, 8], + borderColor: [ + '#ff0000', + '#00ff00', + '#0000ff', + '#ffff00', + '#ff00ff', + '#000000' + ] + }, + { + // option in element (fallback) + data: [0, 2, 4, null, 6, 8], + } + ] + }, + options: { + elements: { + arc: { + backgroundColor: 'transparent', + borderColor: [ + '#ff88ff', + '#888888', + '#ff8800', + '#00ff88', + '#8800ff', + '#ffff88' + ], + borderWidth: 8 + } + }, + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.doughnut/borderColor/indexable.png b/test/fixtures/controller.doughnut/borderColor/indexable.png new file mode 100644 index 00000000000..ccd7b6002a2 Binary files /dev/null and b/test/fixtures/controller.doughnut/borderColor/indexable.png differ diff --git a/test/fixtures/controller.doughnut/borderColor/scriptable.js b/test/fixtures/controller.doughnut/borderColor/scriptable.js new file mode 100644 index 00000000000..95cda7ad7cd --- /dev/null +++ b/test/fixtures/controller.doughnut/borderColor/scriptable.js @@ -0,0 +1,46 @@ +module.exports = { + config: { + type: 'doughnut', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 2, 4, null, 6, 8], + borderColor: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 8 ? '#ff0000' + : value > 6 ? '#00ff00' + : value > 2 ? '#0000ff' + : '#ff00ff'; + } + }, + { + // option in element (fallback) + data: [0, 2, 4, null, 6, 8], + } + ] + }, + options: { + elements: { + arc: { + backgroundColor: 'transparent', + borderColor: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 8 ? '#ff00ff' + : value > 6 ? '#0000ff' + : value > 2 ? '#ff0000' + : '#00ff00'; + }, + borderWidth: 8 + } + }, + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.doughnut/borderColor/scriptable.png b/test/fixtures/controller.doughnut/borderColor/scriptable.png new file mode 100644 index 00000000000..dad1aaa9f4a Binary files /dev/null and b/test/fixtures/controller.doughnut/borderColor/scriptable.png differ diff --git a/test/fixtures/controller.doughnut/borderColor/value.js b/test/fixtures/controller.doughnut/borderColor/value.js new file mode 100644 index 00000000000..04c152f80d2 --- /dev/null +++ b/test/fixtures/controller.doughnut/borderColor/value.js @@ -0,0 +1,34 @@ +module.exports = { + config: { + type: 'doughnut', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 2, 4, null, 6, 8], + borderColor: '#ff0000' + }, + { + // option in element (fallback) + data: [0, 2, 4, null, 6, 8], + } + ] + }, + options: { + elements: { + arc: { + backgroundColor: 'transparent', + borderColor: '#00ff00', + borderWidth: 8 + } + }, + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.doughnut/borderColor/value.png b/test/fixtures/controller.doughnut/borderColor/value.png new file mode 100644 index 00000000000..553d23d733c Binary files /dev/null and b/test/fixtures/controller.doughnut/borderColor/value.png differ diff --git a/test/fixtures/controller.doughnut/borderDash/scriptable.js b/test/fixtures/controller.doughnut/borderDash/scriptable.js new file mode 100644 index 00000000000..d8d2b6900fc --- /dev/null +++ b/test/fixtures/controller.doughnut/borderDash/scriptable.js @@ -0,0 +1,34 @@ +module.exports = { + config: { + type: 'doughnut', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in element (fallback) + data: [5, 2, 4, 7, 6, 8] + } + ] + }, + options: { + elements: { + arc: { + backgroundColor: 'transparent', + borderColor: 'black', + borderWidth: 1, + borderDash: function(ctx) { + var value = (ctx.dataIndex || 0) % 2; + return value === 0 ? [3, 3] : []; + } + + } + }, + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.doughnut/borderDash/scriptable.png b/test/fixtures/controller.doughnut/borderDash/scriptable.png new file mode 100644 index 00000000000..eed3a23fe04 Binary files /dev/null and b/test/fixtures/controller.doughnut/borderDash/scriptable.png differ diff --git a/test/fixtures/controller.doughnut/borderDash/value.js b/test/fixtures/controller.doughnut/borderDash/value.js new file mode 100644 index 00000000000..7e726e16cd4 --- /dev/null +++ b/test/fixtures/controller.doughnut/borderDash/value.js @@ -0,0 +1,36 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [5, 2, 4, 7, 6, 8], + borderAlign: 'inner', + borderColor: 'black' + }, + ] + }, + options: { + elements: { + arc: { + backgroundColor: 'transparent', + borderWidth: 1, + borderDash: [3, 3] + } + }, + scales: { + r: { + display: false + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.doughnut/borderDash/value.png b/test/fixtures/controller.doughnut/borderDash/value.png new file mode 100644 index 00000000000..f7ecdc504aa Binary files /dev/null and b/test/fixtures/controller.doughnut/borderDash/value.png differ diff --git a/test/fixtures/controller.doughnut/borderJoinStyle/bevel-default.js b/test/fixtures/controller.doughnut/borderJoinStyle/bevel-default.js new file mode 100644 index 00000000000..9bb4415ad1b --- /dev/null +++ b/test/fixtures/controller.doughnut/borderJoinStyle/bevel-default.js @@ -0,0 +1,25 @@ +module.exports = { + config: { + type: 'doughnut', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + data: [0, 2, 4, null, 6, 8], + backgroundColor: 'transparent', + borderColor: '#000', + borderWidth: 10, + spacing: 50, + }, + ] + }, + options: { + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.doughnut/borderJoinStyle/bevel-default.png b/test/fixtures/controller.doughnut/borderJoinStyle/bevel-default.png new file mode 100644 index 00000000000..ecb2a2de3e9 Binary files /dev/null and b/test/fixtures/controller.doughnut/borderJoinStyle/bevel-default.png differ diff --git a/test/fixtures/controller.doughnut/borderJoinStyle/miter.js b/test/fixtures/controller.doughnut/borderJoinStyle/miter.js new file mode 100644 index 00000000000..f50e923c085 --- /dev/null +++ b/test/fixtures/controller.doughnut/borderJoinStyle/miter.js @@ -0,0 +1,26 @@ +module.exports = { + config: { + type: 'doughnut', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + data: [0, 2, 4, null, 6, 8], + backgroundColor: 'transparent', + borderColor: '#000', + borderJoinStyle: 'miter', + borderWidth: 10, + spacing: 50, + }, + ] + }, + options: { + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.doughnut/borderJoinStyle/miter.png b/test/fixtures/controller.doughnut/borderJoinStyle/miter.png new file mode 100644 index 00000000000..6ec65b1f031 Binary files /dev/null and b/test/fixtures/controller.doughnut/borderJoinStyle/miter.png differ diff --git a/test/fixtures/controller.doughnut/borderJoinStyle/round.js b/test/fixtures/controller.doughnut/borderJoinStyle/round.js new file mode 100644 index 00000000000..43aa7ca6c51 --- /dev/null +++ b/test/fixtures/controller.doughnut/borderJoinStyle/round.js @@ -0,0 +1,26 @@ +module.exports = { + config: { + type: 'doughnut', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + data: [0, 2, 4, null, 6, 8], + backgroundColor: 'transparent', + borderColor: '#000', + borderJoinStyle: 'round', + borderWidth: 10, + spacing: 50, + }, + ] + }, + options: { + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.doughnut/borderJoinStyle/round.png b/test/fixtures/controller.doughnut/borderJoinStyle/round.png new file mode 100644 index 00000000000..dab62871e83 Binary files /dev/null and b/test/fixtures/controller.doughnut/borderJoinStyle/round.png differ diff --git a/test/fixtures/controller.doughnut/borderRadius/scriptable.js b/test/fixtures/controller.doughnut/borderRadius/scriptable.js new file mode 100644 index 00000000000..9e85810549e --- /dev/null +++ b/test/fixtures/controller.doughnut/borderRadius/scriptable.js @@ -0,0 +1,29 @@ +module.exports = { + config: { + type: 'doughnut', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 2, 4, null, 6, 8], + borderRadius: () => 4, + }, + ] + }, + options: { + elements: { + arc: { + backgroundColor: 'transparent', + borderColor: '#888', + } + }, + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.doughnut/borderRadius/scriptable.png b/test/fixtures/controller.doughnut/borderRadius/scriptable.png new file mode 100644 index 00000000000..15010e3af9c Binary files /dev/null and b/test/fixtures/controller.doughnut/borderRadius/scriptable.png differ diff --git a/test/fixtures/controller.doughnut/borderRadius/value-corners.js b/test/fixtures/controller.doughnut/borderRadius/value-corners.js new file mode 100644 index 00000000000..d6f473115d6 --- /dev/null +++ b/test/fixtures/controller.doughnut/borderRadius/value-corners.js @@ -0,0 +1,32 @@ +module.exports = { + config: { + type: 'doughnut', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 2, 4, null, 6, 8], + borderRadius: { + outerStart: 20, + outerEnd: 40, + } + }, + ] + }, + options: { + elements: { + arc: { + backgroundColor: 'transparent', + borderColor: '#888', + } + }, + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.doughnut/borderRadius/value-corners.png b/test/fixtures/controller.doughnut/borderRadius/value-corners.png new file mode 100644 index 00000000000..ec74b29011f Binary files /dev/null and b/test/fixtures/controller.doughnut/borderRadius/value-corners.png differ diff --git a/test/fixtures/controller.doughnut/borderRadius/value-large-radius.js b/test/fixtures/controller.doughnut/borderRadius/value-large-radius.js new file mode 100644 index 00000000000..141c265d0de --- /dev/null +++ b/test/fixtures/controller.doughnut/borderRadius/value-large-radius.js @@ -0,0 +1,36 @@ +module.exports = { + config: { + type: 'doughnut', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + data: [60, 15, 33, 44, 12], + // Radius is large enough to clip + borderRadius: 200, + backgroundColor: [ + 'rgb(255, 99, 132)', + 'rgb(255, 159, 64)', + 'rgb(255, 205, 86)', + 'rgb(75, 192, 192)', + 'rgb(54, 162, 235)' + ] + }, + ] + }, + // options: { + // elements: { + // arc: { + // backgroundColor: 'transparent', + // borderColor: '#888', + // } + // }, + // } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.doughnut/borderRadius/value-large-radius.png b/test/fixtures/controller.doughnut/borderRadius/value-large-radius.png new file mode 100644 index 00000000000..583e7d202e4 Binary files /dev/null and b/test/fixtures/controller.doughnut/borderRadius/value-large-radius.png differ diff --git a/test/fixtures/controller.doughnut/borderRadius/value-small-number.js b/test/fixtures/controller.doughnut/borderRadius/value-small-number.js new file mode 100644 index 00000000000..31db8434624 --- /dev/null +++ b/test/fixtures/controller.doughnut/borderRadius/value-small-number.js @@ -0,0 +1,29 @@ +module.exports = { + config: { + type: 'doughnut', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 2, 4, null, 6, 8], + borderRadius: 20 + }, + ] + }, + options: { + elements: { + arc: { + backgroundColor: 'transparent', + borderColor: '#888', + } + }, + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.doughnut/borderRadius/value-small-number.png b/test/fixtures/controller.doughnut/borderRadius/value-small-number.png new file mode 100644 index 00000000000..375c053f972 Binary files /dev/null and b/test/fixtures/controller.doughnut/borderRadius/value-small-number.png differ diff --git a/test/fixtures/controller.doughnut/borderWidth/indexable.js b/test/fixtures/controller.doughnut/borderWidth/indexable.js new file mode 100644 index 00000000000..10eb61d7a50 --- /dev/null +++ b/test/fixtures/controller.doughnut/borderWidth/indexable.js @@ -0,0 +1,48 @@ +module.exports = { + config: { + type: 'doughnut', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 2, 4, null, 6, 8], + borderWidth: [ + 0, + 1, + 2, + 3, + 4, + 5 + ] + }, + { + // option in element (fallback) + data: [0, 2, 4, null, 6, 8], + } + ] + }, + options: { + elements: { + arc: { + backgroundColor: 'transparent', + borderColor: '#888', + borderWidth: [ + 5, + 4, + 3, + 2, + 1, + 0 + ] + } + }, + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.doughnut/borderWidth/indexable.png b/test/fixtures/controller.doughnut/borderWidth/indexable.png new file mode 100644 index 00000000000..a1df22b99af Binary files /dev/null and b/test/fixtures/controller.doughnut/borderWidth/indexable.png differ diff --git a/test/fixtures/controller.doughnut/borderWidth/scriptable.js b/test/fixtures/controller.doughnut/borderWidth/scriptable.js new file mode 100644 index 00000000000..94147ae5582 --- /dev/null +++ b/test/fixtures/controller.doughnut/borderWidth/scriptable.js @@ -0,0 +1,39 @@ +module.exports = { + config: { + type: 'doughnut', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 2, 4, null, 6, 8], + borderWidth: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return Math.abs(value); + } + }, + { + // option in element (fallback) + data: [0, 2, 4, null, 6, 8], + } + ] + }, + options: { + elements: { + arc: { + backgroundColor: 'transparent', + borderColor: '#888', + borderWidth: function(ctx) { + return ctx.dataIndex * 2; + } + } + }, + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.doughnut/borderWidth/scriptable.png b/test/fixtures/controller.doughnut/borderWidth/scriptable.png new file mode 100644 index 00000000000..288feb767c9 Binary files /dev/null and b/test/fixtures/controller.doughnut/borderWidth/scriptable.png differ diff --git a/test/fixtures/controller.doughnut/borderWidth/value.js b/test/fixtures/controller.doughnut/borderWidth/value.js new file mode 100644 index 00000000000..52da58db22d --- /dev/null +++ b/test/fixtures/controller.doughnut/borderWidth/value.js @@ -0,0 +1,34 @@ +module.exports = { + config: { + type: 'doughnut', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 2, 4, null, 6, 8], + borderWidth: 2 + }, + { + // option in element (fallback) + data: [0, 2, 4, null, 6, 8], + } + ] + }, + options: { + elements: { + arc: { + backgroundColor: 'transparent', + borderColor: '#888', + borderWidth: 4 + } + }, + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.doughnut/borderWidth/value.png b/test/fixtures/controller.doughnut/borderWidth/value.png new file mode 100644 index 00000000000..92031158d15 Binary files /dev/null and b/test/fixtures/controller.doughnut/borderWidth/value.png differ diff --git a/test/fixtures/controller.doughnut/doughnut-NaN.js b/test/fixtures/controller.doughnut/doughnut-NaN.js new file mode 100644 index 00000000000..47e8d09dfce --- /dev/null +++ b/test/fixtures/controller.doughnut/doughnut-NaN.js @@ -0,0 +1,25 @@ +module.exports = { + config: { + type: 'doughnut', + data: { + labels: ['A', 'B', 'C', 'D', 'E'], + datasets: [{ + data: [1, 5, NaN, 50, 100], + backgroundColor: [ + 'rgba(255, 99, 132, 0.8)', + 'rgba(54, 162, 235, 0.8)', + 'rgba(255, 206, 86, 0.8)', + 'rgba(75, 192, 192, 0.8)', + 'rgba(153, 102, 255, 0.8)' + ], + borderColor: [ + 'rgb(255, 99, 132)', + 'rgb(54, 162, 235)', + 'rgb(255, 206, 86)', + 'rgb(75, 192, 192)', + 'rgb(153, 102, 255)' + ] + }] + }, + } +}; diff --git a/test/fixtures/controller.doughnut/doughnut-NaN.png b/test/fixtures/controller.doughnut/doughnut-NaN.png new file mode 100644 index 00000000000..5f0d8a3d5f9 Binary files /dev/null and b/test/fixtures/controller.doughnut/doughnut-NaN.png differ diff --git a/test/fixtures/controller.doughnut/doughnut-animation-hide-last.js b/test/fixtures/controller.doughnut/doughnut-animation-hide-last.js new file mode 100644 index 00000000000..56af1974bf1 --- /dev/null +++ b/test/fixtures/controller.doughnut/doughnut-animation-hide-last.js @@ -0,0 +1,65 @@ +const canvas = document.createElement('canvas'); +canvas.width = 512; +canvas.height = 512; +const ctx = canvas.getContext('2d'); + +module.exports = { + config: { + type: 'doughnut', + data: { + labels: ['A', 'B', 'C', 'D', 'E'], + datasets: [{ + data: [1], + backgroundColor: 'rgba(255, 99, 132, 0.8)', + borderWidth: 4, + borderColor: 'rgb(255, 99, 132)', + }] + }, + options: { + animation: { + duration: 0, + easing: 'linear', + }, + responsive: false, + plugins: { + legend: false, + title: false, + tooltip: false, + filler: false + } + }, + }, + options: { + canvas: { + height: 512, + width: 512 + }, + run: function(chart) { + chart.options.animation.duration = 8000; + chart.toggleDataVisibility(0); + chart.update(); + const animator = Chart.animator; + // disable animator + const backup = animator._refresh; + animator._refresh = function() { }; + + return new Promise((resolve) => { + window.requestAnimationFrame(() => { + const anims = animator._getAnims(chart); + const start = anims.items[0]._start; + for (let i = 0; i < 16; i++) { + animator._update(start + i * 500); + let x = i % 4 * 128; + let y = Math.floor(i / 4) * 128; + ctx.drawImage(chart.canvas, x, y, 128, 128); + } + Chart.helpers.clearCanvas(chart.canvas); + chart.ctx.drawImage(canvas, 0, 0); + + animator._refresh = backup; + resolve(); + }); + }); + } + } +}; diff --git a/test/fixtures/controller.doughnut/doughnut-animation-hide-last.png b/test/fixtures/controller.doughnut/doughnut-animation-hide-last.png new file mode 100644 index 00000000000..eff14b8f16b Binary files /dev/null and b/test/fixtures/controller.doughnut/doughnut-animation-hide-last.png differ diff --git a/test/fixtures/controller.doughnut/doughnut-animation.js b/test/fixtures/controller.doughnut/doughnut-animation.js new file mode 100644 index 00000000000..281a6847406 --- /dev/null +++ b/test/fixtures/controller.doughnut/doughnut-animation.js @@ -0,0 +1,81 @@ +const canvas = document.createElement('canvas'); +canvas.width = 512; +canvas.height = 512; +const ctx = canvas.getContext('2d'); + +module.exports = { + config: { + type: 'doughnut', + data: { + labels: ['A', 'B', 'C', 'D', 'E'], + datasets: [{ + data: [1, 5, 10, 50, 100], + backgroundColor: [ + 'rgba(255, 99, 132, 0.8)', + 'rgba(54, 162, 235, 0.8)', + 'rgba(255, 206, 86, 0.8)', + 'rgba(75, 192, 192, 0.8)', + 'rgba(153, 102, 255, 0.8)' + ], + borderWidth: 4, + borderColor: [ + 'rgb(255, 99, 132)', + 'rgb(54, 162, 235)', + 'rgb(255, 206, 86)', + 'rgb(75, 192, 192)', + 'rgb(153, 102, 255)' + ] + }] + }, + options: { + animation: { + duration: 8000, + easing: 'linear' + }, + responsive: false, + plugins: { + legend: false, + title: false, + tooltip: false, + filler: false + } + }, + plugins: [{ + id: 'hide', + afterInit(chart) { + chart.toggleDataVisibility(4); + } + }] + }, + options: { + canvas: { + height: 512, + width: 512 + }, + run: function(chart) { + const animator = Chart.animator; + const anims = animator._getAnims(chart); + // disable animator + const backup = animator._refresh; + animator._refresh = function() { }; + + return new Promise((resolve) => { + window.requestAnimationFrame(() => { + + const start = anims.items[0]._start; + for (let i = 0; i < 16; i++) { + animator._update(start + i * 500); + let x = i % 4 * 128; + let y = Math.floor(i / 4) * 128; + ctx.drawImage(chart.canvas, x, y, 128, 128); + } + Chart.helpers.clearCanvas(chart.canvas); + chart.ctx.drawImage(canvas, 0, 0); + + animator._refresh = backup; + resolve(); + }); + }); + } + } +}; diff --git a/test/fixtures/controller.doughnut/doughnut-animation.png b/test/fixtures/controller.doughnut/doughnut-animation.png new file mode 100644 index 00000000000..eb07d1ea79d Binary files /dev/null and b/test/fixtures/controller.doughnut/doughnut-animation.png differ diff --git a/test/fixtures/controller.doughnut/doughnut-border-align-center.json b/test/fixtures/controller.doughnut/doughnut-border-align-center.json new file mode 100644 index 00000000000..cf3fa14ccf5 --- /dev/null +++ b/test/fixtures/controller.doughnut/doughnut-border-align-center.json @@ -0,0 +1,29 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["A", "B", "C", "D", "E"], + "datasets": [{ + "data": [1, 5, 10, 50, 100], + "backgroundColor": [ + "rgba(255, 99, 132, 0.8)", + "rgba(54, 162, 235, 0.8)", + "rgba(255, 206, 86, 0.8)", + "rgba(75, 192, 192, 0.8)", + "rgba(153, 102, 255, 0.8)" + ], + "borderWidth": 20, + "borderColor": [ + "rgb(255, 99, 132)", + "rgb(54, 162, 235)", + "rgb(255, 206, 86)", + "rgb(75, 192, 192)", + "rgb(153, 102, 255)" + ] + }] + }, + "options": { + "responsive": false + } + } +} diff --git a/test/fixtures/controller.doughnut/doughnut-border-align-center.png b/test/fixtures/controller.doughnut/doughnut-border-align-center.png new file mode 100644 index 00000000000..3eec51ff807 Binary files /dev/null and b/test/fixtures/controller.doughnut/doughnut-border-align-center.png differ diff --git a/test/fixtures/controller.doughnut/doughnut-border-align-inner.json b/test/fixtures/controller.doughnut/doughnut-border-align-inner.json new file mode 100644 index 00000000000..6d6c9c0f682 --- /dev/null +++ b/test/fixtures/controller.doughnut/doughnut-border-align-inner.json @@ -0,0 +1,30 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["A", "B", "C", "D", "E"], + "datasets": [{ + "data": [1, 5, 10, 50, 100], + "backgroundColor": [ + "rgba(255, 99, 132, 0.8)", + "rgba(54, 162, 235, 0.8)", + "rgba(255, 206, 86, 0.8)", + "rgba(75, 192, 192, 0.8)", + "rgba(153, 102, 255, 0.8)" + ], + "borderWidth": 20, + "borderColor": [ + "rgb(255, 99, 132)", + "rgb(54, 162, 235)", + "rgb(255, 206, 86)", + "rgb(75, 192, 192)", + "rgb(153, 102, 255)" + ], + "borderAlign": "inner" + }] + }, + "options": { + "responsive": false + } + } +} diff --git a/test/fixtures/controller.doughnut/doughnut-border-align-inner.png b/test/fixtures/controller.doughnut/doughnut-border-align-inner.png new file mode 100644 index 00000000000..d33adf4c206 Binary files /dev/null and b/test/fixtures/controller.doughnut/doughnut-border-align-inner.png differ diff --git a/test/fixtures/controller.doughnut/doughnut-circumference-over-2pi.json b/test/fixtures/controller.doughnut/doughnut-circumference-over-2pi.json new file mode 100644 index 00000000000..5490cac2912 --- /dev/null +++ b/test/fixtures/controller.doughnut/doughnut-circumference-over-2pi.json @@ -0,0 +1,22 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["A"], + "datasets": [{ + "data": [100], + "backgroundColor": [ + "rgba(153, 102, 255, 0.8)" + ], + "borderWidth": 20, + "borderColor": [ + "rgb(153, 102, 255)" + ] + }] + }, + "options": { + "circumference": 400, + "responsive": false + } + } +} diff --git a/test/fixtures/controller.doughnut/doughnut-circumference-over-2pi.png b/test/fixtures/controller.doughnut/doughnut-circumference-over-2pi.png new file mode 100644 index 00000000000..b918ceb921b Binary files /dev/null and b/test/fixtures/controller.doughnut/doughnut-circumference-over-2pi.png differ diff --git a/test/fixtures/controller.doughnut/doughnut-circumference-per-dataset.js b/test/fixtures/controller.doughnut/doughnut-circumference-per-dataset.js new file mode 100644 index 00000000000..98361819e9d --- /dev/null +++ b/test/fixtures/controller.doughnut/doughnut-circumference-per-dataset.js @@ -0,0 +1,31 @@ +module.exports = { + config: { + type: 'doughnut', + data: { + labels: ['A', 'B', 'C', 'D', 'E'], + datasets: [{ + data: [1, 5, 10, 50, 100], + backgroundColor: [ + 'rgba(255, 99, 132, 0.8)', + 'rgba(54, 162, 235, 0.8)', + 'rgba(255, 206, 86, 0.8)', + 'rgba(75, 192, 192, 0.8)', + 'rgba(153, 102, 255, 0.8)' + ], + borderWidth: 1, + borderColor: [ + 'rgb(255, 99, 132)', + 'rgb(54, 162, 235)', + 'rgb(255, 206, 86)', + 'rgb(75, 192, 192)', + 'rgb(153, 102, 255)' + ], + circumference: 180 + }] + }, + options: { + circumference: 57.32, + responsive: false + } + } +}; diff --git a/test/fixtures/controller.doughnut/doughnut-circumference-per-dataset.png b/test/fixtures/controller.doughnut/doughnut-circumference-per-dataset.png new file mode 100644 index 00000000000..7965426bb7e Binary files /dev/null and b/test/fixtures/controller.doughnut/doughnut-circumference-per-dataset.png differ diff --git a/test/fixtures/controller.doughnut/doughnut-circumference.json b/test/fixtures/controller.doughnut/doughnut-circumference.json new file mode 100644 index 00000000000..aa623de6a91 --- /dev/null +++ b/test/fixtures/controller.doughnut/doughnut-circumference.json @@ -0,0 +1,30 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["A", "B", "C", "D", "E"], + "datasets": [{ + "data": [1, 5, 10, 50, 100], + "backgroundColor": [ + "rgba(255, 99, 132, 0.8)", + "rgba(54, 162, 235, 0.8)", + "rgba(255, 206, 86, 0.8)", + "rgba(75, 192, 192, 0.8)", + "rgba(153, 102, 255, 0.8)" + ], + "borderWidth": 20, + "borderColor": [ + "rgb(255, 99, 132)", + "rgb(54, 162, 235)", + "rgb(255, 206, 86)", + "rgb(75, 192, 192)", + "rgb(153, 102, 255)" + ] + }] + }, + "options": { + "circumference": 57.32, + "responsive": false + } + } +} diff --git a/test/fixtures/controller.doughnut/doughnut-circumference.png b/test/fixtures/controller.doughnut/doughnut-circumference.png new file mode 100644 index 00000000000..e5bf2074091 Binary files /dev/null and b/test/fixtures/controller.doughnut/doughnut-circumference.png differ diff --git a/test/fixtures/controller.doughnut/doughnut-full-to-semi.js b/test/fixtures/controller.doughnut/doughnut-full-to-semi.js new file mode 100644 index 00000000000..3e7058a8c41 --- /dev/null +++ b/test/fixtures/controller.doughnut/doughnut-full-to-semi.js @@ -0,0 +1,33 @@ +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/9832', + config: { + type: 'doughnut', + data: { + datasets: [{ + label: 'Set 1', + data: [50, 50, 25], + backgroundColor: ['#BF616A', '#D08770', '#EBCB8B'], + borderWidth: 0 + }, + { + label: 'Se1 2', + data: [50, 50, 25], + backgroundColor: ['#BF616A', '#D08770', '#EBCB8B'], + borderWidth: 0 + }] + }, + options: { + rotation: -90 + } + }, + options: { + canvas: { + width: 512, + height: 512 + }, + run(chart) { + chart.options.circumference = 180; + chart.update(); + } + } +}; diff --git a/test/fixtures/controller.doughnut/doughnut-full-to-semi.png b/test/fixtures/controller.doughnut/doughnut-full-to-semi.png new file mode 100644 index 00000000000..0585b4922f0 Binary files /dev/null and b/test/fixtures/controller.doughnut/doughnut-full-to-semi.png differ diff --git a/test/fixtures/controller.doughnut/doughnut-hidden-single.js b/test/fixtures/controller.doughnut/doughnut-hidden-single.js new file mode 100644 index 00000000000..80f0ee9f795 --- /dev/null +++ b/test/fixtures/controller.doughnut/doughnut-hidden-single.js @@ -0,0 +1,42 @@ +module.exports = { + config: { + type: 'doughnut', + data: { + labels: ['A', 'B', 'C', 'D', 'E'], + datasets: [{ + data: [1, 5, 10, 50, 100], + backgroundColor: [ + 'rgba(255, 99, 132, 0.8)', + 'rgba(54, 162, 235, 0.8)', + 'rgba(255, 206, 86, 0.8)', + 'rgba(75, 192, 192, 0.8)', + 'rgba(153, 102, 255, 0.8)' + ], + }, { + data: [1, 5, 10, 50, 100], + backgroundColor: [ + 'rgba(255, 99, 132, 0.8)', + 'rgba(54, 162, 235, 0.8)', + 'rgba(255, 206, 86, 0.8)', + 'rgba(75, 192, 192, 0.8)', + 'rgba(153, 102, 255, 0.8)' + ], + }] + }, + options: { + responsive: false, + plugins: { + legend: false, + title: false, + tooltip: false, + filler: false + } + }, + }, + options: { + run(chart) { + chart.hide(0, 4); + chart.hide(1, 2); + } + } +}; diff --git a/test/fixtures/controller.doughnut/doughnut-hidden-single.png b/test/fixtures/controller.doughnut/doughnut-hidden-single.png new file mode 100644 index 00000000000..5cfbe2c02d2 Binary files /dev/null and b/test/fixtures/controller.doughnut/doughnut-hidden-single.png differ diff --git a/test/fixtures/controller.doughnut/doughnut-hidden.js b/test/fixtures/controller.doughnut/doughnut-hidden.js new file mode 100644 index 00000000000..07d0f8aca14 --- /dev/null +++ b/test/fixtures/controller.doughnut/doughnut-hidden.js @@ -0,0 +1,41 @@ +module.exports = { + config: { + type: 'doughnut', + data: { + labels: ['A', 'B', 'C', 'D', 'E'], + datasets: [{ + data: [1, 5, 10, 50, 100], + backgroundColor: [ + 'rgba(255, 99, 132, 0.8)', + 'rgba(54, 162, 235, 0.8)', + 'rgba(255, 206, 86, 0.8)', + 'rgba(75, 192, 192, 0.8)', + 'rgba(153, 102, 255, 0.8)' + ], + borderWidth: 4, + borderColor: [ + 'rgb(255, 99, 132)', + 'rgb(54, 162, 235)', + 'rgb(255, 206, 86)', + 'rgb(75, 192, 192)', + 'rgb(153, 102, 255)' + ] + }] + }, + options: { + responsive: false, + plugins: { + legend: false, + title: false, + tooltip: false, + filler: false + } + }, + plugins: [{ + id: 'hide', + afterInit(chart) { + chart.toggleDataVisibility(4); + } + }] + } +}; diff --git a/test/fixtures/controller.doughnut/doughnut-hidden.png b/test/fixtures/controller.doughnut/doughnut-hidden.png new file mode 100644 index 00000000000..bb02a9d1fba Binary files /dev/null and b/test/fixtures/controller.doughnut/doughnut-hidden.png differ diff --git a/test/fixtures/controller.doughnut/doughnut-offset.js b/test/fixtures/controller.doughnut/doughnut-offset.js new file mode 100644 index 00000000000..b1ffd32e1d3 --- /dev/null +++ b/test/fixtures/controller.doughnut/doughnut-offset.js @@ -0,0 +1,18 @@ +module.exports = { + config: { + type: 'doughnut', + data: { + labels: ['Red', 'Blue', 'Yellow'], + datasets: [{ + data: [12, 4, 6], + backgroundColor: ['red', 'blue', 'yellow'] + }] + }, + options: { + offset: 40, + layout: { + padding: 50 + } + } + } +}; diff --git a/test/fixtures/controller.doughnut/doughnut-offset.png b/test/fixtures/controller.doughnut/doughnut-offset.png new file mode 100644 index 00000000000..a45212d6b38 Binary files /dev/null and b/test/fixtures/controller.doughnut/doughnut-offset.png differ diff --git a/test/fixtures/controller.doughnut/doughnut-outer-radius-percent.js b/test/fixtures/controller.doughnut/doughnut-outer-radius-percent.js new file mode 100644 index 00000000000..46cf4b7200b --- /dev/null +++ b/test/fixtures/controller.doughnut/doughnut-outer-radius-percent.js @@ -0,0 +1,28 @@ +module.exports = { + config: { + type: 'doughnut', + data: { + labels: ['A', 'B', 'C', 'D', 'E'], + datasets: [{ + data: [1, 5, 10, 50, 100], + backgroundColor: [ + 'rgba(255, 99, 132, 0.8)', + 'rgba(54, 162, 235, 0.8)', + 'rgba(255, 206, 86, 0.8)', + 'rgba(75, 192, 192, 0.8)', + 'rgba(153, 102, 255, 0.8)' + ], + borderColor: [ + 'rgb(255, 99, 132)', + 'rgb(54, 162, 235)', + 'rgb(255, 206, 86)', + 'rgb(75, 192, 192)', + 'rgb(153, 102, 255)' + ] + }] + }, + options: { + radius: '30%', + } + } +}; diff --git a/test/fixtures/controller.doughnut/doughnut-outer-radius-percent.png b/test/fixtures/controller.doughnut/doughnut-outer-radius-percent.png new file mode 100644 index 00000000000..f2f0dcdff6f Binary files /dev/null and b/test/fixtures/controller.doughnut/doughnut-outer-radius-percent.png differ diff --git a/test/fixtures/controller.doughnut/doughnut-outer-radius-pixels.js b/test/fixtures/controller.doughnut/doughnut-outer-radius-pixels.js new file mode 100644 index 00000000000..8fabaa935ac --- /dev/null +++ b/test/fixtures/controller.doughnut/doughnut-outer-radius-pixels.js @@ -0,0 +1,28 @@ +module.exports = { + config: { + type: 'doughnut', + data: { + labels: ['A', 'B', 'C', 'D', 'E'], + datasets: [{ + data: [1, 5, 10, 50, 100], + backgroundColor: [ + 'rgba(255, 99, 132, 0.8)', + 'rgba(54, 162, 235, 0.8)', + 'rgba(255, 206, 86, 0.8)', + 'rgba(75, 192, 192, 0.8)', + 'rgba(153, 102, 255, 0.8)' + ], + borderColor: [ + 'rgb(255, 99, 132)', + 'rgb(54, 162, 235)', + 'rgb(255, 206, 86)', + 'rgb(75, 192, 192)', + 'rgb(153, 102, 255)' + ] + }] + }, + options: { + radius: 150, + } + } +}; diff --git a/test/fixtures/controller.doughnut/doughnut-outer-radius-pixels.png b/test/fixtures/controller.doughnut/doughnut-outer-radius-pixels.png new file mode 100644 index 00000000000..195218cab3b Binary files /dev/null and b/test/fixtures/controller.doughnut/doughnut-outer-radius-pixels.png differ diff --git a/test/fixtures/controller.doughnut/doughnut-parsing.js b/test/fixtures/controller.doughnut/doughnut-parsing.js new file mode 100644 index 00000000000..f9043b03313 --- /dev/null +++ b/test/fixtures/controller.doughnut/doughnut-parsing.js @@ -0,0 +1,21 @@ +module.exports = { + config: { + type: 'doughnut', + data: { + labels: ['Red', 'Blue', 'Yellow'], + datasets: [{ + data: [ + {foo: 12}, + {foo: 4}, + {foo: 6}, + ], + backgroundColor: ['red', 'blue', 'yellow'] + }] + }, + options: { + parsing: { + key: 'foo' + } + } + } +}; diff --git a/test/fixtures/controller.doughnut/doughnut-parsing.png b/test/fixtures/controller.doughnut/doughnut-parsing.png new file mode 100644 index 00000000000..a01d23fd2c4 Binary files /dev/null and b/test/fixtures/controller.doughnut/doughnut-parsing.png differ diff --git a/test/fixtures/controller.doughnut/doughnut-rotation-300.js b/test/fixtures/controller.doughnut/doughnut-rotation-300.js new file mode 100644 index 00000000000..f4fa4242b3d --- /dev/null +++ b/test/fixtures/controller.doughnut/doughnut-rotation-300.js @@ -0,0 +1,28 @@ +module.exports = { + config: { + type: 'doughnut', + data: { + labels: ['A', 'B', 'C', 'D', 'E'], + datasets: [{ + data: [1, 5, 10, 50, 100], + backgroundColor: [ + 'rgba(255, 99, 132, 0.8)', + 'rgba(54, 162, 235, 0.8)', + 'rgba(255, 206, 86, 0.8)', + 'rgba(75, 192, 192, 0.8)', + 'rgba(153, 102, 255, 0.8)' + ], + borderColor: [ + 'rgb(255, 99, 132)', + 'rgb(54, 162, 235)', + 'rgb(255, 206, 86)', + 'rgb(75, 192, 192)', + 'rgb(153, 102, 255)' + ] + }] + }, + options: { + rotation: 300 + } + } +}; diff --git a/test/fixtures/controller.doughnut/doughnut-rotation-300.png b/test/fixtures/controller.doughnut/doughnut-rotation-300.png new file mode 100644 index 00000000000..9b8bc86c14c Binary files /dev/null and b/test/fixtures/controller.doughnut/doughnut-rotation-300.png differ diff --git a/test/fixtures/controller.doughnut/doughnut-rotation-circumference-8x8.js b/test/fixtures/controller.doughnut/doughnut-rotation-circumference-8x8.js new file mode 100644 index 00000000000..4a0481ef8f2 --- /dev/null +++ b/test/fixtures/controller.doughnut/doughnut-rotation-circumference-8x8.js @@ -0,0 +1,67 @@ +const canvas = document.createElement('canvas'); +canvas.width = 512; +canvas.height = 512; +const ctx = canvas.getContext('2d'); + +module.exports = { + config: { + type: 'doughnut', + data: { + labels: ['A', 'B', 'C', 'D', 'E'], + datasets: [{ + data: [1, 5, 10, 50, 100], + backgroundColor: [ + 'rgba(255, 99, 132, 0.8)', + 'rgba(54, 162, 235, 0.8)', + 'rgba(255, 206, 86, 0.8)', + 'rgba(75, 192, 192, 0.8)', + 'rgba(153, 102, 255, 0.8)' + ], + borderColor: [ + 'rgb(255, 99, 132)', + 'rgb(54, 162, 235)', + 'rgb(255, 206, 86)', + 'rgb(75, 192, 192)', + 'rgb(153, 102, 255)' + ] + }] + }, + options: { + rotation: -360, + circumference: 180, + events: [] + } + }, + options: { + canvas: { + height: 512, + width: 512 + }, + run: function(chart) { + return new Promise((resolve) => { + for (let i = 0; i < 64; i++) { + const col = i % 8; + const row = Math.floor(i / 8); + const evenodd = row % 2 ? 1 : -1; + chart.options.rotation = col * 45 * evenodd; + chart.options.circumference = 360 - row * 45; + chart.update(); + ctx.drawImage(chart.canvas, col * 64, row * 64, 64, 64); + } + ctx.strokeStyle = 'red'; + ctx.lineWidth = 0.5; + ctx.beginPath(); + for (let i = 1; i < 8; i++) { + ctx.moveTo(i * 64, 0); + ctx.lineTo(i * 64, 511); + ctx.moveTo(0, i * 64); + ctx.lineTo(511, i * 64); + } + ctx.stroke(); + Chart.helpers.clearCanvas(chart.canvas); + chart.ctx.drawImage(canvas, 0, 0); + resolve(); + }); + } + } +}; diff --git a/test/fixtures/controller.doughnut/doughnut-rotation-circumference-8x8.png b/test/fixtures/controller.doughnut/doughnut-rotation-circumference-8x8.png new file mode 100644 index 00000000000..ec18f135afa Binary files /dev/null and b/test/fixtures/controller.doughnut/doughnut-rotation-circumference-8x8.png differ diff --git a/test/fixtures/controller.doughnut/doughnut-rotation-per-dataset.js b/test/fixtures/controller.doughnut/doughnut-rotation-per-dataset.js new file mode 100644 index 00000000000..7d604ca8c26 --- /dev/null +++ b/test/fixtures/controller.doughnut/doughnut-rotation-per-dataset.js @@ -0,0 +1,49 @@ +module.exports = { + config: { + type: 'doughnut', + data: { + labels: ['A', 'B', 'C', 'D', 'E'], + datasets: [{ + data: [1, 5, 10, 50, 100], + backgroundColor: [ + 'rgba(255, 99, 132, 0.8)', + 'rgba(54, 162, 235, 0.8)', + 'rgba(255, 206, 86, 0.8)', + 'rgba(75, 192, 192, 0.8)', + 'rgba(153, 102, 255, 0.8)' + ], + borderWidth: 1, + borderColor: [ + 'rgb(255, 99, 132)', + 'rgb(54, 162, 235)', + 'rgb(255, 206, 86)', + 'rgb(75, 192, 192)', + 'rgb(153, 102, 255)' + ], + rotation: -90 + }, { + data: [1, 5, 10, 50, 100], + backgroundColor: [ + 'rgba(255, 99, 132, 0.8)', + 'rgba(54, 162, 235, 0.8)', + 'rgba(255, 206, 86, 0.8)', + 'rgba(75, 192, 192, 0.8)', + 'rgba(153, 102, 255, 0.8)' + ], + borderWidth: 1, + borderColor: [ + 'rgb(255, 99, 132)', + 'rgb(54, 162, 235)', + 'rgb(255, 206, 86)', + 'rgb(75, 192, 192)', + 'rgb(153, 102, 255)' + ], + rotation: 0 + }] + }, + options: { + circumference: 180, + responsive: false + } + } +}; diff --git a/test/fixtures/controller.doughnut/doughnut-rotation-per-dataset.png b/test/fixtures/controller.doughnut/doughnut-rotation-per-dataset.png new file mode 100644 index 00000000000..081367d1fef Binary files /dev/null and b/test/fixtures/controller.doughnut/doughnut-rotation-per-dataset.png differ diff --git a/test/fixtures/controller.doughnut/doughnut-set-active-elements.js b/test/fixtures/controller.doughnut/doughnut-set-active-elements.js new file mode 100644 index 00000000000..7a9a778a92f --- /dev/null +++ b/test/fixtures/controller.doughnut/doughnut-set-active-elements.js @@ -0,0 +1,27 @@ +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/9248', + config: { + type: 'doughnut', + data: { + datasets: [ + { + data: [34, 33, 17, 16], + backgroundColor: ['#D92323', '#E45757', '#ED8D8D', '#F5C4C4'] + } + ] + }, + options: { + events: [], // for easier saving of the fixture only + borderWidth: 0, + hoverBorderWidth: 4, + hoverBorderColor: 'black', + cutout: '80%', + } + }, + options: { + run(chart) { + chart.setActiveElements([{datasetIndex: 0, index: 1}]); + chart.update(); + } + } +}; diff --git a/test/fixtures/controller.doughnut/doughnut-set-active-elements.png b/test/fixtures/controller.doughnut/doughnut-set-active-elements.png new file mode 100644 index 00000000000..14b0c39c022 Binary files /dev/null and b/test/fixtures/controller.doughnut/doughnut-set-active-elements.png differ diff --git a/test/fixtures/controller.doughnut/doughnut-spacing-and-offset.js b/test/fixtures/controller.doughnut/doughnut-spacing-and-offset.js new file mode 100644 index 00000000000..d2e0c59b07a --- /dev/null +++ b/test/fixtures/controller.doughnut/doughnut-spacing-and-offset.js @@ -0,0 +1,29 @@ +module.exports = { + config: { + type: 'doughnut', + data: { + datasets: [{ + data: [10, 20, 40, 50, 5], + label: 'Dataset 1', + backgroundColor: [ + 'red', + 'orange', + 'yellow', + 'green', + 'blue' + ] + }], + labels: [ + 'Item 1', + 'Item 2', + 'Item 3', + 'Item 4', + 'Item 5' + ], + }, + options: { + spacing: 50, + offset: [0, 50, 0, 0, 0], + } + } +}; diff --git a/test/fixtures/controller.doughnut/doughnut-spacing-and-offset.png b/test/fixtures/controller.doughnut/doughnut-spacing-and-offset.png new file mode 100644 index 00000000000..0a68820fa01 Binary files /dev/null and b/test/fixtures/controller.doughnut/doughnut-spacing-and-offset.png differ diff --git a/test/fixtures/controller.doughnut/doughnut-spacing.js b/test/fixtures/controller.doughnut/doughnut-spacing.js new file mode 100644 index 00000000000..c6c84379231 --- /dev/null +++ b/test/fixtures/controller.doughnut/doughnut-spacing.js @@ -0,0 +1,28 @@ +module.exports = { + config: { + type: 'doughnut', + data: { + datasets: [{ + data: [10, 20, 40, 50, 5], + label: 'Dataset 1', + backgroundColor: [ + 'red', + 'orange', + 'yellow', + 'green', + 'blue' + ] + }], + labels: [ + 'Item 1', + 'Item 2', + 'Item 3', + 'Item 4', + 'Item 5' + ], + }, + options: { + spacing: 50, + } + } +}; diff --git a/test/fixtures/controller.doughnut/doughnut-spacing.png b/test/fixtures/controller.doughnut/doughnut-spacing.png new file mode 100644 index 00000000000..d586621e980 Binary files /dev/null and b/test/fixtures/controller.doughnut/doughnut-spacing.png differ diff --git a/test/fixtures/controller.doughnut/doughnut-weight.json b/test/fixtures/controller.doughnut/doughnut-weight.json new file mode 100644 index 00000000000..468f78d7870 --- /dev/null +++ b/test/fixtures/controller.doughnut/doughnut-weight.json @@ -0,0 +1,46 @@ +{ + "config": { + "type": "doughnut", + "data": { + "datasets": [{ + "data": [ 1, 1 ], + "backgroundColor": [ + "rgba(255, 99, 132, 0.8)", + "rgba(54, 162, 235, 0.8)" + ], + "borderWidth": 0 + }, + { + "data": [ 2, 1 ], + "hidden": true, + "borderWidth": 0 + }, + { + "data": [ 3, 3 ], + "weight": 3, + "backgroundColor": [ + "rgba(255, 206, 86, 0.8)", + "rgba(75, 192, 192, 0.8)" + ], + "borderWidth": 0 + }, + { + "data": [ 4, 0 ], + "weight": 0, + "borderWidth": 0 + }, + { + "data": [ 5, 0 ], + "weight": -2, + "borderWidth": 0 + }], + "labels": [ "label0", "label1" ] + } + }, + "options": { + "canvas": { + "height": 500, + "width": 500 + } + } +} diff --git a/test/fixtures/controller.doughnut/doughnut-weight.png b/test/fixtures/controller.doughnut/doughnut-weight.png new file mode 100644 index 00000000000..d6ab34c8a9a Binary files /dev/null and b/test/fixtures/controller.doughnut/doughnut-weight.png differ diff --git a/test/fixtures/controller.doughnut/event-replay.js b/test/fixtures/controller.doughnut/event-replay.js new file mode 100644 index 00000000000..fc367c35865 --- /dev/null +++ b/test/fixtures/controller.doughnut/event-replay.js @@ -0,0 +1,50 @@ +function drawMousePoint(ctx, center) { + ctx.beginPath(); + ctx.arc(center.x, center.y, 8, 0, Math.PI * 2); + ctx.fillStyle = 'yellow'; + ctx.fill(); +} + +const canvas = document.createElement('canvas'); +canvas.width = 512; +canvas.height = 512; +const ctx = canvas.getContext('2d'); + +module.exports = { + config: { + type: 'pie', + data: { + datasets: [{ + backgroundColor: ['red', 'green', 'blue'], + hoverBackgroundColor: 'black', + data: [1, 1, 1] + }] + } + }, + options: { + canvas: { + width: 512, + height: 512 + }, + async run(chart) { + ctx.drawImage(chart.canvas, 0, 0, 256, 256); + + const arc = chart.getDatasetMeta(0).data[0]; + const center = arc.getCenterPoint(); + await jasmine.triggerMouseEvent(chart, 'mousemove', arc); + drawMousePoint(chart.ctx, center); + ctx.drawImage(chart.canvas, 256, 0, 256, 256); + + chart.toggleDataVisibility(0); + chart.update(); + drawMousePoint(chart.ctx, center); + ctx.drawImage(chart.canvas, 0, 256, 256, 256); + + await jasmine.triggerMouseEvent(chart, 'mouseout', arc); + ctx.drawImage(chart.canvas, 256, 256, 256, 256); + + Chart.helpers.clearCanvas(chart.canvas); + chart.ctx.drawImage(canvas, 0, 0); + } + } +}; diff --git a/test/fixtures/controller.doughnut/event-replay.png b/test/fixtures/controller.doughnut/event-replay.png new file mode 100644 index 00000000000..5d14360699b Binary files /dev/null and b/test/fixtures/controller.doughnut/event-replay.png differ diff --git a/test/fixtures/controller.doughnut/pie-border-align-center.json b/test/fixtures/controller.doughnut/pie-border-align-center.json new file mode 100644 index 00000000000..3ea498b8924 --- /dev/null +++ b/test/fixtures/controller.doughnut/pie-border-align-center.json @@ -0,0 +1,29 @@ +{ + "config": { + "type": "pie", + "data": { + "labels": ["A", "B", "C", "D", "E"], + "datasets": [{ + "data": [1, 5, 10, 50, 100], + "backgroundColor": [ + "rgba(255, 99, 132, 0.8)", + "rgba(54, 162, 235, 0.8)", + "rgba(255, 206, 86, 0.8)", + "rgba(75, 192, 192, 0.8)", + "rgba(153, 102, 255, 0.8)" + ], + "borderWidth": 20, + "borderColor": [ + "rgb(255, 99, 132)", + "rgb(54, 162, 235)", + "rgb(255, 206, 86)", + "rgb(75, 192, 192)", + "rgb(153, 102, 255)" + ] + }] + }, + "options": { + "responsive": false + } + } +} diff --git a/test/fixtures/controller.doughnut/pie-border-align-center.png b/test/fixtures/controller.doughnut/pie-border-align-center.png new file mode 100644 index 00000000000..77070fe6fa1 Binary files /dev/null and b/test/fixtures/controller.doughnut/pie-border-align-center.png differ diff --git a/test/fixtures/controller.doughnut/pie-border-align-inner.json b/test/fixtures/controller.doughnut/pie-border-align-inner.json new file mode 100644 index 00000000000..ed91a9567f3 --- /dev/null +++ b/test/fixtures/controller.doughnut/pie-border-align-inner.json @@ -0,0 +1,35 @@ +{ + "config": { + "type": "pie", + "data": { + "labels": ["A", "B", "C", "D", "E"], + "datasets": [{ + "data": [1, 5, 10, 50, 100], + "backgroundColor": [ + "rgba(255, 99, 132, 0.8)", + "rgba(54, 162, 235, 0.8)", + "rgba(255, 206, 86, 0.8)", + "rgba(75, 192, 192, 0.8)", + "rgba(153, 102, 255, 0.8)" + ], + "borderWidth": 20, + "borderColor": [ + "rgb(255, 99, 132)", + "rgb(54, 162, 235)", + "rgb(255, 206, 86)", + "rgb(75, 192, 192)", + "rgb(153, 102, 255)" + ], + "borderAlign": "inner" + }] + }, + "options": { + "responsive": false, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + } +} diff --git a/test/fixtures/controller.doughnut/pie-border-align-inner.png b/test/fixtures/controller.doughnut/pie-border-align-inner.png new file mode 100644 index 00000000000..a2f348eb7dd Binary files /dev/null and b/test/fixtures/controller.doughnut/pie-border-align-inner.png differ diff --git a/test/fixtures/controller.doughnut/pie-circumference.json b/test/fixtures/controller.doughnut/pie-circumference.json new file mode 100644 index 00000000000..39405f1fead --- /dev/null +++ b/test/fixtures/controller.doughnut/pie-circumference.json @@ -0,0 +1,30 @@ +{ + "config": { + "type": "pie", + "data": { + "labels": ["A", "B", "C", "D", "E"], + "datasets": [{ + "data": [1, 5, 10, 50, 100], + "backgroundColor": [ + "rgba(255, 99, 132, 0.8)", + "rgba(54, 162, 235, 0.8)", + "rgba(255, 206, 86, 0.8)", + "rgba(75, 192, 192, 0.8)", + "rgba(153, 102, 255, 0.8)" + ], + "borderWidth": 20, + "borderColor": [ + "rgb(255, 99, 132)", + "rgb(54, 162, 235)", + "rgb(255, 206, 86)", + "rgb(75, 192, 192)", + "rgb(153, 102, 255)" + ] + }] + }, + "options": { + "circumference": 57.32, + "responsive": false + } + } +} diff --git a/test/fixtures/controller.doughnut/pie-circumference.png b/test/fixtures/controller.doughnut/pie-circumference.png new file mode 100644 index 00000000000..7b4a631dbec Binary files /dev/null and b/test/fixtures/controller.doughnut/pie-circumference.png differ diff --git a/test/fixtures/controller.doughnut/pie-offset.js b/test/fixtures/controller.doughnut/pie-offset.js new file mode 100644 index 00000000000..05689f4b40a --- /dev/null +++ b/test/fixtures/controller.doughnut/pie-offset.js @@ -0,0 +1,18 @@ +module.exports = { + config: { + type: 'pie', + data: { + labels: ['Red', 'Blue', 'Yellow'], + datasets: [{ + data: [12, 4, 6], + backgroundColor: ['red', 'blue', 'yellow'] + }] + }, + options: { + offset: 40, + layout: { + padding: 50 + } + } + } +}; diff --git a/test/fixtures/controller.doughnut/pie-offset.png b/test/fixtures/controller.doughnut/pie-offset.png new file mode 100644 index 00000000000..6697d8acd0c Binary files /dev/null and b/test/fixtures/controller.doughnut/pie-offset.png differ diff --git a/test/fixtures/controller.doughnut/pie-weight.json b/test/fixtures/controller.doughnut/pie-weight.json new file mode 100644 index 00000000000..6abcbc95db5 --- /dev/null +++ b/test/fixtures/controller.doughnut/pie-weight.json @@ -0,0 +1,48 @@ +{ + "config": { + "type": "pie", + "data": { + "datasets": [ + { + "data": [ 1, 1 ], + "backgroundColor": [ + "rgba(255, 99, 132, 0.8)", + "rgba(54, 162, 235, 0.8)" + ], + "borderWidth": 0 + }, + { + "data": [ 2, 1 ], + "hidden": true, + "borderWidth": 0 + }, + { + "data": [ 3, 3 ], + "weight": 3, + "backgroundColor": [ + "rgba(255, 206, 86, 0.8)", + "rgba(75, 192, 192, 0.8)" + ], + "borderWidth": 0 + }, + { + "data": [ 4, 0 ], + "weight": 0, + "borderWidth": 0 + }, + { + "data": [ 5, 0 ], + "weight": -2, + "borderWidth": 0 + } + ], + "labels": [ "label0", "label1" ] + } + }, + "options": { + "canvas": { + "height": 500, + "width": 500 + } + } +} diff --git a/test/fixtures/controller.doughnut/pie-weight.png b/test/fixtures/controller.doughnut/pie-weight.png new file mode 100644 index 00000000000..606ae0ebc59 Binary files /dev/null and b/test/fixtures/controller.doughnut/pie-weight.png differ diff --git a/test/fixtures/controller.doughnut/selfJoin/doughnut.js b/test/fixtures/controller.doughnut/selfJoin/doughnut.js new file mode 100644 index 00000000000..f29939cec2a --- /dev/null +++ b/test/fixtures/controller.doughnut/selfJoin/doughnut.js @@ -0,0 +1,25 @@ +module.exports = { + config: { + type: 'doughnut', + data: { + labels: ['Red'], + datasets: [ + { + // option in dataset + data: [100], + borderWidth: 15, + backgroundColor: '#FF0000', + borderColor: '#000000', + borderAlign: 'center', + selfJoin: true + } + ] + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.doughnut/selfJoin/doughnut.png b/test/fixtures/controller.doughnut/selfJoin/doughnut.png new file mode 100644 index 00000000000..af2d4b43873 Binary files /dev/null and b/test/fixtures/controller.doughnut/selfJoin/doughnut.png differ diff --git a/test/fixtures/controller.doughnut/selfJoin/pie.js b/test/fixtures/controller.doughnut/selfJoin/pie.js new file mode 100644 index 00000000000..d0187db0917 --- /dev/null +++ b/test/fixtures/controller.doughnut/selfJoin/pie.js @@ -0,0 +1,26 @@ +module.exports = { + config: { + type: 'pie', + data: { + labels: ['Red'], + datasets: [ + { + // option in dataset + data: [100], + borderWidth: 15, + backgroundColor: '#FF0000', + borderColor: '#000000', + borderAlign: 'center', + borderJoinStyle: 'round', + selfJoin: true + } + ] + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.doughnut/selfJoin/pie.png b/test/fixtures/controller.doughnut/selfJoin/pie.png new file mode 100644 index 00000000000..17a2e3b1951 Binary files /dev/null and b/test/fixtures/controller.doughnut/selfJoin/pie.png differ diff --git a/test/fixtures/controller.doughnut/single-slice-circumference-405.js b/test/fixtures/controller.doughnut/single-slice-circumference-405.js new file mode 100644 index 00000000000..639d7571476 --- /dev/null +++ b/test/fixtures/controller.doughnut/single-slice-circumference-405.js @@ -0,0 +1,21 @@ +module.exports = { + threshold: 0.05, + config: { + type: 'doughnut', + data: { + labels: ['A'], + datasets: [{ + data: [1], + backgroundColor: 'rgba(0,0,0,0.3)', + borderColor: 'rgba(0,0,0,0.5)', + circumference: 405 + }] + }, + }, + options: { + canvas: { + width: 256, + height: 256 + } + } +}; diff --git a/test/fixtures/controller.doughnut/single-slice-circumference-405.png b/test/fixtures/controller.doughnut/single-slice-circumference-405.png new file mode 100644 index 00000000000..db4e2523509 Binary files /dev/null and b/test/fixtures/controller.doughnut/single-slice-circumference-405.png differ diff --git a/test/fixtures/controller.doughnut/single-slice-offset.js b/test/fixtures/controller.doughnut/single-slice-offset.js new file mode 100644 index 00000000000..d2a9ace0c27 --- /dev/null +++ b/test/fixtures/controller.doughnut/single-slice-offset.js @@ -0,0 +1,16 @@ +module.exports = { + config: { + type: 'doughnut', + data: { + labels: ['A'], + datasets: [{ + data: [385], + backgroundColor: 'rgba(0,0,0,0.3)', + borderColor: 'rgba(0,0,0,0.5)', + }] + }, + options: { + offset: 20 + } + } +}; diff --git a/test/fixtures/controller.doughnut/single-slice-offset.png b/test/fixtures/controller.doughnut/single-slice-offset.png new file mode 100644 index 00000000000..b38c18b8033 Binary files /dev/null and b/test/fixtures/controller.doughnut/single-slice-offset.png differ diff --git a/test/fixtures/controller.doughnut/single-slice-opacity.js b/test/fixtures/controller.doughnut/single-slice-opacity.js new file mode 100644 index 00000000000..8dd6dab15c1 --- /dev/null +++ b/test/fixtures/controller.doughnut/single-slice-opacity.js @@ -0,0 +1,20 @@ +module.exports = { + threshold: 0.05, + config: { + type: 'doughnut', + data: { + labels: ['A'], + datasets: [{ + data: [1], + backgroundColor: 'rgba(0,0,0,0.3)', + borderColor: 'rgba(0,0,0,0.5)' + }] + }, + }, + options: { + canvas: { + width: 256, + height: 256 + } + } +}; diff --git a/test/fixtures/controller.doughnut/single-slice-opacity.png b/test/fixtures/controller.doughnut/single-slice-opacity.png new file mode 100644 index 00000000000..c94dd6f7944 Binary files /dev/null and b/test/fixtures/controller.doughnut/single-slice-opacity.png differ diff --git a/test/fixtures/controller.line/backgroundColor/scriptable.js b/test/fixtures/controller.line/backgroundColor/scriptable.js new file mode 100644 index 00000000000..88dc039ab9a --- /dev/null +++ b/test/fixtures/controller.line/backgroundColor/scriptable.js @@ -0,0 +1,63 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [4, 5, 10, null, -10, -5], + backgroundColor: function(ctx) { + var index = ctx.index; + return index === 0 ? '#ff0000' + : index === 1 ? '#00ff00' + : '#ff00ff'; + } + }, + { + // option in element (fallback) + data: [-4, -5, -10, null, 10, 5], + } + ] + }, + options: { + elements: { + line: { + fill: true, + backgroundColor: function(ctx) { + var index = ctx.index; + return index === 0 ? '#ff0000' + : index === 1 ? '#00ff00' + : '#ff00ff'; + } + }, + point: { + backgroundColor: '#0000ff', + radius: 10 + } + }, + layout: { + padding: 32 + }, + scales: { + x: {display: false}, + y: { + display: false, + beginAtZero: true + } + }, + plugins: { + legend: false, + title: false, + tooltip: false, + filler: true + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/backgroundColor/scriptable.png b/test/fixtures/controller.line/backgroundColor/scriptable.png new file mode 100644 index 00000000000..81831ec6c20 Binary files /dev/null and b/test/fixtures/controller.line/backgroundColor/scriptable.png differ diff --git a/test/fixtures/controller.line/backgroundColor/value.js b/test/fixtures/controller.line/backgroundColor/value.js new file mode 100644 index 00000000000..6a52e7b7e45 --- /dev/null +++ b/test/fixtures/controller.line/backgroundColor/value.js @@ -0,0 +1,49 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + backgroundColor: '#ff0000' + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + elements: { + line: { + fill: true, + backgroundColor: '#00ff00' + }, + point: { + radius: 10 + } + }, + layout: { + padding: 32 + }, + scales: { + x: {display: false}, + y: {display: false} + }, + plugins: { + legend: false, + title: false, + tooltip: false, + filler: true + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/backgroundColor/value.png b/test/fixtures/controller.line/backgroundColor/value.png new file mode 100644 index 00000000000..3f303d0242d Binary files /dev/null and b/test/fixtures/controller.line/backgroundColor/value.png differ diff --git a/test/fixtures/controller.line/borderCapStyle/scriptable.js b/test/fixtures/controller.line/borderCapStyle/scriptable.js new file mode 100644 index 00000000000..6d76e2056a5 --- /dev/null +++ b/test/fixtures/controller.line/borderCapStyle/scriptable.js @@ -0,0 +1,62 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3], + datasets: [ + { + // option in dataset + data: [null, 3, 3], + borderCapStyle: function(ctx) { + var index = (ctx.datasetIndex % 2); + return index === 0 ? 'round' + : index === 1 ? 'square' + : 'butt'; + } + }, + { + // option in element (fallback) + data: [null, 2, 2], + }, + { + // option in element (fallback) + data: [null, 1, 1], + } + ] + }, + options: { + elements: { + line: { + borderCapStyle: function(ctx) { + var index = (ctx.datasetIndex % 3); + return index === 0 ? 'round' + : index === 1 ? 'square' + : 'butt'; + }, + borderColor: '#ff0000', + borderWidth: 32, + fill: false + }, + point: { + radius: 10, + } + }, + layout: { + padding: 32 + }, + scales: { + x: {display: false}, + y: { + display: false, + beginAtZero: true + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/borderCapStyle/scriptable.png b/test/fixtures/controller.line/borderCapStyle/scriptable.png new file mode 100644 index 00000000000..2b83b9b96db Binary files /dev/null and b/test/fixtures/controller.line/borderCapStyle/scriptable.png differ diff --git a/test/fixtures/controller.line/borderCapStyle/value.js b/test/fixtures/controller.line/borderCapStyle/value.js new file mode 100644 index 00000000000..25e8135b5eb --- /dev/null +++ b/test/fixtures/controller.line/borderCapStyle/value.js @@ -0,0 +1,50 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3], + datasets: [ + { + // option in dataset + data: [null, 3, 3], + borderCapStyle: 'round', + }, + { + // option in dataset + data: [null, 2, 2], + borderCapStyle: 'square', + }, + { + // option in element (fallback) + data: [null, 1, 1], + } + ] + }, + options: { + elements: { + line: { + borderCapStyle: 'butt', + borderColor: '#00ff00', + borderWidth: 32, + fill: false, + }, + point: { + radius: 10, + } + }, + layout: { + padding: 32 + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/borderCapStyle/value.png b/test/fixtures/controller.line/borderCapStyle/value.png new file mode 100644 index 00000000000..20f3116bb32 Binary files /dev/null and b/test/fixtures/controller.line/borderCapStyle/value.png differ diff --git a/test/fixtures/controller.line/borderColor/scriptable.js b/test/fixtures/controller.line/borderColor/scriptable.js new file mode 100644 index 00000000000..e3052f232f8 --- /dev/null +++ b/test/fixtures/controller.line/borderColor/scriptable.js @@ -0,0 +1,59 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [4, 5, 10, null, -10, -5], + borderColor: function(ctx) { + var index = ctx.index; + return index === 0 ? '#ff0000' + : index === 1 ? '#00ff00' + : '#0000ff'; + } + }, + { + // option in element (fallback) + data: [-4, -5, -10, null, 10, 5] + } + ] + }, + options: { + elements: { + line: { + borderColor: function(ctx) { + var index = ctx.index; + return index === 0 ? '#ff0000' + : index === 1 ? '#00ff00' + : '#0000ff'; + }, + borderWidth: 10, + fill: false + }, + point: { + borderColor: '#ff0000', + borderWidth: 10, + radius: 16 + } + }, + layout: { + padding: 32 + }, + scales: { + x: {display: false}, + y: { + display: false, + beginAtZero: true + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/borderColor/scriptable.png b/test/fixtures/controller.line/borderColor/scriptable.png new file mode 100644 index 00000000000..02a9ea27420 Binary files /dev/null and b/test/fixtures/controller.line/borderColor/scriptable.png differ diff --git a/test/fixtures/controller.line/borderColor/value.js b/test/fixtures/controller.line/borderColor/value.js new file mode 100644 index 00000000000..7a269db5d5a --- /dev/null +++ b/test/fixtures/controller.line/borderColor/value.js @@ -0,0 +1,44 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + borderColor: '#ff0000' + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + elements: { + line: { + borderColor: '#0000ff', + fill: false, + }, + point: { + borderColor: '#0000ff', + radius: 10, + } + }, + layout: { + padding: 32 + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/borderColor/value.png b/test/fixtures/controller.line/borderColor/value.png new file mode 100644 index 00000000000..a5807456380 Binary files /dev/null and b/test/fixtures/controller.line/borderColor/value.png differ diff --git a/test/fixtures/controller.line/borderDash/scriptable.js b/test/fixtures/controller.line/borderDash/scriptable.js new file mode 100644 index 00000000000..b6bf1010667 --- /dev/null +++ b/test/fixtures/controller.line/borderDash/scriptable.js @@ -0,0 +1,50 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [4, 5, 10, null, -10, -5], + borderDash: function(ctx) { + return ctx.datasetIndex === 0 ? [5] : [10]; + } + }, + { + // option in element (fallback) + data: [-4, -5, -10, null, 10, 5] + } + ] + }, + options: { + elements: { + line: { + borderColor: '#00ff00', + borderDash: function(ctx) { + return ctx.datasetIndex === 0 ? [5] : [10]; + } + }, + point: { + radius: 10, + } + }, + layout: { + padding: 32 + }, + scales: { + x: {display: false}, + y: { + display: false, + beginAtZero: true + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/borderDash/scriptable.png b/test/fixtures/controller.line/borderDash/scriptable.png new file mode 100644 index 00000000000..fab773b823a Binary files /dev/null and b/test/fixtures/controller.line/borderDash/scriptable.png differ diff --git a/test/fixtures/controller.line/borderDash/value.js b/test/fixtures/controller.line/borderDash/value.js new file mode 100644 index 00000000000..3f4f01a6b12 --- /dev/null +++ b/test/fixtures/controller.line/borderDash/value.js @@ -0,0 +1,45 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + borderColor: '#ff0000', + borderDash: [5] + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + elements: { + line: { + borderColor: '#00ff00', + borderDash: [10], + fill: false, + }, + point: { + radius: 10, + } + }, + layout: { + padding: 32 + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/borderDash/value.png b/test/fixtures/controller.line/borderDash/value.png new file mode 100644 index 00000000000..8a68ce751ff Binary files /dev/null and b/test/fixtures/controller.line/borderDash/value.png differ diff --git a/test/fixtures/controller.line/borderDashOffset/scriptable.js b/test/fixtures/controller.line/borderDashOffset/scriptable.js new file mode 100644 index 00000000000..5dabacc6e96 --- /dev/null +++ b/test/fixtures/controller.line/borderDashOffset/scriptable.js @@ -0,0 +1,51 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3], + datasets: [ + { + // option in dataset + data: [1, 1, 1, 1], + borderColor: '#ff0000', + borderDash: [20], + borderDashOffset: function(ctx) { + return ctx.datasetIndex === 0 ? 5.0 : 0.0; + } + }, + { + // option in element (fallback) + data: [0, 0, 0, 0] + } + ] + }, + options: { + elements: { + line: { + borderColor: '#00ff00', + borderDash: [20], + borderDashOffset: function(ctx) { + return ctx.datasetIndex === 0 ? 5.0 : 0.0; + }, + fill: false, + }, + point: { + radius: 10, + } + }, + layout: { + padding: 32 + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/borderDashOffset/scriptable.png b/test/fixtures/controller.line/borderDashOffset/scriptable.png new file mode 100644 index 00000000000..a4b254fe44b Binary files /dev/null and b/test/fixtures/controller.line/borderDashOffset/scriptable.png differ diff --git a/test/fixtures/controller.line/borderDashOffset/value.js b/test/fixtures/controller.line/borderDashOffset/value.js new file mode 100644 index 00000000000..b16b12bcbd1 --- /dev/null +++ b/test/fixtures/controller.line/borderDashOffset/value.js @@ -0,0 +1,47 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [1, 1, 1, 1, 1, 1], + borderColor: '#ff0000', + borderDash: [20], + borderDashOffset: 5.0 + }, + { + // option in element (fallback) + data: [0, 0, 0, 0, 0, 0] + } + ] + }, + options: { + elements: { + line: { + borderColor: '#00ff00', + borderDash: [20], + borderDashOffset: 0.0, // default + fill: false, + }, + point: { + radius: 10, + } + }, + layout: { + padding: 32 + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/borderDashOffset/value.png b/test/fixtures/controller.line/borderDashOffset/value.png new file mode 100644 index 00000000000..bea22dd79b5 Binary files /dev/null and b/test/fixtures/controller.line/borderDashOffset/value.png differ diff --git a/test/fixtures/controller.line/borderJoinStyle/scriptable.js b/test/fixtures/controller.line/borderJoinStyle/scriptable.js new file mode 100644 index 00000000000..2fc77bc8e99 --- /dev/null +++ b/test/fixtures/controller.line/borderJoinStyle/scriptable.js @@ -0,0 +1,59 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2], + datasets: [ + { + // option in dataset + data: [6, 18, 6], + borderColor: '#ff0000', + borderJoinStyle: function(ctx) { + var index = ctx.datasetIndex % 3; + return index === 0 ? 'round' + : index === 1 ? 'miter' + : 'bevel'; + } + }, + { + // option in element (fallback) + data: [2, 14, 2], + borderColor: '#0000ff', + }, + { + // option in element (fallback) + data: [-2, 10, -2] + } + ] + }, + options: { + elements: { + line: { + borderColor: '#00ff00', + borderJoinStyle: function(ctx) { + var index = (ctx.datasetIndex % 3); + return index === 0 ? 'round' + : index === 1 ? 'miter' + : 'bevel'; + }, + borderWidth: 25, + fill: false, + tension: 0 + } + }, + layout: { + padding: 32 + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/borderJoinStyle/scriptable.png b/test/fixtures/controller.line/borderJoinStyle/scriptable.png new file mode 100644 index 00000000000..95322cabc65 Binary files /dev/null and b/test/fixtures/controller.line/borderJoinStyle/scriptable.png differ diff --git a/test/fixtures/controller.line/borderJoinStyle/value.js b/test/fixtures/controller.line/borderJoinStyle/value.js new file mode 100644 index 00000000000..303c1a62a60 --- /dev/null +++ b/test/fixtures/controller.line/borderJoinStyle/value.js @@ -0,0 +1,50 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2], + datasets: [ + { + // option in dataset + data: [6, 18, 6], + borderColor: '#ff0000', + borderJoinStyle: 'round', + }, + { + // option in element (fallback) + data: [2, 14, 2], + borderColor: '#0000ff', + borderJoinStyle: 'bevel', + }, + { + // option in element (fallback) + data: [-2, 10, -2] + } + ] + }, + options: { + elements: { + line: { + borderColor: '#00ff00', + borderJoinStyle: 'miter', + borderWidth: 25, + fill: false, + tension: 0 + } + }, + layout: { + padding: 32 + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/borderJoinStyle/value.png b/test/fixtures/controller.line/borderJoinStyle/value.png new file mode 100644 index 00000000000..a4eaaf14a45 Binary files /dev/null and b/test/fixtures/controller.line/borderJoinStyle/value.png differ diff --git a/test/fixtures/controller.line/borderWidth/scriptable.js b/test/fixtures/controller.line/borderWidth/scriptable.js new file mode 100644 index 00000000000..81925c9dcfc --- /dev/null +++ b/test/fixtures/controller.line/borderWidth/scriptable.js @@ -0,0 +1,57 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [4, 5, 10, null, -10, -5], + borderColor: '#0000ff', + borderWidth: function(ctx) { + var index = ctx.index; + return index % 2 ? 10 : 20; + }, + pointBorderColor: '#00ff00' + }, + { + // option in element (fallback) + data: [-4, -5, -10, null, 10, 5], + } + ] + }, + options: { + elements: { + line: { + borderColor: '#ff0000', + borderWidth: function(ctx) { + var index = ctx.index; + return index % 2 ? 10 : 20; + }, + fill: false, + }, + point: { + borderColor: '#00ff00', + borderWidth: 5, + radius: 10 + } + }, + layout: { + padding: 32 + }, + scales: { + x: {display: false}, + y: { + display: false, + beginAtZero: true + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/borderWidth/scriptable.png b/test/fixtures/controller.line/borderWidth/scriptable.png new file mode 100644 index 00000000000..2e77d049ed5 Binary files /dev/null and b/test/fixtures/controller.line/borderWidth/scriptable.png differ diff --git a/test/fixtures/controller.line/borderWidth/value.js b/test/fixtures/controller.line/borderWidth/value.js new file mode 100644 index 00000000000..9c4889e7c3d --- /dev/null +++ b/test/fixtures/controller.line/borderWidth/value.js @@ -0,0 +1,45 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + borderColor: '#0000ff', + borderWidth: 6 + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + elements: { + line: { + borderColor: '#00ff00', + borderWidth: 3, + fill: false, + }, + point: { + radius: 10, + } + }, + layout: { + padding: 32 + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/borderWidth/value.png b/test/fixtures/controller.line/borderWidth/value.png new file mode 100644 index 00000000000..6c18e4999cb Binary files /dev/null and b/test/fixtures/controller.line/borderWidth/value.png differ diff --git a/test/fixtures/controller.line/borderWidth/zero.js b/test/fixtures/controller.line/borderWidth/zero.js new file mode 100644 index 00000000000..4d13be9da98 --- /dev/null +++ b/test/fixtures/controller.line/borderWidth/zero.js @@ -0,0 +1,47 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + backgroundColor: '#0000ff', + borderColor: '#0000ff', + borderWidth: 0 + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + elements: { + line: { + borderColor: '#00ff00', + borderWidth: 3, + fill: false, + }, + point: { + backgroundColor: '#00ff00', + radius: 10, + } + }, + layout: { + padding: 32 + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/borderWidth/zero.png b/test/fixtures/controller.line/borderWidth/zero.png new file mode 100644 index 00000000000..febae92e5e5 Binary files /dev/null and b/test/fixtures/controller.line/borderWidth/zero.png differ diff --git a/test/fixtures/controller.line/clip/default-x-max.json b/test/fixtures/controller.line/clip/default-x-max.json new file mode 100644 index 00000000000..e353be2b631 --- /dev/null +++ b/test/fixtures/controller.line/clip/default-x-max.json @@ -0,0 +1,36 @@ +{ + "config": { + "type": "scatter", + "data": { + "datasets": [{ + "borderColor": "red", + "data": [{"x":-5,"y":5},{"x":-4,"y":6},{"x":-3,"y":7},{"x":-2,"y":6},{"x":-1,"y":5},{"x":0,"y":4},{"x":1,"y":3},{"x":2,"y":2},{"x":3,"y":5},{"x":4,"y":7},{"x":5,"y":9}], + "fill": false, + "showLine": true, + "borderWidth": 20, + "pointRadius": 0 + }] + }, + "options": { + "responsive": false, + "scales": { + "x": { + "max": 3, + "ticks": { + "display": false + } + }, + "y": {"ticks": {"display": false}} + }, + "layout": { + "padding": 24 + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.line/clip/default-x-max.png b/test/fixtures/controller.line/clip/default-x-max.png new file mode 100644 index 00000000000..4379fba82e9 Binary files /dev/null and b/test/fixtures/controller.line/clip/default-x-max.png differ diff --git a/test/fixtures/controller.line/clip/default-x-min.json b/test/fixtures/controller.line/clip/default-x-min.json new file mode 100644 index 00000000000..9c0a95ebdc3 --- /dev/null +++ b/test/fixtures/controller.line/clip/default-x-min.json @@ -0,0 +1,36 @@ +{ + "config": { + "type": "scatter", + "data": { + "datasets": [{ + "borderColor": "red", + "data": [{"x":-5,"y":5},{"x":-4,"y":6},{"x":-3,"y":7},{"x":-2,"y":6},{"x":-1,"y":5},{"x":0,"y":4},{"x":1,"y":3},{"x":2,"y":2},{"x":3,"y":5},{"x":4,"y":7},{"x":5,"y":9}], + "fill": false, + "showLine": true, + "borderWidth": 20, + "pointRadius": 0 + }] + }, + "options": { + "responsive": false, + "scales": { + "x": { + "min": -2, + "ticks": { + "display": false + } + }, + "y": {"ticks": {"display": false}} + }, + "layout": { + "padding": 24 + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.line/clip/default-x-min.png b/test/fixtures/controller.line/clip/default-x-min.png new file mode 100644 index 00000000000..f76bbf52a37 Binary files /dev/null and b/test/fixtures/controller.line/clip/default-x-min.png differ diff --git a/test/fixtures/controller.line/clip/default-x.json b/test/fixtures/controller.line/clip/default-x.json new file mode 100644 index 00000000000..7d75307961b --- /dev/null +++ b/test/fixtures/controller.line/clip/default-x.json @@ -0,0 +1,37 @@ +{ + "config": { + "type": "scatter", + "data": { + "datasets": [{ + "borderColor": "red", + "data": [{"x":-5,"y":5},{"x":-4,"y":6},{"x":-3,"y":7},{"x":-2,"y":6},{"x":-1,"y":5},{"x":0,"y":4},{"x":1,"y":3},{"x":2,"y":2},{"x":3,"y":5},{"x":4,"y":7},{"x":5,"y":9}], + "fill": false, + "showLine": true, + "borderWidth": 20, + "pointRadius": 0 + }] + }, + "options": { + "responsive": false, + "scales": { + "x": { + "min": -2, + "max": 3, + "ticks": { + "display": false + } + }, + "y": {"ticks": {"display": false}} + }, + "layout": { + "padding": 24 + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.line/clip/default-x.png b/test/fixtures/controller.line/clip/default-x.png new file mode 100644 index 00000000000..7b40d771850 Binary files /dev/null and b/test/fixtures/controller.line/clip/default-x.png differ diff --git a/test/fixtures/controller.line/clip/default-y-max.json b/test/fixtures/controller.line/clip/default-y-max.json new file mode 100644 index 00000000000..5fb441fbeed --- /dev/null +++ b/test/fixtures/controller.line/clip/default-y-max.json @@ -0,0 +1,36 @@ +{ + "config": { + "type": "scatter", + "data": { + "datasets": [{ + "borderColor": "red", + "data": [{"x":-5,"y":5},{"x":-4,"y":6},{"x":-3,"y":7},{"x":-2,"y":6},{"x":-1,"y":5},{"x":0,"y":4},{"x":1,"y":3},{"x":2,"y":2},{"x":3,"y":5},{"x":4,"y":7},{"x":5,"y":9}], + "fill": false, + "showLine": true, + "borderWidth": 20, + "pointRadius": 0 + }] + }, + "options": { + "responsive": false, + "scales": { + "x": {"ticks": {"display": false}}, + "y": { + "max": 6, + "ticks": { + "display": false + } + } + }, + "layout": { + "padding": 24 + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.line/clip/default-y-max.png b/test/fixtures/controller.line/clip/default-y-max.png new file mode 100644 index 00000000000..75aaec9841a Binary files /dev/null and b/test/fixtures/controller.line/clip/default-y-max.png differ diff --git a/test/fixtures/controller.line/clip/default-y-min.json b/test/fixtures/controller.line/clip/default-y-min.json new file mode 100644 index 00000000000..622dd0d92af --- /dev/null +++ b/test/fixtures/controller.line/clip/default-y-min.json @@ -0,0 +1,36 @@ +{ + "config": { + "type": "scatter", + "data": { + "datasets": [{ + "borderColor": "red", + "data": [{"x":-5,"y":5},{"x":-4,"y":6},{"x":-3,"y":7},{"x":-2,"y":6},{"x":-1,"y":5},{"x":0,"y":4},{"x":1,"y":3},{"x":2,"y":2},{"x":3,"y":5},{"x":4,"y":7},{"x":5,"y":9}], + "fill": false, + "showLine": true, + "borderWidth": 20, + "pointRadius": 0 + }] + }, + "options": { + "responsive": false, + "scales": { + "x": {"ticks": {"display": false}}, + "y": { + "min": 2, + "ticks": { + "display": false + } + } + }, + "layout": { + "padding": 24 + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.line/clip/default-y-min.png b/test/fixtures/controller.line/clip/default-y-min.png new file mode 100644 index 00000000000..2db3da79f2d Binary files /dev/null and b/test/fixtures/controller.line/clip/default-y-min.png differ diff --git a/test/fixtures/controller.line/clip/default-y.json b/test/fixtures/controller.line/clip/default-y.json new file mode 100644 index 00000000000..0a4eb17e0a0 --- /dev/null +++ b/test/fixtures/controller.line/clip/default-y.json @@ -0,0 +1,37 @@ +{ + "config": { + "type": "scatter", + "data": { + "datasets": [{ + "borderColor": "red", + "data": [{"x":-5,"y":5},{"x":-4,"y":6},{"x":-3,"y":7},{"x":-2,"y":6},{"x":-1,"y":5},{"x":0,"y":4},{"x":1,"y":3},{"x":2,"y":2},{"x":3,"y":5},{"x":4,"y":7},{"x":5,"y":9}], + "fill": false, + "showLine": true, + "borderWidth": 20, + "pointRadius": 0 + }] + }, + "options": { + "responsive": false, + "scales": { + "x": {"ticks": {"display": false}}, + "y": { + "min": 2, + "max": 6, + "ticks": { + "display": false + } + } + }, + "layout": { + "padding": 24 + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.line/clip/default-y.png b/test/fixtures/controller.line/clip/default-y.png new file mode 100644 index 00000000000..6fe1d01dc41 Binary files /dev/null and b/test/fixtures/controller.line/clip/default-y.png differ diff --git a/test/fixtures/controller.line/clip/false.js b/test/fixtures/controller.line/clip/false.js new file mode 100644 index 00000000000..1f86abd2de5 --- /dev/null +++ b/test/fixtures/controller.line/clip/false.js @@ -0,0 +1,46 @@ +const data = []; +for (let x = 0.95; x < 1.15; x += 0.002) { + data.push({x, y: x}); +} + +for (let x = 0.95; x < 1.15; x += 0.001) { + data.push({x, y: 2.1 - x}); +} + +module.exports = { + config: { + type: 'scatter', + data: { + datasets: [{ + clip: false, + radius: 8, + borderWidth: 0, + backgroundColor: (ctx) => ctx.type !== 'data' || ctx.raw.x < 1 || ctx.raw.x > 1.1 ? 'rgba(255,0,0,0.7)' : 'rgba(0,0,255,0.05)', + data + }] + }, + options: { + plugins: false, + scales: { + x: { + min: 1, + max: 1.1 + }, + y: { + min: 1, + max: 1.1 + }, + }, + layout: { + padding: 32 + } + } + }, + options: { + spriteText: true, + canvas: { + height: 240, + width: 320 + } + } +}; diff --git a/test/fixtures/controller.line/clip/false.png b/test/fixtures/controller.line/clip/false.png new file mode 100644 index 00000000000..8b9347bbb1f Binary files /dev/null and b/test/fixtures/controller.line/clip/false.png differ diff --git a/test/fixtures/controller.line/clip/specified.json b/test/fixtures/controller.line/clip/specified.json new file mode 100644 index 00000000000..53f4917f2a5 --- /dev/null +++ b/test/fixtures/controller.line/clip/specified.json @@ -0,0 +1,75 @@ +{ + "config": { + "type": "scatter", + "data": { + "datasets": [ + { + "showLine": true, + "borderColor": "red", + "data": [{"x":-4,"y":-4},{"x":4,"y":4}], + "clip": false + }, + { + "showLine": true, + "borderColor": "green", + "data": [{"x":-4,"y":-5},{"x":4,"y":3}], + "clip": 5 + }, + { + "showLine": true, + "borderColor": "blue", + "data": [{"x":-4,"y":-3},{"x":4,"y":5}], + "clip": -5 + }, + { + "showLine": true, + "borderColor": "brown", + "data": [{"x":-3,"y":-3},{"x":-1,"y":3},{"x":1,"y":-2},{"x":2,"y":3}], + "clip": { + "top": 8, + "left": false, + "right": -20, + "bottom": -20 + } + } + ] + }, + "options": { + "responsive": false, + "scales": { + "x": { + "min": -2, + "max": 2, + "ticks": { + "display": false + } + }, + "y": { + "min": -2, + "max": 2, + "ticks": { + "display": false + } + } + }, + "layout": { + "padding": 24 + }, + "elements": { + "line": { + "fill": false, + "borderWidth": 20 + }, + "point": { + "radius": 0 + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.line/clip/specified.png b/test/fixtures/controller.line/clip/specified.png new file mode 100644 index 00000000000..0f7eeef7cd7 Binary files /dev/null and b/test/fixtures/controller.line/clip/specified.png differ diff --git a/test/fixtures/controller.line/cubicInterpolationMode/scriptable.js b/test/fixtures/controller.line/cubicInterpolationMode/scriptable.js new file mode 100644 index 00000000000..8333490dddd --- /dev/null +++ b/test/fixtures/controller.line/cubicInterpolationMode/scriptable.js @@ -0,0 +1,48 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 4, 2, 6, 4, 8], + borderColor: '#ff0000', + cubicInterpolationMode: function(ctx) { + return ctx.datasetIndex === 0 ? 'monotone' : 'default'; + } + }, + { + // option in element (fallback) + data: [2, 6, 4, 8, 6, 10], + } + ] + }, + options: { + elements: { + line: { + borderColor: '#00ff00', + borderWidth: 20, + cubicInterpolationMode: function(ctx) { + return ctx.datasetIndex === 0 ? 'monotone' : 'default'; + }, + fill: false, + tension: 0.4 + } + }, + layout: { + padding: 32 + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/cubicInterpolationMode/scriptable.png b/test/fixtures/controller.line/cubicInterpolationMode/scriptable.png new file mode 100644 index 00000000000..33e63f8b87b Binary files /dev/null and b/test/fixtures/controller.line/cubicInterpolationMode/scriptable.png differ diff --git a/test/fixtures/controller.line/cubicInterpolationMode/value.js b/test/fixtures/controller.line/cubicInterpolationMode/value.js new file mode 100644 index 00000000000..2ae1a8abc68 --- /dev/null +++ b/test/fixtures/controller.line/cubicInterpolationMode/value.js @@ -0,0 +1,44 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 4, 2, 6, 4, 8], + borderColor: '#ff0000', + cubicInterpolationMode: 'monotone' + }, + { + // option in element (fallback) + data: [2, 6, 4, 8, 6, 10] + } + ] + }, + options: { + elements: { + line: { + borderColor: '#00ff00', + borderWidth: 20, + cubicInterpolationMode: 'default', + fill: false, + tension: 0.4 + } + }, + layout: { + padding: 32 + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/cubicInterpolationMode/value.png b/test/fixtures/controller.line/cubicInterpolationMode/value.png new file mode 100644 index 00000000000..33e63f8b87b Binary files /dev/null and b/test/fixtures/controller.line/cubicInterpolationMode/value.png differ diff --git a/test/fixtures/controller.line/fill/no-border.js b/test/fixtures/controller.line/fill/no-border.js new file mode 100644 index 00000000000..5846762e359 --- /dev/null +++ b/test/fixtures/controller.line/fill/no-border.js @@ -0,0 +1,41 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + data: [12, 19, 3, 5, 2, 3], + backgroundColor: '#ff0000', + borderWidth: 0, + tension: 0.4, + fill: true + }, + ] + }, + options: { + animation: { + duration: 1 + }, + scales: { + x: {display: false}, + y: {display: false} + }, + plugins: { + legend: false, + title: false, + tooltip: false, + filler: true + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + }, + run() { + return new Promise(resolve => setTimeout(resolve, 50)); + } + } +}; diff --git a/test/fixtures/controller.line/fill/no-border.png b/test/fixtures/controller.line/fill/no-border.png new file mode 100644 index 00000000000..b140ceafaef Binary files /dev/null and b/test/fixtures/controller.line/fill/no-border.png differ diff --git a/test/fixtures/controller.line/fill/order-default.js b/test/fixtures/controller.line/fill/order-default.js new file mode 100644 index 00000000000..072acbbc0e4 --- /dev/null +++ b/test/fixtures/controller.line/fill/order-default.js @@ -0,0 +1,49 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [3, 1, 2, 0, 8, 1], + backgroundColor: '#ff0000' + }, + { + // option in element (fallback) + data: [0, 4, 2, 6, 4, 8], + backgroundColor: '#00ff00' + } + ] + }, + options: { + elements: { + line: { + fill: true + }, + point: { + radius: 0 + } + }, + layout: { + padding: 32 + }, + scales: { + x: {display: false}, + y: {display: false} + }, + plugins: { + legend: false, + title: false, + tooltip: false, + filler: true + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/fill/order-default.png b/test/fixtures/controller.line/fill/order-default.png new file mode 100644 index 00000000000..958d591ab88 Binary files /dev/null and b/test/fixtures/controller.line/fill/order-default.png differ diff --git a/test/fixtures/controller.line/fill/order.js b/test/fixtures/controller.line/fill/order.js new file mode 100644 index 00000000000..892043195ae --- /dev/null +++ b/test/fixtures/controller.line/fill/order.js @@ -0,0 +1,49 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + data: [3, 1, 2, 0, 8, 1], + backgroundColor: '#ff0000', + order: 2 + }, + { + data: [0, 4, 2, 6, 4, 8], + backgroundColor: '#00ff00', + order: 1 + } + ] + }, + options: { + elements: { + line: { + fill: true + }, + point: { + radius: 0 + } + }, + layout: { + padding: 32 + }, + scales: { + x: {display: false}, + y: {display: false} + }, + plugins: { + legend: false, + title: false, + tooltip: false, + filler: true + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/fill/order.png b/test/fixtures/controller.line/fill/order.png new file mode 100644 index 00000000000..6660cb8f229 Binary files /dev/null and b/test/fixtures/controller.line/fill/order.png differ diff --git a/test/fixtures/controller.line/fill/scriptable.js b/test/fixtures/controller.line/fill/scriptable.js new file mode 100644 index 00000000000..61d946c857a --- /dev/null +++ b/test/fixtures/controller.line/fill/scriptable.js @@ -0,0 +1,51 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [-2, -6, -4, -8, -6, -10], + backgroundColor: '#ff0000', + fill: function(ctx) { + return ctx.datasetIndex === 0 ? true : false; + } + }, + { + // option in element (fallback) + data: [0, 4, 2, 6, 4, 8], + } + ] + }, + options: { + elements: { + line: { + backgroundColor: '#00ff00', + fill: function(ctx) { + return ctx.datasetIndex === 0 ? true : false; + } + } + }, + layout: { + padding: 32 + }, + scales: { + x: {display: false}, + y: {display: false} + }, + plugins: { + legend: false, + title: false, + tooltip: false, + filler: true + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/fill/scriptable.png b/test/fixtures/controller.line/fill/scriptable.png new file mode 100644 index 00000000000..8127ec409f7 Binary files /dev/null and b/test/fixtures/controller.line/fill/scriptable.png differ diff --git a/test/fixtures/controller.line/fill/value.js b/test/fixtures/controller.line/fill/value.js new file mode 100644 index 00000000000..aebe9f244d6 --- /dev/null +++ b/test/fixtures/controller.line/fill/value.js @@ -0,0 +1,47 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [-2, -6, -4, -8, -6, -10], + backgroundColor: '#ff0000', + fill: false + }, + { + // option in element (fallback) + data: [0, 4, 2, 6, 4, 8], + } + ] + }, + options: { + elements: { + line: { + backgroundColor: '#00ff00', + fill: true, + } + }, + layout: { + padding: 32 + }, + scales: { + x: {display: false}, + y: {display: false} + }, + plugins: { + legend: false, + title: false, + tooltip: false, + filler: true + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/fill/value.png b/test/fixtures/controller.line/fill/value.png new file mode 100644 index 00000000000..6f0601c8912 Binary files /dev/null and b/test/fixtures/controller.line/fill/value.png differ diff --git a/test/fixtures/controller.line/issue-8902.js b/test/fixtures/controller.line/issue-8902.js new file mode 100644 index 00000000000..cb986380708 --- /dev/null +++ b/test/fixtures/controller.line/issue-8902.js @@ -0,0 +1,30 @@ +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/8902', + config: { + type: 'line', + data: { + labels: [1, 2, 3, 4, 5, 6, 7, 8], + datasets: [{ + data: [65, 59, NaN, 48, 56, 57, 40], + borderColor: 'rgb(75, 192, 192)', + }] + }, + options: { + plugins: false, + scales: { + x: { + type: 'linear', + min: 1, + max: 3 + } + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/issue-8902.png b/test/fixtures/controller.line/issue-8902.png new file mode 100644 index 00000000000..c132c579381 Binary files /dev/null and b/test/fixtures/controller.line/issue-8902.png differ diff --git a/test/fixtures/controller.line/non-numeric-y.json b/test/fixtures/controller.line/non-numeric-y.json new file mode 100644 index 00000000000..e4c011f8d83 --- /dev/null +++ b/test/fixtures/controller.line/non-numeric-y.json @@ -0,0 +1,32 @@ +{ + "config": { + "type": "line", + "data": { + "xLabels": ["January", "February", "March", "April", "May", "June", "July"], + "yLabels": ["", "Request Added", "Request Viewed", "Request Accepted", "Request Solved", "Solving Confirmed"], + "datasets": [{ + "label": "My First dataset", + "data": ["", "Request Added", "Request Added", "Request Added", "Request Viewed", "Request Viewed", "Request Viewed"], + "fill": false, + "borderColor": "red", + "backgroundColor": "red" + }] + }, + "options": { + "responsive": false, + "scales": { + "x": {"display": false}, + "y": { + "type": "category", + "display": false + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.line/non-numeric-y.png b/test/fixtures/controller.line/non-numeric-y.png new file mode 100644 index 00000000000..33b21168bfe Binary files /dev/null and b/test/fixtures/controller.line/non-numeric-y.png differ diff --git a/test/fixtures/controller.line/point-style-offscreen-canvas.json b/test/fixtures/controller.line/point-style-offscreen-canvas.json new file mode 100644 index 00000000000..ebd68071213 --- /dev/null +++ b/test/fixtures/controller.line/point-style-offscreen-canvas.json @@ -0,0 +1,95 @@ +{ + "config": { + "type": "line", + "data": { + "labels": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + "datasets": [{ + "borderColor": "transparent", + "data": [3, 3, 3, 3, 3, 3, 3, 3, 3, 3], + "pointBackgroundColor": "#00ff00", + "pointBorderColor": "transparent", + "pointBorderWidth": 0, + "pointStyle": [ + "circle", + "cross", + "crossRot", + "dash", + "line", + "rect", + "rectRounded", + "rectRot", + "star", + "triangle" + ] + }, { + "borderColor": "transparent", + "data": [2, 2, 2, 2, 2, 2, 2, 2, 2, 2], + "pointBackgroundColor": "transparent", + "pointBorderColor": "#0000ff", + "pointBorderWidth": 1, + "pointStyle": [ + "circle", + "cross", + "crossRot", + "dash", + "line", + "rect", + "rectRounded", + "rectRot", + "star", + "triangle" + ] + }, { + "borderColor": "transparent", + "data": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + "pointBackgroundColor": "#00ff00", + "pointBorderColor": "#0000ff", + "pointBorderWidth": 1, + "pointStyle": [ + "circle", + "cross", + "crossRot", + "dash", + "line", + "rect", + "rectRounded", + "rectRot", + "star", + "triangle" + ] + }] + }, + "options": { + "responsive": false, + "scales": { + "x": {"display": false}, + "y": { + "display": false, + "min": 0, + "max": 4 + } + }, + "elements": { + "line": { + "fill": false + }, + "point": { + "radius": 16 + } + }, + "layout": { + "padding": { + "left": 24, + "right": 24 + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + }, + "useOffscreenCanvas": true + } +} diff --git a/test/fixtures/controller.line/point-style-offscreen-canvas.png b/test/fixtures/controller.line/point-style-offscreen-canvas.png new file mode 100644 index 00000000000..47b1d29fe5c Binary files /dev/null and b/test/fixtures/controller.line/point-style-offscreen-canvas.png differ diff --git a/test/fixtures/controller.line/point-style.json b/test/fixtures/controller.line/point-style.json new file mode 100644 index 00000000000..3d008d2f1af --- /dev/null +++ b/test/fixtures/controller.line/point-style.json @@ -0,0 +1,94 @@ +{ + "config": { + "type": "line", + "data": { + "labels": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + "datasets": [{ + "borderColor": "transparent", + "data": [3, 3, 3, 3, 3, 3, 3, 3, 3, 3], + "pointBackgroundColor": "#00ff00", + "pointBorderColor": "transparent", + "pointBorderWidth": 0, + "pointStyle": [ + "circle", + "cross", + "crossRot", + "dash", + "line", + "rect", + "rectRounded", + "rectRot", + "star", + "triangle" + ] + }, { + "borderColor": "transparent", + "data": [2, 2, 2, 2, 2, 2, 2, 2, 2, 2], + "pointBackgroundColor": "transparent", + "pointBorderColor": "#0000ff", + "pointBorderWidth": 1, + "pointStyle": [ + "circle", + "cross", + "crossRot", + "dash", + "line", + "rect", + "rectRounded", + "rectRot", + "star", + "triangle" + ] + }, { + "borderColor": "transparent", + "data": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + "pointBackgroundColor": "#00ff00", + "pointBorderColor": "#0000ff", + "pointBorderWidth": 1, + "pointStyle": [ + "circle", + "cross", + "crossRot", + "dash", + "line", + "rect", + "rectRounded", + "rectRot", + "star", + "triangle" + ] + }] + }, + "options": { + "responsive": false, + "scales": { + "x": {"display": false}, + "y": { + "display": false, + "min": 0, + "max": 4 + } + }, + "elements": { + "line": { + "fill": false + }, + "point": { + "radius": 16 + } + }, + "layout": { + "padding": { + "left": 24, + "right": 24 + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.line/point-style.png b/test/fixtures/controller.line/point-style.png new file mode 100644 index 00000000000..47b1d29fe5c Binary files /dev/null and b/test/fixtures/controller.line/point-style.png differ diff --git a/test/fixtures/controller.line/pointBackgroundColor/indexable.js b/test/fixtures/controller.line/pointBackgroundColor/indexable.js new file mode 100644 index 00000000000..26ef1ee5d2c --- /dev/null +++ b/test/fixtures/controller.line/pointBackgroundColor/indexable.js @@ -0,0 +1,54 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBackgroundColor: [ + '#ff0000', + '#00ff00', + '#0000ff', + '#ffff00', + '#ff00ff', + '#000000' + ] + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + elements: { + line: { + fill: false, + }, + point: { + backgroundColor: [ + '#ff88ff', + '#888888', + '#ff8800', + '#00ff88', + '#8800ff', + '#ffff88' + ], + radius: 10 + } + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/pointBackgroundColor/indexable.png b/test/fixtures/controller.line/pointBackgroundColor/indexable.png new file mode 100644 index 00000000000..7757776ea54 Binary files /dev/null and b/test/fixtures/controller.line/pointBackgroundColor/indexable.png differ diff --git a/test/fixtures/controller.line/pointBackgroundColor/scriptable.js b/test/fixtures/controller.line/pointBackgroundColor/scriptable.js new file mode 100644 index 00000000000..997a53bc711 --- /dev/null +++ b/test/fixtures/controller.line/pointBackgroundColor/scriptable.js @@ -0,0 +1,55 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBackgroundColor: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 8 ? '#ff0000' + : value > 0 ? '#00ff00' + : value > -8 ? '#0000ff' + : '#ff00ff'; + } + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + elements: { + line: { + fill: false, + }, + point: { + backgroundColor: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 8 ? '#ff00ff' + : value > 0 ? '#0000ff' + : value > -8 ? '#ff0000' + : '#00ff00'; + }, + radius: 10, + } + }, + scales: { + x: {display: false}, + y: { + display: false, + beginAtZero: true + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/pointBackgroundColor/scriptable.png b/test/fixtures/controller.line/pointBackgroundColor/scriptable.png new file mode 100644 index 00000000000..990540d6c63 Binary files /dev/null and b/test/fixtures/controller.line/pointBackgroundColor/scriptable.png differ diff --git a/test/fixtures/controller.line/pointBackgroundColor/value.js b/test/fixtures/controller.line/pointBackgroundColor/value.js new file mode 100644 index 00000000000..0a8cbb579a6 --- /dev/null +++ b/test/fixtures/controller.line/pointBackgroundColor/value.js @@ -0,0 +1,40 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBackgroundColor: '#ff0000' + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + elements: { + line: { + fill: false, + }, + point: { + backgroundColor: '#00ff00', + radius: 10, + } + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/pointBackgroundColor/value.png b/test/fixtures/controller.line/pointBackgroundColor/value.png new file mode 100644 index 00000000000..d16d591cbf8 Binary files /dev/null and b/test/fixtures/controller.line/pointBackgroundColor/value.png differ diff --git a/test/fixtures/controller.line/pointBorderColor/indexable.js b/test/fixtures/controller.line/pointBorderColor/indexable.js new file mode 100644 index 00000000000..0b82fcc5d6c --- /dev/null +++ b/test/fixtures/controller.line/pointBorderColor/indexable.js @@ -0,0 +1,54 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBorderColor: [ + '#ff0000', + '#00ff00', + '#0000ff', + '#ffff00', + '#ff00ff', + '#000000' + ] + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + elements: { + line: { + fill: false, + }, + point: { + borderColor: [ + '#ff88ff', + '#888888', + '#ff8800', + '#00ff88', + '#8800ff', + '#ffff88' + ], + radius: 10 + } + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/pointBorderColor/indexable.png b/test/fixtures/controller.line/pointBorderColor/indexable.png new file mode 100644 index 00000000000..707fe6062ed Binary files /dev/null and b/test/fixtures/controller.line/pointBorderColor/indexable.png differ diff --git a/test/fixtures/controller.line/pointBorderColor/scriptable.js b/test/fixtures/controller.line/pointBorderColor/scriptable.js new file mode 100644 index 00000000000..29e338983e8 --- /dev/null +++ b/test/fixtures/controller.line/pointBorderColor/scriptable.js @@ -0,0 +1,55 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBorderColor: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 8 ? '#ff0000' + : value > 0 ? '#00ff00' + : value > -8 ? '#0000ff' + : '#ff00ff'; + } + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + elements: { + line: { + fill: false, + }, + point: { + borderColor: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 8 ? '#ff00ff' + : value > 0 ? '#0000ff' + : value > -8 ? '#ff0000' + : '#00ff00'; + }, + radius: 10, + } + }, + scales: { + x: {display: false}, + y: { + display: false, + beginAtZero: true + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/pointBorderColor/scriptable.png b/test/fixtures/controller.line/pointBorderColor/scriptable.png new file mode 100644 index 00000000000..ce409a3b49d Binary files /dev/null and b/test/fixtures/controller.line/pointBorderColor/scriptable.png differ diff --git a/test/fixtures/controller.line/pointBorderColor/value.js b/test/fixtures/controller.line/pointBorderColor/value.js new file mode 100644 index 00000000000..cd203c80e71 --- /dev/null +++ b/test/fixtures/controller.line/pointBorderColor/value.js @@ -0,0 +1,40 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBorderColor: '#ff0000' + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + elements: { + line: { + fill: false, + }, + point: { + borderColor: '#00ff00', + radius: 10, + } + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/pointBorderColor/value.png b/test/fixtures/controller.line/pointBorderColor/value.png new file mode 100644 index 00000000000..760d4ca3694 Binary files /dev/null and b/test/fixtures/controller.line/pointBorderColor/value.png differ diff --git a/test/fixtures/controller.line/pointBorderWidth/indexable.js b/test/fixtures/controller.line/pointBorderWidth/indexable.js new file mode 100644 index 00000000000..2da75b83e39 --- /dev/null +++ b/test/fixtures/controller.line/pointBorderWidth/indexable.js @@ -0,0 +1,46 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBorderColor: '#00ff00', + pointBorderWidth: [ + 1, 2, 3, 4, 5, 6 + ] + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + elements: { + line: { + fill: false, + }, + point: { + borderColor: '#ff0000', + borderWidth: [ + 6, 5, 4, 3, 2, 1 + ], + radius: 10 + } + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/pointBorderWidth/indexable.png b/test/fixtures/controller.line/pointBorderWidth/indexable.png new file mode 100644 index 00000000000..12a342cd315 Binary files /dev/null and b/test/fixtures/controller.line/pointBorderWidth/indexable.png differ diff --git a/test/fixtures/controller.line/pointBorderWidth/scriptable.js b/test/fixtures/controller.line/pointBorderWidth/scriptable.js new file mode 100644 index 00000000000..1e0b854ce93 --- /dev/null +++ b/test/fixtures/controller.line/pointBorderWidth/scriptable.js @@ -0,0 +1,55 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBorderColor: '#0000ff', + pointBorderWidth: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 4 ? 10 + : value > -4 ? 5 + : 2; + } + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + elements: { + line: { + fill: false, + }, + point: { + borderColor: '#ff0000', + borderWidth: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 4 ? 2 + : value > -4 ? 5 + : 10; + }, + radius: 10, + } + }, + scales: { + x: {display: false}, + y: { + display: false, + beginAtZero: true + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/pointBorderWidth/scriptable.png b/test/fixtures/controller.line/pointBorderWidth/scriptable.png new file mode 100644 index 00000000000..19c9a5035ab Binary files /dev/null and b/test/fixtures/controller.line/pointBorderWidth/scriptable.png differ diff --git a/test/fixtures/controller.line/pointBorderWidth/value.js b/test/fixtures/controller.line/pointBorderWidth/value.js new file mode 100644 index 00000000000..bad244c22d4 --- /dev/null +++ b/test/fixtures/controller.line/pointBorderWidth/value.js @@ -0,0 +1,42 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBorderColor: '#0000ff', + pointBorderWidth: 6 + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + elements: { + line: { + fill: false, + }, + point: { + borderColor: '#00ff00', + borderWidth: 3, + radius: 10, + } + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/pointBorderWidth/value.png b/test/fixtures/controller.line/pointBorderWidth/value.png new file mode 100644 index 00000000000..8c051e611d8 Binary files /dev/null and b/test/fixtures/controller.line/pointBorderWidth/value.png differ diff --git a/test/fixtures/controller.line/pointStyle/indexable.js b/test/fixtures/controller.line/pointStyle/indexable.js new file mode 100644 index 00000000000..93a04a6f4a5 --- /dev/null +++ b/test/fixtures/controller.line/pointStyle/indexable.js @@ -0,0 +1,59 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5, 6], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5, 0], + pointBackgroundColor: '#ff0000', + pointBorderColor: '#ff0000', + pointStyle: [ + 'circle', + 'cross', + 'crossRot', + 'dash', + 'line', + 'rect', + false + ] + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5, -4], + } + ] + }, + options: { + elements: { + line: { + fill: false, + }, + point: { + backgroundColor: '#00ff00', + borderColor: '#00ff00', + pointStyle: [ + 'line', + 'rect', + 'rectRounded', + 'rectRot', + 'star', + 'triangle' + ], + radius: 10 + } + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/pointStyle/indexable.png b/test/fixtures/controller.line/pointStyle/indexable.png new file mode 100644 index 00000000000..6a6dc0021f7 Binary files /dev/null and b/test/fixtures/controller.line/pointStyle/indexable.png differ diff --git a/test/fixtures/controller.line/pointStyle/scriptable.js b/test/fixtures/controller.line/pointStyle/scriptable.js new file mode 100644 index 00000000000..60d6b4ce157 --- /dev/null +++ b/test/fixtures/controller.line/pointStyle/scriptable.js @@ -0,0 +1,59 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBackgroundColor: '#ff0000', + pointBorderColor: '#ff0000', + pointStyle: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 8 ? 'rect' + : value > 0 ? 'star' + : value > -8 ? 'cross' + : 'triangle'; + } + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + elements: { + line: { + fill: false, + }, + point: { + backgroundColor: '#0000ff', + borderColor: '#0000ff', + pointStyle: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 8 ? 'triangle' + : value > 0 ? 'cross' + : value > -8 ? 'star' + : 'rect'; + }, + radius: 10, + } + }, + scales: { + x: {display: false}, + y: { + display: false, + beginAtZero: true + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/pointStyle/scriptable.png b/test/fixtures/controller.line/pointStyle/scriptable.png new file mode 100644 index 00000000000..9caac5b96e9 Binary files /dev/null and b/test/fixtures/controller.line/pointStyle/scriptable.png differ diff --git a/test/fixtures/controller.line/pointStyle/value.js b/test/fixtures/controller.line/pointStyle/value.js new file mode 100644 index 00000000000..945c25b8bc1 --- /dev/null +++ b/test/fixtures/controller.line/pointStyle/value.js @@ -0,0 +1,42 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBorderColor: '#ff0000', + pointStyle: 'star', + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + elements: { + line: { + fill: false, + }, + point: { + backgroundColor: '#00ff00', + pointStyle: 'rect', + radius: 10, + } + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/pointStyle/value.png b/test/fixtures/controller.line/pointStyle/value.png new file mode 100644 index 00000000000..ccdc6437ebc Binary files /dev/null and b/test/fixtures/controller.line/pointStyle/value.png differ diff --git a/test/fixtures/controller.line/radius/indexable.js b/test/fixtures/controller.line/radius/indexable.js new file mode 100644 index 00000000000..7024db7cb49 --- /dev/null +++ b/test/fixtures/controller.line/radius/indexable.js @@ -0,0 +1,45 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBackgroundColor: '#00ff00', + pointRadius: [ + 1, 2, 3, 4, 5, 6 + ] + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + elements: { + line: { + fill: false, + }, + point: { + backgroundColor: '#ff0000', + radius: [ + 6, 5, 4, 3, 2, 1 + ], + } + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/radius/indexable.png b/test/fixtures/controller.line/radius/indexable.png new file mode 100644 index 00000000000..f1b035faf1c Binary files /dev/null and b/test/fixtures/controller.line/radius/indexable.png differ diff --git a/test/fixtures/controller.line/radius/scriptable-to-value.js b/test/fixtures/controller.line/radius/scriptable-to-value.js new file mode 100644 index 00000000000..9492ccf56ce --- /dev/null +++ b/test/fixtures/controller.line/radius/scriptable-to-value.js @@ -0,0 +1,32 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: ['A', 'B', 'C'], + datasets: [{ + data: [12, 19, 3] + }] + }, + options: { + animation: { + duration: 0 + }, + backgroundColor: 'red', + radius: () => 20, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + }, + run: (chart) => { + chart.options.radius = 5; + chart.update(); + } + } +}; diff --git a/test/fixtures/controller.line/radius/scriptable-to-value.png b/test/fixtures/controller.line/radius/scriptable-to-value.png new file mode 100644 index 00000000000..d0e3b21d711 Binary files /dev/null and b/test/fixtures/controller.line/radius/scriptable-to-value.png differ diff --git a/test/fixtures/controller.line/radius/scriptable.js b/test/fixtures/controller.line/radius/scriptable.js new file mode 100644 index 00000000000..2d524999ef0 --- /dev/null +++ b/test/fixtures/controller.line/radius/scriptable.js @@ -0,0 +1,54 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBackgroundColor: '#0000ff', + pointRadius: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 4 ? 10 + : value > -4 ? 5 + : 2; + } + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + elements: { + line: { + fill: false, + }, + point: { + backgroundColor: '#ff0000', + radius: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 4 ? 2 + : value > -4 ? 5 + : 10; + }, + } + }, + scales: { + x: {display: false}, + y: { + display: false, + beginAtZero: true + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/radius/scriptable.png b/test/fixtures/controller.line/radius/scriptable.png new file mode 100644 index 00000000000..c162a9bfacd Binary files /dev/null and b/test/fixtures/controller.line/radius/scriptable.png differ diff --git a/test/fixtures/controller.line/radius/value.js b/test/fixtures/controller.line/radius/value.js new file mode 100644 index 00000000000..fccbe7fc728 --- /dev/null +++ b/test/fixtures/controller.line/radius/value.js @@ -0,0 +1,41 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBackgroundColor: '#0000ff', + pointRadius: 6 + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + elements: { + line: { + fill: false, + }, + point: { + backgroundColor: '#00ff00', + radius: 3, + } + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/radius/value.png b/test/fixtures/controller.line/radius/value.png new file mode 100644 index 00000000000..4b235bcd944 Binary files /dev/null and b/test/fixtures/controller.line/radius/value.png differ diff --git a/test/fixtures/controller.line/rotation/indexable.js b/test/fixtures/controller.line/rotation/indexable.js new file mode 100644 index 00000000000..6c7a86d8e81 --- /dev/null +++ b/test/fixtures/controller.line/rotation/indexable.js @@ -0,0 +1,47 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBorderColor: '#00ff00', + pointRotation: [ + 0, 30, 60, 90, 120, 150 + ] + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + elements: { + line: { + fill: false, + }, + point: { + borderColor: '#ff0000', + borderWidth: 10, + pointStyle: 'line', + rotation: [ + 150, 120, 90, 60, 30, 0 + ], + } + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/rotation/indexable.png b/test/fixtures/controller.line/rotation/indexable.png new file mode 100644 index 00000000000..66e080d908d Binary files /dev/null and b/test/fixtures/controller.line/rotation/indexable.png differ diff --git a/test/fixtures/controller.line/rotation/scriptable.js b/test/fixtures/controller.line/rotation/scriptable.js new file mode 100644 index 00000000000..d22b9d13b95 --- /dev/null +++ b/test/fixtures/controller.line/rotation/scriptable.js @@ -0,0 +1,56 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBorderColor: '#0000ff', + pointRotation: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 4 ? 120 + : value > -4 ? 60 + : 0; + } + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + elements: { + line: { + fill: false, + }, + point: { + borderColor: '#ff0000', + rotation: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 4 ? 0 + : value > -4 ? 60 + : 120; + }, + pointStyle: 'line', + radius: 10, + } + }, + scales: { + x: {display: false}, + y: { + display: false, + beginAtZero: true + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/rotation/scriptable.png b/test/fixtures/controller.line/rotation/scriptable.png new file mode 100644 index 00000000000..4bc6c674a1c Binary files /dev/null and b/test/fixtures/controller.line/rotation/scriptable.png differ diff --git a/test/fixtures/controller.line/rotation/value.js b/test/fixtures/controller.line/rotation/value.js new file mode 100644 index 00000000000..d96e37d2682 --- /dev/null +++ b/test/fixtures/controller.line/rotation/value.js @@ -0,0 +1,43 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBorderColor: '#0000ff', + pointRotation: 90 + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + elements: { + line: { + fill: false, + }, + point: { + borderColor: '#00ff00', + pointStyle: 'line', + radius: 10, + rotation: 0, + } + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/rotation/value.png b/test/fixtures/controller.line/rotation/value.png new file mode 100644 index 00000000000..8423874d8d4 Binary files /dev/null and b/test/fixtures/controller.line/rotation/value.png differ diff --git a/test/fixtures/controller.line/segments/gap.js b/test/fixtures/controller.line/segments/gap.js new file mode 100644 index 00000000000..db0652caa7a --- /dev/null +++ b/test/fixtures/controller.line/segments/gap.js @@ -0,0 +1,23 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: ['a', 'b', 'c', 'd', 'e', 'f'], + datasets: [{ + data: [1, 3, NaN, NaN, 2, 1], + borderColor: 'black', + segment: { + borderColor: ctx => ctx.p0.skip || ctx.p1.skip ? 'red' : undefined, + borderDash: ctx => ctx.p0.skip || ctx.p1.skip ? [5, 5] : undefined + }, + spanGaps: true + }] + }, + options: { + scales: { + x: {display: false}, + y: {display: false} + } + } + } +}; diff --git a/test/fixtures/controller.line/segments/gap.png b/test/fixtures/controller.line/segments/gap.png new file mode 100644 index 00000000000..5a7e5aff351 Binary files /dev/null and b/test/fixtures/controller.line/segments/gap.png differ diff --git a/test/fixtures/controller.line/segments/gradient.js b/test/fixtures/controller.line/segments/gradient.js new file mode 100644 index 00000000000..8a09503e9d7 --- /dev/null +++ b/test/fixtures/controller.line/segments/gradient.js @@ -0,0 +1,34 @@ +const getGradient = (context) => { + const {chart, p0, p1} = context; + const ctx = chart.ctx; + const {x: x0} = p0.getProps(['x'], true); + const {x: x1} = p1.getProps(['x'], true); + const gradient = ctx.createLinearGradient(x0, 0, x1, 0); + gradient.addColorStop(0, p0.options.backgroundColor); + gradient.addColorStop(1, p1.options.backgroundColor); + return gradient; +}; + +module.exports = { + config: { + type: 'line', + data: { + datasets: [{ + data: [{x: 0, y: 0}, {x: 1, y: 1}, {x: 2, y: 2}, {x: 3, y: 3}, {x: 4, y: 4}, {x: 5, y: 5}, {x: 6, y: 6}], + pointBackgroundColor: ['red', 'yellow', 'green', 'green', 'blue', 'pink', 'blue'], + pointBorderWidth: 0, + pointRadius: 10, + borderWidth: 5, + segment: { + borderColor: getGradient, + } + }] + }, + options: { + scales: { + x: {type: 'linear', display: false}, + y: {display: false} + } + } + } +}; diff --git a/test/fixtures/controller.line/segments/gradient.png b/test/fixtures/controller.line/segments/gradient.png new file mode 100644 index 00000000000..74bdef26120 Binary files /dev/null and b/test/fixtures/controller.line/segments/gradient.png differ diff --git a/test/fixtures/controller.line/segments/range.js b/test/fixtures/controller.line/segments/range.js new file mode 100644 index 00000000000..b0adfb2e14c --- /dev/null +++ b/test/fixtures/controller.line/segments/range.js @@ -0,0 +1,33 @@ +function x(ctx, {min = -Infinity, max = Infinity}) { + return (ctx.p0.parsed.x >= min || ctx.p1.parsed.x >= min) && (ctx.p0.parsed.x < max && ctx.p1.parsed.x < max); +} + +function y(ctx, {min = -Infinity, max = Infinity}) { + return (ctx.p0.parsed.y >= min || ctx.p1.parsed.y >= min) && (ctx.p0.parsed.y < max || ctx.p1.parsed.y < max); +} + +function xy(ctx, xr, yr) { + return x(ctx, xr) && y(ctx, yr); +} + +module.exports = { + config: { + type: 'line', + data: { + datasets: [{ + data: [{x: 0, y: 0}, {x: 1, y: 1}, {x: 2, y: 2}, {x: 3, y: 3}, {x: 4, y: 4}, {x: 5, y: 5}, {x: 6, y: 7}, {x: 7, y: 8}], + borderColor: 'black', + segment: { + borderColor: ctx => x(ctx, {min: 3, max: 4}) ? 'red' : y(ctx, {min: 5}) ? 'green' : xy(ctx, {min: 0}, {max: 1}) ? 'blue' : undefined, + borderDash: ctx => x(ctx, {min: 3, max: 4}) || y(ctx, {min: 5}) ? [5, 5] : undefined, + } + }] + }, + options: { + scales: { + x: {type: 'linear', display: false}, + y: {display: false} + } + } + } +}; diff --git a/test/fixtures/controller.line/segments/range.png b/test/fixtures/controller.line/segments/range.png new file mode 100644 index 00000000000..eebb8a41daa Binary files /dev/null and b/test/fixtures/controller.line/segments/range.png differ diff --git a/test/fixtures/controller.line/segments/single.js b/test/fixtures/controller.line/segments/single.js new file mode 100644 index 00000000000..c825dd9ca6b --- /dev/null +++ b/test/fixtures/controller.line/segments/single.js @@ -0,0 +1,27 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: ['a', 'b', 'c', 'd', 'e', 'f'], + datasets: [{ + data: [1, 2, 3, 3, 2, 1], + borderColor: 'black', + segment: { + borderColor: 'red', + } + }] + }, + options: { + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + width: 256, + height: 256 + } + } +}; diff --git a/test/fixtures/controller.line/segments/single.png b/test/fixtures/controller.line/segments/single.png new file mode 100644 index 00000000000..e9c80ea2d27 Binary files /dev/null and b/test/fixtures/controller.line/segments/single.png differ diff --git a/test/fixtures/controller.line/segments/slope.js b/test/fixtures/controller.line/segments/slope.js new file mode 100644 index 00000000000..7fcc948c131 --- /dev/null +++ b/test/fixtures/controller.line/segments/slope.js @@ -0,0 +1,26 @@ +function slope({p0, p1}) { + return (p0.y - p1.y) / (p1.x - p0.x); +} + +module.exports = { + config: { + type: 'line', + data: { + labels: ['a', 'b', 'c', 'd', 'e', 'f'], + datasets: [{ + data: [1, 2, 3, 3, 2, 1], + borderColor: 'black', + segment: { + borderColor: ctx => slope(ctx) > 0 ? 'green' : slope(ctx) < 0 ? 'red' : undefined, + borderDash: ctx => slope(ctx) < 0 ? [5, 5] : undefined + } + }] + }, + options: { + scales: { + x: {display: false}, + y: {display: false} + } + } + } +}; diff --git a/test/fixtures/controller.line/segments/slope.png b/test/fixtures/controller.line/segments/slope.png new file mode 100644 index 00000000000..969373030b6 Binary files /dev/null and b/test/fixtures/controller.line/segments/slope.png differ diff --git a/test/fixtures/controller.line/segments/spanGaps.js b/test/fixtures/controller.line/segments/spanGaps.js new file mode 100644 index 00000000000..816d5bec8b3 --- /dev/null +++ b/test/fixtures/controller.line/segments/spanGaps.js @@ -0,0 +1,31 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: ['a', 'b', 'c', 'd', 'e', 'f'], + datasets: [{ + data: [1, 3, null, null, 2, 1], + segment: { + borderColor: ctx => ctx.p1.parsed.x > 2 ? 'red' : undefined, + borderDash: ctx => ctx.p1.parsed.x > 3 ? [6, 6] : undefined, + }, + spanGaps: true + }, { + data: [0, 2, null, null, 1, 0], + segment: { + borderColor: ctx => ctx.p1.parsed.x > 2 ? 'red' : undefined, + borderDash: ctx => ctx.p1.parsed.x > 3 ? [6, 6] : undefined, + }, + spanGaps: false + }] + }, + options: { + borderColor: 'black', + radius: 0, + scales: { + x: {display: false}, + y: {display: false} + } + } + } +}; diff --git a/test/fixtures/controller.line/segments/spanGaps.png b/test/fixtures/controller.line/segments/spanGaps.png new file mode 100644 index 00000000000..90b964a64bd Binary files /dev/null and b/test/fixtures/controller.line/segments/spanGaps.png differ diff --git a/test/fixtures/controller.line/showLine/dataset.js b/test/fixtures/controller.line/showLine/dataset.js new file mode 100644 index 00000000000..d8553d6086b --- /dev/null +++ b/test/fixtures/controller.line/showLine/dataset.js @@ -0,0 +1,40 @@ +module.exports = { + description: 'should draw all elements except lines turned off per dataset', + config: { + type: 'line', + data: { + datasets: [{ + data: [10, 15, 0, -4], + label: 'dataset1', + borderColor: 'red', + backgroundColor: 'green', + showLine: false, + fill: false + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + showLine: true, + scales: { + x: { + display: false + }, + y: { + display: false + } + }, + plugins: { + legend: false, + title: false, + tooltip: false, + filler: true + } + } + }, + options: { + canvas: { + width: 512, + height: 512 + } + } +}; diff --git a/test/fixtures/controller.line/showLine/dataset.png b/test/fixtures/controller.line/showLine/dataset.png new file mode 100644 index 00000000000..79e1628f2ee Binary files /dev/null and b/test/fixtures/controller.line/showLine/dataset.png differ diff --git a/test/fixtures/controller.line/showLine/false.js b/test/fixtures/controller.line/showLine/false.js new file mode 100644 index 00000000000..2457c2a8b84 --- /dev/null +++ b/test/fixtures/controller.line/showLine/false.js @@ -0,0 +1,33 @@ +module.exports = { + description: 'should draw all elements except lines', + config: { + type: 'line', + data: { + datasets: [{ + data: [10, 15, 0, -4], + label: 'dataset1', + borderColor: 'red', + backgroundColor: 'green', + fill: true + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + showLine: false, + scales: { + x: { + display: false + }, + y: { + display: false + } + }, + plugins: { + legend: false, + title: false, + tooltip: false, + filler: true + } + } + } +}; diff --git a/test/fixtures/controller.line/showLine/false.png b/test/fixtures/controller.line/showLine/false.png new file mode 100644 index 00000000000..3d920d71ec3 Binary files /dev/null and b/test/fixtures/controller.line/showLine/false.png differ diff --git a/test/fixtures/controller.line/stacking/bounds-data.js b/test/fixtures/controller.line/stacking/bounds-data.js new file mode 100644 index 00000000000..413af181f5a --- /dev/null +++ b/test/fixtures/controller.line/stacking/bounds-data.js @@ -0,0 +1,29 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: ['a', 'b'], + datasets: [{ + borderColor: 'red', + data: [50, 75], + }, { + borderColor: 'blue', + data: [25, 50], + }] + }, + options: { + scales: { + x: { + display: false + }, + y: { + stacked: true, + bounds: 'data' + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/controller.line/stacking/bounds-data.png b/test/fixtures/controller.line/stacking/bounds-data.png new file mode 100644 index 00000000000..71ea7e96392 Binary files /dev/null and b/test/fixtures/controller.line/stacking/bounds-data.png differ diff --git a/test/fixtures/controller.line/stacking/order-default.js b/test/fixtures/controller.line/stacking/order-default.js new file mode 100644 index 00000000000..18c694f7521 --- /dev/null +++ b/test/fixtures/controller.line/stacking/order-default.js @@ -0,0 +1,49 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [3, 1, 2, 0, 8, 1], + backgroundColor: '#ff0000' + }, + { + // option in element (fallback) + data: [0, 4, 2, 6, 4, 8], + backgroundColor: '#00ff00' + } + ] + }, + options: { + elements: { + line: { + fill: true + }, + point: { + radius: 0 + } + }, + layout: { + padding: 32 + }, + scales: { + x: {stacked: true, display: false}, + y: {stacked: true, display: false} + }, + plugins: { + legend: false, + title: false, + tooltip: false, + filler: true + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/stacking/order-default.png b/test/fixtures/controller.line/stacking/order-default.png new file mode 100644 index 00000000000..2e93a7bf93d Binary files /dev/null and b/test/fixtures/controller.line/stacking/order-default.png differ diff --git a/test/fixtures/controller.line/stacking/order-specified.js b/test/fixtures/controller.line/stacking/order-specified.js new file mode 100644 index 00000000000..262988d8a37 --- /dev/null +++ b/test/fixtures/controller.line/stacking/order-specified.js @@ -0,0 +1,51 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [3, 1, 2, 0, 8, 1], + backgroundColor: '#ff0000', + order: 2 + }, + { + // option in element (fallback) + data: [0, 4, 2, 6, 4, 8], + backgroundColor: '#00ff00', + order: 1 + } + ] + }, + options: { + elements: { + line: { + fill: true + }, + point: { + radius: 0 + } + }, + layout: { + padding: 32 + }, + scales: { + x: {stacked: true, display: false}, + y: {stacked: true, display: false} + }, + plugins: { + legend: false, + title: false, + tooltip: false, + filler: true + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/stacking/order-specified.png b/test/fixtures/controller.line/stacking/order-specified.png new file mode 100644 index 00000000000..b832bd9db98 Binary files /dev/null and b/test/fixtures/controller.line/stacking/order-specified.png differ diff --git a/test/fixtures/controller.line/stacking/single.js b/test/fixtures/controller.line/stacking/single.js new file mode 100644 index 00000000000..92519880f8e --- /dev/null +++ b/test/fixtures/controller.line/stacking/single.js @@ -0,0 +1,51 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2], + datasets: [ + { + data: [0, -1, -1], + backgroundColor: '#ff0000', + }, + { + data: [0, 2, 2], + backgroundColor: '#00ff00', + }, + { + data: [0, 0, 1], + backgroundColor: '#0000ff', + } + ] + }, + options: { + elements: { + line: { + fill: '-1', + }, + point: { + radius: 0 + } + }, + layout: { + padding: 32 + }, + plugins: { + legend: false, + title: false, + tooltip: false, + filler: true + }, + scales: { + x: {display: false}, + y: {display: false, stacked: 'single'} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/stacking/single.png b/test/fixtures/controller.line/stacking/single.png new file mode 100644 index 00000000000..595773cc4f4 Binary files /dev/null and b/test/fixtures/controller.line/stacking/single.png differ diff --git a/test/fixtures/controller.line/stacking/stacked-scatter.js b/test/fixtures/controller.line/stacking/stacked-scatter.js new file mode 100644 index 00000000000..00da2a77bd5 --- /dev/null +++ b/test/fixtures/controller.line/stacking/stacked-scatter.js @@ -0,0 +1,74 @@ +module.exports = { + config: { + type: 'scatter', + data: { + datasets: [{ + label: 'label1', + data: [{ + x: 0, + y: 30 + }, { + x: 5, + y: 35 + }, { + x: 10, + y: 20 + }], + backgroundColor: '#42A8E4' + }, + { + label: 'label2', + data: [{ + x: 0, + y: 10 + }, { + x: 5, + y: 15 + }, { + x: 10, + y: 15 + }], + backgroundColor: '#FC3F55' + }, + { + label: 'label3', + data: [{ + x: 0, + y: -15 + }, { + x: 5, + y: -10 + }, { + x: 10, + y: -20 + }], + backgroundColor: '#FFBE3F' + }], + }, + options: { + scales: { + x: { + display: false, + position: 'bottom', + }, + y: { + stacked: true, + display: false, + position: 'left', + }, + }, + plugins: { + legend: false, + title: false, + tooltip: false, + filler: true + } + }, + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.line/stacking/stacked-scatter.png b/test/fixtures/controller.line/stacking/stacked-scatter.png new file mode 100644 index 00000000000..3cb6ce32dbf Binary files /dev/null and b/test/fixtures/controller.line/stacking/stacked-scatter.png differ diff --git a/test/fixtures/controller.line/stacking/updates.js b/test/fixtures/controller.line/stacking/updates.js new file mode 100644 index 00000000000..0853f0b35b8 --- /dev/null +++ b/test/fixtures/controller.line/stacking/updates.js @@ -0,0 +1,40 @@ +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/9424', + config: { + type: 'line', + data: { + labels: [0, 1, 2], + datasets: [ + { + data: [1, 1, 1], + stack: 's1', + borderColor: '#ff0000', + }, + { + data: [2, 2, 2], + stack: 's1', + borderColor: '#00ff00', + }, + { + data: [3, 3, 3], + stack: 's1', + borderColor: '#0000ff', + } + ] + }, + options: { + borderWidth: 5, + scales: { + x: {display: false}, + y: {display: true, stacked: true} + } + } + }, + options: { + spriteText: true, + run(chart) { + chart.data.datasets.splice(1, 0, {data: [1.5, 1.5, 1.5], stack: 's2', borderColor: '#000000'}); + chart.update(); + } + } +}; diff --git a/test/fixtures/controller.line/stacking/updates.png b/test/fixtures/controller.line/stacking/updates.png new file mode 100644 index 00000000000..2ca99579bf1 Binary files /dev/null and b/test/fixtures/controller.line/stacking/updates.png differ diff --git a/test/fixtures/controller.polarArea/angle-array.json b/test/fixtures/controller.polarArea/angle-array.json new file mode 100644 index 00000000000..a1f835a38ea --- /dev/null +++ b/test/fixtures/controller.polarArea/angle-array.json @@ -0,0 +1,34 @@ +{ + "config": { + "type": "polarArea", + "data": { + "labels": ["A", "B", "C", "D", "E"], + "datasets": [ + { + "data": [11, 16, 21, 7, 10], + "backgroundColor": [ + "rgba(255, 99, 132, 0.8)", + "rgba(54, 162, 235, 0.8)", + "rgba(255, 206, 86, 0.8)", + "rgba(75, 192, 192, 0.8)", + "rgba(153, 102, 255, 0.8)", + "rgba(255, 159, 64, 0.8)" + ] + } + ] + }, + "options": { + "elements": { + "arc": { + "angle": [60.5387, 100.6457, 60.5387, 123.5641, 14.7021] + } + }, + "responsive": false, + "scales": { + "r": { + "display": false + } + } + } + } +} diff --git a/test/fixtures/controller.polarArea/angle-array.png b/test/fixtures/controller.polarArea/angle-array.png new file mode 100644 index 00000000000..9594be93ac2 Binary files /dev/null and b/test/fixtures/controller.polarArea/angle-array.png differ diff --git a/test/fixtures/controller.polarArea/angle-lines.json b/test/fixtures/controller.polarArea/angle-lines.json new file mode 100644 index 00000000000..55d8a42a270 --- /dev/null +++ b/test/fixtures/controller.polarArea/angle-lines.json @@ -0,0 +1,37 @@ +{ + "threshold": 0.05, + "config": { + "type": "polarArea", + "data": { + "labels": ["A", "B", "C", "D", "E"], + "datasets": [ + { + "data": [11, 16, 21, 7, 10], + "backgroundColor": [ + "rgba(255, 99, 132, 0.8)", + "rgba(54, 162, 235, 0.8)", + "rgba(255, 206, 86, 0.8)", + "rgba(75, 192, 192, 0.8)", + "rgba(153, 102, 255, 0.8)", + "rgba(255, 159, 64, 0.8)" + ] + } + ] + }, + "options": { + "responsive": false, + "scales": { + "r": { + "display": true, + "angleLines": { + "display": true, + "color": "#000" + }, + "ticks": { + "display": false + } + } + } + } + } +} diff --git a/test/fixtures/controller.polarArea/angle-lines.png b/test/fixtures/controller.polarArea/angle-lines.png new file mode 100644 index 00000000000..3890d7cb64a Binary files /dev/null and b/test/fixtures/controller.polarArea/angle-lines.png differ diff --git a/test/fixtures/controller.polarArea/angle-undefined.json b/test/fixtures/controller.polarArea/angle-undefined.json new file mode 100644 index 00000000000..b2fc1a73613 --- /dev/null +++ b/test/fixtures/controller.polarArea/angle-undefined.json @@ -0,0 +1,29 @@ +{ + "config": { + "type": "polarArea", + "data": { + "labels": ["A", "B", "C", "D", "E"], + "datasets": [ + { + "data": [11, 16, 21, 7, 10], + "backgroundColor": [ + "rgba(255, 99, 132, 0.8)", + "rgba(54, 162, 235, 0.8)", + "rgba(255, 206, 86, 0.8)", + "rgba(75, 192, 192, 0.8)", + "rgba(153, 102, 255, 0.8)", + "rgba(255, 159, 64, 0.8)" + ] + } + ] + }, + "options": { + "responsive": false, + "scales": { + "r": { + "display": false + } + } + } + } +} diff --git a/test/fixtures/controller.polarArea/angle-undefined.png b/test/fixtures/controller.polarArea/angle-undefined.png new file mode 100644 index 00000000000..df9b4bf7909 Binary files /dev/null and b/test/fixtures/controller.polarArea/angle-undefined.png differ diff --git a/test/fixtures/controller.polarArea/backgroundColor/indexable-dataset.js b/test/fixtures/controller.polarArea/backgroundColor/indexable-dataset.js new file mode 100644 index 00000000000..1b92d096bae --- /dev/null +++ b/test/fixtures/controller.polarArea/backgroundColor/indexable-dataset.js @@ -0,0 +1,35 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 2, 4, null, 6, 8], + backgroundColor: [ + '#ff0000', + '#00ff00', + '#0000ff', + '#ffff00', + '#ff00ff', + '#000000' + ] + }, + ] + }, + options: { + scales: { + r: { + display: false + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.polarArea/backgroundColor/indexable-dataset.png b/test/fixtures/controller.polarArea/backgroundColor/indexable-dataset.png new file mode 100644 index 00000000000..9b97db092ba Binary files /dev/null and b/test/fixtures/controller.polarArea/backgroundColor/indexable-dataset.png differ diff --git a/test/fixtures/controller.polarArea/backgroundColor/indexable-element-options.js b/test/fixtures/controller.polarArea/backgroundColor/indexable-element-options.js new file mode 100644 index 00000000000..fb3d4f42ce4 --- /dev/null +++ b/test/fixtures/controller.polarArea/backgroundColor/indexable-element-options.js @@ -0,0 +1,39 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in element (fallback) + data: [0, 2, 4, null, 6, 8], + } + ] + }, + options: { + elements: { + arc: { + backgroundColor: [ + '#ff88ff', + '#888888', + '#ff8800', + '#00ff88', + '#8800ff', + '#ffff88' + ] + } + }, + scales: { + r: { + display: false + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.polarArea/backgroundColor/indexable-element-options.png b/test/fixtures/controller.polarArea/backgroundColor/indexable-element-options.png new file mode 100644 index 00000000000..83e7abee843 Binary files /dev/null and b/test/fixtures/controller.polarArea/backgroundColor/indexable-element-options.png differ diff --git a/test/fixtures/controller.polarArea/backgroundColor/scriptable-dataset.js b/test/fixtures/controller.polarArea/backgroundColor/scriptable-dataset.js new file mode 100644 index 00000000000..8d11108aadf --- /dev/null +++ b/test/fixtures/controller.polarArea/backgroundColor/scriptable-dataset.js @@ -0,0 +1,34 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 2, 4, null, 6, 8], + backgroundColor: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 8 ? '#ff0000' + : value > 6 ? '#00ff00' + : value > 2 ? '#0000ff' + : '#ff00ff'; + } + }, + ] + }, + options: { + scales: { + r: { + display: false + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.polarArea/backgroundColor/scriptable-dataset.png b/test/fixtures/controller.polarArea/backgroundColor/scriptable-dataset.png new file mode 100644 index 00000000000..b96ab87c05f Binary files /dev/null and b/test/fixtures/controller.polarArea/backgroundColor/scriptable-dataset.png differ diff --git a/test/fixtures/controller.polarArea/backgroundColor/scriptable-element-options.js b/test/fixtures/controller.polarArea/backgroundColor/scriptable-element-options.js new file mode 100644 index 00000000000..36415025c2c --- /dev/null +++ b/test/fixtures/controller.polarArea/backgroundColor/scriptable-element-options.js @@ -0,0 +1,38 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in element (fallback) + data: [0, 2, 4, null, 6, 8], + } + ] + }, + options: { + elements: { + arc: { + backgroundColor: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 8 ? '#ff0000' + : value > 6 ? '#00ff00' + : value > 2 ? '#0000ff' + : '#ff00ff'; + } + } + }, + scales: { + r: { + display: false + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.polarArea/backgroundColor/scriptable-element-options.png b/test/fixtures/controller.polarArea/backgroundColor/scriptable-element-options.png new file mode 100644 index 00000000000..b96ab87c05f Binary files /dev/null and b/test/fixtures/controller.polarArea/backgroundColor/scriptable-element-options.png differ diff --git a/test/fixtures/controller.polarArea/backgroundColor/value-dataset.js b/test/fixtures/controller.polarArea/backgroundColor/value-dataset.js new file mode 100644 index 00000000000..b9be824a6f0 --- /dev/null +++ b/test/fixtures/controller.polarArea/backgroundColor/value-dataset.js @@ -0,0 +1,28 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 2, 4, null, 6, 8], + backgroundColor: '#ff0000' + }, + ] + }, + options: { + scales: { + r: { + display: false + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.polarArea/backgroundColor/value-dataset.png b/test/fixtures/controller.polarArea/backgroundColor/value-dataset.png new file mode 100644 index 00000000000..ae9c48e60fd Binary files /dev/null and b/test/fixtures/controller.polarArea/backgroundColor/value-dataset.png differ diff --git a/test/fixtures/controller.polarArea/backgroundColor/value-element-options.js b/test/fixtures/controller.polarArea/backgroundColor/value-element-options.js new file mode 100644 index 00000000000..f68b70dbf09 --- /dev/null +++ b/test/fixtures/controller.polarArea/backgroundColor/value-element-options.js @@ -0,0 +1,32 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in element (fallback) + data: [0, 2, 4, null, 6, 8], + } + ] + }, + options: { + elements: { + arc: { + backgroundColor: '#00ff00' + } + }, + scales: { + r: { + display: false + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.polarArea/backgroundColor/value-element-options.png b/test/fixtures/controller.polarArea/backgroundColor/value-element-options.png new file mode 100644 index 00000000000..36bab00a685 Binary files /dev/null and b/test/fixtures/controller.polarArea/backgroundColor/value-element-options.png differ diff --git a/test/fixtures/controller.polarArea/border-align-center.json b/test/fixtures/controller.polarArea/border-align-center.json new file mode 100644 index 00000000000..c3ecf87ee21 --- /dev/null +++ b/test/fixtures/controller.polarArea/border-align-center.json @@ -0,0 +1,41 @@ +{ + "config": { + "type": "polarArea", + "data": { + "labels": ["A", "B", "C", "D", "E"], + "datasets": [ + { + "data": [11, 16, 21, 1, 10], + "backgroundColor": [ + "rgba(255, 99, 132, 0.8)", + "rgba(54, 162, 235, 0.8)", + "rgba(255, 206, 86, 0.8)", + "rgba(75, 192, 192, 0.8)", + "rgba(153, 102, 255, 0.8)" + ], + "borderWidth": 20, + "borderColor": [ + "rgb(255, 99, 132)", + "rgb(54, 162, 235)", + "rgb(255, 206, 86)", + "rgb(75, 192, 192)", + "rgb(153, 102, 255)" + ] + } + ] + }, + "options": { + "elements": { + "arc": { + "angle": [2.1658, 10.8404, 21.6922, 108.4323, 216.8588] + } + }, + "responsive": false, + "scales": { + "r": { + "display": false + } + } + } + } +} diff --git a/test/fixtures/controller.polarArea/border-align-center.png b/test/fixtures/controller.polarArea/border-align-center.png new file mode 100644 index 00000000000..ab0941bbb4d Binary files /dev/null and b/test/fixtures/controller.polarArea/border-align-center.png differ diff --git a/test/fixtures/controller.polarArea/border-align-inner.json b/test/fixtures/controller.polarArea/border-align-inner.json new file mode 100644 index 00000000000..d709b0f8bab --- /dev/null +++ b/test/fixtures/controller.polarArea/border-align-inner.json @@ -0,0 +1,42 @@ +{ + "config": { + "type": "polarArea", + "data": { + "labels": ["A", "B", "C", "D", "E"], + "datasets": [ + { + "data": [11, 16, 21, 1, 10], + "backgroundColor": [ + "rgba(255, 99, 132, 0.8)", + "rgba(54, 162, 235, 0.8)", + "rgba(255, 206, 86, 0.8)", + "rgba(75, 192, 192, 0.8)", + "rgba(153, 102, 255, 0.8)" + ], + "borderWidth": 20, + "borderColor": [ + "rgb(255, 99, 132)", + "rgb(54, 162, 235)", + "rgb(255, 206, 86)", + "rgb(75, 192, 192)", + "rgb(153, 102, 255)" + ], + "borderAlign": "inner" + } + ] + }, + "options": { + "elements": { + "arc": { + "angle": [2.1658, 10.8404, 21.6922, 108.4323, 216.8588] + } + }, + "responsive": false, + "scales": { + "r": { + "display": false + } + } + } + } +} diff --git a/test/fixtures/controller.polarArea/border-align-inner.png b/test/fixtures/controller.polarArea/border-align-inner.png new file mode 100644 index 00000000000..f3911a4754d Binary files /dev/null and b/test/fixtures/controller.polarArea/border-align-inner.png differ diff --git a/test/fixtures/controller.polarArea/borderAlign/indexable-dataset.js b/test/fixtures/controller.polarArea/borderAlign/indexable-dataset.js new file mode 100644 index 00000000000..99b285d385f --- /dev/null +++ b/test/fixtures/controller.polarArea/borderAlign/indexable-dataset.js @@ -0,0 +1,43 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 2, 4, null, 6, 8], + borderAlign: [ + 'center', + 'inner', + 'center', + 'inner', + 'center', + 'inner', + ], + borderColor: '#00ff00' + }, + ] + }, + options: { + elements: { + arc: { + backgroundColor: 'transparent', + borderColor: '#ff0000', + borderWidth: 5, + } + }, + scales: { + r: { + display: false + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.polarArea/borderAlign/indexable-dataset.png b/test/fixtures/controller.polarArea/borderAlign/indexable-dataset.png new file mode 100644 index 00000000000..65ec6d4d0bf Binary files /dev/null and b/test/fixtures/controller.polarArea/borderAlign/indexable-dataset.png differ diff --git a/test/fixtures/controller.polarArea/borderAlign/indexable-element-options.js b/test/fixtures/controller.polarArea/borderAlign/indexable-element-options.js new file mode 100644 index 00000000000..fcdb698d47d --- /dev/null +++ b/test/fixtures/controller.polarArea/borderAlign/indexable-element-options.js @@ -0,0 +1,42 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in element (fallback) + data: [0, 2, 4, null, 6, 8], + } + ] + }, + options: { + elements: { + arc: { + backgroundColor: 'transparent', + borderColor: '#ff0000', + borderWidth: 5, + borderAlign: [ + 'center', + 'inner', + 'center', + 'inner', + 'center', + 'inner', + ] + } + }, + scales: { + r: { + display: false + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.polarArea/borderAlign/indexable-element-options.png b/test/fixtures/controller.polarArea/borderAlign/indexable-element-options.png new file mode 100644 index 00000000000..5b058c23b6d Binary files /dev/null and b/test/fixtures/controller.polarArea/borderAlign/indexable-element-options.png differ diff --git a/test/fixtures/controller.polarArea/borderAlign/scriptable-dataset.js b/test/fixtures/controller.polarArea/borderAlign/scriptable-dataset.js new file mode 100644 index 00000000000..6689e0841ac --- /dev/null +++ b/test/fixtures/controller.polarArea/borderAlign/scriptable-dataset.js @@ -0,0 +1,39 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 2, 4, null, 6, 8], + borderAlign: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 4 ? 'inner' : 'center'; + }, + borderColor: '#0000ff', + }, + ] + }, + options: { + elements: { + arc: { + backgroundColor: 'transparent', + borderColor: '#ff00ff', + borderWidth: 8, + } + }, + scales: { + r: { + display: false + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.polarArea/borderAlign/scriptable-dataset.png b/test/fixtures/controller.polarArea/borderAlign/scriptable-dataset.png new file mode 100644 index 00000000000..9bafab3bb5b Binary files /dev/null and b/test/fixtures/controller.polarArea/borderAlign/scriptable-dataset.png differ diff --git a/test/fixtures/controller.polarArea/borderAlign/scriptable-element-options.js b/test/fixtures/controller.polarArea/borderAlign/scriptable-element-options.js new file mode 100644 index 00000000000..4528cad8dc6 --- /dev/null +++ b/test/fixtures/controller.polarArea/borderAlign/scriptable-element-options.js @@ -0,0 +1,38 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in element (fallback) + data: [0, 2, 4, null, 6, 8], + } + ] + }, + options: { + elements: { + arc: { + backgroundColor: 'transparent', + borderColor: '#ff00ff', + borderWidth: 8, + borderAlign: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 4 ? 'center' : 'inner'; + } + } + }, + scales: { + r: { + display: false + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.polarArea/borderAlign/scriptable-element-options.png b/test/fixtures/controller.polarArea/borderAlign/scriptable-element-options.png new file mode 100644 index 00000000000..0978aa0c1c8 Binary files /dev/null and b/test/fixtures/controller.polarArea/borderAlign/scriptable-element-options.png differ diff --git a/test/fixtures/controller.polarArea/borderAlign/value-dataset.js b/test/fixtures/controller.polarArea/borderAlign/value-dataset.js new file mode 100644 index 00000000000..c201cbfd989 --- /dev/null +++ b/test/fixtures/controller.polarArea/borderAlign/value-dataset.js @@ -0,0 +1,36 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 2, 4, null, 6, 8], + borderAlign: 'inner', + borderColor: '#00ff00', + }, + ] + }, + options: { + elements: { + arc: { + backgroundColor: 'transparent', + borderColor: '#0000ff', + borderWidth: 4, + } + }, + scales: { + r: { + display: false + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.polarArea/borderAlign/value-dataset.png b/test/fixtures/controller.polarArea/borderAlign/value-dataset.png new file mode 100644 index 00000000000..c1eac58e748 Binary files /dev/null and b/test/fixtures/controller.polarArea/borderAlign/value-dataset.png differ diff --git a/test/fixtures/controller.polarArea/borderAlign/value-element-options.js b/test/fixtures/controller.polarArea/borderAlign/value-element-options.js new file mode 100644 index 00000000000..1f3ff0632e0 --- /dev/null +++ b/test/fixtures/controller.polarArea/borderAlign/value-element-options.js @@ -0,0 +1,35 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in element (fallback) + data: [0, 2, 4, null, 6, 8], + } + ] + }, + options: { + elements: { + arc: { + backgroundColor: 'transparent', + borderAlign: 'center', + borderColor: '#0000ff', + borderWidth: 4, + } + }, + scales: { + r: { + display: false + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.polarArea/borderAlign/value-element-options.png b/test/fixtures/controller.polarArea/borderAlign/value-element-options.png new file mode 100644 index 00000000000..d47b5ff9ab1 Binary files /dev/null and b/test/fixtures/controller.polarArea/borderAlign/value-element-options.png differ diff --git a/test/fixtures/controller.polarArea/borderColor/indexable-dataset.js b/test/fixtures/controller.polarArea/borderColor/indexable-dataset.js new file mode 100644 index 00000000000..6d74af80c9c --- /dev/null +++ b/test/fixtures/controller.polarArea/borderColor/indexable-dataset.js @@ -0,0 +1,41 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 2, 4, null, 6, 8], + borderColor: [ + '#ff0000', + '#00ff00', + '#0000ff', + '#ffff00', + '#ff00ff', + '#000000' + ] + }, + ] + }, + options: { + elements: { + arc: { + backgroundColor: 'transparent', + borderWidth: 8 + } + }, + scales: { + r: { + display: false + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.polarArea/borderColor/indexable-dataset.png b/test/fixtures/controller.polarArea/borderColor/indexable-dataset.png new file mode 100644 index 00000000000..10d3e58e4a6 Binary files /dev/null and b/test/fixtures/controller.polarArea/borderColor/indexable-dataset.png differ diff --git a/test/fixtures/controller.polarArea/borderColor/indexable-element-options.js b/test/fixtures/controller.polarArea/borderColor/indexable-element-options.js new file mode 100644 index 00000000000..550ff4c6fc7 --- /dev/null +++ b/test/fixtures/controller.polarArea/borderColor/indexable-element-options.js @@ -0,0 +1,41 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in element (fallback) + data: [0, 2, 4, null, 6, 8], + } + ] + }, + options: { + elements: { + arc: { + backgroundColor: 'transparent', + borderColor: [ + '#ff88ff', + '#888888', + '#ff8800', + '#00ff88', + '#8800ff', + '#ffff88' + ], + borderWidth: 8 + } + }, + scales: { + r: { + display: false + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.polarArea/borderColor/indexable-element-options.png b/test/fixtures/controller.polarArea/borderColor/indexable-element-options.png new file mode 100644 index 00000000000..992bba84cc6 Binary files /dev/null and b/test/fixtures/controller.polarArea/borderColor/indexable-element-options.png differ diff --git a/test/fixtures/controller.polarArea/borderColor/scriptable-dataset.js b/test/fixtures/controller.polarArea/borderColor/scriptable-dataset.js new file mode 100644 index 00000000000..376cd4b951a --- /dev/null +++ b/test/fixtures/controller.polarArea/borderColor/scriptable-dataset.js @@ -0,0 +1,40 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 2, 4, null, 6, 8], + borderColor: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 8 ? '#ff0000' + : value > 6 ? '#00ff00' + : value > 2 ? '#0000ff' + : '#ff00ff'; + } + }, + ] + }, + options: { + elements: { + arc: { + backgroundColor: 'transparent', + borderWidth: 8 + } + }, + scales: { + r: { + display: false + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.polarArea/borderColor/scriptable-dataset.png b/test/fixtures/controller.polarArea/borderColor/scriptable-dataset.png new file mode 100644 index 00000000000..8fadecd2cb8 Binary files /dev/null and b/test/fixtures/controller.polarArea/borderColor/scriptable-dataset.png differ diff --git a/test/fixtures/controller.polarArea/borderColor/scriptable-element-options.js b/test/fixtures/controller.polarArea/borderColor/scriptable-element-options.js new file mode 100644 index 00000000000..985e868cca6 --- /dev/null +++ b/test/fixtures/controller.polarArea/borderColor/scriptable-element-options.js @@ -0,0 +1,40 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in element (fallback) + data: [0, 2, 4, null, 6, 8], + } + ] + }, + options: { + elements: { + arc: { + backgroundColor: 'transparent', + borderColor: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 8 ? '#ff00ff' + : value > 6 ? '#0000ff' + : value > 2 ? '#ff0000' + : '#00ff00'; + }, + borderWidth: 8 + } + }, + scales: { + r: { + display: false + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.polarArea/borderColor/scriptable-element-options.png b/test/fixtures/controller.polarArea/borderColor/scriptable-element-options.png new file mode 100644 index 00000000000..3d60a6e7e75 Binary files /dev/null and b/test/fixtures/controller.polarArea/borderColor/scriptable-element-options.png differ diff --git a/test/fixtures/controller.polarArea/borderColor/value-dataset.js b/test/fixtures/controller.polarArea/borderColor/value-dataset.js new file mode 100644 index 00000000000..d2d40f0c4dd --- /dev/null +++ b/test/fixtures/controller.polarArea/borderColor/value-dataset.js @@ -0,0 +1,34 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 2, 4, null, 6, 8], + borderColor: '#ff0000' + }, + ] + }, + options: { + elements: { + arc: { + backgroundColor: 'transparent', + borderWidth: 8 + } + }, + scales: { + r: { + display: false + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.polarArea/borderColor/value-dataset.png b/test/fixtures/controller.polarArea/borderColor/value-dataset.png new file mode 100644 index 00000000000..3cb271b454a Binary files /dev/null and b/test/fixtures/controller.polarArea/borderColor/value-dataset.png differ diff --git a/test/fixtures/controller.polarArea/borderColor/value-element-options.js b/test/fixtures/controller.polarArea/borderColor/value-element-options.js new file mode 100644 index 00000000000..63b106aba93 --- /dev/null +++ b/test/fixtures/controller.polarArea/borderColor/value-element-options.js @@ -0,0 +1,34 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in element (fallback) + data: [0, 2, 4, null, 6, 8], + } + ] + }, + options: { + elements: { + arc: { + backgroundColor: 'transparent', + borderColor: '#00ff00', + borderWidth: 8 + } + }, + scales: { + r: { + display: false + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.polarArea/borderColor/value-element-options.png b/test/fixtures/controller.polarArea/borderColor/value-element-options.png new file mode 100644 index 00000000000..16ed5d9c76b Binary files /dev/null and b/test/fixtures/controller.polarArea/borderColor/value-element-options.png differ diff --git a/test/fixtures/controller.polarArea/borderDash/scriptable.js b/test/fixtures/controller.polarArea/borderDash/scriptable.js new file mode 100644 index 00000000000..3bce13b38c5 --- /dev/null +++ b/test/fixtures/controller.polarArea/borderDash/scriptable.js @@ -0,0 +1,38 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [5, 2, 4, 7, 6, 8] + } + ] + }, + options: { + elements: { + arc: { + backgroundColor: 'transparent', + borderColor: 'black', + borderWidth: 1, + borderDash: function(ctx) { + var value = (ctx.dataIndex || 0) % 2; + return value === 0 ? [3, 3] : []; + } + } + }, + scales: { + r: { + display: false + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.polarArea/borderDash/scriptable.png b/test/fixtures/controller.polarArea/borderDash/scriptable.png new file mode 100644 index 00000000000..5098dde9d69 Binary files /dev/null and b/test/fixtures/controller.polarArea/borderDash/scriptable.png differ diff --git a/test/fixtures/controller.polarArea/borderDash/value.js b/test/fixtures/controller.polarArea/borderDash/value.js new file mode 100644 index 00000000000..7e726e16cd4 --- /dev/null +++ b/test/fixtures/controller.polarArea/borderDash/value.js @@ -0,0 +1,36 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [5, 2, 4, 7, 6, 8], + borderAlign: 'inner', + borderColor: 'black' + }, + ] + }, + options: { + elements: { + arc: { + backgroundColor: 'transparent', + borderWidth: 1, + borderDash: [3, 3] + } + }, + scales: { + r: { + display: false + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.polarArea/borderDash/value.png b/test/fixtures/controller.polarArea/borderDash/value.png new file mode 100644 index 00000000000..2e44c316eac Binary files /dev/null and b/test/fixtures/controller.polarArea/borderDash/value.png differ diff --git a/test/fixtures/controller.polarArea/borderWidth/indexable-dataset.js b/test/fixtures/controller.polarArea/borderWidth/indexable-dataset.js new file mode 100644 index 00000000000..e968c2cd916 --- /dev/null +++ b/test/fixtures/controller.polarArea/borderWidth/indexable-dataset.js @@ -0,0 +1,41 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 2, 4, null, 6, 8], + borderWidth: [ + 0, + 1, + 2, + 3, + 4, + 5 + ] + }, + ] + }, + options: { + elements: { + arc: { + backgroundColor: 'transparent', + borderColor: '#888', + } + }, + scales: { + r: { + display: false + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.polarArea/borderWidth/indexable-dataset.png b/test/fixtures/controller.polarArea/borderWidth/indexable-dataset.png new file mode 100644 index 00000000000..0111acec541 Binary files /dev/null and b/test/fixtures/controller.polarArea/borderWidth/indexable-dataset.png differ diff --git a/test/fixtures/controller.polarArea/borderWidth/indexable-element-options.js b/test/fixtures/controller.polarArea/borderWidth/indexable-element-options.js new file mode 100644 index 00000000000..c18ce319546 --- /dev/null +++ b/test/fixtures/controller.polarArea/borderWidth/indexable-element-options.js @@ -0,0 +1,41 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in element (fallback) + data: [0, 2, 4, null, 6, 8], + } + ] + }, + options: { + elements: { + arc: { + backgroundColor: 'transparent', + borderColor: '#888', + borderWidth: [ + 5, + 4, + 3, + 2, + 1, + 0 + ] + } + }, + scales: { + r: { + display: false + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.polarArea/borderWidth/indexable-element-options.png b/test/fixtures/controller.polarArea/borderWidth/indexable-element-options.png new file mode 100644 index 00000000000..11a1f79b546 Binary files /dev/null and b/test/fixtures/controller.polarArea/borderWidth/indexable-element-options.png differ diff --git a/test/fixtures/controller.polarArea/borderWidth/scriptable-dataset.js b/test/fixtures/controller.polarArea/borderWidth/scriptable-dataset.js new file mode 100644 index 00000000000..72801329928 --- /dev/null +++ b/test/fixtures/controller.polarArea/borderWidth/scriptable-dataset.js @@ -0,0 +1,37 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 2, 4, null, 6, 8], + borderWidth: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return Math.abs(value); + } + }, + ] + }, + options: { + elements: { + arc: { + backgroundColor: 'transparent', + borderColor: '#888', + } + }, + scales: { + r: { + display: false + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.polarArea/borderWidth/scriptable-dataset.png b/test/fixtures/controller.polarArea/borderWidth/scriptable-dataset.png new file mode 100644 index 00000000000..154efebacc6 Binary files /dev/null and b/test/fixtures/controller.polarArea/borderWidth/scriptable-dataset.png differ diff --git a/test/fixtures/controller.polarArea/borderWidth/scriptable-element-options.js b/test/fixtures/controller.polarArea/borderWidth/scriptable-element-options.js new file mode 100644 index 00000000000..3bc685715be --- /dev/null +++ b/test/fixtures/controller.polarArea/borderWidth/scriptable-element-options.js @@ -0,0 +1,36 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in element (fallback) + data: [0, 2, 4, null, 6, 8], + } + ] + }, + options: { + elements: { + arc: { + backgroundColor: 'transparent', + borderColor: '#888', + borderWidth: function(ctx) { + return ctx.dataIndex * 2; + } + } + }, + scales: { + r: { + display: false + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.polarArea/borderWidth/scriptable-element-options.png b/test/fixtures/controller.polarArea/borderWidth/scriptable-element-options.png new file mode 100644 index 00000000000..0c7e8b88e3f Binary files /dev/null and b/test/fixtures/controller.polarArea/borderWidth/scriptable-element-options.png differ diff --git a/test/fixtures/controller.polarArea/borderWidth/value-dataset.js b/test/fixtures/controller.polarArea/borderWidth/value-dataset.js new file mode 100644 index 00000000000..019fa73c0f9 --- /dev/null +++ b/test/fixtures/controller.polarArea/borderWidth/value-dataset.js @@ -0,0 +1,34 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 2, 4, null, 6, 8], + borderWidth: 2 + }, + ] + }, + options: { + elements: { + arc: { + backgroundColor: 'transparent', + borderColor: '#888', + } + }, + scales: { + r: { + display: false + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.polarArea/borderWidth/value-dataset.png b/test/fixtures/controller.polarArea/borderWidth/value-dataset.png new file mode 100644 index 00000000000..a30dfc88bef Binary files /dev/null and b/test/fixtures/controller.polarArea/borderWidth/value-dataset.png differ diff --git a/test/fixtures/controller.polarArea/borderWidth/value-element-options.js b/test/fixtures/controller.polarArea/borderWidth/value-element-options.js new file mode 100644 index 00000000000..ce13ac48dea --- /dev/null +++ b/test/fixtures/controller.polarArea/borderWidth/value-element-options.js @@ -0,0 +1,34 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in element (fallback) + data: [0, 2, 4, null, 6, 8], + } + ] + }, + options: { + elements: { + arc: { + backgroundColor: 'transparent', + borderColor: '#888', + borderWidth: 4 + } + }, + scales: { + r: { + display: false + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.polarArea/borderWidth/value-element-options.png b/test/fixtures/controller.polarArea/borderWidth/value-element-options.png new file mode 100644 index 00000000000..32389e967f0 Binary files /dev/null and b/test/fixtures/controller.polarArea/borderWidth/value-element-options.png differ diff --git a/test/fixtures/controller.polarArea/last-slice-animate.js b/test/fixtures/controller.polarArea/last-slice-animate.js new file mode 100644 index 00000000000..43e3e866591 --- /dev/null +++ b/test/fixtures/controller.polarArea/last-slice-animate.js @@ -0,0 +1,70 @@ +const canvas = document.createElement('canvas'); +canvas.width = 512; +canvas.height = 512; +const ctx = canvas.getContext('2d'); + +module.exports = { + config: { + type: 'polarArea', + data: { + labels: ['A'], + datasets: [{ + data: [20], + backgroundColor: 'red', + }] + }, + options: { + animation: { + duration: 0, + easing: 'linear' + }, + responsive: false, + plugins: { + legend: false, + title: false, + tooltip: false, + filler: false + }, + scales: { + r: { + ticks: { + display: false, + } + } + } + }, + }, + options: { + canvas: { + height: 512, + width: 512 + }, + run: function(chart) { + chart.options.animation.duration = 8000; + chart.toggleDataVisibility(0); + chart.update(); + const animator = Chart.animator; + // disable animator + const backup = animator._refresh; + animator._refresh = function() { }; + + return new Promise((resolve) => { + window.requestAnimationFrame(() => { + const anims = animator._getAnims(chart); + const start = anims.items[0]._start; + for (let i = 0; i < 16; i++) { + animator._update(start + i * 500); + let x = i % 4 * 128; + let y = Math.floor(i / 4) * 128; + ctx.drawImage(chart.canvas, x, y, 128, 128); + } + Chart.helpers.clearCanvas(chart.canvas); + chart.ctx.drawImage(canvas, 0, 0); + + animator._refresh = backup; + resolve(); + }); + }); + } + } +}; diff --git a/test/fixtures/controller.polarArea/last-slice-animate.png b/test/fixtures/controller.polarArea/last-slice-animate.png new file mode 100644 index 00000000000..f6557037fb6 Binary files /dev/null and b/test/fixtures/controller.polarArea/last-slice-animate.png differ diff --git a/test/fixtures/controller.polarArea/parse-object-data.json b/test/fixtures/controller.polarArea/parse-object-data.json new file mode 100644 index 00000000000..17b8ebe595b --- /dev/null +++ b/test/fixtures/controller.polarArea/parse-object-data.json @@ -0,0 +1,27 @@ +{ + "config": { + "type": "polarArea", + "data": { + "datasets": [ + { + "data": [{"id": "Sales", "nested": {"value": 10}}, {"id": "Purchases", "nested": {"value": 20}}], + "backgroundColor": ["red", "blue"] + } + ] + }, + "options": { + "responsive": false, + "plugins": { + "legend": false + }, + "parsing": { + "key": "nested.value" + }, + "scales": { + "r": { + "display": false + } + } + } + } +} diff --git a/test/fixtures/controller.polarArea/parse-object-data.png b/test/fixtures/controller.polarArea/parse-object-data.png new file mode 100644 index 00000000000..dc248130959 Binary files /dev/null and b/test/fixtures/controller.polarArea/parse-object-data.png differ diff --git a/test/fixtures/controller.polarArea/pointLabels/centered-180.js b/test/fixtures/controller.polarArea/pointLabels/centered-180.js new file mode 100644 index 00000000000..48d75daa914 --- /dev/null +++ b/test/fixtures/controller.polarArea/pointLabels/centered-180.js @@ -0,0 +1,26 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + datasets: [{ + data: [1, 2, 3, 4], + backgroundColor: ['#f003', '#0f03', '#00f3', '#0003'] + }], + labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] + }, + options: { + scales: { + r: { + startAngle: 180, + pointLabels: { + display: true, + centerPointLabels: true + } + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/controller.polarArea/pointLabels/centered-180.png b/test/fixtures/controller.polarArea/pointLabels/centered-180.png new file mode 100644 index 00000000000..c58764812f3 Binary files /dev/null and b/test/fixtures/controller.polarArea/pointLabels/centered-180.png differ diff --git a/test/fixtures/controller.polarArea/pointLabels/centered-45.js b/test/fixtures/controller.polarArea/pointLabels/centered-45.js new file mode 100644 index 00000000000..9dd8c2b3e21 --- /dev/null +++ b/test/fixtures/controller.polarArea/pointLabels/centered-45.js @@ -0,0 +1,26 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + datasets: [{ + data: [1, 2, 3, 4], + backgroundColor: ['#f003', '#0f03', '#00f3', '#0003'] + }], + labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] + }, + options: { + scales: { + r: { + startAngle: 45, + pointLabels: { + display: true, + centerPointLabels: true + } + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/controller.polarArea/pointLabels/centered-45.png b/test/fixtures/controller.polarArea/pointLabels/centered-45.png new file mode 100644 index 00000000000..85330cc8edf Binary files /dev/null and b/test/fixtures/controller.polarArea/pointLabels/centered-45.png differ diff --git a/test/fixtures/controller.polarArea/pointLabels/centered.js b/test/fixtures/controller.polarArea/pointLabels/centered.js new file mode 100644 index 00000000000..3d96615c263 --- /dev/null +++ b/test/fixtures/controller.polarArea/pointLabels/centered.js @@ -0,0 +1,25 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + datasets: [{ + data: [1, 2, 3, 4], + backgroundColor: ['#f003', '#0f03', '#00f3', '#0003'] + }], + labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] + }, + options: { + scales: { + r: { + pointLabels: { + display: true, + centerPointLabels: true + } + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/controller.polarArea/pointLabels/centered.png b/test/fixtures/controller.polarArea/pointLabels/centered.png new file mode 100644 index 00000000000..3d20b326277 Binary files /dev/null and b/test/fixtures/controller.polarArea/pointLabels/centered.png differ diff --git a/test/fixtures/controller.polarArea/pointLabels/default-180.js b/test/fixtures/controller.polarArea/pointLabels/default-180.js new file mode 100644 index 00000000000..0f60f1b4334 --- /dev/null +++ b/test/fixtures/controller.polarArea/pointLabels/default-180.js @@ -0,0 +1,25 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + datasets: [{ + data: [1, 2, 3, 4], + backgroundColor: ['#f003', '#0f03', '#00f3', '#0003'] + }], + labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] + }, + options: { + scales: { + r: { + startAngle: 180, + pointLabels: { + display: true, + } + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/controller.polarArea/pointLabels/default-180.png b/test/fixtures/controller.polarArea/pointLabels/default-180.png new file mode 100644 index 00000000000..7bd2af3d928 Binary files /dev/null and b/test/fixtures/controller.polarArea/pointLabels/default-180.png differ diff --git a/test/fixtures/controller.polarArea/pointLabels/default-45.js b/test/fixtures/controller.polarArea/pointLabels/default-45.js new file mode 100644 index 00000000000..774b0789861 --- /dev/null +++ b/test/fixtures/controller.polarArea/pointLabels/default-45.js @@ -0,0 +1,25 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + datasets: [{ + data: [1, 2, 3, 4], + backgroundColor: ['#f003', '#0f03', '#00f3', '#0003'] + }], + labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] + }, + options: { + scales: { + r: { + startAngle: 45, + pointLabels: { + display: true + } + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/controller.polarArea/pointLabels/default-45.png b/test/fixtures/controller.polarArea/pointLabels/default-45.png new file mode 100644 index 00000000000..afa967a8572 Binary files /dev/null and b/test/fixtures/controller.polarArea/pointLabels/default-45.png differ diff --git a/test/fixtures/controller.polarArea/pointLabels/default.js b/test/fixtures/controller.polarArea/pointLabels/default.js new file mode 100644 index 00000000000..025da82b525 --- /dev/null +++ b/test/fixtures/controller.polarArea/pointLabels/default.js @@ -0,0 +1,24 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + datasets: [{ + data: [1, 2, 3, 4], + backgroundColor: ['#f003', '#0f03', '#00f3', '#0003'] + }], + labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] + }, + options: { + scales: { + r: { + pointLabels: { + display: true, + } + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/controller.polarArea/pointLabels/default.png b/test/fixtures/controller.polarArea/pointLabels/default.png new file mode 100644 index 00000000000..185ec4f7c7b Binary files /dev/null and b/test/fixtures/controller.polarArea/pointLabels/default.png differ diff --git a/test/fixtures/controller.polarArea/pointLabels/displayAuto-180.js b/test/fixtures/controller.polarArea/pointLabels/displayAuto-180.js new file mode 100644 index 00000000000..91e47c69e00 --- /dev/null +++ b/test/fixtures/controller.polarArea/pointLabels/displayAuto-180.js @@ -0,0 +1,25 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + datasets: [{ + data: new Array(50).fill(5), + backgroundColor: ['#f003', '#0f03', '#00f3', '#0003'] + }], + labels: new Array(50).fill(0).map((el, i) => ['label ' + i, 'line 2']) + }, + options: { + scales: { + r: { + startAngle: 180, + pointLabels: { + display: 'auto', + } + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/controller.polarArea/pointLabels/displayAuto-180.png b/test/fixtures/controller.polarArea/pointLabels/displayAuto-180.png new file mode 100644 index 00000000000..c0bb6730ede Binary files /dev/null and b/test/fixtures/controller.polarArea/pointLabels/displayAuto-180.png differ diff --git a/test/fixtures/controller.polarArea/pointLabels/displayAuto.js b/test/fixtures/controller.polarArea/pointLabels/displayAuto.js new file mode 100644 index 00000000000..14b85eab055 --- /dev/null +++ b/test/fixtures/controller.polarArea/pointLabels/displayAuto.js @@ -0,0 +1,24 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + datasets: [{ + data: new Array(50).fill(5), + backgroundColor: ['#f003', '#0f03', '#00f3', '#0003'] + }], + labels: new Array(50).fill(0).map((el, i) => ['label ' + i, 'line 2']) + }, + options: { + scales: { + r: { + pointLabels: { + display: 'auto', + } + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/controller.polarArea/pointLabels/displayAuto.png b/test/fixtures/controller.polarArea/pointLabels/displayAuto.png new file mode 100644 index 00000000000..271fbd2ed52 Binary files /dev/null and b/test/fixtures/controller.polarArea/pointLabels/displayAuto.png differ diff --git a/test/fixtures/controller.polarArea/pointLabels/overlapping.js b/test/fixtures/controller.polarArea/pointLabels/overlapping.js new file mode 100644 index 00000000000..bd97ccd85ca --- /dev/null +++ b/test/fixtures/controller.polarArea/pointLabels/overlapping.js @@ -0,0 +1,24 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + datasets: [{ + data: new Array(50).fill(5), + backgroundColor: ['#f003', '#0f03', '#00f3', '#0003'] + }], + labels: new Array(50).fill(0).map((el, i) => ['label ' + i, 'line 2']) + }, + options: { + scales: { + r: { + pointLabels: { + display: true, + } + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/controller.polarArea/pointLabels/overlapping.png b/test/fixtures/controller.polarArea/pointLabels/overlapping.png new file mode 100644 index 00000000000..33dcebd0bec Binary files /dev/null and b/test/fixtures/controller.polarArea/pointLabels/overlapping.png differ diff --git a/test/fixtures/controller.polarArea/pointLabels/withTitle/bottom-centered-45.js b/test/fixtures/controller.polarArea/pointLabels/withTitle/bottom-centered-45.js new file mode 100644 index 00000000000..08a3cc08b67 --- /dev/null +++ b/test/fixtures/controller.polarArea/pointLabels/withTitle/bottom-centered-45.js @@ -0,0 +1,34 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + datasets: [{ + data: [1, 2, 3, 4], + backgroundColor: ['#f003', '#0f03', '#00f3', '#0003'] + }], + labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] + }, + options: { + plugins: { + title: { + display: true, + position: 'bottom', + text: 'Chart Title' + }, + legend: false + }, + scales: { + r: { + startAngle: 45, + pointLabels: { + display: true, + centerPointLabels: true + } + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/controller.polarArea/pointLabels/withTitle/bottom-centered-45.png b/test/fixtures/controller.polarArea/pointLabels/withTitle/bottom-centered-45.png new file mode 100644 index 00000000000..ceed4c3afdb Binary files /dev/null and b/test/fixtures/controller.polarArea/pointLabels/withTitle/bottom-centered-45.png differ diff --git a/test/fixtures/controller.polarArea/pointLabels/withTitle/bottom-centered.js b/test/fixtures/controller.polarArea/pointLabels/withTitle/bottom-centered.js new file mode 100644 index 00000000000..bef264198ba --- /dev/null +++ b/test/fixtures/controller.polarArea/pointLabels/withTitle/bottom-centered.js @@ -0,0 +1,33 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + datasets: [{ + data: [1, 2, 3, 4], + backgroundColor: ['#f003', '#0f03', '#00f3', '#0003'] + }], + labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] + }, + options: { + plugins: { + title: { + display: true, + position: 'bottom', + text: 'Chart Title' + }, + legend: false + }, + scales: { + r: { + pointLabels: { + display: true, + centerPointLabels: true + } + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/controller.polarArea/pointLabels/withTitle/bottom-centered.png b/test/fixtures/controller.polarArea/pointLabels/withTitle/bottom-centered.png new file mode 100644 index 00000000000..ea7538e6dcd Binary files /dev/null and b/test/fixtures/controller.polarArea/pointLabels/withTitle/bottom-centered.png differ diff --git a/test/fixtures/controller.polarArea/pointLabels/withTitle/left-centered-45.js b/test/fixtures/controller.polarArea/pointLabels/withTitle/left-centered-45.js new file mode 100644 index 00000000000..646bae6a815 --- /dev/null +++ b/test/fixtures/controller.polarArea/pointLabels/withTitle/left-centered-45.js @@ -0,0 +1,34 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + datasets: [{ + data: [1, 2, 3, 4], + backgroundColor: ['#f003', '#0f03', '#00f3', '#0003'] + }], + labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] + }, + options: { + plugins: { + title: { + display: true, + position: 'left', + text: 'Chart Title' + }, + legend: false + }, + scales: { + r: { + startAngle: 45, + pointLabels: { + display: true, + centerPointLabels: true + } + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/controller.polarArea/pointLabels/withTitle/left-centered-45.png b/test/fixtures/controller.polarArea/pointLabels/withTitle/left-centered-45.png new file mode 100644 index 00000000000..6caff5cf1f3 Binary files /dev/null and b/test/fixtures/controller.polarArea/pointLabels/withTitle/left-centered-45.png differ diff --git a/test/fixtures/controller.polarArea/pointLabels/withTitle/left-centered.js b/test/fixtures/controller.polarArea/pointLabels/withTitle/left-centered.js new file mode 100644 index 00000000000..7d1556c183d --- /dev/null +++ b/test/fixtures/controller.polarArea/pointLabels/withTitle/left-centered.js @@ -0,0 +1,33 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + datasets: [{ + data: [1, 2, 3, 4], + backgroundColor: ['#f003', '#0f03', '#00f3', '#0003'] + }], + labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] + }, + options: { + plugins: { + title: { + display: true, + position: 'left', + text: 'Chart Title' + }, + legend: false + }, + scales: { + r: { + pointLabels: { + display: true, + centerPointLabels: true + } + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/controller.polarArea/pointLabels/withTitle/left-centered.png b/test/fixtures/controller.polarArea/pointLabels/withTitle/left-centered.png new file mode 100644 index 00000000000..6e900f04043 Binary files /dev/null and b/test/fixtures/controller.polarArea/pointLabels/withTitle/left-centered.png differ diff --git a/test/fixtures/controller.polarArea/pointLabels/withTitle/right-centered-45.js b/test/fixtures/controller.polarArea/pointLabels/withTitle/right-centered-45.js new file mode 100644 index 00000000000..a48812cb37c --- /dev/null +++ b/test/fixtures/controller.polarArea/pointLabels/withTitle/right-centered-45.js @@ -0,0 +1,34 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + datasets: [{ + data: [1, 2, 3, 4], + backgroundColor: ['#f003', '#0f03', '#00f3', '#0003'] + }], + labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] + }, + options: { + plugins: { + title: { + display: true, + position: 'right', + text: 'Chart Title' + }, + legend: false + }, + scales: { + r: { + startAngle: 45, + pointLabels: { + display: true, + centerPointLabels: true + } + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/controller.polarArea/pointLabels/withTitle/right-centered-45.png b/test/fixtures/controller.polarArea/pointLabels/withTitle/right-centered-45.png new file mode 100644 index 00000000000..42e6bbfa1a1 Binary files /dev/null and b/test/fixtures/controller.polarArea/pointLabels/withTitle/right-centered-45.png differ diff --git a/test/fixtures/controller.polarArea/pointLabels/withTitle/right-centered.js b/test/fixtures/controller.polarArea/pointLabels/withTitle/right-centered.js new file mode 100644 index 00000000000..b4a8cefa343 --- /dev/null +++ b/test/fixtures/controller.polarArea/pointLabels/withTitle/right-centered.js @@ -0,0 +1,33 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + datasets: [{ + data: [1, 2, 3, 4], + backgroundColor: ['#f003', '#0f03', '#00f3', '#0003'] + }], + labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] + }, + options: { + plugins: { + title: { + display: true, + position: 'right', + text: 'Chart Title' + }, + legend: false + }, + scales: { + r: { + pointLabels: { + display: true, + centerPointLabels: true + } + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/controller.polarArea/pointLabels/withTitle/right-centered.png b/test/fixtures/controller.polarArea/pointLabels/withTitle/right-centered.png new file mode 100644 index 00000000000..eeeee14269d Binary files /dev/null and b/test/fixtures/controller.polarArea/pointLabels/withTitle/right-centered.png differ diff --git a/test/fixtures/controller.polarArea/pointLabels/withTitle/top-centered-45.js b/test/fixtures/controller.polarArea/pointLabels/withTitle/top-centered-45.js new file mode 100644 index 00000000000..825e8c3d3b5 --- /dev/null +++ b/test/fixtures/controller.polarArea/pointLabels/withTitle/top-centered-45.js @@ -0,0 +1,33 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + datasets: [{ + data: [1, 2, 3, 4], + backgroundColor: ['#f003', '#0f03', '#00f3', '#0003'] + }], + labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] + }, + options: { + plugins: { + title: { + display: true, + text: 'Chart Title' + }, + legend: false + }, + scales: { + r: { + startAngle: 45, + pointLabels: { + display: true, + centerPointLabels: true + } + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/controller.polarArea/pointLabels/withTitle/top-centered-45.png b/test/fixtures/controller.polarArea/pointLabels/withTitle/top-centered-45.png new file mode 100644 index 00000000000..b814c08a8ed Binary files /dev/null and b/test/fixtures/controller.polarArea/pointLabels/withTitle/top-centered-45.png differ diff --git a/test/fixtures/controller.polarArea/pointLabels/withTitle/top-centered.js b/test/fixtures/controller.polarArea/pointLabels/withTitle/top-centered.js new file mode 100644 index 00000000000..704dab46d66 --- /dev/null +++ b/test/fixtures/controller.polarArea/pointLabels/withTitle/top-centered.js @@ -0,0 +1,32 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + datasets: [{ + data: [1, 2, 3, 4], + backgroundColor: ['#f003', '#0f03', '#00f3', '#0003'] + }], + labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] + }, + options: { + plugins: { + title: { + display: true, + text: 'Chart Title' + }, + legend: false + }, + scales: { + r: { + pointLabels: { + display: true, + centerPointLabels: true + } + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/controller.polarArea/pointLabels/withTitle/top-centered.png b/test/fixtures/controller.polarArea/pointLabels/withTitle/top-centered.png new file mode 100644 index 00000000000..d3217792eb1 Binary files /dev/null and b/test/fixtures/controller.polarArea/pointLabels/withTitle/top-centered.png differ diff --git a/test/fixtures/controller.polarArea/pointLabels/withTitle/top-default-45.js b/test/fixtures/controller.polarArea/pointLabels/withTitle/top-default-45.js new file mode 100644 index 00000000000..32687918756 --- /dev/null +++ b/test/fixtures/controller.polarArea/pointLabels/withTitle/top-default-45.js @@ -0,0 +1,32 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + datasets: [{ + data: [1, 2, 3, 4], + backgroundColor: ['#f003', '#0f03', '#00f3', '#0003'] + }], + labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] + }, + options: { + plugins: { + title: { + display: true, + text: 'Chart Title' + }, + legend: false + }, + scales: { + r: { + startAngle: 45, + pointLabels: { + display: true + } + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/controller.polarArea/pointLabels/withTitle/top-default-45.png b/test/fixtures/controller.polarArea/pointLabels/withTitle/top-default-45.png new file mode 100644 index 00000000000..23b7c16d979 Binary files /dev/null and b/test/fixtures/controller.polarArea/pointLabels/withTitle/top-default-45.png differ diff --git a/test/fixtures/controller.polarArea/pointLabels/withTitle/top-default.js b/test/fixtures/controller.polarArea/pointLabels/withTitle/top-default.js new file mode 100644 index 00000000000..8849ed22cd8 --- /dev/null +++ b/test/fixtures/controller.polarArea/pointLabels/withTitle/top-default.js @@ -0,0 +1,31 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + datasets: [{ + data: [1, 2, 3, 4], + backgroundColor: ['#f003', '#0f03', '#00f3', '#0003'] + }], + labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] + }, + options: { + plugins: { + title: { + display: true, + text: 'Chart Title' + }, + legend: false + }, + scales: { + r: { + pointLabels: { + display: true + } + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/controller.polarArea/pointLabels/withTitle/top-default.png b/test/fixtures/controller.polarArea/pointLabels/withTitle/top-default.png new file mode 100644 index 00000000000..5b339997713 Binary files /dev/null and b/test/fixtures/controller.polarArea/pointLabels/withTitle/top-default.png differ diff --git a/test/fixtures/controller.polarArea/polar-area-animation-rotate.js b/test/fixtures/controller.polarArea/polar-area-animation-rotate.js new file mode 100644 index 00000000000..9f17d3fd734 --- /dev/null +++ b/test/fixtures/controller.polarArea/polar-area-animation-rotate.js @@ -0,0 +1,85 @@ +const canvas = document.createElement('canvas'); +canvas.width = 512; +canvas.height = 512; +const ctx = canvas.getContext('2d'); + +module.exports = { + config: { + type: 'polarArea', + data: { + labels: ['A', 'B', 'C', 'D', 'E'], + datasets: [{ + data: [1, 5, 10, 2, 4], + backgroundColor: [ + 'rgba(255, 99, 132, 0.8)', + 'rgba(54, 162, 235, 0.8)', + 'rgba(255, 206, 86, 0.8)', + 'rgba(75, 192, 192, 0.8)', + 'rgba(153, 102, 255, 0.8)' + ], + borderWidth: 4, + borderColor: [ + 'rgb(255, 99, 132)', + 'rgb(54, 162, 235)', + 'rgb(255, 206, 86)', + 'rgb(75, 192, 192)', + 'rgb(153, 102, 255)' + ] + }] + }, + options: { + animation: { + animateRotate: true, + animateScale: false, + duration: 8000, + easing: 'linear' + }, + responsive: false, + plugins: { + legend: false, + title: false, + tooltip: false, + filler: false + }, + scales: { + r: { + ticks: { + display: false, + } + } + } + }, + }, + options: { + canvas: { + height: 512, + width: 512 + }, + run: function(chart) { + const animator = Chart.animator; + const anims = animator._getAnims(chart); + // disable animator + const backup = animator._refresh; + animator._refresh = function() { }; + + return new Promise((resolve) => { + window.requestAnimationFrame(() => { + + const start = anims.items[0]._start; + for (let i = 0; i < 16; i++) { + animator._update(start + i * 500); + let x = i % 4 * 128; + let y = Math.floor(i / 4) * 128; + ctx.drawImage(chart.canvas, x, y, 128, 128); + } + + Chart.helpers.clearCanvas(chart.canvas); + chart.ctx.drawImage(canvas, 0, 0); + + animator._refresh = backup; + resolve(); + }); + }); + } + } +}; diff --git a/test/fixtures/controller.polarArea/polar-area-animation-rotate.png b/test/fixtures/controller.polarArea/polar-area-animation-rotate.png new file mode 100644 index 00000000000..3d5f3b0ad4b Binary files /dev/null and b/test/fixtures/controller.polarArea/polar-area-animation-rotate.png differ diff --git a/test/fixtures/controller.polarArea/polar-area-animation-scale.js b/test/fixtures/controller.polarArea/polar-area-animation-scale.js new file mode 100644 index 00000000000..e4a1fe717d7 --- /dev/null +++ b/test/fixtures/controller.polarArea/polar-area-animation-scale.js @@ -0,0 +1,84 @@ +const canvas = document.createElement('canvas'); +canvas.width = 512; +canvas.height = 512; +const ctx = canvas.getContext('2d'); + +module.exports = { + config: { + type: 'polarArea', + data: { + labels: ['A', 'B', 'C', 'D', 'E'], + datasets: [{ + data: [1, 5, 10, 2, 4], + backgroundColor: [ + 'rgba(255, 99, 132, 0.8)', + 'rgba(54, 162, 235, 0.8)', + 'rgba(255, 206, 86, 0.8)', + 'rgba(75, 192, 192, 0.8)', + 'rgba(153, 102, 255, 0.8)' + ], + borderWidth: 4, + borderColor: [ + 'rgb(255, 99, 132)', + 'rgb(54, 162, 235)', + 'rgb(255, 206, 86)', + 'rgb(75, 192, 192)', + 'rgb(153, 102, 255)' + ] + }] + }, + options: { + animation: { + animateRotate: false, + animateScale: true, + duration: 8000, + easing: 'linear' + }, + responsive: false, + plugins: { + legend: false, + title: false, + tooltip: false, + filler: false + }, + scales: { + r: { + ticks: { + display: false, + } + } + } + }, + }, + options: { + canvas: { + height: 512, + width: 512 + }, + run: function(chart) { + const animator = Chart.animator; + const anims = animator._getAnims(chart); + // disable animator + const backup = animator._refresh; + animator._refresh = function() { }; + + return new Promise((resolve) => { + window.requestAnimationFrame(() => { + + const start = anims.items[0]._start; + for (let i = 0; i < 16; i++) { + animator._update(start + i * 500); + let x = i % 4 * 128; + let y = Math.floor(i / 4) * 128; + ctx.drawImage(chart.canvas, x, y, 128, 128); + } + Chart.helpers.clearCanvas(chart.canvas); + chart.ctx.drawImage(canvas, 0, 0); + + animator._refresh = backup; + resolve(); + }); + }); + } + } +}; diff --git a/test/fixtures/controller.polarArea/polar-area-animation-scale.png b/test/fixtures/controller.polarArea/polar-area-animation-scale.png new file mode 100644 index 00000000000..308903cba72 Binary files /dev/null and b/test/fixtures/controller.polarArea/polar-area-animation-scale.png differ diff --git a/test/fixtures/controller.radar/backgroundColor/scriptable.js b/test/fixtures/controller.radar/backgroundColor/scriptable.js new file mode 100644 index 00000000000..8183eacb39f --- /dev/null +++ b/test/fixtures/controller.radar/backgroundColor/scriptable.js @@ -0,0 +1,59 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + backgroundColor: function(ctx) { + var index = ctx.index; + return index === 0 ? '#ff0000' + : index === 1 ? '#00ff00' + : '#ff00ff'; + } + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5] + } + ] + }, + options: { + elements: { + line: { + backgroundColor: function(ctx) { + var index = ctx.index; + return index === 0 ? '#ff0000' + : index === 1 ? '#00ff00' + : '#ff00ff'; + }, + fill: true, + }, + point: { + backgroundColor: '#0000ff', + radius: 10 + } + }, + scales: { + r: { + display: false, + min: -15, + }, + }, + plugins: { + legend: false, + title: false, + tooltip: false, + filler: true + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/backgroundColor/scriptable.png b/test/fixtures/controller.radar/backgroundColor/scriptable.png new file mode 100644 index 00000000000..90b4a4d27a1 Binary files /dev/null and b/test/fixtures/controller.radar/backgroundColor/scriptable.png differ diff --git a/test/fixtures/controller.radar/backgroundColor/value.js b/test/fixtures/controller.radar/backgroundColor/value.js new file mode 100644 index 00000000000..6922182c66d --- /dev/null +++ b/test/fixtures/controller.radar/backgroundColor/value.js @@ -0,0 +1,48 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + backgroundColor: '#ff0000' + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5] + } + ] + }, + options: { + elements: { + line: { + backgroundColor: '#00ff00', + fill: true, + }, + point: { + radius: 10 + } + }, + scales: { + r: { + display: false, + min: -15 + } + }, + plugins: { + legend: false, + title: false, + tooltip: false, + filler: true + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/backgroundColor/value.png b/test/fixtures/controller.radar/backgroundColor/value.png new file mode 100644 index 00000000000..de5cec78ddd Binary files /dev/null and b/test/fixtures/controller.radar/backgroundColor/value.png differ diff --git a/test/fixtures/controller.radar/borderCapStyle/scriptable.js b/test/fixtures/controller.radar/borderCapStyle/scriptable.js new file mode 100644 index 00000000000..420e994207d --- /dev/null +++ b/test/fixtures/controller.radar/borderCapStyle/scriptable.js @@ -0,0 +1,61 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2], + datasets: [ + { + // option in dataset + data: [null, 3, 3], + borderCapStyle: function(ctx) { + var index = (ctx.datasetIndex % 2); + return index === 0 ? 'round' + : index === 1 ? 'square' + : 'butt'; + } + }, + { + // option in element (fallback) + data: [null, 2, 2] + }, + { + // option in element (fallback) + data: [null, 1, 1] + } + ] + }, + options: { + elements: { + line: { + borderCapStyle: function(ctx) { + var index = (ctx.datasetIndex % 3); + return index === 0 ? 'round' + : index === 1 ? 'square' + : 'butt'; + }, + borderColor: '#ff0000', + borderWidth: 32, + fill: false + }, + point: { + radius: 10 + } + }, + layout: { + padding: 32 + }, + scales: { + r: { + display: false, + beginAtZero: true + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/borderCapStyle/scriptable.png b/test/fixtures/controller.radar/borderCapStyle/scriptable.png new file mode 100644 index 00000000000..0be3c6212a0 Binary files /dev/null and b/test/fixtures/controller.radar/borderCapStyle/scriptable.png differ diff --git a/test/fixtures/controller.radar/borderCapStyle/value.js b/test/fixtures/controller.radar/borderCapStyle/value.js new file mode 100644 index 00000000000..72fdcab1759 --- /dev/null +++ b/test/fixtures/controller.radar/borderCapStyle/value.js @@ -0,0 +1,52 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2], + datasets: [ + { + // option in dataset + data: [null, 3, 3], + borderCapStyle: 'round' + }, + { + // option in dataset + data: [null, 2, 2], + borderCapStyle: 'square' + }, + { + // option in element (fallback) + data: [null, 1, 1] + } + ] + }, + options: { + elements: { + line: { + borderCapStyle: 'butt', + borderColor: '#00ff00', + borderWidth: 32, + fill: false + }, + point: { + radius: 10 + } + }, + layout: { + padding: 32 + }, + scales: { + r: { + display: false, + beginAtZero: true + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/borderCapStyle/value.png b/test/fixtures/controller.radar/borderCapStyle/value.png new file mode 100644 index 00000000000..9e297d67b42 Binary files /dev/null and b/test/fixtures/controller.radar/borderCapStyle/value.png differ diff --git a/test/fixtures/controller.radar/borderColor/scriptable.js b/test/fixtures/controller.radar/borderColor/scriptable.js new file mode 100644 index 00000000000..14fa9fcb6b6 --- /dev/null +++ b/test/fixtures/controller.radar/borderColor/scriptable.js @@ -0,0 +1,55 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + borderColor: function(ctx) { + var index = ctx.index; + return index === 0 ? '#ff0000' + : index === 1 ? '#00ff00' + : '#0000ff'; + } + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + elements: { + line: { + borderColor: function(ctx) { + var index = ctx.index; + return index === 0 ? '#ff0000' + : index === 1 ? '#00ff00' + : '#0000ff'; + }, + borderWidth: 10, + fill: false + }, + point: { + borderColor: '#ff0000', + borderWidth: 10, + radius: 16 + } + }, + scales: { + r: { + display: false, + min: -15 + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/borderColor/scriptable.png b/test/fixtures/controller.radar/borderColor/scriptable.png new file mode 100644 index 00000000000..58023c1bd93 Binary files /dev/null and b/test/fixtures/controller.radar/borderColor/scriptable.png differ diff --git a/test/fixtures/controller.radar/borderColor/value.js b/test/fixtures/controller.radar/borderColor/value.js new file mode 100644 index 00000000000..5e130c29df6 --- /dev/null +++ b/test/fixtures/controller.radar/borderColor/value.js @@ -0,0 +1,43 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + borderColor: '#ff0000' + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5] + } + ] + }, + options: { + elements: { + line: { + borderColor: '#0000ff', + fill: false + }, + point: { + borderColor: '#0000ff', + radius: 10 + } + }, + scales: { + r: { + display: false, + min: -15 + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/borderColor/value.png b/test/fixtures/controller.radar/borderColor/value.png new file mode 100644 index 00000000000..9f421eec051 Binary files /dev/null and b/test/fixtures/controller.radar/borderColor/value.png differ diff --git a/test/fixtures/controller.radar/borderDash/scriptable.js b/test/fixtures/controller.radar/borderDash/scriptable.js new file mode 100644 index 00000000000..de31f9ddf9c --- /dev/null +++ b/test/fixtures/controller.radar/borderDash/scriptable.js @@ -0,0 +1,47 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + borderDash: function(ctx) { + return ctx.datasetIndex === 0 ? [5] : [10]; + } + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5] + } + ] + }, + options: { + elements: { + line: { + borderColor: '#00ff00', + borderDash: function(ctx) { + return ctx.datasetIndex === 0 ? [5] : [10]; + }, + fill: true, + }, + point: { + radius: 10 + } + }, + scales: { + r: { + display: false, + min: -15 + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/borderDash/scriptable.png b/test/fixtures/controller.radar/borderDash/scriptable.png new file mode 100644 index 00000000000..bd4855832dc Binary files /dev/null and b/test/fixtures/controller.radar/borderDash/scriptable.png differ diff --git a/test/fixtures/controller.radar/borderDash/value.js b/test/fixtures/controller.radar/borderDash/value.js new file mode 100644 index 00000000000..933b8b36a84 --- /dev/null +++ b/test/fixtures/controller.radar/borderDash/value.js @@ -0,0 +1,45 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + borderColor: '#ff0000', + borderDash: [5] + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5] + } + ] + }, + options: { + elements: { + line: { + borderColor: '#00ff00', + borderDash: [10], + fill: false + }, + point: { + radius: 10 + } + }, + scales: { + r: { + display: false, + min: -15 + } + } + + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/borderDash/value.png b/test/fixtures/controller.radar/borderDash/value.png new file mode 100644 index 00000000000..2eef0726c55 Binary files /dev/null and b/test/fixtures/controller.radar/borderDash/value.png differ diff --git a/test/fixtures/controller.radar/borderDashOffset/scriptable.js b/test/fixtures/controller.radar/borderDashOffset/scriptable.js new file mode 100644 index 00000000000..4de8f892523 --- /dev/null +++ b/test/fixtures/controller.radar/borderDashOffset/scriptable.js @@ -0,0 +1,54 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3], + datasets: [ + { + // option in dataset + data: [1, 1, 1, 1], + borderColor: '#ff0000', + borderDash: [20], + borderDashOffset: function(ctx) { + return ctx.datasetIndex === 0 ? 5.0 : 0.0; + } + }, + { + // option in element (fallback) + data: [0, 0, 0, 0] + } + ] + }, + options: { + elements: { + line: { + borderColor: '#00ff00', + borderDash: [20], + borderDashOffset: function(ctx) { + return ctx.datasetIndex === 0 ? 5.0 : 0.0; + }, + fill: false + }, + point: { + radius: 10 + } + }, + layout: { + padding: 32 + }, + scales: { + r: { + display: false, + min: -1 + } + } + + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/borderDashOffset/scriptable.png b/test/fixtures/controller.radar/borderDashOffset/scriptable.png new file mode 100644 index 00000000000..28557b2c0c8 Binary files /dev/null and b/test/fixtures/controller.radar/borderDashOffset/scriptable.png differ diff --git a/test/fixtures/controller.radar/borderDashOffset/value.js b/test/fixtures/controller.radar/borderDashOffset/value.js new file mode 100644 index 00000000000..d24cc4ff15d --- /dev/null +++ b/test/fixtures/controller.radar/borderDashOffset/value.js @@ -0,0 +1,50 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [1, 1, 1, 1, 1, 1], + borderColor: '#ff0000', + borderDash: [20], + borderDashOffset: 5.0 + }, + { + // option in element (fallback) + data: [0, 0, 0, 0, 0, 0] + } + ] + }, + options: { + elements: { + line: { + borderColor: '#00ff00', + borderDash: [20], + borderDashOffset: 0.0, // default + fill: false + }, + point: { + radius: 10 + } + }, + layout: { + padding: 32 + }, + scales: { + r: { + display: false, + min: -1 + } + } + + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/borderDashOffset/value.png b/test/fixtures/controller.radar/borderDashOffset/value.png new file mode 100644 index 00000000000..fb9eb723f8c Binary files /dev/null and b/test/fixtures/controller.radar/borderDashOffset/value.png differ diff --git a/test/fixtures/controller.radar/borderJoinStyle/scriptable.js b/test/fixtures/controller.radar/borderJoinStyle/scriptable.js new file mode 100644 index 00000000000..a1d73c48c2c --- /dev/null +++ b/test/fixtures/controller.radar/borderJoinStyle/scriptable.js @@ -0,0 +1,61 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3], + datasets: [ + { + // option in dataset + data: [3, 3, null, 3], + borderColor: '#ff0000', + borderJoinStyle: function(ctx) { + var index = ctx.datasetIndex % 3; + return index === 0 ? 'round' + : index === 1 ? 'miter' + : 'bevel'; + } + }, + { + // option in element (fallback) + data: [2, 2, null, 2], + borderColor: '#0000ff' + }, + { + // option in element (fallback) + data: [1, 1, null, 1] + } + ] + }, + options: { + elements: { + line: { + borderColor: '#00ff00', + borderJoinStyle: function(ctx) { + var index = (ctx.datasetIndex % 3); + return index === 0 ? 'round' + : index === 1 ? 'miter' + : 'bevel'; + }, + borderWidth: 25, + fill: false, + tension: 0 + } + }, + layout: { + padding: 32 + }, + scales: { + r: { + display: false, + beginAtZero: true + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/borderJoinStyle/scriptable.png b/test/fixtures/controller.radar/borderJoinStyle/scriptable.png new file mode 100644 index 00000000000..9b2248bb2fe Binary files /dev/null and b/test/fixtures/controller.radar/borderJoinStyle/scriptable.png differ diff --git a/test/fixtures/controller.radar/borderJoinStyle/value.js b/test/fixtures/controller.radar/borderJoinStyle/value.js new file mode 100644 index 00000000000..669fa561e10 --- /dev/null +++ b/test/fixtures/controller.radar/borderJoinStyle/value.js @@ -0,0 +1,52 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3], + datasets: [ + { + // option in dataset + data: [3, 3, null, 3], + borderColor: '#ff0000', + borderJoinStyle: 'round' + }, + { + // option in element (fallback) + data: [2, 2, null, 2], + borderColor: '#0000ff', + borderJoinStyle: 'bevel' + }, + { + // option in element (fallback) + data: [1, 1, null, 1] + } + ] + }, + options: { + elements: { + line: { + borderColor: '#00ff00', + borderJoinStyle: 'miter', + borderWidth: 25, + fill: false, + tension: 0 + } + }, + layout: { + padding: 32 + }, + scales: { + r: { + display: false, + beginAtZero: true + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/borderJoinStyle/value.png b/test/fixtures/controller.radar/borderJoinStyle/value.png new file mode 100644 index 00000000000..757d05190ed Binary files /dev/null and b/test/fixtures/controller.radar/borderJoinStyle/value.png differ diff --git a/test/fixtures/controller.radar/borderWidth/scriptable.js b/test/fixtures/controller.radar/borderWidth/scriptable.js new file mode 100644 index 00000000000..2426ea2f127 --- /dev/null +++ b/test/fixtures/controller.radar/borderWidth/scriptable.js @@ -0,0 +1,54 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + borderColor: '#0000ff', + borderWidth: function(ctx) { + var index = ctx.index; + return index % 2 ? 10 : 20; + }, + pointBorderColor: '#00ff00' + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5] + } + ] + }, + options: { + elements: { + line: { + borderColor: '#ff0000', + borderWidth: function(ctx) { + var index = ctx.index; + return index % 2 ? 10 : 20; + }, + fill: false + }, + point: { + borderColor: '#00ff00', + borderWidth: 5, + radius: 10 + } + }, + scales: { + r: { + display: false, + min: -15 + } + } + + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/borderWidth/scriptable.png b/test/fixtures/controller.radar/borderWidth/scriptable.png new file mode 100644 index 00000000000..de84a830b56 Binary files /dev/null and b/test/fixtures/controller.radar/borderWidth/scriptable.png differ diff --git a/test/fixtures/controller.radar/borderWidth/value.js b/test/fixtures/controller.radar/borderWidth/value.js new file mode 100644 index 00000000000..58ee10d23bf --- /dev/null +++ b/test/fixtures/controller.radar/borderWidth/value.js @@ -0,0 +1,45 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + borderColor: '#0000ff', + borderWidth: 6 + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5] + } + ] + }, + options: { + elements: { + line: { + borderColor: '#00ff00', + borderWidth: 3, + fill: false + }, + point: { + radius: 10 + } + }, + scales: { + r: { + display: false, + min: -15 + } + } + + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/borderWidth/value.png b/test/fixtures/controller.radar/borderWidth/value.png new file mode 100644 index 00000000000..3254d2f2334 Binary files /dev/null and b/test/fixtures/controller.radar/borderWidth/value.png differ diff --git a/test/fixtures/controller.radar/borderWidth/zero.js b/test/fixtures/controller.radar/borderWidth/zero.js new file mode 100644 index 00000000000..a1c8a8c618d --- /dev/null +++ b/test/fixtures/controller.radar/borderWidth/zero.js @@ -0,0 +1,47 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + backgroundColor: '#0000ff', + borderColor: '#0000ff', + borderWidth: 0, + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5] + } + ] + }, + options: { + elements: { + line: { + borderColor: '#00ff00', + borderWidth: 1, + fill: false + }, + point: { + backgroundColor: '#00ff00', + radius: 10 + } + }, + scales: { + r: { + display: false, + min: -15 + } + } + + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/borderWidth/zero.png b/test/fixtures/controller.radar/borderWidth/zero.png new file mode 100644 index 00000000000..130348dab18 Binary files /dev/null and b/test/fixtures/controller.radar/borderWidth/zero.png differ diff --git a/test/fixtures/controller.radar/fill/scriptable.js b/test/fixtures/controller.radar/fill/scriptable.js new file mode 100644 index 00000000000..7ef108dea86 --- /dev/null +++ b/test/fixtures/controller.radar/fill/scriptable.js @@ -0,0 +1,50 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + backgroundColor: '#ff0000', + fill: function(ctx) { + return ctx.datasetIndex === 0 ? true : false; + } + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5] + } + ] + }, + options: { + elements: { + line: { + backgroundColor: '#00ff00', + fill: function(ctx) { + return ctx.datasetIndex === 0 ? true : false; + } + } + }, + scales: { + r: { + display: false, + min: -15 + } + }, + plugins: { + legend: false, + title: false, + tooltip: false, + filler: true + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/fill/scriptable.png b/test/fixtures/controller.radar/fill/scriptable.png new file mode 100644 index 00000000000..c0a0d5ca33a Binary files /dev/null and b/test/fixtures/controller.radar/fill/scriptable.png differ diff --git a/test/fixtures/controller.radar/fill/value.js b/test/fixtures/controller.radar/fill/value.js new file mode 100644 index 00000000000..55fe8e197aa --- /dev/null +++ b/test/fixtures/controller.radar/fill/value.js @@ -0,0 +1,46 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + backgroundColor: '#ff0000', + fill: false + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5] + } + ] + }, + options: { + elements: { + line: { + backgroundColor: '#00ff00', + fill: true + } + }, + scales: { + r: { + display: false, + min: -15 + } + }, + plugins: { + legend: false, + title: false, + tooltip: false, + filler: true + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/fill/value.png b/test/fixtures/controller.radar/fill/value.png new file mode 100644 index 00000000000..daae5932aab Binary files /dev/null and b/test/fixtures/controller.radar/fill/value.png differ diff --git a/test/fixtures/controller.radar/point-style.json b/test/fixtures/controller.radar/point-style.json new file mode 100644 index 00000000000..6375eb67b30 --- /dev/null +++ b/test/fixtures/controller.radar/point-style.json @@ -0,0 +1,97 @@ +{ + "config": { + "type": "radar", + "data": { + "labels": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + "datasets": [ + { + "borderColor": "transparent", + "data": [3, 3, 3, 3, 3, 3, 3, 3, 3, 3], + "pointBackgroundColor": "#00ff00", + "pointBorderColor": "transparent", + "pointBorderWidth": 0, + "pointRadius": 16, + "pointStyle": [ + "circle", + "cross", + "crossRot", + "dash", + "line", + "rect", + "rectRounded", + "rectRot", + "star", + "triangle" + ] + }, + { + "borderColor": "transparent", + "data": [2, 2, 2, 2, 2, 2, 2, 2, 2, 2], + "pointBackgroundColor": "transparent", + "pointBorderColor": "#0000ff", + "pointBorderWidth": 1, + "pointRadius": 16, + "pointStyle": [ + "circle", + "cross", + "crossRot", + "dash", + "line", + "rect", + "rectRounded", + "rectRot", + "star", + "triangle" + ] + }, + { + "borderColor": "transparent", + "data": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + "pointBackgroundColor": "#00ff00", + "pointBorderColor": "#0000ff", + "pointBorderWidth": 1, + "pointRadius": 16, + "pointStyle": [ + "circle", + "cross", + "crossRot", + "dash", + "line", + "rect", + "rectRounded", + "rectRot", + "star", + "triangle" + ] + } + ] + }, + "options": { + "responsive": false, + "scales": { + "r": { + "display": false, + "min": 0, + "max": 3 + } + }, + "elements": { + "line": { + "fill": false + } + }, + "layout": { + "padding": { + "left": 24, + "right": 24 + } + } + } + }, + "options": { + "canvas": { + "height": 512, + "width": 512 + } + } +} diff --git a/test/fixtures/controller.radar/point-style.png b/test/fixtures/controller.radar/point-style.png new file mode 100644 index 00000000000..723fdb82647 Binary files /dev/null and b/test/fixtures/controller.radar/point-style.png differ diff --git a/test/fixtures/controller.radar/pointBackgroundColor/indexable.js b/test/fixtures/controller.radar/pointBackgroundColor/indexable.js new file mode 100644 index 00000000000..2e456f6a2ec --- /dev/null +++ b/test/fixtures/controller.radar/pointBackgroundColor/indexable.js @@ -0,0 +1,56 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBackgroundColor: [ + '#ff0000', + '#00ff00', + '#0000ff', + '#ffff00', + '#ff00ff', + '#000000' + ] + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5] + } + ] + }, + options: { + elements: { + line: { + fill: false + }, + point: { + backgroundColor: [ + '#ff88ff', + '#888888', + '#ff8800', + '#00ff88', + '#8800ff', + '#ffff88' + ], + radius: 10 + } + }, + scales: { + r: { + display: false, + min: -15 + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/pointBackgroundColor/indexable.png b/test/fixtures/controller.radar/pointBackgroundColor/indexable.png new file mode 100644 index 00000000000..4c7bc8f0bd5 Binary files /dev/null and b/test/fixtures/controller.radar/pointBackgroundColor/indexable.png differ diff --git a/test/fixtures/controller.radar/pointBackgroundColor/scriptable.js b/test/fixtures/controller.radar/pointBackgroundColor/scriptable.js new file mode 100644 index 00000000000..91b687cf6b2 --- /dev/null +++ b/test/fixtures/controller.radar/pointBackgroundColor/scriptable.js @@ -0,0 +1,54 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBackgroundColor: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 8 ? '#ff0000' + : value > 0 ? '#00ff00' + : value > -8 ? '#0000ff' + : '#ff00ff'; + } + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5] + } + ] + }, + options: { + elements: { + line: { + fill: false + }, + point: { + backgroundColor: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 8 ? '#ff00ff' + : value > 0 ? '#0000ff' + : value > -8 ? '#ff0000' + : '#00ff00'; + }, + radius: 10 + } + }, + scales: { + r: { + display: false, + min: -15 + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/pointBackgroundColor/scriptable.png b/test/fixtures/controller.radar/pointBackgroundColor/scriptable.png new file mode 100644 index 00000000000..56c78b6059f Binary files /dev/null and b/test/fixtures/controller.radar/pointBackgroundColor/scriptable.png differ diff --git a/test/fixtures/controller.radar/pointBackgroundColor/value.js b/test/fixtures/controller.radar/pointBackgroundColor/value.js new file mode 100644 index 00000000000..b10722e6013 --- /dev/null +++ b/test/fixtures/controller.radar/pointBackgroundColor/value.js @@ -0,0 +1,42 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBackgroundColor: '#ff0000' + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5] + } + ] + }, + options: { + elements: { + line: { + fill: false + }, + point: { + backgroundColor: '#00ff00', + radius: 10 + } + }, + scales: { + r: { + display: false, + min: -15 + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/pointBackgroundColor/value.png b/test/fixtures/controller.radar/pointBackgroundColor/value.png new file mode 100644 index 00000000000..0f681424a09 Binary files /dev/null and b/test/fixtures/controller.radar/pointBackgroundColor/value.png differ diff --git a/test/fixtures/controller.radar/pointBorderColor/indexable.js b/test/fixtures/controller.radar/pointBorderColor/indexable.js new file mode 100644 index 00000000000..0b16c30177a --- /dev/null +++ b/test/fixtures/controller.radar/pointBorderColor/indexable.js @@ -0,0 +1,57 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBorderColor: [ + '#ff0000', + '#00ff00', + '#0000ff', + '#ffff00', + '#ff00ff', + '#000000' + ] + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5] + } + ] + }, + options: { + elements: { + line: { + fill: false + }, + point: { + borderColor: [ + '#ff88ff', + '#888888', + '#ff8800', + '#00ff88', + '#8800ff', + '#ffff88' + ], + borderWidth: 5, + radius: 10 + } + }, + scales: { + r: { + display: false, + min: -15 + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/pointBorderColor/indexable.png b/test/fixtures/controller.radar/pointBorderColor/indexable.png new file mode 100644 index 00000000000..309cb1cdab2 Binary files /dev/null and b/test/fixtures/controller.radar/pointBorderColor/indexable.png differ diff --git a/test/fixtures/controller.radar/pointBorderColor/scriptable.js b/test/fixtures/controller.radar/pointBorderColor/scriptable.js new file mode 100644 index 00000000000..dcf5f75bf80 --- /dev/null +++ b/test/fixtures/controller.radar/pointBorderColor/scriptable.js @@ -0,0 +1,55 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBorderColor: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 8 ? '#ff0000' + : value > 0 ? '#00ff00' + : value > -8 ? '#0000ff' + : '#ff00ff'; + } + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5] + } + ] + }, + options: { + elements: { + line: { + fill: false + }, + point: { + borderColor: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 8 ? '#ff00ff' + : value > 0 ? '#0000ff' + : value > -8 ? '#ff0000' + : '#00ff00'; + }, + borderWidth: 5, + radius: 10 + } + }, + scales: { + r: { + display: false, + min: -15 + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/pointBorderColor/scriptable.png b/test/fixtures/controller.radar/pointBorderColor/scriptable.png new file mode 100644 index 00000000000..de47d1b48a7 Binary files /dev/null and b/test/fixtures/controller.radar/pointBorderColor/scriptable.png differ diff --git a/test/fixtures/controller.radar/pointBorderColor/value.js b/test/fixtures/controller.radar/pointBorderColor/value.js new file mode 100644 index 00000000000..a986abdf522 --- /dev/null +++ b/test/fixtures/controller.radar/pointBorderColor/value.js @@ -0,0 +1,43 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBorderColor: '#ff0000' + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5] + } + ] + }, + options: { + elements: { + line: { + fill: false + }, + point: { + borderColor: '#00ff00', + borderWidth: 5, + radius: 10 + } + }, + scales: { + r: { + display: false, + min: -15 + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/pointBorderColor/value.png b/test/fixtures/controller.radar/pointBorderColor/value.png new file mode 100644 index 00000000000..537e49fc9a1 Binary files /dev/null and b/test/fixtures/controller.radar/pointBorderColor/value.png differ diff --git a/test/fixtures/controller.radar/pointBorderWidth/indexable.js b/test/fixtures/controller.radar/pointBorderWidth/indexable.js new file mode 100644 index 00000000000..a8e8838e1bc --- /dev/null +++ b/test/fixtures/controller.radar/pointBorderWidth/indexable.js @@ -0,0 +1,48 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBorderColor: '#00ff00', + pointBorderWidth: [ + 1, 2, 3, 4, 5, 6 + ] + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5] + } + ] + }, + options: { + elements: { + line: { + fill: false + }, + point: { + borderColor: '#ff0000', + borderWidth: [ + 6, 5, 4, 3, 2, 1 + ], + radius: 10 + } + }, + scales: { + r: { + display: false, + min: -15 + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/pointBorderWidth/indexable.png b/test/fixtures/controller.radar/pointBorderWidth/indexable.png new file mode 100644 index 00000000000..fbadc1a7bc3 Binary files /dev/null and b/test/fixtures/controller.radar/pointBorderWidth/indexable.png differ diff --git a/test/fixtures/controller.radar/pointBorderWidth/scriptable.js b/test/fixtures/controller.radar/pointBorderWidth/scriptable.js new file mode 100644 index 00000000000..adb76a66518 --- /dev/null +++ b/test/fixtures/controller.radar/pointBorderWidth/scriptable.js @@ -0,0 +1,54 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBorderColor: '#0000ff', + pointBorderWidth: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 4 ? 10 + : value > -4 ? 5 + : 2; + } + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5] + } + ] + }, + options: { + elements: { + line: { + fill: false + }, + point: { + borderColor: '#ff0000', + borderWidth: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 4 ? 2 + : value > -4 ? 5 + : 10; + }, + radius: 10 + } + }, + scales: { + r: { + display: false, + min: -15 + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/pointBorderWidth/scriptable.png b/test/fixtures/controller.radar/pointBorderWidth/scriptable.png new file mode 100644 index 00000000000..90112da1530 Binary files /dev/null and b/test/fixtures/controller.radar/pointBorderWidth/scriptable.png differ diff --git a/test/fixtures/controller.radar/pointBorderWidth/value.js b/test/fixtures/controller.radar/pointBorderWidth/value.js new file mode 100644 index 00000000000..561ea121b43 --- /dev/null +++ b/test/fixtures/controller.radar/pointBorderWidth/value.js @@ -0,0 +1,44 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBorderColor: '#0000ff', + pointBorderWidth: 6 + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5] + } + ] + }, + options: { + elements: { + line: { + fill: false + }, + point: { + borderColor: '#00ff00', + borderWidth: 3, + radius: 10 + } + }, + scales: { + r: { + display: false, + min: -15 + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/pointBorderWidth/value.png b/test/fixtures/controller.radar/pointBorderWidth/value.png new file mode 100644 index 00000000000..ad9106df81a Binary files /dev/null and b/test/fixtures/controller.radar/pointBorderWidth/value.png differ diff --git a/test/fixtures/controller.radar/pointStyle/indexable.js b/test/fixtures/controller.radar/pointStyle/indexable.js new file mode 100644 index 00000000000..48101e997de --- /dev/null +++ b/test/fixtures/controller.radar/pointStyle/indexable.js @@ -0,0 +1,61 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5, 6], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5, 0], + pointBackgroundColor: '#ff0000', + pointBorderColor: '#ff0000', + pointStyle: [ + 'circle', + 'cross', + 'crossRot', + 'dash', + 'line', + 'rect', + false + ] + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5, -4], + } + ] + }, + options: { + elements: { + line: { + fill: false, + }, + point: { + backgroundColor: '#00ff00', + borderColor: '#00ff00', + pointStyle: [ + 'line', + 'rect', + 'rectRounded', + 'rectRot', + 'star', + 'triangle' + ], + radius: 10 + } + }, + scales: { + r: { + display: false, + min: -15 + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/pointStyle/indexable.png b/test/fixtures/controller.radar/pointStyle/indexable.png new file mode 100644 index 00000000000..7c3a7460f69 Binary files /dev/null and b/test/fixtures/controller.radar/pointStyle/indexable.png differ diff --git a/test/fixtures/controller.radar/pointStyle/scriptable.js b/test/fixtures/controller.radar/pointStyle/scriptable.js new file mode 100644 index 00000000000..5a793cb1e7c --- /dev/null +++ b/test/fixtures/controller.radar/pointStyle/scriptable.js @@ -0,0 +1,58 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBackgroundColor: '#ff0000', + pointBorderColor: '#ff0000', + pointStyle: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 8 ? 'rect' + : value > 0 ? 'star' + : value > -8 ? 'cross' + : 'triangle'; + } + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + elements: { + line: { + fill: false, + }, + point: { + backgroundColor: '#0000ff', + borderColor: '#0000ff', + pointStyle: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 8 ? 'triangle' + : value > 0 ? 'cross' + : value > -8 ? 'star' + : 'rect'; + }, + radius: 10, + } + }, + scales: { + r: { + display: false, + min: -15 + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/pointStyle/scriptable.png b/test/fixtures/controller.radar/pointStyle/scriptable.png new file mode 100644 index 00000000000..35179a3032d Binary files /dev/null and b/test/fixtures/controller.radar/pointStyle/scriptable.png differ diff --git a/test/fixtures/controller.radar/pointStyle/value.js b/test/fixtures/controller.radar/pointStyle/value.js new file mode 100644 index 00000000000..20b0bb48fd0 --- /dev/null +++ b/test/fixtures/controller.radar/pointStyle/value.js @@ -0,0 +1,44 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBorderColor: '#ff0000', + pointStyle: 'star', + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + elements: { + line: { + fill: false, + }, + point: { + backgroundColor: '#00ff00', + pointStyle: 'rect', + radius: 10, + } + }, + scales: { + r: { + display: false, + min: -15 + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/pointStyle/value.png b/test/fixtures/controller.radar/pointStyle/value.png new file mode 100644 index 00000000000..b30deb2d7cb Binary files /dev/null and b/test/fixtures/controller.radar/pointStyle/value.png differ diff --git a/test/fixtures/controller.radar/radius/indexable.js b/test/fixtures/controller.radar/radius/indexable.js new file mode 100644 index 00000000000..da639ea6624 --- /dev/null +++ b/test/fixtures/controller.radar/radius/indexable.js @@ -0,0 +1,47 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBackgroundColor: '#00ff00', + pointRadius: [ + 1, 2, 3, 4, 5, 6 + ] + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + elements: { + line: { + fill: false, + }, + point: { + backgroundColor: '#ff0000', + radius: [ + 6, 5, 4, 3, 2, 1 + ], + } + }, + scales: { + r: { + display: false, + min: -15 + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/radius/indexable.png b/test/fixtures/controller.radar/radius/indexable.png new file mode 100644 index 00000000000..7d735618a59 Binary files /dev/null and b/test/fixtures/controller.radar/radius/indexable.png differ diff --git a/test/fixtures/controller.radar/radius/scriptable.js b/test/fixtures/controller.radar/radius/scriptable.js new file mode 100644 index 00000000000..5be5197ea19 --- /dev/null +++ b/test/fixtures/controller.radar/radius/scriptable.js @@ -0,0 +1,53 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBackgroundColor: '#0000ff', + pointRadius: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 4 ? 10 + : value > -4 ? 5 + : 2; + } + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + elements: { + line: { + fill: false, + }, + point: { + backgroundColor: '#ff0000', + radius: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 4 ? 2 + : value > -4 ? 5 + : 10; + }, + } + }, + scales: { + r: { + display: false, + min: -15 + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/radius/scriptable.png b/test/fixtures/controller.radar/radius/scriptable.png new file mode 100644 index 00000000000..8b58d888163 Binary files /dev/null and b/test/fixtures/controller.radar/radius/scriptable.png differ diff --git a/test/fixtures/controller.radar/radius/value.js b/test/fixtures/controller.radar/radius/value.js new file mode 100644 index 00000000000..92bb6acd3f9 --- /dev/null +++ b/test/fixtures/controller.radar/radius/value.js @@ -0,0 +1,43 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBackgroundColor: '#0000ff', + pointRadius: 6 + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + elements: { + line: { + fill: false, + }, + point: { + backgroundColor: '#00ff00', + radius: 3, + } + }, + scales: { + r: { + display: false, + min: -15 + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/radius/value.png b/test/fixtures/controller.radar/radius/value.png new file mode 100644 index 00000000000..55abc14fe2a Binary files /dev/null and b/test/fixtures/controller.radar/radius/value.png differ diff --git a/test/fixtures/controller.radar/rotation/indexable.js b/test/fixtures/controller.radar/rotation/indexable.js new file mode 100644 index 00000000000..a678634e2e6 --- /dev/null +++ b/test/fixtures/controller.radar/rotation/indexable.js @@ -0,0 +1,49 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBorderColor: '#00ff00', + pointRotation: [ + 0, 30, 60, 90, 120, 150 + ] + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + elements: { + line: { + fill: false, + }, + point: { + borderColor: '#ff0000', + borderWidth: 10, + pointStyle: 'line', + rotation: [ + 150, 120, 90, 60, 30, 0 + ], + } + }, + scales: { + r: { + display: false, + min: -15 + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/rotation/indexable.png b/test/fixtures/controller.radar/rotation/indexable.png new file mode 100644 index 00000000000..403aa7f869f Binary files /dev/null and b/test/fixtures/controller.radar/rotation/indexable.png differ diff --git a/test/fixtures/controller.radar/rotation/scriptable.js b/test/fixtures/controller.radar/rotation/scriptable.js new file mode 100644 index 00000000000..1ccb85c56a4 --- /dev/null +++ b/test/fixtures/controller.radar/rotation/scriptable.js @@ -0,0 +1,55 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBorderColor: '#0000ff', + pointRotation: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 4 ? 120 + : value > -4 ? 60 + : 0; + } + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + elements: { + line: { + fill: false, + }, + point: { + borderColor: '#ff0000', + rotation: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value > 4 ? 0 + : value > -4 ? 60 + : 120; + }, + pointStyle: 'line', + radius: 10, + } + }, + scales: { + r: { + display: false, + min: -15 + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/rotation/scriptable.png b/test/fixtures/controller.radar/rotation/scriptable.png new file mode 100644 index 00000000000..69958a77f2d Binary files /dev/null and b/test/fixtures/controller.radar/rotation/scriptable.png differ diff --git a/test/fixtures/controller.radar/rotation/value.js b/test/fixtures/controller.radar/rotation/value.js new file mode 100644 index 00000000000..93306e88a1e --- /dev/null +++ b/test/fixtures/controller.radar/rotation/value.js @@ -0,0 +1,45 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + pointBorderColor: '#0000ff', + pointRotation: 90 + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5], + } + ] + }, + options: { + elements: { + line: { + fill: false, + }, + point: { + borderColor: '#00ff00', + pointStyle: 'line', + radius: 10, + rotation: 0, + } + }, + scales: { + r: { + display: false, + min: -15 + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/rotation/value.png b/test/fixtures/controller.radar/rotation/value.png new file mode 100644 index 00000000000..5a5cd597709 Binary files /dev/null and b/test/fixtures/controller.radar/rotation/value.png differ diff --git a/test/fixtures/controller.radar/showLine/value.js b/test/fixtures/controller.radar/showLine/value.js new file mode 100644 index 00000000000..9182916f116 --- /dev/null +++ b/test/fixtures/controller.radar/showLine/value.js @@ -0,0 +1,54 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + // option in dataset + data: [0, 5, 10, null, -10, -5], + backgroundColor: '#ff0000', + fill: false, + showLine: true + }, + { + // option in element (fallback) + data: [4, -5, -10, null, 10, 5] + }, + { + data: [1, 1, 1, 1, 1, 1], + showLine: true, + backgroundColor: 'rgba(0,0,255,0.5)' + } + ] + }, + options: { + showLine: false, + elements: { + line: { + borderColor: '#ff0000', + backgroundColor: 'rgba(0,255,0,0.5)', + fill: true + } + }, + scales: { + r: { + display: false, + min: -15 + } + }, + plugins: { + legend: false, + title: false, + tooltip: false, + filler: true + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.radar/showLine/value.png b/test/fixtures/controller.radar/showLine/value.png new file mode 100644 index 00000000000..fc6a9d94e7d Binary files /dev/null and b/test/fixtures/controller.radar/showLine/value.png differ diff --git a/test/fixtures/controller.radar/startAngle/135.js b/test/fixtures/controller.radar/startAngle/135.js new file mode 100644 index 00000000000..b0333801e6e --- /dev/null +++ b/test/fixtures/controller.radar/startAngle/135.js @@ -0,0 +1,30 @@ +module.exports = { + config: { + type: 'radar', + data: { + datasets: [{ + data: [6, 3, 2, 3], + borderWidth: 3, + borderColor: 'blue' + }], + labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] + }, + options: { + scales: { + r: { + min: 0, + startAngle: 135, + pointLabels: { + display: true + }, + grid: { + circular: true + } + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/controller.radar/startAngle/135.png b/test/fixtures/controller.radar/startAngle/135.png new file mode 100644 index 00000000000..78512f524fa Binary files /dev/null and b/test/fixtures/controller.radar/startAngle/135.png differ diff --git a/test/fixtures/controller.radar/startAngle/180.js b/test/fixtures/controller.radar/startAngle/180.js new file mode 100644 index 00000000000..62a7d4a5a63 --- /dev/null +++ b/test/fixtures/controller.radar/startAngle/180.js @@ -0,0 +1,30 @@ +module.exports = { + config: { + type: 'radar', + data: { + datasets: [{ + data: [6, 3, 2, 3], + borderWidth: 3, + borderColor: 'blue' + }], + labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] + }, + options: { + scales: { + r: { + min: 0, + startAngle: 180, + pointLabels: { + display: true + }, + grid: { + circular: true + } + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/controller.radar/startAngle/180.png b/test/fixtures/controller.radar/startAngle/180.png new file mode 100644 index 00000000000..57886e15da5 Binary files /dev/null and b/test/fixtures/controller.radar/startAngle/180.png differ diff --git a/test/fixtures/controller.radar/startAngle/225.js b/test/fixtures/controller.radar/startAngle/225.js new file mode 100644 index 00000000000..b83d99e57d6 --- /dev/null +++ b/test/fixtures/controller.radar/startAngle/225.js @@ -0,0 +1,30 @@ +module.exports = { + config: { + type: 'radar', + data: { + datasets: [{ + data: [6, 3, 2, 3], + borderWidth: 3, + borderColor: 'blue' + }], + labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] + }, + options: { + scales: { + r: { + min: 0, + startAngle: 225, + pointLabels: { + display: true + }, + grid: { + circular: true + } + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/controller.radar/startAngle/225.png b/test/fixtures/controller.radar/startAngle/225.png new file mode 100644 index 00000000000..554fcf13979 Binary files /dev/null and b/test/fixtures/controller.radar/startAngle/225.png differ diff --git a/test/fixtures/controller.radar/startAngle/270.js b/test/fixtures/controller.radar/startAngle/270.js new file mode 100644 index 00000000000..6875bc7c252 --- /dev/null +++ b/test/fixtures/controller.radar/startAngle/270.js @@ -0,0 +1,30 @@ +module.exports = { + config: { + type: 'radar', + data: { + datasets: [{ + data: [6, 3, 2, 3], + borderWidth: 3, + borderColor: 'blue' + }], + labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] + }, + options: { + scales: { + r: { + min: 0, + startAngle: 270, + pointLabels: { + display: true + }, + grid: { + circular: true + } + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/controller.radar/startAngle/270.png b/test/fixtures/controller.radar/startAngle/270.png new file mode 100644 index 00000000000..dd7c379fa0f Binary files /dev/null and b/test/fixtures/controller.radar/startAngle/270.png differ diff --git a/test/fixtures/controller.radar/startAngle/315.js b/test/fixtures/controller.radar/startAngle/315.js new file mode 100644 index 00000000000..65e14a9c5f3 --- /dev/null +++ b/test/fixtures/controller.radar/startAngle/315.js @@ -0,0 +1,30 @@ +module.exports = { + config: { + type: 'radar', + data: { + datasets: [{ + data: [6, 3, 2, 3], + borderWidth: 3, + borderColor: 'blue' + }], + labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] + }, + options: { + scales: { + r: { + min: 0, + startAngle: 315, + pointLabels: { + display: true + }, + grid: { + circular: true + } + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/controller.radar/startAngle/315.png b/test/fixtures/controller.radar/startAngle/315.png new file mode 100644 index 00000000000..c18c77dd2bb Binary files /dev/null and b/test/fixtures/controller.radar/startAngle/315.png differ diff --git a/test/fixtures/controller.radar/startAngle/45.js b/test/fixtures/controller.radar/startAngle/45.js new file mode 100644 index 00000000000..a89f5927138 --- /dev/null +++ b/test/fixtures/controller.radar/startAngle/45.js @@ -0,0 +1,30 @@ +module.exports = { + config: { + type: 'radar', + data: { + datasets: [{ + data: [6, 3, 2, 3], + borderWidth: 3, + borderColor: 'blue' + }], + labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] + }, + options: { + scales: { + r: { + min: 0, + startAngle: 45, + pointLabels: { + display: true + }, + grid: { + circular: true + } + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/controller.radar/startAngle/45.png b/test/fixtures/controller.radar/startAngle/45.png new file mode 100644 index 00000000000..fd65c2b27af Binary files /dev/null and b/test/fixtures/controller.radar/startAngle/45.png differ diff --git a/test/fixtures/controller.radar/startAngle/90.js b/test/fixtures/controller.radar/startAngle/90.js new file mode 100644 index 00000000000..091dad06b88 --- /dev/null +++ b/test/fixtures/controller.radar/startAngle/90.js @@ -0,0 +1,30 @@ +module.exports = { + config: { + type: 'radar', + data: { + datasets: [{ + data: [6, 3, 2, 3], + borderWidth: 3, + borderColor: 'blue' + }], + labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] + }, + options: { + scales: { + r: { + min: 0, + startAngle: 90, + pointLabels: { + display: true + }, + grid: { + circular: true + } + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/controller.radar/startAngle/90.png b/test/fixtures/controller.radar/startAngle/90.png new file mode 100644 index 00000000000..51f5d7f2984 Binary files /dev/null and b/test/fixtures/controller.radar/startAngle/90.png differ diff --git a/test/fixtures/controller.radar/startAngle/default.js b/test/fixtures/controller.radar/startAngle/default.js new file mode 100644 index 00000000000..43aac6890cd --- /dev/null +++ b/test/fixtures/controller.radar/startAngle/default.js @@ -0,0 +1,29 @@ +module.exports = { + config: { + type: 'radar', + data: { + datasets: [{ + data: [6, 3, 2, 3], + borderWidth: 3, + borderColor: 'blue' + }], + labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] + }, + options: { + scales: { + r: { + min: 0, + pointLabels: { + display: true + }, + grid: { + circular: true + } + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/controller.radar/startAngle/default.png b/test/fixtures/controller.radar/startAngle/default.png new file mode 100644 index 00000000000..e7cd2bedab0 Binary files /dev/null and b/test/fixtures/controller.radar/startAngle/default.png differ diff --git a/test/fixtures/controller.scatter/showLine/changed.js b/test/fixtures/controller.scatter/showLine/changed.js new file mode 100644 index 00000000000..ac85be49f92 --- /dev/null +++ b/test/fixtures/controller.scatter/showLine/changed.js @@ -0,0 +1,34 @@ +module.exports = { + description: 'showLine option should draw a line if true', + config: { + type: 'scatter', + data: { + datasets: [{ + data: [{x: 10, y: 15}, {x: 15, y: 10}], + pointRadius: 10, + backgroundColor: 'red', + label: 'dataset1' + }], + }, + options: { + scales: { + x: { + display: false + }, + y: { + display: false + } + } + } + }, + options: { + canvas: { + width: 256, + height: 256 + }, + run(chart) { + chart.options.showLine = true; + chart.update(); + } + } +}; diff --git a/test/fixtures/controller.scatter/showLine/changed.png b/test/fixtures/controller.scatter/showLine/changed.png new file mode 100644 index 00000000000..9e5eae7c4b9 Binary files /dev/null and b/test/fixtures/controller.scatter/showLine/changed.png differ diff --git a/test/fixtures/controller.scatter/showLine/true.js b/test/fixtures/controller.scatter/showLine/true.js new file mode 100644 index 00000000000..34acd44c549 --- /dev/null +++ b/test/fixtures/controller.scatter/showLine/true.js @@ -0,0 +1,31 @@ +module.exports = { + description: 'showLine option should draw a line if true', + config: { + type: 'scatter', + data: { + datasets: [{ + data: [{x: 10, y: 15}, {x: 15, y: 10}], + pointRadius: 10, + backgroundColor: 'red', + showLine: true, + label: 'dataset1' + }], + }, + options: { + scales: { + x: { + display: false + }, + y: { + display: false + } + } + } + }, + options: { + canvas: { + width: 256, + height: 256 + } + } +}; diff --git a/test/fixtures/controller.scatter/showLine/true.png b/test/fixtures/controller.scatter/showLine/true.png new file mode 100644 index 00000000000..21ff96d8c07 Binary files /dev/null and b/test/fixtures/controller.scatter/showLine/true.png differ diff --git a/test/fixtures/controller.scatter/showLine/undefined.js b/test/fixtures/controller.scatter/showLine/undefined.js new file mode 100644 index 00000000000..4a969ed6f77 --- /dev/null +++ b/test/fixtures/controller.scatter/showLine/undefined.js @@ -0,0 +1,30 @@ +module.exports = { + description: 'showLine option should not draw a line if undefined', + config: { + type: 'scatter', + data: { + datasets: [{ + data: [{x: 10, y: 15}, {x: 15, y: 10}], + pointRadius: 10, + backgroundColor: 'red', + label: 'dataset1' + }], + }, + options: { + scales: { + x: { + display: false + }, + y: { + display: false + } + } + } + }, + options: { + canvas: { + width: 256, + height: 256 + } + } +}; diff --git a/test/fixtures/controller.scatter/showLine/undefined.png b/test/fixtures/controller.scatter/showLine/undefined.png new file mode 100644 index 00000000000..4297f50e394 Binary files /dev/null and b/test/fixtures/controller.scatter/showLine/undefined.png differ diff --git a/test/fixtures/core.datasetController/stacked-initial-render.js b/test/fixtures/core.datasetController/stacked-initial-render.js new file mode 100644 index 00000000000..96c7fb977be --- /dev/null +++ b/test/fixtures/core.datasetController/stacked-initial-render.js @@ -0,0 +1,49 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5, 6], + datasets: [ + { + // option in dataset + data: [9, 13, 15, 25, 22, 15, 21], + stack: 'construction_stack', + borderWidth: 10, + borderColor: 'rgb(54, 162, 235)' + }, + { + data: [9, 13, 15, 25, 22, 15, 21], + stack: 'construction_stack', + borderWidth: 10, + borderColor: 'rgb(255, 99, 132)' + } + ] + }, + options: { + scales: { + x: { + ticks: { + display: false + } + }, + y: { + ticks: { + display: false + } + } + }, + plugins: { + legend: false, + title: false, + tooltip: false, + filler: false + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/core.datasetController/stacked-initial-render.png b/test/fixtures/core.datasetController/stacked-initial-render.png new file mode 100644 index 00000000000..8f55664df1e Binary files /dev/null and b/test/fixtures/core.datasetController/stacked-initial-render.png differ diff --git a/test/fixtures/core.interaction/drawActiveElementsOnTop-false.js b/test/fixtures/core.interaction/drawActiveElementsOnTop-false.js new file mode 100644 index 00000000000..71855531531 --- /dev/null +++ b/test/fixtures/core.interaction/drawActiveElementsOnTop-false.js @@ -0,0 +1,41 @@ +module.exports = { + config: { + type: 'bubble', + data: { + datasets: [{ + data: [ + {x: 1, y: 1, r: 80}, + {x: 1, y: 1, r: 20} + ], + drawActiveElementsOnTop: false, + backgroundColor: (ctx) => (ctx.dataIndex === 1 ? 'red' : 'blue'), + hoverBackgroundColor: 'yellow', + hoverRadius: 0, + }] + }, + options: { + scales: { + x: { + display: false + }, + y: { + display: false + }, + }, + plugins: { + tooltip: false, + legend: false + }, + } + }, + options: { + canvas: { + width: 256, + height: 256 + }, + async run(chart) { + const point = chart.getDatasetMeta(0).data[0]; + await jasmine.triggerMouseEvent(chart, 'click', {y: point.y, x: point.x + 25}); + } + } +}; diff --git a/test/fixtures/core.interaction/drawActiveElementsOnTop-false.png b/test/fixtures/core.interaction/drawActiveElementsOnTop-false.png new file mode 100644 index 00000000000..ecfe37700db Binary files /dev/null and b/test/fixtures/core.interaction/drawActiveElementsOnTop-false.png differ diff --git a/test/fixtures/core.interaction/nearest-partial-bar.js b/test/fixtures/core.interaction/nearest-partial-bar.js new file mode 100644 index 00000000000..420f849c95f --- /dev/null +++ b/test/fixtures/core.interaction/nearest-partial-bar.js @@ -0,0 +1,42 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: ['a', 'b', 'c'], + datasets: [ + { + data: [220, 250, 225], + }, + ], + }, + options: { + events: ['click'], + interaction: { + mode: 'nearest' + }, + plugins: { + tooltip: true, + legend: false + }, + scales: { + y: { + beginAtZero: false + } + } + } + }, + options: { + spriteText: true, + canvas: { + width: 256, + height: 256 + }, + async run(chart) { + const point = { + x: chart.chartArea.left + chart.chartArea.width / 2, + y: chart.chartArea.top + chart.chartArea.height / 2, + }; + await jasmine.triggerMouseEvent(chart, 'click', point); + } + } +}; diff --git a/test/fixtures/core.interaction/nearest-partial-bar.png b/test/fixtures/core.interaction/nearest-partial-bar.png new file mode 100644 index 00000000000..907795294c4 Binary files /dev/null and b/test/fixtures/core.interaction/nearest-partial-bar.png differ diff --git a/test/fixtures/core.interaction/nearest-point-behind-scale.js b/test/fixtures/core.interaction/nearest-point-behind-scale.js new file mode 100644 index 00000000000..6dfa25c1a2d --- /dev/null +++ b/test/fixtures/core.interaction/nearest-point-behind-scale.js @@ -0,0 +1,45 @@ +module.exports = { + config: { + type: 'scatter', + data: { + datasets: [{ + data: [{x: 1, y: 1}, {x: 48, y: 1}] + }] + }, + options: { + events: ['click'], + interaction: { + mode: 'nearest', + intersect: false + }, + plugins: { + tooltip: true, + legend: false + }, + scales: { + x: { + min: 5, + max: 50 + }, + y: { + min: 0, + max: 2 + } + }, + layout: { + padding: 50 + } + } + }, + options: { + spriteText: true, + canvas: { + width: 256, + height: 256 + }, + async run(chart) { + const point = chart.getDatasetMeta(0).data[0]; + await jasmine.triggerMouseEvent(chart, 'click', {y: point.y, x: chart.chartArea.left}); + } + } +}; diff --git a/test/fixtures/core.interaction/nearest-point-behind-scale.png b/test/fixtures/core.interaction/nearest-point-behind-scale.png new file mode 100644 index 00000000000..42d92874d51 Binary files /dev/null and b/test/fixtures/core.interaction/nearest-point-behind-scale.png differ diff --git a/test/fixtures/core.layouts/hidden-vertical-boxes.js b/test/fixtures/core.layouts/hidden-vertical-boxes.js new file mode 100644 index 00000000000..5b55600bbeb --- /dev/null +++ b/test/fixtures/core.layouts/hidden-vertical-boxes.js @@ -0,0 +1,62 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [ + {data: [10, 5, 0, 25, 78, -10]} + ], + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', ''] + }, + options: { + plugins: { + legend: false + }, + scales: { + x: { + display: false + }, + y: { + type: 'linear', + position: 'left', + ticks: { + callback: function(value) { + return value + ' very long unit!'; + }, + } + }, + y1: { + type: 'linear', + position: 'left', + display: false + }, + y2: { + type: 'linear', + position: 'left', + display: false + }, + y3: { + type: 'linear', + position: 'left', + display: false + }, + y4: { + type: 'linear', + position: 'left', + display: false + }, + y5: { + type: 'linear', + position: 'left', + display: false + } + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 256 + } + } +}; diff --git a/test/fixtures/core.layouts/hidden-vertical-boxes.png b/test/fixtures/core.layouts/hidden-vertical-boxes.png new file mode 100644 index 00000000000..a236a4c48c3 Binary files /dev/null and b/test/fixtures/core.layouts/hidden-vertical-boxes.png differ diff --git a/test/fixtures/core.layouts/long-labels.js b/test/fixtures/core.layouts/long-labels.js new file mode 100644 index 00000000000..034207680c6 --- /dev/null +++ b/test/fixtures/core.layouts/long-labels.js @@ -0,0 +1,36 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [ + {data: [10, 5, 0, 25, 78, -10]} + ], + labels: ['tick1 is very long one', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6 is very long one'] + }, + options: { + plugins: { + legend: false + }, + scales: { + x: { + type: 'category', + ticks: { + maxRotation: 0, + autoSkip: false + } + }, + y: { + type: 'linear', + position: 'right' + } + } + } + }, + options: { + spriteText: true, + canvas: { + height: 150, + width: 512 + } + } +}; diff --git a/test/fixtures/core.layouts/long-labels.png b/test/fixtures/core.layouts/long-labels.png new file mode 100644 index 00000000000..5f20aa5aaf2 Binary files /dev/null and b/test/fixtures/core.layouts/long-labels.png differ diff --git a/test/fixtures/core.layouts/no-boxes-all-padding.js b/test/fixtures/core.layouts/no-boxes-all-padding.js new file mode 100644 index 00000000000..b50d67210d1 --- /dev/null +++ b/test/fixtures/core.layouts/no-boxes-all-padding.js @@ -0,0 +1,40 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0], + datasets: [{ + data: [0], + radius: 16, + borderWidth: 0, + backgroundColor: 'red' + }], + }, + options: { + plugins: { + legend: false, + tooltip: false, + title: false, + filler: false + }, + scales: { + x: { + display: false, + offset: true + }, + y: { + display: false + } + }, + layout: { + padding: 16 + } + } + }, + options: { + canvas: { + height: 32, + width: 32 + } + } +}; diff --git a/test/fixtures/core.layouts/no-boxes-all-padding.png b/test/fixtures/core.layouts/no-boxes-all-padding.png new file mode 100644 index 00000000000..0e0eefef157 Binary files /dev/null and b/test/fixtures/core.layouts/no-boxes-all-padding.png differ diff --git a/test/fixtures/core.layouts/refit-vertical-boxes.js b/test/fixtures/core.layouts/refit-vertical-boxes.js new file mode 100644 index 00000000000..6ab797c310b --- /dev/null +++ b/test/fixtures/core.layouts/refit-vertical-boxes.js @@ -0,0 +1,53 @@ +module.exports = { + tolerance: 0.002, + config: { + type: 'line', + data: { + labels: [ + 'Aaron', + 'Adam', + 'Albert', + 'Alex', + 'Allan', + 'Aman', + 'Anthony', + 'Autoenrolment', + 'Avril', + 'Bernard' + ], + datasets: [{ + backgroundColor: 'rgba(252,233,79,0.5)', + borderColor: 'rgba(252,233,79,1)', + borderWidth: 1, + data: [101, + 185, + 24, + 311, + 17, + 21, + 462, + 340, + 140, + 24 + ] + }] + }, + options: { + maintainAspectRatio: false, + plugins: { + legend: true, + title: { + display: true, + text: 'test' + } + } + } + }, + options: { + spriteText: true, + canvas: { + height: 185, + width: 185 + } + } +}; diff --git a/test/fixtures/core.layouts/refit-vertical-boxes.png b/test/fixtures/core.layouts/refit-vertical-boxes.png new file mode 100644 index 00000000000..a01b75831b8 Binary files /dev/null and b/test/fixtures/core.layouts/refit-vertical-boxes.png differ diff --git a/test/fixtures/core.layouts/scriptable.js b/test/fixtures/core.layouts/scriptable.js new file mode 100644 index 00000000000..a541f94df74 --- /dev/null +++ b/test/fixtures/core.layouts/scriptable.js @@ -0,0 +1,49 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [ + {data: [10, 5, 0, 25, 78, -10]} + ], + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] + }, + options: { + layout: { + padding: function(ctx) { + // 10% padding + const horizontalPadding = ctx.chart.width * 0.1; + const verticalPadding = ctx.chart.height * 0.1; + return { + top: verticalPadding, + right: horizontalPadding, + bottom: verticalPadding, + left: horizontalPadding + }; + } + }, + plugins: { + legend: false + }, + scales: { + x: { + type: 'category', + ticks: { + maxRotation: 0, + autoSkip: false + } + }, + y: { + type: 'linear', + position: 'right' + } + } + } + }, + options: { + spriteText: true, + canvas: { + height: 150, + width: 512 + } + } +}; diff --git a/test/fixtures/core.layouts/scriptable.png b/test/fixtures/core.layouts/scriptable.png new file mode 100644 index 00000000000..a96bfa9f2e2 Binary files /dev/null and b/test/fixtures/core.layouts/scriptable.png differ diff --git a/test/fixtures/core.layouts/stacked-boxes-max-index-without-clip.js b/test/fixtures/core.layouts/stacked-boxes-max-index-without-clip.js new file mode 100644 index 00000000000..8e2df9cd2a4 --- /dev/null +++ b/test/fixtures/core.layouts/stacked-boxes-max-index-without-clip.js @@ -0,0 +1,115 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [ + {data: [{x: 5, y: 1}, {x: 10, y: 2}, {x: 5, y: 3}], borderColor: 'red'}, + {data: [{x: 5, y: 1}, {x: 10, y: 2}, {x: 5, y: 3}], yAxisID: 'y1', xAxisID: 'x1', borderColor: 'green'}, + {data: [{x: 5, y: 1}, {x: 10, y: 2}, {x: 5, y: 3}], yAxisID: 'y2', xAxisID: 'x2', borderColor: 'blue'}, + ], + labels: ['tick1', 'tick2', 'tick3'] + }, + options: { + plugins: false, + scales: { + x: { + type: 'linear', + position: 'bottom', + stack: '1', + offset: true, + clip: false, + bounds: 'data', + border: { + color: 'red' + }, + ticks: { + autoSkip: false, + maxRotation: 0, + count: 3 + }, + max: 7 + }, + x1: { + type: 'linear', + position: 'bottom', + stack: '1', + offset: true, + clip: false, + bounds: 'data', + border: { + color: 'green' + }, + ticks: { + autoSkip: false, + maxRotation: 0, + count: 3 + }, + max: 7 + }, + x2: { + type: 'linear', + position: 'bottom', + stack: '1', + offset: true, + clip: false, + bounds: 'data', + border: { + color: 'blue' + }, + ticks: { + autoSkip: false, + maxRotation: 0, + count: 3 + }, + max: 7 + }, + y: { + type: 'linear', + position: 'left', + stack: '1', + offset: true, + clip: false, + border: { + color: 'red' + }, + ticks: { + precision: 0 + } + }, + y1: { + type: 'linear', + position: 'left', + stack: '1', + offset: true, + clip: false, + border: { + color: 'green' + }, + ticks: { + precision: 0 + } + }, + y2: { + type: 'linear', + position: 'left', + stack: '1', + offset: true, + clip: false, + border: { + color: 'blue', + }, + ticks: { + precision: 0 + } + } + } + } + }, + options: { + spriteText: true, + canvas: { + height: 384, + width: 384 + } + } +}; diff --git a/test/fixtures/core.layouts/stacked-boxes-max-index-without-clip.png b/test/fixtures/core.layouts/stacked-boxes-max-index-without-clip.png new file mode 100644 index 00000000000..0f7d3290f52 Binary files /dev/null and b/test/fixtures/core.layouts/stacked-boxes-max-index-without-clip.png differ diff --git a/test/fixtures/core.layouts/stacked-boxes-max-index.js b/test/fixtures/core.layouts/stacked-boxes-max-index.js new file mode 100644 index 00000000000..60fd5f45a37 --- /dev/null +++ b/test/fixtures/core.layouts/stacked-boxes-max-index.js @@ -0,0 +1,109 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [ + {data: [{x: 5, y: 1}, {x: 10, y: 2}, {x: 5, y: 3}], borderColor: 'red'}, + {data: [{x: 5, y: 1}, {x: 10, y: 2}, {x: 5, y: 3}], yAxisID: 'y1', xAxisID: 'x1', borderColor: 'green'}, + {data: [{x: 5, y: 1}, {x: 10, y: 2}, {x: 5, y: 3}], yAxisID: 'y2', xAxisID: 'x2', borderColor: 'blue'}, + ], + labels: ['tick1', 'tick2', 'tick3'] + }, + options: { + plugins: false, + scales: { + x: { + type: 'linear', + position: 'bottom', + stack: '1', + offset: true, + bounds: 'data', + border: { + color: 'red' + }, + ticks: { + autoSkip: false, + maxRotation: 0, + count: 3 + }, + max: 7 + }, + x1: { + type: 'linear', + position: 'bottom', + stack: '1', + offset: true, + bounds: 'data', + border: { + color: 'green' + }, + ticks: { + autoSkip: false, + maxRotation: 0, + count: 3 + }, + max: 7 + }, + x2: { + type: 'linear', + position: 'bottom', + stack: '1', + offset: true, + bounds: 'data', + border: { + color: 'blue' + }, + ticks: { + autoSkip: false, + maxRotation: 0, + count: 3 + }, + max: 7 + }, + y: { + type: 'linear', + position: 'left', + stack: '1', + offset: true, + border: { + color: 'red' + }, + ticks: { + precision: 0 + } + }, + y1: { + type: 'linear', + position: 'left', + stack: '1', + offset: true, + border: { + color: 'green' + }, + ticks: { + precision: 0 + } + }, + y2: { + type: 'linear', + position: 'left', + stack: '1', + offset: true, + border: { + color: 'blue', + }, + ticks: { + precision: 0 + } + } + } + } + }, + options: { + spriteText: true, + canvas: { + height: 384, + width: 384 + } + } +}; diff --git a/test/fixtures/core.layouts/stacked-boxes-max-index.png b/test/fixtures/core.layouts/stacked-boxes-max-index.png new file mode 100644 index 00000000000..60c829a478c Binary files /dev/null and b/test/fixtures/core.layouts/stacked-boxes-max-index.png differ diff --git a/test/fixtures/core.layouts/stacked-boxes-max-without-clip.js b/test/fixtures/core.layouts/stacked-boxes-max-without-clip.js new file mode 100644 index 00000000000..cdfc3850fd9 --- /dev/null +++ b/test/fixtures/core.layouts/stacked-boxes-max-without-clip.js @@ -0,0 +1,115 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [ + {data: [{x: 1, y: 5}, {x: 2, y: 10}, {x: 3, y: 5}], borderColor: 'red'}, + {data: [{x: 1, y: 5}, {x: 2, y: 10}, {x: 3, y: 5}], yAxisID: 'y1', xAxisID: 'x1', borderColor: 'green'}, + {data: [{x: 1, y: 5}, {x: 2, y: 10}, {x: 3, y: 5}], yAxisID: 'y2', xAxisID: 'x2', borderColor: 'blue'}, + ], + labels: ['tick1', 'tick2', 'tick3'] + }, + options: { + plugins: false, + scales: { + x: { + type: 'linear', + position: 'bottom', + stack: '1', + offset: true, + clip: false, + bounds: 'data', + border: { + color: 'red' + }, + ticks: { + autoSkip: false, + maxRotation: 0, + count: 3 + } + }, + x1: { + type: 'linear', + position: 'bottom', + stack: '1', + offset: true, + clip: false, + bounds: 'data', + border: { + color: 'green' + }, + ticks: { + autoSkip: false, + maxRotation: 0, + count: 3 + } + }, + x2: { + type: 'linear', + position: 'bottom', + stack: '1', + offset: true, + clip: false, + bounds: 'data', + border: { + color: 'blue' + }, + ticks: { + autoSkip: false, + maxRotation: 0, + count: 3 + } + }, + y: { + type: 'linear', + position: 'left', + stack: '1', + offset: true, + clip: false, + border: { + color: 'red' + }, + ticks: { + precision: 0 + }, + max: 7 + }, + y1: { + type: 'linear', + position: 'left', + stack: '1', + offset: true, + clip: false, + border: { + color: 'green' + }, + ticks: { + precision: 0 + }, + max: 7 + }, + y2: { + type: 'linear', + position: 'left', + stack: '1', + offset: true, + clip: false, + border: { + color: 'blue', + }, + ticks: { + precision: 0 + }, + max: 7 + } + } + } + }, + options: { + spriteText: true, + canvas: { + height: 384, + width: 384 + } + } +}; diff --git a/test/fixtures/core.layouts/stacked-boxes-max-without-clip.png b/test/fixtures/core.layouts/stacked-boxes-max-without-clip.png new file mode 100644 index 00000000000..dc2ab23c815 Binary files /dev/null and b/test/fixtures/core.layouts/stacked-boxes-max-without-clip.png differ diff --git a/test/fixtures/core.layouts/stacked-boxes-max.js b/test/fixtures/core.layouts/stacked-boxes-max.js new file mode 100644 index 00000000000..43fd9d3b375 --- /dev/null +++ b/test/fixtures/core.layouts/stacked-boxes-max.js @@ -0,0 +1,109 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [ + {data: [{x: 1, y: 5}, {x: 2, y: 10}, {x: 3, y: 5}], borderColor: 'red'}, + {data: [{x: 1, y: 5}, {x: 2, y: 10}, {x: 3, y: 5}], yAxisID: 'y1', xAxisID: 'x1', borderColor: 'green'}, + {data: [{x: 1, y: 5}, {x: 2, y: 10}, {x: 3, y: 5}], yAxisID: 'y2', xAxisID: 'x2', borderColor: 'blue'}, + ], + labels: ['tick1', 'tick2', 'tick3'] + }, + options: { + plugins: false, + scales: { + x: { + type: 'linear', + position: 'bottom', + stack: '1', + offset: true, + bounds: 'data', + border: { + color: 'red' + }, + ticks: { + autoSkip: false, + maxRotation: 0, + count: 3 + } + }, + x1: { + type: 'linear', + position: 'bottom', + stack: '1', + offset: true, + bounds: 'data', + border: { + color: 'green' + }, + ticks: { + autoSkip: false, + maxRotation: 0, + count: 3 + } + }, + x2: { + type: 'linear', + position: 'bottom', + stack: '1', + offset: true, + bounds: 'data', + border: { + color: 'blue' + }, + ticks: { + autoSkip: false, + maxRotation: 0, + count: 3 + } + }, + y: { + type: 'linear', + position: 'left', + stack: '1', + offset: true, + border: { + color: 'red' + }, + ticks: { + precision: 0 + }, + max: 7 + }, + y1: { + type: 'linear', + position: 'left', + stack: '1', + offset: true, + border: { + color: 'green' + }, + ticks: { + precision: 0 + }, + max: 7 + }, + y2: { + type: 'linear', + position: 'left', + stack: '1', + offset: true, + border: { + color: 'blue', + }, + ticks: { + precision: 0 + }, + max: 7 + } + } + } + }, + options: { + spriteText: true, + canvas: { + height: 384, + width: 384 + } + } +}; diff --git a/test/fixtures/core.layouts/stacked-boxes-max.png b/test/fixtures/core.layouts/stacked-boxes-max.png new file mode 100644 index 00000000000..b136ab01cda Binary files /dev/null and b/test/fixtures/core.layouts/stacked-boxes-max.png differ diff --git a/test/fixtures/core.layouts/stacked-boxes-with-weight.js b/test/fixtures/core.layouts/stacked-boxes-with-weight.js new file mode 100644 index 00000000000..0c5d993d67a --- /dev/null +++ b/test/fixtures/core.layouts/stacked-boxes-with-weight.js @@ -0,0 +1,112 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [ + {data: [{x: 1, y: 1}, {x: 2, y: 2}, {x: 3, y: 3}], borderColor: 'red'}, + {data: [{x: 1, y: 1}, {x: 2, y: 2}, {x: 3, y: 3}], yAxisID: 'y1', xAxisID: 'x1', borderColor: 'green'}, + {data: [{x: 1, y: 1}, {x: 2, y: 2}, {x: 3, y: 3}], yAxisID: 'y2', xAxisID: 'x2', borderColor: 'blue'}, + ], + labels: ['tick1', 'tick2', 'tick3'] + }, + options: { + plugins: false, + scales: { + x: { + type: 'linear', + position: 'bottom', + stack: '1', + stackWeight: 2, + offset: true, + bounds: 'data', + border: { + color: 'red' + }, + ticks: { + autoSkip: false, + maxRotation: 0, + count: 3 + } + }, + x1: { + type: 'linear', + position: 'bottom', + stack: '1', + stackWeight: 2, + offset: true, + bounds: 'data', + border: { + color: 'green' + }, + ticks: { + autoSkip: false, + maxRotation: 0, + count: 3 + } + }, + x2: { + type: 'linear', + position: 'bottom', + stack: '1', + stackWeight: 6, + offset: true, + bounds: 'data', + border: { + color: 'blue' + }, + ticks: { + autoSkip: false, + maxRotation: 0, + count: 3 + } + }, + y: { + type: 'linear', + position: 'left', + stack: '1', + stackWeight: 2, + offset: true, + border: { + color: 'red' + }, + ticks: { + precision: 0 + } + }, + y1: { + type: 'linear', + position: 'left', + stack: '1', + offset: true, + stackWeight: 2, + border: { + color: 'green' + }, + ticks: { + precision: 0 + } + }, + y2: { + type: 'linear', + position: 'left', + stack: '1', + stackWeight: 3, + offset: true, + border: { + color: 'blue' + }, + ticks: { + precision: 0 + } + } + } + } + }, + options: { + spriteText: true, + canvas: { + height: 384, + width: 384 + } + } +}; diff --git a/test/fixtures/core.layouts/stacked-boxes-with-weight.png b/test/fixtures/core.layouts/stacked-boxes-with-weight.png new file mode 100644 index 00000000000..320e50284d6 Binary files /dev/null and b/test/fixtures/core.layouts/stacked-boxes-with-weight.png differ diff --git a/test/fixtures/core.layouts/stacked-boxes.js b/test/fixtures/core.layouts/stacked-boxes.js new file mode 100644 index 00000000000..7655c90188b --- /dev/null +++ b/test/fixtures/core.layouts/stacked-boxes.js @@ -0,0 +1,106 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [ + {data: [{x: 1, y: 1}, {x: 2, y: 2}, {x: 3, y: 3}], borderColor: 'red'}, + {data: [{x: 1, y: 1}, {x: 2, y: 2}, {x: 3, y: 3}], yAxisID: 'y1', xAxisID: 'x1', borderColor: 'green'}, + {data: [{x: 1, y: 1}, {x: 2, y: 2}, {x: 3, y: 3}], yAxisID: 'y2', xAxisID: 'x2', borderColor: 'blue'}, + ], + labels: ['tick1', 'tick2', 'tick3'] + }, + options: { + plugins: false, + scales: { + x: { + type: 'linear', + position: 'bottom', + stack: '1', + offset: true, + bounds: 'data', + border: { + color: 'red' + }, + ticks: { + autoSkip: false, + maxRotation: 0, + count: 3 + } + }, + x1: { + type: 'linear', + position: 'bottom', + stack: '1', + offset: true, + bounds: 'data', + border: { + color: 'green' + }, + ticks: { + autoSkip: false, + maxRotation: 0, + count: 3 + } + }, + x2: { + type: 'linear', + position: 'bottom', + stack: '1', + offset: true, + bounds: 'data', + border: { + color: 'blue' + }, + ticks: { + autoSkip: false, + maxRotation: 0, + count: 3 + } + }, + y: { + type: 'linear', + position: 'left', + stack: '1', + offset: true, + border: { + color: 'red' + }, + ticks: { + precision: 0 + } + }, + y1: { + type: 'linear', + position: 'left', + stack: '1', + offset: true, + border: { + color: 'green' + }, + ticks: { + precision: 0 + } + }, + y2: { + type: 'linear', + position: 'left', + stack: '1', + offset: true, + border: { + color: 'blue', + }, + ticks: { + precision: 0 + } + } + } + } + }, + options: { + spriteText: true, + canvas: { + height: 384, + width: 384 + } + } +}; diff --git a/test/fixtures/core.layouts/stacked-boxes.png b/test/fixtures/core.layouts/stacked-boxes.png new file mode 100644 index 00000000000..e92cea31244 Binary files /dev/null and b/test/fixtures/core.layouts/stacked-boxes.png differ diff --git a/test/fixtures/core.scale/autoSkip/fit-after.js b/test/fixtures/core.scale/autoSkip/fit-after.js new file mode 100644 index 00000000000..a6781003e5f --- /dev/null +++ b/test/fixtures/core.scale/autoSkip/fit-after.js @@ -0,0 +1,51 @@ +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/3694', + tolerance: 0.002, + config: { + type: 'line', + data: { + labels: [ + 'Aaron', + 'Adam', + 'Albert', + 'Alex', + 'Allan', + 'Aman', + 'Anthony', + 'Autoenrolment', + 'Avril', + 'Bernard' + ], + datasets: [{ + backgroundColor: 'rgba(252,233,79,0.5)', + borderColor: 'rgba(252,233,79,1)', + borderWidth: 1, + data: [101, + 185, + 24, + 311, + 17, + 21, + 462, + 340, + 140, + 24 + ] + }], + }, + options: { + scales: { + x: { + backgroundColor: '#eee' + } + } + } + }, + options: { + spriteText: true, + canvas: { + width: 185, + height: 185 + } + } +}; diff --git a/test/fixtures/core.scale/autoSkip/fit-after.png b/test/fixtures/core.scale/autoSkip/fit-after.png new file mode 100644 index 00000000000..8ca5b8d487f Binary files /dev/null and b/test/fixtures/core.scale/autoSkip/fit-after.png differ diff --git a/test/fixtures/core.scale/autoSkip/no-offset.js b/test/fixtures/core.scale/autoSkip/no-offset.js new file mode 100644 index 00000000000..a029ee97599 --- /dev/null +++ b/test/fixtures/core.scale/autoSkip/no-offset.js @@ -0,0 +1,22 @@ +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/8611', + config: { + type: 'line', + data: { + labels: ['Red Red Red', 'Blue Blue Blue', 'Black Black Black', 'Pink Pink Pink'], + datasets: [ + { + label: '# of Votes', + data: [12, 19, 3, 5] + }, + ] + }, + }, + options: { + spriteText: true, + canvas: { + width: 470, + height: 128 + } + } +}; diff --git a/test/fixtures/core.scale/autoSkip/no-offset.png b/test/fixtures/core.scale/autoSkip/no-offset.png new file mode 100644 index 00000000000..fb50bfcdbbe Binary files /dev/null and b/test/fixtures/core.scale/autoSkip/no-offset.png differ diff --git a/test/fixtures/core.scale/autoSkip/offset.js b/test/fixtures/core.scale/autoSkip/offset.js new file mode 100644 index 00000000000..679a44efc03 --- /dev/null +++ b/test/fixtures/core.scale/autoSkip/offset.js @@ -0,0 +1,22 @@ +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/8611', + config: { + type: 'bar', + data: { + labels: ['Red Red Red', 'Blue Blue Blue', 'Black Black Black', 'Pink Pink Pink'], + datasets: [ + { + label: '# of Votes', + data: [12, 19, 3, 5] + }, + ] + }, + }, + options: { + spriteText: true, + canvas: { + width: 506, + height: 128 + } + } +}; diff --git a/test/fixtures/core.scale/autoSkip/offset.png b/test/fixtures/core.scale/autoSkip/offset.png new file mode 100644 index 00000000000..5c488b1718a Binary files /dev/null and b/test/fixtures/core.scale/autoSkip/offset.png differ diff --git a/test/fixtures/core.scale/backgroundColor.js b/test/fixtures/core.scale/backgroundColor.js new file mode 100644 index 00000000000..2fc36beee1c --- /dev/null +++ b/test/fixtures/core.scale/backgroundColor.js @@ -0,0 +1,58 @@ +const ticks = { + display: false +}; +const grid = { + display: false +}; +const title = { + display: true, + test: '' +}; +module.exports = { + config: { + type: 'line', + options: { + events: [], + scales: { + top: { + type: 'linear', + backgroundColor: 'red', + position: 'top', + ticks, + grid, + title + }, + left: { + type: 'linear', + backgroundColor: 'green', + position: 'left', + ticks, + grid, + title + }, + bottom: { + type: 'linear', + backgroundColor: 'blue', + position: 'bottom', + ticks, + grid, + title + }, + right: { + type: 'linear', + backgroundColor: 'gray', + position: 'right', + ticks, + grid, + title + }, + } + } + }, + options: { + canvas: { + height: 256, + width: 256 + }, + } +}; diff --git a/test/fixtures/core.scale/backgroundColor.png b/test/fixtures/core.scale/backgroundColor.png new file mode 100644 index 00000000000..19d6e583a56 Binary files /dev/null and b/test/fixtures/core.scale/backgroundColor.png differ diff --git a/test/fixtures/core.scale/border-behind-elements.js b/test/fixtures/core.scale/border-behind-elements.js new file mode 100644 index 00000000000..980e730a197 --- /dev/null +++ b/test/fixtures/core.scale/border-behind-elements.js @@ -0,0 +1,49 @@ +module.exports = { + config: { + type: 'bubble', + data: { + datasets: [ + { + label: '# of Votes', + data: [{x: 19, y: 3, r: 3}, {x: 2, y: 2, r: 60}], + radius: 100, + backgroundColor: 'pink' + } + ] + }, + options: { + plugins: { + legend: { + display: false + } + }, + scales: { + y: { + ticks: { + display: false + }, + border: { + color: 'red', + width: 5 + } + }, + x: { + ticks: { + display: false + }, + border: { + color: 'red', + width: 5 + } + } + } + } + }, + + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/core.scale/border-behind-elements.png b/test/fixtures/core.scale/border-behind-elements.png new file mode 100644 index 00000000000..f4a9e019b53 Binary files /dev/null and b/test/fixtures/core.scale/border-behind-elements.png differ diff --git a/test/fixtures/core.scale/cartesian-axis-border-settings.json b/test/fixtures/core.scale/cartesian-axis-border-settings.json new file mode 100644 index 00000000000..3543eaf7280 --- /dev/null +++ b/test/fixtures/core.scale/cartesian-axis-border-settings.json @@ -0,0 +1,61 @@ +{ + "config": { + "type": "scatter", + "data": { + "datasets": [{ + "data": [{ + "x": -20, + "y": -30 + }, { + "x": 0, + "y": 0 + }, { + "x": 20, + "y": 15 + }] + }] + }, + "options": { + "scales": { + "x": { + "axis": "x", + "min": -100, + "max": 100, + "grid": { + "color": "red", + "drawOnChartArea": false + }, + "border": { + "display": true, + "color": "blue", + "width": 5 + }, + "ticks": { + "display": false + } + }, + "y": { + "axis": "y", + "min": -100, + "max": 100, + "border": { + "color": "red" + }, + "grid": { + "color": "red", + "drawOnChartArea": false + }, + "ticks": { + "display": false + } + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/core.scale/cartesian-axis-border-settings.png b/test/fixtures/core.scale/cartesian-axis-border-settings.png new file mode 100644 index 00000000000..8a9759c55a6 Binary files /dev/null and b/test/fixtures/core.scale/cartesian-axis-border-settings.png differ diff --git a/test/fixtures/core.scale/crossAlignment/cross-align-bottom-center.js b/test/fixtures/core.scale/crossAlignment/cross-align-bottom-center.js new file mode 100644 index 00000000000..0aa42829a6a --- /dev/null +++ b/test/fixtures/core.scale/crossAlignment/cross-align-bottom-center.js @@ -0,0 +1,27 @@ +module.exports = { + config: { + type: 'bar', + data: { + datasets: [{ + data: [1, 2, 3], + }], + labels: [['Label1', 'line 2', 'line3'], 'Label2', 'Label3'] + }, + options: { + scales: { + x: { + ticks: { + crossAlign: 'center', + }, + }, + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/core.scale/crossAlignment/cross-align-bottom-center.png b/test/fixtures/core.scale/crossAlignment/cross-align-bottom-center.png new file mode 100644 index 00000000000..6f3aaed90fe Binary files /dev/null and b/test/fixtures/core.scale/crossAlignment/cross-align-bottom-center.png differ diff --git a/test/fixtures/core.scale/crossAlignment/cross-align-bottom-far.js b/test/fixtures/core.scale/crossAlignment/cross-align-bottom-far.js new file mode 100644 index 00000000000..b0e5dc14a51 --- /dev/null +++ b/test/fixtures/core.scale/crossAlignment/cross-align-bottom-far.js @@ -0,0 +1,27 @@ +module.exports = { + config: { + type: 'bar', + data: { + datasets: [{ + data: [1, 2, 3], + }], + labels: [['Label1', 'line 2', 'line3'], 'Label2', 'Label3'] + }, + options: { + scales: { + x: { + ticks: { + crossAlign: 'far', + }, + }, + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/core.scale/crossAlignment/cross-align-bottom-far.png b/test/fixtures/core.scale/crossAlignment/cross-align-bottom-far.png new file mode 100644 index 00000000000..abca217fa27 Binary files /dev/null and b/test/fixtures/core.scale/crossAlignment/cross-align-bottom-far.png differ diff --git a/test/fixtures/core.scale/crossAlignment/cross-align-bottom-near.js b/test/fixtures/core.scale/crossAlignment/cross-align-bottom-near.js new file mode 100644 index 00000000000..744a60a7a59 --- /dev/null +++ b/test/fixtures/core.scale/crossAlignment/cross-align-bottom-near.js @@ -0,0 +1,27 @@ +module.exports = { + config: { + type: 'bar', + data: { + datasets: [{ + data: [1, 2, 3], + }], + labels: [['Label1', 'line 2', 'line3'], 'Label2', 'Label3'] + }, + options: { + scales: { + x: { + ticks: { + crossAlign: 'near', + }, + }, + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/core.scale/crossAlignment/cross-align-bottom-near.png b/test/fixtures/core.scale/crossAlignment/cross-align-bottom-near.png new file mode 100644 index 00000000000..02791ad91bb Binary files /dev/null and b/test/fixtures/core.scale/crossAlignment/cross-align-bottom-near.png differ diff --git a/test/fixtures/core.scale/crossAlignment/cross-align-left-center.js b/test/fixtures/core.scale/crossAlignment/cross-align-left-center.js new file mode 100644 index 00000000000..dd62e7881da --- /dev/null +++ b/test/fixtures/core.scale/crossAlignment/cross-align-left-center.js @@ -0,0 +1,29 @@ +module.exports = { + config: { + type: 'bar', + data: { + datasets: [{ + data: [1, 2, 3], + }], + labels: ['Long long label 1', 'Label2', 'Label3'] + }, + options: { + indexAxis: 'y', + scales: { + y: { + position: 'left', + ticks: { + crossAlign: 'center', + }, + }, + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/core.scale/crossAlignment/cross-align-left-center.png b/test/fixtures/core.scale/crossAlignment/cross-align-left-center.png new file mode 100644 index 00000000000..004856cedea Binary files /dev/null and b/test/fixtures/core.scale/crossAlignment/cross-align-left-center.png differ diff --git a/test/fixtures/core.scale/crossAlignment/cross-align-left-far-clipped.js b/test/fixtures/core.scale/crossAlignment/cross-align-left-far-clipped.js new file mode 100644 index 00000000000..435cb64bcc0 --- /dev/null +++ b/test/fixtures/core.scale/crossAlignment/cross-align-left-far-clipped.js @@ -0,0 +1,32 @@ +module.exports = { + config: { + type: 'bar', + data: { + datasets: [{ + data: [1, 2, 3], + }], + labels: ['Long long long long label 1', 'Label 2', 'Less more longer label 3'] + }, + options: { + indexAxis: 'y', + scales: { + y: { + position: 'left', + ticks: { + crossAlign: 'far', + }, + afterFit: axis => { + axis.width = 64; + }, + }, + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/core.scale/crossAlignment/cross-align-left-far-clipped.png b/test/fixtures/core.scale/crossAlignment/cross-align-left-far-clipped.png new file mode 100644 index 00000000000..d0cb6e39778 Binary files /dev/null and b/test/fixtures/core.scale/crossAlignment/cross-align-left-far-clipped.png differ diff --git a/test/fixtures/core.scale/crossAlignment/cross-align-left-far.js b/test/fixtures/core.scale/crossAlignment/cross-align-left-far.js new file mode 100644 index 00000000000..82a1ab8f237 --- /dev/null +++ b/test/fixtures/core.scale/crossAlignment/cross-align-left-far.js @@ -0,0 +1,29 @@ +module.exports = { + config: { + type: 'bar', + data: { + datasets: [{ + data: [1, 2, 3], + }], + labels: ['Long long label 1', 'Label2', 'Label3'] + }, + options: { + indexAxis: 'y', + scales: { + y: { + position: 'left', + ticks: { + crossAlign: 'far', + }, + }, + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/core.scale/crossAlignment/cross-align-left-far.png b/test/fixtures/core.scale/crossAlignment/cross-align-left-far.png new file mode 100644 index 00000000000..79b92fde7e9 Binary files /dev/null and b/test/fixtures/core.scale/crossAlignment/cross-align-left-far.png differ diff --git a/test/fixtures/core.scale/crossAlignment/cross-align-left-near.js b/test/fixtures/core.scale/crossAlignment/cross-align-left-near.js new file mode 100644 index 00000000000..75ca877b255 --- /dev/null +++ b/test/fixtures/core.scale/crossAlignment/cross-align-left-near.js @@ -0,0 +1,29 @@ +module.exports = { + config: { + type: 'bar', + data: { + datasets: [{ + data: [1, 2, 3], + }], + labels: ['Long long label 1', 'Label2', 'Label3'] + }, + options: { + indexAxis: 'y', + scales: { + y: { + position: 'left', + ticks: { + crossAlign: 'near', + }, + }, + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/core.scale/crossAlignment/cross-align-left-near.png b/test/fixtures/core.scale/crossAlignment/cross-align-left-near.png new file mode 100644 index 00000000000..c9cae15fcc4 Binary files /dev/null and b/test/fixtures/core.scale/crossAlignment/cross-align-left-near.png differ diff --git a/test/fixtures/core.scale/crossAlignment/cross-align-right-center.js b/test/fixtures/core.scale/crossAlignment/cross-align-right-center.js new file mode 100644 index 00000000000..94230cbe670 --- /dev/null +++ b/test/fixtures/core.scale/crossAlignment/cross-align-right-center.js @@ -0,0 +1,29 @@ +module.exports = { + config: { + type: 'bar', + data: { + datasets: [{ + data: [1, 2, 3], + }], + labels: ['Long long label 1', 'Label2', 'Label3'] + }, + options: { + indexAxis: 'y', + scales: { + y: { + position: 'right', + ticks: { + crossAlign: 'center', + }, + }, + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/core.scale/crossAlignment/cross-align-right-center.png b/test/fixtures/core.scale/crossAlignment/cross-align-right-center.png new file mode 100644 index 00000000000..ca66bcd9b96 Binary files /dev/null and b/test/fixtures/core.scale/crossAlignment/cross-align-right-center.png differ diff --git a/test/fixtures/core.scale/crossAlignment/cross-align-right-far-clipped.js b/test/fixtures/core.scale/crossAlignment/cross-align-right-far-clipped.js new file mode 100644 index 00000000000..3bd14fff62a --- /dev/null +++ b/test/fixtures/core.scale/crossAlignment/cross-align-right-far-clipped.js @@ -0,0 +1,33 @@ +module.exports = { + config: { + type: 'bar', + data: { + datasets: [{ + data: [1, 2, 3], + }], + labels: ['Long long long long label 1', 'Label 2', 'Less more longer label 3'] + }, + options: { + indexAxis: 'y', + scales: { + y: { + position: 'right', + ticks: { + crossAlign: 'far', + }, + afterFit: axis => { + axis.width = 64; + }, + }, + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 512 + } + }, + tolerance: 0.1 +}; diff --git a/test/fixtures/core.scale/crossAlignment/cross-align-right-far-clipped.png b/test/fixtures/core.scale/crossAlignment/cross-align-right-far-clipped.png new file mode 100644 index 00000000000..53395107ed5 Binary files /dev/null and b/test/fixtures/core.scale/crossAlignment/cross-align-right-far-clipped.png differ diff --git a/test/fixtures/core.scale/crossAlignment/cross-align-right-far.js b/test/fixtures/core.scale/crossAlignment/cross-align-right-far.js new file mode 100644 index 00000000000..ccde8ff66b1 --- /dev/null +++ b/test/fixtures/core.scale/crossAlignment/cross-align-right-far.js @@ -0,0 +1,29 @@ +module.exports = { + config: { + type: 'bar', + data: { + datasets: [{ + data: [1, 2, 3], + }], + labels: ['Long long label 1', 'Label2', 'Label3'] + }, + options: { + indexAxis: 'y', + scales: { + y: { + position: 'right', + ticks: { + crossAlign: 'far', + }, + }, + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/core.scale/crossAlignment/cross-align-right-far.png b/test/fixtures/core.scale/crossAlignment/cross-align-right-far.png new file mode 100644 index 00000000000..10678359a81 Binary files /dev/null and b/test/fixtures/core.scale/crossAlignment/cross-align-right-far.png differ diff --git a/test/fixtures/core.scale/crossAlignment/cross-align-right-near.js b/test/fixtures/core.scale/crossAlignment/cross-align-right-near.js new file mode 100644 index 00000000000..0e94a47d13e --- /dev/null +++ b/test/fixtures/core.scale/crossAlignment/cross-align-right-near.js @@ -0,0 +1,29 @@ +module.exports = { + config: { + type: 'bar', + data: { + datasets: [{ + data: [1, 2, 3], + }], + labels: ['Long long label 1', 'Label2', 'Label3'] + }, + options: { + indexAxis: 'y', + scales: { + y: { + position: 'right', + ticks: { + crossAlign: 'near', + }, + }, + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/core.scale/crossAlignment/cross-align-right-near.png b/test/fixtures/core.scale/crossAlignment/cross-align-right-near.png new file mode 100644 index 00000000000..5352cbb72ef Binary files /dev/null and b/test/fixtures/core.scale/crossAlignment/cross-align-right-near.png differ diff --git a/test/fixtures/core.scale/crossAlignment/cross-align-top-center.js b/test/fixtures/core.scale/crossAlignment/cross-align-top-center.js new file mode 100644 index 00000000000..0f4ddc20ad8 --- /dev/null +++ b/test/fixtures/core.scale/crossAlignment/cross-align-top-center.js @@ -0,0 +1,28 @@ +module.exports = { + config: { + type: 'bar', + data: { + datasets: [{ + data: [1, 2, 3], + }], + labels: [['Label1', 'line 2', 'line3'], 'Label2', 'Label3'] + }, + options: { + scales: { + x: { + position: 'top', + ticks: { + crossAlign: 'center', + }, + }, + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/core.scale/crossAlignment/cross-align-top-center.png b/test/fixtures/core.scale/crossAlignment/cross-align-top-center.png new file mode 100644 index 00000000000..c9083a830d8 Binary files /dev/null and b/test/fixtures/core.scale/crossAlignment/cross-align-top-center.png differ diff --git a/test/fixtures/core.scale/crossAlignment/cross-align-top-far.js b/test/fixtures/core.scale/crossAlignment/cross-align-top-far.js new file mode 100644 index 00000000000..4232f0ff42a --- /dev/null +++ b/test/fixtures/core.scale/crossAlignment/cross-align-top-far.js @@ -0,0 +1,28 @@ +module.exports = { + config: { + type: 'bar', + data: { + datasets: [{ + data: [1, 2, 3], + }], + labels: [['Label1', 'line 2', 'line3'], 'Label2', 'Label3'] + }, + options: { + scales: { + x: { + position: 'top', + ticks: { + crossAlign: 'far', + }, + }, + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/core.scale/crossAlignment/cross-align-top-far.png b/test/fixtures/core.scale/crossAlignment/cross-align-top-far.png new file mode 100644 index 00000000000..b186168a8d9 Binary files /dev/null and b/test/fixtures/core.scale/crossAlignment/cross-align-top-far.png differ diff --git a/test/fixtures/core.scale/crossAlignment/cross-align-top-near.js b/test/fixtures/core.scale/crossAlignment/cross-align-top-near.js new file mode 100644 index 00000000000..a8fa9e268bc --- /dev/null +++ b/test/fixtures/core.scale/crossAlignment/cross-align-top-near.js @@ -0,0 +1,28 @@ +module.exports = { + config: { + type: 'bar', + data: { + datasets: [{ + data: [1, 2, 3], + }], + labels: [['Label1', 'line 2', 'line3'], 'Label2', 'Label3'] + }, + options: { + scales: { + x: { + position: 'top', + ticks: { + crossAlign: 'near', + }, + }, + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/core.scale/crossAlignment/cross-align-top-near.png b/test/fixtures/core.scale/crossAlignment/cross-align-top-near.png new file mode 100644 index 00000000000..ce9e5c67a8b Binary files /dev/null and b/test/fixtures/core.scale/crossAlignment/cross-align-top-near.png differ diff --git a/test/fixtures/core.scale/crossAlignment/mirror-cross-align-left-center.js b/test/fixtures/core.scale/crossAlignment/mirror-cross-align-left-center.js new file mode 100644 index 00000000000..8c0af63c383 --- /dev/null +++ b/test/fixtures/core.scale/crossAlignment/mirror-cross-align-left-center.js @@ -0,0 +1,30 @@ +module.exports = { + config: { + type: 'bar', + data: { + datasets: [{ + data: [1, 2, 3], + }], + labels: ['Long long label 1', 'Label2', 'Label3'] + }, + options: { + indexAxis: 'y', + scales: { + y: { + position: 'left', + ticks: { + mirror: true, + crossAlign: 'center', + }, + }, + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/core.scale/crossAlignment/mirror-cross-align-left-center.png b/test/fixtures/core.scale/crossAlignment/mirror-cross-align-left-center.png new file mode 100644 index 00000000000..f64fdbebf1e Binary files /dev/null and b/test/fixtures/core.scale/crossAlignment/mirror-cross-align-left-center.png differ diff --git a/test/fixtures/core.scale/crossAlignment/mirror-cross-align-left-far.js b/test/fixtures/core.scale/crossAlignment/mirror-cross-align-left-far.js new file mode 100644 index 00000000000..c5f2e23f53b --- /dev/null +++ b/test/fixtures/core.scale/crossAlignment/mirror-cross-align-left-far.js @@ -0,0 +1,30 @@ +module.exports = { + config: { + type: 'bar', + data: { + datasets: [{ + data: [1, 2, 3], + }], + labels: ['Long long label 1', 'Label2', 'Label3'] + }, + options: { + indexAxis: 'y', + scales: { + y: { + position: 'left', + ticks: { + mirror: true, + crossAlign: 'far', + }, + }, + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/core.scale/crossAlignment/mirror-cross-align-left-far.png b/test/fixtures/core.scale/crossAlignment/mirror-cross-align-left-far.png new file mode 100644 index 00000000000..daf5104542c Binary files /dev/null and b/test/fixtures/core.scale/crossAlignment/mirror-cross-align-left-far.png differ diff --git a/test/fixtures/core.scale/crossAlignment/mirror-cross-align-left-near.js b/test/fixtures/core.scale/crossAlignment/mirror-cross-align-left-near.js new file mode 100644 index 00000000000..c525a1bff18 --- /dev/null +++ b/test/fixtures/core.scale/crossAlignment/mirror-cross-align-left-near.js @@ -0,0 +1,30 @@ +module.exports = { + config: { + type: 'bar', + data: { + datasets: [{ + data: [1, 2, 3], + }], + labels: ['Long long label 1', 'Label2', 'Label3'] + }, + options: { + indexAxis: 'y', + scales: { + y: { + position: 'left', + ticks: { + mirror: true, + crossAlign: 'near', + }, + }, + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/core.scale/crossAlignment/mirror-cross-align-left-near.png b/test/fixtures/core.scale/crossAlignment/mirror-cross-align-left-near.png new file mode 100644 index 00000000000..39edd49d319 Binary files /dev/null and b/test/fixtures/core.scale/crossAlignment/mirror-cross-align-left-near.png differ diff --git a/test/fixtures/core.scale/crossAlignment/mirror-cross-align-right-center.js b/test/fixtures/core.scale/crossAlignment/mirror-cross-align-right-center.js new file mode 100644 index 00000000000..d9472b2de62 --- /dev/null +++ b/test/fixtures/core.scale/crossAlignment/mirror-cross-align-right-center.js @@ -0,0 +1,30 @@ +module.exports = { + config: { + type: 'bar', + data: { + datasets: [{ + data: [1, 2, 3], + }], + labels: ['Long long label 1', 'Label2', 'Label3'] + }, + options: { + indexAxis: 'y', + scales: { + y: { + position: 'right', + ticks: { + mirror: true, + crossAlign: 'center', + }, + }, + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/core.scale/crossAlignment/mirror-cross-align-right-center.png b/test/fixtures/core.scale/crossAlignment/mirror-cross-align-right-center.png new file mode 100644 index 00000000000..6b41efcd92f Binary files /dev/null and b/test/fixtures/core.scale/crossAlignment/mirror-cross-align-right-center.png differ diff --git a/test/fixtures/core.scale/crossAlignment/mirror-cross-align-right-far.js b/test/fixtures/core.scale/crossAlignment/mirror-cross-align-right-far.js new file mode 100644 index 00000000000..6e187c0fede --- /dev/null +++ b/test/fixtures/core.scale/crossAlignment/mirror-cross-align-right-far.js @@ -0,0 +1,30 @@ +module.exports = { + config: { + type: 'bar', + data: { + datasets: [{ + data: [1, 2, 3], + }], + labels: ['Long long label 1', 'Label2', 'Label3'] + }, + options: { + indexAxis: 'y', + scales: { + y: { + position: 'right', + ticks: { + mirror: true, + crossAlign: 'far', + }, + }, + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/core.scale/crossAlignment/mirror-cross-align-right-far.png b/test/fixtures/core.scale/crossAlignment/mirror-cross-align-right-far.png new file mode 100644 index 00000000000..992904590a1 Binary files /dev/null and b/test/fixtures/core.scale/crossAlignment/mirror-cross-align-right-far.png differ diff --git a/test/fixtures/core.scale/crossAlignment/mirror-cross-align-right-near.js b/test/fixtures/core.scale/crossAlignment/mirror-cross-align-right-near.js new file mode 100644 index 00000000000..03dc7f0e43e --- /dev/null +++ b/test/fixtures/core.scale/crossAlignment/mirror-cross-align-right-near.js @@ -0,0 +1,30 @@ +module.exports = { + config: { + type: 'bar', + data: { + datasets: [{ + data: [1, 2, 3], + }], + labels: ['Long long label 1', 'Label2', 'Label3'] + }, + options: { + indexAxis: 'y', + scales: { + y: { + position: 'right', + ticks: { + mirror: true, + crossAlign: 'near', + }, + }, + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/core.scale/crossAlignment/mirror-cross-align-right-near.png b/test/fixtures/core.scale/crossAlignment/mirror-cross-align-right-near.png new file mode 100644 index 00000000000..bbefddc5e64 Binary files /dev/null and b/test/fixtures/core.scale/crossAlignment/mirror-cross-align-right-near.png differ diff --git a/test/fixtures/core.scale/grid/border-over-grid.js b/test/fixtures/core.scale/grid/border-over-grid.js new file mode 100644 index 00000000000..cbe0acde40d --- /dev/null +++ b/test/fixtures/core.scale/grid/border-over-grid.js @@ -0,0 +1,41 @@ +module.exports = { + config: { + type: 'scatter', + options: { + scales: { + x: { + position: {y: 0}, + min: -10, + max: 10, + border: { + color: 'black', + width: 5 + }, + grid: { + color: 'lightGray', + lineWidth: 3, + }, + ticks: { + display: false + }, + }, + y: { + position: {x: 0}, + min: -10, + max: 10, + border: { + color: 'black', + width: 5 + }, + grid: { + color: 'lightGray', + lineWidth: 3, + }, + ticks: { + display: false + }, + } + } + } + } +}; diff --git a/test/fixtures/core.scale/grid/border-over-grid.png b/test/fixtures/core.scale/grid/border-over-grid.png new file mode 100644 index 00000000000..256e5743aa7 Binary files /dev/null and b/test/fixtures/core.scale/grid/border-over-grid.png differ diff --git a/test/fixtures/core.scale/grid/colors.js b/test/fixtures/core.scale/grid/colors.js new file mode 100644 index 00000000000..017ec042e3d --- /dev/null +++ b/test/fixtures/core.scale/grid/colors.js @@ -0,0 +1,42 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: ['1', '2', '3', '4', '5', '6'], + datasets: [{ + label: '# of Votes', + data: [12, 19, 3, 5, 2, 3] + }], + }, + options: { + scales: { + x: { + ticks: { + display: false + }, + border: { + color: 'blue', + width: 2, + }, + grid: { + color: 'green', + drawTicks: false, + } + }, + y: { + ticks: { + display: false + }, + border: { + color: 'black', + width: 2, + }, + grid: { + color: 'red', + drawTicks: false, + } + } + } + } + } +}; diff --git a/test/fixtures/core.scale/grid/colors.png b/test/fixtures/core.scale/grid/colors.png new file mode 100644 index 00000000000..231441d8e68 Binary files /dev/null and b/test/fixtures/core.scale/grid/colors.png differ diff --git a/test/fixtures/core.scale/grid/scriptable-borderDash.js b/test/fixtures/core.scale/grid/scriptable-borderDash.js new file mode 100644 index 00000000000..2471e64304b --- /dev/null +++ b/test/fixtures/core.scale/grid/scriptable-borderDash.js @@ -0,0 +1,39 @@ +module.exports = { + config: { + type: 'scatter', + options: { + scales: { + x: { + position: {y: 0}, + min: -10, + max: 10, + border: { + dash: (ctx) => ctx.index % 2 === 0 ? [6, 3] : [], + }, + grid: { + color: 'lightGray', + lineWidth: 3, + }, + ticks: { + display: false + }, + }, + y: { + position: {x: 0}, + min: -10, + max: 10, + border: { + dash: (ctx) => ctx.index % 2 === 0 ? [6, 3] : [], + }, + grid: { + color: 'lightGray', + lineWidth: 3, + }, + ticks: { + display: false + }, + } + } + } + } +}; diff --git a/test/fixtures/core.scale/grid/scriptable-borderDash.png b/test/fixtures/core.scale/grid/scriptable-borderDash.png new file mode 100644 index 00000000000..365545e9ab0 Binary files /dev/null and b/test/fixtures/core.scale/grid/scriptable-borderDash.png differ diff --git a/test/fixtures/core.scale/label-align-center.js b/test/fixtures/core.scale/label-align-center.js new file mode 100644 index 00000000000..41adf1df602 --- /dev/null +++ b/test/fixtures/core.scale/label-align-center.js @@ -0,0 +1,32 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [{ + data: [1, 2, 3], + }], + labels: ['Label1', 'Label2', 'Label3'] + }, + options: { + scales: { + x: { + ticks: { + align: 'center', + }, + }, + y: { + ticks: { + align: 'center', + } + } + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/core.scale/label-align-center.png b/test/fixtures/core.scale/label-align-center.png new file mode 100644 index 00000000000..e0b0d2030e0 Binary files /dev/null and b/test/fixtures/core.scale/label-align-center.png differ diff --git a/test/fixtures/core.scale/label-align-end.js b/test/fixtures/core.scale/label-align-end.js new file mode 100644 index 00000000000..36aa984b082 --- /dev/null +++ b/test/fixtures/core.scale/label-align-end.js @@ -0,0 +1,32 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [{ + data: [1, 2, 3], + }], + labels: ['Label1', 'Label2', 'Label3'] + }, + options: { + scales: { + x: { + ticks: { + align: 'end', + }, + }, + y: { + ticks: { + align: 'end', + } + } + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/core.scale/label-align-end.png b/test/fixtures/core.scale/label-align-end.png new file mode 100644 index 00000000000..57640fd21c8 Binary files /dev/null and b/test/fixtures/core.scale/label-align-end.png differ diff --git a/test/fixtures/core.scale/label-align-inner-onlyX.js b/test/fixtures/core.scale/label-align-inner-onlyX.js new file mode 100644 index 00000000000..a64157c3134 --- /dev/null +++ b/test/fixtures/core.scale/label-align-inner-onlyX.js @@ -0,0 +1,30 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [{ + data: [1, 2, 3], + }], + labels: ['Label1_long', 'Label2_long', 'Label3_long'] + }, + options: { + scales: { + x: { + ticks: { + align: 'inner', + }, + }, + y: { + display: false + } + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/core.scale/label-align-inner-onlyX.png b/test/fixtures/core.scale/label-align-inner-onlyX.png new file mode 100644 index 00000000000..7d5b039b502 Binary files /dev/null and b/test/fixtures/core.scale/label-align-inner-onlyX.png differ diff --git a/test/fixtures/core.scale/label-align-inner-reverse.js b/test/fixtures/core.scale/label-align-inner-reverse.js new file mode 100644 index 00000000000..d56f6a04fd1 --- /dev/null +++ b/test/fixtures/core.scale/label-align-inner-reverse.js @@ -0,0 +1,28 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [{ + data: [1, 2, 3], + }], + labels: ['Label1', 'Label2', 'Label3'] + }, + options: { + scales: { + x: { + ticks: { + align: 'inner' + }, + reverse: true + } + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/core.scale/label-align-inner-reverse.png b/test/fixtures/core.scale/label-align-inner-reverse.png new file mode 100644 index 00000000000..f1d8330bfed Binary files /dev/null and b/test/fixtures/core.scale/label-align-inner-reverse.png differ diff --git a/test/fixtures/core.scale/label-align-inner-rotate.js b/test/fixtures/core.scale/label-align-inner-rotate.js new file mode 100644 index 00000000000..7cee63a9556 --- /dev/null +++ b/test/fixtures/core.scale/label-align-inner-rotate.js @@ -0,0 +1,29 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [{ + data: [1, 2, 3], + }], + labels: ['Label1', 'Label2', 'Label3'] + }, + options: { + scales: { + x: { + ticks: { + align: 'inner', + maxRotation: 45, + minRotation: 45 + }, + } + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/core.scale/label-align-inner-rotate.png b/test/fixtures/core.scale/label-align-inner-rotate.png new file mode 100644 index 00000000000..cd21b41771d Binary files /dev/null and b/test/fixtures/core.scale/label-align-inner-rotate.png differ diff --git a/test/fixtures/core.scale/label-align-inner.js b/test/fixtures/core.scale/label-align-inner.js new file mode 100644 index 00000000000..ef62e9b66af --- /dev/null +++ b/test/fixtures/core.scale/label-align-inner.js @@ -0,0 +1,27 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [{ + data: [1, 2, 3], + }], + labels: ['Label1', 'Label2', 'Label3'] + }, + options: { + scales: { + x: { + ticks: { + align: 'inner', + }, + } + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/core.scale/label-align-inner.png b/test/fixtures/core.scale/label-align-inner.png new file mode 100644 index 00000000000..f420956f6af Binary files /dev/null and b/test/fixtures/core.scale/label-align-inner.png differ diff --git a/test/fixtures/core.scale/label-align-start.js b/test/fixtures/core.scale/label-align-start.js new file mode 100644 index 00000000000..fa23a49cfa7 --- /dev/null +++ b/test/fixtures/core.scale/label-align-start.js @@ -0,0 +1,32 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [{ + data: [1, 2, 3], + }], + labels: ['Label1', 'Label2', 'Label3'] + }, + options: { + scales: { + x: { + ticks: { + align: 'start', + }, + }, + y: { + ticks: { + align: 'start', + } + } + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/core.scale/label-align-start.png b/test/fixtures/core.scale/label-align-start.png new file mode 100644 index 00000000000..18941892f89 Binary files /dev/null and b/test/fixtures/core.scale/label-align-start.png differ diff --git a/test/fixtures/core.scale/label-offset-vertical-axes.json b/test/fixtures/core.scale/label-offset-vertical-axes.json new file mode 100644 index 00000000000..ea182d4d5d5 --- /dev/null +++ b/test/fixtures/core.scale/label-offset-vertical-axes.json @@ -0,0 +1,44 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["\u25C0", "\u25A0", "\u25C6", "\u25CF"], + "datasets": [{ + "data": [12, 19, 3, 5] + }] + }, + "options": { + "indexAxis": "y", + "scales": { + "x": { + "ticks": { + "display": false + }, + "grid":{ + "display": false + }, + "border": { + "display": false + } + }, + "y": { + "ticks": { + "labelOffset": 25 + }, + "border": { + "display": false + }, + "grid":{ + "display": false + } + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/core.scale/label-offset-vertical-axes.png b/test/fixtures/core.scale/label-offset-vertical-axes.png new file mode 100644 index 00000000000..bf9da7b7676 Binary files /dev/null and b/test/fixtures/core.scale/label-offset-vertical-axes.png differ diff --git a/test/fixtures/core.scale/tick-backdrop-alignment-inner.js b/test/fixtures/core.scale/tick-backdrop-alignment-inner.js new file mode 100644 index 00000000000..f9fbaddc103 --- /dev/null +++ b/test/fixtures/core.scale/tick-backdrop-alignment-inner.js @@ -0,0 +1,48 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], + datasets: [ + { + label: '# of Votes', + data: [12, 19, 3, 5, 2, 3], + }, + { + label: '# of Points', + data: [7, 11, 5, 8, 3, 7], + } + ] + }, + options: { + scales: { + y: { + ticks: { + display: false, + }, + grid: { + lineWidth: 0 + } + }, + x: { + position: 'top', + ticks: { + color: 'transparent', + backdropColor: 'red', + showLabelBackdrop: true, + align: 'inner', + }, + grid: { + lineWidth: 0 + } + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/core.scale/tick-backdrop-alignment-inner.png b/test/fixtures/core.scale/tick-backdrop-alignment-inner.png new file mode 100644 index 00000000000..2cddb8dfdd7 Binary files /dev/null and b/test/fixtures/core.scale/tick-backdrop-alignment-inner.png differ diff --git a/test/fixtures/core.scale/tick-backdrop-rotation.js b/test/fixtures/core.scale/tick-backdrop-rotation.js new file mode 100644 index 00000000000..8e2bad7a58c --- /dev/null +++ b/test/fixtures/core.scale/tick-backdrop-rotation.js @@ -0,0 +1,85 @@ +const grid = { + display: false +}; +const title = { + display: false, +}; +module.exports = { + tolerance: 0.0016, + config: { + type: 'line', + options: { + events: [], + scales: { + top: { + type: 'linear', + position: 'top', + ticks: { + display: true, + showLabelBackdrop: true, + minRotation: 45, + backdropColor: 'blue', + backdropPadding: 5, + align: 'start', + crossAlign: 'near', + }, + grid, + title + }, + left: { + type: 'linear', + position: 'left', + ticks: { + display: true, + showLabelBackdrop: true, + minRotation: 90, + backdropColor: 'green', + backdropPadding: { + x: 2, + y: 5 + }, + crossAlign: 'center', + }, + grid, + title + }, + bottom: { + type: 'linear', + position: 'bottom', + ticks: { + display: true, + showLabelBackdrop: true, + backdropColor: 'blue', + backdropPadding: { + x: 5, + y: 5 + }, + align: 'end', + crossAlign: 'far', + minRotation: 60, + }, + grid, + title + }, + right: { + type: 'linear', + position: 'right', + ticks: { + display: true, + showLabelBackdrop: true, + backdropColor: 'gray', + }, + grid, + title + }, + } + } + }, + options: { + canvas: { + height: 256, + width: 256 + }, + spriteText: true, + } +}; diff --git a/test/fixtures/core.scale/tick-backdrop-rotation.png b/test/fixtures/core.scale/tick-backdrop-rotation.png new file mode 100644 index 00000000000..4c5d47efab9 Binary files /dev/null and b/test/fixtures/core.scale/tick-backdrop-rotation.png differ diff --git a/test/fixtures/core.scale/tick-backdrop.js b/test/fixtures/core.scale/tick-backdrop.js new file mode 100644 index 00000000000..feeee9a4718 --- /dev/null +++ b/test/fixtures/core.scale/tick-backdrop.js @@ -0,0 +1,76 @@ +const grid = { + display: false +}; +const title = { + display: false, +}; +module.exports = { + config: { + type: 'line', + options: { + events: [], + scales: { + top: { + type: 'linear', + position: 'top', + ticks: { + display: true, + showLabelBackdrop: true, + backdropColor: 'red', + backdropPadding: 5, + align: 'start', + crossAlign: 'near', + }, + grid, + title + }, + left: { + type: 'linear', + position: 'left', + ticks: { + display: true, + showLabelBackdrop: true, + backdropColor: 'green', + backdropPadding: 5, + crossAlign: 'center', + }, + grid, + title + }, + bottom: { + type: 'linear', + position: 'bottom', + ticks: { + display: true, + showLabelBackdrop: true, + backdropColor: 'blue', + backdropPadding: 5, + align: 'end', + crossAlign: 'far', + }, + grid, + title + }, + right: { + type: 'linear', + position: 'right', + ticks: { + display: true, + showLabelBackdrop: true, + backdropColor: 'gray', + backdropPadding: 5, + }, + grid, + title + }, + } + } + }, + options: { + canvas: { + height: 256, + width: 256 + }, + spriteText: true, + } +}; diff --git a/test/fixtures/core.scale/tick-backdrop.png b/test/fixtures/core.scale/tick-backdrop.png new file mode 100644 index 00000000000..d6c79096e2b Binary files /dev/null and b/test/fixtures/core.scale/tick-backdrop.png differ diff --git a/test/fixtures/core.scale/tick-drawing.json b/test/fixtures/core.scale/tick-drawing.json new file mode 100644 index 00000000000..9327a2b296a --- /dev/null +++ b/test/fixtures/core.scale/tick-drawing.json @@ -0,0 +1,86 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["January", "February", "March", "April", "May", "June", "July"], + "datasets": [] + }, + "options": { + "indexAxis": "y", + "scales": { + "x": { + "type": "category", + "position": "top", + "id": "x-axis-1", + "ticks": { + "display": false + }, + "border": { + "display": false + }, + "grid":{ + "drawOnChartArea": false, + "color": "rgba(0, 0, 0, 1)" + } + }, + "x2": { + "type": "category", + "position": "bottom", + "ticks": { + "display": false + }, + "border": { + "display": false + }, + "grid":{ + "drawOnChartArea": false, + "color": "rgba(0, 0, 0, 1)" + } + }, + "y": { + "position": "left", + "id": "y-axis-1", + "type": "linear", + "offset": false, + "min": -100, + "max": 100, + "ticks": { + "display": false + }, + "border": { + "display": false + }, + "grid":{ + "offset": false, + "drawOnChartArea": false, + "color": "rgba(0, 0, 0, 1)" + } + }, + "y2": { + "type": "linear", + "position": "right", + "offset": false, + "min": 0, + "max": 50, + "ticks": { + "display": false + }, + "border": { + "display": false + }, + "grid":{ + "offset": false, + "drawOnChartArea": false, + "color": "rgba(0, 0, 0, 1)" + } + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/core.scale/tick-drawing.png b/test/fixtures/core.scale/tick-drawing.png new file mode 100644 index 00000000000..6c34af30f71 Binary files /dev/null and b/test/fixtures/core.scale/tick-drawing.png differ diff --git a/test/fixtures/core.scale/tick-override-styles.json b/test/fixtures/core.scale/tick-override-styles.json new file mode 100644 index 00000000000..23d8e81993d --- /dev/null +++ b/test/fixtures/core.scale/tick-override-styles.json @@ -0,0 +1,60 @@ +{ + "config": { + "type": "bar", + "data": { + "labels": ["January", "February", "March", "April", "May", "June", "July"], + "datasets": [] + }, + "options": { + "indexAxis": "y", + "scales": { + "x": { + "type": "category", + "position": "top", + "id": "x-axis-1", + "ticks": { + "display": false + }, + + "border": { + "display": false + }, + "grid":{ + "drawOnChartArea": false, + "color": "rgba(0, 0, 0, 1)", + "width": 1, + "tickColor": "rgba(255, 0, 0, 1)", + "tickWidth": 5 + } + }, + "y": { + "position": "left", + "id": "y-axis-1", + "type": "linear", + "offset": false, + "min": -100, + "max": 100, + "ticks": { + "display": false + }, + "border": { + "display": false + }, + "grid":{ + "offset": false, + "drawOnChartArea": false, + "color": "rgba(0, 0, 0, 1)", + "tickColor": "rgba(255, 0, 0, 1)", + "tickWidth": 5 + } + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/core.scale/tick-override-styles.png b/test/fixtures/core.scale/tick-override-styles.png new file mode 100644 index 00000000000..568fdf5afb9 Binary files /dev/null and b/test/fixtures/core.scale/tick-override-styles.png differ diff --git a/test/fixtures/core.scale/ticks-mirror-x.js b/test/fixtures/core.scale/ticks-mirror-x.js new file mode 100644 index 00000000000..ec151e58031 --- /dev/null +++ b/test/fixtures/core.scale/ticks-mirror-x.js @@ -0,0 +1,30 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [{ + data: [1, -1, 3], + }], + labels: ['Label1', 'Label2', 'Label3'] + }, + options: { + scales: { + x: { + ticks: { + mirror: true + } + }, + y: { + display: false + } + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/core.scale/ticks-mirror-x.png b/test/fixtures/core.scale/ticks-mirror-x.png new file mode 100644 index 00000000000..e9fe6537c47 Binary files /dev/null and b/test/fixtures/core.scale/ticks-mirror-x.png differ diff --git a/test/fixtures/core.scale/ticks-mirror.js b/test/fixtures/core.scale/ticks-mirror.js new file mode 100644 index 00000000000..8a54a41d1ab --- /dev/null +++ b/test/fixtures/core.scale/ticks-mirror.js @@ -0,0 +1,27 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [{ + data: [1, -1, 3], + }], + labels: ['Label1', 'Label2', 'Label3'] + }, + options: { + scales: { + y: { + ticks: { + mirror: true + } + } + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/core.scale/ticks-mirror.png b/test/fixtures/core.scale/ticks-mirror.png new file mode 100644 index 00000000000..35fba6e5218 Binary files /dev/null and b/test/fixtures/core.scale/ticks-mirror.png differ diff --git a/test/fixtures/core.scale/ticks/rotated-long.js b/test/fixtures/core.scale/ticks/rotated-long.js new file mode 100644 index 00000000000..b8bed15a45c --- /dev/null +++ b/test/fixtures/core.scale/ticks/rotated-long.js @@ -0,0 +1,47 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: ['Red Red Red Red', 'Blue Blue Blue Blue', 'Black Black Black Black', 'Green Green Green Green', 'Purple Purple Purple Purple', 'Orange Orange Orange Orange Orange Orange'], + datasets: [ + { + data: [12, 19, 3, 5, 2, 3] + }, + ] + }, + options: { + plugins: { + legend: false, + tooltip: false, + filler: false, + title: false + }, + scales: { + x: { + type: 'category', + position: 'bottom' + }, + x2: { + type: 'category', + position: 'top' + } + } + }, + plugins: [{ + afterDraw(chart) { + const ctx = chart.ctx; + ctx.save(); + ctx.strokeStyle = 'red'; + ctx.strokeRect(0, 0, chart.width, chart.height); + ctx.restore(); + } + }] + }, + options: { + spriteText: true, + canvas: { + width: 1024, + height: 512 + } + } +}; diff --git a/test/fixtures/core.scale/ticks/rotated-long.png b/test/fixtures/core.scale/ticks/rotated-long.png new file mode 100644 index 00000000000..e50ec2721f5 Binary files /dev/null and b/test/fixtures/core.scale/ticks/rotated-long.png differ diff --git a/test/fixtures/core.scale/ticks/rotated-multi-line.js b/test/fixtures/core.scale/ticks/rotated-multi-line.js new file mode 100644 index 00000000000..82fec478a63 --- /dev/null +++ b/test/fixtures/core.scale/ticks/rotated-multi-line.js @@ -0,0 +1,47 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [['Red', 'Red', 'Red', 'Red'], ['Blue', 'Blue', 'Blue', 'Blue'], ['Black', 'Black', 'Black', 'Black'], ['Green', 'Green', 'Green', 'Green'], ['Purple', 'Purple', 'Purple', 'Purple'], ['Orange Orange', 'Orange', 'Orange', 'Orange', 'Orange Orange']], + datasets: [ + { + data: [12, 19, 3, 5, 2, 3] + }, + ] + }, + options: { + plugins: { + legend: false, + tooltip: false, + filler: false, + title: false + }, + scales: { + x: { + type: 'category', + position: 'bottom' + }, + x2: { + type: 'category', + position: 'top' + } + } + }, + plugins: [{ + afterDraw(chart) { + const ctx = chart.ctx; + ctx.save(); + ctx.strokeStyle = 'red'; + ctx.strokeRect(0, 0, chart.width, chart.height); + ctx.restore(); + } + }] + }, + options: { + spriteText: true, + canvas: { + width: 610, + height: 512 + } + } +}; diff --git a/test/fixtures/core.scale/ticks/rotated-multi-line.png b/test/fixtures/core.scale/ticks/rotated-multi-line.png new file mode 100644 index 00000000000..ca9a7ef79c6 Binary files /dev/null and b/test/fixtures/core.scale/ticks/rotated-multi-line.png differ diff --git a/test/fixtures/core.scale/ticks/skip-by-callback.js b/test/fixtures/core.scale/ticks/skip-by-callback.js new file mode 100644 index 00000000000..53232bffa44 --- /dev/null +++ b/test/fixtures/core.scale/ticks/skip-by-callback.js @@ -0,0 +1,44 @@ +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/8892', + config: { + type: 'line', + data: { + labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], + datasets: [ + { + data: [12, 19, 3, 5, 2, 3], + }, + { + data: [7, 11, 5, 8, 3, 7], + } + ] + }, + options: { + scales: { + x: { + ticks: { + callback: function(val, index) { + if (index === 1) { + return undefined; + } + if (index === 3) { + return null; + } + return this.getLabelForValue(val); + } + } + }, + y: { + ticks: { + callback: function(val, index) { + return index % 2 === 0 ? '' + val : null; + } + } + } + }, + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/core.scale/ticks/skip-by-callback.png b/test/fixtures/core.scale/ticks/skip-by-callback.png new file mode 100644 index 00000000000..d6608d1b04a Binary files /dev/null and b/test/fixtures/core.scale/ticks/skip-by-callback.png differ diff --git a/test/fixtures/core.scale/title/align-end.js b/test/fixtures/core.scale/title/align-end.js new file mode 100644 index 00000000000..f75f12e21c1 --- /dev/null +++ b/test/fixtures/core.scale/title/align-end.js @@ -0,0 +1,77 @@ +module.exports = { + config: { + type: 'line', + options: { + events: [], + scales: { + top: { + type: 'linear', + position: 'top', + ticks: { + display: false + }, + grid: { + display: false + }, + title: { + display: true, + align: 'end', + text: 'top' + } + }, + left: { + type: 'linear', + position: 'left', + ticks: { + display: false + }, + grid: { + display: false + }, + title: { + display: true, + align: 'end', + text: 'left' + } + }, + bottom: { + type: 'linear', + position: 'bottom', + ticks: { + display: false + }, + grid: { + display: false + }, + title: { + display: true, + align: 'end', + text: 'bottom' + } + }, + right: { + type: 'linear', + position: 'right', + ticks: { + display: false + }, + grid: { + display: false + }, + title: { + display: true, + align: 'end', + text: 'right' + } + }, + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 256 + }, + } +}; diff --git a/test/fixtures/core.scale/title/align-end.png b/test/fixtures/core.scale/title/align-end.png new file mode 100644 index 00000000000..dbd5397fa9e Binary files /dev/null and b/test/fixtures/core.scale/title/align-end.png differ diff --git a/test/fixtures/core.scale/title/align-start.js b/test/fixtures/core.scale/title/align-start.js new file mode 100644 index 00000000000..6ca37ffa4d7 --- /dev/null +++ b/test/fixtures/core.scale/title/align-start.js @@ -0,0 +1,77 @@ +module.exports = { + config: { + type: 'line', + options: { + events: [], + scales: { + top: { + type: 'linear', + position: 'top', + ticks: { + display: false + }, + grid: { + display: false + }, + title: { + display: true, + align: 'start', + text: 'top' + } + }, + left: { + type: 'linear', + position: 'left', + ticks: { + display: false + }, + grid: { + display: false + }, + title: { + display: true, + align: 'start', + text: 'left' + } + }, + bottom: { + type: 'linear', + position: 'bottom', + ticks: { + display: false + }, + grid: { + display: false + }, + title: { + display: true, + align: 'start', + text: 'bottom' + } + }, + right: { + type: 'linear', + position: 'right', + ticks: { + display: false + }, + grid: { + display: false + }, + title: { + display: true, + align: 'start', + text: 'right' + } + }, + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 256 + }, + } +}; diff --git a/test/fixtures/core.scale/title/align-start.png b/test/fixtures/core.scale/title/align-start.png new file mode 100644 index 00000000000..21ffbea2699 Binary files /dev/null and b/test/fixtures/core.scale/title/align-start.png differ diff --git a/test/fixtures/core.scale/title/default.js b/test/fixtures/core.scale/title/default.js new file mode 100644 index 00000000000..dfd1c469cbe --- /dev/null +++ b/test/fixtures/core.scale/title/default.js @@ -0,0 +1,73 @@ +module.exports = { + config: { + type: 'line', + options: { + events: [], + scales: { + top: { + type: 'linear', + position: 'top', + ticks: { + display: false + }, + grid: { + display: false + }, + title: { + display: true, + text: 'top' + } + }, + left: { + type: 'linear', + position: 'left', + ticks: { + display: false + }, + grid: { + display: false + }, + title: { + display: true, + text: 'left' + } + }, + bottom: { + type: 'linear', + position: 'bottom', + ticks: { + display: false + }, + grid: { + display: false + }, + title: { + display: true, + text: 'bottom' + } + }, + right: { + type: 'linear', + position: 'right', + ticks: { + display: false + }, + grid: { + display: false + }, + title: { + display: true, + text: 'right' + } + }, + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 256 + }, + } +}; diff --git a/test/fixtures/core.scale/title/default.png b/test/fixtures/core.scale/title/default.png new file mode 100644 index 00000000000..3dc2270ecea Binary files /dev/null and b/test/fixtures/core.scale/title/default.png differ diff --git a/test/fixtures/core.scale/title/horizontal-center.js b/test/fixtures/core.scale/title/horizontal-center.js new file mode 100644 index 00000000000..68e488d8a5a --- /dev/null +++ b/test/fixtures/core.scale/title/horizontal-center.js @@ -0,0 +1,49 @@ +module.exports = { + config: { + type: 'line', + options: { + events: [], + scales: { + y: { + type: 'linear', + position: 'left', + min: 0, + max: 100, + ticks: { + display: false + }, + grid: { + display: false + }, + title: { + display: true, + text: 'vertical' + } + }, + x: { + type: 'linear', + position: 'center', + min: 0, + max: 100, + ticks: { + display: false + }, + grid: { + display: false + }, + title: { + display: true, + text: 'horizontal' + } + }, + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 256 + }, + } +}; diff --git a/test/fixtures/core.scale/title/horizontal-center.png b/test/fixtures/core.scale/title/horizontal-center.png new file mode 100644 index 00000000000..b3abf341cda Binary files /dev/null and b/test/fixtures/core.scale/title/horizontal-center.png differ diff --git a/test/fixtures/core.scale/title/horizontal-value.js b/test/fixtures/core.scale/title/horizontal-value.js new file mode 100644 index 00000000000..7cb97f6ad61 --- /dev/null +++ b/test/fixtures/core.scale/title/horizontal-value.js @@ -0,0 +1,51 @@ +module.exports = { + config: { + type: 'line', + options: { + events: [], + scales: { + y: { + type: 'linear', + position: 'left', + min: 0, + max: 100, + ticks: { + display: false + }, + grid: { + display: false + }, + title: { + display: true, + text: 'vertical' + } + }, + x: { + type: 'linear', + position: { + y: 40, + }, + min: 0, + max: 100, + ticks: { + display: false + }, + grid: { + display: false + }, + title: { + display: true, + text: 'horizontal' + } + }, + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 256 + }, + } +}; diff --git a/test/fixtures/core.scale/title/horizontal-value.png b/test/fixtures/core.scale/title/horizontal-value.png new file mode 100644 index 00000000000..9d3c5dd05c9 Binary files /dev/null and b/test/fixtures/core.scale/title/horizontal-value.png differ diff --git a/test/fixtures/core.scale/title/multi-line/align-end.js b/test/fixtures/core.scale/title/multi-line/align-end.js new file mode 100644 index 00000000000..8e4ff0e8baa --- /dev/null +++ b/test/fixtures/core.scale/title/multi-line/align-end.js @@ -0,0 +1,77 @@ +module.exports = { + config: { + type: 'line', + options: { + events: [], + scales: { + top: { + type: 'linear', + position: 'top', + ticks: { + display: false + }, + grid: { + display: false + }, + title: { + display: true, + align: 'end', + text: ['top', 'line2', 'line3'] + } + }, + left: { + type: 'linear', + position: 'left', + ticks: { + display: false + }, + grid: { + display: false + }, + title: { + display: true, + align: 'end', + text: ['left', 'line2', 'line3'] + } + }, + bottom: { + type: 'linear', + position: 'bottom', + ticks: { + display: false + }, + grid: { + display: false + }, + title: { + display: true, + align: 'end', + text: ['bottom', 'line2', 'line3'] + } + }, + right: { + type: 'linear', + position: 'right', + ticks: { + display: false + }, + grid: { + display: false + }, + title: { + display: true, + align: 'end', + text: ['right', 'line2', 'line3'] + } + }, + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 256 + }, + } +}; diff --git a/test/fixtures/core.scale/title/multi-line/align-end.png b/test/fixtures/core.scale/title/multi-line/align-end.png new file mode 100644 index 00000000000..326db59e540 Binary files /dev/null and b/test/fixtures/core.scale/title/multi-line/align-end.png differ diff --git a/test/fixtures/core.scale/title/multi-line/align-start.js b/test/fixtures/core.scale/title/multi-line/align-start.js new file mode 100644 index 00000000000..fd6a4d6b522 --- /dev/null +++ b/test/fixtures/core.scale/title/multi-line/align-start.js @@ -0,0 +1,77 @@ +module.exports = { + config: { + type: 'line', + options: { + events: [], + scales: { + top: { + type: 'linear', + position: 'top', + ticks: { + display: false + }, + grid: { + display: false + }, + title: { + display: true, + align: 'start', + text: ['top', 'line2', 'line3'] + } + }, + left: { + type: 'linear', + position: 'left', + ticks: { + display: false + }, + grid: { + display: false + }, + title: { + display: true, + align: 'start', + text: ['left', 'line2', 'line3'] + } + }, + bottom: { + type: 'linear', + position: 'bottom', + ticks: { + display: false + }, + grid: { + display: false + }, + title: { + display: true, + align: 'start', + text: ['bottom', 'line2', 'line3'] + } + }, + right: { + type: 'linear', + position: 'right', + ticks: { + display: false + }, + grid: { + display: false + }, + title: { + display: true, + align: 'start', + text: ['right', 'line2', 'line3'] + } + }, + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 256 + }, + } +}; diff --git a/test/fixtures/core.scale/title/multi-line/align-start.png b/test/fixtures/core.scale/title/multi-line/align-start.png new file mode 100644 index 00000000000..b653aa5b6ed Binary files /dev/null and b/test/fixtures/core.scale/title/multi-line/align-start.png differ diff --git a/test/fixtures/core.scale/title/multi-line/default.js b/test/fixtures/core.scale/title/multi-line/default.js new file mode 100644 index 00000000000..28a9f303887 --- /dev/null +++ b/test/fixtures/core.scale/title/multi-line/default.js @@ -0,0 +1,73 @@ +module.exports = { + config: { + type: 'line', + options: { + events: [], + scales: { + top: { + type: 'linear', + position: 'top', + ticks: { + display: false + }, + grid: { + display: false + }, + title: { + display: true, + text: ['top', 'line2', 'line3'] + } + }, + left: { + type: 'linear', + position: 'left', + ticks: { + display: false + }, + grid: { + display: false + }, + title: { + display: true, + text: ['left', 'line2', 'line3'] + } + }, + bottom: { + type: 'linear', + position: 'bottom', + ticks: { + display: false + }, + grid: { + display: false + }, + title: { + display: true, + text: ['bottom', 'line2', 'line3'] + } + }, + right: { + type: 'linear', + position: 'right', + ticks: { + display: false + }, + grid: { + display: false + }, + title: { + display: true, + text: ['right', 'line2', 'line3'] + } + }, + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 256 + }, + } +}; diff --git a/test/fixtures/core.scale/title/multi-line/default.png b/test/fixtures/core.scale/title/multi-line/default.png new file mode 100644 index 00000000000..f53f33e0e82 Binary files /dev/null and b/test/fixtures/core.scale/title/multi-line/default.png differ diff --git a/test/fixtures/core.scale/title/vertical-center.js b/test/fixtures/core.scale/title/vertical-center.js new file mode 100644 index 00000000000..89669be6d3f --- /dev/null +++ b/test/fixtures/core.scale/title/vertical-center.js @@ -0,0 +1,49 @@ +module.exports = { + config: { + type: 'line', + options: { + events: [], + scales: { + y: { + type: 'linear', + position: 'center', + min: 0, + max: 100, + ticks: { + display: false + }, + grid: { + display: false + }, + title: { + display: true, + text: 'vertical' + } + }, + x: { + type: 'linear', + position: 'bottom', + min: 0, + max: 100, + ticks: { + display: false + }, + grid: { + display: false + }, + title: { + display: true, + text: 'horizontal' + } + }, + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 256 + }, + } +}; diff --git a/test/fixtures/core.scale/title/vertical-center.png b/test/fixtures/core.scale/title/vertical-center.png new file mode 100644 index 00000000000..8ebdad43d86 Binary files /dev/null and b/test/fixtures/core.scale/title/vertical-center.png differ diff --git a/test/fixtures/core.scale/title/vertical-value.js b/test/fixtures/core.scale/title/vertical-value.js new file mode 100644 index 00000000000..b8a0de6373b --- /dev/null +++ b/test/fixtures/core.scale/title/vertical-value.js @@ -0,0 +1,51 @@ +module.exports = { + config: { + type: 'line', + options: { + events: [], + scales: { + y: { + type: 'linear', + position: { + x: 40 + }, + min: 0, + max: 100, + ticks: { + display: false + }, + grid: { + display: false + }, + title: { + display: true, + text: 'vertical' + } + }, + x: { + type: 'linear', + position: 'bottom', + min: 0, + max: 100, + ticks: { + display: false + }, + grid: { + display: false + }, + title: { + display: true, + text: 'horizontal' + } + }, + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 256 + }, + } +}; diff --git a/test/fixtures/core.scale/title/vertical-value.png b/test/fixtures/core.scale/title/vertical-value.png new file mode 100644 index 00000000000..eb4b201db5a Binary files /dev/null and b/test/fixtures/core.scale/title/vertical-value.png differ diff --git a/test/fixtures/core.scale/x-axis-position-center.json b/test/fixtures/core.scale/x-axis-position-center.json new file mode 100644 index 00000000000..98dda96fca0 --- /dev/null +++ b/test/fixtures/core.scale/x-axis-position-center.json @@ -0,0 +1,63 @@ +{ + "config": { + "type": "scatter", + "data": { + "datasets": [{ + "data": [{ + "x": -20, + "y": -30 + }, { + "x": 0, + "y": 0 + }, { + "x": 20, + "y": 15 + }] + }] + }, + "options": { + "scales": { + "x": { + "position": "center", + "axis": "x", + "min": -100, + "max": 100, + "border": { + "color": "red" + }, + "grid": { + "color": "red", + "drawOnChartArea": false + }, + "ticks": { + "display": true, + "color": "red" + } + }, + "y": { + "position": "left", + "axis": "y", + "min": -100, + "max": 100, + "border": { + "color": "red" + }, + "grid": { + "color": "red", + "drawOnChartArea": false + }, + "ticks": { + "display": true + } + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + }, + "spriteText": true + } +} diff --git a/test/fixtures/core.scale/x-axis-position-center.png b/test/fixtures/core.scale/x-axis-position-center.png new file mode 100644 index 00000000000..284056f509c Binary files /dev/null and b/test/fixtures/core.scale/x-axis-position-center.png differ diff --git a/test/fixtures/core.scale/x-axis-position-dynamic-margin.js b/test/fixtures/core.scale/x-axis-position-dynamic-margin.js new file mode 100644 index 00000000000..7e8bf6e6e79 --- /dev/null +++ b/test/fixtures/core.scale/x-axis-position-dynamic-margin.js @@ -0,0 +1,27 @@ +module.exports = { + config: { + type: 'line', + options: { + scales: { + x: { + labels: ['Left Label', 'Center Label', 'Right Label'], + position: { + y: 30 + }, + }, + y: { + display: false, + min: -100, + max: 100, + } + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + }, + spriteText: true + } +}; diff --git a/test/fixtures/core.scale/x-axis-position-dynamic-margin.png b/test/fixtures/core.scale/x-axis-position-dynamic-margin.png new file mode 100644 index 00000000000..3dffa316dd7 Binary files /dev/null and b/test/fixtures/core.scale/x-axis-position-dynamic-margin.png differ diff --git a/test/fixtures/core.scale/x-axis-position-dynamic.json b/test/fixtures/core.scale/x-axis-position-dynamic.json new file mode 100644 index 00000000000..7fbe9aed2f9 --- /dev/null +++ b/test/fixtures/core.scale/x-axis-position-dynamic.json @@ -0,0 +1,64 @@ +{ + "config": { + "type": "scatter", + "data": { + "datasets": [{ + "data": [{ + "x": -20, + "y": -30 + }, { + "x": 0, + "y": 0 + }, { + "x": 20, + "y": 15 + }] + }] + }, + "options": { + "scales": { + "x": { + "position": { + "y": 30 + }, + "axis": "x", + "min": -100, + "max": 100, + "border": { + "color": "red" + }, + "grid": { + "color": "red", + "drawOnChartArea": false + }, + "ticks": { + "display": true + } + }, + "y": { + "position": "left", + "axis": "y", + "min": -100, + "max": 100, + "border": { + "color": "red" + }, + "grid": { + "color": "red", + "drawOnChartArea": false + }, + "ticks": { + "display": true + } + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + }, + "spriteText": true + } +} diff --git a/test/fixtures/core.scale/x-axis-position-dynamic.png b/test/fixtures/core.scale/x-axis-position-dynamic.png new file mode 100644 index 00000000000..bbebdd33b7e Binary files /dev/null and b/test/fixtures/core.scale/x-axis-position-dynamic.png differ diff --git a/test/fixtures/core.scale/y-axis-position-center.json b/test/fixtures/core.scale/y-axis-position-center.json new file mode 100644 index 00000000000..a4e92a5db98 --- /dev/null +++ b/test/fixtures/core.scale/y-axis-position-center.json @@ -0,0 +1,62 @@ +{ + "config": { + "type": "scatter", + "data": { + "datasets": [{ + "data": [{ + "x": -20, + "y": -30 + }, { + "x": 0, + "y": 0 + }, { + "x": 20, + "y": 15 + }] + }] + }, + "options": { + "scales": { + "x": { + "position": "bottom", + "axis": "x", + "min": -100, + "max": 100, + "border": { + "color": "red" + }, + "grid": { + "color": "red", + "drawOnChartArea": false + }, + "ticks": { + "display": true + } + }, + "y": { + "position": "center", + "axis": "y", + "min": -100, + "max": 100, + "border": { + "color": "red" + }, + "grid": { + "color": "red", + "drawOnChartArea": false + }, + "ticks": { + "display": true + } + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + }, + "spriteText": true + } +} diff --git a/test/fixtures/core.scale/y-axis-position-center.png b/test/fixtures/core.scale/y-axis-position-center.png new file mode 100644 index 00000000000..a2e6f0110bc Binary files /dev/null and b/test/fixtures/core.scale/y-axis-position-center.png differ diff --git a/test/fixtures/core.scale/y-axis-position-dynamic.json b/test/fixtures/core.scale/y-axis-position-dynamic.json new file mode 100644 index 00000000000..6d8b3fcf933 --- /dev/null +++ b/test/fixtures/core.scale/y-axis-position-dynamic.json @@ -0,0 +1,65 @@ +{ + "config": { + "type": "scatter", + "data": { + "datasets": [{ + "data": [{ + "x": -20, + "y": -30 + }, { + "x": 0, + "y": 0 + }, { + "x": 20, + "y": 15 + }] + }] + }, + "options": { + "scales": { + "x": { + "position": "bottom", + "axis": "x", + "min": -100, + "max": 100, + "border": { + "color": "red" + }, + "grid": { + "color": "red", + "drawOnChartArea": false + }, + "ticks": { + "display": true + } + }, + "y": { + "position": { + "x": -50 + }, + "axis": "y", + "min": -100, + "max": 100, + "border": { + "color": "red" + }, + "grid": { + "color": "red", + "drawOnChartArea": false + }, + "ticks": { + "display": true + } + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + }, + "spriteText": true + }, + "tolerance": 0.01 +} diff --git a/test/fixtures/core.scale/y-axis-position-dynamic.png b/test/fixtures/core.scale/y-axis-position-dynamic.png new file mode 100644 index 00000000000..a3a38a5d11c Binary files /dev/null and b/test/fixtures/core.scale/y-axis-position-dynamic.png differ diff --git a/test/fixtures/element.line/cubicInterpolationMode/monotone-horizontal.js b/test/fixtures/element.line/cubicInterpolationMode/monotone-horizontal.js new file mode 100644 index 00000000000..29f68c95bef --- /dev/null +++ b/test/fixtures/element.line/cubicInterpolationMode/monotone-horizontal.js @@ -0,0 +1,27 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [ + { + data: [{x: 1, y: 10}, {x: 5, y: 0}, {x: 15, y: -10}, {x: 19, y: -5}], + borderColor: 'red', + fill: false, + cubicInterpolationMode: 'monotone' + } + ] + }, + options: { + scales: { + x: {type: 'linear', display: false, min: 0, max: 20}, + y: {display: false, min: -15, max: 15} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/element.line/cubicInterpolationMode/monotone-horizontal.png b/test/fixtures/element.line/cubicInterpolationMode/monotone-horizontal.png new file mode 100644 index 00000000000..6c79efd0780 Binary files /dev/null and b/test/fixtures/element.line/cubicInterpolationMode/monotone-horizontal.png differ diff --git a/test/fixtures/element.line/cubicInterpolationMode/monotone-vertical.js b/test/fixtures/element.line/cubicInterpolationMode/monotone-vertical.js new file mode 100644 index 00000000000..2debc627b10 --- /dev/null +++ b/test/fixtures/element.line/cubicInterpolationMode/monotone-vertical.js @@ -0,0 +1,28 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [ + { + data: [{x: 10, y: 1}, {x: 0, y: 5}, {x: -10, y: 15}, {x: -5, y: 19}], + borderColor: 'red', + fill: false, + cubicInterpolationMode: 'monotone' + } + ] + }, + options: { + indexAxis: 'y', + scales: { + x: {display: false, min: -15, max: 15}, + y: {type: 'linear', display: false, min: 0, max: 20} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/element.line/cubicInterpolationMode/monotone-vertical.png b/test/fixtures/element.line/cubicInterpolationMode/monotone-vertical.png new file mode 100644 index 00000000000..51a80dbf0a6 Binary files /dev/null and b/test/fixtures/element.line/cubicInterpolationMode/monotone-vertical.png differ diff --git a/test/fixtures/element.line/default.js b/test/fixtures/element.line/default.js new file mode 100644 index 00000000000..3fe92986d85 --- /dev/null +++ b/test/fixtures/element.line/default.js @@ -0,0 +1,24 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [ + { + data: [{x: 1, y: 10}, {x: 5, y: 0}, {x: 15, y: -10}, {x: 19, y: -5}], + } + ] + }, + options: { + scales: { + x: {type: 'linear', display: false, min: 0, max: 20}, + y: {display: false, min: -15, max: 15} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/element.line/default.png b/test/fixtures/element.line/default.png new file mode 100644 index 00000000000..714ee868326 Binary files /dev/null and b/test/fixtures/element.line/default.png differ diff --git a/test/fixtures/element.line/skip/all.js b/test/fixtures/element.line/skip/all.js new file mode 100644 index 00000000000..af52ef0f40f --- /dev/null +++ b/test/fixtures/element.line/skip/all.js @@ -0,0 +1,27 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [ + { + data: [{x: 0, y: NaN}, {x: NaN, y: 0}, {x: NaN, y: -10}, {x: 19, y: NaN}], + borderColor: 'red', + fill: true, + tension: 0 + } + ] + }, + options: { + scales: { + x: {type: 'linear', display: false, min: 0, max: 20}, + y: {display: false, min: -15, max: 15} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/element.line/skip/all.png b/test/fixtures/element.line/skip/all.png new file mode 100644 index 00000000000..5031ab9bb1c Binary files /dev/null and b/test/fixtures/element.line/skip/all.png differ diff --git a/test/fixtures/element.line/skip/first-span.js b/test/fixtures/element.line/skip/first-span.js new file mode 100644 index 00000000000..823807de189 --- /dev/null +++ b/test/fixtures/element.line/skip/first-span.js @@ -0,0 +1,28 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [ + { + data: [{x: NaN, y: 10}, {x: 5, y: 0}, {x: 15, y: -10}, {x: 19, y: -5}], + borderColor: 'red', + fill: true, + spanGaps: true, + tension: 0 + } + ] + }, + options: { + scales: { + x: {type: 'linear', display: false, min: 0, max: 20}, + y: {display: false, min: -15, max: 15} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/element.line/skip/first-span.png b/test/fixtures/element.line/skip/first-span.png new file mode 100644 index 00000000000..8d8b5c43cb7 Binary files /dev/null and b/test/fixtures/element.line/skip/first-span.png differ diff --git a/test/fixtures/element.line/skip/first.js b/test/fixtures/element.line/skip/first.js new file mode 100644 index 00000000000..268434154b7 --- /dev/null +++ b/test/fixtures/element.line/skip/first.js @@ -0,0 +1,27 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [ + { + data: [{x: NaN, y: 10}, {x: 5, y: 0}, {x: 15, y: -10}, {x: 19, y: -5}], + borderColor: 'red', + fill: true, + tension: 0 + } + ] + }, + options: { + scales: { + x: {type: 'linear', display: false, min: 0, max: 20}, + y: {display: false, min: -15, max: 15} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/element.line/skip/first.png b/test/fixtures/element.line/skip/first.png new file mode 100644 index 00000000000..8d8b5c43cb7 Binary files /dev/null and b/test/fixtures/element.line/skip/first.png differ diff --git a/test/fixtures/element.line/skip/last-span.js b/test/fixtures/element.line/skip/last-span.js new file mode 100644 index 00000000000..39a02190247 --- /dev/null +++ b/test/fixtures/element.line/skip/last-span.js @@ -0,0 +1,28 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [ + { + data: [{x: 0, y: 10}, {x: 5, y: 0}, {x: 15, y: -10}, {x: NaN, y: -5}], + borderColor: 'red', + fill: true, + spanGaps: true, + tension: 0 + } + ] + }, + options: { + scales: { + x: {type: 'linear', display: false, min: 0, max: 20}, + y: {display: false, min: -15, max: 15} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/element.line/skip/last-span.png b/test/fixtures/element.line/skip/last-span.png new file mode 100644 index 00000000000..172fe4eeea3 Binary files /dev/null and b/test/fixtures/element.line/skip/last-span.png differ diff --git a/test/fixtures/element.line/skip/last.js b/test/fixtures/element.line/skip/last.js new file mode 100644 index 00000000000..239a958a0ba --- /dev/null +++ b/test/fixtures/element.line/skip/last.js @@ -0,0 +1,27 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [ + { + data: [{x: 0, y: 10}, {x: 5, y: 0}, {x: 15, y: -10}, {x: NaN, y: -5}], + borderColor: 'red', + fill: true, + tension: 0 + } + ] + }, + options: { + scales: { + x: {type: 'linear', display: false, min: 0, max: 20}, + y: {display: false, min: -15, max: 15} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/element.line/skip/last.png b/test/fixtures/element.line/skip/last.png new file mode 100644 index 00000000000..172fe4eeea3 Binary files /dev/null and b/test/fixtures/element.line/skip/last.png differ diff --git a/test/fixtures/element.line/skip/middle-span.js b/test/fixtures/element.line/skip/middle-span.js new file mode 100644 index 00000000000..0df72aca97b --- /dev/null +++ b/test/fixtures/element.line/skip/middle-span.js @@ -0,0 +1,29 @@ +module.exports = { + config: { + type: 'line', + parsing: false, + data: { + datasets: [ + { + data: [{x: 0, y: 10}, {x: 5, y: 0}, {x: null, y: -10}, {x: 19, y: -5}], + borderColor: 'red', + fill: true, + spanGaps: true, + tension: 0 + } + ] + }, + options: { + scales: { + x: {type: 'linear', display: false, min: 0, max: 20}, + y: {display: false, min: -15, max: 15} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/element.line/skip/middle-span.png b/test/fixtures/element.line/skip/middle-span.png new file mode 100644 index 00000000000..604a0a35475 Binary files /dev/null and b/test/fixtures/element.line/skip/middle-span.png differ diff --git a/test/fixtures/element.line/skip/middle.js b/test/fixtures/element.line/skip/middle.js new file mode 100644 index 00000000000..af846270fe3 --- /dev/null +++ b/test/fixtures/element.line/skip/middle.js @@ -0,0 +1,27 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [ + { + data: [{x: 0, y: 10}, {x: 5, y: 0}, {x: NaN, y: -10}, {x: 19, y: -5}], + borderColor: 'red', + fill: true, + tension: 0 + } + ] + }, + options: { + scales: { + x: {type: 'linear', display: false, min: 0, max: 20}, + y: {display: false, min: -15, max: 15} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/element.line/skip/middle.png b/test/fixtures/element.line/skip/middle.png new file mode 100644 index 00000000000..9dc65f8e064 Binary files /dev/null and b/test/fixtures/element.line/skip/middle.png differ diff --git a/test/fixtures/element.line/stepped/after.js b/test/fixtures/element.line/stepped/after.js new file mode 100644 index 00000000000..7dcd31aeafe --- /dev/null +++ b/test/fixtures/element.line/stepped/after.js @@ -0,0 +1,28 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [ + { + data: [{x: 1, y: 10}, {x: 5, y: 0}, {x: 15, y: -10}, {x: 19, y: -5}], + borderColor: 'red', + fill: false, + tension: 0, + stepped: 'after' + } + ] + }, + options: { + scales: { + x: {type: 'linear', display: false, min: 0, max: 20}, + y: {display: false, min: -15, max: 15} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/element.line/stepped/after.png b/test/fixtures/element.line/stepped/after.png new file mode 100644 index 00000000000..9c546bf2f4d Binary files /dev/null and b/test/fixtures/element.line/stepped/after.png differ diff --git a/test/fixtures/element.line/stepped/before.js b/test/fixtures/element.line/stepped/before.js new file mode 100644 index 00000000000..5046066bdd9 --- /dev/null +++ b/test/fixtures/element.line/stepped/before.js @@ -0,0 +1,28 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [ + { + data: [{x: 1, y: 10}, {x: 5, y: 0}, {x: 15, y: -10}, {x: 19, y: -5}], + borderColor: 'red', + fill: false, + tension: 0, + stepped: 'before' + } + ] + }, + options: { + scales: { + x: {type: 'linear', display: false, min: 0, max: 20}, + y: {display: false, min: -15, max: 15} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/element.line/stepped/before.png b/test/fixtures/element.line/stepped/before.png new file mode 100644 index 00000000000..ca84977548f Binary files /dev/null and b/test/fixtures/element.line/stepped/before.png differ diff --git a/test/fixtures/element.line/stepped/default.js b/test/fixtures/element.line/stepped/default.js new file mode 100644 index 00000000000..6bbe0514147 --- /dev/null +++ b/test/fixtures/element.line/stepped/default.js @@ -0,0 +1,28 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [ + { + data: [{x: 1, y: 10}, {x: 5, y: 0}, {x: 15, y: -10}, {x: 19, y: -5}], + borderColor: 'red', + fill: false, + tension: 0, + stepped: true + } + ] + }, + options: { + scales: { + x: {type: 'linear', display: false, min: 0, max: 20}, + y: {display: false, min: -15, max: 15} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/element.line/stepped/default.png b/test/fixtures/element.line/stepped/default.png new file mode 100644 index 00000000000..ca84977548f Binary files /dev/null and b/test/fixtures/element.line/stepped/default.png differ diff --git a/test/fixtures/element.line/stepped/middle.js b/test/fixtures/element.line/stepped/middle.js new file mode 100644 index 00000000000..15092814929 --- /dev/null +++ b/test/fixtures/element.line/stepped/middle.js @@ -0,0 +1,28 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [ + { + data: [{x: 1, y: 10}, {x: 5, y: 0}, {x: 15, y: -10}, {x: 19, y: -5}], + borderColor: 'red', + fill: false, + tension: 0, + stepped: 'middle' + } + ] + }, + options: { + scales: { + x: {type: 'linear', display: false, min: 0, max: 20}, + y: {display: false, min: -15, max: 15} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/element.line/stepped/middle.png b/test/fixtures/element.line/stepped/middle.png new file mode 100644 index 00000000000..e1f8adfeba0 Binary files /dev/null and b/test/fixtures/element.line/stepped/middle.png differ diff --git a/test/fixtures/element.line/tension/default.js b/test/fixtures/element.line/tension/default.js new file mode 100644 index 00000000000..0f4f6203435 --- /dev/null +++ b/test/fixtures/element.line/tension/default.js @@ -0,0 +1,26 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [ + { + data: [{x: 1, y: 10}, {x: 5, y: 0}, {x: 15, y: -10}, {x: 19, y: -5}], + borderColor: 'red', + fill: false + } + ] + }, + options: { + scales: { + x: {type: 'linear', display: false, min: 0, max: 20}, + y: {display: false, min: -15, max: 15} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/element.line/tension/default.png b/test/fixtures/element.line/tension/default.png new file mode 100644 index 00000000000..3131b7694ec Binary files /dev/null and b/test/fixtures/element.line/tension/default.png differ diff --git a/test/fixtures/element.line/tension/one.js b/test/fixtures/element.line/tension/one.js new file mode 100644 index 00000000000..9994e339ca4 --- /dev/null +++ b/test/fixtures/element.line/tension/one.js @@ -0,0 +1,27 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [ + { + data: [{x: 1, y: 10}, {x: 5, y: 0}, {x: 15, y: -10}, {x: 19, y: -5}], + borderColor: 'red', + fill: false, + tension: 1 + } + ] + }, + options: { + scales: { + x: {type: 'linear', display: false, min: 0, max: 20}, + y: {display: false, min: -15, max: 15} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/element.line/tension/one.png b/test/fixtures/element.line/tension/one.png new file mode 100644 index 00000000000..c01085a7a0e Binary files /dev/null and b/test/fixtures/element.line/tension/one.png differ diff --git a/test/fixtures/element.line/tension/zero.js b/test/fixtures/element.line/tension/zero.js new file mode 100644 index 00000000000..f251d5ed606 --- /dev/null +++ b/test/fixtures/element.line/tension/zero.js @@ -0,0 +1,27 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [ + { + data: [{x: 1, y: 10}, {x: 5, y: 0}, {x: 15, y: -10}, {x: 19, y: -5}], + borderColor: 'red', + fill: false, + tension: 0 + } + ] + }, + options: { + scales: { + x: {type: 'linear', display: false, min: 0, max: 20}, + y: {display: false, min: -15, max: 15} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/element.line/tension/zero.png b/test/fixtures/element.line/tension/zero.png new file mode 100644 index 00000000000..3131b7694ec Binary files /dev/null and b/test/fixtures/element.line/tension/zero.png differ diff --git a/test/fixtures/element.point/point-style-circle.json b/test/fixtures/element.point/point-style-circle.json new file mode 100644 index 00000000000..b50e1146632 --- /dev/null +++ b/test/fixtures/element.point/point-style-circle.json @@ -0,0 +1,65 @@ +{ + "config": { + "type": "bubble", + "data": { + "datasets": [{ + "data": [ + {"x": 0, "y": 3, "r": 0}, + {"x": 1, "y": 3, "r": 2}, + {"x": 2, "y": 3, "r": 4}, + {"x": 3, "y": 3, "r": 8}, + {"x": 4, "y": 3, "r": 16}, + {"x": 5, "y": 3, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "transparent", + "borderWidth": 0 + }, { + "data": [ + {"x": 0, "y": 2, "r": 0}, + {"x": 1, "y": 2, "r": 2}, + {"x": 2, "y": 2, "r": 4}, + {"x": 3, "y": 2, "r": 8}, + {"x": 4, "y": 2, "r": 16}, + {"x": 5, "y": 2, "r": 32} + ], + "backgroundColor": "transparent", + "borderColor": "#0000ff", + "borderWidth": 1 + }, { + "data": [ + {"x": 0, "y": 1, "r": 0}, + {"x": 1, "y": 1, "r": 2}, + {"x": 2, "y": 1, "r": 4}, + {"x": 3, "y": 1, "r": 8}, + {"x": 4, "y": 1, "r": 16}, + {"x": 5, "y": 1, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "#0000ff", + "borderWidth": 2 + }] + }, + "options": { + "responsive": false, + "elements": { + "point": { + "pointStyle": "circle" + } + }, + "layout": { + "padding": 40 + }, + "scales": { + "x": {"display": false}, + "y": {"display": false} + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/element.point/point-style-circle.png b/test/fixtures/element.point/point-style-circle.png new file mode 100644 index 00000000000..d7b9bf531b2 Binary files /dev/null and b/test/fixtures/element.point/point-style-circle.png differ diff --git a/test/fixtures/element.point/point-style-cross-rot.json b/test/fixtures/element.point/point-style-cross-rot.json new file mode 100644 index 00000000000..f849a97731a --- /dev/null +++ b/test/fixtures/element.point/point-style-cross-rot.json @@ -0,0 +1,65 @@ +{ + "config": { + "type": "bubble", + "data": { + "datasets": [{ + "data": [ + {"x": 0, "y": 3, "r": 0}, + {"x": 1, "y": 3, "r": 2}, + {"x": 2, "y": 3, "r": 4}, + {"x": 3, "y": 3, "r": 8}, + {"x": 4, "y": 3, "r": 16}, + {"x": 5, "y": 3, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "transparent", + "borderWidth": 0 + }, { + "data": [ + {"x": 0, "y": 2, "r": 0}, + {"x": 1, "y": 2, "r": 2}, + {"x": 2, "y": 2, "r": 4}, + {"x": 3, "y": 2, "r": 8}, + {"x": 4, "y": 2, "r": 16}, + {"x": 5, "y": 2, "r": 32} + ], + "backgroundColor": "transparent", + "borderColor": "#0000ff", + "borderWidth": 1 + }, { + "data": [ + {"x": 0, "y": 1, "r": 0}, + {"x": 1, "y": 1, "r": 2}, + {"x": 2, "y": 1, "r": 4}, + {"x": 3, "y": 1, "r": 8}, + {"x": 4, "y": 1, "r": 16}, + {"x": 5, "y": 1, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "#0000ff", + "borderWidth": 2 + }] + }, + "options": { + "responsive": false, + "elements": { + "point": { + "pointStyle": "crossRot" + } + }, + "layout": { + "padding": 40 + }, + "scales": { + "x": {"display": false}, + "y": {"display": false} + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/element.point/point-style-cross-rot.png b/test/fixtures/element.point/point-style-cross-rot.png new file mode 100644 index 00000000000..3f6d1df091c Binary files /dev/null and b/test/fixtures/element.point/point-style-cross-rot.png differ diff --git a/test/fixtures/element.point/point-style-cross.json b/test/fixtures/element.point/point-style-cross.json new file mode 100644 index 00000000000..22686dd901d --- /dev/null +++ b/test/fixtures/element.point/point-style-cross.json @@ -0,0 +1,65 @@ +{ + "config": { + "type": "bubble", + "data": { + "datasets": [{ + "data": [ + {"x": 0, "y": 3, "r": 0}, + {"x": 1, "y": 3, "r": 2}, + {"x": 2, "y": 3, "r": 4}, + {"x": 3, "y": 3, "r": 8}, + {"x": 4, "y": 3, "r": 16}, + {"x": 5, "y": 3, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "transparent", + "borderWidth": 0 + }, { + "data": [ + {"x": 0, "y": 2, "r": 0}, + {"x": 1, "y": 2, "r": 2}, + {"x": 2, "y": 2, "r": 4}, + {"x": 3, "y": 2, "r": 8}, + {"x": 4, "y": 2, "r": 16}, + {"x": 5, "y": 2, "r": 32} + ], + "backgroundColor": "transparent", + "borderColor": "#0000ff", + "borderWidth": 1 + }, { + "data": [ + {"x": 0, "y": 1, "r": 0}, + {"x": 1, "y": 1, "r": 2}, + {"x": 2, "y": 1, "r": 4}, + {"x": 3, "y": 1, "r": 8}, + {"x": 4, "y": 1, "r": 16}, + {"x": 5, "y": 1, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "#0000ff", + "borderWidth": 2 + }] + }, + "options": { + "responsive": false, + "elements": { + "point": { + "pointStyle": "cross" + } + }, + "layout": { + "padding": 40 + }, + "scales": { + "x": {"display": false}, + "y": {"display": false} + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/element.point/point-style-cross.png b/test/fixtures/element.point/point-style-cross.png new file mode 100644 index 00000000000..ecf3cda77e8 Binary files /dev/null and b/test/fixtures/element.point/point-style-cross.png differ diff --git a/test/fixtures/element.point/point-style-dash.json b/test/fixtures/element.point/point-style-dash.json new file mode 100644 index 00000000000..f0ba523fb6d --- /dev/null +++ b/test/fixtures/element.point/point-style-dash.json @@ -0,0 +1,65 @@ +{ + "config": { + "type": "bubble", + "data": { + "datasets": [{ + "data": [ + {"x": 0, "y": 3, "r": 0}, + {"x": 1, "y": 3, "r": 2}, + {"x": 2, "y": 3, "r": 4}, + {"x": 3, "y": 3, "r": 8}, + {"x": 4, "y": 3, "r": 16}, + {"x": 5, "y": 3, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "transparent", + "borderWidth": 0 + }, { + "data": [ + {"x": 0, "y": 2, "r": 0}, + {"x": 1, "y": 2, "r": 2}, + {"x": 2, "y": 2, "r": 4}, + {"x": 3, "y": 2, "r": 8}, + {"x": 4, "y": 2, "r": 16}, + {"x": 5, "y": 2, "r": 32} + ], + "backgroundColor": "transparent", + "borderColor": "#0000ff", + "borderWidth": 1 + }, { + "data": [ + {"x": 0, "y": 1, "r": 0}, + {"x": 1, "y": 1, "r": 2}, + {"x": 2, "y": 1, "r": 4}, + {"x": 3, "y": 1, "r": 8}, + {"x": 4, "y": 1, "r": 16}, + {"x": 5, "y": 1, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "#0000ff", + "borderWidth": 2 + }] + }, + "options": { + "responsive": false, + "elements": { + "point": { + "pointStyle": "dash" + } + }, + "layout": { + "padding": 40 + }, + "scales": { + "x": {"display": false}, + "y": {"display": false} + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/element.point/point-style-dash.png b/test/fixtures/element.point/point-style-dash.png new file mode 100644 index 00000000000..9c381d83d54 Binary files /dev/null and b/test/fixtures/element.point/point-style-dash.png differ diff --git a/test/fixtures/element.point/point-style-image.js b/test/fixtures/element.point/point-style-image.js new file mode 100644 index 00000000000..54eddf0da45 --- /dev/null +++ b/test/fixtures/element.point/point-style-image.js @@ -0,0 +1,56 @@ +var imageCanvas = document.createElement('canvas'); +var imageContext = imageCanvas.getContext('2d'); + +imageCanvas.width = 40; +imageCanvas.height = 40; + +imageContext.fillStyle = '#f00'; +imageContext.beginPath(); +imageContext.moveTo(20, 0); +imageContext.lineTo(10, 40); +imageContext.lineTo(20, 30); +imageContext.closePath(); +imageContext.fill(); + +imageContext.fillStyle = '#a00'; +imageContext.beginPath(); +imageContext.moveTo(20, 0); +imageContext.lineTo(30, 40); +imageContext.lineTo(20, 30); +imageContext.closePath(); +imageContext.fill(); + +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5, 6, 7], + datasets: [{ + data: [0, 0, 0, 0, 0, 0, 0, 0], + showLine: false + }] + }, + options: { + responsive: false, + elements: { + point: { + pointStyle: imageCanvas, + rotation: [0, 45, 90, 135, 180, 225, 270, 315] + } + }, + layout: { + padding: 20 + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/element.point/point-style-image.png b/test/fixtures/element.point/point-style-image.png new file mode 100644 index 00000000000..eaa5e2d7c87 Binary files /dev/null and b/test/fixtures/element.point/point-style-image.png differ diff --git a/test/fixtures/element.point/point-style-line.json b/test/fixtures/element.point/point-style-line.json new file mode 100644 index 00000000000..4f2490a3f45 --- /dev/null +++ b/test/fixtures/element.point/point-style-line.json @@ -0,0 +1,65 @@ +{ + "config": { + "type": "bubble", + "data": { + "datasets": [{ + "data": [ + {"x": 0, "y": 3, "r": 0}, + {"x": 1, "y": 3, "r": 2}, + {"x": 2, "y": 3, "r": 4}, + {"x": 3, "y": 3, "r": 8}, + {"x": 4, "y": 3, "r": 16}, + {"x": 5, "y": 3, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "transparent", + "borderWidth": 0 + }, { + "data": [ + {"x": 0, "y": 2, "r": 0}, + {"x": 1, "y": 2, "r": 2}, + {"x": 2, "y": 2, "r": 4}, + {"x": 3, "y": 2, "r": 8}, + {"x": 4, "y": 2, "r": 16}, + {"x": 5, "y": 2, "r": 32} + ], + "backgroundColor": "transparent", + "borderColor": "#0000ff", + "borderWidth": 1 + }, { + "data": [ + {"x": 0, "y": 1, "r": 0}, + {"x": 1, "y": 1, "r": 2}, + {"x": 2, "y": 1, "r": 4}, + {"x": 3, "y": 1, "r": 8}, + {"x": 4, "y": 1, "r": 16}, + {"x": 5, "y": 1, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "#0000ff", + "borderWidth": 2 + }] + }, + "options": { + "responsive": false, + "elements": { + "point": { + "pointStyle": "line" + } + }, + "layout": { + "padding": 40 + }, + "scales": { + "x": {"display": false}, + "y": {"display": false} + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/element.point/point-style-line.png b/test/fixtures/element.point/point-style-line.png new file mode 100644 index 00000000000..2911fea9775 Binary files /dev/null and b/test/fixtures/element.point/point-style-line.png differ diff --git a/test/fixtures/element.point/point-style-rect-rot.json b/test/fixtures/element.point/point-style-rect-rot.json new file mode 100644 index 00000000000..56dc2584f4d --- /dev/null +++ b/test/fixtures/element.point/point-style-rect-rot.json @@ -0,0 +1,65 @@ +{ + "config": { + "type": "bubble", + "data": { + "datasets": [{ + "data": [ + {"x": 0, "y": 3, "r": 0}, + {"x": 1, "y": 3, "r": 2}, + {"x": 2, "y": 3, "r": 4}, + {"x": 3, "y": 3, "r": 8}, + {"x": 4, "y": 3, "r": 16}, + {"x": 5, "y": 3, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "transparent", + "borderWidth": 0 + }, { + "data": [ + {"x": 0, "y": 2, "r": 0}, + {"x": 1, "y": 2, "r": 2}, + {"x": 2, "y": 2, "r": 4}, + {"x": 3, "y": 2, "r": 8}, + {"x": 4, "y": 2, "r": 16}, + {"x": 5, "y": 2, "r": 32} + ], + "backgroundColor": "transparent", + "borderColor": "#0000ff", + "borderWidth": 1 + }, { + "data": [ + {"x": 0, "y": 1, "r": 0}, + {"x": 1, "y": 1, "r": 2}, + {"x": 2, "y": 1, "r": 4}, + {"x": 3, "y": 1, "r": 8}, + {"x": 4, "y": 1, "r": 16}, + {"x": 5, "y": 1, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "#0000ff", + "borderWidth": 2 + }] + }, + "options": { + "responsive": false, + "elements": { + "point": { + "pointStyle": "rectRot" + } + }, + "layout": { + "padding": 40 + }, + "scales": { + "x": {"display": false}, + "y": {"display": false} + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/element.point/point-style-rect-rot.png b/test/fixtures/element.point/point-style-rect-rot.png new file mode 100644 index 00000000000..a7c12885589 Binary files /dev/null and b/test/fixtures/element.point/point-style-rect-rot.png differ diff --git a/test/fixtures/element.point/point-style-rect-rounded.json b/test/fixtures/element.point/point-style-rect-rounded.json new file mode 100644 index 00000000000..ae7ef93f8c6 --- /dev/null +++ b/test/fixtures/element.point/point-style-rect-rounded.json @@ -0,0 +1,65 @@ +{ + "config": { + "type": "bubble", + "data": { + "datasets": [{ + "data": [ + {"x": 0, "y": 3, "r": 0}, + {"x": 1, "y": 3, "r": 2}, + {"x": 2, "y": 3, "r": 4}, + {"x": 3, "y": 3, "r": 8}, + {"x": 4, "y": 3, "r": 16}, + {"x": 5, "y": 3, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "transparent", + "borderWidth": 0 + }, { + "data": [ + {"x": 0, "y": 2, "r": 0}, + {"x": 1, "y": 2, "r": 2}, + {"x": 2, "y": 2, "r": 4}, + {"x": 3, "y": 2, "r": 8}, + {"x": 4, "y": 2, "r": 16}, + {"x": 5, "y": 2, "r": 32} + ], + "backgroundColor": "transparent", + "borderColor": "#0000ff", + "borderWidth": 1 + }, { + "data": [ + {"x": 0, "y": 1, "r": 0}, + {"x": 1, "y": 1, "r": 2}, + {"x": 2, "y": 1, "r": 4}, + {"x": 3, "y": 1, "r": 8}, + {"x": 4, "y": 1, "r": 16}, + {"x": 5, "y": 1, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "#0000ff", + "borderWidth": 2 + }] + }, + "options": { + "responsive": false, + "elements": { + "point": { + "pointStyle": "rectRounded" + } + }, + "layout": { + "padding": 40 + }, + "scales": { + "x": {"display": false}, + "y": {"display": false} + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/element.point/point-style-rect-rounded.png b/test/fixtures/element.point/point-style-rect-rounded.png new file mode 100644 index 00000000000..8b58b44303a Binary files /dev/null and b/test/fixtures/element.point/point-style-rect-rounded.png differ diff --git a/test/fixtures/element.point/point-style-rect.json b/test/fixtures/element.point/point-style-rect.json new file mode 100644 index 00000000000..2d97ee34a1b --- /dev/null +++ b/test/fixtures/element.point/point-style-rect.json @@ -0,0 +1,65 @@ +{ + "config": { + "type": "bubble", + "data": { + "datasets": [{ + "data": [ + {"x": 0, "y": 3, "r": 0}, + {"x": 1, "y": 3, "r": 2}, + {"x": 2, "y": 3, "r": 4}, + {"x": 3, "y": 3, "r": 8}, + {"x": 4, "y": 3, "r": 16}, + {"x": 5, "y": 3, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "transparent", + "borderWidth": 0 + }, { + "data": [ + {"x": 0, "y": 2, "r": 0}, + {"x": 1, "y": 2, "r": 2}, + {"x": 2, "y": 2, "r": 4}, + {"x": 3, "y": 2, "r": 8}, + {"x": 4, "y": 2, "r": 16}, + {"x": 5, "y": 2, "r": 32} + ], + "backgroundColor": "transparent", + "borderColor": "#0000ff", + "borderWidth": 1 + }, { + "data": [ + {"x": 0, "y": 1, "r": 0}, + {"x": 1, "y": 1, "r": 2}, + {"x": 2, "y": 1, "r": 4}, + {"x": 3, "y": 1, "r": 8}, + {"x": 4, "y": 1, "r": 16}, + {"x": 5, "y": 1, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "#0000ff", + "borderWidth": 2 + }] + }, + "options": { + "responsive": false, + "elements": { + "point": { + "pointStyle": "rect" + } + }, + "layout": { + "padding": 40 + }, + "scales": { + "x": {"display": false}, + "y": {"display": false} + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/element.point/point-style-rect.png b/test/fixtures/element.point/point-style-rect.png new file mode 100644 index 00000000000..493c9a69272 Binary files /dev/null and b/test/fixtures/element.point/point-style-rect.png differ diff --git a/test/fixtures/element.point/point-style-star.json b/test/fixtures/element.point/point-style-star.json new file mode 100644 index 00000000000..7996fd5b0cb --- /dev/null +++ b/test/fixtures/element.point/point-style-star.json @@ -0,0 +1,65 @@ +{ + "config": { + "type": "bubble", + "data": { + "datasets": [{ + "data": [ + {"x": 0, "y": 3, "r": 0}, + {"x": 1, "y": 3, "r": 2}, + {"x": 2, "y": 3, "r": 4}, + {"x": 3, "y": 3, "r": 8}, + {"x": 4, "y": 3, "r": 16}, + {"x": 5, "y": 3, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "transparent", + "borderWidth": 0 + }, { + "data": [ + {"x": 0, "y": 2, "r": 0}, + {"x": 1, "y": 2, "r": 2}, + {"x": 2, "y": 2, "r": 4}, + {"x": 3, "y": 2, "r": 8}, + {"x": 4, "y": 2, "r": 16}, + {"x": 5, "y": 2, "r": 32} + ], + "backgroundColor": "transparent", + "borderColor": "#0000ff", + "borderWidth": 1 + }, { + "data": [ + {"x": 0, "y": 1, "r": 0}, + {"x": 1, "y": 1, "r": 2}, + {"x": 2, "y": 1, "r": 4}, + {"x": 3, "y": 1, "r": 8}, + {"x": 4, "y": 1, "r": 16}, + {"x": 5, "y": 1, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "#0000ff", + "borderWidth": 2 + }] + }, + "options": { + "responsive": false, + "elements": { + "point": { + "pointStyle": "star" + } + }, + "layout": { + "padding": 40 + }, + "scales": { + "x": {"display": false}, + "y": {"display": false} + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/element.point/point-style-star.png b/test/fixtures/element.point/point-style-star.png new file mode 100644 index 00000000000..b5b58c0a486 Binary files /dev/null and b/test/fixtures/element.point/point-style-star.png differ diff --git a/test/fixtures/element.point/point-style-triangle.json b/test/fixtures/element.point/point-style-triangle.json new file mode 100644 index 00000000000..74692ee5a8b --- /dev/null +++ b/test/fixtures/element.point/point-style-triangle.json @@ -0,0 +1,65 @@ +{ + "config": { + "type": "bubble", + "data": { + "datasets": [{ + "data": [ + {"x": 0, "y": 3, "r": 0}, + {"x": 1, "y": 3, "r": 2}, + {"x": 2, "y": 3, "r": 4}, + {"x": 3, "y": 3, "r": 8}, + {"x": 4, "y": 3, "r": 16}, + {"x": 5, "y": 3, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "transparent", + "borderWidth": 0 + }, { + "data": [ + {"x": 0, "y": 2, "r": 0}, + {"x": 1, "y": 2, "r": 2}, + {"x": 2, "y": 2, "r": 4}, + {"x": 3, "y": 2, "r": 8}, + {"x": 4, "y": 2, "r": 16}, + {"x": 5, "y": 2, "r": 32} + ], + "backgroundColor": "transparent", + "borderColor": "#0000ff", + "borderWidth": 1 + }, { + "data": [ + {"x": 0, "y": 1, "r": 0}, + {"x": 1, "y": 1, "r": 2}, + {"x": 2, "y": 1, "r": 4}, + {"x": 3, "y": 1, "r": 8}, + {"x": 4, "y": 1, "r": 16}, + {"x": 5, "y": 1, "r": 32} + ], + "backgroundColor": "#00ff00", + "borderColor": "#0000ff", + "borderWidth": 2 + }] + }, + "options": { + "responsive": false, + "elements": { + "point": { + "pointStyle": "triangle" + } + }, + "layout": { + "padding": 40 + }, + "scales": { + "x": {"display": false}, + "y": {"display": false} + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/element.point/point-style-triangle.png b/test/fixtures/element.point/point-style-triangle.png new file mode 100644 index 00000000000..d2cc5c6f1e8 Binary files /dev/null and b/test/fixtures/element.point/point-style-triangle.png differ diff --git a/test/fixtures/element.point/rotation.js b/test/fixtures/element.point/rotation.js new file mode 100644 index 00000000000..e5fffba12eb --- /dev/null +++ b/test/fixtures/element.point/rotation.js @@ -0,0 +1,54 @@ +var gradient; + +var datasets = ['circle', 'cross', 'crossRot', 'dash', 'line', 'rect', 'rectRounded', 'rectRot', 'star', 'triangle', false].map(function(style, y) { + return { + pointStyle: style, + data: Array.apply(null, Array(17)).map(function(v, x) { + return {x: x, y: 11 - y}; + }) + }; +}); + +var angles = Array.apply(null, Array(17)).map(function(v, i) { + return -180 + i * 22.5; +}); + +module.exports = { + config: { + type: 'bubble', + data: { + datasets: datasets + }, + options: { + responsive: false, + elements: { + point: { + rotation: angles, + radius: 10, + backgroundColor: function(context) { + if (!gradient) { + gradient = context.chart.ctx.createLinearGradient(0, 0, 512, 256); + gradient.addColorStop(0, '#ff0000'); + gradient.addColorStop(1, '#0000ff'); + } + return gradient; + }, + borderColor: '#cccccc' + } + }, + layout: { + padding: 20 + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/element.point/rotation.png b/test/fixtures/element.point/rotation.png new file mode 100644 index 00000000000..11424fffbef Binary files /dev/null and b/test/fixtures/element.point/rotation.png differ diff --git a/test/fixtures/mixed/bar+line-stacked.js b/test/fixtures/mixed/bar+line-stacked.js new file mode 100644 index 00000000000..17f28aa0ba1 --- /dev/null +++ b/test/fixtures/mixed/bar+line-stacked.js @@ -0,0 +1,40 @@ +module.exports = { + config: { + data: { + datasets: [ + { + type: 'bar', + stack: 'mixed', + data: [5, 20, 1, 10], + backgroundColor: '#00ff00', + borderColor: '#ff0000' + }, + { + type: 'line', + stack: 'mixed', + data: [6, 16, 3, 19], + borderColor: '#0000ff', + fill: false + }, + ] + }, + options: { + scales: { + x: { + axis: 'y', + labels: ['a', 'b', 'c', 'd'] + }, + y: { + stacked: true + } + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/mixed/bar+line-stacked.png b/test/fixtures/mixed/bar+line-stacked.png new file mode 100644 index 00000000000..4768675e3da Binary files /dev/null and b/test/fixtures/mixed/bar+line-stacked.png differ diff --git a/test/fixtures/mixed/bar+line.js b/test/fixtures/mixed/bar+line.js new file mode 100644 index 00000000000..ca6e81548da --- /dev/null +++ b/test/fixtures/mixed/bar+line.js @@ -0,0 +1,39 @@ +module.exports = { + config: { + data: { + datasets: [ + { + type: 'line', + data: [6, 16, 3, 19], + borderColor: '#0000ff', + fill: false + }, + { + type: 'bar', + data: [5, 20, 1, 10], + backgroundColor: '#00ff00', + borderColor: '#ff0000' + } + ] + }, + options: { + indexAxis: 'y', + scales: { + x: { + position: 'top' + }, + y: { + axis: 'y', + labels: ['a', 'b', 'c', 'd'] + } + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/mixed/bar+line.png b/test/fixtures/mixed/bar+line.png new file mode 100644 index 00000000000..921a510fd38 Binary files /dev/null and b/test/fixtures/mixed/bar+line.png differ diff --git a/test/fixtures/plugin.colors/bar.js b/test/fixtures/plugin.colors/bar.js new file mode 100644 index 00000000000..0ca99e3b970 --- /dev/null +++ b/test/fixtures/plugin.colors/bar.js @@ -0,0 +1,36 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + data: [0, 5, 10, null, -10, -5], + }, + { + data: [10, 2, 3, null, 10, 5] + } + ] + }, + options: { + scales: { + x: { + ticks: { + display: false, + } + }, + y: { + ticks: { + display: false, + } + } + }, + plugins: { + legend: false, + colors: { + enabled: true + } + } + } + } +}; diff --git a/test/fixtures/plugin.colors/bar.png b/test/fixtures/plugin.colors/bar.png new file mode 100644 index 00000000000..72c10f05ddd Binary files /dev/null and b/test/fixtures/plugin.colors/bar.png differ diff --git a/test/fixtures/plugin.colors/bubble.js b/test/fixtures/plugin.colors/bubble.js new file mode 100644 index 00000000000..3e7c25a8192 --- /dev/null +++ b/test/fixtures/plugin.colors/bubble.js @@ -0,0 +1,32 @@ +module.exports = { + config: { + type: 'bubble', + data: { + datasets: [{ + data: [{x: 12, y: 54, r: 22.4}] + }, { + data: [{x: 18, y: 38, r: 25}] + }] + }, + options: { + scales: { + x: { + ticks: { + display: false, + } + }, + y: { + ticks: { + display: false, + } + } + }, + plugins: { + legend: false, + colors: { + enabled: true + } + } + } + } +}; diff --git a/test/fixtures/plugin.colors/bubble.png b/test/fixtures/plugin.colors/bubble.png new file mode 100644 index 00000000000..539eed1cc7c Binary files /dev/null and b/test/fixtures/plugin.colors/bubble.png differ diff --git a/test/fixtures/plugin.colors/chart-options-colors.js b/test/fixtures/plugin.colors/chart-options-colors.js new file mode 100644 index 00000000000..5add0c25883 --- /dev/null +++ b/test/fixtures/plugin.colors/chart-options-colors.js @@ -0,0 +1,37 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + data: [0, 5, 10, null, -10, -5], + }, + { + data: [10, 2, 3, null, 10, 5] + } + ] + }, + options: { + backgroundColor: ['red', 'green'], + scales: { + x: { + ticks: { + display: false, + } + }, + y: { + ticks: { + display: false, + } + } + }, + plugins: { + legend: false, + colors: { + enabled: true + } + } + } + } +}; diff --git a/test/fixtures/plugin.colors/chart-options-colors.png b/test/fixtures/plugin.colors/chart-options-colors.png new file mode 100644 index 00000000000..5b24310ed0b Binary files /dev/null and b/test/fixtures/plugin.colors/chart-options-colors.png differ diff --git a/test/fixtures/plugin.colors/doughnut.js b/test/fixtures/plugin.colors/doughnut.js new file mode 100644 index 00000000000..077ad624a6f --- /dev/null +++ b/test/fixtures/plugin.colors/doughnut.js @@ -0,0 +1,23 @@ +module.exports = { + config: { + type: 'doughnut', + data: { + datasets: [ + { + data: [0, 2, 4, null, 6, 8] + }, + { + data: [5, 1, 6, 2, null, 9] + } + ] + }, + options: { + plugins: { + legend: false, + colors: { + enabled: true + } + } + } + } +}; diff --git a/test/fixtures/plugin.colors/doughnut.png b/test/fixtures/plugin.colors/doughnut.png new file mode 100644 index 00000000000..d6c28c6c114 Binary files /dev/null and b/test/fixtures/plugin.colors/doughnut.png differ diff --git a/test/fixtures/plugin.colors/dynamic-datasets-default.js b/test/fixtures/plugin.colors/dynamic-datasets-default.js new file mode 100644 index 00000000000..9969d3e2a8e --- /dev/null +++ b/test/fixtures/plugin.colors/dynamic-datasets-default.js @@ -0,0 +1,42 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + data: [5, 5, 5, 5, 5, 5] + } + ] + }, + options: { + scales: { + x: { + ticks: { + display: false, + } + }, + y: { + ticks: { + display: false, + } + } + }, + plugins: { + legend: false, + colors: { + enabled: true + } + } + } + }, + options: { + run(chart) { + chart.data.datasets.push({ + data: [5, 5, 5, 5, 5, 5] + }); + + chart.update(); + } + } +}; diff --git a/test/fixtures/plugin.colors/dynamic-datasets-default.png b/test/fixtures/plugin.colors/dynamic-datasets-default.png new file mode 100644 index 00000000000..38f281077b3 Binary files /dev/null and b/test/fixtures/plugin.colors/dynamic-datasets-default.png differ diff --git a/test/fixtures/plugin.colors/dynamic-datasets-force-override.js b/test/fixtures/plugin.colors/dynamic-datasets-force-override.js new file mode 100644 index 00000000000..404d63068dc --- /dev/null +++ b/test/fixtures/plugin.colors/dynamic-datasets-force-override.js @@ -0,0 +1,43 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + data: [5, 5, 5, 5, 5, 5] + } + ] + }, + options: { + scales: { + x: { + ticks: { + display: false + } + }, + y: { + ticks: { + display: false + } + } + }, + plugins: { + legend: false, + colors: { + enabled: true, + forceOverride: true + } + } + } + }, + options: { + run(chart) { + chart.data.datasets.push({ + data: [5, 5, 5, 5, 5, 5] + }); + + chart.update(); + } + } +}; diff --git a/test/fixtures/plugin.colors/dynamic-datasets-force-override.png b/test/fixtures/plugin.colors/dynamic-datasets-force-override.png new file mode 100644 index 00000000000..58121e43f69 Binary files /dev/null and b/test/fixtures/plugin.colors/dynamic-datasets-force-override.png differ diff --git a/test/fixtures/plugin.colors/line.js b/test/fixtures/plugin.colors/line.js new file mode 100644 index 00000000000..9658956a495 --- /dev/null +++ b/test/fixtures/plugin.colors/line.js @@ -0,0 +1,36 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + data: [0, 5, 10, null, -10, -5], + }, + { + data: [10, 2, 3, null, 10, 5] + } + ] + }, + options: { + scales: { + x: { + ticks: { + display: false, + } + }, + y: { + ticks: { + display: false, + } + } + }, + plugins: { + legend: false, + colors: { + enabled: true + } + } + } + } +}; diff --git a/test/fixtures/plugin.colors/line.png b/test/fixtures/plugin.colors/line.png new file mode 100644 index 00000000000..b9ce7585ec8 Binary files /dev/null and b/test/fixtures/plugin.colors/line.png differ diff --git a/test/fixtures/plugin.colors/mixed.js b/test/fixtures/plugin.colors/mixed.js new file mode 100644 index 00000000000..2ea5b83b910 --- /dev/null +++ b/test/fixtures/plugin.colors/mixed.js @@ -0,0 +1,41 @@ +module.exports = { + config: { + data: { + labels: [0, 1, 2, 3], + datasets: [ + { + type: 'line', + data: [5, 20, 1, 10], + }, + { + type: 'bar', + data: [6, 16, 3, 19] + }, + { + type: 'pie', + data: [5, 20, 1, 10], + } + ] + }, + options: { + scales: { + x: { + ticks: { + display: false, + } + }, + y: { + ticks: { + display: false, + } + } + }, + plugins: { + legend: false, + colors: { + enabled: true + } + } + } + } +}; diff --git a/test/fixtures/plugin.colors/mixed.png b/test/fixtures/plugin.colors/mixed.png new file mode 100644 index 00000000000..d507dd0f844 Binary files /dev/null and b/test/fixtures/plugin.colors/mixed.png differ diff --git a/test/fixtures/plugin.colors/pie.js b/test/fixtures/plugin.colors/pie.js new file mode 100644 index 00000000000..ef58bc14da1 --- /dev/null +++ b/test/fixtures/plugin.colors/pie.js @@ -0,0 +1,23 @@ +module.exports = { + config: { + type: 'pie', + data: { + datasets: [ + { + data: [0, 2, 4, null, 6, 8] + }, + { + data: [5, 1, 6, 2, null, 9] + } + ] + }, + options: { + plugins: { + legend: false, + colors: { + enabled: true + } + } + } + } +}; diff --git a/test/fixtures/plugin.colors/pie.png b/test/fixtures/plugin.colors/pie.png new file mode 100644 index 00000000000..c92b35b05be Binary files /dev/null and b/test/fixtures/plugin.colors/pie.png differ diff --git a/test/fixtures/plugin.colors/polarArea.js b/test/fixtures/plugin.colors/polarArea.js new file mode 100644 index 00000000000..37e4d4af3a4 --- /dev/null +++ b/test/fixtures/plugin.colors/polarArea.js @@ -0,0 +1,28 @@ +module.exports = { + config: { + type: 'polarArea', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + data: [0, 2, 4, null, 6, 8] + } + ] + }, + options: { + scales: { + r: { + ticks: { + display: false + } + } + }, + plugins: { + legend: false, + colors: { + enabled: true + } + } + } + } +}; diff --git a/test/fixtures/plugin.colors/polarArea.png b/test/fixtures/plugin.colors/polarArea.png new file mode 100644 index 00000000000..8671ff67aaa Binary files /dev/null and b/test/fixtures/plugin.colors/polarArea.png differ diff --git a/test/fixtures/plugin.colors/radar.js b/test/fixtures/plugin.colors/radar.js new file mode 100644 index 00000000000..d5fd4318ec7 --- /dev/null +++ b/test/fixtures/plugin.colors/radar.js @@ -0,0 +1,34 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + data: [0, 5, 10, null, -10, -5] + }, + { + data: [4, -5, -10, null, 10, 5] + } + ] + }, + options: { + scales: { + r: { + ticks: { + display: false + }, + pointLabels: { + display: false, + } + } + }, + plugins: { + legend: false, + colors: { + enabled: true + } + } + } + } +}; diff --git a/test/fixtures/plugin.colors/radar.png b/test/fixtures/plugin.colors/radar.png new file mode 100644 index 00000000000..5a39a08ebe9 Binary files /dev/null and b/test/fixtures/plugin.colors/radar.png differ diff --git a/test/fixtures/plugin.colors/scatter.js b/test/fixtures/plugin.colors/scatter.js new file mode 100644 index 00000000000..8822de1d736 --- /dev/null +++ b/test/fixtures/plugin.colors/scatter.js @@ -0,0 +1,37 @@ +module.exports = { + config: { + type: 'scatter', + data: { + datasets: [{ + data: [{x: 10, y: 15}, {x: 15, y: 10}], + pointRadius: 10, + showLine: true, + label: 'dataset1' + }, { + data: [{x: 20, y: 45}, {x: 5, y: 15}], + pointRadius: 20, + label: 'dataset2' + }], + }, + options: { + scales: { + x: { + ticks: { + display: false, + } + }, + y: { + ticks: { + display: false, + } + } + }, + plugins: { + legend: false, + colors: { + enabled: true + }, + } + } + } +}; diff --git a/test/fixtures/plugin.colors/scatter.png b/test/fixtures/plugin.colors/scatter.png new file mode 100644 index 00000000000..12534c720b2 Binary files /dev/null and b/test/fixtures/plugin.colors/scatter.png differ diff --git a/test/fixtures/plugin.filler/line/above-below-vertical-linechart.js b/test/fixtures/plugin.filler/line/above-below-vertical-linechart.js new file mode 100644 index 00000000000..82ada178f01 --- /dev/null +++ b/test/fixtures/plugin.filler/line/above-below-vertical-linechart.js @@ -0,0 +1,46 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: [1, 2, 3, 4], + datasets: [ + { + data: [200, 400, 200, 400], + cubicInterpolationMode: 'monotone', + tension: 0.4, + spanGaps: true, + borderColor: 'blue', + pointRadius: 0, + fill: { + target: 1, + below: 'rgba(255, 0, 0, 0.4)', + above: 'rgba(53, 221, 53, 0.4)', + } + }, + { + data: [400, 200, 400, 200], + cubicInterpolationMode: 'monotone', + tension: 0.4, + spanGaps: true, + borderColor: 'orange', + pointRadius: 0, + }, + ] + }, + options: { + indexAxis: 'y', + // maintainAspectRatio: false, + plugins: { + filler: { + propagate: false + }, + datalabels: { + display: false + }, + legend: { + display: false + }, + } + } + } +}; diff --git a/test/fixtures/plugin.filler/line/above-below-vertical-linechart.png b/test/fixtures/plugin.filler/line/above-below-vertical-linechart.png new file mode 100644 index 00000000000..2052737e792 Binary files /dev/null and b/test/fixtures/plugin.filler/line/above-below-vertical-linechart.png differ diff --git a/test/fixtures/plugin.filler/line/before-dataset-draw.js b/test/fixtures/plugin.filler/line/before-dataset-draw.js new file mode 100644 index 00000000000..3c89a45f60c --- /dev/null +++ b/test/fixtures/plugin.filler/line/before-dataset-draw.js @@ -0,0 +1,41 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: ['15:00', '16:00', '17:00', '18:00', '19:00', '20:00'], + datasets: [ + { + borderColor: '#00ADEE80', + backgroundColor: '#00ADEE', + data: [0, 1, 1, 2, 2, 0], + }, + { + borderColor: '#BD262880', + backgroundColor: '#BD2628', + data: [0, 2, 2, 1, 1, 1], + } + ] + }, + options: { + borderWidth: 4, + fill: true, + radius: 20, + pointBackgroundColor: '#ffff', + cubicInterpolationMode: 'monotone', + plugins: { + legend: false, + filler: { + drawTime: 'beforeDatasetDraw' + } + }, + scales: { + x: { + display: false, + }, + y: { + display: false + } + } + } + } +}; diff --git a/test/fixtures/plugin.filler/line/before-dataset-draw.png b/test/fixtures/plugin.filler/line/before-dataset-draw.png new file mode 100644 index 00000000000..6ed2fe4c076 Binary files /dev/null and b/test/fixtures/plugin.filler/line/before-dataset-draw.png differ diff --git a/test/fixtures/plugin.filler/line/before-datasets-draw.js b/test/fixtures/plugin.filler/line/before-datasets-draw.js new file mode 100644 index 00000000000..3b06f3fbd4a --- /dev/null +++ b/test/fixtures/plugin.filler/line/before-datasets-draw.js @@ -0,0 +1,41 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: ['15:00', '16:00', '17:00', '18:00', '19:00', '20:00'], + datasets: [ + { + borderColor: '#00ADEE80', + backgroundColor: '#00ADEE', + data: [0, 1, 1, 2, 2, 0], + }, + { + borderColor: '#BD262880', + backgroundColor: '#BD2628', + data: [0, 2, 2, 1, 1, 1], + } + ] + }, + options: { + borderWidth: 4, + fill: true, + radius: 20, + pointBackgroundColor: '#ffff', + cubicInterpolationMode: 'monotone', + plugins: { + legend: false, + filler: { + drawTime: 'beforeDatasetsDraw' + } + }, + scales: { + x: { + display: false, + }, + y: { + display: false + } + } + } + } +}; diff --git a/test/fixtures/plugin.filler/line/before-datasets-draw.png b/test/fixtures/plugin.filler/line/before-datasets-draw.png new file mode 100644 index 00000000000..54c9517b85e Binary files /dev/null and b/test/fixtures/plugin.filler/line/before-datasets-draw.png differ diff --git a/test/fixtures/plugin.filler/line/boundary/above-below-line-null-start.json b/test/fixtures/plugin.filler/line/boundary/above-below-line-null-start.json new file mode 100644 index 00000000000..e0bce4bce0c --- /dev/null +++ b/test/fixtures/plugin.filler/line/boundary/above-below-line-null-start.json @@ -0,0 +1,53 @@ +{ + "config": { + "type": "line", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13"], + "datasets": [{ + "borderColor": "rgb(42, 90, 145)", + "data": [null, 12, 30, 36, 45, 53, 68, 79, null, 95, 18, 18, 180], + "fill": { + "target": "+1", + "above": "rgba(4, 142, 43, 0.5)", + "below": "rgba(241, 49, 34, 0.5)" + } + }, { + "borderColor": "#00ADEE", + "data": [null, 0, 0, 0, 0, 0, 20, 108, null, 72, 72, 72, 72], + "fill": false + }] + }, + "options": { + "responsive": false, + "spanGaps": false, + "scales": { + "x": { + "display": false + }, + "y": { + "display": false + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "cubicInterpolationMode": "monotone", + "borderColor": "transparent" + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.filler/line/boundary/above-below-line-null-start.png b/test/fixtures/plugin.filler/line/boundary/above-below-line-null-start.png new file mode 100644 index 00000000000..5414dc9bd93 Binary files /dev/null and b/test/fixtures/plugin.filler/line/boundary/above-below-line-null-start.png differ diff --git a/test/fixtures/plugin.filler/line/boundary/above-below-line-null.json b/test/fixtures/plugin.filler/line/boundary/above-below-line-null.json new file mode 100644 index 00000000000..405542e149f --- /dev/null +++ b/test/fixtures/plugin.filler/line/boundary/above-below-line-null.json @@ -0,0 +1,53 @@ +{ + "config": { + "type": "line", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13"], + "datasets": [{ + "borderColor": "rgb(42, 90, 145)", + "data": [4, 12, 30, 36, 45, 53, 68, 79, null, 95, 18, null, 18, 180], + "fill": { + "target": "+1", + "above": "rgba(4, 142, 43, 0.5)", + "below": "rgba(241, 49, 34, 0.5)" + } + }, { + "borderColor": "#00ADEE", + "data": [0, 0, 0, 0, 0, 0, 20, 108, null, 72, 72, null, 72, 72], + "fill": false + }] + }, + "options": { + "responsive": false, + "spanGaps": false, + "scales": { + "x": { + "display": false + }, + "y": { + "display": false + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "cubicInterpolationMode": "monotone", + "borderColor": "transparent" + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.filler/line/boundary/above-below-line-null.png b/test/fixtures/plugin.filler/line/boundary/above-below-line-null.png new file mode 100644 index 00000000000..7118324e73c Binary files /dev/null and b/test/fixtures/plugin.filler/line/boundary/above-below-line-null.png differ diff --git a/test/fixtures/plugin.filler/line/boundary/end-span.json b/test/fixtures/plugin.filler/line/boundary/end-span.json new file mode 100644 index 00000000000..4f806cdb564 --- /dev/null +++ b/test/fixtures/plugin.filler/line/boundary/end-span.json @@ -0,0 +1,54 @@ +{ + "config": { + "type": "line", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [{ + "backgroundColor": "rgba(0, 0, 192, 0.25)", + "data": [null, null, 2, 3, 4, -4, -2, 1, 0] + }, { + "backgroundColor": "rgba(0, 192, 0, 0.25)", + "data": [5.5, 2, null, 4, 5, null, null, 2, 1] + }, { + "backgroundColor": "rgba(192, 0, 0, 0.25)", + "data": [7, 3, 4, 5, 6, 1, 4, null, null] + }, { + "backgroundColor": "rgba(0, 0, 192, 0.25)", + "data": [8, 7, 6.5, -6, -4, -6, 4, 5, 8] + }] + }, + "options": { + "responsive": false, + "spanGaps": true, + "scales": { + "x": { + "display": false + }, + "y": { + "display": false + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "borderColor": "transparent", + "fill": "end", + "tension": 0 + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.filler/line/boundary/end-span.png b/test/fixtures/plugin.filler/line/boundary/end-span.png new file mode 100644 index 00000000000..0939727d078 Binary files /dev/null and b/test/fixtures/plugin.filler/line/boundary/end-span.png differ diff --git a/test/fixtures/plugin.filler/line/boundary/end.json b/test/fixtures/plugin.filler/line/boundary/end.json new file mode 100644 index 00000000000..08cce0822ba --- /dev/null +++ b/test/fixtures/plugin.filler/line/boundary/end.json @@ -0,0 +1,54 @@ +{ + "config": { + "type": "line", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [{ + "backgroundColor": "rgba(0, 0, 192, 0.25)", + "data": [null, null, 2, 3, 4, -4, -2, 1, 0] + }, { + "backgroundColor": "rgba(0, 192, 0, 0.25)", + "data": [5.5, 2, null, 4, 5, null, null, 2, 1] + }, { + "backgroundColor": "rgba(192, 0, 0, 0.25)", + "data": [7, 3, 4, 5, 6, 1, 4, null, null] + }, { + "backgroundColor": "rgba(0, 0, 192, 0.25)", + "data": [8, 7, 6.5, -6, -4, -6, 4, 5, 8] + }] + }, + "options": { + "responsive": false, + "spanGaps": false, + "scales": { + "x": { + "display": false + }, + "y": { + "display": false + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "borderColor": "transparent", + "fill": "end", + "tension": 0 + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.filler/line/boundary/end.png b/test/fixtures/plugin.filler/line/boundary/end.png new file mode 100644 index 00000000000..fb3011b4209 Binary files /dev/null and b/test/fixtures/plugin.filler/line/boundary/end.png differ diff --git a/test/fixtures/plugin.filler/line/boundary/origin-span-dual.json b/test/fixtures/plugin.filler/line/boundary/origin-span-dual.json new file mode 100644 index 00000000000..599fced3b24 --- /dev/null +++ b/test/fixtures/plugin.filler/line/boundary/origin-span-dual.json @@ -0,0 +1,57 @@ +{ + "config": { + "type": "line", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [{ + "backgroundColor": "rgba(0, 0, 192, 0.25)", + "data": [null, null, 2, 3, 4, -4, -2, 1, 0] + }, { + "backgroundColor": "rgba(0, 192, 0, 0.25)", + "data": [6, 2, null, 4, 5, null, null, 2, 1] + }, { + "backgroundColor": "rgba(192, 0, 0, 0.25)", + "data": [7, 3, 4, 5, 6, 1, 4, null, null] + }, { + "backgroundColor": "rgba(0, 64, 192, 0.25)", + "data": [8, 7, 6, -6, -4, -6, 4, 5, 8] + }] + }, + "options": { + "responsive": false, + "spanGaps": true, + "scales": { + "x": { + "display": false + }, + "y": { + "display": false + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "borderColor": "transparent", + "fill": { + "target": "origin", + "below": "rgba(255, 0, 0, 0.25)" + }, + "tension": 0 + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.filler/line/boundary/origin-span-dual.png b/test/fixtures/plugin.filler/line/boundary/origin-span-dual.png new file mode 100644 index 00000000000..eeecfcc1fc5 Binary files /dev/null and b/test/fixtures/plugin.filler/line/boundary/origin-span-dual.png differ diff --git a/test/fixtures/plugin.filler/line/boundary/origin-span.json b/test/fixtures/plugin.filler/line/boundary/origin-span.json new file mode 100644 index 00000000000..967d5c46381 --- /dev/null +++ b/test/fixtures/plugin.filler/line/boundary/origin-span.json @@ -0,0 +1,54 @@ +{ + "config": { + "type": "line", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [{ + "backgroundColor": "rgba(0, 0, 192, 0.25)", + "data": [null, null, 2, 3, 4, -4, -2, 1, 0] + }, { + "backgroundColor": "rgba(0, 192, 0, 0.25)", + "data": [6, 2, null, 4, 5, null, null, 2, 1] + }, { + "backgroundColor": "rgba(192, 0, 0, 0.25)", + "data": [7, 3, 4, 5, 6, 1, 4, null, null] + }, { + "backgroundColor": "rgba(0, 64, 192, 0.25)", + "data": [8, 7, 6, -6, -4, -6, 4, 5, 8] + }] + }, + "options": { + "responsive": false, + "spanGaps": true, + "scales": { + "x": { + "display": false + }, + "y": { + "display": false + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "borderColor": "transparent", + "fill": "origin", + "tension": 0 + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.filler/line/boundary/origin-span.png b/test/fixtures/plugin.filler/line/boundary/origin-span.png new file mode 100644 index 00000000000..24ccc2490da Binary files /dev/null and b/test/fixtures/plugin.filler/line/boundary/origin-span.png differ diff --git a/test/fixtures/plugin.filler/line/boundary/origin-spline-above.json b/test/fixtures/plugin.filler/line/boundary/origin-spline-above.json new file mode 100644 index 00000000000..a67e9bee29a --- /dev/null +++ b/test/fixtures/plugin.filler/line/boundary/origin-spline-above.json @@ -0,0 +1,57 @@ +{ + "config": { + "type": "line", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [{ + "backgroundColor": "rgba(0, 0, 192, 0.25)", + "data": [null, null, 2, 4, 2, 1, -1, 1, 2] + }, { + "backgroundColor": "rgba(0, 192, 0, 0.25)", + "data": [4, 2, null, 3, 2.5, null, -2, 1.5, 3] + }, { + "backgroundColor": "rgba(192, 0, 0, 0.25)", + "data": [3.5, 2, 1, 2.5, -2, 3, -1, null, null] + }, { + "backgroundColor": "rgba(128, 0, 128, 0.25)", + "data": [5, 6, 5, -2, -4, -3, 4, 2, 4.5] + }] + }, + "options": { + "responsive": false, + "spanGaps": false, + "scales": { + "x": { + "display": false + }, + "y": { + "display": false + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "cubicInterpolationMode": "monotone", + "borderColor": "transparent", + "fill": { + "target": "origin", + "below": "transparent" + } + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.filler/line/boundary/origin-spline-above.png b/test/fixtures/plugin.filler/line/boundary/origin-spline-above.png new file mode 100644 index 00000000000..2d875467fb6 Binary files /dev/null and b/test/fixtures/plugin.filler/line/boundary/origin-spline-above.png differ diff --git a/test/fixtures/plugin.filler/line/boundary/origin-spline-span.json b/test/fixtures/plugin.filler/line/boundary/origin-spline-span.json new file mode 100644 index 00000000000..893edc08abe --- /dev/null +++ b/test/fixtures/plugin.filler/line/boundary/origin-spline-span.json @@ -0,0 +1,54 @@ +{ + "config": { + "type": "line", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [{ + "backgroundColor": "rgba(0, 0, 192, 0.25)", + "data": [null, null, 2, 4, 2, 1, -1, 1, 2] + }, { + "backgroundColor": "rgba(0, 192, 0, 0.25)", + "data": [4, 2, null, 3, 2.5, null, -2, 1.5, 3] + }, { + "backgroundColor": "rgba(192, 0, 0, 0.25)", + "data": [3.5, 2, 1, 2.5, -2, 3, -1, null, null] + }, { + "backgroundColor": "rgba(128, 0, 128, 0.25)", + "data": [5, 6, 5, -2, -4, -3, 4, 2, 4.5] + }] + }, + "options": { + "responsive": false, + "spanGaps": true, + "scales": { + "x": { + "display": false + }, + "y": { + "display": false + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "cubicInterpolationMode": "monotone", + "borderColor": "transparent", + "fill": "origin" + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.filler/line/boundary/origin-spline-span.png b/test/fixtures/plugin.filler/line/boundary/origin-spline-span.png new file mode 100644 index 00000000000..fd54df60cca Binary files /dev/null and b/test/fixtures/plugin.filler/line/boundary/origin-spline-span.png differ diff --git a/test/fixtures/plugin.filler/line/boundary/origin-spline.json b/test/fixtures/plugin.filler/line/boundary/origin-spline.json new file mode 100644 index 00000000000..1dd7757d437 --- /dev/null +++ b/test/fixtures/plugin.filler/line/boundary/origin-spline.json @@ -0,0 +1,54 @@ +{ + "config": { + "type": "line", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [{ + "backgroundColor": "rgba(0, 0, 192, 0.25)", + "data": [null, null, 2, 4, 2, 1, -1, 1, 2] + }, { + "backgroundColor": "rgba(0, 192, 0, 0.25)", + "data": [4, 2, null, 3, 2.5, null, -2, 1.5, 3] + }, { + "backgroundColor": "rgba(192, 0, 0, 0.25)", + "data": [3.5, 2, 1, 2.5, -2, 3, -1, null, null] + }, { + "backgroundColor": "rgba(128, 0, 128, 0.25)", + "data": [5, 6, 5, -2, -4, -3, 4, 2, 4.5] + }] + }, + "options": { + "responsive": false, + "spanGaps": false, + "scales": { + "x": { + "display": false + }, + "y": { + "display": false + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "cubicInterpolationMode": "monotone", + "borderColor": "transparent", + "fill": "origin" + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.filler/line/boundary/origin-spline.png b/test/fixtures/plugin.filler/line/boundary/origin-spline.png new file mode 100644 index 00000000000..94035713e56 Binary files /dev/null and b/test/fixtures/plugin.filler/line/boundary/origin-spline.png differ diff --git a/test/fixtures/plugin.filler/line/boundary/origin-stepped-span.json b/test/fixtures/plugin.filler/line/boundary/origin-stepped-span.json new file mode 100644 index 00000000000..2cf37caaf85 --- /dev/null +++ b/test/fixtures/plugin.filler/line/boundary/origin-stepped-span.json @@ -0,0 +1,55 @@ +{ + "config": { + "type": "line", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [{ + "backgroundColor": "rgba(0, 0, 192, 0.25)", + "data": [null, null, 2, 4, 2, 1, -1, 1, 2] + }, { + "backgroundColor": "rgba(0, 192, 0, 0.25)", + "data": [4, 2, null, 3, 2.5, null, -2, 1.5, 3] + }, { + "backgroundColor": "rgba(192, 0, 0, 0.25)", + "data": [3.5, 2, 1, 2.5, -2, 3, -1, null, null] + }, { + "backgroundColor": "rgba(128, 0, 128, 0.25)", + "data": [5, 6, 5, -2, -4, -3, 4, 2, 4.5] + }] + }, + "options": { + "responsive": false, + "spanGaps": true, + "scales": { + "x": { + "display": false + }, + "y": { + "display": false + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "cubicInterpolationMode": "monotone", + "borderColor": "transparent", + "stepped": true, + "fill": "origin" + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.filler/line/boundary/origin-stepped-span.png b/test/fixtures/plugin.filler/line/boundary/origin-stepped-span.png new file mode 100644 index 00000000000..524a8fb387f Binary files /dev/null and b/test/fixtures/plugin.filler/line/boundary/origin-stepped-span.png differ diff --git a/test/fixtures/plugin.filler/line/boundary/origin-stepped.json b/test/fixtures/plugin.filler/line/boundary/origin-stepped.json new file mode 100644 index 00000000000..8b8c91fe027 --- /dev/null +++ b/test/fixtures/plugin.filler/line/boundary/origin-stepped.json @@ -0,0 +1,55 @@ +{ + "config": { + "type": "line", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [{ + "backgroundColor": "rgba(0, 0, 192, 0.25)", + "data": [null, null, 2, 4, 2, 1, -1, 1, 2] + }, { + "backgroundColor": "rgba(0, 192, 0, 0.25)", + "data": [4, 2, null, 3, 2.5, null, -2, 1.5, 3] + }, { + "backgroundColor": "rgba(192, 0, 0, 0.25)", + "data": [3.5, 2, 1, 2.5, -2, 3, -1, null, null] + }, { + "backgroundColor": "rgba(128, 0, 128, 0.25)", + "data": [5, 6, 5, -2, -4, -3, 4, 2, 4.5] + }] + }, + "options": { + "responsive": false, + "spanGaps": false, + "scales": { + "x": { + "display": false + }, + "y": { + "display": false + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "cubicInterpolationMode": "monotone", + "borderColor": "transparent", + "stepped": true, + "fill": "origin" + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.filler/line/boundary/origin-stepped.png b/test/fixtures/plugin.filler/line/boundary/origin-stepped.png new file mode 100644 index 00000000000..60de6ba21a3 Binary files /dev/null and b/test/fixtures/plugin.filler/line/boundary/origin-stepped.png differ diff --git a/test/fixtures/plugin.filler/line/boundary/origin.json b/test/fixtures/plugin.filler/line/boundary/origin.json new file mode 100644 index 00000000000..b7d5187115d --- /dev/null +++ b/test/fixtures/plugin.filler/line/boundary/origin.json @@ -0,0 +1,54 @@ +{ + "config": { + "type": "line", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [{ + "backgroundColor": "rgba(0, 0, 192, 0.25)", + "data": [null, null, 2, 3, 4, -4, -2, 1, 0] + }, { + "backgroundColor": "rgba(0, 192, 0, 0.25)", + "data": [6, 2, null, 4, 5, null, null, 2, 1] + }, { + "backgroundColor": "rgba(192, 0, 0, 0.25)", + "data": [7, 3, 4, 5, 6, 1, 4, null, null] + }, { + "backgroundColor": "rgba(0, 64, 192, 0.25)", + "data": [8, 7, 6, -6, -4, -6, 4, 5, 8] + }] + }, + "options": { + "responsive": false, + "spanGaps": false, + "scales": { + "x": { + "display": false + }, + "y": { + "display": false + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "borderColor": "transparent", + "fill": "origin", + "tension": 0 + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.filler/line/boundary/origin.png b/test/fixtures/plugin.filler/line/boundary/origin.png new file mode 100644 index 00000000000..29df37b8731 Binary files /dev/null and b/test/fixtures/plugin.filler/line/boundary/origin.png differ diff --git a/test/fixtures/plugin.filler/line/boundary/start-span.json b/test/fixtures/plugin.filler/line/boundary/start-span.json new file mode 100644 index 00000000000..ef6f21cd248 --- /dev/null +++ b/test/fixtures/plugin.filler/line/boundary/start-span.json @@ -0,0 +1,54 @@ +{ + "config": { + "type": "line", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [{ + "backgroundColor": "rgba(0, 0, 255, 0.25)", + "data": [null, null, 2, 3, 4, -4, -2, 1, 0] + }, { + "backgroundColor": "rgba(0, 255, 0, 0.25)", + "data": [6, 2, null, 4, 5, null, null, 2, 1] + }, { + "backgroundColor": "rgba(255, 0, 0, 0.25)", + "data": [7, 3, 4, 5, 6, 1, 4, null, null] + }, { + "backgroundColor": "rgba(0, 0, 255, 0.25)", + "data": [8, 7, 6, -6, -4, -6, 4, 5, 8] + }] + }, + "options": { + "responsive": false, + "spanGaps": true, + "scales": { + "x": { + "display": false + }, + "y": { + "display": false + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "borderColor": "transparent", + "fill": "start", + "tension": 0 + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.filler/line/boundary/start-span.png b/test/fixtures/plugin.filler/line/boundary/start-span.png new file mode 100644 index 00000000000..8100ad07d75 Binary files /dev/null and b/test/fixtures/plugin.filler/line/boundary/start-span.png differ diff --git a/test/fixtures/plugin.filler/line/boundary/start.json b/test/fixtures/plugin.filler/line/boundary/start.json new file mode 100644 index 00000000000..1e2815e54d1 --- /dev/null +++ b/test/fixtures/plugin.filler/line/boundary/start.json @@ -0,0 +1,54 @@ +{ + "config": { + "type": "line", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [{ + "backgroundColor": "rgba(0, 0, 255, 0.25)", + "data": [null, null, 2, 3, 4, -4, -2, 1, 0] + }, { + "backgroundColor": "rgba(0, 255, 0, 0.25)", + "data": [6, 2, null, 4, 5, null, null, 2, 1] + }, { + "backgroundColor": "rgba(255, 0, 0, 0.25)", + "data": [7, 3, 4, 5, 6, 1, 4, null, null] + }, { + "backgroundColor": "rgba(0, 0, 255, 0.25)", + "data": [8, 7, 6, -6, -4, -6, 4, 5, 8] + }] + }, + "options": { + "responsive": false, + "spanGaps": false, + "scales": { + "x": { + "display": false + }, + "y": { + "display": false + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "borderColor": "transparent", + "fill": "start", + "tension": 0 + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.filler/line/boundary/start.png b/test/fixtures/plugin.filler/line/boundary/start.png new file mode 100644 index 00000000000..da20bdcdea5 Binary files /dev/null and b/test/fixtures/plugin.filler/line/boundary/start.png differ diff --git a/test/fixtures/plugin.filler/line/dataset/border.json b/test/fixtures/plugin.filler/line/dataset/border.json new file mode 100644 index 00000000000..13342bb584b --- /dev/null +++ b/test/fixtures/plugin.filler/line/dataset/border.json @@ -0,0 +1,62 @@ +{ + "config": { + "type": "line", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [{ + "backgroundColor": "rgba(255, 0, 0, 0.25)", + "data": [null, null, 0, -1, 0, 1, 0, -1, 0], + "fill": 1 + }, { + "backgroundColor": "rgba(0, 255, 0, 0.25)", + "data": [1, 0, null, 1, 0, null, -1, 0, 1], + "fill": "+1" + }, { + "backgroundColor": "rgba(0, 0, 255, 0.25)", + "data": [0, 2, 0, -2, 0, 2, 0], + "fill": 3 + }, { + "backgroundColor": "rgba(255, 0, 255, 0.25)", + "data": [2, 0, -2, 0, 2, 0, -2, 0, 2], + "fill": "-2" + }, { + "backgroundColor": "rgba(255, 255, 0, 0.25)", + "data": [3, 1, -1, -3, -1, 1, 3, 1, -1], + "fill": "-1" + }] + }, + "options": { + "responsive": false, + "spanGaps": false, + "scales": { + "x": { + "display": false + }, + "y": { + "display": false + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "borderColor": "black", + "borderWidth": 5, + "tension": 0 + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.filler/line/dataset/border.png b/test/fixtures/plugin.filler/line/dataset/border.png new file mode 100644 index 00000000000..ba3aef582f1 Binary files /dev/null and b/test/fixtures/plugin.filler/line/dataset/border.png differ diff --git a/test/fixtures/plugin.filler/line/dataset/clip-bounds-x-off.js b/test/fixtures/plugin.filler/line/dataset/clip-bounds-x-off.js new file mode 100644 index 00000000000..ff437ae80ac --- /dev/null +++ b/test/fixtures/plugin.filler/line/dataset/clip-bounds-x-off.js @@ -0,0 +1,78 @@ +const labels = [1, 2, 3, 4, 5, 6, 7]; +const values = [65, 59, 80, 81, 56, 55, 40]; + +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/12052', + config: { + type: 'line', + data: { + labels, + datasets: [ + { + data: values.map(v => v - 10), + fill: '1', + borderColor: 'rgb(255, 0, 0)', + backgroundColor: 'rgba(255, 0, 0, 0.25)', + xAxisID: 'x1', + }, + { + data: values, + fill: false, + borderColor: 'rgb(255, 0, 0)', + xAxisID: 'x1', + }, + { + data: values, + fill: false, + borderColor: 'rgb(0, 0, 255)', + xAxisID: 'x2', + }, + { + data: values.map(v => v + 10), + fill: '-1', + borderColor: 'rgb(0, 0, 255)', + backgroundColor: 'rgba(0, 0, 255, 0.25)', + xAxisID: 'x2', + } + ] + }, + options: { + clip: false, + indexAxis: 'y', + animation: false, + responsive: false, + plugins: { + legend: false, + title: false, + tooltip: false + }, + elements: { + point: { + radius: 0 + }, + line: { + cubicInterpolationMode: 'monotone', + borderColor: 'transparent', + tension: 0 + } + }, + scales: { + x2: { + axis: 'x', + stack: 'stack', + max: 80, + display: false, + }, + x1: { + min: 50, + axis: 'x', + stack: 'stack', + display: false, + }, + y: { + display: false, + } + } + } + }, +}; diff --git a/test/fixtures/plugin.filler/line/dataset/clip-bounds-x-off.png b/test/fixtures/plugin.filler/line/dataset/clip-bounds-x-off.png new file mode 100644 index 00000000000..f050a4759f3 Binary files /dev/null and b/test/fixtures/plugin.filler/line/dataset/clip-bounds-x-off.png differ diff --git a/test/fixtures/plugin.filler/line/dataset/clip-bounds-x.js b/test/fixtures/plugin.filler/line/dataset/clip-bounds-x.js new file mode 100644 index 00000000000..0ba25ac3122 --- /dev/null +++ b/test/fixtures/plugin.filler/line/dataset/clip-bounds-x.js @@ -0,0 +1,77 @@ +const labels = [1, 2, 3, 4, 5, 6, 7]; +const values = [65, 59, 80, 81, 56, 55, 40]; + +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/12052', + config: { + type: 'line', + data: { + labels, + datasets: [ + { + data: values.map(v => v - 10), + fill: '1', + borderColor: 'rgb(255, 0, 0)', + backgroundColor: 'rgba(255, 0, 0, 0.25)', + xAxisID: 'x1', + }, + { + data: values, + fill: false, + borderColor: 'rgb(255, 0, 0)', + xAxisID: 'x1', + }, + { + data: values, + fill: false, + borderColor: 'rgb(0, 0, 255)', + xAxisID: 'x2', + }, + { + data: values.map(v => v + 10), + fill: '-1', + borderColor: 'rgb(0, 0, 255)', + backgroundColor: 'rgba(0, 0, 255, 0.25)', + xAxisID: 'x2', + } + ] + }, + options: { + indexAxis: 'y', + animation: false, + responsive: false, + plugins: { + legend: false, + title: false, + tooltip: false + }, + elements: { + point: { + radius: 0 + }, + line: { + cubicInterpolationMode: 'monotone', + borderColor: 'transparent', + tension: 0 + } + }, + scales: { + x2: { + axis: 'x', + stack: 'stack', + max: 80, + display: false, + }, + x1: { + min: 50, + axis: 'x', + stack: 'stack', + display: false, + }, + y: { + display: false, + } + } + } + }, +}; diff --git a/test/fixtures/plugin.filler/line/dataset/clip-bounds-x.png b/test/fixtures/plugin.filler/line/dataset/clip-bounds-x.png new file mode 100644 index 00000000000..4f1dfdd6cab Binary files /dev/null and b/test/fixtures/plugin.filler/line/dataset/clip-bounds-x.png differ diff --git a/test/fixtures/plugin.filler/line/dataset/clip-bounds-y-off.js b/test/fixtures/plugin.filler/line/dataset/clip-bounds-y-off.js new file mode 100644 index 00000000000..16a9759bb7d --- /dev/null +++ b/test/fixtures/plugin.filler/line/dataset/clip-bounds-y-off.js @@ -0,0 +1,77 @@ +const labels = [1, 2, 3, 4, 5, 6, 7]; +const values = [65, 59, 80, 81, 56, 55, 40]; + +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/12052', + config: { + type: 'line', + data: { + labels, + datasets: [ + { + data: values.map(v => v - 10), + fill: '1', + borderColor: 'rgb(255, 0, 0)', + backgroundColor: 'rgba(255, 0, 0, 0.25)', + yAxisID: 'y1', + }, + { + data: values, + fill: false, + borderColor: 'rgb(255, 0, 0)', + yAxisID: 'y1', + }, + { + data: values, + fill: false, + borderColor: 'rgb(0, 0, 255)', + yAxisID: 'y2', + }, + { + data: values.map(v => v + 10), + fill: '-1', + borderColor: 'rgb(0, 0, 255)', + backgroundColor: 'rgba(0, 0, 255, 0.25)', + yAxisID: 'y2', + } + ] + }, + options: { + clip: false, + animation: false, + responsive: false, + plugins: { + legend: false, + title: false, + tooltip: false + }, + elements: { + point: { + radius: 0 + }, + line: { + cubicInterpolationMode: 'monotone', + borderColor: 'transparent', + tension: 0 + } + }, + scales: { + y2: { + axis: 'y', + stack: 'stack', + max: 80, + display: false, + }, + y1: { + min: 50, + axis: 'y', + stack: 'stack', + display: false, + }, + x: { + display: false, + } + } + } + }, +}; diff --git a/test/fixtures/plugin.filler/line/dataset/clip-bounds-y-off.png b/test/fixtures/plugin.filler/line/dataset/clip-bounds-y-off.png new file mode 100644 index 00000000000..a2b8766f84d Binary files /dev/null and b/test/fixtures/plugin.filler/line/dataset/clip-bounds-y-off.png differ diff --git a/test/fixtures/plugin.filler/line/dataset/clip-bounds-y.js b/test/fixtures/plugin.filler/line/dataset/clip-bounds-y.js new file mode 100644 index 00000000000..cbfc6d40381 --- /dev/null +++ b/test/fixtures/plugin.filler/line/dataset/clip-bounds-y.js @@ -0,0 +1,76 @@ +const labels = [1, 2, 3, 4, 5, 6, 7]; +const values = [65, 59, 80, 81, 56, 55, 40]; + +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/12052', + config: { + type: 'line', + data: { + labels, + datasets: [ + { + data: values.map(v => v - 10), + fill: '1', + borderColor: 'rgb(255, 0, 0)', + backgroundColor: 'rgba(255, 0, 0, 0.25)', + yAxisID: 'y1', + }, + { + data: values, + fill: false, + borderColor: 'rgb(255, 0, 0)', + yAxisID: 'y1', + }, + { + data: values, + fill: false, + borderColor: 'rgb(0, 0, 255)', + yAxisID: 'y2', + }, + { + data: values.map(v => v + 10), + fill: '-1', + borderColor: 'rgb(0, 0, 255)', + backgroundColor: 'rgba(0, 0, 255, 0.25)', + yAxisID: 'y2', + } + ] + }, + options: { + animation: false, + responsive: false, + plugins: { + legend: false, + title: false, + tooltip: false + }, + elements: { + point: { + radius: 0 + }, + line: { + cubicInterpolationMode: 'monotone', + borderColor: 'transparent', + tension: 0 + } + }, + scales: { + y2: { + axis: 'y', + stack: 'stack', + max: 80, + display: false, + }, + y1: { + min: 50, + axis: 'y', + stack: 'stack', + display: false, + }, + x: { + display: false, + } + } + } + }, +}; diff --git a/test/fixtures/plugin.filler/line/dataset/clip-bounds-y.png b/test/fixtures/plugin.filler/line/dataset/clip-bounds-y.png new file mode 100644 index 00000000000..137e0315bb2 Binary files /dev/null and b/test/fixtures/plugin.filler/line/dataset/clip-bounds-y.png differ diff --git a/test/fixtures/plugin.filler/line/dataset/dual.json b/test/fixtures/plugin.filler/line/dataset/dual.json new file mode 100644 index 00000000000..7e8fec4608c --- /dev/null +++ b/test/fixtures/plugin.filler/line/dataset/dual.json @@ -0,0 +1,51 @@ +{ + "config": { + "type": "line", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [{ + "backgroundColor": "rgba(255, 0, 0, 0.25)", + "data": [0, 1, 2, -1, 0, 2, 1, -1, -2], + "fill": { + "target": "+1", + "above": "rgba(255, 0, 0, 0.25)", + "below": "rgba(0, 0, 255, 0.25)" + } + }, { + "data": [0, 0, 0, 0, 0, 0, 0, 0, 0] + }] + }, + "options": { + "responsive": false, + "spanGaps": true, + "scales": { + "x": { + "display": false + }, + "y": { + "display": false + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "borderColor": "transparent", + "tension": 0 + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.filler/line/dataset/dual.png b/test/fixtures/plugin.filler/line/dataset/dual.png new file mode 100644 index 00000000000..ae2c77dffba Binary files /dev/null and b/test/fixtures/plugin.filler/line/dataset/dual.png differ diff --git a/test/fixtures/plugin.filler/line/dataset/interpolated.js b/test/fixtures/plugin.filler/line/dataset/interpolated.js new file mode 100644 index 00000000000..ff94edf538a --- /dev/null +++ b/test/fixtures/plugin.filler/line/dataset/interpolated.js @@ -0,0 +1,72 @@ +const data1 = []; +const data2 = []; +const data3 = []; +for (let i = 0; i < 200; i++) { + const a = i / Math.PI / 10; + + data1.push({x: i, y: i < 86 || i > 104 && i < 178 ? Math.sin(a) : NaN}); + + if (i % 10 === 0) { + data2.push({x: i, y: Math.cos(a)}); + } + + if (i % 15 === 0) { + data3.push({x: i, y: Math.cos(a + Math.PI / 2)}); + } +} + +module.exports = { + config: { + type: 'line', + data: { + datasets: [{ + borderColor: 'rgba(255, 0, 0, 0.5)', + backgroundColor: 'rgba(255, 0, 0, 0.25)', + data: data1, + fill: false, + }, { + borderColor: 'rgba(0, 0, 255, 0.5)', + backgroundColor: 'rgba(0, 0, 255, 0.25)', + data: data2, + fill: 0, + }, { + borderColor: 'rgba(0, 255, 0, 0.5)', + backgroundColor: 'rgba(0, 255, 0, 0.25)', + data: data3, + fill: 1, + }] + }, + options: { + animation: false, + responsive: false, + datasets: { + line: { + tension: 0.4, + borderWidth: 1, + pointRadius: 1.5, + } + }, + plugins: { + legend: false, + title: false, + tooltip: false + }, + scales: { + x: { + type: 'linear', + display: false + }, + y: { + type: 'linear', + display: false + } + } + } + }, + options: { + canvas: { + height: 512, + width: 512 + } + } +}; diff --git a/test/fixtures/plugin.filler/line/dataset/interpolated.png b/test/fixtures/plugin.filler/line/dataset/interpolated.png new file mode 100644 index 00000000000..f99a3f2ceab Binary files /dev/null and b/test/fixtures/plugin.filler/line/dataset/interpolated.png differ diff --git a/test/fixtures/plugin.filler/line/dataset/no-border.json b/test/fixtures/plugin.filler/line/dataset/no-border.json new file mode 100644 index 00000000000..efd5dfaf924 --- /dev/null +++ b/test/fixtures/plugin.filler/line/dataset/no-border.json @@ -0,0 +1,61 @@ +{ + "config": { + "type": "line", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [{ + "backgroundColor": "rgba(255, 0, 0, 0.25)", + "data": [null, null, 0, -1, 0, 1, 0, -1, 0], + "fill": 1 + }, { + "backgroundColor": "rgba(0, 255, 0, 0.25)", + "data": [1, 0, null, 1, 0, null, -1, 0, 1], + "fill": "+1" + }, { + "backgroundColor": "rgba(0, 0, 255, 0.25)", + "data": [0, 2, 0, -2, 0, 2, 0], + "fill": 3 + }, { + "backgroundColor": "rgba(255, 0, 255, 0.25)", + "data": [2, 0, -2, 0, 2, 0, -2, 0, 2], + "fill": "-2" + }, { + "backgroundColor": "rgba(255, 255, 0, 0.25)", + "data": [3, 1, -1, -3, -1, 1, 3, 1, -1], + "fill": "-1" + }] + }, + "options": { + "responsive": false, + "spanGaps": false, + "scales": { + "x": { + "display": false + }, + "y": { + "display": false + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "borderColor": "transparent", + "tension": 0 + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.filler/line/dataset/no-border.png b/test/fixtures/plugin.filler/line/dataset/no-border.png new file mode 100644 index 00000000000..88f9d4fd855 Binary files /dev/null and b/test/fixtures/plugin.filler/line/dataset/no-border.png differ diff --git a/test/fixtures/plugin.filler/line/dataset/span-dual.json b/test/fixtures/plugin.filler/line/dataset/span-dual.json new file mode 100644 index 00000000000..cb2a3abcd41 --- /dev/null +++ b/test/fixtures/plugin.filler/line/dataset/span-dual.json @@ -0,0 +1,74 @@ +{ + "config": { + "type": "line", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [{ + "backgroundColor": "rgba(255, 0, 0, 0.25)", + "data": [null, null, 0, -1, 0, 1, 0, -1, 0], + "fill": { + "target": 1, + "above": "rgba(255, 0, 0, 0.25)", + "below": "rgba(122, 0, 0, 0.25)" + } + }, { + "backgroundColor": "rgba(0, 255, 0, 0.25)", + "data": [1, 0, null, 1, 0, null, -1, 0, 1], + "fill": { + "target": "+1", + "above": "rgba(0, 255, 0, 0.25)", + "below": "rgba(0, 255, 120, 0.25)" + } + }, { + "backgroundColor": "rgba(255, 0, 255, 0.25)", + "data": [2, 0, -2, 0, 2, 0, -2, 0, 2], + "fill": { + "target": "-2", + "above": "rgba(255, 0, 255, 0.25)", + "below": "rgba(255, 0, 120, 0.25)" + } + }, { + "backgroundColor": "rgba(255, 255, 0, 0.25)", + "data": [3, 1, -1, -3, -1, 1, 3, 1, -1], + "fill": { + "target": "-1", + "above": "rgba(255, 255, 0, 0.25)", + "below": "rgba(255, 120, 0, 0.25)" + } + }] + }, + "options": { + "responsive": false, + "spanGaps": true, + "scales": { + "x": { + "display": false + }, + "y": { + "display": false + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "cubicInterpolationMode": "monotone", + "borderColor": "transparent", + "tension": 0 + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.filler/line/dataset/span-dual.png b/test/fixtures/plugin.filler/line/dataset/span-dual.png new file mode 100644 index 00000000000..158759d3489 Binary files /dev/null and b/test/fixtures/plugin.filler/line/dataset/span-dual.png differ diff --git a/test/fixtures/plugin.filler/line/dataset/span.json b/test/fixtures/plugin.filler/line/dataset/span.json new file mode 100644 index 00000000000..d1fa6e7408f --- /dev/null +++ b/test/fixtures/plugin.filler/line/dataset/span.json @@ -0,0 +1,61 @@ +{ + "config": { + "type": "line", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [{ + "backgroundColor": "rgba(255, 0, 0, 0.25)", + "data": [null, null, 0, -1, 0, 1, 0, -1, 0], + "fill": 1 + }, { + "backgroundColor": "rgba(0, 255, 0, 0.25)", + "data": [1, 0, null, 1, 0, null, -1, 0, 1], + "fill": "+1" + }, { + "backgroundColor": "rgba(0, 0, 255, 0.25)", + "data": [0, 2, 0, -2, 0, 2, 0], + "fill": 3 + }, { + "backgroundColor": "rgba(255, 0, 255, 0.25)", + "data": [2, 0, -2, 0, 2, 0, -2, 0, 2], + "fill": "-2" + }, { + "backgroundColor": "rgba(255, 255, 0, 0.25)", + "data": [3, 1, -1, -3, -1, 1, 3, 1, -1], + "fill": "-1" + }] + }, + "options": { + "responsive": false, + "spanGaps": true, + "scales": { + "x": { + "display": false + }, + "y": { + "display": false + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "borderColor": "transparent", + "tension": 0 + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.filler/line/dataset/span.png b/test/fixtures/plugin.filler/line/dataset/span.png new file mode 100644 index 00000000000..780ce79f33a Binary files /dev/null and b/test/fixtures/plugin.filler/line/dataset/span.png differ diff --git a/test/fixtures/plugin.filler/line/dataset/spline-span-above.json b/test/fixtures/plugin.filler/line/dataset/spline-span-above.json new file mode 100644 index 00000000000..5705220aad7 --- /dev/null +++ b/test/fixtures/plugin.filler/line/dataset/spline-span-above.json @@ -0,0 +1,69 @@ +{ + "config": { + "type": "line", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [{ + "backgroundColor": "rgba(255, 0, 0, 0.25)", + "data": [null, null, 0, -1, 0, 1, 0, -1, 0], + "fill": { + "target": 1, + "below": "transparent" + } + }, { + "backgroundColor": "rgba(0, 255, 0, 0.25)", + "data": [1, 0, null, 1, 0, null, -1, 0, 1], + "fill": { + "target": "+1", + "below": "transparent" + } + }, { + "backgroundColor": "rgba(255, 0, 255, 0.25)", + "data": [2, 0, -2, 0, 2, 0, -2, 0, 2], + "fill": { + "target": "-2", + "below": "transparent" + } + }, { + "backgroundColor": "rgba(255, 255, 0, 0.25)", + "data": [3, 1, -1, -3, -1, 1, 3, 1, -1], + "fill": { + "target": "-1", + "below": "transparent" + } + }] + }, + "options": { + "responsive": false, + "spanGaps": true, + "scales": { + "x": { + "display": false + }, + "y": { + "display": false + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "cubicInterpolationMode": "monotone", + "borderColor": "transparent" + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.filler/line/dataset/spline-span-above.png b/test/fixtures/plugin.filler/line/dataset/spline-span-above.png new file mode 100644 index 00000000000..2c8dbd2a779 Binary files /dev/null and b/test/fixtures/plugin.filler/line/dataset/spline-span-above.png differ diff --git a/test/fixtures/plugin.filler/line/dataset/spline-span-below.json b/test/fixtures/plugin.filler/line/dataset/spline-span-below.json new file mode 100644 index 00000000000..4c874c40202 --- /dev/null +++ b/test/fixtures/plugin.filler/line/dataset/spline-span-below.json @@ -0,0 +1,69 @@ +{ + "config": { + "type": "line", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [{ + "backgroundColor": "rgba(255, 0, 0, 0.25)", + "data": [null, null, 0, -1, 0, 1, 0, -1, 0], + "fill": { + "target": 1, + "above": "transparent" + } + }, { + "backgroundColor": "rgba(0, 255, 0, 0.25)", + "data": [1, 0, null, 1, 0, null, -1, 0, 1], + "fill": { + "target": "+1", + "above": "transparent" + } + }, { + "backgroundColor": "rgba(255, 0, 255, 0.25)", + "data": [2, 0, -2, 0, 2, 0, -2, 0, 2], + "fill": { + "target": "-2", + "above": "transparent" + } + }, { + "backgroundColor": "rgba(255, 255, 0, 0.25)", + "data": [3, 1, -1, -3, -1, 1, 3, 1, -1], + "fill": { + "target": "-1", + "above": "transparent" + } + }] + }, + "options": { + "responsive": false, + "spanGaps": true, + "scales": { + "x": { + "display": false + }, + "y": { + "display": false + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "cubicInterpolationMode": "monotone", + "borderColor": "transparent" + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.filler/line/dataset/spline-span-below.png b/test/fixtures/plugin.filler/line/dataset/spline-span-below.png new file mode 100644 index 00000000000..68a20e2eeea Binary files /dev/null and b/test/fixtures/plugin.filler/line/dataset/spline-span-below.png differ diff --git a/test/fixtures/plugin.filler/line/dataset/spline-span.json b/test/fixtures/plugin.filler/line/dataset/spline-span.json new file mode 100644 index 00000000000..aa7c8e7ea35 --- /dev/null +++ b/test/fixtures/plugin.filler/line/dataset/spline-span.json @@ -0,0 +1,61 @@ +{ + "config": { + "type": "line", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [{ + "backgroundColor": "rgba(255, 0, 0, 0.25)", + "data": [null, null, 0, -1, 0, 1, 0, -1, 0], + "fill": 1 + }, { + "backgroundColor": "rgba(0, 255, 0, 0.25)", + "data": [1, 0, null, 1, 0, null, -1, 0, 1], + "fill": "+1" + }, { + "backgroundColor": "rgba(0, 0, 255, 0.25)", + "data": [0, 2, 0, -2, 0, 2, 0], + "fill": 3 + }, { + "backgroundColor": "rgba(255, 0, 255, 0.25)", + "data": [2, 0, -2, 0, 2, 0, -2, 0, 2], + "fill": "-2" + }, { + "backgroundColor": "rgba(255, 255, 0, 0.25)", + "data": [3, 1, -1, -3, -1, 1, 3, 1, -1], + "fill": "-1" + }] + }, + "options": { + "responsive": false, + "spanGaps": true, + "scales": { + "x": { + "display": false + }, + "y": { + "display": false + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "cubicInterpolationMode": "monotone", + "borderColor": "transparent" + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.filler/line/dataset/spline-span.png b/test/fixtures/plugin.filler/line/dataset/spline-span.png new file mode 100644 index 00000000000..07716a2f210 Binary files /dev/null and b/test/fixtures/plugin.filler/line/dataset/spline-span.png differ diff --git a/test/fixtures/plugin.filler/line/dataset/spline.json b/test/fixtures/plugin.filler/line/dataset/spline.json new file mode 100644 index 00000000000..18d69b73716 --- /dev/null +++ b/test/fixtures/plugin.filler/line/dataset/spline.json @@ -0,0 +1,61 @@ +{ + "config": { + "type": "line", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [{ + "backgroundColor": "rgba(255, 0, 0, 0.25)", + "data": [null, null, 0, -1, 0, 1, 0, -1, 0], + "fill": 1 + }, { + "backgroundColor": "rgba(0, 255, 0, 0.25)", + "data": [1, 0, null, 1, 0, null, -1, 0, 1], + "fill": "+1" + }, { + "backgroundColor": "rgba(0, 0, 255, 0.25)", + "data": [0, 2, 0, -2, 0, 2, 0], + "fill": 3 + }, { + "backgroundColor": "rgba(255, 0, 255, 0.25)", + "data": [2, 0, -2, 0, 2, 0, -2, 0, 2], + "fill": "-2" + }, { + "backgroundColor": "rgba(255, 255, 0, 0.25)", + "data": [3, 1, -1, -3, -1, 1, 3, 1, -1], + "fill": "-1" + }] + }, + "options": { + "responsive": false, + "spanGaps": false, + "scales": { + "x": { + "display": false + }, + "y": { + "display": false + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "cubicInterpolationMode": "monotone", + "borderColor": "transparent" + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.filler/line/dataset/spline.png b/test/fixtures/plugin.filler/line/dataset/spline.png new file mode 100644 index 00000000000..a66f3564887 Binary files /dev/null and b/test/fixtures/plugin.filler/line/dataset/spline.png differ diff --git a/test/fixtures/plugin.filler/line/dataset/stepped.json b/test/fixtures/plugin.filler/line/dataset/stepped.json new file mode 100644 index 00000000000..ad6bd55ae05 --- /dev/null +++ b/test/fixtures/plugin.filler/line/dataset/stepped.json @@ -0,0 +1,65 @@ +{ + "config": { + "type": "line", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [{ + "backgroundColor": "rgba(255, 0, 0, 0.25)", + "stepped": true, + "data": [null, null, 0, -1, 0, 1, 0, -1, 0], + "fill": 1 + }, { + "backgroundColor": "rgba(0, 255, 0, 0.25)", + "stepped": "after", + "data": [1, 0, null, 1, 0, null, -1, 0, 1], + "fill": "+1" + }, { + "backgroundColor": "rgba(0, 0, 255, 0.25)", + "stepped": "before", + "data": [0, 2, 0, -2, 0, 2, 0], + "fill": 3 + }, { + "backgroundColor": "rgba(255, 0, 255, 0.25)", + "stepped": "middle", + "data": [2, 0, -2, 0, 2, 0, -2, 0, 2], + "fill": "-2" + }, { + "backgroundColor": "rgba(255, 255, 0, 0.25)", + "stepped": false, + "data": [3, 1, -1, -3, -1, 1, 3, 1, -1], + "fill": "-1" + }] + }, + "options": { + "responsive": false, + "spanGaps": false, + "scales": { + "x": { + "display": false + }, + "y": { + "display": false + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "borderColor": "black" + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.filler/line/dataset/stepped.png b/test/fixtures/plugin.filler/line/dataset/stepped.png new file mode 100644 index 00000000000..418bca44e03 Binary files /dev/null and b/test/fixtures/plugin.filler/line/dataset/stepped.png differ diff --git a/test/fixtures/plugin.filler/line/drawTimeFillFalse/beforeDatasetDraw.js b/test/fixtures/plugin.filler/line/drawTimeFillFalse/beforeDatasetDraw.js new file mode 100644 index 00000000000..a07432a108b --- /dev/null +++ b/test/fixtures/plugin.filler/line/drawTimeFillFalse/beforeDatasetDraw.js @@ -0,0 +1,22 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: ['0', '1', '2', '3', '4', '5'], + datasets: [{ + backgroundColor: 'red', + data: [3, -3, 0, 5, -5, 0], + fill: false + }] + }, + options: { + plugins: { + legend: false, + title: false, + filler: { + drawTime: 'beforeDatasetDraw' + } + }, + } + }, +}; diff --git a/test/fixtures/plugin.filler/line/drawTimeFillFalse/beforeDatasetDraw.png b/test/fixtures/plugin.filler/line/drawTimeFillFalse/beforeDatasetDraw.png new file mode 100644 index 00000000000..c45eb28cd0a Binary files /dev/null and b/test/fixtures/plugin.filler/line/drawTimeFillFalse/beforeDatasetDraw.png differ diff --git a/test/fixtures/plugin.filler/line/drawTimeFillFalse/beforeDatasetsDraw.js b/test/fixtures/plugin.filler/line/drawTimeFillFalse/beforeDatasetsDraw.js new file mode 100644 index 00000000000..6401ab79dba --- /dev/null +++ b/test/fixtures/plugin.filler/line/drawTimeFillFalse/beforeDatasetsDraw.js @@ -0,0 +1,22 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: ['0', '1', '2', '3', '4', '5'], + datasets: [{ + backgroundColor: 'red', + data: [3, -3, 0, 5, -5, 0], + fill: false + }] + }, + options: { + plugins: { + legend: false, + title: false, + filler: { + drawTime: 'beforeDatasetsDraw' + } + }, + } + }, +}; diff --git a/test/fixtures/plugin.filler/line/drawTimeFillFalse/beforeDatasetsDraw.png b/test/fixtures/plugin.filler/line/drawTimeFillFalse/beforeDatasetsDraw.png new file mode 100644 index 00000000000..c45eb28cd0a Binary files /dev/null and b/test/fixtures/plugin.filler/line/drawTimeFillFalse/beforeDatasetsDraw.png differ diff --git a/test/fixtures/plugin.filler/line/drawTimeFillFalse/beforeDraw.js b/test/fixtures/plugin.filler/line/drawTimeFillFalse/beforeDraw.js new file mode 100644 index 00000000000..5b0c1b1e617 --- /dev/null +++ b/test/fixtures/plugin.filler/line/drawTimeFillFalse/beforeDraw.js @@ -0,0 +1,22 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: ['0', '1', '2', '3', '4', '5'], + datasets: [{ + backgroundColor: 'red', + data: [3, -3, 0, 5, -5, 0], + fill: false + }] + }, + options: { + plugins: { + legend: false, + title: false, + filler: { + drawTime: 'beforeDraw' + } + }, + } + }, +}; diff --git a/test/fixtures/plugin.filler/line/drawTimeFillFalse/beforeDraw.png b/test/fixtures/plugin.filler/line/drawTimeFillFalse/beforeDraw.png new file mode 100644 index 00000000000..c45eb28cd0a Binary files /dev/null and b/test/fixtures/plugin.filler/line/drawTimeFillFalse/beforeDraw.png differ diff --git a/test/fixtures/plugin.filler/line/points-outside-canvas-initial.js b/test/fixtures/plugin.filler/line/points-outside-canvas-initial.js new file mode 100644 index 00000000000..8349c808e78 --- /dev/null +++ b/test/fixtures/plugin.filler/line/points-outside-canvas-initial.js @@ -0,0 +1,29 @@ +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/8699', + config: { + type: 'line', + data: { + datasets: [{ + backgroundColor: 'red', + data: [{x: 0, y: 3}, {x: 2, y: -3}, {x: 4, y: 0}, {x: 6, y: 5}, {x: 8, y: -5}, {x: 10, y: 0}], + fill: 'origin' + }] + }, + options: { + plugins: { + legend: false, + title: false, + }, + scales: { + x: { + display: false, + type: 'linear', + min: 5 + }, + y: { + display: false + } + } + } + }, +}; diff --git a/test/fixtures/plugin.filler/line/points-outside-canvas-initial.png b/test/fixtures/plugin.filler/line/points-outside-canvas-initial.png new file mode 100644 index 00000000000..34f18a625fb Binary files /dev/null and b/test/fixtures/plugin.filler/line/points-outside-canvas-initial.png differ diff --git a/test/fixtures/plugin.filler/line/points-outside-canvas-update.js b/test/fixtures/plugin.filler/line/points-outside-canvas-update.js new file mode 100644 index 00000000000..61c0b38d5c1 --- /dev/null +++ b/test/fixtures/plugin.filler/line/points-outside-canvas-update.js @@ -0,0 +1,34 @@ +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/8699', + config: { + type: 'line', + data: { + datasets: [{ + backgroundColor: 'red', + data: [{x: 0, y: 3}, {x: 2, y: -3}, {x: 4, y: 0}, {x: 6, y: 5}, {x: 8, y: -5}, {x: 10, y: 0}], + fill: 'origin' + }] + }, + options: { + plugins: { + legend: false, + title: false, + }, + scales: { + x: { + type: 'linear', + display: false + }, + y: { + display: false + } + } + } + }, + options: { + run(chart) { + chart.scales.x.options.min = 5; + chart.update(); + } + } +}; diff --git a/test/fixtures/plugin.filler/line/points-outside-canvas-update.png b/test/fixtures/plugin.filler/line/points-outside-canvas-update.png new file mode 100644 index 00000000000..1bb8b8748e5 Binary files /dev/null and b/test/fixtures/plugin.filler/line/points-outside-canvas-update.png differ diff --git a/test/fixtures/plugin.filler/line/segments/alignToPixels.js b/test/fixtures/plugin.filler/line/segments/alignToPixels.js new file mode 100644 index 00000000000..99ece11b362 --- /dev/null +++ b/test/fixtures/plugin.filler/line/segments/alignToPixels.js @@ -0,0 +1,46 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [ + { + data: [ + {x: 0, y: 0}, + {x: 1, y: 20}, + {x: 1.00001, y: 30}, + {x: 2, y: 100}, + {x: 2.00001, y: 100} + ], + backgroundColor: '#FF000070', + borderColor: 'black', + radius: 0, + segment: { + borderDash: ctx => ctx.p0.parsed.x > 1 ? [10, 5] : undefined, + }, + fill: true + } + ] + }, + options: { + plugins: { + legend: false + }, + scales: { + x: { + type: 'linear', + alignToPixels: true, + display: false + }, + y: { + display: false + } + } + } + }, + options: { + canvas: { + width: 300, + height: 240 + } + } +}; diff --git a/test/fixtures/plugin.filler/line/segments/alignToPixels.png b/test/fixtures/plugin.filler/line/segments/alignToPixels.png new file mode 100644 index 00000000000..df12d54ff5c Binary files /dev/null and b/test/fixtures/plugin.filler/line/segments/alignToPixels.png differ diff --git a/test/fixtures/plugin.filler/line/segments/gap.js b/test/fixtures/plugin.filler/line/segments/gap.js new file mode 100644 index 00000000000..1038a0b1183 --- /dev/null +++ b/test/fixtures/plugin.filler/line/segments/gap.js @@ -0,0 +1,24 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: ['a', 'b', 'c', 'd', 'e', 'f'], + datasets: [{ + data: [1, 3, NaN, NaN, 2, 1], + borderColor: 'transparent', + backgroundColor: 'black', + fill: true, + segment: { + backgroundColor: ctx => ctx.p0.skip || ctx.p1.skip ? 'red' : undefined, + }, + spanGaps: true + }] + }, + options: { + scales: { + x: {display: false}, + y: {display: false} + } + } + } +}; diff --git a/test/fixtures/plugin.filler/line/segments/gap.png b/test/fixtures/plugin.filler/line/segments/gap.png new file mode 100644 index 00000000000..eee63218c3d Binary files /dev/null and b/test/fixtures/plugin.filler/line/segments/gap.png differ diff --git a/test/fixtures/plugin.filler/line/segments/slope.js b/test/fixtures/plugin.filler/line/segments/slope.js new file mode 100644 index 00000000000..d9a4d250669 --- /dev/null +++ b/test/fixtures/plugin.filler/line/segments/slope.js @@ -0,0 +1,30 @@ +function slope({p0, p1}) { + return (p0.y - p1.y) / (p1.x - p0.x); +} + +module.exports = { + config: { + type: 'line', + data: { + labels: ['a', 'b', 'c', 'd', 'e', 'f'], + datasets: [{ + data: [1, 2, 3, 3, 2, 1], + backgroundColor: 'black', + borderColor: 'orange', + fill: true, + segment: { + backgroundColor: ctx => slope(ctx) > 0 ? 'green' : slope(ctx) < 0 ? 'red' : undefined, + } + }] + }, + options: { + plugins: { + legend: false + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + } +}; diff --git a/test/fixtures/plugin.filler/line/segments/slope.png b/test/fixtures/plugin.filler/line/segments/slope.png new file mode 100644 index 00000000000..bb9edd28f72 Binary files /dev/null and b/test/fixtures/plugin.filler/line/segments/slope.png differ diff --git a/test/fixtures/plugin.filler/line/shape.js b/test/fixtures/plugin.filler/line/shape.js new file mode 100644 index 00000000000..3a78bc745dc --- /dev/null +++ b/test/fixtures/plugin.filler/line/shape.js @@ -0,0 +1,35 @@ +const data = []; +for (let rad = 0; rad <= Math.PI * 2; rad += Math.PI / 45) { + data.push({ + x: Math.cos(rad), + y: Math.sin(rad) + }); +} + +module.exports = { + config: { + type: 'line', + data: { + datasets: [{ + data, + fill: 'shape', + backgroundColor: 'rgba(255, 0, 0, 0.5)', + }] + }, + options: { + plugins: { + legend: false + }, + scales: { + x: { + type: 'linear', + display: false + }, + y: { + type: 'linear', + display: false + }, + }, + } + } +}; diff --git a/test/fixtures/plugin.filler/line/shape.png b/test/fixtures/plugin.filler/line/shape.png new file mode 100644 index 00000000000..08e59986ccb Binary files /dev/null and b/test/fixtures/plugin.filler/line/shape.png differ diff --git a/test/fixtures/plugin.filler/line/stack-multiple-scales.js b/test/fixtures/plugin.filler/line/stack-multiple-scales.js new file mode 100644 index 00000000000..8ba2d44ac03 --- /dev/null +++ b/test/fixtures/plugin.filler/line/stack-multiple-scales.js @@ -0,0 +1,76 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: ['0', '1', '2', '3'], + datasets: [{ + backgroundColor: 'rgba(255, 0, 0, 0.5)', + data: [null, 1, 1, 1], + fill: 'stack' + }, { + backgroundColor: 'rgba(0, 255, 0, 0.5)', + data: [null, 2, 2, 2], + fill: 'stack' + }, { + backgroundColor: 'rgba(0, 0, 255, 0.5)', + data: [null, 3, 3, 3], + fill: 'stack' + }, { + backgroundColor: 'rgba(255, 0, 255, 0.5)', + data: [0.5, 0.5, 0.5, null], + fill: 'stack', + yAxisID: 'y2' + }, { + backgroundColor: 'rgba(0, 0, 0, 0.5)', + data: [1.5, 1.5, 1.5, null], + fill: 'stack', + yAxisID: 'y2' + }, { + backgroundColor: 'rgba(255, 255, 0, 0.5)', + data: [2.5, 2.5, 2.5, null], + fill: 'stack', + yAxisID: 'y2' + }] + }, + options: { + responsive: false, + spanGaps: false, + scales: { + x: { + display: false + }, + y: { + position: 'right', + stacked: true, + min: 0 + }, + y2: { + position: 'left', + stacked: true, + min: 0 + } + }, + elements: { + point: { + radius: 0 + }, + line: { + borderColor: 'transparent', + tension: 0 + } + }, + plugins: { + legend: false, + title: false, + tooltip: false + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/plugin.filler/line/stack-multiple-scales.png b/test/fixtures/plugin.filler/line/stack-multiple-scales.png new file mode 100644 index 00000000000..cb9c5bb79f9 Binary files /dev/null and b/test/fixtures/plugin.filler/line/stack-multiple-scales.png differ diff --git a/test/fixtures/plugin.filler/line/stack.json b/test/fixtures/plugin.filler/line/stack.json new file mode 100644 index 00000000000..44ca4548973 --- /dev/null +++ b/test/fixtures/plugin.filler/line/stack.json @@ -0,0 +1,67 @@ +{ + "config": { + "type": "line", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [{ + "backgroundColor": "rgba(255, 0, 0, 0.25)", + "data": [null, null, 0, 1, 0, 1, null, 0, 1], + "fill": "stack" + }, { + "backgroundColor": "rgba(0, 255, 0, 0.25)", + "data": [1, 1, null, 1, 0, null, 1, 1, 0], + "fill": "stack" + }, { + "backgroundColor": "rgba(0, 0, 255, 0.25)", + "data": [0, 2, null, 2, 0, 2, 0], + "fill": "stack" + }, { + "backgroundColor": "rgba(255, 0, 255, 0.25)", + "data": [2, 0, null, 0, 2, 0, 2, 0, 2], + "fill": "stack" + }, { + "backgroundColor": "rgba(0, 0, 0, 0.25)", + "data": [null, null, null, 2, null, 2, 2], + "fill": "stack" + }, { + "backgroundColor": "rgba(255, 255, 0, 0.25)", + "data": [3, 1, 1, 3, 1, 1, 3, 1, 1], + "fill": "stack" + }] + }, + "options": { + "responsive": false, + "spanGaps": false, + "scales": { + "x": { + "display": false + }, + "y": { + "display": false, + "stacked": true, + "min": 0 + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "borderColor": "transparent", + "tension": 0 + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.filler/line/stack.png b/test/fixtures/plugin.filler/line/stack.png new file mode 100644 index 00000000000..6c18c54e7e5 Binary files /dev/null and b/test/fixtures/plugin.filler/line/stack.png differ diff --git a/test/fixtures/plugin.filler/line/value.json b/test/fixtures/plugin.filler/line/value.json new file mode 100644 index 00000000000..b89abdfba71 --- /dev/null +++ b/test/fixtures/plugin.filler/line/value.json @@ -0,0 +1,45 @@ +{ + "config": { + "type": "line", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [{ + "backgroundColor": "rgba(255, 0, 0, 0.25)", + "data": [-4, 4, 0, -1, 0, 1, 0, -1, 0], + "fill": { "value": 2 } + }] + }, + "options": { + "responsive": false, + "spanGaps": false, + "scales": { + "x": { + "display": false + }, + "y": { + "display": false + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "borderColor": "transparent", + "tension": 0 + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.filler/line/value.png b/test/fixtures/plugin.filler/line/value.png new file mode 100644 index 00000000000..758b1c1324b Binary files /dev/null and b/test/fixtures/plugin.filler/line/value.png differ diff --git a/test/fixtures/plugin.filler/line/vertical.js b/test/fixtures/plugin.filler/line/vertical.js new file mode 100644 index 00000000000..864f8b4161e --- /dev/null +++ b/test/fixtures/plugin.filler/line/vertical.js @@ -0,0 +1,42 @@ +const data = [ + {y: 1, x: 12}, + {y: 3, x: 14}, + {y: 4, x: 20}, + {y: 6, x: 13}, + {y: 9, x: 18}, +]; + +module.exports = { + config: { + type: 'line', + data: { + datasets: [{ + data: data, + borderColor: 'red', + fill: false, + }, { + data: data.map((v) => ({y: v.y, x: 2 * v.x - 1.5 * v.y})), + fill: '-1', + borderColor: 'blue', + backgroundColor: 'rgba(255, 200, 0, 0.5)', + }] + }, + options: { + indexAxis: 'y', + radius: 0, + plugins: { + legend: false + }, + scales: { + x: { + display: false, + type: 'linear' + }, + y: { + display: false, + type: 'linear' + } + } + } + } +}; diff --git a/test/fixtures/plugin.filler/line/vertical.png b/test/fixtures/plugin.filler/line/vertical.png new file mode 100644 index 00000000000..5d0dd3aa578 Binary files /dev/null and b/test/fixtures/plugin.filler/line/vertical.png differ diff --git a/test/fixtures/plugin.filler/radar/beforeDraw.js b/test/fixtures/plugin.filler/radar/beforeDraw.js new file mode 100644 index 00000000000..956c6a0a2bf --- /dev/null +++ b/test/fixtures/plugin.filler/radar/beforeDraw.js @@ -0,0 +1,47 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], + datasets: [{ + label: '# of Votes', + data: [9, 7, 3, 5, 2, 3], + fill: 'origin', + borderColor: 'red', + backgroundColor: 'green', + pointRadius: 12, + pointBackgroundColor: 'red' + }] + }, + options: { + layout: { + padding: 20 + }, + plugins: { + legend: false, + filler: { + drawTime: 'beforeDraw' + } + }, + scales: { + r: { + angleLines: { + color: 'rgba(0,0,0,0.5)', + lineWidth: 2 + }, + grid: { + color: 'rgba(0,0,0,0.5)', + lineWidth: 2 + }, + pointLabels: { + display: false + }, + ticks: { + beginAtZero: true, + display: false + }, + } + } + } + } +}; diff --git a/test/fixtures/plugin.filler/radar/beforeDraw.png b/test/fixtures/plugin.filler/radar/beforeDraw.png new file mode 100644 index 00000000000..886a204d1a8 Binary files /dev/null and b/test/fixtures/plugin.filler/radar/beforeDraw.png differ diff --git a/test/fixtures/plugin.filler/radar/boundary/end-circular.json b/test/fixtures/plugin.filler/radar/boundary/end-circular.json new file mode 100644 index 00000000000..ecfa8fcd5ee --- /dev/null +++ b/test/fixtures/plugin.filler/radar/boundary/end-circular.json @@ -0,0 +1,58 @@ +{ + "config": { + "type": "radar", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [ + { + "backgroundColor": "rgba(0, 0, 192, 0.25)", + "data": [null, null, 2, 3, 4, -4, -2, 1, 0] + }, + { + "backgroundColor": "rgba(0, 192, 0, 0.25)", + "data": [5.5, 2, null, 4, 5, null, null, 2, 1] + }, + { + "backgroundColor": "rgba(192, 0, 0, 0.25)", + "data": [7, 3, 4, 5, 6, 1, 4, null, null] + }, + { + "backgroundColor": "rgba(0, 0, 192, 0.25)", + "data": [8, 7, 6.5, -6, -4, -6, 4, 5, 8] + } + ] + }, + "options": { + "responsive": false, + "spanGaps": false, + "scales": { + "r": { + "display": false, + "grid": { + "circular": true + } + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "borderColor": "transparent", + "fill": "end" + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 256 + } + } +} diff --git a/test/fixtures/plugin.filler/radar/boundary/end-circular.png b/test/fixtures/plugin.filler/radar/boundary/end-circular.png new file mode 100644 index 00000000000..1cca06775cb Binary files /dev/null and b/test/fixtures/plugin.filler/radar/boundary/end-circular.png differ diff --git a/test/fixtures/plugin.filler/radar/boundary/end-span.json b/test/fixtures/plugin.filler/radar/boundary/end-span.json new file mode 100644 index 00000000000..531a28a3c97 --- /dev/null +++ b/test/fixtures/plugin.filler/radar/boundary/end-span.json @@ -0,0 +1,55 @@ +{ + "config": { + "type": "radar", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [ + { + "backgroundColor": "rgba(0, 0, 192, 0.25)", + "data": [null, null, 2, 3, 4, -4, -2, 1, 0] + }, + { + "backgroundColor": "rgba(0, 192, 0, 0.25)", + "data": [5.5, 2, null, 4, 5, null, null, 2, 1] + }, + { + "backgroundColor": "rgba(192, 0, 0, 0.25)", + "data": [7, 3, 4, 5, 6, 1, 4, null, null] + }, + { + "backgroundColor": "rgba(0, 0, 192, 0.25)", + "data": [8, 7, 6.5, -6, -4, -6, 4, 5, 8] + } + ] + }, + "options": { + "responsive": false, + "spanGaps": true, + "scales": { + "r": { + "display": false + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "borderColor": "transparent", + "fill": "end" + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 256 + } + } +} diff --git a/test/fixtures/plugin.filler/radar/boundary/end-span.png b/test/fixtures/plugin.filler/radar/boundary/end-span.png new file mode 100644 index 00000000000..caa742b44e0 Binary files /dev/null and b/test/fixtures/plugin.filler/radar/boundary/end-span.png differ diff --git a/test/fixtures/plugin.filler/radar/boundary/end.json b/test/fixtures/plugin.filler/radar/boundary/end.json new file mode 100644 index 00000000000..5ea71805552 --- /dev/null +++ b/test/fixtures/plugin.filler/radar/boundary/end.json @@ -0,0 +1,55 @@ +{ + "config": { + "type": "radar", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [ + { + "backgroundColor": "rgba(0, 0, 192, 0.25)", + "data": [null, null, 2, 3, 4, -4, -2, 1, 0] + }, + { + "backgroundColor": "rgba(0, 192, 0, 0.25)", + "data": [5.5, 2, null, 4, 5, null, null, 2, 1] + }, + { + "backgroundColor": "rgba(192, 0, 0, 0.25)", + "data": [7, 3, 4, 5, 6, 1, 4, null, null] + }, + { + "backgroundColor": "rgba(0, 0, 192, 0.25)", + "data": [8, 7, 6.5, -6, -4, -6, 4, 5, 8] + } + ] + }, + "options": { + "responsive": false, + "spanGaps": false, + "scales": { + "r": { + "display": false + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "borderColor": "transparent", + "fill": "end" + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 256 + } + } +} diff --git a/test/fixtures/plugin.filler/radar/boundary/end.png b/test/fixtures/plugin.filler/radar/boundary/end.png new file mode 100644 index 00000000000..c7c7173e11f Binary files /dev/null and b/test/fixtures/plugin.filler/radar/boundary/end.png differ diff --git a/test/fixtures/plugin.filler/radar/boundary/origin-circular.json b/test/fixtures/plugin.filler/radar/boundary/origin-circular.json new file mode 100644 index 00000000000..f234fd5c242 --- /dev/null +++ b/test/fixtures/plugin.filler/radar/boundary/origin-circular.json @@ -0,0 +1,58 @@ +{ + "config": { + "type": "radar", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [ + { + "backgroundColor": "rgba(0, 0, 192, 0.25)", + "data": [null, null, 2, 4, 2, 1, -1, 1, 2] + }, + { + "backgroundColor": "rgba(0, 192, 0, 0.25)", + "data": [4, 2, null, 3, 2.5, null, -2, 1.5, 3] + }, + { + "backgroundColor": "rgba(192, 0, 0, 0.25)", + "data": [3.5, 2, 1, 2.5, -2, 3, -1, null, null] + }, + { + "backgroundColor": "rgba(128, 0, 128, 0.25)", + "data": [5, 6, 5, -2, -4, -3, 4, 2, 4.5] + } + ] + }, + "options": { + "responsive": false, + "spanGaps": false, + "scales": { + "r": { + "display": false, + "grid": { + "circular": true + } + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "borderColor": "transparent", + "fill": "origin" + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 256 + } + } +} diff --git a/test/fixtures/plugin.filler/radar/boundary/origin-circular.png b/test/fixtures/plugin.filler/radar/boundary/origin-circular.png new file mode 100644 index 00000000000..7a4cc427345 Binary files /dev/null and b/test/fixtures/plugin.filler/radar/boundary/origin-circular.png differ diff --git a/test/fixtures/plugin.filler/radar/boundary/origin-span.json b/test/fixtures/plugin.filler/radar/boundary/origin-span.json new file mode 100644 index 00000000000..55e3cb2eb88 --- /dev/null +++ b/test/fixtures/plugin.filler/radar/boundary/origin-span.json @@ -0,0 +1,55 @@ +{ + "config": { + "type": "radar", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [ + { + "backgroundColor": "rgba(0, 0, 192, 0.25)", + "data": [null, null, 2, 3, 4, -4, -2, 1, 0] + }, + { + "backgroundColor": "rgba(0, 192, 0, 0.25)", + "data": [6, 2, null, 4, 5, null, null, 2, 1] + }, + { + "backgroundColor": "rgba(192, 0, 0, 0.25)", + "data": [7, 3, 4, 5, 6, 1, 4, null, null] + }, + { + "backgroundColor": "rgba(0, 64, 192, 0.25)", + "data": [8, 7, 6, -6, -4, -6, 4, 5, 8] + } + ] + }, + "options": { + "responsive": false, + "spanGaps": true, + "scales": { + "r": { + "display": false + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "borderColor": "transparent", + "fill": "origin" + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 256 + } + } +} diff --git a/test/fixtures/plugin.filler/radar/boundary/origin-span.png b/test/fixtures/plugin.filler/radar/boundary/origin-span.png new file mode 100644 index 00000000000..7f043dbd8ba Binary files /dev/null and b/test/fixtures/plugin.filler/radar/boundary/origin-span.png differ diff --git a/test/fixtures/plugin.filler/radar/boundary/origin-spline-span.json b/test/fixtures/plugin.filler/radar/boundary/origin-spline-span.json new file mode 100644 index 00000000000..25d6c468520 --- /dev/null +++ b/test/fixtures/plugin.filler/radar/boundary/origin-spline-span.json @@ -0,0 +1,56 @@ +{ + "config": { + "type": "radar", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [ + { + "backgroundColor": "rgba(0, 0, 192, 0.25)", + "data": [null, null, 2, 4, 2, 1, -1, 1, 2] + }, + { + "backgroundColor": "rgba(0, 192, 0, 0.25)", + "data": [4, 2, null, 3, 2.5, null, -2, 1.5, 3] + }, + { + "backgroundColor": "rgba(192, 0, 0, 0.25)", + "data": [3.5, 2, 1, 2.5, -2, 3, -1, null, null] + }, + { + "backgroundColor": "rgba(128, 0, 128, 0.25)", + "data": [5, 6, 5, -2, -4, -3, 4, 2, 4.5] + } + ] + }, + "options": { + "responsive": false, + "spanGaps": true, + "scales": { + "r": { + "display": false + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "borderColor": "transparent", + "tension": 0.5, + "fill": "origin" + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 256 + } + } +} diff --git a/test/fixtures/plugin.filler/radar/boundary/origin-spline-span.png b/test/fixtures/plugin.filler/radar/boundary/origin-spline-span.png new file mode 100644 index 00000000000..b9b4dde994c Binary files /dev/null and b/test/fixtures/plugin.filler/radar/boundary/origin-spline-span.png differ diff --git a/test/fixtures/plugin.filler/radar/boundary/origin-spline.json b/test/fixtures/plugin.filler/radar/boundary/origin-spline.json new file mode 100644 index 00000000000..957666f5550 --- /dev/null +++ b/test/fixtures/plugin.filler/radar/boundary/origin-spline.json @@ -0,0 +1,56 @@ +{ + "config": { + "type": "radar", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [ + { + "backgroundColor": "rgba(0, 0, 192, 0.25)", + "data": [null, null, 2, 4, 2, 1, -1, 1, 2] + }, + { + "backgroundColor": "rgba(0, 192, 0, 0.25)", + "data": [4, 2, null, 3, 2.5, null, -2, 1.5, 3] + }, + { + "backgroundColor": "rgba(192, 0, 0, 0.25)", + "data": [3.5, 2, 1, 2.5, -2, 3, -1, null, null] + }, + { + "backgroundColor": "rgba(128, 0, 128, 0.25)", + "data": [5, 6, 5, -2, -4, -3, 4, 2, 4.5] + } + ] + }, + "options": { + "responsive": false, + "spanGaps": false, + "scales": { + "r": { + "display": false + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "borderColor": "transparent", + "tension": 0.5, + "fill": "origin" + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 256 + } + } +} diff --git a/test/fixtures/plugin.filler/radar/boundary/origin-spline.png b/test/fixtures/plugin.filler/radar/boundary/origin-spline.png new file mode 100644 index 00000000000..05f8f345015 Binary files /dev/null and b/test/fixtures/plugin.filler/radar/boundary/origin-spline.png differ diff --git a/test/fixtures/plugin.filler/radar/boundary/origin.json b/test/fixtures/plugin.filler/radar/boundary/origin.json new file mode 100644 index 00000000000..4c206813414 --- /dev/null +++ b/test/fixtures/plugin.filler/radar/boundary/origin.json @@ -0,0 +1,55 @@ +{ + "config": { + "type": "radar", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [ + { + "backgroundColor": "rgba(0, 0, 192, 0.25)", + "data": [null, null, 2, 3, 4, -4, -2, 1, 0] + }, + { + "backgroundColor": "rgba(0, 192, 0, 0.25)", + "data": [6, 2, null, 4, 5, null, null, 2, 1] + }, + { + "backgroundColor": "rgba(192, 0, 0, 0.25)", + "data": [7, 3, 4, 5, 6, 1, 4, null, null] + }, + { + "backgroundColor": "rgba(0, 64, 192, 0.25)", + "data": [8, 7, 6, -6, -4, -6, 4, 5, 8] + } + ] + }, + "options": { + "responsive": false, + "spanGaps": false, + "scales": { + "r": { + "display": false + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "borderColor": "transparent", + "fill": "origin" + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 256 + } + } +} diff --git a/test/fixtures/plugin.filler/radar/boundary/origin.png b/test/fixtures/plugin.filler/radar/boundary/origin.png new file mode 100644 index 00000000000..2194bc4ae98 Binary files /dev/null and b/test/fixtures/plugin.filler/radar/boundary/origin.png differ diff --git a/test/fixtures/plugin.filler/radar/boundary/start-circular.json b/test/fixtures/plugin.filler/radar/boundary/start-circular.json new file mode 100644 index 00000000000..51d08ad4a6c --- /dev/null +++ b/test/fixtures/plugin.filler/radar/boundary/start-circular.json @@ -0,0 +1,58 @@ +{ + "config": { + "type": "radar", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [ + { + "backgroundColor": "rgba(0, 0, 255, 0.25)", + "data": [null, null, 2, 3, 4, -4, -2, 1, 0] + }, + { + "backgroundColor": "rgba(0, 255, 0, 0.25)", + "data": [6, 2, null, 4, 5, null, null, 2, 1] + }, + { + "backgroundColor": "rgba(255, 0, 0, 0.25)", + "data": [7, 3, 4, 5, 6, 1, 4, null, null] + }, + { + "backgroundColor": "rgba(0, 0, 255, 0.25)", + "data": [8, 7, 6, -6, -4, -6, 4, 5, 8] + } + ] + }, + "options": { + "responsive": false, + "spanGaps": false, + "scales": { + "r": { + "display": false, + "grid": { + "circular": true + } + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "borderColor": "transparent", + "fill": "start" + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 256 + } + } +} diff --git a/test/fixtures/plugin.filler/radar/boundary/start-circular.png b/test/fixtures/plugin.filler/radar/boundary/start-circular.png new file mode 100644 index 00000000000..b0f8f8f0a96 Binary files /dev/null and b/test/fixtures/plugin.filler/radar/boundary/start-circular.png differ diff --git a/test/fixtures/plugin.filler/radar/boundary/start-span.json b/test/fixtures/plugin.filler/radar/boundary/start-span.json new file mode 100644 index 00000000000..66a0c83f7a6 --- /dev/null +++ b/test/fixtures/plugin.filler/radar/boundary/start-span.json @@ -0,0 +1,55 @@ +{ + "config": { + "type": "radar", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [ + { + "backgroundColor": "rgba(0, 0, 255, 0.25)", + "data": [null, null, 2, 3, 4, -4, -2, 1, 0] + }, + { + "backgroundColor": "rgba(0, 255, 0, 0.25)", + "data": [6, 2, null, 4, 5, null, null, 2, 1] + }, + { + "backgroundColor": "rgba(255, 0, 0, 0.25)", + "data": [7, 3, 4, 5, 6, 1, 4, null, null] + }, + { + "backgroundColor": "rgba(0, 0, 255, 0.25)", + "data": [8, 7, 6, -6, -4, -6, 4, 5, 8] + } + ] + }, + "options": { + "responsive": false, + "spanGaps": true, + "scales": { + "r": { + "display": false + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "borderColor": "transparent", + "fill": "start" + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 256 + } + } +} diff --git a/test/fixtures/plugin.filler/radar/boundary/start-span.png b/test/fixtures/plugin.filler/radar/boundary/start-span.png new file mode 100644 index 00000000000..d97e6919ae6 Binary files /dev/null and b/test/fixtures/plugin.filler/radar/boundary/start-span.png differ diff --git a/test/fixtures/plugin.filler/radar/boundary/start.json b/test/fixtures/plugin.filler/radar/boundary/start.json new file mode 100644 index 00000000000..faede3e5db5 --- /dev/null +++ b/test/fixtures/plugin.filler/radar/boundary/start.json @@ -0,0 +1,54 @@ +{ + "config": { + "type": "radar", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [ + { + "backgroundColor": "rgba(0, 0, 255, 0.25)", + "data": [null, null, 2, 3, 4, -4, -2, 1, 0] + }, + { + "backgroundColor": "rgba(0, 255, 0, 0.25)", + "data": [6, 2, null, 4, 5, null, null, 2, 1] + }, + { + "backgroundColor": "rgba(255, 0, 0, 0.25)", + "data": [7, 3, 4, 5, 6, 1, 4, null, null] + }, + { + "backgroundColor": "rgba(0, 0, 255, 0.25)", + "data": [8, 7, 6, -6, -4, -6, 4, 5, 8] + } + ] + }, + "options": { + "responsive": false, + "spanGaps": false, + "plugins": { + "legend": false, + "title": false + }, + "scales": { + "r": { + "display": false + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "borderColor": "transparent", + "fill": "start" + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 256 + } + } +} diff --git a/test/fixtures/plugin.filler/radar/boundary/start.png b/test/fixtures/plugin.filler/radar/boundary/start.png new file mode 100644 index 00000000000..749913145a1 Binary files /dev/null and b/test/fixtures/plugin.filler/radar/boundary/start.png differ diff --git a/test/fixtures/plugin.filler/radar/dataset/border.json b/test/fixtures/plugin.filler/radar/dataset/border.json new file mode 100644 index 00000000000..b8774b38083 --- /dev/null +++ b/test/fixtures/plugin.filler/radar/dataset/border.json @@ -0,0 +1,65 @@ +{ + "config": { + "type": "radar", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [ + { + "backgroundColor": "rgba(255, 0, 0, 0.25)", + "data": [null, null, 0, -1, 0, 1, 0, -1, 0], + "fill": 1 + }, + { + "backgroundColor": "rgba(0, 255, 0, 0.25)", + "data": [1, 0, null, 1, 0, null, -1, 0, 1], + "fill": "+1" + }, + { + "backgroundColor": "rgba(0, 0, 255, 0.25)", + "data": [0, 2, 0, -2, 0, 2, 0], + "fill": 3 + }, + { + "backgroundColor": "rgba(255, 0, 255, 0.25)", + "data": [2, 0, -2, 0, 2, 0, -2, 0, 2], + "fill": "-2" + }, + { + "backgroundColor": "rgba(255, 255, 0, 0.25)", + "data": [3, 1, -1, -3, -1, 1, 3, 1, -1], + "fill": "-1" + } + ] + }, + "options": { + "responsive": false, + "spanGaps": false, + "scales": { + "r": { + "display": false + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "borderColor": "black", + "borderWidth": 5, + "tension": 0 + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 256 + } + } +} diff --git a/test/fixtures/plugin.filler/radar/dataset/border.png b/test/fixtures/plugin.filler/radar/dataset/border.png new file mode 100644 index 00000000000..32243c2bb67 Binary files /dev/null and b/test/fixtures/plugin.filler/radar/dataset/border.png differ diff --git a/test/fixtures/plugin.filler/radar/dataset/default.json b/test/fixtures/plugin.filler/radar/dataset/default.json new file mode 100644 index 00000000000..2a35cfe7f2f --- /dev/null +++ b/test/fixtures/plugin.filler/radar/dataset/default.json @@ -0,0 +1,64 @@ +{ + "config": { + "type": "radar", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [ + { + "backgroundColor": "rgba(255, 0, 0, 0.25)", + "data": [null, null, 0, -1, 0, 1, 0, -1, 0], + "fill": 1 + }, + { + "backgroundColor": "rgba(0, 255, 0, 0.25)", + "data": [1, 0, null, 1, 0, null, -1, 0, 1], + "fill": "+1" + }, + { + "backgroundColor": "rgba(0, 0, 255, 0.25)", + "data": [0, 2, 0, -2, 0, 2, 0], + "fill": 3 + }, + { + "backgroundColor": "rgba(255, 0, 255, 0.25)", + "data": [2, 0, -2, 0, 2, 0, -2, 0, 2], + "fill": "-2" + }, + { + "backgroundColor": "rgba(255, 255, 0, 0.25)", + "data": [3, 1, -1, -3, -1, 1, 3, 1, -1], + "fill": "-1" + } + ] + }, + "options": { + "responsive": false, + "spanGaps": false, + "scales": { + "r": { + "display": false + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "borderColor": "transparent", + "tension": 0 + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 256 + } + } +} diff --git a/test/fixtures/plugin.filler/radar/dataset/default.png b/test/fixtures/plugin.filler/radar/dataset/default.png new file mode 100644 index 00000000000..a6cb6593ea6 Binary files /dev/null and b/test/fixtures/plugin.filler/radar/dataset/default.png differ diff --git a/test/fixtures/plugin.filler/radar/dataset/order.js b/test/fixtures/plugin.filler/radar/dataset/order.js new file mode 100644 index 00000000000..9747bb37594 --- /dev/null +++ b/test/fixtures/plugin.filler/radar/dataset/order.js @@ -0,0 +1,33 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: ['English', 'Maths', 'Physics', 'Chemistry', 'Biology', 'History'], + datasets: [ + { + order: 1, + borderColor: '#D50000', + backgroundColor: 'rgba(245, 205, 121,0.5)', + data: [65, 75, 70, 80, 60, 80] + }, + { + order: 0, + backgroundColor: 'rgba(0, 168, 255,1)', + data: [54, 65, 60, 70, 70, 75] + } + ] + }, + options: { + plugins: { + legend: false, + title: false, + tooltip: false + }, + scales: { + r: { + display: false + } + } + } + } +}; diff --git a/test/fixtures/plugin.filler/radar/dataset/order.png b/test/fixtures/plugin.filler/radar/dataset/order.png new file mode 100644 index 00000000000..a52b4707169 Binary files /dev/null and b/test/fixtures/plugin.filler/radar/dataset/order.png differ diff --git a/test/fixtures/plugin.filler/radar/dataset/span.json b/test/fixtures/plugin.filler/radar/dataset/span.json new file mode 100644 index 00000000000..aaff57cf439 --- /dev/null +++ b/test/fixtures/plugin.filler/radar/dataset/span.json @@ -0,0 +1,64 @@ +{ + "config": { + "type": "radar", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [ + { + "backgroundColor": "rgba(255, 0, 0, 0.25)", + "data": [null, null, 0, -1, 0, 1, 0, -1, 0], + "fill": 1 + }, + { + "backgroundColor": "rgba(0, 255, 0, 0.25)", + "data": [1, 0, null, 1, 0, null, -1, 0, 1], + "fill": "+1" + }, + { + "backgroundColor": "rgba(0, 0, 255, 0.25)", + "data": [0, 2, 0, -2, 0, 2, 0], + "fill": 3 + }, + { + "backgroundColor": "rgba(255, 0, 255, 0.25)", + "data": [2, 0, -2, 0, 2, 0, -2, 0, 2], + "fill": "-2" + }, + { + "backgroundColor": "rgba(255, 255, 0, 0.25)", + "data": [3, 1, -1, -3, -1, 1, 3, 1, -1], + "fill": "-1" + } + ] + }, + "options": { + "responsive": false, + "spanGaps": true, + "scales": { + "r": { + "display": false + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "borderColor": "transparent", + "tension": 0 + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 256 + } + } +} diff --git a/test/fixtures/plugin.filler/radar/dataset/span.png b/test/fixtures/plugin.filler/radar/dataset/span.png new file mode 100644 index 00000000000..c7da86373bc Binary files /dev/null and b/test/fixtures/plugin.filler/radar/dataset/span.png differ diff --git a/test/fixtures/plugin.filler/radar/dataset/spline.json b/test/fixtures/plugin.filler/radar/dataset/spline.json new file mode 100644 index 00000000000..ec34870e67d --- /dev/null +++ b/test/fixtures/plugin.filler/radar/dataset/spline.json @@ -0,0 +1,63 @@ +{ + "config": { + "type": "radar", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [ + { + "backgroundColor": "rgba(255, 0, 0, 0.25)", + "data": [null, null, 0, -1, 0, 1, 0, -1, 0], + "fill": 1 + }, + { + "backgroundColor": "rgba(0, 255, 0, 0.25)", + "data": [1, 0, null, 1, 0, null, -1, 0, 1], + "fill": "+1" + }, + { + "backgroundColor": "rgba(0, 0, 255, 0.25)", + "data": [0, 2, 0, -2, 0, 2, 0], + "fill": 3 + }, + { + "backgroundColor": "rgba(255, 0, 255, 0.25)", + "data": [2, 0, -2, 0, 2, 0, -2, 0, 2], + "fill": "-2" + }, + { + "backgroundColor": "rgba(255, 255, 0, 0.25)", + "data": [3, 1, -1, -3, -1, 1, 3, 1, -1], + "fill": "-1" + } + ] + }, + "options": { + "responsive": false, + "spanGaps": false, + "plugins": { + "legend": false, + "title": false + }, + "scales": { + "r": { + "display": false + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "borderColor": "transparent", + "tension": 0.5 + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 256 + } + } +} diff --git a/test/fixtures/plugin.filler/radar/dataset/spline.png b/test/fixtures/plugin.filler/radar/dataset/spline.png new file mode 100644 index 00000000000..c9e9b0b9f06 Binary files /dev/null and b/test/fixtures/plugin.filler/radar/dataset/spline.png differ diff --git a/test/fixtures/plugin.filler/radar/value.json b/test/fixtures/plugin.filler/radar/value.json new file mode 100644 index 00000000000..8a62822f54d --- /dev/null +++ b/test/fixtures/plugin.filler/radar/value.json @@ -0,0 +1,46 @@ +{ + "config": { + "type": "radar", + "data": { + "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], + "datasets": [ + { + "backgroundColor": "rgba(0, 0, 192, 0.25)", + "data": [0, -4, 2, 4, 2, 1, -1, 1, 2] + } + ] + }, + "options": { + "responsive": false, + "spanGaps": false, + "scales": { + "r": { + "display": false, + "grid": { + "circular": true + } + } + }, + "elements": { + "point": { + "radius": 0 + }, + "line": { + "borderColor": "transparent", + "fill": { "value": 3 } + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 256 + } + } +} diff --git a/test/fixtures/plugin.filler/radar/value.png b/test/fixtures/plugin.filler/radar/value.png new file mode 100644 index 00000000000..f74ee51e4c4 Binary files /dev/null and b/test/fixtures/plugin.filler/radar/value.png differ diff --git a/test/fixtures/plugin.legend/borderRadius/legend-border-radius.js b/test/fixtures/plugin.legend/borderRadius/legend-border-radius.js new file mode 100644 index 00000000000..d765134ba30 --- /dev/null +++ b/test/fixtures/plugin.legend/borderRadius/legend-border-radius.js @@ -0,0 +1,55 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], + datasets: [ + { + label: '# of Votes', + data: [12, 19, 3, 5, 2, 3], + borderWidth: 1, + borderColor: '#FF0000', + backgroundColor: '#00FF00', + }, + { + label: '# of Points', + data: [7, 11, 5, 8, 3, 7], + borderWidth: 2, + borderColor: '#FF00FF', + backgroundColor: '#0000FF', + } + ] + }, + options: { + scales: { + x: {display: false}, + y: {display: false} + }, + plugins: { + title: false, + tooltip: false, + filler: false, + legend: { + labels: { + generateLabels: (chart) => { + const items = Chart.defaults.plugins.legend.labels.generateLabels(chart); + + for (const item of items) { + item.borderRadius = 5; + } + + return items; + } + } + } + } + } + }, + options: { + spriteText: true, + canvas: { + width: 512, + height: 256 + } + } +}; diff --git a/test/fixtures/plugin.legend/borderRadius/legend-border-radius.png b/test/fixtures/plugin.legend/borderRadius/legend-border-radius.png new file mode 100644 index 00000000000..600c3db1f04 Binary files /dev/null and b/test/fixtures/plugin.legend/borderRadius/legend-border-radius.png differ diff --git a/test/fixtures/plugin.legend/horizontal-rtl-hitbox.js b/test/fixtures/plugin.legend/horizontal-rtl-hitbox.js new file mode 100644 index 00000000000..99218cec23b --- /dev/null +++ b/test/fixtures/plugin.legend/horizontal-rtl-hitbox.js @@ -0,0 +1,52 @@ +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/9278', + config: { + type: 'pie', + data: { + labels: ['aaa', 'bb', 'c'], + datasets: [{ + data: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + backgroundColor: 'red' + }] + }, + options: { + plugins: { + legend: { + position: 'top', + rtl: 'true', + } + }, + layout: { + padding: { + top: 50, + left: 30, + right: 30, + bottom: 50 + } + } + }, + plugins: [{ + id: 'legend-hit-box', + afterDraw(chart) { + const ctx = chart.ctx; + ctx.save(); + ctx.strokeStyle = 'green'; + ctx.lineWidth = 1; + + const legend = chart.legend; + legend.legendHitBoxes.forEach(box => { + ctx.strokeRect(box.left, box.top, box.width, box.height); + }); + + ctx.restore(); + } + }] + }, + options: { + spriteText: true, + canvas: { + width: 400, + height: 300 + }, + } +}; diff --git a/test/fixtures/plugin.legend/horizontal-rtl-hitbox.png b/test/fixtures/plugin.legend/horizontal-rtl-hitbox.png new file mode 100644 index 00000000000..a2ae13e45cb Binary files /dev/null and b/test/fixtures/plugin.legend/horizontal-rtl-hitbox.png differ diff --git a/test/fixtures/plugin.legend/label-textAlign/center.js b/test/fixtures/plugin.legend/label-textAlign/center.js new file mode 100644 index 00000000000..e46fbfdc683 --- /dev/null +++ b/test/fixtures/plugin.legend/label-textAlign/center.js @@ -0,0 +1,30 @@ +module.exports = { + config: { + type: 'pie', + data: { + labels: ['aaaa', 'bb', 'c'], + datasets: [ + { + data: [1, 2, 3] + } + ] + }, + options: { + plugins: { + legend: { + position: 'right', + labels: { + textAlign: 'center' + } + } + } + } + }, + options: { + spriteText: true, + canvas: { + width: 256, + height: 256 + } + } +}; diff --git a/test/fixtures/plugin.legend/label-textAlign/center.png b/test/fixtures/plugin.legend/label-textAlign/center.png new file mode 100644 index 00000000000..e0fb8ff11ba Binary files /dev/null and b/test/fixtures/plugin.legend/label-textAlign/center.png differ diff --git a/test/fixtures/plugin.legend/label-textAlign/horizontal-left.js b/test/fixtures/plugin.legend/label-textAlign/horizontal-left.js new file mode 100644 index 00000000000..46458c532e4 --- /dev/null +++ b/test/fixtures/plugin.legend/label-textAlign/horizontal-left.js @@ -0,0 +1,30 @@ +module.exports = { + config: { + type: 'pie', + data: { + labels: ['aaaa', 'bb', 'c'], + datasets: [ + { + data: [1, 2, 3] + } + ] + }, + options: { + plugins: { + legend: { + position: 'top', + labels: { + textAlign: 'left' + } + } + } + } + }, + options: { + spriteText: true, + canvas: { + width: 256, + height: 256 + } + } +}; diff --git a/test/fixtures/plugin.legend/label-textAlign/horizontal-left.png b/test/fixtures/plugin.legend/label-textAlign/horizontal-left.png new file mode 100644 index 00000000000..84907658eba Binary files /dev/null and b/test/fixtures/plugin.legend/label-textAlign/horizontal-left.png differ diff --git a/test/fixtures/plugin.legend/label-textAlign/horizontal-right.js b/test/fixtures/plugin.legend/label-textAlign/horizontal-right.js new file mode 100644 index 00000000000..729c663ac0a --- /dev/null +++ b/test/fixtures/plugin.legend/label-textAlign/horizontal-right.js @@ -0,0 +1,30 @@ +module.exports = { + config: { + type: 'pie', + data: { + labels: ['aaaa', 'bb', 'c'], + datasets: [ + { + data: [1, 2, 3] + } + ] + }, + options: { + plugins: { + legend: { + position: 'top', + labels: { + textAlign: 'right' + } + } + } + } + }, + options: { + spriteText: true, + canvas: { + width: 256, + height: 256 + } + } +}; diff --git a/test/fixtures/plugin.legend/label-textAlign/horizontal-right.png b/test/fixtures/plugin.legend/label-textAlign/horizontal-right.png new file mode 100644 index 00000000000..84907658eba Binary files /dev/null and b/test/fixtures/plugin.legend/label-textAlign/horizontal-right.png differ diff --git a/test/fixtures/plugin.legend/label-textAlign/horizontal-rtl-left.js b/test/fixtures/plugin.legend/label-textAlign/horizontal-rtl-left.js new file mode 100644 index 00000000000..5fff9c72a2b --- /dev/null +++ b/test/fixtures/plugin.legend/label-textAlign/horizontal-rtl-left.js @@ -0,0 +1,31 @@ +module.exports = { + config: { + type: 'pie', + data: { + labels: ['aaaa', 'bb', 'c'], + datasets: [ + { + data: [1, 2, 3] + } + ] + }, + options: { + plugins: { + legend: { + position: 'top', + rtl: true, + labels: { + textAlign: 'left' + } + } + } + } + }, + options: { + spriteText: true, + canvas: { + width: 256, + height: 256 + } + } +}; diff --git a/test/fixtures/plugin.legend/label-textAlign/horizontal-rtl-left.png b/test/fixtures/plugin.legend/label-textAlign/horizontal-rtl-left.png new file mode 100644 index 00000000000..79642112e73 Binary files /dev/null and b/test/fixtures/plugin.legend/label-textAlign/horizontal-rtl-left.png differ diff --git a/test/fixtures/plugin.legend/label-textAlign/horizontal-rtl-right.js b/test/fixtures/plugin.legend/label-textAlign/horizontal-rtl-right.js new file mode 100644 index 00000000000..ae900510368 --- /dev/null +++ b/test/fixtures/plugin.legend/label-textAlign/horizontal-rtl-right.js @@ -0,0 +1,31 @@ +module.exports = { + config: { + type: 'pie', + data: { + labels: ['aaaa', 'bb', 'c'], + datasets: [ + { + data: [1, 2, 3] + } + ] + }, + options: { + plugins: { + legend: { + rtl: true, + position: 'top', + labels: { + textAlign: 'right' + } + } + } + } + }, + options: { + spriteText: true, + canvas: { + width: 256, + height: 256 + } + } +}; diff --git a/test/fixtures/plugin.legend/label-textAlign/horizontal-rtl-right.png b/test/fixtures/plugin.legend/label-textAlign/horizontal-rtl-right.png new file mode 100644 index 00000000000..79642112e73 Binary files /dev/null and b/test/fixtures/plugin.legend/label-textAlign/horizontal-rtl-right.png differ diff --git a/test/fixtures/plugin.legend/label-textAlign/left.js b/test/fixtures/plugin.legend/label-textAlign/left.js new file mode 100644 index 00000000000..c587f938515 --- /dev/null +++ b/test/fixtures/plugin.legend/label-textAlign/left.js @@ -0,0 +1,30 @@ +module.exports = { + config: { + type: 'pie', + data: { + labels: ['aaaa', 'bb', 'c'], + datasets: [ + { + data: [1, 2, 3] + } + ] + }, + options: { + plugins: { + legend: { + position: 'right', + labels: { + textAlign: 'left' + } + } + } + } + }, + options: { + spriteText: true, + canvas: { + width: 256, + height: 256 + } + } +}; diff --git a/test/fixtures/plugin.legend/label-textAlign/left.png b/test/fixtures/plugin.legend/label-textAlign/left.png new file mode 100644 index 00000000000..fd737d28c1b Binary files /dev/null and b/test/fixtures/plugin.legend/label-textAlign/left.png differ diff --git a/test/fixtures/plugin.legend/label-textAlign/right.js b/test/fixtures/plugin.legend/label-textAlign/right.js new file mode 100644 index 00000000000..b745f9624ca --- /dev/null +++ b/test/fixtures/plugin.legend/label-textAlign/right.js @@ -0,0 +1,30 @@ +module.exports = { + config: { + type: 'pie', + data: { + labels: ['aaaa', 'bb', 'c'], + datasets: [ + { + data: [1, 2, 3] + } + ] + }, + options: { + plugins: { + legend: { + position: 'right', + labels: { + textAlign: 'right' + } + } + } + } + }, + options: { + spriteText: true, + canvas: { + width: 256, + height: 256 + } + } +}; diff --git a/test/fixtures/plugin.legend/label-textAlign/right.png b/test/fixtures/plugin.legend/label-textAlign/right.png new file mode 100644 index 00000000000..cf5feff64b2 Binary files /dev/null and b/test/fixtures/plugin.legend/label-textAlign/right.png differ diff --git a/test/fixtures/plugin.legend/label-textAlign/rtl-center.js b/test/fixtures/plugin.legend/label-textAlign/rtl-center.js new file mode 100644 index 00000000000..dd86154bfbc --- /dev/null +++ b/test/fixtures/plugin.legend/label-textAlign/rtl-center.js @@ -0,0 +1,31 @@ +module.exports = { + config: { + type: 'pie', + data: { + labels: ['aaaa', 'bb', 'c'], + datasets: [ + { + data: [1, 2, 3] + } + ] + }, + options: { + plugins: { + legend: { + position: 'right', + rtl: true, + labels: { + textAlign: 'center' + } + } + } + } + }, + options: { + spriteText: true, + canvas: { + width: 256, + height: 256 + } + } +}; diff --git a/test/fixtures/plugin.legend/label-textAlign/rtl-center.png b/test/fixtures/plugin.legend/label-textAlign/rtl-center.png new file mode 100644 index 00000000000..10d42be4bb6 Binary files /dev/null and b/test/fixtures/plugin.legend/label-textAlign/rtl-center.png differ diff --git a/test/fixtures/plugin.legend/label-textAlign/rtl-left.js b/test/fixtures/plugin.legend/label-textAlign/rtl-left.js new file mode 100644 index 00000000000..96c0bcdf5e6 --- /dev/null +++ b/test/fixtures/plugin.legend/label-textAlign/rtl-left.js @@ -0,0 +1,31 @@ +module.exports = { + config: { + type: 'pie', + data: { + labels: ['aaaa', 'bb', 'c'], + datasets: [ + { + data: [1, 2, 3] + } + ] + }, + options: { + plugins: { + legend: { + position: 'right', + rtl: true, + labels: { + textAlign: 'left' + } + } + } + } + }, + options: { + spriteText: true, + canvas: { + width: 256, + height: 256 + } + } +}; diff --git a/test/fixtures/plugin.legend/label-textAlign/rtl-left.png b/test/fixtures/plugin.legend/label-textAlign/rtl-left.png new file mode 100644 index 00000000000..294d61dd6fb Binary files /dev/null and b/test/fixtures/plugin.legend/label-textAlign/rtl-left.png differ diff --git a/test/fixtures/plugin.legend/label-textAlign/rtl-right.js b/test/fixtures/plugin.legend/label-textAlign/rtl-right.js new file mode 100644 index 00000000000..a2f342af758 --- /dev/null +++ b/test/fixtures/plugin.legend/label-textAlign/rtl-right.js @@ -0,0 +1,31 @@ +module.exports = { + config: { + type: 'pie', + data: { + labels: ['aaaa', 'bb', 'c'], + datasets: [ + { + data: [1, 2, 3] + } + ] + }, + options: { + plugins: { + legend: { + rtl: true, + position: 'right', + labels: { + textAlign: 'right' + } + } + } + } + }, + options: { + spriteText: true, + canvas: { + width: 256, + height: 256 + } + } +}; diff --git a/test/fixtures/plugin.legend/label-textAlign/rtl-right.png b/test/fixtures/plugin.legend/label-textAlign/rtl-right.png new file mode 100644 index 00000000000..80789d65f03 Binary files /dev/null and b/test/fixtures/plugin.legend/label-textAlign/rtl-right.png differ diff --git a/test/fixtures/plugin.legend/legend-doughnut-bottom-center-mulitiline.json b/test/fixtures/plugin.legend/legend-doughnut-bottom-center-mulitiline.json new file mode 100644 index 00000000000..5550ef7460b --- /dev/null +++ b/test/fixtures/plugin.legend/legend-doughnut-bottom-center-mulitiline.json @@ -0,0 +1,27 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], + "datasets": [{ + "data": [10, 20, 30, 40, 50, 60, 70, 10, 20, 30, 40, 50, 60, 70, 10, 20, 30, 20, 10], + "backgroundColor": "#00ff00", + "borderWidth": 0 + }] + }, + "options": { + "plugins": { + "legend": { + "position": "bottom", + "align": "center" + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/legend-doughnut-bottom-center-mulitiline.png b/test/fixtures/plugin.legend/legend-doughnut-bottom-center-mulitiline.png new file mode 100644 index 00000000000..bad507684b4 Binary files /dev/null and b/test/fixtures/plugin.legend/legend-doughnut-bottom-center-mulitiline.png differ diff --git a/test/fixtures/plugin.legend/legend-doughnut-bottom-center-single.json b/test/fixtures/plugin.legend/legend-doughnut-bottom-center-single.json new file mode 100644 index 00000000000..b5fcfb92b3c --- /dev/null +++ b/test/fixtures/plugin.legend/legend-doughnut-bottom-center-single.json @@ -0,0 +1,27 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": [""], + "datasets": [{ + "data": [10], + "backgroundColor": "#00ff00", + "borderWidth": 0 + }] + }, + "options": { + "plugins": { + "legend": { + "position": "bottom", + "align": "center" + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/legend-doughnut-bottom-center-single.png b/test/fixtures/plugin.legend/legend-doughnut-bottom-center-single.png new file mode 100644 index 00000000000..139a16b863d Binary files /dev/null and b/test/fixtures/plugin.legend/legend-doughnut-bottom-center-single.png differ diff --git a/test/fixtures/plugin.legend/legend-doughnut-bottom-end-mulitiline.json b/test/fixtures/plugin.legend/legend-doughnut-bottom-end-mulitiline.json new file mode 100644 index 00000000000..bc916c9318c --- /dev/null +++ b/test/fixtures/plugin.legend/legend-doughnut-bottom-end-mulitiline.json @@ -0,0 +1,27 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], + "datasets": [{ + "data": [10, 20, 30, 40, 50, 60, 70, 10, 20, 30, 40, 50, 60, 70, 10], + "backgroundColor": "#00ff00", + "borderWidth": 0 + }] + }, + "options": { + "plugins": { + "legend": { + "position": "bottom", + "align": "end" + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/legend-doughnut-bottom-end-mulitiline.png b/test/fixtures/plugin.legend/legend-doughnut-bottom-end-mulitiline.png new file mode 100644 index 00000000000..e7fc84b85f0 Binary files /dev/null and b/test/fixtures/plugin.legend/legend-doughnut-bottom-end-mulitiline.png differ diff --git a/test/fixtures/plugin.legend/legend-doughnut-bottom-start-mulitiline.json b/test/fixtures/plugin.legend/legend-doughnut-bottom-start-mulitiline.json new file mode 100644 index 00000000000..c1e94d91b80 --- /dev/null +++ b/test/fixtures/plugin.legend/legend-doughnut-bottom-start-mulitiline.json @@ -0,0 +1,27 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], + "datasets": [{ + "data": [10, 20, 30, 40, 50, 60, 70, 10, 20, 30, 40, 50, 60, 70, 10], + "backgroundColor": "#00ff00", + "borderWidth": 0 + }] + }, + "options": { + "plugins": { + "legend": { + "position": "bottom", + "align": "start" + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/legend-doughnut-bottom-start-mulitiline.png b/test/fixtures/plugin.legend/legend-doughnut-bottom-start-mulitiline.png new file mode 100644 index 00000000000..32700c17d81 Binary files /dev/null and b/test/fixtures/plugin.legend/legend-doughnut-bottom-start-mulitiline.png differ diff --git a/test/fixtures/plugin.legend/legend-doughnut-left-center-mulitiline.json b/test/fixtures/plugin.legend/legend-doughnut-left-center-mulitiline.json new file mode 100644 index 00000000000..8662d9080de --- /dev/null +++ b/test/fixtures/plugin.legend/legend-doughnut-left-center-mulitiline.json @@ -0,0 +1,27 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], + "datasets": [{ + "data": [10, 20, 30, 40, 50, 60, 70, 10, 20, 30, 40, 50, 60, 70, 10, 20, 30], + "backgroundColor": "#00ff00", + "borderWidth": 0 + }] + }, + "options": { + "plugins": { + "legend": { + "position": "left", + "align": "center" + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/legend-doughnut-left-center-mulitiline.png b/test/fixtures/plugin.legend/legend-doughnut-left-center-mulitiline.png new file mode 100644 index 00000000000..5160e356f74 Binary files /dev/null and b/test/fixtures/plugin.legend/legend-doughnut-left-center-mulitiline.png differ diff --git a/test/fixtures/plugin.legend/legend-doughnut-left-center-single.json b/test/fixtures/plugin.legend/legend-doughnut-left-center-single.json new file mode 100644 index 00000000000..154783fc038 --- /dev/null +++ b/test/fixtures/plugin.legend/legend-doughnut-left-center-single.json @@ -0,0 +1,27 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": [""], + "datasets": [{ + "data": [10], + "backgroundColor": "#00ff00", + "borderWidth": 0 + }] + }, + "options": { + "plugins": { + "legend": { + "position": "left", + "align": "center" + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/legend-doughnut-left-center-single.png b/test/fixtures/plugin.legend/legend-doughnut-left-center-single.png new file mode 100644 index 00000000000..72072bfbefd Binary files /dev/null and b/test/fixtures/plugin.legend/legend-doughnut-left-center-single.png differ diff --git a/test/fixtures/plugin.legend/legend-doughnut-left-default-center.json b/test/fixtures/plugin.legend/legend-doughnut-left-default-center.json new file mode 100644 index 00000000000..35303dd1b90 --- /dev/null +++ b/test/fixtures/plugin.legend/legend-doughnut-left-default-center.json @@ -0,0 +1,26 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["", "", "", "", "", ""], + "datasets": [{ + "data": [10, 20, 30, 40, 50], + "backgroundColor": "#00ff00", + "borderWidth": 0 + }] + }, + "options": { + "plugins": { + "legend": { + "position": "left" + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/legend-doughnut-left-default-center.png b/test/fixtures/plugin.legend/legend-doughnut-left-default-center.png new file mode 100644 index 00000000000..3d9829833e2 Binary files /dev/null and b/test/fixtures/plugin.legend/legend-doughnut-left-default-center.png differ diff --git a/test/fixtures/plugin.legend/legend-doughnut-left-end-mulitiline.json b/test/fixtures/plugin.legend/legend-doughnut-left-end-mulitiline.json new file mode 100644 index 00000000000..e866ffe5a56 --- /dev/null +++ b/test/fixtures/plugin.legend/legend-doughnut-left-end-mulitiline.json @@ -0,0 +1,27 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], + "datasets": [{ + "data": [10, 20, 30, 40, 50, 60, 70, 10, 20, 30, 40, 50, 60, 70, 10, 20, 30], + "backgroundColor": "#00ff00", + "borderWidth": 0 + }] + }, + "options": { + "plugins": { + "legend": { + "position": "left", + "align": "end" + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/legend-doughnut-left-end-mulitiline.png b/test/fixtures/plugin.legend/legend-doughnut-left-end-mulitiline.png new file mode 100644 index 00000000000..46c6014eb6a Binary files /dev/null and b/test/fixtures/plugin.legend/legend-doughnut-left-end-mulitiline.png differ diff --git a/test/fixtures/plugin.legend/legend-doughnut-left-start-mulitiline.json b/test/fixtures/plugin.legend/legend-doughnut-left-start-mulitiline.json new file mode 100644 index 00000000000..f3abdef4310 --- /dev/null +++ b/test/fixtures/plugin.legend/legend-doughnut-left-start-mulitiline.json @@ -0,0 +1,27 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], + "datasets": [{ + "data": [10, 20, 30, 40, 50, 60, 70, 10, 20, 30, 40, 50, 60, 70, 10, 20, 30], + "backgroundColor": "#00ff00", + "borderWidth": 0 + }] + }, + "options": { + "plugins": { + "legend": { + "position": "left", + "align": "start" + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/legend-doughnut-left-start-mulitiline.png b/test/fixtures/plugin.legend/legend-doughnut-left-start-mulitiline.png new file mode 100644 index 00000000000..91c0c31b0a5 Binary files /dev/null and b/test/fixtures/plugin.legend/legend-doughnut-left-start-mulitiline.png differ diff --git a/test/fixtures/plugin.legend/legend-doughnut-point-style.json b/test/fixtures/plugin.legend/legend-doughnut-point-style.json new file mode 100644 index 00000000000..6e644a39efd --- /dev/null +++ b/test/fixtures/plugin.legend/legend-doughnut-point-style.json @@ -0,0 +1,29 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": [""], + "datasets": [{ + "data": [10], + "backgroundColor": "#00ff00", + "borderWidth": 0 + }] + }, + "options": { + "plugins": { + "legend": { + "labels": { + "pointStyle": "triangle", + "usePointStyle": true + } + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/legend-doughnut-point-style.png b/test/fixtures/plugin.legend/legend-doughnut-point-style.png new file mode 100644 index 00000000000..b0b216b0b25 Binary files /dev/null and b/test/fixtures/plugin.legend/legend-doughnut-point-style.png differ diff --git a/test/fixtures/plugin.legend/legend-doughnut-right-center-mulitiline-labels.json b/test/fixtures/plugin.legend/legend-doughnut-right-center-mulitiline-labels.json new file mode 100644 index 00000000000..4c271597251 --- /dev/null +++ b/test/fixtures/plugin.legend/legend-doughnut-right-center-mulitiline-labels.json @@ -0,0 +1,28 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["Example Label", ["I like these colors", "Red", "Green", "Blue", "Yellow"], "Example Label", "Example Label", "Example Label"], + "datasets": [{ + "data": [10, 20, 30, 40, 50], + "backgroundColor": "#00ff00", + "borderWidth": 0 + }] + }, + "options": { + "plugins": { + "legend": { + "position": "right", + "align": "center" + } + } + } + }, + "options": { + "spriteText": true, + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/legend-doughnut-right-center-mulitiline-labels.png b/test/fixtures/plugin.legend/legend-doughnut-right-center-mulitiline-labels.png new file mode 100644 index 00000000000..1e92045fc06 Binary files /dev/null and b/test/fixtures/plugin.legend/legend-doughnut-right-center-mulitiline-labels.png differ diff --git a/test/fixtures/plugin.legend/legend-doughnut-right-center-mulitiline.json b/test/fixtures/plugin.legend/legend-doughnut-right-center-mulitiline.json new file mode 100644 index 00000000000..319dfaed022 --- /dev/null +++ b/test/fixtures/plugin.legend/legend-doughnut-right-center-mulitiline.json @@ -0,0 +1,27 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], + "datasets": [{ + "data": [10, 20, 30, 40, 50, 60, 70, 10, 20, 30, 40, 50, 60, 70, 10, 20, 30], + "backgroundColor": "#00ff00", + "borderWidth": 0 + }] + }, + "options": { + "plugins": { + "legend": { + "position": "right", + "align": "center" + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/legend-doughnut-right-center-mulitiline.png b/test/fixtures/plugin.legend/legend-doughnut-right-center-mulitiline.png new file mode 100644 index 00000000000..72cbe903dc4 Binary files /dev/null and b/test/fixtures/plugin.legend/legend-doughnut-right-center-mulitiline.png differ diff --git a/test/fixtures/plugin.legend/legend-doughnut-right-center-single.json b/test/fixtures/plugin.legend/legend-doughnut-right-center-single.json new file mode 100644 index 00000000000..f0729430511 --- /dev/null +++ b/test/fixtures/plugin.legend/legend-doughnut-right-center-single.json @@ -0,0 +1,27 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": [""], + "datasets": [{ + "data": [10], + "backgroundColor": "#00ff00", + "borderWidth": 0 + }] + }, + "options": { + "plugins": { + "legend": { + "position": "right", + "align": "center" + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/legend-doughnut-right-center-single.png b/test/fixtures/plugin.legend/legend-doughnut-right-center-single.png new file mode 100644 index 00000000000..aa8c1043c33 Binary files /dev/null and b/test/fixtures/plugin.legend/legend-doughnut-right-center-single.png differ diff --git a/test/fixtures/plugin.legend/legend-doughnut-right-default-center.json b/test/fixtures/plugin.legend/legend-doughnut-right-default-center.json new file mode 100644 index 00000000000..a16c4aacb41 --- /dev/null +++ b/test/fixtures/plugin.legend/legend-doughnut-right-default-center.json @@ -0,0 +1,26 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["", "", "", "", "", ""], + "datasets": [{ + "data": [10, 20, 30, 40, 50], + "backgroundColor": "#00ff00", + "borderWidth": 0 + }] + }, + "options": { + "plugins": { + "legend": { + "position": "right" + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/legend-doughnut-right-default-center.png b/test/fixtures/plugin.legend/legend-doughnut-right-default-center.png new file mode 100644 index 00000000000..1ba7052ea0b Binary files /dev/null and b/test/fixtures/plugin.legend/legend-doughnut-right-default-center.png differ diff --git a/test/fixtures/plugin.legend/legend-doughnut-right-end-mulitiline.json b/test/fixtures/plugin.legend/legend-doughnut-right-end-mulitiline.json new file mode 100644 index 00000000000..1d9e1370cde --- /dev/null +++ b/test/fixtures/plugin.legend/legend-doughnut-right-end-mulitiline.json @@ -0,0 +1,27 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], + "datasets": [{ + "data": [10, 20, 30, 40, 50, 60, 70, 10, 20, 30, 40, 50, 60, 70, 10, 20, 30], + "backgroundColor": "#00ff00", + "borderWidth": 0 + }] + }, + "options": { + "plugins": { + "legend": { + "position": "right", + "align": "end" + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/legend-doughnut-right-end-mulitiline.png b/test/fixtures/plugin.legend/legend-doughnut-right-end-mulitiline.png new file mode 100644 index 00000000000..00e366460ea Binary files /dev/null and b/test/fixtures/plugin.legend/legend-doughnut-right-end-mulitiline.png differ diff --git a/test/fixtures/plugin.legend/legend-doughnut-right-start-mulitiline.json b/test/fixtures/plugin.legend/legend-doughnut-right-start-mulitiline.json new file mode 100644 index 00000000000..03aa500bd19 --- /dev/null +++ b/test/fixtures/plugin.legend/legend-doughnut-right-start-mulitiline.json @@ -0,0 +1,27 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], + "datasets": [{ + "data": [10, 20, 30, 40, 50, 60, 70, 10, 20, 30, 40, 50, 60, 70, 10, 20, 30], + "backgroundColor": "#00ff00", + "borderWidth": 0 + }] + }, + "options": { + "plugins": { + "legend": { + "position": "right", + "align": "start" + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/legend-doughnut-right-start-mulitiline.png b/test/fixtures/plugin.legend/legend-doughnut-right-start-mulitiline.png new file mode 100644 index 00000000000..73c51c0706f Binary files /dev/null and b/test/fixtures/plugin.legend/legend-doughnut-right-start-mulitiline.png differ diff --git a/test/fixtures/plugin.legend/legend-doughnut-top-center-mulitiline.json b/test/fixtures/plugin.legend/legend-doughnut-top-center-mulitiline.json new file mode 100644 index 00000000000..13725324478 --- /dev/null +++ b/test/fixtures/plugin.legend/legend-doughnut-top-center-mulitiline.json @@ -0,0 +1,27 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], + "datasets": [{ + "data": [10, 20, 30, 40, 50, 60, 70, 10, 20, 30, 40, 50, 60, 70, 10, 20, 30, 20, 10], + "backgroundColor": "#00ff00", + "borderWidth": 0 + }] + }, + "options": { + "plugins": { + "legend": { + "position": "top", + "align": "center" + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/legend-doughnut-top-center-mulitiline.png b/test/fixtures/plugin.legend/legend-doughnut-top-center-mulitiline.png new file mode 100644 index 00000000000..f3b552abe0b Binary files /dev/null and b/test/fixtures/plugin.legend/legend-doughnut-top-center-mulitiline.png differ diff --git a/test/fixtures/plugin.legend/legend-doughnut-top-center-single.json b/test/fixtures/plugin.legend/legend-doughnut-top-center-single.json new file mode 100644 index 00000000000..59762b915b0 --- /dev/null +++ b/test/fixtures/plugin.legend/legend-doughnut-top-center-single.json @@ -0,0 +1,27 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": [""], + "datasets": [{ + "data": [10], + "backgroundColor": "#00ff00", + "borderWidth": 0 + }] + }, + "options": { + "plugins": { + "legend": { + "position": "top", + "align": "center" + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/legend-doughnut-top-center-single.png b/test/fixtures/plugin.legend/legend-doughnut-top-center-single.png new file mode 100644 index 00000000000..6aac5226310 Binary files /dev/null and b/test/fixtures/plugin.legend/legend-doughnut-top-center-single.png differ diff --git a/test/fixtures/plugin.legend/legend-doughnut-top-end-mulitiline.json b/test/fixtures/plugin.legend/legend-doughnut-top-end-mulitiline.json new file mode 100644 index 00000000000..d689dc4e3dd --- /dev/null +++ b/test/fixtures/plugin.legend/legend-doughnut-top-end-mulitiline.json @@ -0,0 +1,27 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], + "datasets": [{ + "data": [10, 20, 30, 40, 50, 60, 70, 10, 20, 30, 40, 50, 60, 70, 10], + "backgroundColor": "#00ff00", + "borderWidth": 0 + }] + }, + "options": { + "plugins": { + "legend": { + "position": "top", + "align": "end" + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/legend-doughnut-top-end-mulitiline.png b/test/fixtures/plugin.legend/legend-doughnut-top-end-mulitiline.png new file mode 100644 index 00000000000..e6e56e4701f Binary files /dev/null and b/test/fixtures/plugin.legend/legend-doughnut-top-end-mulitiline.png differ diff --git a/test/fixtures/plugin.legend/legend-doughnut-top-start-mulitiline.json b/test/fixtures/plugin.legend/legend-doughnut-top-start-mulitiline.json new file mode 100644 index 00000000000..e8bd710d9bc --- /dev/null +++ b/test/fixtures/plugin.legend/legend-doughnut-top-start-mulitiline.json @@ -0,0 +1,27 @@ +{ + "config": { + "type": "doughnut", + "data": { + "labels": ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], + "datasets": [{ + "data": [10, 20, 30, 40, 50, 60, 70, 10, 20, 30, 40, 50, 60, 70, 10], + "backgroundColor": "#00ff00", + "borderWidth": 0 + }] + }, + "options": { + "plugins": { + "legend": { + "position": "top", + "align": "start" + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} diff --git a/test/fixtures/plugin.legend/legend-doughnut-top-start-mulitiline.png b/test/fixtures/plugin.legend/legend-doughnut-top-start-mulitiline.png new file mode 100644 index 00000000000..2e7e7fe88aa Binary files /dev/null and b/test/fixtures/plugin.legend/legend-doughnut-top-start-mulitiline.png differ diff --git a/test/fixtures/plugin.legend/legend-line-chart-area.json b/test/fixtures/plugin.legend/legend-line-chart-area.json new file mode 100644 index 00000000000..e3e2520a1ac --- /dev/null +++ b/test/fixtures/plugin.legend/legend-line-chart-area.json @@ -0,0 +1,35 @@ +{ + "config": { + "type": "line", + "data": { + "labels": ["A", "B", "C", "D", "E"], + "datasets": [{ + "data": [10, 20, 30, 40, 50], + "backgroundColor": "#00ff00", + "borderWidth": 0, + "label": "" + }] + }, + "options": { + "plugins": { + "legend": { + "position": "chartArea" + } + }, + "scales": { + "x": { + "display": false + }, + "y": { + "display": false + } + } + } + }, + "options": { + "canvas": { + "height": 256, + "width": 512 + } + } +} \ No newline at end of file diff --git a/test/fixtures/plugin.legend/legend-line-chart-area.png b/test/fixtures/plugin.legend/legend-line-chart-area.png new file mode 100644 index 00000000000..f3b020f7bf6 Binary files /dev/null and b/test/fixtures/plugin.legend/legend-line-chart-area.png differ diff --git a/test/fixtures/plugin.legend/maxWidth/infinity.js b/test/fixtures/plugin.legend/maxWidth/infinity.js new file mode 100644 index 00000000000..315b5a6489f --- /dev/null +++ b/test/fixtures/plugin.legend/maxWidth/infinity.js @@ -0,0 +1,42 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], + datasets: [ + { + label: '# of Votes', + data: [12, 19, 3, 5, 2, 3], + borderWidth: 1 + }, + { + label: '# of Points', + data: [7, 11, 5, 8, 3, 7], + borderWidth: 1 + } + ] + }, + options: { + scales: { + x: {display: false}, + y: {display: false} + }, + plugins: { + title: false, + tooltip: false, + filler: false, + legend: { + position: 'left', + maxWidth: Infinity + } + } + } + }, + options: { + spriteText: true, + canvas: { + width: 150, + height: 75 + } + } +}; diff --git a/test/fixtures/plugin.legend/maxWidth/infinity.png b/test/fixtures/plugin.legend/maxWidth/infinity.png new file mode 100644 index 00000000000..9a0a9a28938 Binary files /dev/null and b/test/fixtures/plugin.legend/maxWidth/infinity.png differ diff --git a/test/fixtures/plugin.legend/maxWidth/undefined.js b/test/fixtures/plugin.legend/maxWidth/undefined.js new file mode 100644 index 00000000000..bd488b190f9 --- /dev/null +++ b/test/fixtures/plugin.legend/maxWidth/undefined.js @@ -0,0 +1,41 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], + datasets: [ + { + label: '# of Votes', + data: [12, 19, 3, 5, 2, 3], + borderWidth: 1 + }, + { + label: '# of Points', + data: [7, 11, 5, 8, 3, 7], + borderWidth: 1 + } + ] + }, + options: { + scales: { + x: {display: false}, + y: {display: false} + }, + plugins: { + title: false, + tooltip: false, + filler: false, + legend: { + position: 'left', + } + } + } + }, + options: { + spriteText: true, + canvas: { + width: 150, + height: 75 + } + } +}; diff --git a/test/fixtures/plugin.legend/maxWidth/undefined.png b/test/fixtures/plugin.legend/maxWidth/undefined.png new file mode 100644 index 00000000000..858ff0e3f52 Binary files /dev/null and b/test/fixtures/plugin.legend/maxWidth/undefined.png differ diff --git a/test/fixtures/plugin.legend/maxWidth/value.js b/test/fixtures/plugin.legend/maxWidth/value.js new file mode 100644 index 00000000000..f5d02b06425 --- /dev/null +++ b/test/fixtures/plugin.legend/maxWidth/value.js @@ -0,0 +1,42 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], + datasets: [ + { + label: '# of Votes', + data: [12, 19, 3, 5, 2, 3], + borderWidth: 1 + }, + { + label: '# of Points', + data: [7, 11, 5, 8, 3, 7], + borderWidth: 1 + } + ] + }, + options: { + scales: { + x: {display: false}, + y: {display: false} + }, + plugins: { + title: false, + tooltip: false, + filler: false, + legend: { + position: 'left', + maxWidth: 100 + } + } + } + }, + options: { + spriteText: true, + canvas: { + width: 150, + height: 75 + } + } +}; diff --git a/test/fixtures/plugin.legend/maxWidth/value.png b/test/fixtures/plugin.legend/maxWidth/value.png new file mode 100644 index 00000000000..64f40cd79b7 Binary files /dev/null and b/test/fixtures/plugin.legend/maxWidth/value.png differ diff --git a/test/fixtures/plugin.legend/padding/2cols-with-padding.js b/test/fixtures/plugin.legend/padding/2cols-with-padding.js new file mode 100644 index 00000000000..604ade6b7df --- /dev/null +++ b/test/fixtures/plugin.legend/padding/2cols-with-padding.js @@ -0,0 +1,35 @@ +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/9278', + config: { + type: 'pie', + data: { + labels: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k'], + datasets: [{ + data: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + backgroundColor: 'red' + }] + }, + options: { + plugins: { + legend: { + position: 'left' + } + }, + layout: { + padding: { + top: 50, + left: 30, + right: 30, + bottom: 50 + } + } + } + }, + options: { + spriteText: true, + canvas: { + width: 400, + height: 300 + }, + } +}; diff --git a/test/fixtures/plugin.legend/padding/2cols-with-padding.png b/test/fixtures/plugin.legend/padding/2cols-with-padding.png new file mode 100644 index 00000000000..8871ccddc6b Binary files /dev/null and b/test/fixtures/plugin.legend/padding/2cols-with-padding.png differ diff --git a/test/fixtures/plugin.legend/padding/add-column.js b/test/fixtures/plugin.legend/padding/add-column.js new file mode 100644 index 00000000000..7a1e6d5e5a0 --- /dev/null +++ b/test/fixtures/plugin.legend/padding/add-column.js @@ -0,0 +1,39 @@ +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/9278', + config: { + type: 'pie', + data: { + labels: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'], + datasets: [{ + data: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + backgroundColor: 'red' + }] + }, + options: { + plugins: { + legend: { + position: 'left' + } + }, + layout: { + padding: { + top: 55, + left: 30, + right: 30 + } + } + } + }, + options: { + spriteText: true, + canvas: { + width: 400, + height: 300 + }, + run(chart) { + chart.data.labels.push('k'); + chart.data.datasets[0].data.push(11); + chart.update(); + } + } +}; diff --git a/test/fixtures/plugin.legend/padding/add-column.png b/test/fixtures/plugin.legend/padding/add-column.png new file mode 100644 index 00000000000..187dcda8c16 Binary files /dev/null and b/test/fixtures/plugin.legend/padding/add-column.png differ diff --git a/test/fixtures/plugin.legend/pointStyle-width/legend-pointStyle-width-default.json b/test/fixtures/plugin.legend/pointStyle-width/legend-pointStyle-width-default.json new file mode 100644 index 00000000000..ad3c2c84e81 --- /dev/null +++ b/test/fixtures/plugin.legend/pointStyle-width/legend-pointStyle-width-default.json @@ -0,0 +1,111 @@ + { + "config": { + "type": "line", + "data": { + "labels": ["A", "B", "C"], + "datasets": [{ + "data": [10, 10, 10], + "backgroundColor": "#00ff00", + "borderColor": "#ff0000", + "borderWidth": 1, + "label": "", + "pointStyle": "line" + }, + { + "data": [15, 15, 15], + "backgroundColor": "#00ff00", + "borderColor": "#ff0000", + "borderWidth": 1, + "label": "", + "pointStyle": "triangle" + }, + { + "data": [20, 20, 20], + "backgroundColor": "#00ff00", + "borderColor": "#ff0000", + "borderWidth": 1, + "label": "", + "pointStyle": "rectRounded" + }, + { + "data": [30, 30, 30], + "backgroundColor": "#00ff00", + "borderColor": "#ff0000", + "borderWidth": 1, + "label": "" + }, + { + "data": [40, 40, 40], + "backgroundColor": "#00ff00", + "borderColor": "#ff0000", + "borderWidth": 1, + "label": "", + "pointStyle": "rect" + }, + { + "data": [25, 25, 25], + "backgroundColor": "#00ff00", + "borderColor": "#ff0000", + "borderWidth": 1, + "label": "", + "pointStyle": "rectRot" + }, + { + "data": [35, 35, 35], + "backgroundColor": "#00ff00", + "borderColor": "#ff0000", + "borderWidth": 1, + "label": "", + "pointStyle": "crossRot" + }, + { + "data": [45, 45, 45], + "backgroundColor": "#00ff00", + "borderColor": "#ff0000", + "borderWidth": 1, + "label": "", + "pointStyle": "cross" + }, + { + "data": [50, 50, 50], + "backgroundColor": "#00ff00", + "borderColor": "#ff0000", + "borderWidth": 1, + "label": "", + "pointStyle": "star" + }, + { + "data": [55, 55, 55], + "backgroundColor": "#00ff00", + "borderColor": "#ff0000", + "borderWidth": 1, + "label": "", + "pointStyle": "dash" + }] + }, + "options": { + "plugins": { + "legend": { + "display": true, + "labels": { + "usePointStyle": true + } + } + }, + "scales": { + "x": { + "display": false + }, + "y": { + "display": false + } + } + } + }, + "options": { + "canvas": { + "height": 512, + "width": 1024 + } + } +} \ No newline at end of file diff --git a/test/fixtures/plugin.legend/pointStyle-width/legend-pointStyle-width-default.png b/test/fixtures/plugin.legend/pointStyle-width/legend-pointStyle-width-default.png new file mode 100644 index 00000000000..d646921225f Binary files /dev/null and b/test/fixtures/plugin.legend/pointStyle-width/legend-pointStyle-width-default.png differ diff --git a/test/fixtures/plugin.legend/pointStyle-width/legend-pointStyle-width.json b/test/fixtures/plugin.legend/pointStyle-width/legend-pointStyle-width.json new file mode 100644 index 00000000000..b359ca4a68f --- /dev/null +++ b/test/fixtures/plugin.legend/pointStyle-width/legend-pointStyle-width.json @@ -0,0 +1,112 @@ + { + "config": { + "type": "line", + "data": { + "labels": ["A", "B", "C"], + "datasets": [{ + "data": [10, 10, 10], + "backgroundColor": "#00ff00", + "borderColor": "#ff0000", + "borderWidth": 1, + "label": "", + "pointStyle": "line" + }, + { + "data": [15, 15, 15], + "backgroundColor": "#00ff00", + "borderColor": "#ff0000", + "borderWidth": 1, + "label": "", + "pointStyle": "triangle" + }, + { + "data": [20, 20, 20], + "backgroundColor": "#00ff00", + "borderColor": "#ff0000", + "borderWidth": 1, + "label": "", + "pointStyle": "rectRounded" + }, + { + "data": [30, 30, 30], + "backgroundColor": "#00ff00", + "borderColor": "#ff0000", + "borderWidth": 1, + "label": "" + }, + { + "data": [40, 40, 40], + "backgroundColor": "#00ff00", + "borderColor": "#ff0000", + "borderWidth": 1, + "label": "", + "pointStyle": "rect" + }, + { + "data": [25, 25, 25], + "backgroundColor": "#00ff00", + "borderColor": "#ff0000", + "borderWidth": 1, + "label": "", + "pointStyle": "rectRot" + }, + { + "data": [35, 35, 35], + "backgroundColor": "#00ff00", + "borderColor": "#ff0000", + "borderWidth": 1, + "label": "", + "pointStyle": "crossRot" + }, + { + "data": [45, 45, 45], + "backgroundColor": "#00ff00", + "borderColor": "#ff0000", + "borderWidth": 1, + "label": "", + "pointStyle": "cross" + }, + { + "data": [50, 50, 50], + "backgroundColor": "#00ff00", + "borderColor": "#ff0000", + "borderWidth": 1, + "label": "", + "pointStyle": "star" + }, + { + "data": [55, 55, 55], + "backgroundColor": "#00ff00", + "borderColor": "#ff0000", + "borderWidth": 1, + "label": "", + "pointStyle": "dash" + }] + }, + "options": { + "plugins": { + "legend": { + "display": true, + "labels": { + "usePointStyle": true, + "pointStyleWidth": 75 + } + } + }, + "scales": { + "x": { + "display": false + }, + "y": { + "display": false + } + } + } + }, + "options": { + "canvas": { + "height": 512, + "width": 1024 + } + } +} \ No newline at end of file diff --git a/test/fixtures/plugin.legend/pointStyle-width/legend-pointStyle-width.png b/test/fixtures/plugin.legend/pointStyle-width/legend-pointStyle-width.png new file mode 100644 index 00000000000..e28ed492c92 Binary files /dev/null and b/test/fixtures/plugin.legend/pointStyle-width/legend-pointStyle-width.png differ diff --git a/test/fixtures/plugin.legend/title/bottom-center-center.js b/test/fixtures/plugin.legend/title/bottom-center-center.js new file mode 100644 index 00000000000..200b73ac58a --- /dev/null +++ b/test/fixtures/plugin.legend/title/bottom-center-center.js @@ -0,0 +1,36 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [ + {label: 'a', data: []}, + {label: 'b', data: []}, + {label: 'c', data: []} + ] + }, + options: { + plugins: { + legend: { + position: 'bottom', + align: 'center', + title: { + display: true, + position: 'center', + text: 'title' + } + } + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 256 + } + } +}; diff --git a/test/fixtures/plugin.legend/title/bottom-center-center.png b/test/fixtures/plugin.legend/title/bottom-center-center.png new file mode 100644 index 00000000000..7782dc715e5 Binary files /dev/null and b/test/fixtures/plugin.legend/title/bottom-center-center.png differ diff --git a/test/fixtures/plugin.legend/title/bottom-end-end.js b/test/fixtures/plugin.legend/title/bottom-end-end.js new file mode 100644 index 00000000000..f624512b755 --- /dev/null +++ b/test/fixtures/plugin.legend/title/bottom-end-end.js @@ -0,0 +1,36 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [ + {label: 'a', data: []}, + {label: 'b', data: []}, + {label: 'c', data: []} + ] + }, + options: { + plugins: { + legend: { + position: 'bottom', + align: 'end', + title: { + display: true, + position: 'end', + text: 'title' + } + } + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 256 + } + } +}; diff --git a/test/fixtures/plugin.legend/title/bottom-end-end.png b/test/fixtures/plugin.legend/title/bottom-end-end.png new file mode 100644 index 00000000000..1fb3cbeb18b Binary files /dev/null and b/test/fixtures/plugin.legend/title/bottom-end-end.png differ diff --git a/test/fixtures/plugin.legend/title/bottom-start-start.js b/test/fixtures/plugin.legend/title/bottom-start-start.js new file mode 100644 index 00000000000..8f993e14041 --- /dev/null +++ b/test/fixtures/plugin.legend/title/bottom-start-start.js @@ -0,0 +1,36 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [ + {label: 'a', data: []}, + {label: 'b', data: []}, + {label: 'c', data: []} + ] + }, + options: { + plugins: { + legend: { + position: 'bottom', + align: 'start', + title: { + display: true, + position: 'start', + text: 'title' + } + } + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 256 + } + } +}; diff --git a/test/fixtures/plugin.legend/title/bottom-start-start.png b/test/fixtures/plugin.legend/title/bottom-start-start.png new file mode 100644 index 00000000000..def10089a3e Binary files /dev/null and b/test/fixtures/plugin.legend/title/bottom-start-start.png differ diff --git a/test/fixtures/plugin.legend/title/left-center-center.js b/test/fixtures/plugin.legend/title/left-center-center.js new file mode 100644 index 00000000000..b269670dee8 --- /dev/null +++ b/test/fixtures/plugin.legend/title/left-center-center.js @@ -0,0 +1,36 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [ + {label: 'a', data: []}, + {label: 'b', data: []}, + {label: 'c', data: []} + ] + }, + options: { + plugins: { + legend: { + position: 'left', + align: 'center', + title: { + display: true, + position: 'center', + text: 'title' + } + } + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 256 + } + } +}; diff --git a/test/fixtures/plugin.legend/title/left-center-center.png b/test/fixtures/plugin.legend/title/left-center-center.png new file mode 100644 index 00000000000..7ffd4009449 Binary files /dev/null and b/test/fixtures/plugin.legend/title/left-center-center.png differ diff --git a/test/fixtures/plugin.legend/title/left-end-end.js b/test/fixtures/plugin.legend/title/left-end-end.js new file mode 100644 index 00000000000..c88b95d1cac --- /dev/null +++ b/test/fixtures/plugin.legend/title/left-end-end.js @@ -0,0 +1,36 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [ + {label: 'a', data: []}, + {label: 'b', data: []}, + {label: 'c', data: []} + ] + }, + options: { + plugins: { + legend: { + position: 'left', + align: 'end', + title: { + display: true, + position: 'end', + text: 'title' + } + } + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 256 + } + } +}; diff --git a/test/fixtures/plugin.legend/title/left-end-end.png b/test/fixtures/plugin.legend/title/left-end-end.png new file mode 100644 index 00000000000..a60610ab500 Binary files /dev/null and b/test/fixtures/plugin.legend/title/left-end-end.png differ diff --git a/test/fixtures/plugin.legend/title/left-start-start.js b/test/fixtures/plugin.legend/title/left-start-start.js new file mode 100644 index 00000000000..06decc309a3 --- /dev/null +++ b/test/fixtures/plugin.legend/title/left-start-start.js @@ -0,0 +1,36 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [ + {label: 'a', data: []}, + {label: 'b', data: []}, + {label: 'c', data: []} + ] + }, + options: { + plugins: { + legend: { + position: 'left', + align: 'start', + title: { + display: true, + position: 'start', + text: 'title' + } + } + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 256 + } + } +}; diff --git a/test/fixtures/plugin.legend/title/left-start-start.png b/test/fixtures/plugin.legend/title/left-start-start.png new file mode 100644 index 00000000000..c1d4267a3c0 Binary files /dev/null and b/test/fixtures/plugin.legend/title/left-start-start.png differ diff --git a/test/fixtures/plugin.legend/title/right-center-center.js b/test/fixtures/plugin.legend/title/right-center-center.js new file mode 100644 index 00000000000..c9ba4aaf9e2 --- /dev/null +++ b/test/fixtures/plugin.legend/title/right-center-center.js @@ -0,0 +1,36 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [ + {label: 'a', data: []}, + {label: 'b', data: []}, + {label: 'c', data: []} + ] + }, + options: { + plugins: { + legend: { + position: 'right', + align: 'center', + title: { + display: true, + position: 'center', + text: 'title' + } + } + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 256 + } + } +}; diff --git a/test/fixtures/plugin.legend/title/right-center-center.png b/test/fixtures/plugin.legend/title/right-center-center.png new file mode 100644 index 00000000000..9900784a4b3 Binary files /dev/null and b/test/fixtures/plugin.legend/title/right-center-center.png differ diff --git a/test/fixtures/plugin.legend/title/right-end-end.js b/test/fixtures/plugin.legend/title/right-end-end.js new file mode 100644 index 00000000000..4add990ea2a --- /dev/null +++ b/test/fixtures/plugin.legend/title/right-end-end.js @@ -0,0 +1,36 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [ + {label: 'a', data: []}, + {label: 'b', data: []}, + {label: 'c', data: []} + ] + }, + options: { + plugins: { + legend: { + position: 'right', + align: 'end', + title: { + display: true, + position: 'end', + text: 'title' + } + } + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 256 + } + } +}; diff --git a/test/fixtures/plugin.legend/title/right-end-end.png b/test/fixtures/plugin.legend/title/right-end-end.png new file mode 100644 index 00000000000..ec2acbf1312 Binary files /dev/null and b/test/fixtures/plugin.legend/title/right-end-end.png differ diff --git a/test/fixtures/plugin.legend/title/right-start-start.js b/test/fixtures/plugin.legend/title/right-start-start.js new file mode 100644 index 00000000000..43e9e00430f --- /dev/null +++ b/test/fixtures/plugin.legend/title/right-start-start.js @@ -0,0 +1,36 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [ + {label: 'a', data: []}, + {label: 'b', data: []}, + {label: 'c', data: []} + ] + }, + options: { + plugins: { + legend: { + position: 'right', + align: 'start', + title: { + display: true, + position: 'start', + text: 'title' + } + } + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 256 + } + } +}; diff --git a/test/fixtures/plugin.legend/title/right-start-start.png b/test/fixtures/plugin.legend/title/right-start-start.png new file mode 100644 index 00000000000..89c5286ef37 Binary files /dev/null and b/test/fixtures/plugin.legend/title/right-start-start.png differ diff --git a/test/fixtures/plugin.legend/title/top-center-center.js b/test/fixtures/plugin.legend/title/top-center-center.js new file mode 100644 index 00000000000..1f94799c107 --- /dev/null +++ b/test/fixtures/plugin.legend/title/top-center-center.js @@ -0,0 +1,36 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [ + {label: 'a', data: []}, + {label: 'b', data: []}, + {label: 'c', data: []} + ] + }, + options: { + plugins: { + legend: { + position: 'top', + align: 'center', + title: { + display: true, + position: 'center', + text: 'title' + } + } + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 256 + } + } +}; diff --git a/test/fixtures/plugin.legend/title/top-center-center.png b/test/fixtures/plugin.legend/title/top-center-center.png new file mode 100644 index 00000000000..0a1a3f33acd Binary files /dev/null and b/test/fixtures/plugin.legend/title/top-center-center.png differ diff --git a/test/fixtures/plugin.legend/title/top-end-end.js b/test/fixtures/plugin.legend/title/top-end-end.js new file mode 100644 index 00000000000..22210df59f9 --- /dev/null +++ b/test/fixtures/plugin.legend/title/top-end-end.js @@ -0,0 +1,36 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [ + {label: 'a', data: []}, + {label: 'b', data: []}, + {label: 'c', data: []} + ] + }, + options: { + plugins: { + legend: { + position: 'top', + align: 'end', + title: { + display: true, + position: 'end', + text: 'title' + } + } + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 256 + } + } +}; diff --git a/test/fixtures/plugin.legend/title/top-end-end.png b/test/fixtures/plugin.legend/title/top-end-end.png new file mode 100644 index 00000000000..76b8a0ba028 Binary files /dev/null and b/test/fixtures/plugin.legend/title/top-end-end.png differ diff --git a/test/fixtures/plugin.legend/title/top-start-start.js b/test/fixtures/plugin.legend/title/top-start-start.js new file mode 100644 index 00000000000..38430746acb --- /dev/null +++ b/test/fixtures/plugin.legend/title/top-start-start.js @@ -0,0 +1,36 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [ + {label: 'a', data: []}, + {label: 'b', data: []}, + {label: 'c', data: []} + ] + }, + options: { + plugins: { + legend: { + position: 'top', + align: 'start', + title: { + display: true, + position: 'start', + text: 'title' + } + } + }, + scales: { + x: {display: false}, + y: {display: false} + } + } + }, + options: { + spriteText: true, + canvas: { + height: 256, + width: 256 + } + } +}; diff --git a/test/fixtures/plugin.legend/title/top-start-start.png b/test/fixtures/plugin.legend/title/top-start-start.png new file mode 100644 index 00000000000..b29e80e22ae Binary files /dev/null and b/test/fixtures/plugin.legend/title/top-start-start.png differ diff --git a/test/fixtures/plugin.subtitle/basic.js b/test/fixtures/plugin.subtitle/basic.js new file mode 100644 index 00000000000..273d3215f64 --- /dev/null +++ b/test/fixtures/plugin.subtitle/basic.js @@ -0,0 +1,41 @@ + +module.exports = { + config: { + type: 'scatter', + data: { + datasets: [{ + data: [{x: 0, y: 0}, {x: 1, y: 1}, {x: 2, y: 2}], + backgroundColor: 'red', + radius: 1, + hoverRadius: 0 + }], + }, + options: { + scales: { + x: {display: false}, + y: {display: false} + }, + plugins: { + legend: false, + title: { + display: true, + text: 'Title Text', + }, + subtitle: { + display: true, + text: 'SubTitle Text', + }, + filler: false, + tooltip: false + }, + }, + + }, + options: { + spriteText: true, + canvas: { + height: 400, + width: 400 + } + } +}; diff --git a/test/fixtures/plugin.subtitle/basic.png b/test/fixtures/plugin.subtitle/basic.png new file mode 100644 index 00000000000..795ebad6953 Binary files /dev/null and b/test/fixtures/plugin.subtitle/basic.png differ diff --git a/test/fixtures/plugin.title/scriptable-options.js b/test/fixtures/plugin.title/scriptable-options.js new file mode 100644 index 00000000000..f961fe2af4d --- /dev/null +++ b/test/fixtures/plugin.title/scriptable-options.js @@ -0,0 +1,43 @@ +const data = []; +for (let x = 0; x < 3; x++) { + for (let y = 0; y < 3; y++) { + data.push({x, y}); + } +} + +module.exports = { + config: { + type: 'scatter', + data: { + datasets: [{ + data, + backgroundColor: 'red', + radius: 1, + hoverRadius: 0 + }], + }, + options: { + scales: { + x: {display: false}, + y: {display: false} + }, + plugins: { + legend: false, + title: { + display: true, + text: () => 'Title Text', + }, + filler: false, + tooltip: false + }, + }, + + }, + options: { + spriteText: true, + canvas: { + height: 400, + width: 400 + } + } +}; diff --git a/test/fixtures/plugin.title/scriptable-options.png b/test/fixtures/plugin.title/scriptable-options.png new file mode 100644 index 00000000000..8a3086ab0ef Binary files /dev/null and b/test/fixtures/plugin.title/scriptable-options.png differ diff --git a/test/fixtures/plugin.tooltip/box-padding.js b/test/fixtures/plugin.tooltip/box-padding.js new file mode 100644 index 00000000000..434620eddda --- /dev/null +++ b/test/fixtures/plugin.tooltip/box-padding.js @@ -0,0 +1,71 @@ +const data = []; +for (let x = 0; x < 3; x++) { + for (let y = 0; y < 3; y++) { + data.push({x, y}); + } +} + +module.exports = { + config: { + type: 'scatter', + data: { + datasets: [{ + data, + backgroundColor: 'red', + radius: 1, + hoverRadius: 0 + }], + }, + options: { + scales: { + x: {display: false}, + y: {display: false} + }, + plugins: { + legend: false, + title: false, + filler: false, + tooltip: { + mode: 'point', + intersect: true, + // spriteText: use white background to hide any gaps between fonts + backgroundColor: 'white', + borderColor: 'black', + borderWidth: 1, + callbacks: { + label: () => 'label', + }, + boxPadding: 30 + }, + }, + }, + plugins: [{ + afterDraw: function(chart) { + const canvas = chart.canvas; + const rect = canvas.getBoundingClientRect(); + const meta = chart.getDatasetMeta(0); + let point, event; + + for (let i = 0; i < data.length; i++) { + point = meta.data[i]; + event = { + type: 'mousemove', + target: canvas, + clientX: rect.left + point.x, + clientY: rect.top + point.y + }; + chart._handleEvent(event); + chart.tooltip.handleEvent(event); + chart.tooltip.draw(chart.ctx); + } + } + }] + }, + options: { + spriteText: true, + canvas: { + height: 400, + width: 500 + } + } +}; diff --git a/test/fixtures/plugin.tooltip/box-padding.png b/test/fixtures/plugin.tooltip/box-padding.png new file mode 100644 index 00000000000..d3cd19e202e Binary files /dev/null and b/test/fixtures/plugin.tooltip/box-padding.png differ diff --git a/test/fixtures/plugin.tooltip/caret-position.js b/test/fixtures/plugin.tooltip/caret-position.js new file mode 100644 index 00000000000..0997c11b28b --- /dev/null +++ b/test/fixtures/plugin.tooltip/caret-position.js @@ -0,0 +1,70 @@ +const data = []; +for (let x = 1; x < 4; x++) { + for (let y = 1; y < 4; y++) { + data.push({x, y}); + } +} + +module.exports = { + config: { + type: 'scatter', + data: { + datasets: [{ + data, + backgroundColor: 'red', + radius: 8, + hoverRadius: 0 + }], + }, + options: { + scales: { + x: {display: false, min: 0.96, max: 3.04}, + y: {display: false, min: 1, max: 3} + }, + plugins: { + legend: false, + title: false, + filler: false, + tooltip: { + mode: 'point', + intersect: true, + // spriteText: use white background to hide any gaps between fonts + backgroundColor: 'white', + borderColor: 'black', + borderWidth: 1, + callbacks: { + label: () => 'label', + } + }, + }, + }, + plugins: [{ + afterDraw: function(chart) { + const canvas = chart.canvas; + const rect = canvas.getBoundingClientRect(); + const meta = chart.getDatasetMeta(0); + let point, event; + + for (let i = 0; i < data.length; i++) { + point = meta.data[i]; + event = { + type: 'mousemove', + target: canvas, + clientX: rect.left + point.x, + clientY: rect.top + point.y + }; + chart._handleEvent(event); + chart.tooltip.handleEvent(event); + chart.tooltip.draw(chart.ctx); + } + } + }] + }, + options: { + spriteText: true, + canvas: { + height: 240, + width: 320 + } + } +}; diff --git a/test/fixtures/plugin.tooltip/caret-position.png b/test/fixtures/plugin.tooltip/caret-position.png new file mode 100644 index 00000000000..147f87e5bf3 Binary files /dev/null and b/test/fixtures/plugin.tooltip/caret-position.png differ diff --git a/test/fixtures/plugin.tooltip/color-box-border-dash.js b/test/fixtures/plugin.tooltip/color-box-border-dash.js new file mode 100644 index 00000000000..b24472a681b --- /dev/null +++ b/test/fixtures/plugin.tooltip/color-box-border-dash.js @@ -0,0 +1,75 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [{ + data: [8, 7, 6, 5], + pointBorderColor: '#ff0000', + pointBackgroundColor: '#00ff00', + showLine: false + }], + labels: ['', '', '', ''] + }, + options: { + scales: { + x: {display: false}, + y: {display: false} + }, + elements: { + line: { + fill: false + } + }, + plugins: { + legend: false, + title: false, + filler: false, + tooltip: { + mode: 'nearest', + intersect: false, + callbacks: { + label: function() { + return '\u200b'; + }, + labelColor: function(tooltipItem) { + const meta = tooltipItem.chart.getDatasetMeta(tooltipItem.datasetIndex); + const options = meta.controller.getStyle(tooltipItem.dataIndex); + return { + borderColor: options.borderColor, + backgroundColor: options.backgroundColor, + borderWidth: 2, + borderDash: [2, 2] + }; + }, + } + }, + }, + + layout: { + padding: 15 + } + }, + plugins: [{ + afterDraw: function(chart) { + const canvas = chart.canvas; + const rect = canvas.getBoundingClientRect(); + const point = chart.getDatasetMeta(0).data[1]; + const event = { + type: 'mousemove', + target: canvas, + clientX: rect.left + point.x, + clientY: rect.top + point.y + }; + chart._handleEvent(event); + chart.tooltip.handleEvent(event); + chart.tooltip.draw(chart.ctx); + } + }] + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/plugin.tooltip/color-box-border-dash.png b/test/fixtures/plugin.tooltip/color-box-border-dash.png new file mode 100644 index 00000000000..8eb6868f8b3 Binary files /dev/null and b/test/fixtures/plugin.tooltip/color-box-border-dash.png differ diff --git a/test/fixtures/plugin.tooltip/color-box-border-radius.js b/test/fixtures/plugin.tooltip/color-box-border-radius.js new file mode 100644 index 00000000000..170719d8469 --- /dev/null +++ b/test/fixtures/plugin.tooltip/color-box-border-radius.js @@ -0,0 +1,78 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [{ + data: [8, 7, 6, 5], + pointBorderColor: '#ff0000', + pointBackgroundColor: '#00ff00', + showLine: false + }], + labels: ['', '', '', ''] + }, + options: { + scales: { + x: {display: false}, + y: {display: false} + }, + elements: { + line: { + fill: false + } + }, + plugins: { + legend: false, + title: false, + filler: false, + tooltip: { + mode: 'nearest', + intersect: false, + callbacks: { + label: function() { + return '\u200b'; + }, + labelColor: function(tooltipItem) { + const meta = tooltipItem.chart.getDatasetMeta(tooltipItem.datasetIndex); + const options = meta.controller.getStyle(tooltipItem.dataIndex); + return { + borderColor: options.borderColor, + backgroundColor: options.backgroundColor, + borderWidth: 2, + borderRadius: { + topRight: 5, + bottomRight: 5, + }, + }; + }, + } + }, + }, + + layout: { + padding: 15 + } + }, + plugins: [{ + afterDraw: function(chart) { + const canvas = chart.canvas; + const rect = canvas.getBoundingClientRect(); + const point = chart.getDatasetMeta(0).data[1]; + const event = { + type: 'mousemove', + target: canvas, + clientX: rect.left + point.x, + clientY: rect.top + point.y + }; + chart._handleEvent(event); + chart.tooltip.handleEvent(event); + chart.tooltip.draw(chart.ctx); + } + }] + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/plugin.tooltip/color-box-border-radius.png b/test/fixtures/plugin.tooltip/color-box-border-radius.png new file mode 100644 index 00000000000..f59f6284994 Binary files /dev/null and b/test/fixtures/plugin.tooltip/color-box-border-radius.png differ diff --git a/test/fixtures/plugin.tooltip/corner-radius.js b/test/fixtures/plugin.tooltip/corner-radius.js new file mode 100644 index 00000000000..e5c98a46255 --- /dev/null +++ b/test/fixtures/plugin.tooltip/corner-radius.js @@ -0,0 +1,78 @@ +const data = []; +for (let x = 0; x < 3; x++) { + for (let y = 0; y < 3; y++) { + data.push({x, y}); + } +} + +module.exports = { + config: { + type: 'scatter', + data: { + datasets: [{ + data, + backgroundColor: 'red', + radius: 1, + hoverRadius: 0 + }], + }, + options: { + scales: { + x: {display: false}, + y: {display: false} + }, + plugins: { + legend: false, + title: false, + filler: false, + tooltip: { + mode: 'point', + intersect: true, + // spriteText: use white background to hide any gaps between fonts + backgroundColor: 'white', + borderColor: 'black', + borderWidth: 1, + callbacks: { + beforeLabel: () => 'before label', + label: () => 'label', + afterLabel: () => 'after1\nafter2\nafter3\nafter4\nafter5' + }, + cornerRadius: { + topLeft: 10, + topRight: 20, + bottomRight: 5, + bottomLeft: 0, + } + }, + }, + }, + plugins: [{ + afterDraw: function(chart) { + const canvas = chart.canvas; + const rect = canvas.getBoundingClientRect(); + const meta = chart.getDatasetMeta(0); + let point, event; + + for (let i = 0; i < data.length; i++) { + point = meta.data[i]; + event = { + type: 'mousemove', + target: canvas, + clientX: rect.left + point.x, + clientY: rect.top + point.y + }; + chart._handleEvent(event); + chart.tooltip.handleEvent(event); + chart.tooltip.draw(chart.ctx); + } + } + }] + }, + options: { + spriteText: true, + canvas: { + height: 400, + width: 500 + } + } +}; diff --git a/test/fixtures/plugin.tooltip/corner-radius.png b/test/fixtures/plugin.tooltip/corner-radius.png new file mode 100644 index 00000000000..0e71cb26f99 Binary files /dev/null and b/test/fixtures/plugin.tooltip/corner-radius.png differ diff --git a/test/fixtures/plugin.tooltip/opacity.js b/test/fixtures/plugin.tooltip/opacity.js new file mode 100644 index 00000000000..c6c4424ecb2 --- /dev/null +++ b/test/fixtures/plugin.tooltip/opacity.js @@ -0,0 +1,107 @@ +var patternCanvas = document.createElement('canvas'); +var patternContext = patternCanvas.getContext('2d'); + +patternCanvas.width = 6; +patternCanvas.height = 6; +patternContext.fillStyle = '#ff0000'; +patternContext.fillRect(0, 0, 6, 6); +patternContext.fillStyle = '#ffff00'; +patternContext.fillRect(0, 0, 4, 4); + +var pattern = patternContext.createPattern(patternCanvas, 'repeat'); + +var gradient; + +module.exports = { + config: { + type: 'line', + data: { + datasets: [{ + data: [8, 8, 8, 8, 8, 8, 7, 8, 8, 8, 8], + pointBorderColor: '#ff0000', + pointBackgroundColor: '#00ff00', + showLine: false + }, { + label: '', + data: [4, 4, 4, 4, 4, 5, 3, 4, 4, 4, 4], + pointBorderColor: pattern, + pointBackgroundColor: pattern, + showLine: false + }, { + label: '', + data: [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0], + showLine: false + }], + labels: ['', '', '', '', '', '', '', '', '', '', ''] + }, + options: { + scales: { + x: {display: false}, + y: {display: false} + }, + elements: { + line: { + fill: false + } + }, + plugins: { + legend: false, + title: false, + filler: false, + tooltip: { + mode: 'nearest', + intersect: false, + callbacks: { + label: function() { + return '\u200b'; + }, + } + }, + }, + + layout: { + padding: 15 + } + }, + plugins: [{ + beforeDatasetsUpdate: function(chart) { + if (!gradient) { + gradient = chart.ctx.createLinearGradient(0, 0, 512, 256); + gradient.addColorStop(0, '#ff0000'); + gradient.addColorStop(1, '#0000ff'); + } + chart.config.data.datasets[2].pointBorderColor = gradient; + chart.config.data.datasets[2].pointBackgroundColor = gradient; + + return true; + }, + afterDraw: function(chart) { + var canvas = chart.canvas; + var rect = canvas.getBoundingClientRect(); + var point, event; + + for (var i = 0; i < 3; ++i) { + for (var j = 0; j < 11; ++j) { + point = chart.getDatasetMeta(i).data[j]; + event = { + type: 'mousemove', + target: canvas, + clientX: rect.left + point.x, + clientY: rect.top + point.y + }; + chart._handleEvent(event); + chart.tooltip.handleEvent(event); + chart.tooltip.opacity = j / 10; + chart.tooltip.draw(chart.ctx); + } + } + } + }] + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/plugin.tooltip/opacity.png b/test/fixtures/plugin.tooltip/opacity.png new file mode 100644 index 00000000000..716459732fd Binary files /dev/null and b/test/fixtures/plugin.tooltip/opacity.png differ diff --git a/test/fixtures/plugin.tooltip/point-style.js b/test/fixtures/plugin.tooltip/point-style.js new file mode 100644 index 00000000000..f8a07167138 --- /dev/null +++ b/test/fixtures/plugin.tooltip/point-style.js @@ -0,0 +1,77 @@ +const pointStyles = ['circle', 'cross', 'crossRot', 'dash', 'line', 'rect', 'rectRounded', 'rectRot', 'star', 'triangle', false]; + +function newDataset(pointStyle, i) { + return { + label: '', + data: pointStyles.map(() => i), + pointStyle: pointStyle, + pointBackgroundColor: '#0000ff', + pointBorderColor: '#00ff00', + showLine: false + }; +} +module.exports = { + config: { + type: 'line', + data: { + datasets: pointStyles.map((pointStyle, i) => newDataset(pointStyle, i)), + labels: pointStyles.map(() => '') + }, + options: { + scales: { + x: {display: false}, + y: {display: false} + }, + elements: { + line: { + fill: false + } + }, + plugins: { + legend: false, + title: false, + filler: false, + tooltip: { + mode: 'nearest', + intersect: false, + padding: 5, + usePointStyle: true, + callbacks: { + label: function() { + return '\u200b'; + } + } + }, + }, + layout: { + padding: 15 + } + }, + plugins: [{ + afterDraw: function(chart) { + var canvas = chart.canvas; + var rect = canvas.getBoundingClientRect(); + var point, event; + + for (var i = 0; i < pointStyles.length; ++i) { + point = chart.getDatasetMeta(i).data[i]; + event = { + type: 'mousemove', + target: canvas, + clientX: rect.left + point.x, + clientY: rect.top + point.y + }; + chart._handleEvent(event); + chart.tooltip.handleEvent(event); + chart.tooltip.draw(chart.ctx); + } + } + }] + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/plugin.tooltip/point-style.png b/test/fixtures/plugin.tooltip/point-style.png new file mode 100644 index 00000000000..e11b8cc5350 Binary files /dev/null and b/test/fixtures/plugin.tooltip/point-style.png differ diff --git a/test/fixtures/plugin.tooltip/positioning.js b/test/fixtures/plugin.tooltip/positioning.js new file mode 100644 index 00000000000..494bada5fce --- /dev/null +++ b/test/fixtures/plugin.tooltip/positioning.js @@ -0,0 +1,72 @@ +const data = []; +for (let x = 0; x < 3; x++) { + for (let y = 0; y < 3; y++) { + data.push({x, y}); + } +} + +module.exports = { + config: { + type: 'scatter', + data: { + datasets: [{ + data, + backgroundColor: 'red', + radius: 1, + hoverRadius: 0 + }], + }, + options: { + scales: { + x: {display: false}, + y: {display: false} + }, + plugins: { + legend: false, + title: false, + filler: false, + tooltip: { + mode: 'point', + intersect: true, + // spriteText: use white background to hide any gaps between fonts + backgroundColor: 'white', + borderColor: 'black', + borderWidth: 1, + callbacks: { + beforeLabel: () => 'before label', + label: () => 'label', + afterLabel: () => 'after1\nafter2\nafter3\nafter4\nafter5' + } + } + }, + }, + plugins: [{ + afterDraw: function(chart) { + const canvas = chart.canvas; + const rect = canvas.getBoundingClientRect(); + const meta = chart.getDatasetMeta(0); + let point, event; + + for (let i = 0; i < data.length; i++) { + point = meta.data[i]; + event = { + type: 'mousemove', + target: canvas, + clientX: rect.left + point.x, + clientY: rect.top + point.y + }; + chart._handleEvent(event); + chart.tooltip.handleEvent(event); + chart.tooltip.draw(chart.ctx); + } + } + }] + }, + options: { + spriteText: true, + canvas: { + height: 400, + width: 500 + } + } +}; diff --git a/test/fixtures/plugin.tooltip/positioning.png b/test/fixtures/plugin.tooltip/positioning.png new file mode 100644 index 00000000000..288764e0185 Binary files /dev/null and b/test/fixtures/plugin.tooltip/positioning.png differ diff --git a/test/fixtures/scale.category/invalid-data.js b/test/fixtures/scale.category/invalid-data.js new file mode 100644 index 00000000000..33d1ee58b9b --- /dev/null +++ b/test/fixtures/scale.category/invalid-data.js @@ -0,0 +1,41 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: ['a', 'b', 'c', 'd', 'e', 'f', 'g'], + datasets: [{ + data: [ + {x: 'a', y: 1}, + {x: null, y: 1}, + {x: 2, y: 1}, + {x: undefined, y: 1}, + {x: 4, y: 1}, + {x: NaN, y: 1}, + {x: 6, y: 1} + ], + backgroundColor: 'red', + borderColor: 'red', + borderWidth: 5 + }] + }, + options: { + scales: { + y: { + display: false + }, + x: { + grid: { + display: false + } + } + } + } + }, + options: { + spriteText: true, + canvas: { + width: 256, + height: 256 + } + } +}; diff --git a/test/fixtures/scale.category/invalid-data.png b/test/fixtures/scale.category/invalid-data.png new file mode 100644 index 00000000000..126c0c888d4 Binary files /dev/null and b/test/fixtures/scale.category/invalid-data.png differ diff --git a/test/fixtures/scale.category/max-ticks-limit-a.js b/test/fixtures/scale.category/max-ticks-limit-a.js new file mode 100644 index 00000000000..9a89bcb9c4e --- /dev/null +++ b/test/fixtures/scale.category/max-ticks-limit-a.js @@ -0,0 +1,37 @@ +const data = Array.from({length: 42}, (_, i) => i + 1); +const labels = data.map(v => 'tick' + v); + +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/7302', + config: { + type: 'bar', + data: { + datasets: [{ + data + }], + labels + }, + options: { + scales: { + x: { + ticks: { + display: false, + maxTicksLimit: 7 + }, + grid: { + color: 'red' + } + }, + y: {display: false} + }, + layout: { + padding: { + right: 2 + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.category/max-ticks-limit-a.png b/test/fixtures/scale.category/max-ticks-limit-a.png new file mode 100644 index 00000000000..8b0c7b8f898 Binary files /dev/null and b/test/fixtures/scale.category/max-ticks-limit-a.png differ diff --git a/test/fixtures/scale.category/max-ticks-limit-b.js b/test/fixtures/scale.category/max-ticks-limit-b.js new file mode 100644 index 00000000000..21654fd6a9f --- /dev/null +++ b/test/fixtures/scale.category/max-ticks-limit-b.js @@ -0,0 +1,37 @@ +const data = Array.from({length: 42}, (_, i) => i + 1); +const labels = data.map(v => 'tick' + v); + +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/7302', + config: { + type: 'bar', + data: { + datasets: [{ + data + }], + labels + }, + options: { + scales: { + x: { + ticks: { + display: false, + maxTicksLimit: 6 + }, + grid: { + color: 'red' + } + }, + y: {display: false} + }, + layout: { + padding: { + right: 2 + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.category/max-ticks-limit-b.png b/test/fixtures/scale.category/max-ticks-limit-b.png new file mode 100644 index 00000000000..f9f1a951627 Binary files /dev/null and b/test/fixtures/scale.category/max-ticks-limit-b.png differ diff --git a/test/fixtures/scale.category/max-ticks-limit-norotation.js b/test/fixtures/scale.category/max-ticks-limit-norotation.js new file mode 100644 index 00000000000..d9c86c53727 --- /dev/null +++ b/test/fixtures/scale.category/max-ticks-limit-norotation.js @@ -0,0 +1,37 @@ +const data = Array.from({length: 42}, (_, i) => i + 1); +const labels = data.map(v => 'tick' + v); + +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/10856', + config: { + type: 'bar', + data: { + datasets: [{ + data + }], + labels + }, + options: { + scales: { + x: { + ticks: { + display: true, + maxTicksLimit: 6 + }, + grid: { + color: 'red' + } + }, + y: {display: false} + }, + layout: { + padding: { + right: 2 + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.category/max-ticks-limit-norotation.png b/test/fixtures/scale.category/max-ticks-limit-norotation.png new file mode 100644 index 00000000000..063f06d1a96 Binary files /dev/null and b/test/fixtures/scale.category/max-ticks-limit-norotation.png differ diff --git a/test/fixtures/scale.category/ticks-from-data.js b/test/fixtures/scale.category/ticks-from-data.js new file mode 100644 index 00000000000..a82f643a332 --- /dev/null +++ b/test/fixtures/scale.category/ticks-from-data.js @@ -0,0 +1,27 @@ +module.exports = { + threshold: 0.01, + config: { + type: 'bar', + data: { + datasets: [{ + data: [10, 5, 0, 25, 78], + backgroundColor: 'transparent' + }], + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5'] + }, + options: { + indexAxis: 'y', + scales: { + x: {display: false}, + y: {display: true} + } + } + }, + options: { + spriteText: true, + canvas: { + width: 128, + height: 256 + } + } +}; diff --git a/test/fixtures/scale.category/ticks-from-data.png b/test/fixtures/scale.category/ticks-from-data.png new file mode 100644 index 00000000000..6ce9dc90cec Binary files /dev/null and b/test/fixtures/scale.category/ticks-from-data.png differ diff --git a/test/fixtures/scale.linear/grace/grace-10%.js b/test/fixtures/scale.linear/grace/grace-10%.js new file mode 100644 index 00000000000..3001fecca41 --- /dev/null +++ b/test/fixtures/scale.linear/grace/grace-10%.js @@ -0,0 +1,29 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: ['a', 'b'], + datasets: [{ + data: [90, -10], + }], + }, + options: { + indexAxis: 'y', + scales: { + y: { + display: false + }, + x: { + grace: '10%' + } + } + } + }, + options: { + spriteText: true, + canvas: { + width: 512, + height: 128 + } + } +}; diff --git a/test/fixtures/scale.linear/grace/grace-10%.png b/test/fixtures/scale.linear/grace/grace-10%.png new file mode 100644 index 00000000000..ae3db686608 Binary files /dev/null and b/test/fixtures/scale.linear/grace/grace-10%.png differ diff --git a/test/fixtures/scale.linear/grace/grace-beginAtZero.js b/test/fixtures/scale.linear/grace/grace-beginAtZero.js new file mode 100644 index 00000000000..77965d6c3fb --- /dev/null +++ b/test/fixtures/scale.linear/grace/grace-beginAtZero.js @@ -0,0 +1,42 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: ['a', 'b'], + datasets: [{ + data: [100, 0], + backgroundColor: 'blue' + }, { + xAxisID: 'x2', + data: [0, 100], + backgroundColor: 'red' + }], + }, + options: { + indexAxis: 'y', + scales: { + y: { + display: false + }, + x: { + position: 'top', + beginAtZero: true, + grace: '10%', + }, + x2: { + position: 'bottom', + type: 'linear', + beginAtZero: false, + grace: '10%', + } + } + } + }, + options: { + spriteText: true, + canvas: { + width: 512, + height: 128 + } + } +}; diff --git a/test/fixtures/scale.linear/grace/grace-beginAtZero.png b/test/fixtures/scale.linear/grace/grace-beginAtZero.png new file mode 100644 index 00000000000..4f1883fd399 Binary files /dev/null and b/test/fixtures/scale.linear/grace/grace-beginAtZero.png differ diff --git a/test/fixtures/scale.linear/grace/grace-neg.js b/test/fixtures/scale.linear/grace/grace-neg.js new file mode 100644 index 00000000000..90bdf0eb523 --- /dev/null +++ b/test/fixtures/scale.linear/grace/grace-neg.js @@ -0,0 +1,29 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: ['a'], + datasets: [{ + data: [-0.18], + }], + }, + options: { + indexAxis: 'y', + scales: { + y: { + display: false + }, + x: { + grace: '5%' + } + } + } + }, + options: { + spriteText: true, + canvas: { + width: 512, + height: 128 + } + } +}; diff --git a/test/fixtures/scale.linear/grace/grace-neg.png b/test/fixtures/scale.linear/grace/grace-neg.png new file mode 100644 index 00000000000..76ffd239d72 Binary files /dev/null and b/test/fixtures/scale.linear/grace/grace-neg.png differ diff --git a/test/fixtures/scale.linear/grace/grace-pos.js b/test/fixtures/scale.linear/grace/grace-pos.js new file mode 100644 index 00000000000..72df79a86de --- /dev/null +++ b/test/fixtures/scale.linear/grace/grace-pos.js @@ -0,0 +1,29 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: ['a'], + datasets: [{ + data: [0.18], + }], + }, + options: { + indexAxis: 'y', + scales: { + y: { + display: false + }, + x: { + grace: '5%' + } + } + } + }, + options: { + spriteText: true, + canvas: { + width: 512, + height: 128 + } + } +}; diff --git a/test/fixtures/scale.linear/grace/grace-pos.png b/test/fixtures/scale.linear/grace/grace-pos.png new file mode 100644 index 00000000000..bb902fee9c9 Binary files /dev/null and b/test/fixtures/scale.linear/grace/grace-pos.png differ diff --git a/test/fixtures/scale.linear/grace/grace.js b/test/fixtures/scale.linear/grace/grace.js new file mode 100644 index 00000000000..a2950e5b197 --- /dev/null +++ b/test/fixtures/scale.linear/grace/grace.js @@ -0,0 +1,29 @@ +module.exports = { + config: { + type: 'bar', + data: { + labels: ['a', 'b'], + datasets: [{ + data: [1.2, -0.2], + }], + }, + options: { + indexAxis: 'y', + scales: { + y: { + display: false + }, + x: { + grace: 0.3 + } + } + } + }, + options: { + spriteText: true, + canvas: { + width: 512, + height: 128 + } + } +}; diff --git a/test/fixtures/scale.linear/grace/grace.png b/test/fixtures/scale.linear/grace/grace.png new file mode 100644 index 00000000000..5e45396829e Binary files /dev/null and b/test/fixtures/scale.linear/grace/grace.png differ diff --git a/test/fixtures/scale.linear/grace/issue-8912.js b/test/fixtures/scale.linear/grace/issue-8912.js new file mode 100644 index 00000000000..3c11da49db9 --- /dev/null +++ b/test/fixtures/scale.linear/grace/issue-8912.js @@ -0,0 +1,26 @@ +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/8912', + config: { + type: 'bar', + data: { + labels: ['Red', 'Blue'], + datasets: [{ + data: [10, -10] + }] + }, + options: { + plugins: false, + scales: { + x: { + display: false, + }, + y: { + grace: '100%' + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.linear/grace/issue-8912.png b/test/fixtures/scale.linear/grace/issue-8912.png new file mode 100644 index 00000000000..0a6e768040d Binary files /dev/null and b/test/fixtures/scale.linear/grace/issue-8912.png differ diff --git a/test/fixtures/scale.linear/issue-8806.js b/test/fixtures/scale.linear/issue-8806.js new file mode 100644 index 00000000000..ec53498fc32 --- /dev/null +++ b/test/fixtures/scale.linear/issue-8806.js @@ -0,0 +1,26 @@ +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/8806', + config: { + type: 'bar', + data: { + labels: ['0', '1', '2', '3', '4', '5', '6'], + datasets: [{ + label: '# of Votes', + data: [32, 46, 28, 21, 20, 13, 27] + }] + }, + options: { + scales: { + x: {display: false}, + y: {ticks: {maxTicksLimit: 4}, min: 0} + } + } + }, + options: { + spriteText: true, + canvas: { + width: 256, + height: 256 + } + } +}; diff --git a/test/fixtures/scale.linear/issue-8806.png b/test/fixtures/scale.linear/issue-8806.png new file mode 100644 index 00000000000..9db9d332422 Binary files /dev/null and b/test/fixtures/scale.linear/issue-8806.png differ diff --git a/test/fixtures/scale.linear/min-max-skip/edge-case-1.js b/test/fixtures/scale.linear/min-max-skip/edge-case-1.js new file mode 100644 index 00000000000..ba0d1bedafa --- /dev/null +++ b/test/fixtures/scale.linear/min-max-skip/edge-case-1.js @@ -0,0 +1,31 @@ +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/8982', + config: { + type: 'scatter', + options: { + scales: { + y: { + max: 1069, + min: 230, + ticks: { + autoSkip: false + } + }, + x: { + max: 1069, + min: 230, + ticks: { + autoSkip: false + } + } + } + } + }, + options: { + spriteText: true, + canvas: { + height: 196, + width: 407 + } + } +}; diff --git a/test/fixtures/scale.linear/min-max-skip/edge-case-1.png b/test/fixtures/scale.linear/min-max-skip/edge-case-1.png new file mode 100644 index 00000000000..aff42592ee9 Binary files /dev/null and b/test/fixtures/scale.linear/min-max-skip/edge-case-1.png differ diff --git a/test/fixtures/scale.linear/min-max-skip/edge-case-2.js b/test/fixtures/scale.linear/min-max-skip/edge-case-2.js new file mode 100644 index 00000000000..b99037e14c1 --- /dev/null +++ b/test/fixtures/scale.linear/min-max-skip/edge-case-2.js @@ -0,0 +1,31 @@ +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/8982', + config: { + type: 'scatter', + options: { + scales: { + y: { + max: 1069, + min: 230, + ticks: { + autoSkip: false + } + }, + x: { + max: 1069, + min: 230, + ticks: { + autoSkip: false + } + } + } + } + }, + options: { + spriteText: true, + canvas: { + height: 197, + width: 420 + } + } +}; diff --git a/test/fixtures/scale.linear/min-max-skip/edge-case-2.png b/test/fixtures/scale.linear/min-max-skip/edge-case-2.png new file mode 100644 index 00000000000..b49359b0ccf Binary files /dev/null and b/test/fixtures/scale.linear/min-max-skip/edge-case-2.png differ diff --git a/test/fixtures/scale.linear/min-max-skip/edge-case-3.js b/test/fixtures/scale.linear/min-max-skip/edge-case-3.js new file mode 100644 index 00000000000..6b0d03aa9aa --- /dev/null +++ b/test/fixtures/scale.linear/min-max-skip/edge-case-3.js @@ -0,0 +1,31 @@ +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/8982', + config: { + type: 'scatter', + options: { + scales: { + y: { + max: 1069, + min: 230, + ticks: { + autoSkip: false + } + }, + x: { + max: 1069, + min: 230, + ticks: { + autoSkip: false + } + } + } + } + }, + options: { + spriteText: true, + canvas: { + height: 199, + width: 556 + } + } +}; diff --git a/test/fixtures/scale.linear/min-max-skip/edge-case-3.png b/test/fixtures/scale.linear/min-max-skip/edge-case-3.png new file mode 100644 index 00000000000..bb9b350f466 Binary files /dev/null and b/test/fixtures/scale.linear/min-max-skip/edge-case-3.png differ diff --git a/test/fixtures/scale.linear/min-max-skip/edge-case-4.js b/test/fixtures/scale.linear/min-max-skip/edge-case-4.js new file mode 100644 index 00000000000..2ff7b8b7313 --- /dev/null +++ b/test/fixtures/scale.linear/min-max-skip/edge-case-4.js @@ -0,0 +1,31 @@ +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/8982', + config: { + type: 'scatter', + options: { + scales: { + y: { + max: 1069, + min: 230, + ticks: { + autoSkip: false + } + }, + x: { + max: 1069, + min: 230, + ticks: { + autoSkip: false + } + } + } + } + }, + options: { + spriteText: true, + canvas: { + height: 200, + width: 557 + } + } +}; diff --git a/test/fixtures/scale.linear/min-max-skip/edge-case-4.png b/test/fixtures/scale.linear/min-max-skip/edge-case-4.png new file mode 100644 index 00000000000..9106a49f8dd Binary files /dev/null and b/test/fixtures/scale.linear/min-max-skip/edge-case-4.png differ diff --git a/test/fixtures/scale.linear/min-max-skip/includeBounds.js b/test/fixtures/scale.linear/min-max-skip/includeBounds.js new file mode 100644 index 00000000000..c1a1808563b --- /dev/null +++ b/test/fixtures/scale.linear/min-max-skip/includeBounds.js @@ -0,0 +1,26 @@ +module.exports = { + config: { + type: 'scatter', + options: { + scales: { + y: { + max: 1225.2, + min: 369.5, + ticks: { + includeBounds: false + } + }, + x: { + min: 20, + max: 100, + ticks: { + includeBounds: false + } + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.linear/min-max-skip/includeBounds.png b/test/fixtures/scale.linear/min-max-skip/includeBounds.png new file mode 100644 index 00000000000..afb6fe407af Binary files /dev/null and b/test/fixtures/scale.linear/min-max-skip/includeBounds.png differ diff --git a/test/fixtures/scale.linear/min-max-skip/min-max-skip.js b/test/fixtures/scale.linear/min-max-skip/min-max-skip.js new file mode 100644 index 00000000000..03fee6f9358 --- /dev/null +++ b/test/fixtures/scale.linear/min-max-skip/min-max-skip.js @@ -0,0 +1,20 @@ +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/7734', + config: { + type: 'line', + options: { + scales: { + y: { + max: 1225.2, + min: 369.5, + }, + x: { + display: false + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.linear/min-max-skip/min-max-skip.png b/test/fixtures/scale.linear/min-max-skip/min-max-skip.png new file mode 100644 index 00000000000..1ede5649364 Binary files /dev/null and b/test/fixtures/scale.linear/min-max-skip/min-max-skip.png differ diff --git a/test/fixtures/scale.linear/min-max-skip/no-collision.js b/test/fixtures/scale.linear/min-max-skip/no-collision.js new file mode 100644 index 00000000000..7d2b3a3e555 --- /dev/null +++ b/test/fixtures/scale.linear/min-max-skip/no-collision.js @@ -0,0 +1,41 @@ +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/9025', + threshold: 0.2, + config: { + type: 'line', + data: { + datasets: [{ + data: [ + {x: 10000000, y: 65}, + {x: 20000000, y: 12}, + {x: 30000000, y: 23}, + {x: 40000000, y: 51}, + {x: 50000000, y: 17}, + {x: 60000000, y: 23} + ] + }] + }, + options: { + scales: { + x: { + type: 'linear', + min: 10000000, + max: 60000000, + ticks: { + minRotation: 45, + maxRotation: 45, + count: 6 + } + } + } + } + }, + options: { + canvas: { + width: 200, + height: 200 + }, + spriteText: true + } +}; + diff --git a/test/fixtures/scale.linear/min-max-skip/no-collision.png b/test/fixtures/scale.linear/min-max-skip/no-collision.png new file mode 100644 index 00000000000..57180871d51 Binary files /dev/null and b/test/fixtures/scale.linear/min-max-skip/no-collision.png differ diff --git a/test/fixtures/scale.linear/min-max-skip/rotated-case-1.js b/test/fixtures/scale.linear/min-max-skip/rotated-case-1.js new file mode 100644 index 00000000000..cbc8999b21c --- /dev/null +++ b/test/fixtures/scale.linear/min-max-skip/rotated-case-1.js @@ -0,0 +1,34 @@ +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/9025', + threshold: 0.2, + config: { + type: 'scatter', + options: { + scales: { + y: { + max: 1069, + min: 230, + ticks: { + autoSkip: false, + minRotation: 22.5 + } + }, + x: { + max: 1069, + min: 230, + ticks: { + autoSkip: false, + minRotation: 67.5 + } + } + } + } + }, + options: { + spriteText: true, + canvas: { + height: 231, + width: 221 + } + } +}; diff --git a/test/fixtures/scale.linear/min-max-skip/rotated-case-1.png b/test/fixtures/scale.linear/min-max-skip/rotated-case-1.png new file mode 100644 index 00000000000..f2e445b4d92 Binary files /dev/null and b/test/fixtures/scale.linear/min-max-skip/rotated-case-1.png differ diff --git a/test/fixtures/scale.linear/min-max-skip/rotated-case-2.js b/test/fixtures/scale.linear/min-max-skip/rotated-case-2.js new file mode 100644 index 00000000000..5f1fbb8d9cb --- /dev/null +++ b/test/fixtures/scale.linear/min-max-skip/rotated-case-2.js @@ -0,0 +1,34 @@ +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/9025', + threshold: 0.2, + config: { + type: 'scatter', + options: { + scales: { + y: { + max: 1069, + min: 230, + ticks: { + autoSkip: false, + minRotation: 22.5 + } + }, + x: { + max: 1069, + min: 230, + ticks: { + autoSkip: false, + minRotation: 67.5 + } + } + } + } + }, + options: { + spriteText: true, + canvas: { + height: 232, + width: 222 + } + } +}; diff --git a/test/fixtures/scale.linear/min-max-skip/rotated-case-2.png b/test/fixtures/scale.linear/min-max-skip/rotated-case-2.png new file mode 100644 index 00000000000..6e2a3d6b7d5 Binary files /dev/null and b/test/fixtures/scale.linear/min-max-skip/rotated-case-2.png differ diff --git a/test/fixtures/scale.linear/min-max-skip/rotated-case-3.js b/test/fixtures/scale.linear/min-max-skip/rotated-case-3.js new file mode 100644 index 00000000000..71fb0e70495 --- /dev/null +++ b/test/fixtures/scale.linear/min-max-skip/rotated-case-3.js @@ -0,0 +1,34 @@ +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/9025', + threshold: 0.2, + config: { + type: 'scatter', + options: { + scales: { + y: { + max: 1069, + min: 230, + ticks: { + autoSkip: false, + minRotation: 22.5 + } + }, + x: { + max: 1069, + min: 230, + ticks: { + autoSkip: false, + minRotation: 67.5 + } + } + } + } + }, + options: { + spriteText: true, + canvas: { + height: 234, + width: 224 + } + } +}; diff --git a/test/fixtures/scale.linear/min-max-skip/rotated-case-3.png b/test/fixtures/scale.linear/min-max-skip/rotated-case-3.png new file mode 100644 index 00000000000..c0c3ddd6eab Binary files /dev/null and b/test/fixtures/scale.linear/min-max-skip/rotated-case-3.png differ diff --git a/test/fixtures/scale.linear/min-max-skip/rotated-case-4.js b/test/fixtures/scale.linear/min-max-skip/rotated-case-4.js new file mode 100644 index 00000000000..c639b3c5bdb --- /dev/null +++ b/test/fixtures/scale.linear/min-max-skip/rotated-case-4.js @@ -0,0 +1,34 @@ +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/9025', + threshold: 0.2, + config: { + type: 'scatter', + options: { + scales: { + y: { + max: 1069, + min: 230, + ticks: { + autoSkip: false, + minRotation: 22.5 + } + }, + x: { + max: 1069, + min: 230, + ticks: { + autoSkip: false, + minRotation: 67.5 + } + } + } + } + }, + options: { + spriteText: true, + canvas: { + height: 235, + width: 225 + } + } +}; diff --git a/test/fixtures/scale.linear/min-max-skip/rotated-case-4.png b/test/fixtures/scale.linear/min-max-skip/rotated-case-4.png new file mode 100644 index 00000000000..33792bcd43d Binary files /dev/null and b/test/fixtures/scale.linear/min-max-skip/rotated-case-4.png differ diff --git a/test/fixtures/scale.linear/rotated-45.js b/test/fixtures/scale.linear/rotated-45.js new file mode 100644 index 00000000000..7d9060f4780 --- /dev/null +++ b/test/fixtures/scale.linear/rotated-45.js @@ -0,0 +1,38 @@ +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/9025', + threshold: 0.2, + config: { + type: 'scatter', + options: { + scales: { + y: { + min: 1612781975085.5466, + max: 1620287255085.5466, + ticks: { + autoSkip: false, + minRotation: 45, + maxRotation: 45, + count: 13 + } + }, + x: { + min: 1612781975085.5466, + max: 1620287255085.5466, + ticks: { + autoSkip: false, + minRotation: 45, + maxRotation: 45, + count: 13 + } + } + } + } + }, + options: { + spriteText: true, + canvas: { + height: 350, + width: 350 + } + } +}; diff --git a/test/fixtures/scale.linear/rotated-45.png b/test/fixtures/scale.linear/rotated-45.png new file mode 100644 index 00000000000..04b8539a7b6 Binary files /dev/null and b/test/fixtures/scale.linear/rotated-45.png differ diff --git a/test/fixtures/scale.linear/rotated-5.js b/test/fixtures/scale.linear/rotated-5.js new file mode 100644 index 00000000000..0f654816e9e --- /dev/null +++ b/test/fixtures/scale.linear/rotated-5.js @@ -0,0 +1,34 @@ +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/9025', + threshold: 0.2, + config: { + type: 'scatter', + options: { + scales: { + y: { + min: 0, + max: 500000, + ticks: { + minRotation: 5, + maxRotation: 5, + } + }, + x: { + min: 0, + max: 500000, + ticks: { + minRotation: 5, + maxRotation: 5, + } + } + } + } + }, + options: { + spriteText: true, + canvas: { + height: 350, + width: 350 + } + } +}; diff --git a/test/fixtures/scale.linear/rotated-5.png b/test/fixtures/scale.linear/rotated-5.png new file mode 100644 index 00000000000..4c833348fa2 Binary files /dev/null and b/test/fixtures/scale.linear/rotated-5.png differ diff --git a/test/fixtures/scale.linear/rotated-85.js b/test/fixtures/scale.linear/rotated-85.js new file mode 100644 index 00000000000..d367beac983 --- /dev/null +++ b/test/fixtures/scale.linear/rotated-85.js @@ -0,0 +1,34 @@ +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/9025', + threshold: 0.2, + config: { + type: 'scatter', + options: { + scales: { + y: { + min: 0, + max: 500000, + ticks: { + minRotation: 85, + maxRotation: 85, + } + }, + x: { + min: 0, + max: 500000, + ticks: { + minRotation: 85, + maxRotation: 85, + } + } + } + } + }, + options: { + spriteText: true, + canvas: { + height: 350, + width: 350 + } + } +}; diff --git a/test/fixtures/scale.linear/rotated-85.png b/test/fixtures/scale.linear/rotated-85.png new file mode 100644 index 00000000000..da9e20e758c Binary files /dev/null and b/test/fixtures/scale.linear/rotated-85.png differ diff --git a/test/fixtures/scale.linear/tick-count-data-limits.js b/test/fixtures/scale.linear/tick-count-data-limits.js new file mode 100644 index 00000000000..497ed114573 --- /dev/null +++ b/test/fixtures/scale.linear/tick-count-data-limits.js @@ -0,0 +1,28 @@ +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/4234', + config: { + type: 'line', + data: { + datasets: [{ + data: [0, 2, 45, 30] + }], + labels: ['A', 'B', 'C', 'D'] + }, + options: { + scales: { + y: { + ticks: { + count: 21, + callback: (v) => v.toString(), + } + }, + x: { + display: false + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.linear/tick-count-data-limits.png b/test/fixtures/scale.linear/tick-count-data-limits.png new file mode 100644 index 00000000000..fd3f4ebbd1a Binary files /dev/null and b/test/fixtures/scale.linear/tick-count-data-limits.png differ diff --git a/test/fixtures/scale.linear/tick-count-min-max-not-aligned.js b/test/fixtures/scale.linear/tick-count-min-max-not-aligned.js new file mode 100644 index 00000000000..1747f9f39be --- /dev/null +++ b/test/fixtures/scale.linear/tick-count-min-max-not-aligned.js @@ -0,0 +1,23 @@ +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/4234', + config: { + type: 'line', + options: { + scales: { + y: { + max: 27, + min: -3, + ticks: { + count: 11, + } + }, + x: { + display: false + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.linear/tick-count-min-max-not-aligned.png b/test/fixtures/scale.linear/tick-count-min-max-not-aligned.png new file mode 100644 index 00000000000..6e4b21d9f4e Binary files /dev/null and b/test/fixtures/scale.linear/tick-count-min-max-not-aligned.png differ diff --git a/test/fixtures/scale.linear/tick-count-min-max-not-int.js b/test/fixtures/scale.linear/tick-count-min-max-not-int.js new file mode 100644 index 00000000000..a9f6e02f01a --- /dev/null +++ b/test/fixtures/scale.linear/tick-count-min-max-not-int.js @@ -0,0 +1,38 @@ +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/9078', + config: { + type: 'bar', + data: { + datasets: [{ + data: [ + {x: 1, y: 3.5}, + {x: 2, y: 4.7}, + {x: 3, y: 7.3}, + {x: 4, y: 6.7} + ] + }] + }, + options: { + scales: { + x: { + type: 'linear', + display: false, + }, + y: { + min: 3.5, + max: 8.5, + ticks: { + count: 6, + } + } + } + } + }, + options: { + spriteText: true, + canvas: { + width: 256, + height: 256 + } + } +}; diff --git a/test/fixtures/scale.linear/tick-count-min-max-not-int.png b/test/fixtures/scale.linear/tick-count-min-max-not-int.png new file mode 100644 index 00000000000..0b94172fd7a Binary files /dev/null and b/test/fixtures/scale.linear/tick-count-min-max-not-int.png differ diff --git a/test/fixtures/scale.linear/tick-count-min-max.js b/test/fixtures/scale.linear/tick-count-min-max.js new file mode 100644 index 00000000000..49ec3fba861 --- /dev/null +++ b/test/fixtures/scale.linear/tick-count-min-max.js @@ -0,0 +1,23 @@ +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/4234', + config: { + type: 'line', + options: { + scales: { + y: { + max: 50, + min: 0, + ticks: { + count: 21, + } + }, + x: { + display: false + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.linear/tick-count-min-max.png b/test/fixtures/scale.linear/tick-count-min-max.png new file mode 100644 index 00000000000..a82179124e7 Binary files /dev/null and b/test/fixtures/scale.linear/tick-count-min-max.png differ diff --git a/test/fixtures/scale.linear/tick-step-min-max-step-fp.js b/test/fixtures/scale.linear/tick-step-min-max-step-fp.js new file mode 100644 index 00000000000..00d3f9c4955 --- /dev/null +++ b/test/fixtures/scale.linear/tick-step-min-max-step-fp.js @@ -0,0 +1,24 @@ +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/9334', + config: { + type: 'line', + options: { + scales: { + y: { + display: false, + }, + x: { + type: 'linear', + min: 7.2, + max: 21.6, + ticks: { + stepSize: 1.8 + } + }, + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.linear/tick-step-min-max-step-fp.png b/test/fixtures/scale.linear/tick-step-min-max-step-fp.png new file mode 100644 index 00000000000..b11e88478e6 Binary files /dev/null and b/test/fixtures/scale.linear/tick-step-min-max-step-fp.png differ diff --git a/test/fixtures/scale.linear/tick-step-min-max.js b/test/fixtures/scale.linear/tick-step-min-max.js new file mode 100644 index 00000000000..115549b91b9 --- /dev/null +++ b/test/fixtures/scale.linear/tick-step-min-max.js @@ -0,0 +1,23 @@ +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/4234', + config: { + type: 'line', + options: { + scales: { + y: { + max: 27, + min: -3, + ticks: { + stepSize: 3, + } + }, + x: { + display: false + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.linear/tick-step-min-max.png b/test/fixtures/scale.linear/tick-step-min-max.png new file mode 100644 index 00000000000..6e4b21d9f4e Binary files /dev/null and b/test/fixtures/scale.linear/tick-step-min-max.png differ diff --git a/test/fixtures/scale.linear/tiny-numbers.js b/test/fixtures/scale.linear/tiny-numbers.js new file mode 100644 index 00000000000..65ddbbfdfd6 --- /dev/null +++ b/test/fixtures/scale.linear/tiny-numbers.js @@ -0,0 +1,18 @@ +// Should generate max and min that are not equal when data contains values that are very close to each other + +module.exports = { + config: { + type: 'scatter', + data: { + datasets: [{ + data: [ + {x: 1, y: 1.8548483304974972}, + {x: 2, y: 1.8548483304974974}, + ] + }], + }, + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.linear/tiny-numbers.png b/test/fixtures/scale.linear/tiny-numbers.png new file mode 100644 index 00000000000..a336cd10e41 Binary files /dev/null and b/test/fixtures/scale.linear/tiny-numbers.png differ diff --git a/test/fixtures/scale.logarithmic/large-range.js b/test/fixtures/scale.logarithmic/large-range.js new file mode 100644 index 00000000000..ba123837eff --- /dev/null +++ b/test/fixtures/scale.logarithmic/large-range.js @@ -0,0 +1,31 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], + datasets: [{ + backgroundColor: 'red', + borderColor: 'red', + fill: false, + data: [23, 21, 34, 52, 115, 3333, 5116] + }] + }, + options: { + responsive: true, + scales: { + x: { + display: false, + }, + y: { + type: 'logarithmic', + ticks: { + autoSkip: false + } + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.logarithmic/large-range.png b/test/fixtures/scale.logarithmic/large-range.png new file mode 100644 index 00000000000..13e4538c1d1 Binary files /dev/null and b/test/fixtures/scale.logarithmic/large-range.png differ diff --git a/test/fixtures/scale.logarithmic/large-values-small-range.js b/test/fixtures/scale.logarithmic/large-values-small-range.js new file mode 100644 index 00000000000..726d49353cf --- /dev/null +++ b/test/fixtures/scale.logarithmic/large-values-small-range.js @@ -0,0 +1,31 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], + datasets: [{ + backgroundColor: 'red', + borderColor: 'red', + fill: false, + data: [5000.002, 5000.012, 5000.01, 5000.03, 5000.04, 5000.004, 5000.032] + }] + }, + options: { + responsive: true, + scales: { + x: { + display: false, + }, + y: { + type: 'logarithmic', + ticks: { + autoSkip: false + } + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.logarithmic/large-values-small-range.png b/test/fixtures/scale.logarithmic/large-values-small-range.png new file mode 100644 index 00000000000..47c70393edd Binary files /dev/null and b/test/fixtures/scale.logarithmic/large-values-small-range.png differ diff --git a/test/fixtures/scale.logarithmic/med-range.js b/test/fixtures/scale.logarithmic/med-range.js new file mode 100644 index 00000000000..a6191fbb101 --- /dev/null +++ b/test/fixtures/scale.logarithmic/med-range.js @@ -0,0 +1,31 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], + datasets: [{ + backgroundColor: 'red', + borderColor: 'red', + fill: false, + data: [25, 24, 27, 32, 45, 30, 28] + }] + }, + options: { + responsive: true, + scales: { + x: { + display: false, + }, + y: { + type: 'logarithmic', + ticks: { + autoSkip: false + } + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.logarithmic/med-range.png b/test/fixtures/scale.logarithmic/med-range.png new file mode 100644 index 00000000000..ed9b5bfa7f5 Binary files /dev/null and b/test/fixtures/scale.logarithmic/med-range.png differ diff --git a/test/fixtures/scale.logarithmic/min-max.js b/test/fixtures/scale.logarithmic/min-max.js new file mode 100644 index 00000000000..ff577180088 --- /dev/null +++ b/test/fixtures/scale.logarithmic/min-max.js @@ -0,0 +1,33 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], + datasets: [{ + backgroundColor: 'red', + borderColor: 'red', + fill: false, + data: [250, 240, 270, 320, 450, 300, 280] + }] + }, + options: { + responsive: true, + scales: { + x: { + display: false, + }, + y: { + type: 'logarithmic', + min: 233, + max: 471, + ticks: { + autoSkip: false + } + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.logarithmic/min-max.png b/test/fixtures/scale.logarithmic/min-max.png new file mode 100644 index 00000000000..c5e582f481c Binary files /dev/null and b/test/fixtures/scale.logarithmic/min-max.png differ diff --git a/test/fixtures/scale.logarithmic/null-values.js b/test/fixtures/scale.logarithmic/null-values.js new file mode 100644 index 00000000000..a9450f0d623 --- /dev/null +++ b/test/fixtures/scale.logarithmic/null-values.js @@ -0,0 +1,49 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], + datasets: [{ + backgroundColor: 'red', + borderColor: 'red', + fill: false, + data: [ + 150, + null, + 1500, + 200, + 9000, + 3000, + 8888 + ], + spanGaps: true + }, { + backgroundColor: 'blue', + borderColor: 'blue', + fill: false, + data: [ + 1000, + 5500, + 800, + 7777, + null, + 6666, + 5555 + ], + spanGaps: false + }] + }, + options: { + responsive: true, + scales: { + x: { + display: false, + }, + y: { + display: false, + type: 'logarithmic', + } + } + } + } +}; diff --git a/test/fixtures/scale.logarithmic/null-values.png b/test/fixtures/scale.logarithmic/null-values.png new file mode 100644 index 00000000000..d75929610c1 Binary files /dev/null and b/test/fixtures/scale.logarithmic/null-values.png differ diff --git a/test/fixtures/scale.logarithmic/small-range.js b/test/fixtures/scale.logarithmic/small-range.js new file mode 100644 index 00000000000..d60ed2a1871 --- /dev/null +++ b/test/fixtures/scale.logarithmic/small-range.js @@ -0,0 +1,31 @@ +module.exports = { + config: { + type: 'line', + data: { + labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], + datasets: [{ + backgroundColor: 'red', + borderColor: 'red', + fill: false, + data: [3, 1, 4, 2, 5, 3, 16] + }] + }, + options: { + responsive: true, + scales: { + x: { + display: false, + }, + y: { + type: 'logarithmic', + ticks: { + autoSkip: false + } + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.logarithmic/small-range.png b/test/fixtures/scale.logarithmic/small-range.png new file mode 100644 index 00000000000..8ccb0dbdb85 Binary files /dev/null and b/test/fixtures/scale.logarithmic/small-range.png differ diff --git a/test/fixtures/scale.radialLinear/anglelines-disable.json b/test/fixtures/scale.radialLinear/anglelines-disable.json new file mode 100644 index 00000000000..ac1585d6028 --- /dev/null +++ b/test/fixtures/scale.radialLinear/anglelines-disable.json @@ -0,0 +1,28 @@ +{ + "config": { + "type": "radar", + "data": { + "labels": ["A", "B", "C", "D", "E"] + }, + "options": { + "responsive": false, + "scales": { + "r": { + "grid": { + "color": "rgb(0, 0, 0)", + "lineWidth": 1 + }, + "angleLines": { + "display": false + }, + "pointLabels": { + "display": false + }, + "ticks": { + "display": false + } + } + } + } + } +} diff --git a/test/fixtures/scale.radialLinear/anglelines-disable.png b/test/fixtures/scale.radialLinear/anglelines-disable.png new file mode 100644 index 00000000000..dcba77a8df7 Binary files /dev/null and b/test/fixtures/scale.radialLinear/anglelines-disable.png differ diff --git a/test/fixtures/scale.radialLinear/anglelines-indexable.js b/test/fixtures/scale.radialLinear/anglelines-indexable.js new file mode 100644 index 00000000000..8430a5ebc45 --- /dev/null +++ b/test/fixtures/scale.radialLinear/anglelines-indexable.js @@ -0,0 +1,28 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: ['A', 'B', 'C', 'D', 'E'] + }, + options: { + responsive: false, + scales: { + r: { + grid: { + display: true, + }, + angleLines: { + color: ['red', 'green'], + lineWidth: [1, 5] + }, + pointLabels: { + display: false + }, + ticks: { + display: false + } + } + } + } + } +}; diff --git a/test/fixtures/scale.radialLinear/anglelines-indexable.png b/test/fixtures/scale.radialLinear/anglelines-indexable.png new file mode 100644 index 00000000000..dfbb15bb113 Binary files /dev/null and b/test/fixtures/scale.radialLinear/anglelines-indexable.png differ diff --git a/test/fixtures/scale.radialLinear/anglelines-reverse-scale.js b/test/fixtures/scale.radialLinear/anglelines-reverse-scale.js new file mode 100644 index 00000000000..af35a68bcc9 --- /dev/null +++ b/test/fixtures/scale.radialLinear/anglelines-reverse-scale.js @@ -0,0 +1,35 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: ['A', 'B', 'C', 'D', 'E'], + datasets: [{ + data: [1, 1, 2, 3, 5] + }] + }, + options: { + responsive: false, + scales: { + r: { + reverse: true, + grid: { + display: true, + }, + angleLines: { + color: 'red', + lineWidth: 5, + }, + pointLabels: { + display: false + }, + ticks: { + display: true, + } + } + } + } + }, + options: { + spriteText: true, + } +}; diff --git a/test/fixtures/scale.radialLinear/anglelines-reverse-scale.png b/test/fixtures/scale.radialLinear/anglelines-reverse-scale.png new file mode 100644 index 00000000000..f2367b980c2 Binary files /dev/null and b/test/fixtures/scale.radialLinear/anglelines-reverse-scale.png differ diff --git a/test/fixtures/scale.radialLinear/anglelines-scriptable.js b/test/fixtures/scale.radialLinear/anglelines-scriptable.js new file mode 100644 index 00000000000..170dbfd7989 --- /dev/null +++ b/test/fixtures/scale.radialLinear/anglelines-scriptable.js @@ -0,0 +1,32 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: ['A', 'B', 'C', 'D', 'E'] + }, + options: { + responsive: false, + scales: { + r: { + grid: { + display: true, + }, + angleLines: { + color: function(context) { + return context.index % 2 === 0 ? 'red' : 'green'; + }, + lineWidth: function(context) { + return context.index % 2 === 0 ? 1 : 5; + }, + }, + pointLabels: { + display: false + }, + ticks: { + display: false + } + } + } + } + } +}; diff --git a/test/fixtures/scale.radialLinear/anglelines-scriptable.png b/test/fixtures/scale.radialLinear/anglelines-scriptable.png new file mode 100644 index 00000000000..dfbb15bb113 Binary files /dev/null and b/test/fixtures/scale.radialLinear/anglelines-scriptable.png differ diff --git a/test/fixtures/scale.radialLinear/backgroundColor.js b/test/fixtures/scale.radialLinear/backgroundColor.js new file mode 100644 index 00000000000..405877d3d3c --- /dev/null +++ b/test/fixtures/scale.radialLinear/backgroundColor.js @@ -0,0 +1,37 @@ +module.exports = { + threshold: 0.01, + config: { + type: 'radar', + data: { + labels: [1, 2, 3, 4, 5, 6], + datasets: [ + { + data: [3, 2, 2, 1, 3, 1] + } + ] + }, + options: { + plugins: { + legend: false, + tooltip: false, + filler: false + }, + scales: { + r: { + backgroundColor: '#00FF00', + min: 0, + max: 3, + pointLabels: { + display: false + }, + ticks: { + display: false, + stepSize: 1, + } + } + }, + responsive: true, + maintainAspectRatio: false + } + }, +}; diff --git a/test/fixtures/scale.radialLinear/backgroundColor.png b/test/fixtures/scale.radialLinear/backgroundColor.png new file mode 100644 index 00000000000..d61a1c9218d Binary files /dev/null and b/test/fixtures/scale.radialLinear/backgroundColor.png differ diff --git a/test/fixtures/scale.radialLinear/border-dash.json b/test/fixtures/scale.radialLinear/border-dash.json new file mode 100644 index 00000000000..356f4e508b8 --- /dev/null +++ b/test/fixtures/scale.radialLinear/border-dash.json @@ -0,0 +1,35 @@ +{ + "config": { + "type": "radar", + "data": { + "labels": ["A", "B", "C", "D", "E"] + }, + "options": { + "responsive": false, + "scales": { + "r": { + "grid": { + "color": "rgba(0, 0, 255, 0.5)", + "lineWidth": 1 + }, + "border": { + "dash": [4, 2], + "dashOffset": 2 + }, + "angleLines": { + "color": "rgba(0, 0, 255, 0.5)", + "lineWidth": 1, + "borderDash": [4, 2], + "borderDashOffset": 2 + }, + "pointLabels": { + "display": false + }, + "ticks": { + "display": false + } + } + } + } + } +} diff --git a/test/fixtures/scale.radialLinear/border-dash.png b/test/fixtures/scale.radialLinear/border-dash.png new file mode 100644 index 00000000000..e13494fd485 Binary files /dev/null and b/test/fixtures/scale.radialLinear/border-dash.png differ diff --git a/test/fixtures/scale.radialLinear/circular-backgroundColor.js b/test/fixtures/scale.radialLinear/circular-backgroundColor.js new file mode 100644 index 00000000000..0e47fe0b0fc --- /dev/null +++ b/test/fixtures/scale.radialLinear/circular-backgroundColor.js @@ -0,0 +1,40 @@ +module.exports = { + threshold: 0.05, + config: { + type: 'radar', + data: { + labels: [1, 2, 3, 4, 5, 6], + datasets: [ + { + data: [3, 2, 2, 1, 3, 1] + } + ] + }, + options: { + plugins: { + legend: false, + tooltip: false, + filler: false + }, + scales: { + r: { + backgroundColor: '#00FF00', + min: 0, + max: 3, + grid: { + circular: true + }, + pointLabels: { + display: false + }, + ticks: { + display: false, + stepSize: 1, + } + } + }, + responsive: true, + maintainAspectRatio: false + } + }, +}; diff --git a/test/fixtures/scale.radialLinear/circular-backgroundColor.png b/test/fixtures/scale.radialLinear/circular-backgroundColor.png new file mode 100644 index 00000000000..5da9a2d6213 Binary files /dev/null and b/test/fixtures/scale.radialLinear/circular-backgroundColor.png differ diff --git a/test/fixtures/scale.radialLinear/circular-border-dash.json b/test/fixtures/scale.radialLinear/circular-border-dash.json new file mode 100644 index 00000000000..3425e0e7b96 --- /dev/null +++ b/test/fixtures/scale.radialLinear/circular-border-dash.json @@ -0,0 +1,36 @@ +{ + "config": { + "type": "radar", + "data": { + "labels": ["A", "B", "C", "D", "E"] + }, + "options": { + "responsive": false, + "scales": { + "r": { + "border": { + "dash": [4, 2], + "dashOffset": 2 + }, + "grid": { + "circular": true, + "color": "rgba(0, 0, 255, 0.5)", + "lineWidth": 1 + }, + "angleLines": { + "color": "rgba(0, 0, 255, 0.5)", + "lineWidth": 1, + "borderDash": [4, 2], + "borderDashOffset": 2 + }, + "pointLabels": { + "display": false + }, + "ticks": { + "display": false + } + } + } + } + } +} diff --git a/test/fixtures/scale.radialLinear/circular-border-dash.png b/test/fixtures/scale.radialLinear/circular-border-dash.png new file mode 100644 index 00000000000..28983505526 Binary files /dev/null and b/test/fixtures/scale.radialLinear/circular-border-dash.png differ diff --git a/test/fixtures/scale.radialLinear/gridlines-disable.json b/test/fixtures/scale.radialLinear/gridlines-disable.json new file mode 100644 index 00000000000..34ddf053b84 --- /dev/null +++ b/test/fixtures/scale.radialLinear/gridlines-disable.json @@ -0,0 +1,28 @@ +{ + "config": { + "type": "radar", + "data": { + "labels": ["A", "B", "C", "D", "E"] + }, + "options": { + "responsive": false, + "scales": { + "r": { + "grid": { + "display": false + }, + "angleLines": { + "color": "rgb(0, 0, 0)", + "lineWidth": 1 + }, + "pointLabels": { + "display": false + }, + "ticks": { + "display": false + } + } + } + } + } +} diff --git a/test/fixtures/scale.radialLinear/gridlines-disable.png b/test/fixtures/scale.radialLinear/gridlines-disable.png new file mode 100644 index 00000000000..d756eac2796 Binary files /dev/null and b/test/fixtures/scale.radialLinear/gridlines-disable.png differ diff --git a/test/fixtures/scale.radialLinear/gridlines-no-z.json b/test/fixtures/scale.radialLinear/gridlines-no-z.json new file mode 100644 index 00000000000..c2ee605967d --- /dev/null +++ b/test/fixtures/scale.radialLinear/gridlines-no-z.json @@ -0,0 +1,41 @@ +{ + "config": { + "type": "radar", + "data": { + "labels": ["A", "B", "C", "D", "E"], + "datasets": [ + { + "backgroundColor": "rgba(255, 0, 0, 1)", + "data": [1, 2, 3, 3, 3] + } + ] + }, + "options": { + "responsive": false, + "scales": { + "r": { + "grid": { + "color": "rgba(0, 0, 0, 1)", + "lineWidth": 1 + }, + "angleLines": { + "color": "rgba(0, 0, 255, 1)", + "lineWidth": 1 + }, + "pointLabels": { + "display": false + }, + "ticks": { + "display": false + } + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false, + "filler": true + } + } + } +} diff --git a/test/fixtures/scale.radialLinear/gridlines-no-z.png b/test/fixtures/scale.radialLinear/gridlines-no-z.png new file mode 100644 index 00000000000..c545c564d53 Binary files /dev/null and b/test/fixtures/scale.radialLinear/gridlines-no-z.png differ diff --git a/test/fixtures/scale.radialLinear/gridlines-scriptable.js b/test/fixtures/scale.radialLinear/gridlines-scriptable.js new file mode 100644 index 00000000000..9c21e30cf14 --- /dev/null +++ b/test/fixtures/scale.radialLinear/gridlines-scriptable.js @@ -0,0 +1,34 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: ['A', 'B', 'C', 'D', 'E'] + }, + options: { + responsive: false, + scales: { + r: { + grid: { + display: true, + color: function(context) { + return context.index % 2 === 0 ? 'green' : 'red'; + }, + lineWidth: function(context) { + return context.index % 2 === 0 ? 5 : 1; + }, + }, + angleLines: { + color: 'rgba(255, 255, 255, 0.5)', + lineWidth: 2 + }, + pointLabels: { + display: false + }, + ticks: { + display: false + } + } + } + } + } +}; diff --git a/test/fixtures/scale.radialLinear/gridlines-scriptable.png b/test/fixtures/scale.radialLinear/gridlines-scriptable.png new file mode 100644 index 00000000000..7cbf28962de Binary files /dev/null and b/test/fixtures/scale.radialLinear/gridlines-scriptable.png differ diff --git a/test/fixtures/scale.radialLinear/gridlines-z.json b/test/fixtures/scale.radialLinear/gridlines-z.json new file mode 100644 index 00000000000..17aed68a292 --- /dev/null +++ b/test/fixtures/scale.radialLinear/gridlines-z.json @@ -0,0 +1,42 @@ +{ + "config": { + "type": "radar", + "data": { + "labels": ["A", "B", "C", "D", "E"], + "datasets": [ + { + "backgroundColor": "rgba(255, 0, 0, 1)", + "data": [1, 2, 3, 3, 3] + } + ] + }, + "options": { + "responsive": false, + "scales": { + "r": { + "grid": { + "color": "rgba(0, 0, 0, 1)", + "lineWidth": 1, + "z": 1 + }, + "angleLines": { + "color": "rgba(0, 0, 255, 1)", + "lineWidth": 1 + }, + "pointLabels": { + "display": false + }, + "ticks": { + "display": false + } + } + }, + "plugins": { + "legend": false, + "title": false, + "tooltip": false, + "filler": true + } + } + } +} diff --git a/test/fixtures/scale.radialLinear/gridlines-z.png b/test/fixtures/scale.radialLinear/gridlines-z.png new file mode 100644 index 00000000000..61485f315e1 Binary files /dev/null and b/test/fixtures/scale.radialLinear/gridlines-z.png differ diff --git a/test/fixtures/scale.radialLinear/indexable-gridlines.json b/test/fixtures/scale.radialLinear/indexable-gridlines.json new file mode 100644 index 00000000000..7aaae2b2ae7 --- /dev/null +++ b/test/fixtures/scale.radialLinear/indexable-gridlines.json @@ -0,0 +1,41 @@ +{ + "config": { + "type": "radar", + "data": { + "labels": ["A", "B", "C", "D", "E"] + }, + "options": { + "responsive": false, + "scales": { + "r": { + "grid": { + "display": true, + "color": [ + "rgba(0, 0, 0, 0.5)", + "rgba(255, 255, 255, 0.5)", + false, + "", + "rgba(255, 0, 0, 0.5)", + "rgba(0, 255, 0, 0.5)", + "rgba(0, 0, 255, 0.5)", + "rgba(255, 255, 0, 0.5)", + "rgba(255, 0, 255, 0.5)", + "rgba(0, 255, 255, 0.5)" + ], + "lineWidth": [false, 0, 1, 2, 1, 2, 1, 2, 1, 2] + }, + "angleLines": { + "color": "rgba(255, 255, 255, 0.5)", + "lineWidth": 2 + }, + "pointLabels": { + "display": false + }, + "ticks": { + "display": false + } + } + } + } + } +} diff --git a/test/fixtures/scale.radialLinear/indexable-gridlines.png b/test/fixtures/scale.radialLinear/indexable-gridlines.png new file mode 100644 index 00000000000..55f39c6991a Binary files /dev/null and b/test/fixtures/scale.radialLinear/indexable-gridlines.png differ diff --git a/test/fixtures/scale.radialLinear/pointLabels/background.js b/test/fixtures/scale.radialLinear/pointLabels/background.js new file mode 100644 index 00000000000..62b90a182b6 --- /dev/null +++ b/test/fixtures/scale.radialLinear/pointLabels/background.js @@ -0,0 +1,50 @@ +module.exports = { + tolerance: 0.01, + config: { + type: 'radar', + data: { + labels: [ + ['VENTE ET', 'COMMERCIALISATION'], + ['GESTION', 'FINANCIÈRE'], + 'NUMÉRIQUE', + ['ADMINISTRATION', 'ET OPÉRATION'], + ['RESSOURCES', 'HUMAINES'], + 'INNOVATION' + ], + datasets: [ + { + backgroundColor: '#E43E51', + label: 'Compétences entrepreunariales', + data: [3, 2, 2, 1, 3, 1] + } + ] + }, + options: { + plugins: { + legend: false, + tooltip: false, + filler: false + }, + scales: { + r: { + min: 0, + max: 3, + pointLabels: { + backdropColor: 'blue', + backdropPadding: {left: 5, right: 5, top: 2, bottom: 2}, + }, + ticks: { + display: false, + stepSize: 1, + maxTicksLimit: 1 + } + } + }, + responsive: true, + maintainAspectRatio: false + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.radialLinear/pointLabels/background.png b/test/fixtures/scale.radialLinear/pointLabels/background.png new file mode 100644 index 00000000000..95bf8b28b1e Binary files /dev/null and b/test/fixtures/scale.radialLinear/pointLabels/background.png differ diff --git a/test/fixtures/scale.radialLinear/pointLabels/border-radius.js b/test/fixtures/scale.radialLinear/pointLabels/border-radius.js new file mode 100644 index 00000000000..f3d3347268a --- /dev/null +++ b/test/fixtures/scale.radialLinear/pointLabels/border-radius.js @@ -0,0 +1,51 @@ +module.exports = { + tolerance: 0.01, + config: { + type: 'radar', + data: { + labels: [ + ['VENTE ET', 'COMMERCIALISATION'], + ['GESTION', 'FINANCIÈRE'], + 'NUMÉRIQUE', + ['ADMINISTRATION', 'ET OPÉRATION'], + ['RESSOURCES', 'HUMAINES'], + 'INNOVATION' + ], + datasets: [ + { + backgroundColor: '#E43E51', + label: 'Compétences entrepreunariales', + data: [3, 2, 2, 1, 3, 1] + } + ] + }, + options: { + plugins: { + legend: false, + tooltip: false, + filler: false + }, + scales: { + r: { + min: 0, + max: 3, + pointLabels: { + backdropColor: 'blue', + backdropPadding: {left: 5, right: 5, top: 2, bottom: 2}, + borderRadius: 10, + }, + ticks: { + display: false, + stepSize: 1, + maxTicksLimit: 1 + } + } + }, + responsive: true, + maintainAspectRatio: false + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.radialLinear/pointLabels/border-radius.png b/test/fixtures/scale.radialLinear/pointLabels/border-radius.png new file mode 100644 index 00000000000..30aba3ec06a Binary files /dev/null and b/test/fixtures/scale.radialLinear/pointLabels/border-radius.png differ diff --git a/test/fixtures/scale.radialLinear/pointLabels/no-more-than-half-radius.js b/test/fixtures/scale.radialLinear/pointLabels/no-more-than-half-radius.js new file mode 100644 index 00000000000..cddd4362daf --- /dev/null +++ b/test/fixtures/scale.radialLinear/pointLabels/no-more-than-half-radius.js @@ -0,0 +1,34 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: ['Too long label 1', 'Too long label 2', 'Too long label 3', 'Too long label 4'], + datasets: [ + { + backgroundColor: '#E43E51', + data: [1, 1, 1, 1] + } + ] + }, + options: { + scales: { + r: { + max: 1, + ticks: { + display: false, + }, + grid: { + display: false + } + } + }, + } + }, + options: { + spriteText: true, + canvas: { + width: 256, + height: 256 + } + } +}; diff --git a/test/fixtures/scale.radialLinear/pointLabels/no-more-than-half-radius.png b/test/fixtures/scale.radialLinear/pointLabels/no-more-than-half-radius.png new file mode 100644 index 00000000000..522292ef8cf Binary files /dev/null and b/test/fixtures/scale.radialLinear/pointLabels/no-more-than-half-radius.png differ diff --git a/test/fixtures/scale.radialLinear/pointLabels/padding.js b/test/fixtures/scale.radialLinear/pointLabels/padding.js new file mode 100644 index 00000000000..348ab539737 --- /dev/null +++ b/test/fixtures/scale.radialLinear/pointLabels/padding.js @@ -0,0 +1,49 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: [ + ['VENTE ET', 'COMMERCIALISATION'], + ['GESTION', 'FINANCIÈRE'], + 'NUMÉRIQUE', + ['ADMINISTRATION', 'ET OPÉRATION'], + ['RESSOURCES', 'HUMAINES'], + 'INNOVATION' + ], + datasets: [ + { + radius: 12, + backgroundColor: '#E43E51', + label: 'Compétences entrepreunariales', + data: [3, 2, 2, 1, 3, 1] + } + ] + }, + options: { + plugins: { + legend: false, + tooltip: false, + filler: false + }, + scales: { + r: { + min: 0, + max: 3, + pointLabels: { + padding: 30 + }, + ticks: { + display: false, + stepSize: 1, + maxTicksLimit: 1 + } + } + }, + responsive: true, + maintainAspectRatio: false + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.radialLinear/pointLabels/padding.png b/test/fixtures/scale.radialLinear/pointLabels/padding.png new file mode 100644 index 00000000000..2b55d4b09b6 Binary files /dev/null and b/test/fixtures/scale.radialLinear/pointLabels/padding.png differ diff --git a/test/fixtures/scale.radialLinear/pointLabels/scriptable-color-small.js b/test/fixtures/scale.radialLinear/pointLabels/scriptable-color-small.js new file mode 100644 index 00000000000..640bf2992b1 --- /dev/null +++ b/test/fixtures/scale.radialLinear/pointLabels/scriptable-color-small.js @@ -0,0 +1,33 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange', 'Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange', 'Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange', 'Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], + datasets: [{ + label: '# of Votes', + data: [12, 19, 3, 5, 1, 3, 12, 19, 3, 5, 1, 3, 12, 19, 3, 5, 1, 3, 12, 19, 3, 5, 1, 3] + }] + }, + options: { + scales: { + r: { + ticks: { + display: false, + }, + angleLines: { + color: (ctx) => { + return ctx.index % 2 === 0 ? 'green' : 'red'; + } + }, + pointLabels: { + display: false, + } + } + }, + } + }, + options: { + spriteText: true, + width: 300, + } +}; diff --git a/test/fixtures/scale.radialLinear/pointLabels/scriptable-color-small.png b/test/fixtures/scale.radialLinear/pointLabels/scriptable-color-small.png new file mode 100644 index 00000000000..6ab82f867b7 Binary files /dev/null and b/test/fixtures/scale.radialLinear/pointLabels/scriptable-color-small.png differ diff --git a/test/fixtures/scale.radialLinear/ticks-below-zero.js b/test/fixtures/scale.radialLinear/ticks-below-zero.js new file mode 100644 index 00000000000..75647815ab5 --- /dev/null +++ b/test/fixtures/scale.radialLinear/ticks-below-zero.js @@ -0,0 +1,45 @@ +module.exports = { + config: { + type: 'radar', + data: { + labels: ['A', 'B', 'C', 'D', 'E'] + }, + options: { + responsive: false, + scales: { + r: { + min: -1, + max: 1, + grid: { + display: true, + color: 'blue', + lineWidth: 2 + }, + angleLines: { + color: 'rgba(255, 255, 255, 0.5)', + lineWidth: 2 + }, + pointLabels: { + display: false + }, + ticks: { + display: true, + autoSkip: false, + stepSize: 0.2, + callback: function(value) { + if (value === 0.8) { + return 'Strong'; + } + if (value === 0.4) { + return 'Weak'; + } + if (value === 0) { + return 'No'; + } + } + } + } + } + } + } +}; diff --git a/test/fixtures/scale.radialLinear/ticks-below-zero.png b/test/fixtures/scale.radialLinear/ticks-below-zero.png new file mode 100644 index 00000000000..36435fd1e1c Binary files /dev/null and b/test/fixtures/scale.radialLinear/ticks-below-zero.png differ diff --git a/test/fixtures/scale.time/autoskip-major.js b/test/fixtures/scale.time/autoskip-major.js new file mode 100644 index 00000000000..e207d697c38 --- /dev/null +++ b/test/fixtures/scale.time/autoskip-major.js @@ -0,0 +1,41 @@ +var date = moment('Jan 01 1990', 'MMM DD YYYY'); +var data = []; +for (var i = 0; i < 60; i++) { + data.push({x: date.valueOf(), y: i}); + date = date.clone().add(1, 'month'); +} + +module.exports = { + threshold: 0.05, + config: { + type: 'line', + data: { + datasets: [{ + xAxisID: 'x', + data: data, + fill: false + }], + }, + options: { + scales: { + x: { + type: 'time', + ticks: { + major: { + enabled: true + }, + source: 'data', + autoSkip: true, + maxRotation: 0 + } + }, + y: { + display: false + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.time/autoskip-major.png b/test/fixtures/scale.time/autoskip-major.png new file mode 100644 index 00000000000..0c1d3a323b4 Binary files /dev/null and b/test/fixtures/scale.time/autoskip-major.png differ diff --git a/test/fixtures/scale.time/bar-large-gap-between-data.js b/test/fixtures/scale.time/bar-large-gap-between-data.js new file mode 100644 index 00000000000..965a008a152 --- /dev/null +++ b/test/fixtures/scale.time/bar-large-gap-between-data.js @@ -0,0 +1,55 @@ +var date = moment('May 24 2020', 'MMM DD YYYY'); + +module.exports = { + threshold: 0.05, + config: { + type: 'bar', + data: { + datasets: [{ + backgroundColor: 'rgba(255, 0, 0, 0.5)', + data: [ + { + x: date.clone().add(-2, 'day'), + y: 20, + }, + { + x: date.clone().add(-1, 'day'), + y: 30, + }, + { + x: date, + y: 40, + }, + { + x: date.clone().add(1, 'day'), + y: 50, + }, + { + x: date.clone().add(7, 'day'), + y: 10, + } + ] + }] + }, + options: { + scales: { + x: { + display: false, + type: 'time', + ticks: { + source: 'auto' + }, + time: { + unit: 'day' + } + }, + y: { + display: false + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.time/bar-large-gap-between-data.png b/test/fixtures/scale.time/bar-large-gap-between-data.png new file mode 100644 index 00000000000..c228f066dd4 Binary files /dev/null and b/test/fixtures/scale.time/bar-large-gap-between-data.png differ diff --git a/test/fixtures/scale.time/custom-parser.js b/test/fixtures/scale.time/custom-parser.js new file mode 100644 index 00000000000..8a449a39e4c --- /dev/null +++ b/test/fixtures/scale.time/custom-parser.js @@ -0,0 +1,41 @@ +module.exports = { + threshold: 0.01, + tolerance: 0.0025, + config: { + type: 'line', + data: { + labels: ['foo', 'bar'], + datasets: [{ + data: [0, 1], + fill: false + }], + }, + options: { + scales: { + x: { + type: 'time', + position: 'bottom', + time: { + unit: 'day', + round: true, + parser: function(label) { + return label === 'foo' ? + moment('2000/01/02', 'YYYY/MM/DD') : + moment('2016/05/08', 'YYYY/MM/DD'); + } + }, + ticks: { + source: 'labels' + } + }, + y: { + display: false + } + } + } + }, + options: { + spriteText: true, + canvas: {width: 256, height: 128} + } +}; diff --git a/test/fixtures/scale.time/custom-parser.png b/test/fixtures/scale.time/custom-parser.png new file mode 100644 index 00000000000..adc1d768256 Binary files /dev/null and b/test/fixtures/scale.time/custom-parser.png differ diff --git a/test/fixtures/scale.time/data-ty.js b/test/fixtures/scale.time/data-ty.js new file mode 100644 index 00000000000..0b0bb53272f --- /dev/null +++ b/test/fixtures/scale.time/data-ty.js @@ -0,0 +1,59 @@ +function newDateFromRef(days) { + return moment('01/01/2015 12:00', 'DD/MM/YYYY HH:mm').add(days, 'd').toDate(); +} + +module.exports = { + threshold: 0.01, + tolerance: 0.003, + config: { + type: 'line', + data: { + datasets: [{ + data: [{ + t: newDateFromRef(0), + y: 1 + }, { + t: newDateFromRef(1), + y: 10 + }, { + t: newDateFromRef(2), + y: 0 + }, { + t: newDateFromRef(4), + y: 5 + }, { + t: newDateFromRef(6), + y: 77 + }, { + t: newDateFromRef(7), + y: 9 + }, { + t: newDateFromRef(9), + y: 5 + }], + fill: false, + parsing: { + xAxisKey: 't' + } + }], + }, + options: { + scales: { + x: { + type: 'time', + position: 'bottom', + ticks: { + maxRotation: 0 + } + }, + y: { + display: false + } + } + } + }, + options: { + spriteText: true, + canvas: {width: 800, height: 200} + } +}; diff --git a/test/fixtures/scale.time/data-ty.png b/test/fixtures/scale.time/data-ty.png new file mode 100644 index 00000000000..c1a05e72a2a Binary files /dev/null and b/test/fixtures/scale.time/data-ty.png differ diff --git a/test/fixtures/scale.time/data-xy.js b/test/fixtures/scale.time/data-xy.js new file mode 100644 index 00000000000..759fdf3d554 --- /dev/null +++ b/test/fixtures/scale.time/data-xy.js @@ -0,0 +1,56 @@ +function newDateFromRef(days) { + return moment('01/01/2015 12:00', 'DD/MM/YYYY HH:mm').add(days, 'd').toDate(); +} + +module.exports = { + threshold: 0.01, + tolerance: 0.003, + config: { + type: 'line', + data: { + datasets: [{ + data: [{ + x: newDateFromRef(0), + y: 1 + }, { + x: newDateFromRef(1), + y: 10 + }, { + x: newDateFromRef(2), + y: 0 + }, { + x: newDateFromRef(4), + y: 5 + }, { + x: newDateFromRef(6), + y: 77 + }, { + x: newDateFromRef(7), + y: 9 + }, { + x: newDateFromRef(9), + y: 5 + }], + fill: false + }], + }, + options: { + scales: { + x: { + type: 'time', + position: 'bottom', + ticks: { + maxRotation: 0 + } + }, + y: { + display: false + } + } + } + }, + options: { + spriteText: true, + canvas: {width: 800, height: 200} + } +}; diff --git a/test/fixtures/scale.time/data-xy.png b/test/fixtures/scale.time/data-xy.png new file mode 100644 index 00000000000..c1a05e72a2a Binary files /dev/null and b/test/fixtures/scale.time/data-xy.png differ diff --git a/test/fixtures/scale.time/invalid-data.js b/test/fixtures/scale.time/invalid-data.js new file mode 100644 index 00000000000..fa5faadcaed --- /dev/null +++ b/test/fixtures/scale.time/invalid-data.js @@ -0,0 +1,50 @@ +module.exports = { + description: 'Invalid data, https://github.com/chartjs/Chart.js/issues/5563', + config: { + type: 'line', + data: { + datasets: [{ + data: [{ + x: '14:45:00', + y: 20, + }, { + x: '20:30:00', + y: 10, + }, { + x: '25:15:00', + y: 15, + }, { + x: null, + y: 15, + }, { + x: undefined, + y: 15, + }, { + x: NaN, + y: 15, + }, { + x: 'monday', + y: 15, + }], + }] + }, + options: { + scales: { + x: { + type: 'time', + time: { + parser: 'HH:mm:ss', + unit: 'hour' + }, + }, + }, + layout: { + padding: 16 + } + } + }, + options: { + spriteText: true, + canvas: {width: 1000, height: 200} + } +}; diff --git a/test/fixtures/scale.time/invalid-data.png b/test/fixtures/scale.time/invalid-data.png new file mode 100644 index 00000000000..60c1ec16f95 Binary files /dev/null and b/test/fixtures/scale.time/invalid-data.png differ diff --git a/test/fixtures/scale.time/labels-date.js b/test/fixtures/scale.time/labels-date.js new file mode 100644 index 00000000000..165f9ab14f9 --- /dev/null +++ b/test/fixtures/scale.time/labels-date.js @@ -0,0 +1,29 @@ +function newDateFromRef(days) { + return moment('01/01/2015 12:00', 'DD/MM/YYYY HH:mm').add(days, 'd').toDate(); +} + +module.exports = { + threshold: 0.1, + tolerance: 0.002, + config: { + type: 'line', + data: { + labels: [newDateFromRef(0), newDateFromRef(1), newDateFromRef(2), newDateFromRef(4), newDateFromRef(6), newDateFromRef(7), newDateFromRef(9)], + fill: false + }, + options: { + scales: { + x: { + type: 'time', + }, + y: { + display: false + } + } + } + }, + options: { + spriteText: true, + canvas: {width: 1000, height: 200} + } +}; diff --git a/test/fixtures/scale.time/labels-date.png b/test/fixtures/scale.time/labels-date.png new file mode 100644 index 00000000000..2e88d0b0a7c Binary files /dev/null and b/test/fixtures/scale.time/labels-date.png differ diff --git a/test/fixtures/scale.time/labels-strings.js b/test/fixtures/scale.time/labels-strings.js new file mode 100644 index 00000000000..2417c48e619 --- /dev/null +++ b/test/fixtures/scale.time/labels-strings.js @@ -0,0 +1,24 @@ +module.exports = { + threshold: 0.05, + tolerance: 0.002, + config: { + type: 'line', + data: { + labels: ['2015-01-01T12:00:00', '2015-01-02T21:00:00', '2015-01-03T22:00:00', '2015-01-05T23:00:00', '2015-01-07T03:00', '2015-01-08T10:00', '2015-01-10T12:00'] + }, + options: { + scales: { + x: { + type: 'time', + }, + y: { + display: false + } + } + } + }, + options: { + spriteText: true, + canvas: {width: 1000, height: 200} + } +}; diff --git a/test/fixtures/scale.time/labels-strings.png b/test/fixtures/scale.time/labels-strings.png new file mode 100644 index 00000000000..2e88d0b0a7c Binary files /dev/null and b/test/fixtures/scale.time/labels-strings.png differ diff --git a/test/fixtures/scale.time/labels.js b/test/fixtures/scale.time/labels.js new file mode 100644 index 00000000000..0ec95e01971 --- /dev/null +++ b/test/fixtures/scale.time/labels.js @@ -0,0 +1,47 @@ +var timeOpts = { + parser: 'YYYY', + unit: 'year', + displayFormats: { + year: 'YYYY' + } +}; + +module.exports = { + threshold: 0.01, + tolerance: 0.002, + config: { + type: 'line', + data: { + labels: ['1975', '1976', '1977'], + xLabels: ['1985', '1986', '1987'], + yLabels: ['1995', '1996', '1997'] + }, + options: { + scales: { + x: { + type: 'time', + labels: ['2015', '2016', '2017'], + time: timeOpts + }, + x2: { + type: 'time', + position: 'bottom', + time: timeOpts + }, + y: { + type: 'time', + time: timeOpts + }, + y2: { + position: 'left', + type: 'time', + labels: ['2005', '2006', '2007'], + time: timeOpts + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.time/labels.png b/test/fixtures/scale.time/labels.png new file mode 100644 index 00000000000..a6c3db6a0dd Binary files /dev/null and b/test/fixtures/scale.time/labels.png differ diff --git a/test/fixtures/scale.time/negative-times.js b/test/fixtures/scale.time/negative-times.js new file mode 100644 index 00000000000..9b3e80d13fd --- /dev/null +++ b/test/fixtures/scale.time/negative-times.js @@ -0,0 +1,41 @@ +module.exports = { + config: { + type: 'line', + data: { + datasets: [{ + fill: true, + backgroundColor: 'red', + data: [ + {x: -1000000, y: 1}, + {x: 1000000000, y: 2} + ] + }] + }, + options: { + scales: { + x: { + type: 'time', + time: { + unit: 'day' + }, + ticks: { + display: false + } + }, + y: { + ticks: { + display: false + } + } + }, + plugins: { + legend: false, + title: false, + tooltip: false + } + } + }, + options: { + canvas: {width: 1000, height: 200} + } +}; diff --git a/test/fixtures/scale.time/negative-times.png b/test/fixtures/scale.time/negative-times.png new file mode 100644 index 00000000000..8bb66db48ff Binary files /dev/null and b/test/fixtures/scale.time/negative-times.png differ diff --git a/test/fixtures/scale.time/offset-auto-skip-ticks.js b/test/fixtures/scale.time/offset-auto-skip-ticks.js new file mode 100644 index 00000000000..ae28ce0bdcd --- /dev/null +++ b/test/fixtures/scale.time/offset-auto-skip-ticks.js @@ -0,0 +1,52 @@ +const data = { + labels: [], + datasets: [{ + label: 'Dataset', + borderColor: '#2f54eb', + data: [{ + y: 3, + x: 1646345700000 + }, { + y: 7, + x: 1646346600000 + }, { + y: 9, + x: 1646347500000 + }, { + y: 5, + x: 1646348400000 + }, { + y: 5, + x: 1646349300000 + }], + }] +}; + +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/10215', + config: { + type: 'bar', + data, + options: { + maintainAspectRatio: false, + scales: { + x: { + type: 'time', + offset: true, + offsetAfterAutoskip: true, + axis: 'x', + grid: { + offset: true + }, + }, + y: { + display: false, + } + } + } + }, + options: { + spriteText: true, + canvas: {width: 600, height: 400} + } +}; diff --git a/test/fixtures/scale.time/offset-auto-skip-ticks.png b/test/fixtures/scale.time/offset-auto-skip-ticks.png new file mode 100644 index 00000000000..a7168a27726 Binary files /dev/null and b/test/fixtures/scale.time/offset-auto-skip-ticks.png differ diff --git a/test/fixtures/scale.time/offset-with-1-tick.js b/test/fixtures/scale.time/offset-with-1-tick.js new file mode 100644 index 00000000000..74873079159 --- /dev/null +++ b/test/fixtures/scale.time/offset-with-1-tick.js @@ -0,0 +1,59 @@ +const data = { + datasets: [ + { + label: 6, + backgroundColor: 'red', + data: [ + { + x: '2021-03-24', + y: 464 + } + ] + }, + { + label: 1, + backgroundColor: 'red', + data: [ + { + x: '2021-03-24', + y: 464 + } + ] + }, + { + label: 17, + backgroundColor: 'blue', + data: [ + { + x: '2021-03-24', + y: 390 + } + ] + } + ] +}; + +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/8718', + config: { + type: 'bar', + data, + options: { + scales: { + x: { + type: 'time', + time: { + unit: 'day', + }, + }, + y: { + display: false + } + } + } + }, + options: { + spriteText: true, + canvas: {width: 256, height: 128} + } +}; diff --git a/test/fixtures/scale.time/offset-with-1-tick.png b/test/fixtures/scale.time/offset-with-1-tick.png new file mode 100644 index 00000000000..87870bff50b Binary files /dev/null and b/test/fixtures/scale.time/offset-with-1-tick.png differ diff --git a/test/fixtures/scale.time/offset-with-2-ticks.js b/test/fixtures/scale.time/offset-with-2-ticks.js new file mode 100644 index 00000000000..f5708d013a1 --- /dev/null +++ b/test/fixtures/scale.time/offset-with-2-ticks.js @@ -0,0 +1,89 @@ +const data = { + datasets: [ + { + label: 1, + backgroundColor: 'orange', + data: [ + { + x: '2021-03-24', + y: 464 + } + ] + }, + { + label: 2, + backgroundColor: 'red', + data: [ + { + x: '2021-03-24', + y: 464 + } + ] + }, + { + label: 3, + backgroundColor: 'blue', + data: [ + { + x: '2021-03-24', + y: 390 + } + ] + }, + { + label: 4, + backgroundColor: 'purple', + data: [ + { + x: '2021-03-25', + y: 464 + } + ] + }, + { + label: 5, + backgroundColor: 'black', + data: [ + { + x: '2021-03-25', + y: 464 + } + ] + }, + { + label: 6, + backgroundColor: 'cyan', + data: [ + { + x: '2021-03-25', + y: 390 + } + ] + } + ] +}; + +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/8718', + config: { + type: 'bar', + data, + options: { + scales: { + x: { + type: 'time', + time: { + unit: 'day', + }, + }, + y: { + display: false + } + } + } + }, + options: { + spriteText: true, + canvas: {width: 256, height: 128} + } +}; diff --git a/test/fixtures/scale.time/offset-with-2-ticks.png b/test/fixtures/scale.time/offset-with-2-ticks.png new file mode 100644 index 00000000000..5b02f001598 Binary files /dev/null and b/test/fixtures/scale.time/offset-with-2-ticks.png differ diff --git a/test/fixtures/scale.time/offset-with-no-ticks.js b/test/fixtures/scale.time/offset-with-no-ticks.js new file mode 100644 index 00000000000..c3b184e3654 --- /dev/null +++ b/test/fixtures/scale.time/offset-with-no-ticks.js @@ -0,0 +1,80 @@ +const data = { + datasets: [ + { + data: [ + { + x: moment('15/10/2020', 'DD/MM/YYYY').valueOf(), + y: 55 + }, + { + x: moment('18/10/2020', 'DD/MM/YYYY').valueOf(), + y: 10 + }, + { + x: moment('19/10/2020', 'DD/MM/YYYY').valueOf(), + y: 15 + } + ], + backgroundColor: 'blue' + }, + { + data: [ + { + x: moment('15/10/2020', 'DD/MM/YYYY').valueOf(), + y: 6 + }, + { + x: moment('18/10/2020', 'DD/MM/YYYY').valueOf(), + y: 11 + }, + { + x: moment('19/10/2020', 'DD/MM/YYYY').valueOf(), + y: 16 + } + ], + backgroundColor: 'green', + }, + { + data: [ + { + x: moment('15/10/2020', 'DD/MM/YYYY').valueOf(), + y: 7 + }, + { + x: moment('18/10/2020', 'DD/MM/YYYY').valueOf(), + y: 12 + }, + { + x: moment('19/10/2020', 'DD/MM/YYYY').valueOf(), + y: 17 + } + ], + backgroundColor: 'red', + } + ] +}; + +module.exports = { + description: 'https://github.com/chartjs/Chart.js/issues/7991', + config: { + type: 'bar', + data, + options: { + scales: { + x: { + type: 'time', + time: { + unit: 'month', + }, + }, + y: { + display: false + } + } + } + }, + options: { + spriteText: true, + canvas: {width: 256, height: 128} + } +}; diff --git a/test/fixtures/scale.time/offset-with-no-ticks.png b/test/fixtures/scale.time/offset-with-no-ticks.png new file mode 100644 index 00000000000..cfc78366b5e Binary files /dev/null and b/test/fixtures/scale.time/offset-with-no-ticks.png differ diff --git a/test/fixtures/scale.time/skip-null-gridlines.js b/test/fixtures/scale.time/skip-null-gridlines.js new file mode 100644 index 00000000000..f8ded031883 --- /dev/null +++ b/test/fixtures/scale.time/skip-null-gridlines.js @@ -0,0 +1,32 @@ +module.exports = { + threshold: 0.01, + tolerance: 0.0025, + config: { + type: 'line', + data: { + labels: ['2017', '2018', '2019', '2020', '2025'], + datasets: [{data: [0, 1, 2, 3, 4], fill: false}] + }, + options: { + scales: { + x: { + type: 'time', + time: { + parser: 'YYYY', + unit: 'year' + }, + ticks: { + source: 'auto', + callback: (tick, index) => index % 2 === 0 ? null : tick, + } + }, + y: { + display: false + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.time/skip-null-gridlines.png b/test/fixtures/scale.time/skip-null-gridlines.png new file mode 100644 index 00000000000..d0609a8db5c Binary files /dev/null and b/test/fixtures/scale.time/skip-null-gridlines.png differ diff --git a/test/fixtures/scale.time/skip-undefined-gridlines.js b/test/fixtures/scale.time/skip-undefined-gridlines.js new file mode 100644 index 00000000000..2871a818405 --- /dev/null +++ b/test/fixtures/scale.time/skip-undefined-gridlines.js @@ -0,0 +1,32 @@ +module.exports = { + threshold: 0.01, + tolerance: 0.0025, + config: { + type: 'line', + data: { + labels: ['2017', '2018', '2019', '2020', '2025'], + datasets: [{data: [0, 1, 2, 3, 4], fill: false}] + }, + options: { + scales: { + x: { + type: 'time', + time: { + parser: 'YYYY', + unit: 'year' + }, + ticks: { + source: 'auto', + callback: (tick, index) => index % 2 === 0 ? undefined : tick, + } + }, + y: { + display: false + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.time/skip-undefined-gridlines.png b/test/fixtures/scale.time/skip-undefined-gridlines.png new file mode 100644 index 00000000000..d0609a8db5c Binary files /dev/null and b/test/fixtures/scale.time/skip-undefined-gridlines.png differ diff --git a/test/fixtures/scale.time/source-auto-linear.js b/test/fixtures/scale.time/source-auto-linear.js new file mode 100644 index 00000000000..3313ecd29a0 --- /dev/null +++ b/test/fixtures/scale.time/source-auto-linear.js @@ -0,0 +1,31 @@ +module.exports = { + threshold: 0.01, + tolerance: 0.0025, + config: { + type: 'line', + data: { + labels: ['2017', '2018', '2019', '2020', '2025'], + datasets: [{data: [0, 1, 2, 3, 4], fill: false}] + }, + options: { + scales: { + x: { + type: 'time', + time: { + parser: 'YYYY', + unit: 'year' + }, + ticks: { + source: 'auto' + } + }, + y: { + display: false + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.time/source-auto-linear.png b/test/fixtures/scale.time/source-auto-linear.png new file mode 100644 index 00000000000..fb668ecc209 Binary files /dev/null and b/test/fixtures/scale.time/source-auto-linear.png differ diff --git a/test/fixtures/scale.time/source-data-linear.js b/test/fixtures/scale.time/source-data-linear.js new file mode 100644 index 00000000000..34c00ba81ee --- /dev/null +++ b/test/fixtures/scale.time/source-data-linear.js @@ -0,0 +1,31 @@ +module.exports = { + threshold: 0.01, + tolerance: 0.0015, + config: { + type: 'line', + data: { + labels: ['2017', '2018', '2019', '2020', '2025'], + datasets: [{data: [0, 1, 2, 3, 4], fill: false}] + }, + options: { + scales: { + x: { + type: 'time', + time: { + parser: 'YYYY', + unit: 'year' + }, + ticks: { + source: 'data' + } + }, + y: { + display: false + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.time/source-data-linear.png b/test/fixtures/scale.time/source-data-linear.png new file mode 100644 index 00000000000..3b4d1a03c3e Binary files /dev/null and b/test/fixtures/scale.time/source-data-linear.png differ diff --git a/test/fixtures/scale.time/source-labels-linear-offset-min-max.js b/test/fixtures/scale.time/source-labels-linear-offset-min-max.js new file mode 100644 index 00000000000..4b54a3d072e --- /dev/null +++ b/test/fixtures/scale.time/source-labels-linear-offset-min-max.js @@ -0,0 +1,33 @@ +module.exports = { + threshold: 0.01, + tolerance: 0.0015, + config: { + type: 'line', + data: { + labels: ['2017', '2019', '2020', '2025', '2042'], + datasets: [{data: [0, 1, 2, 3, 4, 5], fill: false}] + }, + options: { + scales: { + x: { + type: 'time', + min: '2012', + max: '2051', + offset: true, + time: { + parser: 'YYYY', + }, + ticks: { + source: 'labels' + } + }, + y: { + display: false + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.time/source-labels-linear-offset-min-max.png b/test/fixtures/scale.time/source-labels-linear-offset-min-max.png new file mode 100644 index 00000000000..f7c2fbe9da4 Binary files /dev/null and b/test/fixtures/scale.time/source-labels-linear-offset-min-max.png differ diff --git a/test/fixtures/scale.time/source-labels-linear.js b/test/fixtures/scale.time/source-labels-linear.js new file mode 100644 index 00000000000..69565ff7e36 --- /dev/null +++ b/test/fixtures/scale.time/source-labels-linear.js @@ -0,0 +1,31 @@ +module.exports = { + threshold: 0.01, + tolerance: 0.0015, + config: { + type: 'line', + data: { + labels: ['2017', '2018', '2019', '2020', '2025'], + datasets: [{data: [0, 1, 2, 3, 4], fill: false}] + }, + options: { + scales: { + x: { + type: 'time', + time: { + parser: 'YYYY', + unit: 'year' + }, + ticks: { + source: 'labels' + } + }, + y: { + display: false + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.time/source-labels-linear.png b/test/fixtures/scale.time/source-labels-linear.png new file mode 100644 index 00000000000..3b4d1a03c3e Binary files /dev/null and b/test/fixtures/scale.time/source-labels-linear.png differ diff --git a/test/fixtures/scale.time/ticks-capacity.js b/test/fixtures/scale.time/ticks-capacity.js new file mode 100644 index 00000000000..225bc916484 --- /dev/null +++ b/test/fixtures/scale.time/ticks-capacity.js @@ -0,0 +1,29 @@ +module.exports = { + threshold: 0.01, + tolerance: 0.002, + config: { + type: 'line', + data: { + labels: [ + '2012-01-01', '2013-01-01', '2014-01-01', '2015-01-01', + '2016-01-01', '2017-01-01', '2018-01-01', '2019-01-01' + ] + }, + options: { + scales: { + x: { + type: 'time', + time: { + unit: 'year' + } + }, + y: { + display: false + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.time/ticks-capacity.png b/test/fixtures/scale.time/ticks-capacity.png new file mode 100644 index 00000000000..b17a91cb892 Binary files /dev/null and b/test/fixtures/scale.time/ticks-capacity.png differ diff --git a/test/fixtures/scale.time/ticks-minunit.js b/test/fixtures/scale.time/ticks-minunit.js new file mode 100644 index 00000000000..43836601919 --- /dev/null +++ b/test/fixtures/scale.time/ticks-minunit.js @@ -0,0 +1,28 @@ +module.exports = { + threshold: 0.01, + tolerance: 0.002, + config: { + type: 'line', + data: { + labels: ['2015-01-01T20:00:00', '2015-01-02T21:00:00'], + }, + options: { + scales: { + x: { + type: 'time', + bounds: 'ticks', + time: { + minUnit: 'day' + } + }, + y: { + display: false + } + } + } + }, + options: { + spriteText: true, + canvas: {width: 256, height: 128} + } +}; diff --git a/test/fixtures/scale.time/ticks-minunit.png b/test/fixtures/scale.time/ticks-minunit.png new file mode 100644 index 00000000000..dbe3221453f Binary files /dev/null and b/test/fixtures/scale.time/ticks-minunit.png differ diff --git a/test/fixtures/scale.time/ticks-reverse-linear-min-max.js b/test/fixtures/scale.time/ticks-reverse-linear-min-max.js new file mode 100644 index 00000000000..6785fdf13a5 --- /dev/null +++ b/test/fixtures/scale.time/ticks-reverse-linear-min-max.js @@ -0,0 +1,33 @@ +module.exports = { + threshold: 0.01, + tolerance: 0.0015, + config: { + type: 'line', + data: { + labels: ['2017', '2019', '2020', '2025', '2042'], + datasets: [{data: [0, 1, 2, 3, 4, 5], fill: false}] + }, + options: { + scales: { + x: { + type: 'time', + min: '2012', + max: '2050', + time: { + parser: 'YYYY' + }, + reverse: true, + ticks: { + source: 'labels' + } + }, + y: { + display: false + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.time/ticks-reverse-linear-min-max.png b/test/fixtures/scale.time/ticks-reverse-linear-min-max.png new file mode 100644 index 00000000000..01c9c763108 Binary files /dev/null and b/test/fixtures/scale.time/ticks-reverse-linear-min-max.png differ diff --git a/test/fixtures/scale.time/ticks-reverse-linear.js b/test/fixtures/scale.time/ticks-reverse-linear.js new file mode 100644 index 00000000000..991fcc84460 --- /dev/null +++ b/test/fixtures/scale.time/ticks-reverse-linear.js @@ -0,0 +1,30 @@ +module.exports = { + threshold: 0.01, + config: { + type: 'line', + data: { + labels: ['2017', '2019', '2020', '2025', '2042'], + datasets: [{data: [0, 1, 2, 3, 4], fill: false}] + }, + options: { + scales: { + x: { + type: 'time', + time: { + parser: 'YYYY' + }, + reverse: true, + ticks: { + source: 'labels' + } + }, + y: { + display: false + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.time/ticks-reverse-linear.png b/test/fixtures/scale.time/ticks-reverse-linear.png new file mode 100644 index 00000000000..992504c1cc9 Binary files /dev/null and b/test/fixtures/scale.time/ticks-reverse-linear.png differ diff --git a/test/fixtures/scale.time/ticks-reverse-offset.js b/test/fixtures/scale.time/ticks-reverse-offset.js new file mode 100644 index 00000000000..37a3ebb89f9 --- /dev/null +++ b/test/fixtures/scale.time/ticks-reverse-offset.js @@ -0,0 +1,33 @@ +module.exports = { + threshold: 0.01, + tolerance: 0.002, + config: { + type: 'line', + data: { + labels: ['2017', '2018', '2019', '2020', '2021'], + datasets: [{data: [0, 1, 2, 3, 4], fill: false}] + }, + options: { + scales: { + x: { + type: 'time', + reverse: true, + offset: true, + time: { + parser: 'YYYY', + unit: 'year' + }, + ticks: { + source: 'labels', + }, + }, + y: { + display: false + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.time/ticks-reverse-offset.png b/test/fixtures/scale.time/ticks-reverse-offset.png new file mode 100644 index 00000000000..91ed7304fcf Binary files /dev/null and b/test/fixtures/scale.time/ticks-reverse-offset.png differ diff --git a/test/fixtures/scale.time/ticks-reverse.js b/test/fixtures/scale.time/ticks-reverse.js new file mode 100644 index 00000000000..02df39fce1c --- /dev/null +++ b/test/fixtures/scale.time/ticks-reverse.js @@ -0,0 +1,32 @@ +module.exports = { + threshold: 0.01, + tolerance: 0.0015, + config: { + type: 'line', + data: { + labels: ['2017', '2018', '2019', '2020', '2021'], + datasets: [{data: [0, 1, 2, 3, 4], fill: false}] + }, + options: { + scales: { + x: { + type: 'time', + reverse: true, + time: { + parser: 'YYYY', + unit: 'year' + }, + ticks: { + source: 'labels', + }, + }, + y: { + display: false + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.time/ticks-reverse.png b/test/fixtures/scale.time/ticks-reverse.png new file mode 100644 index 00000000000..7fb0144390c Binary files /dev/null and b/test/fixtures/scale.time/ticks-reverse.png differ diff --git a/test/fixtures/scale.time/ticks-round.js b/test/fixtures/scale.time/ticks-round.js new file mode 100644 index 00000000000..92a09bd7ce1 --- /dev/null +++ b/test/fixtures/scale.time/ticks-round.js @@ -0,0 +1,28 @@ +module.exports = { + threshold: 0.05, + config: { + type: 'line', + data: { + labels: ['2015-01-01T20:00:00', '2015-02-02T21:00:00', '2015-02-21T01:00:00'] + }, + options: { + scales: { + x: { + type: 'time', + bounds: 'ticks', + time: { + unit: 'week', + round: 'week' + } + }, + y: { + display: false + } + } + } + }, + options: { + spriteText: true, + canvas: {width: 512, height: 256} + } +}; diff --git a/test/fixtures/scale.time/ticks-round.png b/test/fixtures/scale.time/ticks-round.png new file mode 100644 index 00000000000..4a463d56cf1 Binary files /dev/null and b/test/fixtures/scale.time/ticks-round.png differ diff --git a/test/fixtures/scale.time/ticks-stepsize.js b/test/fixtures/scale.time/ticks-stepsize.js new file mode 100644 index 00000000000..f32d24e551a --- /dev/null +++ b/test/fixtures/scale.time/ticks-stepsize.js @@ -0,0 +1,30 @@ +module.exports = { + threshold: 0.01, + config: { + type: 'line', + data: { + labels: ['2015-01-01T20:00:00', '2015-01-01T21:00:00'] + }, + options: { + scales: { + x: { + type: 'time', + bounds: 'ticks', + time: { + unit: 'hour', + }, + ticks: { + stepSize: 2 + } + }, + y: { + display: false + } + } + } + }, + options: { + spriteText: true, + canvas: {width: 512, height: 128} + } +}; diff --git a/test/fixtures/scale.time/ticks-stepsize.png b/test/fixtures/scale.time/ticks-stepsize.png new file mode 100644 index 00000000000..bb192fb0ccd Binary files /dev/null and b/test/fixtures/scale.time/ticks-stepsize.png differ diff --git a/test/fixtures/scale.time/ticks-unit.js b/test/fixtures/scale.time/ticks-unit.js new file mode 100644 index 00000000000..b18150b07fb --- /dev/null +++ b/test/fixtures/scale.time/ticks-unit.js @@ -0,0 +1,27 @@ +module.exports = { + threshold: 0.05, + tolerance: 0.002, + config: { + type: 'line', + data: { + labels: ['2015-01-01T20:00:00', '2015-01-02T21:00:00'], + }, + options: { + scales: { + x: { + type: 'time', + time: { + unit: 'hour', + } + }, + y: { + display: false + } + } + } + }, + options: { + spriteText: true, + canvas: {width: 1200, height: 200} + } +}; diff --git a/test/fixtures/scale.time/ticks-unit.png b/test/fixtures/scale.time/ticks-unit.png new file mode 100644 index 00000000000..a9867b48983 Binary files /dev/null and b/test/fixtures/scale.time/ticks-unit.png differ diff --git a/test/fixtures/scale.timeseries/data-timestamps.js b/test/fixtures/scale.timeseries/data-timestamps.js new file mode 100644 index 00000000000..b1df5c0ef30 --- /dev/null +++ b/test/fixtures/scale.timeseries/data-timestamps.js @@ -0,0 +1,46 @@ +module.exports = { + threshold: 0.01, + tolerance: 0.0015, + config: { + type: 'line', + data: { + datasets: [{data: [ + {x: 1687849697000, y: 904}, + {x: 1687817063000, y: 905}, + {x: 1687694268000, y: 913}, + {x: 1687609438000, y: 914}, + {x: 1687561387000, y: 916}, + {x: 1686875127000, y: 918}, + {x: 1686873138000, y: 920}, + {x: 1686872777000, y: 928}, + {x: 1686081641000, y: 915} + ], fill: false}, {data: [ + {x: 1687816803000, y: 1105}, + {x: 1686869490000, y: 1114}, + {x: 1686869397000, y: 1103}, + {x: 1686869225000, y: 1091}, + {x: 1686556516000, y: 1078} + ]}] + }, + options: { + scales: { + x: { + type: 'timeseries', + bounds: 'data', + time: { + unit: 'day' + }, + ticks: { + source: 'auto' + } + }, + y: { + display: false + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.timeseries/data-timestamps.png b/test/fixtures/scale.timeseries/data-timestamps.png new file mode 100644 index 00000000000..31d9a3f4717 Binary files /dev/null and b/test/fixtures/scale.timeseries/data-timestamps.png differ diff --git a/test/fixtures/scale.timeseries/financial-daily.js b/test/fixtures/scale.timeseries/financial-daily.js new file mode 100644 index 00000000000..d529be4ff44 --- /dev/null +++ b/test/fixtures/scale.timeseries/financial-daily.js @@ -0,0 +1,60 @@ +const data = [{x: 631180800000, y: 31.80}, {x: 631267200000, y: 30.20}, {x: 631353600000, y: 29.84}, {x: 631440000000, y: 29.72}, {x: 631526400000, y: 28.91}, {x: 631785600000, y: 29.55}, {x: 631872000000, y: 30.39}, {x: 631958400000, y: 29.54}, {x: 632044800000, y: 28.86}, {x: 632131200000, y: 30.75}, {x: 632390400000, y: 31.86}, {x: 632476800000, y: 33.59}, {x: 632563200000, y: 31.22}, {x: 632649600000, y: 30.12}, {x: 632736000000, y: 30.68}, {x: 632995200000, y: 31.46}, {x: 633081600000, y: 30.77}, {x: 633168000000, y: 30.27}, {x: 633254400000, y: 29.64}, {x: 633340800000, y: 30.53}, {x: 633600000000, y: 30.79}, {x: 633686400000, y: 30.27}, {x: 633772800000, y: 30.18}, {x: 633859200000, y: 27.72}, {x: 633945600000, y: 27.83}, {x: 634204800000, y: 27.82}, {x: 634291200000, y: 29.10}, {x: 634377600000, y: 28.34}, {x: 634464000000, y: 29.52}, {x: 634550400000, y: 28.69}, {x: 634809600000, y: 28.23}, {x: 634896000000, y: 27.45}, {x: 634982400000, y: 27.40}, {x: 635068800000, y: 28.39}, {x: 635155200000, y: 30.03}, {x: 635414400000, y: 31.19}, {x: 635500800000, y: 32.30}, {x: 635587200000, y: 33.84}, {x: 635673600000, y: 32.34}, {x: 635760000000, y: 31.96}, {x: 636019200000, y: 31.95}, {x: 636105600000, y: 32.84}, {x: 636192000000, y: 30.80}, {x: 636278400000, y: 31.54}, {x: 636364800000, y: 30.81}, {x: 636624000000, y: 32.99}, {x: 636710400000, y: 32.25}, {x: 636796800000, y: 33.87}, {x: 636883200000, y: 35.75}, {x: 636969600000, y: 35.71}, {x: 637228800000, y: 36.60}, {x: 637315200000, y: 35.65}, {x: 637401600000, y: 34.36}, {x: 637488000000, y: 33.61}, {x: 637574400000, y: 34.24}, {x: 637833600000, y: 32.79}, {x: 637920000000, y: 34.41}, {x: 638006400000, y: 34.11}, {x: 638092800000, y: 33.91}, {x: 638179200000, y: 33.33}, {x: 638438400000, y: 32.99}, {x: 638524800000, y: 34.17}, {x: 638611200000, y: 33.50}, {x: 638697600000, y: 35.64}, {x: 638784000000, y: 35.50}, {x: 639039600000, y: 33.11}, {x: 639126000000, y: 34.08}, {x: 639212400000, y: 35.69}, {x: 639298800000, y: 38.24}, {x: 639385200000, y: 40.86}, {x: 639644400000, y: 41.99}, {x: 639730800000, y: 44.45}, {x: 639817200000, y: 45.06}, {x: 639903600000, y: 44.32}, {x: 639990000000, y: 43.70}, {x: 640249200000, y: 44.97}, {x: 640335600000, y: 44.92}, {x: 640422000000, y: 44.11}, {x: 640508400000, y: 44.42}, {x: 640594800000, y: 43.90}, {x: 640854000000, y: 41.91}, {x: 640940400000, y: 41.60}, {x: 641026800000, y: 41.84}, {x: 641113200000, y: 42.55}, {x: 641199600000, y: 40.56}, {x: 641458800000, y: 39.99}, {x: 641545200000, y: 43.51}, {x: 641631600000, y: 43.17}, {x: 641718000000, y: 40.52}, {x: 641804400000, y: 41.06}, {x: 642063600000, y: 40.15}, {x: 642150000000, y: 43.82}, {x: 642236400000, y: 43.19}, {x: 642322800000, y: 40.99}, {x: 642409200000, y: 41.16}, {x: 642668400000, y: 41.02}, {x: 642754800000, y: 40.03}, {x: 642841200000, y: 36.46}, {x: 642927600000, y: 39.11}, {x: 643014000000, y: 41.10}, {x: 643273200000, y: 41.15}, {x: 643359600000, y: 39.01}, {x: 643446000000, y: 39.48}, {x: 643532400000, y: 41.89}, {x: 643618800000, y: 40.74}, {x: 643878000000, y: 38.88}, {x: 643964400000, y: 38.11}, {x: 644050800000, y: 40.39}, {x: 644137200000, y: 38.28}, {x: 644223600000, y: 39.96}, {x: 644482800000, y: 39.37}, {x: 644569200000, y: 39.39}, {x: 644655600000, y: 39.62}, {x: 644742000000, y: 38.99}, {x: 644828400000, y: 40.25}, {x: 645087600000, y: 42.85}, {x: 645174000000, y: 45.91}, {x: 645260400000, y: 46.66}, {x: 645346800000, y: 48.08}, {x: 645433200000, y: 51.00}, {x: 645692400000, y: 50.61}, {x: 645778800000, y: 54.55}, {x: 645865200000, y: 53.59}, {x: 645951600000, y: 53.39}, {x: 646038000000, y: 54.61}, {x: 646297200000, y: 55.02}, {x: 646383600000, y: 57.35}, {x: 646470000000, y: 56.95}, {x: 646556400000, y: 60.08}, {x: 646642800000, y: 59.80}, {x: 646902000000, y: 61.29}, {x: 646988400000, y: 63.45}, {x: 647074800000, y: 62.07}, {x: 647161200000, y: 59.01}, {x: 647247600000, y: 59.76}, {x: 647506800000, y: 60.08}, {x: 647593200000, y: 60.96}, {x: 647679600000, y: 60.56}, {x: 647766000000, y: 58.60}, {x: 647852400000, y: 57.40}, {x: 648111600000, y: 59.86}, {x: 648198000000, y: 58.76}, {x: 648284400000, y: 57.54}, {x: 648370800000, y: 57.78}, {x: 648457200000, y: 54.33}, {x: 648716400000, y: 54.57}, {x: 648802800000, y: 53.69}, {x: 648889200000, y: 57.02}, {x: 648975600000, y: 52.30}, {x: 649062000000, y: 49.79}, {x: 649321200000, y: 47.40}, {x: 649407600000, y: 45.44}, {x: 649494000000, y: 46.75}, {x: 649580400000, y: 44.19}, {x: 649666800000, y: 43.05}, {x: 649926000000, y: 43.99}, {x: 650012400000, y: 45.99}, {x: 650098800000, y: 42.15}, {x: 650185200000, y: 41.84}, {x: 650271600000, y: 43.30}, {x: 650530800000, y: 41.57}, {x: 650617200000, y: 42.13}, {x: 650703600000, y: 43.29}, {x: 650790000000, y: 43.98}, {x: 650876400000, y: 44.51}, {x: 651135600000, y: 45.50}, {x: 651222000000, y: 43.63}, {x: 651308400000, y: 41.93}, {x: 651394800000, y: 38.41}, {x: 651481200000, y: 41.01}, {x: 651740400000, y: 38.17}, {x: 651826800000, y: 38.32}, {x: 651913200000, y: 38.27}, {x: 651999600000, y: 36.10}, {x: 652086000000, y: 34.62}, {x: 652345200000, y: 33.91}, {x: 652431600000, y: 34.25}, {x: 652518000000, y: 33.97}, {x: 652604400000, y: 35.11}, {x: 652690800000, y: 35.05}, {x: 652950000000, y: 36.37}, {x: 653036400000, y: 35.54}, {x: 653122800000, y: 35.80}, {x: 653209200000, y: 36.75}, {x: 653295600000, y: 35.48}, {x: 653554800000, y: 36.78}, {x: 653641200000, y: 34.35}, {x: 653727600000, y: 32.62}, {x: 653814000000, y: 32.66}, {x: 653900400000, y: 31.45}, {x: 654159600000, y: 29.29}, {x: 654246000000, y: 31.18}, {x: 654332400000, y: 29.47}, {x: 654418800000, y: 28.40}, {x: 654505200000, y: 28.21}, {x: 654764400000, y: 27.73}, {x: 654850800000, y: 27.08}, {x: 654937200000, y: 25.32}, {x: 655023600000, y: 25.69}, {x: 655110000000, y: 27.28}, {x: 655369200000, y: 28.53}, {x: 655455600000, y: 27.88}, {x: 655542000000, y: 28.17}, {x: 655628400000, y: 26.22}, {x: 655714800000, y: 26.07}, {x: 655974000000, y: 28.42}, {x: 656060400000, y: 28.27}, {x: 656146800000, y: 29.76}, {x: 656233200000, y: 29.58}, {x: 656319600000, y: 29.41}, {x: 656578800000, y: 29.34}, {x: 656665200000, y: 29.45}, {x: 656751600000, y: 27.93}, {x: 656838000000, y: 27.68}, {x: 656924400000, y: 27.42}, {x: 657187200000, y: 25.79}, {x: 657273600000, y: 25.84}, {x: 657360000000, y: 26.00}, {x: 657446400000, y: 26.57}, {x: 657532800000, y: 26.66}, {x: 657792000000, y: 26.40}, {x: 657878400000, y: 28.06}, {x: 657964800000, y: 27.58}, {x: 658051200000, y: 27.18}, {x: 658137600000, y: 27.71}, {x: 658396800000, y: 26.37}, {x: 658483200000, y: 26.53}, {x: 658569600000, y: 26.19}, {x: 658656000000, y: 25.29}, {x: 658742400000, y: 27.33}, {x: 659001600000, y: 26.08}, {x: 659088000000, y: 26.26}, {x: 659174400000, y: 26.35}, {x: 659260800000, y: 24.88}, {x: 659347200000, y: 23.71}, {x: 659606400000, y: 25.77}, {x: 659692800000, y: 26.03}, {x: 659779200000, y: 27.38}, {x: 659865600000, y: 27.82}, {x: 659952000000, y: 27.61}, {x: 660211200000, y: 26.15}, {x: 660297600000, y: 26.79}, {x: 660384000000, y: 26.78}, {x: 660470400000, y: 28.69}, {x: 660556800000, y: 29.38}, {x: 660816000000, y: 30.16}, {x: 660902400000, y: 29.42}, {x: 660988800000, y: 29.06}, {x: 661075200000, y: 28.05}, {x: 661161600000, y: 29.48}, {x: 661420800000, y: 28.48}, {x: 661507200000, y: 28.67}, {x: 661593600000, y: 28.27}, {x: 661680000000, y: 27.29}, {x: 661766400000, y: 26.88}, {x: 662025600000, y: 27.12}, {x: 662112000000, y: 27.02}, {x: 662198400000, y: 27.08}, {x: 662284800000, y: 24.53}, {x: 662371200000, y: 25.19}, {x: 662630400000, y: 26.70}, {x: 662716800000, y: 27.23}, {x: 662803200000, y: 26.26}, {x: 662889600000, y: 26.46}, {x: 662976000000, y: 25.38}, {x: 663235200000, y: 25.23}, {x: 663321600000, y: 25.53}, {x: 663408000000, y: 25.71}, {x: 663494400000, y: 25.39}, {x: 663580800000, y: 24.35}, {x: 663840000000, y: 23.64}, {x: 663926400000, y: 22.98}, {x: 664012800000, y: 22.75}, {x: 664099200000, y: 22.70}, {x: 664185600000, y: 21.56}, {x: 664444800000, y: 22.65}, {x: 664531200000, y: 21.54}, {x: 664617600000, y: 20.68}, {x: 664704000000, y: 21.37}, {x: 664790400000, y: 22.44}, {x: 665049600000, y: 23.89}, {x: 665136000000, y: 25.02}, {x: 665222400000, y: 26.84}, {x: 665308800000, y: 26.11}, {x: 665395200000, y: 25.91}, {x: 665654400000, y: 27.21}, {x: 665740800000, y: 26.37}, {x: 665827200000, y: 26.81}, {x: 665913600000, y: 26.42}, {x: 666000000000, y: 26.73}, {x: 666259200000, y: 27.25}, {x: 666345600000, y: 25.01}, {x: 666432000000, y: 24.55}, {x: 666518400000, y: 25.34}, {x: 666604800000, y: 25.37}, {x: 666864000000, y: 27.51}, {x: 666950400000, y: 27.51}, {x: 667036800000, y: 28.65}, {x: 667123200000, y: 28.90}, {x: 667209600000, y: 29.22}, {x: 667468800000, y: 29.77}, {x: 667555200000, y: 29.21}, {x: 667641600000, y: 29.81}, {x: 667728000000, y: 27.75}, {x: 667814400000, y: 28.56}, {x: 668073600000, y: 28.06}, {x: 668160000000, y: 26.70}, {x: 668246400000, y: 26.39}, {x: 668332800000, y: 26.42}, {x: 668419200000, y: 29.05}, {x: 668678400000, y: 27.84}, {x: 668764800000, y: 27.67}, {x: 668851200000, y: 26.75}, {x: 668937600000, y: 26.20}, {x: 669024000000, y: 27.33}, {x: 669283200000, y: 27.55}, {x: 669369600000, y: 26.79}, {x: 669456000000, y: 25.29}, {x: 669542400000, y: 25.17}, {x: 669628800000, y: 25.55}, {x: 669888000000, y: 23.87}, {x: 669974400000, y: 22.92}, {x: 670060800000, y: 23.80}, {x: 670147200000, y: 24.18}, {x: 670233600000, y: 22.56}, {x: 670492800000, y: 21.93}, {x: 670579200000, y: 20.96}, {x: 670665600000, y: 21.94}, {x: 670752000000, y: 21.48}, {x: 670838400000, y: 22.17}, {x: 671094000000, y: 22.68}, {x: 671180400000, y: 20.56}, {x: 671266800000, y: 18.98}, {x: 671353200000, y: 19.93}, {x: 671439600000, y: 19.53}, {x: 671698800000, y: 18.93}, {x: 671785200000, y: 19.41}, {x: 671871600000, y: 18.61}, {x: 671958000000, y: 18.88}, {x: 672044400000, y: 18.70}, {x: 672303600000, y: 18.80}, {x: 672390000000, y: 17.72}, {x: 672476400000, y: 17.65}, {x: 672562800000, y: 17.99}, {x: 672649200000, y: 17.01}, {x: 672908400000, y: 17.05}, {x: 672994800000, y: 16.39}, {x: 673081200000, y: 15.96}, {x: 673167600000, y: 15.82}, {x: 673254000000, y: 16.26}, {x: 673513200000, y: 16.33}, {x: 673599600000, y: 15.73}, {x: 673686000000, y: 15.02}, {x: 673772400000, y: 14.51}, {x: 673858800000, y: 14.71}, {x: 674118000000, y: 15.29}, {x: 674204400000, y: 15.46}, {x: 674290800000, y: 15.30}, {x: 674377200000, y: 14.14}, {x: 674463600000, y: 13.94}, {x: 674722800000, y: 13.01}, {x: 674809200000, y: 13.59}, {x: 674895600000, y: 13.67}, {x: 674982000000, y: 13.28}, {x: 675068400000, y: 13.11}, {x: 675327600000, y: 13.52}, {x: 675414000000, y: 14.02}, {x: 675500400000, y: 14.53}, {x: 675586800000, y: 14.61}, {x: 675673200000, y: 14.53}, {x: 675932400000, y: 14.29}, {x: 676018800000, y: 14.46}, {x: 676105200000, y: 14.07}, {x: 676191600000, y: 13.91}, {x: 676278000000, y: 14.08}, {x: 676537200000, y: 13.63}, {x: 676623600000, y: 14.38}, {x: 676710000000, y: 14.86}, {x: 676796400000, y: 14.82}, {x: 676882800000, y: 14.04}, {x: 677142000000, y: 14.63}, {x: 677228400000, y: 14.83}, {x: 677314800000, y: 15.33}, {x: 677401200000, y: 14.67}, {x: 677487600000, y: 14.18}, {x: 677746800000, y: 14.40}, {x: 677833200000, y: 14.45}, {x: 677919600000, y: 14.78}, {x: 678006000000, y: 14.93}, {x: 678092400000, y: 14.09}, {x: 678351600000, y: 13.56}, {x: 678438000000, y: 14.26}, {x: 678524400000, y: 14.36}, {x: 678610800000, y: 14.82}, {x: 678697200000, y: 15.96}, {x: 678956400000, y: 15.83}, {x: 679042800000, y: 15.92}, {x: 679129200000, y: 15.29}, {x: 679215600000, y: 16.29}, {x: 679302000000, y: 15.31}, {x: 679561200000, y: 15.13}, {x: 679647600000, y: 15.59}, {x: 679734000000, y: 14.97}, {x: 679820400000, y: 15.81}, {x: 679906800000, y: 15.59}, {x: 680166000000, y: 14.83}, {x: 680252400000, y: 14.57}, {x: 680338800000, y: 14.24}, {x: 680425200000, y: 14.49}, {x: 680511600000, y: 13.80}, {x: 680770800000, y: 14.17}, {x: 680857200000, y: 14.40}, {x: 680943600000, y: 14.31}, {x: 681030000000, y: 13.89}, {x: 681116400000, y: 13.59}, {x: 681375600000, y: 13.36}, {x: 681462000000, y: 13.33}, {x: 681548400000, y: 13.26}, {x: 681634800000, y: 13.71}, {x: 681721200000, y: 13.67}, {x: 681980400000, y: 12.87}, {x: 682066800000, y: 14.03}, {x: 682153200000, y: 13.95}, {x: 682239600000, y: 13.11}, {x: 682326000000, y: 14.05}, {x: 682585200000, y: 14.47}, {x: 682671600000, y: 14.45}, {x: 682758000000, y: 15.14}, {x: 682844400000, y: 15.65}, {x: 682930800000, y: 15.15}, {x: 683190000000, y: 15.22}, {x: 683276400000, y: 15.38}, {x: 683362800000, y: 16.42}, {x: 683449200000, y: 16.26}, {x: 683535600000, y: 16.51}, {x: 683794800000, y: 15.66}, {x: 683881200000, y: 15.88}, {x: 683967600000, y: 16.36}, {x: 684054000000, y: 15.87}, {x: 684140400000, y: 15.61}, {x: 684399600000, y: 16.63}, {x: 684486000000, y: 15.88}, {x: 684572400000, y: 17.21}, {x: 684658800000, y: 18.46}, {x: 684745200000, y: 18.76}, {x: 685004400000, y: 18.39}, {x: 685090800000, y: 18.14}, {x: 685177200000, y: 17.31}, {x: 685263600000, y: 17.21}, {x: 685350000000, y: 17.17}, {x: 685609200000, y: 17.21}, {x: 685695600000, y: 16.86}, {x: 685782000000, y: 17.17}, {x: 685868400000, y: 16.20}, {x: 685954800000, y: 15.14}, {x: 686214000000, y: 15.05}, {x: 686300400000, y: 16.09}, {x: 686386800000, y: 16.40}, {x: 686473200000, y: 15.83}, {x: 686559600000, y: 16.53}, {x: 686818800000, y: 16.32}, {x: 686905200000, y: 16.47}, {x: 686991600000, y: 16.59}, {x: 687078000000, y: 16.51}, {x: 687164400000, y: 17.41}, {x: 687423600000, y: 18.17}, {x: 687510000000, y: 17.63}, {x: 687596400000, y: 17.62}, {x: 687682800000, y: 17.69}, {x: 687769200000, y: 17.54}, {x: 688028400000, y: 16.56}, {x: 688114800000, y: 16.83}, {x: 688201200000, y: 15.98}, {x: 688287600000, y: 16.52}, {x: 688374000000, y: 17.08}, {x: 688636800000, y: 17.27}, {x: 688723200000, y: 18.18}, {x: 688809600000, y: 18.67}, {x: 688896000000, y: 18.97}, {x: 688982400000, y: 20.31}, {x: 689241600000, y: 21.30}, {x: 689328000000, y: 20.96}, {x: 689414400000, y: 20.01}, {x: 689500800000, y: 21.13}, {x: 689587200000, y: 21.52}, {x: 689846400000, y: 22.08}, {x: 689932800000, y: 21.88}, {x: 690019200000, y: 21.18}, {x: 690105600000, y: 22.79}, {x: 690192000000, y: 22.51}, {x: 690451200000, y: 23.66}, {x: 690537600000, y: 23.43}, {x: 690624000000, y: 24.08}, {x: 690710400000, y: 24.83}, {x: 690796800000, y: 23.49}, {x: 691056000000, y: 23.43}, {x: 691142400000, y: 23.98}, {x: 691228800000, y: 24.52}, {x: 691315200000, y: 23.32}, {x: 691401600000, y: 23.63}, {x: 691660800000, y: 21.74}, {x: 691747200000, y: 20.03}, {x: 691833600000, y: 20.37}, {x: 691920000000, y: 21.09}, {x: 692006400000, y: 21.33}, {x: 692265600000, y: 20.48}, {x: 692352000000, y: 20.15}, {x: 692438400000, y: 20.33}, {x: 692524800000, y: 19.53}, {x: 692611200000, y: 19.34}, {x: 692870400000, y: 18.63}, {x: 692956800000, y: 18.42}, {x: 693043200000, y: 19.49}, {x: 693129600000, y: 18.75}, {x: 693216000000, y: 18.11}, {x: 693475200000, y: 17.40}, {x: 693561600000, y: 17.40}, {x: 693648000000, y: 17.73}, {x: 693734400000, y: 18.36}, {x: 693820800000, y: 18.14}, {x: 694080000000, y: 18.71}, {x: 694166400000, y: 17.97}, {x: 694252800000, y: 18.90}, {x: 694339200000, y: 18.31}, {x: 694425600000, y: 18.67}, {x: 694684800000, y: 18.78}, {x: 694771200000, y: 19.53}, {x: 694857600000, y: 19.41}, {x: 694944000000, y: 19.42}, {x: 695030400000, y: 20.29}, {x: 695289600000, y: 21.08}, {x: 695376000000, y: 20.69}, {x: 695462400000, y: 21.37}, {x: 695548800000, y: 20.67}, {x: 695635200000, y: 20.79}, {x: 695894400000, y: 20.39}, {x: 695980800000, y: 19.98}, {x: 696067200000, y: 19.35}, {x: 696153600000, y: 18.60}, {x: 696240000000, y: 18.67}, {x: 696499200000, y: 19.41}, {x: 696585600000, y: 20.62}, {x: 696672000000, y: 21.09}, {x: 696758400000, y: 21.43}, {x: 696844800000, y: 20.31}, {x: 697104000000, y: 19.40}, {x: 697190400000, y: 19.82}, {x: 697276800000, y: 19.55}, {x: 697363200000, y: 19.77}, {x: 697449600000, y: 19.33}, {x: 697708800000, y: 18.75}, {x: 697795200000, y: 18.50}, {x: 697881600000, y: 18.39}, {x: 697968000000, y: 19.19}, {x: 698054400000, y: 19.93}, {x: 698313600000, y: 20.15}, {x: 698400000000, y: 22.09}, {x: 698486400000, y: 20.29}, {x: 698572800000, y: 20.37}, {x: 698659200000, y: 19.06}, {x: 698918400000, y: 20.51}, {x: 699004800000, y: 20.06}, {x: 699091200000, y: 19.54}, {x: 699177600000, y: 17.89}, {x: 699264000000, y: 17.57}, {x: 699523200000, y: 16.88}, {x: 699609600000, y: 17.26}, {x: 699696000000, y: 17.15}, {x: 699782400000, y: 15.73}, {x: 699868800000, y: 15.08}, {x: 700128000000, y: 14.73}, {x: 700214400000, y: 14.58}, {x: 700300800000, y: 14.33}, {x: 700387200000, y: 14.76}, {x: 700473600000, y: 15.44}, {x: 700732800000, y: 16.63}, {x: 700819200000, y: 15.63}, {x: 700905600000, y: 15.61}, {x: 700992000000, y: 16.88}, {x: 701078400000, y: 16.26}, {x: 701337600000, y: 15.95}, {x: 701424000000, y: 15.41}, {x: 701510400000, y: 16.14}, {x: 701596800000, y: 15.77}, {x: 701683200000, y: 15.84}, {x: 701942400000, y: 14.41}, {x: 702028800000, y: 15.62}, {x: 702115200000, y: 15.62}, {x: 702201600000, y: 15.85}, {x: 702288000000, y: 17.18}, {x: 702543600000, y: 17.58}, {x: 702630000000, y: 19.25}, {x: 702716400000, y: 19.77}, {x: 702802800000, y: 20.66}, {x: 702889200000, y: 19.70}, {x: 703148400000, y: 20.01}, {x: 703234800000, y: 19.93}, {x: 703321200000, y: 19.94}, {x: 703407600000, y: 19.77}, {x: 703494000000, y: 19.83}]; + +module.exports = { + threshold: 0.01, + tolerance: 0.0015, + config: { + data: { + datasets: [{ + data, + type: 'line', + pointRadius: 0, + fill: false, + tension: 0, + borderWidth: 2 + }] + }, + options: { + animation: { + duration: 0 + }, + scales: { + x: { + type: 'timeseries', + offset: true, + ticks: { + major: { + enabled: true, + }, + font: function(context) { + return context.tick && context.tick.major ? {weight: 'bold'} : undefined; + }, + source: 'data', + autoSkip: true, + autoSkipPadding: 75, + maxRotation: 0, + sampleSize: 100, + maxTicksLimit: 3 + }, + // manually set major ticks so that test passes in all time zones with moment adapter + afterBuildTicks: function(scale) { + const ticks = scale.ticks; + const major = [0, 264, 522]; + for (let i = 0; i < ticks.length; i++) { + ticks[i].major = major.indexOf(i) >= 0; + } + } + }, + y: { + type: 'linear', + border: { + display: false + } + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.timeseries/financial-daily.png b/test/fixtures/scale.timeseries/financial-daily.png new file mode 100644 index 00000000000..1537512dd84 Binary files /dev/null and b/test/fixtures/scale.timeseries/financial-daily.png differ diff --git a/test/fixtures/scale.timeseries/normalize.js b/test/fixtures/scale.timeseries/normalize.js new file mode 100644 index 00000000000..af1d8c0422d --- /dev/null +++ b/test/fixtures/scale.timeseries/normalize.js @@ -0,0 +1,39 @@ +module.exports = { + threshold: 0.01, + tolerance: 0.002, + config: { + type: 'line', + data: { + datasets: [{ + data: [ + {x: '2017', y: null}, + {x: '2018', y: 1}, + {x: '2019', y: 2}, + {x: '2020', y: 3}, + {x: '2021', y: 4} + ], + fill: false + }] + }, + options: { + normalized: true, + scales: { + x: { + type: 'timeseries', + time: { + parser: 'YYYY' + }, + ticks: { + source: 'data' + } + }, + y: { + display: false + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.timeseries/normalize.png b/test/fixtures/scale.timeseries/normalize.png new file mode 100644 index 00000000000..ea7fb8ce289 Binary files /dev/null and b/test/fixtures/scale.timeseries/normalize.png differ diff --git a/test/fixtures/scale.timeseries/source-auto.js b/test/fixtures/scale.timeseries/source-auto.js new file mode 100644 index 00000000000..81494377536 --- /dev/null +++ b/test/fixtures/scale.timeseries/source-auto.js @@ -0,0 +1,31 @@ +module.exports = { + threshold: 0.01, + tolerance: 0.0025, + config: { + type: 'line', + data: { + labels: ['2017', '2018', '2019', '2020', '2025'], + datasets: [{data: [0, 1, 2, 3, 4], fill: false}] + }, + options: { + scales: { + x: { + type: 'timeseries', + time: { + parser: 'YYYY', + unit: 'year' + }, + ticks: { + source: 'auto' + } + }, + y: { + display: false + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.timeseries/source-auto.png b/test/fixtures/scale.timeseries/source-auto.png new file mode 100644 index 00000000000..aa6255b6c83 Binary files /dev/null and b/test/fixtures/scale.timeseries/source-auto.png differ diff --git a/test/fixtures/scale.timeseries/source-data-offset-min-max.js b/test/fixtures/scale.timeseries/source-data-offset-min-max.js new file mode 100644 index 00000000000..84e9a265253 --- /dev/null +++ b/test/fixtures/scale.timeseries/source-data-offset-min-max.js @@ -0,0 +1,33 @@ +module.exports = { + threshold: 0.01, + tolerance: 0.002, + config: { + type: 'line', + data: { + labels: ['2017', '2019', '2020', '2025', '2042'], + datasets: [{data: [0, 1, 2, 3, 4], fill: false}] + }, + options: { + scales: { + x: { + type: 'timeseries', + min: '2012', + max: '2051', + offset: true, + time: { + parser: 'YYYY', + }, + ticks: { + source: 'data' + } + }, + y: { + display: false + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.timeseries/source-data-offset-min-max.png b/test/fixtures/scale.timeseries/source-data-offset-min-max.png new file mode 100644 index 00000000000..fe7165cfef2 Binary files /dev/null and b/test/fixtures/scale.timeseries/source-data-offset-min-max.png differ diff --git a/test/fixtures/scale.timeseries/source-data.js b/test/fixtures/scale.timeseries/source-data.js new file mode 100644 index 00000000000..ae3a3e1ccd9 --- /dev/null +++ b/test/fixtures/scale.timeseries/source-data.js @@ -0,0 +1,31 @@ +module.exports = { + threshold: 0.01, + tolerance: 0.0015, + config: { + type: 'line', + data: { + labels: ['2017', '2018', '2019', '2020', '2025'], + datasets: [{data: [0, 1, 2, 3, 4], fill: false}] + }, + options: { + scales: { + x: { + type: 'timeseries', + time: { + parser: 'YYYY', + unit: 'year' + }, + ticks: { + source: 'data' + } + }, + y: { + display: false + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.timeseries/source-data.png b/test/fixtures/scale.timeseries/source-data.png new file mode 100644 index 00000000000..b352b93f276 Binary files /dev/null and b/test/fixtures/scale.timeseries/source-data.png differ diff --git a/test/fixtures/scale.timeseries/source-labels-offset-min-max.js b/test/fixtures/scale.timeseries/source-labels-offset-min-max.js new file mode 100644 index 00000000000..897a2fd7550 --- /dev/null +++ b/test/fixtures/scale.timeseries/source-labels-offset-min-max.js @@ -0,0 +1,33 @@ +module.exports = { + threshold: 0.01, + tolerance: 0.0015, + config: { + type: 'line', + data: { + labels: ['2017', '2019', '2020', '2025', '2042'], + datasets: [{data: [0, 1, 2, 3, 4], fill: false}] + }, + options: { + scales: { + x: { + type: 'timeseries', + min: '2012', + max: '2051', + offset: true, + time: { + parser: 'YYYY', + }, + ticks: { + source: 'labels' + } + }, + y: { + display: false + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.timeseries/source-labels-offset-min-max.png b/test/fixtures/scale.timeseries/source-labels-offset-min-max.png new file mode 100644 index 00000000000..824bf36bd51 Binary files /dev/null and b/test/fixtures/scale.timeseries/source-labels-offset-min-max.png differ diff --git a/test/fixtures/scale.timeseries/source-labels.js b/test/fixtures/scale.timeseries/source-labels.js new file mode 100644 index 00000000000..8b02c83d18b --- /dev/null +++ b/test/fixtures/scale.timeseries/source-labels.js @@ -0,0 +1,31 @@ +module.exports = { + threshold: 0.01, + tolerance: 0.0015, + config: { + type: 'line', + data: { + labels: ['2017', '2018', '2019', '2020', '2025'], + datasets: [{data: [0, 1, 2, 3, 4], fill: false}] + }, + options: { + scales: { + x: { + type: 'timeseries', + time: { + parser: 'YYYY', + unit: 'year' + }, + ticks: { + source: 'labels' + } + }, + y: { + display: false + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.timeseries/source-labels.png b/test/fixtures/scale.timeseries/source-labels.png new file mode 100644 index 00000000000..b352b93f276 Binary files /dev/null and b/test/fixtures/scale.timeseries/source-labels.png differ diff --git a/test/fixtures/scale.timeseries/ticks-reverse-max.js b/test/fixtures/scale.timeseries/ticks-reverse-max.js new file mode 100644 index 00000000000..727770969e4 --- /dev/null +++ b/test/fixtures/scale.timeseries/ticks-reverse-max.js @@ -0,0 +1,31 @@ +module.exports = { + threshold: 0.01, + config: { + type: 'line', + data: { + labels: ['2017', '2019', '2020', '2025', '2042'], + datasets: [{data: [0, 1, 2, 3, 4], fill: false}] + }, + options: { + scales: { + x: { + type: 'timeseries', + max: '2050', + time: { + parser: 'YYYY' + }, + reverse: true, + ticks: { + source: 'labels' + } + }, + y: { + display: false + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.timeseries/ticks-reverse-max.png b/test/fixtures/scale.timeseries/ticks-reverse-max.png new file mode 100644 index 00000000000..9887ed88db3 Binary files /dev/null and b/test/fixtures/scale.timeseries/ticks-reverse-max.png differ diff --git a/test/fixtures/scale.timeseries/ticks-reverse-min-max.js b/test/fixtures/scale.timeseries/ticks-reverse-min-max.js new file mode 100644 index 00000000000..4222a2e8d88 --- /dev/null +++ b/test/fixtures/scale.timeseries/ticks-reverse-min-max.js @@ -0,0 +1,33 @@ +module.exports = { + threshold: 0.01, + tolerance: 0.002, + config: { + type: 'line', + data: { + labels: ['2017', '2019', '2020', '2025', '2042'], + datasets: [{data: [0, 1, 2, 3, 4], fill: false}] + }, + options: { + scales: { + x: { + type: 'timeseries', + min: '2012', + max: '2050', + time: { + parser: 'YYYY' + }, + reverse: true, + ticks: { + source: 'labels' + } + }, + y: { + display: false + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.timeseries/ticks-reverse-min-max.png b/test/fixtures/scale.timeseries/ticks-reverse-min-max.png new file mode 100644 index 00000000000..dc7e79882f8 Binary files /dev/null and b/test/fixtures/scale.timeseries/ticks-reverse-min-max.png differ diff --git a/test/fixtures/scale.timeseries/ticks-reverse-min.js b/test/fixtures/scale.timeseries/ticks-reverse-min.js new file mode 100644 index 00000000000..39f7fe05f82 --- /dev/null +++ b/test/fixtures/scale.timeseries/ticks-reverse-min.js @@ -0,0 +1,31 @@ +module.exports = { + threshold: 0.01, + config: { + type: 'line', + data: { + labels: ['2017', '2019', '2020', '2025', '2042'], + datasets: [{data: [0, 1, 2, 3, 4], fill: false}] + }, + options: { + scales: { + x: { + type: 'timeseries', + min: '2012', + time: { + parser: 'YYYY' + }, + reverse: true, + ticks: { + source: 'labels' + } + }, + y: { + display: false + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.timeseries/ticks-reverse-min.png b/test/fixtures/scale.timeseries/ticks-reverse-min.png new file mode 100644 index 00000000000..5a30d3f0260 Binary files /dev/null and b/test/fixtures/scale.timeseries/ticks-reverse-min.png differ diff --git a/test/fixtures/scale.timeseries/ticks-reverse.js b/test/fixtures/scale.timeseries/ticks-reverse.js new file mode 100644 index 00000000000..f71c6d8e89b --- /dev/null +++ b/test/fixtures/scale.timeseries/ticks-reverse.js @@ -0,0 +1,31 @@ +module.exports = { + threshold: 0.01, + tolerance: 0.0015, + config: { + type: 'line', + data: { + labels: ['2017', '2019', '2020', '2025', '2042'], + datasets: [{data: [0, 1, 2, 3, 4], fill: false}] + }, + options: { + scales: { + x: { + type: 'timeseries', + time: { + parser: 'YYYY' + }, + reverse: true, + ticks: { + source: 'labels' + } + }, + y: { + display: false + } + } + } + }, + options: { + spriteText: true + } +}; diff --git a/test/fixtures/scale.timeseries/ticks-reverse.png b/test/fixtures/scale.timeseries/ticks-reverse.png new file mode 100644 index 00000000000..d7d13f4ee96 Binary files /dev/null and b/test/fixtures/scale.timeseries/ticks-reverse.png differ diff --git a/test/index.js b/test/index.js new file mode 100644 index 00000000000..f250fb6a889 --- /dev/null +++ b/test/index.js @@ -0,0 +1,36 @@ +import {acquireChart, releaseChart, createMockContext, afterEvent, waitForResize, injectWrapperCSS, specsFromFixtures, triggerMouseEvent, addMatchers, releaseCharts} from 'chartjs-test-utils'; + +// force ratio=1 for tests on high-res/retina devices +// fixes https://github.com/chartjs/Chart.js/issues/4515 +window.devicePixelRatio = 1; + +window.acquireChart = acquireChart; +window.afterEvent = afterEvent; +window.releaseChart = releaseChart; +window.waitForResize = waitForResize; +window.createMockContext = createMockContext; + +injectWrapperCSS(); + +jasmine.fixture = { + specs: specsFromFixtures +}; + +jasmine.triggerMouseEvent = triggerMouseEvent; + +// Set a fixed time zone (and, in particular, disable Daylight Saving Time) for +// more stable test results. +window.moment.tz.setDefault('Etc/UTC'); + +beforeAll(() => { + // Disable colors plugin for tests. + window.Chart.defaults.plugins.colors.enabled = false; +}); + +beforeEach(() => { + addMatchers(); +}); + +afterEach(() => { + releaseCharts(); +}); diff --git a/test/integration/node-commonjs/package.json b/test/integration/node-commonjs/package.json new file mode 100644 index 00000000000..b2a0e280434 --- /dev/null +++ b/test/integration/node-commonjs/package.json @@ -0,0 +1,12 @@ +{ + "private": true, + "description": "chart.js should work in Node", + "scripts": { + "test": "npm run test-index && npm run test-auto", + "test-index": "node test.js", + "test-auto": "node test-auto.js" + }, + "dependencies": { + "chart.js": "workspace:*" + } +} diff --git a/test/integration/node-commonjs/test-auto.js b/test/integration/node-commonjs/test-auto.js new file mode 100644 index 00000000000..a0a58ff619e --- /dev/null +++ b/test/integration/node-commonjs/test-auto.js @@ -0,0 +1,7 @@ +const Chart = require('chart.js/auto'); +const {valueOrDefault} = require('chart.js/helpers'); + +Chart.register({ + id: 'TEST_PLUGIN', + dummyValue: valueOrDefault(0, 1) +}); diff --git a/test/integration/node-commonjs/test.js b/test/integration/node-commonjs/test.js new file mode 100644 index 00000000000..1d43d2d239b --- /dev/null +++ b/test/integration/node-commonjs/test.js @@ -0,0 +1,7 @@ +const {Chart} = require('chart.js'); +const {valueOrDefault} = require('chart.js/helpers'); + +Chart.register({ + id: 'TEST_PLUGIN', + dummyValue: valueOrDefault(0, 1) +}); diff --git a/test/integration/node/package.json b/test/integration/node/package.json new file mode 100644 index 00000000000..4730ebe05b8 --- /dev/null +++ b/test/integration/node/package.json @@ -0,0 +1,13 @@ +{ + "private": true, + "description": "chart.js should work in Node", + "type": "module", + "scripts": { + "test": "npm run test-mjs && npm run test-cjs", + "test-mjs": "node test.js", + "test-cjs": "node test.cjs" + }, + "dependencies": { + "chart.js": "workspace:*" + } +} diff --git a/test/integration/node/test.cjs b/test/integration/node/test.cjs new file mode 100644 index 00000000000..1d43d2d239b --- /dev/null +++ b/test/integration/node/test.cjs @@ -0,0 +1,7 @@ +const {Chart} = require('chart.js'); +const {valueOrDefault} = require('chart.js/helpers'); + +Chart.register({ + id: 'TEST_PLUGIN', + dummyValue: valueOrDefault(0, 1) +}); diff --git a/test/integration/node/test.js b/test/integration/node/test.js new file mode 100644 index 00000000000..25a5b817956 --- /dev/null +++ b/test/integration/node/test.js @@ -0,0 +1,7 @@ +import {Chart} from 'chart.js'; +import {valueOrDefault} from 'chart.js/helpers'; + +Chart.register({ + id: 'TEST_PLUGIN', + dummyValue: valueOrDefault(0, 1) +}); diff --git a/test/integration/react-browser/package.json b/test/integration/react-browser/package.json new file mode 100644 index 00000000000..fd0d0da9f08 --- /dev/null +++ b/test/integration/react-browser/package.json @@ -0,0 +1,33 @@ +{ + "private": true, + "description": "chart.js should work in react-browser (Web)", + "dependencies": { + "@babel/core": "^7.0.0", + "@babel/plugin-syntax-flow": "^7.14.5", + "@babel/plugin-transform-react-jsx": "^7.14.9", + "@types/node": "^18.7.6", + "@types/react": "^18.0.17", + "@types/react-dom": "^18.0.6", + "chart.js": "workspace:*", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-scripts": "5.0.1", + "typescript": "^4.7.4", + "web-vitals": "^2.1.4" + }, + "scripts": { + "test": "react-scripts build" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/test/integration/react-browser/public/index.html b/test/integration/react-browser/public/index.html new file mode 100644 index 00000000000..3051d247d8e --- /dev/null +++ b/test/integration/react-browser/public/index.html @@ -0,0 +1,36 @@ + + + + + + + + + Chartjs test React App + + + +

    + + + diff --git a/test/integration/react-browser/src/App.tsx b/test/integration/react-browser/src/App.tsx new file mode 100644 index 00000000000..e5861e17511 --- /dev/null +++ b/test/integration/react-browser/src/App.tsx @@ -0,0 +1,35 @@ +import React, {useEffect} from 'react'; +import {Chart, DoughnutController, ArcElement} from 'chart.js'; +import {merge} from 'chart.js/helpers'; + +Chart.register(DoughnutController, ArcElement); + +function App() { + useEffect(() => { + const c = Chart.getChart('myChart'); + if (c) { + c.destroy(); + } + + merge({a: 1}, {b: 2}); + + // eslint-disable-next-line no-new + new Chart('myChart', { + type: 'doughnut', + data: { + labels: ['Chart', 'JS'], + datasets: [{ + data: [2, 3] + }] + } + }); + }, []); + + return ( +
    + +
    + ); +} + +export default App; diff --git a/test/integration/react-browser/src/AppAuto.tsx b/test/integration/react-browser/src/AppAuto.tsx new file mode 100644 index 00000000000..75d949311db --- /dev/null +++ b/test/integration/react-browser/src/AppAuto.tsx @@ -0,0 +1,33 @@ +import React, {useEffect} from 'react'; +import Chart from 'chart.js/auto'; +import {merge} from 'chart.js/helpers'; + +function AppAuto() { + useEffect(() => { + const c = Chart.getChart('myChart'); + if (c) { + c.destroy(); + } + + merge({a: 1}, {b: 2}); + + // eslint-disable-next-line no-new + new Chart('myChart', { + type: 'doughnut', + data: { + labels: ['Chart', 'JS'], + datasets: [{ + data: [2, 3] + }] + } + }); + }, []); + + return ( +
    + +
    + ); +} + +export default AppAuto; diff --git a/test/integration/react-browser/src/index.tsx b/test/integration/react-browser/src/index.tsx new file mode 100644 index 00000000000..7657c03b6e3 --- /dev/null +++ b/test/integration/react-browser/src/index.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import {render} from 'react-dom'; +import App from './App'; +import AppAuto from './AppAuto'; + +render( + + + + , + document.getElementById('root') +); diff --git a/test/integration/react-browser/tsconfig.json b/test/integration/react-browser/tsconfig.json new file mode 100644 index 00000000000..584ba40bd6d --- /dev/null +++ b/test/integration/react-browser/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "jsx": "react", + "target": "ES6", + "moduleResolution": "Node", + "allowSyntheticDefaultImports": true, + "alwaysStrict": true, + "strict": true, + "noEmit": true + }, + "include": [ + "./**/*.tsx", + ] + } diff --git a/test/integration/typescript-node-next/package.json b/test/integration/typescript-node-next/package.json new file mode 100644 index 00000000000..814afb7aced --- /dev/null +++ b/test/integration/typescript-node-next/package.json @@ -0,0 +1,15 @@ +{ + "private": true, + "type": "module", + "description": "chart.js should work in node next typescript project", + "dependencies": { + "chart.js": "workspace:*", + "typescript": "^4.7.4" + }, + "scripts": { + "test": "tsc" + }, + "devDependencies": { + "ts-expect": "^1.3.0" + } +} diff --git a/test/integration/typescript-node-next/src/index.ts b/test/integration/typescript-node-next/src/index.ts new file mode 100644 index 00000000000..4b2ba1aa0dc --- /dev/null +++ b/test/integration/typescript-node-next/src/index.ts @@ -0,0 +1,9 @@ +/* eslint-disable @typescript-eslint/no-explicit-any, no-console */ +import {Chart} from 'chart.js'; +import AutoChart from 'chart.js/auto'; +import {debounce} from 'chart.js/helpers'; +import {TypeOf, expectType} from 'ts-expect'; + +expectType>(false); +expectType>(false); +expectType>(false); diff --git a/test/integration/typescript-node-next/tsconfig.json b/test/integration/typescript-node-next/tsconfig.json new file mode 100644 index 00000000000..0abf67dcf94 --- /dev/null +++ b/test/integration/typescript-node-next/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ES6", + "moduleResolution": "NodeNext", + "noEmit": true, + "lib": ["es2018", "DOM"] + }, + "include": [ + "./src/**/*.ts", + ] +} diff --git a/test/integration/typescript-node/package.json b/test/integration/typescript-node/package.json new file mode 100644 index 00000000000..49143cb10b8 --- /dev/null +++ b/test/integration/typescript-node/package.json @@ -0,0 +1,15 @@ +{ + "private": true, + "type": "module", + "description": "chart.js should work in node typescript project", + "dependencies": { + "chart.js": "workspace:*", + "typescript": "^4.7.4" + }, + "scripts": { + "test": "tsc" + }, + "devDependencies": { + "ts-expect": "^1.3.0" + } +} diff --git a/test/integration/typescript-node/src/index.ts b/test/integration/typescript-node/src/index.ts new file mode 100644 index 00000000000..4b2ba1aa0dc --- /dev/null +++ b/test/integration/typescript-node/src/index.ts @@ -0,0 +1,9 @@ +/* eslint-disable @typescript-eslint/no-explicit-any, no-console */ +import {Chart} from 'chart.js'; +import AutoChart from 'chart.js/auto'; +import {debounce} from 'chart.js/helpers'; +import {TypeOf, expectType} from 'ts-expect'; + +expectType>(false); +expectType>(false); +expectType>(false); diff --git a/test/integration/typescript-node/tsconfig.json b/test/integration/typescript-node/tsconfig.json new file mode 100644 index 00000000000..0036ee25208 --- /dev/null +++ b/test/integration/typescript-node/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ES6", + "moduleResolution": "Node", + "noEmit": true, + "lib": ["es2018", "DOM"] + }, + "include": [ + "./src/**/*.ts", + ] +} diff --git a/test/seed-reporter.cjs b/test/seed-reporter.cjs new file mode 100644 index 00000000000..6328b0bcdda --- /dev/null +++ b/test/seed-reporter.cjs @@ -0,0 +1,13 @@ +const SeedReporter = function(baseReporterDecorator) { + baseReporterDecorator(this); + + this.onBrowserComplete = function(browser, result) { + if (result.order && result.order.random && result.order.seed) { + this.write('%s: Randomized with seed %s\n', browser, result.order.seed); + } + }; +}; + +module.exports = { + 'reporter:jasmine-seed': ['type', SeedReporter] +}; diff --git a/test/specs/controller.bar.tests.js b/test/specs/controller.bar.tests.js new file mode 100644 index 00000000000..128ef241d00 --- /dev/null +++ b/test/specs/controller.bar.tests.js @@ -0,0 +1,1853 @@ +describe('Chart.controllers.bar', function() { + describe('auto', jasmine.fixture.specs('controller.bar')); + + it('should be registered as dataset controller', function() { + expect(typeof Chart.controllers.bar).toBe('function'); + }); + + it('should be constructed', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [ + {data: []}, + {data: []} + ], + labels: [] + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.type).toEqual('bar'); + expect(meta.data).toEqual([]); + expect(meta.hidden).toBe(null); + expect(meta.controller).not.toBe(undefined); + expect(meta.controller.index).toBe(1); + expect(meta.xAxisID).not.toBe(null); + expect(meta.yAxisID).not.toBe(null); + + meta.controller.updateIndex(0); + expect(meta.controller.index).toBe(0); + }); + + it('should set null bars to the reset state', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [10, null, 0, -4], + label: 'dataset1', + }], + labels: ['label1', 'label2', 'label3', 'label4'] + } + }); + + var meta = chart.getDatasetMeta(0); + var bar = meta.data[1]; + var {x, y, base} = bar.getProps(['x', 'y', 'base'], true); + expect(isNaN(x)).toBe(false); + expect(isNaN(y)).toBe(false); + expect(isNaN(base)).toBe(false); + }); + + it('should use the first scale IDs if the dataset does not specify them', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [ + {data: []}, + {data: []} + ], + labels: [] + }, + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.xAxisID).toBe('x'); + expect(meta.yAxisID).toBe('y'); + }); + + it('should correctly count the number of stacks ignoring datasets of other types and hidden datasets', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [ + {data: [], type: 'line'}, + {data: [], hidden: true}, + {data: []}, + {data: []} + ], + labels: [] + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller._getStackCount()).toBe(2); + }); + + it('should correctly count the number of stacks when a group is not specified', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [ + {data: []}, + {data: []}, + {data: []}, + {data: []} + ], + labels: [] + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller._getStackCount()).toBe(4); + }); + + it('should correctly count the number of stacks when a group is not specified and the scale is stacked', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [ + {data: []}, + {data: []}, + {data: []}, + {data: []} + ], + labels: [] + }, + options: { + scales: { + x: { + stacked: true + }, + y: { + stacked: true + } + } + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller._getStackCount()).toBe(1); + }); + + it('should correctly count the number of stacks when a group is not specified and the scale is not stacked', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [ + {data: []}, + {data: []}, + {data: []}, + {data: []} + ], + labels: [] + }, + options: { + scales: { + x: { + stacked: false + }, + y: { + stacked: false + } + } + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller._getStackCount()).toBe(4); + }); + + it('should correctly count the number of stacks when a group is specified for some', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: []}, + {data: []} + ], + labels: [] + } + }); + + var meta = chart.getDatasetMeta(3); + expect(meta.controller._getStackCount()).toBe(3); + }); + + it('should correctly count the number of stacks when a group is specified for some and the scale is stacked', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: []}, + {data: []} + ], + labels: [] + }, + options: { + scales: { + x: { + stacked: true + }, + y: { + stacked: true + } + } + } + }); + + var meta = chart.getDatasetMeta(3); + expect(meta.controller._getStackCount()).toBe(2); + }); + + it('should correctly count the number of stacks when a group is specified for some and the scale is not stacked', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: []}, + {data: []} + ], + labels: [] + }, + options: { + scales: { + x: { + stacked: false + }, + y: { + stacked: false + } + } + } + }); + + var meta = chart.getDatasetMeta(3); + expect(meta.controller._getStackCount()).toBe(4); + }); + + it('should correctly count the number of stacks when a group is specified for all', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack2'}, + {data: [], stack: 'stack2'} + ], + labels: [] + } + }); + + var meta = chart.getDatasetMeta(3); + expect(meta.controller._getStackCount()).toBe(2); + }); + + it('should correctly count the number of stacks when a group is specified for all and the scale is stacked', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack2'}, + {data: [], stack: 'stack2'} + ], + labels: [] + }, + options: { + scales: { + x: { + stacked: true + }, + y: { + stacked: true + } + } + } + }); + + var meta = chart.getDatasetMeta(3); + expect(meta.controller._getStackCount()).toBe(2); + }); + + it('should correctly count the number of stacks when a group is specified for all and the scale is not stacked', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack2'}, + {data: [], stack: 'stack2'} + ], + labels: [] + }, + options: { + scales: { + x: { + stacked: false + }, + y: { + stacked: false + } + } + } + }); + + var meta = chart.getDatasetMeta(3); + expect(meta.controller._getStackCount()).toBe(4); + }); + + it('should correctly get the stack index accounting for datasets of other types and hidden datasets', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [ + {data: []}, + {data: [], hidden: true}, + {data: [], type: 'line'}, + {data: []} + ], + labels: [] + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller._getStackIndex(0)).toBe(0); + expect(meta.controller._getStackIndex(3)).toBe(1); + }); + + it('should correctly get the stack index when a group is not specified', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [ + {data: []}, + {data: []}, + {data: []}, + {data: []} + ], + labels: [] + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller._getStackIndex(0)).toBe(0); + expect(meta.controller._getStackIndex(1)).toBe(1); + expect(meta.controller._getStackIndex(2)).toBe(2); + expect(meta.controller._getStackIndex(3)).toBe(3); + }); + + it('should correctly get the stack index when a group is not specified and the scale is stacked', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [ + {data: []}, + {data: []}, + {data: []}, + {data: []} + ], + labels: [] + }, + options: { + scales: { + x: { + stacked: true + }, + y: { + stacked: true + } + } + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller._getStackIndex(0)).toBe(0); + expect(meta.controller._getStackIndex(1)).toBe(0); + expect(meta.controller._getStackIndex(2)).toBe(0); + expect(meta.controller._getStackIndex(3)).toBe(0); + }); + + it('should correctly get the stack index when a group is not specified and the scale is not stacked', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [ + {data: []}, + {data: []}, + {data: []}, + {data: []} + ], + labels: [] + }, + options: { + scales: { + x: { + stacked: false + }, + y: { + stacked: false + } + } + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller._getStackIndex(0)).toBe(0); + expect(meta.controller._getStackIndex(1)).toBe(1); + expect(meta.controller._getStackIndex(2)).toBe(2); + expect(meta.controller._getStackIndex(3)).toBe(3); + }); + + it('should correctly get the stack index when a group is specified for some', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: []}, + {data: []} + ], + labels: [] + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller._getStackIndex(0)).toBe(0); + expect(meta.controller._getStackIndex(1)).toBe(0); + expect(meta.controller._getStackIndex(2)).toBe(1); + expect(meta.controller._getStackIndex(3)).toBe(2); + }); + + it('should correctly get the stack index when a group is specified for some and the scale is stacked', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: []}, + {data: []} + ], + labels: [] + }, + options: { + scales: { + x: { + stacked: true + }, + y: { + stacked: true + } + } + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller._getStackIndex(0)).toBe(0); + expect(meta.controller._getStackIndex(1)).toBe(0); + expect(meta.controller._getStackIndex(2)).toBe(1); + expect(meta.controller._getStackIndex(3)).toBe(1); + }); + + it('should correctly get the stack index when a group is specified for some and the scale is not stacked', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: []}, + {data: []} + ], + labels: [] + }, + options: { + scales: { + x: { + stacked: false + }, + y: { + stacked: false + } + } + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller._getStackIndex(0)).toBe(0); + expect(meta.controller._getStackIndex(1)).toBe(1); + expect(meta.controller._getStackIndex(2)).toBe(2); + expect(meta.controller._getStackIndex(3)).toBe(3); + }); + + it('should correctly get the stack index when a group is specified for all', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack2'}, + {data: [], stack: 'stack2'} + ], + labels: [] + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller._getStackIndex(0)).toBe(0); + expect(meta.controller._getStackIndex(1)).toBe(0); + expect(meta.controller._getStackIndex(2)).toBe(1); + expect(meta.controller._getStackIndex(3)).toBe(1); + }); + + it('should correctly get the stack index when a group is specified for all and the scale is stacked', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack2'}, + {data: [], stack: 'stack2'} + ], + labels: [] + }, + options: { + scales: { + x: { + stacked: true + }, + y: { + stacked: true + } + } + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller._getStackIndex(0)).toBe(0); + expect(meta.controller._getStackIndex(1)).toBe(0); + expect(meta.controller._getStackIndex(2)).toBe(1); + expect(meta.controller._getStackIndex(3)).toBe(1); + }); + + it('should correctly get the stack index when a group is specified for all and the scale is not stacked', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [ + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack1'}, + {data: [], stack: 'stack2'}, + {data: [], stack: 'stack2'} + ], + labels: [] + }, + options: { + scales: { + x: { + stacked: false + }, + y: { + stacked: false + } + } + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.controller._getStackIndex(0)).toBe(0); + expect(meta.controller._getStackIndex(1)).toBe(1); + expect(meta.controller._getStackIndex(2)).toBe(2); + expect(meta.controller._getStackIndex(3)).toBe(3); + }); + + it('should create bar elements for each data item during initialization', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [ + {data: []}, + {data: [10, 15, 0, -4]} + ], + labels: [] + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.data.length).toBe(4); // 4 bars created + expect(meta.data[0] instanceof Chart.elements.BarElement).toBe(true); + expect(meta.data[1] instanceof Chart.elements.BarElement).toBe(true); + expect(meta.data[2] instanceof Chart.elements.BarElement).toBe(true); + expect(meta.data[3] instanceof Chart.elements.BarElement).toBe(true); + }); + + it('should update elements when modifying data', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [1, 2], + label: 'dataset1' + }, { + data: [10, 15, 0, -4], + label: 'dataset2', + borderColor: 'blue' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + plugins: { + legend: false, + title: false + }, + elements: { + bar: { + backgroundColor: 'red', + borderSkipped: 'top', + borderColor: 'green', + borderWidth: 2, + } + }, + scales: { + x: { + type: 'category', + display: false + }, + y: { + type: 'linear', + display: false, + beginAtZero: false + } + } + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.data.length).toBe(4); + + chart.data.datasets[1].data = [1, 2]; // remove 2 items + chart.data.datasets[1].borderWidth = 1; + chart.update(); + + expect(meta.data.length).toBe(2); + expect(meta._parsed.length).toBe(2); + + [ + {x: 89, y: 512}, + {x: 217, y: 0} + ].forEach(function(expected, i) { + expect(meta.data[i].x).toBeCloseToPixel(expected.x); + expect(meta.data[i].y).toBeCloseToPixel(expected.y); + expect(meta.data[i].base).toBeCloseToPixel(1024); + expect(meta.data[i].width).toBeCloseToPixel(46); + expect(meta.data[i].options).toEqual(jasmine.objectContaining({ + backgroundColor: 'red', + borderSkipped: 'top', + borderColor: 'blue', + borderWidth: 1 + })); + }); + + chart.data.datasets[1].data = [1, 2, 3]; // add 1 items + chart.update(); + + expect(meta.data.length).toBe(3); // should add a new meta data item + }); + + it('should get the correct bar points when datasets of different types exist', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [1, 2], + label: 'dataset1' + }, { + type: 'line', + data: [4, 6], + label: 'dataset2' + }, { + data: [8, 10], + label: 'dataset3' + }], + labels: ['label1', 'label2'] + }, + options: { + plugins: { + legend: false, + title: false + }, + scales: { + x: { + type: 'category', + display: false + }, + y: { + type: 'linear', + display: false, + beginAtZero: false + } + } + } + }); + + var meta = chart.getDatasetMeta(2); + expect(meta.data.length).toBe(2); + + var bar1 = meta.data[0]; + var bar2 = meta.data[1]; + + expect(bar1.x).toBeCloseToPixel(179); + expect(bar1.y).toBeCloseToPixel(117); + expect(bar2.x).toBeCloseToPixel(431); + expect(bar2.y).toBeCloseToPixel(4); + }); + + it('should get the bar points for hidden dataset', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [1, 2], + label: 'dataset1', + hidden: true + }], + labels: ['label1', 'label2'] + }, + options: { + plugins: { + legend: false, + title: false + }, + scales: { + x: { + type: 'category', + display: false + }, + y: { + type: 'linear', + min: 0, + max: 2, + display: false + } + } + } + }); + + var meta = chart.getDatasetMeta(0); + expect(meta.data.length).toBe(2); + + var bar1 = meta.data[0]; + var bar2 = meta.data[1]; + + expect(bar1.x).toBeCloseToPixel(128); + expect(bar1.y).toBeCloseToPixel(256); + expect(bar2.x).toBeCloseToPixel(384); + expect(bar2.y).toBeCloseToPixel(0); + }); + + + it('should update elements when the scales are stacked', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [10, -10, 10, -10], + label: 'dataset1' + }, { + data: [10, 15, 0, -4], + label: 'dataset2' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + plugins: { + legend: false, + title: false + }, + scales: { + x: { + type: 'category', + display: false + }, + y: { + type: 'linear', + display: false, + stacked: true + } + } + } + }); + + var meta0 = chart.getDatasetMeta(0); + + [ + {b: 293, w: 92 / 2, x: 38, y: 146}, + {b: 293, w: 92 / 2, x: 166, y: 439}, + {b: 293, w: 92 / 2, x: 295, y: 146}, + {b: 293, w: 92 / 2, x: 422, y: 439} + ].forEach(function(values, i) { + expect(meta0.data[i].base).toBeCloseToPixel(values.b); + expect(meta0.data[i].width).toBeCloseToPixel(values.w); + expect(meta0.data[i].x).toBeCloseToPixel(values.x); + expect(meta0.data[i].y).toBeCloseToPixel(values.y); + }); + + var meta1 = chart.getDatasetMeta(1); + + [ + {b: 146, w: 92 / 2, x: 89, y: 0}, + {b: 293, w: 92 / 2, x: 217, y: 73}, + {b: 146, w: 92 / 2, x: 345, y: 146}, + {b: 439, w: 92 / 2, x: 473, y: 497} + ].forEach(function(values, i) { + expect(meta1.data[i].base).toBeCloseToPixel(values.b); + expect(meta1.data[i].width).toBeCloseToPixel(values.w); + expect(meta1.data[i].x).toBeCloseToPixel(values.x); + expect(meta1.data[i].y).toBeCloseToPixel(values.y); + }); + }); + + it('should update elements when the scales are stacked and the y axis has a user defined minimum', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [50, 20, 10, 100], + label: 'dataset1' + }, { + data: [50, 80, 90, 0], + label: 'dataset2' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + plugins: { + legend: false, + title: false + }, + scales: { + x: { + type: 'category', + display: false + }, + y: { + type: 'linear', + display: false, + stacked: true, + min: 50, + max: 100 + } + } + } + }); + + var meta0 = chart.getDatasetMeta(0); + + [ + {b: 1024, w: 92 / 2, x: 38, y: 512}, + {b: 1024, w: 92 / 2, x: 166, y: 819}, + {b: 1024, w: 92 / 2, x: 294, y: 922}, + {b: 1024, w: 92 / 2, x: 422.5, y: 0} + ].forEach(function(values, i) { + expect(meta0.data[i].base).toBeCloseToPixel(values.b); + expect(meta0.data[i].width).toBeCloseToPixel(values.w); + expect(meta0.data[i].x).toBeCloseToPixel(values.x); + expect(meta0.data[i].y).toBeCloseToPixel(values.y); + }); + + var meta1 = chart.getDatasetMeta(1); + + [ + {b: 512, w: 92 / 2, x: 89, y: 0}, + {b: 819.2, w: 92 / 2, x: 217, y: 0}, + {b: 921.6, w: 92 / 2, x: 345, y: 0}, + {b: 0, w: 92 / 2, x: 473.5, y: 0} + ].forEach(function(values, i) { + expect(meta1.data[i].base).toBeCloseToPixel(values.b); + expect(meta1.data[i].width).toBeCloseToPixel(values.w); + expect(meta1.data[i].x).toBeCloseToPixel(values.x); + expect(meta1.data[i].y).toBeCloseToPixel(values.y); + }); + }); + + it('should update elements when only the category scale is stacked', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [20, -10, 10, -10], + label: 'dataset1' + }, { + data: [10, 15, 0, -14], + label: 'dataset2' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + plugins: { + legend: false, + title: false + }, + scales: { + x: { + type: 'category', + display: false, + stacked: true + }, + y: { + type: 'linear', + display: false + } + } + } + }); + + var meta0 = chart.getDatasetMeta(0); + + [ + {b: 293, w: 92, x: 64, y: 0}, + {b: 293, w: 92, x: 192, y: 439}, + {b: 293, w: 92, x: 320, y: 146}, + {b: 293, w: 92, x: 448, y: 439} + ].forEach(function(values, i) { + expect(meta0.data[i].base).toBeCloseToPixel(values.b); + expect(meta0.data[i].width).toBeCloseToPixel(values.w); + expect(meta0.data[i].x).toBeCloseToPixel(values.x); + expect(meta0.data[i].y).toBeCloseToPixel(values.y); + }); + + var meta1 = chart.getDatasetMeta(1); + + [ + {b: 293, w: 92, x: 64, y: 146}, + {b: 293, w: 92, x: 192, y: 73}, + {b: 293, w: 92, x: 320, y: 293}, + {b: 293, w: 92, x: 448, y: 497} + ].forEach(function(values, i) { + expect(meta1.data[i].base).toBeCloseToPixel(values.b); + expect(meta1.data[i].width).toBeCloseToPixel(values.w); + expect(meta1.data[i].x).toBeCloseToPixel(values.x); + expect(meta1.data[i].y).toBeCloseToPixel(values.y); + }); + }); + + it('should update elements when the scales are stacked and data is strings', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: ['10', '-10', '10', '-10'], + label: 'dataset1' + }, { + data: ['10', '15', '0', '-4'], + label: 'dataset2' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + plugins: { + legend: false, + title: false + }, + scales: { + x: { + type: 'category', + display: false + }, + y: { + type: 'linear', + display: false, + stacked: true + } + } + } + }); + + var meta0 = chart.getDatasetMeta(0); + + [ + {b: 293, w: 92 / 2, x: 38, y: 146}, + {b: 293, w: 92 / 2, x: 166, y: 439}, + {b: 293, w: 92 / 2, x: 295, y: 146}, + {b: 293, w: 92 / 2, x: 422, y: 439} + ].forEach(function(values, i) { + expect(meta0.data[i].base).toBeCloseToPixel(values.b); + expect(meta0.data[i].width).toBeCloseToPixel(values.w); + expect(meta0.data[i].x).toBeCloseToPixel(values.x); + expect(meta0.data[i].y).toBeCloseToPixel(values.y); + }); + + var meta1 = chart.getDatasetMeta(1); + + [ + {b: 146, w: 92 / 2, x: 89, y: 0}, + {b: 293, w: 92 / 2, x: 217, y: 73}, + {b: 146, w: 92 / 2, x: 345, y: 146}, + {b: 439, w: 92 / 2, x: 473, y: 497} + ].forEach(function(values, i) { + expect(meta1.data[i].base).toBeCloseToPixel(values.b); + expect(meta1.data[i].width).toBeCloseToPixel(values.w); + expect(meta1.data[i].x).toBeCloseToPixel(values.x); + expect(meta1.data[i].y).toBeCloseToPixel(values.y); + }); + }); + + it('should get the correct bar points for grouped stacked chart if the group name is same', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [10, -10, 10, -10], + label: 'dataset1', + stack: 'stack1' + }, { + data: [10, 15, 0, -4], + label: 'dataset2', + stack: 'stack1' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + plugins: { + legend: false, + title: false + }, + scales: { + x: { + type: 'category', + display: false + }, + y: { + type: 'linear', + display: false, + stacked: true + } + } + } + }); + + var meta0 = chart.getDatasetMeta(0); + + [ + {b: 293, w: 92, x: 64, y: 146}, + {b: 293, w: 92, x: 192, y: 439}, + {b: 293, w: 92, x: 320, y: 146}, + {b: 293, w: 92, x: 448, y: 439} + ].forEach(function(values, i) { + expect(meta0.data[i].base).toBeCloseToPixel(values.b); + expect(meta0.data[i].width).toBeCloseToPixel(values.w); + expect(meta0.data[i].x).toBeCloseToPixel(values.x); + expect(meta0.data[i].y).toBeCloseToPixel(values.y); + }); + + var meta = chart.getDatasetMeta(1); + + [ + {b: 146, w: 92, x: 64, y: 0}, + {b: 293, w: 92, x: 192, y: 73}, + {b: 146, w: 92, x: 320, y: 146}, + {b: 439, w: 92, x: 448, y: 497} + ].forEach(function(values, i) { + expect(meta.data[i].base).toBeCloseToPixel(values.b); + expect(meta.data[i].width).toBeCloseToPixel(values.w); + expect(meta.data[i].x).toBeCloseToPixel(values.x); + expect(meta.data[i].y).toBeCloseToPixel(values.y); + }); + }); + + it('should get the correct bar points for grouped stacked chart if the group name is different', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [1, 2], + stack: 'stack1' + }, { + data: [1, 2], + stack: 'stack2' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + plugins: { + legend: false, + title: false + }, + scales: { + x: { + type: 'category', + display: false + }, + y: { + type: 'linear', + display: false, + stacked: true, + } + } + } + }); + + var meta = chart.getDatasetMeta(1); + + [ + {x: 89, y: 256}, + {x: 217, y: 0} + ].forEach(function(values, i) { + expect(meta.data[i].base).toBeCloseToPixel(512); + expect(meta.data[i].width).toBeCloseToPixel(46); + expect(meta.data[i].x).toBeCloseToPixel(values.x); + expect(meta.data[i].y).toBeCloseToPixel(values.y); + }); + }); + + it('should get the correct bar points for grouped stacked chart', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [1, 2], + stack: 'stack1' + }, { + data: [0.5, 1], + stack: 'stack2' + }, { + data: [0.5, 1], + stack: 'stack2' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + plugins: { + legend: false, + title: false + }, + scales: { + x: { + type: 'category', + display: false + }, + y: { + type: 'linear', + display: false, + stacked: true + } + } + } + }); + + var meta = chart.getDatasetMeta(2); + + [ + {b: 384, x: 89, y: 256}, + {b: 256, x: 217, y: 0} + ].forEach(function(values, i) { + expect(meta.data[i].base).toBeCloseToPixel(values.b); + expect(meta.data[i].width).toBeCloseToPixel(46); + expect(meta.data[i].x).toBeCloseToPixel(values.x); + expect(meta.data[i].y).toBeCloseToPixel(values.y); + }); + }); + + it('should draw all bars', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [], + }, { + data: [10, 15, 0, -4], + label: 'dataset2' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + } + }); + + var meta = chart.getDatasetMeta(1); + + spyOn(meta.data[0], 'draw'); + spyOn(meta.data[1], 'draw'); + spyOn(meta.data[2], 'draw'); + spyOn(meta.data[3], 'draw'); + + chart.update(); + + expect(meta.data[0].draw.calls.count()).toBe(1); + expect(meta.data[1].draw.calls.count()).toBe(1); + expect(meta.data[2].draw.calls.count()).toBe(1); + expect(meta.data[3].draw.calls.count()).toBe(1); + }); + + it('should set hover styles on bars', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [], + }, { + data: [10, 15, 0, -4], + label: 'dataset2' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + elements: { + bar: { + backgroundColor: 'rgb(255, 0, 0)', + borderColor: 'rgb(0, 0, 255)', + borderWidth: 2, + } + } + } + }); + + var meta = chart.getDatasetMeta(1); + var bar = meta.data[0]; + + meta.controller.setHoverStyle(bar, 1, 0); + expect(bar.options.backgroundColor).toBe('#E60000'); + expect(bar.options.borderColor).toBe('#0000E6'); + expect(bar.options.borderWidth).toBe(2); + + // Set a dataset style + chart.data.datasets[1].hoverBackgroundColor = 'rgb(128, 128, 128)'; + chart.data.datasets[1].hoverBorderColor = 'rgb(0, 0, 0)'; + chart.data.datasets[1].hoverBorderWidth = 5; + chart.update(); + + meta.controller.setHoverStyle(bar, 1, 0); + expect(bar.options.backgroundColor).toBe('rgb(128, 128, 128)'); + expect(bar.options.borderColor).toBe('rgb(0, 0, 0)'); + expect(bar.options.borderWidth).toBe(5); + + // Should work with array styles so that we can set per bar + chart.data.datasets[1].hoverBackgroundColor = ['rgb(255, 255, 255)', 'rgb(128, 128, 128)']; + chart.data.datasets[1].hoverBorderColor = ['rgb(9, 9, 9)', 'rgb(0, 0, 0)']; + chart.data.datasets[1].hoverBorderWidth = [2.5, 5]; + chart.update(); + + meta.controller.setHoverStyle(bar, 1, 0); + expect(bar.options.backgroundColor).toBe('rgb(255, 255, 255)'); + expect(bar.options.borderColor).toBe('rgb(9, 9, 9)'); + expect(bar.options.borderWidth).toBe(2.5); + }); + + it('should remove a hover style from a bar', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [], + }, { + data: [10, 15, 0, -4], + label: 'dataset2' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + elements: { + bar: { + backgroundColor: 'rgb(255, 0, 0)', + borderColor: 'rgb(0, 0, 255)', + borderWidth: 2, + } + } + } + }); + + var meta = chart.getDatasetMeta(1); + var bar = meta.data[0]; + var helpers = window.Chart.helpers; + + // Change default + chart.options.elements.bar.backgroundColor = 'rgb(128, 128, 128)'; + chart.options.elements.bar.borderColor = 'rgb(15, 15, 15)'; + chart.options.elements.bar.borderWidth = 3.14; + + chart.update(); + expect(bar.options.backgroundColor).toBe('rgb(128, 128, 128)'); + expect(bar.options.borderColor).toBe('rgb(15, 15, 15)'); + expect(bar.options.borderWidth).toBe(3.14); + meta.controller.setHoverStyle(bar, 1, 0); + expect(bar.options.backgroundColor).toBe(helpers.getHoverColor('rgb(128, 128, 128)')); + expect(bar.options.borderColor).toBe(helpers.getHoverColor('rgb(15, 15, 15)')); + expect(bar.options.borderWidth).toBe(3.14); + meta.controller.removeHoverStyle(bar); + expect(bar.options.backgroundColor).toBe('rgb(128, 128, 128)'); + expect(bar.options.borderColor).toBe('rgb(15, 15, 15)'); + expect(bar.options.borderWidth).toBe(3.14); + + // Should work with array styles so that we can set per bar + chart.data.datasets[1].backgroundColor = ['rgb(255, 255, 255)', 'rgb(128, 128, 128)']; + chart.data.datasets[1].borderColor = ['rgb(9, 9, 9)', 'rgb(0, 0, 0)']; + chart.data.datasets[1].borderWidth = [2.5, 5]; + + chart.update(); + expect(bar.options.backgroundColor).toBe('rgb(255, 255, 255)'); + expect(bar.options.borderColor).toBe('rgb(9, 9, 9)'); + expect(bar.options.borderWidth).toBe(2.5); + meta.controller.setHoverStyle(bar, 1, 0); + expect(bar.options.backgroundColor).toBe(helpers.getHoverColor('rgb(255, 255, 255)')); + expect(bar.options.borderColor).toBe(helpers.getHoverColor('rgb(9, 9, 9)')); + expect(bar.options.borderWidth).toBe(2.5); + meta.controller.removeHoverStyle(bar); + expect(bar.options.backgroundColor).toBe('rgb(255, 255, 255)'); + expect(bar.options.borderColor).toBe('rgb(9, 9, 9)'); + expect(bar.options.borderWidth).toBe(2.5); + }); + + describe('Bar width', function() { + beforeEach(function() { + // 2 datasets + this.data = { + labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], + datasets: [{ + data: [10, 20, 30, 40, 50, 60, 70], + }, { + data: [10, 20, 30, 40, 50, 60, 70], + }] + }; + }); + + afterEach(function() { + var chart = window.acquireChart(this.config); + var meta = chart.getDatasetMeta(0); + var xScale = chart.scales[meta.xAxisID]; + var options = Chart.defaults.datasets.bar; + + var categoryPercentage = options.categoryPercentage; + var barPercentage = options.barPercentage; + var stacked = xScale.options.stacked; + + var totalBarWidth = 0; + for (var i = 0; i < chart.data.datasets.length; i++) { + var bars = chart.getDatasetMeta(i).data; + for (var j = xScale.min; j <= xScale.max; j++) { + totalBarWidth += bars[j].width; + } + if (stacked) { + break; + } + } + + var actualValue = totalBarWidth; + var expectedValue = xScale.width * categoryPercentage * barPercentage; + expect(actualValue).toBeCloseToPixel(expectedValue); + + }); + + it('should correctly set bar width when min and max option is set.', function() { + this.config = { + type: 'bar', + data: this.data, + options: { + scales: { + x: { + min: 'March', + max: 'May', + } + } + } + }; + }); + + it('should correctly set bar width when scale are stacked with min and max options.', function() { + this.config = { + type: 'bar', + data: this.data, + options: { + scales: { + x: { + min: 'March', + max: 'May', + }, + y: { + stacked: true + } + } + } + }; + }); + }); + + describe('Bar height (horizontal type)', function() { + beforeEach(function() { + // 2 datasets + this.data = { + labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], + datasets: [{ + data: [10, 20, 30, 40, 50, 60, 70], + }, { + data: [10, 20, 30, 40, 50, 60, 70], + }] + }; + }); + + afterEach(function() { + var chart = window.acquireChart(this.config); + var meta = chart.getDatasetMeta(0); + var yScale = chart.scales[meta.yAxisID]; + + var config = meta.controller.options; + var categoryPercentage = config.categoryPercentage; + var barPercentage = config.barPercentage; + var stacked = yScale.options.stacked; + + var totalBarHeight = 0; + for (var i = 0; i < chart.data.datasets.length; i++) { + var bars = chart.getDatasetMeta(i).data; + for (var j = yScale.min; j <= yScale.max; j++) { + totalBarHeight += bars[j].height; + } + if (stacked) { + break; + } + } + + var actualValue = totalBarHeight; + var expectedValue = yScale.height * categoryPercentage * barPercentage; + expect(actualValue).toBeCloseToPixel(expectedValue); + + }); + + it('should correctly set bar height when min and max option is set.', function() { + this.config = { + type: 'bar', + data: this.data, + options: { + indexAxis: 'y', + scales: { + y: { + min: 'March', + max: 'May', + } + } + } + }; + }); + + it('should correctly set bar height when scale are stacked with min and max options.', function() { + this.config = { + type: 'bar', + data: this.data, + options: { + indexAxis: 'y', + scales: { + x: { + stacked: true + }, + y: { + min: 'March', + max: 'May', + } + } + } + }; + }); + }); + + describe('Bar thickness with a category scale', function() { + [undefined, 20].forEach(function(barThickness) { + describe('When barThickness is ' + barThickness, function() { + beforeEach(function() { + this.chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [1, 2] + }, { + data: [1, 2] + }], + labels: ['label1', 'label2', 'label3'] + }, + options: { + legend: false, + title: false, + datasets: { + bar: { + barThickness: barThickness + } + }, + scales: { + x: { + id: 'x', + type: 'category', + }, + y: { + type: 'linear', + } + } + } + }); + }); + + it('should correctly set bar width', function() { + var chart = this.chart; + var expected, i, ilen, meta; + + if (barThickness) { + expected = barThickness; + } else { + var scale = chart.scales.x; + var options = Chart.defaults.datasets.bar; + var categoryPercentage = options.categoryPercentage; + var barPercentage = options.barPercentage; + var tickInterval = scale.getPixelForTick(1) - scale.getPixelForTick(0); + + expected = tickInterval * categoryPercentage / 2 * barPercentage; + } + + for (i = 0, ilen = chart.data.datasets.length; i < ilen; ++i) { + meta = chart.getDatasetMeta(i); + expect(meta.data[0].width).toBeCloseToPixel(expected); + expect(meta.data[1].width).toBeCloseToPixel(expected); + } + }); + + it('should correctly set bar width if maxBarThickness is specified', function() { + var chart = this.chart; + var i, ilen, meta; + + chart.data.datasets[0].maxBarThickness = 10; + chart.data.datasets[1].maxBarThickness = 10; + chart.update(); + + for (i = 0, ilen = chart.data.datasets.length; i < ilen; ++i) { + meta = chart.getDatasetMeta(i); + expect(meta.data[0].width).toBeCloseToPixel(10); + expect(meta.data[1].width).toBeCloseToPixel(10); + } + }); + }); + }); + }); + + it('minBarLength settings should be used on Y axis on bar chart', function() { + var minBarLength = 4; + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + minBarLength: minBarLength, + data: [0.05, -0.05, 10, 15, 20, 25, 30, 35] + }] + } + }); + + var data = chart.getDatasetMeta(0).data; + var halfBaseLine = chart.scales.y.getLineWidthForValue(0) / 2; + + expect(data[0].base - minBarLength + halfBaseLine).toEqual(data[0].y); + expect(data[1].base + minBarLength - halfBaseLine).toEqual(data[1].y); + }); + + it('minBarLength settings should be used on X axis on horizontal bar chart', function() { + var minBarLength = 4; + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + indexAxis: 'y', + minBarLength: minBarLength, + data: [0.05, -0.05, 10, 15, 20, 25, 30, 35] + }] + } + }); + + var data = chart.getDatasetMeta(0).data; + var halfBaseLine = chart.scales.x.getLineWidthForValue(0) / 2; + + expect(data[0].base + minBarLength - halfBaseLine).toEqual(data[0].x); + expect(data[1].base - minBarLength + halfBaseLine).toEqual(data[1].x); + }); + + it('should respect the data visibility settings', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [1, 2, 3, 4] + }], + labels: ['A', 'B', 'C', 'D'] + }, + options: { + plugins: { + legend: false, + title: false + }, + scales: { + x: { + type: 'category', + display: false + }, + y: { + type: 'linear', + display: false, + } + } + } + }); + + var data = chart.getDatasetMeta(0).data; + expect(data[0].base).toBeCloseToPixel(512); + expect(data[0].y).toBeCloseToPixel(384); + + chart.toggleDataVisibility(0); + chart.update(); + + data = chart.getDatasetMeta(0).data; + expect(data[0].base).toBeCloseToPixel(512); + expect(data[0].y).toBeCloseToPixel(512); + }); + + it('should hide bar dataset beneath the chart for correct animations', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [1, 2, 3, 4] + }, { + data: [1, 2, 3, 4] + }], + labels: ['A', 'B', 'C', 'D'] + }, + options: { + plugins: { + legend: false, + title: false + }, + scales: { + x: { + type: 'category', + display: false, + stacked: true, + }, + y: { + type: 'linear', + display: false, + stacked: true, + } + } + } + }); + + var data = chart.getDatasetMeta(0).data; + expect(data[0].base).toBeCloseToPixel(512); + expect(data[0].y).toBeCloseToPixel(448); + + chart.setDatasetVisibility(0, false); + chart.update(); + + data = chart.getDatasetMeta(0).data; + expect(data[0].base).toBeCloseToPixel(640); + expect(data[0].y).toBeCloseToPixel(512); + }); + + describe('Float bar', function() { + it('Should return correct values from getMinMax', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + labels: ['a'], + datasets: [{ + data: [[10, -10]] + }] + } + }); + + expect(chart.scales.y.getMinMax()).toEqual({min: -10, max: 10}); + }); + }); + + describe('clip', function() { + it('Should not use ctx.clip when clip=false', function() { + var ctx = window.createMockContext(); + ctx.resetTransform = function() {}; + + var chart = window.acquireChart({ + type: 'bar', + data: { + labels: ['a', 'b', 'c'], + datasets: [{ + data: [1, 2, 3], + clip: false + }] + } + }); + var orig = chart.ctx; + + // Draw on mock context + chart.ctx = ctx; + chart.draw(); + + chart.ctx = orig; + + expect(ctx.getCalls().filter(x => x.name === 'clip').length).toEqual(0); + }); + }); + + it('should not crash with skipNull and uneven datasets', function() { + function unevenChart() { + window.acquireChart({ + type: 'bar', + data: { + labels: [1, 2], + datasets: [ + {data: [1, 2]}, + {data: [1, 2, 3]}, + ] + }, + options: { + skipNull: true, + } + }); + } + + expect(unevenChart).not.toThrow(); + }); + + it('should correctly count the number of stacks when skipNull and different order datasets', function() { + + const chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [ + { + id: '1', + label: 'USA', + data: [ + { + xScale: 'First', + Country: 'USA', + yScale: 524 + }, + { + xScale: 'Second', + Country: 'USA', + yScale: 325 + } + ], + + yAxisID: 'yScale', + xAxisID: 'xScale', + + parsing: { + yAxisKey: 'yScale', + xAxisKey: 'xScale' + } + }, + { + id: '2', + label: 'BRA', + data: [ + { + xScale: 'Second', + Country: 'BRA', + yScale: 183 + }, + { + xScale: 'First', + Country: 'BRA', + yScale: 177 + } + ], + + yAxisID: 'yScale', + xAxisID: 'xScale', + + parsing: { + yAxisKey: 'yScale', + xAxisKey: 'xScale' + } + }, + { + id: '3', + label: 'DEU', + data: [ + { + xScale: 'First', + Country: 'DEU', + yScale: 162 + } + ], + + yAxisID: 'yScale', + xAxisID: 'xScale', + + parsing: { + yAxisKey: 'yScale', + xAxisKey: 'xScale' + } + } + ] + }, + options: { + skipNull: true + } + }); + + var meta = chart.getDatasetMeta(0); + expect(meta.controller._getStackCount(0)).toBe(3); + expect(meta.controller._getStackCount(1)).toBe(2); + + }); + + it('should not override tooltip title and label callbacks', async() => { + const chart = window.acquireChart({ + type: 'bar', + data: { + labels: ['Label 1', 'Label 2'], + datasets: [{ + data: [21, 79], + label: 'Dataset 1' + }, { + data: [33, 67], + label: 'Dataset 2' + }] + }, + options: { + responsive: true, + maintainAspectRatio: true, + } + }); + const {tooltip} = chart; + const point = chart.getDatasetMeta(0).data[0]; + + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + + expect(tooltip.title).toEqual(['Label 1']); + expect(tooltip.body).toEqual([{ + before: [], + lines: ['Dataset 1: 21'], + after: [] + }]); + + chart.options.plugins.tooltip = {mode: 'dataset'}; + chart.update(); + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + + expect(tooltip.title).toEqual(['Dataset 1']); + expect(tooltip.body).toEqual([{ + before: [], + lines: ['Label 1: 21'], + after: [] + }, { + before: [], + lines: ['Label 2: 79'], + after: [] + }]); + }); +}); diff --git a/test/specs/controller.bubble.tests.js b/test/specs/controller.bubble.tests.js new file mode 100644 index 00000000000..ed15ec5f0c8 --- /dev/null +++ b/test/specs/controller.bubble.tests.js @@ -0,0 +1,426 @@ +describe('Chart.controllers.bubble', function() { + describe('auto', jasmine.fixture.specs('controller.bubble')); + + it('should be registered as dataset controller', function() { + expect(typeof Chart.controllers.bubble).toBe('function'); + }); + + it('should be constructed', function() { + var chart = window.acquireChart({ + type: 'bubble', + data: { + datasets: [{ + data: [] + }] + } + }); + + var meta = chart.getDatasetMeta(0); + expect(meta.type).toBe('bubble'); + expect(meta.controller).not.toBe(undefined); + expect(meta.controller.index).toBe(0); + expect(meta.data).toEqual([]); + + meta.controller.updateIndex(1); + expect(meta.controller.index).toBe(1); + }); + + it('should use the first scale IDs if the dataset does not specify them', function() { + var chart = window.acquireChart({ + type: 'bubble', + data: { + datasets: [{ + data: [] + }] + }, + }); + + var meta = chart.getDatasetMeta(0); + + expect(meta.xAxisID).toBe('x'); + expect(meta.yAxisID).toBe('y'); + }); + + it('should create point elements for each data item during initialization', function() { + var chart = window.acquireChart({ + type: 'bubble', + data: { + datasets: [{ + data: [10, 15, 0, -4] + }] + } + }); + + var meta = chart.getDatasetMeta(0); + + expect(meta.data.length).toBe(4); // 4 points created + expect(meta.data[0] instanceof Chart.elements.PointElement).toBe(true); + expect(meta.data[1] instanceof Chart.elements.PointElement).toBe(true); + expect(meta.data[2] instanceof Chart.elements.PointElement).toBe(true); + expect(meta.data[3] instanceof Chart.elements.PointElement).toBe(true); + }); + + it('should draw all elements', function() { + var chart = window.acquireChart({ + type: 'bubble', + data: { + datasets: [{ + data: [10, 15, 0, -4] + }] + }, + options: { + animation: false, + showLine: true + } + }); + + var meta = chart.getDatasetMeta(0); + + spyOn(meta.data[0], 'draw'); + spyOn(meta.data[1], 'draw'); + spyOn(meta.data[2], 'draw'); + spyOn(meta.data[3], 'draw'); + + chart.update(); + + expect(meta.data[0].draw.calls.count()).toBe(1); + expect(meta.data[1].draw.calls.count()).toBe(1); + expect(meta.data[2].draw.calls.count()).toBe(1); + expect(meta.data[3].draw.calls.count()).toBe(1); + }); + + it('should update elements when modifying style', function() { + var chart = window.acquireChart({ + type: 'bubble', + data: { + datasets: [{ + data: [{ + x: 10, + y: 10, + r: 5 + }, { + x: -15, + y: -10, + r: 1 + }, { + x: 0, + y: -9, + r: 2 + }, { + x: -4, + y: 10, + r: 1 + }] + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + plugins: { + legend: false, + title: false + }, + scales: { + x: { + type: 'category', + display: false + }, + y: { + type: 'linear', + display: false + } + } + } + }); + + var meta = chart.getDatasetMeta(0); + + [ + {r: 5, x: 5, y: 5}, + {r: 1, x: 171, y: 507}, + {r: 2, x: 341, y: 482}, + {r: 1, x: 507, y: 5} + ].forEach(function(expected, i) { + expect(meta.data[i].x).toBeCloseToPixel(expected.x); + expect(meta.data[i].y).toBeCloseToPixel(expected.y); + expect(meta.data[i].options).toEqual(jasmine.objectContaining({ + backgroundColor: Chart.defaults.backgroundColor, + borderColor: Chart.defaults.borderColor, + borderWidth: 1, + hitRadius: 1, + radius: expected.r + })); + }); + + // Use dataset level styles for lines & points + chart.data.datasets[0].backgroundColor = 'rgb(98, 98, 98)'; + chart.data.datasets[0].borderColor = 'rgb(8, 8, 8)'; + chart.data.datasets[0].borderWidth = 0.55; + + // point styles + chart.data.datasets[0].radius = 22; + chart.data.datasets[0].hitRadius = 3.3; + + chart.update(); + + for (var i = 0; i < 4; ++i) { + expect(meta.data[i].options).toEqual(jasmine.objectContaining({ + backgroundColor: 'rgb(98, 98, 98)', + borderColor: 'rgb(8, 8, 8)', + borderWidth: 0.55, + hitRadius: 3.3 + })); + } + }); + + it('should handle number of data point changes in update', function() { + var chart = window.acquireChart({ + type: 'bubble', + data: { + datasets: [{ + data: [{ + x: 10, + y: 10, + r: 5 + }, { + x: -15, + y: -10, + r: 1 + }, { + x: 0, + y: -9, + r: 2 + }, { + x: -4, + y: 10, + r: 1 + }] + }], + labels: ['label1', 'label2', 'label3', 'label4'] + } + }); + + var meta = chart.getDatasetMeta(0); + + expect(meta.data.length).toBe(4); + + chart.data.datasets[0].data = [{ + x: 1, + y: 1, + r: 10 + }, { + x: 10, + y: 5, + r: 2 + }]; // remove 2 items + + chart.update(); + + expect(meta.data.length).toBe(2); + expect(meta.data[0] instanceof Chart.elements.PointElement).toBe(true); + expect(meta.data[1] instanceof Chart.elements.PointElement).toBe(true); + + chart.data.datasets[0].data = [{ + x: 10, + y: 10, + r: 5 + }, { + x: -15, + y: -10, + r: 1 + }, { + x: 0, + y: -9, + r: 2 + }, { + x: -4, + y: 10, + r: 1 + }, { + x: -5, + y: 0, + r: 3 + }]; // add 3 items + + chart.update(); + + expect(meta.data.length).toBe(5); + expect(meta.data[0] instanceof Chart.elements.PointElement).toBe(true); + expect(meta.data[1] instanceof Chart.elements.PointElement).toBe(true); + expect(meta.data[2] instanceof Chart.elements.PointElement).toBe(true); + expect(meta.data[3] instanceof Chart.elements.PointElement).toBe(true); + expect(meta.data[4] instanceof Chart.elements.PointElement).toBe(true); + }); + + describe('Interactions', function() { + beforeEach(function() { + this.chart = window.acquireChart({ + type: 'bubble', + data: { + labels: ['label1', 'label2', 'label3', 'label4'], + datasets: [{ + data: [{ + x: 5, + y: 5, + r: 20 + }, { + x: -15, + y: -10, + r: 15 + }, { + x: 15, + y: 10, + r: 10 + }, { + x: -15, + y: 10, + r: 5 + }] + }] + }, + options: { + elements: { + point: { + backgroundColor: 'rgb(100, 150, 200)', + borderColor: 'rgb(50, 100, 150)', + borderWidth: 2, + radius: 3 + } + } + } + }); + }); + + it ('should handle default hover styles', async function() { + var chart = this.chart; + var point = chart.getDatasetMeta(0).data[0]; + + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + expect(point.options.backgroundColor).toBe('#3187DD'); + expect(point.options.borderColor).toBe('#175A9D'); + expect(point.options.borderWidth).toBe(1); + expect(point.options.radius).toBe(20 + 4); + + await jasmine.triggerMouseEvent(chart, 'mouseout', point); + expect(point.options.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(point.options.borderColor).toBe('rgb(50, 100, 150)'); + expect(point.options.borderWidth).toBe(2); + expect(point.options.radius).toBe(20); + }); + + it ('should handle hover styles defined via dataset properties', async function() { + var chart = this.chart; + var point = chart.getDatasetMeta(0).data[0]; + + Chart.helpers.merge(chart.data.datasets[0], { + hoverBackgroundColor: 'rgb(200, 100, 150)', + hoverBorderColor: 'rgb(150, 50, 100)', + hoverBorderWidth: 8.4, + hoverRadius: 4.2 + }); + + chart.update(); + + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + expect(point.options.backgroundColor).toBe('rgb(200, 100, 150)'); + expect(point.options.borderColor).toBe('rgb(150, 50, 100)'); + expect(point.options.borderWidth).toBe(8.4); + expect(point.options.radius).toBe(20 + 4.2); + + await jasmine.triggerMouseEvent(chart, 'mouseout', point); + expect(point.options.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(point.options.borderColor).toBe('rgb(50, 100, 150)'); + expect(point.options.borderWidth).toBe(2); + expect(point.options.radius).toBe(20); + }); + + it ('should handle hover styles defined via element options', async function() { + var chart = this.chart; + var point = chart.getDatasetMeta(0).data[0]; + + Chart.helpers.merge(chart.options.elements.point, { + hoverBackgroundColor: 'rgb(200, 100, 150)', + hoverBorderColor: 'rgb(150, 50, 100)', + hoverBorderWidth: 8.4, + hoverRadius: 4.2 + }); + + chart.update(); + + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + expect(point.options.backgroundColor).toBe('rgb(200, 100, 150)'); + expect(point.options.borderColor).toBe('rgb(150, 50, 100)'); + expect(point.options.borderWidth).toBe(8.4); + expect(point.options.radius).toBe(20 + 4.2); + + await jasmine.triggerMouseEvent(chart, 'mouseout', point); + expect(point.options.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(point.options.borderColor).toBe('rgb(50, 100, 150)'); + expect(point.options.borderWidth).toBe(2); + expect(point.options.radius).toBe(20); + }); + }); + + it('should not override tooltip title and label callbacks', async() => { + const chart = window.acquireChart({ + type: 'bubble', + data: { + labels: ['Label 1', 'Label 2'], + datasets: [{ + data: [{ + x: 10, + y: 15, + r: 15 + }, + { + x: 12, + y: 10, + r: 10 + }], + label: 'Dataset 1' + }, { + data: [{ + x: 20, + y: 10, + r: 5 + }, + { + x: 4, + y: 8, + r: 30 + }], + label: 'Dataset 2' + }] + }, + options: { + responsive: true, + maintainAspectRatio: true, + } + }); + const {tooltip} = chart; + const point = chart.getDatasetMeta(0).data[0]; + + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + + expect(tooltip.title).toEqual(['Label 1']); + expect(tooltip.body).toEqual([{ + before: [], + lines: ['Dataset 1: (10, 15, 15)'], + after: [] + }]); + + chart.options.plugins.tooltip = {mode: 'dataset'}; + chart.update(); + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + + expect(tooltip.title).toEqual(['Dataset 1']); + expect(tooltip.body).toEqual([{ + before: [], + lines: ['Label 1: (10, 15, 15)'], + after: [] + }, { + before: [], + lines: ['Label 2: (12, 10, 10)'], + after: [] + }]); + }); +}); diff --git a/test/specs/controller.doughnut.tests.js b/test/specs/controller.doughnut.tests.js new file mode 100644 index 00000000000..a4596b69024 --- /dev/null +++ b/test/specs/controller.doughnut.tests.js @@ -0,0 +1,447 @@ +describe('Chart.controllers.doughnut', function() { + describe('auto', jasmine.fixture.specs('controller.doughnut')); + + it('should be registered as dataset controller', function() { + expect(typeof Chart.controllers.doughnut).toBe('function'); + expect(typeof Chart.controllers.pie).toBe('function'); + }); + + it('should be constructed', function() { + var chart = window.acquireChart({ + type: 'doughnut', + data: { + datasets: [{ + data: [] + }], + labels: [] + } + }); + + var meta = chart.getDatasetMeta(0); + expect(meta.type).toBe('doughnut'); + expect(meta.controller).not.toBe(undefined); + expect(meta.controller.index).toBe(0); + expect(meta.data).toEqual([]); + + meta.controller.updateIndex(1); + expect(meta.controller.index).toBe(1); + }); + + it('should create arc elements for each data item during initialization', function() { + var chart = window.acquireChart({ + type: 'doughnut', + data: { + datasets: [{ + data: [10, 15, 0, 4] + }], + labels: [] + } + }); + + var meta = chart.getDatasetMeta(0); + expect(meta.data.length).toBe(4); // 4 arcs created + expect(meta.data[0] instanceof Chart.elements.ArcElement).toBe(true); + expect(meta.data[1] instanceof Chart.elements.ArcElement).toBe(true); + expect(meta.data[2] instanceof Chart.elements.ArcElement).toBe(true); + expect(meta.data[3] instanceof Chart.elements.ArcElement).toBe(true); + }); + + it ('should reset and update elements', function() { + var chart = window.acquireChart({ + type: 'doughnut', + data: { + datasets: [{ + data: [1, 2, 3, 4], + hidden: true + }, { + data: [5, 6, 0, 7] + }, { + data: [8, 9, 10, 11] + }], + labels: ['label0', 'label1', 'label2', 'label3'] + }, + options: { + plugins: { + legend: false, + title: false, + }, + animation: { + duration: 0, + animateRotate: true, + animateScale: false + }, + cutout: '50%', + rotation: 0, + circumference: 360, + elements: { + arc: { + backgroundColor: 'rgb(255, 0, 0)', + borderColor: 'rgb(0, 0, 255)', + borderWidth: 2 + } + } + } + }); + + var meta = chart.getDatasetMeta(1); + + meta.controller.reset(); // reset first + + expect(meta.data.length).toBe(4); + + [ + {c: 0}, + {c: 0}, + {c: 0}, + {c: 0} + ].forEach(function(expected, i) { + expect(meta.data[i].x).toBeCloseToPixel(256); + expect(meta.data[i].y).toBeCloseToPixel(256); + expect(meta.data[i].outerRadius).toBeCloseToPixel(256); + expect(meta.data[i].innerRadius).toBeCloseToPixel(192); + expect(meta.data[i].circumference).toBeCloseTo(expected.c, 8); + expect(meta.data[i].startAngle).toBeCloseToPixel(Math.PI * -0.5); + expect(meta.data[i].endAngle).toBeCloseToPixel(Math.PI * -0.5); + expect(meta.data[i].options).toEqual(jasmine.objectContaining({ + backgroundColor: 'rgb(255, 0, 0)', + borderColor: 'rgb(0, 0, 255)', + borderWidth: 2 + })); + }); + + chart.update(); + + [ + {c: 1.7453292519, s: -1.5707963267, e: 0.1745329251}, + {c: 2.0943951023, s: 0.1745329251, e: 2.2689280275}, + {c: 0, s: 2.2689280275, e: 2.2689280275}, + {c: 2.4434609527, s: 2.2689280275, e: 4.7123889803} + ].forEach(function(expected, i) { + expect(meta.data[i].x).toBeCloseToPixel(256); + expect(meta.data[i].y).toBeCloseToPixel(256); + expect(meta.data[i].outerRadius).toBeCloseToPixel(256); + expect(meta.data[i].innerRadius).toBeCloseToPixel(192); + expect(meta.data[i].circumference).toBeCloseTo(expected.c, 8); + expect(meta.data[i].startAngle).toBeCloseTo(expected.s, 8); + expect(meta.data[i].endAngle).toBeCloseTo(expected.e, 8); + expect(meta.data[i].options).toEqual(jasmine.objectContaining({ + backgroundColor: 'rgb(255, 0, 0)', + borderColor: 'rgb(0, 0, 255)', + borderWidth: 2 + })); + }); + + // Change the amount of data and ensure that arcs are updated accordingly + chart.data.datasets[1].data = [1, 2]; // remove 2 elements from dataset 0 + chart.update(); + + expect(meta.data.length).toBe(2); + expect(meta.data[0] instanceof Chart.elements.ArcElement).toBe(true); + expect(meta.data[1] instanceof Chart.elements.ArcElement).toBe(true); + + // Add data + chart.data.datasets[1].data = [1, 2, 3, 4]; + chart.update(); + + expect(meta.data.length).toBe(4); + expect(meta.data[0] instanceof Chart.elements.ArcElement).toBe(true); + expect(meta.data[1] instanceof Chart.elements.ArcElement).toBe(true); + expect(meta.data[2] instanceof Chart.elements.ArcElement).toBe(true); + expect(meta.data[3] instanceof Chart.elements.ArcElement).toBe(true); + }); + + it ('should rotate and limit circumference', function() { + var chart = window.acquireChart({ + type: 'doughnut', + data: { + datasets: [{ + data: [2, 4], + hidden: true + }, { + data: [1, 3] + }, { + data: [1, 0] + }], + labels: ['label0', 'label1', 'label2'] + }, + options: { + plugins: { + legend: false, + title: false, + }, + cutout: '50%', + rotation: 270, + circumference: 90, + elements: { + arc: { + backgroundColor: 'rgb(255, 0, 0)', + borderColor: 'rgb(0, 0, 255)', + borderWidth: 2 + } + } + } + }); + + var meta = chart.getDatasetMeta(1); + + expect(meta.data.length).toBe(2); + + // Only startAngle, endAngle and circumference should be different. + [ + {c: Math.PI / 8, s: Math.PI, e: Math.PI + Math.PI / 8}, + {c: 3 * Math.PI / 8, s: Math.PI + Math.PI / 8, e: Math.PI + Math.PI / 2} + ].forEach(function(expected, i) { + expect(meta.data[i].x).toBeCloseToPixel(512); + expect(meta.data[i].y).toBeCloseToPixel(512); + expect(meta.data[i].outerRadius).toBeCloseToPixel(512); + expect(meta.data[i].innerRadius).toBeCloseToPixel(384); + expect(meta.data[i].circumference).toBeCloseTo(expected.c, 8); + expect(meta.data[i].startAngle).toBeCloseTo(expected.s, 8); + expect(meta.data[i].endAngle).toBeCloseTo(expected.e, 8); + }); + }); + + it('should treat negative values as positive', function() { + var chart = window.acquireChart({ + type: 'doughnut', + data: { + datasets: [{ + data: [-1, -3] + }], + labels: ['label0', 'label1'] + }, + options: { + plugins: { + legend: false, + title: false + }, + cutout: '50%', + rotation: 270, + circumference: 90, + elements: { + arc: { + backgroundColor: 'rgb(255, 0, 0)', + borderColor: 'rgb(0, 0, 255)', + borderWidth: 2 + } + } + } + }); + + var meta = chart.getDatasetMeta(0); + + expect(meta.data.length).toBe(2); + + // Only startAngle, endAngle and circumference should be different. + [ + {c: Math.PI / 8, s: Math.PI, e: Math.PI + Math.PI / 8}, + {c: 3 * Math.PI / 8, s: Math.PI + Math.PI / 8, e: Math.PI + Math.PI / 2} + ].forEach(function(expected, i) { + expect(meta.data[i].circumference).toBeCloseTo(expected.c, 8); + expect(meta.data[i].startAngle).toBeCloseTo(expected.s, 8); + expect(meta.data[i].endAngle).toBeCloseTo(expected.e, 8); + }); + }); + + it ('should draw all arcs', function() { + var chart = window.acquireChart({ + type: 'doughnut', + data: { + datasets: [{ + data: [10, 15, 0, 4] + }], + labels: ['label0', 'label1', 'label2', 'label3'] + } + }); + + var meta = chart.getDatasetMeta(0); + + spyOn(meta.data[0], 'draw'); + spyOn(meta.data[1], 'draw'); + spyOn(meta.data[2], 'draw'); + spyOn(meta.data[3], 'draw'); + + chart.update(); + + expect(meta.data[0].draw.calls.count()).toBe(1); + expect(meta.data[1].draw.calls.count()).toBe(1); + expect(meta.data[2].draw.calls.count()).toBe(1); + expect(meta.data[3].draw.calls.count()).toBe(1); + }); + + it ('should calculate radiuses based on the border widths of the visible outermost dataset', function() { + var chart = window.acquireChart({ + type: 'doughnut', + data: { + datasets: [{ + data: [2, 4], + borderWidth: 4, + hidden: true + }, { + data: [1, 3], + borderWidth: 8 + }, { + data: [1, 0], + borderWidth: 12 + }], + labels: ['label0', 'label1'] + }, + options: { + plugins: { + legend: false, + title: false + } + } + }); + + chart.update(); + + var controller = chart.getDatasetMeta(0).controller; + expect(chart.chartArea.bottom - chart.chartArea.top).toBe(512); + + expect(controller.getMaxBorderWidth()).toBe(8); + expect(controller.outerRadius).toBe(252); + expect(controller.innerRadius).toBe(189); + + controller = chart.getDatasetMeta(1).controller; + expect(controller.getMaxBorderWidth()).toBe(8); + expect(controller.outerRadius).toBe(252); + expect(controller.innerRadius).toBe(189); + + controller = chart.getDatasetMeta(2).controller; + expect(controller.getMaxBorderWidth()).toBe(8); + expect(controller.outerRadius).toBe(189); + expect(controller.innerRadius).toBe(126); + }); + + describe('Interactions', function() { + beforeEach(function() { + this.chart = window.acquireChart({ + type: 'doughnut', + data: { + labels: ['label1', 'label2', 'label3', 'label4'], + datasets: [{ + data: [10, 15, 0, 4] + }] + }, + options: { + cutout: '50%', + elements: { + arc: { + backgroundColor: 'rgb(100, 150, 200)', + borderColor: 'rgb(50, 100, 150)', + borderWidth: 2, + } + } + } + }); + }); + + it ('should handle default hover styles', async function() { + var chart = this.chart; + var arc = chart.getDatasetMeta(0).data[0]; + + await jasmine.triggerMouseEvent(chart, 'mousemove', arc); + expect(arc.options.backgroundColor).toBe('#3187DD'); + expect(arc.options.borderColor).toBe('#175A9D'); + expect(arc.options.borderWidth).toBe(2); + + await jasmine.triggerMouseEvent(chart, 'mouseout', arc); + expect(arc.options.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(arc.options.borderColor).toBe('rgb(50, 100, 150)'); + expect(arc.options.borderWidth).toBe(2); + }); + + it ('should handle hover styles defined via dataset properties', async function() { + var chart = this.chart; + var arc = chart.getDatasetMeta(0).data[0]; + + Chart.helpers.merge(chart.data.datasets[0], { + hoverBackgroundColor: 'rgb(200, 100, 150)', + hoverBorderColor: 'rgb(150, 50, 100)', + hoverBorderWidth: 8.4, + }); + + chart.update(); + + await jasmine.triggerMouseEvent(chart, 'mousemove', arc); + expect(arc.options.backgroundColor).toBe('rgb(200, 100, 150)'); + expect(arc.options.borderColor).toBe('rgb(150, 50, 100)'); + expect(arc.options.borderWidth).toBe(8.4); + + await jasmine.triggerMouseEvent(chart, 'mouseout', arc); + expect(arc.options.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(arc.options.borderColor).toBe('rgb(50, 100, 150)'); + expect(arc.options.borderWidth).toBe(2); + }); + + it ('should handle hover styles defined via element options', async function() { + var chart = this.chart; + var arc = chart.getDatasetMeta(0).data[0]; + + Chart.helpers.merge(chart.options.elements.arc, { + hoverBackgroundColor: 'rgb(200, 100, 150)', + hoverBorderColor: 'rgb(150, 50, 100)', + hoverBorderWidth: 8.4, + }); + + chart.update(); + + await jasmine.triggerMouseEvent(chart, 'mousemove', arc); + expect(arc.options.backgroundColor).toBe('rgb(200, 100, 150)'); + expect(arc.options.borderColor).toBe('rgb(150, 50, 100)'); + expect(arc.options.borderWidth).toBe(8.4); + + await jasmine.triggerMouseEvent(chart, 'mouseout', arc); + expect(arc.options.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(arc.options.borderColor).toBe('rgb(50, 100, 150)'); + expect(arc.options.borderWidth).toBe(2); + }); + }); + + it('should not override tooltip title and label callbacks', async() => { + const chart = window.acquireChart({ + type: 'doughnut', + data: { + labels: ['Label 1', 'Label 2'], + datasets: [{ + data: [21, 79], + label: 'Dataset 1' + }, { + data: [33, 67], + label: 'Dataset 2' + }] + }, + options: { + responsive: true, + maintainAspectRatio: true, + } + }); + const {tooltip} = chart; + const point = chart.getDatasetMeta(0).data[0]; + + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + + expect(tooltip.title).toEqual(['Label 1']); + expect(tooltip.body).toEqual([{ + before: [], + lines: ['Dataset 1: 21'], + after: [] + }]); + + chart.options.plugins.tooltip = {mode: 'dataset'}; + chart.update(); + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + + expect(tooltip.title).toEqual(['Dataset 1']); + expect(tooltip.body).toEqual([{ + before: [], + lines: ['Label 1: 21'], + after: [] + }, { + before: [], + lines: ['Label 2: 79'], + after: [] + }]); + }); +}); diff --git a/test/specs/controller.line.tests.js b/test/specs/controller.line.tests.js new file mode 100644 index 00000000000..3924a79887d --- /dev/null +++ b/test/specs/controller.line.tests.js @@ -0,0 +1,1206 @@ +describe('Chart.controllers.line', function() { + describe('auto', jasmine.fixture.specs('controller.line')); + + it('should be registered as dataset controller', function() { + expect(typeof Chart.controllers.line).toBe('function'); + }); + + it('should be constructed', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [] + }], + labels: [] + } + }); + + var meta = chart.getDatasetMeta(0); + expect(meta.type).toBe('line'); + expect(meta.controller).not.toBe(undefined); + expect(meta.controller.index).toBe(0); + expect(meta.data).toEqual([]); + + meta.controller.updateIndex(1); + expect(meta.controller.index).toBe(1); + }); + + it('Should use the first scale IDs if the dataset does not specify them', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [] + }], + labels: [] + }, + }); + + var meta = chart.getDatasetMeta(0); + expect(meta.xAxisID).toBe('x'); + expect(meta.yAxisID).toBe('y'); + }); + + it('Should not throw with empty dataset when tension is non-zero', function() { + // https://github.com/chartjs/Chart.js/issues/8676 + function createChart() { + return window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [], + tension: 0.5 + }], + labels: [] + }, + }); + } + expect(createChart).not.toThrow(); + }); + + it('should find min and max for stacked chart', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [10, 11, 12, 13] + }, { + data: [1, 2, 3, 4] + }], + labels: ['a', 'b', 'c', 'd'] + }, + options: { + scales: { + y: { + stacked: true + } + } + } + }); + expect(chart.getDatasetMeta(0).controller.getMinMax(chart.scales.y, true)).toEqual({min: 10, max: 13}); + expect(chart.getDatasetMeta(1).controller.getMinMax(chart.scales.y, true)).toEqual({min: 11, max: 17}); + chart.hide(0); + expect(chart.getDatasetMeta(0).controller.getMinMax(chart.scales.y, true)).toEqual({min: 10, max: 13}); + expect(chart.getDatasetMeta(1).controller.getMinMax(chart.scales.y, true)).toEqual({min: 1, max: 4}); + }); + + it('Should create line elements and point elements for each data item during initialization', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [10, 15, 0, -4], + label: 'dataset1' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + } + }); + + var meta = chart.getDatasetMeta(0); + expect(meta.data.length).toBe(4); // 4 points created + expect(meta.data[0] instanceof Chart.elements.PointElement).toBe(true); + expect(meta.data[1] instanceof Chart.elements.PointElement).toBe(true); + expect(meta.data[2] instanceof Chart.elements.PointElement).toBe(true); + expect(meta.data[3] instanceof Chart.elements.PointElement).toBe(true); + expect(meta.dataset instanceof Chart.elements.LineElement).toBe(true); // 1 line element + }); + + it('should draw all elements', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [10, 15, 0, -4], + label: 'dataset1' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + showLine: true + } + }); + + var meta = chart.getDatasetMeta(0); + spyOn(meta.dataset, 'updateControlPoints'); + spyOn(meta.dataset, 'draw'); + spyOn(meta.data[0], 'draw'); + spyOn(meta.data[1], 'draw'); + spyOn(meta.data[2], 'draw'); + spyOn(meta.data[3], 'draw'); + + chart.update(); + + expect(meta.dataset.updateControlPoints.calls.count()).toBeGreaterThanOrEqual(1); + expect(meta.dataset.draw.calls.count()).toBe(1); + expect(meta.data[0].draw.calls.count()).toBe(1); + expect(meta.data[1].draw.calls.count()).toBe(1); + expect(meta.data[2].draw.calls.count()).toBe(1); + expect(meta.data[3].draw.calls.count()).toBe(1); + }); + + it('should update elements when modifying data', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [10, 15, 0, -4], + label: 'dataset', + xAxisID: 'x', + yAxisID: 'y' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + showLine: true, + plugins: { + legend: false, + title: false + }, + elements: { + point: { + backgroundColor: 'red', + borderColor: 'blue', + } + }, + scales: { + x: { + display: false + }, + y: { + display: false + } + } + }, + }); + + var meta = chart.getDatasetMeta(0); + expect(meta.data.length).toBe(4); + + chart.data.datasets[0].data = [1, 2]; // remove 2 items + chart.data.datasets[0].borderWidth = 1; + chart.update(); + + expect(meta.data.length).toBe(2); + expect(meta._parsed.length).toBe(2); + + [ + {x: 5, y: 507}, + {x: 171, y: 5} + ].forEach(function(expected, i) { + expect(meta.data[i].x).toBeCloseToPixel(expected.x); + expect(meta.data[i].y).toBeCloseToPixel(expected.y); + expect(meta.data[i].options).toEqual(jasmine.objectContaining({ + backgroundColor: 'red', + borderColor: 'blue', + })); + }); + + chart.data.datasets[0].data = [1, 2, 3]; // add 1 items + chart.update(); + + expect(meta.data.length).toBe(3); // should add a new meta data item + }); + + it('should correctly calculate x scale for label and point', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + labels: ['One'], + datasets: [{ + data: [1], + }] + }, + options: { + plugins: { + legend: false, + title: false + }, + hover: { + mode: 'nearest', + intersect: true + }, + scales: { + x: { + display: false, + }, + y: { + display: false, + beginAtZero: true + } + } + } + }); + + var meta = chart.getDatasetMeta(0); + // 1 point + var point = meta.data[0]; + expect(point.x).toBeCloseToPixel(5); + + // 2 points + chart.data.labels = ['One', 'Two']; + chart.data.datasets[0].data = [1, 2]; + chart.update(); + + var points = meta.data; + + expect(points[0].x).toBeCloseToPixel(5); + expect(points[1].x).toBeCloseToPixel(507); + + // 3 points + chart.data.labels = ['One', 'Two', 'Three']; + chart.data.datasets[0].data = [1, 2, 3]; + chart.update(); + + points = meta.data; + + expect(points[0].x).toBeCloseToPixel(5); + expect(points[1].x).toBeCloseToPixel(256); + expect(points[2].x).toBeCloseToPixel(507); + + // 4 points + chart.data.labels = ['One', 'Two', 'Three', 'Four']; + chart.data.datasets[0].data = [1, 2, 3, 4]; + chart.update(); + + points = meta.data; + + expect(points[0].x).toBeCloseToPixel(5); + expect(points[1].x).toBeCloseToPixel(171); + expect(points[2].x).toBeCloseToPixel(340); + expect(points[3].x).toBeCloseToPixel(507); + }); + + it('should update elements when the y scale is stacked', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [10, -10, 10, -10], + label: 'dataset1' + }, { + data: [10, 15, 0, -4], + label: 'dataset2' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + plugins: { + legend: false, + title: false + }, + scales: { + x: { + display: false, + }, + y: { + display: false, + stacked: true + } + } + } + }); + + var meta0 = chart.getDatasetMeta(0); + + [ + {x: 5, y: 148}, + {x: 171, y: 435}, + {x: 341, y: 148}, + {x: 507, y: 435} + ].forEach(function(values, i) { + expect(meta0.data[i].x).toBeCloseToPixel(values.x); + expect(meta0.data[i].y).toBeCloseToPixel(values.y); + }); + + var meta1 = chart.getDatasetMeta(1); + + [ + {x: 5, y: 5}, + {x: 171, y: 76}, + {x: 341, y: 148}, + {x: 507, y: 492} + ].forEach(function(values, i) { + expect(meta1.data[i].x).toBeCloseToPixel(values.x); + expect(meta1.data[i].y).toBeCloseToPixel(values.y); + }); + + }); + + it('should update elements when the y scale is stacked with multiple axes', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [10, -10, 10, -10], + label: 'dataset1' + }, { + data: [10, 15, 0, -4], + label: 'dataset2' + }, { + data: [10, 10, -10, -10], + label: 'dataset3', + yAxisID: 'y2' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + plugins: { + legend: false, + title: false, + }, + scales: { + x: { + display: false, + }, + y: { + display: false, + stacked: true + }, + y2: { + type: 'linear', + position: 'right', + display: false + } + } + } + }); + + var meta0 = chart.getDatasetMeta(0); + + [ + {x: 5, y: 148}, + {x: 171, y: 435}, + {x: 341, y: 148}, + {x: 507, y: 435} + ].forEach(function(values, i) { + expect(meta0.data[i].x).toBeCloseToPixel(values.x); + expect(meta0.data[i].y).toBeCloseToPixel(values.y); + }); + + var meta1 = chart.getDatasetMeta(1); + + [ + {x: 5, y: 5}, + {x: 171, y: 76}, + {x: 341, y: 148}, + {x: 507, y: 492} + ].forEach(function(values, i) { + expect(meta1.data[i].x).toBeCloseToPixel(values.x); + expect(meta1.data[i].y).toBeCloseToPixel(values.y); + }); + + }); + + it('should update elements when the y scale is stacked and datasets is scatter data', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [{ + x: 0, + y: 10 + }, { + x: 1, + y: -10 + }, { + x: 2, + y: 10 + }, { + x: 3, + y: -10 + }], + label: 'dataset1' + }, { + data: [{ + x: 0, + y: 10 + }, { + x: 1, + y: 15 + }, { + x: 2, + y: 0 + }, { + x: 3, + y: -4 + }], + label: 'dataset2' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + plugins: { + legend: false, + title: false + }, + scales: { + x: { + display: false, + }, + y: { + display: false, + stacked: true + } + } + } + }); + + var meta0 = chart.getDatasetMeta(0); + + [ + {x: 5, y: 148}, + {x: 171, y: 435}, + {x: 341, y: 148}, + {x: 507, y: 435} + ].forEach(function(values, i) { + expect(meta0.data[i].x).toBeCloseToPixel(values.x); + expect(meta0.data[i].y).toBeCloseToPixel(values.y); + }); + + var meta1 = chart.getDatasetMeta(1); + + [ + {x: 5, y: 5}, + {x: 171, y: 76}, + {x: 341, y: 148}, + {x: 507, y: 492} + ].forEach(function(values, i) { + expect(meta1.data[i].x).toBeCloseToPixel(values.x); + expect(meta1.data[i].y).toBeCloseToPixel(values.y); + }); + + }); + + it('should update elements when the y scale is stacked and data is strings', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: ['10', '-10', '10', '-10'], + label: 'dataset1' + }, { + data: ['10', '15', '0', '-4'], + label: 'dataset2' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + plugins: { + legend: false, + title: false + }, + scales: { + x: { + display: false, + }, + y: { + display: false, + stacked: true + } + } + } + }); + + var meta0 = chart.getDatasetMeta(0); + + [ + {x: 5, y: 148}, + {x: 171, y: 435}, + {x: 341, y: 148}, + {x: 507, y: 435} + ].forEach(function(values, i) { + expect(meta0.data[i].x).toBeCloseToPixel(values.x); + expect(meta0.data[i].y).toBeCloseToPixel(values.y); + }); + + var meta1 = chart.getDatasetMeta(1); + + [ + {x: 5, y: 5}, + {x: 171, y: 76}, + {x: 341, y: 148}, + {x: 507, y: 492} + ].forEach(function(values, i) { + expect(meta1.data[i].x).toBeCloseToPixel(values.x); + expect(meta1.data[i].y).toBeCloseToPixel(values.y); + }); + + }); + + it('should fall back to the line styles for points', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [0, 0], + label: 'dataset1', + + // line styles + backgroundColor: 'rgb(98, 98, 98)', + borderColor: 'rgb(8, 8, 8)', + borderWidth: 0.55, + }], + labels: ['label1', 'label2'] + } + }); + + var meta = chart.getDatasetMeta(0); + + expect(meta.dataset.options.backgroundColor).toBe('rgb(98, 98, 98)'); + expect(meta.dataset.options.borderColor).toBe('rgb(8, 8, 8)'); + expect(meta.dataset.options.borderWidth).toBe(0.55); + }); + + describe('dataset global defaults', function() { + beforeEach(function() { + this._defaults = Chart.helpers.clone(Chart.defaults.datasets.line); + }); + + afterEach(function() { + Chart.defaults.datasets.line = this._defaults; + delete this._defaults; + }); + + it('should utilize the dataset global default options', function() { + Chart.helpers.merge(Chart.defaults.datasets.line, { + spanGaps: true, + tension: 0.231, + backgroundColor: '#add', + borderWidth: '#daa', + borderColor: '#dad', + borderCapStyle: 'round', + borderDash: [0], + borderDashOffset: 0.871, + borderJoinStyle: 'miter', + fill: 'start', + cubicInterpolationMode: 'monotone' + }); + + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [0, 0], + label: 'dataset1' + }], + labels: ['label1', 'label2'] + } + }); + + var options = chart.getDatasetMeta(0).dataset.options; + + expect(options.spanGaps).toBe(true); + expect(options.tension).toBe(0.231); + expect(options.backgroundColor).toBe('#add'); + expect(options.borderWidth).toBe('#daa'); + expect(options.borderColor).toBe('#dad'); + expect(options.borderCapStyle).toBe('round'); + expect(options.borderDash).toEqual([0]); + expect(options.borderDashOffset).toBe(0.871); + expect(options.borderJoinStyle).toBe('miter'); + expect(options.fill).toBe('start'); + expect(options.cubicInterpolationMode).toBe('monotone'); + }); + + it('should be overridden by user-supplied values', function() { + Chart.helpers.merge(Chart.defaults.datasets.line, { + spanGaps: true, + tension: 0.231 + }); + + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [0, 0], + label: 'dataset1', + spanGaps: true, + backgroundColor: '#dad' + }], + labels: ['label1', 'label2'] + }, + options: { + datasets: { + line: { + tension: 0.345, + backgroundColor: '#add' + } + } + } + }); + + var options = chart.getDatasetMeta(0).dataset.options; + + // dataset-level option overrides global default + expect(options.spanGaps).toBe(true); + // chart-level default overrides global default + expect(options.tension).toBe(0.345); + // dataset-level option overrides chart-level default + expect(options.backgroundColor).toBe('#dad'); + }); + }); + + it('should obey the chart-level dataset options', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [0, 0], + label: 'dataset1' + }], + labels: ['label1', 'label2'] + }, + options: { + datasets: { + line: { + spanGaps: true, + tension: 0.231, + backgroundColor: '#add', + borderWidth: '#daa', + borderColor: '#dad', + borderCapStyle: 'round', + borderDash: [0], + borderDashOffset: 0.871, + borderJoinStyle: 'miter', + fill: 'start', + cubicInterpolationMode: 'monotone' + } + } + } + }); + + var options = chart.getDatasetMeta(0).dataset.options; + + expect(options.spanGaps).toBe(true); + expect(options.tension).toBe(0.231); + expect(options.backgroundColor).toBe('#add'); + expect(options.borderWidth).toBe('#daa'); + expect(options.borderColor).toBe('#dad'); + expect(options.borderCapStyle).toBe('round'); + expect(options.borderDash).toEqual([0]); + expect(options.borderDashOffset).toBe(0.871); + expect(options.borderJoinStyle).toBe('miter'); + expect(options.fill).toBe('start'); + expect(options.cubicInterpolationMode).toBe('monotone'); + }); + + it('should obey the dataset options', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [0, 0], + label: 'dataset1', + spanGaps: true, + tension: 0.231, + backgroundColor: '#add', + borderWidth: '#daa', + borderColor: '#dad', + borderCapStyle: 'round', + borderDash: [0], + borderDashOffset: 0.871, + borderJoinStyle: 'miter', + fill: 'start', + cubicInterpolationMode: 'monotone' + }], + labels: ['label1', 'label2'] + } + }); + + var options = chart.getDatasetMeta(0).dataset.options; + + expect(options.spanGaps).toBe(true); + expect(options.tension).toBe(0.231); + expect(options.backgroundColor).toBe('#add'); + expect(options.borderWidth).toBe('#daa'); + expect(options.borderColor).toBe('#dad'); + expect(options.borderCapStyle).toBe('round'); + expect(options.borderDash).toEqual([0]); + expect(options.borderDashOffset).toBe(0.871); + expect(options.borderJoinStyle).toBe('miter'); + expect(options.fill).toBe('start'); + expect(options.cubicInterpolationMode).toBe('monotone'); + }); + + it('should handle number of data point changes in update', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [10, 15, 0, -4], + label: 'dataset1', + }], + labels: ['label1', 'label2', 'label3', 'label4'] + } + }); + + var meta = chart.getDatasetMeta(0); + + chart.data.datasets[0].data = [1, 2]; // remove 2 items + chart.update(); + expect(meta.data.length).toBe(2); + expect(meta.data[0] instanceof Chart.elements.PointElement).toBe(true); + expect(meta.data[1] instanceof Chart.elements.PointElement).toBe(true); + + chart.data.datasets[0].data = [1, 2, 3, 4, 5]; // add 3 items + chart.update(); + expect(meta.data.length).toBe(5); + expect(meta.data[0] instanceof Chart.elements.PointElement).toBe(true); + expect(meta.data[1] instanceof Chart.elements.PointElement).toBe(true); + expect(meta.data[2] instanceof Chart.elements.PointElement).toBe(true); + expect(meta.data[3] instanceof Chart.elements.PointElement).toBe(true); + expect(meta.data[4] instanceof Chart.elements.PointElement).toBe(true); + }); + + describe('Interactions', function() { + beforeEach(function() { + this.chart = window.acquireChart({ + type: 'line', + data: { + labels: ['label1', 'label2', 'label3', 'label4'], + datasets: [{ + data: [10, 15, 0, -4] + }] + }, + options: { + scales: { + x: { + offset: true + } + }, + elements: { + point: { + backgroundColor: 'rgb(100, 150, 200)', + borderColor: 'rgb(50, 100, 150)', + borderWidth: 2, + radius: 3 + } + } + } + }); + }); + + it ('should handle default hover styles', async function() { + var chart = this.chart; + var point = chart.getDatasetMeta(0).data[0]; + + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + expect(point.options.backgroundColor).toBe('#3187DD'); + expect(point.options.borderColor).toBe('#175A9D'); + expect(point.options.borderWidth).toBe(1); + expect(point.options.radius).toBe(4); + + await jasmine.triggerMouseEvent(chart, 'mouseout', point); + expect(point.options.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(point.options.borderColor).toBe('rgb(50, 100, 150)'); + expect(point.options.borderWidth).toBe(2); + expect(point.options.radius).toBe(3); + }); + + it ('should handle hover styles defined via dataset properties', async function() { + var chart = this.chart; + var point = chart.getDatasetMeta(0).data[0]; + + Chart.helpers.merge(chart.data.datasets[0], { + hoverBackgroundColor: 'rgb(200, 100, 150)', + hoverBorderColor: 'rgb(150, 50, 100)', + hoverBorderWidth: 8.4, + hoverRadius: 4.2 + }); + + chart.update(); + + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + expect(point.options.backgroundColor).toBe('rgb(200, 100, 150)'); + expect(point.options.borderColor).toBe('rgb(150, 50, 100)'); + expect(point.options.borderWidth).toBe(8.4); + expect(point.options.radius).toBe(4.2); + + await jasmine.triggerMouseEvent(chart, 'mouseout', point); + expect(point.options.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(point.options.borderColor).toBe('rgb(50, 100, 150)'); + expect(point.options.borderWidth).toBe(2); + expect(point.options.radius).toBe(3); + }); + + it('should handle hover styles defined via element options', async function() { + var chart = this.chart; + var point = chart.getDatasetMeta(0).data[0]; + + Chart.helpers.merge(chart.options.elements.point, { + hoverBackgroundColor: 'rgb(200, 100, 150)', + hoverBorderColor: 'rgb(150, 50, 100)', + hoverBorderWidth: 8.4, + hoverRadius: 4.2 + }); + + chart.update(); + + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + expect(point.options.backgroundColor).toBe('rgb(200, 100, 150)'); + expect(point.options.borderColor).toBe('rgb(150, 50, 100)'); + expect(point.options.borderWidth).toBe(8.4); + expect(point.options.radius).toBe(4.2); + + await jasmine.triggerMouseEvent(chart, 'mouseout', point); + expect(point.options.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(point.options.borderColor).toBe('rgb(50, 100, 150)'); + expect(point.options.borderWidth).toBe(2); + expect(point.options.radius).toBe(3); + }); + + it('should handle dataset hover styles defined via dataset properties', async function() { + var chart = this.chart; + var point = chart.getDatasetMeta(0).data[0]; + var dataset = chart.getDatasetMeta(0).dataset; + + Chart.helpers.merge(chart.data.datasets[0], { + backgroundColor: '#AAA', + borderColor: '#BBB', + borderWidth: 6, + hoverBackgroundColor: '#000', + hoverBorderColor: '#111', + hoverBorderWidth: 12 + }); + + chart.options.hover = {mode: 'dataset'}; + chart.update(); + + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + expect(dataset.options.backgroundColor).toBe('#000'); + expect(dataset.options.borderColor).toBe('#111'); + expect(dataset.options.borderWidth).toBe(12); + + await jasmine.triggerMouseEvent(chart, 'mouseout', point); + expect(dataset.options.backgroundColor).toBe('#AAA'); + expect(dataset.options.borderColor).toBe('#BBB'); + expect(dataset.options.borderWidth).toBe(6); + }); + }); + + it('should allow 0 as a point border width', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [10, 15, 0, -4], + label: 'dataset1', + pointBorderWidth: 0 + }], + labels: ['label1', 'label2', 'label3', 'label4'] + } + }); + + var meta = chart.getDatasetMeta(0); + var point = meta.data[0]; + + expect(point.options.borderWidth).toBe(0); + }); + + it('should allow an array as the point border width setting', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [10, 15, 0, -4], + label: 'dataset1', + pointBorderWidth: [1, 2, 3, 4] + }], + labels: ['label1', 'label2', 'label3', 'label4'] + } + }); + + var meta = chart.getDatasetMeta(0); + expect(meta.data[0].options.borderWidth).toBe(1); + expect(meta.data[1].options.borderWidth).toBe(2); + expect(meta.data[2].options.borderWidth).toBe(3); + expect(meta.data[3].options.borderWidth).toBe(4); + }); + + it('should render a million points', function() { + var data = []; + for (let x = 0; x < 1e6; x++) { + data.push({x, y: Math.sin(x / 10000)}); + } + function createChart() { + window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data, + borderWidth: 1, + radius: 0 + }], + }, + options: { + scales: { + x: {type: 'linear'}, + y: {type: 'linear'} + } + } + }); + } + expect(createChart).not.toThrow(); + }); + + it('should set skipped points to the reset state', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [10, null, 0, -4], + label: 'dataset1', + pointBorderWidth: [1, 2, 3, 4] + }], + labels: ['label1', 'label2', 'label3', 'label4'] + } + }); + + var meta = chart.getDatasetMeta(0); + var point = meta.data[1]; + var {x, y} = point.getProps(['x', 'y'], true); + expect(point.skip).toBe(true); + expect(isNaN(x)).toBe(false); + expect(isNaN(y)).toBe(false); + }); + + it('should honor spangap interval forwards', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + spanGaps: 10, + data: [{x: 10, y: 123}, {x: 15, y: 124}, {x: 26, y: 125}, {x: 30, y: 126}, {x: 35, y: 127}], + label: 'dataset1', + }], + }, + options: { + scales: { + x: { + type: 'linear', + } + } + } + }); + + var meta = chart.getDatasetMeta(0); + for (var i = 0; i < meta.data.length; ++i) { + var point = meta.data[i]; + expect(point.stop).toBe(i === 2); + } + }); + + it('should honor spangap interval backwards', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + spanGaps: 10, + data: [{x: 35, y: 123}, {x: 30, y: 124}, {x: 26, y: 125}, {x: 15, y: 126}, {x: 10, y: 127}], + label: 'dataset1', + }], + }, + options: { + scales: { + x: { + type: 'linear', + } + } + } + }); + + var meta = chart.getDatasetMeta(0); + for (var i = 0; i < meta.data.length; ++i) { + var point = meta.data[i]; + expect(point.stop).toBe(i === 3); + } + }); + + it('should correctly calc visible points on update', async() => { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [ + {x: 10, y: 20}, + {x: 15, y: 19}, + ] + }], + }, + options: { + scales: { + y: { + type: 'linear', + min: 0, + max: 25, + }, + x: { + type: 'linear', + min: 0, + max: 50 + }, + } + } + }); + + chart.data.datasets[0].data = [ + {x: 10, y: 20}, + {x: 15, y: 19}, + {x: 17, y: 12}, + {x: 50, y: 9}, + {x: 50, y: 9}, + {x: 50, y: 9}, + {x: 51, y: 9}, + {x: 52, y: 9}, + {x: 52, y: 9}, + ]; + chart.update(); + + var point = chart.getDatasetMeta(0).data[0]; + var event = { + type: 'mousemove', + native: true, + ...point + }; + + chart._handleEvent(event, false, true); + + const visiblePoints = chart.getSortedVisibleDatasetMetas()[0].data.filter(_ => !_.skip); + + expect(visiblePoints.length).toBe(6); + }, 500); + + it('should correctly calc _drawStart and _drawCount when first points beyond scale limits are null and spanGaps=true', async() => { + var chart = window.acquireChart({ + type: 'line', + data: { + labels: [0, 10, 20, 30, 40, 50], + datasets: [{ + data: [3, null, 2, 3, null, 1.5], + spanGaps: true, + tension: 0.4 + }] + }, + options: { + scales: { + x: { + type: 'linear', + min: 11, + max: 40, + } + } + } + }); + + chart.update(); + var controller = chart.getDatasetMeta(0).controller; + + expect(controller._drawStart).toBe(0); + expect(controller._drawCount).toBe(6); + }, 500); + + it('should correctly calc _drawStart and _drawCount when all points beyond scale limits are null and spanGaps=true', async() => { + var chart = window.acquireChart({ + type: 'line', + data: { + labels: [0, 10, 20, 30, 40, 50], + datasets: [{ + data: [null, null, 2, 3, null, null], + spanGaps: true, + tension: 0.4 + }] + }, + options: { + scales: { + x: { + type: 'linear', + min: 11, + max: 40, + } + } + } + }); + + chart.update(); + var controller = chart.getDatasetMeta(0).controller; + + expect(controller._drawStart).toBe(1); + expect(controller._drawCount).toBe(4); + }, 500); + + it('should correctly calc _drawStart and _drawCount when spanGaps=false', async() => { + var chart = window.acquireChart({ + type: 'line', + data: { + labels: [0, 10, 20, 30, 40, 50], + datasets: [{ + data: [3, null, 2, 3, null, 1.5], + spanGaps: false, + tension: 0.4 + }] + }, + options: { + scales: { + x: { + type: 'linear', + min: 11, + max: 40, + } + } + } + }); + + chart.update(); + var controller = chart.getDatasetMeta(0).controller; + + expect(controller._drawStart).toBe(1); + expect(controller._drawCount).toBe(4); + }, 500); + + it('should not override tooltip title and label callbacks', async() => { + const chart = window.acquireChart({ + type: 'line', + data: { + labels: ['Label 1', 'Label 2'], + datasets: [{ + data: [21, 79], + label: 'Dataset 1' + }, { + data: [33, 67], + label: 'Dataset 2' + }] + }, + options: { + responsive: true, + maintainAspectRatio: true, + } + }); + const {tooltip} = chart; + const point = chart.getDatasetMeta(0).data[0]; + + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + + expect(tooltip.title).toEqual(['Label 1']); + expect(tooltip.body).toEqual([{ + before: [], + lines: ['Dataset 1: 21'], + after: [] + }]); + + chart.options.plugins.tooltip = {mode: 'dataset'}; + chart.update(); + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + + expect(tooltip.title).toEqual(['Dataset 1']); + expect(tooltip.body).toEqual([{ + before: [], + lines: ['Label 1: 21'], + after: [] + }, { + before: [], + lines: ['Label 2: 79'], + after: [] + }]); + }); +}); diff --git a/test/specs/controller.polarArea.tests.js b/test/specs/controller.polarArea.tests.js new file mode 100644 index 00000000000..394cdb57340 --- /dev/null +++ b/test/specs/controller.polarArea.tests.js @@ -0,0 +1,396 @@ +describe('Chart.controllers.polarArea', function() { + describe('auto', jasmine.fixture.specs('controller.polarArea')); + + it('should update the scale correctly when data visibility is changed', function() { + var expectedScaleMax = 1; + var chart = window.acquireChart({ + type: 'polarArea', + data: { + datasets: [ + {data: [100]} + ], + labels: ['x'] + } + }); + + chart.toggleDataVisibility(0); + chart.update(); + + expect(chart.scales.r.max).toBe(expectedScaleMax); + }); + + it('should be registered as dataset controller', function() { + expect(typeof Chart.controllers.polarArea).toBe('function'); + }); + + it('should be constructed', function() { + var chart = window.acquireChart({ + type: 'polarArea', + data: { + datasets: [ + {data: []}, + {data: []} + ], + labels: [] + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.type).toEqual('polarArea'); + expect(meta.data).toEqual([]); + expect(meta.hidden).toBe(null); + expect(meta.controller).not.toBe(undefined); + expect(meta.controller.index).toBe(1); + + meta.controller.updateIndex(0); + expect(meta.controller.index).toBe(0); + }); + + it('should create arc elements for each data item during initialization', function() { + var chart = window.acquireChart({ + type: 'polarArea', + data: { + datasets: [ + {data: []}, + {data: [10, 15, 0, -4]} + ], + labels: [] + } + }); + + var meta = chart.getDatasetMeta(1); + expect(meta.data.length).toBe(4); // 4 arcs created + expect(meta.data[0] instanceof Chart.elements.ArcElement).toBe(true); + expect(meta.data[1] instanceof Chart.elements.ArcElement).toBe(true); + expect(meta.data[2] instanceof Chart.elements.ArcElement).toBe(true); + expect(meta.data[3] instanceof Chart.elements.ArcElement).toBe(true); + }); + + it('should draw all elements', function() { + var chart = window.acquireChart({ + type: 'polarArea', + data: { + datasets: [{ + data: [10, 15, 0, -4], + label: 'dataset2' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + } + }); + + var meta = chart.getDatasetMeta(0); + + spyOn(meta.data[0], 'draw'); + spyOn(meta.data[1], 'draw'); + spyOn(meta.data[2], 'draw'); + spyOn(meta.data[3], 'draw'); + + chart.update(); + + expect(meta.data[0].draw.calls.count()).toBe(1); + expect(meta.data[1].draw.calls.count()).toBe(1); + expect(meta.data[2].draw.calls.count()).toBe(1); + expect(meta.data[3].draw.calls.count()).toBe(1); + }); + + it('should update elements when modifying data', function() { + var chart = window.acquireChart({ + type: 'polarArea', + data: { + datasets: [{ + data: [10, 15, 0, -4], + label: 'dataset2' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + showLine: true, + plugins: { + legend: false, + title: false + }, + elements: { + arc: { + backgroundColor: 'rgb(255, 0, 0)', + borderColor: 'rgb(0, 255, 0)', + borderWidth: 1.2 + } + } + } + }); + + var meta = chart.getDatasetMeta(0); + expect(meta.data.length).toBe(4); + + [ + {o: 174, s: -0.5 * Math.PI, e: 0}, + {o: 236, s: 0, e: 0.5 * Math.PI}, + {o: 51, s: 0.5 * Math.PI, e: Math.PI}, + {o: 0, s: Math.PI, e: 1.5 * Math.PI} + ].forEach(function(expected, i) { + expect(meta.data[i].x).withContext(i).toBeCloseToPixel(256); + expect(meta.data[i].y).withContext(i).toBeCloseToPixel(256); + expect(meta.data[i].innerRadius).withContext(i).toBeCloseToPixel(0); + expect(meta.data[i].outerRadius).withContext(i).toBeCloseToPixel(expected.o); + expect(meta.data[i].startAngle).withContext(i).toBe(expected.s); + expect(meta.data[i].endAngle).withContext(i).toBe(expected.e); + expect(meta.data[i].options).withContext(i).toEqual(jasmine.objectContaining({ + backgroundColor: 'rgb(255, 0, 0)', + borderColor: 'rgb(0, 255, 0)', + borderWidth: 1.2 + })); + }); + + // arc styles + chart.data.datasets[0].backgroundColor = 'rgb(128, 129, 130)'; + chart.data.datasets[0].borderColor = 'rgb(56, 57, 58)'; + chart.data.datasets[0].borderWidth = 1.123; + + chart.update(); + + for (var i = 0; i < 4; ++i) { + expect(meta.data[i].options.backgroundColor).toBe('rgb(128, 129, 130)'); + expect(meta.data[i].options.borderColor).toBe('rgb(56, 57, 58)'); + expect(meta.data[i].options.borderWidth).toBe(1.123); + } + + chart.update(); + + expect(meta.data[0].x).toBeCloseToPixel(256); + expect(meta.data[0].y).toBeCloseToPixel(256); + expect(meta.data[0].innerRadius).toBeCloseToPixel(0); + expect(meta.data[0].outerRadius).toBeCloseToPixel(174); + }); + + it('should update elements with start angle from options', function() { + var chart = window.acquireChart({ + type: 'polarArea', + data: { + datasets: [{ + data: [10, 15, 0, -4], + label: 'dataset2' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + showLine: true, + plugins: { + legend: false, + title: false, + }, + scales: { + r: { + startAngle: 90, // default is 0 + } + }, + elements: { + arc: { + backgroundColor: 'rgb(255, 0, 0)', + borderColor: 'rgb(0, 255, 0)', + borderWidth: 1.2 + } + } + } + }); + + var meta = chart.getDatasetMeta(0); + expect(meta.data.length).toBe(4); + + [ + {o: 174, s: 0, e: 0.5 * Math.PI}, + {o: 236, s: 0.5 * Math.PI, e: Math.PI}, + {o: 51, s: Math.PI, e: 1.5 * Math.PI}, + {o: 0, s: 1.5 * Math.PI, e: 2.0 * Math.PI} + ].forEach(function(expected, i) { + expect(meta.data[i].x).withContext(i).toBeCloseToPixel(256); + expect(meta.data[i].y).withContext(i).toBeCloseToPixel(256); + expect(meta.data[i].innerRadius).withContext(i).toBeCloseToPixel(0); + expect(meta.data[i].outerRadius).withContext(i).toBeCloseToPixel(expected.o); + expect(meta.data[i].startAngle).withContext(i).toBe(expected.s); + expect(meta.data[i].endAngle).withContext(i).toBe(expected.e); + expect(meta.data[i].options).withContext(i).toEqual(jasmine.objectContaining({ + backgroundColor: 'rgb(255, 0, 0)', + borderColor: 'rgb(0, 255, 0)', + borderWidth: 1.2 + })); + }); + }); + + it('should handle number of data point changes in update', function() { + var chart = window.acquireChart({ + type: 'polarArea', + data: { + datasets: [{ + data: [10, 15, 0, -4], + label: 'dataset2' + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + showLine: true, + elements: { + arc: { + backgroundColor: 'rgb(255, 0, 0)', + borderColor: 'rgb(0, 255, 0)', + borderWidth: 1.2 + } + } + } + }); + + var meta = chart.getDatasetMeta(0); + expect(meta.data.length).toBe(4); + + // remove 2 items + chart.data.labels = ['label1', 'label2']; + chart.data.datasets[0].data = [1, 2]; + chart.update(); + + expect(meta.data.length).toBe(2); + expect(meta.data[0] instanceof Chart.elements.ArcElement).toBe(true); + expect(meta.data[1] instanceof Chart.elements.ArcElement).toBe(true); + + // add 3 items + chart.data.labels = ['label1', 'label2', 'label3', 'label4', 'label5']; + chart.data.datasets[0].data = [1, 2, 3, 4, 5]; + chart.update(); + + expect(meta.data.length).toBe(5); + expect(meta.data[0] instanceof Chart.elements.ArcElement).toBe(true); + expect(meta.data[1] instanceof Chart.elements.ArcElement).toBe(true); + expect(meta.data[2] instanceof Chart.elements.ArcElement).toBe(true); + expect(meta.data[3] instanceof Chart.elements.ArcElement).toBe(true); + expect(meta.data[4] instanceof Chart.elements.ArcElement).toBe(true); + }); + + describe('Interactions', function() { + beforeEach(function() { + this.chart = window.acquireChart({ + type: 'polarArea', + data: { + labels: ['label1', 'label2', 'label3', 'label4'], + datasets: [{ + data: [10, 15, 0, 4] + }] + }, + options: { + cutoutPercentage: 0, + elements: { + arc: { + backgroundColor: 'rgb(100, 150, 200)', + borderColor: 'rgb(50, 100, 150)', + borderWidth: 2, + } + } + } + }); + }); + + it('should handle default hover styles', async function() { + var chart = this.chart; + var arc = chart.getDatasetMeta(0).data[0]; + + await jasmine.triggerMouseEvent(chart, 'mousemove', arc); + expect(arc.options.backgroundColor).toBe('#3187DD'); + expect(arc.options.borderColor).toBe('#175A9D'); + expect(arc.options.borderWidth).toBe(2); + + await jasmine.triggerMouseEvent(chart, 'mouseout', arc); + expect(arc.options.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(arc.options.borderColor).toBe('rgb(50, 100, 150)'); + expect(arc.options.borderWidth).toBe(2); + }); + + it('should handle hover styles defined via dataset properties', async function() { + var chart = this.chart; + var arc = chart.getDatasetMeta(0).data[0]; + + Chart.helpers.merge(chart.data.datasets[0], { + hoverBackgroundColor: 'rgb(200, 100, 150)', + hoverBorderColor: 'rgb(150, 50, 100)', + hoverBorderWidth: 8.4, + }); + + chart.update(); + + await jasmine.triggerMouseEvent(chart, 'mousemove', arc); + expect(arc.options.backgroundColor).toBe('rgb(200, 100, 150)'); + expect(arc.options.borderColor).toBe('rgb(150, 50, 100)'); + expect(arc.options.borderWidth).toBe(8.4); + + await jasmine.triggerMouseEvent(chart, 'mouseout', arc); + expect(arc.options.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(arc.options.borderColor).toBe('rgb(50, 100, 150)'); + expect(arc.options.borderWidth).toBe(2); + }); + + it('should handle hover styles defined via element options', async function() { + var chart = this.chart; + var arc = chart.getDatasetMeta(0).data[0]; + + Chart.helpers.merge(chart.options.elements.arc, { + hoverBackgroundColor: 'rgb(200, 100, 150)', + hoverBorderColor: 'rgb(150, 50, 100)', + hoverBorderWidth: 8.4, + }); + + chart.update(); + + await jasmine.triggerMouseEvent(chart, 'mousemove', arc); + expect(arc.options.backgroundColor).toBe('rgb(200, 100, 150)'); + expect(arc.options.borderColor).toBe('rgb(150, 50, 100)'); + expect(arc.options.borderWidth).toBe(8.4); + + await jasmine.triggerMouseEvent(chart, 'mouseout', arc); + expect(arc.options.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(arc.options.borderColor).toBe('rgb(50, 100, 150)'); + expect(arc.options.borderWidth).toBe(2); + }); + }); + + it('should not override tooltip title and label callbacks', async() => { + const chart = window.acquireChart({ + type: 'polarArea', + data: { + labels: ['Label 1', 'Label 2'], + datasets: [{ + data: [21, 79], + label: 'Dataset 1' + }, { + data: [33, 67], + label: 'Dataset 2' + }] + }, + options: { + responsive: true, + maintainAspectRatio: true, + } + }); + const {tooltip} = chart; + const point = chart.getDatasetMeta(0).data[0]; + + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + + expect(tooltip.title).toEqual(['Label 1']); + expect(tooltip.body).toEqual([{ + before: [], + lines: ['Dataset 1: 21'], + after: [] + }]); + + chart.options.plugins.tooltip = {mode: 'dataset'}; + chart.update(); + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + + expect(tooltip.title).toEqual(['Dataset 1']); + expect(tooltip.body).toEqual([{ + before: [], + lines: ['Label 1: 21'], + after: [] + }, { + before: [], + lines: ['Label 2: 79'], + after: [] + }]); + }); +}); diff --git a/test/specs/controller.radar.tests.js b/test/specs/controller.radar.tests.js new file mode 100644 index 00000000000..154b548219d --- /dev/null +++ b/test/specs/controller.radar.tests.js @@ -0,0 +1,455 @@ +describe('Chart.controllers.radar', function() { + describe('auto', jasmine.fixture.specs('controller.radar')); + + it('should be registered as dataset controller', function() { + expect(typeof Chart.controllers.radar).toBe('function'); + }); + + it('Should be constructed', function() { + var chart = window.acquireChart({ + type: 'radar', + data: { + datasets: [{ + data: [] + }], + labels: [] + } + }); + + var meta = chart.getDatasetMeta(0); + expect(meta.type).toBe('radar'); + expect(meta.controller).not.toBe(undefined); + expect(meta.controller.index).toBe(0); + expect(meta.data).toEqual([]); + + meta.controller.updateIndex(1); + expect(meta.controller.index).toBe(1); + }); + + it('Should create arc elements for each data item during initialization', function() { + var chart = window.acquireChart({ + type: 'radar', + data: { + datasets: [{ + data: [10, 15, 0, 4] + }], + labels: ['label1', 'label2', 'label3', 'label4'] + } + }); + + var meta = chart.getDatasetMeta(0); + expect(meta.dataset instanceof Chart.elements.LineElement).toBe(true); // line element + expect(meta.data.length).toBe(4); // 4 points created + expect(meta.data[0] instanceof Chart.elements.PointElement).toBe(true); + expect(meta.data[1] instanceof Chart.elements.PointElement).toBe(true); + expect(meta.data[2] instanceof Chart.elements.PointElement).toBe(true); + expect(meta.data[3] instanceof Chart.elements.PointElement).toBe(true); + }); + + it('should draw all elements', function() { + var chart = window.acquireChart({ + type: 'radar', + data: { + datasets: [{ + data: [10, 15, 0, 4] + }], + labels: ['label1', 'label2', 'label3', 'label4'] + } + }); + + var meta = chart.getDatasetMeta(0); + + spyOn(meta.dataset, 'draw'); + spyOn(meta.data[0], 'draw'); + spyOn(meta.data[1], 'draw'); + spyOn(meta.data[2], 'draw'); + spyOn(meta.data[3], 'draw'); + + chart.update(); + + expect(meta.dataset.draw.calls.count()).toBe(1); + expect(meta.data[0].draw.calls.count()).toBe(1); + expect(meta.data[1].draw.calls.count()).toBe(1); + expect(meta.data[2].draw.calls.count()).toBe(1); + expect(meta.data[3].draw.calls.count()).toBe(1); + }); + + it('should draw all elements with object notation and default key', function() { + var chart = window.acquireChart({ + type: 'radar', + data: { + datasets: [{ + data: [{r: 10}, {r: 20}, {r: 15}] + }], + labels: ['label1', 'label2', 'label3'] + } + }); + + var meta = chart.getDatasetMeta(0); + + spyOn(meta.dataset, 'draw'); + spyOn(meta.data[0], 'draw'); + spyOn(meta.data[1], 'draw'); + spyOn(meta.data[2], 'draw'); + + chart.update(); + + expect(meta.dataset.draw.calls.count()).toBe(1); + expect(meta.data[0].draw.calls.count()).toBe(1); + expect(meta.data[1].draw.calls.count()).toBe(1); + expect(meta.data[2].draw.calls.count()).toBe(1); + }); + + it('should update elements', function() { + var chart = window.acquireChart({ + type: 'radar', + data: { + datasets: [{ + data: [10, 15, 0, 4] + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + showLine: true, + plugins: { + legend: false, + title: false, + }, + elements: { + line: { + backgroundColor: 'rgb(255, 0, 0)', + borderCapStyle: 'round', + borderColor: 'rgb(0, 255, 0)', + borderDash: [], + borderDashOffset: 0.1, + borderJoinStyle: 'bevel', + borderWidth: 1.2, + fill: true, + tension: 0.1, + }, + point: { + backgroundColor: Chart.defaults.backgroundColor, + borderWidth: 1, + borderColor: Chart.defaults.borderColor, + hitRadius: 1, + hoverRadius: 4, + hoverBorderWidth: 1, + radius: 3, + pointStyle: 'circle' + } + } + } + }); + + var meta = chart.getDatasetMeta(0); + + chart.reset(); // reset first + + // Line element + expect(meta.dataset.options).toEqual(jasmine.objectContaining({ + backgroundColor: 'rgb(255, 0, 0)', + borderCapStyle: 'round', + borderColor: 'rgb(0, 255, 0)', + borderDash: [], + borderDashOffset: 0.1, + borderJoinStyle: 'bevel', + borderWidth: 1.2, + fill: true, + tension: 0.1, + })); + + [ + {x: 256, y: 256}, + {x: 256, y: 256}, + {x: 256, y: 256}, + {x: 256, y: 256}, + ].forEach(function(expected, i) { + expect(meta.data[i].x).withContext(i).toBeCloseToPixel(expected.x); + expect(meta.data[i].y).withContext(i).toBeCloseToPixel(expected.y); + expect(meta.data[i].options).withContext(i).toEqual(jasmine.objectContaining({ + backgroundColor: Chart.defaults.backgroundColor, + borderWidth: 1, + borderColor: Chart.defaults.borderColor, + hitRadius: 1, + radius: 3, + pointStyle: 'circle', + })); + }); + + chart.update(); + + [ + {x: 256, y: 122, cppx: 246, cppy: 122, cpnx: 272, cpny: 122}, + {x: 457, y: 256, cppx: 457, cppy: 249, cpnx: 457, cpny: 262}, + {x: 256, y: 256, cppx: 277, cppy: 256, cpnx: 250, cpny: 256}, + {x: 202, y: 256, cppx: 202, cppy: 260, cpnx: 202, cpny: 246}, + ].forEach(function(expected, i) { + expect(meta.data[i].x).withContext(i).toBeCloseToPixel(expected.x); + expect(meta.data[i].y).withContext(i).toBeCloseToPixel(expected.y); + expect(meta.data[i].cp1x).withContext(i).toBeCloseToPixel(expected.cppx); + expect(meta.data[i].cp1y).withContext(i).toBeCloseToPixel(expected.cppy); + expect(meta.data[i].cp2x).withContext(i).toBeCloseToPixel(expected.cpnx); + expect(meta.data[i].cp2y).withContext(i).toBeCloseToPixel(expected.cpny); + expect(meta.data[i].options).withContext(i).toEqual(jasmine.objectContaining({ + backgroundColor: Chart.defaults.backgroundColor, + borderWidth: 1, + borderColor: Chart.defaults.borderColor, + hitRadius: 1, + radius: 3, + pointStyle: 'circle', + })); + }); + + // Use dataset level styles for lines & points + chart.data.datasets[0].tension = 0; + chart.data.datasets[0].backgroundColor = 'rgb(98, 98, 98)'; + chart.data.datasets[0].borderColor = 'rgb(8, 8, 8)'; + chart.data.datasets[0].borderWidth = 0.55; + chart.data.datasets[0].borderCapStyle = 'butt'; + chart.data.datasets[0].borderDash = [2, 3]; + chart.data.datasets[0].borderDashOffset = 7; + chart.data.datasets[0].borderJoinStyle = 'miter'; + chart.data.datasets[0].fill = false; + + // point styles + chart.data.datasets[0].pointRadius = 22; + chart.data.datasets[0].hitRadius = 3.3; + chart.data.datasets[0].pointBackgroundColor = 'rgb(128, 129, 130)'; + chart.data.datasets[0].pointBorderColor = 'rgb(56, 57, 58)'; + chart.data.datasets[0].pointBorderWidth = 1.123; + + chart.update(); + + expect(meta.dataset.options).toEqual(jasmine.objectContaining({ + backgroundColor: 'rgb(98, 98, 98)', + borderCapStyle: 'butt', + borderColor: 'rgb(8, 8, 8)', + borderDash: [2, 3], + borderDashOffset: 7, + borderJoinStyle: 'miter', + borderWidth: 0.55, + fill: false, + tension: 0, + })); + + // Since tension is now 0, we don't care about the control points + [ + {x: 256, y: 122}, + {x: 457, y: 256}, + {x: 256, y: 256}, + {x: 202, y: 256}, + ].forEach(function(expected, i) { + expect(meta.data[i].x).withContext(i).toBeCloseToPixel(expected.x); + expect(meta.data[i].y).withContext(i).toBeCloseToPixel(expected.y); + expect(meta.data[i].options).withContext(i).toEqual(jasmine.objectContaining({ + backgroundColor: 'rgb(128, 129, 130)', + borderWidth: 1.123, + borderColor: 'rgb(56, 57, 58)', + hitRadius: 3.3, + radius: 22, + pointStyle: 'circle' + })); + }); + }); + + describe('Interactions', function() { + beforeEach(function() { + this.chart = window.acquireChart({ + type: 'radar', + data: { + labels: ['label1', 'label2', 'label3', 'label4'], + datasets: [{ + data: [10, 15, 0, 4] + }] + }, + options: { + elements: { + point: { + backgroundColor: 'rgb(100, 150, 200)', + borderColor: 'rgb(50, 100, 150)', + borderWidth: 2, + radius: 3 + } + } + } + }); + }); + + it('should handle default hover styles', async function() { + var chart = this.chart; + var point = chart.getDatasetMeta(0).data[0]; + + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + expect(point.options.backgroundColor).toBe('#3187DD'); + expect(point.options.borderColor).toBe('#175A9D'); + expect(point.options.borderWidth).toBe(1); + expect(point.options.radius).toBe(4); + + await jasmine.triggerMouseEvent(chart, 'mouseout', point); + expect(point.options.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(point.options.borderColor).toBe('rgb(50, 100, 150)'); + expect(point.options.borderWidth).toBe(2); + expect(point.options.radius).toBe(3); + }); + + it('should handle hover styles defined via dataset properties', async function() { + var chart = this.chart; + var point = chart.getDatasetMeta(0).data[0]; + + Chart.helpers.merge(chart.data.datasets[0], { + hoverBackgroundColor: 'rgb(200, 100, 150)', + hoverBorderColor: 'rgb(150, 50, 100)', + hoverBorderWidth: 8.4, + hoverRadius: 4.2 + }); + + chart.update(); + + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + expect(point.options.backgroundColor).toBe('rgb(200, 100, 150)'); + expect(point.options.borderColor).toBe('rgb(150, 50, 100)'); + expect(point.options.borderWidth).toBe(8.4); + expect(point.options.radius).toBe(4.2); + + await jasmine.triggerMouseEvent(chart, 'mouseout', point); + expect(point.options.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(point.options.borderColor).toBe('rgb(50, 100, 150)'); + expect(point.options.borderWidth).toBe(2); + expect(point.options.radius).toBe(3); + }); + + it('should handle hover styles defined via element options', async function() { + var chart = this.chart; + var point = chart.getDatasetMeta(0).data[0]; + + Chart.helpers.merge(chart.options.elements.point, { + hoverBackgroundColor: 'rgb(200, 100, 150)', + hoverBorderColor: 'rgb(150, 50, 100)', + hoverBorderWidth: 8.4, + hoverRadius: 4.2 + }); + + chart.update(); + + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + expect(point.options.backgroundColor).toBe('rgb(200, 100, 150)'); + expect(point.options.borderColor).toBe('rgb(150, 50, 100)'); + expect(point.options.borderWidth).toBe(8.4); + expect(point.options.radius).toBe(4.2); + + await jasmine.triggerMouseEvent(chart, 'mouseout', point); + expect(point.options.backgroundColor).toBe('rgb(100, 150, 200)'); + expect(point.options.borderColor).toBe('rgb(50, 100, 150)'); + expect(point.options.borderWidth).toBe(2); + expect(point.options.radius).toBe(3); + }); + }); + + it('should allow pointBorderWidth to be set to 0', function() { + var chart = window.acquireChart({ + type: 'radar', + data: { + datasets: [{ + data: [10, 15, 0, 4], + pointBorderWidth: 0 + }], + labels: ['label1', 'label2', 'label3', 'label4'] + } + }); + + var meta = chart.getDatasetMeta(0); + var point = meta.data[0]; + expect(point.options.borderWidth).toBe(0); + }); + + it('should use the pointRadius setting over the radius setting', function() { + var chart = window.acquireChart({ + type: 'radar', + data: { + datasets: [{ + data: [10, 15, 0, 4], + pointRadius: 10, + radius: 15, + }, { + data: [20, 20, 20, 20], + radius: 20 + }], + labels: ['label1', 'label2', 'label3', 'label4'] + } + }); + + var meta0 = chart.getDatasetMeta(0); + var meta1 = chart.getDatasetMeta(1); + expect(meta0.data[0].options.radius).toBe(10); + expect(meta1.data[0].options.radius).toBe(20); + }); + + it('should return id for value scale', function() { + var chart = window.acquireChart({ + type: 'radar', + data: { + datasets: [{ + data: [10, 15, 0, 4], + pointBorderWidth: 0 + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + scales: { + test: { + axis: 'r' + } + } + } + }); + + var meta = chart.getDatasetMeta(0); + expect(meta.vScale.id).toBe('test'); + }); + + it('should not override tooltip title and label callbacks', async() => { + const chart = window.acquireChart({ + type: 'radar', + data: { + labels: ['Label 1', 'Label 2'], + datasets: [{ + data: [21, 79], + label: 'Dataset 1' + }, { + data: [33, 67], + label: 'Dataset 2' + }] + }, + options: { + responsive: true, + maintainAspectRatio: true, + } + }); + const {tooltip} = chart; + const point = chart.getDatasetMeta(0).data[0]; + + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + + expect(tooltip.title).toEqual(['Label 1']); + expect(tooltip.body).toEqual([{ + before: [], + lines: ['Dataset 1: 21'], + after: [] + }]); + + chart.options.plugins.tooltip = {mode: 'dataset'}; + chart.update(); + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + + expect(tooltip.title).toEqual(['Dataset 1']); + expect(tooltip.body).toEqual([{ + before: [], + lines: ['Label 1: 21'], + after: [] + }, { + before: [], + lines: ['Label 2: 79'], + after: [] + }]); + }); +}); diff --git a/test/specs/controller.scatter.tests.js b/test/specs/controller.scatter.tests.js new file mode 100644 index 00000000000..b8bcbc297c4 --- /dev/null +++ b/test/specs/controller.scatter.tests.js @@ -0,0 +1,205 @@ +describe('Chart.controllers.scatter', function() { + describe('auto', jasmine.fixture.specs('controller.scatter')); + + it('should be registered as dataset controller', function() { + expect(typeof Chart.controllers.scatter).toBe('function'); + }); + + it('should only show a single point in the tooltip on multiple datasets', async function() { + var chart = window.acquireChart({ + type: 'scatter', + data: { + datasets: [{ + data: [{ + x: 10, + y: 15 + }, + { + x: 12, + y: 10 + }], + label: 'dataset1' + }, + { + data: [{ + x: 20, + y: 10 + }, + { + x: 4, + y: 8 + }], + label: 'dataset2' + }] + }, + options: {} + }); + var point = chart.getDatasetMeta(0).data[1]; + + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + expect(chart.tooltip.body.length).toEqual(1); + }); + + it('should not create line element by default', function() { + var chart = window.acquireChart({ + type: 'scatter', + data: { + datasets: [{ + data: [{ + x: 10, + y: 15 + }, + { + x: 12, + y: 10 + }], + label: 'dataset1' + }, + { + data: [{ + x: 20, + y: 10 + }, + { + x: 4, + y: 8 + }], + label: 'dataset2' + }] + }, + }); + + var meta = chart.getDatasetMeta(0); + expect(meta.dataset instanceof Chart.elements.LineElement).toBe(false); + }); + + it('should create line element if showline is true at datasets options', function() { + var chart = window.acquireChart({ + type: 'scatter', + data: { + datasets: [{ + showLine: true, + data: [{ + x: 10, + y: 15 + }, + { + x: 12, + y: 10 + }], + label: 'dataset1' + }, + { + data: [{ + x: 20, + y: 10 + }, + { + x: 4, + y: 8 + }], + label: 'dataset2' + }] + }, + }); + + var meta = chart.getDatasetMeta(0); + expect(meta.dataset instanceof Chart.elements.LineElement).toBe(true); + }); + + it('should create line element if showline is true at root options', function() { + var chart = window.acquireChart({ + type: 'scatter', + data: { + datasets: [{ + data: [{ + x: 10, + y: 15 + }, + { + x: 12, + y: 10 + }], + label: 'dataset1' + }, + { + data: [{ + x: 20, + y: 10 + }, + { + x: 4, + y: 8 + }], + label: 'dataset2' + }] + }, + options: { + showLine: true + } + }); + + var meta = chart.getDatasetMeta(0); + expect(meta.dataset instanceof Chart.elements.LineElement).toBe(true); + }); + + it('should not override tooltip title and label callbacks', async() => { + const chart = window.acquireChart({ + type: 'scatter', + data: { + labels: ['Label 1', 'Label 2'], + datasets: [{ + data: [{ + x: 10, + y: 15 + }, + { + x: 12, + y: 10 + }], + label: 'Dataset 1' + }, { + data: [{ + x: 20, + y: 10 + }, + { + x: 4, + y: 8 + }], + label: 'Dataset 2' + }] + }, + options: { + responsive: true, + maintainAspectRatio: true, + } + }); + const {tooltip} = chart; + const point = chart.getDatasetMeta(0).data[0]; + + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + + expect(tooltip.title).toEqual(['Label 1']); + expect(tooltip.body).toEqual([{ + before: [], + lines: ['Dataset 1: (10, 15)'], + after: [] + }]); + + chart.options.plugins.tooltip = {mode: 'dataset'}; + chart.update(); + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + + expect(tooltip.title).toEqual(['Dataset 1']); + expect(tooltip.body).toEqual([{ + before: [], + lines: ['Label 1: (10, 15)'], + after: [] + }, { + before: [], + lines: ['Label 2: (12, 10)'], + after: [] + }]); + }); +}); diff --git a/test/specs/core.animation.tests.js b/test/specs/core.animation.tests.js new file mode 100644 index 00000000000..ab4d7e4311c --- /dev/null +++ b/test/specs/core.animation.tests.js @@ -0,0 +1,86 @@ +describe('Chart.Animation', function() { + it('should animate boolean', function() { + const target = {prop: false}; + const anim = new Chart.Animation({duration: 1000}, target, 'prop', true); + expect(anim.active()).toBeTrue(); + + anim.tick(anim._start + 500); + expect(anim.active()).toBeTrue(); + expect(target.prop).toBeFalse(); + + anim.tick(anim._start + 501); + expect(anim.active()).toBeTrue(); + expect(target.prop).toBeTrue(); + + anim.tick(anim._start - 100); + expect(anim.active()).toBeTrue(); + expect(target.prop).toBeFalse(); + + anim.tick(anim._start + 1000); + expect(anim.active()).toBeFalse(); + expect(target.prop).toBeTrue(); + }); + + describe('color', function() { + it('should fall back to transparent', function() { + const target = {}; + const anim = new Chart.Animation({duration: 1000, type: 'color'}, target, 'color', 'red'); + anim._from = undefined; + anim.tick(anim._start + 500); + expect(target.color).toEqual('#FF000080'); + + anim._from = 'blue'; + anim._to = undefined; + anim.tick(anim._start + 500); + expect(target.color).toEqual('#0000FF80'); + }); + + it('should not try to mix invalid color', function() { + const target = {color: 'blue'}; + const anim = new Chart.Animation({duration: 1000, type: 'color'}, target, 'color', 'invalid'); + anim.tick(anim._start + 500); + expect(target.color).toEqual('invalid'); + }); + }); + + it('should loop', function() { + const target = {value: 0}; + const anim = new Chart.Animation({duration: 100, loop: true}, target, 'value', 10); + anim.tick(anim._start + 50); + expect(target.value).toEqual(5); + anim.tick(anim._start + 100); + expect(target.value).toEqual(10); + anim.tick(anim._start + 150); + expect(target.value).toEqual(5); + anim.tick(anim._start + 400); + expect(target.value).toEqual(0); + }); + + it('should update', function() { + const target = {testColor: 'transparent'}; + const anim = new Chart.Animation({duration: 100, type: 'color'}, target, 'testColor', 'red'); + + anim.tick(anim._start + 50); + expect(target.testColor).toEqual('#FF000080'); + + anim.update({duration: 500}, 'blue', Date.now()); + anim.tick(anim._start + 250); + expect(target.testColor).toEqual('#4000BFBF'); + + anim.tick(anim._start + 500); + expect(target.testColor).toEqual('blue'); + }); + + it('should not update when finished', function() { + const target = {testColor: 'transparent'}; + const anim = new Chart.Animation({duration: 100, type: 'color'}, target, 'testColor', 'red'); + + anim.tick(anim._start + 100); + expect(target.testColor).toEqual('red'); + expect(anim.active()).toBeFalse(); + + anim.update({duration: 500}, 'blue', Date.now()); + expect(anim._duration).toEqual(100); + expect(anim._to).toEqual('red'); + }); +}); diff --git a/test/specs/core.animations.tests.js b/test/specs/core.animations.tests.js new file mode 100644 index 00000000000..6fddb20445f --- /dev/null +++ b/test/specs/core.animations.tests.js @@ -0,0 +1,175 @@ +describe('Chart.animations', function() { + it('should override property collection with property', function() { + const chart = {}; + const anims = new Chart.Animations(chart, { + collection1: { + properties: ['property1', 'property2'], + duration: 1000 + }, + property2: { + duration: 2000 + } + }); + expect(anims._properties.get('property1')).toEqual(jasmine.objectContaining({duration: 1000})); + expect(anims._properties.get('property2')).toEqual(jasmine.objectContaining({duration: 2000})); + }); + + it('should ignore duplicate definitions from collections', function() { + const chart = {}; + const anims = new Chart.Animations(chart, { + collection1: { + properties: ['property1'], + duration: 1000 + }, + collection2: { + properties: ['property1', 'property2'], + duration: 2000 + } + }); + expect(anims._properties.get('property1')).toEqual(jasmine.objectContaining({duration: 1000})); + expect(anims._properties.get('property2')).toEqual(jasmine.objectContaining({duration: 2000})); + }); + + it('should not animate undefined options key', function() { + const chart = {}; + const anims = new Chart.Animations(chart, {value: {duration: 100}, option: {duration: 200}}); + const target = { + value: 1, + options: { + option: 2 + } + }; + expect(anims.update(target, { + options: undefined + })).toBeUndefined(); + }); + + it('should assign options directly, if target does not have previous options', function() { + const chart = {}; + const anims = new Chart.Animations(chart, {option: {duration: 200}}); + const target = {}; + expect(anims.update(target, {options: {option: 1}})).toBeUndefined(); + }); + + it('should clone the target options, if those are shared and new options are not', function() { + const chart = {options: {}}; + const anims = new Chart.Animations(chart, {option: {duration: 200}}); + const options = {option: 0, $shared: true}; + const target = {options}; + expect(anims.update(target, {options: {option: 1}})).toBeTrue(); + expect(target.options.$shared).not.toBeTrue(); + expect(target.options !== options).toBeTrue(); + }); + + it('should assign shared options to target after animations complete', function(done) { + const chart = { + draw: function() {}, + options: {} + }; + const anims = new Chart.Animations(chart, {value: {duration: 100}, option: {duration: 200}}); + + const target = { + value: 1, + options: { + option: 2 + } + }; + const sharedOpts = {option: 10, $shared: true}; + + expect(anims.update(target, { + options: sharedOpts + })).toBeTrue(); + + expect(target.options !== sharedOpts).toBeTrue(); + + Chart.animator.start(chart); + + setTimeout(function() { + expect(Chart.animator.running(chart)).toBeFalse(); + expect(target.options === sharedOpts).toBeTrue(); + + Chart.animator.remove(chart); + done(); + }, 300); + }); + + it('should not assign shared options to target when animations are cancelled', function(done) { + const chart = { + draw: function() {}, + options: {} + }; + const anims = new Chart.Animations(chart, {value: {duration: 100}, option: {duration: 200}}); + + const target = { + value: 1, + options: { + option: 2 + } + }; + const sharedOpts = {option: 10, $shared: true}; + + expect(anims.update(target, { + options: sharedOpts + })).toBeTrue(); + + expect(target.options !== sharedOpts).toBeTrue(); + + Chart.animator.start(chart); + + setTimeout(function() { + expect(Chart.animator.running(chart)).toBeTrue(); + Chart.animator.stop(chart); + expect(Chart.animator.running(chart)).toBeFalse(); + + setTimeout(function() { + expect(target.options === sharedOpts).toBeFalse(); + + Chart.animator.remove(chart); + done(); + }, 250); + }, 50); + }); + + it('should assign final shared options to target after animations complete', function(done) { + const chart = { + draw: function() {}, + options: {} + }; + const anims = new Chart.Animations(chart, {value: {duration: 100}, option: {duration: 200}}); + + const origOpts = {option: 2}; + const target = { + value: 1, + options: origOpts + }; + const sharedOpts = {option: 10, $shared: true}; + const sharedOpts2 = {option: 20, $shared: true}; + + expect(anims.update(target, { + options: sharedOpts + })).toBeTrue(); + + expect(target.options !== sharedOpts).toBeTrue(); + + Chart.animator.start(chart); + + setTimeout(function() { + expect(Chart.animator.running(chart)).toBeTrue(); + + expect(target.options === origOpts).toBeTrue(); + + expect(anims.update(target, { + options: sharedOpts2 + })).toBeUndefined(); + + expect(target.options === origOpts).toBeTrue(); + + setTimeout(function() { + expect(target.options === sharedOpts2).toBeTrue(); + + Chart.animator.remove(chart); + done(); + }, 250); + }, 50); + }); +}); diff --git a/test/specs/core.animator.tests.js b/test/specs/core.animator.tests.js new file mode 100644 index 00000000000..218ff396a5e --- /dev/null +++ b/test/specs/core.animator.tests.js @@ -0,0 +1,48 @@ +describe('Chart.animator', function() { + it('should fire onProgress for each draw', function(done) { + let count = 0; + let drawCount = 0; + const progress = (animation) => { + count++; + expect(animation.numSteps).toEqual(250); + expect(animation.currentStep <= 250).toBeTrue(); + }; + acquireChart({ + type: 'bar', + data: { + datasets: [ + {data: [10, 5, 0, 25, 78, -10]} + ], + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] + }, + options: { + animation: { + duration: 250, + onProgress: progress, + onComplete: function() { + expect(count).toEqual(drawCount); + done(); + } + } + }, + plugins: [{ + afterDraw() { + drawCount++; + } + }] + }, { + canvas: { + height: 150, + width: 250 + }, + }); + }); + + it('should not fail when adding no items', function() { + const chart = {}; + Chart.animator.add(chart, undefined); + Chart.animator.add(chart, []); + Chart.animator.start(chart); + expect(Chart.animator.running(chart)).toBeFalse(); + }); +}); diff --git a/test/specs/core.controller.tests.js b/test/specs/core.controller.tests.js new file mode 100644 index 00000000000..d1b2e424603 --- /dev/null +++ b/test/specs/core.controller.tests.js @@ -0,0 +1,2339 @@ +describe('Chart', function() { + + const overrides = Chart.overrides; + + // https://github.com/chartjs/Chart.js/issues/2481 + // See global.deprecations.tests.js for backward compatibility + it('should be defined and prototype of chart instances', function() { + var chart = acquireChart({}); + expect(Chart).toBeDefined(); + expect(Chart instanceof Object).toBeTruthy(); + expect(chart.constructor).toBe(Chart); + expect(chart instanceof Chart).toBeTruthy(); + }); + + it('should throw an error if the canvas is already in use', function() { + var config = { + type: 'line', + data: { + datasets: [{ + data: [1, 2, 3, 4] + }], + labels: ['A', 'B', 'C', 'D'] + } + }; + var chart = acquireChart(config); + var canvas = chart.canvas; + + function createChart() { + return new Chart(canvas, config); + } + + expect(createChart).toThrow(new Error( + 'Canvas is already in use. ' + + 'Chart with ID \'' + chart.id + '\'' + + ' must be destroyed before the canvas with ID \'' + chart.canvas.id + '\' can be reused.' + )); + + chart.destroy(); + expect(createChart).not.toThrow(); + }); + + describe('config initialization', function() { + it('should create missing config.data properties', function() { + var chart = acquireChart({}); + var data = chart.data; + + expect(data instanceof Object).toBeTruthy(); + expect(data.labels instanceof Array).toBeTruthy(); + expect(data.labels.length).toBe(0); + expect(data.datasets instanceof Array).toBeTruthy(); + expect(data.datasets.length).toBe(0); + }); + + it('should not alter config.data references', function() { + var ds0 = {data: [10, 11, 12, 13]}; + var ds1 = {data: [20, 21, 22, 23]}; + var datasets = [ds0, ds1]; + var labels = [0, 1, 2, 3]; + var data = {labels: labels, datasets: datasets}; + + var chart = acquireChart({ + type: 'line', + data: data + }); + + expect(chart.data).toBe(data); + expect(chart.data.labels).toBe(labels); + expect(chart.data.datasets).toBe(datasets); + expect(chart.data.datasets[0]).toBe(ds0); + expect(chart.data.datasets[1]).toBe(ds1); + expect(chart.data.datasets[0].data).toBe(ds0.data); + expect(chart.data.datasets[1].data).toBe(ds1.data); + }); + + it('should define chart.data as an alias for config.data', function() { + var config = {data: {labels: [], datasets: []}}; + var chart = acquireChart(config); + + expect(chart.data).toBe(config.data); + + chart.data = {labels: [1, 2, 3], datasets: [{data: [4, 5, 6]}]}; + + expect(config.data).toBe(chart.data); + expect(config.data.labels).toEqual([1, 2, 3]); + expect(config.data.datasets[0].data).toEqual([4, 5, 6]); + + config.data = {labels: [7, 8, 9], datasets: [{data: [10, 11, 12]}]}; + + expect(chart.data).toBe(config.data); + expect(chart.data.labels).toEqual([7, 8, 9]); + expect(chart.data.datasets[0].data).toEqual([10, 11, 12]); + }); + + it('should initialize config with default interaction options', function() { + var callback = function() {}; + var defaults = Chart.defaults; + + defaults.onHover = callback; + overrides.line.interaction = { + mode: 'test' + }; + + var chart = acquireChart({ + type: 'line' + }); + + var options = chart.options; + expect(options.font.size).toBe(defaults.font.size); + expect(options.onHover).toBe(callback); + expect(options.hover.mode).toBe('test'); + + defaults.onHover = null; + delete overrides.line.interaction; + }); + + it('should initialize config with default hover options', function() { + var callback = function() {}; + var defaults = Chart.defaults; + + defaults.onHover = callback; + overrides.line.hover = { + mode: 'test' + }; + + var chart = acquireChart({ + type: 'line' + }); + + var options = chart.options; + expect(options.font.size).toBe(defaults.font.size); + expect(options.onHover).toBe(callback); + expect(options.hover.mode).toBe('test'); + + defaults.onHover = null; + delete overrides.line.hover; + }); + + it('should override default options', function() { + var callback = function() {}; + var defaults = Chart.defaults; + var defaultSpanGaps = defaults.datasets.line.spanGaps; + + defaults.onHover = callback; + overrides.line.hover = { + mode: 'x-axis' + }; + defaults.datasets.line.spanGaps = true; + + var chart = acquireChart({ + type: 'line', + options: { + spanGaps: false, + hover: { + mode: 'dataset', + }, + plugins: { + title: { + position: 'bottom' + } + } + } + }); + + var options = chart.options; + expect(options.spanGaps).toBe(false); + expect(options.hover.mode).toBe('dataset'); + expect(options.plugins.title.position).toBe('bottom'); + + defaults.onHover = null; + delete overrides.line.hover; + defaults.datasets.line.spanGaps = defaultSpanGaps; + }); + + it('should initialize config with default dataset options', function() { + var defaults = Chart.defaults.datasets.pie; + + var chart = acquireChart({ + type: 'pie' + }); + + var options = chart.options; + expect(options.circumference).toBe(defaults.circumference); + }); + + it('should override axis positions that are incorrect', function() { + var chart = acquireChart({ + type: 'line', + options: { + scales: { + x: { + position: 'left', + }, + y: { + position: 'bottom' + } + } + } + }); + + var scaleOptions = chart.options.scales; + expect(scaleOptions.x.position).toBe('bottom'); + expect(scaleOptions.y.position).toBe('left'); + }); + + it('should throw an error if the chart type is incorrect', function() { + function createChart() { + acquireChart({ + type: 'area', + data: { + datasets: [{ + label: 'first', + data: [10, 20] + }], + labels: ['0', '1'], + }, + options: { + scales: { + x: { + type: 'linear', + position: 'left', + }, + y: { + type: 'category', + position: 'bottom' + } + } + } + }); + } + expect(createChart).toThrow(new Error('"area" is not a registered controller.')); + }); + + it('should initialize the data object', function() { + const chart = acquireChart({type: 'bar'}); + expect(chart.data).toEqual(jasmine.objectContaining({labels: [], datasets: []})); + chart.data = {}; + expect(chart.data).toEqual(jasmine.objectContaining({labels: [], datasets: []})); + chart.data = null; + expect(chart.data).toEqual(jasmine.objectContaining({labels: [], datasets: []})); + chart.data = undefined; + expect(chart.data).toEqual(jasmine.objectContaining({labels: [], datasets: []})); + }); + + describe('should disable hover', function() { + it('when options.hover=false', function() { + var chart = acquireChart({ + type: 'line', + options: { + hover: false + } + }); + expect(chart.options.hover).toBeFalse(); + }); + + it('when options.interaction=false and options.hover is not defined', function() { + var chart = acquireChart({ + type: 'line', + options: { + interaction: false + } + }); + expect(chart.options.hover).toBeFalse(); + }); + + it('when options.interaction=false and options.hover is defined', function() { + var chart = acquireChart({ + type: 'line', + options: { + interaction: false, + hover: {mode: 'nearest'} + } + }); + expect(chart.options.hover).toBeFalse(); + }); + }); + + it('should activate element on hover', async function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + } + }); + + var point = chart.getDatasetMeta(0).data[1]; + + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + expect(chart.getActiveElements()).toEqual([{datasetIndex: 0, index: 1, element: point}]); + }); + + it('should handle changing the events at runtime', async function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + events: ['click'] + } + }); + + var point1 = chart.getDatasetMeta(0).data[1]; + var point2 = chart.getDatasetMeta(0).data[2]; + + await jasmine.triggerMouseEvent(chart, 'click', point1); + expect(chart.getActiveElements()).toEqual([{datasetIndex: 0, index: 1, element: point1}]); + + chart.options.events = ['mousemove']; + chart.update(); + + await jasmine.triggerMouseEvent(chart, 'mousemove', point2); + expect(chart.getActiveElements()).toEqual([{datasetIndex: 0, index: 2, element: point2}]); + }); + + it('should activate element on hover when minPadding pixels outside chart area', async function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100], + hoverRadius: 0 + }], + }, + options: { + scales: { + x: {display: false}, + y: {display: false} + } + } + }); + + var point = chart.getDatasetMeta(0).data[0]; + + await jasmine.triggerMouseEvent(chart, 'mousemove', {x: 1, y: point.y}); + expect(chart.getActiveElements()).toEqual([{datasetIndex: 0, index: 0, element: point}]); + }); + + it('should not activate elements when hover is disabled', async function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + hover: false + } + }); + + var point = chart.getDatasetMeta(0).data[1]; + + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + expect(chart.getActiveElements()).toEqual([]); + }); + + it('should not change the active elements when outside chartArea, except for mouseout', async function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100], + hoverRadius: 0 + }], + }, + options: { + scales: { + x: {display: false}, + y: {display: false} + }, + layout: { + padding: 5 + } + } + }); + + var point = chart.getDatasetMeta(0).data[0]; + + await jasmine.triggerMouseEvent(chart, 'mousemove', {x: point.x, y: point.y}); + expect(chart.getActiveElements()).toEqual([{datasetIndex: 0, index: 0, element: point}]); + + await jasmine.triggerMouseEvent(chart, 'mousemove', {x: 1, y: 1}); + expect(chart.getActiveElements()).toEqual([{datasetIndex: 0, index: 0, element: point}]); + + await jasmine.triggerMouseEvent(chart, 'mouseout', {x: 1, y: 1}); + expect(chart.tooltip.getActiveElements()).toEqual([]); + }); + }); + + describe('when merging scale options', function() { + beforeEach(function() { + Chart.helpers.merge(Chart.defaults.scale, { + _jasmineCheckA: 'a0', + _jasmineCheckB: 'b0', + _jasmineCheckC: 'c0' + }); + + Chart.helpers.merge(Chart.defaults.scales.logarithmic, { + _jasmineCheckB: 'b1', + _jasmineCheckC: 'c1', + }); + }); + + afterEach(function() { + delete Chart.defaults.scale._jasmineCheckA; + delete Chart.defaults.scale._jasmineCheckB; + delete Chart.defaults.scale._jasmineCheckC; + delete Chart.defaults.scales.logarithmic._jasmineCheckB; + delete Chart.defaults.scales.logarithmic._jasmineCheckC; + }); + + it('should default to "category" for x scales and "linear" for y scales', function() { + var chart = acquireChart({ + type: 'line', + options: { + scales: { + xFoo0: {}, + xFoo1: {}, + yBar0: {}, + yBar1: {}, + } + } + }); + + expect(chart.scales.xFoo0.type).toBe('category'); + expect(chart.scales.xFoo1.type).toBe('category'); + expect(chart.scales.yBar0.type).toBe('linear'); + expect(chart.scales.yBar1.type).toBe('linear'); + }); + + it('should correctly apply defaults on central scale', function() { + var chart = acquireChart({ + type: 'line', + options: { + scales: { + foo: { + axis: 'x', + type: 'logarithmic', + _jasmineCheckC: 'c2', + _jasmineCheckD: 'd2' + } + } + } + }); + + // let's check a few values from the user options and defaults + + expect(chart.scales.foo.type).toBe('logarithmic'); + expect(chart.scales.foo.options).toEqual(chart.options.scales.foo); + expect(chart.scales.foo.options).toEqual( + jasmine.objectContaining({ + _jasmineCheckA: 'a0', + _jasmineCheckB: 'b1', + _jasmineCheckC: 'c2', + _jasmineCheckD: 'd2' + })); + }); + + it('should correctly apply defaults on xy scales', function() { + var chart = acquireChart({ + type: 'line', + options: { + scales: { + x: { + type: 'logarithmic', + _jasmineCheckC: 'c2', + _jasmineCheckD: 'd2' + }, + y: { + type: 'time', + _jasmineCheckC: 'c2', + _jasmineCheckE: 'e2' + } + } + } + }); + + expect(chart.scales.x.type).toBe('logarithmic'); + expect(chart.scales.x.options).toEqual(chart.options.scales.x); + expect(chart.scales.x.options).toEqual( + jasmine.objectContaining({ + _jasmineCheckA: 'a0', + _jasmineCheckB: 'b1', + _jasmineCheckC: 'c2', + _jasmineCheckD: 'd2' + })); + + expect(chart.scales.y.type).toBe('time'); + expect(chart.scales.y.options).toEqual(chart.options.scales.y); + expect(chart.scales.y.options).toEqual( + jasmine.objectContaining({ + _jasmineCheckA: 'a0', + _jasmineCheckB: 'b0', + _jasmineCheckC: 'c2', + _jasmineCheckE: 'e2' + })); + }); + + it('should not alter defaults when merging config', function() { + var chart = acquireChart({ + type: 'line', + options: { + _jasmineCheck: 42, + scales: { + x: { + type: 'linear', + _jasmineCheck: 42, + }, + y: { + type: 'category', + _jasmineCheck: 42, + } + } + } + }); + + expect(chart.options._jasmineCheck).toBeDefined(); + expect(chart.scales.x.options._jasmineCheck).toBeDefined(); + expect(chart.scales.y.options._jasmineCheck).toBeDefined(); + + expect(Chart.overrides.line._jasmineCheck).not.toBeDefined(); + expect(Chart.defaults._jasmineCheck).not.toBeDefined(); + expect(Chart.defaults.scales.linear._jasmineCheck).not.toBeDefined(); + expect(Chart.defaults.scales.category._jasmineCheck).not.toBeDefined(); + }); + + it('should ignore proxy passed as scale options', function() { + let failure = false; + const chart = acquireChart({ + type: 'line', + data: [], + options: { + scales: { + x: { + grid: { + color: ctx => { + if (!ctx.tick) { + failure = true; + } + } + } + } + } + } + }); + chart.options.scales = { + x: chart.options.scales.x, + y: { + type: 'linear', + position: 'right' + } + }; + chart.update(); + expect(failure).toEqual(false); + }); + + it('should ignore array passed as scale options', function() { + const chart = acquireChart({ + type: 'line', + data: [], + options: { + scales: { + xAxes: [{id: 'xAxes', type: 'category'}] + } + } + }); + expect(chart.scales.xAxes).not.toBeDefined(); + }); + }); + + describe('Updating options', function() { + it('update should result to same set of options as construct', function() { + var chart = acquireChart({ + type: 'line', + data: [], + options: { + animation: false, + locale: 'en-US', + responsive: false + } + }); + const options = chart.options; + chart.options = { + animation: false, + locale: 'en-US', + responsive: false + }; + chart.update(); + expect(chart.options).toEqualOptions(options); + }); + }); + + describe('config.options.responsive: true (maintainAspectRatio: false)', function() { + it('should fill parent width and height', function() { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: false + } + }, { + canvas: { + style: 'width: 150px; height: 245px' + }, + wrapper: { + style: 'width: 300px; height: 350px' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 300, dh: 350, + rw: 300, rh: 350, + }); + }); + + it('should call onResize with correct arguments and context', function() { + let count = 0; + let correctThis = false; + let size = { + width: 0, + height: 0 + }; + acquireChart({ + options: { + responsive: true, + maintainAspectRatio: false, + onResize(chart, newSize) { + count++; + correctThis = this === chart; + size.width = newSize.width; + size.height = newSize.height; + } + } + }, { + canvas: { + style: 'width: 150px; height: 245px' + }, + wrapper: { + style: 'width: 300px; height: 350px' + } + }); + + expect(count).toEqual(1); + expect(correctThis).toBeTrue(); + expect(size).toEqual({width: 300, height: 350}); + }); + + + it('should resize the canvas when parent width changes', function(done) { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: false + } + }, { + canvas: { + style: '' + }, + wrapper: { + style: 'width: 300px; height: 350px; position: relative' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 300, dh: 350, + rw: 300, rh: 350, + }); + + var wrapper = chart.canvas.parentNode; + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 455, dh: 350, + rw: 455, rh: 350, + }); + + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 150, dh: 350, + rw: 150, rh: 350, + }); + + done(); + }); + wrapper.style.width = '150px'; + }); + wrapper.style.width = '455px'; + }); + + it('should restore the original size when parent became invisible', function(done) { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: false + } + }, { + canvas: { + style: '' + }, + wrapper: { + style: 'width: 300px; height: 350px; position: relative' + } + }); + + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 300, dh: 350, + rw: 300, rh: 350, + }); + + var original = chart.resize; + chart.resize = function() { + fail('resize should not have been called'); + }; + + var wrapper = chart.canvas.parentNode; + wrapper.style.display = 'none'; + + setTimeout(function() { + expect(wrapper.clientWidth).toEqual(0); + expect(wrapper.clientHeight).toEqual(0); + + expect(chart).toBeChartOfSize({ + dw: 300, dh: 350, + rw: 300, rh: 350, + }); + + chart.resize = original; + + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 300, dh: 350, + rw: 300, rh: 350, + }); + + done(); + }); + wrapper.style.display = 'block'; + }, 200); + }); + }); + + it('should resize the canvas when parent is RTL and width changes', function(done) { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: false + } + }, { + canvas: { + style: '' + }, + wrapper: { + style: 'width: 300px; height: 350px; position: relative; direction: rtl' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 300, dh: 350, + rw: 300, rh: 350, + }); + + var wrapper = chart.canvas.parentNode; + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 455, dh: 350, + rw: 455, rh: 350, + }); + + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 150, dh: 350, + rw: 150, rh: 350, + }); + + done(); + }); + wrapper.style.width = '150px'; + }); + wrapper.style.width = '455px'; + }); + + it('should resize the canvas when parent height changes', function(done) { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: false + } + }, { + canvas: { + style: '' + }, + wrapper: { + style: 'width: 300px; height: 350px; position: relative' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 300, dh: 350, + rw: 300, rh: 350, + }); + + var wrapper = chart.canvas.parentNode; + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 300, dh: 455, + rw: 300, rh: 455, + }); + + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 300, dh: 150, + rw: 300, rh: 150, + }); + + done(); + }); + wrapper.style.height = '150px'; + }); + wrapper.style.height = '455px'; + }); + + it('should not include parent padding when resizing the canvas', function(done) { + var chart = acquireChart({ + type: 'line', + options: { + responsive: true, + maintainAspectRatio: false + } + }, { + canvas: { + style: '' + }, + wrapper: { + style: 'padding: 50px; width: 320px; height: 350px; position: relative' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 320, dh: 350, + rw: 320, rh: 350, + }); + + var wrapper = chart.canvas.parentNode; + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 455, dh: 355, + rw: 455, rh: 355, + }); + + done(); + }); + wrapper.style.height = '355px'; + wrapper.style.width = '455px'; + }); + + it('should resize the canvas when the canvas display style changes from "none" to "block"', function(done) { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: false + } + }, { + canvas: { + style: 'display: none;' + }, + wrapper: { + style: 'width: 320px; height: 350px' + } + }); + + var canvas = chart.canvas; + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 320, dh: 350, + rw: 320, rh: 350, + }); + + done(); + }); + canvas.style.display = 'block'; + }); + + it('should resize the canvas when the wrapper display style changes from "none" to "block"', function(done) { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: false + } + }, { + canvas: { + style: '' + }, + wrapper: { + style: 'display: none; width: 460px; height: 380px' + } + }); + + var wrapper = chart.canvas.parentNode; + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 460, dh: 380, + rw: 460, rh: 380, + }); + + done(); + }); + wrapper.style.display = 'block'; + }); + + it('should resize the canvas when the wrapper has display style changes from "none" to "block"', function(done) { + // https://github.com/chartjs/Chart.js/issues/4659 + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: false + } + }, { + canvas: { + style: '' + }, + wrapper: { + style: 'display: none; max-width: 600px; max-height: 400px;' + } + }); + + var wrapper = chart.canvas.parentNode; + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 600, dh: 300, + rw: 600, rh: 300, + }); + + done(); + }); + wrapper.style.display = 'block'; + }); + + // https://github.com/chartjs/Chart.js/issues/5485 + it('should resize the canvas when the devicePixelRatio changes', function(done) { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: false, + devicePixelRatio: 1 + } + }, { + canvas: { + style: '' + }, + wrapper: { + style: 'width: 400px; height: 200px; position: relative' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 400, dh: 200, + rw: 400, rh: 200, + }); + + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 400, dh: 200, + rw: 800, rh: 400, + }); + + done(); + }); + chart.options.devicePixelRatio = 2; + chart.resize(); + }); + + // https://github.com/chartjs/Chart.js/issues/3790 + it('should resize the canvas if attached to the DOM after construction', function(done) { + var canvas = document.createElement('canvas'); + var wrapper = document.createElement('div'); + var body = window.document.body; + var chart = new Chart(canvas, { + type: 'line', + options: { + responsive: true, + maintainAspectRatio: false + } + }); + + expect(chart).toBeChartOfSize({ + dw: 0, dh: 0, + rw: 0, rh: 0, + }); + expect(chart.chartArea).toBeUndefined(); + + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 455, dh: 355, + rw: 455, rh: 355, + }); + + expect(chart.chartArea).not.toBeUndefined(); + + body.removeChild(wrapper); + chart.destroy(); + done(); + }); + + wrapper.style.cssText = 'width: 455px; height: 355px'; + wrapper.appendChild(canvas); + body.appendChild(wrapper); + }); + + it('should resize the canvas when attached to a different parent', function(done) { + var canvas = document.createElement('canvas'); + var wrapper = document.createElement('div'); + var body = window.document.body; + var chart = new Chart(canvas, { + type: 'line', + options: { + responsive: true, + maintainAspectRatio: false + } + }); + + expect(chart).toBeChartOfSize({ + dw: 0, dh: 0, + rw: 0, rh: 0, + }); + + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 455, dh: 355, + rw: 455, rh: 355, + }); + + var target = document.createElement('div'); + + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 640, dh: 480, + rw: 640, rh: 480, + }); + + body.removeChild(wrapper); + body.removeChild(target); + chart.destroy(); + done(); + }); + + target.style.cssText = 'width: 640px; height: 480px'; + target.appendChild(canvas); + body.appendChild(target); + }); + + wrapper.style.cssText = 'width: 455px; height: 355px'; + wrapper.appendChild(canvas); + body.appendChild(wrapper); + }); + + // https://github.com/chartjs/Chart.js/issues/3521 + it('should resize the canvas after the wrapper has been re-attached to the DOM', function(done) { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: false + } + }, { + canvas: { + style: '' + }, + wrapper: { + style: 'width: 320px; height: 350px' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 320, dh: 350, + rw: 320, rh: 350, + }); + + var wrapper = chart.canvas.parentNode; + var parent = wrapper.parentNode; + + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 320, dh: 355, + rw: 320, rh: 355, + }); + + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 455, dh: 355, + rw: 455, rh: 355, + }); + + done(); + }); + + parent.removeChild(wrapper); + wrapper.style.width = '455px'; + parent.appendChild(wrapper); + }); + + parent.removeChild(wrapper); + setTimeout(() => { + parent.appendChild(wrapper); + wrapper.style.height = '355px'; + }, 0); + }); + + // https://github.com/chartjs/Chart.js/issues/9875 + it('should detect detach/attach in series', function(done) { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: false + } + }, { + canvas: { + style: '' + }, + wrapper: { + style: 'width: 320px; height: 350px' + } + }); + + var wrapper = chart.canvas.parentNode; + var parent = wrapper.parentNode; + + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 320, dh: 350, + rw: 320, rh: 350, + }); + + done(); + }); + + parent.removeChild(wrapper); + parent.appendChild(wrapper); + }); + + it('should detect detach/attach/detach in series', function(done) { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: false + } + }, { + canvas: { + style: '' + }, + wrapper: { + style: 'width: 320px; height: 350px' + } + }); + + var wrapper = chart.canvas.parentNode; + var parent = wrapper.parentNode; + + waitForResize(chart, function() { + fail(); + }); + + parent.removeChild(wrapper); + parent.appendChild(wrapper); + parent.removeChild(wrapper); + + setTimeout(function() { + expect(chart.attached).toBeFalse(); + done(); + }, 100); + }); + + it('should detect attach/detach in series', function(done) { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: false + } + }, { + canvas: { + style: '' + }, + wrapper: { + style: 'width: 320px; height: 350px' + } + }); + + var wrapper = chart.canvas.parentNode; + var parent = wrapper.parentNode; + + parent.removeChild(wrapper); + + setTimeout(function() { + expect(chart.attached).toBeFalse(); + + waitForResize(chart, function() { + fail(); + }); + + parent.appendChild(wrapper); + parent.removeChild(wrapper); + + setTimeout(function() { + expect(chart.attached).toBeFalse(); + + done(); + }, 100); + }, 100); + }); + + // https://github.com/chartjs/Chart.js/issues/4737 + it('should resize the canvas when re-creating the chart', function(done) { + var chart = acquireChart({ + options: { + responsive: true + } + }, { + wrapper: { + style: 'width: 320px' + } + }); + + var wrapper = chart.canvas.parentNode; + + waitForResize(chart, function() { + var canvas = chart.canvas; + expect(chart).toBeChartOfSize({ + dw: 320, dh: 320, + rw: 320, rh: 320, + }); + + chart.destroy(); + chart = new Chart(canvas, { + type: 'line', + options: { + responsive: true + } + }); + + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 455, dh: 455, + rw: 455, rh: 455, + }); + + chart.destroy(); + window.document.body.removeChild(wrapper); + done(); + }); + canvas.parentNode.style.width = '455px'; + canvas.parentNode.style.height = '455px'; + }); + }); + + it('should resize the canvas if attached to the DOM after construction with multiple parents', function(done) { + var canvas = document.createElement('canvas'); + var wrapper = document.createElement('div'); + var wrapper2 = document.createElement('div'); + var wrapper3 = document.createElement('div'); + var body = window.document.body; + + var chart = new Chart(canvas, { + type: 'line', + options: { + responsive: true, + maintainAspectRatio: false + } + }); + + expect(chart).toBeChartOfSize({ + dw: 0, dh: 0, + rw: 0, rh: 0, + }); + expect(chart.chartArea).toBeUndefined(); + + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 455, dh: 355, + rw: 455, rh: 355, + }); + + expect(chart.chartArea).not.toBeUndefined(); + + body.removeChild(wrapper3); + chart.destroy(); + done(); + }); + + wrapper3.appendChild(wrapper2); + wrapper2.appendChild(wrapper); + wrapper.style.cssText = 'width: 455px; height: 355px'; + wrapper.appendChild(canvas); + body.appendChild(wrapper3); + }); + }); + + describe('config.options.responsive: true (maintainAspectRatio: true)', function() { + it('should resize the canvas with correct aspect ratio when parent width changes', function(done) { + var chart = acquireChart({ + type: 'line', // AR == 2 + options: { + responsive: true, + maintainAspectRatio: true + } + }, { + canvas: { + style: '' + }, + wrapper: { + style: 'width: 300px; height: 350px; position: relative' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 300, dh: 150, + rw: 300, rh: 150, + }); + + var wrapper = chart.canvas.parentNode; + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 450, dh: 225, + rw: 450, rh: 225, + }); + + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 150, dh: 75, + rw: 150, rh: 75, + }); + + done(); + }); + wrapper.style.width = '150px'; + }); + wrapper.style.width = '450px'; + }); + + it('should maintain aspect ratio when parent height changes', function(done) { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: true + } + }, { + canvas: { + style: '' + }, + wrapper: { + style: 'width: 320px; height: 350px; position: relative' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 320, dh: 160, + rw: 320, rh: 160, + }); + + var wrapper = chart.canvas.parentNode; + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 320, dh: 160, + rw: 320, rh: 160, + }); + + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 300, dh: 150, + rw: 300, rh: 150, + }); + + done(); + }); + wrapper.style.height = '150px'; + }); + wrapper.style.height = '455px'; + }); + }); + + describe('Retina scale (a.k.a. device pixel ratio)', function() { + beforeEach(function() { + this.devicePixelRatio = window.devicePixelRatio; + window.devicePixelRatio = 3; + }); + + afterEach(function() { + window.devicePixelRatio = this.devicePixelRatio; + }); + + // see https://github.com/chartjs/Chart.js/issues/3575 + it ('should scale the render size but not the "implicit" display size', function() { + var chart = acquireChart({ + options: { + responsive: false + } + }, { + canvas: { + width: 320, + height: 240, + } + }); + + expect(chart).toBeChartOfSize({ + dw: 320, dh: 240, + rw: 960, rh: 720, + }); + }); + + it ('should scale the render size but not the "explicit" display size', function() { + var chart = acquireChart({ + options: { + responsive: false + } + }, { + canvas: { + style: 'width: 320px; height: 240px' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 320, dh: 240, + rw: 960, rh: 720, + }); + }); + }); + + describe('config.options.devicePixelRatio', function() { + beforeEach(function() { + this.devicePixelRatio = window.devicePixelRatio; + window.devicePixelRatio = 1; + }); + + afterEach(function() { + window.devicePixelRatio = this.devicePixelRatio; + }); + + // see https://github.com/chartjs/Chart.js/issues/3575 + it ('should scale the render size but not the "implicit" display size', function() { + var chart = acquireChart({ + options: { + responsive: false, + devicePixelRatio: 3 + } + }, { + canvas: { + width: 320, + height: 240, + } + }); + + expect(chart).toBeChartOfSize({ + dw: 320, dh: 240, + rw: 960, rh: 720, + }); + }); + + it ('should scale the render size but not the "explicit" display size', function() { + var chart = acquireChart({ + options: { + responsive: false, + devicePixelRatio: 3 + } + }, { + canvas: { + style: 'width: 320px; height: 240px' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 320, dh: 240, + rw: 960, rh: 720, + }); + }); + }); + + describe('config.options.aspectRatio', function() { + it('should resize the canvas when the aspectRatio option changes', function(done) { + var chart = acquireChart({ + options: { + responsive: true, + aspectRatio: 1, + } + }, { + canvas: { + style: '', + width: 400, + }, + }); + + expect(chart).toBeChartOfSize({ + dw: 400, dh: 400, + rw: 400, rh: 400, + }); + + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 400, dh: 200, + rw: 400, rh: 200, + }); + + done(); + }); + chart.options.aspectRatio = 2; + chart.resize(); + }); + }); + + describe('controller.reset', function() { + it('should reset the chart elements', function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 0] + }] + }, + options: { + responsive: true + } + }); + + var meta = chart.getDatasetMeta(0); + + // Verify that points are at their initial correct location, + // then we will reset and see that they moved + expect(meta.data[0].y).toBeCloseToPixel(333); + expect(meta.data[1].y).toBeCloseToPixel(183); + expect(meta.data[2].y).toBeCloseToPixel(32); + expect(meta.data[3].y).toBeCloseToPixel(482); + + chart.reset(); + + // For a line chart, the animation state is the bottom + expect(meta.data[0].y).toBeCloseToPixel(482); + expect(meta.data[1].y).toBeCloseToPixel(482); + expect(meta.data[2].y).toBeCloseToPixel(482); + expect(meta.data[3].y).toBeCloseToPixel(482); + }); + }); + + describe('config update', function() { + it ('should update options', function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + responsive: true + } + }); + + chart.options = { + responsive: false, + scales: { + y: { + min: 0, + max: 10 + } + } + }; + chart.update(); + + var yScale = chart.scales.y; + expect(yScale.options.min).toBe(0); + expect(yScale.options.max).toBe(10); + }); + + it ('should update scales options', function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + responsive: true + } + }); + + chart.options.scales.y.min = 0; + chart.options.scales.y.max = 10; + chart.update(); + + var yScale = chart.scales.y; + expect(yScale.options.min).toBe(0); + expect(yScale.options.max).toBe(10); + }); + + it ('should update scales options from new object', function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + responsive: true + } + }); + + var newScalesConfig = { + y: { + min: 0, + max: 10 + } + }; + chart.options.scales = newScalesConfig; + + chart.update(); + + var yScale = chart.scales.y; + expect(yScale.options.min).toBe(0); + expect(yScale.options.max).toBe(10); + }); + + it ('should remove discarded scale', function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + responsive: true, + scales: { + yAxis0: { + min: 0, + max: 10 + } + } + } + }); + + var newScalesConfig = { + y: { + min: 0, + max: 10 + } + }; + chart.options.scales = newScalesConfig; + + chart.update(); + + var yScale = chart.scales.yAxis0; + expect(yScale).toBeUndefined(); + var newyScale = chart.scales.y; + expect(newyScale.options.min).toBe(0); + expect(newyScale.options.max).toBe(10); + }); + + it ('should update tooltip options', function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + responsive: true + } + }); + + var newTooltipConfig = { + mode: 'dataset', + intersect: false + }; + chart.options.plugins.tooltip = newTooltipConfig; + + chart.update(); + expect(chart.tooltip.options).toEqualOptions(newTooltipConfig); + }); + + it ('should update the tooltip on update', async function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + responsive: true, + tooltip: { + mode: 'nearest' + } + } + }); + + // Trigger an event over top of a point to + // put an item into the tooltip + var meta = chart.getDatasetMeta(0); + var point = meta.data[1]; + + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + // Check and see if tooltip was displayed + var tooltip = chart.tooltip; + + expect(chart._active[0].element).toEqual(point); + expect(tooltip._active[0].element).toEqual(point); + + // Update and confirm tooltip is updated + chart.update(); + expect(chart._active[0].element).toEqual(point); + expect(tooltip._active[0].element).toEqual(point); + }); + + it ('should update the metadata', function() { + var cfg = { + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + type: 'line', + data: [10, 20, 30, 0] + }] + }, + options: { + responsive: true, + scales: { + x: { + type: 'category' + }, + y: { + type: 'linear', + title: { + display: true, + text: 'Value' + } + } + } + } + }; + var chart = acquireChart(cfg); + var meta = chart.getDatasetMeta(0); + expect(meta.type).toBe('line'); + + // change the dataset to bar and check that meta was updated + chart.config.data.datasets[0].type = 'bar'; + chart.update(); + meta = chart.getDatasetMeta(0); + expect(meta.type).toBe('bar'); + }); + }); + + describe('plugin.extensions', function() { + var hooks = { + install: ['install'], + uninstall: ['uninstall'], + init: [ + 'beforeInit', + 'resize', + 'afterInit' + ], + start: ['start'], + stop: ['stop'], + update: [ + 'beforeUpdate', + 'beforeLayout', + 'beforeDataLimits', // y-axis fit + 'afterDataLimits', + 'beforeBuildTicks', + 'afterBuildTicks', + 'beforeDataLimits', // x-axis fit + 'afterDataLimits', + 'beforeBuildTicks', + 'afterBuildTicks', + // 'beforeBuildTicks', // y-axis re-fit + // 'afterBuildTicks', + 'afterLayout', + 'beforeDatasetsUpdate', + 'beforeDatasetUpdate', + 'afterDatasetUpdate', + 'afterDatasetsUpdate', + 'afterUpdate', + ], + render: [ + 'beforeRender', + 'beforeDraw', + 'beforeDatasetsDraw', + 'beforeDatasetDraw', + 'afterDatasetDraw', + 'afterDatasetsDraw', + // 'beforeTooltipDraw', + // 'afterTooltipDraw', + 'afterDraw', + 'afterRender', + ], + resize: [ + 'resize' + ], + destroy: [ + 'beforeDestroy', + 'afterDestroy' + ] + }; + + it ('should notify plugin in correct order', function(done) { + var plugin = this.plugin = {}; + var sequence = []; + + Object.keys(hooks).forEach(function(group) { + hooks[group].forEach(function(name) { + plugin[name] = function() { + sequence.push(name); + }; + }); + }); + + var chart = window.acquireChart({ + type: 'line', + data: {datasets: [{}]}, + plugins: [plugin], + options: { + responsive: true + } + }, { + wrapper: { + style: 'width: 300px' + } + }); + + waitForResize(chart, function() { + chart.destroy(); + + expect(sequence).toEqual([].concat( + hooks.install, + hooks.start, + hooks.init, + hooks.update, + hooks.render, + hooks.resize, + hooks.update, + hooks.render, + hooks.destroy, + hooks.stop, + hooks.uninstall + )); + + done(); + }); + chart.canvas.parentNode.style.width = '400px'; + chart.canvas.parentNode.style.height = '400px'; + }); + + it ('should notify initially disabled plugin in correct order', function() { + var plugin = this.plugin = {id: 'plugin'}; + var sequence = []; + + Object.keys(hooks).forEach(function(group) { + hooks[group].forEach(function(name) { + plugin[name] = function() { + sequence.push(name); + }; + }); + }); + + var chart = window.acquireChart({ + type: 'line', + data: {datasets: [{}]}, + plugins: [plugin], + options: { + plugins: { + plugin: false + } + } + }); + + expect(sequence).toEqual([].concat( + hooks.install + )); + + sequence = []; + chart.options.plugins.plugin = true; + chart.update(); + + expect(sequence).toEqual([].concat( + hooks.start, + hooks.update, + hooks.render + )); + + sequence = []; + chart.options.plugins.plugin = false; + chart.update(); + + expect(sequence).toEqual(hooks.stop); + + sequence = []; + chart.destroy(); + + expect(sequence).toEqual(hooks.uninstall); + }); + + it('should not notify before/afterDatasetDraw if dataset is hidden', function() { + var sequence = []; + var plugin = this.plugin = { + beforeDatasetDraw: function(chart, args) { + sequence.push('before-' + args.index); + }, + afterDatasetDraw: function(chart, args) { + sequence.push('after-' + args.index); + } + }; + + window.acquireChart({ + type: 'line', + data: {datasets: [{}, {hidden: true}, {}]}, + plugins: [plugin] + }); + + expect(sequence).toEqual([ + 'before-2', 'after-2', + 'before-0', 'after-0' + ]); + }); + + it('should not crash when accessing options of a blank inline plugin', function() { + var chart = window.acquireChart({ + type: 'line', + data: {datasets: [{}]}, + plugins: [{}], + }); + + function iterateOptions() { + for (const plugin of chart._plugins._init) { + // triggering bug https://github.com/chartjs/Chart.js/issues/9368 + expect(Object.getPrototypeOf(plugin.options)).toBeNull(); + } + } + + expect(iterateOptions).not.toThrow(); + }); + }); + + describe('metasets', function() { + beforeEach(function() { + this.chart = acquireChart({ + type: 'line', + data: { + datasets: [ + {label: '1', order: 2}, + {label: '2', order: 1}, + {label: '3', order: 4}, + {label: '4', order: 3}, + ] + } + }); + }); + afterEach(function() { + const metasets = this.chart._metasets; + expect(metasets.length).toEqual(this.chart.data.datasets.length); + for (let i = 0; i < metasets.length; i++) { + expect(metasets[i].index).toEqual(i); + expect(metasets[i]._dataset).toEqual(this.chart.data.datasets[i]); + } + }); + it('should build metasets array in order', function() { + const metasets = this.chart._metasets; + expect(metasets[0].order).toEqual(2); + expect(metasets[1].order).toEqual(1); + expect(metasets[2].order).toEqual(4); + expect(metasets[3].order).toEqual(3); + }); + it('should build sorted metasets array in correct order', function() { + const metasets = this.chart._sortedMetasets; + expect(metasets[0].order).toEqual(1); + expect(metasets[1].order).toEqual(2); + expect(metasets[2].order).toEqual(3); + expect(metasets[3].order).toEqual(4); + }); + it('should be moved when datasets are removed from beginning', function() { + this.chart.data.datasets.splice(0, 2); + this.chart.update(); + const metasets = this.chart._metasets; + expect(metasets[0].order).toEqual(4); + expect(metasets[1].order).toEqual(3); + }); + it('should be moved when datasets are removed from middle', function() { + this.chart.data.datasets.splice(1, 2); + this.chart.update(); + const metasets = this.chart._metasets; + expect(metasets[0].order).toEqual(2); + expect(metasets[1].order).toEqual(3); + }); + it('should be moved when datasets are inserted', function() { + this.chart.data.datasets.splice(1, 0, {label: '1.5', order: 5}); + this.chart.update(); + const metasets = this.chart._metasets; + expect(metasets[0].order).toEqual(2); + expect(metasets[1].order).toEqual(5); + expect(metasets[2].order).toEqual(1); + expect(metasets[3].order).toEqual(4); + expect(metasets[4].order).toEqual(3); + }); + it('should be replaced when dataset is replaced', function() { + this.chart.data.datasets.splice(1, 1, {label: '1.5', order: 5}); + this.chart.update(); + const metasets = this.chart._metasets; + expect(metasets[0].order).toEqual(2); + expect(metasets[1].order).toEqual(5); + expect(metasets[2].order).toEqual(4); + expect(metasets[3].order).toEqual(3); + }); + it('should update properly when dataset locations are swapped', function() { + const orig = this.chart.data.datasets; + this.chart.data.datasets = [orig[0], orig[2], orig[1], orig[3]]; + this.chart.update(); + let metasets = this.chart._metasets; + expect(metasets[0].label).toEqual('1'); + expect(metasets[1].label).toEqual('3'); + expect(metasets[2].label).toEqual('2'); + expect(metasets[3].label).toEqual('4'); + + this.chart.data.datasets = [{label: 'new', order: 10}, orig[3], orig[2], orig[1], orig[0]]; + this.chart.update(); + metasets = this.chart._metasets; + expect(metasets[0].label).toEqual('new'); + expect(metasets[1].label).toEqual('4'); + expect(metasets[2].label).toEqual('3'); + expect(metasets[3].label).toEqual('2'); + expect(metasets[4].label).toEqual('1'); + + this.chart.data.datasets = [orig[3], orig[2], orig[1], {label: 'new', order: 10}]; + this.chart.update(); + metasets = this.chart._metasets; + expect(metasets[0].label).toEqual('4'); + expect(metasets[1].label).toEqual('3'); + expect(metasets[2].label).toEqual('2'); + expect(metasets[3].label).toEqual('new'); + }); + }); + + describe('_destroyDatasetMeta', function() { + beforeEach(function() { + this.chart = acquireChart({ + type: 'line', + data: { + datasets: [ + {label: '1', order: 2}, + {label: '2', order: 1}, + {label: '3', order: 4}, + {label: '4', order: 3}, + ] + } + }); + }); + it('cleans up metasets when the chart is destroyed', function() { + this.chart.destroy(); + expect(this.chart._metasets).toEqual([undefined, undefined, undefined, undefined]); + }); + }); + + describe('data visibility', function() { + it('should hide a dataset', function() { + var chart = acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [0, 1, 2] + }], + labels: ['a', 'b', 'c'] + } + }); + + chart.setDatasetVisibility(0, false); + + var meta = chart.getDatasetMeta(0); + expect(meta.hidden).toBe(true); + }); + + it('should toggle data visibility by index', function() { + var chart = acquireChart({ + type: 'pie', + data: { + datasets: [{ + data: [1, 2, 3] + }] + } + }); + + expect(chart.getDataVisibility(1)).toBe(true); + + chart.toggleDataVisibility(1); + expect(chart.getDataVisibility(1)).toBe(false); + + chart.update(); + expect(chart.getDataVisibility(1)).toBe(false); + }); + + it('should maintain data visibility indices when data changes', function() { + var chart = acquireChart({ + type: 'pie', + data: { + labels: ['0', '1', '2', '3'], + datasets: [{ + data: [0, 1, 2, 3] + }, { + data: [0, 1, 2, 3] + }] + } + }); + + chart.toggleDataVisibility(3); + + chart.data.labels.splice(1, 1); + chart.data.datasets[0].data.splice(1, 1); + chart.data.datasets[1].data.splice(1, 1); + chart.update(); + + expect(chart.getDataVisibility(0)).toBe(true); + expect(chart.getDataVisibility(1)).toBe(true); + expect(chart.getDataVisibility(2)).toBe(false); + + chart.data.labels.unshift('-1', '-2'); + chart.data.datasets[0].data.unshift(-1, -2); + chart.data.datasets[1].data.unshift(-1, -2); + chart.update(); + + expect(chart.getDataVisibility(0)).toBe(true); + expect(chart.getDataVisibility(1)).toBe(true); + expect(chart.getDataVisibility(2)).toBe(true); + expect(chart.getDataVisibility(3)).toBe(true); + expect(chart.getDataVisibility(4)).toBe(false); + + chart.data.labels.shift(); + chart.data.datasets[0].data.shift(); + chart.data.datasets[1].data.shift(); + chart.update(); + + expect(chart.getDataVisibility(0)).toBe(true); + expect(chart.getDataVisibility(1)).toBe(true); + expect(chart.getDataVisibility(2)).toBe(true); + expect(chart.getDataVisibility(3)).toBe(false); + + chart.data.labels.pop(); + chart.data.datasets[0].data.pop(); + chart.data.datasets[1].data.pop(); + chart.update(); + + expect(chart.getDataVisibility(0)).toBe(true); + expect(chart.getDataVisibility(1)).toBe(true); + expect(chart.getDataVisibility(2)).toBe(true); + expect(chart.getDataVisibility(3)).toBe(true); + + chart.toggleDataVisibility(1); + chart.data.labels.splice(1, 0, 'b'); + chart.data.datasets[0].data.splice(1, 0, 1); + chart.data.datasets[1].data.splice(1, 0, 1); + chart.update(); + + expect(chart.getDataVisibility(0)).toBe(true); + expect(chart.getDataVisibility(1)).toBe(true); + expect(chart.getDataVisibility(2)).toBe(false); + expect(chart.getDataVisibility(3)).toBe(true); + }); + + it('should leave data visibility indices intact when data changes in non-uniform way', function() { + var chart = acquireChart({ + type: 'pie', + data: { + labels: ['0', '1', '2', '3'], + datasets: [{ + data: [0, 1, 2, 3] + }, { + data: [0, 1, 2, 3] + }] + } + }); + + chart.toggleDataVisibility(0); + + chart.data.labels.push('a'); + chart.data.datasets[0].data.pop(); + chart.data.datasets[1].data.push(5); + chart.update(); + + expect(chart.getDataVisibility(0)).toBe(false); + expect(chart.getDataVisibility(1)).toBe(true); + expect(chart.getDataVisibility(2)).toBe(true); + expect(chart.getDataVisibility(3)).toBe(true); + }); + }); + + describe('isDatasetVisible', function() { + it('should return false if index is out of bounds', function() { + var chart = acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [0, 1, 2] + }], + labels: ['a', 'b', 'c'] + } + }); + + expect(chart.isDatasetVisible(1)).toBe(false); + }); + }); + + describe('getChart', function() { + it('should get the chart from the canvas ID', function() { + var chart = acquireChart({ + type: 'pie', + data: { + datasets: [{ + data: [1, 2, 3] + }] + } + }); + chart.canvas.id = 'myID'; + + expect(Chart.getChart('myID')).toBe(chart); + }); + + it('should get the chart from an HTMLCanvasElement', function() { + var chart = acquireChart({ + type: 'pie', + data: { + datasets: [{ + data: [1, 2, 3] + }] + } + }); + expect(Chart.getChart(chart.canvas)).toBe(chart); + }); + + it('should get the chart from an CanvasRenderingContext2D', function() { + var chart = acquireChart({ + type: 'pie', + data: { + datasets: [{ + data: [1, 2, 3] + }] + } + }); + expect(Chart.getChart(chart.ctx)).toBe(chart); + }); + + it('should return undefined when a chart is not found or bad data is provided', function() { + expect(Chart.getChart(1)).toBeUndefined(); + }); + }); + + describe('active elements', function() { + it('should set the active elements', function() { + var chart = acquireChart({ + type: 'pie', + data: { + datasets: [{ + data: [1, 2, 3], + borderColor: 'red', + hoverBorderColor: 'blue', + }] + } + }); + + const meta = chart.getDatasetMeta(0); + let props = meta.data[0].getProps(['borderColor']); + expect(props.options.borderColor).toEqual('red'); + + chart.setActiveElements([{ + datasetIndex: 0, + index: 0, + }]); + + props = meta.data[0].getProps(['borderColor']); + expect(props.options.borderColor).toEqual('blue'); + + const active = chart.getActiveElements(); + expect(active.length).toEqual(1); + expect(active[0].element).toBe(meta.data[0]); + }); + }); + + it('should not replace the user set active elements by event replay', async function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: [1, 2, 3], + datasets: [{ + data: [1, 2, 3], + borderColor: 'red', + hoverBorderColor: 'blue', + }] + } + }); + + const meta = chart.getDatasetMeta(0); + const point0 = meta.data[0]; + const point1 = meta.data[1]; + + let props = meta.data[0].getProps(['borderColor']); + expect(props.options.borderColor).toEqual('red'); + + await jasmine.triggerMouseEvent(chart, 'mousemove', {x: point0.x, y: point0.y}); + expect(chart.getActiveElements()).toEqual([{datasetIndex: 0, index: 0, element: point0}]); + expect(point0.options.borderColor).toEqual('blue'); + expect(point1.options.borderColor).toEqual('red'); + + chart.setActiveElements([{datasetIndex: 0, index: 1}]); + expect(chart.getActiveElements()).toEqual([{datasetIndex: 0, index: 1, element: point1}]); + expect(point0.options.borderColor).toEqual('red'); + expect(point1.options.borderColor).toEqual('blue'); + + chart.update(); + expect(chart.getActiveElements()).toEqual([{datasetIndex: 0, index: 1, element: point1}]); + expect(point0.options.borderColor).toEqual('red'); + expect(point1.options.borderColor).toEqual('blue'); + }); + + describe('platform', function() { + it('should use the platform constructor provided in config', function() { + const chart = acquireChart({ + platform: Chart.platforms.BasicPlatform, + type: 'line', + }); + expect(chart.platform).toBeInstanceOf(Chart.platforms.BasicPlatform); + }); + }); +}); diff --git a/test/specs/core.datasetController.tests.js b/test/specs/core.datasetController.tests.js new file mode 100644 index 00000000000..18011efcb95 --- /dev/null +++ b/test/specs/core.datasetController.tests.js @@ -0,0 +1,1310 @@ +describe('Chart.DatasetController', function() { + describe('auto', jasmine.fixture.specs('core.datasetController')); + + it('should listen for dataset data insertions or removals', function() { + var data = [0, 1, 2, 3, 4, 5]; + var chart = acquireChart({ + type: 'line', + data: { + datasets: [{ + data: data + }] + } + }); + + var controller = chart.getDatasetMeta(0).controller; + var methods = [ + '_onDataPush', + '_onDataPop', + '_onDataShift', + '_onDataSplice', + '_onDataUnshift' + ]; + + methods.forEach(function(method) { + spyOn(controller, method); + }); + + data.push(6, 7, 8); + data.push(9); + data.pop(); + data.shift(); + data.shift(); + data.shift(); + data.splice(1, 4, 10, 11); + data.unshift(12, 13, 14, 15); + data.unshift(16, 17); + + [2, 1, 3, 1, 2].forEach(function(expected, index) { + expect(controller[methods[index]].calls.count()).toBe(expected); + }); + }); + + it('should not try to delete non existent stacks', function() { + function createAndUpdateChart() { + var chart = acquireChart({ + data: { + labels: ['q'], + datasets: [ + { + id: 'dismissed', + label: 'Test before', + yAxisID: 'count', + data: [816], + type: 'bar', + stack: 'stack' + } + ] + }, + options: { + scales: { + count: { + axis: 'y', + type: 'linear' + } + } + } + }); + + chart.data = { + datasets: [ + { + id: 'tests', + yAxisID: 'count', + label: 'Test after', + data: [38300], + type: 'bar' + } + ], + labels: ['q'] + }; + + chart.update(); + } + + expect(createAndUpdateChart).not.toThrow(); + }); + + describe('inextensible data', function() { + it('should handle a frozen data object', function() { + function createChart() { + var data = Object.freeze([0, 1, 2, 3, 4, 5]); + expect(Object.isExtensible(data)).toBeFalsy(); + + var chart = acquireChart({ + type: 'line', + data: { + datasets: [{ + data: data + }] + } + }); + + var dataset = chart.data.datasets[0]; + dataset.data = Object.freeze([5, 4, 3, 2, 1, 0]); + expect(Object.isExtensible(dataset.data)).toBeFalsy(); + chart.update(); + + // Tests that the unlisten path also works for frozen objects + chart.destroy(); + } + + expect(createChart).not.toThrow(); + }); + + it('should handle a sealed data object', function() { + function createChart() { + var data = Object.seal([0, 1, 2, 3, 4, 5]); + expect(Object.isExtensible(data)).toBeFalsy(); + + var chart = acquireChart({ + type: 'line', + data: { + datasets: [{ + data: data + }] + } + }); + + var dataset = chart.data.datasets[0]; + dataset.data = Object.seal([5, 4, 3, 2, 1, 0]); + expect(Object.isExtensible(dataset.data)).toBeFalsy(); + chart.update(); + + // Tests that the unlisten path also works for frozen objects + chart.destroy(); + } + + expect(createChart).not.toThrow(); + }); + + it('should handle an unextendable data object', function() { + function createChart() { + var data = Object.preventExtensions([0, 1, 2, 3, 4, 5]); + expect(Object.isExtensible(data)).toBeFalsy(); + + var chart = acquireChart({ + type: 'line', + data: { + datasets: [{ + data: data + }] + } + }); + + var dataset = chart.data.datasets[0]; + dataset.data = Object.preventExtensions([5, 4, 3, 2, 1, 0]); + expect(Object.isExtensible(dataset.data)).toBeFalsy(); + chart.update(); + + // Tests that the unlisten path also works for frozen objects + chart.destroy(); + } + + expect(createChart).not.toThrow(); + }); + }); + + it('should parse data using correct scales', function() { + const data1 = [0, 1, 2, 3, 4, 5]; + const data2 = ['a', 'b', 'c', 'd', 'a']; + const chart = acquireChart({ + type: 'line', + data: { + datasets: [ + {data: data1}, + {data: data2, xAxisID: 'x2', yAxisID: 'y2'} + ] + }, + options: { + scales: { + x: { + type: 'category', + labels: ['one', 'two', 'three', 'four', 'five', 'six'] + }, + x2: { + type: 'logarithmic', + labels: ['1', '10', '100', '1000', '2000'] + }, + y: { + type: 'linear' + }, + y2: { + type: 'category', + labels: ['a', 'b', 'c', 'd', 'e'] + } + } + } + }); + + const meta1 = chart.getDatasetMeta(0); + const parsedXValues1 = meta1._parsed.map(p => p.x); + const parsedYValues1 = meta1._parsed.map(p => p.y); + + expect(meta1.data.length).toBe(6); + expect(parsedXValues1).toEqual([0, 1, 2, 3, 4, 5]); // label indices + expect(parsedYValues1).toEqual(data1); + + const meta2 = chart.getDatasetMeta(1); + const parsedXValues2 = meta2._parsed.map(p => p.x); + const parsedYValues2 = meta2._parsed.map(p => p.y); + + expect(meta2.data.length).toBe(5); + expect(parsedXValues2).toEqual([1, 10, 100, 1000, 2000]); // logarithmic scale labels + expect(parsedYValues2).toEqual([0, 1, 2, 3, 0]); // label indices + }); + + it('should parse using provided keys', function() { + const chart = acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [ + {x: 1, data: {key: 'one', value: 20}}, + {data: {key: 'two', value: 30}} + ] + }] + }, + options: { + parsing: { + xAxisKey: 'data.key', + yAxisKey: 'data.value' + }, + scales: { + x: { + type: 'category', + labels: ['one', 'two'] + }, + y: { + type: 'linear' + }, + } + } + }); + + const meta = chart.getDatasetMeta(0); + const parsedXValues = meta._parsed.map(p => p.x); + const parsedYValues = meta._parsed.map(p => p.y); + + expect(meta.data.length).toBe(2); + expect(parsedXValues).toEqual([0, 1]); // label indices + expect(parsedYValues).toEqual([20, 30]); + }); + + describe('labels array synchronization', function() { + const data1 = [ + {x: 'One', name: 'One', y: 1, value: 1}, + {x: 'Two', name: 'Two', y: 2, value: 2} + ]; + const data2 = [ + {x: 'Three', name: 'Three', y: 3, value: 3}, + {x: 'Four', name: 'Four', y: 4, value: 4}, + {x: 'Five', name: 'Five', y: 5, value: 5} + ]; + [ + true, + false, + { + xAxisKey: 'name', + yAxisKey: 'value' + } + ].forEach(function(parsing) { + describe('when parsing is ' + JSON.stringify(parsing), function() { + it('should remove old labels when data is updated', function() { + const chart = acquireChart({ + type: 'line', + data: { + datasets: [{ + data: data1 + }] + }, + options: { + parsing + } + }); + + chart.data.datasets[0].data = data2; + chart.update(); + + const meta = chart.getDatasetMeta(0); + const labels = meta.iScale.getLabels(); + expect(labels).toEqual(data2.map(n => n.x)); + }); + + it('should not remove any user added labels', function() { + const chart = acquireChart({ + type: 'line', + data: { + datasets: [{ + data: data1 + }] + }, + options: { + parsing + } + }); + + chart.data.labels.push('user-added'); + chart.data.datasets[0].data = []; + chart.update(); + + const meta = chart.getDatasetMeta(0); + const labels = meta.iScale.getLabels(); + expect(labels).toEqual(['user-added']); + }); + + it('should not remove any user defined labels', function() { + const chart = acquireChart({ + type: 'line', + data: { + datasets: [{ + data: data1 + }], + labels: ['user1', 'user2'] + }, + options: { + parsing + } + }); + + const meta = chart.getDatasetMeta(0); + + expect(meta.iScale.getLabels()).toEqual(['user1', 'user2'].concat(data1.map(n => n.x))); + + chart.data.datasets[0].data = data2; + chart.update(); + + expect(meta.iScale.getLabels()).toEqual(['user1', 'user2'].concat(data2.map(n => n.x))); + }); + + it('should keep up with multiple datasets', function() { + const chart = acquireChart({ + type: 'line', + data: { + datasets: [{ + data: data1 + }, { + data: data2 + }], + labels: ['One', 'Three'] + }, + options: { + parsing + } + }); + + const scale = chart.scales.x; + + expect(scale.getLabels()).toEqual(['One', 'Three', 'Two', 'Four', 'Five']); + + chart.data.datasets[0].data = data2; + chart.data.datasets[1].data = data1; + chart.update(); + + expect(scale.getLabels()).toEqual(['One', 'Three', 'Four', 'Five', 'Two']); + }); + + }); + }); + }); + + + it('should synchronize metadata when data are inserted or removed and parsing is on', function() { + const data = [0, 1, 2, 3, 4, 5]; + const chart = acquireChart({ + type: 'line', + data: { + datasets: [{ + data: data + }] + } + }); + + const meta = chart.getDatasetMeta(0); + const parsedYValues = () => meta._parsed.map(p => p.y); + let first, second, last; + + first = meta.data[0]; + last = meta.data[5]; + data.push(6, 7, 8); + data.push(9); + chart.update(); + expect(meta.data.length).toBe(10); + expect(meta.data[0]).toBe(first); + expect(meta.data[5]).toBe(last); + expect(parsedYValues()).toEqual(data); + + last = meta.data[9]; + data.pop(); + chart.update(); + expect(meta.data.length).toBe(9); + expect(meta.data[0]).toBe(first); + expect(meta.data.indexOf(last)).toBe(-1); + expect(parsedYValues()).toEqual(data); + + last = meta.data[8]; + data.shift(); + data.shift(); + data.shift(); + chart.update(); + expect(meta.data.length).toBe(6); + expect(meta.data.indexOf(first)).toBe(-1); + expect(meta.data[5]).toBe(last); + expect(parsedYValues()).toEqual(data); + + first = meta.data[0]; + second = meta.data[1]; + last = meta.data[5]; + data.splice(1, 4, 10, 11); + chart.update(); + expect(meta.data.length).toBe(4); + expect(meta.data[0]).toBe(first); + expect(meta.data[3]).toBe(last); + expect(meta.data.indexOf(second)).toBe(-1); + expect(parsedYValues()).toEqual(data); + + data.unshift(12, 13, 14, 15); + data.unshift(16, 17); + chart.update(); + expect(meta.data.length).toBe(10); + expect(meta.data[6]).toBe(first); + expect(meta.data[9]).toBe(last); + expect(parsedYValues()).toEqual(data); + }); + + it('should synchronize metadata when data are inserted or removed and parsing is off', function() { + var data = [{x: 0, y: 0}, {x: 1, y: 1}, {x: 2, y: 2}, {x: 3, y: 3}, {x: 4, y: 4}, {x: 5, y: 5}]; + var chart = acquireChart({ + type: 'line', + data: { + datasets: [{ + data: data + }] + }, + options: { + parsing: false, + scales: { + x: {type: 'linear'}, + y: {type: 'linear'} + } + } + }); + + var meta = chart.getDatasetMeta(0); + var controller = meta.controller; + var first, last; + + first = controller.getParsed(0); + last = controller.getParsed(5); + data.push({x: 6, y: 6}, {x: 7, y: 7}, {x: 8, y: 8}); + data.push({x: 9, y: 9}); + chart.update(); + expect(meta.data.length).toBe(10); + expect(controller.getParsed(0)).toBe(first); + expect(controller.getParsed(5)).toBe(last); + + last = controller.getParsed(9); + data.pop(); + chart.update(); + expect(meta.data.length).toBe(9); + expect(controller.getParsed(0)).toBe(first); + expect(controller.getParsed(9)).toBe(undefined); + expect(controller.getParsed(8)).toEqual({x: 8, y: 8}); + + last = controller.getParsed(8); + data.shift(); + data.shift(); + data.shift(); + chart.update(); + expect(meta.data.length).toBe(6); + expect(controller.getParsed(5)).toBe(last); + + first = controller.getParsed(0); + last = controller.getParsed(5); + data.splice(1, 4, {x: 10, y: 10}, {x: 11, y: 11}); + chart.update(); + expect(meta.data.length).toBe(4); + expect(controller.getParsed(0)).toBe(first); + expect(controller.getParsed(3)).toBe(last); + expect(controller.getParsed(1)).toEqual({x: 10, y: 10}); + + data.unshift({x: 12, y: 12}, {x: 13, y: 13}, {x: 14, y: 14}, {x: 15, y: 15}); + data.unshift({x: 16, y: 16}, {x: 17, y: 17}); + chart.update(); + expect(meta.data.length).toBe(10); + expect(controller.getParsed(6)).toBe(first); + expect(controller.getParsed(9)).toBe(last); + }); + + it('should synchronize insert before removal when parsing is off', function() { + // https://github.com/chartjs/Chart.js/issues/9511 + const data = [{x: 0, y: 1}, {x: 2, y: 7}, {x: 3, y: 5}]; + var chart = acquireChart({ + type: 'scatter', + data: { + datasets: [{ + data: data, + }], + }, + options: { + parsing: false, + scales: { + x: { + type: 'linear', + min: 0, + max: 10, + }, + y: { + type: 'linear', + min: 0, + max: 10, + }, + }, + }, + }); + + var meta = chart.getDatasetMeta(0); + var controller = meta.controller; + + data.push({ + x: 10, + y: 6 + }); + data.splice(0, 1); + chart.update(); + + expect(meta.data.length).toBe(3); + expect(controller.getParsed(0)).toBe(data[0]); + expect(controller.getParsed(2)).toBe(data[2]); + }); + + it('should re-synchronize metadata when the data object reference changes', function() { + var data0 = [0, 1, 2, 3, 4, 5]; + var data1 = [6, 7, 8]; + var data2 = [1, 2, 3, 4, 5, 6, 7, 8]; + + var chart = acquireChart({ + type: 'line', + data: { + datasets: [{ + data: data0 + }] + } + }); + + var meta = chart.getDatasetMeta(0); + + expect(meta.data.length).toBe(6); + expect(meta._parsed.map(p => p.y)).toEqual(data0); + const point0 = meta.data[0]; + + chart.data.datasets[0].data = data1; + chart.update(); + + expect(meta.data.length).toBe(3); + expect(meta._parsed.map(p => p.y)).toEqual(data1); + expect(meta.data[0]).toEqual(point0); + + data1.push(9); + chart.update(); + expect(meta.data.length).toBe(4); + + chart.data.datasets[0].data = data0; + chart.update(); + + expect(meta.data.length).toBe(6); + expect(meta._parsed.map(p => p.y)).toEqual(data0); + + chart.data.datasets[0].data = data2; + chart.update(); + + expect(meta.data.length).toBe(8); + expect(meta._parsed.map(p => p.y)).toEqual(data2); + }); + + it('should re-synchronize metadata when the data object reference changes, with animation', function() { + var data0 = [0, 1, 2, 3, 4, 5]; + var data1 = [6, 7, 8]; + var data2 = [1, 2, 3, 4, 5, 6, 7, 8]; + + var chart = acquireChart({ + type: 'line', + data: { + datasets: [{ + data: data0 + }] + }, + options: { + animation: true + } + }); + + var meta = chart.getDatasetMeta(0); + + expect(meta.data.length).toBe(6); + expect(meta._parsed.map(p => p.y)).toEqual(data0); + const point0 = meta.data[0]; + + chart.data.datasets[0].data = data1; + chart.update(); + + expect(meta.data.length).toBe(3); + expect(meta._parsed.map(p => p.y)).toEqual(data1); + expect(meta.data[0]).toEqual(point0); + + data1.push(9); + chart.update(); + expect(meta.data.length).toBe(4); + + chart.data.datasets[0].data = data0; + chart.update(); + + expect(meta.data.length).toBe(6); + expect(meta._parsed.map(p => p.y)).toEqual(data0); + + chart.data.datasets[0].data = data2; + chart.update(); + + expect(meta.data.length).toBe(8); + expect(meta._parsed.map(p => p.y)).toEqual(data2); + }); + + it('should re-synchronize metadata when data are unusually altered', function() { + var data = [0, 1, 2, 3, 4, 5]; + var chart = acquireChart({ + type: 'line', + data: { + datasets: [{ + data: data + }] + } + }); + + var meta = chart.getDatasetMeta(0); + + expect(meta.data.length).toBe(6); + + data.length = 2; + chart.update(); + + expect(meta.data.length).toBe(2); + + data.length = 42; + chart.update(); + + expect(meta.data.length).toBe(42); + }); + + // https://github.com/chartjs/Chart.js/issues/7243 + it('should re-synchronize metadata when data is moved and values are equal', function() { + var data = [10, 10, 10, 10, 10, 10]; + var chart = acquireChart({ + type: 'line', + data: { + labels: ['a', 'b', 'c', 'd', 'e', 'f'], + datasets: [{ + data, + fill: true + }] + } + }); + + var meta = chart.getDatasetMeta(0); + + expect(meta.data.length).toBe(6); + const firstX = meta.data[0].x; + + data.push(data.shift()); + chart.update(); + + expect(meta.data.length).toBe(6); + expect(meta.data[0].x).toEqual(firstX); + }); + + // https://github.com/chartjs/Chart.js/issues/7445 + it('should re-synchronize metadata when data is objects and directly altered', function() { + var data = [{x: 'a', y: 1}, {x: 'b', y: 2}, {x: 'c', y: 3}]; + var chart = acquireChart({ + type: 'line', + data: { + labels: ['a', 'b', 'c'], + datasets: [{ + data, + fill: true + }] + } + }); + + var meta = chart.getDatasetMeta(0); + + expect(meta.data.length).toBe(3); + const y3 = meta.data[2].y; + + data[0].y = 3; + chart.update(); + expect(meta.data[0].y).toEqual(y3); + }); + + it('should re-synchronize metadata when scaleID changes', function() { + var chart = acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [], + xAxisID: 'firstXScaleID', + yAxisID: 'firstYScaleID', + }] + }, + options: { + scales: { + firstXScaleID: { + type: 'category', + position: 'bottom' + }, + secondXScaleID: { + type: 'category', + position: 'bottom' + }, + firstYScaleID: { + type: 'linear', + position: 'left' + }, + secondYScaleID: { + type: 'linear', + position: 'left' + }, + } + } + }); + + var meta = chart.getDatasetMeta(0); + + expect(meta.xAxisID).toBe('firstXScaleID'); + expect(meta.yAxisID).toBe('firstYScaleID'); + + chart.data.datasets[0].xAxisID = 'secondXScaleID'; + chart.data.datasets[0].yAxisID = 'secondYScaleID'; + chart.update(); + + expect(meta.xAxisID).toBe('secondXScaleID'); + expect(meta.yAxisID).toBe('secondYScaleID'); + }); + + it('should re-synchronize stacks when stack is changed', function() { + var chart = acquireChart({ + type: 'bar', + data: { + labels: ['a', 'b'], + datasets: [{ + data: [1, 10], + stack: '1' + }, { + data: [2, 20], + stack: '2' + }, { + data: [3, 30], + stack: '1' + }] + } + }); + + expect(chart._stacks).toEqual({ + 'x.y.1': { + 0: {0: 1, 2: 3, _top: 2, _bottom: null, _visualValues: {0: 1, 2: 3}}, + 1: {0: 10, 2: 30, _top: 2, _bottom: null, _visualValues: {0: 10, 2: 30}} + }, + 'x.y.2': { + 0: {1: 2, _top: 1, _bottom: null, _visualValues: {1: 2}}, + 1: {1: 20, _top: 1, _bottom: null, _visualValues: {1: 20}} + } + }); + + chart.data.datasets[2].stack = '2'; + chart.update(); + + expect(chart._stacks).toEqual({ + 'x.y.1': { + 0: {0: 1, _top: 2, _bottom: null, _visualValues: {0: 1}}, + 1: {0: 10, _top: 2, _bottom: null, _visualValues: {0: 10}} + }, + 'x.y.2': { + 0: {1: 2, 2: 3, _top: 2, _bottom: null, _visualValues: {1: 2, 2: 3}}, + 1: {1: 20, 2: 30, _top: 2, _bottom: null, _visualValues: {1: 20, 2: 30}} + } + }); + }); + + it('should re-synchronize stacks when data is removed', function() { + var chart = acquireChart({ + type: 'bar', + data: { + labels: ['a', 'b'], + datasets: [{ + data: [1, 10], + stack: '1' + }, { + data: [2, 20], + stack: '2' + }, { + data: [3, 30], + stack: '1' + }] + } + }); + + expect(chart._stacks).toEqual({ + 'x.y.1': { + 0: {0: 1, 2: 3, _top: 2, _bottom: null, _visualValues: {0: 1, 2: 3}}, + 1: {0: 10, 2: 30, _top: 2, _bottom: null, _visualValues: {0: 10, 2: 30}} + }, + 'x.y.2': { + 0: {1: 2, _top: 1, _bottom: null, _visualValues: {1: 2}}, + 1: {1: 20, _top: 1, _bottom: null, _visualValues: {1: 20}} + } + }); + + chart.data.datasets[2].data = [4]; + chart.update(); + + expect(chart._stacks).toEqual({ + 'x.y.1': { + 0: {0: 1, 2: 4, _top: 2, _bottom: null, _visualValues: {0: 1, 2: 4}}, + 1: {0: 10, _top: 2, _bottom: null, _visualValues: {0: 10}} + }, + 'x.y.2': { + 0: {1: 2, _top: 1, _bottom: null, _visualValues: {1: 2}}, + 1: {1: 20, _top: 1, _bottom: null, _visualValues: {1: 20}} + } + }); + }); + + it('should cleanup attached properties when the reference changes or when the chart is destroyed', function() { + var data0 = [0, 1, 2, 3, 4, 5]; + var data1 = [6, 7, 8]; + var chart = acquireChart({ + type: 'line', + data: { + datasets: [{ + data: data0 + }] + } + }); + + var hooks = ['push', 'pop', 'shift', 'splice', 'unshift']; + + expect(data0._chartjs).toBeDefined(); + hooks.forEach(function(hook) { + expect(data0[hook]).not.toBe(Array.prototype[hook]); + }); + + expect(data1._chartjs).not.toBeDefined(); + hooks.forEach(function(hook) { + expect(data1[hook]).toBe(Array.prototype[hook]); + }); + + chart.data.datasets[0].data = data1; + chart.update(); + + expect(data0._chartjs).not.toBeDefined(); + hooks.forEach(function(hook) { + expect(data0[hook]).toBe(Array.prototype[hook]); + }); + + expect(data1._chartjs).toBeDefined(); + hooks.forEach(function(hook) { + expect(data1[hook]).not.toBe(Array.prototype[hook]); + }); + + chart.destroy(); + + expect(data1._chartjs).not.toBeDefined(); + hooks.forEach(function(hook) { + expect(data1[hook]).toBe(Array.prototype[hook]); + }); + }); + + it('should resolve data element options to the default color', function() { + var data0 = [0, 1, 2, 3, 4, 5]; + var oldColor = Chart.defaults.borderColor; + Chart.defaults.borderColor = 'red'; + var chart = acquireChart({ + type: 'line', + data: { + datasets: [{ + data: data0 + }] + } + }); + + var meta = chart.getDatasetMeta(0); + expect(meta.dataset.options.borderColor).toBe('red'); + expect(meta.data[0].options.borderColor).toBe('red'); + + // Reset old shared state + Chart.defaults.borderColor = oldColor; + }); + + it('should read parsing from options when default is false', function() { + const originalDefault = Chart.defaults.parsing; + Chart.defaults.parsing = false; + + var chart = acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [{t: 1, y: 0}] + }] + }, + options: { + parsing: { + xAxisKey: 't' + } + } + }); + + var meta = chart.getDatasetMeta(0); + expect(meta.data[0].x).not.toBeNaN(); + + // Reset old shared state + Chart.defaults.parsing = originalDefault; + }); + + it('should not fail to produce stacks when parsing is off', function() { + var chart = acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [{x: 1, y: 10}] + }, { + data: [{x: 1, y: 20}] + }] + }, + options: { + parsing: false, + scales: { + x: {stacked: true}, + y: {stacked: true} + } + } + }); + + var meta = chart.getDatasetMeta(0); + expect(meta._parsed[0]._stacks).toEqual(jasmine.objectContaining({y: {0: 10, 1: 20, _top: 1, _bottom: null, _visualValues: {0: 10, 1: 20}}})); + }); + + describe('resolveDataElementOptions', function() { + it('should cache options when possible', function() { + const chart = acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [1, 2, 3], + }] + }, + }); + + const controller = chart.getDatasetMeta(0).controller; + + expect(controller.enableOptionSharing).toBeTrue(); + + const opts0 = controller.resolveDataElementOptions(0); + const opts1 = controller.resolveDataElementOptions(1); + + expect(opts0 === opts1).toBeTrue(); + expect(opts0.$shared).toBeTrue(); + expect(Object.isFrozen(opts0)).toBeTrue(); + }); + + it('should not cache options when option sharing is disabled', function() { + const chart = acquireChart({ + type: 'radar', + data: { + datasets: [{ + data: [1, 2, 3], + }] + }, + }); + + const controller = chart.getDatasetMeta(0).controller; + + expect(controller.enableOptionSharing).toBeFalse(); + + const opts0 = controller.resolveDataElementOptions(0); + const opts1 = controller.resolveDataElementOptions(1); + + expect(opts0 === opts1).toBeFalse(); + expect(opts0.$shared).not.toBeTrue(); + expect(Object.isFrozen(opts0)).toBeFalse(); + }); + + it('should not cache options when functions are used', function() { + const chart = acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [1, 2, 3], + backgroundColor: () => 'red' + }] + }, + }); + + const controller = chart.getDatasetMeta(0).controller; + + const opts0 = controller.resolveDataElementOptions(0); + const opts1 = controller.resolveDataElementOptions(1); + + expect(opts0 === opts1).toBeFalse(); + expect(opts0.$shared).not.toBeTrue(); + expect(Object.isFrozen(opts0)).toBeFalse(); + }); + + it('should support nested scriptable options', function() { + const chart = acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [100, 120, 130], + fill: { + value: (ctx) => ctx.type === 'dataset' ? 75 : 0 + } + }] + }, + }); + + const controller = chart.getDatasetMeta(0).controller; + const opts = controller.resolveDatasetElementOptions(); + expect(opts).toEqualOptions({ + fill: { + value: 75 + } + }); + }); + + it('should support nested scriptable defaults', function() { + Chart.defaults.datasets.line.fill = { + value: (ctx) => ctx.type === 'dataset' ? 75 : 0 + }; + const chart = acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [100, 120, 130], + }] + }, + }); + + const controller = chart.getDatasetMeta(0).controller; + const opts = controller.resolveDatasetElementOptions(); + expect(opts).toEqualOptions({ + fill: { + value: 75 + } + }); + delete Chart.defaults.datasets.line.fill; + }); + + }); + + describe('_resolveAnimations', function() { + function animationsExpectations(anims, props) { + for (const [prop, opts] of Object.entries(props)) { + const anim = anims._properties.get(prop); + expect(anim).withContext(prop).toBeInstanceOf(Object); + if (anim) { + for (const [name, value] of Object.entries(opts)) { + expect(anim[name]).withContext('"' + name + '" of ' + JSON.stringify(anim)).toEqual(value); + } + } + } + } + + it('should resolve to empty Animations when globally disabled', function() { + const chart = acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [1], + animation: { + test: {duration: 10} + } + }] + }, + options: { + animation: false + } + }); + + const controller = chart.getDatasetMeta(0).controller; + + expect(controller._resolveAnimations(0)._properties.size).toEqual(0); + }); + + it('should resolve to empty Animations when disabled at dataset level', function() { + const chart = acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [1], + animation: false + }] + } + }); + + const controller = chart.getDatasetMeta(0).controller; + + expect(controller._resolveAnimations(0)._properties.size).toEqual(0); + }); + + it('should fallback properly', function() { + const chart = acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [1], + animation: { + duration: 200 + } + }, { + type: 'bar', + data: [2] + }] + }, + options: { + animation: { + delay: 100 + }, + animations: { + x: { + delay: 200 + } + }, + transitions: { + show: { + x: { + delay: 300 + } + } + }, + datasets: { + bar: { + animation: { + duration: 500 + } + } + } + } + }); + const controller = chart.getDatasetMeta(0).controller; + + expect(Chart.defaults.animation.duration).toEqual(1000); + + const def0 = controller._resolveAnimations(0, 'default', false); + animationsExpectations(def0, { + x: { + delay: 200, + duration: 200 + }, + y: { + delay: 100, + duration: 200 + } + }); + + const controller2 = chart.getDatasetMeta(1).controller; + const def1 = controller2._resolveAnimations(0, 'default', false); + animationsExpectations(def1, { + x: { + delay: 200, + duration: 500 + } + }); + }); + }); + + describe('getContext', function() { + it('should reflect updated data', function() { + var chart = acquireChart({ + type: 'scatter', + data: { + datasets: [{ + data: [{x: 1, y: 0}, {x: 2, y: '1'}] + }] + }, + }); + let meta = chart.getDatasetMeta(0); + + expect(meta.controller.getContext(undefined, true, 'test')).toEqual(jasmine.objectContaining({ + active: true, + datasetIndex: 0, + dataset: chart.data.datasets[0], + index: 0, + mode: 'test' + })); + expect(meta.controller.getContext(1, false, 'datatest')).toEqual(jasmine.objectContaining({ + active: false, + datasetIndex: 0, + dataset: chart.data.datasets[0], + dataIndex: 1, + element: meta.data[1], + index: 1, + parsed: {x: 2, y: 1}, + raw: {x: 2, y: '1'}, + mode: 'datatest' + })); + + chart.data.datasets[0].data[1].y = 5; + chart.update(); + + expect(meta.controller.getContext(1, false, 'datatest')).toEqual(jasmine.objectContaining({ + active: false, + datasetIndex: 0, + dataset: chart.data.datasets[0], + dataIndex: 1, + element: meta.data[1], + index: 1, + parsed: {x: 2, y: 5}, + raw: {x: 2, y: 5}, + mode: 'datatest' + })); + + chart.data.datasets = [{ + data: [{x: 0, y: 0}, {x: 1, y: 1}] + }]; + chart.update(); + // meta is re-created when dataset is replaced + meta = chart.getDatasetMeta(0); + + expect(meta.controller.getContext(undefined, false, 'test2')).toEqual(jasmine.objectContaining({ + active: false, + datasetIndex: 0, + dataset: chart.data.datasets[0], + index: 0, + mode: 'test2' + })); + expect(meta.controller.getContext(1, true, 'datatest2')).toEqual(jasmine.objectContaining({ + active: true, + datasetIndex: 0, + dataset: chart.data.datasets[0], + dataIndex: 1, + element: meta.data[1], + index: 1, + parsed: {x: 1, y: 1}, + raw: {x: 1, y: 1}, + mode: 'datatest2' + })); + + chart.data.datasets[0].data.unshift({x: -1, y: -1}); + chart.update(); + expect(meta.controller.getContext(0, true, 'unshift')).toEqual(jasmine.objectContaining({ + active: true, + datasetIndex: 0, + dataset: chart.data.datasets[0], + dataIndex: 0, + element: meta.data[0], + index: 0, + parsed: {x: -1, y: -1}, + raw: {x: -1, y: -1}, + mode: 'unshift' + })); + expect(meta.controller.getContext(2, true, 'unshift2')).toEqual(jasmine.objectContaining({ + active: true, + datasetIndex: 0, + dataset: chart.data.datasets[0], + dataIndex: 2, + element: meta.data[2], + index: 2, + parsed: {x: 1, y: 1}, + raw: {x: 1, y: 1}, + mode: 'unshift2' + })); + + chart.data.datasets.unshift({data: [{x: 10, y: 20}]}); + chart.update(); + meta = chart.getDatasetMeta(0); + expect(meta.controller.getContext(0, true, 'unshift3')).toEqual(jasmine.objectContaining({ + active: true, + datasetIndex: 0, + dataset: chart.data.datasets[0], + dataIndex: 0, + element: meta.data[0], + index: 0, + parsed: {x: 10, y: 20}, + raw: {x: 10, y: 20}, + mode: 'unshift3' + })); + + meta = chart.getDatasetMeta(1); + expect(meta.controller.getContext(2, true, 'unshift4')).toEqual(jasmine.objectContaining({ + active: true, + datasetIndex: 1, + dataset: chart.data.datasets[1], + dataIndex: 2, + element: meta.data[2], + index: 2, + parsed: {x: 1, y: 1}, + raw: {x: 1, y: 1}, + mode: 'unshift4' + })); + }); + }); +}); diff --git a/test/specs/core.defaults.tests.js b/test/specs/core.defaults.tests.js new file mode 100644 index 00000000000..5b22439bc61 --- /dev/null +++ b/test/specs/core.defaults.tests.js @@ -0,0 +1,38 @@ +describe('Chart.defaults', function() { + describe('.set', function() { + it('Should set defaults directly to root when scope is not provided', function() { + expect(Chart.defaults.test).toBeUndefined(); + Chart.defaults.set({test: true}); + expect(Chart.defaults.test).toEqual(true); + delete Chart.defaults.test; + }); + + it('Should create scope when it does not exist', function() { + expect(Chart.defaults.test).toBeUndefined(); + Chart.defaults.set('test', {value: true}); + expect(Chart.defaults.test.value).toEqual(true); + delete Chart.defaults.test; + }); + }); + + describe('.route', function() { + it('Should read the source, but not change it', function() { + expect(Chart.defaults.testscope).toBeUndefined(); + + Chart.defaults.set('testscope', {test: true}); + Chart.defaults.route('testscope', 'test2', 'testscope', 'test'); + + expect(Chart.defaults.testscope.test).toEqual(true); + expect(Chart.defaults.testscope.test2).toEqual(true); + + Chart.defaults.set('testscope', {test2: false}); + expect(Chart.defaults.testscope.test).toEqual(true); + expect(Chart.defaults.testscope.test2).toEqual(false); + + Chart.defaults.set('testscope', {test2: undefined}); + expect(Chart.defaults.testscope.test2).toEqual(true); + + delete Chart.defaults.testscope; + }); + }); +}); diff --git a/test/specs/core.element.tests.js b/test/specs/core.element.tests.js new file mode 100644 index 00000000000..07e8adbf073 --- /dev/null +++ b/test/specs/core.element.tests.js @@ -0,0 +1,16 @@ +describe('Chart.element', function() { + describe('getProps', function() { + it('should return requested properties', function() { + const elem = new Chart.Element(); + elem.x = 10; + elem.y = 1.5; + + expect(elem.getProps(['x', 'y'])).toEqual(jasmine.objectContaining({x: 10, y: 1.5})); + expect(elem.getProps(['x', 'y'], true)).toEqual(jasmine.objectContaining({x: 10, y: 1.5})); + + elem.$animations = {x: {active: () => true, _to: 20}}; + expect(elem.getProps(['x', 'y'])).toEqual(jasmine.objectContaining({x: 10, y: 1.5})); + expect(elem.getProps(['x', 'y'], true)).toEqual(jasmine.objectContaining({x: 20, y: 1.5})); + }); + }); +}); diff --git a/test/specs/core.helpers.tests.js b/test/specs/core.helpers.tests.js new file mode 100644 index 00000000000..30dcb98bff6 --- /dev/null +++ b/test/specs/core.helpers.tests.js @@ -0,0 +1,24 @@ +describe('Core helper tests', function() { + + var helpers; + + beforeAll(function() { + helpers = window.Chart.helpers; + }); + + it('should generate integer ids', function() { + var uid = helpers.uid(); + expect(uid).toEqual(jasmine.any(Number)); + expect(helpers.uid()).toBe(uid + 1); + expect(helpers.uid()).toBe(uid + 2); + expect(helpers.uid()).toBe(uid + 3); + }); + + describe('clone', function() { + it('should not allow prototype pollution', function() { + const test = helpers.clone(JSON.parse('{"__proto__":{"polluted": true}}')); + expect(test.prototype).toBeUndefined(); + expect(Object.prototype.polluted).toBeUndefined(); + }); + }); +}); diff --git a/test/specs/core.interaction.tests.js b/test/specs/core.interaction.tests.js new file mode 100644 index 00000000000..ab6377dc94d --- /dev/null +++ b/test/specs/core.interaction.tests.js @@ -0,0 +1,1025 @@ +describe('Core.Interaction', function() { + describe('auto', jasmine.fixture.specs('core.interaction')); + + describe('point mode', function() { + beforeEach(function() { + this.chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 20, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + } + }); + }); + + it ('should return all items under the point', function() { + var chart = this.chart; + var meta0 = chart.getDatasetMeta(0); + var meta1 = chart.getDatasetMeta(1); + var point = meta0.data[1]; + + var evt = { + type: 'click', + chart: chart, + native: true, // needed otherwise things its a DOM event + x: point.x, + y: point.y, + }; + + var elements = Chart.Interaction.modes.point(chart, evt, {}).map(item => item.element); + expect(elements).toEqual([point, meta1.data[1]]); + }); + + it ('should return an empty array when no items are found', function() { + var chart = this.chart; + var evt = { + type: 'click', + chart: chart, + native: true, // needed otherwise things its a DOM event + x: 0, + y: 0 + }; + + var elements = Chart.Interaction.modes.point(chart, evt, {}).map(item => item.element); + expect(elements).toEqual([]); + }); + }); + + describe('index mode', function() { + describe('intersect: true', function() { + beforeEach(function() { + this.chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + } + }); + }); + + it ('gets correct items', function() { + var chart = this.chart; + var meta0 = chart.getDatasetMeta(0); + var meta1 = chart.getDatasetMeta(1); + var point = meta0.data[1]; + + var evt = { + type: 'click', + chart: chart, + native: true, // needed otherwise things its a DOM event + x: point.x, + y: point.y, + }; + + var elements = Chart.Interaction.modes.index(chart, evt, {intersect: true}).map(item => item.element); + expect(elements).toEqual([point, meta1.data[1]]); + }); + + it ('returns empty array when nothing found', function() { + var chart = this.chart; + var evt = { + type: 'click', + chart: chart, + native: true, // needed otherwise things its a DOM event + x: 0, + y: 0, + }; + + var elements = Chart.Interaction.modes.index(chart, evt, {intersect: true}).map(item => item.element); + expect(elements).toEqual([]); + }); + }); + + describe ('intersect: false', function() { + var data = { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + }; + + beforeEach(function() { + this.chart = window.acquireChart({ + type: 'line', + data: data + }); + }); + + it ('axis: x gets correct items', function() { + var chart = this.chart; + var meta0 = chart.getDatasetMeta(0); + var meta1 = chart.getDatasetMeta(1); + + var evt = { + type: 'click', + chart: chart, + native: true, // needed otherwise things its a DOM event + x: chart.chartArea.left, + y: chart.chartArea.top + }; + + var elements = Chart.Interaction.modes.index(chart, evt, {intersect: false}).map(item => item.element); + expect(elements).toEqual([meta0.data[0], meta1.data[0]]); + }); + + it ('axis: y gets correct items', function() { + var chart = window.acquireChart({ + type: 'bar', + data: data, + options: { + indexAxis: 'y', + } + }); + + var meta0 = chart.getDatasetMeta(0); + var meta1 = chart.getDatasetMeta(1); + var center = meta0.data[0].getCenterPoint(); + + var evt = { + type: 'click', + chart: chart, + native: true, // needed otherwise things its a DOM event + x: center.x, + y: center.y + 30, + }; + + var elements = Chart.Interaction.modes.index(chart, evt, {axis: 'y', intersect: false}).map(item => item.element); + expect(elements).toEqual([meta0.data[0], meta1.data[0]]); + }); + + it ('axis: xy gets correct items', function() { + var chart = this.chart; + var meta0 = chart.getDatasetMeta(0); + var meta1 = chart.getDatasetMeta(1); + + var evt = { + type: 'click', + chart: chart, + native: true, // needed otherwise things its a DOM event + x: chart.chartArea.left, + y: chart.chartArea.top + }; + + var elements = Chart.Interaction.modes.index(chart, evt, {axis: 'xy', intersect: false}).map(item => item.element); + expect(elements).toEqual([meta0.data[0], meta1.data[0]]); + }); + }); + }); + + describe('dataset mode', function() { + describe('intersect: true', function() { + beforeEach(function() { + this.chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + } + }); + }); + + it ('should return all items in the dataset of the first item found', function() { + var chart = this.chart; + var meta = chart.getDatasetMeta(0); + var point = meta.data[1]; + + var evt = { + type: 'click', + chart: chart, + native: true, // needed otherwise things its a DOM event + x: point.x, + y: point.y + }; + + var elements = Chart.Interaction.modes.dataset(chart, evt, {intersect: true}).map(item => item.element); + expect(elements).toEqual(meta.data); + }); + + it ('should return an empty array if nothing found', function() { + var chart = this.chart; + var evt = { + type: 'click', + chart: chart, + native: true, // needed otherwise things its a DOM event + x: 0, + y: 0 + }; + + var elements = Chart.Interaction.modes.dataset(chart, evt, {intersect: true}); + expect(elements).toEqual([]); + }); + }); + + describe('intersect: false', function() { + var data = { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + }; + + beforeEach(function() { + this.chart = window.acquireChart({ + type: 'line', + data: data + }); + }); + + it ('axis: x gets correct items', function() { + var chart = window.acquireChart({ + type: 'bar', + data: data, + options: { + indexAxis: 'y', + } + }); + + var evt = { + type: 'click', + chart: chart, + native: true, // needed otherwise things its a DOM event + x: chart.chartArea.left, + y: chart.chartArea.top + }; + + var elements = Chart.Interaction.modes.dataset(chart, evt, {axis: 'x', intersect: false}).map(item => item.element); + expect(elements).toEqual(chart.getDatasetMeta(0).data); + }); + + it ('axis: y gets correct items', function() { + var chart = this.chart; + var evt = { + type: 'click', + chart: chart, + native: true, // needed otherwise things its a DOM event + x: chart.chartArea.left, + y: chart.chartArea.top + }; + + var elements = Chart.Interaction.modes.dataset(chart, evt, {axis: 'y', intersect: false}).map(item => item.element); + expect(elements).toEqual(chart.getDatasetMeta(1).data); + }); + + it ('axis: xy gets correct items', function() { + var chart = this.chart; + var evt = { + type: 'click', + chart: chart, + native: true, // needed otherwise things its a DOM event + x: chart.chartArea.left, + y: chart.chartArea.top + }; + + var elements = Chart.Interaction.modes.dataset(chart, evt, {intersect: false}).map(item => item.element); + expect(elements).toEqual(chart.getDatasetMeta(1).data); + }); + }); + }); + + describe('nearest mode', function() { + describe('intersect: false', function() { + beforeEach(function() { + this.lineChart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 40, 30], + pointRadius: [5, 5, 5], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointRadius: [10, 10, 10], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + } + }); + this.polarChart = window.acquireChart({ + type: 'polarArea', + data: { + datasets: [{ + data: [1, 9, 5] + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + }, + options: { + plugins: { + legend: { + display: false + }, + }, + } + }); + }); + + describe('axis: xy', function() { + it ('should return the nearest item', function() { + var chart = this.lineChart; + var evt = { + type: 'click', + chart: chart, + native: true, // needed otherwise things its a DOM event + x: chart.chartArea.left, + y: chart.chartArea.top + }; + + // Nearest to 0,0 (top left) will be first point of dataset 2 + var elements = Chart.Interaction.modes.nearest(chart, evt, {intersect: false}).map(item => item.element); + var meta = chart.getDatasetMeta(1); + expect(elements).toEqual([meta.data[0]]); + }); + + it ('should return all items at the same nearest distance', function() { + var chart = this.lineChart; + var meta0 = chart.getDatasetMeta(0); + var meta1 = chart.getDatasetMeta(1); + + // Halfway between 2 mid points + var pt = { + x: meta0.data[1].x, + y: (meta0.data[1].y + meta1.data[1].y) / 2 + }; + + var evt = { + type: 'click', + chart: chart, + native: true, // needed otherwise things its a DOM event + x: pt.x, + y: pt.y + }; + + // Both points are nearest + var elements = Chart.Interaction.modes.nearest(chart, evt, {intersect: false}).map(item => item.element); + expect(elements).toEqual([meta0.data[1], meta1.data[1]]); + }); + }); + + describe('axis: x', function() { + it ('should return all items at current x', function() { + var chart = this.lineChart; + var meta0 = chart.getDatasetMeta(0); + var meta1 = chart.getDatasetMeta(1); + + // At 'Point 2', 10 + var pt = { + x: meta0.data[1].x, + y: meta0.data[0].y + }; + + var evt = { + type: 'click', + chart: chart, + native: true, // needed otherwise things its a DOM event + x: pt.x, + y: pt.y + }; + + // Middle point from both series are nearest + var elements = Chart.Interaction.modes.nearest(chart, evt, {axis: 'x', intersect: false}).map(item => item.element); + expect(elements).toEqual([meta0.data[1], meta1.data[1]]); + }); + + it ('should return all items at nearest x-distance', function() { + var chart = this.lineChart; + var meta0 = chart.getDatasetMeta(0); + var meta1 = chart.getDatasetMeta(1); + + // Haflway between 'Point 1' and 'Point 2', y=10 + var pt = { + x: (meta0.data[0].x + meta0.data[1].x) / 2, + y: meta0.data[0].y + }; + + var evt = { + type: 'click', + chart: chart, + native: true, // needed otherwise things its a DOM event + x: pt.x, + y: pt.y + }; + + // Should return all (4) points from 'Point 1' and 'Point 2' + var elements = Chart.Interaction.modes.nearest(chart, evt, {axis: 'x', intersect: false}).map(item => item.element); + expect(elements).toEqual([meta0.data[0], meta0.data[1], meta1.data[0], meta1.data[1]]); + }); + }); + + describe('axis: y', function() { + it ('should return item with value 30', function() { + var chart = this.lineChart; + var meta0 = chart.getDatasetMeta(0); + + // 'Point 1', y = 30 + var pt = { + x: meta0.data[0].x, + y: meta0.data[2].y + }; + + var evt = { + type: 'click', + chart: chart, + native: true, // needed otherwise things its a DOM event + x: pt.x, + y: pt.y + }; + + // Middle point from both series are nearest + var elements = Chart.Interaction.modes.nearest(chart, evt, {axis: 'y', intersect: false}).map(item => item.element); + expect(elements).toEqual([meta0.data[2]]); + }); + + it ('should return all items at value 40', function() { + var chart = this.lineChart; + var meta0 = chart.getDatasetMeta(0); + var meta1 = chart.getDatasetMeta(1); + + // 'Point 1', y = 40 + var pt = { + x: meta0.data[0].x, + y: meta0.data[1].y + }; + + var evt = { + type: 'click', + chart: chart, + native: true, // needed otherwise things its a DOM event + x: pt.x, + y: pt.y + }; + + // Should return points with value 40 + var elements = Chart.Interaction.modes.nearest(chart, evt, {axis: 'y', intersect: false}).map(item => item.element); + expect(elements).toEqual([meta0.data[1], meta1.data[0], meta1.data[1], meta1.data[2]]); + }); + }); + + describe('axis: r', function() { + it ('should return item with value 9', function() { + var chart = this.polarChart; + var meta0 = chart.getDatasetMeta(0); + + var evt = { + type: 'click', + chart: chart, + native: true, // Needed, otherwise assumed to be a DOM event + x: chart.width / 2, + y: chart.height / 2 + 5, + }; + + var elements = Chart.Interaction.modes.nearest(chart, evt, {axis: 'r'}).map(item => item.element); + expect(elements).toEqual([meta0.data[1]]); + }); + + it ('should return item with value 1 when clicked outside of it', function() { + var chart = this.polarChart; + var meta0 = chart.getDatasetMeta(0); + + var evt = { + type: 'click', + chart: chart, + native: true, // Needed, otherwise assumed to be a DOM event + x: chart.width, + y: 0, + }; + + var elements = Chart.Interaction.modes.nearest(chart, evt, {axis: 'r', intersect: false}).map(item => item.element); + expect(elements).toEqual([meta0.data[0]]); + }); + }); + }); + + describe('intersect: true', function() { + beforeEach(function() { + this.chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + } + }); + }); + + describe('axis=xy', function() { + it ('should return the nearest item', function() { + var chart = this.chart; + var meta = chart.getDatasetMeta(1); + var point = meta.data[1]; + + var evt = { + type: 'click', + chart: chart, + native: true, // needed otherwise things its a DOM event + x: point.x + 15, + y: point.y + }; + + // Nothing intersects so find nothing + var elements = Chart.Interaction.modes.nearest(chart, evt, {intersect: true}).map(item => item.element); + expect(elements).toEqual([]); + + evt = { + type: 'click', + chart: chart, + native: true, + x: point.x, + y: point.y + }; + elements = Chart.Interaction.modes.nearest(chart, evt, {intersect: true}).map(item => item.element); + expect(elements).toEqual([point]); + }); + + it ('should return the nearest item even if 2 intersect', function() { + var chart = this.chart; + chart.data.datasets[0].pointRadius = [5, 30, 5]; + chart.data.datasets[0].data[1] = 39; + + chart.data.datasets[1].pointRadius = [10, 10, 10]; + + chart.update(); + + // Trigger an event over top of the + var meta0 = chart.getDatasetMeta(0); + + // Halfway between 2 mid points + var pt = { + x: meta0.data[1].x, + y: meta0.data[1].y + }; + + var evt = { + type: 'click', + chart: chart, + native: true, // needed otherwise things its a DOM event + x: pt.x, + y: pt.y + }; + + var elements = Chart.Interaction.modes.nearest(chart, evt, {intersect: true}).map(item => item.element); + expect(elements).toEqual([meta0.data[1]]); + }); + + it ('should return the all items if more than 1 are at the same distance', function() { + var chart = this.chart; + chart.data.datasets[0].pointRadius = [5, 5, 5]; + chart.data.datasets[0].data[1] = 40; + + chart.data.datasets[1].pointRadius = [10, 10, 10]; + + chart.update(); + + var meta0 = chart.getDatasetMeta(0); + var meta1 = chart.getDatasetMeta(1); + + // Halfway between 2 mid points + var pt = { + x: meta0.data[1].x, + y: meta0.data[1].y + }; + + var evt = { + type: 'click', + chart: chart, + native: true, // needed otherwise things its a DOM event + x: pt.x, + y: pt.y + }; + + var elements = Chart.Interaction.modes.nearest(chart, evt, {intersect: true}).map(item => item.element); + expect(elements).toEqual([meta0.data[1], meta1.data[1]]); + }); + }); + }); + }); + + describe('x mode', function() { + beforeEach(function() { + this.chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 40, 30], + pointRadius: [5, 10, 5], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointRadius: [10, 10, 10], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + } + }); + }); + + it('should return items at the same x value when intersect is false', function() { + var chart = this.chart; + var meta0 = chart.getDatasetMeta(0); + var meta1 = chart.getDatasetMeta(1); + + // Halfway between 2 mid points + var pt = { + x: meta0.data[1].x, + y: meta0.data[1].y + }; + + var evt = { + type: 'click', + chart: chart, + native: true, // needed otherwise things its a DOM event + x: pt.x, + y: 0 + }; + + var elements = Chart.Interaction.modes.x(chart, evt, {intersect: false}).map(item => item.element); + expect(elements).toEqual([meta0.data[1], meta1.data[1]]); + + evt = { + type: 'click', + chart: chart, + native: true, // needed otherwise things its a DOM event + x: pt.x + 20, + y: 0 + }; + + elements = Chart.Interaction.modes.x(chart, evt, {intersect: false}).map(item => item.element); + expect(elements).toEqual([]); + }); + + it('should return items at the same x value when intersect is true', function() { + var chart = this.chart; + var meta0 = chart.getDatasetMeta(0); + var meta1 = chart.getDatasetMeta(1); + + // Halfway between 2 mid points + var pt = { + x: meta0.data[1].x, + y: meta0.data[1].y + }; + + var evt = { + type: 'click', + chart: chart, + native: true, // needed otherwise things its a DOM event + x: pt.x, + y: 0 + }; + + var elements = Chart.Interaction.modes.x(chart, evt, {intersect: true}).map(item => item.element); + expect(elements).toEqual([]); // we don't intersect anything + + evt = { + type: 'click', + chart: chart, + native: true, // needed otherwise things its a DOM event + x: pt.x, + y: pt.y + }; + + elements = Chart.Interaction.modes.x(chart, evt, {intersect: true}).map(item => item.element); + expect(elements).toEqual([meta0.data[1], meta1.data[1]]); + }); + }); + + describe('y mode', function() { + beforeEach(function() { + this.chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 40, 30], + pointRadius: [5, 10, 5], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointRadius: [10, 10, 10], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + } + }); + }); + + it('should return items at the same y value when intersect is false', function() { + var chart = this.chart; + var meta0 = chart.getDatasetMeta(0); + var meta1 = chart.getDatasetMeta(1); + + // Halfway between 2 mid points + var pt = { + x: meta0.data[1].x, + y: meta0.data[1].y + }; + + var evt = { + type: 'click', + chart: chart, + native: true, + x: 0, + y: pt.y, + }; + + var elements = Chart.Interaction.modes.y(chart, evt, {intersect: false}).map(item => item.element); + expect(elements).toEqual([meta0.data[1], meta1.data[0], meta1.data[1], meta1.data[2]]); + + evt = { + type: 'click', + chart: chart, + native: true, + x: pt.x, + y: pt.y + 20, // out of range + }; + + elements = Chart.Interaction.modes.y(chart, evt, {intersect: false}).map(item => item.element); + expect(elements).toEqual([]); + }); + + it('should return items at the same y value when intersect is true', function() { + var chart = this.chart; + var meta0 = chart.getDatasetMeta(0); + var meta1 = chart.getDatasetMeta(1); + + // Halfway between 2 mid points + var pt = { + x: meta0.data[1].x, + y: meta0.data[1].y + }; + + var evt = { + type: 'click', + chart: chart, + native: true, + x: 0, + y: pt.y + }; + + var elements = Chart.Interaction.modes.y(chart, evt, {intersect: true}).map(item => item.element); + expect(elements).toEqual([]); // we don't intersect anything + + evt = { + type: 'click', + chart: chart, + native: true, + x: pt.x, + y: pt.y, + }; + + elements = Chart.Interaction.modes.y(chart, evt, {intersect: true}).map(item => item.element); + expect(elements).toEqual([meta0.data[1], meta1.data[0], meta1.data[1], meta1.data[2]]); + }); + }); + + describe('tooltip element of scatter chart', function() { + it ('out-of-range datapoints are not shown in tooltip', function() { + let data = []; + for (let i = 0; i < 1000; i++) { + data.push({x: i, y: i}); + } + + const chart = window.acquireChart({ + type: 'scatter', + data: { + datasets: [{data}] + }, + options: { + scales: { + x: { + min: 2 + } + } + } + }); + + const meta0 = chart.getDatasetMeta(0); + const firstElement = meta0.data[0]; + + const evt = { + type: 'click', + chart: chart, + native: true, // needed otherwise things its a DOM event + x: firstElement.x, + y: firstElement.y + }; + + const elements = Chart.Interaction.modes.point(chart, evt, {intersect: true}).map(item => item.element); + expect(elements).not.toContain(firstElement); + }); + + it ('out-of-range datapoints are shown in tooltip if included', function() { + let data = []; + for (let i = 0; i < 1000; i++) { + data.push({x: i, y: i}); + } + + const chart = window.acquireChart({ + type: 'scatter', + data: { + datasets: [{data}] + }, + options: { + scales: { + x: { + min: 2 + } + } + } + }); + + const meta0 = chart.getDatasetMeta(0); + const firstElement = meta0.data[0]; + + const evt = { + type: 'click', + chart: chart, + native: true, // needed otherwise it thinks its a DOM event + x: firstElement.x, + y: firstElement.y + }; + + const elements = Chart.Interaction.modes.point( + chart, + evt, + { + intersect: true, + includeInvisible: true + }).map(item => item.element); + expect(elements).toContain(firstElement); + }); + }); + + const testCases = [ + { + data: [12, 19, null, null, null, null, 5, 2], + clickPointIndex: 0, + expectedNearestPointIndex: 0 + }, + { + data: [12, 19, null, null, null, null, 5, 2], + clickPointIndex: 1, + expectedNearestPointIndex: 1}, + { + data: [12, 19, null, null, null, null, 5, 2], + clickPointIndex: 2, + expectedNearestPointIndex: 1 + }, + { + data: [12, 19, null, null, null, null, 5, 2], + clickPointIndex: 3, + expectedNearestPointIndex: 1 + }, + { + data: [12, 19, null, null, null, null, 5, 2], + clickPointIndex: 4, + expectedNearestPointIndex: 6 + }, + { + data: [12, 19, null, null, null, null, 5, 2], + clickPointIndex: 5, + expectedNearestPointIndex: 6 + }, + { + data: [12, 19, null, null, null, null, 5, 2], + clickPointIndex: 6, + expectedNearestPointIndex: 6 + }, + { + data: [12, 19, null, null, null, null, 5, 2], + clickPointIndex: 7, + expectedNearestPointIndex: 7 + }, + { + data: [12, 0, null, null, null, null, 0, 2], + clickPointIndex: 3, + expectedNearestPointIndex: 1 + }, + { + data: [12, 0, null, null, null, null, 0, 2], + clickPointIndex: 4, + expectedNearestPointIndex: 6 + }, + { + data: [12, -1, null, null, null, null, -1, 2], + clickPointIndex: 3, + expectedNearestPointIndex: 1 + }, + { + data: [12, -1, null, null, null, null, -1, 2], + clickPointIndex: 4, + expectedNearestPointIndex: 6 + }, + { + data: [null, 2], + clickPointIndex: 0, + expectedNearestPointIndex: 1 + }, + { + data: [2, null], + clickPointIndex: 1, + expectedNearestPointIndex: 0 + }, + { + data: [null, null, 2], + clickPointIndex: 0, + expectedNearestPointIndex: 2 + }, + { + data: [2, null, null], + clickPointIndex: 2, + expectedNearestPointIndex: 0 + } + ]; + testCases.forEach(({data, clickPointIndex, expectedNearestPointIndex}, i) => { + it(`should select nearest non-null element with index ${expectedNearestPointIndex} when clicking on element with index ${clickPointIndex} in test case ${i + 1} if spanGaps=true`, function() { + const chart = window.acquireChart({ + type: 'line', + data: { + labels: [1, 2, 3, 4, 5, 6, 7, 8, 9], + datasets: [{ + data: data, + spanGaps: true, + }] + } + }); + chart.update(); + const meta = chart.getDatasetMeta(0); + const point = meta.data[clickPointIndex]; + + const evt = { + type: 'click', + chart: chart, + native: true, // needed otherwise things its a DOM event + x: point.x, + y: point.y, + }; + + const elements = Chart.Interaction.modes.nearest(chart, evt, {axis: 'x', intersect: false}).map(item => item.element); + expect(elements).toEqual([meta.data[expectedNearestPointIndex]]); + }); + }); +}); diff --git a/test/specs/core.layouts.tests.js b/test/specs/core.layouts.tests.js new file mode 100644 index 00000000000..c484f8737da --- /dev/null +++ b/test/specs/core.layouts.tests.js @@ -0,0 +1,572 @@ +function getLabels(scale) { + return scale.ticks.map(t => t.label); +} + +describe('Chart.layouts', function() { + describe('auto', jasmine.fixture.specs('core.layouts')); + + it('should be exposed through Chart.layouts', function() { + expect(Chart.layouts).toBeDefined(); + expect(typeof Chart.layouts).toBe('object'); + expect(Chart.layouts.addBox).toBeDefined(); + expect(Chart.layouts.removeBox).toBeDefined(); + expect(Chart.layouts.configure).toBeDefined(); + expect(Chart.layouts.update).toBeDefined(); + }); + + it('should fit a simple chart with 2 scales', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [ + {data: [10, 5, 0, 25, 78, -10]} + ], + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] + } + }, { + canvas: { + height: 150, + width: 250 + } + }); + + expect(chart.chartArea.bottom).toBeCloseToPixel(120); + expect(chart.chartArea.left).toBeCloseToPixel(31); + expect(chart.chartArea.right).toBeCloseToPixel(250); + expect(chart.chartArea.top).toBeCloseToPixel(32); + + // Is xScale at the right spot + expect(chart.scales.x.bottom).toBeCloseToPixel(150); + expect(chart.scales.x.left).toBeCloseToPixel(31); + expect(chart.scales.x.right).toBeCloseToPixel(250); + expect(chart.scales.x.top).toBeCloseToPixel(120); + expect(chart.scales.x.labelRotation).toBeCloseTo(0); + + // Is yScale at the right spot + expect(chart.scales.y.bottom).toBeCloseToPixel(120); + expect(chart.scales.y.left).toBeCloseToPixel(0); + expect(chart.scales.y.right).toBeCloseToPixel(31); + expect(chart.scales.y.top).toBeCloseToPixel(32); + expect(chart.scales.y.labelRotation).toBeCloseTo(0); + }); + + it('should fit scales that are in the top and right positions', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [ + {data: [10, 5, 0, 25, 78, -10]} + ], + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] + }, + options: { + scales: { + x: { + type: 'category', + position: 'top' + }, + y: { + type: 'linear', + position: 'right' + } + } + } + }, { + canvas: { + height: 150, + width: 250 + } + }); + + expect(chart.chartArea.bottom).toBeCloseToPixel(139); + expect(chart.chartArea.left).toBeCloseToPixel(0); + expect(chart.chartArea.right).toBeCloseToPixel(218); + expect(chart.chartArea.top).toBeCloseToPixel(62); + + // Is xScale at the right spot + expect(chart.scales.x.bottom).toBeCloseToPixel(62); + expect(chart.scales.x.left).toBeCloseToPixel(0); + expect(chart.scales.x.right).toBeCloseToPixel(218); + expect(chart.scales.x.top).toBeCloseToPixel(32); + expect(chart.scales.x.labelRotation).toBeCloseTo(0); + + // Is yScale at the right spot + expect(chart.scales.y.bottom).toBeCloseToPixel(139); + expect(chart.scales.y.left).toBeCloseToPixel(218); + expect(chart.scales.y.right).toBeCloseToPixel(250); + expect(chart.scales.y.top).toBeCloseToPixel(62); + expect(chart.scales.y.labelRotation).toBeCloseTo(0); + }); + + it('should fit scales that overlap the chart area', function() { + var chart = window.acquireChart({ + type: 'radar', + data: { + datasets: [{ + data: [10, 5, 0, 25, 78, -10] + }, { + data: [-19, -20, 0, -99, -50, 0] + }], + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] + } + }); + + expect(chart.chartArea.bottom).toBeCloseToPixel(512); + expect(chart.chartArea.left).toBeCloseToPixel(0); + expect(chart.chartArea.right).toBeCloseToPixel(512); + expect(chart.chartArea.top).toBeCloseToPixel(32); + + var scale = chart.scales.r; + expect(scale.bottom).toBeCloseToPixel(512); + expect(scale.left).toBeCloseToPixel(0); + expect(scale.right).toBeCloseToPixel(512); + expect(scale.top).toBeCloseToPixel(32); + expect(scale.width).toBeCloseToPixel(496); + expect(scale.height).toBeCloseToPixel(464); + }); + + it('should fit multiple axes in the same position', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + yAxisID: 'y', + data: [10, 5, 0, 25, 78, -10] + }, { + yAxisID: 'y2', + data: [-19, -20, 0, -99, -50, 0] + }], + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] + }, + options: { + scales: { + x: { + type: 'category' + }, + y: { + type: 'linear' + }, + y2: { + type: 'linear' + } + } + } + }, { + canvas: { + height: 150, + width: 250 + } + }); + + expect(chart.chartArea.bottom).toBeCloseToPixel(110); + expect(chart.chartArea.left).toBeCloseToPixel(70); + expect(chart.chartArea.right).toBeCloseToPixel(250); + expect(chart.chartArea.top).toBeCloseToPixel(32); + + // Is xScale at the right spot + expect(chart.scales.x.bottom).toBeCloseToPixel(150); + expect(chart.scales.x.left).toBeCloseToPixel(70); + expect(chart.scales.x.right).toBeCloseToPixel(250); + expect(chart.scales.x.top).toBeCloseToPixel(110); + expect(chart.scales.x.labelRotation).toBeCloseTo(40, -1); + + // Are yScales at the right spot + expect(chart.scales.y.bottom).toBeCloseToPixel(110); + expect(chart.scales.y.left).toBeCloseToPixel(38); + expect(chart.scales.y.right).toBeCloseToPixel(70); + expect(chart.scales.y.top).toBeCloseToPixel(32); + expect(chart.scales.y.labelRotation).toBeCloseTo(0); + + expect(chart.scales.y2.bottom).toBeCloseToPixel(110); + expect(chart.scales.y2.left).toBeCloseToPixel(0); + expect(chart.scales.y2.right).toBeCloseToPixel(38); + expect(chart.scales.y2.top).toBeCloseToPixel(32); + expect(chart.scales.y2.labelRotation).toBeCloseTo(0); + }); + + it ('should fit a full width box correctly', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + xAxisID: 'x', + data: [10, 5, 0, 25, 78, -10] + }, { + xAxisID: 'x2', + data: [-19, -20, 0, -99, -50, 0] + }], + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] + }, + options: { + scales: { + x: { + type: 'category', + offset: false + }, + x2: { + type: 'category', + position: 'top', + fullSize: true, + offset: false + }, + y: { + type: 'linear' + } + } + } + }); + + expect(chart.chartArea.bottom).toBeCloseToPixel(484); + expect(chart.chartArea.left).toBeCloseToPixel(39); + expect(chart.chartArea.right).toBeCloseToPixel(496); + expect(chart.chartArea.top).toBeCloseToPixel(62); + + // Are xScales at the right spot + expect(chart.scales.x.bottom).toBeCloseToPixel(512); + expect(chart.scales.x.left).toBeCloseToPixel(39); + expect(chart.scales.x.right).toBeCloseToPixel(496); + expect(chart.scales.x.top).toBeCloseToPixel(484); + + expect(chart.scales.x2.bottom).toBeCloseToPixel(62); + expect(chart.scales.x2.left).toBeCloseToPixel(0); + expect(chart.scales.x2.right).toBeCloseToPixel(512); + expect(chart.scales.x2.top).toBeCloseToPixel(32); + + // Is yScale at the right spot + expect(chart.scales.y.bottom).toBeCloseToPixel(484); + expect(chart.scales.y.left).toBeCloseToPixel(0); + expect(chart.scales.y.right).toBeCloseToPixel(39); + expect(chart.scales.y.top).toBeCloseToPixel(62); + }); + + describe('padding settings', function() { + it('should apply a single padding to all dimensions', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [ + { + data: [10, 5, 0, 25, 78, -10] + } + ], + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] + }, + options: { + scales: { + x: { + type: 'category', + display: false + }, + y: { + type: 'linear', + display: false + } + }, + plugins: { + legend: false, + title: false + }, + layout: { + padding: 10 + } + } + }, { + canvas: { + height: 150, + width: 250 + } + }); + + expect(chart.chartArea.bottom).toBeCloseToPixel(140); + expect(chart.chartArea.left).toBeCloseToPixel(10); + expect(chart.chartArea.right).toBeCloseToPixel(240); + expect(chart.chartArea.top).toBeCloseToPixel(10); + }); + + it('should apply padding in all positions', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [ + { + data: [10, 5, 0, 25, 78, -10] + } + ], + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] + }, + options: { + scales: { + x: { + type: 'category', + display: false + }, + y: { + type: 'linear', + display: false + } + }, + plugins: { + legend: false, + title: false + }, + layout: { + padding: { + left: 5, + right: 15, + top: 8, + bottom: 12 + } + } + } + }, { + canvas: { + height: 150, + width: 250 + } + }); + + expect(chart.chartArea.bottom).toBeCloseToPixel(138); + expect(chart.chartArea.left).toBeCloseToPixel(5); + expect(chart.chartArea.right).toBeCloseToPixel(235); + expect(chart.chartArea.top).toBeCloseToPixel(8); + }); + + it('should default to 0 padding if no dimensions specified', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [ + { + data: [10, 5, 0, 25, 78, -10] + } + ], + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] + }, + options: { + scales: { + x: { + type: 'category', + display: false + }, + y: { + type: 'linear', + display: false + } + }, + plugins: { + legend: false, + title: false + }, + layout: { + padding: {} + } + } + }, { + canvas: { + height: 150, + width: 250 + } + }); + + expect(chart.chartArea.bottom).toBeCloseToPixel(150); + expect(chart.chartArea.left).toBeCloseToPixel(0); + expect(chart.chartArea.right).toBeCloseToPixel(250); + expect(chart.chartArea.top).toBeCloseToPixel(0); + }); + }); + + describe('ordering by weight', function() { + it('should keep higher weights outside', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [ + { + data: [10, 5, 0, 25, 78, -10] + } + ], + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] + }, + options: { + plugins: { + legend: { + display: true, + position: 'left', + }, + title: { + display: true, + position: 'bottom', + }, + } + }, + }, { + canvas: { + height: 150, + width: 250 + } + }); + + var xAxis = chart.scales.x; + var yAxis = chart.scales.y; + var legend = chart.legend; + var title = chart.titleBlock; + + expect(yAxis.left).toBe(legend.right); + expect(xAxis.bottom).toBe(title.top); + }); + + it('should correctly set weights of scales and order them', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [ + { + data: [10, 5, 0, 25, 78, -10] + } + ], + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] + }, + options: { + scales: { + x: { + type: 'category', + position: 'bottom', + display: true, + weight: 1 + }, + x1: { + type: 'category', + position: 'bottom', + display: true, + weight: 2 + }, + x2: { + type: 'category', + position: 'bottom', + display: true + }, + x3: { + type: 'category', + display: true, + position: 'top', + weight: 1 + }, + x4: { + type: 'category', + display: true, + position: 'top', + weight: 2 + }, + y: { + type: 'linear', + display: true, + weight: 1 + }, + y1: { + type: 'linear', + position: 'left', + display: true, + weight: 2 + }, + y2: { + type: 'linear', + position: 'left', + display: true + }, + y3: { + type: 'linear', + display: true, + position: 'right', + weight: 1 + }, + y4: { + type: 'linear', + display: true, + position: 'right', + weight: 2 + } + } + } + }, { + canvas: { + height: 150, + width: 250 + } + }); + + var xScale0 = chart.scales.x; + var xScale1 = chart.scales.x1; + var xScale2 = chart.scales.x2; + var xScale3 = chart.scales.x3; + var xScale4 = chart.scales.x4; + + var yScale0 = chart.scales.y; + var yScale1 = chart.scales.y1; + var yScale2 = chart.scales.y2; + var yScale3 = chart.scales.y3; + var yScale4 = chart.scales.y4; + + expect(xScale0.weight).toBe(1); + expect(xScale1.weight).toBe(2); + expect(xScale2.weight).toBe(0); + + expect(xScale3.weight).toBe(1); + expect(xScale4.weight).toBe(2); + + expect(yScale0.weight).toBe(1); + expect(yScale1.weight).toBe(2); + expect(yScale2.weight).toBe(0); + + expect(yScale3.weight).toBe(1); + expect(yScale4.weight).toBe(2); + + var isOrderCorrect = false; + + // bottom axes + isOrderCorrect = xScale2.top < xScale0.top && xScale0.top < xScale1.top; + expect(isOrderCorrect).toBe(true); + + // top axes + isOrderCorrect = xScale4.top < xScale3.top; + expect(isOrderCorrect).toBe(true); + + // left axes + isOrderCorrect = yScale1.left < yScale0.left && yScale0.left < yScale2.left; + expect(isOrderCorrect).toBe(true); + + // right axes + isOrderCorrect = yScale3.left < yScale4.left; + expect(isOrderCorrect).toBe(true); + }); + }); + + describe('box sizing', function() { + it('should correctly compute y-axis width to fit labels', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + labels: ['tick 1', 'tick 2', 'tick 3', 'tick 4', 'tick 5'], + datasets: [{ + data: [0, 2.25, 1.5, 1.25, 2.5] + }], + }, + options: { + plugins: { + legend: false + }, + }, + }, { + canvas: { + height: 256, + width: 256 + } + }); + var yAxis = chart.scales.y; + + // issue #4441: y-axis labels partially hidden. + // minimum horizontal space required to fit labels + expect(yAxis.width).toBeCloseToPixel(30); + expect(getLabels(yAxis)).toEqual(['0', '0.5', '1.0', '1.5', '2.0', '2.5']); + }); + }); +}); diff --git a/test/specs/core.plugin.tests.js b/test/specs/core.plugin.tests.js new file mode 100644 index 00000000000..76f7b7109b1 --- /dev/null +++ b/test/specs/core.plugin.tests.js @@ -0,0 +1,510 @@ +describe('Chart.plugins', function() { + describe('Chart.notifyPlugins', function() { + it('should call inline plugins with arguments', function() { + var plugin = {hook: function() {}}; + var chart = window.acquireChart({ + plugins: [plugin] + }); + var args = {value: 42}; + + spyOn(plugin, 'hook'); + + chart.notifyPlugins('hook', args); + expect(plugin.hook.calls.count()).toBe(1); + expect(plugin.hook.calls.first().args[0]).toBe(chart); + expect(plugin.hook.calls.first().args[1]).toBe(args); + expect(plugin.hook.calls.first().args[2]).toEqualOptions({}); + }); + + it('should call global plugins with arguments', function() { + var plugin = {id: 'a', hook: function() {}}; + var chart = window.acquireChart({}); + var args = {value: 42}; + + spyOn(plugin, 'hook'); + + Chart.register(plugin); + chart.notifyPlugins('hook', args); + expect(plugin.hook.calls.count()).toBe(1); + expect(plugin.hook.calls.first().args[0]).toBe(chart); + expect(plugin.hook.calls.first().args[1]).toBe(args); + expect(plugin.hook.calls.first().args[2]).toEqualOptions({}); + Chart.unregister(plugin); + }); + + it('should call plugin only once even if registered multiple times', function() { + var plugin = {id: 'test', hook: function() {}}; + var chart = window.acquireChart({ + plugins: [plugin, plugin] + }); + + spyOn(plugin, 'hook'); + + Chart.register([plugin, plugin]); + chart.notifyPlugins('hook'); + expect(plugin.hook.calls.count()).toBe(1); + Chart.unregister(plugin); + }); + + it('should call plugins in the correct order (global first)', function() { + var results = []; + var chart = window.acquireChart({ + plugins: [{ + hook: function() { + results.push(1); + } + }, { + hook: function() { + results.push(2); + } + }, { + hook: function() { + results.push(3); + } + }] + }); + + var plugins = [{ + id: 'a', + hook: function() { + results.push(4); + } + }, { + id: 'b', + hook: function() { + results.push(5); + } + }, { + id: 'c', + hook: function() { + results.push(6); + } + }]; + Chart.register(plugins); + + var ret = chart.notifyPlugins('hook'); + expect(ret).toBeTruthy(); + expect(results).toEqual([4, 5, 6, 1, 2, 3]); + Chart.unregister(plugins); + }); + + it('should return TRUE if no plugin explicitly returns FALSE', function() { + var chart = window.acquireChart({ + plugins: [{ + hook: function() {} + }, { + hook: function() { + return null; + } + }, { + hook: function() { + return 0; + } + }, { + hook: function() { + return true; + } + }, { + hook: function() { + return 1; + } + }] + }); + + var plugins = chart.config.plugins; + plugins.forEach(function(plugin) { + spyOn(plugin, 'hook').and.callThrough(); + }); + + var ret = chart.notifyPlugins('hook'); + expect(ret).toBeTruthy(); + plugins.forEach(function(plugin) { + expect(plugin.hook).toHaveBeenCalled(); + }); + }); + + it('should return FALSE if any plugin explicitly returns FALSE', function() { + var chart = window.acquireChart({ + plugins: [{ + hook: function() {} + }, { + hook: function() { + return null; + } + }, { + hook: function() { + return false; + } + }, { + hook: function() { + return 42; + } + }, { + hook: function() { + return 'bar'; + } + }] + }); + + var plugins = chart.config.plugins; + plugins.forEach(function(plugin) { + spyOn(plugin, 'hook').and.callThrough(); + }); + + var ret = chart.notifyPlugins('hook', {cancelable: true}); + expect(ret).toBeFalsy(); + expect(plugins[0].hook).toHaveBeenCalled(); + expect(plugins[1].hook).toHaveBeenCalled(); + expect(plugins[2].hook).toHaveBeenCalled(); + expect(plugins[3].hook).not.toHaveBeenCalled(); + expect(plugins[4].hook).not.toHaveBeenCalled(); + }); + }); + + describe('config.options.plugins', function() { + it('should call plugins with options at last argument', function() { + var plugin = {id: 'foo', hook: function() {}}; + + var chart = window.acquireChart({ + options: { + plugins: { + foo: {a: '123'}, + } + } + }); + + spyOn(plugin, 'hook'); + + Chart.register(plugin); + chart.notifyPlugins('hook'); + chart.notifyPlugins('hook', {arg1: 'bla'}); + chart.notifyPlugins('hook', {arg1: 'bla', arg2: 42}); + + expect(plugin.hook.calls.count()).toBe(3); + expect(plugin.hook.calls.argsFor(0)[2]).toEqualOptions({a: '123'}); + expect(plugin.hook.calls.argsFor(1)[2]).toEqualOptions({a: '123'}); + expect(plugin.hook.calls.argsFor(2)[2]).toEqualOptions({a: '123'}); + + Chart.unregister(plugin); + }); + + it('should call plugins with options associated to their identifier', function() { + var plugins = { + a: {id: 'a', hook: function() {}}, + b: {id: 'b', hook: function() {}}, + c: {id: 'c', hook: function() {}} + }; + + Chart.register(plugins.a); + + var chart = window.acquireChart({ + plugins: [plugins.b, plugins.c], + options: { + plugins: { + a: {a: '123'}, + b: {b: '456'}, + c: {c: '789'} + } + } + }); + + spyOn(plugins.a, 'hook'); + spyOn(plugins.b, 'hook'); + spyOn(plugins.c, 'hook'); + + chart.notifyPlugins('hook'); + + expect(plugins.a.hook).toHaveBeenCalled(); + expect(plugins.b.hook).toHaveBeenCalled(); + expect(plugins.c.hook).toHaveBeenCalled(); + expect(plugins.a.hook.calls.first().args[2]).toEqualOptions({a: '123'}); + expect(plugins.b.hook.calls.first().args[2]).toEqualOptions({b: '456'}); + expect(plugins.c.hook.calls.first().args[2]).toEqualOptions({c: '789'}); + + Chart.unregister(plugins.a); + }); + + it('should not call plugins when config.options.plugins.{id} is FALSE', function() { + var plugins = { + a: {id: 'a', hook: function() {}}, + b: {id: 'b', hook: function() {}}, + c: {id: 'c', hook: function() {}} + }; + + Chart.register(plugins.a); + + var chart = window.acquireChart({ + plugins: [plugins.b, plugins.c], + options: { + plugins: { + a: false, + b: false + } + } + }); + + spyOn(plugins.a, 'hook'); + spyOn(plugins.b, 'hook'); + spyOn(plugins.c, 'hook'); + + chart.notifyPlugins('hook'); + + expect(plugins.a.hook).not.toHaveBeenCalled(); + expect(plugins.b.hook).not.toHaveBeenCalled(); + expect(plugins.c.hook).toHaveBeenCalled(); + + Chart.unregister(plugins.a); + }); + + it('should call plugins with default options when plugin options is TRUE', function() { + var plugin = {id: 'a', hook: function() {}, defaults: {a: 42}}; + + Chart.register(plugin); + + var chart = window.acquireChart({ + options: { + plugins: { + a: true + } + } + }); + + spyOn(plugin, 'hook'); + + chart.notifyPlugins('hook'); + + expect(plugin.hook).toHaveBeenCalled(); + expect(Object.keys(plugin.hook.calls.first().args[2])).toEqual(['a']); + expect(plugin.hook.calls.first().args[2]).toEqual(jasmine.objectContaining({a: 42})); + + Chart.unregister(plugin); + }); + + + it('should call plugins with default options if plugin config options is undefined', function() { + var plugin = {id: 'a', hook: function() {}, defaults: {a: 'foobar'}}; + + Chart.register(plugin); + spyOn(plugin, 'hook'); + + var chart = window.acquireChart(); + + chart.notifyPlugins('hook'); + + expect(plugin.hook).toHaveBeenCalled(); + expect(plugin.hook.calls.first().args[2]).toEqualOptions({a: 'foobar'}); + + Chart.unregister(plugin); + }); + + // https://github.com/chartjs/Chart.js/issues/10482 + it('should resolve defaults for local plugins', function() { + var plugin = {id: 'a', hook: function() {}, defaults: {bar: 'bar'}}; + var chart = window.acquireChart({ + plugins: [plugin], + options: { + plugins: { + a: { + foo: 'foo' + } + } + }, + }); + + spyOn(plugin, 'hook'); + chart.notifyPlugins('hook'); + + expect(plugin.hook).toHaveBeenCalled(); + expect(plugin.hook.calls.first().args[2]).toEqualOptions({foo: 'foo', bar: 'bar'}); + + Chart.unregister(plugin); + }); + + // https://github.com/chartjs/Chart.js/issues/5111#issuecomment-355934167 + it('should update plugin options', function() { + var plugin = {id: 'a', hook: function() {}}; + var chart = window.acquireChart({ + plugins: [plugin], + options: { + plugins: { + a: { + foo: 'foo' + } + } + }, + }); + + spyOn(plugin, 'hook'); + + chart.notifyPlugins('hook'); + + expect(plugin.hook).toHaveBeenCalled(); + expect(plugin.hook.calls.first().args[2]).toEqualOptions({foo: 'foo'}); + + chart.options.plugins.a = {bar: 'bar'}; + chart.update(); + + plugin.hook.calls.reset(); + chart.notifyPlugins('hook'); + + expect(plugin.hook).toHaveBeenCalled(); + expect(plugin.hook.calls.first().args[2]).toEqualOptions({bar: 'bar'}); + }); + + // https://github.com/chartjs/Chart.js/issues/10654 + it('should resolve options even if some subnodes are set as undefined', function() { + var runtimeOptions; + var plugin = { + id: 'a', + afterUpdate: function(chart, args, options) { + options.l1.l2.l3.display = true; + runtimeOptions = options; + }, + defaults: { + l1: { + l2: { + l3: { + display: false + } + } + } + } + }; + window.acquireChart({ + plugins: [plugin], + options: { + plugins: { + a: { + l1: { + l2: undefined + } + }, + } + }, + }); + + expect(runtimeOptions.l1.l2.l3.display).toBe(true); + Chart.unregister(plugin); + }); + + it('should disable all plugins', function() { + var plugin = {id: 'a', hook: function() {}}; + var chart = window.acquireChart({ + plugins: [plugin], + options: { + plugins: false + } + }); + + spyOn(plugin, 'hook'); + + chart.notifyPlugins('hook'); + + expect(plugin.hook).not.toHaveBeenCalled(); + }); + + it('should not restart plugins when a double register occurs', function() { + var results = []; + var chart = window.acquireChart({ + plugins: [{ + start: function() { + results.push(1); + } + }] + }); + + Chart.register({id: 'abc', hook: function() {}}); + Chart.register({id: 'def', hook: function() {}}); + + chart.update(); + + // The plugin on the chart should only be started once + expect(results).toEqual([1]); + }); + + it('should default to false for _scriptable, _indexable', function(done) { + const plugin = { + id: 'test', + start: function(chart, args, opts) { + expect(opts.fun).toEqual(jasmine.any(Function)); + expect(opts.fun()).toEqual('test'); + expect(opts.arr).toEqual([1, 2, 3]); + + expect(opts.sub.subfun).toEqual(jasmine.any(Function)); + expect(opts.sub.subfun()).toEqual('subtest'); + expect(opts.sub.subarr).toEqual([3, 2, 1]); + done(); + } + }; + window.acquireChart({ + options: { + plugins: { + test: { + fun: () => 'test', + arr: [1, 2, 3], + sub: { + subfun: () => 'subtest', + subarr: [3, 2, 1], + } + } + } + }, + plugins: [plugin] + }); + }); + + it('should filter event callbacks by plugin events array', async function() { + const results = []; + const chart = window.acquireChart({ + options: { + events: ['mousemove', 'test', 'test2', 'pointerleave'], + plugins: { + testPlugin: { + events: ['test', 'pointerleave'] + } + } + }, + plugins: [{ + id: 'testPlugin', + beforeEvent: function(_chart, args) { + results.push('before' + args.event.type); + }, + afterEvent: function(_chart, args) { + results.push('after' + args.event.type); + } + }] + }); + await jasmine.triggerMouseEvent(chart, 'mousemove', {x: 0, y: 0}); + await jasmine.triggerMouseEvent(chart, 'test', {x: 0, y: 0}); + await jasmine.triggerMouseEvent(chart, 'test2', {x: 0, y: 0}); + await jasmine.triggerMouseEvent(chart, 'pointerleave', {x: 0, y: 0}); + expect(results).toEqual(['beforetest', 'aftertest', 'beforemouseout', 'aftermouseout']); + }); + + it('should not call plugins after uninstall', async function() { + const results = []; + const chart = window.acquireChart({ + options: { + events: ['test'], + plugins: { + testPlugin: { + events: ['test'] + } + } + }, + plugins: [{ + id: 'testPlugin', + reset: () => results.push('reset'), + afterDestroy: () => results.push('afterDestroy'), + uninstall: () => results.push('uninstall'), + }] + }); + chart.reset(); + expect(results).toEqual(['reset']); + chart.destroy(); + expect(results).toEqual(['reset', 'afterDestroy', 'uninstall']); + chart.reset(); + expect(results).toEqual(['reset', 'afterDestroy', 'uninstall']); + }); + }); +}); diff --git a/test/specs/core.registry.tests.js b/test/specs/core.registry.tests.js new file mode 100644 index 00000000000..2d94105f14b --- /dev/null +++ b/test/specs/core.registry.tests.js @@ -0,0 +1,269 @@ +describe('Chart.registry', function() { + it('should handle an ES6 controller extension', function() { + class CustomController extends Chart.DatasetController {} + CustomController.id = 'custom'; + CustomController.defaults = { + foo: 'bar' + }; + CustomController.overrides = { + bar: 'foo' + }; + Chart.register(CustomController); + + expect(Chart.registry.getController('custom')).toEqual(CustomController); + expect(Chart.defaults.datasets.custom).toEqual(CustomController.defaults); + expect(Chart.overrides.custom).toEqual(CustomController.overrides); + + Chart.unregister(CustomController); + + expect(function() { + Chart.registry.getController('custom'); + }).toThrow(new Error('"custom" is not a registered controller.')); + expect(Chart.overrides.custom).not.toBeDefined(); + expect(Chart.defaults.datasets.custom).not.toBeDefined(); + }); + + it('should handle an ES6 scale extension', function() { + class CustomScale extends Chart.Scale {} + CustomScale.id = 'es6Scale'; + CustomScale.defaults = { + foo: 'bar' + }; + Chart.register(CustomScale); + + expect(Chart.registry.getScale('es6Scale')).toEqual(CustomScale); + expect(Chart.defaults.scales.es6Scale).toEqual(CustomScale.defaults); + + Chart.unregister(CustomScale); + + expect(function() { + Chart.registry.getScale('es6Scale'); + }).toThrow(new Error('"es6Scale" is not a registered scale.')); + expect(Chart.defaults.scales.es6Scale).not.toBeDefined(); + }); + + it('should handle an ES6 element extension', function() { + class CustomElement extends Chart.Element {} + CustomElement.id = 'es6element'; + CustomElement.defaults = { + foo: 'bar' + }; + Chart.register(CustomElement); + + expect(Chart.registry.getElement('es6element')).toEqual(CustomElement); + expect(Chart.defaults.elements.es6element).toEqual(CustomElement.defaults); + + Chart.unregister(CustomElement); + + expect(function() { + Chart.registry.getElement('es6element'); + }).toThrow(new Error('"es6element" is not a registered element.')); + expect(Chart.defaults.elements.es6element).not.toBeDefined(); + }); + + it('should handle an ES6 plugin', function() { + class CustomPlugin {} + CustomPlugin.id = 'es6plugin'; + CustomPlugin.defaults = { + foo: 'bar' + }; + Chart.register(CustomPlugin); + + expect(Chart.registry.getPlugin('es6plugin')).toEqual(CustomPlugin); + expect(Chart.defaults.plugins.es6plugin).toEqual(CustomPlugin.defaults); + + Chart.unregister(CustomPlugin); + + expect(function() { + Chart.registry.getPlugin('es6plugin'); + }).toThrow(new Error('"es6plugin" is not a registered plugin.')); + expect(Chart.defaults.plugins.es6plugin).not.toBeDefined(); + }); + + it('should not accept an object without id', function() { + expect(function() { + Chart.register({foo: 'bar'}); + }).toThrow(new Error('class does not have id: bar')); + + class FaultyPlugin {} + + expect(function() { + Chart.register(FaultyPlugin); + }).toThrow(new Error('class does not have id: class FaultyPlugin {}')); + }); + + it('should not fail when unregistering an object that is not registered', function() { + expect(function() { + Chart.unregister({id: 'foo'}); + }).not.toThrow(); + }); + + describe('Should allow registering explicitly', function() { + class customExtension {} + customExtension.id = 'custom'; + customExtension.defaults = { + prop: true + }; + + it('as controller', function() { + Chart.registry.addControllers(customExtension); + + expect(Chart.registry.getController('custom')).toEqual(customExtension); + expect(Chart.defaults.datasets.custom).toEqual(customExtension.defaults); + + Chart.registry.removeControllers(customExtension); + + expect(function() { + Chart.registry.getController('custom'); + }).toThrow(new Error('"custom" is not a registered controller.')); + expect(Chart.defaults.datasets.custom).not.toBeDefined(); + }); + + it('as scale', function() { + Chart.registry.addScales(customExtension); + + expect(Chart.registry.getScale('custom')).toEqual(customExtension); + expect(Chart.defaults.scales.custom).toEqual(customExtension.defaults); + + Chart.registry.removeScales(customExtension); + + expect(function() { + Chart.registry.getScale('custom'); + }).toThrow(new Error('"custom" is not a registered scale.')); + expect(Chart.defaults.scales.custom).not.toBeDefined(); + }); + + it('as element', function() { + Chart.registry.addElements(customExtension); + + expect(Chart.registry.getElement('custom')).toEqual(customExtension); + expect(Chart.defaults.elements.custom).toEqual(customExtension.defaults); + + Chart.registry.removeElements(customExtension); + + expect(function() { + Chart.registry.getElement('custom'); + }).toThrow(new Error('"custom" is not a registered element.')); + expect(Chart.defaults.elements.custom).not.toBeDefined(); + }); + + it('as plugin', function() { + Chart.registry.addPlugins(customExtension); + + expect(Chart.registry.getPlugin('custom')).toEqual(customExtension); + expect(Chart.defaults.plugins.custom).toEqual(customExtension.defaults); + + Chart.registry.removePlugins(customExtension); + + expect(function() { + Chart.registry.getPlugin('custom'); + }).toThrow(new Error('"custom" is not a registered plugin.')); + expect(Chart.defaults.plugins.custom).not.toBeDefined(); + }); + }); + + it('should fire before/after callbacks', function() { + let beforeRegisterCount = 0; + let afterRegisterCount = 0; + let beforeUnregisterCount = 0; + let afterUnregisterCount = 0; + class custom {} + custom.id = 'custom'; + custom.beforeRegister = () => beforeRegisterCount++; + custom.afterRegister = () => afterRegisterCount++; + custom.beforeUnregister = () => beforeUnregisterCount++; + custom.afterUnregister = () => afterUnregisterCount++; + + Chart.registry.addControllers(custom); + expect(beforeRegisterCount).withContext('beforeRegister').toBe(1); + expect(afterRegisterCount).withContext('afterRegister').toBe(1); + Chart.registry.removeControllers(custom); + expect(beforeUnregisterCount).withContext('beforeUnregister').toBe(1); + expect(afterUnregisterCount).withContext('afterUnregister').toBe(1); + + Chart.registry.addScales(custom); + expect(beforeRegisterCount).withContext('beforeRegister').toBe(2); + expect(afterRegisterCount).withContext('afterRegister').toBe(2); + Chart.registry.removeScales(custom); + expect(beforeUnregisterCount).withContext('beforeUnregister').toBe(2); + expect(afterUnregisterCount).withContext('afterUnregister').toBe(2); + + Chart.registry.addElements(custom); + expect(beforeRegisterCount).withContext('beforeRegister').toBe(3); + expect(afterRegisterCount).withContext('afterRegister').toBe(3); + Chart.registry.removeElements(custom); + expect(beforeUnregisterCount).withContext('beforeUnregister').toBe(3); + expect(afterUnregisterCount).withContext('afterUnregister').toBe(3); + + Chart.register(custom); + expect(beforeRegisterCount).withContext('beforeRegister').toBe(4); + expect(afterRegisterCount).withContext('afterRegister').toBe(4); + Chart.unregister(custom); + expect(beforeUnregisterCount).withContext('beforeUnregister').toBe(4); + expect(afterUnregisterCount).withContext('afterUnregister').toBe(4); + }); + + it('should preserve existing defaults', function() { + Chart.defaults.datasets.test = {test1: true, test3: false}; + Chart.overrides.test = {testA: true, testC: false}; + + class testController extends Chart.DatasetController {} + testController.id = 'test'; + testController.defaults = {test1: false, test2: true}; + testController.overrides = {testA: false, testB: true}; + + Chart.register(testController); + expect(Chart.defaults.datasets.test).toEqual({test1: false, test2: true, test3: false}); + expect(Chart.overrides.test).toEqual({testA: false, testB: true, testC: false}); + + Chart.unregister(testController); + expect(Chart.defaults.datasets.test).not.toBeDefined(); + expect(Chart.overrides.test).not.toBeDefined(); + }); + + describe('should handle multiple items', function() { + class test1 extends Chart.DatasetController {} + test1.id = 'test1'; + class test2 extends Chart.Scale {} + test2.id = 'test2'; + + it('separately', function() { + Chart.register(test1, test2); + expect(Chart.registry.getController('test1')).toEqual(test1); + expect(Chart.registry.getScale('test2')).toEqual(test2); + Chart.unregister(test1, test2); + expect(function() { + Chart.registry.getController('test1'); + }).toThrow(); + expect(function() { + Chart.registry.getScale('test2'); + }).toThrow(); + }); + + it('as array', function() { + Chart.register([test1, test2]); + expect(Chart.registry.getController('test1')).toEqual(test1); + expect(Chart.registry.getScale('test2')).toEqual(test2); + Chart.unregister([test1, test2]); + expect(function() { + Chart.registry.getController('test1'); + }).toThrow(); + expect(function() { + Chart.registry.getScale('test2'); + }).toThrow(); + }); + + it('as object', function() { + Chart.register({test1, test2}); + expect(Chart.registry.getController('test1')).toEqual(test1); + expect(Chart.registry.getScale('test2')).toEqual(test2); + Chart.unregister({test1, test2}); + expect(function() { + Chart.registry.getController('test1'); + }).toThrow(); + expect(function() { + Chart.registry.getScale('test2'); + }).toThrow(); + }); + }); +}); diff --git a/test/specs/core.scale.tests.js b/test/specs/core.scale.tests.js new file mode 100644 index 00000000000..a388e7c9ca5 --- /dev/null +++ b/test/specs/core.scale.tests.js @@ -0,0 +1,900 @@ +function getLabels(scale) { + return scale.ticks.map(t => t.label); +} + +describe('Core.scale', function() { + describe('auto', jasmine.fixture.specs('core.scale')); + + it('should provide default scale label options', function() { + expect(Chart.defaults.scale.title).toEqual({ + color: Chart.defaults.color, + display: false, + text: '', + padding: { + top: 4, + bottom: 4 + } + }); + }); + + describe('displaying xAxis ticks with autoSkip=true', function() { + function getChart(data) { + return window.acquireChart({ + type: 'line', + data: data, + options: { + scales: { + x: { + ticks: { + autoSkip: true + } + } + } + } + }); + } + + function getChartBigData(maxTicksLimit) { + return window.acquireChart({ + type: 'line', + data: { + labels: new Array(300).fill('red'), + datasets: [{ + data: new Array(300).fill(5), + }] + }, + options: { + scales: { + x: { + ticks: { + autoSkip: true, + maxTicksLimit + } + } + } + } + }); + } + + function lastTick(chart) { + var xAxis = chart.scales.x; + var ticks = xAxis.getTicks(); + return ticks[ticks.length - 1]; + } + + it('should use autoSkip amount of ticks when maxTicksLimit is set to a larger number as autoSkip calculation', function() { + var chart = getChartBigData(300); + expect(chart.scales.x.ticks.length).toEqual(20); + }); + + it('should use maxTicksLimit amount of ticks when maxTicksLimit is set to a smaller number as autoSkip calculation', function() { + var chart = getChartBigData(3); + expect(chart.scales.x.ticks.length).toEqual(3); + }); + + it('should display the last tick if it fits evenly with other ticks', function() { + var chart = getChart({ + labels: [ + 'January 2018', 'February 2018', 'March 2018', 'April 2018', + 'May 2018', 'June 2018', 'July 2018', 'August 2018', + 'September 2018' + ], + datasets: [{ + data: [12, 19, 3, 5, 2, 3, 7, 8, 9] + }] + }); + + expect(lastTick(chart).label).toEqual('September 2018'); + }); + + it('should not display the last tick if it does not fit evenly', function() { + var chart = getChart({ + labels: [ + 'January 2018', 'February 2018', 'March 2018', 'April 2018', + 'May 2018', 'June 2018', 'July 2018', 'August 2018', + 'September 2018', 'October 2018', 'November 2018', 'December 2018', + 'January 2019', 'February 2019', 'March 2019', 'April 2019', + 'May 2019', 'June 2019', 'July 2019', 'August 2019', + 'September 2019', 'October 2019', 'November 2019', 'December 2019', + 'January 2020', 'February 2020', 'March 2020', 'April 2020' + ], + datasets: [{ + data: [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7] + }] + }); + + expect(lastTick(chart).label).toEqual('March 2020'); + }); + }); + + var gridLineTests = [{ + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5'], + offsetGridLines: false, + offset: false, + expected: [0.5, 128.5, 256.5, 384.5, 512.5] + }, { + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5'], + offsetGridLines: false, + offset: true, + expected: [51.5, 153.5, 256.5, 358.5, 460.5] + }, { + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5'], + offsetGridLines: true, + offset: false, + expected: [64.5, 192.5, 320.5, 448.5] + }, { + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5'], + offsetGridLines: true, + offset: true, + expected: [0.5, 102.5, 204.5, 307.5, 409.5, 512.5] + }, { + labels: ['tick1'], + offsetGridLines: false, + offset: false, + expected: [0.5] + }, { + labels: ['tick1'], + offsetGridLines: false, + offset: true, + expected: [256.5] + }, { + labels: ['tick1'], + offsetGridLines: true, + offset: false, + expected: [512.5] + }, { + labels: ['tick1'], + offsetGridLines: true, + offset: true, + expected: [0.5, 512.5] + }]; + + gridLineTests.forEach(function(test) { + it('should get the correct pixels for gridLine(s) for the horizontal scale when offsetGridLines is ' + test.offsetGridLines + ' and offset is ' + test.offset, function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [] + }], + labels: test.labels + }, + options: { + scales: { + x: { + grid: { + offset: test.offsetGridLines, + drawTicks: false + }, + ticks: { + display: false + }, + offset: test.offset + }, + y: { + display: false + } + }, + plugins: { + legend: false + } + } + }); + + var xScale = chart.scales.x; + xScale.ctx = window.createMockContext(); + chart.draw(); + + expect(xScale.ctx.getCalls().filter(function(x) { + return x.name === 'moveTo' && x.args[1] === 0; + }).map(function(x) { + return x.args[0]; + })).toEqual(test.expected); + }); + }); + + gridLineTests.forEach(function(test) { + it('should get the correct pixels for gridLine(s) for the vertical scale when offsetGridLines is ' + test.offsetGridLines + ' and offset is ' + test.offset, function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [] + }], + labels: test.labels + }, + options: { + scales: { + x: { + display: false + }, + y: { + type: 'category', + grid: { + offset: test.offsetGridLines, + drawTicks: false + }, + ticks: { + display: false + }, + offset: test.offset + } + }, + plugins: { + legend: false + } + } + }); + + var yScale = chart.scales.y; + yScale.ctx = window.createMockContext(); + chart.draw(); + + expect(yScale.ctx.getCalls().filter(function(x) { + return x.name === 'moveTo' && x.args[0] === 1; + }).map(function(x) { + return x.args[1]; + })).toEqual(test.expected); + }); + }); + + it('should add the correct padding for long tick labels', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + labels: [ + 'This is a very long label', + 'This is a very long label' + ], + datasets: [{ + data: [0, 1] + }] + }, + options: { + scales: { + y: { + display: false + } + }, + plugins: { + legend: false + } + } + }, { + canvas: { + height: 100, + width: 200 + } + }); + + var scale = chart.scales.x; + expect(scale.left).toBeGreaterThan(100); + expect(scale.right).toBeGreaterThan(190); + }); + + describe('given the axes display option is set to auto', function() { + describe('for the x axes', function() { + it('should draw the axes if at least one associated dataset is visible', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [100, 200, 100, 50], + xAxisId: 'foo', + hidden: true, + labels: ['Q1', 'Q2', 'Q3', 'Q4'] + }, { + data: [100, 200, 100, 50], + xAxisId: 'foo', + labels: ['Q1', 'Q2', 'Q3', 'Q4'] + }] + }, + options: { + scales: { + x: { + display: 'auto' + }, + y: { + type: 'category', + } + } + } + }); + + var scale = chart.scales.x; + scale.ctx = window.createMockContext(); + chart.draw(); + + expect(scale.ctx.getCalls().length).toBeGreaterThan(0); + expect(scale.height).toBeGreaterThan(0); + }); + + it('should not draw the axes if no associated datasets are visible', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [100, 200, 100, 50], + xAxisId: 'foo', + hidden: true, + labels: ['Q1', 'Q2', 'Q3', 'Q4'] + }] + }, + options: { + scales: { + x: { + display: 'auto' + } + } + } + }); + + var scale = chart.scales.x; + scale.ctx = window.createMockContext(); + chart.draw(); + + expect(scale.ctx.getCalls().length).toBe(0); + expect(scale.height).toBe(0); + }); + }); + + describe('for the y axes', function() { + it('should draw the axes if at least one associated dataset is visible', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [100, 200, 100, 50], + yAxisId: 'foo', + hidden: true, + labels: ['Q1', 'Q2', 'Q3', 'Q4'] + }, { + data: [100, 200, 100, 50], + yAxisId: 'foo', + labels: ['Q1', 'Q2', 'Q3', 'Q4'] + }] + }, + options: { + scales: { + y: { + display: 'auto' + } + } + } + }); + + var scale = chart.scales.y; + scale.ctx = window.createMockContext(); + chart.draw(); + + expect(scale.ctx.getCalls().length).toBeGreaterThan(0); + expect(scale.width).toBeGreaterThan(0); + }); + + it('should not draw the axes if no associated datasets are visible', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [100, 200, 100, 50], + yAxisId: 'foo', + hidden: true, + labels: ['Q1', 'Q2', 'Q3', 'Q4'] + }] + }, + options: { + scales: { + y: { + display: 'auto' + } + } + } + }); + + var scale = chart.scales.y; + scale.ctx = window.createMockContext(); + chart.draw(); + + expect(scale.ctx.getCalls().length).toBe(0); + expect(scale.width).toBe(0); + }); + }); + }); + + describe('afterBuildTicks', function() { + it('should allow filtering of ticks', function() { + var labels = ['tick1', 'tick2', 'tick3', 'tick4', 'tick5']; + var chart = window.acquireChart({ + type: 'line', + options: { + scales: { + x: { + type: 'category', + labels: labels, + afterBuildTicks: function(scale) { + scale.ticks = scale.ticks.slice(1); + } + } + } + } + }); + + var scale = chart.scales.x; + expect(getLabels(scale)).toEqual(labels.slice(1)); + }); + + it('should allow no return value from callback', function() { + var labels = ['tick1', 'tick2', 'tick3', 'tick4', 'tick5']; + var chart = window.acquireChart({ + type: 'line', + options: { + scales: { + x: { + type: 'category', + labels: labels, + afterBuildTicks: function() { } + } + } + } + }); + + var scale = chart.scales.x; + expect(getLabels(scale)).toEqual(labels); + }); + + it('should allow empty ticks', function() { + var labels = ['tick1', 'tick2', 'tick3', 'tick4', 'tick5']; + var chart = window.acquireChart({ + type: 'line', + options: { + scales: { + x: { + type: 'category', + labels: labels, + afterBuildTicks: function(scale) { + scale.ticks = []; + } + } + } + } + }); + + var scale = chart.scales.x; + expect(scale.ticks.length).toBe(0); + }); + }); + + describe('_layers', function() { + it('should default to three layers', function() { + var chart = window.acquireChart({ + type: 'line', + options: { + scales: { + x: { + type: 'linear', + } + } + } + }); + + var scale = chart.scales.x; + expect(scale._layers().length).toEqual(3); + }); + + it('should create the chart with custom scale ids without axis or position options', function() { + function createChart() { + return window.acquireChart({ + type: 'scatter', + data: { + datasets: [{ + data: [{x: 0, y: 0}, {x: 1, y: 1}, {x: 2, y: 2}], + xAxisID: 'customIDx', + yAxisID: 'customIDy' + }] + }, + options: { + scales: { + customIDx: { + type: 'linear', + display: false + }, + customIDy: { + type: 'linear', + display: false + } + } + } + }); + } + + expect(createChart).not.toThrow(); + }); + + it('should default to one layer for custom scales', function() { + class CustomScale extends Chart.Scale { + draw() {} + convertTicksToLabels() { + return ['tick']; + } + } + CustomScale.id = 'customScale'; + CustomScale.defaults = {}; + Chart.register(CustomScale); + + var chart = window.acquireChart({ + type: 'line', + options: { + scales: { + x: { + type: 'customScale', + grid: { + z: 10 + }, + ticks: { + z: 20 + } + } + } + } + }); + + var scale = chart.scales.x; + expect(scale._layers().length).toEqual(1); + expect(scale._layers()[0].z).toEqual(20); + }); + + it('should default to one layer for custom scales for axis', function() { + class CustomScale1 extends Chart.Scale { + draw() {} + convertTicksToLabels() { + return ['tick']; + } + } + CustomScale1.id = 'customScale1'; + CustomScale1.defaults = {axis: 'x'}; + Chart.register(CustomScale1); + + var chart = window.acquireChart({ + type: 'line', + options: { + scales: { + my: { + type: 'customScale1', + grid: { + z: 10 + }, + ticks: { + z: 20 + } + } + } + } + }); + + var scale = chart.scales.my; + expect(scale._layers().length).toEqual(1); + expect(scale._layers()[0].z).toEqual(20); + }); + + it('should fail for custom scales without any axis or position', function() { + class CustomScale2 extends Chart.Scale { + draw() {} + } + CustomScale2.id = 'customScale2'; + CustomScale2.defaults = {}; + Chart.register(CustomScale2); + + function createChart() { + return window.acquireChart({ + type: 'line', + options: { + scales: { + my: { + type: 'customScale2' + } + } + } + }); + } + + expect(createChart).toThrow(new Error('Cannot determine type of \'my\' axis. Please provide \'axis\' or \'position\' option.')); + }); + + it('should return 3 layers when z is not equal between ticks and grid', function() { + var chart = window.acquireChart({ + type: 'line', + options: { + scales: { + x: { + type: 'linear', + ticks: { + z: 10 + } + } + } + } + }); + + expect(chart.scales.x._layers().length).toEqual(3); + + chart = window.acquireChart({ + type: 'line', + options: { + scales: { + x: { + type: 'linear', + grid: { + z: 11 + } + } + } + } + }); + + expect(chart.scales.x._layers().length).toEqual(3); + + chart = window.acquireChart({ + type: 'line', + options: { + scales: { + x: { + type: 'linear', + ticks: { + z: 10 + }, + grid: { + z: 11 + } + } + } + } + }); + + expect(chart.scales.x._layers().length).toEqual(3); + + }); + + }); + + describe('min and max', function() { + it('should be limited to visible data', function() { + var chart = window.acquireChart({ + type: 'scatter', + data: { + datasets: [{ + data: [{x: 100, y: 100}, {x: -100, y: -100}] + }, { + data: [{x: 10, y: 10}, {x: -10, y: -10}] + }] + }, + options: { + scales: { + x: { + id: 'x', + type: 'linear', + min: -20, + max: 20 + }, + y: { + id: 'y', + type: 'linear' + } + } + } + }); + + expect(chart.scales.x.min).toEqual(-20); + expect(chart.scales.x.max).toEqual(20); + expect(chart.scales.y.min).toEqual(-10); + expect(chart.scales.y.max).toEqual(10); + }); + }); + + describe('overrides', () => { + it('should create new scale', () => { + const chart = window.acquireChart({ + type: 'scatter', + data: { + datasets: [{ + data: [{x: 100, y: 100}, {x: -100, y: -100}] + }, { + data: [{x: 10, y: 10}, {x: -10, y: -10}] + }] + }, + options: { + scales: { + x2: { + type: 'linear', + min: -20, + max: 20 + } + } + } + }); + + expect(Object.keys(chart.scales).sort()).toEqual(['x', 'x2', 'y']); + }); + + it('should create new scale with custom name', () => { + const chart = window.acquireChart({ + type: 'scatter', + data: { + datasets: [{ + data: [{x: 100, y: 100}, {x: -100, y: -100}] + }, { + data: [{x: 10, y: 10}, {x: -10, y: -10}] + }] + }, + options: { + scales: { + scaleX: { + axis: 'x', + type: 'linear', + min: -20, + max: 20 + } + } + } + }); + + expect(Object.keys(chart.scales).sort()).toEqual(['scaleX', 'x', 'y']); + }); + + it('should throw error on scale with custom name without axis type', () => { + expect(() => window.acquireChart({ + type: 'scatter', + data: { + datasets: [{ + data: [{x: 100, y: 100}, {x: -100, y: -100}] + }, { + data: [{x: 10, y: 10}, {x: -10, y: -10}] + }] + }, + options: { + scales: { + scaleX: { + type: 'linear', + min: -20, + max: 20 + } + } + } + })).toThrow(); + }); + + it('should read options first to determine axis', () => { + const chart = window.acquireChart({ + type: 'scatter', + data: { + datasets: [{ + data: [{x: 100, y: 100}, {x: -100, y: -100}] + }, { + data: [{x: 10, y: 10}, {x: -10, y: -10}] + }] + }, + options: { + scales: { + xavier: { + axis: 'y', + type: 'linear', + min: -20, + max: 20 + } + } + } + }); + + expect(chart.scales.xavier.axis).toBe('y'); + }); + it('should center labels when rotated in x axis', () => { + const chart = window.acquireChart({ + type: 'line', + data: { + labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], + datasets: [{ + label: '# of Votes', + data: [12, 19, 3, 5, 2, 3] + }] + }, + options: { + scales: { + x: { + ticks: { + minRotation: 90, + } + } + } + } + }); + const mapper = item => parseFloat(item.options.translation[0].toFixed(2)); + const expected = [20.15, 113.6, 207.05, 300.5, 393.95, 487.4]; + const actual = chart.scales.x.getLabelItems().map(mapper); + const len = expected.length; + for (let i = 0; i < len; ++i) { + const actualValue = actual[i]; + const expectedValue = expected[i]; + expect(actualValue).toBeCloseTo(expectedValue, 1); + } + }); + }); + describe('Scale Title stroke', ()=>{ + function getChartWithScaleTitleStroke() { + return window.acquireChart({ + type: 'line', + options: { + scales: { + x: { + type: 'linear', + title: { + display: true, + text: 'title-x', + color: '#ddd', + strokeWidth: 1, + strokeColor: '#333' + } + }, + y: { + type: 'linear', + title: { + display: true, + text: 'title-y', + color: '#ddd', + strokeWidth: 2, + strokeColor: '#222' + } + } + } + } + }); + } + + function getChartWithoutScaleTitleStroke() { + return window.acquireChart({ + type: 'line', + options: { + scales: { + x: { + type: 'linear', + title: { + display: true, + text: 'title-x', + color: '#ddd' + } + }, + y: { + type: 'linear', + title: { + display: true, + text: 'title-y', + color: '#ddd' + } + } + } + } + }); + } + + it('should draw a scale title stroke when provided x-axis', function() { + var chart = getChartWithScaleTitleStroke(); + var scale = chart.scales.x; + expect(scale.options.title.strokeColor).toEqual('#333'); + expect(scale.options.title.strokeWidth).toEqual(1); + }); + + it('should draw a scale title stroke when provided y-axis', function() { + var chart = getChartWithScaleTitleStroke(); + var scale = chart.scales.y; + expect(scale.options.title.strokeColor).toEqual('#222'); + expect(scale.options.title.strokeWidth).toEqual(2); + }); + + it('should not draw a scale title stroke when not provided', function() { + var chart = getChartWithoutScaleTitleStroke(); + var scales = chart.scales; + expect(scales.y.options.title.strokeColor).toBeUndefined(); + expect(scales.y.options.title.strokeWidth).toBeUndefined(); + expect(scales.x.options.title.strokeColor).toBeUndefined(); + expect(scales.x.options.title.strokeWidth).toBeUndefined(); + }); + }); +}); diff --git a/test/specs/core.ticks.tests.js b/test/specs/core.ticks.tests.js new file mode 100644 index 00000000000..01db0cdce30 --- /dev/null +++ b/test/specs/core.ticks.tests.js @@ -0,0 +1,110 @@ +function getLabels(scale) { + return scale.ticks.map(t => t.label); +} + +describe('Test tick generators', function() { + // formatters are used as default config values so users want to be able to reference them + it('Should expose formatters api', function() { + expect(typeof Chart.Ticks).toBeDefined(); + expect(typeof Chart.Ticks.formatters).toBeDefined(); + expect(typeof Chart.Ticks.formatters.values).toBe('function'); + expect(typeof Chart.Ticks.formatters.numeric).toBe('function'); + }); + + it('Should generate linear spaced ticks with correct precision', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [] + }], + }, + options: { + plugins: { + legend: false + }, + scales: { + x: { + type: 'linear', + position: 'bottom', + ticks: { + callback: function(value) { + return value.toString(); + } + } + }, + y: { + type: 'linear', + ticks: { + callback: function(value) { + return value.toString(); + } + } + } + } + } + }); + + var xLabels = getLabels(chart.scales.x); + var yLabels = getLabels(chart.scales.y); + + expect(xLabels).toEqual(['0', '0.1', '0.2', '0.3', '0.4', '0.5', '0.6', '0.7', '0.8', '0.9', '1']); + expect(yLabels).toEqual(['0', '0.1', '0.2', '0.3', '0.4', '0.5', '0.6', '0.7', '0.8', '0.9', '1']); + }); + + it('Should generate logarithmic spaced ticks with correct precision', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [] + }], + }, + options: { + plugins: { + legend: false + }, + scales: { + x: { + type: 'logarithmic', + position: 'bottom', + min: 0.1, + max: 1, + ticks: { + autoSkip: false, + callback: function(value) { + return value.toString(); + } + } + }, + y: { + type: 'logarithmic', + min: 0.1, + max: 1, + ticks: { + autoSkip: false, + callback: function(value) { + return value.toString(); + } + } + } + } + } + }); + + var xLabels = getLabels(chart.scales.x); + var yLabels = getLabels(chart.scales.y); + + expect(xLabels).toEqual(['0.1', '0.11', '0.12', '0.13', '0.14', '0.15', '0.16', '0.17', '0.18', '0.19', '0.2', '0.25', '0.3', '0.4', '0.5', '0.6', '0.7', '0.8', '0.9', '1']); + expect(yLabels).toEqual(['0.1', '0.11', '0.12', '0.13', '0.14', '0.15', '0.16', '0.17', '0.18', '0.19', '0.2', '0.25', '0.3', '0.4', '0.5', '0.6', '0.7', '0.8', '0.9', '1']); + }); + + describe('formatters.numeric', function() { + it('should not fail on empty or 1 item array', function() { + const scale = {chart: {options: {locale: 'en'}}, options: {ticks: {format: {}}}}; + expect(Chart.Ticks.formatters.numeric.apply(scale, [1, 0, []])).toEqual('1'); + expect(Chart.Ticks.formatters.numeric.apply(scale, [1, 0, [{value: 1}]])).toEqual('1'); + expect(Chart.Ticks.formatters.numeric.apply(scale, [1, 0, [{value: 1}, {value: 1.01}]])).toEqual('1.00'); + }); + }); +}); diff --git a/test/specs/element.arc.tests.js b/test/specs/element.arc.tests.js new file mode 100644 index 00000000000..63d20caaec4 --- /dev/null +++ b/test/specs/element.arc.tests.js @@ -0,0 +1,306 @@ +// Test the rectangle element + +describe('Arc element tests', function() { + it ('should determine if in range', function() { + // Mock out the arc as if the controller put it there + var arc = new Chart.elements.ArcElement({ + startAngle: 0, + endAngle: Math.PI / 2, + x: 0, + y: 0, + innerRadius: 5, + outerRadius: 10, + options: { + spacing: 0, + offset: 0, + borderWidth: 0 + } + }); + + expect(arc.inRange(2, 2)).toBe(false); + expect(arc.inRange(7, 0)).toBe(true); + expect(arc.inRange(0, 11)).toBe(false); + expect(arc.inRange(Math.sqrt(32), Math.sqrt(32))).toBe(true); + expect(arc.inRange(-1.0 * Math.sqrt(7), Math.sqrt(7))).toBe(false); + }); + + it ('should determine if in range when full circle', function() { + // Mock out the arc as if the controller put it there + var arc = new Chart.elements.ArcElement({ + startAngle: 0, + endAngle: Math.PI * 2, + x: 0, + y: 0, + innerRadius: 5, + outerRadius: 10, + options: { + spacing: 0, + offset: 0, + borderWidth: 0 + } + }); + + for (const radius of [5, 7.5, 10]) { + for (let angle = 0; angle <= 360; angle += 22.5) { + const rad = angle / 180 * Math.PI; + const x = Math.sin(rad) * radius; + const y = Math.cos(rad) * radius; + expect(arc.inRange(x, y)).withContext(`radius: ${radius}, angle: ${angle}`).toBeTrue(); + } + } + for (const radius of [4, 11]) { + for (let angle = 0; angle <= 360; angle += 22.5) { + const rad = angle / 180 * Math.PI; + const x = Math.sin(rad) * radius; + const y = Math.cos(rad) * radius; + expect(arc.inRange(x, y)).withContext(`radius: ${radius}, angle: ${angle}`).toBeFalse(); + } + } + }); + + it ('should include spacing for in range check', function() { + // Mock out the arc as if the controller put it there + var arc = new Chart.elements.ArcElement({ + startAngle: 0, + endAngle: Math.PI / 2, + x: 0, + y: 0, + innerRadius: 5, + outerRadius: 10, + options: { + spacing: 10, + offset: 0, + borderWidth: 0 + } + }); + + expect(arc.inRange(7, 0)).toBe(false); + expect(arc.inRange(15, 0)).toBe(true); + }); + + it ('should include borderWidth for in range check', function() { + // Mock out the arc as if the controller put it there + var arc = new Chart.elements.ArcElement({ + startAngle: 0, + endAngle: Math.PI / 2, + x: 0, + y: 0, + innerRadius: 5, + outerRadius: 10, + options: { + spacing: 0, + offset: 0, + borderWidth: 10 + } + }); + + expect(arc.inRange(7, 0)).toBe(false); + expect(arc.inRange(15, 0)).toBe(true); + }); + + it ('should determine if in range, when full circle', function() { + // Mock out the arc as if the controller put it there + var arc = new Chart.elements.ArcElement({ + startAngle: -Math.PI, + endAngle: Math.PI * 1.5, + x: 0, + y: 0, + innerRadius: 0, + outerRadius: 10, + circumference: Math.PI * 2, + options: { + spacing: 0, + offset: 0, + borderWidth: 0 + } + }); + + expect(arc.inRange(7, 7)).toBe(true); + }); + + it ('should get the tooltip position', function() { + // Mock out the arc as if the controller put it there + var arc = new Chart.elements.ArcElement({ + startAngle: 0, + endAngle: Math.PI / 2, + x: 0, + y: 0, + innerRadius: 0, + outerRadius: Math.sqrt(2), + options: { + spacing: 0, + offset: 0, + borderWidth: 0 + } + }); + + var pos = arc.tooltipPosition(); + expect(pos.x).toBeCloseTo(0.5); + expect(pos.y).toBeCloseTo(0.5); + }); + + it ('should get the center', function() { + // Mock out the arc as if the controller put it there + var arc = new Chart.elements.ArcElement({ + startAngle: 0, + endAngle: Math.PI / 2, + x: 0, + y: 0, + innerRadius: 0, + outerRadius: Math.sqrt(2), + options: { + spacing: 0, + offset: 0, + borderWidth: 0 + } + }); + + var center = arc.getCenterPoint(); + expect(center.x).toBeCloseTo(0.5, 6); + expect(center.y).toBeCloseTo(0.5, 6); + }); + + it ('should get the center with offset and spacing', function() { + // Mock out the arc as if the controller put it there + var arc = new Chart.elements.ArcElement({ + startAngle: 0, + endAngle: Math.PI / 2, + x: 0, + y: 0, + innerRadius: 0, + outerRadius: Math.sqrt(2), + options: { + spacing: 10, + offset: 10, + borderWidth: 0 + } + }); + + var center = arc.getCenterPoint(); + expect(center.x).toBeCloseTo(7.57, 1); + expect(center.y).toBeCloseTo(7.57, 1); + }); + + it ('should get the center of full circle before and after draw', function() { + // Mock out the arc as if the controller put it there + var arc = new Chart.elements.ArcElement({ + startAngle: 0, + endAngle: Math.PI * 2, + x: 2, + y: 2, + innerRadius: 0, + outerRadius: 2, + options: { + spacing: 0, + offset: 0, + borderWidth: 0 + } + }); + + var center = arc.getCenterPoint(); + expect(center.x).toBeCloseTo(1, 6); + expect(center.y).toBeCloseTo(2, 6); + + var ctx = window.createMockContext(); + arc.draw(ctx); + + center = arc.getCenterPoint(); + expect(center.x).toBeCloseTo(1, 6); + expect(center.y).toBeCloseTo(2, 6); + }); + + it('should not draw when radius < 0', function() { + var ctx = window.createMockContext(); + + var arc = new Chart.elements.ArcElement({ + startAngle: 0, + endAngle: Math.PI / 2, + x: 0, + y: 0, + innerRadius: -0.1, + outerRadius: Math.sqrt(2), + options: { + spacing: 0, + offset: 0, + borderWidth: 0 + } + }); + + arc.draw(ctx); + + expect(ctx.getCalls().length).toBe(0); + + arc = new Chart.elements.ArcElement({ + startAngle: 0, + endAngle: Math.PI / 2, + x: 0, + y: 0, + innerRadius: 0, + outerRadius: -1, + options: { + spacing: 0, + offset: 0, + borderWidth: 0 + } + }); + + arc.draw(ctx); + + expect(ctx.getCalls().length).toBe(0); + }); + + it('should draw when circular: false', function() { + var arc = new Chart.elements.ArcElement({ + startAngle: 0, + endAngle: Math.PI * 2, + x: 2, + y: 2, + innerRadius: 0, + outerRadius: 2, + options: { + spacing: 0, + offset: 0, + borderWidth: 0, + scales: { + r: { + grid: { + circular: false, + }, + }, + }, + elements: { + arc: { + circular: false + }, + }, + } + }); + + var ctx = window.createMockContext(); + arc.draw(ctx); + + expect(ctx.getCalls().length).toBeGreaterThan(0); + }); + + it ('should determine not in range when angle 0', function() { + // Mock out the arc as if the controller put it there + var arc = new Chart.elements.ArcElement({ + startAngle: 0, + endAngle: 0, + x: 0, + y: 0, + innerRadius: 0, + outerRadius: 10, + circumference: 0, + options: { + spacing: 0, + offset: 0, + borderWidth: 0 + } + }); + + var center = arc.getCenterPoint(); + + expect(arc.inRange(center.x, 1)).toBe(false); + }); +}); diff --git a/test/specs/element.bar.tests.js b/test/specs/element.bar.tests.js new file mode 100644 index 00000000000..128bdddc12e --- /dev/null +++ b/test/specs/element.bar.tests.js @@ -0,0 +1,67 @@ +// Test the bar element + +describe('Bar element tests', function() { + it('Should correctly identify as in range', function() { + var bar = new Chart.elements.BarElement({ + base: 0, + width: 4, + x: 10, + y: 15 + }); + + expect(bar.inRange(10, 15)).toBe(true); + expect(bar.inRange(10, 10)).toBe(true); + expect(bar.inRange(10, 16)).toBe(false); + expect(bar.inRange(5, 5)).toBe(false); + + // Test when the y is below the base (negative bar) + var negativeBar = new Chart.elements.BarElement({ + base: 0, + width: 4, + x: 10, + y: -15 + }); + + expect(negativeBar.inRange(10, -16)).toBe(false); + expect(negativeBar.inRange(10, 1)).toBe(false); + expect(negativeBar.inRange(10, -5)).toBe(true); + }); + + it('should get the correct tooltip position', function() { + var bar = new Chart.elements.BarElement({ + base: 0, + width: 4, + x: 10, + y: 15 + }); + + expect(bar.tooltipPosition()).toEqual({ + x: 10, + y: 15, + }); + + // Test when the y is below the base (negative bar) + var negativeBar = new Chart.elements.BarElement({ + base: -10, + width: 4, + x: 10, + y: -15 + }); + + expect(negativeBar.tooltipPosition()).toEqual({ + x: 10, + y: -15, + }); + }); + + it('should get the center', function() { + var bar = new Chart.elements.BarElement({ + base: 0, + width: 4, + x: 10, + y: 15 + }); + + expect(bar.getCenterPoint()).toEqual({x: 10, y: 7.5}); + }); +}); diff --git a/test/specs/element.line.tests.js b/test/specs/element.line.tests.js new file mode 100644 index 00000000000..b368a3e19af --- /dev/null +++ b/test/specs/element.line.tests.js @@ -0,0 +1,35 @@ +// Tests for the line element +describe('Chart.elements.LineElement', function() { + describe('auto', jasmine.fixture.specs('element.line')); + + it('should be constructed', function() { + var line = new Chart.elements.LineElement({ + points: [1, 2, 3, 4] + }); + + expect(line).not.toBe(undefined); + expect(line.points).toEqual([1, 2, 3, 4]); + }); + + it('should not cache path when animations are enabled', function(done) { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [0, -1, 0], + label: 'dataset1', + }], + labels: ['label1', 'label2', 'label3'] + }, + options: { + animation: { + duration: 50, + onComplete: () => { + expect(chart.getDatasetMeta(0).dataset._path).toBeUndefined(); + done(); + } + } + } + }); + }); +}); diff --git a/test/specs/element.point.tests.js b/test/specs/element.point.tests.js new file mode 100644 index 00000000000..90526e6f72f --- /dev/null +++ b/test/specs/element.point.tests.js @@ -0,0 +1,69 @@ +describe('Chart.elements.PointElement', function() { + describe('auto', jasmine.fixture.specs('element.point')); + + it ('Should correctly identify as in range', function() { + // Mock out the point as if we were made by the controller + var point = new Chart.elements.PointElement({ + options: { + radius: 2, + hitRadius: 3, + }, + x: 10, + y: 15 + }); + + expect(point.inRange(10, 15)).toBe(true); + expect(point.inRange(10, 10)).toBe(false); + expect(point.inRange(10, 5)).toBe(false); + expect(point.inRange(5, 5)).toBe(false); + }); + + it ('should get the correct tooltip position', function() { + // Mock out the point as if we were made by the controller + var point = new Chart.elements.PointElement({ + options: { + radius: 2, + borderWidth: 6, + }, + x: 10, + y: 15 + }); + + expect(point.tooltipPosition()).toEqual({ + x: 10, + y: 15 + }); + }); + + it('should get the correct center point', function() { + // Mock out the point as if we were made by the controller + var point = new Chart.elements.PointElement({ + options: { + radius: 2, + }, + x: 10, + y: 10 + }); + + expect(point.getCenterPoint()).toEqual({x: 10, y: 10}); + }); + + it ('should not draw if skipped', function() { + var mockContext = window.createMockContext(); + + // Mock out the point as if we were made by the controller + var point = new Chart.elements.PointElement({ + options: { + radius: 2, + hitRadius: 3, + }, + x: 10, + y: 15, + skip: true + }); + + point.draw(mockContext); + + expect(mockContext.getCalls()).toEqual([]); + }); +}); diff --git a/test/specs/global.defaults.tests.js b/test/specs/global.defaults.tests.js new file mode 100644 index 00000000000..c551270fd33 --- /dev/null +++ b/test/specs/global.defaults.tests.js @@ -0,0 +1,198 @@ +describe('Default Configs', function() { + describe('Doughnut Chart', function() { + it('should return correct legend label objects', function() { + var chart = window.acquireChart({ + type: 'doughnut', + data: { + labels: ['label1', 'label2', 'label3'], + datasets: [{ + data: [10, 20, NaN], + backgroundColor: ['red', 'green', 'blue'], + borderWidth: 2, + borderColor: '#000' + }] + }, + }); + + var expectedCommon = { + fontColor: '#666', + hidden: false, + strokeStyle: '#000', + textAlign: undefined, + lineWidth: 2, + pointStyle: undefined, + lineDash: [], + lineDashOffset: 0, + lineJoin: undefined, + borderRadius: undefined, + }; + + var expected = [{ + text: 'label1', + fillStyle: 'red', + index: 0, + ...expectedCommon, + }, { + text: 'label2', + fillStyle: 'green', + index: 1, + ...expectedCommon, + }, { + text: 'label3', + fillStyle: 'blue', + index: 2, + ...expectedCommon, + }]; + expect(chart.legend.legendItems).toEqual(expected); + }); + + it('should return correct legend label objects with border radius', function() { + var chart = window.acquireChart({ + type: 'doughnut', + data: { + labels: ['label1'], + datasets: [{ + data: [10], + backgroundColor: ['red'], + borderWidth: 2, + borderColor: '#000', + borderDash: [1, 2, 3], + borderDashOffset: 1, + borderJoinStyle: 'miter', + borderRadius: 3, + }] + }, + options: { + plugins: { + legend: { + labels: { + useBorderRadius: true, + borderRadius: 5, + textAlign: 'left', + } + } + } + } + }); + + var expected = [{ + text: 'label1', + fillStyle: 'red', + index: 0, + fontColor: '#666', + hidden: false, + strokeStyle: '#000', + textAlign: 'left', + lineWidth: 2, + pointStyle: undefined, + lineDash: [1, 2, 3], + lineDashOffset: 1, + lineJoin: 'miter', + borderRadius: 5 + }]; + expect(chart.legend.legendItems).toEqual(expected); + }); + + it('should hide the correct arc when a legend item is clicked', function() { + var config = Chart.overrides.doughnut; + var chart = window.acquireChart({ + type: 'doughnut', + data: { + labels: ['label1', 'label2', 'label3'], + datasets: [{ + data: [10, 20, NaN], + backgroundColor: ['red', 'green', 'blue'], + borderWidth: 2, + borderColor: '#000' + }] + }, + }); + spyOn(chart, 'update').and.callThrough(); + + var legendItem = chart.legend.legendItems[0]; + config.plugins.legend.onClick(null, legendItem, chart.legend); + + expect(chart.getDataVisibility(0)).toBe(false); + expect(chart.update).toHaveBeenCalled(); + + config.plugins.legend.onClick(null, legendItem, chart.legend); + expect(chart.getDataVisibility(0)).toBe(true); + }); + }); + + describe('Polar Area Chart', function() { + it('should return correct legend label objects', function() { + var chart = window.acquireChart({ + type: 'polarArea', + data: { + labels: ['label1', 'label2', 'label3'], + datasets: [{ + data: [10, 20, NaN], + backgroundColor: ['red', 'green', 'blue'], + borderWidth: 2, + borderColor: '#000' + }] + }, + }); + + var expected = [{ + text: 'label1', + fillStyle: 'red', + fontColor: '#666', + hidden: false, + index: 0, + strokeStyle: '#000', + textAlign: undefined, + lineWidth: 2, + pointStyle: undefined + }, { + text: 'label2', + fillStyle: 'green', + fontColor: '#666', + hidden: false, + index: 1, + strokeStyle: '#000', + textAlign: undefined, + lineWidth: 2, + pointStyle: undefined + }, { + text: 'label3', + fillStyle: 'blue', + fontColor: '#666', + hidden: false, + index: 2, + strokeStyle: '#000', + textAlign: undefined, + lineWidth: 2, + pointStyle: undefined + }]; + expect(chart.legend.legendItems).toEqual(expected); + }); + + it('should hide the correct arc when a legend item is clicked', function() { + var config = Chart.overrides.polarArea; + var chart = window.acquireChart({ + type: 'polarArea', + data: { + labels: ['label1', 'label2', 'label3'], + datasets: [{ + data: [10, 20, NaN], + backgroundColor: ['red', 'green', 'blue'], + borderWidth: 2, + borderColor: '#000' + }] + }, + }); + spyOn(chart, 'update').and.callThrough(); + + var legendItem = chart.legend.legendItems[0]; + config.plugins.legend.onClick(null, legendItem, chart.legend); + + expect(chart.getDataVisibility(0)).toBe(false); + expect(chart.update).toHaveBeenCalled(); + + config.plugins.legend.onClick(null, legendItem, chart.legend); + expect(chart.getDataVisibility(0)).toBe(true); + }); + }); +}); diff --git a/test/specs/global.namespace.tests.js b/test/specs/global.namespace.tests.js new file mode 100644 index 00000000000..011bfc21409 --- /dev/null +++ b/test/specs/global.namespace.tests.js @@ -0,0 +1,39 @@ +describe('Chart namespace', function() { + describe('Chart', function() { + it('should a function (constructor)', function() { + expect(Chart instanceof Function).toBeTruthy(); + }); + it('should define "core" properties', function() { + expect(Chart instanceof Function).toBeTruthy(); + expect(Chart.Animation instanceof Object).toBeTruthy(); + expect(Chart.Animations instanceof Object).toBeTruthy(); + expect(Chart.defaults instanceof Object).toBeTruthy(); + expect(Chart.Element instanceof Object).toBeTruthy(); + expect(Chart.Interaction instanceof Object).toBeTruthy(); + expect(Chart.layouts instanceof Object).toBeTruthy(); + + expect(Chart.platforms.BasePlatform instanceof Function).toBeTruthy(); + expect(Chart.platforms.BasicPlatform instanceof Function).toBeTruthy(); + expect(Chart.platforms.DomPlatform instanceof Function).toBeTruthy(); + + expect(Chart.registry instanceof Object).toBeTruthy(); + expect(Chart.Scale instanceof Object).toBeTruthy(); + expect(Chart.Ticks instanceof Object).toBeTruthy(); + }); + }); + + describe('Chart.elements', function() { + it('should contains "elements" classes', function() { + expect(Chart.elements.ArcElement instanceof Function).toBeTruthy(); + expect(Chart.elements.BarElement instanceof Function).toBeTruthy(); + expect(Chart.elements.LineElement instanceof Function).toBeTruthy(); + expect(Chart.elements.PointElement instanceof Function).toBeTruthy(); + }); + }); + + describe('Chart.helpers', function() { + it('should be an object', function() { + expect(Chart.helpers instanceof Object).toBeTruthy(); + }); + }); +}); diff --git a/test/specs/helpers.canvas.tests.js b/test/specs/helpers.canvas.tests.js new file mode 100644 index 00000000000..ba28e3f78d9 --- /dev/null +++ b/test/specs/helpers.canvas.tests.js @@ -0,0 +1,355 @@ +'use strict'; + +describe('Chart.helpers.canvas', function() { + describe('auto', jasmine.fixture.specs('helpers')); + + var helpers = Chart.helpers; + + describe('clearCanvas', function() { + it('should clear the chart canvas', function() { + var chart = acquireChart({}, { + canvas: { + style: 'width: 150px; height: 245px' + } + }); + + spyOn(chart.ctx, 'clearRect'); + + helpers.clearCanvas(chart.canvas, chart.ctx); + + expect(chart.ctx.clearRect.calls.count()).toBe(1); + expect(chart.ctx.clearRect.calls.first().object).toBe(chart.ctx); + expect(chart.ctx.clearRect.calls.first().args).toEqual([0, 0, 150, 245]); + }); + + it('should not throw error when chart is null', function() { + function createAndClearChart() { + var chart = acquireChart({}, { + canvas: null + }); + // explicitly set canvas and ctx to null since setting it in acquireChart doesn't do anything + chart.canvas = null; + chart.ctx = null; + + helpers.clearCanvas(chart.canvas, chart.ctx); + } + + expect(createAndClearChart).not.toThrow(); + }); + }); + + describe('isPointInArea', function() { + it('should return true when no area is provided', function() { + expect(helpers._isPointInArea({x: 1, y: 1})).toBe(true); + }); + it('should determine if a point is in the area', function() { + var isPointInArea = helpers._isPointInArea; + var area = {left: 0, top: 0, right: 512, bottom: 256}; + + expect(isPointInArea({x: 0, y: 0}, area)).toBe(true); + expect(isPointInArea({x: -1e-12, y: -1e-12}, area)).toBe(true); + expect(isPointInArea({x: 512, y: 256}, area)).toBe(true); + expect(isPointInArea({x: 512 + 1e-12, y: 256 + 1e-12}, area)).toBe(true); + expect(isPointInArea({x: -0.5, y: 0}, area)).toBe(false); + expect(isPointInArea({x: 0, y: 256.5}, area)).toBe(false); + }); + }); + + it('should return the width of the longest text in an Array and 2D Array', function() { + var context = window.createMockContext(); + var font = "normal 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"; + var arrayOfThings1D = ['FooBar', 'Bar']; + var arrayOfThings2D = [['FooBar_1', 'Bar_2'], 'Foo_1']; + + + // Regardless 'FooBar' is the longest label it should return (characters * 10) + expect(helpers._longestText(context, font, arrayOfThings1D, {})).toEqual(60); + expect(helpers._longestText(context, font, arrayOfThings2D, {})).toEqual(80); + // We check to make sure we made the right calls to the canvas. + expect(context.getCalls()).toEqual([{ + name: 'save', + args: [] + }, { + name: 'setFont', + args: ["normal 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"], + }, { + name: 'measureText', + args: ['FooBar'] + }, { + name: 'measureText', + args: ['Bar'] + }, { + name: 'restore', + args: [] + }, { + name: 'save', + args: [] + }, { + name: 'setFont', + args: ["normal 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"], + }, { + name: 'measureText', + args: ['FooBar_1'] + }, { + name: 'measureText', + args: ['Bar_2'] + }, { + name: 'measureText', + args: ['Foo_1'] + }, { + name: 'restore', + args: [] + }]); + }); + + it('compare text with current longest and update', function() { + var context = window.createMockContext(); + var data = {}; + var gc = []; + var longest = 70; + + expect(helpers._measureText(context, data, gc, longest, 'foobar')).toEqual(70); + expect(helpers._measureText(context, data, gc, longest, 'foobar_')).toEqual(70); + expect(helpers._measureText(context, data, gc, longest, 'foobar_1')).toEqual(80); + // We check to make sure we made the right calls to the canvas. + expect(context.getCalls()).toEqual([{ + name: 'measureText', + args: ['foobar'] + }, { + name: 'measureText', + args: ['foobar_'] + }, { + name: 'measureText', + args: ['foobar_1'] + }]); + }); + + describe('renderText', function() { + it('should render multiple lines of text', function() { + var context = window.createMockContext(); + var font = {string: '12px arial', lineHeight: 20}; + helpers.renderText(context, ['foo', 'foo2'], 0, 0, font); + + expect(context.getCalls()).toEqual([{ + name: 'save', + args: [], + }, { + name: 'setFont', + args: ['12px arial'], + }, { + name: 'fillText', + args: ['foo', 0, 0, undefined], + }, { + name: 'fillText', + args: ['foo2', 0, 20, undefined], + }, { + name: 'restore', + args: [], + }]); + }); + + it('should accept the text maxWidth', function() { + var context = window.createMockContext(); + var font = {string: '12px arial', lineHeight: 20}; + helpers.renderText(context, 'foo', 0, 0, font, {maxWidth: 30}); + expect(context.getCalls()).toEqual([{ + name: 'save', + args: [], + }, { + name: 'setFont', + args: ['12px arial'], + }, { + name: 'fillText', + args: ['foo', 0, 0, 30], + }, { + name: 'restore', + args: [], + }]); + }); + + it('should underline the text', function() { + var context = window.createMockContext(); + var font = {string: '12px arial', lineHeight: 20}; + helpers.renderText(context, 'foo', 0, 0, font, {decorationWidth: 3, underline: true}); + + expect(context.getCalls()).toEqual([{ + name: 'save', + args: [], + }, { + name: 'setFont', + args: ['12px arial'], + }, { + name: 'fillText', + args: ['foo', 0, 0, undefined], + }, { + name: 'measureText', + args: ['foo'], + }, { + name: 'setStrokeStyle', + args: [null], + }, { + name: 'beginPath', + args: [], + }, { + name: 'setLineWidth', + args: [3], + }, { + name: 'moveTo', + args: [-15, 8], + }, { + name: 'lineTo', + args: [25, 8], + }, { + name: 'stroke', + args: [], + }, { + name: 'restore', + args: [], + }]); + }); + + it('should strikethrough the text', function() { + var context = window.createMockContext(); + var font = {string: '12px arial', lineHeight: 20}; + helpers.renderText(context, 'foo', 0, 0, font, {strikethrough: true}); + + expect(context.getCalls()).toEqual([{ + name: 'save', + args: [], + }, { + name: 'setFont', + args: ['12px arial'], + }, { + name: 'fillText', + args: ['foo', 0, 0, undefined], + }, { + name: 'measureText', + args: ['foo'], + }, { + name: 'setStrokeStyle', + args: [null], + }, { + name: 'beginPath', + args: [], + }, { + name: 'setLineWidth', + args: [2], + }, { + name: 'moveTo', + args: [-15, 2], + }, { + name: 'lineTo', + args: [25, 2], + }, { + name: 'stroke', + args: [], + }, { + name: 'restore', + args: [], + }]); + }); + + it('should set the fill style if supplied', function() { + var context = window.createMockContext(); + var font = {string: '12px arial', lineHeight: 20}; + helpers.renderText(context, 'foo', 0, 0, font, {color: 'red'}); + + expect(context.getCalls()).toEqual([{ + name: 'save', + args: [], + }, { + name: 'setFont', + args: ['12px arial'], + }, { + name: 'setFillStyle', + args: ['red'], + }, { + name: 'fillText', + args: ['foo', 0, 0, undefined], + }, { + name: 'restore', + args: [], + }]); + }); + + it('should set the stroke style if supplied', function() { + var context = window.createMockContext(); + var font = {string: '12px arial', lineHeight: 20}; + helpers.renderText(context, 'foo', 0, 0, font, {strokeColor: 'red', strokeWidth: 2}); + expect(context.getCalls()).toEqual([{ + name: 'save', + args: [], + }, { + name: 'setFont', + args: ['12px arial'], + }, { + name: 'setStrokeStyle', + args: ['red'], + }, { + name: 'setLineWidth', + args: [2], + }, { + name: 'strokeText', + args: ['foo', 0, 0, undefined], + }, { + name: 'fillText', + args: ['foo', 0, 0, undefined], + }, { + name: 'restore', + args: [], + }]); + }); + + it('should set the text alignment', function() { + var context = window.createMockContext(); + var font = {string: '12px arial', lineHeight: 20}; + helpers.renderText(context, 'foo', 0, 0, font, {textAlign: 'left', textBaseline: 'middle'}); + + expect(context.getCalls()).toEqual([{ + name: 'save', + args: [], + }, { + name: 'setFont', + args: ['12px arial'], + }, { + name: 'setTextAlign', + args: ['left'], + }, { + name: 'setTextBaseline', + args: ['middle'], + }, { + name: 'fillText', + args: ['foo', 0, 0, undefined], + }, { + name: 'restore', + args: [], + }]); + }); + + it('should translate and rotate text', function() { + var context = window.createMockContext(); + var font = {string: '12px arial', lineHeight: 20}; + helpers.renderText(context, 'foo', 0, 0, font, {rotation: 90, translation: [10, 20]}); + + expect(context.getCalls()).toEqual([{ + name: 'save', + args: [], + }, { + name: 'setFont', + args: ['12px arial'], + }, { + name: 'translate', + args: [10, 20], + }, { + name: 'rotate', + args: [90], + }, { + name: 'fillText', + args: ['foo', 0, 0, undefined], + }, { + name: 'restore', + args: [], + }]); + }); + }); +}); diff --git a/test/specs/helpers.collection.tests.js b/test/specs/helpers.collection.tests.js new file mode 100644 index 00000000000..432bfb54297 --- /dev/null +++ b/test/specs/helpers.collection.tests.js @@ -0,0 +1,46 @@ +const {_filterBetween, _lookup, _lookupByKey, _rlookupByKey} = Chart.helpers; + +describe('helpers.collection', function() { + it('Should do binary search', function() { + const data = [0, 2, 6, 9]; + expect(_lookup(data, 0)).toEqual({lo: 0, hi: 1}); + expect(_lookup(data, 1)).toEqual({lo: 0, hi: 1}); + expect(_lookup(data, 3)).toEqual({lo: 1, hi: 2}); + expect(_lookup(data, 6)).toEqual({lo: 1, hi: 2}); + expect(_lookup(data, 9)).toEqual({lo: 2, hi: 3}); + }); + + it('Should do binary search by key', function() { + const data = [{x: 0}, {x: 2}, {x: 6}, {x: 9}]; + expect(_lookupByKey(data, 'x', 0)).toEqual({lo: 0, hi: 1}); + expect(_lookupByKey(data, 'x', 1)).toEqual({lo: 0, hi: 1}); + expect(_lookupByKey(data, 'x', 3)).toEqual({lo: 1, hi: 2}); + expect(_lookupByKey(data, 'x', 6)).toEqual({lo: 1, hi: 2}); + expect(_lookupByKey(data, 'x', 9)).toEqual({lo: 2, hi: 3}); + }); + + it('Should do binary search by key with last', () => { + expect(_lookupByKey([{x: 0}, {x: 2}, {x: 6}, {x: 9}], 'x', 25, true)).toEqual({lo: 2, hi: 3}); + expect(_lookupByKey([{x: 0}, {x: 2}, {x: 9}, {x: 9}], 'x', 25, true)).toEqual({lo: 2, hi: 3}); + expect(_lookupByKey([{x: 0}, {x: 2}, {x: 9}, {x: 9}, {x: 22}], 'x', 25, true)).toEqual({lo: 3, hi: 4}); + expect(_lookupByKey([{x: 0}, {x: 2}, {x: 25}, {x: 28}], 'x', 25, true)).toEqual({lo: 1, hi: 2}); + expect(_lookupByKey([{x: 0}, {x: 2}, {x: 25}, {x: 25}], 'x', 25, true)).toEqual({lo: 2, hi: 3}); + expect(_lookupByKey([{x: 0}, {x: 2}, {x: 25}, {x: 25}, {x: 28}], 'x', 25, true)).toEqual({lo: 2, hi: 3}); + expect(_lookupByKey([{x: 0}, {x: 2}, {x: 25}, {x: 25}, {x: 25}, {x: 28}, {x: 29}], 'x', 25, true)).toEqual({lo: 3, hi: 4}); + }); + + it('Should do reverse binary search by key', function() { + const data = [{x: 10}, {x: 7}, {x: 3}, {x: 0}]; + expect(_rlookupByKey(data, 'x', 0)).toEqual({lo: 2, hi: 3}); + expect(_rlookupByKey(data, 'x', 3)).toEqual({lo: 2, hi: 3}); + expect(_rlookupByKey(data, 'x', 5)).toEqual({lo: 1, hi: 2}); + expect(_rlookupByKey(data, 'x', 8)).toEqual({lo: 0, hi: 1}); + expect(_rlookupByKey(data, 'x', 10)).toEqual({lo: 0, hi: 1}); + }); + + it('Should filter a sorted array', function() { + expect(_filterBetween([1, 2, 3, 4, 5, 6, 7, 8, 9], 5, 8)).toEqual([5, 6, 7, 8]); + expect(_filterBetween([1], 1, 1)).toEqual([1]); + expect(_filterBetween([1583049600000], 1584816327553, 1585680327553)).toEqual([]); + }); +}); diff --git a/test/specs/helpers.color.tests.js b/test/specs/helpers.color.tests.js new file mode 100644 index 00000000000..228d1980774 --- /dev/null +++ b/test/specs/helpers.color.tests.js @@ -0,0 +1,47 @@ +const {color, getHoverColor} = Chart.helpers; + +describe('Color helper', function() { + function isColorInstance(obj) { + return typeof obj === 'object' && obj.valid; + } + + it('should return a color when called with a color', function() { + expect(isColorInstance(color('rgb(1, 2, 3)'))).toBe(true); + }); +}); + +describe('Background hover color helper', function() { + it('should return a modified version of color when called with a color', function() { + var originalColorRGB = 'rgb(70, 191, 189)'; + + expect(getHoverColor('#46BFBD')).not.toEqual(originalColorRGB); + }); +}); + +describe('color and getHoverColor helpers', function() { + it('should return a CanvasPattern when called with a CanvasPattern', function(done) { + var dots = new Image(); + dots.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAMAAAAolt3jAAAAD1BMVEUAAAD///////////////+PQt5oAAAABXRSTlMAHlFhZsfk/BEAAAAqSURBVHgBY2BgZGJmYmSAAUYWEIDzmcBcJhiXGcxlRpPFrhdmMiqgvX0AcGIBEUAo6UAAAAAASUVORK5CYII='; + dots.onload = function() { + var chartContext = document.createElement('canvas').getContext('2d'); + var patternCanvas = document.createElement('canvas'); + var patternContext = patternCanvas.getContext('2d'); + var pattern = patternContext.createPattern(dots, 'repeat'); + patternContext.fillStyle = pattern; + var chartPattern = chartContext.createPattern(patternCanvas, 'repeat'); + + expect(color(chartPattern) instanceof CanvasPattern).toBe(true); + expect(getHoverColor(chartPattern) instanceof CanvasPattern).toBe(true); + + done(); + }; + }); + + it('should return a CanvasGradient when called with a CanvasGradient', function() { + var context = document.createElement('canvas').getContext('2d'); + var gradient = context.createLinearGradient(0, 1, 2, 3); + + expect(color(gradient) instanceof CanvasGradient).toBe(true); + expect(getHoverColor(gradient) instanceof CanvasGradient).toBe(true); + }); +}); diff --git a/test/specs/helpers.config.tests.js b/test/specs/helpers.config.tests.js new file mode 100644 index 00000000000..50054dfed8a --- /dev/null +++ b/test/specs/helpers.config.tests.js @@ -0,0 +1,877 @@ +describe('Chart.helpers.config', function() { + const {getHoverColor, _createResolver, _attachContext} = Chart.helpers; + + describe('_createResolver', function() { + it('should resolve to raw values', function() { + const defaults = { + color: 'red', + backgroundColor: 'green', + hoverColor: (ctx, options) => getHoverColor(options.color) + }; + const options = { + color: 'blue' + }; + const resolver = _createResolver([options, defaults]); + expect(resolver.color).toEqual('blue'); + expect(resolver.backgroundColor).toEqual('green'); + expect(resolver.hoverColor).toEqual(defaults.hoverColor); + }); + + it('should resolve to parent scopes, when _fallback is true', function() { + const descriptors = { + _fallback: true + }; + const defaults = { + root: true, + sub: { + child: true + } + }; + const options = { + child: 'sub default comes before this', + opt: 'opt' + }; + const resolver = _createResolver([options, defaults, descriptors]); + const sub = resolver.sub; + expect(sub.root).toEqual(true); + expect(sub.child).toEqual(true); + expect(sub.opt).toEqual('opt'); + }); + + it('should support overriding options', function() { + const defaults = { + option1: 'defaults1', + option2: 'defaults2', + option3: 'defaults3', + }; + const options = { + option1: 'options1', + option2: 'options2' + }; + const overrides = { + option1: 'override1' + }; + const resolver = _createResolver([options, defaults]); + expect(resolver).toEqualOptions({ + option1: 'options1', + option2: 'options2', + option3: 'defaults3' + }); + expect(resolver.override(overrides)).toEqualOptions({ + option1: 'override1', + option2: 'options2', + option3: 'defaults3' + }); + }); + + it('should support common object methods', function() { + const defaults = { + option1: 'defaults' + }; + class Options { + constructor() { + this.option2 = 'options'; + } + get getter() { + return 'options getter'; + } + } + const options = new Options(); + + const resolver = _createResolver([options, defaults]); + + expect(Object.prototype.hasOwnProperty.call(resolver, 'option2')).toBeTrue(); + + expect(Object.prototype.hasOwnProperty.call(resolver, 'option1')).toBeFalse(); + expect(Object.prototype.hasOwnProperty.call(resolver, 'getter')).toBeFalse(); + expect(Object.prototype.hasOwnProperty.call(resolver, 'nonexistent')).toBeFalse(); + + expect(Object.keys(resolver)).toEqual(['option2']); + expect(Object.getOwnPropertyNames(resolver)).toEqual(['option2', 'option1']); + + expect('option2' in resolver).toBeTrue(); + expect('option1' in resolver).toBeTrue(); + expect('getter' in resolver).toBeFalse(); + expect('nonexistent' in resolver).toBeFalse(); + + expect(resolver instanceof Options).toBeTrue(); + + expect(resolver.getter).toEqual('options getter'); + }); + + it('should not fail on when options are frozen', function() { + function create() { + const defaults = Object.freeze({default: true}); + const options = Object.freeze({value: true}); + return _createResolver([options, defaults]); + } + expect(create).not.toThrow(); + }); + + describe('_fallback', function() { + it('should follow simple _fallback', function() { + const defaults = { + interaction: { + mode: 'test', + priority: 'fall' + }, + hover: { + _fallback: 'interaction', + priority: 'main' + } + }; + const options = { + interaction: { + a: 1 + }, + hover: { + b: 2 + } + }; + const resolver = _createResolver([options, defaults]); + expect(resolver.hover).toEqualOptions({ + mode: 'test', + priority: 'main', + a: 1, + b: 2 + }); + }); + + it('should support _fallback as function', function() { + const descriptors = { + _fallback: (prop, value) => prop === 'hover' && value.shouldFall && 'interaction', + }; + const defaults = { + interaction: { + mode: 'test', + priority: 'fall' + }, + hover: { + priority: 'main' + } + }; + const options = { + interaction: { + a: 1 + }, + hover: { + shouldFall: true, + b: 2 + } + }; + const resolver = _createResolver([options, defaults, descriptors]); + expect(resolver.hover).toEqualOptions({ + mode: 'test', + priority: 'main', + a: 1, + b: 2 + }); + }); + + it('should not fallback by default', function() { + const defaults = { + hover: { + a: 'defaults.hover' + }, + controllers: { + y: 'defaults.controllers', + bar: { + z: 'defaults.controllers.bar', + hover: { + b: 'defaults.controllers.bar.hover' + } + } + }, + x: 'defaults root' + }; + const options = { + x: 'options', + hover: { + c: 'options.hover', + sub: { + f: 'options.hover.sub' + } + }, + controllers: { + y: 'options.controllers', + bar: { + z: 'options.controllers.bar', + hover: { + d: 'options.controllers.bar.hover', + sub: { + e: 'options.controllers.bar.hover.sub' + } + } + } + } + }; + const resolver = _createResolver([options, options.controllers.bar, options.controllers, defaults.controllers.bar, defaults.controllers, defaults]); + expect(resolver.hover).toEqualOptions({ + a: 'defaults.hover', + b: 'defaults.controllers.bar.hover', + c: 'options.hover', + d: 'options.controllers.bar.hover', + e: undefined, + f: undefined, + x: undefined, + y: undefined, + z: undefined + }); + expect(resolver.hover.sub).toEqualOptions({ + a: undefined, + b: undefined, + c: undefined, + d: undefined, + e: 'options.controllers.bar.hover.sub', + f: 'options.hover.sub', + x: undefined, + y: undefined, + z: undefined + }); + }); + + it('should fallback to specific scope', function() { + const defaults = { + hover: { + _fallback: 'hover', + a: 'defaults.hover' + }, + controllers: { + y: 'defaults.controllers', + bar: { + z: 'defaults.controllers.bar', + hover: { + b: 'defaults.controllers.bar.hover' + } + } + }, + x: 'defaults root' + }; + const options = { + x: 'options', + hover: { + c: 'options.hover', + sub: { + f: 'options.hover.sub' + } + }, + controllers: { + y: 'options.controllers', + bar: { + z: 'options.controllers.bar', + hover: { + d: 'options.controllers.bar.hover', + sub: { + e: 'options.controllers.bar.hover.sub' + } + } + } + } + }; + const resolver = _createResolver([options, options.controllers.bar, options.controllers, defaults.controllers.bar, defaults.controllers, defaults]); + expect(resolver.hover).toEqualOptions({ + a: 'defaults.hover', + b: 'defaults.controllers.bar.hover', + c: 'options.hover', + d: 'options.controllers.bar.hover', + e: undefined, + f: undefined, + x: undefined, + y: undefined, + z: undefined + }); + expect(resolver.hover.sub).toEqualOptions({ + a: 'defaults.hover', + b: 'defaults.controllers.bar.hover', + c: 'options.hover', + d: 'options.controllers.bar.hover', + e: 'options.controllers.bar.hover.sub', + f: 'options.hover.sub', + x: undefined, + y: undefined, + z: undefined + }); + }); + + it('should fallback through multiple routes', function() { + const descriptors = { + _fallback: 'level1', + level1: { + _fallback: 'root' + }, + level2: { + _fallback: 'level1' + } + }; + const defaults = { + root: { + a: 'root' + }, + level1: { + b: 'level1', + }, + level2: { + level1: { + g: 'level2.level1' + }, + c: 'level2', + sublevel1: { + d: 'sublevel1' + }, + sublevel2: { + e: 'sublevel2', + level1: { + f: 'sublevel2.level1' + } + } + } + }; + const resolver = _createResolver([defaults, descriptors]); + expect(resolver.level1).toEqualOptions({ + a: 'root', + b: 'level1', + c: undefined + }); + expect(resolver.level2).toEqualOptions({ + a: 'root', + b: 'level1', + c: 'level2', + d: undefined + }); + expect(resolver.level2.sublevel1).toEqualOptions({ + a: 'root', + b: 'level1', + c: undefined, + d: 'sublevel1', + e: undefined, + f: undefined, + g: 'level2.level1' + }); + expect(resolver.level2.sublevel2).toEqualOptions({ + a: 'root', + b: 'level1', + c: undefined, + d: undefined, + e: 'sublevel2', + f: undefined, + g: 'level2.level1' + }); + expect(resolver.level2.sublevel2.level1).toEqualOptions({ + a: 'root', + b: 'level1', + c: undefined, + d: undefined, + e: undefined, + f: 'sublevel2.level1', + g: undefined // same key only included from immediate parents and root + }); + }); + + it('should fallback through multiple routes (animations)', function() { + const descriptors = { + animations: { + _fallback: 'animation', + }, + }; + const defaults = { + animation: { + duration: 1000, + easing: 'easeInQuad' + }, + animations: { + colors: { + properties: ['color', 'backgroundColor'], + type: 'color' + }, + numbers: { + properties: ['x', 'y'], + type: 'number' + } + }, + transitions: { + resize: { + animation: { + duration: 0 + } + }, + show: { + animation: { + duration: 400 + }, + animations: { + colors: { + from: 'transparent' + } + } + } + } + }; + const options = { + animation: { + easing: 'linear' + }, + animations: { + colors: { + properties: ['color', 'borderColor', 'backgroundColor'], + }, + duration: { + properties: ['a', 'b'], + type: 'boolean' + } + } + }; + + const show = _createResolver([options, defaults.transitions.show, defaults, descriptors]); + expect(show.animation).toEqualOptions({ + duration: 400, + easing: 'linear' + }); + expect(show.animations.colors._scopes).toEqual([ + options.animations.colors, + defaults.transitions.show.animations.colors, + defaults.animations.colors, + options.animation, + defaults.transitions.show.animation, + defaults.animation + ]); + expect(show.animations.colors).toEqualOptions({ + duration: 400, + from: 'transparent', + easing: 'linear', + type: 'color', + properties: ['color', 'borderColor', 'backgroundColor'] + }); + expect(show.animations.duration).toEqualOptions({ + duration: 400, + easing: 'linear', + type: 'boolean', + properties: ['a', 'b'] + }); + expect(Object.getOwnPropertyNames(show.animations).filter(k => Chart.helpers.isObject(show.animations[k]))).toEqual([ + 'colors', + 'duration', + 'numbers', + ]); + const def = _createResolver([options, defaults, descriptors]); + expect(def.animation).toEqualOptions({ + duration: 1000, + easing: 'linear' + }); + expect(def.animations.colors._scopes).toEqual([ + options.animations.colors, + defaults.animations.colors, + options.animation, + defaults.animation + ]); + expect(def.animations.colors).toEqualOptions({ + duration: 1000, + easing: 'linear', + type: 'color', + properties: ['color', 'borderColor', 'backgroundColor'] + }); + expect(def.animations.duration).toEqualOptions({ + duration: 1000, + easing: 'linear', + type: 'boolean', + properties: ['a', 'b'] + }); + expect(Object.getOwnPropertyNames(def.animations).filter(k => Chart.helpers.isObject(show.animations[k]))).toEqual([ + 'colors', + 'duration', + 'numbers', + ]); + }); + }); + describe('setting values', function() { + it('should set values to first scope', function() { + const defaults = { + value: true + }; + const options = {}; + const resolver = _createResolver([options, defaults]); + resolver.value = false; + expect(options.value).toBeFalse(); + expect(defaults.value).toBeTrue(); + expect(resolver.value).toBeFalse(); + }); + + it('should set values of sub-objects to first scope', function() { + const defaults = { + sub: { + value: true + } + }; + const options = {}; + const resolver = _createResolver([options, defaults]); + resolver.sub.value = false; + expect(options.sub.value).toBeFalse(); + expect(defaults.sub.value).toBeTrue(); + expect(resolver.sub.value).toBeFalse(); + }); + + it('should throw when setting a value and options is frozen', function() { + const defaults = Object.freeze({default: true}); + const options = Object.freeze({value: true}); + const resolver = _createResolver([options, defaults]); + function set() { + resolver.value = false; + } + expect(set).toThrow(); + }); + }); + }); + + describe('_attachContext', function() { + it('should resolve to final values', function() { + const defaults = { + color: 'red', + backgroundColor: 'green', + hoverColor: (ctx, options) => getHoverColor(options.color) + }; + const options = { + color: ['white', 'blue'] + }; + const resolver = _createResolver([options, defaults]); + const opts = _attachContext(resolver, {index: 1}); + expect(opts.color).toEqual('blue'); + expect(opts.backgroundColor).toEqual('green'); + expect(opts.hoverColor).toEqual(getHoverColor('blue')); + }); + + it('should thrown on recursion', function() { + const options = { + foo: (ctx, opts) => opts.bar, + bar: (ctx, opts) => opts.xyz, + xyz: (ctx, opts) => opts.foo + }; + const resolver = _createResolver([options]); + const opts = _attachContext(resolver, {test: true}); + expect(function() { + return opts.foo; + }).toThrowError('Recursion detected: foo->bar->xyz->foo'); + }); + + it('should support scriptable options in subscopes', function() { + const defaults = { + elements: { + point: { + backgroundColor: 'red' + } + } + }; + const options = { + elements: { + point: { + borderColor: (ctx, opts) => getHoverColor(opts.backgroundColor) + } + } + }; + const resolver = _createResolver([options, defaults]); + const opts = _attachContext(resolver, {}); + expect(opts.elements.point.borderColor).toEqual(getHoverColor('red')); + expect(opts.elements.point.backgroundColor).toEqual('red'); + }); + + it('same resolver should be usable with multiple contexts', function() { + const defaults = { + animation: { + delay: 10 + } + }; + const options = { + animation: (ctx) => ctx.index === 0 ? {duration: 1000} : {duration: 500} + }; + const resolver = _createResolver([options, defaults]); + const opts1 = _attachContext(resolver, {index: 0}); + const opts2 = _attachContext(resolver, {index: 1}); + + expect(opts1.animation.duration).toEqual(1000); + expect(opts1.animation.delay).toEqual(10); + + expect(opts2.animation.duration).toEqual(500); + expect(opts2.animation.delay).toEqual(10); + }); + + it('should fall back from object returned from scriptable option', function() { + const defaults = { + mainScope: { + main: true, + subScope: { + sub: true + } + } + }; + const options = { + mainScope: (ctx) => ({ + mainTest: ctx.contextValue, + subScope: { + subText: 'a' + } + }) + }; + const opts = _attachContext(_createResolver([options, defaults]), {contextValue: 'test'}); + expect(opts.mainScope).toEqualOptions({ + main: true, + mainTest: 'test', + subScope: { + sub: true, + subText: 'a' + } + }); + }); + + it('should resolve array of non-indexable objects properly', function() { + const defaults = { + label: { + value: 42, + text: (ctx) => ctx.text + }, + labels: { + _fallback: 'label', + _indexable: false + } + }; + + const options = { + labels: [{text: 'a'}, {text: 'b'}, {value: 1}] + }; + const opts = _attachContext(_createResolver([options, defaults]), {text: 'context'}); + expect(opts).toEqualOptions({ + labels: [ + { + text: 'a', + value: 42 + }, + { + text: 'b', + value: 42 + }, + { + text: 'context', + value: 1 + } + ] + }); + }); + + it('should call _fallback with proper value from array when descriptor is object', function() { + const spy = jasmine.createSpy('fallback'); + const descriptors = { + items: { + _fallback: spy + } + }; + const options = { + items: [{test: true}] + }; + const resolver = _createResolver([options, descriptors]); + const opts = _attachContext(resolver, {dymmy: true}); + const item0 = opts.items[0]; + expect(item0.test).toEqual(true); + expect(spy).toHaveBeenCalledWith('items', options.items[0]); + }); + + it('should call _fallback with proper value from array when descriptor and defaults are objects', function() { + const spy = jasmine.createSpy('fallback'); + const descriptors = { + items: { + _fallback: spy + } + }; + const defaults = { + items: { + type: 'defaultType' + } + }; + const options = { + items: [{test: true}] + }; + const resolver = _createResolver([options, defaults, descriptors]); + const opts = _attachContext(resolver, {dymmy: true}); + const item0 = opts.items[0]; + expect(item0.test).toEqual(true); + expect(spy).toHaveBeenCalledWith('items', options.items[0]); + }); + + it('should support overriding options', function() { + const options = { + fn1: ctx => ctx.index, + fn2: ctx => ctx.type + }; + const override = { + fn1: ctx => ctx.index * 2 + }; + const opts = _attachContext(_createResolver([options]), {index: 2, type: 'test'}); + expect(opts).toEqualOptions({ + fn1: 2, + fn2: 'test' + }); + expect(opts.override(override)).toEqualOptions({ + fn1: 4, + fn2: 'test' + }); + }); + + it('should support changing context', function() { + const opts = _attachContext(_createResolver([{fn: ctx => ctx.test}]), {test: 1}); + expect(opts.fn).toEqual(1); + expect(opts.setContext({test: 2}).fn).toEqual(2); + expect(opts.fn).toEqual(1); + }); + + it('should support common object methods', function() { + const defaults = { + option1: 'defaults' + }; + class Options { + constructor() { + this.option2 = () => 'options'; + } + get getter() { + return 'options getter'; + } + } + const options = new Options(); + const resolver = _createResolver([options, defaults]); + const opts = _attachContext(resolver, {index: 1}); + + expect(Object.prototype.hasOwnProperty.call(opts, 'option2')).toBeTrue(); + + expect(Object.prototype.hasOwnProperty.call(opts, 'option1')).toBeFalse(); + expect(Object.prototype.hasOwnProperty.call(opts, 'getter')).toBeFalse(); + expect(Object.prototype.hasOwnProperty.call(opts, 'nonexistent')).toBeFalse(); + + expect(Object.keys(opts)).toEqual(['option2']); + expect(Object.getOwnPropertyNames(opts)).toEqual(['option2', 'option1']); + + expect('option2' in opts).toBeTrue(); + expect('option1' in opts).toBeTrue(); + expect('getter' in opts).toBeFalse(); + expect('nonexistent' in opts).toBeFalse(); + + expect(opts instanceof Options).toBeTrue(); + + expect(opts.getter).toEqual('options getter'); + + expect('test' in opts).toBeFalse(); + expect(opts.test).toBeUndefined(); + + opts.test = true; + expect('test' in opts).toBeTrue(); + expect(opts.test).toBeTrue(); + + delete opts.test; + expect('test' in opts).toBeFalse(); + + opts.test = (ctx) => ctx.index; + expect('test' in opts).toBeTrue(); + expect(opts.test).toBe(1); + + delete opts.test; + expect('test' in opts).toBeFalse(); + }); + + it('should not create proxy for adapters', function() { + const defaults = { + scales: { + time: { + adapters: { + date: { + locale: { + method: (arg) => arg === undefined ? 'ok' : 'fail' + } + } + } + } + } + }; + + const resolver = _createResolver([{}, defaults]); + const opts = _attachContext(resolver, {index: 1}); + const fn = opts.scales.time.adapters.date.locale.method; + expect(typeof fn).toBe('function'); + expect(fn()).toEqual('ok'); + }); + + it('should not create proxy for objects with custom constructor', function() { + class MyClass { + constructor() { + this.string = 'test string'; + } + method(arg) { + return arg === undefined ? 'ok' : 'fail'; + } + } + + const defaults = { + test: new MyClass() + }; + + const resolver = _createResolver([{}, defaults]); + const opts = _attachContext(resolver, {index: 1}); + const fn = opts.test.method; + expect(typeof fn).toBe('function'); + expect(fn()).toEqual('ok'); + expect(opts.test.string).toEqual('test string'); + expect(opts.test.constructor).toEqual(MyClass); + }); + + it('should properly set value to object in array of objects', function() { + const defaults = {}; + const options = { + annotations: [{ + value: 10 + }, { + value: 20 + }] + }; + const resolver = _attachContext(_createResolver([options, defaults]), {test: true}); + expect(resolver.annotations[0].value).toEqual(10); + + resolver.annotations[0].value = 15; + expect(options.annotations[0].value).toEqual(15); + expect(options.annotations[1].value).toEqual(20); + }); + + describe('_indexable and _scriptable', function() { + it('should default to true', function() { + const options = { + array: [1, 2, 3], + func: (ctx) => ctx.index * 10 + }; + const opts = _attachContext(_createResolver([options]), {index: 1}); + expect(opts.array).toEqual(2); + expect(opts.func).toEqual(10); + }); + + it('should allow false', function() { + const fn = () => 'test'; + const options = { + _indexable: false, + _scriptable: false, + array: [1, 2, 3], + func: fn + }; + const opts = _attachContext(_createResolver([options]), {index: 1}); + expect(opts.array).toEqual([1, 2, 3]); + expect(opts.func).toEqual(fn); + expect(opts.func()).toEqual('test'); + }); + + it('should allow function', function() { + const fn = () => 'test'; + const options = { + _indexable: (prop) => prop !== 'array', + _scriptable: (prop) => prop === 'func', + array: [1, 2, 3], + array2: ['a', 'b', 'c'], + func: fn + }; + const opts = _attachContext(_createResolver([options]), {index: 1}); + expect(opts.array).toEqual([1, 2, 3]); + expect(opts.func).toEqual('test'); + expect(opts.array2).toEqual('b'); + }); + }); + }); +}); diff --git a/test/specs/helpers.core.tests.js b/test/specs/helpers.core.tests.js new file mode 100644 index 00000000000..c5c51434c88 --- /dev/null +++ b/test/specs/helpers.core.tests.js @@ -0,0 +1,509 @@ +'use strict'; + +describe('Chart.helpers.core', function() { + var helpers = Chart.helpers; + + describe('noop', function() { + it('should be callable', function() { + expect(helpers.noop).toBeDefined(); + expect(typeof helpers.noop).toBe('function'); + expect(typeof helpers.noop.call).toBe('function'); + }); + it('should returns "undefined"', function() { + expect(helpers.noop(42)).not.toBeDefined(); + expect(helpers.noop.call(this, 42)).not.toBeDefined(); + }); + }); + + describe('isArray', function() { + it('should return true if value is an array', function() { + expect(helpers.isArray([])).toBeTruthy(); + expect(helpers.isArray([42])).toBeTruthy(); + expect(helpers.isArray(new Array())).toBeTruthy(); + expect(helpers.isArray(Array.prototype)).toBeTruthy(); + expect(helpers.isArray(new Int8Array(2))).toBeTruthy(); + expect(helpers.isArray(new Uint8Array())).toBeTruthy(); + expect(helpers.isArray(new Uint8ClampedArray([128, 244]))).toBeTruthy(); + expect(helpers.isArray(new Int16Array())).toBeTruthy(); + expect(helpers.isArray(new Uint16Array())).toBeTruthy(); + expect(helpers.isArray(new Int32Array())).toBeTruthy(); + expect(helpers.isArray(new Uint32Array())).toBeTruthy(); + expect(helpers.isArray(new Float32Array([1.2]))).toBeTruthy(); + expect(helpers.isArray(new Float64Array([]))).toBeTruthy(); + }); + it('should return false if value is not an array', function() { + expect(helpers.isArray()).toBeFalsy(); + expect(helpers.isArray({})).toBeFalsy(); + expect(helpers.isArray(undefined)).toBeFalsy(); + expect(helpers.isArray(null)).toBeFalsy(); + expect(helpers.isArray(true)).toBeFalsy(); + expect(helpers.isArray(false)).toBeFalsy(); + expect(helpers.isArray(42)).toBeFalsy(); + expect(helpers.isArray('Array')).toBeFalsy(); + expect(helpers.isArray({__proto__: Array.prototype})).toBeFalsy(); + }); + }); + + describe('isObject', function() { + it('should return true if value is an object', function() { + expect(helpers.isObject({})).toBeTruthy(); + expect(helpers.isObject({a: 42})).toBeTruthy(); + expect(helpers.isObject(new Object())).toBeTruthy(); + }); + it('should return false if value is not an object', function() { + expect(helpers.isObject()).toBeFalsy(); + expect(helpers.isObject(undefined)).toBeFalsy(); + expect(helpers.isObject(null)).toBeFalsy(); + expect(helpers.isObject(true)).toBeFalsy(); + expect(helpers.isObject(false)).toBeFalsy(); + expect(helpers.isObject(42)).toBeFalsy(); + expect(helpers.isObject('Object')).toBeFalsy(); + expect(helpers.isObject([])).toBeFalsy(); + expect(helpers.isObject([42])).toBeFalsy(); + expect(helpers.isObject(new Array())).toBeFalsy(); + expect(helpers.isObject(new Date())).toBeFalsy(); + }); + }); + + describe('isFinite', function() { + it('should return true if value is a finite number', function() { + expect(helpers.isFinite(0)).toBeTruthy(); + // eslint-disable-next-line no-new-wrappers + expect(helpers.isFinite(new Number(10))).toBeTruthy(); + }); + + it('should return false if the value is infinite', function() { + expect(helpers.isFinite(Number.POSITIVE_INFINITY)).toBeFalsy(); + expect(helpers.isFinite(Number.NEGATIVE_INFINITY)).toBeFalsy(); + }); + + it('should return false if the value is not a number', function() { + expect(helpers.isFinite('a')).toBeFalsy(); + expect(helpers.isFinite({})).toBeFalsy(); + }); + }); + + describe('isNullOrUndef', function() { + it('should return true if value is null/undefined', function() { + expect(helpers.isNullOrUndef(null)).toBeTruthy(); + expect(helpers.isNullOrUndef(undefined)).toBeTruthy(); + }); + it('should return false if value is not null/undefined', function() { + expect(helpers.isNullOrUndef(true)).toBeFalsy(); + expect(helpers.isNullOrUndef(false)).toBeFalsy(); + expect(helpers.isNullOrUndef('')).toBeFalsy(); + expect(helpers.isNullOrUndef('String')).toBeFalsy(); + expect(helpers.isNullOrUndef(0)).toBeFalsy(); + expect(helpers.isNullOrUndef([])).toBeFalsy(); + expect(helpers.isNullOrUndef({})).toBeFalsy(); + expect(helpers.isNullOrUndef([42])).toBeFalsy(); + expect(helpers.isNullOrUndef(new Date())).toBeFalsy(); + }); + }); + + describe('valueOrDefault', function() { + it('should return value if defined', function() { + var object = {}; + var array = []; + + expect(helpers.valueOrDefault(null, 42)).toBe(null); + expect(helpers.valueOrDefault(false, 42)).toBe(false); + expect(helpers.valueOrDefault(object, 42)).toBe(object); + expect(helpers.valueOrDefault(array, 42)).toBe(array); + expect(helpers.valueOrDefault('', 42)).toBe(''); + expect(helpers.valueOrDefault(0, 42)).toBe(0); + }); + it('should return default if undefined', function() { + expect(helpers.valueOrDefault(undefined, 42)).toBe(42); + expect(helpers.valueOrDefault({}.foo, 42)).toBe(42); + }); + }); + + describe('callback', function() { + it('should return undefined if fn is not a function', function() { + expect(helpers.callback()).not.toBeDefined(); + expect(helpers.callback(null)).not.toBeDefined(); + expect(helpers.callback(42)).not.toBeDefined(); + expect(helpers.callback([])).not.toBeDefined(); + expect(helpers.callback({})).not.toBeDefined(); + }); + it('should call fn with the given args', function() { + var spy = jasmine.createSpy('spy'); + helpers.callback(spy); + helpers.callback(spy, []); + helpers.callback(spy, ['foo']); + helpers.callback(spy, [42, 'bar']); + + expect(spy.calls.argsFor(0)).toEqual([]); + expect(spy.calls.argsFor(1)).toEqual([]); + expect(spy.calls.argsFor(2)).toEqual(['foo']); + expect(spy.calls.argsFor(3)).toEqual([42, 'bar']); + }); + it('should call fn with the given scope', function() { + var spy = jasmine.createSpy('spy'); + var scope = {}; + + helpers.callback(spy); + helpers.callback(spy, [], null); + helpers.callback(spy, [], undefined); + helpers.callback(spy, [], scope); + + expect(spy.calls.all()[0].object).toBe(window); + expect(spy.calls.all()[1].object).toBe(window); + expect(spy.calls.all()[2].object).toBe(window); + expect(spy.calls.all()[3].object).toBe(scope); + }); + it('should return the value returned by fn', function() { + expect(helpers.callback(helpers.noop, [41])).toBe(undefined); + expect(helpers.callback(function(i) { + return i + 1; + }, [41])).toBe(42); + }); + }); + + describe('each', function() { + it('should iterate over an array forward if reverse === false', function() { + var scope = {}; + var scopes = []; + var items = []; + var keys = []; + + helpers.each(['foo', 'bar', 42], function(item, key) { + scopes.push(this); + items.push(item); + keys.push(key); + }, scope); + + expect(scopes).toEqual([scope, scope, scope]); + expect(items).toEqual(['foo', 'bar', 42]); + expect(keys).toEqual([0, 1, 2]); + }); + it('should iterate over an array backward if reverse === true', function() { + var scope = {}; + var scopes = []; + var items = []; + var keys = []; + + helpers.each(['foo', 'bar', 42], function(item, key) { + scopes.push(this); + items.push(item); + keys.push(key); + }, scope, true); + + expect(scopes).toEqual([scope, scope, scope]); + expect(items).toEqual([42, 'bar', 'foo']); + expect(keys).toEqual([2, 1, 0]); + }); + it('should iterate over object properties', function() { + var scope = {}; + var scopes = []; + var items = []; + + helpers.each({a: 'foo', b: 'bar', c: 42}, function(item, key) { + scopes.push(this); + items[key] = item; + }, scope); + + expect(scopes).toEqual([scope, scope, scope]); + expect(items).toEqual(jasmine.objectContaining({a: 'foo', b: 'bar', c: 42})); + }); + it('should not throw when called with a non iterable object', function() { + expect(function() { + helpers.each(undefined); + }).not.toThrow(); + expect(function() { + helpers.each(null); + }).not.toThrow(); + expect(function() { + helpers.each(42); + }).not.toThrow(); + }); + }); + + describe('_elementsEqual', function() { + it('should return true if arrays are the same', function() { + expect(helpers._elementsEqual( + [{datasetIndex: 0, index: 1}, {datasetIndex: 0, index: 2}], + [{datasetIndex: 0, index: 1}, {datasetIndex: 0, index: 2}])).toBeTruthy(); + }); + it('should return false if arrays are not the same', function() { + expect(helpers._elementsEqual([], [{datasetIndex: 0, index: 1}])).toBeFalsy(); + expect(helpers._elementsEqual([{datasetIndex: 0, index: 2}], [{datasetIndex: 0, index: 1}])).toBeFalsy(); + }); + }); + + describe('clone', function() { + it('should clone primitive values', function() { + expect(helpers.clone()).toBe(undefined); + expect(helpers.clone(null)).toBe(null); + expect(helpers.clone(true)).toBe(true); + expect(helpers.clone(42)).toBe(42); + expect(helpers.clone('foo')).toBe('foo'); + }); + it('should perform a deep copy of arrays', function() { + var o0 = {a: 42}; + var o1 = {s: 's'}; + var a0 = ['bar']; + var a1 = [a0, o0, 2]; + var f0 = function() {}; + var input = [a1, o1, f0, 42, 'foo']; + var output = helpers.clone(input); + + expect(output).toEqual(input); + expect(output).not.toBe(input); + expect(output[0]).not.toBe(a1); + expect(output[0][0]).not.toBe(a0); + expect(output[1]).not.toBe(o1); + }); + it('should perform a deep copy of objects', function() { + var a0 = ['bar']; + var a1 = [1, 2, 3]; + var o0 = {a: a1, i: 42}; + var f0 = function() {}; + var input = {o: o0, a: a0, f: f0, s: 'foo', i: 42}; + var output = helpers.clone(input); + + expect(output).toEqual(input); + expect(output).not.toBe(input); + expect(output.o).not.toBe(o0); + expect(output.o.a).not.toBe(a1); + expect(output.a).not.toBe(a0); + }); + }); + + describe('merge', function() { + it('should not allow prototype pollution', function() { + var test = helpers.merge({}, JSON.parse('{"__proto__":{"polluted": true}}')); + expect(test.prototype).toBeUndefined(); + expect(Object.prototype.polluted).toBeUndefined(); + }); + it('should update target and return it', function() { + var target = {a: 1}; + var result = helpers.merge(target, {a: 2, b: 'foo'}); + expect(target).toEqual({a: 2, b: 'foo'}); + expect(target).toBe(result); + }); + it('should return target if not an object', function() { + expect(helpers.merge(undefined, {a: 42})).toEqual(undefined); + expect(helpers.merge(null, {a: 42})).toEqual(null); + expect(helpers.merge('foo', {a: 42})).toEqual('foo'); + expect(helpers.merge(['foo', 'bar'], {a: 42})).toEqual(['foo', 'bar']); + }); + it('should ignore sources which are not objects', function() { + expect(helpers.merge({a: 42})).toEqual({a: 42}); + expect(helpers.merge({a: 42}, null)).toEqual({a: 42}); + expect(helpers.merge({a: 42}, 42)).toEqual({a: 42}); + }); + it('should recursively overwrite target with source properties', function() { + expect(helpers.merge({a: {b: 1}}, {a: {c: 2}})).toEqual({a: {b: 1, c: 2}}); + expect(helpers.merge({a: {b: 1}}, {a: {b: 2}})).toEqual({a: {b: 2}}); + expect(helpers.merge({a: [1, 2]}, {a: [3, 4]})).toEqual({a: [3, 4]}); + expect(helpers.merge({a: 42}, {a: {b: 0}})).toEqual({a: {b: 0}}); + expect(helpers.merge({a: 42}, {a: null})).toEqual({a: null}); + expect(helpers.merge({a: 42}, {a: undefined})).toEqual({a: undefined}); + }); + it('should merge multiple sources in the correct order', function() { + var t0 = {a: {b: 1, c: [1, 2]}}; + var s0 = {a: {d: 3}, e: {f: 4}}; + var s1 = {a: {b: 5}}; + var s2 = {a: {c: [6, 7]}, e: 'foo'}; + + expect(helpers.merge(t0, [s0, s1, s2])).toEqual({a: {b: 5, c: [6, 7], d: 3}, e: 'foo'}); + }); + it('should deep copy merged values from sources', function() { + var a0 = ['foo']; + var a1 = [1, 2, 3]; + var o0 = {a: a1, i: 42}; + var output = helpers.merge({}, {a: a0, o: o0}); + + expect(output).toEqual({a: a0, o: o0}); + expect(output.a).not.toBe(a0); + expect(output.o).not.toBe(o0); + expect(output.o.a).not.toBe(a1); + }); + }); + + describe('mergeIf', function() { + it('should not allow prototype pollution', function() { + var test = helpers.mergeIf({}, JSON.parse('{"__proto__":{"polluted": true}}')); + expect(test.prototype).toBeUndefined(); + expect(Object.prototype.polluted).toBeUndefined(); + }); + it('should update target and return it', function() { + var target = {a: 1}; + var result = helpers.mergeIf(target, {a: 2, b: 'foo'}); + expect(target).toEqual({a: 1, b: 'foo'}); + expect(target).toBe(result); + }); + it('should return target if not an object', function() { + expect(helpers.mergeIf(undefined, {a: 42})).toEqual(undefined); + expect(helpers.mergeIf(null, {a: 42})).toEqual(null); + expect(helpers.mergeIf('foo', {a: 42})).toEqual('foo'); + expect(helpers.mergeIf(['foo', 'bar'], {a: 42})).toEqual(['foo', 'bar']); + }); + it('should ignore sources which are not objects', function() { + expect(helpers.mergeIf({a: 42})).toEqual({a: 42}); + expect(helpers.mergeIf({a: 42}, null)).toEqual({a: 42}); + expect(helpers.mergeIf({a: 42}, 42)).toEqual({a: 42}); + }); + it('should recursively copy source properties in target only if they do not exist in target', function() { + expect(helpers.mergeIf({a: {b: 1}}, {a: {c: 2}})).toEqual({a: {b: 1, c: 2}}); + expect(helpers.mergeIf({a: {b: 1}}, {a: {b: 2}})).toEqual({a: {b: 1}}); + expect(helpers.mergeIf({a: [1, 2]}, {a: [3, 4]})).toEqual({a: [1, 2]}); + expect(helpers.mergeIf({a: 0}, {a: {b: 2}})).toEqual({a: 0}); + expect(helpers.mergeIf({a: null}, {a: 42})).toEqual({a: null}); + expect(helpers.mergeIf({a: undefined}, {a: 42})).toEqual({a: undefined}); + }); + it('should merge multiple sources in the correct order', function() { + var t0 = {a: {b: 1, c: [1, 2]}}; + var s0 = {a: {d: 3}, e: {f: 4}}; + var s1 = {a: {b: 5}}; + var s2 = {a: {c: [6, 7]}, e: 'foo'}; + + expect(helpers.mergeIf(t0, [s0, s1, s2])).toEqual({a: {b: 1, c: [1, 2], d: 3}, e: {f: 4}}); + }); + it('should deep copy merged values from sources', function() { + var a0 = ['foo']; + var a1 = [1, 2, 3]; + var o0 = {a: a1, i: 42}; + var output = helpers.mergeIf({}, {a: a0, o: o0}); + + expect(output).toEqual({a: a0, o: o0}); + expect(output.a).not.toBe(a0); + expect(output.o).not.toBe(o0); + expect(output.o.a).not.toBe(a1); + }); + }); + + describe('resolveObjectKey', function() { + it('should resolve empty key to root object', function() { + const obj = {test: true}; + expect(helpers.resolveObjectKey(obj, '')).toEqual(obj); + }); + it('should resolve one level', function() { + const obj = { + bool: true, + str: 'test', + int: 42, + obj: {name: 'object'} + }; + expect(helpers.resolveObjectKey(obj, 'bool')).toEqual(true); + expect(helpers.resolveObjectKey(obj, 'str')).toEqual('test'); + expect(helpers.resolveObjectKey(obj, 'int')).toEqual(42); + expect(helpers.resolveObjectKey(obj, 'obj')).toEqual(obj.obj); + }); + it('should resolve multiple levels', function() { + const obj = { + child: { + level: 1, + child: { + level: 2, + child: { + level: 3 + } + } + } + }; + expect(helpers.resolveObjectKey(obj, 'child.level')).toEqual(1); + expect(helpers.resolveObjectKey(obj, 'child.child.level')).toEqual(2); + expect(helpers.resolveObjectKey(obj, 'child.child.child.level')).toEqual(3); + }); + it('should resolve circular reference', function() { + const root = {}; + const child = {root}; + child.child = child; + root.child = child; + expect(helpers.resolveObjectKey(root, 'child')).toEqual(child); + expect(helpers.resolveObjectKey(root, 'child.child.child.child.child.child')).toEqual(child); + expect(helpers.resolveObjectKey(root, 'child.child.root')).toEqual(root); + }); + it('should break at empty key', function() { + const obj = { + child: { + level: 1, + child: { + level: 2, + child: { + level: 3 + } + } + } + }; + expect(helpers.resolveObjectKey(obj, 'child..level')).toEqual(obj.child); + expect(helpers.resolveObjectKey(obj, 'child.child.level...')).toEqual(2); + expect(helpers.resolveObjectKey(obj, '.')).toEqual(obj); + expect(helpers.resolveObjectKey(obj, '..')).toEqual(obj); + }); + it('should resolve undefined', function() { + const obj = { + child: { + level: 1, + child: { + level: 2, + child: { + level: 3 + } + } + } + }; + expect(helpers.resolveObjectKey(obj, 'level')).toEqual(undefined); + expect(helpers.resolveObjectKey(obj, 'child.level.a')).toEqual(undefined); + }); + it('should throw on invalid input', function() { + expect(() => helpers.resolveObjectKey(undefined, undefined)).toThrow(); + expect(() => helpers.resolveObjectKey({}, null)).toThrow(); + expect(() => helpers.resolveObjectKey({}, false)).toThrow(); + expect(() => helpers.resolveObjectKey({}, true)).toThrow(); + expect(() => helpers.resolveObjectKey({}, 1)).toThrow(); + }); + it('should allow escaping dot symbol', function() { + expect(helpers.resolveObjectKey({'test.dot': 10}, 'test\\.dot')).toEqual(10); + expect(helpers.resolveObjectKey({test: {dot: 10}}, 'test\\.dot')).toEqual(undefined); + }); + it('should allow nested keys with a dot', function() { + expect(helpers.resolveObjectKey({ + a: { + 'bb.ccc': 'works', + bb: { + ccc: 'fails' + } + } + }, 'a.bb\\.ccc')).toEqual('works'); + }); + + }); + + describe('_splitKey', function() { + it('should return array with one entry for string without a dot', function() { + expect(helpers._splitKey('')).toEqual(['']); + expect(helpers._splitKey('test')).toEqual(['test']); + const asciiWithoutDot = ' !"#$%&\'()*+,-/0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~'; + expect(helpers._splitKey(asciiWithoutDot)).toEqual([asciiWithoutDot]); + }); + + it('should split on dot', function() { + expect(helpers._splitKey('test1.test2')).toEqual(['test1', 'test2']); + expect(helpers._splitKey('a.b.c')).toEqual(['a', 'b', 'c']); + expect(helpers._splitKey('a.b.')).toEqual(['a', 'b', '']); + expect(helpers._splitKey('a..c')).toEqual(['a', '', 'c']); + }); + + it('should preserve escaped dot', function() { + expect(helpers._splitKey('test1\\.test2')).toEqual(['test1.test2']); + expect(helpers._splitKey('a\\.b.c')).toEqual(['a.b', 'c']); + expect(helpers._splitKey('a.b\\.c')).toEqual(['a', 'b.c']); + expect(helpers._splitKey('a.\\.c')).toEqual(['a', '.c']); + }); + }); + + describe('setsEqual', function() { + it('should handle set comparison', function() { + var a = new Set([1]); + var b = new Set(['1']); + var c = new Set([1]); + + expect(helpers.setsEqual(a, b)).toBeFalse(); + expect(helpers.setsEqual(a, c)).toBeTrue(); + }); + }); +}); diff --git a/test/specs/helpers.curve.tests.js b/test/specs/helpers.curve.tests.js new file mode 100644 index 00000000000..96e7956a935 --- /dev/null +++ b/test/specs/helpers.curve.tests.js @@ -0,0 +1,183 @@ +describe('Curve helper tests', function() { + let helpers; + + beforeAll(function() { + helpers = window.Chart.helpers; + }); + + it('should spline curves', function() { + expect(helpers.splineCurve({ + x: 0, + y: 0 + }, { + x: 1, + y: 1 + }, { + x: 2, + y: 0 + }, 0)).toEqual({ + previous: { + x: 1, + y: 1, + }, + next: { + x: 1, + y: 1, + } + }); + + expect(helpers.splineCurve({ + x: 0, + y: 0 + }, { + x: 1, + y: 1 + }, { + x: 2, + y: 0 + }, 1)).toEqual({ + previous: { + x: 0, + y: 1, + }, + next: { + x: 2, + y: 1, + } + }); + }); + + it('should spline curves with monotone cubic interpolation', function() { + var dataPoints = [ + {x: 0, y: 0, skip: false}, + {x: 3, y: 6, skip: false}, + {x: 9, y: 6, skip: false}, + {x: 12, y: 60, skip: false}, + {x: 15, y: 60, skip: false}, + {x: 18, y: 120, skip: false}, + {x: null, y: null, skip: true}, + {x: 21, y: 180, skip: false}, + {x: 24, y: 120, skip: false}, + {x: 27, y: 125, skip: false}, + {x: 30, y: 105, skip: false}, + {x: 33, y: 110, skip: false}, + {x: 33, y: 110, skip: false}, + {x: 36, y: 170, skip: false} + ]; + helpers.splineCurveMonotone(dataPoints); + expect(dataPoints).toEqual([{ + x: 0, + y: 0, + skip: false, + cp2x: 1, + cp2y: 2 + }, + { + x: 3, + y: 6, + skip: false, + cp1x: 2, + cp1y: 6, + cp2x: 5, + cp2y: 6 + }, + { + x: 9, + y: 6, + skip: false, + cp1x: 7, + cp1y: 6, + cp2x: 10, + cp2y: 6 + }, + { + x: 12, + y: 60, + skip: false, + cp1x: 11, + cp1y: 60, + cp2x: 13, + cp2y: 60 + }, + { + x: 15, + y: 60, + skip: false, + cp1x: 14, + cp1y: 60, + cp2x: 16, + cp2y: 60 + }, + { + x: 18, + y: 120, + skip: false, + cp1x: 17, + cp1y: 100 + }, + { + x: null, + y: null, + skip: true + }, + { + x: 21, + y: 180, + skip: false, + cp2x: 22, + cp2y: 160 + }, + { + x: 24, + y: 120, + skip: false, + cp1x: 23, + cp1y: 120, + cp2x: 25, + cp2y: 120 + }, + { + x: 27, + y: 125, + skip: false, + cp1x: 26, + cp1y: 125, + cp2x: 28, + cp2y: 125 + }, + { + x: 30, + y: 105, + skip: false, + cp1x: 29, + cp1y: 105, + cp2x: 31, + cp2y: 105 + }, + { + x: 33, + y: 110, + skip: false, + cp1x: 32, + cp1y: 110, + cp2x: 33, + cp2y: 110 + }, + { + x: 33, + y: 110, + skip: false, + cp1x: 33, + cp1y: 110, + cp2x: 34, + cp2y: 110 + }, + { + x: 36, + y: 170, + skip: false, + cp1x: 35, + cp1y: 150 + }]); + }); +}); diff --git a/test/specs/helpers.dom.tests.js b/test/specs/helpers.dom.tests.js new file mode 100644 index 00000000000..51957168422 --- /dev/null +++ b/test/specs/helpers.dom.tests.js @@ -0,0 +1,548 @@ +describe('DOM helpers tests', function() { + let helpers; + + beforeAll(function() { + helpers = window.Chart.helpers; + }); + + it ('should get the maximum size for a node', function() { + // Create div with fixed size as a test bed + var div = document.createElement('div'); + div.style.width = '200px'; + div.style.height = '300px'; + + document.body.appendChild(div); + + // Create the div we want to get the max size for + var innerDiv = document.createElement('div'); + div.appendChild(innerDiv); + + expect(helpers.getMaximumSize(innerDiv)).toEqual(jasmine.objectContaining({width: 200, height: 300})); + + document.body.removeChild(div); + }); + + it ('should get the maximum width and height for a node in a ShadowRoot', function() { + // Create div with fixed size as a test bed + var div = document.createElement('div'); + div.style.width = '200px'; + div.style.height = '300px'; + + document.body.appendChild(div); + + if (!div.attachShadow) { + // Shadow DOM is not natively supported + return; + } + + var shadow = div.attachShadow({mode: 'closed'}); + + // Create the div we want to get the max size for + var innerDiv = document.createElement('div'); + shadow.appendChild(innerDiv); + + expect(helpers.getMaximumSize(innerDiv)).toEqual(jasmine.objectContaining({width: 200, height: 300})); + + document.body.removeChild(div); + }); + + it ('should get the maximum width of a node that has a max-width style', function() { + // Create div with fixed size as a test bed + var div = document.createElement('div'); + div.style.width = '200px'; + div.style.height = '300px'; + + document.body.appendChild(div); + + // Create the div we want to get the max size for and set a max-width style + var innerDiv = document.createElement('div'); + innerDiv.style.maxWidth = '150px'; + div.appendChild(innerDiv); + + expect(helpers.getMaximumSize(innerDiv)).toEqual(jasmine.objectContaining({width: 150})); + + document.body.removeChild(div); + }); + + it ('should get the maximum height of a node that has a max-height style', function() { + // Create div with fixed size as a test bed + var div = document.createElement('div'); + div.style.width = '200px'; + div.style.height = '300px'; + + document.body.appendChild(div); + + // Create the div we want to get the max size for and set a max-height style + var innerDiv = document.createElement('div'); + innerDiv.style.maxHeight = '150px'; + div.appendChild(innerDiv); + + expect(helpers.getMaximumSize(innerDiv)).toEqual(jasmine.objectContaining({height: 150})); + + document.body.removeChild(div); + }); + + it ('should get the maximum width of a node when the parent has a max-width style', function() { + // Create div with fixed size as a test bed + var div = document.createElement('div'); + div.style.width = '200px'; + div.style.height = '300px'; + + document.body.appendChild(div); + + // Create an inner wrapper around our div we want to size and give that a max-width style + var parentDiv = document.createElement('div'); + parentDiv.style.maxWidth = '150px'; + div.appendChild(parentDiv); + + // Create the div we want to get the max size for + var innerDiv = document.createElement('div'); + parentDiv.appendChild(innerDiv); + + expect(helpers.getMaximumSize(innerDiv)).toEqual(jasmine.objectContaining({width: 150})); + + document.body.removeChild(div); + }); + + it ('should get the maximum height of a node when the parent has a max-height style', function() { + // Create div with fixed size as a test bed + var div = document.createElement('div'); + div.style.width = '200px'; + div.style.height = '300px'; + + document.body.appendChild(div); + + // Create an inner wrapper around our div we want to size and give that a max-height style + var parentDiv = document.createElement('div'); + parentDiv.style.maxHeight = '150px'; + div.appendChild(parentDiv); + + // Create the div we want to get the max size for + var innerDiv = document.createElement('div'); + innerDiv.style.height = '300px'; // make it large + parentDiv.appendChild(innerDiv); + + expect(helpers.getMaximumSize(innerDiv)).toEqual(jasmine.objectContaining({height: 150})); + + document.body.removeChild(div); + }); + + it ('should get the maximum width of a node that has a percentage max-width style', function() { + // Create div with fixed size as a test bed + var div = document.createElement('div'); + div.style.width = '200px'; + div.style.height = '300px'; + + document.body.appendChild(div); + + // Create the div we want to get the max size for and set a max-width style + var innerDiv = document.createElement('div'); + innerDiv.style.maxWidth = '50%'; + div.appendChild(innerDiv); + + expect(helpers.getMaximumSize(innerDiv)).toEqual(jasmine.objectContaining({width: 100})); + + document.body.removeChild(div); + }); + + it('should get the maximum height of a node that has a percentage max-height style', function() { + // Create div with fixed size as a test bed + var div = document.createElement('div'); + div.style.width = '200px'; + div.style.height = '300px'; + + document.body.appendChild(div); + + // Create the div we want to get the max size for and set a max-height style + var innerDiv = document.createElement('div'); + innerDiv.style.maxHeight = '50%'; + div.appendChild(innerDiv); + + expect(helpers.getMaximumSize(innerDiv)).toEqual(jasmine.objectContaining({height: 150})); + + document.body.removeChild(div); + }); + + it ('should get the maximum width of a node when the parent has a percentage max-width style', function() { + // Create div with fixed size as a test bed + var div = document.createElement('div'); + div.style.width = '200px'; + div.style.height = '300px'; + + document.body.appendChild(div); + + // Create an inner wrapper around our div we want to size and give that a max-width style + var parentDiv = document.createElement('div'); + parentDiv.style.maxWidth = '50%'; + div.appendChild(parentDiv); + + // Create the div we want to get the max size for + var innerDiv = document.createElement('div'); + parentDiv.appendChild(innerDiv); + + expect(helpers.getMaximumSize(innerDiv)).toEqual(jasmine.objectContaining({width: 100})); + + document.body.removeChild(div); + }); + + it ('should get the maximum height of a node when the parent has a percentage max-height style', function() { + // Create div with fixed size as a test bed + var div = document.createElement('div'); + div.style.width = '200px'; + div.style.height = '300px'; + + document.body.appendChild(div); + + // Create an inner wrapper around our div we want to size and give that a max-height style + var parentDiv = document.createElement('div'); + parentDiv.style.maxHeight = '50%'; + div.appendChild(parentDiv); + + var innerDiv = document.createElement('div'); + innerDiv.style.height = '300px'; // make it large + parentDiv.appendChild(innerDiv); + + expect(helpers.getMaximumSize(innerDiv)).toEqual(jasmine.objectContaining({height: 150})); + + document.body.removeChild(div); + }); + + it ('Should get padding of parent as number (pixels) when defined as percent (returns incorrectly in IE11)', function() { + + // Create div with fixed size as a test bed + var div = document.createElement('div'); + div.style.width = '300px'; + div.style.height = '300px'; + document.body.appendChild(div); + + // Inner DIV to have 5% padding of parent + var innerDiv = document.createElement('div'); + + div.appendChild(innerDiv); + + var canvas = document.createElement('canvas'); + innerDiv.appendChild(canvas); + + // No padding + expect(helpers.getMaximumSize(canvas)).toEqual(jasmine.objectContaining({width: 300})); + + // test with percentage + innerDiv.style.padding = '5%'; + expect(helpers.getMaximumSize(canvas)).toEqual(jasmine.objectContaining({width: 270})); + + // test with pixels + innerDiv.style.padding = '10px'; + expect(helpers.getMaximumSize(canvas)).toEqual(jasmine.objectContaining({width: 280})); + + document.body.removeChild(div); + }); + + it ('should leave styled height and width on canvas if explicitly set', function() { + var chart = window.acquireChart({}, { + canvas: { + height: 200, + width: 200, + style: 'height: 400px; width: 400px;' + } + }); + + helpers.retinaScale(chart, true); + + var canvas = chart.canvas; + + expect(canvas.style.height).toBe('400px'); + expect(canvas.style.width).toBe('400px'); + }); + + it ('should handle devicePixelRatio correctly', function() { + const chartWidth = 800; + const chartHeight = 400; + let devicePixelRatio = 0.8999999761581421; // 1.7999999523162842; + var chart = window.acquireChart({}, { + canvas: { + width: chartWidth, + height: chartHeight, + } + }); + + helpers.retinaScale(chart, devicePixelRatio, true); + + var canvas = chart.canvas; + expect(canvas.width).toBe(Math.round(chartWidth * devicePixelRatio)); + expect(canvas.height).toBe(Math.round(chartHeight * devicePixelRatio)); + + expect(chart.width).toBe(chartWidth); + expect(chart.height).toBe(chartHeight); + + expect(canvas.style.width).toBe(`${chartWidth}px`); + expect(canvas.style.height).toBe(`${chartHeight}px`); + }); + + describe('getRelativePosition', function() { + it('should use offsetX/Y when available', function() { + const event = {offsetX: 50, offsetY: 100}; + const chart = window.acquireChart({}, { + canvas: { + height: 200, + width: 200, + } + }); + expect(helpers.getRelativePosition(event, chart)).toEqual({x: 50, y: 100}); + + const chart2 = window.acquireChart({}, { + canvas: { + height: 200, + width: 200, + style: 'padding: 10px' + } + }); + expect(helpers.getRelativePosition(event, chart2)).toEqual({ + x: Math.round((event.offsetX - 10) / 180 * 200), + y: Math.round((event.offsetY - 10) / 180 * 200) + }); + + const chart3 = window.acquireChart({}, { + canvas: { + height: 200, + width: 200, + style: 'width: 400px, height: 400px; padding: 10px' + } + }); + expect(helpers.getRelativePosition(event, chart3)).toEqual({ + x: Math.round((event.offsetX - 10) / 360 * 400), + y: Math.round((event.offsetY - 10) / 360 * 400) + }); + + const chart4 = window.acquireChart({}, { + canvas: { + height: 200, + width: 200, + style: 'width: 400px, height: 400px; padding: 10px; position: absolute; left: 20, top: 20' + } + }); + expect(helpers.getRelativePosition(event, chart4)).toEqual({ + x: Math.round((event.offsetX - 10) / 360 * 400), + y: Math.round((event.offsetY - 10) / 360 * 400) + }); + + }); + + it('should calculate from clientX/Y as fallback', function() { + const chart = window.acquireChart({}, { + canvas: { + height: 200, + width: 200, + } + }); + + const event = { + clientX: 50, + clientY: 100 + }; + + const rect = chart.canvas.getBoundingClientRect(); + const pos = helpers.getRelativePosition(event, chart); + expect(Math.abs(pos.x - Math.round(event.clientX - rect.x))).toBeLessThanOrEqual(1); + expect(Math.abs(pos.y - Math.round(event.clientY - rect.y))).toBeLessThanOrEqual(1); + + const chart2 = window.acquireChart({}, { + canvas: { + height: 200, + width: 200, + style: 'padding: 10px' + } + }); + const rect2 = chart2.canvas.getBoundingClientRect(); + const pos2 = helpers.getRelativePosition(event, chart2); + expect(Math.abs(pos2.x - Math.round((event.clientX - rect2.x - 10) / 180 * 200))).toBeLessThanOrEqual(1); + expect(Math.abs(pos2.y - Math.round((event.clientY - rect2.y - 10) / 180 * 200))).toBeLessThanOrEqual(1); + + const chart3 = window.acquireChart({}, { + canvas: { + height: 200, + width: 200, + style: 'width: 400px, height: 400px; padding: 10px' + } + }); + const rect3 = chart3.canvas.getBoundingClientRect(); + const pos3 = helpers.getRelativePosition(event, chart3); + expect(Math.abs(pos3.x - Math.round((event.clientX - rect3.x - 10) / 360 * 400))).toBeLessThanOrEqual(1); + expect(Math.abs(pos3.y - Math.round((event.clientY - rect3.y - 10) / 360 * 400))).toBeLessThanOrEqual(1); + }); + + it ('should get the correct relative position for a node in a ShadowRoot', function() { + const event = { + offsetX: 50, + offsetY: 100, + clientX: 50, + clientY: 100 + }; + + const chart = window.acquireChart({}, { + canvas: { + height: 200, + width: 200, + }, + useShadowDOM: true + }); + + event.target = chart.canvas.parentNode.host; + expect(event.target.shadowRoot).not.toEqual(null); + const rect = chart.canvas.getBoundingClientRect(); + const pos = helpers.getRelativePosition(event, chart); + expect(Math.abs(pos.x - Math.round(event.clientX - rect.x))).toBeLessThanOrEqual(1); + expect(Math.abs(pos.y - Math.round(event.clientY - rect.y))).toBeLessThanOrEqual(1); + + const chart2 = window.acquireChart({}, { + canvas: { + height: 200, + width: 200, + style: 'padding: 10px' + }, + useShadowDOM: true + }); + + event.target = chart2.canvas.parentNode.host; + const rect2 = chart2.canvas.getBoundingClientRect(); + const pos2 = helpers.getRelativePosition(event, chart2); + expect(Math.abs(pos2.x - Math.round((event.clientX - rect2.x - 10) / 180 * 200))).toBeLessThanOrEqual(1); + expect(Math.abs(pos2.y - Math.round((event.clientY - rect2.y - 10) / 180 * 200))).toBeLessThanOrEqual(1); + + const chart3 = window.acquireChart({}, { + canvas: { + height: 200, + width: 200, + style: 'width: 400px, height: 400px; padding: 10px' + }, + useShadowDOM: true + }); + + event.target = chart3.canvas.parentNode.host; + const rect3 = chart3.canvas.getBoundingClientRect(); + const pos3 = helpers.getRelativePosition(event, chart3); + expect(Math.abs(pos3.x - Math.round((event.clientX - rect3.x - 10) / 360 * 400))).toBeLessThanOrEqual(1); + expect(Math.abs(pos3.y - Math.round((event.clientY - rect3.y - 10) / 360 * 400))).toBeLessThanOrEqual(1); + }); + + it('Should not return NaN with a custom event', async function() { + let dataX = null; + let dataY = null; + const chart = window.acquireChart( + { + type: 'bar', + data: { + datasets: [{ + data: [{x: 'first', y: 10}, {x: 'second', y: 5}, {x: 'third', y: 15}] + }] + }, + options: { + onHover: (e) => { + const canvasPosition = Chart.helpers.getRelativePosition(e, chart); + + dataX = canvasPosition.x; + dataY = canvasPosition.y; + } + } + }); + + const point = chart.getDatasetMeta(0).data[1]; + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + + expect(dataX).not.toEqual(NaN); + expect(dataY).not.toEqual(NaN); + }); + + it('Should give consistent results for native and chart events', async function() { + let chartPosition = null; + const chart = window.acquireChart( + { + type: 'bar', + data: { + datasets: [{ + data: [{x: 'first', y: 10}, {x: 'second', y: 5}, {x: 'third', y: 15}] + }] + }, + options: { + onHover: (chartEvent) => { + chartPosition = Chart.helpers.getRelativePosition(chartEvent, chart); + } + } + }); + + const point = chart.getDatasetMeta(0).data[1]; + const nativeEvent = await jasmine.triggerMouseEvent(chart, 'mousemove', point); + const nativePosition = Chart.helpers.getRelativePosition(nativeEvent, chart); + + expect(chartPosition).not.toBeNull(); + expect(nativePosition).toEqual({x: chartPosition.x, y: chartPosition.y}); + }); + }); + + it('should respect aspect ratio and container width', () => { + const container = document.createElement('div'); + container.style.width = '200px'; + container.style.height = '500px'; + + document.body.appendChild(container); + + const target = document.createElement('div'); + target.style.width = '500px'; + target.style.height = '500px'; + container.appendChild(target); + + expect(helpers.getMaximumSize(target, 200, 500, 1)).toEqual(jasmine.objectContaining({width: 200, height: 200})); + + document.body.removeChild(container); + }); + + it('should respect aspect ratio and container height', () => { + const container = document.createElement('div'); + container.style.width = '500px'; + container.style.height = '200px'; + + document.body.appendChild(container); + + const target = document.createElement('div'); + target.style.width = '500px'; + target.style.height = '500px'; + container.appendChild(target); + + expect(helpers.getMaximumSize(target, 500, 200, 1)).toEqual(jasmine.objectContaining({width: 200, height: 200})); + + document.body.removeChild(container); + }); + + it('should respect aspect ratio and skip container height', () => { + const container = document.createElement('div'); + container.style.width = '500px'; + container.style.height = '200px'; + + document.body.appendChild(container); + + const target = document.createElement('div'); + target.style.width = '500px'; + target.style.height = '500px'; + container.appendChild(target); + + expect(helpers.getMaximumSize(target, undefined, undefined, 1)).toEqual(jasmine.objectContaining({width: 500, height: 500})); + + document.body.removeChild(container); + }); + + it('should round non-integer container dimensions', () => { + const container = document.createElement('div'); + container.style.width = '799.999px'; + container.style.height = '299.999px'; + + document.body.appendChild(container); + + const target = document.createElement('div'); + target.style.width = '200px'; + target.style.height = '100px'; + container.appendChild(target); + + expect(helpers.getMaximumSize(target, undefined, undefined, 2)).toEqual(jasmine.objectContaining({width: 800, height: 400})); + + document.body.removeChild(container); + }); +}); diff --git a/test/specs/helpers.easing.tests.js b/test/specs/helpers.easing.tests.js new file mode 100644 index 00000000000..cae4ca98488 --- /dev/null +++ b/test/specs/helpers.easing.tests.js @@ -0,0 +1,61 @@ +'use strict'; + +describe('Chart.helpers.easingEffects', function() { + var helpers = Chart.helpers; + + describe('effects', function() { + var expected = { + easeInOutBack: [-0, -0.03751855, -0.09255566, -0.07883348, 0.08992579, 0.5, 0.91007421, 1.07883348, 1.09255566, 1.03751855, 1], + easeInOutBounce: [0, 0.03, 0.11375, 0.045, 0.34875, 0.5, 0.65125, 0.955, 0.88625, 0.97, 1], + easeInOutCirc: [-0, 0.01010205, 0.04174243, 0.1, 0.2, 0.5, 0.8, 0.9, 0.95825757, 0.98989795, 1], + easeInOutCubic: [0, 0.004, 0.032, 0.108, 0.256, 0.5, 0.744, 0.892, 0.968, 0.996, 1], + easeInOutElastic: [0, 0.00033916, -0.00390625, 0.02393889, -0.11746158, 0.5, 1.11746158, 0.97606111, 1.00390625, 0.99966084, 1], + easeInOutExpo: [0, 0.00195313, 0.0078125, 0.03125, 0.125, 0.5, 0.875, 0.96875, 0.9921875, 0.99804688, 1], + easeInOutQuad: [0, 0.02, 0.08, 0.18, 0.32, 0.5, 0.68, 0.82, 0.92, 0.98, 1], + easeInOutQuart: [0, 0.0008, 0.0128, 0.0648, 0.2048, 0.5, 0.7952, 0.9352, 0.9872, 0.9992, 1], + easeInOutQuint: [0, 0.00016, 0.00512, 0.03888, 0.16384, 0.5, 0.83616, 0.96112, 0.99488, 0.99984, 1], + easeInOutSine: [-0, 0.02447174, 0.0954915, 0.20610737, 0.3454915, 0.5, 0.6545085, 0.79389263, 0.9045085, 0.97552826, 1], + easeInBack: [-0, -0.01431422, -0.04645056, -0.08019954, -0.09935168, -0.0876975, -0.02902752, 0.09286774, 0.29419776, 0.59117202, 1], + easeInBounce: [0, 0.011875, 0.06, 0.069375, 0.2275, 0.234375, 0.09, 0.319375, 0.6975, 0.924375, 1], + easeInCirc: [-0, 0.00501256, 0.0202041, 0.0460608, 0.08348486, 0.1339746, 0.2, 0.28585716, 0.4, 0.56411011, 1], + easeInCubic: [0, 0.001, 0.008, 0.027, 0.064, 0.125, 0.216, 0.343, 0.512, 0.729, 1], + easeInExpo: [0, 0.00195313, 0.00390625, 0.0078125, 0.015625, 0.03125, 0.0625, 0.125, 0.25, 0.5, 1], + easeInElastic: [0, 0.00195313, -0.00195313, -0.00390625, 0.015625, -0.015625, -0.03125, 0.125, -0.125, -0.25, 1], + easeInQuad: [0, 0.01, 0.04, 0.09, 0.16, 0.25, 0.36, 0.49, 0.64, 0.81, 1], + easeInQuart: [0, 0.0001, 0.0016, 0.0081, 0.0256, 0.0625, 0.1296, 0.2401, 0.4096, 0.6561, 1], + easeInQuint: [0, 0.00001, 0.00032, 0.00243, 0.01024, 0.03125, 0.07776, 0.16807, 0.32768, 0.59049, 1], + easeInSine: [0, 0.01231166, 0.04894348, 0.10899348, 0.19098301, 0.29289322, 0.41221475, 0.5460095, 0.69098301, 0.84356553, 1], + easeOutBack: [0, 0.40882798, 0.70580224, 0.90713226, 1.02902752, 1.0876975, 1.09935168, 1.08019954, 1.04645056, 1.01431422, 1], + easeOutBounce: [0, 0.075625, 0.3025, 0.680625, 0.91, 0.765625, 0.7725, 0.930625, 0.94, 0.988125, 1], + easeOutCirc: [0, 0.43588989, 0.6, 0.71414284, 0.8, 0.8660254, 0.91651514, 0.9539392, 0.9797959, 0.99498744, 1], + easeOutElastic: [0, 1.25, 1.125, 0.875, 1.03125, 1.015625, 0.984375, 1.00390625, 1.00195313, 0.99804688, 1], + easeOutExpo: [0, 0.5, 0.75, 0.875, 0.9375, 0.96875, 0.984375, 0.9921875, 0.99609375, 0.99804688, 1], + easeOutCubic: [0, 0.271, 0.488, 0.657, 0.784, 0.875, 0.936, 0.973, 0.992, 0.999, 1], + easeOutQuad: [0, 0.19, 0.36, 0.51, 0.64, 0.75, 0.84, 0.91, 0.96, 0.99, 1], + easeOutQuart: [-0, 0.3439, 0.5904, 0.7599, 0.8704, 0.9375, 0.9744, 0.9919, 0.9984, 0.9999, 1], + easeOutQuint: [0, 0.40951, 0.67232, 0.83193, 0.92224, 0.96875, 0.98976, 0.99757, 0.99968, 0.99999, 1], + easeOutSine: [0, 0.15643447, 0.30901699, 0.4539905, 0.58778525, 0.70710678, 0.80901699, 0.89100652, 0.95105652, 0.98768834, 1], + linear: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1] + }; + + function generate(method) { + var fn = helpers.easingEffects[method]; + var accuracy = Math.pow(10, 8); + var count = 10; + var values = []; + var i; + + for (i = 0; i <= count; ++i) { + values.push(Math.round(accuracy * fn(i / count)) / accuracy); + } + + return values; + } + + Object.keys(helpers.easingEffects).forEach(function(method) { + it ('"' + method + '" should return expected values', function() { + expect(generate(method)).toEqual(expected[method]); + }); + }); + }); +}); diff --git a/test/specs/helpers.interpolation.tests.js b/test/specs/helpers.interpolation.tests.js new file mode 100644 index 00000000000..55f72805690 --- /dev/null +++ b/test/specs/helpers.interpolation.tests.js @@ -0,0 +1,35 @@ +const {_pointInLine, _steppedInterpolation, _bezierInterpolation} = Chart.helpers; + +describe('helpers.interpolation', function() { + it('Should interpolate a point in line', function() { + expect(_pointInLine({x: 10, y: 10}, {x: 20, y: 20}, 0)).toEqual({x: 10, y: 10}); + expect(_pointInLine({x: 10, y: 10}, {x: 20, y: 20}, 0.5)).toEqual({x: 15, y: 15}); + expect(_pointInLine({x: 10, y: 10}, {x: 20, y: 20}, 1)).toEqual({x: 20, y: 20}); + }); + + it('Should interpolate a point in stepped line', function() { + expect(_steppedInterpolation({x: 10, y: 10}, {x: 20, y: 20}, 0, 'before')).toEqual({x: 10, y: 10}); + expect(_steppedInterpolation({x: 10, y: 10}, {x: 20, y: 20}, 0.4, 'before')).toEqual({x: 14, y: 20}); + expect(_steppedInterpolation({x: 10, y: 10}, {x: 20, y: 20}, 0.5, 'before')).toEqual({x: 15, y: 20}); + expect(_steppedInterpolation({x: 10, y: 10}, {x: 20, y: 20}, 1, 'before')).toEqual({x: 20, y: 20}); + + expect(_steppedInterpolation({x: 10, y: 10}, {x: 20, y: 20}, 0, 'middle')).toEqual({x: 10, y: 10}); + expect(_steppedInterpolation({x: 10, y: 10}, {x: 20, y: 20}, 0.4, 'middle')).toEqual({x: 14, y: 10}); + expect(_steppedInterpolation({x: 10, y: 10}, {x: 20, y: 20}, 0.5, 'middle')).toEqual({x: 15, y: 20}); + expect(_steppedInterpolation({x: 10, y: 10}, {x: 20, y: 20}, 1, 'middle')).toEqual({x: 20, y: 20}); + + expect(_steppedInterpolation({x: 10, y: 10}, {x: 20, y: 20}, 0, 'after')).toEqual({x: 10, y: 10}); + expect(_steppedInterpolation({x: 10, y: 10}, {x: 20, y: 20}, 0.4, 'after')).toEqual({x: 14, y: 10}); + expect(_steppedInterpolation({x: 10, y: 10}, {x: 20, y: 20}, 0.5, 'after')).toEqual({x: 15, y: 10}); + expect(_steppedInterpolation({x: 10, y: 10}, {x: 20, y: 20}, 1, 'after')).toEqual({x: 20, y: 20}); + }); + + it('Should interpolate a point in curve', function() { + const pt1 = {x: 10, y: 10, cp2x: 12, cp2y: 12}; + const pt2 = {x: 20, y: 30, cp1x: 18, cp1y: 28}; + + expect(_bezierInterpolation(pt1, pt2, 0)).toEqual({x: 10, y: 10}); + expect(_bezierInterpolation(pt1, pt2, 0.2)).toBeCloseToPoint({x: 11.616, y: 12.656}); + expect(_bezierInterpolation(pt1, pt2, 1)).toEqual({x: 20, y: 30}); + }); +}); diff --git a/test/specs/helpers.math.tests.js b/test/specs/helpers.math.tests.js new file mode 100644 index 00000000000..938742959da --- /dev/null +++ b/test/specs/helpers.math.tests.js @@ -0,0 +1,141 @@ +const math = Chart.helpers; + +describe('Chart.helpers.math', function() { + var factorize = math._factorize; + var decimalPlaces = math._decimalPlaces; + + it('should factorize', function() { + expect(factorize(1000)).toEqual([1, 2, 4, 5, 8, 10, 20, 25, 40, 50, 100, 125, 200, 250, 500]); + expect(factorize(60)).toEqual([1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30]); + expect(factorize(30)).toEqual([1, 2, 3, 5, 6, 10, 15]); + expect(factorize(24)).toEqual([1, 2, 3, 4, 6, 8, 12]); + expect(factorize(12)).toEqual([1, 2, 3, 4, 6]); + expect(factorize(4)).toEqual([1, 2]); + expect(factorize(-1)).toEqual([]); + expect(factorize(2.76)).toEqual([]); + }); + + it('should do a log10 operation', function() { + expect(math.log10(0)).toBe(-Infinity); + + // Check all allowed powers of 10, which should return integer values + var maxPowerOf10 = Math.floor(math.log10(Number.MAX_VALUE)); + for (var i = 0; i < maxPowerOf10; i += 1) { + expect(math.log10(Math.pow(10, i))).toBe(i); + } + }); + + it('should get the correct number of decimal places', function() { + expect(decimalPlaces(100)).toBe(0); + expect(decimalPlaces(1)).toBe(0); + expect(decimalPlaces(0)).toBe(0); + expect(decimalPlaces(0.01)).toBe(2); + expect(decimalPlaces(-0.01)).toBe(2); + expect(decimalPlaces('1')).toBe(undefined); + expect(decimalPlaces('')).toBe(undefined); + expect(decimalPlaces(undefined)).toBe(undefined); + expect(decimalPlaces(12345678.1234)).toBe(4); + expect(decimalPlaces(1234567890.1234567)).toBe(7); + }); + + it('should get an angle from a point', function() { + var center = { + x: 0, + y: 0 + }; + + expect(math.getAngleFromPoint(center, { + x: 0, + y: 10 + })).toEqual({ + angle: Math.PI / 2, + distance: 10, + }); + + expect(math.getAngleFromPoint(center, { + x: Math.sqrt(2), + y: Math.sqrt(2) + })).toEqual({ + angle: Math.PI / 4, + distance: 2 + }); + + expect(math.getAngleFromPoint(center, { + x: -1.0 * Math.sqrt(2), + y: -1.0 * Math.sqrt(2) + })).toEqual({ + angle: Math.PI * 1.25, + distance: 2 + }); + }); + + it('should convert between radians and degrees', function() { + expect(math.toRadians(180)).toBe(Math.PI); + expect(math.toRadians(90)).toBe(0.5 * Math.PI); + expect(math.toDegrees(Math.PI)).toBe(180); + expect(math.toDegrees(Math.PI * 3 / 2)).toBe(270); + }); + + it('should correctly determine if two numbers are essentially equal', function() { + expect(math.almostEquals(0, Number.EPSILON, 2 * Number.EPSILON)).toBe(true); + expect(math.almostEquals(1, 1.1, 0.0001)).toBe(false); + expect(math.almostEquals(1e30, 1e30 + Number.EPSILON, 0)).toBe(false); + expect(math.almostEquals(1e30, 1e30 + Number.EPSILON, 2 * Number.EPSILON)).toBe(true); + }); + + it('should get the correct sign', function() { + expect(math.sign(0)).toBe(0); + expect(math.sign(10)).toBe(1); + expect(math.sign(-5)).toBe(-1); + }); + + it('should correctly determine if a numbers are essentially whole', function() { + expect(math.almostWhole(0.99999, 0.0001)).toBe(true); + expect(math.almostWhole(0.9, 0.0001)).toBe(false); + expect(math.almostWhole(1234567890123, 0.0001)).toBe(true); + expect(math.almostWhole(1234567890123.001, 0.0001)).toBe(false); + }); + + it('should detect a number', function() { + expect(math.isNumber(123)).toBe(true); + expect(math.isNumber('123')).toBe(true); + expect(math.isNumber(null)).toBe(false); + expect(math.isNumber(NaN)).toBe(false); + expect(math.isNumber(undefined)).toBe(false); + expect(math.isNumber('cbc')).toBe(false); + expect(math.isNumber(Symbol())).toBe(false); + expect(math.isNumber(Object.create(null))).toBe(false); + }); + + it('should compute shortest distance between angles', function() { + expect(math._angleDiff(1, 2)).toEqual(-1); + expect(math._angleDiff(2, 1)).toEqual(1); + expect(math._angleDiff(0, 3.15)).toBeCloseTo(3.13, 2); + expect(math._angleDiff(0, 3.13)).toEqual(-3.13); + expect(math._angleDiff(6.2, 0)).toBeCloseTo(-0.08, 2); + expect(math._angleDiff(6.3, 0)).toBeCloseTo(0.02, 2); + expect(math._angleDiff(4 * Math.PI, -4 * Math.PI)).toBeCloseTo(0, 4); + expect(math._angleDiff(4 * Math.PI, -3 * Math.PI)).toBeCloseTo(-3.14, 2); + expect(math._angleDiff(6.28, 3.1)).toBeCloseTo(-3.1, 2); + expect(math._angleDiff(6.28, 3.2)).toBeCloseTo(3.08, 2); + }); + + it('should normalize angles correctly', function() { + expect(math._normalizeAngle(-Math.PI)).toEqual(Math.PI); + expect(math._normalizeAngle(Math.PI)).toEqual(Math.PI); + expect(math._normalizeAngle(2)).toEqual(2); + expect(math._normalizeAngle(5 * Math.PI)).toEqual(Math.PI); + expect(math._normalizeAngle(-50 * Math.PI)).toBeCloseTo(6.28, 2); + }); + + it('should determine if angle is between boundaries', function() { + expect(math._angleBetween(2, 1, 3)).toBeTrue(); + expect(math._angleBetween(2, 3, 1)).toBeFalse(); + expect(math._angleBetween(-3.14, 2, 4)).toBeTrue(); + expect(math._angleBetween(-3.14, 4, 2)).toBeFalse(); + expect(math._angleBetween(0, -1, 1)).toBeTrue(); + expect(math._angleBetween(-1, 0, 1)).toBeFalse(); + expect(math._angleBetween(-15 * Math.PI, 3.1, 3.2)).toBeTrue(); + expect(math._angleBetween(15 * Math.PI, -3.2, -3.1)).toBeTrue(); + }); +}); diff --git a/test/specs/helpers.options.tests.js b/test/specs/helpers.options.tests.js new file mode 100644 index 00000000000..c7186e1fb96 --- /dev/null +++ b/test/specs/helpers.options.tests.js @@ -0,0 +1,256 @@ +const {toLineHeight, toPadding, toFont, resolve, toTRBLCorners} = Chart.helpers; + +describe('Chart.helpers.options', function() { + describe('toLineHeight', function() { + it ('should support keyword values', function() { + expect(toLineHeight('normal', 16)).toBe(16 * 1.2); + }); + it ('should support unitless values', function() { + expect(toLineHeight(1.4, 16)).toBe(16 * 1.4); + expect(toLineHeight('1.4', 16)).toBe(16 * 1.4); + }); + it ('should support length values', function() { + expect(toLineHeight('42px', 16)).toBe(42); + expect(toLineHeight('1.4em', 16)).toBe(16 * 1.4); + }); + it ('should support percentage values', function() { + expect(toLineHeight('140%', 16)).toBe(16 * 1.4); + }); + it ('should fallback to default (1.2) for invalid values', function() { + expect(toLineHeight(null, 16)).toBe(16 * 1.2); + expect(toLineHeight(undefined, 16)).toBe(16 * 1.2); + expect(toLineHeight('foobar', 16)).toBe(16 * 1.2); + }); + }); + + describe('toTRBLCorners', function() { + it('should support number values', function() { + expect(toTRBLCorners(4)).toEqual( + {topLeft: 4, topRight: 4, bottomLeft: 4, bottomRight: 4}); + expect(toTRBLCorners(4.5)).toEqual( + {topLeft: 4.5, topRight: 4.5, bottomLeft: 4.5, bottomRight: 4.5}); + }); + it('should support string values', function() { + expect(toTRBLCorners('4')).toEqual( + {topLeft: 4, topRight: 4, bottomLeft: 4, bottomRight: 4}); + expect(toTRBLCorners('4.5')).toEqual( + {topLeft: 4.5, topRight: 4.5, bottomLeft: 4.5, bottomRight: 4.5}); + }); + it('should support object values', function() { + expect(toTRBLCorners({topLeft: 1, topRight: 2, bottomLeft: 3, bottomRight: 4})).toEqual( + {topLeft: 1, topRight: 2, bottomLeft: 3, bottomRight: 4}); + expect(toTRBLCorners({topLeft: 1.5, topRight: 2.5, bottomLeft: 3.5, bottomRight: 4.5})).toEqual( + {topLeft: 1.5, topRight: 2.5, bottomLeft: 3.5, bottomRight: 4.5}); + expect(toTRBLCorners({topLeft: '1', topRight: '2', bottomLeft: '3', bottomRight: '4'})).toEqual( + {topLeft: 1, topRight: 2, bottomLeft: 3, bottomRight: 4}); + }); + it('should fallback to 0 for invalid values', function() { + expect(toTRBLCorners({topLeft: 'foo', topRight: 'foo', bottomLeft: 'foo', bottomRight: 'foo'})).toEqual( + {topLeft: 0, topRight: 0, bottomLeft: 0, bottomRight: 0}); + expect(toTRBLCorners({topLeft: null, topRight: null, bottomLeft: null, bottomRight: null})).toEqual( + {topLeft: 0, topRight: 0, bottomLeft: 0, bottomRight: 0}); + expect(toTRBLCorners({})).toEqual( + {topLeft: 0, topRight: 0, bottomLeft: 0, bottomRight: 0}); + expect(toTRBLCorners('foo')).toEqual( + {topLeft: 0, topRight: 0, bottomLeft: 0, bottomRight: 0}); + expect(toTRBLCorners(null)).toEqual( + {topLeft: 0, topRight: 0, bottomLeft: 0, bottomRight: 0}); + expect(toTRBLCorners(undefined)).toEqual( + {topLeft: 0, topRight: 0, bottomLeft: 0, bottomRight: 0}); + }); + }); + + describe('toPadding', function() { + it ('should support number values', function() { + expect(toPadding(4)).toEqual( + {top: 4, right: 4, bottom: 4, left: 4, height: 8, width: 8}); + expect(toPadding(4.5)).toEqual( + {top: 4.5, right: 4.5, bottom: 4.5, left: 4.5, height: 9, width: 9}); + }); + it ('should support string values', function() { + expect(toPadding('4')).toEqual( + {top: 4, right: 4, bottom: 4, left: 4, height: 8, width: 8}); + expect(toPadding('4.5')).toEqual( + {top: 4.5, right: 4.5, bottom: 4.5, left: 4.5, height: 9, width: 9}); + }); + it ('should support object values', function() { + expect(toPadding({top: 1, right: 2, bottom: 3, left: 4})).toEqual( + {top: 1, right: 2, bottom: 3, left: 4, height: 4, width: 6}); + expect(toPadding({top: 1.5, right: 2.5, bottom: 3.5, left: 4.5})).toEqual( + {top: 1.5, right: 2.5, bottom: 3.5, left: 4.5, height: 5, width: 7}); + expect(toPadding({top: '1', right: '2', bottom: '3', left: '4'})).toEqual( + {top: 1, right: 2, bottom: 3, left: 4, height: 4, width: 6}); + }); + it ('should fallback to 0 for invalid values', function() { + expect(toPadding({top: 'foo', right: 'foo', bottom: 'foo', left: 'foo'})).toEqual( + {top: 0, right: 0, bottom: 0, left: 0, height: 0, width: 0}); + expect(toPadding({top: null, right: null, bottom: null, left: null})).toEqual( + {top: 0, right: 0, bottom: 0, left: 0, height: 0, width: 0}); + expect(toPadding({})).toEqual( + {top: 0, right: 0, bottom: 0, left: 0, height: 0, width: 0}); + expect(toPadding('foo')).toEqual( + {top: 0, right: 0, bottom: 0, left: 0, height: 0, width: 0}); + expect(toPadding(null)).toEqual( + {top: 0, right: 0, bottom: 0, left: 0, height: 0, width: 0}); + expect(toPadding(undefined)).toEqual( + {top: 0, right: 0, bottom: 0, left: 0, height: 0, width: 0}); + }); + it('should support x / y shorthands', function() { + expect(toPadding({x: 1, y: 2})).toEqual( + {top: 2, right: 1, bottom: 2, left: 1, height: 4, width: 2}); + expect(toPadding({x: 1, left: 0})).toEqual( + {top: 0, right: 1, bottom: 0, left: 0, height: 0, width: 1}); + expect(toPadding({y: 5, bottom: 0})).toEqual( + {top: 5, right: 0, bottom: 0, left: 0, height: 5, width: 0}); + }); + }); + + describe('toFont', function() { + it('should return a font with default values', function() { + const defaultFont = Object.assign({}, Chart.defaults.font); + + Object.assign(Chart.defaults.font, { + family: 'foobar', + size: 42, + style: 'oblique 9deg', + lineHeight: 1.5 + }); + + expect(toFont({})).toEqual({ + family: 'foobar', + lineHeight: 63, + size: 42, + string: 'oblique 9deg 42px foobar', + style: 'oblique 9deg', + weight: null + }); + + Object.assign(Chart.defaults.font, defaultFont); + }); + it ('should return a font with given values', function() { + expect(toFont({ + family: 'bla', + lineHeight: 8, + size: 21, + style: 'oblique -90deg' + })).toEqual({ + family: 'bla', + lineHeight: 8 * 21, + size: 21, + string: 'oblique -90deg 21px bla', + style: 'oblique -90deg', + weight: null + }); + }); + it ('should handle a string font size', function() { + expect(toFont({ + family: 'bla', + lineHeight: 8, + size: '21', + style: 'italic' + })).toEqual({ + family: 'bla', + lineHeight: 8 * 21, + size: 21, + string: 'italic 21px bla', + style: 'italic', + weight: null + }); + }); + it('should return null as a font string if size or family are missing', function() { + const fontFamily = Chart.defaults.font.family; + const fontSize = Chart.defaults.font.size; + delete Chart.defaults.font.family; + delete Chart.defaults.font.size; + + expect(toFont({ + style: 'italic', + size: 12 + }).string).toBeNull(); + expect(toFont({ + style: 'italic', + family: 'serif' + }).string).toBeNull(); + + Chart.defaults.font.family = fontFamily; + Chart.defaults.font.size = fontSize; + }); + it('font.style should be optional for font strings', function() { + const fontStyle = Chart.defaults.font.style; + delete Chart.defaults.font.style; + + expect(toFont({ + size: 12, + family: 'serif' + }).string).toBe('12px serif'); + + Chart.defaults.font.style = fontStyle; + }); + }); + + describe('resolve', function() { + it ('should fallback to the first defined input', function() { + expect(resolve([42])).toBe(42); + expect(resolve([42, 'foo'])).toBe(42); + expect(resolve([undefined, 42, 'foo'])).toBe(42); + expect(resolve([42, 'foo', undefined])).toBe(42); + expect(resolve([undefined])).toBe(undefined); + }); + it ('should correctly handle empty values (null, 0, "")', function() { + expect(resolve([0, 'foo'])).toBe(0); + expect(resolve(['', 'foo'])).toBe(''); + expect(resolve([null, 'foo'])).toBe(null); + }); + it ('should support indexable options if index is provided', function() { + var input = [42, 'foo', 'bar']; + expect(resolve([input], undefined, 0)).toBe(42); + expect(resolve([input], undefined, 1)).toBe('foo'); + expect(resolve([input], undefined, 2)).toBe('bar'); + }); + it ('should fallback if an indexable option value is undefined', function() { + var input = [42, undefined, 'bar']; + expect(resolve([input], undefined, 1)).toBe(undefined); + expect(resolve([input, 'foo'], undefined, 1)).toBe('foo'); + }); + it ('should loop if an indexable option index is out of bounds', function() { + var input = [42, undefined, 'bar']; + expect(resolve([input], undefined, 3)).toBe(42); + expect(resolve([input, 'foo'], undefined, 4)).toBe('foo'); + expect(resolve([input, 'foo'], undefined, 5)).toBe('bar'); + }); + it ('should not handle indexable options if index is undefined', function() { + var array = [42, 'foo', 'bar']; + expect(resolve([array])).toBe(array); + expect(resolve([array], undefined, undefined)).toBe(array); + }); + it ('should support scriptable options if context is provided', function() { + var input = function(context) { + return context.v * 2; + }; + expect(resolve([42], {v: 42})).toBe(42); + expect(resolve([input], {v: 42})).toBe(84); + }); + it ('should fallback if a scriptable option returns undefined', function() { + var input = function() {}; + expect(resolve([input], {v: 42})).toBe(undefined); + expect(resolve([input, 'foo'], {v: 42})).toBe('foo'); + expect(resolve([input, undefined, 'foo'], {v: 42})).toBe('foo'); + }); + it ('should not handle scriptable options if context is undefined', function() { + var input = function(context) { + return context.v * 2; + }; + expect(resolve([input])).toBe(input); + expect(resolve([input], undefined)).toBe(input); + }); + it ('should handle scriptable and indexable option', function() { + var input = function(context) { + return [context.v, undefined, 'bar']; + }; + expect(resolve([input, 'foo'], {v: 42}, 0)).toBe(42); + expect(resolve([input, 'foo'], {v: 42}, 1)).toBe('foo'); + expect(resolve([input, 'foo'], {v: 42}, 5)).toBe('bar'); + expect(resolve([input, ['foo', 'bar']], {v: 42}, 1)).toBe('bar'); + }); + }); +}); diff --git a/test/specs/helpers.segment.tests.js b/test/specs/helpers.segment.tests.js new file mode 100644 index 00000000000..f603a6ebc5e --- /dev/null +++ b/test/specs/helpers.segment.tests.js @@ -0,0 +1,61 @@ +const {_boundSegment} = Chart.helpers; + +describe('helpers.segments', function() { + describe('_boundSegment', function() { + const points = [{x: 10, y: 1}, {x: 20, y: 2}, {x: 30, y: 3}]; + const segment = {start: 0, end: 2, loop: false}; + + it('should not find segment from before the line', function() { + expect(_boundSegment(segment, points, {property: 'x', start: 5, end: 9.99999})).toEqual([]); + }); + + it('should not find segment from after the line', function() { + expect(_boundSegment(segment, points, {property: 'x', start: 30.00001, end: 800})).toEqual([]); + }); + + it('should find segment when starting before line', function() { + expect(_boundSegment(segment, points, {property: 'x', start: 5, end: 15})).toEqual([{start: 0, end: 1, loop: false, style: undefined}]); + }); + + it('should find segment directly on point', function() { + expect(_boundSegment(segment, points, {property: 'x', start: 10, end: 10})).toEqual([{start: 0, end: 0, loop: false, style: undefined}]); + }); + + it('should find segment from range between points', function() { + expect(_boundSegment(segment, points, {property: 'x', start: 11, end: 14})).toEqual([{start: 0, end: 1, loop: false, style: undefined}]); + }); + + it('should find segment from point between points', function() { + expect(_boundSegment(segment, points, {property: 'x', start: 22, end: 22})).toEqual([{start: 1, end: 2, loop: false, style: undefined}]); + }); + + it('should find whole segment', function() { + expect(_boundSegment(segment, points, {property: 'x', start: 0, end: 50})).toEqual([{start: 0, end: 2, loop: false, style: undefined}]); + }); + + it('should find correct segment from near points', function() { + expect(_boundSegment(segment, points, {property: 'x', start: 10.001, end: 29.999})).toEqual([{start: 0, end: 2, loop: false, style: undefined}]); + }); + + it('should find segment from after the line', function() { + expect(_boundSegment(segment, points, {property: 'x', start: 25, end: 35})).toEqual([{start: 1, end: 2, loop: false, style: undefined}]); + }); + + it('should find multiple segments', function() { + const points2 = [{x: 0, y: 100}, {x: 1, y: 50}, {x: 2, y: 70}, {x: 4, y: 80}, {x: 5, y: -100}]; + expect(_boundSegment({start: 0, end: 4, loop: false}, points2, {property: 'y', start: 60, end: 60})).toEqual([ + {start: 0, end: 1, loop: false, style: undefined}, + {start: 1, end: 2, loop: false, style: undefined}, + {start: 3, end: 4, loop: false, style: undefined}, + ]); + }); + + it('should find correct segments when there are multiple points with same property value', function() { + const repeatedPoints = [{x: 1, y: 5}, {x: 1, y: 6}, {x: 2, y: 5}, {x: 2, y: 6}, {x: 3, y: 5}, {x: 3, y: 6}, {x: 3, y: 7}]; + expect(_boundSegment({start: 0, end: 6, loop: false}, repeatedPoints, {property: 'x', start: 1, end: 1.1})).toEqual([{start: 0, end: 2, loop: false, style: undefined}]); + expect(_boundSegment({start: 0, end: 6, loop: false}, repeatedPoints, {property: 'x', start: 2, end: 2.1})).toEqual([{start: 2, end: 4, loop: false, style: undefined}]); + expect(_boundSegment({start: 0, end: 6, loop: false}, repeatedPoints, {property: 'x', start: 2, end: 3.1})).toEqual([{start: 2, end: 6, loop: false, style: undefined}]); + expect(_boundSegment({start: 0, end: 6, loop: false}, repeatedPoints, {property: 'x', start: 0, end: 8})).toEqual([{start: 0, end: 6, loop: false, style: undefined}]); + }); + }); +}); diff --git a/test/specs/mixed.tests.js b/test/specs/mixed.tests.js new file mode 100644 index 00000000000..16d7d2f485c --- /dev/null +++ b/test/specs/mixed.tests.js @@ -0,0 +1,46 @@ +describe('Mixed charts', function() { + describe('auto', jasmine.fixture.specs('mixed')); + + it('shoud be constructed with doughnuts chart', function() { + const chart = window.acquireChart({ + data: { + datasets: [{ + type: 'line', + data: [10, 20, 30, 40], + }, { + type: 'doughnut', + data: [10, 20, 30, 50], + } + ], + labels: [] + } + }); + + const meta0 = chart.getDatasetMeta(0); + expect(meta0.type).toEqual('line'); + const meta1 = chart.getDatasetMeta(1); + expect(meta1.type).toEqual('doughnut'); + }); + + it('shoud be constructed with pie chart', function() { + const chart = window.acquireChart({ + data: { + datasets: [{ + type: 'bar', + data: [10, 20, 30, 40], + }, { + type: 'pie', + data: [10, 20, 30, 50], + } + ], + labels: [] + } + }); + + const meta0 = chart.getDatasetMeta(0); + expect(meta0.type).toEqual('bar'); + const meta1 = chart.getDatasetMeta(1); + expect(meta1.type).toEqual('pie'); + }); + +}); diff --git a/test/specs/platform.basic.tests.js b/test/specs/platform.basic.tests.js new file mode 100644 index 00000000000..a2f7a2188d2 --- /dev/null +++ b/test/specs/platform.basic.tests.js @@ -0,0 +1,100 @@ +describe('Platform.basic', function() { + + it('should automatically choose the BasicPlatform for offscreen canvas', function() { + const chart = acquireChart({type: 'line'}, {useOffscreenCanvas: true}); + + expect(chart.platform).toBeInstanceOf(Chart.platforms.BasicPlatform); + + chart.destroy(); + }); + + it('should disable animations', function() { + const chart = acquireChart({type: 'line', options: {animation: {}}}, {useOffscreenCanvas: true}); + + expect(chart.options.animation).toEqual(false); + + chart.destroy(); + }); + + + it('supports choosing the BasicPlatform in a web worker', function(done) { + const canvas = document.createElement('canvas'); + if (!canvas.transferControlToOffscreen) { + pending(); + } + const offscreenCanvas = canvas.transferControlToOffscreen(); + + const worker = new Worker('base/test/BasicChartWebWorker.js'); + worker.onmessage = (event) => { + worker.terminate(); + const {type, errorMessage} = event.data; + if (type === 'error') { + done.fail(errorMessage); + } else if (type === 'success') { + expect(type).toEqual('success'); + done(); + } else { + done.fail('invalid message type sent by worker: ' + type); + } + }; + + worker.postMessage({type: 'initialize', canvas: offscreenCanvas}, [offscreenCanvas]); + }); + + describe('with offscreenCanvas', function() { + it('supports laying out a simple chart', function() { + const chart = acquireChart({ + type: 'bar', + data: { + datasets: [ + {data: [10, 5, 0, 25, 78, -10]} + ], + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] + } + }, { + canvas: { + height: 150, + width: 250 + }, + useOffscreenCanvas: true, + }); + + expect(chart.platform).toBeInstanceOf(Chart.platforms.BasicPlatform); + + expect(chart.chartArea.bottom).toBeCloseToPixel(120); + expect(chart.chartArea.left).toBeCloseToPixel(31); + expect(chart.chartArea.right).toBeCloseToPixel(250); + expect(chart.chartArea.top).toBeCloseToPixel(32); + }); + + it('supports resizing a chart', function() { + const chart = acquireChart({ + type: 'bar', + data: { + datasets: [ + {data: [10, 5, 0, 25, 78, -10]} + ], + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] + } + }, { + canvas: { + height: 150, + width: 250 + }, + useOffscreenCanvas: true, + }); + + expect(chart.platform).toBeInstanceOf(Chart.platforms.BasicPlatform); + + const canvasElement = chart.canvas; + canvasElement.height = 200; + canvasElement.width = 300; + chart.resize(); + + expect(chart.chartArea.bottom).toBeCloseToPixel(150); + expect(chart.chartArea.left).toBeCloseToPixel(31); + expect(chart.chartArea.right).toBeCloseToPixel(300); + expect(chart.chartArea.top).toBeCloseToPixel(32); + }); + }); +}); diff --git a/test/specs/platform.dom.tests.js b/test/specs/platform.dom.tests.js new file mode 100644 index 00000000000..13bc3a61098 --- /dev/null +++ b/test/specs/platform.dom.tests.js @@ -0,0 +1,432 @@ +const DomPlatform = Chart.platforms.DomPlatform; + +describe('Platform.dom', function() { + + describe('context acquisition', function() { + var canvasId = 'chartjs-canvas'; + + beforeEach(function() { + var canvas = document.createElement('canvas'); + canvas.setAttribute('id', canvasId); + window.document.body.appendChild(canvas); + }); + + afterEach(function() { + document.getElementById(canvasId).remove(); + }); + + it('should use the DomPlatform by default', function() { + var chart = acquireChart({type: 'line'}); + + expect(chart.platform).toBeInstanceOf(Chart.platforms.DomPlatform); + + chart.destroy(); + }); + + // see https://github.com/chartjs/Chart.js/issues/2807 + it('should gracefully handle invalid item', function() { + var chart = new Chart('foobar'); + + expect(chart).not.toBeValidChart(); + + chart.destroy(); + }); + + it('should accept a DOM element id', function() { + var canvas = document.getElementById(canvasId); + var chart = new Chart(canvasId); + + expect(chart).toBeValidChart(); + expect(chart.canvas).toBe(canvas); + expect(chart.ctx).toBe(canvas.getContext('2d')); + + chart.destroy(); + }); + + it('should accept a canvas element', function() { + var canvas = document.getElementById(canvasId); + var chart = new Chart(canvas); + + expect(chart).toBeValidChart(); + expect(chart.canvas).toBe(canvas); + expect(chart.ctx).toBe(canvas.getContext('2d')); + + chart.destroy(); + }); + + it('should accept a canvas context2D', function() { + var canvas = document.getElementById(canvasId); + var context = canvas.getContext('2d'); + var chart = new Chart(context); + + expect(chart).toBeValidChart(); + expect(chart.canvas).toBe(canvas); + expect(chart.ctx).toBe(context); + + chart.destroy(); + }); + + it('should accept an array containing canvas', function() { + var canvas = document.getElementById(canvasId); + var chart = new Chart([canvas]); + + expect(chart).toBeValidChart(); + expect(chart.canvas).toBe(canvas); + expect(chart.ctx).toBe(canvas.getContext('2d')); + + chart.destroy(); + }); + + it('should accept a canvas from an iframe', function(done) { + var iframe = document.createElement('iframe'); + iframe.onload = function() { + var doc = iframe.contentDocument; + doc.body.innerHTML += ''; + var canvas = doc.getElementById('chart'); + var chart = new Chart(canvas); + + expect(chart).toBeValidChart(); + expect(chart.canvas).toBe(canvas); + expect(chart.ctx).toBe(canvas.getContext('2d')); + + chart.destroy(); + canvas.remove(); + iframe.remove(); + + done(); + }; + + document.body.appendChild(iframe); + }); + }); + + describe('config.options.aspectRatio', function() { + it('should use default "global" aspect ratio for render and display sizes', function() { + var chart = acquireChart({ + options: { + responsive: false + } + }, { + canvas: { + style: 'width: 620px' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 620, dh: 310, + rw: 620, rh: 310, + }); + }); + + it('should use default "chart" aspect ratio for render and display sizes', function() { + var ratio = Chart.overrides.doughnut.aspectRatio; + Chart.overrides.doughnut.aspectRatio = 1; + + var chart = acquireChart({ + type: 'doughnut', + options: { + responsive: false + } + }, { + canvas: { + style: 'width: 425px' + } + }); + + Chart.overrides.doughnut.aspectRatio = ratio; + + expect(chart).toBeChartOfSize({ + dw: 425, dh: 425, + rw: 425, rh: 425, + }); + }); + + it('should use "user" aspect ratio for render and display sizes', function() { + var chart = acquireChart({ + options: { + responsive: false, + aspectRatio: 3 + } + }, { + canvas: { + style: 'width: 405px' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 405, dh: 135, + rw: 405, rh: 135, + }); + }); + + it('should not apply aspect ratio when height specified', function() { + var chart = acquireChart({ + options: { + responsive: false, + aspectRatio: 3 + } + }, { + canvas: { + style: 'width: 400px; height: 410px' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 400, dh: 410, + rw: 400, rh: 410, + }); + }); + }); + + describe('config.options.responsive: false', function() { + it('should use default canvas size for render and display sizes', function() { + var chart = acquireChart({ + options: { + responsive: false + } + }, { + canvas: { + style: '' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 300, dh: 150, + rw: 300, rh: 150, + }); + }); + + it('should use canvas attributes for render and display sizes', function() { + var chart = acquireChart({ + options: { + responsive: false + } + }, { + canvas: { + style: '', + width: 305, + height: 245, + } + }); + + expect(chart).toBeChartOfSize({ + dw: 305, dh: 245, + rw: 305, rh: 245, + }); + }); + + it('should use canvas style for render and display sizes (if no attributes)', function() { + var chart = acquireChart({ + options: { + responsive: false + } + }, { + canvas: { + style: 'width: 345px; height: 125px' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 345, dh: 125, + rw: 345, rh: 125, + }); + }); + + it('should use attributes for the render size and style for the display size', function() { + var chart = acquireChart({ + options: { + responsive: false + } + }, { + canvas: { + style: 'width: 345px; height: 125px;', + width: 165, + height: 85, + } + }); + + expect(chart).toBeChartOfSize({ + dw: 345, dh: 125, + rw: 165, rh: 85, + }); + }); + + // https://github.com/chartjs/Chart.js/issues/3860 + it('should support decimal display width and/or height', function() { + var chart = acquireChart({ + options: { + responsive: false + } + }, { + canvas: { + style: 'width: 345.42px; height: 125.42px;' + } + }); + + expect(chart).toBeChartOfSize({ + dw: 345, dh: 125, + rw: 345, rh: 125, + }); + }); + }); + + describe('config.options.responsive: true (maintainAspectRatio: true)', function() { + it('should fit parent using aspect ratio to calculate size', function() { + var chart = acquireChart({ + options: { + responsive: true, + maintainAspectRatio: true + } + }, { + canvas: { + style: 'width: 150px; height: 245px' + }, + wrapper: { + style: 'width: 300px; height: 350px' + } + }); + + waitForResize(chart, () => { + expect(chart).toBeChartOfSize({ + dw: 214, dh: 350, + rw: 214, rh: 350, + }); + }); + }); + }); + + describe('controller.destroy', function() { + it('should reset context to default values', function() { + var wrapper = document.createElement('div'); + var canvas = document.createElement('canvas'); + wrapper.appendChild(canvas); + window.document.body.appendChild(wrapper); + var chart = new Chart(canvas, {}); + var context = chart.ctx; + + chart.destroy(); + + // https://www.w3.org/TR/2dcontext/#conformance-requirements + Chart.helpers.each({ + fillStyle: '#000000', + font: '10px sans-serif', + lineJoin: 'miter', + lineCap: 'butt', + lineWidth: 1, + miterLimit: 10, + shadowBlur: 0, + shadowColor: 'rgba(0, 0, 0, 0)', + shadowOffsetX: 0, + shadowOffsetY: 0, + strokeStyle: '#000000', + textAlign: 'start', + textBaseline: 'alphabetic' + }, function(value, key) { + expect(context[key]).toBe(value); + }); + + wrapper.parentNode.removeChild(wrapper); + }); + + it('should restore canvas initial values', function(done) { + var wrapper = document.createElement('div'); + var canvas = document.createElement('canvas'); + + canvas.setAttribute('width', 180); + canvas.setAttribute('style', 'width: 512px; height: 480px'); + wrapper.setAttribute('style', 'width: 450px; height: 450px; position: relative'); + + wrapper.appendChild(canvas); + window.document.body.appendChild(wrapper); + + var chart = new Chart(canvas.getContext('2d'), { + options: { + responsive: true, + maintainAspectRatio: false + } + }); + + waitForResize(chart, function() { + expect(chart).toBeChartOfSize({ + dw: 475, dh: 450, + rw: 475, rh: 450, + }); + + chart.destroy(); + + expect(canvas.getAttribute('width')).toBe('180'); + expect(canvas.getAttribute('height')).toBe(null); + expect(canvas.style.width).toBe('512px'); + expect(canvas.style.height).toBe('480px'); + expect(canvas.style.display).toBe(''); + + wrapper.parentNode.removeChild(wrapper); + done(); + }); + wrapper.style.width = '475px'; + }); + }); + + describe('event handling', function() { + it('should notify plugins about events', async function() { + var notifiedEvent; + var plugin = { + afterEvent: function(chart, args) { + notifiedEvent = args.event; + } + }; + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + responsive: true + }, + plugins: [plugin] + }); + + await jasmine.triggerMouseEvent(chart, 'click', { + x: chart.width / 2, + y: chart.height / 2 + }); + // Check that notifiedEvent is correct + expect(notifiedEvent).not.toBe(undefined); + + // Is type correctly translated + expect(notifiedEvent.type).toBe('click'); + + // Relative Position + expect(notifiedEvent.x).toBeCloseToPixel(chart.width / 2); + expect(notifiedEvent.y).toBeCloseToPixel(chart.height / 2); + }); + }); + + describe('isAttached', function() { + it('should detect detached when canvas is attached to DOM', function() { + var platform = new DomPlatform(); + var canvas = document.createElement('canvas'); + var div = document.createElement('div'); + var anotherDiv = document.createElement('div'); + + expect(platform.isAttached(canvas)).toEqual(false); + div.appendChild(canvas); + expect(platform.isAttached(canvas)).toEqual(false); + anotherDiv.appendChild(div); + expect(platform.isAttached(canvas)).toEqual(false); + document.body.appendChild(anotherDiv); + + expect(platform.isAttached(canvas)).toEqual(true); + + anotherDiv.removeChild(div); + expect(platform.isAttached(canvas)).toEqual(false); + div.removeChild(canvas); + expect(platform.isAttached(canvas)).toEqual(false); + document.body.removeChild(anotherDiv); + expect(platform.isAttached(canvas)).toEqual(false); + }); + }); +}); diff --git a/test/specs/plugin.colors.tests.js b/test/specs/plugin.colors.tests.js new file mode 100644 index 00000000000..9e66f6f5ea8 --- /dev/null +++ b/test/specs/plugin.colors.tests.js @@ -0,0 +1,37 @@ +describe('Plugin.colors', () => { + describe('auto', jasmine.fixture.specs('plugin.colors')); + + describe('Plugin.colors.chartDefaults', () => { + beforeAll(() => { + Chart.defaults.backgroundColor = ['green', 'yellow']; + }); + + afterAll(() => { + Chart.defaults.backgroundColor = 'rgba(0,0,0,0.1)'; + }); + + it('should not use colors plugin when chart defaults are given', () => { + const chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [1, 10], + label: 'dataset1' + }], + labels: ['label1', 'label2'] + }, + options: { + plugins: { + colors: { + enabled: true + } + } + } + }); + + const meta = chart.getDatasetMeta(0); + expect(meta.data[0].options.backgroundColor).toBe('green'); + expect(meta.data[1].options.backgroundColor).toBe('yellow'); + }); + }); +}); diff --git a/test/specs/plugin.decimation.tests.js b/test/specs/plugin.decimation.tests.js new file mode 100644 index 00000000000..9f8320b1e72 --- /dev/null +++ b/test/specs/plugin.decimation.tests.js @@ -0,0 +1,219 @@ +describe('Plugin.decimation', function() { + + describe('auto', jasmine.fixture.specs('plugin.decimation')); + + describe('lttb', function() { + const originalData = [ + {x: 0, y: 0}, + {x: 1, y: 1}, + {x: 2, y: 2}, + {x: 3, y: 3}, + {x: 4, y: 4}, + {x: 5, y: 5}, + {x: 6, y: 6}, + {x: 7, y: 7}, + {x: 8, y: 8}, + {x: 9, y: 9}]; + + it('should draw all element if sample is greater than data based on canvas width', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: originalData, + label: 'dataset1' + }] + }, + scales: { + x: { + type: 'linear', + min: 0, + max: 9 + } + }, + options: { + plugins: { + decimation: { + enabled: true, + algorithm: 'lttb', + samples: 100 + } + } + } + }, { + canvas: { + height: 1, + width: 1 + }, + wrapper: { + height: 1, + width: 1 + } + }); + + expect(chart.data.datasets[0].data.length).toBe(10); + }); + + it('should draw the specified number of elements based on canvas width', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: originalData, + label: 'dataset1' + }] + }, + options: { + parsing: false, + scales: { + x: { + type: 'linear', + min: 0, + max: 9 + } + }, + plugins: { + decimation: { + enabled: true, + algorithm: 'lttb', + samples: 7 + } + } + } + }, { + canvas: { + height: 1, + width: 1 + }, + wrapper: { + height: 1, + width: 1 + } + }); + + expect(chart.data.datasets[0].data.length).toBe(7); + }); + + it('should draw the specified number of elements based on threshold', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: originalData, + label: 'dataset1' + }] + }, + options: { + parsing: false, + scales: { + x: { + type: 'linear' + } + }, + plugins: { + decimation: { + enabled: true, + algorithm: 'lttb', + samples: 5, + threshold: 7 + } + } + } + }, { + canvas: { + height: 100, + width: 100 + }, + wrapper: { + height: 100, + width: 100 + } + }); + + expect(chart.data.datasets[0].data.length).toBe(5); + }); + + it('should draw all element only in range', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: originalData, + label: 'dataset1' + }] + }, + options: { + parsing: false, + scales: { + x: { + type: 'linear', + min: 3, + max: 6 + } + }, + plugins: { + decimation: { + enabled: true, + algorithm: 'lttb', + samples: 7 + } + } + } + }, { + canvas: { + height: 1, + width: 1 + }, + wrapper: { + height: 1, + width: 1 + } + }); + + // Data range is 4 (3->6) and the first point is added + const expectedPoints = 5; + expect(chart.data.datasets[0].data.length).toBe(expectedPoints); + expect(chart.data.datasets[0].data[0].x).toBe(originalData[2].x); + expect(chart.data.datasets[0].data[1].x).toBe(originalData[3].x); + expect(chart.data.datasets[0].data[2].x).toBe(originalData[4].x); + expect(chart.data.datasets[0].data[3].x).toBe(originalData[5].x); + expect(chart.data.datasets[0].data[4].x).toBe(originalData[6].x); + }); + + it('should not crash with uneven points', function() { + const data = []; + for (let i = 0; i < 15552; i++) { + data.push({x: i, y: i}); + } + + function createChart() { + return window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data + }] + }, + options: { + devicePixelRatio: 1.25, + parsing: false, + scales: { + x: { + type: 'linear' + } + }, + plugins: { + decimation: { + enabled: true, + algorithm: 'lttb' + } + } + } + }, { + canvas: {width: 511, height: 511}, + }); + } + expect(createChart).not.toThrow(); + }); + }); +}); diff --git a/test/specs/plugin.filler.tests.js b/test/specs/plugin.filler.tests.js new file mode 100644 index 00000000000..a01f2ff53cc --- /dev/null +++ b/test/specs/plugin.filler.tests.js @@ -0,0 +1,299 @@ +describe('Plugin.filler', function() { + const fillerPluginRegisterWarning = 'Tried to use the \'fill\' option without the \'Filler\' plugin enabled. Please import and register the \'Filler\' plugin and make sure it is not disabled in the options'; + function decodedFillValues(chart) { + return chart.data.datasets.map(function(dataset, index) { + var meta = chart.getDatasetMeta(index) || {}; + expect(meta.$filler).toBeDefined(); + return meta.$filler.fill; + }); + } + + describe('auto', jasmine.fixture.specs('plugin.filler')); + + describe('dataset.fill', function() { + it('Should show a warning when trying to use the filler plugin in the dataset when it\'s not registered', function() { + spyOn(console, 'warn'); + Chart.unregister(Chart.Filler); + window.acquireChart({ + type: 'line', + data: { + datasets: [{ + fill: true + }] + } + }); + + expect(console.warn).toHaveBeenCalledWith(fillerPluginRegisterWarning); + + Chart.register(Chart.Filler); + }); + + it('Should show a warning when trying to use the filler plugin in the root options when it\'s not registered', function() { + // jasmine.createSpy('warn'); + spyOn(console, 'warn'); + Chart.unregister(Chart.Filler); + window.acquireChart({ + type: 'line', + data: { + datasets: [{ + }] + }, + options: { + fill: true + } + }); + + expect(console.warn).toHaveBeenCalledWith(fillerPluginRegisterWarning); + + Chart.register(Chart.Filler); + }); + + it('should support boundaries', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [ + {fill: 'origin'}, + {fill: 'start'}, + {fill: 'end'}, + ] + } + }); + + expect(decodedFillValues(chart)).toEqual(['origin', 'start', 'end']); + }); + + it('should support absolute dataset index', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [ + {fill: 1}, + {fill: 3}, + {fill: 0}, + {fill: 2}, + ] + } + }); + + expect(decodedFillValues(chart)).toEqual([1, 3, 0, 2]); + }); + + it('should support relative dataset index', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [ + {fill: '+3'}, + {fill: '-1'}, + {fill: '+1'}, + {fill: '-2'}, + ] + } + }); + + expect(decodedFillValues(chart)).toEqual([ + 3, // 0 + 3 + 0, // 1 - 1 + 3, // 2 + 1 + 1, // 3 - 2 + ]); + }); + + it('should handle default fill when true (origin)', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [ + {fill: true}, + {fill: false}, + ] + } + }); + + expect(decodedFillValues(chart)).toEqual(['origin', false]); + }); + + it('should ignore self dataset index', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [ + {fill: 0}, + {fill: '-0'}, + {fill: '+0'}, + {fill: 3}, + ] + } + }); + + expect(decodedFillValues(chart)).toEqual([ + false, // 0 === 0 + false, // 1 === 1 - 0 + false, // 2 === 2 + 0 + false, // 3 === 3 + ]); + }); + + it('should ignore out of bounds dataset index', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [ + {fill: -2}, + {fill: 4}, + {fill: '-3'}, + {fill: '+1'}, + ] + } + }); + + expect(decodedFillValues(chart)).toEqual([ + false, // 0 - 2 < 0 + false, // 1 + 4 > 3 + false, // 2 - 3 < 0 + false, // 3 + 1 > 3 + ]); + }); + + it('should ignore invalid values', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [ + {fill: 'foo'}, + {fill: '+foo'}, + {fill: '-foo'}, + {fill: '+1.1'}, + {fill: '-2.2'}, + {fill: 3.3}, + {fill: -4.4}, + {fill: NaN}, + {fill: Infinity}, + {fill: ''}, + {fill: null}, + {fill: []}, + ] + } + }); + + expect(decodedFillValues(chart)).toEqual([ + false, // NaN (string) + false, // NaN (string) + false, // NaN (string) + false, // float (string) + false, // float (string) + false, // float (number) + false, // float (number) + false, // NaN + false, // !isFinite + false, // empty string + false, // null + false, // array + ]); + }); + }); + + describe('options.plugins.filler.propagate', function() { + it('should compute propagated fill targets if true', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [ + {fill: 'start', hidden: true}, + {fill: '-1', hidden: true}, + {fill: 1, hidden: true}, + {fill: '-2', hidden: true}, + {fill: '+1'}, + {fill: '+2'}, + {fill: '-1'}, + {fill: 'end', hidden: true}, + ] + }, + options: { + plugins: { + filler: { + propagate: true + } + } + } + }); + + + expect(decodedFillValues(chart)).toEqual([ + 'start', // 'start' + 'start', // 1 - 1 -> 0 (hidden) -> 'start' + 'start', // 1 (hidden) -> 0 (hidden) -> 'start' + 'start', // 3 - 2 -> 1 (hidden) -> 0 (hidden) -> 'start' + 5, // 4 + 1 + 'end', // 5 + 2 -> 7 (hidden) -> 'end' + 5, // 6 - 1 -> 5 + 'end', // 'end' + ]); + }); + + it('should preserve initial fill targets if false', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [ + {fill: 'start', hidden: true}, + {fill: '-1', hidden: true}, + {fill: 1, hidden: true}, + {fill: '-2', hidden: true}, + {fill: '+1'}, + {fill: '+2'}, + {fill: '-1'}, + {fill: 'end', hidden: true}, + ] + }, + options: { + plugins: { + filler: { + propagate: false + } + } + } + }); + + expect(decodedFillValues(chart)).toEqual([ + 'start', // 'origin' + 0, // 1 - 1 + 1, // 1 + 1, // 3 - 2 + 5, // 4 + 1 + 7, // 5 + 2 + 5, // 6 - 1 + 'end', // 'end' + ]); + }); + + it('should prevent recursive propagation', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [ + {fill: '+2', hidden: true}, + {fill: '-1', hidden: true}, + {fill: '-1', hidden: true}, + {fill: '-2'} + ] + }, + options: { + plugins: { + filler: { + propagate: true + } + } + } + }); + + expect(decodedFillValues(chart)).toEqual([ + false, // 0 + 2 -> 2 (hidden) -> 1 (hidden) -> 0 (loop) + false, // 1 - 1 -> 0 (hidden) -> 2 (hidden) -> 1 (loop) + false, // 2 - 1 -> 1 (hidden) -> 0 (hidden) -> 2 (loop) + false, // 3 - 2 -> 1 (hidden) -> 0 (hidden) -> 2 (hidden) -> 1 (loop) + ]); + }); + }); +}); diff --git a/test/specs/plugin.legend.tests.js b/test/specs/plugin.legend.tests.js new file mode 100644 index 00000000000..e0bed42c263 --- /dev/null +++ b/test/specs/plugin.legend.tests.js @@ -0,0 +1,1185 @@ +// Test the rectangle element +describe('Legend block tests', function() { + describe('auto', jasmine.fixture.specs('plugin.legend')); + + it('should have the correct default config', function() { + expect(Chart.defaults.plugins.legend).toEqual({ + display: true, + position: 'top', + align: 'center', + fullSize: true, + reverse: false, + weight: 1000, + + // a callback that will handle + onClick: jasmine.any(Function), + onHover: null, + onLeave: null, + + labels: { + color: jasmine.any(Function), + boxWidth: 40, + padding: 10, + generateLabels: jasmine.any(Function) + }, + + title: { + color: jasmine.any(Function), + display: false, + position: 'center', + text: '', + } + }); + }); + + it('should update bar chart correctly', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + label: 'dataset1', + backgroundColor: '#f31', + borderCapStyle: 'butt', + borderDash: [2, 2], + borderDashOffset: 5.5, + data: [] + }, { + label: 'dataset2', + hidden: true, + borderJoinStyle: 'miter', + data: [] + }, { + label: 'dataset3', + borderWidth: 10, + borderColor: 'green', + pointStyle: 'crossRot', + data: [] + }], + labels: [] + } + }); + + expect(chart.legend.legendItems).toEqual([{ + text: 'dataset1', + borderRadius: undefined, + fillStyle: '#f31', + fontColor: '#666', + hidden: false, + lineCap: undefined, + lineDash: undefined, + lineDashOffset: undefined, + lineJoin: undefined, + lineWidth: 0, + strokeStyle: 'rgba(0,0,0,0.1)', + pointStyle: undefined, + rotation: undefined, + textAlign: undefined, + datasetIndex: 0 + }, { + text: 'dataset2', + borderRadius: undefined, + fillStyle: 'rgba(0,0,0,0.1)', + fontColor: '#666', + hidden: true, + lineCap: undefined, + lineDash: undefined, + lineDashOffset: undefined, + lineJoin: undefined, + lineWidth: 0, + strokeStyle: 'rgba(0,0,0,0.1)', + pointStyle: undefined, + rotation: undefined, + textAlign: undefined, + datasetIndex: 1 + }, { + text: 'dataset3', + borderRadius: undefined, + fillStyle: 'rgba(0,0,0,0.1)', + fontColor: '#666', + hidden: false, + lineCap: undefined, + lineDash: undefined, + lineDashOffset: undefined, + lineJoin: undefined, + lineWidth: 10, + strokeStyle: 'green', + pointStyle: 'crossRot', + rotation: undefined, + textAlign: undefined, + datasetIndex: 2 + }]); + }); + + it('should update line chart correctly', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'dataset1', + backgroundColor: '#f31', + borderCapStyle: 'round', + borderDash: [2, 2], + borderDashOffset: 5.5, + data: [] + }, { + label: 'dataset2', + hidden: true, + borderJoinStyle: 'round', + data: [] + }, { + label: 'dataset3', + borderWidth: 10, + borderColor: 'green', + pointStyle: 'crossRot', + fill: false, + data: [] + }], + labels: [] + } + }); + + expect(chart.legend.legendItems).toEqual([{ + text: 'dataset1', + borderRadius: undefined, + fillStyle: '#f31', + fontColor: '#666', + hidden: false, + lineCap: 'round', + lineDash: [2, 2], + lineDashOffset: 5.5, + lineJoin: 'miter', + lineWidth: 3, + strokeStyle: 'rgba(0,0,0,0.1)', + pointStyle: undefined, + rotation: undefined, + textAlign: undefined, + datasetIndex: 0 + }, { + text: 'dataset2', + borderRadius: undefined, + fillStyle: 'rgba(0,0,0,0.1)', + fontColor: '#666', + hidden: true, + lineCap: 'butt', + lineDash: [], + lineDashOffset: 0, + lineJoin: 'round', + lineWidth: 3, + strokeStyle: 'rgba(0,0,0,0.1)', + pointStyle: undefined, + rotation: undefined, + textAlign: undefined, + datasetIndex: 1 + }, { + text: 'dataset3', + borderRadius: undefined, + fillStyle: 'rgba(0,0,0,0.1)', + fontColor: '#666', + hidden: false, + lineCap: 'butt', + lineDash: [], + lineDashOffset: 0, + lineJoin: 'miter', + lineWidth: 10, + strokeStyle: 'green', + pointStyle: undefined, + rotation: undefined, + textAlign: undefined, + datasetIndex: 2 + }]); + }); + + it('should reverse correctly', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'dataset1', + backgroundColor: '#f31', + borderCapStyle: 'round', + borderDash: [2, 2], + borderDashOffset: 5.5, + data: [] + }, { + label: 'dataset2', + hidden: true, + borderJoinStyle: 'round', + data: [] + }, { + label: 'dataset3', + borderWidth: 10, + borderColor: 'green', + pointStyle: 'crossRot', + fill: false, + data: [] + }], + labels: [] + }, + options: { + plugins: { + legend: { + reverse: true + } + } + } + }); + + expect(chart.legend.legendItems).toEqual([{ + text: 'dataset3', + borderRadius: undefined, + fillStyle: 'rgba(0,0,0,0.1)', + fontColor: '#666', + hidden: false, + lineCap: 'butt', + lineDash: [], + lineDashOffset: 0, + lineJoin: 'miter', + lineWidth: 10, + strokeStyle: 'green', + pointStyle: undefined, + rotation: undefined, + textAlign: undefined, + datasetIndex: 2 + }, { + text: 'dataset2', + borderRadius: undefined, + fillStyle: 'rgba(0,0,0,0.1)', + fontColor: '#666', + hidden: true, + lineCap: 'butt', + lineDash: [], + lineDashOffset: 0, + lineJoin: 'round', + lineWidth: 3, + strokeStyle: 'rgba(0,0,0,0.1)', + pointStyle: undefined, + rotation: undefined, + textAlign: undefined, + datasetIndex: 1 + }, { + text: 'dataset1', + borderRadius: undefined, + fillStyle: '#f31', + fontColor: '#666', + hidden: false, + lineCap: 'round', + lineDash: [2, 2], + lineDashOffset: 5.5, + lineJoin: 'miter', + lineWidth: 3, + strokeStyle: 'rgba(0,0,0,0.1)', + pointStyle: undefined, + rotation: undefined, + textAlign: undefined, + datasetIndex: 0 + }]); + }); + + it('should filter items', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + label: 'dataset1', + backgroundColor: '#f31', + borderCapStyle: 'butt', + borderDash: [2, 2], + borderDashOffset: 5.5, + data: [] + }, { + label: 'dataset2', + hidden: true, + borderJoinStyle: 'miter', + data: [], + legendHidden: true, + }, { + label: 'dataset3', + borderWidth: 10, + borderRadius: 10, + borderColor: 'green', + pointStyle: 'crossRot', + data: [] + }], + labels: [] + }, + options: { + plugins: { + legend: { + labels: { + filter: function(legendItem, data) { + var dataset = data.datasets[legendItem.datasetIndex]; + return !dataset.legendHidden; + } + } + } + } + } + }); + + expect(chart.legend.legendItems).toEqual([{ + text: 'dataset1', + borderRadius: undefined, + fillStyle: '#f31', + fontColor: '#666', + hidden: false, + lineCap: undefined, + lineDash: undefined, + lineDashOffset: undefined, + lineJoin: undefined, + lineWidth: 0, + strokeStyle: 'rgba(0,0,0,0.1)', + pointStyle: undefined, + rotation: undefined, + textAlign: undefined, + datasetIndex: 0 + }, { + text: 'dataset3', + borderRadius: undefined, + fillStyle: 'rgba(0,0,0,0.1)', + fontColor: '#666', + hidden: false, + lineCap: undefined, + lineDash: undefined, + lineDashOffset: undefined, + lineJoin: undefined, + lineWidth: 10, + strokeStyle: 'green', + pointStyle: 'crossRot', + rotation: undefined, + textAlign: undefined, + datasetIndex: 2 + }]); + }); + + it('should sort items', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'dataset1', + backgroundColor: '#f31', + borderCapStyle: 'round', + borderDash: [2, 2], + borderDashOffset: 5.5, + data: [] + }, { + label: 'dataset2', + hidden: true, + borderJoinStyle: 'round', + data: [] + }, { + label: 'dataset3', + borderWidth: 10, + borderColor: 'green', + pointStyle: 'crossRot', + fill: false, + data: [] + }], + labels: [] + }, + options: { + plugins: { + legend: { + labels: { + sort: function(a, b) { + return b.datasetIndex > a.datasetIndex ? 1 : -1; + } + } + } + } + } + }); + + expect(chart.legend.legendItems).toEqual([{ + text: 'dataset3', + borderRadius: undefined, + fillStyle: 'rgba(0,0,0,0.1)', + fontColor: '#666', + hidden: false, + lineCap: 'butt', + lineDash: [], + lineDashOffset: 0, + lineJoin: 'miter', + lineWidth: 10, + strokeStyle: 'green', + pointStyle: undefined, + rotation: undefined, + textAlign: undefined, + datasetIndex: 2 + }, { + text: 'dataset2', + borderRadius: undefined, + fillStyle: 'rgba(0,0,0,0.1)', + fontColor: '#666', + hidden: true, + lineCap: 'butt', + lineDash: [], + lineDashOffset: 0, + lineJoin: 'round', + lineWidth: 3, + strokeStyle: 'rgba(0,0,0,0.1)', + pointStyle: undefined, + rotation: undefined, + textAlign: undefined, + datasetIndex: 1 + }, { + text: 'dataset1', + borderRadius: undefined, + fillStyle: '#f31', + fontColor: '#666', + hidden: false, + lineCap: 'round', + lineDash: [2, 2], + lineDashOffset: 5.5, + lineJoin: 'miter', + lineWidth: 3, + strokeStyle: 'rgba(0,0,0,0.1)', + pointStyle: undefined, + rotation: undefined, + textAlign: undefined, + datasetIndex: 0 + }]); + }); + + it('should not throw when the label options are missing', function() { + var makeChart = function() { + window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + label: 'dataset1', + backgroundColor: '#f31', + borderCapStyle: 'butt', + borderDash: [2, 2], + borderDashOffset: 5.5, + data: [] + }], + labels: [] + }, + options: { + plugins: { + legend: { + labels: false, + } + } + } + }); + }; + expect(makeChart).not.toThrow(); + }); + + it('should not draw legend items outside of the chart bounds', function() { + var chart = window.acquireChart( + { + type: 'line', + data: { + datasets: [1, 2, 3].map(function(n) { + return { + label: 'dataset' + n, + data: [] + }; + }), + labels: [] + }, + options: { + plugins: { + legend: { + position: 'right' + } + } + } + }, + { + canvas: { + width: 512, + height: 105 + } + } + ); + + // Check some basic assertions about the test setup + expect(chart.width).toBe(512); + expect(chart.legend.legendHitBoxes.length).toBe(3); + + // Check whether any legend items reach outside the established bounds + chart.legend.legendHitBoxes.forEach(function(item) { + expect(item.left + item.width).toBeLessThanOrEqual(chart.width); + }); + }); + + it('should draw legend with multiline labels', function() { + const chart = window.acquireChart({ + type: 'doughnut', + data: { + labels: [ + 'ABCDE', + [ + 'ABCDE', + 'ABCDE', + ], + [ + 'Some Text', + 'Some Text', + 'Some Text', + ], + 'ABCDE', + ], + datasets: [ + { + label: 'test', + data: [ + 73.42, + 18.13, + 7.54, + 0.9, + 0.0025, + 1.8e-5, + ], + backgroundColor: [ + '#0078C2', + '#56CAF5', + '#B1E3F9', + '#FBBC8D', + '#F6A3BE', + '#4EC2C1', + ], + }, + ], + }, + options: { + plugins: { + legend: { + labels: { + usePointStyle: true, + pointStyle: 'rect', + }, + position: 'right', + align: 'center', + maxWidth: 860, + }, + }, + aspectRatio: 3, + }, + }); + + // Check some basic assertions about the test setup + expect(chart.legend.legendHitBoxes.length).toBe(4); + + // Check whether any legend items reach outside the established bounds + chart.legend.legendHitBoxes.forEach(function(item) { + expect(item.left + item.width).toBeLessThanOrEqual(chart.width); + }); + }); + + it('should draw items with a custom boxHeight', function() { + var chart = window.acquireChart( + { + type: 'line', + data: { + datasets: [{ + label: 'dataset1', + data: [] + }], + labels: [] + }, + options: { + plugins: { + legend: { + position: 'right', + labels: { + boxHeight: 40 + } + } + } + } + }, + { + canvas: { + width: 512, + height: 105 + } + } + ); + const hitBox = chart.legend.legendHitBoxes[0]; + expect(hitBox.height).toBe(40); + }); + + it('should pick up the first item when the property is an array', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + label: 'dataset1', + backgroundColor: ['#f31', '#666', '#14e'], + borderWidth: [5, 10, 15], + borderColor: ['red', 'green', 'blue'], + data: [] + }], + labels: [] + } + }); + + expect(chart.legend.legendItems).toEqual([{ + text: 'dataset1', + borderRadius: undefined, + fillStyle: '#f31', + fontColor: '#666', + hidden: false, + lineCap: undefined, + lineDash: undefined, + lineDashOffset: undefined, + lineJoin: undefined, + lineWidth: 5, + strokeStyle: 'red', + pointStyle: undefined, + rotation: undefined, + textAlign: undefined, + datasetIndex: 0 + }]); + }); + + it('should use the borderRadius in the legend', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + label: 'dataset1', + backgroundColor: ['#f31', '#666', '#14e'], + borderWidth: [5, 10, 15], + borderColor: ['red', 'green', 'blue'], + borderRadius: 10, + data: [] + }], + labels: [] + }, + options: { + plugins: { + legend: { + labels: { + useBorderRadius: true, + } + } + } + } + }); + + expect(chart.legend.legendItems).toEqual([{ + text: 'dataset1', + borderRadius: 10, + fillStyle: '#f31', + fontColor: '#666', + hidden: false, + lineCap: undefined, + lineDash: undefined, + lineDashOffset: undefined, + lineJoin: undefined, + lineWidth: 5, + strokeStyle: 'red', + pointStyle: undefined, + rotation: undefined, + textAlign: undefined, + datasetIndex: 0 + }]); + }); + + it('should use the value for the first item when the property is a function', function() { + var helpers = window.Chart.helpers; + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + label: 'dataset1', + backgroundColor: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return helpers.color({r: value * 10, g: 0, b: 0}).rgbString(); + }, + borderWidth: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return value; + }, + borderColor: function(ctx) { + var value = ctx.dataset.data[ctx.dataIndex] || 0; + return helpers.color({r: 255 - value * 10, g: 0, b: 0}).rgbString(); + }, + data: [5, 10, 15, 20] + }], + labels: ['A', 'B', 'C', 'D'] + } + }); + + expect(chart.legend.legendItems).toEqual([{ + text: 'dataset1', + borderRadius: undefined, + fillStyle: 'rgb(50, 0, 0)', + fontColor: '#666', + hidden: false, + lineCap: undefined, + lineDash: undefined, + lineDashOffset: undefined, + lineJoin: undefined, + lineWidth: 5, + strokeStyle: 'rgb(205, 0, 0)', + pointStyle: undefined, + rotation: undefined, + textAlign: undefined, + datasetIndex: 0 + }]); + }); + + it('should draw correctly when usePointStyle is true', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'dataset1', + backgroundColor: '#f31', + borderCapStyle: 'butt', + borderDash: [2, 2], + borderDashOffset: 5.5, + borderWidth: 0, + borderColor: '#f31', + pointStyle: 'crossRot', + pointBackgroundColor: 'rgba(0,0,0,0.1)', + pointBorderWidth: 5, + pointBorderColor: 'green', + data: [] + }, { + label: 'dataset2', + backgroundColor: '#f31', + borderJoinStyle: 'miter', + borderWidth: 2, + borderColor: '#f31', + pointStyle: 'crossRot', + pointRotation: 15, + data: [] + }], + labels: [] + }, + options: { + plugins: { + legend: { + labels: { + usePointStyle: true + } + } + } + } + }); + + expect(chart.legend.legendItems).toEqual([{ + text: 'dataset1', + borderRadius: undefined, + fillStyle: 'rgba(0,0,0,0.1)', + fontColor: '#666', + hidden: false, + lineCap: undefined, + lineDash: undefined, + lineDashOffset: undefined, + lineJoin: undefined, + lineWidth: 5, + strokeStyle: 'green', + pointStyle: 'crossRot', + rotation: 0, + textAlign: undefined, + datasetIndex: 0 + }, { + text: 'dataset2', + borderRadius: undefined, + fillStyle: '#f31', + fontColor: '#666', + hidden: false, + lineCap: undefined, + lineDash: undefined, + lineDashOffset: undefined, + lineJoin: undefined, + lineWidth: 2, + strokeStyle: '#f31', + pointStyle: 'crossRot', + rotation: 15, + textAlign: undefined, + datasetIndex: 1 + }]); + }); + + it('should draw correctly when usePointStyle is true and pointStyle override is set', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'dataset1', + backgroundColor: '#f31', + borderCapStyle: 'butt', + borderDash: [2, 2], + borderDashOffset: 5.5, + borderWidth: 0, + borderColor: '#f31', + pointStyle: 'crossRot', + pointBackgroundColor: 'rgba(0,0,0,0.1)', + pointBorderWidth: 5, + pointBorderColor: 'green', + data: [] + }, { + label: 'dataset2', + backgroundColor: '#f31', + borderJoinStyle: 'miter', + borderWidth: 2, + borderColor: '#f31', + pointStyle: 'crossRot', + pointRotation: 15, + data: [] + }], + labels: [] + }, + options: { + plugins: { + legend: { + labels: { + usePointStyle: true, + pointStyle: 'star' + } + } + } + } + }); + + expect(chart.legend.legendItems).toEqual([{ + text: 'dataset1', + borderRadius: undefined, + fillStyle: 'rgba(0,0,0,0.1)', + fontColor: '#666', + hidden: false, + lineCap: undefined, + lineDash: undefined, + lineDashOffset: undefined, + lineJoin: undefined, + lineWidth: 5, + strokeStyle: 'green', + pointStyle: 'star', + rotation: 0, + textAlign: undefined, + datasetIndex: 0 + }, { + text: 'dataset2', + borderRadius: undefined, + fillStyle: '#f31', + fontColor: '#666', + hidden: false, + lineCap: undefined, + lineDash: undefined, + lineDashOffset: undefined, + lineJoin: undefined, + lineWidth: 2, + strokeStyle: '#f31', + pointStyle: 'star', + rotation: 15, + textAlign: undefined, + datasetIndex: 1 + }]); + }); + + it('should not crash when the legend defaults are false', function() { + const oldDefaults = Chart.defaults.plugins.legend; + + Chart.defaults.set({ + plugins: { + legend: false, + }, + }); + + var chart = window.acquireChart({ + type: 'doughnut', + data: { + datasets: [{ + label: 'dataset1', + data: [1, 2, 3, 4] + }], + labels: ['', '', '', ''] + }, + }); + expect(chart).toBeDefined(); + + Chart.defaults.set({ + plugins: { + legend: oldDefaults, + }, + }); + }); + + it('should not read onClick from chart options', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May'], + datasets: [{ + label: 'dataset', + backgroundColor: 'red', + borderColor: 'red', + data: [120, 23, 24, 45, 51] + }] + }, + options: { + responsive: true, + onClick() { }, + plugins: { + legend: { + display: true + } + } + } + }); + expect(chart.legend.options.onClick).toBe(Chart.defaults.plugins.legend.onClick); + }); + + it('should read labels.color from chart options', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May'], + datasets: [{ + label: 'dataset', + backgroundColor: 'red', + borderColor: 'red', + data: [120, 23, 24, 45, 51] + }] + }, + options: { + responsive: true, + color: 'green', + plugins: { + legend: { + display: true + } + } + } + }); + expect(chart.legend.options.labels.color).toBe('green'); + expect(chart.legend.options.title.color).toBe('green'); + }); + + + describe('config update', function() { + it('should update the options', function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + plugins: { + legend: { + display: true + } + } + } + }); + expect(chart.legend.options.display).toBe(true); + + chart.options.plugins.legend.display = false; + chart.update(); + expect(chart.legend.options.display).toBe(false); + }); + + it('should update the associated layout item', function() { + var chart = acquireChart({ + type: 'line', + data: {}, + options: { + plugins: { + legend: { + fullSize: true, + position: 'top', + weight: 150 + } + } + } + }); + + expect(chart.legend.fullSize).toBe(true); + expect(chart.legend.position).toBe('top'); + expect(chart.legend.weight).toBe(150); + + chart.options.plugins.legend.fullSize = false; + chart.options.plugins.legend.position = 'left'; + chart.options.plugins.legend.weight = 42; + chart.update(); + + expect(chart.legend.fullSize).toBe(false); + expect(chart.legend.position).toBe('left'); + expect(chart.legend.weight).toBe(42); + }); + + it('should remove the legend if the new options are false', function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + } + }); + expect(chart.legend).not.toBe(undefined); + + chart.options.plugins.legend = false; + chart.update(); + expect(chart.legend).toBe(undefined); + }); + + it('should create the legend if the legend options are changed to exist', function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + plugins: { + legend: false + } + } + }); + expect(chart.legend).toBe(undefined); + + chart.options.plugins.legend = {}; + chart.update(); + expect(chart.legend).not.toBe(undefined); + expect(chart.legend.options).toEqualOptions(Object.assign({}, + // replace scriptable options with resolved values + Chart.defaults.plugins.legend, + { + labels: {color: Chart.defaults.color}, + title: {color: Chart.defaults.color} + } + )); + }); + }); + + describe('callbacks', function() { + it('should call onClick, onHover and onLeave at the correct times', async function() { + var clickItem = null; + var hoverItem = null; + var leaveItem = null; + + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + plugins: { + legend: { + onClick: function(_, item) { + clickItem = item; + }, + onHover: function(_, item) { + hoverItem = item; + }, + onLeave: function(_, item) { + leaveItem = item; + } + } + } + } + }); + + var hb = chart.legend.legendHitBoxes[0]; + var el = { + x: hb.left + (hb.width / 2), + y: hb.top + (hb.height / 2) + }; + + await jasmine.triggerMouseEvent(chart, 'click', el); + expect(clickItem).toBe(chart.legend.legendItems[0]); + + await jasmine.triggerMouseEvent(chart, 'mousemove', el); + expect(hoverItem).toBe(chart.legend.legendItems[0]); + + await jasmine.triggerMouseEvent(chart, 'mousemove', chart.getDatasetMeta(0).data[0]); + expect(leaveItem).toBe(chart.legend.legendItems[0]); + }); + + it('should call onLeave when the mouse leaves the canvas', async function() { + var hoverItem = null; + var leaveItem = null; + + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + plugins: { + legend: { + onHover: function(_, item) { + hoverItem = item; + }, + onLeave: function(_, item) { + leaveItem = item; + } + } + } + } + }); + + var hb = chart.legend.legendHitBoxes[0]; + var el = { + x: hb.left + (hb.width / 2), + y: hb.top + (hb.height / 2) + }; + + await jasmine.triggerMouseEvent(chart, 'mousemove', el); + expect(hoverItem).toBe(chart.legend.legendItems[0]); + + await jasmine.triggerMouseEvent(chart, 'mouseout'); + expect(leaveItem).toBe(chart.legend.legendItems[0]); + }); + + + it('should call onClick for the correct item when in RTL mode', async function() { + var clickItem = null; + + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100], + label: 'dataset 1' + }, { + data: [10, 20, 30, 100], + label: 'dataset 2' + }] + }, + options: { + plugins: { + legend: { + onClick: function(_, item) { + clickItem = item; + }, + } + } + } + }); + + var hb = chart.legend.legendHitBoxes[0]; + var el = { + x: hb.left + (hb.width / 2), + y: hb.top + (hb.height / 2) + }; + + await jasmine.triggerMouseEvent(chart, 'click', el); + expect(clickItem).toBe(chart.legend.legendItems[0]); + }); + }); +}); diff --git a/test/specs/plugin.subtitle.tests.js b/test/specs/plugin.subtitle.tests.js new file mode 100644 index 00000000000..10d8ac0a1cb --- /dev/null +++ b/test/specs/plugin.subtitle.tests.js @@ -0,0 +1,3 @@ +describe('plugin.subtitle', function() { + describe('auto', jasmine.fixture.specs('plugin.subtitle')); +}); diff --git a/test/specs/plugin.title.tests.js b/test/specs/plugin.title.tests.js new file mode 100644 index 00000000000..c08b9d4c902 --- /dev/null +++ b/test/specs/plugin.title.tests.js @@ -0,0 +1,358 @@ +// Test the rectangle element + +var Title = Chart.registry.getPlugin('title')._element; + +describe('Plugin.title', function() { + describe('auto', jasmine.fixture.specs('plugin.title')); + + it('Should have the correct default config', function() { + expect(Chart.defaults.plugins.title).toEqual({ + align: 'center', + color: Chart.defaults.color, + display: false, + position: 'top', + fullSize: true, + weight: 2000, + font: { + weight: 'bold' + }, + padding: 10, + text: '' + }); + }); + + it('should update correctly', function() { + var chart = { + options: Chart.helpers.clone(Chart.defaults) + }; + + var options = Chart.helpers.clone(Chart.defaults.plugins.title); + options.text = 'My title'; + + var title = new Title({ + chart: chart, + options: options + }); + + title.update(400, 200); + + expect(title.width).toEqual(0); + expect(title.height).toEqual(0); + + // Now we have a height since we display + title.options.display = true; + + title.update(400, 200); + + expect(title.width).toEqual(400); + expect(title.height).toEqual(34.4); + }); + + it('should update correctly when vertical', function() { + var chart = { + options: Chart.helpers.clone(Chart.defaults) + }; + + var options = Chart.helpers.clone(Chart.defaults.plugins.title); + options.text = 'My title'; + options.position = 'left'; + + var title = new Title({ + chart: chart, + options: options + }); + + title.update(200, 400); + + expect(title.width).toEqual(0); + expect(title.height).toEqual(0); + + // Now we have a height since we display + title.options.display = true; + + title.update(200, 400); + + expect(title.width).toEqual(34.4); + expect(title.height).toEqual(400); + }); + + it('should have the correct size when there are multiple lines of text', function() { + var chart = { + options: Chart.helpers.clone(Chart.defaults) + }; + + var options = Chart.helpers.clone(Chart.defaults.plugins.title); + options.text = ['line1', 'line2']; + options.position = 'left'; + options.display = true; + options.font.lineHeight = 1.5; + + var title = new Title({ + chart: chart, + options: options + }); + + title.update(200, 400); + + expect(title.width).toEqual(56); + expect(title.height).toEqual(400); + }); + + it('should draw correctly horizontally', function() { + var chart = { + options: Chart.helpers.clone(Chart.defaults) + }; + var context = window.createMockContext(); + + var options = Chart.helpers.clone(Chart.defaults.plugins.title); + options.text = 'My title'; + + var title = new Title({ + chart: chart, + options: options, + ctx: context + }); + + title.update(400, 200); + title.draw(); + + expect(context.getCalls()).toEqual([]); + + // Now we have a height since we display + title.options.display = true; + + title.update(400, 200); + title.top = 50; + title.left = 100; + title.bottom = title.top + title.height; + title.right = title.left + title.width; + title.draw(); + + expect(context.getCalls()).toEqual([{ + name: 'save', + args: [] + }, { + name: 'setFont', + args: ["normal bold 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"], + }, { + name: 'translate', + args: [300, 67.2] + }, { + name: 'rotate', + args: [0] + }, { + name: 'setFillStyle', + args: ['#666'] + }, { + name: 'setTextAlign', + args: ['center'], + }, { + name: 'setTextBaseline', + args: ['middle'], + }, { + name: 'fillText', + args: ['My title', 0, 0, 400] + }, { + name: 'restore', + args: [] + }]); + }); + + it ('should draw correctly vertically', function() { + var chart = { + options: Chart.helpers.clone(Chart.defaults) + }; + var context = window.createMockContext(); + + var options = Chart.helpers.clone(Chart.defaults.plugins.title); + options.text = 'My title'; + options.position = 'left'; + + var title = new Title({ + chart: chart, + options: options, + ctx: context + }); + + title.update(200, 400); + title.draw(); + + expect(context.getCalls()).toEqual([]); + + // Now we have a height since we display + title.options.display = true; + + title.update(200, 400); + title.top = 50; + title.left = 100; + title.bottom = title.top + title.height; + title.right = title.left + title.width; + title.draw(); + + expect(context.getCalls()).toEqual([{ + name: 'save', + args: [] + }, { + name: 'setFont', + args: ["normal bold 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"], + }, { + name: 'translate', + args: [117.2, 250] + }, { + name: 'rotate', + args: [-0.5 * Math.PI] + }, { + name: 'setFillStyle', + args: ['#666'] + }, { + name: 'setTextAlign', + args: ['center'], + }, { + name: 'setTextBaseline', + args: ['middle'], + }, { + name: 'fillText', + args: ['My title', 0, 0, 400] + }, { + name: 'restore', + args: [] + }]); + + // Rotation is other way on right side + title.options.position = 'right'; + + // Reset call tracker + context.resetCalls(); + + title.update(200, 400); + title.top = 50; + title.left = 100; + title.bottom = title.top + title.height; + title.right = title.left + title.width; + title.draw(); + + expect(context.getCalls()).toEqual([{ + name: 'save', + args: [] + }, { + name: 'setFont', + args: ["normal bold 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"], + }, { + name: 'translate', + args: [117.2, 250] + }, { + name: 'rotate', + args: [0.5 * Math.PI] + }, { + name: 'setFillStyle', + args: ['#666'] + }, { + name: 'setTextAlign', + args: ['center'], + }, { + name: 'setTextBaseline', + args: ['middle'], + }, { + name: 'fillText', + args: ['My title', 0, 0, 400] + }, { + name: 'restore', + args: [] + }]); + }); + + describe('config update', function() { + it ('should update the options', function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + plugins: { + title: { + display: true + } + } + } + }); + expect(chart.titleBlock.options.display).toBe(true); + + chart.options.plugins.title.display = false; + chart.update(); + expect(chart.titleBlock.options.display).toBe(false); + }); + + it ('should update the associated layout item', function() { + var chart = acquireChart({ + type: 'line', + data: {}, + options: { + plugins: { + title: { + fullSize: true, + position: 'top', + weight: 150 + } + } + } + }); + + expect(chart.titleBlock.fullSize).toBe(true); + expect(chart.titleBlock.position).toBe('top'); + expect(chart.titleBlock.weight).toBe(150); + + chart.options.plugins.title.fullSize = false; + chart.options.plugins.title.position = 'left'; + chart.options.plugins.title.weight = 42; + chart.update(); + + expect(chart.titleBlock.fullSize).toBe(false); + expect(chart.titleBlock.position).toBe('left'); + expect(chart.titleBlock.weight).toBe(42); + }); + + it ('should remove the title if the new options are false', function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + } + }); + expect(chart.titleBlock).not.toBe(undefined); + + chart.options.plugins.title = false; + chart.update(); + expect(chart.titleBlock).toBe(undefined); + }); + + it ('should create the title if the title options are changed to exist', function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }] + }, + options: { + plugins: { + title: false + } + } + }); + expect(chart.titleBlock).toBe(undefined); + + chart.options.plugins.title = {}; + chart.update(); + expect(chart.titleBlock).not.toBe(undefined); + expect(chart.titleBlock.options).toEqualOptions(Chart.defaults.plugins.title); + }); + }); +}); diff --git a/test/specs/plugin.tooltip.tests.js b/test/specs/plugin.tooltip.tests.js new file mode 100644 index 00000000000..94ae45b7246 --- /dev/null +++ b/test/specs/plugin.tooltip.tests.js @@ -0,0 +1,1951 @@ +// Test the rectangle element +const tooltipPlugin = Chart.registry.getPlugin('tooltip'); +const Tooltip = tooltipPlugin._element; + +describe('Plugin.Tooltip', function() { + describe('auto', jasmine.fixture.specs('plugin.tooltip')); + + describe('config', function() { + it('should not include the dataset label in the body string if not defined', function() { + var data = { + datasets: [{ + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + }; + var tooltipItem = { + index: 1, + datasetIndex: 0, + dataset: data.datasets[0], + label: 'Point 2', + formattedValue: '20' + }; + + var label = Chart.defaults.plugins.tooltip.callbacks.label(tooltipItem); + expect(label).toBe('20'); + + data.datasets[0].label = 'My dataset'; + label = Chart.defaults.plugins.tooltip.callbacks.label(tooltipItem); + expect(label).toBe('My dataset: 20'); + }); + }); + + describe('index mode', function() { + it('Should only use x distance when intersect is false', async function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + }, + options: { + plugins: { + tooltip: { + mode: 'index', + intersect: false, + padding: { + left: 6, + top: 6, + right: 6, + bottom: 6 + } + } + }, + hover: { + mode: 'index', + intersect: false + } + } + }); + + // Trigger an event over top of the + var meta = chart.getDatasetMeta(0); + var point = meta.data[1]; + + // Check and see if tooltip was displayed + var tooltip = chart.tooltip; + var defaults = Chart.defaults; + + await jasmine.triggerMouseEvent(chart, 'mousemove', {x: point.x, y: chart.chartArea.top + 10}); + + expect(tooltip.options.padding).toEqualOptions({ + left: 6, + top: 6, + right: 6, + bottom: 6, + }); + expect(tooltip.xAlign).toEqual('left'); + expect(tooltip.yAlign).toEqual('center'); + expect(tooltip.options.bodyColor).toEqual('#fff'); + + expect(tooltip.options.bodyFont).toEqualOptions({ + family: defaults.font.family, + style: defaults.font.style, + size: defaults.font.size, + }); + + expect(tooltip.options).toEqualOptions({ + bodyAlign: 'left', + bodySpacing: 2, + }); + + expect(tooltip.options.titleColor).toEqual('#fff'); + expect(tooltip.options.titleFont).toEqualOptions({ + family: defaults.font.family, + weight: 'bold', + size: defaults.font.size, + }); + + expect(tooltip.options).toEqualOptions({ + titleAlign: 'left', + titleSpacing: 2, + titleMarginBottom: 6, + }); + + expect(tooltip.options.footerColor).toEqual('#fff'); + expect(tooltip.options.footerFont).toEqualOptions({ + family: defaults.font.family, + weight: 'bold', + size: defaults.font.size, + }); + + expect(tooltip.options).toEqualOptions({ + footerAlign: 'left', + footerSpacing: 2, + footerMarginTop: 6, + }); + + expect(tooltip.options).toEqualOptions({ + // Appearance + caretSize: 5, + caretPadding: 2, + cornerRadius: 6, + backgroundColor: 'rgba(0,0,0,0.8)', + multiKeyBackground: '#fff', + displayColors: true + }); + + expect(tooltip).toEqual(jasmine.objectContaining({ + opacity: 1, + + // Text + title: ['Point 2'], + beforeBody: [], + body: [{ + before: [], + lines: ['Dataset 1: 20'], + after: [] + }, { + before: [], + lines: ['Dataset 2: 40'], + after: [] + }], + afterBody: [], + footer: [], + labelColors: [{ + borderColor: defaults.borderColor, + backgroundColor: defaults.backgroundColor, + borderWidth: 1, + borderDash: undefined, + borderDashOffset: undefined, + borderRadius: 0, + }, { + borderColor: defaults.borderColor, + backgroundColor: defaults.backgroundColor, + borderWidth: 1, + borderDash: undefined, + borderDashOffset: undefined, + borderRadius: 0, + }] + })); + + expect(tooltip.x).toBeCloseToPixel(266); + expect(tooltip.y).toBeCloseToPixel(150); + }); + + it('Should only display if intersecting if intersect is set', async function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + }, + options: { + plugins: { + tooltip: { + mode: 'index', + intersect: true + } + } + } + }); + + // Trigger an event over top of the + var meta = chart.getDatasetMeta(0); + var point = meta.data[1]; + + await jasmine.triggerMouseEvent(chart, 'mousemove', {x: point.x, y: 0}); + // Check and see if tooltip was displayed + var tooltip = chart.tooltip; + + expect(tooltip).toEqual(jasmine.objectContaining({ + opacity: 0, + })); + }); + }); + + it('Should display in single mode', async function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + }, + options: { + plugins: { + tooltip: { + mode: 'nearest', + intersect: true + } + } + } + }); + + // Trigger an event over top of the + var meta = chart.getDatasetMeta(0); + var point = meta.data[1]; + + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + // Check and see if tooltip was displayed + var tooltip = chart.tooltip; + var defaults = Chart.defaults; + + expect(tooltip.options.padding).toEqual(6); + expect(tooltip.xAlign).toEqual('left'); + expect(tooltip.yAlign).toEqual('center'); + + expect(tooltip.options.bodyFont).toEqual(jasmine.objectContaining({ + family: defaults.font.family, + style: defaults.font.style, + size: defaults.font.size, + })); + + expect(tooltip.options).toEqualOptions({ + bodyAlign: 'left', + bodySpacing: 2, + }); + + expect(tooltip.options.titleFont).toEqual(jasmine.objectContaining({ + family: defaults.font.family, + weight: 'bold', + size: defaults.font.size, + })); + + expect(tooltip.options).toEqualOptions({ + titleAlign: 'left', + titleSpacing: 2, + titleMarginBottom: 6, + }); + + expect(tooltip.options.footerFont).toEqualOptions({ + family: defaults.font.family, + weight: 'bold', + size: defaults.font.size, + }); + + expect(tooltip.options).toEqualOptions({ + footerAlign: 'left', + footerSpacing: 2, + footerMarginTop: 6, + }); + + expect(tooltip.options).toEqualOptions({ + // Appearance + caretSize: 5, + caretPadding: 2, + cornerRadius: 6, + backgroundColor: 'rgba(0,0,0,0.8)', + multiKeyBackground: '#fff', + displayColors: true + }); + + expect(tooltip.opacity).toEqual(1); + expect(tooltip.title).toEqual(['Point 2']); + expect(tooltip.beforeBody).toEqual([]); + expect(tooltip.body).toEqual([{ + before: [], + lines: ['Dataset 1: 20'], + after: [] + }]); + expect(tooltip.afterBody).toEqual([]); + expect(tooltip.footer).toEqual([]); + expect(tooltip.labelTextColors).toEqual(['#fff']); + + expect(tooltip.labelColors).toEqual([{ + borderColor: defaults.borderColor, + backgroundColor: defaults.backgroundColor, + borderWidth: 1, + borderDash: undefined, + borderDashOffset: undefined, + borderRadius: 0, + }]); + + expect(tooltip.x).toBeCloseToPixel(267); + expect(tooltip.y).toBeCloseToPixel(308); + }); + + it('Should display information from user callbacks', async function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + }, + options: { + plugins: { + tooltip: { + mode: 'index', + callbacks: { + beforeTitle: function() { + return 'beforeTitle'; + }, + title: function() { + return 'title'; + }, + afterTitle: function() { + return 'afterTitle'; + }, + beforeBody: function() { + return 'beforeBody'; + }, + beforeLabel: function() { + return 'beforeLabel'; + }, + label: function() { + return 'label'; + }, + afterLabel: function() { + return 'afterLabel'; + }, + afterBody: function() { + return 'afterBody'; + }, + beforeFooter: function() { + return 'beforeFooter'; + }, + footer: function() { + return 'footer'; + }, + afterFooter: function() { + return 'afterFooter'; + }, + labelTextColor: function() { + return 'labelTextColor'; + }, + labelPointStyle: function() { + return { + pointStyle: 'labelPointStyle', + rotation: 42 + }; + } + } + } + } + } + }); + + // Trigger an event over top of the + var meta = chart.getDatasetMeta(0); + var point = meta.data[1]; + + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + // Check and see if tooltip was displayed + var tooltip = chart.tooltip; + var defaults = Chart.defaults; + + expect(tooltip.options.padding).toEqual(6); + expect(tooltip.xAlign).toEqual('left'); + expect(tooltip.yAlign).toEqual('center'); + + expect(tooltip.options.bodyFont).toEqual(jasmine.objectContaining({ + family: defaults.font.family, + style: defaults.font.style, + size: defaults.font.size, + })); + + expect(tooltip.options).toEqualOptions({ + bodyAlign: 'left', + bodySpacing: 2, + }); + + expect(tooltip.options.titleFont).toEqual(jasmine.objectContaining({ + family: defaults.font.family, + weight: 'bold', + size: defaults.font.size, + })); + + expect(tooltip.options).toEqualOptions({ + titleSpacing: 2, + titleMarginBottom: 6, + }); + + expect(tooltip.options.footerFont).toEqual(jasmine.objectContaining({ + family: defaults.font.family, + weight: 'bold', + size: defaults.font.size, + })); + + expect(tooltip.options).toEqualOptions({ + footerAlign: 'left', + footerSpacing: 2, + footerMarginTop: 6, + }); + + expect(tooltip.options).toEqualOptions({ + // Appearance + caretSize: 5, + caretPadding: 2, + cornerRadius: 6, + backgroundColor: 'rgba(0,0,0,0.8)', + multiKeyBackground: '#fff', + }); + + expect(tooltip).toEqual(jasmine.objectContaining({ + opacity: 1, + + // Text + title: ['beforeTitle', 'title', 'afterTitle'], + beforeBody: ['beforeBody'], + body: [{ + before: ['beforeLabel'], + lines: ['label'], + after: ['afterLabel'] + }, { + before: ['beforeLabel'], + lines: ['label'], + after: ['afterLabel'] + }], + afterBody: ['afterBody'], + footer: ['beforeFooter', 'footer', 'afterFooter'], + labelTextColors: ['labelTextColor', 'labelTextColor'], + labelColors: [{ + borderColor: defaults.borderColor, + backgroundColor: defaults.backgroundColor, + borderWidth: 1, + borderDash: undefined, + borderDashOffset: undefined, + borderRadius: 0, + }, { + borderColor: defaults.borderColor, + backgroundColor: defaults.backgroundColor, + borderWidth: 1, + borderDash: undefined, + borderDashOffset: undefined, + borderRadius: 0, + }], + labelPointStyles: [{ + pointStyle: 'labelPointStyle', + rotation: 42 + }, { + pointStyle: 'labelPointStyle', + rotation: 42 + }] + })); + + expect(tooltip.x).toBeCloseToPixel(267); + expect(tooltip.y).toBeCloseToPixel(58); + }); + + it('Should provide context object to user callbacks', async function() { + const chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [{x: 1, y: 10}, {x: 2, y: 20}, {x: 3, y: 30}] + }] + }, + options: { + scales: { + x: { + type: 'linear' + } + }, + plugins: { + tooltip: { + mode: 'index', + callbacks: { + beforeLabel: function(ctx) { + return ctx.parsed.x + ',' + ctx.parsed.y; + } + } + } + } + } + }); + + // Trigger an event over top of the + const meta = chart.getDatasetMeta(0); + const point = meta.data[1]; + + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + expect(chart.tooltip.body[0].before).toEqual(['2,20']); + }); + + it('Should allow sorting items', async function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + }, + options: { + plugins: { + tooltip: { + mode: 'index', + itemSort: function(a, b) { + return a.datasetIndex > b.datasetIndex ? -1 : 1; + } + } + } + } + }); + + // Trigger an event over top of the + var meta0 = chart.getDatasetMeta(0); + var point0 = meta0.data[1]; + + await jasmine.triggerMouseEvent(chart, 'mousemove', point0); + // Check and see if tooltip was displayed + var tooltip = chart.tooltip; + var defaults = Chart.defaults; + + expect(tooltip).toEqual(jasmine.objectContaining({ + // Positioning + xAlign: 'left', + yAlign: 'center', + + // Text + title: ['Point 2'], + beforeBody: [], + body: [{ + before: [], + lines: ['Dataset 2: 40'], + after: [] + }, { + before: [], + lines: ['Dataset 1: 20'], + after: [] + }], + afterBody: [], + footer: [], + labelColors: [{ + borderColor: defaults.borderColor, + backgroundColor: defaults.backgroundColor, + borderWidth: 1, + borderDash: undefined, + borderDashOffset: undefined, + borderRadius: 0, + }, { + borderColor: defaults.borderColor, + backgroundColor: defaults.backgroundColor, + borderWidth: 1, + borderDash: undefined, + borderDashOffset: undefined, + borderRadius: 0, + }] + })); + + expect(tooltip.x).toBeCloseToPixel(267); + expect(tooltip.y).toBeCloseToPixel(150); + }); + + it('Should allow reversing items', async function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + }, + options: { + plugins: { + tooltip: { + mode: 'index', + reverse: true + } + } + } + }); + + // Trigger an event over top of the + var meta0 = chart.getDatasetMeta(0); + var point0 = meta0.data[1]; + + await jasmine.triggerMouseEvent(chart, 'mousemove', point0); + // Check and see if tooltip was displayed + var tooltip = chart.tooltip; + var defaults = Chart.defaults; + + expect(tooltip).toEqual(jasmine.objectContaining({ + // Positioning + xAlign: 'left', + yAlign: 'center', + + // Text + title: ['Point 2'], + beforeBody: [], + body: [{ + before: [], + lines: ['Dataset 2: 40'], + after: [] + }, { + before: [], + lines: ['Dataset 1: 20'], + after: [] + }], + afterBody: [], + footer: [], + labelColors: [{ + borderColor: defaults.borderColor, + backgroundColor: defaults.backgroundColor, + borderWidth: 1, + borderDash: undefined, + borderDashOffset: undefined, + borderRadius: 0, + }, { + borderColor: defaults.borderColor, + backgroundColor: defaults.backgroundColor, + borderWidth: 1, + borderDash: undefined, + borderDashOffset: undefined, + borderRadius: 0, + }] + })); + + expect(tooltip.x).toBeCloseToPixel(267); + expect(tooltip.y).toBeCloseToPixel(150); + }); + + it('Should follow dataset order', async function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)', + order: 10 + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)', + order: 5 + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + }, + options: { + plugins: { + tooltip: { + mode: 'index' + } + } + } + }); + + // Trigger an event over top of the + var meta0 = chart.getDatasetMeta(0); + var point0 = meta0.data[1]; + + await jasmine.triggerMouseEvent(chart, 'mousemove', point0); + // Check and see if tooltip was displayed + var tooltip = chart.tooltip; + var defaults = Chart.defaults; + + expect(tooltip).toEqual(jasmine.objectContaining({ + // Positioning + xAlign: 'left', + yAlign: 'center', + + // Text + title: ['Point 2'], + beforeBody: [], + body: [{ + before: [], + lines: ['Dataset 2: 40'], + after: [] + }, { + before: [], + lines: ['Dataset 1: 20'], + after: [] + }], + afterBody: [], + footer: [], + labelColors: [{ + borderColor: defaults.borderColor, + backgroundColor: defaults.backgroundColor, + borderWidth: 1, + borderDash: undefined, + borderDashOffset: undefined, + borderRadius: 0, + }, { + borderColor: defaults.borderColor, + backgroundColor: defaults.backgroundColor, + borderWidth: 1, + borderDash: undefined, + borderDashOffset: undefined, + borderRadius: 0, + }] + })); + + expect(tooltip.x).toBeCloseToPixel(267); + expect(tooltip.y).toBeCloseToPixel(150); + }); + + it('should filter items from the tooltip using the callback', async function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)', + tooltipHidden: true + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + }, + options: { + plugins: { + tooltip: { + mode: 'index', + filter: function(tooltipItem, index, tooltipItems, data) { + // For testing purposes remove the first dataset that has a tooltipHidden property + return !data.datasets[tooltipItem.datasetIndex].tooltipHidden; + } + } + } + } + }); + + // Trigger an event over top of the + var meta0 = chart.getDatasetMeta(0); + var point0 = meta0.data[1]; + + await jasmine.triggerMouseEvent(chart, 'mousemove', point0); + // Check and see if tooltip was displayed + var tooltip = chart.tooltip; + var defaults = Chart.defaults; + + expect(tooltip).toEqual(jasmine.objectContaining({ + // Positioning + xAlign: 'left', + yAlign: 'center', + + // Text + title: ['Point 2'], + beforeBody: [], + body: [{ + before: [], + lines: ['Dataset 2: 40'], + after: [] + }], + afterBody: [], + footer: [], + labelColors: [{ + borderColor: defaults.borderColor, + backgroundColor: defaults.backgroundColor, + borderWidth: 1, + borderDash: undefined, + borderDashOffset: undefined, + borderRadius: 0, + }] + })); + }); + + it('should set the caretPadding based on a config setting', async function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)', + tooltipHidden: true + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + }, + options: { + plugins: { + tooltip: { + caretPadding: 10 + } + } + } + }); + + // Trigger an event over top of the + var meta0 = chart.getDatasetMeta(0); + var point0 = meta0.data[1]; + + await jasmine.triggerMouseEvent(chart, 'mousemove', point0); + // Check and see if tooltip was displayed + var tooltip = chart.tooltip; + + expect(tooltip.options).toEqualOptions({ + // Positioning + caretPadding: 10, + }); + }); + + ['line', 'bar'].forEach(function(type) { + it('Should have dataPoints in a ' + type + ' chart', async function() { + var chart = window.acquireChart({ + type: type, + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + }, + options: { + plugins: { + tooltip: { + mode: 'nearest', + intersect: true + } + } + } + }); + + // Trigger an event over top of the element + var pointIndex = 1; + var datasetIndex = 0; + var point = chart.getDatasetMeta(datasetIndex).data[pointIndex]; + + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + // Check and see if tooltip was displayed + var tooltip = chart.tooltip; + + expect(tooltip instanceof Object).toBe(true); + expect(tooltip.dataPoints instanceof Array).toBe(true); + expect(tooltip.dataPoints.length).toBe(1); + + var tooltipItem = tooltip.dataPoints[0]; + + expect(tooltipItem.dataIndex).toBe(pointIndex); + expect(tooltipItem.datasetIndex).toBe(datasetIndex); + expect(typeof tooltipItem.label).toBe('string'); + expect(tooltipItem.label).toBe(chart.data.labels[pointIndex]); + expect(typeof tooltipItem.formattedValue).toBe('string'); + expect(tooltipItem.formattedValue).toBe('' + chart.data.datasets[datasetIndex].data[pointIndex]); + }); + }); + + it('Should not update if active element has not changed', async function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + }, + options: { + plugins: { + tooltip: { + mode: 'nearest', + intersect: true, + callbacks: { + title: function() { + return 'registering callback...'; + } + } + } + } + } + }); + + // Trigger an event over top of the + var meta = chart.getDatasetMeta(0); + var firstPoint = meta.data[1]; + + var tooltip = chart.tooltip; + spyOn(tooltip, 'update').and.callThrough(); + + // First dispatch change event, should update tooltip + await jasmine.triggerMouseEvent(chart, 'mousemove', firstPoint); + expect(tooltip.update).toHaveBeenCalledWith(true, undefined); + + // Reset calls + tooltip.update.calls.reset(); + + // Second dispatch change event (same event), should not update tooltip + await jasmine.triggerMouseEvent(chart, 'mousemove', firstPoint); + expect(tooltip.update).not.toHaveBeenCalled(); + }); + + it('Should update if active elements are the same, but the position has changed', async function() { + const chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + }, + options: { + scales: { + x: { + stacked: true, + }, + y: { + stacked: true + } + }, + plugins: { + tooltip: { + mode: 'nearest', + position: 'nearest', + intersect: true, + callbacks: { + title: function() { + return 'registering callback...'; + } + } + } + } + } + }); + + // Trigger an event over top of the + const meta = chart.getDatasetMeta(0); + const firstPoint = meta.data[1]; + + const meta2 = chart.getDatasetMeta(1); + const secondPoint = meta2.data[1]; + + const tooltip = chart.tooltip; + spyOn(tooltip, 'update'); + + // First dispatch change event, should update tooltip + await jasmine.triggerMouseEvent(chart, 'mousemove', firstPoint); + expect(tooltip.update).toHaveBeenCalledWith(true, undefined); + + // Reset calls + tooltip.update.calls.reset(); + + // Second dispatch change event (same event), should update tooltip + // because position mode is 'nearest' + await jasmine.triggerMouseEvent(chart, 'mousemove', secondPoint); + expect(tooltip.update).toHaveBeenCalledWith(true, undefined); + }); + + describe('positioners', function() { + it('Should call custom positioner with correct parameters and scope', async function() { + + tooltipPlugin.positioners.test = function() { + return {x: 0, y: 0}; + }; + + spyOn(tooltipPlugin.positioners, 'test').and.callThrough(); + + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + }, + options: { + plugins: { + tooltip: { + mode: 'nearest', + position: 'test' + } + } + } + }); + + // Trigger an event over top of the + var pointIndex = 1; + var datasetIndex = 0; + var meta = chart.getDatasetMeta(datasetIndex); + var point = meta.data[pointIndex]; + var fn = tooltipPlugin.positioners.test; + + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + expect(fn.calls.count()).toBe(2); + expect(fn.calls.first().args[0] instanceof Array).toBe(true); + expect(Object.prototype.hasOwnProperty.call(fn.calls.first().args[1], 'x')).toBe(true); + expect(Object.prototype.hasOwnProperty.call(fn.calls.first().args[1], 'y')).toBe(true); + expect(fn.calls.first().object instanceof Tooltip).toBe(true); + }); + + it('Should ignore same x position when calculating average position with index interaction on stacked bar', async function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)', + stack: 'stack1', + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)', + stack: 'stack1', + }, { + label: 'Dataset 3', + data: [90, 100, 110], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + }, + options: { + interaction: { + mode: 'index' + }, + plugins: { + position: 'average', + }, + } + }); + + // Trigger an event over top of the + var pointIndex = 1; + var datasetIndex = 0; + var meta = chart.getDatasetMeta(datasetIndex); + var point = meta.data[pointIndex]; + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + + var tooltipModel = chart.tooltip; + const activeElements = tooltipModel.getActiveElements(); + + const xPositionArray = activeElements.map((element) => element.element.x); + const xPositionArrayAverage = xPositionArray.reduce((a, b) => a + b) / xPositionArray.length; + + const xPositionSet = new Set(xPositionArray); + const xPositionSetAverage = [...xPositionSet].reduce((a, b) => a + b) / xPositionSet.size; + + expect(xPositionArray.length).toBe(3); + expect(xPositionSet.size).toBe(2); + expect(tooltipModel.caretX).not.toBe(xPositionArrayAverage); + expect(tooltipModel.caretX).toBe(xPositionSetAverage); + }); + + it('Should not fail with all hiden data elements on the average positioner', function() { + const averagePositioner = tooltipPlugin.positioners.average; + + // Simulate `hasValue` returns false + expect(() => averagePositioner([{x: 'invalidNumber', y: 'invalidNumber'}])).not.toThrow(); + const result = averagePositioner([{x: 'invalidNumber', y: 'invalidNumber'}]); + expect(result).toBe(false); + }); + }); + + it('Should avoid tooltip truncation in x axis if there is enough space to show tooltip without truncation', async function() { + var chart = window.acquireChart({ + type: 'pie', + data: { + datasets: [{ + data: [ + 50, + 50 + ], + backgroundColor: [ + 'rgb(255, 0, 0)', + 'rgb(0, 255, 0)' + ], + label: 'Dataset 1' + }], + labels: [ + 'Red long tooltip text to avoid unnecessary loop steps', + 'Green long tooltip text to avoid unnecessary loop steps' + ] + }, + options: { + responsive: true, + animation: { + // without this slice center point is calculated wrong + animateRotate: false + }, + plugins: { + tooltip: { + animation: false + } + } + } + }); + + async function testSlice(slice, count) { + var meta = chart.getDatasetMeta(0); + var point = meta.data[slice].getCenterPoint(); + var tooltipPosition = meta.data[slice].tooltipPosition(); + + async function recursive(left) { + chart.config.data.labels[slice] = chart.config.data.labels[slice] + 'XX'; + chart.update(); + + await jasmine.triggerMouseEvent(chart, 'mouseout', point); + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + var tooltip = chart.tooltip; + expect(tooltip.dataPoints.length).toBe(1); + expect(tooltip.x).toBeGreaterThanOrEqual(0); + if (tooltip.width <= chart.width) { + expect(tooltip.x + tooltip.width).toBeLessThanOrEqual(chart.width); + } + expect(tooltip.caretX).toBeCloseToPixel(tooltipPosition.x); + // if tooltip is longer than chart area then all tests done + if (left === 0) { + throw new Error('max iterations reached'); + } + if (tooltip.width < chart.width) { + await recursive(left - 1); + } + } + + await recursive(count); + } + + // Trigger an event over top of the slice + for (var slice = 0; slice < 2; slice++) { + await testSlice(slice, 20); + } + }); + + it('Should split newlines into separate lines in user callbacks', async function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + }, + options: { + plugins: { + tooltip: { + mode: 'index', + callbacks: { + beforeTitle: function() { + return 'beforeTitle\nnewline'; + }, + title: function() { + return 'title\nnewline'; + }, + afterTitle: function() { + return 'afterTitle\nnewline'; + }, + beforeBody: function() { + return 'beforeBody\nnewline'; + }, + beforeLabel: function() { + return 'beforeLabel\nnewline'; + }, + label: function() { + return 'label'; + }, + afterLabel: function() { + return 'afterLabel\nnewline'; + }, + afterBody: function() { + return 'afterBody\nnewline'; + }, + beforeFooter: function() { + return 'beforeFooter\nnewline'; + }, + footer: function() { + return 'footer\nnewline'; + }, + afterFooter: function() { + return 'afterFooter\nnewline'; + }, + labelTextColor: function() { + return 'labelTextColor'; + } + } + } + } + } + }); + + // Trigger an event over top of the + var meta = chart.getDatasetMeta(0); + var point = meta.data[1]; + + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + + // Check and see if tooltip was displayed + var tooltip = chart.tooltip; + var defaults = Chart.defaults; + + expect(tooltip.options.padding).toEqual(6); + expect(tooltip.xAlign).toEqual('center'); + expect(tooltip.yAlign).toEqual('top'); + + expect(tooltip.options.bodyFont).toEqualOptions({ + family: defaults.font.family, + style: defaults.font.style, + size: defaults.font.size, + }); + + expect(tooltip.options).toEqualOptions({ + bodyAlign: 'left', + bodySpacing: 2, + }); + + expect(tooltip.options.titleFont).toEqualOptions({ + family: defaults.font.family, + weight: 'bold', + size: defaults.font.size, + }); + + expect(tooltip.options).toEqualOptions({ + titleAlign: 'left', + titleSpacing: 2, + titleMarginBottom: 6, + }); + + expect(tooltip.options.footerFont).toEqualOptions({ + family: defaults.font.family, + weight: 'bold', + size: defaults.font.size, + }); + + expect(tooltip.options).toEqualOptions({ + footerAlign: 'left', + footerSpacing: 2, + footerMarginTop: 6, + }); + + expect(tooltip.options).toEqualOptions({ + // Appearance + caretSize: 5, + caretPadding: 2, + cornerRadius: 6, + backgroundColor: 'rgba(0,0,0,0.8)', + multiKeyBackground: '#fff', + }); + + expect(tooltip).toEqualOptions({ + opacity: 1, + + // Text + title: ['beforeTitle', 'newline', 'title', 'newline', 'afterTitle', 'newline'], + beforeBody: ['beforeBody', 'newline'], + body: [{ + before: ['beforeLabel', 'newline'], + lines: ['label'], + after: ['afterLabel', 'newline'] + }, { + before: ['beforeLabel', 'newline'], + lines: ['label'], + after: ['afterLabel', 'newline'] + }], + afterBody: ['afterBody', 'newline'], + footer: ['beforeFooter', 'newline', 'footer', 'newline', 'afterFooter', 'newline'], + labelTextColors: ['labelTextColor', 'labelTextColor'], + labelColors: [{ + borderColor: defaults.borderColor, + backgroundColor: defaults.backgroundColor + }, { + borderColor: defaults.borderColor, + backgroundColor: defaults.backgroundColor + }] + }); + }); + + describe('text align', function() { + var defaults = Chart.defaults; + var makeView = function(title, body, footer) { + const model = { + // Positioning + x: 100, + y: 100, + width: 100, + height: 100, + xAlign: 'left', + yAlign: 'top', + + options: { + setContext: () => model.options, + enabled: true, + + padding: 5, + + // Body + bodyFont: { + family: defaults.font.family, + style: defaults.font.style, + size: defaults.font.size, + }, + bodyColor: '#fff', + bodyAlign: body, + bodySpacing: 2, + + // Title + titleFont: { + family: defaults.font.family, + weight: 'bold', + size: defaults.font.size, + }, + titleColor: '#fff', + titleAlign: title, + titleSpacing: 2, + titleMarginBottom: 6, + + // Footer + footerFont: { + family: defaults.font.family, + weight: 'bold', + size: defaults.font.size, + }, + footerColor: '#fff', + footerAlign: footer, + footerSpacing: 2, + footerMarginTop: 6, + + // Appearance + caretSize: 5, + cornerRadius: 6, + caretPadding: 2, + borderColor: '#aaa', + borderWidth: 1, + backgroundColor: 'rgba(0,0,0,0.8)', + multiKeyBackground: '#fff', + displayColors: false + + }, + opacity: 1, + + // Text + title: ['title'], + beforeBody: [], + body: [{ + before: [], + lines: ['label'], + after: [] + }], + afterBody: [], + footer: ['footer'], + labelTextColors: ['#fff'], + labelColors: [{ + borderColor: 'rgb(255, 0, 0)', + backgroundColor: 'rgb(0, 255, 0)' + }, { + borderColor: 'rgb(0, 0, 255)', + backgroundColor: 'rgb(0, 255, 255)' + }] + }; + return model; + }; + var drawBody = [ + {name: 'save', args: []}, + {name: 'setFillStyle', args: ['rgba(0,0,0,0.8)']}, + {name: 'setStrokeStyle', args: ['#aaa']}, + {name: 'setLineWidth', args: [1]}, + {name: 'beginPath', args: []}, + {name: 'moveTo', args: [106, 100]}, + {name: 'lineTo', args: [106, 100]}, + {name: 'lineTo', args: [111, 95]}, + {name: 'lineTo', args: [116, 100]}, + {name: 'lineTo', args: [194, 100]}, + {name: 'quadraticCurveTo', args: [200, 100, 200, 106]}, + {name: 'lineTo', args: [200, 194]}, + {name: 'quadraticCurveTo', args: [200, 200, 194, 200]}, + {name: 'lineTo', args: [106, 200]}, + {name: 'quadraticCurveTo', args: [100, 200, 100, 194]}, + {name: 'lineTo', args: [100, 106]}, + {name: 'quadraticCurveTo', args: [100, 100, 106, 100]}, + {name: 'closePath', args: []}, + {name: 'fill', args: []}, + {name: 'stroke', args: []} + ]; + + var mockContext = window.createMockContext(); + var tooltip = new Tooltip({ + chart: { + getContext: () => ({}), + options: { + plugins: { + tooltip: { + animation: false, + } + } + } + } + }); + + it('Should go left', function() { + mockContext.resetCalls(); + Chart.helpers.merge(tooltip, makeView('left', 'left', 'left')); + tooltip.draw(mockContext); + + expect(mockContext.getCalls()).toEqual(Array.prototype.concat(drawBody, [ + {name: 'setTextAlign', args: ['left']}, + {name: 'setTextBaseline', args: ['middle']}, + {name: 'setFillStyle', args: ['#fff']}, + {name: 'setFont', args: ["normal bold 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"]}, + {name: 'fillText', args: ['title', 105, 112.2]}, + {name: 'setTextAlign', args: ['left']}, + {name: 'setTextBaseline', args: ['middle']}, + {name: 'setFont', args: ["normal 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"]}, + {name: 'setFillStyle', args: ['#fff']}, + {name: 'setFillStyle', args: ['#fff']}, + {name: 'fillText', args: ['label', 105, 132.6]}, + {name: 'setTextAlign', args: ['left']}, + {name: 'setTextBaseline', args: ['middle']}, + {name: 'setFillStyle', args: ['#fff']}, + {name: 'setFont', args: ["normal bold 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"]}, + {name: 'fillText', args: ['footer', 105, 153]}, + {name: 'restore', args: []} + ])); + }); + + it('Should go right', function() { + mockContext.resetCalls(); + Chart.helpers.merge(tooltip, makeView('right', 'right', 'right')); + tooltip.draw(mockContext); + + expect(mockContext.getCalls()).toEqual(Array.prototype.concat(drawBody, [ + {name: 'setTextAlign', args: ['right']}, + {name: 'setTextBaseline', args: ['middle']}, + {name: 'setFillStyle', args: ['#fff']}, + {name: 'setFont', args: ["normal bold 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"]}, + {name: 'fillText', args: ['title', 195, 112.2]}, + {name: 'setTextAlign', args: ['right']}, + {name: 'setTextBaseline', args: ['middle']}, + {name: 'setFont', args: ["normal 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"]}, + {name: 'setFillStyle', args: ['#fff']}, + {name: 'setFillStyle', args: ['#fff']}, + {name: 'fillText', args: ['label', 195, 132.6]}, + {name: 'setTextAlign', args: ['right']}, + {name: 'setTextBaseline', args: ['middle']}, + {name: 'setFillStyle', args: ['#fff']}, + {name: 'setFont', args: ["normal bold 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"]}, + {name: 'fillText', args: ['footer', 195, 153]}, + {name: 'restore', args: []} + ])); + }); + + it('Should center', function() { + mockContext.resetCalls(); + Chart.helpers.merge(tooltip, makeView('center', 'center', 'center')); + tooltip.draw(mockContext); + + expect(mockContext.getCalls()).toEqual(Array.prototype.concat(drawBody, [ + {name: 'setTextAlign', args: ['center']}, + {name: 'setTextBaseline', args: ['middle']}, + {name: 'setFillStyle', args: ['#fff']}, + {name: 'setFont', args: ["normal bold 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"]}, + {name: 'fillText', args: ['title', 150, 112.2]}, + {name: 'setTextAlign', args: ['center']}, + {name: 'setTextBaseline', args: ['middle']}, + {name: 'setFont', args: ["normal 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"]}, + {name: 'setFillStyle', args: ['#fff']}, + {name: 'setFillStyle', args: ['#fff']}, + {name: 'fillText', args: ['label', 150, 132.6]}, + {name: 'setTextAlign', args: ['center']}, + {name: 'setTextBaseline', args: ['middle']}, + {name: 'setFillStyle', args: ['#fff']}, + {name: 'setFont', args: ["normal bold 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"]}, + {name: 'fillText', args: ['footer', 150, 153]}, + {name: 'restore', args: []} + ])); + }); + + it('Should allow mixed', function() { + mockContext.resetCalls(); + Chart.helpers.merge(tooltip, makeView('right', 'center', 'left')); + tooltip.draw(mockContext); + + expect(mockContext.getCalls()).toEqual(Array.prototype.concat(drawBody, [ + {name: 'setTextAlign', args: ['right']}, + {name: 'setTextBaseline', args: ['middle']}, + {name: 'setFillStyle', args: ['#fff']}, + {name: 'setFont', args: ["normal bold 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"]}, + {name: 'fillText', args: ['title', 195, 112.2]}, + {name: 'setTextAlign', args: ['center']}, + {name: 'setTextBaseline', args: ['middle']}, + {name: 'setFont', args: ["normal 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"]}, + {name: 'setFillStyle', args: ['#fff']}, + {name: 'setFillStyle', args: ['#fff']}, + {name: 'fillText', args: ['label', 150, 132.6]}, + {name: 'setTextAlign', args: ['left']}, + {name: 'setTextBaseline', args: ['middle']}, + {name: 'setFillStyle', args: ['#fff']}, + {name: 'setFont', args: ["normal bold 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"]}, + {name: 'fillText', args: ['footer', 105, 153]}, + {name: 'restore', args: []} + ])); + }); + }); + + describe('active elements', function() { + it('should set the active elements', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + }, + }); + + const meta = chart.getDatasetMeta(0); + chart.tooltip.setActiveElements([{datasetIndex: 0, index: 0}], {x: 0, y: 0}); + expect(chart.tooltip.getActiveElements()[0].element).toBe(meta.data[0]); + }); + + it('should not replace the user set active elements by event replay', async function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + }, + options: { + events: ['pointerdown', 'pointerup'] + } + }); + + const meta = chart.getDatasetMeta(0); + const point0 = meta.data[0]; + const point1 = meta.data[1]; + + await jasmine.triggerMouseEvent(chart, 'pointerdown', {x: point0.x, y: point0.y}); + expect(chart.tooltip.opacity).toBe(1); + expect(chart.tooltip.getActiveElements()).toEqual([{datasetIndex: 0, index: 0, element: point0}]); + + chart.tooltip.setActiveElements([{datasetIndex: 0, index: 1}]); + chart.update(); + expect(chart.tooltip.opacity).toBe(1); + expect(chart.tooltip.getActiveElements()).toEqual([{datasetIndex: 0, index: 1, element: point1}]); + + chart.tooltip.setActiveElements([]); + chart.update(); + expect(chart.tooltip.opacity).toBe(0); + expect(chart.tooltip.getActiveElements().length).toBe(0); + }); + + it('should not change the active elements on events outside chartArea, except for mouseout', async function() { + var chart = acquireChart({ + type: 'line', + data: { + labels: ['A', 'B', 'C', 'D'], + datasets: [{ + data: [10, 20, 30, 100] + }], + }, + options: { + scales: { + x: {display: false}, + y: {display: false} + }, + layout: { + padding: 5 + } + } + }); + + var point = chart.getDatasetMeta(0).data[0]; + + await jasmine.triggerMouseEvent(chart, 'mousemove', {x: point.x, y: point.y}); + expect(chart.tooltip.getActiveElements()).toEqual([{datasetIndex: 0, index: 0, element: point}]); + + await jasmine.triggerMouseEvent(chart, 'mousemove', {x: 1, y: 1}); + expect(chart.tooltip.getActiveElements()).toEqual([{datasetIndex: 0, index: 0, element: point}]); + + await jasmine.triggerMouseEvent(chart, 'mouseout', {x: 1, y: 1}); + expect(chart.tooltip.getActiveElements()).toEqual([]); + }); + + it('should update active elements when datasets are removed and added', async function() { + var dataset = { + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }; + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [dataset], + labels: ['Point 1', 'Point 2', 'Point 3'] + }, + options: { + plugins: { + tooltip: { + mode: 'nearest', + intersect: true + } + } + } + }); + + var meta = chart.getDatasetMeta(0); + var point = meta.data[1]; + var expectedPoint = jasmine.objectContaining({datasetIndex: 0, index: 1}); + + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + + expect(chart.tooltip.getActiveElements()).toEqual([expectedPoint]); + + chart.data.datasets = []; + chart.update(); + + expect(chart.tooltip.getActiveElements()).toEqual([]); + + chart.data.datasets = [dataset]; + chart.update(); + + expect(chart.tooltip.getActiveElements()).toEqual([expectedPoint]); + }); + }); + + it('should tolerate datasets removed on events outside chartArea', async function() { + const dataset1 = { + label: 'Dataset 1', + data: [10, 20, 30], + }; + const dataset2 = { + label: 'Dataset 2', + data: [10, 25, 35], + }; + const chart = window.acquireChart({ + type: 'line', + data: { + datasets: [dataset1, dataset2], + labels: ['Point 1', 'Point 2', 'Point 3'] + }, + options: { + plugins: { + tooltip: { + mode: 'index', + intersect: false + } + } + } + }); + + const meta = chart.getDatasetMeta(0); + const point = meta.data[1]; + const expectedPoints = [jasmine.objectContaining({datasetIndex: 0, index: 1}), jasmine.objectContaining({datasetIndex: 1, index: 1})]; + + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + await jasmine.triggerMouseEvent(chart, 'mousemove', {x: chart.chartArea.left - 5, y: point.y}); + + expect(chart.tooltip.getActiveElements()).toEqual(expectedPoints); + + chart.data.datasets = [dataset1]; + chart.update(); + + await jasmine.triggerMouseEvent(chart, 'mousemove', {x: 2, y: 1}); + + expect(chart.tooltip.getActiveElements()).toEqual([expectedPoints[0]]); + }); + + it('should tolerate elements removed on events outside chartArea', async function() { + const dataset1 = { + label: 'Dataset 1', + data: [10, 20, 30], + }; + const dataset2 = { + label: 'Dataset 2', + data: [10, 25, 35], + }; + const chart = window.acquireChart({ + type: 'line', + data: { + datasets: [dataset1, dataset2], + labels: ['Point 1', 'Point 2', 'Point 3'] + }, + options: { + plugins: { + tooltip: { + mode: 'index', + intersect: false + } + } + } + }); + + const meta = chart.getDatasetMeta(0); + const point = meta.data[1]; + const expectedPoints = [jasmine.objectContaining({datasetIndex: 0, index: 1}), jasmine.objectContaining({datasetIndex: 1, index: 1})]; + + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + await jasmine.triggerMouseEvent(chart, 'mousemove', {x: chart.chartArea.left - 5, y: point.y}); + + expect(chart.tooltip.getActiveElements()).toEqual(expectedPoints); + + dataset1.data = dataset1.data.slice(0, 1); + chart.data.datasets = [dataset1]; + chart.update(); + + await jasmine.triggerMouseEvent(chart, 'mousemove', {x: 2, y: 1}); + + expect(chart.tooltip.getActiveElements()).toEqual([]); + }); + + describe('events', function() { + it('should not be called on events not in plugin events array', async function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + }, + options: { + plugins: { + tooltip: { + events: ['click'] + } + } + } + }); + + const meta = chart.getDatasetMeta(0); + const point = meta.data[1]; + + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + expect(chart.tooltip.opacity).toEqual(0); + await jasmine.triggerMouseEvent(chart, 'click', point); + expect(chart.tooltip.opacity).toEqual(1); + }); + }); + + it('should use default callback if user callback returns undefined', async() => { + const chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + label: 'Dataset 1', + data: [10, 20, 30], + pointHoverBorderColor: 'rgb(255, 0, 0)', + pointHoverBackgroundColor: 'rgb(0, 255, 0)' + }, { + label: 'Dataset 2', + data: [40, 40, 40], + pointHoverBorderColor: 'rgb(0, 0, 255)', + pointHoverBackgroundColor: 'rgb(0, 255, 255)' + }], + labels: ['Point 1', 'Point 2', 'Point 3'] + }, + options: { + plugins: { + tooltip: { + callbacks: { + beforeTitle() { + return undefined; + }, + title() { + return undefined; + }, + afterTitle() { + return undefined; + }, + beforeBody() { + return undefined; + }, + beforeLabel() { + return undefined; + }, + label() { + return undefined; + }, + afterLabel() { + return undefined; + }, + afterBody() { + return undefined; + }, + beforeFooter() { + return undefined; + }, + footer() { + return undefined; + }, + afterFooter() { + return undefined; + }, + labelTextColor() { + return undefined; + }, + labelPointStyle() { + return undefined; + } + } + } + } + } + }); + const {defaults} = Chart; + const {tooltip} = chart; + const point = chart.getDatasetMeta(0).data[0]; + + await jasmine.triggerMouseEvent(chart, 'mousemove', point); + + expect(tooltip).toEqual(jasmine.objectContaining({ + opacity: 1, + + // Text + title: ['Point 1'], + beforeBody: [], + body: [{ + before: [], + lines: ['Dataset 1: 10'], + after: [] + }], + afterBody: [], + footer: [], + labelTextColors: ['#fff'], + labelColors: [{ + borderColor: defaults.borderColor, + backgroundColor: defaults.backgroundColor, + borderWidth: 1, + borderDash: undefined, + borderDashOffset: undefined, + borderRadius: 0, + }], + labelPointStyles: [{ + pointStyle: 'circle', + rotation: 0 + }] + })); + }); +}); diff --git a/test/specs/scale.category.tests.js b/test/specs/scale.category.tests.js new file mode 100644 index 00000000000..70b0bb9a383 --- /dev/null +++ b/test/specs/scale.category.tests.js @@ -0,0 +1,571 @@ +function getLabels(scale) { + return scale.ticks.map(t => t.label); +} + +describe('Category scale tests', function() { + describe('auto', jasmine.fixture.specs('scale.category')); + + it('Should register the constructor with the registry', function() { + var Constructor = Chart.registry.getScale('category'); + expect(Constructor).not.toBe(undefined); + expect(typeof Constructor).toBe('function'); + }); + + it('Should have the correct default config', function() { + var defaultConfig = Chart.defaults.scales.category; + expect(defaultConfig).toEqual({ + ticks: { + callback: Chart.registry.getScale('category').defaults.ticks.callback + } + }); + }); + + it('Should generate ticks from the data xLabels', function() { + var labels = ['tick1', 'tick2', 'tick3', 'tick4', 'tick5']; + var chart = window.acquireChart({ + type: 'line', + data: { + xLabels: labels, + datasets: [{ + data: [10, 5, 0, 25, 78] + }] + }, + options: { + scales: { + x: { + type: 'category', + } + } + } + }); + + var scale = chart.scales.x; + expect(getLabels(scale)).toEqual(labels); + }); + + it('Should generate ticks from the data yLabels', function() { + var labels = ['tick1', 'tick2', 'tick3', 'tick4', 'tick5']; + var chart = window.acquireChart({ + type: 'line', + data: { + yLabels: labels, + datasets: [{ + data: [10, 5, 0, 25, 78] + }] + }, + options: { + scales: { + y: { + type: 'category' + } + } + } + }); + + var scale = chart.scales.y; + expect(getLabels(scale)).toEqual(labels); + }); + + it('Should generate ticks from the axis labels', function() { + var labels = ['tick1', 'tick2', 'tick3', 'tick4', 'tick5']; + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [10, 5, 0, 25, 78] + }] + }, + options: { + scales: { + x: { + type: 'category', + labels: labels + } + } + } + }); + + var scale = chart.scales.x; + expect(getLabels(scale)).toEqual(labels); + }); + + it('Should generate missing labels', function() { + var labels = ['a', 'b', 'c', 'd']; + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: {a: 1, b: 3, c: -1, d: 10} + }] + }, + options: { + scales: { + x: { + type: 'category', + labels: ['a'] + } + } + } + }); + + var scale = chart.scales.x; + expect(getLabels(scale)).toEqual(labels); + + }); + + it('should parse only to a valid index', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'x', + yAxisID: 'y', + data: [10, 5, 0, 25, 78] + }], + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5'] + }, + options: { + scales: { + x: { + type: 'category', + position: 'bottom' + }, + y: { + type: 'linear' + } + } + } + }); + + var scale = chart.scales.x; + + expect(scale.parse(-10)).toEqual(0); + expect(scale.parse(-0.1)).toEqual(0); + expect(scale.parse(4.1)).toEqual(4); + expect(scale.parse(5)).toEqual(4); + expect(scale.parse(1)).toEqual(1); + expect(scale.parse(1.4)).toEqual(1); + expect(scale.parse(1.5)).toEqual(2); + expect(scale.parse('tick2')).toEqual(1); + }); + + it('should get the correct label for the index', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'x', + yAxisID: 'y', + data: [10, 5, 0, 25, 78] + }], + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5'] + }, + options: { + scales: { + x: { + type: 'category', + position: 'bottom' + }, + y: { + type: 'linear' + } + } + } + }); + + var scale = chart.scales.x; + + expect(scale.getLabelForValue(1)).toBe('tick2'); + }); + + it('Should get the correct pixel for a value when horizontal', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'x', + yAxisID: 'y', + data: [10, 5, 0, 25, 78] + }], + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick_last'] + }, + options: { + scales: { + x: { + type: 'category', + position: 'bottom' + }, + y: { + type: 'linear' + } + } + } + }); + + var xScale = chart.scales.x; + expect(xScale.getPixelForValue(0)).toBeCloseToPixel(23 + 6); // plus lineHeight + expect(xScale.getValueForPixel(23)).toBe(0); + + expect(xScale.getPixelForValue(4)).toBeCloseToPixel(487); + expect(xScale.getValueForPixel(487)).toBe(4); + + xScale.options.offset = true; + chart.update(); + + expect(xScale.getPixelForValue(0)).toBeCloseToPixel(71 + 6); // plus lineHeight + expect(xScale.getValueForPixel(69)).toBe(0); + + expect(xScale.getPixelForValue(4)).toBeCloseToPixel(461); + expect(xScale.getValueForPixel(417)).toBe(4); + }); + + it('Should get the correct pixel for a value when there are repeated labels', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'x', + yAxisID: 'y', + data: [10, 5, 0, 25, 78] + }], + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick_last'] + }, + options: { + scales: { + x: { + type: 'category', + position: 'bottom' + }, + y: { + type: 'linear' + } + } + } + }); + + var xScale = chart.scales.x; + expect(xScale.getPixelForValue('tick1')).toBeCloseToPixel(23 + 6); // plus lineHeight + }); + + it('Should get the correct pixel for a value when horizontal and zoomed', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'x', + yAxisID: 'y', + data: [10, 5, 0, 25, 78] + }], + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick_last'] + }, + options: { + scales: { + x: { + type: 'category', + position: 'bottom', + min: 'tick2', + max: 'tick4' + }, + y: { + type: 'linear' + } + } + } + }); + + var xScale = chart.scales.x; + expect(xScale.getPixelForValue(1)).toBeCloseToPixel(23 + 6); // plus lineHeight + expect(xScale.getPixelForValue(3)).toBeCloseToPixel(496); + + xScale.options.offset = true; + chart.update(); + + expect(xScale.getPixelForValue(1)).toBeCloseToPixel(103 + 6); // plus lineHeight + expect(xScale.getPixelForValue(3)).toBeCloseToPixel(429); + }); + + it('should get the correct pixel for a value when vertical', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'x', + yAxisID: 'y', + data: ['3', '5', '1', '4', '2'] + }], + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5'], + yLabels: ['1', '2', '3', '4', '5'] + }, + options: { + scales: { + x: { + type: 'category', + position: 'bottom', + }, + y: { + type: 'category', + position: 'left' + } + } + } + }); + + var yScale = chart.scales.y; + expect(yScale.getPixelForValue(0)).toBeCloseToPixel(32); + expect(yScale.getValueForPixel(257)).toBe(2); + + expect(yScale.getPixelForValue(4)).toBeCloseToPixel(484); + expect(yScale.getValueForPixel(144)).toBe(1); + + yScale.options.offset = true; + chart.update(); + + expect(yScale.getPixelForValue(0)).toBeCloseToPixel(77); + expect(yScale.getValueForPixel(256)).toBe(2); + + expect(yScale.getPixelForValue(4)).toBeCloseToPixel(438); + expect(yScale.getValueForPixel(167)).toBe(1); + }); + + it('should get the correct pixel for a value when vertical and zoomed', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'x', + yAxisID: 'y', + data: ['3', '5', '1', '4', '2'] + }], + labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5'], + yLabels: ['1', '2', '3', '4', '5'] + }, + options: { + scales: { + x: { + type: 'category', + position: 'bottom', + }, + y: { + type: 'category', + position: 'left', + min: '2', + max: '4' + } + } + } + }); + + var yScale = chart.scales.y; + + expect(yScale.getPixelForValue(1)).toBeCloseToPixel(32); + expect(yScale.getPixelForValue(3)).toBeCloseToPixel(482); + + yScale.options.offset = true; + chart.update(); + + expect(yScale.getPixelForValue(1)).toBeCloseToPixel(107); + expect(yScale.getPixelForValue(3)).toBeCloseToPixel(407); + }); + + it('Should get the correct pixel for an object value when horizontal', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'x', + yAxisID: 'y', + data: [ + {x: 0, y: 10}, + {x: 1, y: 5}, + {x: 2, y: 0}, + {x: 3, y: 25}, + {x: 0, y: 78} + ] + }], + labels: [0, 1, 2, 3] + }, + options: { + scales: { + x: { + type: 'category', + position: 'bottom' + }, + y: { + type: 'linear' + } + } + } + }); + + var xScale = chart.scales.x; + expect(xScale.getPixelForValue(0)).toBeCloseToPixel(29); + expect(xScale.getPixelForValue(3)).toBeCloseToPixel(506); + expect(xScale.getPixelForValue(4)).toBeCloseToPixel(664); + }); + + it('Should get the correct pixel for an object value when vertical', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'x', + yAxisID: 'y', + data: [ + {x: 0, y: 2}, + {x: 1, y: 4}, + {x: 2, y: 0}, + {x: 3, y: 3}, + {x: 0, y: 1} + ] + }], + labels: [0, 1, 2, 3], + yLabels: [0, 1, 2, 3, 4] + }, + options: { + scales: { + x: { + type: 'category', + position: 'bottom' + }, + y: { + type: 'category', + position: 'left' + } + } + } + }); + + var yScale = chart.scales.y; + expect(yScale.getPixelForValue(0)).toBeCloseToPixel(32); + expect(yScale.getPixelForValue(4)).toBeCloseToPixel(483); + }); + + it('Should get the correct pixel for an object value in a bar chart', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + xAxisID: 'x', + yAxisID: 'y', + data: [ + {x: 0, y: 10}, + {x: 1, y: 5}, + {x: 2, y: 0}, + {x: 3, y: 25}, + {x: 0, y: 78} + ] + }], + labels: [0, 1, 2, 3] + }, + options: { + scales: { + x: { + type: 'category', + position: 'bottom' + }, + y: { + type: 'linear' + } + } + } + }); + + var xScale = chart.scales.x; + expect(xScale.getPixelForValue(0)).toBeCloseToPixel(89); + expect(xScale.getPixelForValue(3)).toBeCloseToPixel(451); + expect(xScale.getPixelForValue(4)).toBeCloseToPixel(572); + }); + + it('Should get the correct pixel for an object value in a horizontal bar chart', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [ + {x: 10, y: 0}, + {x: 5, y: 1}, + {x: 0, y: 2}, + {x: 25, y: 3}, + {x: 78, y: 0} + ] + }], + labels: [0, 1, 2, 3] + }, + options: { + indexAxis: 'y', + scales: { + x: { + type: 'linear', + position: 'bottom' + }, + y: { + type: 'category' + } + } + } + }); + + var yScale = chart.scales.y; + expect(yScale.getPixelForValue(0)).toBeCloseToPixel(88); + expect(yScale.getPixelForValue(3)).toBeCloseToPixel(426); + expect(yScale.getPixelForValue(4)).toBeCloseToPixel(538); + }); + + it('Should be consistent on pixels and values with autoSkipped ticks', function() { + var labels = []; + for (let i = 0; i < 50; i++) { + labels.push('very long label ' + i); + } + var chart = window.acquireChart({ + type: 'bar', + data: { + labels, + datasets: [{ + data: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + }] + } + }); + + var scale = chart.scales.x; + expect(scale.ticks.length).toBeLessThan(50); + + let x = 0; + for (let i = 0; i < 50; i++) { + var x2 = scale.getPixelForValue(labels[i]); + var x3 = scale.getPixelForValue(i); + expect(x2).toEqual(x3); + expect(x2).toBeGreaterThan(x); + expect(scale.getValueForPixel(x2)).toBe(i); + x = x2; + } + }); + + it('Should bound to ticks/data', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + labels: ['a', 'b', 'c', 'd'], + datasets: [{ + data: {b: 1, c: 99} + }] + }, + options: { + scales: { + x: { + type: 'category', + bounds: 'data' + } + } + } + }); + + expect(chart.scales.x.min).toEqual(1); + expect(chart.scales.x.max).toEqual(2); + + chart.options.scales.x.bounds = 'ticks'; + chart.update(); + + expect(chart.scales.x.min).toEqual(0); + expect(chart.scales.x.max).toEqual(3); + }); +}); diff --git a/test/specs/scale.linear.tests.js b/test/specs/scale.linear.tests.js new file mode 100644 index 00000000000..a8ad53995b1 --- /dev/null +++ b/test/specs/scale.linear.tests.js @@ -0,0 +1,1398 @@ +function getLabels(scale) { + return scale.ticks.map(t => t.label); +} + +describe('Linear Scale', function() { + describe('auto', jasmine.fixture.specs('scale.linear')); + + it('Should register the constructor with the registry', function() { + var Constructor = Chart.registry.getScale('linear'); + expect(Constructor).not.toBe(undefined); + expect(typeof Constructor).toBe('function'); + }); + + it('Should have the correct default config', function() { + var defaultConfig = Chart.defaults.scales.linear; + + expect(defaultConfig.ticks.callback).toEqual(jasmine.any(Function)); + }); + + it('Should correctly determine the max & min data values', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + yAxisID: 'y', + data: [10, 5, 0, -5, 78, -100] + }, { + yAxisID: 'y2', + data: [-1000, 1000], + }, { + yAxisID: 'y', + data: [150] + }], + labels: ['a', 'b', 'c', 'd', 'e', 'f'] + }, + options: { + scales: { + y: { + type: 'linear' + }, + y2: { + type: 'linear', + position: 'right', + } + } + } + }); + + expect(chart.scales.y).not.toEqual(undefined); // must construct + expect(chart.scales.y.min).toBe(-100); + expect(chart.scales.y.max).toBe(150); + }); + + it('Should handle when only a min value is provided', () => { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + yAxisID: 'y', + data: [200] + }], + }, + options: { + scales: { + y: { + type: 'linear', + min: 250 + } + } + } + }); + + expect(chart.scales.y.min).toBe(250); + }); + + it('Should handle when only a max value is provided', () => { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + yAxisID: 'y', + data: [200] + }], + }, + options: { + scales: { + y: { + type: 'linear', + max: 150 + } + } + } + }); + + expect(chart.scales.y).not.toEqual(undefined); // must construct + expect(chart.scales.y.max).toBe(150); + }); + + it('Should correctly determine the max & min of string data values', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + yAxisID: 'y', + data: ['10', '5', '0', '-5', '78', '-100'] + }, { + yAxisID: 'y2', + data: ['-1000', '1000'], + }, { + yAxisID: 'y', + data: ['150'] + }], + labels: ['a', 'b', 'c', 'd', 'e', 'f'] + }, + options: { + scales: { + y: { + type: 'linear' + }, + y2: { + type: 'linear', + position: 'right' + } + } + } + }); + + expect(chart.scales.y).not.toEqual(undefined); // must construct + expect(chart.scales.y.min).toBe(-100); + expect(chart.scales.y.max).toBe(150); + }); + + it('Should correctly determine the max & min when no values provided and suggested minimum and maximum are set', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + yAxisID: 'y', + data: [] + }], + labels: ['a', 'b', 'c', 'd', 'e', 'f'] + }, + options: { + scales: { + y: { + type: 'linear', + suggestedMin: -10, + suggestedMax: 15 + } + } + } + }); + + expect(chart.scales.y).not.toEqual(undefined); // must construct + expect(chart.scales.y.min).toBe(-10); + expect(chart.scales.y.max).toBe(15); + }); + + it('Should correctly determine the max & min when no datasets are associated and suggested minimum and maximum are set', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [] + }, + options: { + scales: { + y: { + type: 'linear', + suggestedMin: -10, + suggestedMax: 0 + } + } + } + }); + + expect(chart.scales.y.min).toBe(-10); + expect(chart.scales.y.max).toBe(0); + }); + + it('Should correctly determine the max & min data values ignoring hidden datasets', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + yAxisID: 'y', + data: ['10', '5', '0', '-5', '78', '-100'] + }, { + yAxisID: 'y2', + data: ['-1000', '1000'], + }, { + yAxisID: 'y', + data: ['150'], + hidden: true + }], + labels: ['a', 'b', 'c', 'd', 'e', 'f'] + }, + options: { + scales: { + y: { + type: 'linear' + }, + y2: { + position: 'right', + type: 'linear' + } + } + } + }); + + expect(chart.scales.y).not.toEqual(undefined); // must construct + expect(chart.scales.y.min).toBe(-100); + expect(chart.scales.y.max).toBe(80); + }); + + it('Should correctly determine the max & min data values ignoring data that is NaN', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + yAxisID: 'y', + data: [null, 90, NaN, undefined, 45, 30, Infinity, -Infinity] + }], + labels: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'] + }, + options: { + scales: { + y: { + type: 'linear', + beginAtZero: false + } + } + } + }); + + expect(chart.scales.y.min).toBe(30); + expect(chart.scales.y.max).toBe(90); + + // Scale is now stacked + chart.scales.y.options.stacked = true; + chart.update(); + + expect(chart.scales.y.min).toBe(30); + expect(chart.scales.y.max).toBe(90); + + chart.scales.y.options.beginAtZero = true; + chart.update(); + + expect(chart.scales.y.min).toBe(0); + expect(chart.scales.y.max).toBe(90); + }); + + it('Should correctly determine the max & min data values for small numbers', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + yAxisID: 'y', + data: [-1e-8, 3e-8, -4e-8, 6e-8] + }], + labels: ['a', 'b', 'c', 'd'] + }, + options: { + scales: { + y: { + type: 'linear' + } + } + } + }); + + expect(chart.scales.y).not.toEqual(undefined); // must construct + expect(chart.scales.y.min * 1e8).toBeCloseTo(-4); + expect(chart.scales.y.max * 1e8).toBeCloseTo(6); + }); + + it('Should correctly determine the max & min for scatter data', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'x', + yAxisID: 'y', + data: [{ + x: 10, + y: 100 + }, { + x: -10, + y: 0 + }, { + x: 0, + y: 0 + }, { + x: 99, + y: 7 + }] + }], + }, + options: { + scales: { + x: { + type: 'linear', + position: 'bottom' + }, + y: { + type: 'linear' + } + } + } + }); + chart.update(); + + expect(chart.scales.x.min).toBe(-20); + expect(chart.scales.x.max).toBe(100); + expect(chart.scales.y.min).toBe(0); + expect(chart.scales.y.max).toBe(100); + }); + + it('Should correctly get the label for the given index', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'x', + yAxisID: 'y', + data: [{ + x: 10, + y: 100 + }, { + x: -10, + y: 0 + }, { + x: 0, + y: 0 + }, { + x: 99, + y: 7 + }] + }], + }, + options: { + scales: { + x: { + type: 'linear', + position: 'bottom' + }, + y: { + type: 'linear' + } + } + } + }); + chart.update(); + + expect(chart.scales.y.getLabelForValue(7)).toBe('7'); + }); + + it('Should correctly use the locale setting when getting a label', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'x', + yAxisID: 'y', + data: [{ + x: 10, + y: 100 + }, { + x: -10, + y: 0 + }, { + x: 0, + y: 0 + }, { + x: 99, + y: 7 + }] + }], + }, + options: { + locale: 'de-DE', + scales: { + x: { + type: 'linear', + position: 'bottom' + }, + y: { + type: 'linear' + } + } + } + }); + chart.update(); + + expect(chart.scales.y.getLabelForValue(7.07)).toBe('7,07'); + }); + + it('Should correctly determine the min and max data values when stacked mode is turned on', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + yAxisID: 'y', + data: [10, 5, 0, -5, 78, -100], + type: 'bar' + }, { + yAxisID: 'y2', + data: [-1000, 1000], + }, { + yAxisID: 'y', + data: [150, 0, 0, -100, -10, 9], + type: 'bar' + }, { + yAxisID: 'y', + data: [10, 10, 10, 10, 10, 10], + type: 'line' + }], + labels: ['a', 'b', 'c', 'd', 'e', 'f'] + }, + options: { + scales: { + y: { + type: 'linear', + stacked: true + }, + y2: { + position: 'right', + type: 'linear' + } + } + } + }); + chart.update(); + + expect(chart.scales.y.min).toBe(-150); + expect(chart.scales.y.max).toBe(200); + }); + + it('Should correctly determine the min and max data values when stacked mode is turned on and there are hidden datasets', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + yAxisID: 'y', + data: [10, 5, 0, -5, 78, -100], + }, { + yAxisID: 'y2', + data: [-1000, 1000], + }, { + yAxisID: 'y', + data: [150, 0, 0, -100, -10, 9], + }, { + yAxisID: 'y', + data: [10, 20, 30, 40, 50, 60], + hidden: true + }], + labels: ['a', 'b', 'c', 'd', 'e', 'f'] + }, + options: { + scales: { + y: { + type: 'linear', + stacked: true + }, + y2: { + position: 'right', + type: 'linear' + } + } + } + }); + chart.update(); + + expect(chart.scales.y.min).toBe(-150); + expect(chart.scales.y.max).toBe(200); + }); + + it('Should correctly determine the min and max data values when stacked mode is turned on there are multiple types of datasets', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + yAxisID: 'y', + type: 'bar', + data: [10, 5, 0, -5, 78, -100] + }, { + type: 'line', + data: [10, 10, 10, 10, 10, 10], + }, { + type: 'bar', + data: [150, 0, 0, -100, -10, 9] + }], + labels: ['a', 'b', 'c', 'd', 'e', 'f'] + }, + options: { + scales: { + y: { + type: 'linear', + stacked: true + } + } + } + }); + + chart.scales.y.determineDataLimits(); + expect(chart.scales.y.min).toBe(-105); + expect(chart.scales.y.max).toBe(160); + }); + + it('Should ensure that the scale has a max and min that are not equal', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [], + labels: ['a', 'b', 'c', 'd', 'e', 'f'] + }, + options: { + scales: { + y: { + type: 'linear' + } + } + } + }); + + expect(chart.scales.y).not.toEqual(undefined); // must construct + expect(chart.scales.y.min).toBe(0); + expect(chart.scales.y.max).toBe(1); + }); + + it('Should ensure that the scale has a max and min that are not equal - large positive numbers', function() { + // https://github.com/chartjs/Chart.js/issues/9377 + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + // Value larger than Number.MAX_SAFE_INTEGER + data: [10000000000000000] + }], + labels: ['a'] + }, + options: { + scales: { + y: { + type: 'linear' + } + } + } + }); + + expect(chart.scales.y).not.toEqual(undefined); // must construct + expect(chart.scales.y.min).toBe(10000000000000000 * 0.95); + expect(chart.scales.y.max).toBe(10000000000000000 * 1.05); + }); + + it('Should ensure that the scale has a max and min that are not equal - large negative numbers', function() { + // https://github.com/chartjs/Chart.js/issues/9377 + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + // Value larger than Number.MAX_SAFE_INTEGER + data: [-10000000000000000] + }], + labels: ['a'] + }, + options: { + scales: { + y: { + type: 'linear' + } + } + } + }); + + expect(chart.scales.y).not.toEqual(undefined); // must construct + expect(chart.scales.y.max).toBe(-10000000000000000 * 0.95); + expect(chart.scales.y.min).toBe(-10000000000000000 * 1.05); + }); + + it('Should ensure that the scale has a max and min that are not equal when beginAtZero is set', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [], + labels: ['a', 'b', 'c', 'd', 'e', 'f'] + }, + options: { + scales: { + y: { + type: 'linear', + beginAtZero: true + } + } + } + }); + + expect(chart.scales.y).not.toEqual(undefined); // must construct + expect(chart.scales.y.min).toBe(0); + expect(chart.scales.y.max).toBe(1); + }); + + it('Should use the suggestedMin and suggestedMax options', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + yAxisID: 'y', + data: [1, 1, 1, 2, 1, 0] + }], + labels: ['a', 'b', 'c', 'd', 'e', 'f'] + }, + options: { + scales: { + y: { + type: 'linear', + suggestedMax: 10, + suggestedMin: -10 + } + } + } + }); + + expect(chart.scales.y).not.toEqual(undefined); // must construct + expect(chart.scales.y.min).toBe(-10); + expect(chart.scales.y.max).toBe(10); + }); + + it('Should use the min and max options', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + yAxisID: 'y', + data: [1, 1, 1, 2, 1, 0] + }], + labels: ['a', 'b', 'c', 'd', 'e', 'f'] + }, + options: { + scales: { + y: { + type: 'linear', + max: 1010, + min: -1010 + } + } + } + }); + + expect(chart.scales.y).not.toEqual(undefined); // must construct + expect(chart.scales.y.min).toBe(-1010); + expect(chart.scales.y.max).toBe(1010); + var labels = getLabels(chart.scales.y); + expect(labels[0]).toBe('-1,010'); + expect(labels[labels.length - 1]).toBe('1,010'); + }); + + it('Should use min, max and stepSize to create fixed spaced ticks', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + yAxisID: 'y', + data: [10, 3, 6, 8, 3, 1] + }], + labels: ['a', 'b', 'c', 'd', 'e', 'f'] + }, + options: { + scales: { + y: { + type: 'linear', + min: 1, + max: 11, + ticks: { + stepSize: 2 + } + } + } + } + }); + + expect(chart.scales.y).not.toEqual(undefined); // must construct + expect(chart.scales.y.min).toBe(1); + expect(chart.scales.y.max).toBe(11); + expect(getLabels(chart.scales.y)).toEqual(['1', '3', '5', '7', '9', '11']); + }); + + it('Should not generate any ticks > max if max is specified', function() { + var chart = window.acquireChart({ + type: 'line', + options: { + scales: { + x: { + type: 'linear', + min: 2.404e-8, + max: 2.4143e-8, + ticks: { + includeBounds: false, + }, + }, + }, + }, + }); + + expect(chart.scales.x.min).toBe(2.404e-8); + expect(chart.scales.x.max).toBe(2.4143e-8); + expect(chart.scales.x.ticks[chart.scales.x.ticks.length - 1].value).toBeLessThanOrEqual(2.4143e-8); + }); + + it('Should not generate insane amounts of ticks with small stepSize and large range', function() { + var chart = window.acquireChart({ + type: 'bar', + options: { + scales: { + y: { + type: 'linear', + min: 1, + max: 1E10, + ticks: { + stepSize: 2, + autoSkip: false + } + } + } + } + }); + + expect(chart.scales.y.min).toBe(1); + expect(chart.scales.y.max).toBe(1E10); + expect(chart.scales.y.ticks.length).toBeLessThanOrEqual(1000); + }); + + it('Should create decimal steps if stepSize is a decimal number', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + yAxisID: 'y', + data: [10, 3, 6, 8, 3, 1] + }], + labels: ['a', 'b', 'c', 'd', 'e', 'f'] + }, + options: { + scales: { + y: { + type: 'linear', + ticks: { + stepSize: 2.5 + } + } + } + } + }); + + expect(chart.scales.y).not.toEqual(undefined); // must construct + expect(chart.scales.y.min).toBe(0); + expect(chart.scales.y.max).toBe(10); + expect(getLabels(chart.scales.y)).toEqual(['0', '2.5', '5', '7.5', '10']); + }); + + describe('precision', function() { + it('Should create integer steps if precision is 0', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + yAxisID: 'y', + data: [0, 1, 2, 1, 0, 1] + }], + labels: ['a', 'b', 'c', 'd', 'e', 'f'] + }, + options: { + scales: { + y: { + type: 'linear', + ticks: { + precision: 0 + } + } + } + } + }); + + expect(chart.scales.y).not.toEqual(undefined); // must construct + expect(chart.scales.y.min).toBe(0); + expect(chart.scales.y.max).toBe(2); + expect(getLabels(chart.scales.y)).toEqual(['0', '1', '2']); + }); + + it('Should round the step size to the given number of decimal places', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + yAxisID: 'y', + data: [0, 0.001, 0.002, 0.003, 0, 0.001] + }], + labels: ['a', 'b', 'c', 'd', 'e', 'f'] + }, + options: { + scales: { + y: { + type: 'linear', + ticks: { + precision: 2 + } + } + } + } + }); + + expect(chart.scales.y).not.toEqual(undefined); // must construct + expect(chart.scales.y.min).toBe(0); + expect(chart.scales.y.max).toBe(0.01); + expect(getLabels(chart.scales.y)).toEqual(['0', '0.01']); + }); + }); + + + it('should forcibly include 0 in the range if the beginAtZero option is used', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + yAxisID: 'y', + data: [20, 30, 40, 50] + }], + labels: ['a', 'b', 'c', 'd'] + }, + options: { + scales: { + y: { + type: 'linear', + beginAtZero: false + } + } + } + }); + + expect(chart.scales.y).not.toEqual(undefined); // must construct + expect(getLabels(chart.scales.y)).toEqual(['20', '25', '30', '35', '40', '45', '50']); + + chart.scales.y.options.beginAtZero = true; + chart.update(); + expect(getLabels(chart.scales.y)).toEqual(['0', '5', '10', '15', '20', '25', '30', '35', '40', '45', '50']); + + chart.data.datasets[0].data = [-20, -30, -40, -50]; + chart.update(); + expect(getLabels(chart.scales.y)).toEqual(['-50', '-45', '-40', '-35', '-30', '-25', '-20', '-15', '-10', '-5', '0']); + + chart.scales.y.options.beginAtZero = false; + chart.update(); + expect(getLabels(chart.scales.y)).toEqual(['-50', '-45', '-40', '-35', '-30', '-25', '-20']); + }); + + it('Should generate tick marks in the correct order in reversed mode', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + yAxisID: 'y', + data: [10, 5, 0, 25, 78] + }], + labels: ['a', 'b', 'c', 'd'] + }, + options: { + scales: { + y: { + type: 'linear', + reverse: true + } + } + } + }); + + expect(getLabels(chart.scales.y)).toEqual(['80', '70', '60', '50', '40', '30', '20', '10', '0']); + expect(chart.scales.y.start).toBe(80); + expect(chart.scales.y.end).toBe(0); + }); + + it('should use the correct number of decimal places in the default format function', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + yAxisID: 'y', + data: [0.06, 0.005, 0, 0.025, 0.0078] + }], + labels: ['a', 'b', 'c', 'd'] + }, + options: { + scales: { + y: { + type: 'linear', + } + } + } + }); + expect(getLabels(chart.scales.y)).toEqual(['0', '0.01', '0.02', '0.03', '0.04', '0.05', '0.06']); + }); + + it('Should correctly limit the maximum number of ticks', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + labels: ['a', 'b'], + datasets: [{ + data: [0.5, 2.5] + }] + }, + options: { + scales: { + y: { + beginAtZero: false + } + } + } + }); + + expect(getLabels(chart.scales.y)).toEqual(['0.5', '1.0', '1.5', '2.0', '2.5']); + + chart.options.scales.y.ticks.maxTicksLimit = 11; + chart.update(); + + expect(getLabels(chart.scales.y)).toEqual(['0.5', '1.0', '1.5', '2.0', '2.5']); + + chart.options.scales.y.ticks.maxTicksLimit = 21; + chart.update(); + + expect(getLabels(chart.scales.y)).toEqual([ + '0.5', + '0.6', '0.7', '0.8', '0.9', '1.0', '1.1', '1.2', '1.3', '1.4', '1.5', + '1.6', '1.7', '1.8', '1.9', '2.0', '2.1', '2.2', '2.3', '2.4', '2.5' + ]); + + chart.options.scales.y.ticks.maxTicksLimit = 11; + chart.options.scales.y.ticks.stepSize = 0.01; + chart.update(); + + expect(getLabels(chart.scales.y)).toEqual(['0.5', '1.0', '1.5', '2.0', '2.5']); + + chart.options.scales.y.min = 0.3; + chart.options.scales.y.max = 2.8; + chart.update(); + + expect(getLabels(chart.scales.y)).toEqual(['0.3', '0.8', '1.3', '1.8', '2.3', '2.8']); + }); + + it('Should bound to data', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + labels: ['a', 'b'], + datasets: [{ + data: [1, 99] + }] + }, + options: { + scales: { + y: { + bounds: 'data' + } + } + } + }); + + expect(chart.scales.y.min).toEqual(1); + expect(chart.scales.y.max).toEqual(99); + }); + + it('Should build labels using the user supplied callback', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + yAxisID: 'y', + data: [10, 5, 0, 25, 78] + }], + labels: ['a', 'b', 'c', 'd'] + }, + options: { + scales: { + y: { + type: 'linear', + ticks: { + callback: function(value, index) { + return index.toString(); + } + } + } + } + } + }); + + // Just the index + expect(getLabels(chart.scales.y)).toEqual(['0', '1', '2', '3', '4', '5', '6', '7', '8']); + }); + + it('Should get the correct pixel value for a point', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + labels: [-1, 1], + datasets: [{ + xAxisID: 'x', + yAxisID: 'y', + data: [-1, 1] + }], + }, + options: { + scales: { + x: { + type: 'linear', + position: 'bottom' + }, + y: { + type: 'linear' + } + } + } + }); + + var xScale = chart.scales.x; + expect(xScale.getPixelForValue(1)).toBeCloseToPixel(501); // right - paddingRight + expect(xScale.getPixelForValue(-1)).toBeCloseToPixel(31 + 3); // left + paddingLeft + tick padding + expect(xScale.getPixelForValue(0)).toBeCloseToPixel(266 + 3 / 2); // halfway*/ + + expect(xScale.getValueForPixel(501)).toBeCloseTo(1, 1e-2); + expect(xScale.getValueForPixel(31)).toBeCloseTo(-1, 1e-2); + expect(xScale.getValueForPixel(266)).toBeCloseTo(0, 1e-2); + + var yScale = chart.scales.y; + expect(yScale.getPixelForValue(1)).toBeCloseToPixel(32); // right - paddingRight + expect(yScale.getPixelForValue(-1)).toBeCloseToPixel(484); // left + paddingLeft + expect(yScale.getPixelForValue(0)).toBeCloseToPixel(258); // halfway*/ + + expect(yScale.getValueForPixel(32)).toBeCloseTo(1, 1e-2); + expect(yScale.getValueForPixel(484)).toBeCloseTo(-1, 1e-2); + expect(yScale.getValueForPixel(258)).toBeCloseTo(0, 1e-2); + }); + + it('should fit correctly', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'x', + yAxisID: 'y', + data: [{ + x: 10, + y: 100 + }, { + x: -10, + y: 0 + }, { + x: 0, + y: 0 + }, { + x: 99, + y: 7 + }] + }], + }, + options: { + scales: { + x: { + type: 'linear', + position: 'bottom' + }, + y: { + type: 'linear' + } + } + } + }); + + var xScale = chart.scales.x; + var yScale = chart.scales.y; + expect(xScale.paddingTop).toBeCloseToPixel(0); + expect(xScale.paddingBottom).toBeCloseToPixel(0); + expect(xScale.paddingLeft).toBeCloseToPixel(12); + expect(xScale.paddingRight).toBeCloseToPixel(13.5); + expect(xScale.width).toBeCloseToPixel(468 - 3); // minus tick padding + expect(xScale.height).toBeCloseToPixel(30); + + expect(yScale.paddingTop).toBeCloseToPixel(10); + expect(yScale.paddingBottom).toBeCloseToPixel(10); + expect(yScale.paddingLeft).toBeCloseToPixel(0); + expect(yScale.paddingRight).toBeCloseToPixel(0); + expect(yScale.width).toBeCloseToPixel(31 + 3); // plus tick padding + expect(yScale.height).toBeCloseToPixel(450); + + // Extra size when scale label showing + xScale.options.title.display = true; + yScale.options.title.display = true; + chart.update(); + + expect(xScale.paddingTop).toBeCloseToPixel(0); + expect(xScale.paddingBottom).toBeCloseToPixel(0); + expect(xScale.paddingLeft).toBeCloseToPixel(12); + expect(xScale.paddingRight).toBeCloseToPixel(13.5); + expect(xScale.width).toBeCloseToPixel(442); + expect(xScale.height).toBeCloseToPixel(50); + + expect(yScale.paddingTop).toBeCloseToPixel(10); + expect(yScale.paddingBottom).toBeCloseToPixel(10); + expect(yScale.paddingLeft).toBeCloseToPixel(0); + expect(yScale.paddingRight).toBeCloseToPixel(0); + expect(yScale.width).toBeCloseToPixel(58); + expect(yScale.height).toBeCloseToPixel(429); + }); + + it('should fit correctly when display is turned off', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'x', + yAxisID: 'y', + data: [{ + x: 10, + y: 100 + }, { + x: -10, + y: 0 + }, { + x: 0, + y: 0 + }, { + x: 99, + y: 7 + }] + }], + }, + options: { + scales: { + x: { + type: 'linear', + position: 'bottom' + }, + y: { + type: 'linear', + grid: { + drawTicks: false, + }, + border: { + display: false + }, + title: { + display: false, + lineHeight: 1.2 + }, + ticks: { + display: false, + padding: 0 + } + } + } + } + }); + + var yScale = chart.scales.y; + expect(yScale.width).toBeCloseToPixel(0); + }); + + it('max and min value should be valid and finite when charts datasets are hidden', function() { + var barData = { + labels: ['S1', 'S2', 'S3'], + datasets: [{ + label: 'Closed', + backgroundColor: '#382765', + data: [2500, 2000, 1500] + }, { + label: 'In Progress', + backgroundColor: '#7BC225', + data: [1000, 2000, 1500] + }, { + label: 'Assigned', + backgroundColor: '#ffC225', + data: [1000, 2000, 1500] + }] + }; + + var chart = window.acquireChart({ + type: 'bar', + data: barData, + options: { + indexAxis: 'y', + scales: { + x: { + stacked: true + }, + y: { + stacked: true + } + } + } + }); + + barData.datasets.forEach(function(data, index) { + var meta = chart.getDatasetMeta(index); + meta.hidden = true; + chart.update(); + }); + + expect(chart.scales.x.min).toEqual(0); + expect(chart.scales.x.max).toEqual(1); + }); + + it('max and min value should be valid when min is set and all datasets are hidden', function() { + var barData = { + labels: ['S1', 'S2', 'S3'], + datasets: [{ + label: 'dataset 1', + backgroundColor: '#382765', + data: [2500, 2000, 1500], + hidden: true, + }] + }; + + var chart = window.acquireChart({ + type: 'bar', + data: barData, + options: { + indexAxis: 'y', + scales: { + x: { + min: 20 + } + } + } + }); + + expect(chart.scales.x.min).toEqual(20); + expect(chart.scales.x.max).toEqual(21); + }); + + it('min settings should be used if set to zero', function() { + var barData = { + labels: ['S1', 'S2', 'S3'], + datasets: [{ + label: 'dataset 1', + backgroundColor: '#382765', + data: [2500, 2000, 1500] + }] + }; + + var chart = window.acquireChart({ + type: 'bar', + data: barData, + options: { + indexAxis: 'y', + scales: { + x: { + min: 0, + max: 3000 + } + } + } + }); + + expect(chart.scales.x.min).toEqual(0); + }); + + it('max settings should be used if set to zero', function() { + var barData = { + labels: ['S1', 'S2', 'S3'], + datasets: [{ + label: 'dataset 1', + backgroundColor: '#382765', + data: [-2500, -2000, -1500] + }] + }; + + var chart = window.acquireChart({ + type: 'bar', + data: barData, + options: { + indexAxis: 'y', + scales: { + x: { + min: -3000, + max: 0 + } + } + } + }); + + expect(chart.scales.x.max).toEqual(0); + }); + + it('Should get correct pixel values when horizontal', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [0.05, -25, 10, 15, 20, 25, 30, 35] + }] + }, + options: { + indexAxis: 'y', + scales: { + x: { + type: 'linear', + } + } + } + }); + + var start = chart.chartArea.left; + var end = chart.chartArea.right; + var min = -30; + var max = 40; + var scale = chart.scales.x; + + expect(scale.getPixelForValue(max)).toBeCloseToPixel(end); + expect(scale.getPixelForValue(min)).toBeCloseToPixel(start); + expect(scale.getValueForPixel(end)).toBeCloseTo(max, 4); + expect(scale.getValueForPixel(start)).toBeCloseTo(min, 4); + + scale.options.reverse = true; + chart.update(); + + start = chart.chartArea.left; + end = chart.chartArea.right; + + expect(scale.getPixelForValue(max)).toBeCloseToPixel(start); + expect(scale.getPixelForValue(min)).toBeCloseToPixel(end); + expect(scale.getValueForPixel(end)).toBeCloseTo(min, 4); + expect(scale.getValueForPixel(start)).toBeCloseTo(max, 4); + }); + + it('Should get correct pixel values when vertical', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [0.05, -25, 10, 15, 20, 25, 30, 35] + }] + }, + options: { + scales: { + y: { + type: 'linear', + } + } + } + }); + + var start = chart.chartArea.bottom; + var end = chart.chartArea.top; + var min = -30; + var max = 40; + var scale = chart.scales.y; + + expect(scale.getPixelForValue(max)).toBeCloseToPixel(end); + expect(scale.getPixelForValue(min)).toBeCloseToPixel(start); + expect(scale.getValueForPixel(end)).toBeCloseTo(max, 4); + expect(scale.getValueForPixel(start)).toBeCloseTo(min, 4); + + scale.options.reverse = true; + chart.update(); + + start = chart.chartArea.bottom; + end = chart.chartArea.top; + + expect(scale.getPixelForValue(max)).toBeCloseToPixel(start); + expect(scale.getPixelForValue(min)).toBeCloseToPixel(end); + expect(scale.getValueForPixel(end)).toBeCloseTo(min, 4); + expect(scale.getValueForPixel(start)).toBeCloseTo(max, 4); + }); + + it('should not throw errors when chart size is negative', function() { + function createChart() { + return window.acquireChart({ + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5, 6, 7, '7+'], + datasets: [{ + data: [29.05, 4, 15.69, 11.69, 2.84, 4, 0, 3.84, 4], + }], + }, + options: { + plugins: false, + layout: { + padding: {top: 30, left: 1, right: 1, bottom: 1} + } + } + }, { + canvas: { + height: 0, + width: 0 + } + }); + } + + expect(createChart).not.toThrow(); + }); +}); diff --git a/test/specs/scale.logarithmic.tests.js b/test/specs/scale.logarithmic.tests.js new file mode 100644 index 00000000000..27cd830d8e6 --- /dev/null +++ b/test/specs/scale.logarithmic.tests.js @@ -0,0 +1,1208 @@ +function getLabels(scale) { + return scale.ticks.map(t => t.label); +} + +describe('Logarithmic Scale tests', function() { + describe('auto', jasmine.fixture.specs('scale.logarithmic')); + + it('should register', function() { + var Constructor = Chart.registry.getScale('logarithmic'); + expect(Constructor).not.toBe(undefined); + expect(typeof Constructor).toBe('function'); + }); + + it('should have the correct default config', function() { + var defaultConfig = Chart.defaults.scales.logarithmic; + expect(defaultConfig).toEqual({ + ticks: { + callback: Chart.Ticks.formatters.logarithmic, + major: { + enabled: true + } + } + }); + + // Is this actually a function + expect(defaultConfig.ticks.callback).toEqual(jasmine.any(Function)); + }); + + it('should correctly determine the max & min data values', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + yAxisID: 'y', + data: [42, 1000, 64, 100], + }, { + yAxisID: 'y1', + data: [10, 5, 5000, 78, 450] + }, { + yAxisID: 'y1', + data: [150] + }, { + yAxisID: 'y2', + data: [20, 0, 150, 1800, 3040] + }, { + yAxisID: 'y3', + data: [67, 0.0004, 0, 820, 0.001] + }], + labels: ['a', 'b', 'c', 'd', 'e'] + }, + options: { + scales: { + y: { + id: 'y', + type: 'logarithmic' + }, + y1: { + type: 'logarithmic', + position: 'right' + }, + y2: { + type: 'logarithmic', + position: 'right' + }, + y3: { + position: 'right', + type: 'logarithmic' + } + } + } + }); + + expect(chart.scales.y).not.toEqual(undefined); // must construct + expect(chart.scales.y.min).toBe(10); + expect(chart.scales.y.max).toBe(1000); + + expect(chart.scales.y1).not.toEqual(undefined); // must construct + expect(chart.scales.y1.min).toBe(1); + expect(chart.scales.y1.max).toBe(5000); + + expect(chart.scales.y2).not.toEqual(undefined); // must construct + expect(chart.scales.y2.min).toBe(10); + expect(chart.scales.y2.max).toBe(4000); + + expect(chart.scales.y3).not.toEqual(undefined); // must construct + expect(chart.scales.y3.min).toBeCloseTo(0.0001, 4); + expect(chart.scales.y3.max).toBe(900); + }); + + it('should correctly determine the max & min of string data values', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + yAxisID: 'y', + data: ['42', '1000', '64', '100'], + }, { + yAxisID: 'y1', + data: ['10', '5', '5000', '78', '450'] + }, { + yAxisID: 'y1', + data: ['150'] + }, { + yAxisID: 'y2', + data: ['20', '0', '150', '1800', '3040'] + }, { + yAxisID: 'y3', + data: ['67', '0.0004', '0', '820', '0.001'] + }], + labels: ['a', 'b', 'c', 'd', 'e'] + }, + options: { + scales: { + y: { + type: 'logarithmic' + }, + y1: { + position: 'right', + type: 'logarithmic' + }, + y2: { + position: 'right', + type: 'logarithmic' + }, + y3: { + position: 'right', + type: 'logarithmic' + } + } + } + }); + + expect(chart.scales.y).not.toEqual(undefined); // must construct + expect(chart.scales.y.min).toBe(40); + expect(chart.scales.y.max).toBe(1000); + + expect(chart.scales.y1).not.toEqual(undefined); // must construct + expect(chart.scales.y1.min).toBe(5); + expect(chart.scales.y1.max).toBe(5000); + + expect(chart.scales.y2).not.toEqual(undefined); // must construct + expect(chart.scales.y2.min).toBe(10); + expect(chart.scales.y2.max).toBe(4000); + + expect(chart.scales.y3).not.toEqual(undefined); // must construct + expect(chart.scales.y3.min).toBeCloseTo(0.0001, 4); + expect(chart.scales.y3.max).toBe(900); + }); + + it('should correctly determine the max & min data values when there are hidden datasets', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + yAxisID: 'y1', + data: [10, 5, 5000, 78, 450] + }, { + yAxisID: 'y', + data: [42, 1000, 64, 100], + }, { + yAxisID: 'y1', + data: [50000], + hidden: true + }, { + yAxisID: 'y2', + data: [20, 0, 7400, 14, 291] + }, { + yAxisID: 'y2', + data: [6, 0.0007, 9, 890, 60000], + hidden: true + }], + labels: ['a', 'b', 'c', 'd', 'e'] + }, + options: { + scales: { + y: { + type: 'logarithmic' + }, + y1: { + position: 'right', + type: 'logarithmic' + }, + y2: { + position: 'right', + type: 'logarithmic' + } + } + } + }); + + expect(chart.scales.y1).not.toEqual(undefined); // must construct + expect(chart.scales.y1.min).toBe(5); + expect(chart.scales.y1.max).toBe(5000); + + expect(chart.scales.y2).not.toEqual(undefined); // must construct + expect(chart.scales.y2.min).toBe(10); + expect(chart.scales.y2.max).toBe(8000); + }); + + it('should correctly determine the max & min data values when there is NaN data', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + yAxisID: 'y', + data: [undefined, 10, null, 5, 5000, NaN, 78, 450] + }, { + yAxisID: 'y', + data: [undefined, 28, null, 1000, 500, NaN, 50, 42, Infinity, -Infinity] + }, { + yAxisID: 'y1', + data: [undefined, 30, null, 9400, 0, NaN, 54, 836] + }, { + yAxisID: 'y1', + data: [undefined, 0, null, 800, 9, NaN, 894, 21] + }], + labels: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'] + }, + options: { + scales: { + y: { + type: 'logarithmic' + }, + y1: { + position: 'right', + type: 'logarithmic' + } + } + } + }); + + expect(chart.scales.y).not.toEqual(undefined); // must construct + expect(chart.scales.y.min).toBe(1); + expect(chart.scales.y.max).toBe(5000); + + // Turn on stacked mode since it uses it's own + chart.options.scales.y.stacked = true; + chart.update(); + + expect(chart.scales.y.min).toBe(1); + expect(chart.scales.y.max).toBe(6000); + + expect(chart.scales.y1).not.toEqual(undefined); // must construct + expect(chart.scales.y1.min).toBe(1); + expect(chart.scales.y1.max).toBe(10000); + }); + + it('should correctly determine the max & min for scatter data', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [ + {x: 10, y: 100}, + {x: 2, y: 6}, + {x: 65, y: 121}, + {x: 99, y: 7} + ] + }] + }, + options: { + scales: { + x: { + type: 'logarithmic', + position: 'bottom' + }, + y: { + type: 'logarithmic' + } + } + } + }); + + expect(chart.scales.x.min).toBe(2); + expect(chart.scales.x.max).toBe(100); + + expect(chart.scales.y.min).toBe(6); + expect(chart.scales.y.max).toBe(150); + }); + + it('should correctly determine the max & min for scatter data when 0 values are present', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [ + {x: 7, y: 950}, + {x: 289, y: 0}, + {x: 0, y: 8}, + {x: 23, y: 0.04} + ] + }] + }, + options: { + scales: { + x: { + type: 'logarithmic', + position: 'bottom' + }, + y: { + type: 'logarithmic' + } + } + } + }); + + expect(chart.scales.x.min).toBe(1); + expect(chart.scales.x.max).toBe(30); + + expect(chart.scales.y.min).toBe(0.01); + expect(chart.scales.y.max).toBe(1000); + }); + + it('should correctly determine the min and max data values when stacked mode is turned on', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + type: 'bar', + yAxisID: 'y', + data: [10, 5, 1, 5, 78, 100] + }, { + yAxisID: 'y1', + data: [0, 1000], + }, { + type: 'bar', + yAxisID: 'y', + data: [150, 10, 10, 100, 10, 9] + }, { + type: 'line', + yAxisID: 'y', + data: [100, 100, 100, 100, 100, 100] + }], + labels: ['a', 'b', 'c', 'd', 'e', 'f'] + }, + options: { + scales: { + y: { + type: 'logarithmic', + stacked: true + }, + y1: { + position: 'right', + type: 'logarithmic' + } + } + } + }); + + expect(chart.scales.y.min).toBe(0.1); + expect(chart.scales.y.max).toBe(200); + }); + + it('should correctly determine the min and max data values when stacked mode is turned on ignoring hidden datasets', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + yAxisID: 'y', + data: [10, 5, 1, 5, 78, 100], + type: 'bar' + }, { + yAxisID: 'y1', + data: [0, 1000], + type: 'bar' + }, { + yAxisID: 'y', + data: [150, 10, 10, 100, 10, 9], + type: 'bar' + }, { + yAxisID: 'y', + data: [10000, 10000, 10000, 10000, 10000, 10000], + hidden: true, + type: 'bar' + }], + labels: ['a', 'b', 'c', 'd', 'e', 'f'] + }, + options: { + scales: { + y: { + type: 'logarithmic', + stacked: true + }, + y1: { + position: 'right', + type: 'logarithmic' + } + } + } + }); + + expect(chart.scales.y.min).toBe(0.1); + expect(chart.scales.y.max).toBe(200); + }); + + it('should ensure that the scale has a max and min that are not equal', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [] + }], + labels: [] + }, + options: { + scales: { + y: { + type: 'logarithmic' + } + } + } + }); + + expect(chart.scales.y.min).toBe(1); + expect(chart.scales.y.max).toBe(10); + + chart.data.datasets[0].data = [0.15, 0.15]; + chart.update(); + + expect(chart.scales.y.min).toBe(0.1); + expect(chart.scales.y.max).toBe(0.15); + }); + + it('should use the min and max options', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [1, 1, 1, 2, 1, 0] + }], + labels: [] + }, + options: { + scales: { + y: { + type: 'logarithmic', + min: 10, + max: 1010, + ticks: { + callback: function(value) { + return value; + } + } + } + } + } + }); + + var yScale = chart.scales.y; + var tickCount = yScale.ticks.length; + expect(yScale.min).toBe(10); + expect(yScale.max).toBe(1010); + expect(yScale.ticks[0].value).toBe(10); + expect(yScale.ticks[tickCount - 1].value).toBe(1010); + }); + + it('should ignore negative min and max options', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [1, 1, 1, 2, 1, 0] + }], + labels: [] + }, + options: { + scales: { + y: { + type: 'logarithmic', + min: -10, + max: -1010, + ticks: { + callback: function(value) { + return value; + } + } + } + } + } + }); + + var y = chart.scales.y; + expect(y.min).toBe(0.1); + expect(y.max).toBe(2); + }); + + it('should ignore invalid min and max options', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [1, 1, 1, 2, 1, 0] + }], + labels: ['a', 'b', 'c', 'd', 'e', 'f'] + }, + options: { + scales: { + y: { + type: 'logarithmic', + min: 'zero', + max: null, + ticks: { + callback: function(value) { + return value; + } + } + } + } + } + }); + + var y = chart.scales.y; + expect(y.min).toBe(0.1); + expect(y.max).toBe(2); + }); + + it('should generate tick marks', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [10, 5, 2, 25, 78] + }], + labels: [] + }, + options: { + scales: { + y: { + type: 'logarithmic', + ticks: { + autoSkip: false, + callback: function(value) { + return value; + } + } + } + } + } + }); + + var scale = chart.scales.y; + expect(getLabels(scale)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 30, 40, 50, 60, 70, 80]); + expect(scale.start).toEqual(1); + expect(scale.end).toEqual(80); + }); + + it('should generate tick marks when 0 values are present', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [11, 0.8, 0, 28, 7] + }], + labels: [] + }, + options: { + scales: { + y: { + type: 'logarithmic', + ticks: { + callback: function(value) { + return value; + } + } + } + } + } + }); + + var scale = chart.scales.y; + // Counts down because the lines are drawn top to bottom + expect(getLabels(scale)).toEqual([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1, 1.5, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 30]); + expect(scale.start).toEqual(0.1); + expect(scale.end).toEqual(30); + }); + + + it('should generate tick marks in the correct order in reversed mode', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [10, 5, 1, 25, 78] + }], + labels: [] + }, + options: { + scales: { + y: { + type: 'logarithmic', + reverse: true, + ticks: { + autoSkip: false, + callback: function(value) { + return value; + } + } + } + } + } + }); + + var scale = chart.scales.y; + expect(getLabels(scale)).toEqual([80, 70, 60, 50, 40, 30, 20, 15, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]); + expect(scale.start).toEqual(80); + expect(scale.end).toEqual(1); + }); + + it('should generate tick marks in the correct order in reversed mode when 0 values are present', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [21, 9, 0, 10, 25] + }], + labels: [] + }, + options: { + scales: { + y: { + type: 'logarithmic', + reverse: true, + ticks: { + callback: function(value) { + return value; + } + } + } + } + } + }); + + var scale = chart.scales.y; + expect(getLabels(scale)).toEqual([30, 20, 15, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]); + expect(scale.start).toEqual(30); + expect(scale.end).toEqual(1); + }); + + it('should build labels using the default template', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [10, 5, 1.1, 25, 0, 78] + }], + labels: [] + }, + options: { + scales: { + y: { + type: 'logarithmic', + ticks: { + autoSkip: false + } + } + } + } + }); + + expect(getLabels(chart.scales.y)).toEqual(['1', '2', '3', '', '5', '', '', '', '', '10', '15', '20', '30', '', '50', '60', '70', '80']); + }); + + it('should build labels using the user supplied callback', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [10, 5, 2, 25, 78] + }], + labels: [] + }, + options: { + scales: { + y: { + type: 'logarithmic', + ticks: { + callback: function(value, index) { + return index.toString(); + } + } + } + } + } + }); + + // Just the index + expect(getLabels(chart.scales.y)).toEqual(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17']); + }); + + it('should correctly get the correct label for a data item', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + yAxisID: 'y', + data: [10, 5, 5000, 78, 450] + }, { + yAxisID: 'y1', + data: [1, 1000, 10, 100], + }, { + yAxisID: 'y', + data: [150] + }], + labels: [] + }, + options: { + scales: { + y: { + type: 'logarithmic' + }, + y1: { + position: 'right', + type: 'logarithmic' + } + } + } + }); + + expect(chart.scales.y.getLabelForValue(150)).toBe('150'); + }); + + it('should correctly use the locale when generating the label', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + yAxisID: 'y', + data: [10, 5, 5000, 78, 450] + }, { + yAxisID: 'y1', + data: [1, 1000, 10, 100], + }, { + yAxisID: 'y', + data: [150] + }], + labels: [] + }, + options: { + locale: 'de-DE', + scales: { + y: { + type: 'logarithmic' + }, + y1: { + position: 'right', + type: 'logarithmic' + } + } + } + }); + + expect(chart.scales.y.getLabelForValue(10.25)).toBe('10,25'); + }); + + describe('when', function() { + var data = [ + { + data: [1, 39], + stack: 'stack' + }, + { + data: [1, 39], + stack: 'stack' + }, + ]; + var dataWithEmptyStacks = [ + { + data: [] + }, + { + data: [] + } + ].concat(data); + var config = [ + { + axis: 'y', + firstTick: 1, // start of the axis (minimum) + describe: 'all stacks are defined' + }, + { + axis: 'y', + data: dataWithEmptyStacks, + firstTick: 1, + describe: 'not all stacks are defined' + }, + { + axis: 'y', + scale: { + y: { + min: 0 + } + }, + firstTick: 0.1, + describe: 'all stacks are defined and min: 0' + }, + { + axis: 'y', + data: dataWithEmptyStacks, + scale: { + y: { + min: 0 + } + }, + firstTick: 0.1, + describe: 'not stacks are defined and min: 0' + }, + { + axis: 'x', + firstTick: 1, + describe: 'all stacks are defined' + }, + { + axis: 'x', + data: dataWithEmptyStacks, + firstTick: 1, + describe: 'not all stacks are defined' + }, + { + axis: 'x', + scale: { + x: { + min: 0 + } + }, + firstTick: 0.1, + describe: 'all stacks are defined and min: 0' + }, + { + axis: 'x', + data: dataWithEmptyStacks, + scale: { + x: { + min: 0 + } + }, + firstTick: 0.1, + describe: 'not all stacks are defined and min: 0' + }, + ]; + config.forEach(function(setup) { + var scaleConfig = {}; + var indexAxis, chartStart, chartEnd; + + if (setup.axis === 'x') { + indexAxis = 'y'; + chartStart = 'left'; + chartEnd = 'right'; + } else { + indexAxis = 'x'; + chartStart = 'bottom'; + chartEnd = 'top'; + } + scaleConfig[setup.axis] = { + type: 'logarithmic', + beginAtZero: false + }; + Object.assign(scaleConfig, setup.scale); + scaleConfig[setup.axis].type = 'logarithmic'; + + var description = 'dataset has stack option and ' + setup.describe + + ' and axis is "' + setup.axis + '";'; + describe(description, function() { + it('should define the correct axis limits', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + labels: ['category 1', 'category 2'], + datasets: setup.data || data, + }, + options: { + indexAxis, + scales: scaleConfig + } + }); + + var axisID = setup.axis; + var scale = chart.scales[axisID]; + var firstTick = setup.firstTick; + var lastTick = 80; // last tick (should be first available tick after: 2 * 39) + var start = chart.chartArea[chartStart]; + var end = chart.chartArea[chartEnd]; + + expect(scale.getPixelForValue(firstTick)).toBeCloseToPixel(start); + expect(scale.getPixelForValue(lastTick)).toBeCloseToPixel(end); + + expect(scale.getValueForPixel(start)).toBeCloseTo(firstTick, 4); + expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); + + chart.scales[axisID].options.reverse = true; // Reverse mode + chart.update(); + + // chartArea might have been resized in update + start = chart.chartArea[chartEnd]; + end = chart.chartArea[chartStart]; + + expect(scale.getPixelForValue(firstTick)).toBeCloseToPixel(start); + expect(scale.getPixelForValue(lastTick)).toBeCloseToPixel(end); + + expect(scale.getValueForPixel(start)).toBeCloseTo(firstTick, 4); + expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); + }); + }); + }); + }); + + describe('when', function() { + var config = [ + { + dataset: [], + firstTick: 1, // value of the first tick + lastTick: 10, // value of the last tick + describe: 'empty dataset, without min/max' + }, + { + dataset: [], + scale: {stacked: true}, + firstTick: 1, + lastTick: 10, + describe: 'empty dataset, without min/max, with stacked: true' + }, + { + data: { + datasets: [ + {data: [], stack: 'stack'}, + {data: [], stack: 'stack'}, + ], + }, + type: 'bar', + firstTick: 1, + lastTick: 10, + describe: 'empty dataset with stack option, without min/max' + }, + { + dataset: [], + scale: {min: 1}, + firstTick: 1, + lastTick: 10, + describe: 'empty dataset, min: 1, without max' + }, + { + dataset: [], + scale: {max: 80}, + firstTick: 1, + lastTick: 80, + describe: 'empty dataset, max: 80, without min' + }, + { + dataset: [], + scale: {max: 0.8}, + firstTick: 0.01, + lastTick: 0.8, + describe: 'empty dataset, max: 0.8, without min' + }, + { + dataset: [{x: 10, y: 10}, {x: 5, y: 5}, {x: 1, y: 1}, {x: 25, y: 25}, {x: 78, y: 78}], + firstTick: 1, + lastTick: 80, + describe: 'dataset min point {x: 1, y: 1}, max point {x:78, y:78}' + }, + ]; + config.forEach(function(setup) { + var axes = [ + { + id: 'x', // horizontal scale + start: 'left', + end: 'right' + }, + { + id: 'y', // vertical scale + start: 'bottom', + end: 'top' + } + ]; + axes.forEach(function(axis) { + var expectation = 'min = ' + setup.firstTick + ', max = ' + setup.lastTick; + describe(setup.describe + ' and axis is "' + axis.id + '"; expect: ' + expectation + ';', function() { + beforeEach(function() { + var xConfig = { + type: 'logarithmic', + position: 'bottom' + }; + var yConfig = { + type: 'logarithmic', + position: 'left' + }; + var data = setup.data || { + datasets: [{ + data: setup.dataset + }], + }; + Object.assign(xConfig, setup.scale); + Object.assign(yConfig, setup.scale); + Object.assign(data, setup.data || {}); + this.chart = window.acquireChart({ + type: 'line', + data: data, + options: { + scales: { + x: xConfig, + y: yConfig + } + } + }); + }); + + it('should get the correct pixel value for a point', function() { + var chart = this.chart; + var axisID = axis.id; + var scale = chart.scales[axisID]; + var firstTick = setup.firstTick; + var lastTick = setup.lastTick; + var start = chart.chartArea[axis.start]; + var end = chart.chartArea[axis.end]; + + expect(scale.getPixelForValue(firstTick)).toBeCloseToPixel(start); + expect(scale.getPixelForValue(lastTick)).toBeCloseToPixel(end); + expect(scale.getPixelForValue(0)).toBeCloseToPixel(start); // 0 is invalid, put it at the start. + + expect(scale.getValueForPixel(start)).toBeCloseTo(firstTick, 4); + expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); + + chart.scales[axisID].options.reverse = true; // Reverse mode + chart.update(); + + // chartArea might have been resized in update + start = chart.chartArea[axis.end]; + end = chart.chartArea[axis.start]; + + expect(scale.getPixelForValue(firstTick)).toBeCloseToPixel(start); + expect(scale.getPixelForValue(lastTick)).toBeCloseToPixel(end); + + expect(scale.getValueForPixel(start)).toBeCloseTo(firstTick, 4); + expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); + }); + }); + }); + }); + }); + + describe('when', function() { + var config = [ + { + dataset: [], + scale: {min: 0}, + lastTick: 10, // value of the last tick + describe: 'empty dataset, min: 0, without max' + }, + { + dataset: [], + scale: {min: 0, max: 80}, + lastTick: 80, + describe: 'empty dataset, min: 0, max: 80' + }, + { + dataset: [], + scale: {min: 0, max: 0.8}, + lastTick: 0.8, + describe: 'empty dataset, min: 0, max: 0.8' + }, + { + dataset: [{x: 0, y: 0}, {x: 10, y: 10}, {x: 1.2, y: 1.2}, {x: 25, y: 25}, {x: 78, y: 78}], + lastTick: 80, + describe: 'dataset min point {x: 0, y: 0}, max point {x:78, y:78}' + }, + { + dataset: [{x: 0, y: 0}, {x: 10, y: 10}, {x: 6.3, y: 6.3}, {x: 25, y: 25}, {x: 78, y: 78}], + lastTick: 80, + describe: 'dataset min point {x: 0, y: 0}, max point {x:78, y:78}' + }, + { + dataset: [{x: 10, y: 10}, {x: 1.2, y: 1.2}, {x: 25, y: 25}, {x: 78, y: 78}], + scale: {min: 0}, + lastTick: 80, + describe: 'dataset min point {x: 1.2, y: 1.2}, max point {x:78, y:78}, min: 0' + }, + { + dataset: [{x: 10, y: 10}, {x: 6.3, y: 6.3}, {x: 25, y: 25}, {x: 78, y: 78}], + scale: {min: 0}, + lastTick: 80, + describe: 'dataset min point {x: 6.3, y: 6.3}, max point {x:78, y:78}, min: 0' + }, + ]; + config.forEach(function(setup) { + var axes = [ + { + id: 'x', // horizontal scale + start: 'left', + end: 'right' + }, + { + id: 'y', // vertical scale + start: 'bottom', + end: 'top' + } + ]; + axes.forEach(function(axis) { + var expectation = 'min = 0, max = ' + setup.lastTick; + describe(setup.describe + ' and axis is "' + axis.id + '"; expect: ' + expectation + ';', function() { + beforeEach(function() { + var xConfig = { + type: 'logarithmic', + position: 'bottom' + }; + var yConfig = { + type: 'logarithmic', + position: 'left' + }; + var data = setup.data || { + datasets: [{ + data: setup.dataset + }], + }; + Object.assign(xConfig, setup.scale); + Object.assign(yConfig, setup.scale); + Object.assign(data, setup.data || {}); + this.chart = window.acquireChart({ + type: 'line', + data: data, + options: { + scales: { + x: xConfig, + y: yConfig + } + } + }); + }); + + it('should get the correct pixel value for a point', function() { + var chart = this.chart; + var axisID = axis.id; + var scale = chart.scales[axisID]; + var lastTick = setup.lastTick; + var start = chart.chartArea[axis.start]; + var end = chart.chartArea[axis.end]; + + expect(scale.getPixelForValue(0)).toBeCloseToPixel(start); + expect(scale.getPixelForValue(lastTick)).toBeCloseToPixel(end); + + expect(scale.getValueForPixel(start)).toBeCloseTo(scale.min, 4); + expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); + + chart.scales[axisID].options.reverse = true; // Reverse mode + chart.update(); + + // chartArea might have been resized in update + start = chart.chartArea[axis.end]; + end = chart.chartArea[axis.start]; + + expect(scale.getPixelForValue(0)).toBeCloseToPixel(start); + expect(scale.getPixelForValue(lastTick)).toBeCloseToPixel(end); + + expect(scale.getValueForPixel(start)).toBeCloseTo(scale.min, 4); + expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); + }); + }); + }); + }); + }); + + it('Should correctly determine the max & min when no values provided and suggested minimum and maximum are set', function() { + var chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + yAxisID: 'y', + data: [] + }], + labels: ['a', 'b', 'c', 'd', 'e', 'f'] + }, + options: { + scales: { + y: { + type: 'logarithmic', + suggestedMin: 10, + suggestedMax: 100 + } + } + } + }); + + expect(chart.scales.y).not.toEqual(undefined); // must construct + expect(chart.scales.y.min).toBe(10); + expect(chart.scales.y.max).toBe(100); + }); + + it('Should bound to data', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + labels: ['a', 'b'], + datasets: [{ + data: [1.1, 99] + }] + }, + options: { + scales: { + y: { + type: 'logarithmic', + bounds: 'data' + } + } + } + }); + + expect(chart.scales.y.min).toEqual(1.1); + expect(chart.scales.y.max).toEqual(99); + }); +}); diff --git a/test/specs/scale.radialLinear.tests.js b/test/specs/scale.radialLinear.tests.js new file mode 100644 index 00000000000..451f6fb8db8 --- /dev/null +++ b/test/specs/scale.radialLinear.tests.js @@ -0,0 +1,656 @@ +function getLabels(scale) { + return scale.ticks.map(t => t.label); +} + +// Tests for the radial linear scale used by the polar area and radar charts +describe('Test the radial linear scale', function() { + describe('auto', jasmine.fixture.specs('scale.radialLinear')); + + it('Should register the constructor with the registry', function() { + var Constructor = Chart.registry.getScale('radialLinear'); + expect(Constructor).not.toBe(undefined); + expect(typeof Constructor).toBe('function'); + }); + + it('Should have the correct default config', function() { + var defaultConfig = Chart.defaults.scales.radialLinear; + expect(defaultConfig).toEqual({ + display: true, + animate: true, + position: 'chartArea', + + angleLines: { + display: true, + color: 'rgba(0,0,0,0.1)', + lineWidth: 1, + borderDash: [], + borderDashOffset: 0.0 + }, + + grid: { + circular: false + }, + + startAngle: 0, + + ticks: { + color: Chart.defaults.color, + showLabelBackdrop: true, + callback: defaultConfig.ticks.callback + }, + + pointLabels: { + backdropColor: undefined, + backdropPadding: 2, + color: Chart.defaults.color, + display: true, + font: { + size: 10 + }, + callback: defaultConfig.pointLabels.callback, + padding: 5, + centerPointLabels: false + } + }); + + // Is this actually a function + expect(defaultConfig.ticks.callback).toEqual(jasmine.any(Function)); + expect(defaultConfig.pointLabels.callback).toEqual(jasmine.any(Function)); + }); + + it('Should correctly determine the max & min data values', function() { + var chart = window.acquireChart({ + type: 'radar', + data: { + datasets: [{ + data: [10, 5, 0, -5, 78, -100] + }, { + data: [150] + }], + labels: ['label1', 'label2', 'label3', 'label4', 'label5', 'label6'] + }, + options: { + scales: {} + } + }); + + expect(chart.scales.r.min).toBe(-100); + expect(chart.scales.r.max).toBe(150); + }); + + it('Should correctly determine the max & min of string data values', function() { + var chart = window.acquireChart({ + type: 'radar', + data: { + datasets: [{ + data: ['10', '5', '0', '-5', '78', '-100'] + }, { + data: ['150'] + }], + labels: ['label1', 'label2', 'label3', 'label4', 'label5', 'label6'] + }, + options: { + scales: {} + } + }); + + expect(chart.scales.r.min).toBe(-100); + expect(chart.scales.r.max).toBe(150); + }); + + it('Should correctly determine the max & min data values when there are hidden datasets', function() { + var chart = window.acquireChart({ + type: 'radar', + data: { + datasets: [{ + data: ['10', '5', '0', '-5', '78', '-100'] + }, { + data: ['150'] + }, { + data: [1000], + hidden: true + }], + labels: ['label1', 'label2', 'label3', 'label4', 'label5', 'label6'] + }, + options: { + scales: {} + } + }); + + expect(chart.scales.r.min).toBe(-100); + expect(chart.scales.r.max).toBe(150); + }); + + it('Should correctly determine the max & min data values when there is NaN data', function() { + var chart = window.acquireChart({ + type: 'radar', + data: { + datasets: [{ + data: [50, 60, NaN, 70, null, undefined, Infinity, -Infinity] + }], + labels: ['label1', 'label2', 'label3', 'label4', 'label5', 'label6', 'label7', 'label8'] + }, + options: { + scales: {} + } + }); + + expect(chart.scales.r.min).toBe(50); + expect(chart.scales.r.max).toBe(70); + }); + + it('Should ensure that the scale has a max and min that are not equal', function() { + var chart = window.acquireChart({ + type: 'radar', + data: { + datasets: [], + labels: [] + }, + options: { + scales: { + rScale: {} + } + } + }); + + var scale = chart.scales.rScale; + + expect(scale.min).toBe(-1); + expect(scale.max).toBe(1); + }); + + it('Should use the suggestedMin and suggestedMax options', function() { + var chart = window.acquireChart({ + type: 'radar', + data: { + datasets: [{ + data: [1, 1, 1, 2, 1, 0] + }], + labels: ['label1', 'label2', 'label3', 'label4', 'label5', 'label6'] + }, + options: { + scales: { + r: { + suggestedMin: -10, + suggestedMax: 10 + } + } + } + }); + + expect(chart.scales.r.min).toBe(-10); + expect(chart.scales.r.max).toBe(10); + }); + + it('Should use the min and max options', function() { + var chart = window.acquireChart({ + type: 'radar', + data: { + datasets: [{ + data: [1, 1, 1, 2, 1, 0] + }], + labels: ['label1', 'label2', 'label3', 'label4', 'label5', 'label6'] + }, + options: { + scales: { + r: { + min: -1010, + max: 1010 + } + } + } + }); + + expect(chart.scales.r.min).toBe(-1010); + expect(chart.scales.r.max).toBe(1010); + expect(getLabels(chart.scales.r)).toEqual(['-1,010', '-500', '0', '500', '1,010']); + }); + + it('should forcibly include 0 in the range if the beginAtZero option is used', function() { + var chart = window.acquireChart({ + type: 'radar', + data: { + datasets: [{ + data: [20, 30, 40, 50] + }], + labels: ['label1', 'label2', 'label3', 'label4'] + }, + options: { + scales: { + r: { + beginAtZero: false + } + } + } + }); + + expect(getLabels(chart.scales.r)).toEqual(['20', '25', '30', '35', '40', '45', '50']); + + chart.scales.r.options.beginAtZero = true; + chart.update(); + + expect(getLabels(chart.scales.r)).toEqual(['0', '5', '10', '15', '20', '25', '30', '35', '40', '45', '50']); + + chart.data.datasets[0].data = [-20, -30, -40, -50]; + chart.update(); + + expect(getLabels(chart.scales.r)).toEqual(['-50', '-45', '-40', '-35', '-30', '-25', '-20', '-15', '-10', '-5', '0']); + + chart.scales.r.options.beginAtZero = false; + chart.update(); + + expect(getLabels(chart.scales.r)).toEqual(['-50', '-45', '-40', '-35', '-30', '-25', '-20']); + }); + + it('Should generate tick marks in the correct order in reversed mode', function() { + var chart = window.acquireChart({ + type: 'radar', + data: { + datasets: [{ + data: [10, 5, 0, 25, 78] + }], + labels: ['label1', 'label2', 'label3', 'label4', 'label5'] + }, + options: { + scales: { + r: { + reverse: true + } + } + } + }); + + expect(getLabels(chart.scales.r)).toEqual(['80', '70', '60', '50', '40', '30', '20', '10', '0']); + expect(chart.scales.r.start).toBe(80); + expect(chart.scales.r.end).toBe(0); + }); + + it('Should correctly limit the maximum number of ticks', function() { + var chart = window.acquireChart({ + type: 'radar', + data: { + labels: ['label1', 'label2', 'label3'], + datasets: [{ + data: [0.5, 1.5, 2.5] + }] + }, + options: { + scales: { + r: { + pointLabels: { + display: false + } + } + } + } + }); + + expect(getLabels(chart.scales.r)).toEqual(['0.5', '1.0', '1.5', '2.0', '2.5']); + + chart.options.scales.r.ticks.maxTicksLimit = 11; + chart.update(); + + expect(getLabels(chart.scales.r)).toEqual(['0.5', '1.0', '1.5', '2.0', '2.5']); + + chart.options.scales.r.ticks.stepSize = 0.01; + chart.update(); + + expect(getLabels(chart.scales.r)).toEqual(['0.5', '1.0', '1.5', '2.0', '2.5']); + + chart.options.scales.r.min = 0.3; + chart.options.scales.r.max = 2.8; + chart.update(); + + expect(getLabels(chart.scales.r)).toEqual(['0.3', '0.8', '1.3', '1.8', '2.3', '2.8']); + }); + + it('Should build labels using the user supplied callback', function() { + var chart = window.acquireChart({ + type: 'radar', + data: { + datasets: [{ + data: [10, 5, 0, 25, 78] + }], + labels: ['label1', 'label2', 'label3', 'label4', 'label5'] + }, + options: { + scales: { + r: { + ticks: { + callback: function(value, index) { + return index.toString(); + } + } + } + } + } + }); + + expect(getLabels(chart.scales.r)).toEqual(['0', '1', '2', '3', '4', '5', '6', '7', '8']); + expect(chart.scales.r._pointLabels).toEqual(['label1', 'label2', 'label3', 'label4', 'label5']); + }); + + it('Should build point labels using the user supplied callback', function() { + var chart = window.acquireChart({ + type: 'radar', + data: { + datasets: [{ + data: [10, 5, 0, 25, 78] + }], + labels: ['label1', 'label2', 'label3', 'label4', 'label5'] + }, + options: { + scales: { + r: { + pointLabels: { + callback: function(value, index) { + return index.toString(); + } + } + } + } + } + }); + + expect(chart.scales.r._pointLabels).toEqual(['0', '1', '2', '3', '4']); + }); + + it('Should build point labels from falsy values', function() { + var chart = window.acquireChart({ + type: 'radar', + data: { + datasets: [{ + data: [10, 5, 0, 25, 78, 20] + }], + labels: [0, '', undefined, null, NaN, false] + } + }); + + expect(chart.scales.r._pointLabels).toEqual([0, '', '', '', '', '']); + }); + + it('Should build point labels considering hidden data', function() { + const chart = window.acquireChart({ + type: 'polarArea', + data: { + datasets: [{ + data: [10, 5, 0, 25, 78, 20] + }], + labels: ['a', 'b', 'c', 'd', 'e', 'f'] + } + }); + chart.toggleDataVisibility(3); + chart.update(); + + expect(chart.scales.r._pointLabels).toEqual(['a', 'b', 'c', 'e', 'f']); + }); + + it('should correctly set the center point', function() { + var chart = window.acquireChart({ + type: 'radar', + data: { + datasets: [{ + data: [10, 5, 0, 25, 78] + }], + labels: ['label1', 'label2', 'label3', 'label4', 'label5'] + }, + options: { + scales: { + r: { + pointLabels: { + callback: function(value, index) { + return index.toString(); + } + } + } + } + } + }); + + expect(chart.scales.r.drawingArea).toBe(215); + expect(chart.scales.r.xCenter).toBe(256); + expect(chart.scales.r.yCenter).toBe(280); + }); + + it('should correctly get the label for a given data index', function() { + var chart = window.acquireChart({ + type: 'radar', + data: { + datasets: [{ + data: [10, 5, 0, 25, 78] + }], + labels: ['label1', 'label2', 'label3', 'label4', 'label5'] + }, + options: { + scales: { + r: { + pointLabels: { + callback: function(value, index) { + return index.toString(); + } + } + } + } + } + }); + expect(chart.scales.r.getLabelForValue(5)).toBe('5'); + }); + + it('should get the correct distance from the center point', function() { + var chart = window.acquireChart({ + type: 'radar', + data: { + datasets: [{ + data: [10, 5, 0, 25, 78] + }], + labels: ['label1', 'label2', 'label3', 'label4', 'label5'] + }, + options: { + scales: { + r: { + pointLabels: { + callback: function(value, index) { + return index.toString(); + } + } + } + } + } + }); + + expect(chart.scales.r.getDistanceFromCenterForValue(chart.scales.r.min)).toBe(0); + expect(chart.scales.r.getDistanceFromCenterForValue(chart.scales.r.max)).toBe(215); + + var position = chart.scales.r.getPointPositionForValue(1, 5); + expect(position.x).toBeCloseToPixel(269); + expect(position.y).toBeCloseToPixel(276); + + chart.scales.r.options.reverse = true; + chart.update(); + + expect(chart.scales.r.getDistanceFromCenterForValue(chart.scales.r.min)).toBe(215); + expect(chart.scales.r.getDistanceFromCenterForValue(chart.scales.r.max)).toBe(0); + }); + + it('should get the correct value for a distance from the center point', function() { + var chart = window.acquireChart({ + type: 'radar', + data: { + datasets: [{ + data: [10, 5, 0, 25, 78] + }], + labels: ['label1', 'label2', 'label3', 'label4', 'label5'] + }, + options: { + scales: { + r: { + pointLabels: { + callback: function(value, index) { + return index.toString(); + } + } + } + } + } + }); + + expect(chart.scales.r.getValueForDistanceFromCenter(0)).toBe(chart.scales.r.min); + expect(chart.scales.r.getValueForDistanceFromCenter(215)).toBe(chart.scales.r.max); + + var dist = chart.scales.r.getDistanceFromCenterForValue(5); + expect(chart.scales.r.getValueForDistanceFromCenter(dist)).toBe(5); + + chart.scales.r.options.reverse = true; + chart.update(); + + expect(chart.scales.r.getValueForDistanceFromCenter(0)).toBe(chart.scales.r.max); + expect(chart.scales.r.getValueForDistanceFromCenter(215)).toBe(chart.scales.r.min); + }); + + it('should correctly get angles for all points', function() { + var chart = window.acquireChart({ + type: 'radar', + data: { + datasets: [{ + data: [10, 5, 0, 25, 78] + }], + labels: ['label1', 'label2', 'label3', 'label4', 'label5'] + }, + options: { + scales: { + r: { + startAngle: 15, + pointLabels: { + callback: function(value, index) { + return index.toString(); + } + } + } + }, + } + }); + + var radToNearestDegree = function(rad) { + return Math.round((360 * rad) / (2 * Math.PI)); + }; + + var slice = 72; // (360 / 5) + + for (var i = 0; i < 5; i++) { + expect(radToNearestDegree(chart.scales.r.getIndexAngle(i))).toBe(15 + (slice * i)); + } + + chart.scales.r.options.startAngle = 0; + chart.update(); + + for (var x = 0; x < 5; x++) { + expect(radToNearestDegree(chart.scales.r.getIndexAngle(x))).toBe((slice * x)); + } + }); + + it('should correctly get the correct label alignment for all points', function() { + var chart = window.acquireChart({ + type: 'radar', + data: { + datasets: [{ + data: [10, 5, 0, 25, 78] + }], + labels: ['label1', 'label2', 'label3', 'label4', 'label5'] + }, + options: { + scales: { + r: { + pointLabels: { + callback: function(value, index) { + return index.toString(); + } + }, + ticks: { + display: false + } + } + } + } + }); + + var scale = chart.scales.r; + + [{ + startAngle: 30, + textAlign: ['right', 'right', 'left', 'left', 'left'], + }, { + startAngle: -30, + textAlign: ['right', 'right', 'left', 'left', 'right'], + }, { + startAngle: 750, + textAlign: ['right', 'right', 'left', 'left', 'left'], + }].forEach(function(expected) { + scale.options.startAngle = expected.startAngle; + chart.update(); + + scale.ctx = window.createMockContext(); + chart.draw(); + + scale.ctx.getCalls().filter(function(x) { + return x.name === 'setTextAlign'; + }).forEach(function(x, i) { + expect(x.args[0]).withContext('startAngle: ' + expected.startAngle + ', tick: ' + i).toBe(expected.textAlign[i]); + }); + }); + }); + + it('should correctly get the point positions in center', function() { + var chart = window.acquireChart({ + type: 'polarArea', + data: { + datasets: [{ + data: [10, 5, 0, 25, 78] + }], + labels: ['label1', 'label2', 'label3', 'label4', 'label5'] + }, + options: { + scales: { + r: { + pointLabels: { + display: true, + padding: 5, + centerPointLabels: true + }, + ticks: { + display: false + } + } + } + } + }); + + const PI = Math.PI; + const lavelNum = 5; + const padding = 5; + const pointLabelItems = chart.scales.r._pointLabelItems; + const additionalAngle = PI / lavelNum; + const opts = chart.scales.r.options; + const outerDistance = chart.scales.r.getDistanceFromCenterForValue(opts.ticks.reverse ? chart.scales.r.min : chart.scales.r.max); + const tickBackdropHeight = 0; + const yForAngle = function(y, h, angle) { + if (angle === 90 || angle === 270) { + y -= (h / 2); + } else if (angle > 270 || angle < 90) { + y -= h; + } + return y; + }; + const toDegrees = function(radians) { + return radians * (180 / PI); + }; + + for (var i = 0; i < 5; i++) { + const extra = (i === 0 ? tickBackdropHeight / 2 : 0); + const pointLabelItem = pointLabelItems[i]; + const pointPosition = chart.scales.r.getPointPosition(i, outerDistance + extra + padding, additionalAngle); + expect(pointLabelItem.x).toBe(pointPosition.x); + expect(pointLabelItem.y).toBe(yForAngle(pointPosition.y, 12, toDegrees(pointPosition.angle + PI / 2))); + } + + }); +}); diff --git a/test/specs/scale.time.tests.js b/test/specs/scale.time.tests.js new file mode 100644 index 00000000000..42817ae15c9 --- /dev/null +++ b/test/specs/scale.time.tests.js @@ -0,0 +1,1336 @@ +// Time scale tests +describe('Time scale tests', function() { + describe('auto', jasmine.fixture.specs('scale.time')); + + function createScale(data, options, dimensions) { + var width = (dimensions && dimensions.width) || 400; + var height = (dimensions && dimensions.height) || 50; + + options = options || {}; + options.type = 'time'; + options.id = 'xScale0'; + + var chart = window.acquireChart({ + type: 'line', + data: data, + options: { + scales: { + x: options + } + } + }, {canvas: {width: width, height: height}}); + + + return chart.scales.x; + } + + function getLabels(scale) { + return scale.ticks.map(t => t.label); + } + + beforeEach(function() { + // Need a time matcher for getValueFromPixel + jasmine.addMatchers({ + toBeCloseToTime: function() { + return { + compare: function(time, expected) { + var result = false; + var actual = moment(time); + var diff = actual.diff(expected.value, expected.unit, true); + result = Math.abs(diff) < (expected.threshold !== undefined ? expected.threshold : 0.01); + + return { + pass: result + }; + } + }; + } + }); + }); + + it('should load moment.js as a dependency', function() { + expect(window.moment).not.toBe(undefined); + }); + + it('should register the constructor with the registry', function() { + var Constructor = Chart.registry.getScale('time'); + expect(Constructor).not.toBe(undefined); + expect(typeof Constructor).toBe('function'); + }); + + it('should have the correct default config', function() { + var defaultConfig = Chart.defaults.scales.time; + expect(defaultConfig).toEqual({ + bounds: 'data', + adapters: {}, + time: { + parser: false, // false == a pattern string from or a custom callback that converts its argument to a timestamp + unit: false, // false == automatic or override with week, month, year, etc. + round: false, // none, or override with week, month, year, etc. + isoWeekday: false, // override week start day + minUnit: 'millisecond', + displayFormats: {} + }, + ticks: { + source: 'auto', + callback: false, + major: { + enabled: false + } + } + }); + }); + + it('should correctly determine the unit', function() { + var date = moment('Jan 01 1990', 'MMM DD YYYY'); + var data = []; + for (var i = 0; i < 60; i++) { + data.push({x: date.valueOf(), y: Math.random()}); + date = date.clone().add(1, 'month'); + } + + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'x', + data: data + }], + }, + options: { + scales: { + x: { + type: 'time', + ticks: { + source: 'data', + autoSkip: true + } + }, + } + } + }); + + var scale = chart.scales.x; + + expect(scale._unit).toEqual('month'); + }); + + describe('when specifying limits', function() { + var mockData = { + labels: ['2015-01-01T20:00:00', '2015-01-02T20:00:00', '2015-01-03T20:00:00'], + }; + + var config; + beforeEach(function() { + config = Chart.helpers.clone(Chart.defaults.scales.time); + config.ticks.source = 'labels'; + config.time.unit = 'day'; + }); + + it('should use the min option when less than first label for building ticks', function() { + config.min = '2014-12-29T04:00:00'; + + var labels = getLabels(createScale(mockData, config)); + expect(labels[0]).toEqual('Jan 1'); + }); + + it('should use the min option when greater than first label for building ticks', function() { + config.min = '2015-01-02T04:00:00'; + + var labels = getLabels(createScale(mockData, config)); + expect(labels[0]).toEqual('Jan 2'); + }); + + it('should use the max option when greater than last label for building ticks', function() { + config.max = '2015-01-05T06:00:00'; + + var labels = getLabels(createScale(mockData, config)); + expect(labels[labels.length - 1]).toEqual('Jan 3'); + }); + + it('should use the max option when less than last label for building ticks', function() { + config.max = '2015-01-02T23:00:00'; + + var labels = getLabels(createScale(mockData, config)); + expect(labels[labels.length - 1]).toEqual('Jan 2'); + }); + }); + + it('should use the isoWeekday option', function() { + var mockData = { + labels: [ + '2015-01-01T20:00:00', // Thursday + '2015-01-02T20:00:00', // Friday + '2015-01-03T20:00:00' // Saturday + ] + }; + + var config = Chart.helpers.mergeIf({ + bounds: 'ticks', + time: { + unit: 'week', + isoWeekday: 3 // Wednesday + } + }, Chart.defaults.scales.time); + + var scale = createScale(mockData, config); + var ticks = getLabels(scale); + + expect(ticks).toEqual(['Dec 31, 2014', 'Jan 7, 2015']); + }); + + describe('when rendering several days', function() { + beforeEach(function() { + this.chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'x', + data: [] + }], + labels: [ + '2015-01-01T20:00:00', + '2015-01-02T21:00:00', + '2015-01-03T22:00:00', + '2015-01-05T23:00:00', + '2015-01-07T03:00', + '2015-01-08T10:00', + '2015-01-10T12:00' + ] + }, + options: { + scales: { + x: { + type: 'time', + position: 'bottom' + }, + } + } + }); + + this.scale = this.chart.scales.x; + }); + + it('should be bounded by the nearest week beginnings', function() { + var chart = this.chart; + var scale = this.scale; + expect(scale.getValueForPixel(scale.left)).toBeGreaterThan(moment(chart.data.labels[0]).startOf('week')); + expect(scale.getValueForPixel(scale.right)).toBeLessThan(moment(chart.data.labels[chart.data.labels.length - 1]).add(1, 'week').endOf('week')); + }); + + it('should convert between screen coordinates and times', function() { + var chart = this.chart; + var scale = this.scale; + var timeRange = moment(scale.max).valueOf() - moment(scale.min).valueOf(); + var msPerPix = timeRange / scale.width; + var firstPointOffsetMs = moment(chart.config.data.labels[0]).valueOf() - scale.min; + var firstPointPixel = scale.left + firstPointOffsetMs / msPerPix; + var lastPointOffsetMs = moment(chart.config.data.labels[chart.config.data.labels.length - 1]).valueOf() - scale.min; + var lastPointPixel = scale.left + lastPointOffsetMs / msPerPix; + + expect(scale.getPixelForValue(moment('2015-01-01T20:00:00').valueOf())).toBeCloseToPixel(firstPointPixel); + expect(scale.getPixelForValue(moment(chart.data.labels[0]).valueOf())).toBeCloseToPixel(firstPointPixel); + expect(scale.getValueForPixel(firstPointPixel)).toBeCloseToTime({ + value: moment(chart.data.labels[0]), + unit: 'hour', + }); + + expect(scale.getPixelForValue(moment('2015-01-10T12:00').valueOf())).toBeCloseToPixel(lastPointPixel); + expect(scale.getValueForPixel(lastPointPixel)).toBeCloseToTime({ + value: moment(chart.data.labels[6]), + unit: 'hour' + }); + }); + }); + + describe('when rendering several years', function() { + beforeEach(function() { + this.chart = window.acquireChart({ + type: 'line', + data: { + labels: ['2005-07-04', '2017-01-20'], + }, + options: { + scales: { + x: { + type: 'time', + bounds: 'ticks', + position: 'bottom' + }, + } + } + }, {canvas: {width: 800, height: 200}}); + + this.scale = this.chart.scales.x; + }); + + it('should be bounded by nearest step\'s year start and end', function() { + var scale = this.scale; + var ticks = scale.getTicks(); + var step = ticks[1].value - ticks[0].value; + var stepsAmount = Math.floor((scale.max - scale.min) / step); + + expect(scale.getValueForPixel(scale.left)).toBeCloseToTime({ + value: moment(scale.min).startOf('year'), + unit: 'hour', + }); + expect(scale.getValueForPixel(scale.right)).toBeCloseToTime({ + value: moment(scale.min + step * stepsAmount).endOf('year'), + unit: 'hour', + }); + }); + + it('should build the correct ticks', function() { + expect(getLabels(this.scale)).toEqual(['2005', '2006', '2007', '2008', '2009', '2010', '2011', '2012', '2013', '2014', '2015', '2016', '2017', '2018']); + }); + + it('should have ticks with accurate labels', function() { + var scale = this.scale; + var ticks = scale.getTicks(); + // pixelsPerTick is an approximation which assumes same number of milliseconds per year (not true) + // we use a threshold of 1 day so that we still match these values + var pixelsPerTick = scale.width / (ticks.length - 1); + + for (var i = 0; i < ticks.length - 1; i++) { + var offset = pixelsPerTick * i; + expect(scale.getValueForPixel(scale.left + offset)).toBeCloseToTime({ + value: moment(ticks[i].label + '-01-01'), + unit: 'day', + threshold: 1, + }); + } + }); + }); + + it('should get the correct label for a data value', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'x', + data: [null, 10, 3] + }], + labels: ['2015-01-01T20:00:00', '2015-01-02T21:00:00', '2015-01-03T22:00:00', '2015-01-05T23:00:00', '2015-01-07T03:00', '2015-01-08T10:00', '2015-01-10T12:00'], // days + }, + options: { + scales: { + x: { + type: 'time', + position: 'bottom', + ticks: { + source: 'labels', + autoSkip: false + } + } + } + } + }); + + var xScale = chart.scales.x; + var controller = chart.getDatasetMeta(0).controller; + expect(xScale.getLabelForValue(controller.getParsed(0)[xScale.id])).toBeTruthy(); + expect(xScale.getLabelForValue(controller.getParsed(0)[xScale.id])).toBe('Jan 1, 2015, 8:00:00 pm'); + expect(xScale.getLabelForValue(xScale.getValueForPixel(xScale.getPixelForTick(6)))).toBe('Jan 10, 2015, 12:00:00 pm'); + }); + + describe('when ticks.callback is specified', function() { + beforeEach(function() { + this.chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'x', + data: [0, 0] + }], + labels: ['2015-01-01T20:00:00', '2015-01-01T20:01:00'] + }, + options: { + scales: { + x: { + type: 'time', + time: { + displayFormats: { + second: 'h:mm:ss' + } + }, + ticks: { + callback: function(_, i) { + return '<' + i + '>'; + } + } + } + } + } + }); + this.scale = this.chart.scales.x; + }); + + it('should get the correct labels for ticks', function() { + var labels = getLabels(this.scale); + + expect(labels.length).toEqual(21); + expect(labels[0]).toEqual('<0>'); + expect(labels[labels.length - 1]).toEqual('<60>'); + }); + + it('should update ticks.callback correctly', function() { + var chart = this.chart; + chart.options.scales.x.ticks.callback = function(_, i) { + return '{' + i + '}'; + }; + chart.update(); + + var labels = getLabels(this.scale); + expect(labels.length).toEqual(21); + expect(labels[0]).toEqual('{0}'); + expect(labels[labels.length - 1]).toEqual('{60}'); + }); + }); + + it('should get the correct label when time is specified as a string', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'x', + data: [{x: '2015-01-01T20:00:00', y: 10}, {x: '2015-01-02T21:00:00', y: 3}] + }], + }, + options: { + scales: { + x: { + type: 'time', + position: 'bottom' + }, + } + } + }); + + var xScale = chart.scales.x; + var controller = chart.getDatasetMeta(0).controller; + var value = controller.getParsed(0)[xScale.id]; + expect(xScale.getLabelForValue(value)).toBeTruthy(); + expect(xScale.getLabelForValue(value)).toBe('Jan 1, 2015, 8:00:00 pm'); + }); + + it('should get the correct label for a data value by format', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'x', + data: [null, 10, 3] + }], + labels: ['2015-01-01T20:00:00', '2015-01-02T21:00:00', '2015-01-03T22:00:00', '2015-01-05T23:00:00', '2015-01-07T03:00', '2015-01-08T10:00', '2015-01-10T12:00'], // days + }, + options: { + scales: { + x: { + type: 'time', + time: { + unit: 'day', + displayFormats: { + day: 'YYYY-MM-DD' + } + }, + position: 'bottom', + ticks: { + source: 'labels', + autoSkip: false + } + } + } + } + }); + + var xScale = chart.scales.x; + for (const lbl of chart.data.labels) { + var dd = xScale._adapter.parse(lbl); + var parsed = lbl.split('T'); + expect(xScale.format(dd)).toBe(parsed[0]); + } + for (const lbl of chart.data.labels) { + var mm = xScale._adapter.parse(lbl); + var yearMonth = lbl.substring(0, 7); + expect(xScale.format(mm, 'YYYY-MM')).toBe(yearMonth); + } + }); + + it('should round to isoWeekday', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [{x: '2020-04-12T20:00:00', y: 1}, {x: '2020-04-13T20:00:00', y: 2}] + }] + }, + options: { + scales: { + x: { + type: 'time', + ticks: { + source: 'data' + }, + time: { + unit: 'week', + round: 'week', + isoWeekday: 1, + displayFormats: { + week: 'WW' + } + } + }, + } + } + }); + + expect(getLabels(chart.scales.x)).toEqual(['15', '16']); + }); + + it('should get the correct label for a timestamp', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'x', + data: [ + // Normally (at least with the moment.js adapter), times would be in + // the user's local time zone. To allow for more stable tests, our + // tests/index.js sets moment.js to use UTC; use `Z` here to match. + {t: +new Date('2018-01-08 05:14:23.234Z'), y: 10}, + {t: +new Date('2018-01-09 06:17:43.426Z'), y: 3} + ] + }], + }, + options: { + parsing: {xAxisKey: 't'}, + scales: { + x: { + type: 'time', + position: 'bottom' + }, + } + } + }); + + var xScale = chart.scales.x; + var controller = chart.getDatasetMeta(0).controller; + var label = xScale.getLabelForValue(controller.getParsed(0)[xScale.id]); + expect(label).toEqual('Jan 8, 2018, 5:14:23 am'); + }); + + it('should get the correct pixel for only one data in the dataset', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + labels: ['2016-05-27'], + datasets: [{ + xAxisID: 'x', + data: [5] + }] + }, + options: { + scales: { + x: { + display: true, + type: 'time' + } + } + } + }); + + var xScale = chart.scales.x; + var pixel = xScale.getPixelForValue(moment('2016-05-27').valueOf()); + + expect(xScale.getValueForPixel(pixel)).toEqual(moment(chart.data.labels[0]).valueOf()); + }); + + it('does not create a negative width chart when hidden', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + datasets: [{ + data: [] + }] + }, + options: { + scales: { + x: { + type: 'time', + ticks: { + min: moment().subtract(1, 'months'), + max: moment(), + } + }, + }, + responsive: true, + }, + }, { + wrapper: { + style: 'display: none', + }, + }); + expect(chart.scales.y.width).toEqual(0); + expect(chart.scales.y.maxWidth).toEqual(0); + expect(chart.width).toEqual(0); + }); + + describe('when ticks.source', function() { + describe('is "labels"', function() { + beforeEach(function() { + this.chart = window.acquireChart({ + type: 'line', + data: { + labels: ['2017', '2019', '2020', '2025', '2042'], + datasets: [{data: [0, 1, 2, 3, 4, 5]}] + }, + options: { + scales: { + x: { + type: 'time', + time: { + parser: 'YYYY' + }, + ticks: { + source: 'labels' + } + } + } + } + }); + }); + + it ('should generate ticks from "data.labels"', function() { + var scale = this.chart.scales.x; + + expect(scale.min).toEqual(+moment('2017', 'YYYY')); + expect(scale.max).toEqual(+moment('2042', 'YYYY')); + expect(getLabels(scale)).toEqual([ + '2017', '2019', '2020', '2025', '2042']); + }); + it ('should not add ticks for min and max if they extend the labels range', function() { + var chart = this.chart; + var scale = chart.scales.x; + var options = chart.options.scales.x; + + options.min = '2012'; + options.max = '2051'; + chart.update(); + + expect(scale.min).toEqual(+moment('2012', 'YYYY')); + expect(scale.max).toEqual(+moment('2051', 'YYYY')); + expect(getLabels(scale)).toEqual([ + '2017', '2019', '2020', '2025', '2042']); + }); + it ('should not duplicate ticks if min and max are the labels limits', function() { + var chart = this.chart; + var scale = chart.scales.x; + var options = chart.options.scales.x; + + options.min = '2017'; + options.max = '2042'; + chart.update(); + + expect(scale.min).toEqual(+moment('2017', 'YYYY')); + expect(scale.max).toEqual(+moment('2042', 'YYYY')); + expect(getLabels(scale)).toEqual([ + '2017', '2019', '2020', '2025', '2042']); + }); + it ('should correctly handle empty `data.labels` using "day" if `time.unit` is undefined`', function() { + var chart = this.chart; + var scale = chart.scales.x; + + chart.data.labels = []; + chart.update(); + + expect(scale.min).toEqual(+moment().startOf('day')); + expect(scale.max).toEqual(+moment().endOf('day') + 1); + expect(getLabels(scale)).toEqual([]); + }); + it ('should correctly handle empty `data.labels` using `time.unit`', function() { + var chart = this.chart; + var scale = chart.scales.x; + var options = chart.options.scales.x; + + options.time.unit = 'year'; + chart.data.labels = []; + chart.update(); + + expect(scale.min).toEqual(+moment().startOf('year')); + expect(scale.max).toEqual(+moment().endOf('year') + 1); + expect(getLabels(scale)).toEqual([]); + }); + }); + + describe('is "data"', function() { + beforeEach(function() { + this.chart = window.acquireChart({ + type: 'line', + data: { + labels: ['2017', '2019', '2020', '2025', '2042'], + datasets: [ + {data: [0, 1, 2, 3, 4, 5]}, + {data: [ + {x: '2018', y: 6}, + {x: '2020', y: 7}, + {x: '2043', y: 8} + ]} + ] + }, + options: { + scales: { + x: { + type: 'time', + time: { + parser: 'YYYY' + }, + ticks: { + source: 'data' + } + } + } + } + }); + }); + + it ('should generate ticks from "datasets.data"', function() { + var scale = this.chart.scales.x; + + expect(scale.min).toEqual(+moment('2017', 'YYYY')); + expect(scale.max).toEqual(+moment('2043', 'YYYY')); + expect(getLabels(scale)).toEqual([ + '2017', '2018', '2019', '2020', '2025', '2042', '2043']); + }); + it ('should not add ticks for min and max if they extend the labels range', function() { + var chart = this.chart; + var scale = chart.scales.x; + var options = chart.options.scales.x; + + options.min = '2012'; + options.max = '2051'; + chart.update(); + + expect(scale.min).toEqual(+moment('2012', 'YYYY')); + expect(scale.max).toEqual(+moment('2051', 'YYYY')); + expect(getLabels(scale)).toEqual([ + '2017', '2018', '2019', '2020', '2025', '2042', '2043']); + }); + it ('should not duplicate ticks if min and max are the labels limits', function() { + var chart = this.chart; + var scale = chart.scales.x; + var options = chart.options.scales.x; + + options.min = '2017'; + options.max = '2043'; + chart.update(); + + expect(scale.min).toEqual(+moment('2017', 'YYYY')); + expect(scale.max).toEqual(+moment('2043', 'YYYY')); + expect(getLabels(scale)).toEqual([ + '2017', '2018', '2019', '2020', '2025', '2042', '2043']); + }); + it ('should correctly handle empty `data.labels` using "day" if `time.unit` is undefined`', function() { + var chart = this.chart; + var scale = chart.scales.x; + + chart.data.labels = []; + chart.update(); + + expect(scale.min).toEqual(+moment('2018', 'YYYY')); + expect(scale.max).toEqual(+moment('2043', 'YYYY')); + expect(getLabels(scale)).toEqual([ + '2018', '2020', '2043']); + }); + it ('should correctly handle empty `data.labels` and hidden datasets using `time.unit`', function() { + var chart = this.chart; + var scale = chart.scales.x; + var options = chart.options.scales.x; + + options.time.unit = 'year'; + chart.data.labels = []; + var meta = chart.getDatasetMeta(1); + meta.hidden = true; + chart.update(); + + expect(scale.min).toEqual(+moment().startOf('year')); + expect(scale.max).toEqual(+moment().endOf('year') + 1); + expect(getLabels(scale)).toEqual([]); + }); + }); + }); + + [true, false].forEach(function(normalized) { + describe('when normalized is ' + normalized + ' and scale type', function() { + describe('is "timeseries"', function() { + beforeEach(function() { + this.chart = window.acquireChart({ + type: 'line', + data: { + labels: ['2017', '2019', '2020', '2025', '2042'], + datasets: [{data: [0, 1, 2, 3, 4]}] + }, + options: { + normalized, + scales: { + x: { + type: 'timeseries', + time: { + parser: 'YYYY' + }, + ticks: { + source: 'labels' + } + }, + y: { + display: false + } + } + } + }); + }); + + it ('should space data out with the same gap, whatever their time values', function() { + var scale = this.chart.scales.x; + var start = scale.left; + var slice = scale.width / 4; + + expect(scale.getPixelForValue(moment('2017').valueOf(), 0)).toBeCloseToPixel(start); + expect(scale.getPixelForValue(moment('2019').valueOf(), 1)).toBeCloseToPixel(start + slice); + expect(scale.getPixelForValue(moment('2020').valueOf(), 2)).toBeCloseToPixel(start + slice * 2); + expect(scale.getPixelForValue(moment('2025').valueOf(), 3)).toBeCloseToPixel(start + slice * 3); + expect(scale.getPixelForValue(moment('2042').valueOf(), 4)).toBeCloseToPixel(start + slice * 4); + }); + it ('should add a step before if scale.min is before the first data', function() { + var chart = this.chart; + var scale = chart.scales.x; + var options = chart.options.scales.x; + + options.min = '2012'; + chart.update(); + + var start = scale.left; + var slice = scale.width / 5; + + expect(scale.getPixelForValue(moment('2017').valueOf(), 1)).toBeCloseToPixel(86); + expect(scale.getPixelForValue(moment('2042').valueOf(), 5)).toBeCloseToPixel(start + slice * 5); + }); + it ('should add a step after if scale.max is after the last data', function() { + var chart = this.chart; + var scale = chart.scales.x; + var options = chart.options.scales.x; + + options.max = '2050'; + chart.update(); + + var start = scale.left; + + expect(scale.getPixelForValue(moment('2017').valueOf(), 0)).toBeCloseToPixel(start); + expect(scale.getPixelForValue(moment('2042').valueOf(), 4)).toBeCloseToPixel(388); + }); + it ('should add steps before and after if scale.min/max are outside the data range', function() { + var chart = this.chart; + var scale = chart.scales.x; + var options = chart.options.scales.x; + + options.min = '2012'; + options.max = '2050'; + chart.update(); + + expect(scale.getPixelForValue(moment('2017').valueOf(), 1)).toBeCloseToPixel(71); + expect(scale.getPixelForValue(moment('2042').valueOf(), 5)).toBeCloseToPixel(401); + }); + }); + describe('is "time"', function() { + beforeEach(function() { + this.chart = window.acquireChart({ + type: 'line', + data: { + labels: ['2017', '2019', '2020', '2025', '2042'], + datasets: [{data: [0, 1, 2, 3, 4, 5]}] + }, + options: { + scales: { + x: { + type: 'time', + time: { + parser: 'YYYY' + }, + ticks: { + source: 'labels' + } + }, + y: { + display: false + } + } + } + }); + }); + + it ('should space data out with a gap relative to their time values', function() { + var scale = this.chart.scales.x; + var start = scale.left; + var slice = scale.width / (2042 - 2017); + + expect(scale.getPixelForValue(moment('2017').valueOf(), 0)).toBeCloseToPixel(start); + expect(scale.getPixelForValue(moment('2019').valueOf(), 1)).toBeCloseToPixel(start + slice * (2019 - 2017)); + expect(scale.getPixelForValue(moment('2020').valueOf(), 2)).toBeCloseToPixel(start + slice * (2020 - 2017)); + expect(scale.getPixelForValue(moment('2025').valueOf(), 3)).toBeCloseToPixel(start + slice * (2025 - 2017)); + expect(scale.getPixelForValue(moment('2042').valueOf(), 4)).toBeCloseToPixel(start + slice * (2042 - 2017)); + }); + it ('should take in account scale min and max if outside the ticks range', function() { + var chart = this.chart; + var scale = chart.scales.x; + var options = chart.options.scales.x; + + options.min = '2012'; + options.max = '2050'; + chart.update(); + + var start = scale.left; + var slice = scale.width / (2050 - 2012); + + expect(scale.getPixelForValue(moment('2017').valueOf(), 0)).toBeCloseToPixel(start + slice * (2017 - 2012)); + expect(scale.getPixelForValue(moment('2019').valueOf(), 1)).toBeCloseToPixel(start + slice * (2019 - 2012)); + expect(scale.getPixelForValue(moment('2020').valueOf(), 2)).toBeCloseToPixel(start + slice * (2020 - 2012)); + expect(scale.getPixelForValue(moment('2025').valueOf(), 3)).toBeCloseToPixel(start + slice * (2025 - 2012)); + expect(scale.getPixelForValue(moment('2042').valueOf(), 4)).toBeCloseToPixel(start + slice * (2042 - 2012)); + }); + }); + }); + }); + + describe('when bounds', function() { + describe('is "data"', function() { + it ('should preserve the data range', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + labels: ['02/20 08:00', '02/21 09:00', '02/22 10:00', '02/23 11:00'], + datasets: [{data: [0, 1, 2, 3, 4, 5]}] + }, + options: { + scales: { + x: { + type: 'time', + bounds: 'data', + time: { + parser: 'MM/DD HH:mm', + unit: 'day' + } + }, + y: { + display: false + } + } + } + }); + + var scale = chart.scales.x; + + expect(scale.min).toEqual(+moment('02/20 08:00', 'MM/DD HH:mm')); + expect(scale.max).toEqual(+moment('02/23 11:00', 'MM/DD HH:mm')); + expect(scale.getPixelForValue(moment('02/20 08:00', 'MM/DD HH:mm').valueOf())).toBeCloseToPixel(scale.left); + expect(scale.getPixelForValue(moment('02/23 11:00', 'MM/DD HH:mm').valueOf())).toBeCloseToPixel(scale.left + scale.width); + expect(getLabels(scale)).toEqual([ + 'Feb 21', 'Feb 22', 'Feb 23']); + }); + }); + + describe('is "labels"', function() { + it('should preserve the label range', function() { + var chart = window.acquireChart({ + type: 'line', + data: { + labels: ['02/20 08:00', '02/21 09:00', '02/22 10:00', '02/23 11:00'], + datasets: [{data: [0, 1, 2, 3, 4, 5]}] + }, + options: { + scales: { + x: { + type: 'time', + bounds: 'ticks', + time: { + parser: 'MM/DD HH:mm', + unit: 'day' + } + }, + y: { + display: false + } + } + } + }); + + var scale = chart.scales.x; + var ticks = scale.getTicks(); + + expect(scale.min).toEqual(ticks[0].value); + expect(scale.max).toEqual(ticks[ticks.length - 1].value); + expect(scale.getPixelForValue(moment('02/20 08:00', 'MM/DD HH:mm').valueOf())).toBeCloseToPixel(60); + expect(scale.getPixelForValue(moment('02/23 11:00', 'MM/DD HH:mm').valueOf())).toBeCloseToPixel(426); + expect(getLabels(scale)).toEqual([ + 'Feb 20', 'Feb 21', 'Feb 22', 'Feb 23', 'Feb 24']); + }); + }); + }); + + describe('when min and/or max are defined', function() { + ['auto', 'data', 'labels'].forEach(function(source) { + ['data', 'ticks'].forEach(function(bounds) { + describe('and ticks.source is "' + source + '" and bounds "' + bounds + '"', function() { + beforeEach(function() { + this.chart = window.acquireChart({ + type: 'line', + data: { + labels: ['02/20 08:00', '02/21 09:00', '02/22 10:00', '02/23 11:00'], + datasets: [{data: [0, 1, 2, 3, 4, 5]}] + }, + options: { + scales: { + x: { + type: 'time', + bounds: bounds, + time: { + parser: 'MM/DD HH:mm', + unit: 'day' + }, + ticks: { + source: source + } + }, + y: { + display: false + } + } + } + }); + }); + + it ('should expand scale to the min/max range', function() { + var chart = this.chart; + var scale = chart.scales.x; + var options = chart.options.scales.x; + var min = '02/19 07:00'; + var max = '02/24 08:00'; + var minMillis = +moment(min, 'MM/DD HH:mm'); + var maxMillis = +moment(max, 'MM/DD HH:mm'); + + options.min = min; + options.max = max; + chart.update(); + + expect(scale.min).toEqual(minMillis); + expect(scale.max).toEqual(maxMillis); + expect(scale.getPixelForValue(minMillis)).toBeCloseToPixel(scale.left); + expect(scale.getPixelForValue(maxMillis)).toBeCloseToPixel(scale.left + scale.width); + scale.getTicks().forEach(function(tick) { + expect(tick.value >= minMillis).toBeTruthy(); + expect(tick.value <= maxMillis).toBeTruthy(); + }); + }); + it ('should shrink scale to the min/max range', function() { + var chart = this.chart; + var scale = chart.scales.x; + var options = chart.options.scales.x; + var min = '02/21 07:00'; + var max = '02/22 20:00'; + var minMillis = +moment(min, 'MM/DD HH:mm'); + var maxMillis = +moment(max, 'MM/DD HH:mm'); + + options.min = min; + options.max = max; + chart.update(); + + expect(scale.min).toEqual(minMillis); + expect(scale.max).toEqual(maxMillis); + expect(scale.getPixelForValue(minMillis)).toBeCloseToPixel(scale.left); + expect(scale.getPixelForValue(maxMillis)).toBeCloseToPixel(scale.left + scale.width); + scale.getTicks().forEach(function(tick) { + expect(tick.value >= minMillis).toBeTruthy(); + expect(tick.value <= maxMillis).toBeTruthy(); + }); + }); + }); + }); + }); + }); + + ['auto', 'data', 'labels'].forEach(function(source) { + ['timeseries', 'time'].forEach(function(type) { + describe('when ticks.source is "' + source + '" and scale type is "' + type + '"', function() { + beforeEach(function() { + this.chart = window.acquireChart({ + type: 'line', + data: { + labels: ['2017', '2018', '2019', '2020', '2021'], + datasets: [{data: [0, 1, 2, 3, 4]}] + }, + options: { + scales: { + x: { + type: type, + time: { + parser: 'YYYY', + unit: 'year' + }, + ticks: { + source: source + } + } + } + } + }); + }); + + it ('should not add offset from the edges', function() { + var scale = this.chart.scales.x; + + expect(scale.getPixelForValue(moment('2017').valueOf())).toBeCloseToPixel(scale.left); + expect(scale.getPixelForValue(moment('2021').valueOf())).toBeCloseToPixel(scale.left + scale.width); + }); + + it ('should add offset from the edges if offset is true', function() { + var chart = this.chart; + var scale = chart.scales.x; + var options = chart.options.scales.x; + + options.offset = true; + chart.update(); + + var numTicks = scale.ticks.length; + var firstTickInterval = scale.getPixelForTick(1) - scale.getPixelForTick(0); + var lastTickInterval = scale.getPixelForTick(numTicks - 1) - scale.getPixelForTick(numTicks - 2); + + expect(scale.getPixelForValue(moment('2017').valueOf())).toBeCloseToPixel(scale.left + firstTickInterval / 2); + expect(scale.getPixelForValue(moment('2021').valueOf())).toBeCloseToPixel(scale.left + scale.width - lastTickInterval / 2); + }); + + it ('should not add offset if min and max extend the labels range', function() { + var chart = this.chart; + var scale = chart.scales.x; + var options = chart.options.scales.x; + + options.min = '2012'; + options.max = '2051'; + chart.update(); + + expect(scale.getPixelForValue(moment('2012').valueOf())).toBeCloseToPixel(scale.left); + expect(scale.getPixelForValue(moment('2051').valueOf())).toBeCloseToPixel(scale.left + scale.width); + }); + }); + }); + }); + + it ('should handle offset when there are more data points than ticks', function() { + const chart = window.acquireChart({ + type: 'bar', + data: { + datasets: [{ + data: [{x: 631180800000, y: '31.84'}, {x: 631267200000, y: '30.89'}, {x: 631353600000, y: '33.00'}, {x: 631440000000, y: '33.52'}, {x: 631526400000, y: '32.24'}, {x: 631785600000, y: '32.74'}, {x: 631872000000, y: '31.45'}, {x: 631958400000, y: '32.60'}, {x: 632044800000, y: '31.77'}, {x: 632131200000, y: '32.45'}, {x: 632390400000, y: '31.13'}, {x: 632476800000, y: '31.82'}, {x: 632563200000, y: '30.81'}, {x: 632649600000, y: '30.07'}, {x: 632736000000, y: '29.31'}, {x: 632995200000, y: '29.82'}, {x: 633081600000, y: '30.20'}, {x: 633168000000, y: '30.78'}, {x: 633254400000, y: '30.72'}, {x: 633340800000, y: '31.62'}, {x: 633600000000, y: '30.64'}, {x: 633686400000, y: '32.36'}, {x: 633772800000, y: '34.66'}, {x: 633859200000, y: '33.96'}, {x: 633945600000, y: '34.20'}, {x: 634204800000, y: '32.20'}, {x: 634291200000, y: '32.44'}, {x: 634377600000, y: '32.72'}, {x: 634464000000, y: '32.95'}, {x: 634550400000, y: '32.95'}, {x: 634809600000, y: '30.88'}, {x: 634896000000, y: '29.44'}, {x: 634982400000, y: '29.36'}, {x: 635068800000, y: '28.84'}, {x: 635155200000, y: '30.85'}, {x: 635414400000, y: '32.00'}, {x: 635500800000, y: '32.74'}, {x: 635587200000, y: '33.16'}, {x: 635673600000, y: '34.73'}, {x: 635760000000, y: '32.89'}, {x: 636019200000, y: '32.41'}, {x: 636105600000, y: '31.15'}, {x: 636192000000, y: '30.63'}, {x: 636278400000, y: '29.60'}, {x: 636364800000, y: '29.31'}, {x: 636624000000, y: '29.83'}, {x: 636710400000, y: '27.97'}, {x: 636796800000, y: '26.18'}, {x: 636883200000, y: '26.06'}, {x: 636969600000, y: '26.34'}, {x: 637228800000, y: '27.75'}, {x: 637315200000, y: '29.05'}, {x: 637401600000, y: '28.82'}, {x: 637488000000, y: '29.43'}, {x: 637574400000, y: '29.53'}, {x: 637833600000, y: '28.50'}, {x: 637920000000, y: '28.87'}, {x: 638006400000, y: '28.11'}, {x: 638092800000, y: '27.79'}, {x: 638179200000, y: '28.18'}, {x: 638438400000, y: '28.27'}, {x: 638524800000, y: '28.29'}, {x: 638611200000, y: '29.63'}, {x: 638697600000, y: '29.13'}, {x: 638784000000, y: '26.57'}, {x: 639039600000, y: '27.19'}, {x: 639126000000, y: '27.48'}, {x: 639212400000, y: '27.79'}, {x: 639298800000, y: '28.48'}, {x: 639385200000, y: '27.88'}, {x: 639644400000, y: '25.63'}, {x: 639730800000, y: '25.02'}, {x: 639817200000, y: '25.26'}, {x: 639903600000, y: '25.00'}, {x: 639990000000, y: '26.23'}, {x: 640249200000, y: '26.22'}, {x: 640335600000, y: '26.36'}, {x: 640422000000, y: '25.45'}, {x: 640508400000, y: '24.62'}, {x: 640594800000, y: '26.65'}, {x: 640854000000, y: '26.28'}, {x: 640940400000, y: '27.25'}, {x: 641026800000, y: '25.93'}], + backgroundColor: '#ff6666' + }] + }, + options: { + scales: { + x: { + type: 'timeseries', + offset: true, + ticks: { + source: 'data', + autoSkip: true, + autoSkipPadding: 0, + maxRotation: 0 + } + }, + y: { + type: 'linear', + border: { + display: false + } + } + } + }, + plugins: { + legend: false + } + }); + const scale = chart.scales.x; + expect(scale.getPixelForDecimal(0)).toBeCloseToPixel(29); + expect(scale.getPixelForDecimal(1.0)).toBeCloseToPixel(512); + }); + + ['data', 'labels'].forEach(function(source) { + ['timeseries', 'time'].forEach(function(type) { + describe('when ticks.source is "' + source + '" and scale type is "' + type + '"', function() { + beforeEach(function() { + this.chart = window.acquireChart({ + type: 'line', + data: { + labels: ['2017', '2019', '2020', '2025', '2042'], + datasets: [{data: [0, 1, 2, 3, 4, 5]}] + }, + options: { + scales: { + x: { + id: 'x', + type: type, + time: { + parser: 'YYYY' + }, + ticks: { + source: source + } + } + } + } + }); + }); + + it ('should add offset if min and max extend the labels range and offset is true', function() { + var chart = this.chart; + var scale = chart.scales.x; + var options = chart.options.scales.x; + + options.min = '2012'; + options.max = '2051'; + options.offset = true; + chart.update(); + + var numTicks = scale.ticks.length; + var firstTickInterval = scale.getPixelForTick(1) - scale.getPixelForTick(0); + var lastTickInterval = scale.getPixelForTick(numTicks - 1) - scale.getPixelForTick(numTicks - 2); + expect(scale.getPixelForValue(moment('2012').valueOf())).toBeCloseToPixel(scale.left + firstTickInterval / 2); + expect(scale.getPixelForValue(moment('2051').valueOf())).toBeCloseToPixel(scale.left + scale.width - lastTickInterval / 2); + }); + }); + }); + }); + + describe('Deprecations', function() { + describe('options.time.displayFormats', function() { + it('should generate defaults from adapter presets', function() { + var chart = window.acquireChart({ + type: 'line', + data: {}, + options: { + scales: { + x: { + type: 'time' + } + } + } + }); + + // NOTE: the test suite is configured to use moment + var expected = { + datetime: 'MMM D, YYYY, h:mm:ss a', + millisecond: 'h:mm:ss.SSS a', + second: 'h:mm:ss a', + minute: 'h:mm a', + hour: 'hA', + day: 'MMM D', + week: 'll', + month: 'MMM YYYY', + quarter: '[Q]Q - YYYY', + year: 'YYYY' + }; + + expect(chart.scales.x.options.time.displayFormats).toEqual(expected); + expect(chart.options.scales.x.time.displayFormats).toEqual(expected); + }); + + it('should merge user formats with adapter presets', function() { + var chart = window.acquireChart({ + type: 'line', + data: {}, + options: { + scales: { + x: { + type: 'time', + time: { + displayFormats: { + millisecond: 'foo', + hour: 'bar', + month: 'bla' + } + } + } + } + } + }); + + // NOTE: the test suite is configured to use moment + var expected = { + datetime: 'MMM D, YYYY, h:mm:ss a', + millisecond: 'foo', + second: 'h:mm:ss a', + minute: 'h:mm a', + hour: 'bar', + day: 'MMM D', + week: 'll', + month: 'bla', + quarter: '[Q]Q - YYYY', + year: 'YYYY' + }; + + expect(chart.scales.x.options.time.displayFormats).toEqual(expected); + expect(chart.options.scales.x.time.displayFormats).toEqual(expected); + }); + }); + }); + + it('should pass chart options to date adapter', function() { + let chartOptions; + + Chart._adapters._date.override({ + init(options) { + chartOptions = options; + } + }); + + var chart = window.acquireChart({ + type: 'line', + data: {}, + options: { + locale: 'es', + scales: { + x: { + type: 'time' + }, + } + } + }); + + expect(chartOptions).toEqual(chart.options); + }); + + it('should pass timestamp to ticks callback', () => { + let callbackValue; + window.acquireChart({ + type: 'line', + data: { + datasets: [{ + xAxisID: 'x', + data: [0, 0] + }], + labels: ['2015-01-01T20:00:00', '2015-01-01T20:01:00'] + }, + options: { + scales: { + x: { + type: 'time', + ticks: { + callback(value) { + callbackValue = value; + return value; + } + } + } + } + } + }); + + expect(typeof callbackValue).toBe('number'); + }); +}); diff --git a/test/types/.eslintrc.yml b/test/types/.eslintrc.yml new file mode 100644 index 00000000000..07b109145d8 --- /dev/null +++ b/test/types/.eslintrc.yml @@ -0,0 +1,6 @@ +rules: + '@typescript-eslint/no-unused-vars': 'off' + object-curly-spacing: ["warn", "always"] + '@typescript-eslint/no-empty-interface': "warn" + '@typescript-eslint/ban-types': "warn" + '@typescript-eslint/adjacent-overload-signatures': "warn" diff --git a/test/types/animation.ts b/test/types/animation.ts new file mode 100644 index 00000000000..634862404c0 --- /dev/null +++ b/test/types/animation.ts @@ -0,0 +1,70 @@ +import { Chart } from '../../src/types.js'; + +const chart = new Chart('id', { + type: 'bar', + data: { + labels: [], + datasets: [{ + data: [] + }] + }, + options: { + animation: false, + animations: { + colors: false, + numbers: { + properties: ['a', 'b'], + type: 'number', + from: 0, + to: 10, + delay: (ctx) => ctx.dataIndex * 100, + duration: (ctx) => ctx.datasetIndex * 1000, + loop: true, + easing: 'linear' + } + }, + transitions: { + show: { + animation: { + duration: 10 + }, + animations: { + numbers: false + } + }, + custom: { + animation: { + duration: 10 + } + } + } + }, +}); + + +const pie = new Chart('id', { + type: 'pie', + data: { + labels: [], + datasets: [{ + data: [] + }] + }, + options: { + animation: false, + } +}); + + +const polarArea = new Chart('id', { + type: 'polarArea', + data: { + labels: [], + datasets: [{ + data: [] + }] + }, + options: { + animation: false, + } +}); diff --git a/test/types/autogen.js b/test/types/autogen.js new file mode 100644 index 00000000000..cd8768a7330 --- /dev/null +++ b/test/types/autogen.js @@ -0,0 +1,25 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { fileURLToPath } from 'url'; +import * as helpers from '../../dist/helpers.js'; + +const __dirname = fileURLToPath(new URL('.', import.meta.url)); + +let fd; + +try { + const fn = path.resolve(__dirname, 'autogen_helpers.ts'); + fd = fs.openSync(fn, 'w+'); + fs.writeSync(fd, 'import * as helpers from \'../../dist/helpers/index.js\';\n\n'); + + fs.writeSync(fd, 'const testKeys: unknown[] = [];\n'); + for (const key of Object.keys(helpers)) { + if (key[0] !== '_' && typeof helpers[key] === 'function') { + fs.writeSync(fd, `testKeys.push(helpers.${key});\n`); + } + } +} finally { + if (fd !== undefined) { + fs.closeSync(fd); + } +} diff --git a/test/types/chart_types.ts b/test/types/chart_types.ts new file mode 100644 index 00000000000..f1dfb0ff6d2 --- /dev/null +++ b/test/types/chart_types.ts @@ -0,0 +1,69 @@ +import { Chart } from '../../src/types.js'; + +const chart = new Chart('chart', { + type: 'bar', + data: { + labels: ['1', '2', '3'], + datasets: [{ + data: [1, 2, 3] + }, + { + data: [1, 2, 3], + categoryPercentage: 10 + }], + } +}); + +const chart2 = new Chart('chart', { + type: 'bar', + data: { + labels: ['1', '2', '3'], + datasets: [{ + type: 'line', + data: [1, 2, 3], + // @ts-expect-error should not allow bar properties to be defined in a line dataset + categoryPercentage: 10 + }, + { + type: 'line', + pointBackgroundColor: 'red', + data: [1, 2, 3] + }, + { + data: [1, 2, 3], + categoryPercentage: 10 + }], + } +}); + +const chart3 = new Chart('chart', { + data: { + labels: ['1', '2', '3'], + datasets: [{ + type: 'bar', + data: [1, 2, 3], + categoryPercentage: 10 + }, + { + type: 'bar', + data: [1, 2, 3], + // @ts-expect-error should not allow line properties to be defined in a bar dataset + pointBackgroundColor: 'red', + }], + } +}); + +// @ts-expect-error all datasets should have a type property or a default fallback type should be set +const chart4 = new Chart('chart', { + data: { + labels: ['1', '2', '3'], + datasets: [{ + type: 'bar', + data: [1, 2, 3], + categoryPercentage: 10 + }, + { + data: [1, 2, 3] + }], + } +}); diff --git a/test/types/controllers/bar_floating_data.ts b/test/types/controllers/bar_floating_data.ts new file mode 100644 index 00000000000..36edfb8fa36 --- /dev/null +++ b/test/types/controllers/bar_floating_data.ts @@ -0,0 +1,11 @@ +import { Chart } from '../../../src/types.js'; + +const chart = new Chart('id', { + type: 'bar', + data: { + labels: ['1', '2', '3'], + datasets: [{ + data: [[1, 2], [3, 4], [5, 6]] + }] + }, +}); diff --git a/test/types/controllers/bubble_chart_options.ts b/test/types/controllers/bubble_chart_options.ts new file mode 100644 index 00000000000..0143083882c --- /dev/null +++ b/test/types/controllers/bubble_chart_options.ts @@ -0,0 +1,22 @@ +import { Chart, ChartOptions } from '../../../src/types.js'; + +const chart = new Chart('test', { + type: 'bubble', + data: { + datasets: [] + }, + options: { + scales: { + x: { + min: 0, + max: 30, + ticks: {} + }, + y: { + min: 0, + max: 30, + ticks: {}, + }, + } + } +}); diff --git a/test/types/controllers/doughnut_meta_total.ts b/test/types/controllers/doughnut_meta_total.ts new file mode 100644 index 00000000000..d749765d912 --- /dev/null +++ b/test/types/controllers/doughnut_meta_total.ts @@ -0,0 +1,16 @@ +import { Chart, ChartMeta, Element } from '../../../src/types.js'; + +const chart = new Chart('id', { + type: 'doughnut', + data: { + labels: [], + datasets: [{ + data: [], + }] + }, +}); + +// A cast is required because the exact type of ChartMeta will vary with +// mixed charts +const meta = >chart.getDatasetMeta(0); +const total = meta.total; diff --git a/test/types/controllers/doughnut_offset.ts b/test/types/controllers/doughnut_offset.ts new file mode 100644 index 00000000000..ed70838188f --- /dev/null +++ b/test/types/controllers/doughnut_offset.ts @@ -0,0 +1,15 @@ +import { Chart, ChartMeta, Element } from '../../../src/types.js'; + +const chart = new Chart('id', { + type: 'doughnut', + data: { + labels: [], + datasets: [{ + data: [], + offset: 40, + }] + }, + options: { + offset: 20, + } +}); diff --git a/test/types/controllers/doughnut_outer_radius.ts b/test/types/controllers/doughnut_outer_radius.ts new file mode 100644 index 00000000000..7d651ed0038 --- /dev/null +++ b/test/types/controllers/doughnut_outer_radius.ts @@ -0,0 +1,14 @@ +import { Chart } from '../../../src/types.js'; + +const chart = new Chart('id', { + type: 'doughnut', + data: { + labels: [], + datasets: [{ + data: [], + }] + }, + options: { + radius: () => Math.random() > 0.5 ? 50 : '50%', + } +}); diff --git a/test/types/controllers/doughnut_spacing_offset.ts b/test/types/controllers/doughnut_spacing_offset.ts new file mode 100644 index 00000000000..228f8f02505 --- /dev/null +++ b/test/types/controllers/doughnut_spacing_offset.ts @@ -0,0 +1,29 @@ +import { Chart, ChartMeta, Element } from '../../../src/types.js'; + +const chart = new Chart('id', { + type: 'doughnut', + data: { + datasets: [{ + data: [10, 20, 40, 50, 5], + label: 'Dataset 1', + backgroundColor: [ + 'red', + 'orange', + 'yellow', + 'green', + 'blue' + ] + }], + labels: [ + 'Item 1', + 'Item 2', + 'Item 3', + 'Item 4', + 'Item 5' + ], + }, + options: { + spacing: 50, + offset: [0, 50, 0, 0, 0], + } +}); diff --git a/test/types/controllers/line_scriptable_parsed_data.ts b/test/types/controllers/line_scriptable_parsed_data.ts new file mode 100644 index 00000000000..e484e09cff9 --- /dev/null +++ b/test/types/controllers/line_scriptable_parsed_data.ts @@ -0,0 +1,14 @@ +import { Chart } from '../../../src/types.js'; + +const chart = new Chart('id', { + type: 'line', + data: { + labels: [], + datasets: [{ + data: [], + backgroundColor: (context) => { + return context.parsed.y > 10 ? 'green' : 'red'; + } + }] + }, +}); diff --git a/test/types/controllers/line_segments.ts b/test/types/controllers/line_segments.ts new file mode 100644 index 00000000000..2e4d0170741 --- /dev/null +++ b/test/types/controllers/line_segments.ts @@ -0,0 +1,16 @@ +import { Chart } from '../../../src/types.js'; + +const chart = new Chart('id', { + type: 'line', + data: { + labels: [], + datasets: [{ + data: [], + segment: { + backgroundColor: ctx => ctx.p0.skip ? 'transparent' : undefined, + borderColor: ctx => ctx.p0.skip ? 'gray' : undefined, + borderWidth: ctx => ctx.p1.parsed.y > 10 ? 5 : undefined, + } + }] + }, +}); diff --git a/test/types/controllers/line_span_gaps.ts b/test/types/controllers/line_span_gaps.ts new file mode 100644 index 00000000000..38c94b67387 --- /dev/null +++ b/test/types/controllers/line_span_gaps.ts @@ -0,0 +1,32 @@ +import { Chart } from '../../../src/types.js'; + +const chart = new Chart('id', { + type: 'line', + data: { + datasets: [ + { + label: 'Cats', + data: [], + } + ] + }, + options: { + elements: { + line: { + spanGaps: true + } + }, + scales: { + x: { + type: 'linear', + min: 1, + max: 10 + }, + y: { + type: 'linear', + min: 0, + max: 50 + } + } + } +}); diff --git a/test/types/controllers/line_styling_array.ts b/test/types/controllers/line_styling_array.ts new file mode 100644 index 00000000000..673eb386fb1 --- /dev/null +++ b/test/types/controllers/line_styling_array.ts @@ -0,0 +1,13 @@ +import { Chart } from '../../../src/types.js'; + +const chart = new Chart('id', { + type: 'line', + data: { + labels: [], + datasets: [{ + data: [], + backgroundColor: ['red', 'blue'], + hoverBackgroundColor: ['red', 'blue'], + }] + }, +}); diff --git a/test/types/controllers/radar_dataset_indexable_options.ts b/test/types/controllers/radar_dataset_indexable_options.ts new file mode 100644 index 00000000000..28aeecf539e --- /dev/null +++ b/test/types/controllers/radar_dataset_indexable_options.ts @@ -0,0 +1,26 @@ +import { Chart, ChartOptions } from '../../../src/types.js'; + +const chart = new Chart('test', { + type: 'radar', + data: { + labels: ['a', 'b', 'c'], + datasets: [{ + data: [1, 2, 3], + backgroundColor: ['red', 'green', 'blue'], + borderColor: ['red', 'green', 'blue'], + hoverRadius: [1, 2, 3], + pointBackgroundColor: ['red', 'green', 'blue'], + pointBorderColor: ['red', 'green', 'blue'], + pointBorderWidth: [1, 2, 3], + pointHitRadius: [1, 2, 3], + pointHoverBackgroundColor: ['red', 'green', 'blue'], + pointHoverBorderColor: ['red', 'green', 'blue'], + pointHoverBorderWidth: [1, 2, 3], + pointHoverRadius: [1, 2, 3], + pointRadius: [1, 2, 3], + pointRotation: [1, 2, 3], + pointStyle: ['circle', 'cross', 'crossRot'], + radius: [1, 2, 3], + }] + }, +}); diff --git a/test/types/data_types.ts b/test/types/data_types.ts new file mode 100644 index 00000000000..48a60598ffe --- /dev/null +++ b/test/types/data_types.ts @@ -0,0 +1,20 @@ +import { Chart } from '../../src/types.js'; + +const chart = new Chart('chart', { + type: 'bar', + data: { + labels: ['1', '2', '3'], + datasets: [{ data: [[1, 2], [1, 2], [1, 2]] }], + } +}); + +const chart2 = new Chart('chart2', { + type: 'bar', + data: { + datasets: [{ + data: [{ id: 'Sales', nested: { value: 1500 } }, { id: 'Purchases', nested: { value: 500 } }], + }], + }, + options: { parsing: { xAxisKey: 'id', yAxisKey: 'nested.value' }, + }, +}); diff --git a/test/types/dataset_null_data.ts b/test/types/dataset_null_data.ts new file mode 100644 index 00000000000..9b02635ad5b --- /dev/null +++ b/test/types/dataset_null_data.ts @@ -0,0 +1,16 @@ +import type { ChartDataset } from '../../src/types.js'; + +const dataset: ChartDataset = { + data: [10, null, 20], +}; + +const lineDataset: ChartDataset<'line'> = { + data: [10, null, 20], +}; +const scatterDataset: ChartDataset<'scatter'> = { + data: [10, null, 20], +}; +const radarDataset: ChartDataset<'radar'> = { + data: [10, null, 20], +}; + diff --git a/test/types/date_adapter.ts b/test/types/date_adapter.ts new file mode 100644 index 00000000000..c514ae7eba5 --- /dev/null +++ b/test/types/date_adapter.ts @@ -0,0 +1,14 @@ +import { _adapters } from '../../src/types.js'; + +_adapters._date.override<{myOption: boolean}>({ + init() { + const booleanOption: boolean = this.options.myOption; + + // @ts-expect-error Options is readonly. + this.options = {}; + }, + // @ts-expect-error Should return string. + format(timestamp) { + const numberArg: number = timestamp; + } +}); diff --git a/test/types/defaults.ts b/test/types/defaults.ts new file mode 100644 index 00000000000..ae3eff23ac5 --- /dev/null +++ b/test/types/defaults.ts @@ -0,0 +1,46 @@ +import { Chart } from '../../src/types.js'; + +Chart.defaults.scales.time.time.minUnit = 'day'; + +Chart.defaults.plugins.title.display = false; + +Chart.defaults.datasets.bar.backgroundColor = 'red'; + +Chart.defaults.animation = { duration: 500 }; + +Chart.defaults.font.size = 8; + +// @ts-expect-error should be number +Chart.defaults.font.size = '8'; + +// @ts-expect-error should be number +Chart.defaults.font.size = () => '10'; + +Chart.defaults.backgroundColor = 'red'; +Chart.defaults.backgroundColor = ['red', 'blue']; +Chart.defaults.backgroundColor = (ctx) => ctx.datasetIndex % 2 === 0 ? 'red' : 'blue'; + +Chart.defaults.borderColor = 'red'; +Chart.defaults.borderColor = ['red', 'blue']; +Chart.defaults.borderColor = (ctx) => ctx.datasetIndex % 2 === 0 ? 'red' : 'blue'; + +Chart.defaults.hoverBackgroundColor = 'red'; +Chart.defaults.hoverBackgroundColor = ['red', 'blue']; +Chart.defaults.hoverBackgroundColor = (ctx) => ctx.datasetIndex % 2 === 0 ? 'red' : 'blue'; + +Chart.defaults.hoverBorderColor = 'red'; +Chart.defaults.hoverBorderColor = ['red', 'blue']; +Chart.defaults.hoverBorderColor = (ctx) => ctx.datasetIndex % 2 === 0 ? 'red' : 'blue'; + +Chart.defaults.font = { + family: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", + size: 10 +}; + +Chart.defaults.layout = { + padding: { + bottom: 10, + }, +}; + +Chart.defaults.plugins.tooltip.boxPadding = 3; diff --git a/test/types/elements/scriptable_element_options.ts b/test/types/elements/scriptable_element_options.ts new file mode 100644 index 00000000000..90dd3ee14a5 --- /dev/null +++ b/test/types/elements/scriptable_element_options.ts @@ -0,0 +1,50 @@ +import { Chart } from '../../../src/types.js'; + +const chart = new Chart('id', { + type: 'line', + data: { + labels: [], + datasets: [] + }, + options: { + elements: { + line: { + borderWidth: () => 2, + }, + point: { + pointStyle: (ctx) => 'star', + } + } + } +}); + +const chart2 = new Chart('id', { + type: 'bar', + data: { + labels: [], + datasets: [] + }, + options: { + elements: { + bar: { + borderWidth: (ctx) => 2, + } + } + } +}); + +const chart3 = new Chart('id', { + type: 'doughnut', + data: { + labels: [], + datasets: [] + }, + options: { + elements: { + arc: { + borderWidth: (ctx) => 3, + borderJoinStyle: (ctx) => 'miter' + } + } + } +}); diff --git a/test/types/extensions/plugin.ts b/test/types/extensions/plugin.ts new file mode 100644 index 00000000000..79bce4f5a59 --- /dev/null +++ b/test/types/extensions/plugin.ts @@ -0,0 +1,28 @@ +import { Chart } from '../../../src/types.js'; + +Chart.register({ + id: 'my-plugin', + afterDraw: (chart: Chart) => { + // noop + } +}); + +Chart.register([{ + id: 'my-plugin', + afterDraw: (chart: Chart) => { + // noop + }, +}]); + +// @ts-expect-error not assignable +Chart.register({ + id: 'fail', + noComponentHasThisMethod: () => 'test' +}); + +// @ts-expect-error missing id +Chart.register([{ + afterDraw: (chart: Chart) => { + // noop + }, +}]); diff --git a/test/types/extensions/scale.ts b/test/types/extensions/scale.ts new file mode 100644 index 00000000000..3623eb3e7dc --- /dev/null +++ b/test/types/extensions/scale.ts @@ -0,0 +1,48 @@ +import { AnyObject } from '../../../src/types/basic.js'; +import { CartesianScaleOptions, Chart, Scale } from '../../../src/types.js'; + +export type TestScaleOptions = CartesianScaleOptions & { + testOption?: boolean +} + +export class TestScale extends Scale { + static id: 'test'; + + getBasePixel(): number { + return 0; + } + + testMethod(): void { + // + } +} + +declare module '../../../src/types/index.js' { + interface CartesianScaleTypeRegistry { + test: { + options: TestScaleOptions + } + } +} + + +Chart.register(TestScale); + +const chart = new Chart('id', { + type: 'line', + data: { + datasets: [] + }, + options: { + scales: { + x: { + type: 'test', + position: 'bottom', + testOption: true, + min: 0 + } + } + } +}); + +Chart.unregister([TestScale]); diff --git a/test/types/helpers/dom.ts b/test/types/helpers/dom.ts new file mode 100644 index 00000000000..dc1c44f5066 --- /dev/null +++ b/test/types/helpers/dom.ts @@ -0,0 +1,11 @@ +import { getRelativePosition } from '../../../src/helpers/helpers.dom.js'; +import { Chart, ChartOptions } from '../../../src/types.js'; + +const chart = new Chart('test', { + type: 'line', + data: { + datasets: [] + } +}); + +getRelativePosition(new MouseEvent('click'), chart); diff --git a/test/types/helpers/options.ts b/test/types/helpers/options.ts new file mode 100644 index 00000000000..454f2495758 --- /dev/null +++ b/test/types/helpers/options.ts @@ -0,0 +1,10 @@ +import { createContext } from '../../../src/helpers/helpers.options.js'; + +const context1 = createContext(null, { type: 'test1', parent: true }); +const context2 = createContext(context1, { type: 'test2' }); + +const sSest: string = context1.type + context2.type; +const bTest: boolean = context1.parent && context2.parent; + +// @ts-expect-error Property 'notThere' does not exist on type '{ type: string; parent: boolean; } & { type: string; }' +context2.notThere = ''; diff --git a/test/types/interaction.ts b/test/types/interaction.ts new file mode 100644 index 00000000000..234a98bda98 --- /dev/null +++ b/test/types/interaction.ts @@ -0,0 +1,17 @@ +import { + Chart, ChartData, ChartConfiguration, Element +} from '../../src/types.js'; + +const data: ChartData<'line'> = { datasets: [] }; +const chartItem = 'item'; +const config: ChartConfiguration<'line'> = { type: 'line', data }; +const chart: Chart = new Chart(chartItem, config); + +type Item = { + element: Element, + datasetIndex: number, + index: number +} + +const elements: Item[] = []; +chart.updateHoverStyle(elements, 'dataset', true); diff --git a/test/types/layout/position.ts b/test/types/layout/position.ts new file mode 100644 index 00000000000..10233d681a7 --- /dev/null +++ b/test/types/layout/position.ts @@ -0,0 +1,11 @@ +import type { LayoutPosition } from '../../../src/types.js'; + +const left: LayoutPosition = 'left'; +const right: LayoutPosition = 'right'; +const top: LayoutPosition = 'top'; +const bottom: LayoutPosition = 'bottom'; +const center: LayoutPosition = 'center'; +const axis: LayoutPosition = { x: 10 }; + +// @ts-expect-error invalid position +const invalid: LayoutPosition = 'none'; diff --git a/test/types/options.ts b/test/types/options.ts new file mode 100644 index 00000000000..21d0ccf17c7 --- /dev/null +++ b/test/types/options.ts @@ -0,0 +1,45 @@ +import { Chart, ChartOptions, ChartType, DoughnutControllerChartOptions } from '../../src/types.js'; + +const chart = new Chart('test', { + type: 'bar', + data: { + labels: ['a'], + datasets: [{ + data: [1], + }, { + type: 'line', + data: [{ x: 1, y: 1 }] + }] + }, + options: { + animation: { + duration: 500 + }, + backgroundColor: 'red', + datasets: { + line: { + animation: { + duration: 600 + }, + backgroundColor: 'blue', + } + }, + elements: { + point: { + backgroundColor: 'red' + } + } + } +}); + +const doughnutOptions: DoughnutControllerChartOptions = { + circumference: 360, + cutout: '50%', + offset: 0, + radius: 100, + rotation: 0, + spacing: 0, + animation: false, +}; + +const chartOptions: ChartOptions = doughnutOptions; diff --git a/test/types/overrides.ts b/test/types/overrides.ts new file mode 100644 index 00000000000..b4da296a322 --- /dev/null +++ b/test/types/overrides.ts @@ -0,0 +1,10 @@ +import { Chart } from '../../src/types.js'; + +Chart.overrides.bar.scales.x.type = 'time'; + +Chart.overrides.bar.plugins.title.display = false; + +Chart.overrides.line.datasets.bar.backgroundColor = 'red'; + +Chart.overrides.line.animation = false; +Chart.overrides.line.datasets.bar.animation = { duration: 100 }; diff --git a/test/types/parsed.data.type.ts b/test/types/parsed.data.type.ts new file mode 100644 index 00000000000..86f34c102a5 --- /dev/null +++ b/test/types/parsed.data.type.ts @@ -0,0 +1,18 @@ +import type { ParsedDataType } from '../../src/types.js'; + +interface test { + pie: ParsedDataType<'pie'>, + line: ParsedDataType<'line'>, + testA: ParsedDataType<'pie' | 'line' | 'bar'> + testB: ParsedDataType<'pie' | 'line' | 'bar'> + testC: ParsedDataType<'pie' | 'line' | 'bar'> +} + +const testImpl: test = { + pie: 1, + line: { x: 1, y: 2 }, + testA: 1, + testB: { x: 1, y: 2 }, + // @ts-expect-error testC should be limited to pie/line datatypes + testC: 'test' +}; diff --git a/test/types/plugins/defaults.ts b/test/types/plugins/defaults.ts new file mode 100644 index 00000000000..55a08ac7ad8 --- /dev/null +++ b/test/types/plugins/defaults.ts @@ -0,0 +1,11 @@ +import { defaults } from '../../../src/types.js'; + +// https://github.com/chartjs/Chart.js/issues/8711 +const original = defaults.plugins.legend.labels.generateLabels; + +defaults.plugins.legend.labels.generateLabels = function(chart) { + return [{ + datasetIndex: 0, + text: 'test' + }]; +}; diff --git a/test/types/plugins/plugin.colors/colors.ts b/test/types/plugins/plugin.colors/colors.ts new file mode 100644 index 00000000000..180709bdb88 --- /dev/null +++ b/test/types/plugins/plugin.colors/colors.ts @@ -0,0 +1,19 @@ +import { Chart } from '../../../../src/types.js'; + +const chart = new Chart('id', { + type: 'bubble', + data: { + labels: [], + datasets: [{ + data: [] + }] + }, + options: { + plugins: { + colors: { + enabled: true, + forceOverride: false, + } + } + } +}); diff --git a/test/types/plugins/plugin.decimation/decimation_algorithm.ts b/test/types/plugins/plugin.decimation/decimation_algorithm.ts new file mode 100644 index 00000000000..0667968cbf5 --- /dev/null +++ b/test/types/plugins/plugin.decimation/decimation_algorithm.ts @@ -0,0 +1,72 @@ +import { Chart, DecimationAlgorithm } from '../../../../src/types.js'; + +const chart = new Chart('id', { + type: 'bubble', + data: { + labels: [], + datasets: [{ + data: [] + }] + }, + options: { + plugins: { + decimation: { + algorithm: DecimationAlgorithm.lttb, + } + } + } +}); + + +const chart2 = new Chart('id', { + type: 'bubble', + data: { + labels: [], + datasets: [{ + data: [] + }] + }, + options: { + plugins: { + decimation: { + algorithm: 'lttb', + } + } + } +}); + + +const chart3 = new Chart('id', { + type: 'bubble', + data: { + labels: [], + datasets: [{ + data: [] + }] + }, + options: { + plugins: { + decimation: { + algorithm: DecimationAlgorithm.minmax, + } + } + } +}); + + +const chart4 = new Chart('id', { + type: 'bubble', + data: { + labels: [], + datasets: [{ + data: [] + }] + }, + options: { + plugins: { + decimation: { + algorithm: 'min-max', + } + } + } +}); diff --git a/test/types/plugins/plugin.filler/fill_target_true.ts b/test/types/plugins/plugin.filler/fill_target_true.ts new file mode 100644 index 00000000000..057dbecf7df --- /dev/null +++ b/test/types/plugins/plugin.filler/fill_target_true.ts @@ -0,0 +1,6 @@ +import type { ChartDataset } from '../../../../src/types.js'; + +const dataset: ChartDataset = { + data: [], + fill: true, +}; diff --git a/test/types/plugins/plugin.tooltip/chart.tooltip.ts b/test/types/plugins/plugin.tooltip/chart.tooltip.ts new file mode 100644 index 00000000000..338b62df858 --- /dev/null +++ b/test/types/plugins/plugin.tooltip/chart.tooltip.ts @@ -0,0 +1,15 @@ +import { Chart } from '../../../../src/types.js'; + +const chart = new Chart('id', { + type: 'line', + data: { + labels: [], + datasets: [{ + data: [] + }] + }, +}); + +const tooltip = chart.tooltip; + +const active = tooltip && tooltip.getActiveElements(); diff --git a/test/types/plugins/plugin.tooltip/tooltip_dataset_type.ts b/test/types/plugins/plugin.tooltip/tooltip_dataset_type.ts new file mode 100644 index 00000000000..045476265ad --- /dev/null +++ b/test/types/plugins/plugin.tooltip/tooltip_dataset_type.ts @@ -0,0 +1,22 @@ +import { Chart } from '../../../../src/types.js'; + +const chart = new Chart('id', { + type: 'line', + data: { + labels: [], + datasets: [{ + data: [] + }] + }, + options: { + plugins: { + tooltip: { + callbacks: { + label: (item) => { + return `Y Axis ${item.dataset.yAxisID}`; + } + } + } + } + }, +}); diff --git a/test/types/plugins/plugin.tooltip/tooltip_parsed_data.ts b/test/types/plugins/plugin.tooltip/tooltip_parsed_data.ts new file mode 100644 index 00000000000..4c35c8b8abe --- /dev/null +++ b/test/types/plugins/plugin.tooltip/tooltip_parsed_data.ts @@ -0,0 +1,22 @@ +import { Chart } from '../../../../src/types.js'; + +const chart = new Chart('id', { + type: 'bar', + data: { + labels: [], + datasets: [{ + data: [] + }] + }, + options: { + plugins: { + tooltip: { + callbacks: { + label: (item) => { + return `Foo data ${item.parsed.y}`; + } + } + } + } + }, +}); diff --git a/test/types/plugins/plugin.tooltip/tooltip_parsed_data_chart_defaults.ts b/test/types/plugins/plugin.tooltip/tooltip_parsed_data_chart_defaults.ts new file mode 100644 index 00000000000..5072824bcf9 --- /dev/null +++ b/test/types/plugins/plugin.tooltip/tooltip_parsed_data_chart_defaults.ts @@ -0,0 +1,16 @@ +import { Chart } from '../../../../src/types.js'; + +Chart.overrides.bubble.plugins.tooltip.callbacks.label = (item) => { + const { x, y, _custom: r } = item.parsed; + return `${item.label}: (${x}, ${y}, ${r})`; +}; + +const chart = new Chart('id', { + type: 'bubble', + data: { + labels: [], + datasets: [{ + data: [] + }] + }, +}); diff --git a/test/types/plugins/plugin.tooltip/tooltip_scriptable_background_color.ts b/test/types/plugins/plugin.tooltip/tooltip_scriptable_background_color.ts new file mode 100644 index 00000000000..2c6fd47e64b --- /dev/null +++ b/test/types/plugins/plugin.tooltip/tooltip_scriptable_background_color.ts @@ -0,0 +1,18 @@ +import { Chart } from '../../../../src/types.js'; + +const chart = new Chart('id', { + type: 'bar', + data: { + labels: [], + datasets: [{ + data: [] + }] + }, + options: { + plugins: { + tooltip: { + backgroundColor: (ctx) => 'black', + } + } + }, +}); diff --git a/test/types/register.ts b/test/types/register.ts new file mode 100644 index 00000000000..9efa5a3e8da --- /dev/null +++ b/test/types/register.ts @@ -0,0 +1,56 @@ +import { + Chart, + ArcElement, + LineElement, + BarElement, + PointElement, + BarController, + BubbleController, + DoughnutController, + LineController, + PieController, + PolarAreaController, + RadarController, + ScatterController, + CategoryScale, + LinearScale, + LogarithmicScale, + RadialLinearScale, + TimeScale, + TimeSeriesScale, + Decimation, + Filler, + Legend, + Title, + SubTitle, + Tooltip, + Colors +} from '../../src/types.js'; + +Chart.register( + ArcElement, + LineElement, + BarElement, + PointElement, + BarController, + BubbleController, + DoughnutController, + LineController, + PieController, + PolarAreaController, + RadarController, + ScatterController, + CategoryScale, + LinearScale, + LogarithmicScale, + RadialLinearScale, + TimeScale, + TimeSeriesScale, + Decimation, + Filler, + Legend, + Title, + SubTitle, + Tooltip, + Colors +); diff --git a/test/types/scales/chart_options.ts b/test/types/scales/chart_options.ts new file mode 100644 index 00000000000..fc5c20e6886 --- /dev/null +++ b/test/types/scales/chart_options.ts @@ -0,0 +1,12 @@ +import type { ChartOptions } from '../../../src/types.js'; + +const chartOptions: ChartOptions<'line'> = { + scales: { + x: { + type: 'time', + time: { + unit: 'year' + } + }, + } +}; diff --git a/test/types/scales/options.ts b/test/types/scales/options.ts new file mode 100644 index 00000000000..2b186d79917 --- /dev/null +++ b/test/types/scales/options.ts @@ -0,0 +1,68 @@ +import { Chart, ScaleOptions } from '../../../src/types.js'; + +const chart = new Chart('test', { + type: 'bar', + data: { + labels: ['a'], + datasets: [{ + data: [1], + }, { + type: 'line', + data: [{ x: 1, y: 1 }] + }] + }, + options: { + scales: { + x: { + type: 'time', + time: { + unit: 'year' + }, + ticks: { + stepSize: 1 + } + }, + x1: { + type: 'linear', + // @ts-expect-error 'time' does not exist in 'linear' options + time: { + unit: 'year' + } + }, + y: { + ticks: { + callback(tickValue) { + const value = this.getLabelForValue(tickValue as number); + return '$' + value; + } + } + } + } + } +}); + +function makeChartScale(range: number): ScaleOptions<'linear'> { + return { + type: 'linear', + min: 0, + suggestedMax: range, + }; +} + +const composedChart = new Chart('test2', { + type: 'bar', + data: { + labels: ['a'], + datasets: [{ + data: [1], + }, { + type: 'line', + data: [{ x: 1, y: 1 }] + }] + }, + options: { + scales: { + x: makeChartScale(10) + } + } +}); diff --git a/test/types/scales/time_string_max.ts b/test/types/scales/time_string_max.ts new file mode 100644 index 00000000000..734e2eda125 --- /dev/null +++ b/test/types/scales/time_string_max.ts @@ -0,0 +1,30 @@ +import { Chart } from '../../../src/types.js'; + +const chart = new Chart('id', { + type: 'line', + data: { + datasets: [ + { + label: 'Pie', + data: [ + ], + borderColor: '#000000', + backgroundColor: '#00FF00' + } + ] + }, + options: { + scales: { + x: { + type: 'time', + min: '2021-01-01', + max: '2021-12-01' + }, + y: { + type: 'linear', + min: 0, + max: 10 + } + } + } +}); diff --git a/test/types/scriptable.ts b/test/types/scriptable.ts new file mode 100644 index 00000000000..89c40d894b8 --- /dev/null +++ b/test/types/scriptable.ts @@ -0,0 +1,22 @@ +import type { ChartType, Scriptable, ScriptableContext } from '../../src/types.js'; + +interface test { + pie?: Scriptable>, + line?: Scriptable>, + testA?: Scriptable> + testB?: Scriptable> + testC?: Scriptable> + testD?: Scriptable> +} + +const testImpl: test = { + pie: (ctx) => ctx.parsed + ctx.chart.width, + line: (ctx) => ctx.parsed.x + ctx.parsed.y, + testA: (ctx) => ctx.parsed + ctx.dataset.data[0], + testB: (ctx) => ctx.parsed.x + ctx.parsed.y, + // @ts-expect-error combined type should not be any + testC: (ctx) => ctx.fail, + // combined types are intersections and permit invalid usage + testD: (ctx) => ctx.parsed + ctx.parsed.x + ctx.parsed.r + ctx.parsed._custom.barEnd +}; + diff --git a/test/types/scriptable_core_chart_options.ts b/test/types/scriptable_core_chart_options.ts new file mode 100644 index 00000000000..b638505525c --- /dev/null +++ b/test/types/scriptable_core_chart_options.ts @@ -0,0 +1,14 @@ +import type { ChartConfiguration } from '../../src/types.js'; + +const getConfig = (): ChartConfiguration<'bar'> => { + return { + type: 'bar', + data: { + datasets: [] + }, + options: { + backgroundColor: (context) => context.active ? '#fff' : undefined, + } + }; +}; + diff --git a/test/types/test_instance_assignment.ts b/test/types/test_instance_assignment.ts new file mode 100644 index 00000000000..5d84637735c --- /dev/null +++ b/test/types/test_instance_assignment.ts @@ -0,0 +1,23 @@ +import { Chart } from '../../src/types.js'; + +const chart = new Chart('id', { + type: 'scatter', + data: { + labels: [], + datasets: [{ + data: [{ x: 0, y: 1 }], + pointRadius: (ctx) => ctx.parsed.x, + }] + }, +}); + +interface Context { + chart: Chart; +} + +const ctx: Context = { + chart: chart +}; + +// @ts-expect-error Type '{ x: number; y: number; }[]' is not assignable to type 'number[]'. +const dataArray: number[] = chart.data.datasets[0].data; diff --git a/test/types/ticks/ticks.ts b/test/types/ticks/ticks.ts new file mode 100644 index 00000000000..a5a9e28bef8 --- /dev/null +++ b/test/types/ticks/ticks.ts @@ -0,0 +1,15 @@ +import { Chart, Ticks } from '../../../src/types.js'; + +// @ts-expect-error The 'this' context... is not assignable to method's 'this' of type 'Scale'. +Ticks.formatters.numeric(0, 0, [{ value: 0 }]); + +const chart = new Chart('test', { + type: 'line', + data: { + datasets: [{ + data: [{ x: 1, y: 1 }] + }] + }, +}); + +Ticks.formatters.numeric.call(chart.scales.x, 0, 0, [{ value: 0 }]); diff --git a/test/types/tsconfig.json b/test/types/tsconfig.json new file mode 100644 index 00000000000..091024595a2 --- /dev/null +++ b/test/types/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "noEmit": true, + "rootDir": "../../" + }, + "include": [ + "./", + "../../src/", + "../../dist/**/*.d.ts" + ], + "exclude": [ + "./**/*.js" + ] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000000..bbcd0a350da --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,48 @@ +{ + "compilerOptions": { + /* Type Checking */ + "alwaysStrict": true, + "strictBindCallApply": true, + "strictFunctionTypes": true, + /* todo: uncomment after transition to TS */ + // "noFallthroughCasesInSwitch": true, + // "noImplicitOverride": true, + // "noImplicitReturns": true, + // "noUnusedLocals": true, + // "noUnusedParameters": true, + /* Modules */ + "baseUrl": ".", + "module": "ESNext", + "moduleResolution": "NodeNext", + "resolveJsonModule": true, + "rootDir": "src", + "types": ["offscreencanvas"], + /* Emit */ + "declaration": true, + "importsNotUsedAsValues": "error", + "inlineSourceMap": true, + "outDir": "dist", + /* JavaScript Support */ + "allowJs": true, + "checkJs": true, + /* Interop Constraints */ + "allowSyntheticDefaultImports": true, + /* Language and Environment */ + "target": "ES6", + "lib": ["es2018", "DOM"] + }, + "typedocOptions": { + "name": "Chart.js", + "entryPoints": ["src/types/index.d.ts"], + "readme": "none", + "excludeExternals": true, + "includeVersion": true, + "out": "./dist/docs/typedoc" + }, + "include": [ + "./src/**/*" + ], + "exclude": [ + "./dist/**" + ] +}