├── .eslintignore ├── treemap.png ├── types ├── tests │ ├── .eslintrc.yml │ ├── tsconfig.json │ └── options.ts ├── .eslintrc.yml └── index.esm.d.ts ├── .gitignore ├── test ├── fixtures │ ├── issues │ │ ├── 77.png │ │ └── 77.js │ ├── basic │ │ ├── basic.png │ │ ├── labels.png │ │ ├── noXAxes.png │ │ ├── noYAxes.png │ │ ├── spacing.png │ │ ├── update.png │ │ ├── basic-rtl.png │ │ ├── labelsFont.png │ │ ├── borderRadius.png │ │ ├── borderWidth.png │ │ ├── differentAxes.png │ │ ├── labelsAlign.png │ │ ├── labelsColor.png │ │ ├── labelsPadding.png │ │ ├── update-tree.png │ │ ├── labelsFormatter.png │ │ ├── labelsPosition.png │ │ ├── labelsOverflowCut.png │ │ ├── labelsOverflowFit.png │ │ ├── borderRadiusAsObject.png │ │ ├── labelsMultilineFonts.png │ │ ├── labelsOverflowHidden.png │ │ ├── labelsMultilineColors.png │ │ ├── labelsMultilineFormatter.png │ │ ├── labelsMultilineOverflowCut.png │ │ ├── labelsMultilineOverflowHidden.png │ │ ├── basic.js │ │ ├── spacing.js │ │ ├── basic-rtl.js │ │ ├── borderRadius.js │ │ ├── borderWidth.js │ │ ├── labels.js │ │ ├── borderRadiusAsObject.js │ │ ├── update.js │ │ ├── update-tree.js │ │ ├── labelsAlign.js │ │ ├── labelsFormatter.js │ │ ├── labelsOverflowFit.js │ │ ├── labelsPosition.js │ │ ├── labelsOverflowCut.js │ │ ├── labelsPadding.js │ │ ├── labelsOverflowHidden.js │ │ ├── labelsFont.js │ │ ├── labelsMultilineOverflowCut.js │ │ ├── labelsMultilineOverflowHidden.js │ │ ├── labelsColor.js │ │ ├── labelsMultilineFormatter.js │ │ ├── labelsMultilineColors.js │ │ ├── labelsMultilineFonts.js │ │ ├── noXAxes.js │ │ ├── noYAxes.js │ │ └── differentAxes.js │ ├── advanced │ │ ├── zoom.png │ │ ├── multiple.png │ │ ├── toggle-rtl.png │ │ ├── toggle-rtl.js │ │ ├── zoom.js │ │ └── multiple.js │ ├── events │ │ ├── hover.png │ │ ├── tooltip.png │ │ ├── hoverLabels.png │ │ ├── hoverCaptions.png │ │ ├── hoverNegativeSpacing.png │ │ ├── hover.js │ │ ├── hoverNegativeSpacing.js │ │ ├── hoverLabels.js │ │ ├── tooltip.js │ │ └── hoverCaptions.js │ ├── grouped │ │ ├── basic.png │ │ ├── border.png │ │ ├── dividers.png │ │ ├── sumKeys.png │ │ ├── basic-large.png │ │ ├── basic-rtl.png │ │ ├── treeBasic.png │ │ ├── treeSumKeys.png │ │ ├── captionsAlign.png │ │ ├── captionsFont.png │ │ ├── treeUngrouped.png │ │ ├── variableDepth.png │ │ ├── captionsPadding.png │ │ ├── captionsFormatter.png │ │ ├── captionsTruncating.png │ │ ├── borderWidthAsObject.png │ │ ├── treeBasicAndCaptions.png │ │ ├── treeBasicWithLeafKey.png │ │ ├── captionsWithoutDisplay.png │ │ ├── treeBasicWithGroupsNumbers.png │ │ ├── basic.js │ │ ├── dividers.js │ │ ├── border.js │ │ ├── basic-rtl.js │ │ ├── borderWidthAsObject.js │ │ ├── variableDepth.js │ │ ├── basic-large.js │ │ ├── treeUngrouped.js │ │ ├── sumKeys.js │ │ ├── treeBasicAndCaptions.js │ │ ├── treeBasicWithGroupsNumbers.js │ │ ├── treeBasic.js │ │ ├── treeBasicWithLeafKey.js │ │ ├── captionsWithoutDisplay.js │ │ ├── treeSumKeys.js │ │ ├── captionsAlign.js │ │ ├── captionsPadding.js │ │ ├── captionsFont.js │ │ ├── captionsFormatter.js │ │ └── captionsTruncating.js │ └── headersbox │ │ ├── grouped.png │ │ ├── grouped-large.png │ │ ├── no-captions.png │ │ ├── variableDepth.png │ │ ├── large-captions.png │ │ ├── grouped.js │ │ ├── variableDepth.js │ │ ├── grouped-large.js │ │ ├── large-captions.js │ │ └── no-captions.js ├── .eslintrc.yml ├── specs │ ├── rectangle.spec.js │ ├── rect.spec.js │ ├── controller.spec.js │ ├── utils.spec.js │ └── squarify.spec.js └── index.js ├── docs ├── .vuepress │ ├── public │ │ ├── logo.png │ │ ├── favicon.ico │ │ ├── treemap.png │ │ └── logo.svg │ └── config.ts ├── scripts │ ├── helpers.js │ ├── register.js │ ├── utils.js │ └── data.js ├── index.md ├── integration.md └── samples │ ├── dividers.md │ ├── basic.md │ ├── tree.md │ ├── zoom.md │ ├── datalabels.md │ ├── rtl.md │ ├── groups.md │ ├── labelsFontsAndColors.md │ ├── captions.md │ ├── labels.md │ └── displayMode.md ├── samples ├── style.css ├── .eslintrc.yml ├── utils.js ├── dividers.html ├── basic.html ├── us-population.html ├── us-switchable.html └── us_stats_by_state.js ├── src ├── index.esm.js ├── index.js ├── helpers │ └── index.js ├── statArray.js ├── squarify.js ├── rect.js ├── utils.js ├── controller.js └── element.js ├── .github ├── dependabot.yml ├── workflows │ ├── compressed-size.yml │ ├── release-drafter.yml │ ├── npmpublish.yml │ └── ci.yml └── release-drafter.yml ├── .editorconfig ├── sonar-project.properties ├── .eslintrc.yml ├── LICENSE ├── rollup.config.js ├── README.md ├── karma.conf.cjs └── package.json /.eslintignore: -------------------------------------------------------------------------------- 1 | **/*{.,-}min.js 2 | dist/**/* -------------------------------------------------------------------------------- /treemap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/treemap.png -------------------------------------------------------------------------------- /types/tests/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | '@typescript-eslint/no-unused-vars': 'off' 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode/ 3 | coverage/ 4 | dist/ 5 | build/ 6 | node_modules/ 7 | *.stackdump 8 | -------------------------------------------------------------------------------- /test/fixtures/issues/77.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/issues/77.png -------------------------------------------------------------------------------- /test/fixtures/basic/basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/basic/basic.png -------------------------------------------------------------------------------- /docs/.vuepress/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/docs/.vuepress/public/logo.png -------------------------------------------------------------------------------- /samples/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 0; 3 | margin: 0; 4 | } 5 | #canvas-holder{ 6 | width:80%; 7 | margin: auto; 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/advanced/zoom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/advanced/zoom.png -------------------------------------------------------------------------------- /test/fixtures/basic/labels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/basic/labels.png -------------------------------------------------------------------------------- /test/fixtures/basic/noXAxes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/basic/noXAxes.png -------------------------------------------------------------------------------- /test/fixtures/basic/noYAxes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/basic/noYAxes.png -------------------------------------------------------------------------------- /test/fixtures/basic/spacing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/basic/spacing.png -------------------------------------------------------------------------------- /test/fixtures/basic/update.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/basic/update.png -------------------------------------------------------------------------------- /test/fixtures/events/hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/events/hover.png -------------------------------------------------------------------------------- /test/fixtures/events/tooltip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/events/tooltip.png -------------------------------------------------------------------------------- /test/fixtures/grouped/basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/grouped/basic.png -------------------------------------------------------------------------------- /test/fixtures/grouped/border.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/grouped/border.png -------------------------------------------------------------------------------- /docs/.vuepress/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/docs/.vuepress/public/favicon.ico -------------------------------------------------------------------------------- /docs/.vuepress/public/treemap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/docs/.vuepress/public/treemap.png -------------------------------------------------------------------------------- /test/fixtures/basic/basic-rtl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/basic/basic-rtl.png -------------------------------------------------------------------------------- /test/fixtures/basic/labelsFont.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/basic/labelsFont.png -------------------------------------------------------------------------------- /test/fixtures/grouped/dividers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/grouped/dividers.png -------------------------------------------------------------------------------- /test/fixtures/grouped/sumKeys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/grouped/sumKeys.png -------------------------------------------------------------------------------- /src/index.esm.js: -------------------------------------------------------------------------------- 1 | export {default as TreemapController} from './controller'; 2 | export {default as TreemapElement} from './element'; 3 | -------------------------------------------------------------------------------- /test/fixtures/advanced/multiple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/advanced/multiple.png -------------------------------------------------------------------------------- /test/fixtures/advanced/toggle-rtl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/advanced/toggle-rtl.png -------------------------------------------------------------------------------- /test/fixtures/basic/borderRadius.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/basic/borderRadius.png -------------------------------------------------------------------------------- /test/fixtures/basic/borderWidth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/basic/borderWidth.png -------------------------------------------------------------------------------- /test/fixtures/basic/differentAxes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/basic/differentAxes.png -------------------------------------------------------------------------------- /test/fixtures/basic/labelsAlign.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/basic/labelsAlign.png -------------------------------------------------------------------------------- /test/fixtures/basic/labelsColor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/basic/labelsColor.png -------------------------------------------------------------------------------- /test/fixtures/basic/labelsPadding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/basic/labelsPadding.png -------------------------------------------------------------------------------- /test/fixtures/basic/update-tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/basic/update-tree.png -------------------------------------------------------------------------------- /test/fixtures/events/hoverLabels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/events/hoverLabels.png -------------------------------------------------------------------------------- /test/fixtures/grouped/basic-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/grouped/basic-large.png -------------------------------------------------------------------------------- /test/fixtures/grouped/basic-rtl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/grouped/basic-rtl.png -------------------------------------------------------------------------------- /test/fixtures/grouped/treeBasic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/grouped/treeBasic.png -------------------------------------------------------------------------------- /test/fixtures/grouped/treeSumKeys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/grouped/treeSumKeys.png -------------------------------------------------------------------------------- /test/fixtures/headersbox/grouped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/headersbox/grouped.png -------------------------------------------------------------------------------- /test/fixtures/basic/labelsFormatter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/basic/labelsFormatter.png -------------------------------------------------------------------------------- /test/fixtures/basic/labelsPosition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/basic/labelsPosition.png -------------------------------------------------------------------------------- /test/fixtures/events/hoverCaptions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/events/hoverCaptions.png -------------------------------------------------------------------------------- /test/fixtures/grouped/captionsAlign.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/grouped/captionsAlign.png -------------------------------------------------------------------------------- /test/fixtures/grouped/captionsFont.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/grouped/captionsFont.png -------------------------------------------------------------------------------- /test/fixtures/grouped/treeUngrouped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/grouped/treeUngrouped.png -------------------------------------------------------------------------------- /test/fixtures/grouped/variableDepth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/grouped/variableDepth.png -------------------------------------------------------------------------------- /test/fixtures/basic/labelsOverflowCut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/basic/labelsOverflowCut.png -------------------------------------------------------------------------------- /test/fixtures/basic/labelsOverflowFit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/basic/labelsOverflowFit.png -------------------------------------------------------------------------------- /test/fixtures/grouped/captionsPadding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/grouped/captionsPadding.png -------------------------------------------------------------------------------- /test/fixtures/headersbox/grouped-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/headersbox/grouped-large.png -------------------------------------------------------------------------------- /test/fixtures/headersbox/no-captions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/headersbox/no-captions.png -------------------------------------------------------------------------------- /test/fixtures/headersbox/variableDepth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/headersbox/variableDepth.png -------------------------------------------------------------------------------- /test/fixtures/basic/borderRadiusAsObject.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/basic/borderRadiusAsObject.png -------------------------------------------------------------------------------- /test/fixtures/basic/labelsMultilineFonts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/basic/labelsMultilineFonts.png -------------------------------------------------------------------------------- /test/fixtures/basic/labelsOverflowHidden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/basic/labelsOverflowHidden.png -------------------------------------------------------------------------------- /test/fixtures/grouped/captionsFormatter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/grouped/captionsFormatter.png -------------------------------------------------------------------------------- /test/fixtures/grouped/captionsTruncating.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/grouped/captionsTruncating.png -------------------------------------------------------------------------------- /test/fixtures/headersbox/large-captions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/headersbox/large-captions.png -------------------------------------------------------------------------------- /test/fixtures/basic/labelsMultilineColors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/basic/labelsMultilineColors.png -------------------------------------------------------------------------------- /test/fixtures/events/hoverNegativeSpacing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/events/hoverNegativeSpacing.png -------------------------------------------------------------------------------- /test/fixtures/grouped/borderWidthAsObject.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/grouped/borderWidthAsObject.png -------------------------------------------------------------------------------- /test/fixtures/grouped/treeBasicAndCaptions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/grouped/treeBasicAndCaptions.png -------------------------------------------------------------------------------- /test/fixtures/grouped/treeBasicWithLeafKey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/grouped/treeBasicWithLeafKey.png -------------------------------------------------------------------------------- /test/fixtures/basic/labelsMultilineFormatter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/basic/labelsMultilineFormatter.png -------------------------------------------------------------------------------- /test/fixtures/grouped/captionsWithoutDisplay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/grouped/captionsWithoutDisplay.png -------------------------------------------------------------------------------- /test/fixtures/basic/labelsMultilineOverflowCut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/basic/labelsMultilineOverflowCut.png -------------------------------------------------------------------------------- /test/fixtures/grouped/treeBasicWithGroupsNumbers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/grouped/treeBasicWithGroupsNumbers.png -------------------------------------------------------------------------------- /docs/scripts/helpers.js: -------------------------------------------------------------------------------- 1 | // Add helpers needed in samples here. 2 | // Usable through `helpers[name]`. 3 | export {color, getHoverColor} from 'chart.js/helpers'; 4 | 5 | -------------------------------------------------------------------------------- /test/fixtures/basic/labelsMultilineOverflowHidden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kurkle/chartjs-chart-treemap/HEAD/test/fixtures/basic/labelsMultilineOverflowHidden.png -------------------------------------------------------------------------------- /samples/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | globals: 2 | Chart: true 3 | Utils: true 4 | statsByState: true 5 | 6 | rules: 7 | indent: ["error", "tab", {flatTernaryExpressions: true}] 8 | no-console: "off" 9 | consistent-this: "off" 10 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import {Chart} from 'chart.js'; 2 | import TreemapController from './controller'; 3 | import TreemapElement from './element'; 4 | 5 | Chart.register(TreemapController, TreemapElement); 6 | 7 | export * from './utils'; 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | -------------------------------------------------------------------------------- /test/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | jasmine: true 3 | 4 | globals: 5 | acquireChart: true 6 | afterEvent: true 7 | Chart: true 8 | createMockContext: true 9 | __karma__: true 10 | releaseChart: true 11 | waitForResize: true 12 | triggerMouseEvent: true 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.html] 13 | indent_style = tab 14 | indent_size = 4 15 | -------------------------------------------------------------------------------- /types/tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "moduleResolution": "Node", 5 | "alwaysStrict": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true 9 | }, 10 | "include": [ 11 | "../index.esm.d.ts", 12 | "./**/*.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/compressed-size.yml: -------------------------------------------------------------------------------- 1 | name: Compressed Size 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: preactjs/compressed-size-action@v2 13 | with: 14 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 15 | pattern: "./dist/*.{js,cjs}" 16 | -------------------------------------------------------------------------------- /test/fixtures/basic/basic.js: -------------------------------------------------------------------------------- 1 | export default { 2 | config: { 3 | type: 'treemap', 4 | data: { 5 | datasets: [{ 6 | label: 'Simple treemap', 7 | data: [6, 6, 4, 3, 2, 2, 1], 8 | backgroundColor: 'red' 9 | }] 10 | }, 11 | }, 12 | options: { 13 | canvas: { 14 | height: 256, 15 | width: 512 16 | } 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /test/fixtures/basic/spacing.js: -------------------------------------------------------------------------------- 1 | export default { 2 | config: { 3 | type: 'treemap', 4 | data: { 5 | datasets: [{ 6 | label: 'spacing', 7 | data: [4, 3, 2, 1], 8 | backgroundColor: 'green', 9 | spacing: 10 10 | }] 11 | }, 12 | }, 13 | options: { 14 | canvas: { 15 | height: 256, 16 | width: 512 17 | } 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - 2.x 8 | 9 | jobs: 10 | update_release_draft: 11 | if: github.repository_owner == 'kurkle' 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: release-drafter/release-drafter@v6 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | -------------------------------------------------------------------------------- /test/fixtures/basic/basic-rtl.js: -------------------------------------------------------------------------------- 1 | export default { 2 | config: { 3 | type: 'treemap', 4 | data: { 5 | datasets: [{ 6 | label: 'Simple treemap', 7 | data: [6, 6, 4, 3, 2, 2, 1], 8 | backgroundColor: 'red', 9 | rtl: true 10 | }] 11 | }, 12 | }, 13 | options: { 14 | canvas: { 15 | height: 256, 16 | width: 512 17 | } 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | [Chart.js](https://www.chartjs.org/) **v3.8+, v4+** extension for creating treemap charts. 4 | 5 | ![TreeMap Example Image](treemap.png) 6 | 7 | ## Installation 8 | 9 | ```bash 10 | > npm install chartjs-chart-treemap 11 | ``` 12 | 13 | :::tip 14 | 15 | **Important Note:** For Chart.js v2 support, see [2.x branch](https://github.com/kurkle/chartjs-chart-treemap/tree/2.x) 16 | 17 | ::: 18 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=kurkle_chartjs-chart-treemap 2 | sonar.organization=kurkle 3 | 4 | # This is the name and version displayed in the SonarCloud UI. 5 | sonar.projectName=chartjs-chart-treemap 6 | 7 | # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. 8 | sonar.sources=src/ 9 | sonar.tests=test/ 10 | 11 | sonar.javascript.lcov.reportPaths=coverage/chrome/lcov.info,coverage/firefox/lcov.info 12 | -------------------------------------------------------------------------------- /test/fixtures/basic/borderRadius.js: -------------------------------------------------------------------------------- 1 | export default { 2 | config: { 3 | type: 'treemap', 4 | data: { 5 | datasets: [{ 6 | label: 'Simple treemap', 7 | data: [6, 6, 4, 3, 2, 2, 1], 8 | backgroundColor: 'red', 9 | borderRadius: 6 10 | }] 11 | }, 12 | options: { 13 | events: [] 14 | } 15 | }, 16 | options: { 17 | canvas: { 18 | height: 256, 19 | width: 512 20 | } 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /test/specs/rectangle.spec.js: -------------------------------------------------------------------------------- 1 | import {parseBorderWidth} from '../../src/element'; 2 | 3 | describe('Rectangle', function() { 4 | describe('parseBorderWidth', function() { 5 | it('should parse object', function() { 6 | expect(parseBorderWidth({top: 1, right: 5}, 5, 5)).toEqual({t: 1, r: 5, b: 0, l: 0}); 7 | }); 8 | it('should parse number', function() { 9 | expect(parseBorderWidth(5, 5, 5)).toEqual({t: 5, r: 5, b: 5, l: 5}); 10 | }); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /test/fixtures/basic/borderWidth.js: -------------------------------------------------------------------------------- 1 | export default { 2 | config: { 3 | type: 'treemap', 4 | data: { 5 | datasets: [{ 6 | label: 'Simple treemap', 7 | data: [6, 6, 4, 3, 2, 2, 1], 8 | backgroundColor: 'red', 9 | borderColor: 'black', 10 | borderWidth: 10 11 | }] 12 | }, 13 | options: { 14 | events: [] 15 | } 16 | }, 17 | options: { 18 | canvas: { 19 | height: 256, 20 | width: 512 21 | } 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /test/fixtures/basic/labels.js: -------------------------------------------------------------------------------- 1 | export default { 2 | config: { 3 | type: 'treemap', 4 | data: { 5 | datasets: [{ 6 | label: 'Simple treemap', 7 | data: [6, 6, 4, 3, 2, 2, 1], 8 | backgroundColor: 'red', 9 | labels: { 10 | display: true, 11 | formatter: (ctx) => ctx.raw.v + '' 12 | } 13 | }] 14 | } 15 | }, 16 | options: { 17 | spriteText: true, 18 | canvas: { 19 | height: 256, 20 | width: 512 21 | } 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /test/fixtures/basic/borderRadiusAsObject.js: -------------------------------------------------------------------------------- 1 | export default { 2 | config: { 3 | type: 'treemap', 4 | data: { 5 | datasets: [{ 6 | label: 'Simple treemap', 7 | data: [6, 6, 4, 3, 2, 2, 1], 8 | backgroundColor: 'red', 9 | borderRadius: { 10 | topLeft: 10, 11 | bottomRight: 20 12 | } 13 | }] 14 | }, 15 | options: { 16 | events: [] 17 | } 18 | }, 19 | options: { 20 | canvas: { 21 | height: 256, 22 | width: 512 23 | } 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /test/fixtures/basic/update.js: -------------------------------------------------------------------------------- 1 | export default { 2 | config: { 3 | type: 'treemap', 4 | data: { 5 | datasets: [{ 6 | tree: [6, 6, 4, 3, 2, 2, 1], 7 | backgroundColor: 'red', 8 | borderWidth: 10, 9 | borderColor: 'black' 10 | }] 11 | }, 12 | options: { 13 | events: [] 14 | } 15 | }, 16 | options: { 17 | canvas: { 18 | height: 256, 19 | width: 512 20 | }, 21 | run(chart) { 22 | chart.data.datasets[0].tree = [1, 2, 3]; 23 | chart.update(); 24 | } 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /test/fixtures/basic/update-tree.js: -------------------------------------------------------------------------------- 1 | export default { 2 | config: { 3 | type: 'treemap', 4 | data: { 5 | datasets: [{ 6 | tree: [6, 6, 4, 3, 2, 2, 1], 7 | backgroundColor: 'red', 8 | borderWidth: 10, 9 | borderColor: 'black' 10 | }] 11 | }, 12 | options: { 13 | events: [] 14 | } 15 | }, 16 | options: { 17 | canvas: { 18 | height: 256, 19 | width: 512 20 | }, 21 | run(chart) { 22 | chart.data.datasets[0].tree = [1, 2, 3, 4]; 23 | chart.update(); 24 | } 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /test/fixtures/advanced/toggle-rtl.js: -------------------------------------------------------------------------------- 1 | export default { 2 | config: { 3 | type: 'treemap', 4 | data: { 5 | datasets: [{ 6 | data: [6, 6, 4, 3, 2, 2, 1], 7 | backgroundColor: 'red', 8 | borderColor: 'black', 9 | borderWidth: 5, 10 | rtl: false 11 | }] 12 | }, 13 | options: { 14 | events: [] 15 | } 16 | }, 17 | options: { 18 | canvas: { 19 | height: 256, 20 | width: 512 21 | }, 22 | run(chart) { 23 | chart.data.datasets[0].rtl = true; 24 | chart.update(); 25 | } 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /test/fixtures/events/hover.js: -------------------------------------------------------------------------------- 1 | export default { 2 | config: { 3 | type: 'treemap', 4 | data: { 5 | datasets: [{ 6 | label: 'Simple treemap', 7 | data: [6, 6, 4, 3, 2, 2, 1], 8 | backgroundColor: 'red', 9 | hoverBackgroundColor: 'green' 10 | }] 11 | } 12 | }, 13 | options: { 14 | canvas: { 15 | height: 256, 16 | width: 512 17 | }, 18 | run: (chart) => { 19 | const elem = chart.getDatasetMeta(0).data[0]; 20 | return triggerMouseEvent(chart, 'mousemove', elem.tooltipPosition()); 21 | } 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /test/fixtures/basic/labelsAlign.js: -------------------------------------------------------------------------------- 1 | export default { 2 | config: { 3 | type: 'treemap', 4 | data: { 5 | datasets: [{ 6 | label: 'Simple treemap', 7 | data: [6, 6, 4, 3, 2, 2, 1], 8 | backgroundColor: 'red', 9 | labels: { 10 | display: true, 11 | align: 'left', 12 | formatter: (ctx) => ctx.raw.v + '' 13 | } 14 | }] 15 | }, 16 | options: { 17 | events: [] 18 | } 19 | }, 20 | options: { 21 | spriteText: true, 22 | canvas: { 23 | height: 256, 24 | width: 512 25 | } 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /test/fixtures/grouped/basic.js: -------------------------------------------------------------------------------- 1 | const data = [ 2 | {category: 'main', value: 1}, 3 | {category: 'main', value: 2}, 4 | {category: 'main', value: 3}, 5 | {category: 'other', value: 4}, 6 | {category: 'other', value: 5}, 7 | ]; 8 | 9 | export default { 10 | config: { 11 | type: 'treemap', 12 | data: { 13 | datasets: [{ 14 | tree: data, 15 | key: 'value', 16 | groups: ['category'], 17 | captions: { 18 | color: 'transparent' 19 | } 20 | }] 21 | }, 22 | }, 23 | options: { 24 | canvas: { 25 | height: 256, 26 | width: 512 27 | } 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /test/fixtures/basic/labelsFormatter.js: -------------------------------------------------------------------------------- 1 | export default { 2 | config: { 3 | type: 'treemap', 4 | data: { 5 | datasets: [{ 6 | label: 'Simple treemap', 7 | data: [6, 6, 4, 3, 2, 2, 1], 8 | backgroundColor: 'red', 9 | labels: { 10 | display: true, 11 | formatter(ctx) { 12 | return ctx.type === 'data' ? ctx.raw.v + '' : ''; 13 | } 14 | } 15 | }] 16 | }, 17 | options: { 18 | events: [] 19 | } 20 | }, 21 | options: { 22 | spriteText: true, 23 | canvas: { 24 | height: 256, 25 | width: 512 26 | } 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /test/fixtures/basic/labelsOverflowFit.js: -------------------------------------------------------------------------------- 1 | export default { 2 | tolerance: 0.0075, 3 | config: { 4 | type: 'treemap', 5 | data: { 6 | datasets: [{ 7 | label: 'Simple treemap', 8 | data: [6, 6, 6, 4, 4, 2, 1, 1, 1, 1, 1, 1], 9 | backgroundColor: 'red', 10 | labels: { 11 | display: true, 12 | overflow: 'fit', 13 | font: { 14 | size: 64 15 | } 16 | } 17 | }] 18 | }, 19 | options: { 20 | events: [] 21 | } 22 | }, 23 | options: { 24 | canvas: { 25 | height: 256, 26 | width: 512 27 | } 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /test/fixtures/basic/labelsPosition.js: -------------------------------------------------------------------------------- 1 | export default { 2 | config: { 3 | type: 'treemap', 4 | data: { 5 | datasets: [{ 6 | label: 'Simple treemap', 7 | data: [6, 6, 4, 3, 2, 2, 1], 8 | backgroundColor: 'red', 9 | labels: { 10 | display: true, 11 | align: 'center', 12 | position: 'top', 13 | formatter: (ctx) => ctx.raw.v + '' 14 | } 15 | }] 16 | }, 17 | options: { 18 | events: [] 19 | } 20 | }, 21 | options: { 22 | spriteText: true, 23 | canvas: { 24 | height: 256, 25 | width: 512 26 | } 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /test/fixtures/basic/labelsOverflowCut.js: -------------------------------------------------------------------------------- 1 | export default { 2 | tolerance: 0.0105, 3 | config: { 4 | type: 'treemap', 5 | data: { 6 | datasets: [{ 7 | label: 'Simple treemap', 8 | data: [6, 6, 4, 3, 2, 2, 1], 9 | backgroundColor: 'red', 10 | labels: { 11 | display: true, 12 | overflow: 'cut', 13 | formatter: (ctx) => ('value is ' + ctx.raw.v).repeat(5) 14 | } 15 | }] 16 | }, 17 | options: { 18 | events: [] 19 | } 20 | }, 21 | options: { 22 | spriteText: true, 23 | canvas: { 24 | height: 256, 25 | width: 512 26 | } 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /test/fixtures/events/hoverNegativeSpacing.js: -------------------------------------------------------------------------------- 1 | export default { 2 | config: { 3 | type: 'treemap', 4 | data: { 5 | datasets: [{ 6 | label: 'Simple treemap', 7 | data: [6, 6, 4, 3, 2, 2, 1], 8 | borderWidth: 1, 9 | backgroundColor: 'red', 10 | hoverBackgroundColor: 'green', 11 | spacing: -0.5 12 | }] 13 | } 14 | }, 15 | options: { 16 | canvas: { 17 | height: 256, 18 | width: 512 19 | }, 20 | run: (chart) => { 21 | const elem = chart.getDatasetMeta(0).data[0]; 22 | return triggerMouseEvent(chart, 'mousemove', elem.tooltipPosition()); 23 | } 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /test/fixtures/basic/labelsPadding.js: -------------------------------------------------------------------------------- 1 | export default { 2 | config: { 3 | type: 'treemap', 4 | data: { 5 | datasets: [{ 6 | label: 'Simple treemap', 7 | data: [6, 6, 4, 3, 2, 2, 1], 8 | backgroundColor: 'red', 9 | labels: { 10 | display: true, 11 | align: 'center', 12 | position: 'top', 13 | padding: 25, 14 | formatter: (ctx) => ctx.raw.v + '' 15 | } 16 | }] 17 | }, 18 | options: { 19 | events: [] 20 | } 21 | }, 22 | options: { 23 | spriteText: true, 24 | canvas: { 25 | height: 256, 26 | width: 512 27 | } 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /test/fixtures/basic/labelsOverflowHidden.js: -------------------------------------------------------------------------------- 1 | export default { 2 | tolerance: 0.0015, 3 | config: { 4 | type: 'treemap', 5 | data: { 6 | datasets: [{ 7 | label: 'Simple treemap', 8 | data: [6, 6, 4, 3, 2, 2, 1], 9 | backgroundColor: 'red', 10 | labels: { 11 | display: true, 12 | overflow: 'hidden', 13 | formatter: (ctx) => ('value is ' + ctx.raw.v).repeat(ctx.index) 14 | } 15 | }] 16 | }, 17 | options: { 18 | events: [] 19 | } 20 | }, 21 | options: { 22 | spriteText: true, 23 | canvas: { 24 | height: 256, 25 | width: 512 26 | } 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import {acquireChart, addMatchers, releaseCharts, specsFromFixtures, triggerMouseEvent, afterEvent} from 'chartjs-test-utils'; 2 | 3 | window.devicePixelRatio = 1; 4 | window.acquireChart = acquireChart; 5 | window.afterEvent = afterEvent; 6 | window.triggerMouseEvent = triggerMouseEvent; 7 | 8 | jasmine.fixtures = specsFromFixtures; 9 | 10 | beforeAll(() => { 11 | // Disable colors plugin for tests. 12 | window.Chart.defaults.plugins.colors.enabled = false; 13 | }); 14 | 15 | beforeEach(function() { 16 | addMatchers(); 17 | }); 18 | 19 | afterEach(function() { 20 | releaseCharts(); 21 | }); 22 | 23 | console.warn('Testing with chart.js v' + Chart.version); 24 | -------------------------------------------------------------------------------- /test/fixtures/basic/labelsFont.js: -------------------------------------------------------------------------------- 1 | export default { 2 | tolerance: 0.0040, 3 | config: { 4 | type: 'treemap', 5 | data: { 6 | datasets: [{ 7 | label: 'Simple treemap', 8 | data: [6, 6, 4, 3, 2, 2, 1], 9 | backgroundColor: 'red', 10 | labels: { 11 | display: true, 12 | align: 'center', 13 | position: 'top', 14 | font: { 15 | size: 16, 16 | weight: 'bold' 17 | } 18 | } 19 | }] 20 | }, 21 | options: { 22 | events: [] 23 | } 24 | }, 25 | options: { 26 | canvas: { 27 | height: 256, 28 | width: 512 29 | } 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /test/fixtures/basic/labelsMultilineOverflowCut.js: -------------------------------------------------------------------------------- 1 | export default { 2 | tolerance: 0.0200, 3 | config: { 4 | type: 'treemap', 5 | data: { 6 | datasets: [{ 7 | label: 'Simple treemap', 8 | data: [6, 6, 4, 3, 2, 2, 1], 9 | backgroundColor: 'red', 10 | labels: { 11 | display: true, 12 | overflow: 'cut', 13 | formatter: (ctx) => ('value is ' + ctx.raw.v + ',').repeat(8).split(',') 14 | } 15 | }] 16 | }, 17 | options: { 18 | events: [] 19 | } 20 | }, 21 | options: { 22 | spriteText: true, 23 | canvas: { 24 | height: 256, 25 | width: 512 26 | } 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /test/fixtures/grouped/dividers.js: -------------------------------------------------------------------------------- 1 | const data = [ 2 | {category: 'main', value: 1}, 3 | {category: 'main', value: 2}, 4 | {category: 'other', value: 4}, 5 | ]; 6 | 7 | export default { 8 | config: { 9 | type: 'treemap', 10 | data: { 11 | datasets: [{ 12 | tree: data, 13 | key: 'value', 14 | groups: ['category'], 15 | borderWidth: 1, 16 | borderColor: '#777', 17 | dividers: { 18 | display: true, 19 | lineDash: [3, 5], 20 | lineWidth: 2, 21 | } 22 | }] 23 | }, 24 | }, 25 | options: { 26 | canvas: { 27 | height: 256, 28 | width: 512 29 | } 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /test/fixtures/advanced/zoom.js: -------------------------------------------------------------------------------- 1 | export default { 2 | config: { 3 | type: 'treemap', 4 | data: { 5 | datasets: [{ 6 | tree: [6, 6, 4, 3, 2, 2, 1], 7 | backgroundColor: 'green', 8 | borderColor: 'black', 9 | borderWidth: 8 10 | }] 11 | }, 12 | options: { 13 | events: [] 14 | } 15 | }, 16 | options: { 17 | canvas: { 18 | height: 256, 19 | width: 512 20 | }, 21 | run(chart) { 22 | chart.scales.x.options.min = 100; 23 | chart.scales.x.options.max = 400; 24 | chart.scales.y.options.min = 50; 25 | chart.scales.y.options.max = 200; 26 | chart.update(); 27 | } 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /test/fixtures/basic/labelsMultilineOverflowHidden.js: -------------------------------------------------------------------------------- 1 | export default { 2 | tolerance: 0.0100, 3 | config: { 4 | type: 'treemap', 5 | data: { 6 | datasets: [{ 7 | label: 'Simple treemap', 8 | data: [6, 6, 4, 3, 2, 2, 1], 9 | backgroundColor: 'red', 10 | labels: { 11 | display: true, 12 | overflow: 'hidden', 13 | formatter: (ctx) => ('value is ' + ctx.raw.v + ',').repeat(6).split(',') 14 | } 15 | }] 16 | }, 17 | options: { 18 | events: [] 19 | } 20 | }, 21 | options: { 22 | spriteText: true, 23 | canvas: { 24 | height: 256, 25 | width: 512 26 | } 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /test/fixtures/issues/77.js: -------------------------------------------------------------------------------- 1 | const tree = [ 2 | { 3 | p1: '/etc', 4 | p2: 'passwd', 5 | value: 1000 6 | }, 7 | { 8 | p1: '/etc', 9 | p2: 'shadow', 10 | value: 5 11 | } 12 | ]; 13 | 14 | export default { 15 | config: { 16 | type: 'treemap', 17 | data: { 18 | datasets: [{ 19 | tree, 20 | groups: ['p1', 'p2'], 21 | key: 'value', 22 | spacing: 20, 23 | borderWidth: 1, 24 | backgroundColor: (ctx) => ctx.index === 2 ? 'red' : 'rgba(0,0,0,0.5)', 25 | }] 26 | }, 27 | }, 28 | options: { 29 | canvas: { 30 | height: 256, 31 | width: 512 32 | }, 33 | spriteText: true 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /test/fixtures/grouped/border.js: -------------------------------------------------------------------------------- 1 | const data = [ 2 | {category: 'main', value: 1}, 3 | {category: 'main', value: 2}, 4 | {category: 'main', value: 3}, 5 | {category: 'other', value: 4}, 6 | {category: 'other', value: 5}, 7 | ]; 8 | 9 | export default { 10 | config: { 11 | type: 'treemap', 12 | data: { 13 | datasets: [{ 14 | tree: data, 15 | key: 'value', 16 | groups: ['category'], 17 | borderWidth: {top: 10, left: 5, right: 15, bottom: 20}, 18 | captions: { 19 | color: 'transparent' 20 | } 21 | }] 22 | }, 23 | }, 24 | options: { 25 | canvas: { 26 | height: 256, 27 | width: 512 28 | } 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /test/fixtures/basic/labelsColor.js: -------------------------------------------------------------------------------- 1 | export default { 2 | tolerance: 0.0040, 3 | config: { 4 | type: 'treemap', 5 | data: { 6 | datasets: [{ 7 | label: 'Simple treemap', 8 | data: [6, 6, 4, 3, 2, 2, 1], 9 | backgroundColor: 'red', 10 | labels: { 11 | display: true, 12 | align: 'left', 13 | position: 'top', 14 | color: 'yellow', 15 | font: { 16 | size: 16, 17 | weight: 'bold' 18 | } 19 | } 20 | }] 21 | }, 22 | options: { 23 | events: [] 24 | } 25 | }, 26 | options: { 27 | canvas: { 28 | height: 256, 29 | width: 512 30 | } 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /docs/integration.md: -------------------------------------------------------------------------------- 1 | # Integration 2 | 3 | `chartjs-chart-treemap` can be integrated with plain JavaScript or with different module loaders. The examples below show to load the plugin in different systems. 4 | 5 | ## Script Tag 6 | 7 | ```html 8 | 9 | 10 | 13 | ``` 14 | 15 | ## Bundlers (Webpack, Rollup, etc.) 16 | 17 | ```javascript 18 | import { Chart } from 'chart.js'; 19 | import {TreemapController, TreemapElement} from 'chartjs-chart-treemap'; 20 | 21 | Chart.register(TreemapController, TreemapElement); 22 | ``` 23 | -------------------------------------------------------------------------------- /test/fixtures/basic/labelsMultilineFormatter.js: -------------------------------------------------------------------------------- 1 | export default { 2 | tolerance: 0.0060, 3 | config: { 4 | type: 'treemap', 5 | data: { 6 | datasets: [{ 7 | label: 'Simple treemap', 8 | data: [6, 6, 4, 3, 2, 2, 1], 9 | backgroundColor: 'red', 10 | labels: { 11 | display: true, 12 | align: 'left', 13 | position: 'top', 14 | formatter(ctx) { 15 | return ctx.type === 'data' ? ['The value is', ctx.raw.v + ''] : []; 16 | } 17 | } 18 | }] 19 | }, 20 | options: { 21 | events: [] 22 | } 23 | }, 24 | options: { 25 | spriteText: true, 26 | canvas: { 27 | height: 256, 28 | width: 512 29 | } 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /test/fixtures/basic/labelsMultilineColors.js: -------------------------------------------------------------------------------- 1 | export default { 2 | tolerance: 0.0220, 3 | config: { 4 | type: 'treemap', 5 | data: { 6 | datasets: [{ 7 | label: 'Simple treemap', 8 | data: [16, 16, 14, 13, 12, 12], 9 | labels: { 10 | display: true, 11 | align: 'left', 12 | position: 'top', 13 | color: () => ['red', 'green'], 14 | formatter(ctx) { 15 | return ctx.type === 'data' ? ['The value is', ctx.raw.v + '', 'The value is', ctx.raw.v + ''] : []; 16 | } 17 | } 18 | }] 19 | }, 20 | options: { 21 | events: [] 22 | } 23 | }, 24 | options: { 25 | canvas: { 26 | height: 256, 27 | width: 512 28 | } 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /test/fixtures/grouped/basic-rtl.js: -------------------------------------------------------------------------------- 1 | const data = [ 2 | {category: 'main', subcategory: 'one', value: 1}, 3 | {category: 'main', subcategory: 'two', value: 2}, 4 | {category: 'main', subcategory: 'free', value: 3}, 5 | {category: 'other', subcategory: 'one', value: 4}, 6 | {category: 'other', subcategory: 'two', value: 5}, 7 | ]; 8 | 9 | export default { 10 | config: { 11 | type: 'treemap', 12 | data: { 13 | datasets: [{ 14 | tree: data, 15 | key: 'value', 16 | groups: ['category', 'subcategory'], 17 | rtl: true, 18 | captions: { 19 | color: 'transparent' 20 | } 21 | }] 22 | }, 23 | }, 24 | options: { 25 | canvas: { 26 | height: 256, 27 | width: 512 28 | } 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /types/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | parser: '@typescript-eslint/parser' 2 | 3 | plugins: 4 | - '@typescript-eslint' 5 | 6 | extends: 7 | - chartjs 8 | - plugin:@typescript-eslint/recommended 9 | 10 | rules: 11 | # Replace stock eslint rules with typescript-eslint equivalents for proper 12 | # TypeScript support. 13 | no-use-before-define: "off" 14 | '@typescript-eslint/no-use-before-define': "error" 15 | no-shadow: "off" 16 | '@typescript-eslint/no-shadow': "error" 17 | 18 | # These rules were set to warning to make the linting pass initially, 19 | # without making any major changes to types. 20 | object-curly-spacing: ["warn", "always"] 21 | '@typescript-eslint/no-empty-interface': "warn" 22 | '@typescript-eslint/ban-types': "warn" 23 | '@typescript-eslint/adjacent-overload-signatures': "warn" 24 | -------------------------------------------------------------------------------- /test/fixtures/basic/labelsMultilineFonts.js: -------------------------------------------------------------------------------- 1 | export default { 2 | tolerance: 0.0400, 3 | config: { 4 | type: 'treemap', 5 | data: { 6 | datasets: [{ 7 | label: 'Simple treemap', 8 | data: [16, 16, 14, 13, 12, 12], 9 | backgroundColor: 'red', 10 | labels: { 11 | display: true, 12 | align: 'left', 13 | position: 'top', 14 | font: () => [{size: 24}, {size: 12}], 15 | formatter(ctx) { 16 | return ctx.type === 'data' ? ['The value is', ctx.raw.v + '', 'The value is', ctx.raw.v + ''] : []; 17 | } 18 | } 19 | }] 20 | }, 21 | options: { 22 | events: [] 23 | } 24 | }, 25 | options: { 26 | canvas: { 27 | height: 256, 28 | width: 512 29 | } 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /test/fixtures/events/hoverLabels.js: -------------------------------------------------------------------------------- 1 | export default { 2 | tolerance: 0.0016, 3 | config: { 4 | type: 'treemap', 5 | data: { 6 | datasets: [{ 7 | label: 'Simple treemap', 8 | data: [6, 6, 4, 3, 2, 2, 1], 9 | backgroundColor: 'red', 10 | hoverBackgroundColor: 'green', 11 | labels: { 12 | display: true, 13 | hoverColor: 'white', 14 | font: { 15 | size: 16, 16 | weight: 'bold' 17 | } 18 | } 19 | }] 20 | } 21 | }, 22 | options: { 23 | canvas: { 24 | height: 256, 25 | width: 512 26 | }, 27 | run: (chart) => { 28 | const elem = chart.getDatasetMeta(0).data[0]; 29 | return triggerMouseEvent(chart, 'mousemove', elem.tooltipPosition()); 30 | } 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /test/fixtures/events/tooltip.js: -------------------------------------------------------------------------------- 1 | export default { 2 | config: { 3 | type: 'treemap', 4 | data: { 5 | datasets: [{ 6 | label: 'Simple treemap', 7 | data: [6, 6, 4, 3, 2, 2, 1], 8 | backgroundColor: 'red', 9 | hoverBackgroundColor: 'red' 10 | }] 11 | }, 12 | options: { 13 | plugins: { 14 | legend: false, 15 | tooltip: { 16 | enabled: true, 17 | callbacks: { 18 | title: () => '', 19 | label: () => '', 20 | } 21 | } 22 | } 23 | } 24 | }, 25 | options: { 26 | canvas: { 27 | height: 256, 28 | width: 512 29 | }, 30 | run: (chart) => { 31 | const elem = chart.getDatasetMeta(0).data[2]; 32 | return triggerMouseEvent(chart, 'mousemove', elem.tooltipPosition()); 33 | } 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /test/fixtures/grouped/borderWidthAsObject.js: -------------------------------------------------------------------------------- 1 | const tree = [ 2 | { 3 | p1: '/etc', 4 | p2: 'passwd', 5 | value: 1000 6 | }, 7 | { 8 | p1: '/etc', 9 | p2: 'shadow', 10 | value: 15 11 | } 12 | ]; 13 | 14 | export default { 15 | config: { 16 | type: 'treemap', 17 | data: { 18 | datasets: [{ 19 | tree, 20 | groups: ['p1', 'p2'], 21 | key: 'value', 22 | borderColor: 'black', 23 | spacing: 2, 24 | borderWidth: { 25 | left: 2, 26 | right: 4, 27 | bottom: 6, 28 | top: 8 29 | }, 30 | backgroundColor: (ctx) => ctx.index === 0 ? 'green' : 'rgba(255,255,255,0.8)', 31 | }] 32 | }, 33 | }, 34 | options: { 35 | canvas: { 36 | height: 256, 37 | width: 512 38 | }, 39 | spriteText: true 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /test/fixtures/advanced/multiple.js: -------------------------------------------------------------------------------- 1 | export default { 2 | config: { 3 | type: 'treemap', 4 | data: { 5 | datasets: [{ 6 | data: [1, 2, 3, 3], 7 | backgroundColor: 'red', 8 | yAxisID: 'y' 9 | }, { 10 | data: [2, 3, 4, 4], 11 | backgroundColor: 'green', 12 | yAxisID: 'y2', 13 | }, { 14 | data: [3, 4, 5, 5], 15 | backgroundColor: 'blue', 16 | yAxisID: 'y3', 17 | }] 18 | }, 19 | options: { 20 | events: [], 21 | spacing: 4, 22 | scales: { 23 | x: {position: 'bottom'}, 24 | y: {position: 'left', stack: 'y'}, 25 | y2: {position: 'left', stack: 'y'}, 26 | y3: {position: 'left', stack: 'y'}, 27 | } 28 | } 29 | }, 30 | options: { 31 | canvas: { 32 | height: 768, 33 | width: 512 34 | } 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /docs/scripts/register.js: -------------------------------------------------------------------------------- 1 | import {Chart, registerables} from 'chart.js'; 2 | import {TreemapController, TreemapElement} from '../../dist/chartjs-chart-treemap.esm'; 3 | import ChartDataLabels from 'chartjs-plugin-datalabels'; 4 | import zoomPlugin from 'chartjs-plugin-zoom'; 5 | 6 | Chart.register(...registerables); 7 | Chart.register(TreemapController, TreemapElement, ChartDataLabels, zoomPlugin); 8 | Chart.defaults.plugins.datalabels.display = false; 9 | 10 | Chart.register({ 11 | id: 'version', 12 | afterDraw(chart) { 13 | const ctx = chart.ctx; 14 | ctx.save(); 15 | ctx.font = '9px monospace'; 16 | ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; 17 | ctx.textAlign = 'right'; 18 | ctx.textBaseline = 'top'; 19 | ctx.fillText('Chart.js v' + Chart.version + ' + chartjs-chart-treemap v' + TreemapController.version, chart.chartArea.right, 0); 20 | ctx.restore(); 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /test/fixtures/grouped/variableDepth.js: -------------------------------------------------------------------------------- 1 | const data = [ 2 | {year: '2025', value: 10}, 3 | {year: '2026', quarter: 'Q1', value: 2}, 4 | {year: '2026', quarter: 'Q2', value: 6}, 5 | {year: '2027', quarter: 'Q1', month: 'January', value: 2}, 6 | {year: '2027', quarter: 'Q1', month: 'February', value: 2}, 7 | ]; 8 | 9 | export default { 10 | config: { 11 | type: 'treemap', 12 | data: { 13 | datasets: [{ 14 | tree: data, 15 | key: 'value', 16 | groups: ['year', 'quarter', 'month'], 17 | spacing: 2, 18 | borderWidth: 1, 19 | backgroundColor: (ctx) => ctx.raw.isLeaf ? 'lightblue' : 'darkgray', 20 | captions: {display: true}, 21 | labels: {display: true}, 22 | }] 23 | }, 24 | }, 25 | options: { 26 | spriteText: true, 27 | canvas: { 28 | height: 256, 29 | width: 512 30 | } 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /test/fixtures/grouped/basic-large.js: -------------------------------------------------------------------------------- 1 | const arrayN = (n) => Array.from({length: n}).map((_, i) => i); 2 | 3 | const groups = arrayN(10); 4 | const tree = groups.reduce((acc, grp) => [ 5 | ...acc, 6 | ...arrayN(grp * 10).map(i => ({grp: `group: ${grp}`, sub: `sub: ${i}`, value: (i % 10) * 10})) 7 | ], []); 8 | 9 | export default { 10 | config: { 11 | type: 'treemap', 12 | data: { 13 | datasets: [{ 14 | tree, 15 | backgroundColor: (ctx) => ctx.raw.l ? 'dimgray' : 'silver', 16 | borderColor: (ctx) => ctx.raw.l ? 'white' : 'black', 17 | borderWidth: 1, 18 | spacing: 0, 19 | key: 'value', 20 | groups: ['grp', 'sub'] 21 | }] 22 | }, 23 | options: { 24 | events: [] 25 | } 26 | }, 27 | options: { 28 | spriteText: true, 29 | canvas: { 30 | height: 300, 31 | width: 800 32 | } 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /test/specs/rect.spec.js: -------------------------------------------------------------------------------- 1 | import Rect from '../../src/rect'; 2 | 3 | describe('Rect', function() { 4 | 5 | it('should be a function', function() { 6 | expect(typeof Rect).toBe('function'); 7 | }); 8 | 9 | it('should construct with undefined input', function() { 10 | expect(new Rect()).toEqual(jasmine.objectContaining({x: 0, y: 0, w: 1, h: 1})); 11 | expect(new Rect(false)).toEqual(jasmine.objectContaining({x: 0, y: 0, w: 1, h: 1})); 12 | }); 13 | 14 | it('should construct from different kinds of input', function() { 15 | expect(new Rect({x: 1, y: 2, w: 3, h: 4})).toEqual(jasmine.objectContaining({x: 1, y: 2, w: 3, h: 4})); 16 | expect(new Rect({x: 1, y: 2, width: 3, height: 4})).toEqual(jasmine.objectContaining({x: 1, y: 2, w: 3, h: 4})); 17 | expect(new Rect({left: 1, top: 2, right: 4, bottom: 6})).toEqual(jasmine.objectContaining({x: 1, y: 2, w: 3, h: 4})); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | 2 | extends: 3 | - chartjs 4 | - plugin:es/no-new-in-es2019 5 | - plugin:markdown/recommended 6 | 7 | env: 8 | es6: true 9 | browser: true 10 | node: true 11 | 12 | parserOptions: 13 | ecmaVersion: 2022 14 | sourceType: module 15 | ecmaFeatures: 16 | impliedStrict: true 17 | modules: true 18 | 19 | plugins: ['html', 'es'] 20 | 21 | rules: 22 | class-methods-use-this: "off" 23 | complexity: ["warn", 10] 24 | max-statements: ["warn", 30] 25 | no-empty-function: "off" 26 | no-use-before-define: ["error", { "functions": false }] 27 | # disable everything, except Rest/Spread Properties in ES2018 28 | es/no-async-iteration: "error" 29 | es/no-malformed-template-literals: "error" 30 | es/no-regexp-lookbehind-assertions: "error" 31 | es/no-regexp-named-capture-groups: "error" 32 | es/no-regexp-s-flag: "error" 33 | es/no-regexp-unicode-property-escapes: "error" 34 | -------------------------------------------------------------------------------- /test/fixtures/headersbox/grouped.js: -------------------------------------------------------------------------------- 1 | const arrayN = (n) => Array.from({length: n}).map((_, i) => i); 2 | 3 | const groups = arrayN(4); 4 | const tree = groups.reduce((acc, grp) => [ 5 | ...acc, 6 | ...arrayN(grp * 4).map(i => ({grp: `group: ${grp}`, sub: `sub: ${i}`, value: (i % 4) * 4})) 7 | ], []); 8 | 9 | export default { 10 | config: { 11 | type: 'treemap', 12 | data: { 13 | datasets: [{ 14 | tree, 15 | backgroundColor: (ctx) => ctx.raw.l ? 'lightblue' : 'dimgray', 16 | borderColor: 'dimgray', 17 | borderWidth: 1, 18 | spacing: 1, 19 | key: 'value', 20 | groups: ['grp', 'sub'], 21 | displayMode: 'headerBoxes' 22 | }] 23 | }, 24 | options: { 25 | events: [] 26 | } 27 | }, 28 | options: { 29 | spriteText: true, 30 | canvas: { 31 | height: 300, 32 | width: 800 33 | } 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /test/fixtures/headersbox/variableDepth.js: -------------------------------------------------------------------------------- 1 | const data = [ 2 | {year: '2025', value: 10}, 3 | {year: '2026', quarter: 'Q1', value: 2}, 4 | {year: '2026', quarter: 'Q2', value: 6}, 5 | {year: '2027', quarter: 'Q1', month: 'January', value: 2}, 6 | {year: '2027', quarter: 'Q1', month: 'February', value: 2}, 7 | ]; 8 | 9 | 10 | export default { 11 | config: { 12 | type: 'treemap', 13 | data: { 14 | datasets: [{ 15 | tree: data, 16 | key: 'value', 17 | groups: ['year', 'quarter', 'month'], 18 | spacing: 2, 19 | backgroundColor: (ctx) => ctx.raw.isLeaf ? 'lightblue' : 'darkgray', 20 | captions: {display: true}, 21 | labels: {display: true}, 22 | displayMode: 'headerBoxes', 23 | }] 24 | }, 25 | }, 26 | options: { 27 | spriteText: true, 28 | canvas: { 29 | height: 256, 30 | width: 512 31 | } 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /test/fixtures/grouped/treeUngrouped.js: -------------------------------------------------------------------------------- 1 | const data = { 2 | A: { 3 | C: { 4 | C1: { 5 | C1a: { 6 | value: 6.25 7 | }, 8 | C1b: { 9 | value: 6.25 10 | }, 11 | }, 12 | C2: { 13 | value: 12.5 14 | } 15 | }, 16 | D: { 17 | value: 25 18 | } 19 | }, 20 | B: { 21 | value: 50, 22 | }, 23 | G: { 24 | C: { 25 | value: 50 26 | } 27 | }, 28 | }; 29 | 30 | export default { 31 | config: { 32 | type: 'treemap', 33 | data: { 34 | datasets: [{ 35 | tree: data, 36 | key: 'value', 37 | captions: { 38 | display: false 39 | }, 40 | labels: { 41 | display: true, 42 | } 43 | }] 44 | }, 45 | }, 46 | options: { 47 | spriteText: true, 48 | canvas: { 49 | height: 256, 50 | width: 512 51 | } 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /test/fixtures/headersbox/grouped-large.js: -------------------------------------------------------------------------------- 1 | const arrayN = (n) => Array.from({length: n}).map((_, i) => i); 2 | 3 | const groups = arrayN(10); 4 | const tree = groups.reduce((acc, grp) => [ 5 | ...acc, 6 | ...arrayN(grp * 10).map(i => ({grp: `group: ${grp}`, sub: `sub: ${i}`, value: (i % 10) * 10})) 7 | ], []); 8 | 9 | export default { 10 | config: { 11 | type: 'treemap', 12 | data: { 13 | datasets: [{ 14 | tree, 15 | backgroundColor: (ctx) => ctx.raw.l ? 'dimgray' : 'silver', 16 | borderColor: (ctx) => ctx.raw.l ? 'white' : 'black', 17 | borderWidth: 0, 18 | spacing: 1, 19 | key: 'value', 20 | groups: ['grp', 'sub'], 21 | displayMode: 'headerBoxes', 22 | }] 23 | }, 24 | options: { 25 | events: [] 26 | } 27 | }, 28 | options: { 29 | spriteText: true, 30 | canvas: { 31 | height: 300, 32 | width: 800 33 | } 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /test/fixtures/grouped/sumKeys.js: -------------------------------------------------------------------------------- 1 | const data = [ 2 | {category: 'main', value: 1, another: 5}, 3 | {category: 'main', value: 2, another: 4}, 4 | {category: 'main', value: 3, another: 0}, 5 | {category: 'other', value: 4, another: 3}, 6 | {category: 'other', value: 5, another: 8}, 7 | ]; 8 | 9 | export default { 10 | config: { 11 | type: 'treemap', 12 | data: { 13 | datasets: [{ 14 | tree: data, 15 | key: 'value', 16 | groups: ['category'], 17 | sumKeys: ['another'], 18 | backgroundColor: (ctx) => ctx.raw.vs.another > 10 ? 'red' : 'yellow', 19 | labels: { 20 | display: true, 21 | formatter: (ctx) => [ctx.raw.g, ctx.raw.vs.another + ''] 22 | } 23 | }] 24 | }, 25 | options: { 26 | events: [] 27 | } 28 | }, 29 | options: { 30 | spriteText: true, 31 | canvas: { 32 | height: 256, 33 | width: 512 34 | } 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /test/fixtures/grouped/treeBasicAndCaptions.js: -------------------------------------------------------------------------------- 1 | const data = { 2 | A: { 3 | C: { 4 | C1: { 5 | C1a: { 6 | value: 6.25 7 | }, 8 | C1b: { 9 | value: 6.25 10 | }, 11 | }, 12 | C2: { 13 | value: 12.5 14 | } 15 | }, 16 | D: { 17 | value: 25 18 | } 19 | }, 20 | B: { 21 | value: 50, 22 | }, 23 | G: { 24 | C: { 25 | value: 50 26 | } 27 | }, 28 | }; 29 | 30 | export default { 31 | config: { 32 | type: 'treemap', 33 | data: { 34 | datasets: [{ 35 | tree: data, 36 | key: 'value', 37 | groups: ['0', '1', '2', '_leaf'], 38 | captions: { 39 | display: true 40 | }, 41 | labels: { 42 | display: true 43 | } 44 | }] 45 | }, 46 | }, 47 | options: { 48 | spriteText: true, 49 | canvas: { 50 | height: 256, 51 | width: 512 52 | } 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /test/fixtures/grouped/treeBasicWithGroupsNumbers.js: -------------------------------------------------------------------------------- 1 | const data = { 2 | A: { 3 | C: { 4 | C1: { 5 | C1a: { 6 | value: 6.25 7 | }, 8 | C1b: { 9 | value: 6.25 10 | }, 11 | }, 12 | C2: { 13 | value: 12.5 14 | } 15 | }, 16 | D: { 17 | value: 25 18 | } 19 | }, 20 | B: { 21 | value: 50, 22 | }, 23 | G: { 24 | C: { 25 | value: 50 26 | } 27 | }, 28 | }; 29 | 30 | export default { 31 | config: { 32 | type: 'treemap', 33 | data: { 34 | datasets: [{ 35 | tree: data, 36 | key: 'value', 37 | groups: [0, 1, 2, '_leaf'], 38 | captions: { 39 | display: false 40 | }, 41 | labels: { 42 | display: true 43 | } 44 | }] 45 | }, 46 | }, 47 | options: { 48 | spriteText: true, 49 | canvas: { 50 | height: 256, 51 | width: 512 52 | } 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /test/fixtures/grouped/treeBasic.js: -------------------------------------------------------------------------------- 1 | const data = { 2 | A: { 3 | C: { 4 | C1: { 5 | C1a: { 6 | value: 6.25 7 | }, 8 | C1b: { 9 | value: 6.25 10 | }, 11 | }, 12 | C2: { 13 | value: 12.5 14 | } 15 | }, 16 | D: { 17 | value: 25 18 | } 19 | }, 20 | B: { 21 | value: 50, 22 | }, 23 | G: { 24 | C: { 25 | value: 50 26 | } 27 | }, 28 | }; 29 | 30 | export default { 31 | tolerance: 0.0012, 32 | config: { 33 | type: 'treemap', 34 | data: { 35 | datasets: [{ 36 | tree: data, 37 | key: 'value', 38 | groups: ['0', '1', '2', '_leaf'], 39 | captions: { 40 | display: false 41 | }, 42 | labels: { 43 | display: true, 44 | } 45 | }] 46 | }, 47 | }, 48 | options: { 49 | spriteText: true, 50 | canvas: { 51 | height: 256, 52 | width: 512 53 | } 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /test/fixtures/headersbox/large-captions.js: -------------------------------------------------------------------------------- 1 | const arrayN = (n) => Array.from({length: n}).map((_, i) => i); 2 | 3 | const groups = arrayN(10); 4 | const tree = groups.reduce((acc, grp) => [ 5 | ...acc, 6 | ...arrayN(grp * 10).map(i => ({grp: `group: ${grp}`, sub: `sub: ${i}`, value: (i % 10) * 10})) 7 | ], []); 8 | 9 | export default { 10 | config: { 11 | type: 'treemap', 12 | data: { 13 | datasets: [{ 14 | tree, 15 | backgroundColor: (ctx) => ctx.raw.l ? 'dimgray' : 'silver', 16 | borderColor: (ctx) => ctx.raw.l ? 'white' : 'black', 17 | borderWidth: 0, 18 | spacing: 1, 19 | key: 'value', 20 | groups: ['grp', 'sub'], 21 | displayMode: 'headerBoxes', 22 | captions: { 23 | padding: 20, 24 | } 25 | }] 26 | }, 27 | options: { 28 | events: [] 29 | } 30 | }, 31 | options: { 32 | spriteText: true, 33 | canvas: { 34 | height: 200, 35 | width: 800 36 | } 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /test/fixtures/basic/noXAxes.js: -------------------------------------------------------------------------------- 1 | export default { 2 | tolerance: 0.0320, 3 | config: { 4 | type: 'treemap', 5 | data: { 6 | datasets: [{ 7 | label: 'Simple treemap', 8 | data: [6, 6, 4, 3, 2, 2, 1], 9 | backgroundColor: 'red', 10 | labels: { 11 | display: true, 12 | formatter({chart, datasetIndex}) { 13 | const meta = chart.getDatasetMeta(datasetIndex); 14 | return 'x: ' + meta.xScale.id + ', y: ' + meta.yScale.id; 15 | } 16 | } 17 | }] 18 | }, 19 | options: { 20 | events: [], 21 | scales: { 22 | a: { 23 | type: 'linear', 24 | display: false, 25 | axis: 'y' 26 | }, 27 | b: { 28 | type: 'linear', 29 | display: false, 30 | axis: 'y' 31 | }, 32 | } 33 | } 34 | }, 35 | options: { 36 | spriteText: true, 37 | canvas: { 38 | height: 256, 39 | width: 512 40 | } 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /test/fixtures/basic/noYAxes.js: -------------------------------------------------------------------------------- 1 | export default { 2 | tolerance: 0.0240, 3 | config: { 4 | type: 'treemap', 5 | data: { 6 | datasets: [{ 7 | label: 'Simple treemap', 8 | data: [6, 6, 4, 3, 2, 2, 1], 9 | backgroundColor: 'red', 10 | labels: { 11 | display: true, 12 | formatter({chart, datasetIndex}) { 13 | const meta = chart.getDatasetMeta(datasetIndex); 14 | return 'x: ' + meta.xScale.id + ', y: ' + meta.yScale.id; 15 | } 16 | } 17 | }] 18 | }, 19 | options: { 20 | events: [], 21 | scales: { 22 | a: { 23 | type: 'linear', 24 | display: false, 25 | axis: 'x' 26 | }, 27 | b: { 28 | type: 'linear', 29 | display: false, 30 | axis: 'x' 31 | }, 32 | } 33 | } 34 | }, 35 | options: { 36 | spriteText: true, 37 | canvas: { 38 | height: 256, 39 | width: 512 40 | } 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /test/fixtures/basic/differentAxes.js: -------------------------------------------------------------------------------- 1 | export default { 2 | tolerance: 0.0280, 3 | config: { 4 | type: 'treemap', 5 | data: { 6 | datasets: [{ 7 | label: 'Simple treemap', 8 | data: [6, 6, 4, 3, 2, 2, 1], 9 | backgroundColor: 'red', 10 | labels: { 11 | display: true, 12 | formatter({chart, datasetIndex}) { 13 | const meta = chart.getDatasetMeta(datasetIndex); 14 | return 'x: ' + meta.xScale.id + ', y: ' + meta.yScale.id; 15 | } 16 | } 17 | }] 18 | }, 19 | options: { 20 | events: [], 21 | scales: { 22 | a: { 23 | type: 'linear', 24 | display: false, 25 | axis: 'x' 26 | }, 27 | b: { 28 | type: 'linear', 29 | display: false, 30 | axis: 'y' 31 | }, 32 | } 33 | } 34 | }, 35 | options: { 36 | spriteText: true, 37 | canvas: { 38 | height: 256, 39 | width: 512 40 | } 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /test/fixtures/grouped/treeBasicWithLeafKey.js: -------------------------------------------------------------------------------- 1 | const data = { 2 | A: { 3 | C: { 4 | C1: { 5 | C1a: { 6 | value: 6.25 7 | }, 8 | C1b: { 9 | value: 6.25 10 | }, 11 | }, 12 | C2: { 13 | value: 12.5 14 | } 15 | }, 16 | D: { 17 | value: 25 18 | } 19 | }, 20 | B: { 21 | value: 50, 22 | }, 23 | G: { 24 | C: { 25 | value: 50 26 | } 27 | }, 28 | }; 29 | 30 | export default { 31 | tolerance: 0.0012, 32 | config: { 33 | type: 'treemap', 34 | data: { 35 | datasets: [{ 36 | tree: data, 37 | treeLeafKey: '_test', 38 | key: 'value', 39 | groups: ['0', '1', '2', '_test'], 40 | captions: { 41 | display: false 42 | }, 43 | labels: { 44 | display: true 45 | } 46 | }] 47 | }, 48 | }, 49 | options: { 50 | spriteText: true, 51 | canvas: { 52 | height: 256, 53 | width: 512 54 | } 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /src/helpers/index.js: -------------------------------------------------------------------------------- 1 | export function scaleRect(sq, xScale, yScale, sp) { 2 | const sp2 = sp * 2; 3 | const x = xScale.getPixelForValue(sq.x); 4 | const y = yScale.getPixelForValue(sq.y); 5 | const w = xScale.getPixelForValue(sq.x + sq.w) - x; 6 | const h = yScale.getPixelForValue(sq.y + sq.h) - y; 7 | return { 8 | x: x + sp, 9 | y: y + sp, 10 | width: w - sp2, 11 | height: h - sp2, 12 | hidden: sp2 > w || sp2 > h, 13 | }; 14 | } 15 | 16 | export function rectNotEqual(r1, r2) { 17 | return !r1 || !r2 18 | || r1.x !== r2.x 19 | || r1.y !== r2.y 20 | || r1.w !== r2.w 21 | || r1.h !== r2.h 22 | || r1.rtl !== r2.rtl 23 | || r1.unsorted !== r2.unsorted; 24 | } 25 | 26 | export function arrayNotEqual(a, b) { 27 | let i, n; 28 | 29 | if (!a || !b) { 30 | return true; 31 | } 32 | 33 | if (a === b) { 34 | return false; 35 | } 36 | 37 | if (a.length !== b.length) { 38 | return true; 39 | } 40 | 41 | for (i = 0, n = a.length; i < n; ++i) { 42 | if (a[i] !== b[i]) { 43 | return true; 44 | } 45 | } 46 | return false; 47 | } 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2021 Jukka Kurkela 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$RESOLVED_VERSION' 2 | tag-template: 'v$RESOLVED_VERSION' 3 | categories: 4 | - title: '🚀 Features' 5 | labels: 6 | - 'enhancement' 7 | - title: '🐛 Bug Fixes' 8 | labels: 9 | - 'bug' 10 | - title: 'Types' 11 | labels: 12 | - 'types' 13 | - title: 'Documentation' 14 | labels: 15 | - 'documentation' 16 | - title: '🧰 Maintenance' 17 | labels: 18 | - 'chore' 19 | - 'dependencies' 20 | - 'performance' 21 | - 'refactoring' 22 | exclude-labels: 23 | - 'infrastructure' 24 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)' 25 | change-title-escapes: '\<*_&`#@' 26 | version-resolver: 27 | major: 28 | labels: 29 | - 'major' 30 | minor: 31 | labels: 32 | - 'minor' 33 | - 'enhancement' 34 | patch: 35 | labels: 36 | - 'patch' 37 | default: patch 38 | template: | 39 | # Essential Links 40 | 41 | * [npm](https://www.npmjs.com/package/chartjs-chart-treemap) 42 | * [Docs](https://chartjs-chart-treemap.pages.dev/) 43 | * [Samples](https://chartjs-chart-treemap.pages.dev/samples/basic.html) 44 | 45 | $CHANGES 46 | -------------------------------------------------------------------------------- /test/fixtures/grouped/captionsWithoutDisplay.js: -------------------------------------------------------------------------------- 1 | const data = [ 2 | {category: 'main', subcategory: 'one', value: 1}, 3 | {category: 'main', subcategory: 'one', value: 2}, 4 | {category: 'main', subcategory: 'one', value: 3}, 5 | {category: 'main', subcategory: 'two', value: 5}, 6 | {category: 'main', subcategory: 'two', value: 1}, 7 | {category: 'main', subcategory: 'two', value: 1}, 8 | {category: 'other', subcategory: 'one', value: 4}, 9 | {category: 'other', subcategory: 'one', value: 5}, 10 | {category: 'other', subcategory: 'two', value: 2}, 11 | {category: 'other', subcategory: 'two', value: 6}, 12 | ]; 13 | 14 | export default { 15 | tolerance: 0.0040, 16 | config: { 17 | type: 'treemap', 18 | data: { 19 | datasets: [{ 20 | tree: data, 21 | key: 'value', 22 | groups: ['category', 'subcategory', 'value'], 23 | backgroundColor: 'lightGreen', 24 | captions: { 25 | align: 'center' 26 | }, 27 | }] 28 | }, 29 | options: { 30 | events: [] 31 | } 32 | }, 33 | options: { 34 | spriteText: true, 35 | canvas: { 36 | height: 256, 37 | width: 512 38 | } 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /test/fixtures/headersbox/no-captions.js: -------------------------------------------------------------------------------- 1 | const arrayN = (n) => Array.from({length: n}).map((_, i) => i); 2 | 3 | const groups = arrayN(5); 4 | const tree = groups.reduce((acc, grp) => [ 5 | ...acc, 6 | ...arrayN(grp * 5).map(i => ({grp: `group: ${grp}`, sub: `sub: ${i}`, value: (i % 5) * 5})) 7 | ], []); 8 | 9 | const colors = ['red', 'green', 'blue', 'yellow', 'purple']; 10 | 11 | export default { 12 | config: { 13 | type: 'treemap', 14 | data: { 15 | datasets: [{ 16 | tree, 17 | backgroundColor: (ctx) => { 18 | if (ctx.raw.l === 0) { 19 | return 'dimgray'; 20 | } 21 | const parentGrp = ctx.raw._data.grp; 22 | return colors[parentGrp[parentGrp.length - 1]]; 23 | }, 24 | borderWidth: 0, 25 | spacing: 1, 26 | key: 'value', 27 | groups: ['grp', 'sub'], 28 | displayMode: 'headerBoxes', 29 | captions: { 30 | display: false, 31 | } 32 | }] 33 | }, 34 | options: { 35 | events: [] 36 | } 37 | }, 38 | options: { 39 | spriteText: true, 40 | canvas: { 41 | height: 300, 42 | width: 800 43 | } 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /docs/samples/dividers.md: -------------------------------------------------------------------------------- 1 | # Dividers 2 | 3 | ```js chart-editor 4 | // 5 | const data = [ 6 | {category: 'main', value: 1}, 7 | {category: 'main', value: 2}, 8 | {category: 'main', value: 3}, 9 | {category: 'other', value: 4}, 10 | {category: 'other', value: 5}, 11 | ]; 12 | // 13 | 14 | // 15 | const options = { 16 | plugins: { 17 | title: { 18 | display: true, 19 | text: 'Using dividers' 20 | }, 21 | legend: { 22 | display: false 23 | }, 24 | } 25 | }; 26 | // 27 | 28 | // 29 | const config = { 30 | type: 'treemap', 31 | data: { 32 | datasets: [{ 33 | tree: data, 34 | key: 'value', 35 | groups: ['category'], 36 | borderWidth: 1, 37 | borderColor: 'rgba(200,200,200,1)', 38 | borderRadius: 10, 39 | backgroundColor: 'rgba(220,230,220,0.3)', 40 | hoverBackgroundColor: 'rgba(220,230,220,0.5)', 41 | dividers: { 42 | display: true, 43 | lineDash: [3, 5], 44 | lineWidth: 2, 45 | } 46 | }] 47 | }, 48 | options: options 49 | }; 50 | 51 | // 52 | 53 | module.exports = { 54 | config, 55 | }; 56 | ``` 57 | -------------------------------------------------------------------------------- /docs/scripts/utils.js: -------------------------------------------------------------------------------- 1 | 2 | import {valueOrDefault} from 'chart.js/helpers'; 3 | 4 | // Adapted from http://indiegamr.com/generate-repeatable-random-numbers-in-js/ 5 | let _seed = Date.now(); 6 | 7 | export function srand(seed) { 8 | _seed = seed; 9 | } 10 | 11 | export function rand(min, max) { 12 | min = valueOrDefault(min, 0); 13 | max = valueOrDefault(max, 0); 14 | _seed = (_seed * 9301 + 49297) % 233280; 15 | return min + (_seed / 233280) * (max - min); 16 | } 17 | 18 | export function numbers(config) { 19 | const cfg = config || {}; 20 | const min = valueOrDefault(cfg.min, 0); 21 | const max = valueOrDefault(cfg.max, 100); 22 | const from = valueOrDefault(cfg.from, []); 23 | const count = valueOrDefault(cfg.count, 8); 24 | const decimals = valueOrDefault(cfg.decimals, 8); 25 | const continuity = valueOrDefault(cfg.continuity, 1); 26 | const dfactor = Math.pow(10, decimals) || 0; 27 | const data = []; 28 | let i, value; 29 | 30 | for (i = 0; i < count; ++i) { 31 | value = (from[i] || 0) + this.rand(min, max); 32 | if (this.rand() <= continuity) { 33 | data.push(Math.round(dfactor * value) / dfactor); 34 | } else { 35 | data.push(null); 36 | } 37 | } 38 | 39 | return data; 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/npmpublish.yml: -------------------------------------------------------------------------------- 1 | name: Node.js Package 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-node@v4 14 | with: 15 | cache: npm 16 | - name: Test 17 | run: | 18 | npm ci 19 | xvfb-run --auto-servernum npm test 20 | 21 | publish-npm: 22 | needs: test 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: actions/setup-node@v4 27 | with: 28 | cache: npm 29 | registry-url: https://registry.npmjs.org/ 30 | - name: Setup and build 31 | run: | 32 | npm ci 33 | npm run build 34 | npm pack 35 | - name: Publish @next 36 | run: npm publish --access=public --tag next 37 | if: ${{ github.event.release.prerelease }} 38 | env: 39 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 40 | - name: Publish @latest 41 | run: npm publish --access=public --tag latest 42 | if: ${{ !github.event.release.prerelease }} 43 | env: 44 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 45 | -------------------------------------------------------------------------------- /docs/.vuepress/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 9 | 12 | 14 | 15 | -------------------------------------------------------------------------------- /test/fixtures/grouped/treeSumKeys.js: -------------------------------------------------------------------------------- 1 | const data = { 2 | A: { 3 | C: { 4 | C1: { 5 | C1a: { 6 | value: 6.25, 7 | another: 1 8 | }, 9 | C1b: { 10 | value: 6.25, 11 | another: 2 12 | }, 13 | }, 14 | C2: { 15 | value: 12.5, 16 | another: 3 17 | } 18 | }, 19 | D: { 20 | value: 25, 21 | another: 4 22 | } 23 | }, 24 | B: { 25 | value: 50, 26 | another: 10 27 | }, 28 | G: { 29 | C: { 30 | value: 50, 31 | another: 5 32 | } 33 | }, 34 | }; 35 | 36 | export default { 37 | config: { 38 | type: 'treemap', 39 | data: { 40 | datasets: [{ 41 | tree: data, 42 | key: 'value', 43 | sumKeys: ['another'], 44 | groups: ['0', '1', '2', '_leaf'], 45 | backgroundColor: (ctx) => ctx.raw.vs.another % 2 === 1 ? 'red' : 'yellow', 46 | captions: { 47 | display: false 48 | }, 49 | labels: { 50 | display: true, 51 | formatter: (ctx) => [ctx.raw._data.label, ctx.raw.vs.another + ''] 52 | } 53 | }] 54 | }, 55 | options: { 56 | events: [] 57 | } 58 | }, 59 | options: { 60 | spriteText: true, 61 | canvas: { 62 | height: 256, 63 | width: 512 64 | } 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /test/fixtures/grouped/captionsAlign.js: -------------------------------------------------------------------------------- 1 | const data = [ 2 | {category: 'main', subcategory: 'one', value: 1}, 3 | {category: 'main', subcategory: 'one', value: 2}, 4 | {category: 'main', subcategory: 'one', value: 3}, 5 | {category: 'main', subcategory: 'two', value: 5}, 6 | {category: 'main', subcategory: 'two', value: 1}, 7 | {category: 'main', subcategory: 'two', value: 1}, 8 | {category: 'other', subcategory: 'one', value: 4}, 9 | {category: 'other', subcategory: 'one', value: 5}, 10 | {category: 'other', subcategory: 'two', value: 2}, 11 | {category: 'other', subcategory: 'two', value: 6}, 12 | ]; 13 | 14 | export default { 15 | tolerance: 0.0040, 16 | config: { 17 | type: 'treemap', 18 | data: { 19 | datasets: [{ 20 | tree: data, 21 | key: 'value', 22 | groups: ['category', 'subcategory', 'value'], 23 | backgroundColor: 'lightGreen', 24 | captions: { 25 | display: true, 26 | align: 'center' 27 | }, 28 | labels: { 29 | display: true, 30 | formatter(ctx) { 31 | return ctx.type === 'data' ? ctx.raw.v + '' : ''; 32 | } 33 | } 34 | }] 35 | }, 36 | options: { 37 | events: [] 38 | } 39 | }, 40 | options: { 41 | spriteText: true, 42 | canvas: { 43 | height: 256, 44 | width: 512 45 | } 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /test/fixtures/grouped/captionsPadding.js: -------------------------------------------------------------------------------- 1 | const data = [ 2 | {category: 'main', subcategory: 'one', value: 1}, 3 | {category: 'main', subcategory: 'one', value: 4}, 4 | {category: 'main', subcategory: 'one', value: 1}, 5 | {category: 'main', subcategory: 'two', value: 2}, 6 | {category: 'main', subcategory: 'two', value: 2}, 7 | {category: 'main', subcategory: 'two', value: 6}, 8 | {category: 'other', subcategory: 'one', value: 4}, 9 | {category: 'other', subcategory: 'one', value: 5}, 10 | {category: 'other', subcategory: 'two', value: 2}, 11 | {category: 'other', subcategory: 'two', value: 7}, 12 | ]; 13 | 14 | export default { 15 | tolerance: 0.0039, 16 | config: { 17 | type: 'treemap', 18 | data: { 19 | datasets: [{ 20 | tree: data, 21 | key: 'value', 22 | groups: ['category', 'subcategory', 'value'], 23 | backgroundColor: 'lightGreen', 24 | captions: { 25 | display: true, 26 | align: 'center', 27 | padding: 10 28 | }, 29 | labels: { 30 | display: true, 31 | formatter(ctx) { 32 | return ctx.type === 'data' ? ctx.raw.v + '' : ''; 33 | } 34 | } 35 | }] 36 | }, 37 | options: { 38 | events: [] 39 | } 40 | }, 41 | options: { 42 | spriteText: true, 43 | canvas: { 44 | height: 256, 45 | width: 512 46 | } 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /docs/samples/basic.md: -------------------------------------------------------------------------------- 1 | # Basic 2 | 3 | ```js chart-editor 4 | // 5 | const DATA_COUNT = 12; 6 | const NUMBER_CFG = {count: DATA_COUNT, min: 2, max: 40}; 7 | // 8 | 9 | // 10 | function colorFromRaw(ctx, border) { 11 | if (ctx.type !== 'data') { 12 | return 'transparent'; 13 | } 14 | const value = ctx.raw.v; 15 | let alpha = (1 + Math.log(value)) / 5; 16 | const color = 'purple'; 17 | if (border) { 18 | alpha += 0.5; 19 | } 20 | return helpers.color(color) 21 | .alpha(alpha) 22 | .rgbString(); 23 | } 24 | // 25 | 26 | // 27 | const config = { 28 | type: 'treemap', 29 | data: { 30 | datasets: [ 31 | { 32 | label: 'My First dataset', 33 | tree: Utils.numbers(NUMBER_CFG), 34 | borderColor: (ctx) => colorFromRaw(ctx, true), 35 | borderWidth: 1, 36 | spacing: -0.5, // Animations look better when overlapping a little 37 | backgroundColor: (ctx) => colorFromRaw(ctx), 38 | } 39 | ], 40 | }, 41 | }; 42 | 43 | // 44 | 45 | const actions = [ 46 | { 47 | name: 'Randomize', 48 | handler(chart) { 49 | chart.data.datasets.forEach(dataset => { 50 | dataset.tree = Utils.numbers(NUMBER_CFG); 51 | }); 52 | chart.update(); 53 | } 54 | }, 55 | ]; 56 | 57 | module.exports = { 58 | actions, 59 | config 60 | }; 61 | ``` 62 | -------------------------------------------------------------------------------- /test/fixtures/events/hoverCaptions.js: -------------------------------------------------------------------------------- 1 | const data = [ 2 | {category: 'main', subcategory: 'one', value: 1}, 3 | {category: 'main', subcategory: 'one', value: 6}, 4 | {category: 'main', subcategory: 'one', value: 3}, 5 | {category: 'main', subcategory: 'two', value: 5}, 6 | {category: 'main', subcategory: 'two', value: 8}, 7 | {category: 'main', subcategory: 'two', value: 2}, 8 | {category: 'other', subcategory: 'one', value: 5}, 9 | {category: 'other', subcategory: 'one', value: 4}, 10 | {category: 'other', subcategory: 'two', value: 3}, 11 | {category: 'other', subcategory: 'two', value: 6}, 12 | ]; 13 | 14 | export default { 15 | tolerance: 0.0045, 16 | config: { 17 | type: 'treemap', 18 | data: { 19 | datasets: [{ 20 | label: 'Simple treemap', 21 | tree: data, 22 | key: 'value', 23 | groups: ['category', 'subcategory', 'value'], 24 | backgroundColor: 'red', 25 | hoverBackgroundColor: 'green', 26 | captions: { 27 | hoverColor: 'white', 28 | font: { 29 | size: 16, 30 | weight: 'bold' 31 | } 32 | } 33 | }] 34 | } 35 | }, 36 | options: { 37 | canvas: { 38 | height: 256, 39 | width: 512 40 | }, 41 | run: (chart) => { 42 | const elem = chart.getDatasetMeta(0).data[0]; 43 | return triggerMouseEvent(chart, 'mousemove', elem.tooltipPosition()); 44 | } 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /samples/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function(Utils) { 4 | const chartjsUrl = 'https://cdn.jsdelivr.net/npm/chart.js/dist/chart.umd.js'; 5 | const dateFnsUrl = 'https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.bundle.js'; 6 | const localUrl = '../dist/chartjs-chart-treemap.js'; 7 | const remoteUrl = 'https://cdn.jsdelivr.net/npm/chartjs-chart-treemap@next/dist/chartjs-chart-treemap.js'; 8 | 9 | function addScript(url, done, error) { 10 | const head = document.getElementsByTagName('head')[0]; 11 | const script = document.createElement('script'); 12 | script.type = 'text/javascript'; 13 | script.onreadystatechange = function() { 14 | if (this.readyState === 'complete') { 15 | done(); 16 | } 17 | }; 18 | script.onload = done; 19 | script.onerror = error; 20 | script.src = url; 21 | head.appendChild(script); 22 | return true; 23 | } 24 | 25 | function loadError() { 26 | const msg = document.createTextNode('Error loading chartjs-chart-treemap'); 27 | document.body.appendChild(msg); 28 | return true; 29 | } 30 | 31 | Utils.load = function(done) { 32 | addScript(chartjsUrl, () => { 33 | addScript(dateFnsUrl, () => { 34 | addScript(localUrl, done, (event) => { 35 | event.preventDefault(); 36 | event.stopPropagation(); 37 | addScript(remoteUrl, done, loadError); 38 | }); 39 | }, loadError); 40 | }, loadError); 41 | }; 42 | }(window.Utils = window.Utils || {})); 43 | -------------------------------------------------------------------------------- /test/fixtures/grouped/captionsFont.js: -------------------------------------------------------------------------------- 1 | const data = [ 2 | {category: 'main', subcategory: 'one', value: 1}, 3 | {category: 'main', subcategory: 'one', value: 6}, 4 | {category: 'main', subcategory: 'one', value: 3}, 5 | {category: 'main', subcategory: 'two', value: 5}, 6 | {category: 'main', subcategory: 'two', value: 8}, 7 | {category: 'main', subcategory: 'two', value: 2}, 8 | {category: 'other', subcategory: 'one', value: 5}, 9 | {category: 'other', subcategory: 'one', value: 4}, 10 | {category: 'other', subcategory: 'two', value: 3}, 11 | {category: 'other', subcategory: 'two', value: 6}, 12 | ]; 13 | 14 | export default { 15 | tolerance: 0.011, 16 | config: { 17 | type: 'treemap', 18 | data: { 19 | datasets: [{ 20 | tree: data, 21 | key: 'value', 22 | groups: ['category', 'subcategory', 'value'], 23 | backgroundColor: 'lightGreen', 24 | spacing: 0, 25 | captions: { 26 | display: true, 27 | align: 'center', 28 | font: { 29 | size: 20, 30 | family: 'Courier' 31 | } 32 | }, 33 | labels: { 34 | display: true, 35 | formatter(ctx) { 36 | return ctx.type === 'data' ? ctx.raw.v : ''; 37 | } 38 | } 39 | }] 40 | }, 41 | options: { 42 | events: [] 43 | } 44 | }, 45 | options: { 46 | canvas: { 47 | height: 256, 48 | width: 512 49 | } 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /test/fixtures/grouped/captionsFormatter.js: -------------------------------------------------------------------------------- 1 | const data = [ 2 | {category: 'main', subcategory: 'one', value: 1}, 3 | {category: 'main', subcategory: 'one', value: 5}, 4 | {category: 'main', subcategory: 'one', value: 3}, 5 | {category: 'main', subcategory: 'two', value: 2}, 6 | {category: 'main', subcategory: 'two', value: 1}, 7 | {category: 'main', subcategory: 'two', value: 8}, 8 | {category: 'other', subcategory: 'one', value: 4}, 9 | {category: 'other', subcategory: 'one', value: 5}, 10 | {category: 'other', subcategory: 'two', value: 4}, 11 | {category: 'other', subcategory: 'two', value: 1}, 12 | ]; 13 | 14 | export default { 15 | tolerance: 0.0050, 16 | config: { 17 | type: 'treemap', 18 | data: { 19 | datasets: [{ 20 | tree: data, 21 | key: 'value', 22 | groups: ['category', 'subcategory', 'value'], 23 | backgroundColor: 'lightGreen', 24 | captions: { 25 | display: true, 26 | align: 'center', 27 | padding: 10, 28 | formatter(ctx) { 29 | return ctx.type === 'data' ? 'G: ' + ctx.raw.g : ''; 30 | } 31 | }, 32 | labels: { 33 | display: true, 34 | formatter(ctx) { 35 | return ctx.type === 'data' ? ctx.raw.v + '' : ''; 36 | } 37 | } 38 | }] 39 | }, 40 | options: { 41 | events: [] 42 | } 43 | }, 44 | options: { 45 | spriteText: true, 46 | canvas: { 47 | height: 256, 48 | width: 512 49 | } 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /src/statArray.js: -------------------------------------------------------------------------------- 1 | const min = Math.min; 2 | const max = Math.max; 3 | 4 | function getStat(sa) { 5 | return { 6 | min: sa.min, 7 | max: sa.max, 8 | sum: sa.sum, 9 | nmin: sa.nmin, 10 | nmax: sa.nmax, 11 | nsum: sa.nsum 12 | }; 13 | } 14 | 15 | function getNewStat(sa, o) { 16 | const v = +o[sa.key]; 17 | const n = v * sa.ratio; 18 | o._normalized = n; 19 | 20 | return { 21 | min: min(sa.min, v), 22 | max: max(sa.max, v), 23 | sum: sa.sum + v, 24 | nmin: min(sa.nmin, n), 25 | nmax: max(sa.nmax, n), 26 | nsum: sa.nsum + n 27 | }; 28 | } 29 | 30 | function setStat(sa, stat) { 31 | Object.assign(sa, stat); 32 | } 33 | 34 | function push(sa, o, stat) { 35 | sa._arr.push(o); 36 | setStat(sa, stat); 37 | } 38 | 39 | export default class StatArray { 40 | constructor(key, ratio) { 41 | const me = this; 42 | me.key = key; 43 | me.ratio = ratio; 44 | me.reset(); 45 | } 46 | 47 | get length() { 48 | return this._arr.length; 49 | } 50 | 51 | reset() { 52 | const me = this; 53 | me._arr = []; 54 | me._hist = []; 55 | me.sum = 0; 56 | me.nsum = 0; 57 | me.min = Infinity; 58 | me.max = -Infinity; 59 | me.nmin = Infinity; 60 | me.nmax = -Infinity; 61 | } 62 | 63 | push(o) { 64 | push(this, o, getNewStat(this, o)); 65 | } 66 | 67 | pushIf(o, fn, ...args) { 68 | const nstat = getNewStat(this, o); 69 | if (!fn(getStat(this), nstat, args)) { 70 | return o; 71 | } 72 | push(this, o, nstat); 73 | } 74 | 75 | get() { 76 | return this._arr; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /samples/dividers.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Basic treemap sample with dividers 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 |
17 | 18 | 19 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /docs/samples/tree.md: -------------------------------------------------------------------------------- 1 | # Tree 2 | 3 | ```js chart-editor 4 | // 5 | const options = { 6 | plugins: { 7 | title: { 8 | display: true 9 | }, 10 | legend: { 11 | display: false 12 | }, 13 | tooltip: { 14 | callbacks: { 15 | title(items) { 16 | const dataItem = items[0].raw; 17 | const obj = dataItem._data; 18 | return obj.name; 19 | }, 20 | } 21 | } 22 | } 23 | }; 24 | // 25 | 26 | // 27 | const config = { 28 | type: 'treemap', 29 | data: { 30 | datasets: [{ 31 | tree: Data.objectsTree, 32 | treeLeafKey: 'name', 33 | key: 'value', 34 | groups: [], 35 | spacing: 1, 36 | borderWidth: 0.5, 37 | borderColor: '#FF8F00', 38 | backgroundColor: 'rgba(255,167,38,0.3)', 39 | hoverBackgroundColor: 'rgba(238,238,238,0.5)', 40 | captions: { 41 | align: 'center' 42 | }, 43 | labels: { 44 | display: true, 45 | formatter: (ctx) => { 46 | return ctx.raw.v; 47 | } 48 | } 49 | }] 50 | }, 51 | options 52 | }; 53 | // 54 | 55 | function toggle(chart) { 56 | const dataset = chart.data.datasets[0]; 57 | if (dataset.groups.length) { 58 | dataset.groups = []; 59 | } else { 60 | dataset.groups = [0, 1]; 61 | dataset.groups.push('name'); 62 | } 63 | chart.update(); 64 | } 65 | 66 | const actions = [ 67 | { 68 | name: 'Toggle GroupBy', 69 | handler: (chart) => toggle(chart) 70 | } 71 | ]; 72 | 73 | module.exports = { 74 | actions, 75 | config, 76 | }; 77 | ``` 78 | -------------------------------------------------------------------------------- /docs/samples/zoom.md: -------------------------------------------------------------------------------- 1 | # Using Zoom plugin 2 | 3 | ```js chart-editor 4 | // 5 | const DATA_COUNT = 12; 6 | const NUMBER_CFG = {count: DATA_COUNT, min: 2, max: 40}; 7 | // 8 | 9 | // 10 | function colorFromRaw(ctx, border) { 11 | if (ctx.type !== 'data') { 12 | return 'transparent'; 13 | } 14 | const value = ctx.raw.v; 15 | let alpha = (1 + Math.log(value)) / 5; 16 | const color = 'orange'; 17 | if (border) { 18 | alpha += 0.01; 19 | } 20 | return helpers.color(color) 21 | .alpha(alpha) 22 | .rgbString(); 23 | } 24 | // 25 | 26 | // 27 | const config = { 28 | type: 'treemap', 29 | data: { 30 | datasets: [ 31 | { 32 | label: 'My First dataset', 33 | tree: Utils.numbers(NUMBER_CFG), 34 | borderWidth: 0, 35 | backgroundColor: (ctx) => colorFromRaw(ctx), 36 | labels: { 37 | display: true, 38 | formatter: (ctx) => ctx.raw.v.toFixed(2), 39 | font: { 40 | size: 16 41 | }, 42 | overflow: 'fit' 43 | } 44 | } 45 | ], 46 | }, 47 | options: { 48 | plugins: { 49 | zoom: { 50 | zoom: { 51 | wheel: { 52 | enabled: true, 53 | } 54 | }, 55 | pan: { 56 | enabled: true, 57 | } 58 | } 59 | } 60 | } 61 | }; 62 | 63 | // 64 | 65 | const actions = [ 66 | { 67 | name: 'Reset zoom', 68 | handler(chart) { 69 | chart.resetZoom(); 70 | } 71 | }, 72 | ]; 73 | 74 | module.exports = { 75 | actions, 76 | config, 77 | }; 78 | ``` 79 | -------------------------------------------------------------------------------- /samples/basic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Basic treemap sample 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 |
17 | 18 | 19 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /test/fixtures/grouped/captionsTruncating.js: -------------------------------------------------------------------------------- 1 | const longCategory1 = 'This is a long category name that should be truncated'; 2 | const longCategory2 = 'This is another long category name that should be truncated'; 3 | const longSubcategory1 = 'This is a long subcategory name that should be truncated'; 4 | const longSubcategory2 = 'This is another long subcategory name that should be truncated'; 5 | 6 | const data = [ 7 | {category: longCategory1, subcategory: longSubcategory1, value: 1}, 8 | {category: longCategory1, subcategory: longSubcategory1, value: 5}, 9 | {category: longCategory1, subcategory: longSubcategory1, value: 3}, 10 | {category: longCategory1, subcategory: longSubcategory2, value: 2}, 11 | {category: longCategory1, subcategory: longSubcategory2, value: 1}, 12 | {category: longCategory1, subcategory: longSubcategory2, value: 8}, 13 | {category: longCategory2, subcategory: longSubcategory1, value: 4}, 14 | {category: longCategory2, subcategory: longSubcategory1, value: 5}, 15 | {category: longCategory2, subcategory: longSubcategory2, value: 4}, 16 | {category: longCategory2, subcategory: longSubcategory2, value: 1}, 17 | ]; 18 | 19 | export default { 20 | tolerance: 0.0050, 21 | config: { 22 | type: 'treemap', 23 | data: { 24 | datasets: [{ 25 | tree: data, 26 | key: 'value', 27 | groups: ['category', 'subcategory', 'value'], 28 | backgroundColor: 'lightGreen', 29 | captions: { 30 | display: true, 31 | align: 'center', 32 | padding: 10, 33 | }, 34 | }] 35 | }, 36 | options: { 37 | events: [] 38 | } 39 | }, 40 | options: { 41 | spriteText: true, 42 | canvas: { 43 | height: 256, 44 | width: 512 45 | } 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve'; 2 | import terser from '@rollup/plugin-terser'; 3 | import json from '@rollup/plugin-json'; 4 | import {readFileSync} from 'fs'; 5 | 6 | const {author, name, module, jsdelivr, version, homepage, main, license} = JSON.parse(readFileSync('./package.json')); 7 | 8 | const banner = `/*! 9 | * ${name} v${version} 10 | * ${homepage} 11 | * (c) ${(new Date(process.env.SOURCE_DATE_EPOCH ? (process.env.SOURCE_DATE_EPOCH * 1000) : new Date().getTime())).getFullYear()} ${author} 12 | * Released under the ${license} license 13 | */`; 14 | 15 | const input = 'src/index.js'; 16 | const inputESM = 'src/index.esm.js'; 17 | const external = [ 18 | 'chart.js', 19 | 'chart.js/helpers' 20 | ]; 21 | const globals = { 22 | 'chart.js': 'Chart', 23 | 'chart.js/helpers': 'Chart.helpers' 24 | }; 25 | 26 | export default [ 27 | { 28 | input: inputESM, 29 | output: { 30 | file: module, 31 | banner, 32 | format: 'esm', 33 | indent: false 34 | }, 35 | plugins: [ 36 | resolve(), 37 | json(), 38 | ], 39 | external 40 | }, 41 | { 42 | input, 43 | output: { 44 | name, 45 | banner, 46 | file: main, 47 | format: 'umd', 48 | indent: false, 49 | globals 50 | }, 51 | plugins: [ 52 | json() 53 | ], 54 | external 55 | }, 56 | { 57 | input, 58 | output: { 59 | name: name, 60 | file: jsdelivr, 61 | format: 'umd', 62 | indent: false, 63 | sourcemap: true, 64 | globals 65 | }, 66 | plugins: [ 67 | resolve(), 68 | json(), 69 | terser({ 70 | output: { 71 | preamble: banner 72 | } 73 | }), 74 | ], 75 | external 76 | }, 77 | ]; 78 | -------------------------------------------------------------------------------- /docs/samples/datalabels.md: -------------------------------------------------------------------------------- 1 | # Using Datalabels plugin 2 | 3 | ```js chart-editor 4 | // 5 | const DATA_COUNT = 12; 6 | const NUMBER_CFG = {count: DATA_COUNT, min: 2, max: 40}; 7 | // 8 | 9 | // 10 | function colorFromRaw(ctx, border) { 11 | if (ctx.type !== 'data') { 12 | return 'transparent'; 13 | } 14 | const value = ctx.raw.v; 15 | let alpha = (1 + Math.log(value)) / 5; 16 | const color = 'purple'; 17 | if (border) { 18 | alpha += 0.01; 19 | } 20 | return helpers.color(color) 21 | .alpha(alpha) 22 | .rgbString(); 23 | } 24 | // 25 | 26 | // 27 | const config = { 28 | type: 'treemap', 29 | data: { 30 | datasets: [ 31 | { 32 | label: 'My First dataset', 33 | tree: Utils.numbers(NUMBER_CFG), 34 | borderColor: (ctx) => colorFromRaw(ctx, true), 35 | borderWidth: 1, 36 | spacing: 0, 37 | backgroundColor: (ctx) => colorFromRaw(ctx), 38 | datalabels: { 39 | display: 'auto', 40 | anchor: 'start', 41 | align: 45, 42 | formatter: (value) => Math.trunc(value.v), 43 | color: 'white', 44 | font: { 45 | size: 20 46 | } 47 | } 48 | } 49 | ], 50 | }, 51 | options: { 52 | plugins: { 53 | datalabels: { 54 | display: true, 55 | } 56 | } 57 | } 58 | }; 59 | 60 | // 61 | 62 | const actions = [ 63 | { 64 | name: 'Randomize', 65 | handler(chart) { 66 | chart.data.datasets.forEach(dataset => { 67 | dataset.tree = Utils.numbers(NUMBER_CFG); 68 | }); 69 | chart.update(); 70 | } 71 | }, 72 | ]; 73 | 74 | module.exports = { 75 | actions, 76 | config, 77 | }; 78 | ``` 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chartjs-chart-treemap 2 | 3 | [Chart.js](https://www.chartjs.org/) **v3.8+, v4+** module for creating treemap charts. Implementation for Chart.js v2 is in [2.x branch](https://github.com/kurkle/chartjs-chart-treemap/tree/2.x) 4 | 5 | [![npm](https://img.shields.io/npm/v/chartjs-chart-treemap.svg)](https://www.npmjs.com/package/chartjs-chart-treemap) 6 | [![release](https://img.shields.io/github/release/kurkle/chartjs-chart-treemap.svg?style=flat-square)](https://github.com/kurkle/chartjs-chart-treemap/releases/latest) 7 | ![npm bundle size](https://img.shields.io/bundlephobia/min/chartjs-chart-treemap.svg) 8 | ![GitHub](https://img.shields.io/github/license/kurkle/chartjs-chart-treemap.svg) 9 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=kurkle_chartjs-chart-treemap&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=kurkle_chartjs-chart-treemap) 10 | [![documentation](https://img.shields.io/static/v1?message=Documentation&color=informational)](https://chartjs-chart-treemap.pages.dev) 11 | 12 | ![TreeMap Example Image](treemap.png) 13 | 14 | ## Documentation 15 | 16 | You can find documentation for chartjs-chart-treemap at [https://chartjs-chart-treemap.pages.dev/](https://chartjs-chart-treemap.pages.dev/). 17 | 18 | ## Development 19 | 20 | You first need to install node dependencies (requires [Node.js](https://nodejs.org/)): 21 | 22 | ```bash 23 | > npm install 24 | ``` 25 | 26 | The following commands will then be available from the repository root: 27 | 28 | ```bash 29 | > npm run build // build dist files 30 | > npm run dev // build and watch for changes 31 | > npm test // run all tests 32 | > npm run lint // perform code linting 33 | > npm package // create an archive with dist files and samples 34 | ``` 35 | 36 | ## License 37 | 38 | chartjs-chart-treemap is available under the [MIT license](https://opensource.org/licenses/MIT). 39 | -------------------------------------------------------------------------------- /docs/samples/rtl.md: -------------------------------------------------------------------------------- 1 | # RTL 2 | 3 | ```js chart-editor 4 | // 5 | const DATA_COUNT = 12; 6 | const NUMBER_CFG = {count: DATA_COUNT, min: 2, max: 40}; 7 | 8 | function capitalizeFirstLetter(string) { 9 | return string.charAt(0).toUpperCase() + string.slice(1); 10 | } 11 | // 12 | 13 | // 14 | const options = { 15 | plugins: { 16 | title: { 17 | display: true, 18 | text: (ctx) => 'RTL: ' + !!ctx.chart.data.datasets[0].rtl 19 | }, 20 | legend: { 21 | display: false 22 | }, 23 | tooltip: { 24 | callbacks: { 25 | title(items) { 26 | return capitalizeFirstLetter(items[0].dataset.key); 27 | }, 28 | label(item) { 29 | const dataItem = item.raw; 30 | const obj = dataItem._data; 31 | const label = obj.state || obj.division || obj.region; 32 | return label + ': ' + dataItem.v; 33 | } 34 | } 35 | } 36 | } 37 | }; 38 | // 39 | 40 | // 41 | const config = { 42 | type: 'treemap', 43 | data: { 44 | datasets: [{ 45 | tree: Data.statsByState, 46 | key: 'area', 47 | groups: ['state'], 48 | spacing: -0.5, 49 | borderWidth: 0.5, 50 | borderColor: 'rgba(200,200,200,1)', 51 | hoverBackgroundColor: 'rgba(220,230,220,0.5)', 52 | rtl: false 53 | }] 54 | }, 55 | options: options 56 | }; 57 | 58 | // 59 | function toggle(chart, group) { 60 | const idx = GROUPS.indexOf(group); 61 | if (idx === -1) { 62 | GROUPS.push(group); 63 | } else { 64 | GROUPS.splice(idx, 1); 65 | } 66 | chart.update(); 67 | } 68 | 69 | const actions = [ 70 | { 71 | name: 'Toggle RTL', 72 | handler: (chart) => { 73 | chart.data.datasets[0].rtl = !chart.data.datasets[0].rtl; 74 | chart.update(); 75 | } 76 | }, 77 | ]; 78 | 79 | module.exports = { 80 | actions, 81 | config, 82 | }; 83 | ``` 84 | -------------------------------------------------------------------------------- /docs/samples/groups.md: -------------------------------------------------------------------------------- 1 | # Groups 2 | 3 | ```js chart-editor 4 | // 5 | const GROUPS = ['region']; 6 | 7 | function capitalizeFirstLetter(string) { 8 | return string.charAt(0).toUpperCase() + string.slice(1); 9 | } 10 | // 11 | 12 | // 13 | const options = { 14 | plugins: { 15 | title: { 16 | display: true, 17 | text: (ctx) => 'US area by ' + GROUPS.join(' / ') 18 | }, 19 | legend: { 20 | display: false 21 | }, 22 | tooltip: { 23 | callbacks: { 24 | title(items) { 25 | return capitalizeFirstLetter(items[0].dataset.key); 26 | }, 27 | label(item) { 28 | const dataItem = item.raw; 29 | const obj = dataItem._data; 30 | const label = obj.state || obj.division || obj.region; 31 | return label + ': ' + dataItem.v; 32 | } 33 | } 34 | } 35 | } 36 | }; 37 | // 38 | 39 | // 40 | const config = { 41 | type: 'treemap', 42 | data: { 43 | datasets: [{ 44 | tree: Data.statsByState, 45 | key: 'area', 46 | groups: GROUPS, 47 | spacing: 1, 48 | borderWidth: 0.5, 49 | borderColor: 'rgba(200,200,200,1)', 50 | backgroundColor: 'rgba(220,230,220,0.3)', 51 | hoverBackgroundColor: 'rgba(220,230,220,0.5)', 52 | }] 53 | }, 54 | options: options 55 | }; 56 | 57 | // 58 | function toggle(chart, group) { 59 | const idx = GROUPS.indexOf(group); 60 | if (idx === -1) { 61 | GROUPS.push(group); 62 | } else { 63 | GROUPS.splice(idx, 1); 64 | } 65 | chart.update(); 66 | } 67 | 68 | const actions = [ 69 | { 70 | name: 'Toggle Region', 71 | handler: (chart) => toggle(chart, 'region') 72 | }, 73 | { 74 | name: 'Toggle Division', 75 | handler: (chart) => toggle(chart, 'division') 76 | }, 77 | { 78 | name: 'Toggle State', 79 | handler: (chart) => toggle(chart, 'state') 80 | }, 81 | ]; 82 | 83 | module.exports = { 84 | actions, 85 | config, 86 | }; 87 | ``` 88 | -------------------------------------------------------------------------------- /docs/samples/labelsFontsAndColors.md: -------------------------------------------------------------------------------- 1 | # Fonts and colors 2 | 3 | ```js chart-editor 4 | // 5 | const DATA = [ 6 | { 7 | what: 'Apples', 8 | value: 98, 9 | color: 'rgb(191, 77, 114)' 10 | }, 11 | { 12 | what: 'Orange', 13 | value: 75, 14 | color: 'rgb(228, 148, 55)' 15 | }, 16 | { 17 | what: 'Lime', 18 | value: 69, 19 | color: 'rgb(147, 119, 214)' 20 | }, 21 | { 22 | what: 'Grapes', 23 | value: 55, 24 | color: 'rgb(80, 134, 55)' 25 | }, 26 | { 27 | what: 'Apricots', 28 | value: 49, 29 | color: 'rgb(90, 97, 110)' 30 | }, 31 | { 32 | what: 'Blackberries', 33 | value: 35, 34 | color: 'rgb(34, 38, 82)' 35 | } 36 | ]; 37 | 38 | // 39 | 40 | // 41 | const config = { 42 | type: 'treemap', 43 | data: { 44 | datasets: [ 45 | { 46 | label: 'Fruits', 47 | tree: DATA, 48 | key: 'value', 49 | borderWidth: 0, 50 | borderRadius: 6, 51 | spacing: 1, 52 | backgroundColor(ctx) { 53 | if (ctx.type !== 'data') { 54 | return 'transparent'; 55 | } 56 | return ctx.raw._data.color; 57 | }, 58 | labels: { 59 | align: 'left', 60 | display: true, 61 | formatter(ctx) { 62 | if (ctx.type !== 'data') { 63 | return; 64 | } 65 | return [ctx.raw._data.what, 'Value is ' + ctx.raw.v]; 66 | }, 67 | color: ['white', 'whiteSmoke'], 68 | font: [{size: 20, weight: 'bold'}, {size: 12}], 69 | position: 'top' 70 | } 71 | } 72 | ], 73 | }, 74 | options: { 75 | events: [], 76 | plugins: { 77 | title: { 78 | display: true, 79 | text: 'Different fonts and colors on labels' 80 | }, 81 | legend: { 82 | display: false 83 | }, 84 | tooltip: { 85 | enabled: false 86 | } 87 | } 88 | } 89 | }; 90 | 91 | // 92 | 93 | module.exports = { 94 | config 95 | }; 96 | ``` 97 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main, 2.x ] 6 | pull_request: 7 | branches: [ main, 2.x ] 8 | 9 | jobs: 10 | ci: 11 | name: Continuous Integration 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-node@v4 16 | with: 17 | cache: npm 18 | 19 | - name: install 20 | run: npm ci 21 | 22 | - name: build 23 | run: npm run build 24 | 25 | - name: test 26 | run: xvfb-run --auto-servernum npm test 27 | shell: bash 28 | 29 | - name: cache coverage reports 30 | uses: actions/cache/save@v4 31 | with: 32 | path: | 33 | coverage/chrome/lcov.info 34 | coverage/firefox/lcov.info 35 | key: coverage 36 | 37 | sonar: 38 | runs-on: ubuntu-latest 39 | needs: [ci] 40 | # dependabot etc pull request can't have the token, so lets run only on merges. 41 | if: ${{ github.event_name == 'push' }} 42 | steps: 43 | - uses: actions/checkout@v4 44 | with: 45 | fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis (SonarCloud) 46 | 47 | - uses: actions/setup-node@v4 48 | with: 49 | cache: npm 50 | 51 | - name: Restore coverage from cache 52 | uses: actions/cache/restore@v4 53 | with: 54 | path: | 55 | coverage/chrome/lcov.info 56 | coverage/firefox/lcov.info 57 | key: coverage 58 | restore-keys: coverage 59 | 60 | - name: Read package version 61 | id: package-version 62 | uses: martinbeentjes/npm-get-version-action@v1.3.1 63 | 64 | - name: Set version 65 | run: echo -e "\nsonar.projectVersion=${{ steps.package-version.outputs.current-version}}" >> sonar-project.properties 66 | 67 | - name: SonarCloud Scan 68 | uses: SonarSource/sonarcloud-github-action@master 69 | env: 70 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 71 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 72 | -------------------------------------------------------------------------------- /src/squarify.js: -------------------------------------------------------------------------------- 1 | import {sum, index, sort, flatten} from './utils'; 2 | import Rect from './rect'; 3 | import StatArray from './statArray'; 4 | 5 | function compareAspectRatio(oldStat, newStat, args) { 6 | if (oldStat.sum === 0) { 7 | return true; 8 | } 9 | 10 | const [length] = args; 11 | const os2 = oldStat.nsum * oldStat.nsum; 12 | const ns2 = newStat.nsum * newStat.nsum; 13 | const l2 = length * length; 14 | const or = Math.max(l2 * oldStat.nmax / os2, os2 / (l2 * oldStat.nmin)); 15 | const nr = Math.max(l2 * newStat.nmax / ns2, ns2 / (l2 * newStat.nmin)); 16 | return nr <= or; 17 | } 18 | 19 | /** 20 | * 21 | * @param {number[]|object[]} values 22 | * @param {object} rectangle 23 | * @param {string} [key] 24 | * @param {string} [grp] 25 | * @param {number} [lvl] 26 | * @param {number} [gsum] 27 | */ 28 | export default function squarify(values, rectangle, keys = [], grp, lvl, gsum) { 29 | values = values || []; 30 | const rows = []; 31 | const rect = new Rect(rectangle); 32 | const row = new StatArray('value', rect.area / sum(values, keys[0])); 33 | let length = rect.side; 34 | const n = values.length; 35 | let i, o; 36 | 37 | if (!n) { 38 | return rows; 39 | } 40 | 41 | const tmp = values.slice(); 42 | let key = index(tmp, keys[0]); 43 | 44 | if (!rectangle?.unsorted) { 45 | sort(tmp, key); 46 | } 47 | 48 | const val = (idx) => key ? +tmp[idx][key] : +tmp[idx]; 49 | const gval = (idx) => grp && tmp[idx][grp]; 50 | 51 | for (i = 0; i < n; ++i) { 52 | o = {value: val(i), groupSum: gsum, _data: values[tmp[i]._idx], level: undefined, group: undefined}; 53 | if (grp) { 54 | o.level = lvl; 55 | o.group = gval(i); 56 | const tmpRef = tmp[i]; 57 | o.values = keys.reduce(function(obj, k) { 58 | obj[k] = +tmpRef[k]; 59 | return obj; 60 | }, {}); 61 | } 62 | o = row.pushIf(o, compareAspectRatio, length); 63 | if (o) { 64 | rows.push(rect.map(row)); 65 | length = rect.side; 66 | row.reset(); 67 | row.push(o); 68 | } 69 | } 70 | if (row.length) { 71 | rows.push(rect.map(row)); 72 | } 73 | return flatten(rows); 74 | } 75 | -------------------------------------------------------------------------------- /docs/samples/captions.md: -------------------------------------------------------------------------------- 1 | # Captions 2 | 3 | ```js chart-editor 4 | // 5 | const GROUPS = ['region', 'division', 'state']; 6 | const DATA_COUNT = 12; 7 | const NUMBER_CFG = {count: DATA_COUNT, min: 2, max: 40}; 8 | 9 | function capitalizeFirstLetter(string) { 10 | return string.charAt(0).toUpperCase() + string.slice(1); 11 | } 12 | // 13 | 14 | // 15 | const options = { 16 | plugins: { 17 | title: { 18 | display: true, 19 | text: (ctx) => 'US area by ' + GROUPS.join(' / ') 20 | }, 21 | legend: { 22 | display: false 23 | }, 24 | tooltip: { 25 | callbacks: { 26 | title(items) { 27 | return capitalizeFirstLetter(items[0].dataset.key); 28 | }, 29 | label(item) { 30 | const dataItem = item.raw; 31 | const obj = dataItem._data; 32 | const label = obj.state || obj.division || obj.region; 33 | return label + ': ' + dataItem.v; 34 | } 35 | } 36 | } 37 | } 38 | }; 39 | // 40 | 41 | // 42 | const config = { 43 | type: 'treemap', 44 | data: { 45 | datasets: [{ 46 | tree: Data.statsByState, 47 | key: 'area', 48 | groups: GROUPS, 49 | spacing: 1, 50 | borderWidth: 0.5, 51 | borderColor: 'rgba(200,200,200,1)', 52 | backgroundColor: 'rgba(220,230,220,0.3)', 53 | hoverBackgroundColor: 'rgba(220,230,220,0.5)', 54 | captions: { 55 | align: 'center', 56 | display: true, 57 | color: 'red', 58 | font: { 59 | size: 14, 60 | }, 61 | hoverFont: { 62 | size: 16, 63 | weight: 'bold' 64 | }, 65 | padding: 5 66 | }, 67 | labels: { 68 | display: false, 69 | overflow: 'hidden' 70 | } 71 | }] 72 | }, 73 | options: options 74 | }; 75 | // 76 | 77 | const actions = [ 78 | { 79 | name: 'Toggle labels', 80 | handler(chart) { 81 | const labels = chart.data.datasets[0].labels; 82 | labels.display = !labels.display; 83 | chart.update(); 84 | } 85 | } 86 | ]; 87 | 88 | module.exports = { 89 | config, 90 | actions 91 | }; 92 | ``` 93 | -------------------------------------------------------------------------------- /docs/samples/labels.md: -------------------------------------------------------------------------------- 1 | # Labels 2 | 3 | ```js chart-editor 4 | // 5 | const DATA_COUNT = 12; 6 | const NUMBER_CFG = {count: DATA_COUNT, min: 2, max: 40}; 7 | 8 | const INTL_NUM_FORMAT = new Intl.NumberFormat('us', { 9 | style: 'unit', 10 | unit: 'kilometer', 11 | unitDisplay: 'short', 12 | minimumFractionDigits: 1, 13 | maximumFractionDigits: 1}); 14 | 15 | // 16 | 17 | // 18 | function colorFromRaw(ctx) { 19 | if (ctx.type !== 'data') { 20 | return 'transparent'; 21 | } 22 | const value = ctx.raw.v; 23 | let alpha = (1 + Math.log(value)) / 5; 24 | const color = 'orange'; 25 | return helpers.color(color) 26 | .alpha(alpha) 27 | .rgbString(); 28 | } 29 | 30 | // 31 | 32 | // 33 | const config = { 34 | type: 'treemap', 35 | data: { 36 | datasets: [ 37 | { 38 | label: 'My First dataset', 39 | tree: Utils.numbers(NUMBER_CFG), 40 | borderColor: 'red', 41 | borderWidth: 0.5, 42 | spacing: 0, 43 | backgroundColor: (ctx) => colorFromRaw(ctx), 44 | labels: { 45 | align: 'left', 46 | display: true, 47 | formatter(ctx) { 48 | if (ctx.type !== 'data') { 49 | return; 50 | } 51 | return INTL_NUM_FORMAT.format(ctx.raw.v); 52 | }, 53 | color: 'black', 54 | font: { 55 | size: 16, 56 | }, 57 | hoverFont: { 58 | size: 24, 59 | weight: 'bold' 60 | }, 61 | position: 'center' 62 | } 63 | } 64 | ], 65 | }, 66 | options: { 67 | plugins: { 68 | title: { 69 | display: true, 70 | text: 'Labelling data' 71 | }, 72 | legend: { 73 | display: false 74 | } 75 | } 76 | } 77 | }; 78 | 79 | // 80 | 81 | const actions = [ 82 | { 83 | name: 'Randomize', 84 | handler(chart) { 85 | chart.data.datasets.forEach(dataset => { 86 | dataset.tree = Utils.numbers(NUMBER_CFG); 87 | }); 88 | chart.update(); 89 | } 90 | }, 91 | ]; 92 | 93 | module.exports = { 94 | actions, 95 | config, 96 | }; 97 | ``` 98 | -------------------------------------------------------------------------------- /src/rect.js: -------------------------------------------------------------------------------- 1 | function getDims(itm, w2, s2, key) { 2 | const a = itm._normalized; 3 | const ar = w2 * a / s2; 4 | const d1 = Math.sqrt(a * ar); 5 | const d2 = a / d1; 6 | const w = key === '_ix' ? d1 : d2; 7 | const h = key === '_ix' ? d2 : d1; 8 | 9 | return {d1, d2, w, h}; 10 | } 11 | 12 | const getX = (rect, w) => rect.rtl ? rect.x + rect.iw - w : rect.x + rect._ix; 13 | 14 | function buildRow(rect, itm, dims, sum) { 15 | const r = { 16 | x: getX(rect, dims.w), 17 | y: rect.y + rect._iy, 18 | w: dims.w, 19 | h: dims.h, 20 | a: itm._normalized, 21 | v: itm.value, 22 | vs: itm.values, 23 | s: sum, 24 | _data: itm._data 25 | }; 26 | if (itm.group) { 27 | r.g = itm.group; 28 | r.l = itm.level; 29 | r.gs = itm.groupSum; 30 | } 31 | return r; 32 | } 33 | 34 | export default class Rect { 35 | constructor(r) { 36 | r = r || {w: 1, h: 1}; 37 | this.rtl = !!r.rtl; 38 | this.unsorted = !!r.unsorted; 39 | this.x = r.x || r.left || 0; 40 | this.y = r.y || r.top || 0; 41 | this._ix = 0; 42 | this._iy = 0; 43 | this.w = r.w || r.width || (r.right - r.left); 44 | this.h = r.h || r.height || (r.bottom - r.top); 45 | } 46 | 47 | get area() { 48 | return this.w * this.h; 49 | } 50 | 51 | get iw() { 52 | return this.w - this._ix; 53 | } 54 | 55 | get ih() { 56 | return this.h - this._iy; 57 | } 58 | 59 | get dir() { 60 | const ih = this.ih; 61 | return ih <= this.iw && ih > 0 ? 'y' : 'x'; 62 | } 63 | 64 | get side() { 65 | return this.dir === 'x' ? this.iw : this.ih; 66 | } 67 | 68 | map(arr) { 69 | const {dir, side} = this; 70 | const key = dir === 'x' ? '_ix' : '_iy'; 71 | const sum = arr.nsum; 72 | const row = arr.get(); 73 | const w2 = side * side; 74 | const s2 = sum * sum; 75 | const ret = []; 76 | let maxd2 = 0; 77 | let totd1 = 0; 78 | for (const itm of row) { 79 | const dims = getDims(itm, w2, s2, key); 80 | totd1 += dims.d1; 81 | maxd2 = Math.max(maxd2, dims.d2); 82 | ret.push(buildRow(this, itm, dims, arr.sum)); 83 | this[key] += dims.d1; 84 | } 85 | 86 | this[dir === 'x' ? '_iy' : '_ix'] += maxd2; 87 | this[key] -= totd1; 88 | return ret; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /docs/samples/displayMode.md: -------------------------------------------------------------------------------- 1 | # Display Mode 2 | 3 | ```js chart-editor 4 | // 5 | let DISPLAY_MODE = 'containerBoxes'; 6 | 7 | function capitalizeFirstLetter(string) { 8 | return string.charAt(0).toUpperCase() + string.slice(1); 9 | } 10 | // 11 | 12 | // 13 | const options = { 14 | plugins: { 15 | title: { 16 | display: true, 17 | text: 'US area by division / state' 18 | }, 19 | legend: { 20 | display: false 21 | }, 22 | tooltip: { 23 | callbacks: { 24 | title(items) { 25 | return capitalizeFirstLetter(items[0].dataset.key); 26 | }, 27 | label(item) { 28 | const dataItem = item.raw; 29 | const obj = dataItem._data; 30 | const label = obj.state || obj.division || obj.region; 31 | return label + ': ' + dataItem.v; 32 | } 33 | } 34 | } 35 | } 36 | }; 37 | // 38 | 39 | // 40 | const config = { 41 | type: 'treemap', 42 | data: { 43 | datasets: [{ 44 | tree: Data.statsByState, 45 | key: 'area', 46 | groups: ['division', 'state'], 47 | spacing: 2, 48 | borderWidth: 1, 49 | borderColor: 'rgba(200,200,200,1)', 50 | backgroundColor: (ctx) => { 51 | if (ctx.type !== 'data') { 52 | return 'transparent'; 53 | } 54 | if (DISPLAY_MODE === 'containerBoxes') { 55 | return 'rgba(220,230,220,0.3)'; 56 | } 57 | return ctx.raw.l ? 'rgb(220,230,220)' : 'lightgray'; 58 | }, 59 | displayMode: DISPLAY_MODE, 60 | captions: { 61 | padding: 6, 62 | }, 63 | }] 64 | }, 65 | options: options 66 | }; 67 | 68 | // 69 | function toggle(chart, mode) { 70 | const dataset = {...config.data.datasets[0], displayMode: mode}; 71 | DISPLAY_MODE = mode; 72 | chart.data.datasets = [dataset]; 73 | chart.update(); 74 | } 75 | 76 | const actions = [ 77 | { 78 | name: 'Container Boxes', 79 | handler: (chart) => toggle(chart, 'containerBoxes') 80 | }, 81 | { 82 | name: 'Header Boxes', 83 | handler: (chart) => toggle(chart, 'headerBoxes') 84 | }, 85 | ]; 86 | 87 | module.exports = { 88 | actions, 89 | config, 90 | }; 91 | ``` 92 | -------------------------------------------------------------------------------- /samples/us-population.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | US population by state 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | 19 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /docs/.vuepress/config.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { DefaultThemeConfig, defineConfig, PluginTuple } from 'vuepress/config'; 3 | 4 | export default defineConfig({ 5 | title: 'chartjs-chart-treemap', 6 | description: 'Chart.js module for creating treemap charts', 7 | theme: 'chartjs', 8 | //base: '', 9 | dest: path.resolve(__dirname, '../../dist/docs'), 10 | head: [ 11 | ['link', {rel: 'icon', href: '/favicon.ico'}], 12 | ], 13 | plugins: [ 14 | ['flexsearch'], 15 | ['redirect', { 16 | redirectors: [ 17 | // Default sample page when accessing /samples. 18 | {base: '/samples', alternative: ['basic']}, 19 | ], 20 | }], 21 | ] as PluginTuple[], 22 | chainWebpack: (config) => { 23 | config.module 24 | .rule('chart.js') 25 | .include.add(path.resolve('node_modules/chart.js')).end() 26 | .use('babel-loader') 27 | .loader('babel-loader') 28 | .options({ 29 | presets: ['@babel/preset-env'] 30 | }) 31 | .end(); 32 | config.merge({ 33 | resolve: { 34 | alias: { 35 | // Hammerjs requires window, using ng-hammerjs instead 36 | 'hammerjs': 'ng-hammerjs', 37 | } 38 | } 39 | }); 40 | }, 41 | themeConfig: { 42 | repo: 'kurkle/chartjs-chart-treemap', 43 | logo: '/favicon.ico', 44 | lastUpdated: 'Last Updated', 45 | searchPlaceholder: 'Search...', 46 | editLinks: false, 47 | docsDir: 'docs', 48 | chart: { 49 | imports: [ 50 | ['scripts/register.js', 'Register'], 51 | ['scripts/data.js', 'Data'], 52 | ['scripts/utils.js', 'Utils'], 53 | ['scripts/helpers.js', 'helpers'], 54 | ] 55 | }, 56 | nav: [ 57 | {text: 'Home', link: '/'}, 58 | {text: 'Samples', link: `/samples/`}, 59 | { 60 | text: 'Ecosystem', 61 | ariaLabel: 'Community Menu', 62 | items: [ 63 | { text: 'Awesome', link: 'https://github.com/chartjs/awesome' }, 64 | ] 65 | } 66 | ], 67 | sidebar: { 68 | '/samples/': [ 69 | 'basic', 70 | 'labels', 71 | 'labelsFontsAndColors', 72 | 'groups', 73 | 'tree', 74 | 'captions', 75 | 'dividers', 76 | 'displayMode', 77 | 'rtl', 78 | 'datalabels', 79 | 'zoom' 80 | ], 81 | '/': [ 82 | '', 83 | 'integration', 84 | 'usage' 85 | ], 86 | } 87 | } as DefaultThemeConfig 88 | }); 89 | -------------------------------------------------------------------------------- /test/specs/controller.spec.js: -------------------------------------------------------------------------------- 1 | describe('auto', jasmine.fixtures('basic')); 2 | describe('auto', jasmine.fixtures('grouped')); 3 | describe('auto', jasmine.fixtures('headersbox')); 4 | describe('auto', jasmine.fixtures('events')); 5 | describe('auto', jasmine.fixtures('advanced')); 6 | describe('auto', jasmine.fixtures('issues')); 7 | 8 | describe('controller', function() { 9 | it('should be registered', function() { 10 | expect(Chart.controllers.treemap).toBeDefined(); 11 | }); 12 | 13 | it('should not rebuild data when nothing has changes', function() { 14 | const origData = [1, 2, 3]; 15 | const chart = acquireChart({ 16 | type: 'treemap', 17 | data: { 18 | datasets: [{ 19 | tree: origData 20 | }] 21 | } 22 | }); 23 | const buildData = chart.data.datasets[0].data; 24 | expect(buildData).not.toBe(origData); 25 | chart.update(); 26 | expect(buildData).toBe(chart.data.datasets[0].data); 27 | }); 28 | 29 | it('should group 3 levels of data', function() { 30 | const tree = [ 31 | {key: 10, a: 'a1', b: 'b1', c: 'c1'}, 32 | {key: 20, a: 'a1', b: 'b1', c: 'c1'}, 33 | {key: 40, a: 'a2', b: 'b1', c: 'c1'}, 34 | {key: 99, a: 'a2', b: 'b1', c: 'c1'}, 35 | {key: 10, a: 'a3', b: 'b1', c: 'c1'}, 36 | {key: 20, a: 'a3', b: 'b1', c: 'c2'}, 37 | {key: 40, a: 'a3', b: 'b2', c: 'c3'}, 38 | {key: 99, a: 'a3', b: 'b2', c: 'c4'}, 39 | {key: 50, a: 'a3', b: 'b3', c: 'c4'} 40 | ]; 41 | const chart = acquireChart({ 42 | type: 'treemap', 43 | data: { 44 | datasets: [{ 45 | key: 'key', 46 | groups: ['a', 'b', 'c'], 47 | tree: tree 48 | }] 49 | } 50 | }); 51 | const buildData = chart.data.datasets[0].data; 52 | 53 | const a1b1 = buildData.find((o) => o._data.path === 'a1.b1'); 54 | expect(a1b1.v).toBe(30); 55 | expect(a1b1._data.children.length).toBe(2); 56 | 57 | const a1b1c1 = buildData.find((o) => o._data.path === 'a1.b1.c1'); 58 | expect(a1b1c1.v).toBe(30); 59 | expect(a1b1c1._data.children.length).toBe(2); 60 | 61 | const a2b1c1 = buildData.find((o) => o._data.path === 'a2.b1.c1'); 62 | expect(a2b1c1.v).toBe(139); 63 | expect(a2b1c1._data.children.length).toBe(2); 64 | 65 | const a3 = buildData.find((o) => o._data.path === 'a3'); 66 | expect(a3.v).toBe(10 + 20 + 40 + 99 + 50); 67 | expect(a3._data.children.length).toBe(5); 68 | 69 | const a3b1c1 = buildData.find((o) => o._data.path === 'a3.b1.c1'); 70 | expect(a3b1c1.v).toBe(10); 71 | expect(a3b1c1._data.children.length).toBe(1); 72 | 73 | const a3b1 = buildData.find((o) => o._data.path === 'a3.b1'); 74 | expect(a3b1.v).toBe(10 + 20); 75 | expect(a3b1._data.children.length).toBe(2); 76 | 77 | const a3b2 = buildData.find((o) => o._data.path === 'a3.b2'); 78 | expect(a3b2.v).toBe(40 + 99); 79 | expect(a3b2._data.children.length).toBe(2); 80 | 81 | const a3b3 = buildData.find((o) => o._data.path === 'a3.b3'); 82 | expect(a3b3.v).toBe(50); 83 | expect(a3b3._data.children.length).toBe(1); 84 | 85 | const a3b3c4 = buildData.find((o) => o._data.path === 'a3.b3.c4'); 86 | expect(a3b3c4.v).toBe(50); 87 | expect(a3b3c4._data.children.length).toBe(1); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /samples/us-switchable.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | US population, area or population/area by state 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 |
30 | 31 |
32 | 33 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /karma.conf.cjs: -------------------------------------------------------------------------------- 1 | const istanbul = require('rollup-plugin-istanbul'); 2 | const resolve = require('@rollup/plugin-node-resolve').nodeResolve; 3 | const json = require('@rollup/plugin-json'); 4 | const env = process.env.NODE_ENV; 5 | 6 | module.exports = async function(karma) { 7 | const builds = (await import('./rollup.config.js')).default; 8 | const regex = karma.autoWatch ? /chartjs-chart-treemap\.cjs$/ : /chartjs-chart-treemap\.min\.js$/; 9 | const build = builds.filter(v => v.output.file && v.output.file.match(regex))[0]; 10 | 11 | if (env === 'test') { 12 | build.plugins = [ 13 | resolve(), 14 | json(), 15 | istanbul({exclude: ['node_modules/**/*.js', 'package.json']}) 16 | ]; 17 | } 18 | 19 | karma.set({ 20 | browsers: ['chrome', 'firefox'], 21 | frameworks: ['jasmine'], 22 | reporters: ['progress', 'summary', 'kjhtml'], 23 | logLevel: karma.autoWatch ? karma.LOG_INFO : karma.LOG_WARN, 24 | 25 | summaryReporter: { 26 | show: 'failed', 27 | specLength: 50, 28 | overviewColumn: false 29 | }, 30 | 31 | client: { 32 | clearContext: false, 33 | jasmine: { 34 | stopOnSpecFailure: false, 35 | timeoutInterval: 1000 36 | } 37 | }, 38 | 39 | // Explicitly disable hardware acceleration to make image 40 | // diff more stable when ran on Travis and dev machine. 41 | // https://github.com/chartjs/Chart.js/pull/5629 42 | // Since FF 110 https://github.com/chartjs/Chart.js/issues/11164 43 | customLaunchers: { 44 | chrome: { 45 | base: 'Chrome', 46 | flags: [ 47 | '--disable-accelerated-2d-canvas', 48 | '--disable-background-timer-throttling', 49 | '--disable-backgrounding-occluded-windows', 50 | '--disable-renderer-backgrounding' 51 | ] 52 | }, 53 | firefox: { 54 | base: 'Firefox', 55 | prefs: { 56 | 'layers.acceleration.disabled': true, 57 | 'gfx.canvas.accelerated': false 58 | } 59 | } 60 | }, 61 | 62 | files: [ 63 | {pattern: './test/fixtures/**/*.js', included: false}, 64 | {pattern: './test/fixtures/**/*.png', included: false}, 65 | 'node_modules/chart.js/dist/chart.umd.js', 66 | {pattern: 'test/index.js', watched: false}, 67 | {pattern: 'src/index.js', watched: false}, 68 | {pattern: 'test/specs/**/*.js', watched: false} 69 | ], 70 | 71 | preprocessors: { 72 | 'test/fixtures/**/*.js': ['fixtures'], 73 | 'test/specs/**/*.js': ['rollup'], 74 | 'test/index.js': ['rollup'], 75 | 'src/index.js': ['sources'] 76 | }, 77 | 78 | rollupPreprocessor: { 79 | plugins: [ 80 | resolve(), 81 | json(), 82 | ], 83 | output: { 84 | name: 'test', 85 | format: 'umd', 86 | sourcemap: karma.autoWatch ? 'inline' : false 87 | }, 88 | }, 89 | 90 | customPreprocessors: { 91 | fixtures: { 92 | base: 'rollup', 93 | options: { 94 | output: { 95 | format: 'iife', 96 | name: 'fixture', 97 | globals: { 98 | 'chart.js': 'Chart', 99 | 'chart.js/helpers': 'Chart.helpers' 100 | }, 101 | } 102 | } 103 | }, 104 | sources: { 105 | base: 'rollup', 106 | options: build 107 | } 108 | }, 109 | }); 110 | 111 | if (env === 'test') { 112 | karma.reporters.push('coverage'); 113 | karma.coverageReporter = { 114 | dir: 'coverage/', 115 | reporters: [ 116 | {type: 'html', subdir: 'html'}, 117 | {type: 'lcovonly', subdir: (browser) => browser.toLowerCase().split(/[ /-]/)[0]} 118 | ] 119 | }; 120 | } 121 | }; 122 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chartjs-chart-treemap", 3 | "homepage": "https://chartjs-chart-treemap.pages.dev/", 4 | "version": "3.1.0", 5 | "description": "Chart.js module for creating treemap charts", 6 | "type": "module", 7 | "main": "dist/chartjs-chart-treemap.cjs", 8 | "module": "dist/chartjs-chart-treemap.esm.js", 9 | "types": "types/index.esm.d.ts", 10 | "jsdelivr": "dist/chartjs-chart-treemap.min.js", 11 | "unpkg": "dist/chartjs-chart-treemap.min.js", 12 | "exports": { 13 | "types": "./types/index.esm.d.ts", 14 | "import": "./dist/chartjs-chart-treemap.esm.js", 15 | "require": "./dist/chartjs-chart-treemap.cjs", 16 | "script": "./dist/chartjs-chart-treemap.min.js" 17 | }, 18 | "sideEffects": [ 19 | "dist/chartjs-chart-treemap.cjs", 20 | "dist/chartjs-chart-treemap.min.js" 21 | ], 22 | "scripts": { 23 | "autobuild": "rollup -c -w", 24 | "build": "rollup -c", 25 | "dev": "karma start ./karma.conf.cjs --no-single-run --auto-watch --browsers chrome", 26 | "dev:ff": "karma start ./karma.conf.cjs --no-single-run --auto-watch --browsers firefox", 27 | "docs": "npm run build && vuepress build docs --no-cache", 28 | "docs:dev": "concurrently \"npm:autobuild\" \"vuepress dev docs --no-cache\"", 29 | "lint": "concurrently -r \"npm:lint-*\"", 30 | "lint-js": "eslint \"src/**/*.js\" \"test/**/*.js\" \"docs/**/*.js\"", 31 | "lint-md": "eslint \"**/*.md\"", 32 | "lint-types": "eslint \"types/**/*.ts\" && tsc -p types/tests/", 33 | "test": "cross-env NODE_ENV=test concurrently \"npm:test-*\"", 34 | "test-lint": "npm run lint", 35 | "test-types": "tsc -p types/tests/", 36 | "test-karma": "karma start ./karma.conf.cjs --no-auto-watch --single-run" 37 | }, 38 | "repository": { 39 | "type": "git", 40 | "url": "git+https://github.com/kurkle/chartjs-chart-treemap.git" 41 | }, 42 | "keywords": [ 43 | "chart.js", 44 | "chart", 45 | "treemap" 46 | ], 47 | "files": [ 48 | "dist/*", 49 | "!dist/docs/**", 50 | "types/index.esm.d.ts" 51 | ], 52 | "author": "Jukka Kurkela", 53 | "license": "MIT", 54 | "bugs": { 55 | "url": "https://github.com/kurkle/chartjs-chart-treemap/issues" 56 | }, 57 | "devDependencies": { 58 | "@rollup/plugin-commonjs": "^28.0.0", 59 | "@rollup/plugin-json": "^6.1.0", 60 | "@rollup/plugin-node-resolve": "^15.0.1", 61 | "@rollup/plugin-terser": "^0.4.4", 62 | "@typescript-eslint/eslint-plugin": "^5.4.0", 63 | "@typescript-eslint/parser": "^5.4.0", 64 | "chart.js": "^4.0.1", 65 | "chartjs-plugin-datalabels": "^2.2.0", 66 | "chartjs-plugin-zoom": "^2.0.0", 67 | "chartjs-test-utils": "^0.5.0", 68 | "concurrently": "^9.0.0", 69 | "cross-env": "^7.0.3", 70 | "eslint": "^8.3.0", 71 | "eslint-config-chartjs": "^0.3.0", 72 | "eslint-plugin-es": "^4.1.0", 73 | "eslint-plugin-html": "^8.1.2", 74 | "eslint-plugin-markdown": "^3.0.0", 75 | "jasmine-core": "^5.3.0", 76 | "karma": "^6.3.2", 77 | "karma-chrome-launcher": "^3.1.0", 78 | "karma-coverage": "^2.0.3", 79 | "karma-firefox-launcher": "^2.1.0", 80 | "karma-jasmine": "^5.1.0", 81 | "karma-jasmine-html-reporter": "^2.0.0", 82 | "karma-rollup-preprocessor": "7.0.7", 83 | "karma-spec-reporter": "^0.0.36", 84 | "karma-summary-reporter": "^4.0.1", 85 | "ng-hammerjs": "^2.0.8", 86 | "pixelmatch": "^7.1.0", 87 | "rollup": "^4.21.2", 88 | "rollup-plugin-analyzer": "^4.0.0", 89 | "rollup-plugin-istanbul": "^5.0.0", 90 | "typescript": "^5.6.2", 91 | "vuepress": "^1.9.7", 92 | "vuepress-plugin-flexsearch": "^0.3.0", 93 | "vuepress-plugin-redirect": "^1.2.5", 94 | "vuepress-theme-chartjs": "^0.2.0" 95 | }, 96 | "peerDependencies": { 97 | "chart.js": ">=3.0.0" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /types/index.esm.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Chart, 3 | ChartComponent, 4 | CoreChartOptions, 5 | DatasetController, 6 | Element, VisualElement, 7 | ScriptableContext, Color, Scriptable, FontSpec 8 | } from 'chart.js'; 9 | 10 | type AnyObject = Record; 11 | 12 | type TreemapScriptableContext = ScriptableContext<'treemap'> & { 13 | raw: TreemapDataPoint 14 | } 15 | 16 | type TreemapControllerDatasetCaptionsOptions = { 17 | align?: Scriptable, 18 | color?: Scriptable, 19 | display?: boolean; 20 | formatter?: Scriptable, 21 | font?: FontSpec, 22 | hoverColor?: Scriptable, 23 | hoverFont?: FontSpec, 24 | padding?: number, 25 | } 26 | 27 | type TreemapControllerDatasetLabelsOptions = { 28 | align?: Scriptable, 29 | color?: Scriptable, 30 | display?: boolean; 31 | formatter?: Scriptable, TreemapScriptableContext>, 32 | font?: Scriptable, 33 | hoverColor?: Scriptable, 34 | hoverFont?: Scriptable, 35 | overflow?: Scriptable 36 | padding?: number, 37 | position?: Scriptable 38 | } 39 | 40 | export type LabelPosition = 'top' | 'middle' | 'bottom'; 41 | 42 | export type LabelAlign = 'left' | 'center' | 'right'; 43 | 44 | export type LabelOverflow = 'cut' | 'hidden' | 'fit'; 45 | 46 | type TreemapControllerDatasetDividersOptions = { 47 | display?: boolean, 48 | lineCapStyle?: string, 49 | lineColor?: string, 50 | lineDash?: number[], 51 | lineDashOffset?: number, 52 | lineWidth?: number, 53 | } 54 | 55 | export interface TreemapControllerDatasetOptions { 56 | spacing?: number, 57 | rtl?: boolean, 58 | displayType?: 'containerBoxes' | 'headerBoxes'; 59 | 60 | backgroundColor?: Scriptable; 61 | borderColor?: Scriptable; 62 | borderWidth?: number; 63 | 64 | hoverBackgroundColor?: Scriptable; 65 | hoverBorderColor?: Scriptable; 66 | hoverBorderWidth?: number; 67 | 68 | captions?: TreemapControllerDatasetCaptionsOptions; 69 | dividers?: TreemapControllerDatasetDividersOptions; 70 | labels?: TreemapControllerDatasetLabelsOptions; 71 | label?: string; 72 | 73 | data: TreemapDataPoint[]; // This will be auto-generated from `tree` 74 | groups?: Array; 75 | sumKeys?: Array; 76 | tree: number[] | DType[] | AnyObject; 77 | treeLeafKey?: keyof DType; 78 | key?: keyof DType; 79 | } 80 | 81 | export interface TreemapDataPoint { 82 | x: number, 83 | y: number, 84 | w: number, 85 | h: number, 86 | /** 87 | * Value 88 | */ 89 | v: number, 90 | /** 91 | * Sum 92 | */ 93 | s: number, 94 | /** 95 | * Depth, only available if grouping 96 | */ 97 | l?: number, 98 | /** 99 | * Group name, only available if grouping 100 | */ 101 | g?: string, 102 | /** 103 | * Group Sum, only available if grouping 104 | */ 105 | gs?: number, 106 | /** 107 | * additonal keys sums, only available if grouping 108 | */ 109 | vs?: AnyObject 110 | } 111 | 112 | /* 113 | export interface TreemapInteractionOptions { 114 | position: Scriptable<"treemap", ScriptableTooltipContext<"treemap">> 115 | }*/ 116 | 117 | declare module 'chart.js' { 118 | export interface ChartTypeRegistry { 119 | treemap: { 120 | chartOptions: CoreChartOptions<'treemap'>; 121 | datasetOptions: TreemapControllerDatasetOptions>; 122 | defaultDataPoint: TreemapDataPoint; 123 | metaExtensions: AnyObject; 124 | parsedDataType: unknown, 125 | scales: never; 126 | } 127 | } 128 | 129 | // interface TooltipOptions extends CoreInteractionOptions, TreemapInteractionOptions { 130 | // } 131 | } 132 | 133 | export interface TreemapOptions { 134 | backgroundColor: Color; 135 | borderColor: Color; 136 | borderWidth: number | { top?: number, right?: number, bottom?: number, left?: number } 137 | } 138 | 139 | export interface TreemapConfig { 140 | x: number; 141 | y: number; 142 | width: number; 143 | height: number; 144 | } 145 | 146 | export type TreemapController = DatasetController; 147 | export const TreemapController: ChartComponent & { 148 | prototype: TreemapController; 149 | new(chart: Chart, datasetIndex: number): TreemapController 150 | }; 151 | 152 | export interface TreemapElement< 153 | T extends TreemapConfig = TreemapConfig, 154 | O extends TreemapOptions = TreemapOptions 155 | > extends Element, VisualElement {} 156 | 157 | export const TreemapElement: ChartComponent & { 158 | prototype: TreemapElement; 159 | new(cfg: TreemapConfig): TreemapElement 160 | }; 161 | -------------------------------------------------------------------------------- /test/specs/utils.spec.js: -------------------------------------------------------------------------------- 1 | import {flatten, group, sort, sum, normalizeTreeToArray, requireVersion} from '../../src/utils'; 2 | 3 | describe('utils', function() { 4 | 5 | describe('flatten', function() { 6 | it('should flatten array', function() { 7 | const a = [1, [2, 3, [4, 5, 6]], 7, [8, 9]]; 8 | expect(flatten(a)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]); 9 | }); 10 | }); 11 | 12 | describe('group', function() { 13 | it('should group 1 level of data', function() { 14 | const a = [{k: 'a', v: 1}, {k: 'b', v: 2}, {k: 'a', v: 3}]; 15 | const g1 = group(a, 'k', ['v'], 'leaf'); 16 | expect(g1).toEqual([ 17 | jasmine.objectContaining({k: 'a', v: 4}), 18 | jasmine.objectContaining({k: 'b', v: 2}) 19 | ]); 20 | }); 21 | it('should group 2 levels of data', function() { 22 | const a = [{k: 'a', k2: 'z', v: 1}, {k: 'b', k2: 'z', v: 2}, {k: 'a', k2: 'x', v: 3}]; 23 | const g1 = group(a, 'k2', ['v'], 'leaf', 'k', 'a'); 24 | expect(g1).toEqual([ 25 | jasmine.objectContaining({k2: 'z', v: 1}), 26 | jasmine.objectContaining({k2: 'x', v: 3}) 27 | ]); 28 | }); 29 | it('should group 2 levels of data with additionl keys', function() { 30 | const a = [{k: 'a', k2: 'z', v: 1, v1: 2}, {k: 'b', k2: 'z', v: 2, v1: 1}, {k: 'a', k2: 'x', v: 3, v1: 10}]; 31 | const g1 = group(a, 'k2', ['v', 'v1'], 'leaf', 'k', 'a'); 32 | expect(g1).toEqual([ 33 | jasmine.objectContaining({k2: 'z', v: 1, v1: 2}), 34 | jasmine.objectContaining({k2: 'x', v: 3, v1: 10}) 35 | ]); 36 | }); 37 | }); 38 | 39 | describe('normalize tree object to array', function() { 40 | it('should have 2 elements of data', function() { 41 | const a = {A: {C: {value: 0}}, B: {D: {value: 0}}}; 42 | const g1 = normalizeTreeToArray(['value'], 'leaf', a); 43 | expect(g1).toEqual([ 44 | jasmine.objectContaining({0: 'A', leaf: 'C', value: 0}), 45 | jasmine.objectContaining({0: 'B', leaf: 'D', value: 0}) 46 | ]); 47 | }); 48 | it('should have 1 element of data', function() { 49 | const a = {A: {C: {value: 0}}, B: {D: {none: 0}}}; 50 | const g1 = normalizeTreeToArray(['value'], 'leaf', a); 51 | expect(g1).toEqual([ 52 | jasmine.objectContaining({0: 'A', leaf: 'C', value: 0}) 53 | ]); 54 | }); 55 | it('should not have any elements of data', function() { 56 | const a = {A: {C: {value: 0}}, B: {D: {value: 0}}}; 57 | const g1 = normalizeTreeToArray(['none'], 'leaf', a); 58 | expect(g1).toEqual([]); 59 | }); 60 | it('should have 2 elements of data with sum keys', function() { 61 | const a = {A: {C: {value: 0, another: 3}}, B: {D: {value: 0, another: 2}}}; 62 | const g1 = normalizeTreeToArray(['value', 'another'], 'leaf', a); 63 | expect(g1).toEqual([ 64 | jasmine.objectContaining({0: 'A', leaf: 'C', value: 0, another: 3}), 65 | jasmine.objectContaining({0: 'B', leaf: 'D', value: 0, another: 2}) 66 | ]); 67 | }); 68 | }); 69 | 70 | describe('sort', function() { 71 | it('should reverse sort array', function() { 72 | const a = [8, 3, 5, 4, 1, 3, 6, 2, 7]; 73 | sort(a); 74 | expect(a).toEqual([8, 7, 6, 5, 4, 3, 3, 2, 1]); 75 | }); 76 | 77 | it('should reverse sort array by key', function() { 78 | const a = [{x: 8, y: 1}, {x: 3, y: 2}, {x: 5, y: 3}]; 79 | sort(a, 'x'); 80 | expect(a).toEqual([{x: 8, y: 1}, {x: 5, y: 3}, {x: 3, y: 2}]); 81 | sort(a, 'y'); 82 | expect(a).toEqual([{x: 5, y: 3}, {x: 3, y: 2}, {x: 8, y: 1}]); 83 | }); 84 | }); 85 | 86 | describe('sum', function() { 87 | it('should compute sum of array', function() { 88 | const a = [8, 3, 5, 4, 1, 3, 6, 2, 7]; 89 | expect(sum(a)).toEqual(39); 90 | }); 91 | 92 | it('should compute sum of numeric string array', function() { 93 | const a = ['8', '3', '5', '4', '1', '3', '6', '2', '7']; 94 | expect(sum(a)).toEqual(39); 95 | }); 96 | 97 | it('should compute sum of array by given key', function() { 98 | const a = [{x: 8, y: 1}, {x: 3, y: 2}, {x: 5, y: 3}]; 99 | expect(sum(a, 'x')).toEqual(16); 100 | expect(sum(a, 'y')).toEqual(6); 101 | }); 102 | }); 103 | 104 | describe('requireVersion', function() { 105 | it('should throw error for too old version', function() { 106 | expect(() => requireVersion('test', '3.7', '2.9.3')).toThrowError(); 107 | expect(() => requireVersion('test', '3.7', '3.6.99-alpha3')).toThrowError(); 108 | expect(() => requireVersion('test', '16.13.2.8', '16.13.2.8-beta')).toThrowError(); 109 | }); 110 | 111 | it('should not throw error for new enough version', function() { 112 | expect(() => requireVersion('test', '3.7', '3.7.0-beta.1')).not.toThrowError(); 113 | expect(() => requireVersion('test', '3.7.1', '3.7.19')).not.toThrowError(); 114 | expect(() => requireVersion('test', '3.7', '4.0.0')).not.toThrowError(); 115 | expect(() => requireVersion('test', '16.13.2', '16.13.3-rc')).not.toThrowError(); 116 | }); 117 | 118 | it('should return boolean when `strict` parameter is false', function() { 119 | expect(requireVersion('test', '3.7', '2.9.3', false)).toBeFalse(); 120 | expect(requireVersion('test', '3.7', '3.8', false)).toBeTrue(); 121 | }); 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /types/tests/options.ts: -------------------------------------------------------------------------------- 1 | import '../index.esm'; 2 | import { Chart } from 'chart.js'; 3 | import { color as colorLib } from 'chart.js/helpers'; 4 | 5 | function colorFromValue(value: number, border?: boolean) { 6 | let alpha = (1 + Math.log(value)) / 5; 7 | const color = 'purple'; 8 | if (border) { 9 | alpha += 0.01; 10 | } 11 | return colorLib(color) 12 | .alpha(alpha) 13 | .rgbString(); 14 | } 15 | 16 | const chart = new Chart('test', { 17 | type: 'treemap', 18 | data: { 19 | datasets: [{ 20 | label: 'Basic treemap', 21 | data: undefined, 22 | tree: [15, 6, 6, 5, 4, 3, 2, 2], 23 | backgroundColor(ctx) { 24 | const item = ctx.dataset.data[ctx.dataIndex]; 25 | if (!item) { 26 | return 'transparent'; 27 | } 28 | return colorFromValue(item.v); 29 | }, 30 | labels: { 31 | display: true, 32 | formatter: (ctx) => ctx.raw.g ? [ctx.raw.g, ctx.raw.v.toFixed(1)] : ctx.raw.v.toFixed(1), 33 | }, 34 | spacing: 0.1, 35 | borderWidth: 2, 36 | borderColor: 'rgba(180,180,180, 0.15)' 37 | }] 38 | }, 39 | }); 40 | 41 | const chart1 = new Chart('test', { 42 | type: 'treemap', 43 | data: { 44 | datasets: [{ 45 | label: 'Basic treemap', 46 | data: undefined, 47 | tree: [15, 6, 6, 5, 4, 3, 2, 2], 48 | backgroundColor(ctx) { 49 | return 'transparent'; 50 | }, 51 | labels: { 52 | display: true, 53 | padding: 25, 54 | position: 'bottom', 55 | align: 'right' 56 | }, 57 | spacing: 1, 58 | borderWidth: 2, 59 | borderColor: 'black' 60 | }] 61 | }, 62 | }); 63 | 64 | const statsByState = [ 65 | { 66 | state: 'Alabama', 67 | code: 'AL', 68 | region: 'South', 69 | division: 'East South Central', 70 | income: 48123, 71 | population: 4887871, 72 | area: 135767 73 | }, 74 | { 75 | state: 'Alaska', 76 | code: 'AK', 77 | region: 'West', 78 | division: 'Pacific', 79 | income: 73181, 80 | population: 737438, 81 | area: 1723337 82 | }, 83 | ]; 84 | 85 | const chart2 = new Chart('test', { 86 | type: 'treemap', 87 | data: { 88 | datasets: [{ 89 | data: [], 90 | tree: statsByState, 91 | key: 'population', 92 | groups: ['region', 'division', 'code'], 93 | backgroundColor(ctx) { 94 | const item = ctx.dataset.data[ctx.dataIndex]; 95 | if (!item) { 96 | return 'black'; 97 | } 98 | const a = item.v / (item.gs || item.s) / 2 + 0.5; 99 | switch (item.l) { 100 | case 0: 101 | switch (item.g) { 102 | case 'Midwest': return '#4363d8'; 103 | case 'Northeast': return '#469990'; 104 | case 'South': return '#9A6324'; 105 | case 'West': return '#f58231'; 106 | default: return '#e6beff'; 107 | } 108 | case 1: 109 | return colorLib('white').alpha(0.3).rgbString(); 110 | default: 111 | return colorLib('green').alpha(a).rgbString(); 112 | } 113 | }, 114 | spacing: 2, 115 | borderWidth: 0.5, 116 | borderColor: 'rgba(160,160,160,0.5)', 117 | captions: { 118 | color: '#FFF', 119 | hoverColor: '#F0B90B', 120 | font: { 121 | family: 'Tahoma', 122 | size: 8, 123 | weight: 'bold' 124 | }, 125 | hoverFont: { 126 | family: 'Tahoma', 127 | size: 8, 128 | weight: 'bold' 129 | } 130 | } 131 | }] 132 | }, 133 | }); 134 | 135 | const chart3 = new Chart('test', { 136 | type: 'treemap', 137 | data: { 138 | datasets: [{ 139 | data: [], 140 | tree: statsByState, 141 | key: 'population', 142 | groups: ['region', 'division', 'code'], 143 | backgroundColor(ctx) { 144 | const item = ctx.dataset.data[ctx.dataIndex]; 145 | if (!item) { 146 | return 'black'; 147 | } 148 | const a = item.v / (item.gs || item.s) / 2 + 0.5; 149 | switch (item.l) { 150 | case 0: 151 | return '#e6beff'; 152 | case 1: 153 | return colorLib('white').alpha(0.3).rgbString(); 154 | default: 155 | return colorLib('green').alpha(a).rgbString(); 156 | } 157 | }, 158 | borderWidth: 0.5, 159 | borderColor: 'rgba(255,255,255)', 160 | captions: { 161 | display: false, 162 | }, 163 | labels: { 164 | display: false, 165 | color: '#FFF', 166 | hoverColor: '#F0B90B', 167 | font: { 168 | family: 'Tahoma', 169 | size: 8, 170 | weight: 'bold' 171 | }, 172 | hoverFont: { 173 | family: 'Tahoma', 174 | size: 8, 175 | weight: 'bold' 176 | } 177 | } 178 | }] 179 | }, 180 | }); 181 | 182 | const chart4 = new Chart('test', { 183 | type: 'treemap', 184 | data: { 185 | datasets: [{ 186 | data: [], 187 | tree: statsByState, 188 | key: 'population', 189 | groups: ['region', 'division', 'code'], 190 | backgroundColor(ctx) { 191 | return '#e6beff'; 192 | }, 193 | dividers: { 194 | display: false, 195 | lineWidth: 12, 196 | lineDash: [1, 3] 197 | }, 198 | labels: { 199 | display: false, 200 | } 201 | }] 202 | }, 203 | }); 204 | 205 | const chart5 = new Chart('test', { 206 | type: 'treemap', 207 | data: { 208 | datasets: [{ 209 | data: [], 210 | tree: statsByState, 211 | key: 'population', 212 | groups: ['region', 'division', 'code'], 213 | backgroundColor(ctx) { 214 | return '#e6beff'; 215 | }, 216 | labels: { 217 | display: false, 218 | color: ['red', 'green'], 219 | font: [{ size: 24 }, { size: 12 }] 220 | } 221 | }] 222 | }, 223 | }); 224 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import {isObject} from 'chart.js/helpers'; 2 | 3 | const isOlderPart = (act, req) => req > act || (act.length > req.length && act.slice(0, req.length) === req); 4 | 5 | export const getGroupKey = (lvl) => '' + lvl; 6 | 7 | function scanTreeObject(keys, treeLeafKey, obj, tree = [], lvl = 0, result = []) { 8 | const objIndex = lvl - 1; 9 | if (keys[0] in obj && lvl > 0) { 10 | const record = tree.reduce(function(reduced, item, i) { 11 | if (i !== objIndex) { 12 | reduced[getGroupKey(i)] = item; 13 | } 14 | return reduced; 15 | }, {}); 16 | record[treeLeafKey] = tree[objIndex]; 17 | keys.forEach(function(k) { 18 | record[k] = obj[k]; 19 | }); 20 | result.push(record); 21 | } else { 22 | for (const childKey of Object.keys(obj)) { 23 | const child = obj[childKey]; 24 | if (isObject(child)) { 25 | tree.push(childKey); 26 | scanTreeObject(keys, treeLeafKey, child, tree, lvl + 1, result); 27 | } 28 | } 29 | } 30 | tree.splice(objIndex, 1); 31 | return result; 32 | } 33 | 34 | export function normalizeTreeToArray(keys, treeLeafKey, obj) { 35 | const data = scanTreeObject(keys, treeLeafKey, obj); 36 | if (!data.length) { 37 | return data; 38 | } 39 | const max = data.reduce(function(maxVal, element) { 40 | // minus 2 because _leaf and value properties are added 41 | // on top to groups ones 42 | const ikeys = Object.keys(element).length - 2; 43 | return maxVal > ikeys ? maxVal : ikeys; 44 | }); 45 | data.forEach(function(element) { 46 | for (let i = 0; i < max; i++) { 47 | const groupKey = getGroupKey(i); 48 | if (!element[groupKey]) { 49 | element[groupKey] = ''; 50 | } 51 | } 52 | }); 53 | return data; 54 | } 55 | 56 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flat 57 | export function flatten(input) { 58 | const stack = [...input]; 59 | const res = []; 60 | while (stack.length) { 61 | // pop value from stack 62 | const next = stack.pop(); 63 | if (Array.isArray(next)) { 64 | // push back array items, won't modify the original input 65 | stack.push(...next); 66 | } else { 67 | res.push(next); 68 | } 69 | } 70 | // reverse to restore input order 71 | return res.reverse(); 72 | } 73 | 74 | function getPath(groups, value, defaultValue) { 75 | if (!groups.length) { 76 | return; 77 | } 78 | const path = []; 79 | for (const grp of groups) { 80 | const item = value[grp]; 81 | if (item === '') { 82 | path.push(defaultValue); 83 | break; 84 | } 85 | path.push(item); 86 | } 87 | return path.length ? path.join('.') : defaultValue; 88 | } 89 | 90 | /** 91 | * @param {[]} values 92 | * @param {string} grp 93 | * @param {[string]} keys 94 | * @param {string} treeeLeafKey 95 | * @param {string} [mainGrp] 96 | * @param {*} [mainValue] 97 | * @param {[]} groups 98 | */ 99 | export function group(values, grp, keys, treeLeafKey, mainGrp, mainValue, groups = []) { 100 | const key = keys[0]; 101 | const addKeys = keys.slice(1); 102 | const tmp = Object.create(null); 103 | const data = Object.create(null); 104 | const ret = []; 105 | let g, i, n; 106 | for (i = 0, n = values.length; i < n; ++i) { 107 | const v = values[i]; 108 | if (mainGrp && v[mainGrp] !== mainValue) { 109 | continue; 110 | } 111 | g = v[grp] || v[treeLeafKey] || ''; 112 | if (!g) { 113 | return []; 114 | } 115 | if (!(g in tmp)) { 116 | const tmpRef = tmp[g] = {value: 0}; 117 | addKeys.forEach(function(k) { 118 | tmpRef[k] = 0; 119 | }); 120 | data[g] = []; 121 | } 122 | tmp[g].value += +v[key]; 123 | tmp[g].label = v[grp] || ''; 124 | const tmpRef = tmp[g]; 125 | addKeys.forEach(function(k) { 126 | tmpRef[k] += v[k]; 127 | }); 128 | tmp[g].path = getPath(groups, v, g); 129 | data[g].push(v); 130 | } 131 | 132 | Object.keys(tmp).forEach((k) => { 133 | const v = {children: data[k]}; 134 | v[key] = +tmp[k].value; 135 | addKeys.forEach(function(ak) { 136 | v[ak] = +tmp[k][ak]; 137 | }); 138 | v[grp] = tmp[k].label; 139 | v.label = k; 140 | v.path = tmp[k].path; 141 | 142 | if (mainGrp) { 143 | v[mainGrp] = mainValue; 144 | } 145 | ret.push(v); 146 | }); 147 | 148 | return ret; 149 | } 150 | 151 | export function index(values, key) { 152 | let n = values.length; 153 | let i; 154 | 155 | if (!n) { 156 | return key; 157 | } 158 | 159 | const obj = isObject(values[0]); 160 | key = obj ? key : 'v'; 161 | 162 | for (i = 0, n = values.length; i < n; ++i) { 163 | if (obj) { 164 | values[i]._idx = i; 165 | } else { 166 | values[i] = {v: values[i], _idx: i}; 167 | } 168 | } 169 | return key; 170 | } 171 | 172 | export function sort(values, key) { 173 | if (key) { 174 | values.sort((a, b) => +b[key] - +a[key]); 175 | } else { 176 | values.sort((a, b) => +b - +a); 177 | } 178 | } 179 | 180 | export function sum(values, key) { 181 | let s, i, n; 182 | 183 | for (s = 0, i = 0, n = values.length; i < n; ++i) { 184 | s += key ? +values[i][key] : +values[i]; 185 | } 186 | 187 | return s; 188 | } 189 | 190 | /** 191 | * @param {string} pkg 192 | * @param {string} min 193 | * @param {string} ver 194 | * @param {boolean} [strict=true] 195 | * @returns {boolean} 196 | */ 197 | export function requireVersion(pkg, min, ver, strict = true) { 198 | const parts = ver.split('.'); 199 | let i = 0; 200 | for (const req of min.split('.')) { 201 | const act = parts[i++]; 202 | if (parseInt(req, 10) < parseInt(act, 10)) { 203 | break; 204 | } 205 | if (isOlderPart(act, req)) { 206 | if (strict) { 207 | throw new Error(`${pkg} v${ver} is not supported. v${min} or newer is required.`); 208 | } else { 209 | return false; 210 | } 211 | } 212 | } 213 | return true; 214 | } 215 | -------------------------------------------------------------------------------- /test/specs/squarify.spec.js: -------------------------------------------------------------------------------- 1 | import squarify from '../../src/squarify'; 2 | const round4 = (v) => +(Math.round(+`${v}e+4`) + 'e-4') || 0; 3 | const roundsq4 = sq => ({...sq, x: round4(sq.x), y: round4(sq.y), w: round4(sq.w), h: round4(sq.h)}); 4 | 5 | describe('squarify', function() { 6 | 7 | it('should be a function', function() { 8 | expect(typeof squarify).toBe('function'); 9 | }); 10 | 11 | it('should squarify 4 equal areas equally 4x4', function() { 12 | let sq = squarify([4, 4, 4, 4], {x: 0, y: 0, w: 4, h: 4}); 13 | expect(sq).toEqual([ 14 | jasmine.objectContaining({x: 0, y: 0, w: 2, h: 2}), 15 | jasmine.objectContaining({x: 0, y: 2, w: 2, h: 2}), 16 | jasmine.objectContaining({x: 2, y: 0, w: 2, h: 2}), 17 | jasmine.objectContaining({x: 2, y: 2, w: 2, h: 2}) 18 | ]); 19 | }); 20 | 21 | it('should squarify 4 equal areas equally 6x6', function() { 22 | let sq = squarify([4, 4, 4, 4], {x: 0, y: 0, w: 6, h: 6}); 23 | expect(sq).toEqual([ 24 | jasmine.objectContaining({x: 0, y: 0, w: 3, h: 3}), 25 | jasmine.objectContaining({x: 0, y: 3, w: 3, h: 3}), 26 | jasmine.objectContaining({x: 3, y: 0, w: 3, h: 3}), 27 | jasmine.objectContaining({x: 3, y: 3, w: 3, h: 3}) 28 | ]); 29 | }); 30 | 31 | it('should squarify 4 equal areas equally 8x6', function() { 32 | let sq = squarify([4, 4, 4, 4], {x: 0, y: 0, w: 8, h: 6}); 33 | expect(sq).toEqual([ 34 | jasmine.objectContaining({x: 0, y: 0, w: 4, h: 3}), 35 | jasmine.objectContaining({x: 0, y: 3, w: 4, h: 3}), 36 | jasmine.objectContaining({x: 4, y: 0, w: 4, h: 3}), 37 | jasmine.objectContaining({x: 4, y: 3, w: 4, h: 3}) 38 | ]); 39 | }); 40 | 41 | it('should squarify 4 equal areas equally 6x8', function() { 42 | let sq = squarify([4, 4, 4, 4], {x: 0, y: 0, w: 6, h: 8}); 43 | expect(sq).toEqual([ 44 | jasmine.objectContaining({x: 0, y: 0, w: 3, h: 4}), 45 | jasmine.objectContaining({x: 3, y: 0, w: 3, h: 4}), 46 | jasmine.objectContaining({x: 0, y: 4, w: 3, h: 4}), 47 | jasmine.objectContaining({x: 3, y: 4, w: 3, h: 4}) 48 | ]); 49 | }); 50 | 51 | it('should squarify 4 equal areas equally 8x2', function() { 52 | let sq = squarify([4, 4, 4, 4], {x: 0, y: 0, w: 8, h: 2}); 53 | expect(sq).toEqual([ 54 | jasmine.objectContaining({x: 0, y: 0, w: 2, h: 2}), 55 | jasmine.objectContaining({x: 2, y: 0, w: 2, h: 2}), 56 | jasmine.objectContaining({x: 4, y: 0, w: 2, h: 2}), 57 | jasmine.objectContaining({x: 6, y: 0, w: 2, h: 2}) 58 | ]); 59 | }); 60 | 61 | it('should squarify 4 equal areas equally 1x8', function() { 62 | let sq = squarify([4, 4, 4, 4], {x: 0, y: 0, w: 1, h: 8}); 63 | expect(sq).toEqual([ 64 | jasmine.objectContaining({x: 0, y: 0, w: 1, h: 2}), 65 | jasmine.objectContaining({x: 0, y: 2, w: 1, h: 2}), 66 | jasmine.objectContaining({x: 0, y: 4, w: 1, h: 2}), 67 | jasmine.objectContaining({x: 0, y: 6, w: 1, h: 2}) 68 | ]); 69 | }); 70 | 71 | it('should squarify correctly', function() { 72 | let sq = squarify([6, 6, 4, 3, 2, 2, 1], {x: 0, y: 0, w: 6, h: 4}).map(roundsq4); 73 | expect(sq).toEqual([ 74 | jasmine.objectContaining({x: 0, y: 0, w: 3, h: 2}), 75 | jasmine.objectContaining({x: 0, y: 2, w: 3, h: 2}), 76 | jasmine.objectContaining({x: 3, y: 0, w: 1.7143, h: 2.3333}), 77 | jasmine.objectContaining({x: 4.7143, y: 0, w: 1.2857, h: 2.3333}), 78 | jasmine.objectContaining({x: 3, y: 2.3333, w: 1.2, h: 1.6667}), 79 | jasmine.objectContaining({x: 4.2, y: 2.3333, w: 1.2, h: 1.6667}), 80 | jasmine.objectContaining({x: 5.4, y: 2.3333, w: 0.6, h: 1.6667}) 81 | ]); 82 | }); 83 | 84 | it('should squarify unordered data correctly', function() { 85 | let sq = squarify([3, 2, 1, 6, 4, 6, 2], {x: 0, y: 0, w: 6, h: 4}).map(roundsq4); 86 | expect(sq).toEqual([ 87 | jasmine.objectContaining({x: 0, y: 0, w: 3, h: 2}), 88 | jasmine.objectContaining({x: 0, y: 2, w: 3, h: 2}), 89 | jasmine.objectContaining({x: 3, y: 0, w: 1.7143, h: 2.3333}), 90 | jasmine.objectContaining({x: 4.7143, y: 0, w: 1.2857, h: 2.3333}), 91 | jasmine.objectContaining({x: 3, y: 2.3333, w: 1.2, h: 1.6667}), 92 | jasmine.objectContaining({x: 4.2, y: 2.3333, w: 1.2, h: 1.6667}), 93 | jasmine.objectContaining({x: 5.4, y: 2.3333, w: 0.6, h: 1.6667}) 94 | ]); 95 | }); 96 | 97 | it('should squarify by given key', function() { 98 | let data = [{v: 4}, {v: 4}, {v: 4}, {v: 4}]; 99 | let rect = {x: 0, y: 0, w: 4, h: 4}; 100 | let sq = squarify(data, rect, 'v'); 101 | expect(sq).toEqual([ 102 | jasmine.objectContaining({x: 0, y: 0, w: 2, h: 2}), 103 | jasmine.objectContaining({x: 0, y: 2, w: 2, h: 2}), 104 | jasmine.objectContaining({x: 2, y: 0, w: 2, h: 2}), 105 | jasmine.objectContaining({x: 2, y: 2, w: 2, h: 2}) 106 | ]); 107 | }); 108 | 109 | it('should squarify by given group', function() { 110 | let data = [{g: 'a', v: 1}, {g: 'a', v: 2}, {g: 'b', v: 3}, {g: 'c', v: 4}]; 111 | let rect = {x: 0, y: 0, w: 4, h: 4}; 112 | let sq = squarify(data, rect, ['v'], 'g', 0, 0).map(roundsq4); 113 | expect(sq).toEqual([ 114 | jasmine.objectContaining({x: 0, y: 0, w: 2.8, h: 2.2857, a: 6.4, g: 'c', l: 0, gs: 0}), 115 | jasmine.objectContaining({x: 0, y: 2.2857, w: 2.8, h: 1.7143, a: 4.800000000000001, g: 'b', l: 0, gs: 0}), 116 | jasmine.objectContaining({x: 2.8, y: 0, w: 1.2, h: 2.6667, a: 3.2, v: 2, g: 'a', l: 0, gs: 0}), 117 | jasmine.objectContaining({x: 2.8, y: 2.6667, w: 1.2, h: 1.3333, a: 1.6, v: 1, g: 'a', l: 0, gs: 0}), 118 | ]); 119 | }); 120 | 121 | it('should not fail with empty array', function() { 122 | let sq = squarify([], {x: 0, y: 0, w: 10, h: 10}); 123 | expect(sq).toEqual([]); 124 | }); 125 | 126 | it('should not fail with undefined input', function() { 127 | let sq = squarify(undefined, {x: 0, y: 0, w: 10, h: 10}); 128 | expect(sq).toEqual([]); 129 | 130 | sq = squarify([]); 131 | expect(sq).toEqual([]); 132 | 133 | sq = squarify([1]); 134 | expect(sq).toEqual([jasmine.objectContaining({x: 0, y: 0, w: 1, h: 1, a: 1, v: 1, s: 1})]); 135 | 136 | sq = squarify(); 137 | expect(sq).toEqual([]); 138 | }); 139 | }); 140 | -------------------------------------------------------------------------------- /src/controller.js: -------------------------------------------------------------------------------- 1 | import {Chart, DatasetController, registry} from 'chart.js'; 2 | import {toFont, valueOrDefault, isObject, clipArea, unclipArea} from 'chart.js/helpers'; 3 | import {group, requireVersion, normalizeTreeToArray, getGroupKey} from './utils'; 4 | import {shouldDrawCaption, parseBorderWidth, getCaptionHeight} from './element'; 5 | import squarify from './squarify'; 6 | import {version} from '../package.json'; 7 | import {arrayNotEqual, rectNotEqual, scaleRect} from './helpers/index'; 8 | 9 | function buildData(tree, dataset, keys, mainRect) { 10 | const treeLeafKey = dataset.treeLeafKey || '_leaf'; 11 | if (isObject(tree)) { 12 | tree = normalizeTreeToArray(keys, treeLeafKey, tree); 13 | } 14 | const groups = dataset.groups || []; 15 | const glen = groups.length; 16 | const sp = dataset.displayMode === 'headerBoxes' ? 0 : valueOrDefault(dataset.spacing, 0); 17 | const captions = dataset.captions || {}; 18 | const font = toFont(captions.font); 19 | const padding = valueOrDefault(captions.padding, 3); 20 | 21 | function recur(treeElements, gidx, rect, parent, gs) { 22 | const g = getGroupKey(groups[gidx]); 23 | const pg = (gidx > 0) && getGroupKey(groups[gidx - 1]); 24 | const gdata = group(treeElements, g, keys, treeLeafKey, pg, parent, groups.filter((item, index) => index <= gidx)); 25 | const gsq = squarify(gdata, rect, keys, g, gidx, gs); 26 | const ret = gsq.slice(); 27 | if (gidx < glen - 1) { 28 | gsq.forEach((sq) => { 29 | const bw = dataset.displayMode === 'headerBoxes' 30 | ? {l: 0, r: 0, t: 0, b: 0} 31 | : parseBorderWidth(dataset.borderWidth, sq.w / 2, sq.h / 2); 32 | const subRect = { 33 | ...rect, 34 | x: sq.x + sp + bw.l, 35 | y: sq.y + sp + bw.t, 36 | w: sq.w - 2 * sp - bw.l - bw.r, 37 | h: sq.h - 2 * sp - bw.t - bw.b, 38 | }; 39 | if (shouldDrawCaption(dataset.displayMode, subRect, captions)) { 40 | const captionHeight = getCaptionHeight(dataset.displayMode, subRect, font, padding); 41 | subRect.y += captionHeight; 42 | subRect.h -= captionHeight; 43 | } 44 | const children = []; 45 | gdata.forEach((gEl) => { 46 | children.push(...recur(gEl.children, gidx + 1, subRect, sq.g, sq.s)); 47 | }); 48 | ret.push(...children); 49 | sq.isLeaf = !children.length; 50 | }); 51 | } else { 52 | gsq.forEach((sq) => { 53 | sq.isLeaf = true; 54 | }); 55 | } 56 | return ret; 57 | } 58 | 59 | const result = glen 60 | ? recur(tree, 0, mainRect) 61 | : squarify(tree, mainRect, keys); 62 | return result.map((d) => { 63 | if (dataset.displayMode !== 'headerBoxes' || d.isLeaf) { 64 | return d; 65 | } 66 | if (!shouldDrawCaption(dataset.displayMode, d, captions)) { 67 | return undefined; 68 | } 69 | const captionHeight = getCaptionHeight(dataset.displayMode, d, font, padding); 70 | return {...d, h: captionHeight}; 71 | }).filter((d) => d); 72 | 73 | } 74 | 75 | export default class TreemapController extends DatasetController { 76 | constructor(chart, datasetIndex) { 77 | super(chart, datasetIndex); 78 | 79 | this._groups = undefined; 80 | this._keys = undefined; 81 | this._rect = undefined; 82 | this._rectChanged = true; 83 | } 84 | 85 | initialize() { 86 | this.enableOptionSharing = true; 87 | super.initialize(); 88 | } 89 | 90 | getMinMax(scale) { 91 | return { 92 | min: 0, 93 | max: scale.axis === 'x' ? scale.right - scale.left : scale.bottom - scale.top 94 | }; 95 | } 96 | 97 | configure() { 98 | super.configure(); 99 | const {xScale, yScale} = this.getMeta(); 100 | if (!xScale || !yScale) { 101 | // configure is called once before `linkScales`, and at that call we don't have any scales linked yet 102 | return; 103 | } 104 | 105 | const w = xScale.right - xScale.left; 106 | const h = yScale.bottom - yScale.top; 107 | const rect = {x: 0, y: 0, w, h, rtl: !!this.options.rtl, unsorted: !!this.options.unsorted}; 108 | 109 | if (rectNotEqual(this._rect, rect)) { 110 | this._rect = rect; 111 | this._rectChanged = true; 112 | } 113 | 114 | if (this._rectChanged) { 115 | xScale.max = w; 116 | xScale.configure(); 117 | yScale.max = h; 118 | yScale.configure(); 119 | } 120 | } 121 | 122 | update(mode) { 123 | const dataset = this.getDataset(); 124 | const {data} = this.getMeta(); 125 | const groups = dataset.groups || []; 126 | const keys = [dataset.key || ''].concat(dataset.sumKeys || []); 127 | const tree = dataset.tree = dataset.tree || dataset.data || []; 128 | 129 | if (mode === 'reset') { 130 | // reset is called before 2nd configure and is only called if animations are enabled. So wen need an extra configure call here. 131 | this.configure(); 132 | } 133 | 134 | if (this._rectChanged || arrayNotEqual(this._keys, keys) || arrayNotEqual(this._groups, groups) || this._prevTree !== tree) { 135 | this._groups = groups.slice(); 136 | this._keys = keys.slice(); 137 | this._prevTree = tree; 138 | this._rectChanged = false; 139 | 140 | dataset.data = buildData(tree, dataset, this._keys, this._rect); 141 | // @ts-ignore using private stuff 142 | this._dataCheck(); 143 | // @ts-ignore using private stuff 144 | this._resyncElements(); 145 | } 146 | 147 | this.updateElements(data, 0, data.length, mode); 148 | } 149 | 150 | updateElements(rects, start, count, mode) { 151 | const reset = mode === 'reset'; 152 | const dataset = this.getDataset(); 153 | const firstOpts = this._rect.options = this.resolveDataElementOptions(start, mode); 154 | const sharedOptions = this.getSharedOptions(firstOpts); 155 | const includeOptions = this.includeOptions(mode, sharedOptions); 156 | const {xScale, yScale} = this.getMeta(this.index); 157 | 158 | for (let i = start; i < start + count; i++) { 159 | const options = sharedOptions || this.resolveDataElementOptions(i, mode); 160 | const properties = scaleRect(dataset.data[i], xScale, yScale, options.spacing); 161 | if (reset) { 162 | properties.width = 0; 163 | properties.height = 0; 164 | } 165 | 166 | if (includeOptions) { 167 | properties.options = options; 168 | } 169 | this.updateElement(rects[i], i, properties, mode); 170 | } 171 | 172 | this.updateSharedOptions(sharedOptions, mode, firstOpts); 173 | } 174 | 175 | draw() { 176 | const {ctx, chartArea} = this.chart; 177 | const metadata = this.getMeta().data || []; 178 | const dataset = this.getDataset(); 179 | const data = dataset.data; 180 | 181 | clipArea(ctx, chartArea); 182 | for (let i = 0, ilen = metadata.length; i < ilen; ++i) { 183 | const rect = metadata[i]; 184 | if (!rect.hidden) { 185 | rect.draw(ctx, data[i]); 186 | } 187 | } 188 | unclipArea(ctx); 189 | } 190 | } 191 | 192 | TreemapController.id = 'treemap'; 193 | 194 | TreemapController.version = version; 195 | 196 | TreemapController.defaults = { 197 | dataElementType: 'treemap', 198 | 199 | animations: { 200 | numbers: { 201 | type: 'number', 202 | properties: ['x', 'y', 'width', 'height'] 203 | }, 204 | }, 205 | 206 | }; 207 | 208 | TreemapController.descriptors = { 209 | _scriptable: true, 210 | _indexable: false 211 | }; 212 | 213 | TreemapController.overrides = { 214 | interaction: { 215 | mode: 'point', 216 | includeInvisible: true, 217 | intersect: true 218 | }, 219 | 220 | hover: {}, 221 | 222 | plugins: { 223 | tooltip: { 224 | position: 'treemap', 225 | intersect: true, 226 | callbacks: { 227 | title(items) { 228 | if (items.length) { 229 | const item = items[0]; 230 | return item.dataset.key || ''; 231 | } 232 | return ''; 233 | }, 234 | label(item) { 235 | const dataset = item.dataset; 236 | const dataItem = dataset.data[item.dataIndex]; 237 | const label = dataItem.g || dataItem._data.label || dataset.label; 238 | return (label ? label + ': ' : '') + dataItem.v; 239 | } 240 | } 241 | }, 242 | }, 243 | scales: { 244 | x: { 245 | type: 'linear', 246 | alignToPixels: true, 247 | bounds: 'data', 248 | display: false 249 | }, 250 | y: { 251 | type: 'linear', 252 | alignToPixels: true, 253 | bounds: 'data', 254 | display: false, 255 | reverse: true 256 | } 257 | }, 258 | }; 259 | 260 | TreemapController.beforeRegister = function() { 261 | requireVersion('chart.js', '3.8', Chart.version); 262 | }; 263 | 264 | TreemapController.afterRegister = function() { 265 | const tooltipPlugin = registry.plugins.get('tooltip'); 266 | if (tooltipPlugin) { 267 | tooltipPlugin.positioners.treemap = function(active) { 268 | if (!active.length) { 269 | return false; 270 | } 271 | 272 | const item = active[active.length - 1]; 273 | const el = item.element; 274 | 275 | return el.tooltipPosition(); 276 | }; 277 | } else { 278 | console.warn('Unable to register the treemap positioner because tooltip plugin is not registered'); 279 | } 280 | }; 281 | 282 | TreemapController.afterUnregister = function() { 283 | const tooltipPlugin = registry.plugins.get('tooltip'); 284 | if (tooltipPlugin) { 285 | delete tooltipPlugin.positioners.treemap; 286 | } 287 | }; 288 | -------------------------------------------------------------------------------- /samples/us_stats_by_state.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | const statsByState = [ 4 | { 5 | state: 'Alabama', 6 | code: 'AL', 7 | region: 'South', 8 | division: 'East South Central', 9 | income: 48123, 10 | population: 4887871, 11 | area: 135767 12 | }, 13 | { 14 | state: 'Alaska', 15 | code: 'AK', 16 | region: 'West', 17 | division: 'Pacific', 18 | income: 73181, 19 | population: 737438, 20 | area: 1723337 21 | }, 22 | { 23 | state: 'Arizona', 24 | code: 'AZ', 25 | region: 'West', 26 | division: 'Mountain', 27 | income: 56581, 28 | population: 7171646, 29 | area: 295234 30 | }, 31 | { 32 | state: 'Arkansas', 33 | code: 'AR', 34 | region: 'South', 35 | division: 'West South Central', 36 | income: 45869, 37 | population: 3013825, 38 | area: 137732 39 | }, 40 | { 41 | state: 'California', 42 | code: 'CA', 43 | region: 'West', 44 | division: 'Pacific', 45 | income: 71805, 46 | population: 39557045, 47 | area: 423972 48 | }, 49 | { 50 | state: 'Colorado', 51 | code: 'CO', 52 | region: 'West', 53 | division: 'Mountain', 54 | income: 69117, 55 | population: 5695564, 56 | area: 269601 57 | }, 58 | { 59 | state: 'Connecticut', 60 | code: 'CT', 61 | region: 'Northeast', 62 | division: 'New England', 63 | income: 74168, 64 | population: 3572665, 65 | area: 14357 66 | }, 67 | { 68 | state: 'Delaware', 69 | code: 'DE', 70 | region: 'South', 71 | division: 'South Atlantic', 72 | income: 62852, 73 | population: 967171, 74 | area: 6446 75 | }, 76 | { 77 | state: 'District of Columbia', 78 | code: 'DC', 79 | region: 'South', 80 | division: 'South Atlantic', 81 | income: 82372, 82 | population: 702455, 83 | area: 177 84 | }, 85 | { 86 | state: 'Florida', 87 | code: 'FL', 88 | region: 'South', 89 | division: 'South Atlantic', 90 | income: 52594, 91 | population: 21299325, 92 | area: 170312 93 | }, 94 | { 95 | state: 'Georgia', 96 | code: 'GA', 97 | region: 'South', 98 | division: 'South Atlantic', 99 | income: 56183, 100 | population: 10519475, 101 | area: 153910 102 | }, 103 | { 104 | state: 'Hawaii', 105 | code: 'HI', 106 | region: 'West', 107 | division: 'Pacific', 108 | income: 77765, 109 | population: 1420491, 110 | area: 28313 111 | }, 112 | { 113 | state: 'Idaho', 114 | code: 'ID', 115 | region: 'West', 116 | division: 'Mountain', 117 | income: 52225, 118 | population: 1754208, 119 | area: 216443 120 | }, 121 | { 122 | state: 'Illinois', 123 | code: 'IL', 124 | region: 'Midwest', 125 | division: 'East North Central', 126 | income: 62992, 127 | population: 12741080, 128 | area: 149995 129 | }, 130 | { 131 | state: 'Indiana', 132 | code: 'IN', 133 | region: 'Midwest', 134 | division: 'East North Central', 135 | income: 54181, 136 | population: 6691878, 137 | area: 94326 138 | }, 139 | { 140 | state: 'Iowa', 141 | code: 'IA', 142 | region: 'Midwest', 143 | division: 'West North Central', 144 | income: 5857, 145 | population: 3156145, 146 | area: 145746 147 | }, 148 | { 149 | state: 'Kansas', 150 | code: 'KS', 151 | region: 'Midwest', 152 | division: 'West North Central', 153 | income: 56422, 154 | population: 2911505, 155 | area: 213100 156 | }, 157 | { 158 | state: 'Kentucky', 159 | code: 'KY', 160 | region: 'South', 161 | division: 'East South Central', 162 | income: 45215, 163 | population: 4468402, 164 | area: 104656 165 | }, 166 | { 167 | state: 'Louisiana', 168 | code: 'LA', 169 | region: 'South', 170 | division: 'West South Central', 171 | income: 46145, 172 | population: 4659978, 173 | area: 135659 174 | }, 175 | { 176 | state: 'Maine', 177 | code: 'ME', 178 | region: 'Northeast', 179 | division: 'New England', 180 | income: 55277, 181 | population: 1338404, 182 | area: 91633 183 | }, 184 | { 185 | state: 'Maryland', 186 | code: 'MD', 187 | region: 'South', 188 | division: 'South Atlantic', 189 | income: 80776, 190 | population: 6042718, 191 | area: 32131 192 | }, 193 | { 194 | state: 'Massachusetts', 195 | code: 'MA', 196 | region: 'Northeast', 197 | division: 'New England', 198 | income: 77385, 199 | population: 6902149, 200 | area: 27336 201 | }, 202 | { 203 | state: 'Michigan', 204 | code: 'MI', 205 | region: 'Midwest', 206 | division: 'East North Central', 207 | income: 54909, 208 | population: 9995915, 209 | area: 250487 210 | }, 211 | { 212 | state: 'Minnesota', 213 | code: 'MN', 214 | region: 'Midwest', 215 | division: 'West North Central', 216 | income: 68388, 217 | population: 5611179, 218 | area: 225163 219 | }, 220 | { 221 | state: 'Mississippi', 222 | code: 'MS', 223 | region: 'South', 224 | division: 'East South Central', 225 | income: 43529, 226 | population: 2986530, 227 | area: 125438 228 | }, 229 | { 230 | state: 'Missouri', 231 | code: 'MO', 232 | region: 'Midwest', 233 | division: 'West North Central', 234 | income: 53578, 235 | population: 6126452, 236 | area: 180540 237 | }, 238 | { 239 | state: 'Montana', 240 | code: 'MT', 241 | region: 'West', 242 | division: 'Mountain', 243 | income: 53386, 244 | population: 1062305, 245 | area: 380831 246 | }, 247 | { 248 | state: 'Nebraska', 249 | code: 'NE', 250 | region: 'Midwest', 251 | division: 'West North Central', 252 | income: 59970, 253 | population: 1929268, 254 | area: 200330 255 | }, 256 | { 257 | state: 'Nevada', 258 | code: 'NV', 259 | region: 'West', 260 | division: 'Mountain', 261 | income: 58003, 262 | population: 3034392, 263 | area: 286380 264 | }, 265 | { 266 | state: 'New Hampshire', 267 | code: 'NH', 268 | region: 'Northeast', 269 | division: 'New England', 270 | income: 73381, 271 | population: 1356458, 272 | area: 24214 273 | }, 274 | { 275 | state: 'New Jersey', 276 | code: 'NJ', 277 | region: 'Northeast', 278 | division: 'Middle Atlantic', 279 | income: 80088, 280 | population: 8908520, 281 | area: 22591 282 | }, 283 | { 284 | state: 'New Mexico', 285 | code: 'NM', 286 | region: 'West', 287 | division: 'Mountain', 288 | income: 46744, 289 | population: 2095428, 290 | area: 314917 291 | }, 292 | { 293 | state: 'New York', 294 | code: 'NY', 295 | region: 'Northeast', 296 | division: 'Middle Atlantic', 297 | income: 64894, 298 | population: 19542209, 299 | area: 141297 300 | }, 301 | { 302 | state: 'North Carolina', 303 | code: 'NC', 304 | region: 'South', 305 | division: 'South Atlantic', 306 | income: 52752, 307 | population: 10383620, 308 | area: 139391 309 | }, 310 | { 311 | state: 'North Dakota', 312 | code: 'ND', 313 | region: 'Midwest', 314 | division: 'West North Central', 315 | income: 61843, 316 | population: 760077, 317 | area: 183108 318 | }, 319 | { 320 | state: 'Ohio', 321 | code: 'OH', 322 | region: 'Midwest', 323 | division: 'East North Central', 324 | income: 54021, 325 | population: 11689442, 326 | area: 116098 327 | }, 328 | { 329 | state: 'Oklahoma', 330 | code: 'OK', 331 | region: 'South', 332 | division: 'West South Central', 333 | income: 50051, 334 | population: 3943079, 335 | area: 181037 336 | }, 337 | { 338 | state: 'Oregon', 339 | code: 'OR', 340 | region: 'West', 341 | division: 'Pacific', 342 | income: 60212, 343 | population: 4190713, 344 | area: 254799 345 | }, 346 | { 347 | state: 'Pennsylvania', 348 | code: 'PA', 349 | region: 'Northeast', 350 | division: 'Middle Atlantic', 351 | income: 59105, 352 | population: 12807060, 353 | area: 119280 354 | }, 355 | { 356 | state: 'Rhode Island', 357 | code: 'RI', 358 | region: 'Northeast', 359 | division: 'New England', 360 | income: 63870, 361 | population: 1057315, 362 | area: 4001 363 | }, 364 | { 365 | state: 'South Carolina', 366 | code: 'SC', 367 | region: 'South', 368 | division: 'South Atlantic', 369 | income: 50570, 370 | population: 5084127, 371 | area: 82933 372 | }, 373 | { 374 | state: 'South Dakota', 375 | code: 'SD', 376 | region: 'Midwest', 377 | division: 'West North Central', 378 | income: 56521, 379 | population: 882235, 380 | area: 199729 381 | }, 382 | { 383 | state: 'Tennessee', 384 | code: 'TN', 385 | region: 'South', 386 | division: 'East South Central', 387 | income: 51340, 388 | population: 6770010, 389 | area: 109153 390 | }, 391 | { 392 | state: 'Texas', 393 | code: 'TX', 394 | region: 'South', 395 | division: 'West South Central', 396 | income: 59206, 397 | population: 28701845, 398 | area: 695662 399 | }, 400 | { 401 | state: 'Utah', 402 | code: 'UT', 403 | region: 'West', 404 | division: 'Mountain', 405 | income: 65358, 406 | population: 3161105, 407 | area: 219882 408 | }, 409 | { 410 | state: 'Vermont', 411 | code: 'VT', 412 | region: 'Northeast', 413 | division: 'New England', 414 | income: 57513, 415 | population: 626299, 416 | area: 24906 417 | }, 418 | { 419 | state: 'Virginia', 420 | code: 'VA', 421 | region: 'South', 422 | division: 'South Atlantic', 423 | income: 71535, 424 | population: 8517685, 425 | area: 110787 426 | }, 427 | { 428 | state: 'Washington', 429 | code: 'WA', 430 | region: 'West', 431 | division: 'Pacific', 432 | income: 70979, 433 | population: 7535591, 434 | area: 184661 435 | }, 436 | { 437 | state: 'West Virginia', 438 | code: 'WV', 439 | region: 'South', 440 | division: 'South Atlantic', 441 | income: 43469, 442 | population: 1805832, 443 | area: 62756 444 | }, 445 | { 446 | state: 'Wisconsin', 447 | code: 'WI', 448 | region: 'Midwest', 449 | division: 'East North Central', 450 | income: 59305, 451 | population: 5813568, 452 | area: 169635 453 | }, 454 | { 455 | state: 'Wyoming', 456 | code: 'WY', 457 | region: 'West', 458 | division: 'Mountain', 459 | income: 60434, 460 | population: 577737, 461 | area: 253335 462 | } 463 | ]; 464 | -------------------------------------------------------------------------------- /docs/scripts/data.js: -------------------------------------------------------------------------------- 1 | 2 | export const statsByState = [ 3 | { 4 | state: 'Alabama', 5 | code: 'AL', 6 | region: 'South', 7 | division: 'East South Central', 8 | income: 48123, 9 | population: 4887871, 10 | area: 135767 11 | }, 12 | { 13 | state: 'Alaska', 14 | code: 'AK', 15 | region: 'West', 16 | division: 'Pacific', 17 | income: 73181, 18 | population: 737438, 19 | area: 1723337 20 | }, 21 | { 22 | state: 'Arizona', 23 | code: 'AZ', 24 | region: 'West', 25 | division: 'Mountain', 26 | income: 56581, 27 | population: 7171646, 28 | area: 295234 29 | }, 30 | { 31 | state: 'Arkansas', 32 | code: 'AR', 33 | region: 'South', 34 | division: 'West South Central', 35 | income: 45869, 36 | population: 3013825, 37 | area: 137732 38 | }, 39 | { 40 | state: 'California', 41 | code: 'CA', 42 | region: 'West', 43 | division: 'Pacific', 44 | income: 71805, 45 | population: 39557045, 46 | area: 423972 47 | }, 48 | { 49 | state: 'Colorado', 50 | code: 'CO', 51 | region: 'West', 52 | division: 'Mountain', 53 | income: 69117, 54 | population: 5695564, 55 | area: 269601 56 | }, 57 | { 58 | state: 'Connecticut', 59 | code: 'CT', 60 | region: 'Northeast', 61 | division: 'New England', 62 | income: 74168, 63 | population: 3572665, 64 | area: 14357 65 | }, 66 | { 67 | state: 'Delaware', 68 | code: 'DE', 69 | region: 'South', 70 | division: 'South Atlantic', 71 | income: 62852, 72 | population: 967171, 73 | area: 6446 74 | }, 75 | { 76 | state: 'District of Columbia', 77 | code: 'DC', 78 | region: 'South', 79 | division: 'South Atlantic', 80 | income: 82372, 81 | population: 702455, 82 | area: 177 83 | }, 84 | { 85 | state: 'Florida', 86 | code: 'FL', 87 | region: 'South', 88 | division: 'South Atlantic', 89 | income: 52594, 90 | population: 21299325, 91 | area: 170312 92 | }, 93 | { 94 | state: 'Georgia', 95 | code: 'GA', 96 | region: 'South', 97 | division: 'South Atlantic', 98 | income: 56183, 99 | population: 10519475, 100 | area: 153910 101 | }, 102 | { 103 | state: 'Hawaii', 104 | code: 'HI', 105 | region: 'West', 106 | division: 'Pacific', 107 | income: 77765, 108 | population: 1420491, 109 | area: 28313 110 | }, 111 | { 112 | state: 'Idaho', 113 | code: 'ID', 114 | region: 'West', 115 | division: 'Mountain', 116 | income: 52225, 117 | population: 1754208, 118 | area: 216443 119 | }, 120 | { 121 | state: 'Illinois', 122 | code: 'IL', 123 | region: 'Midwest', 124 | division: 'East North Central', 125 | income: 62992, 126 | population: 12741080, 127 | area: 149995 128 | }, 129 | { 130 | state: 'Indiana', 131 | code: 'IN', 132 | region: 'Midwest', 133 | division: 'East North Central', 134 | income: 54181, 135 | population: 6691878, 136 | area: 94326 137 | }, 138 | { 139 | state: 'Iowa', 140 | code: 'IA', 141 | region: 'Midwest', 142 | division: 'West North Central', 143 | income: 5857, 144 | population: 3156145, 145 | area: 145746 146 | }, 147 | { 148 | state: 'Kansas', 149 | code: 'KS', 150 | region: 'Midwest', 151 | division: 'West North Central', 152 | income: 56422, 153 | population: 2911505, 154 | area: 213100 155 | }, 156 | { 157 | state: 'Kentucky', 158 | code: 'KY', 159 | region: 'South', 160 | division: 'East South Central', 161 | income: 45215, 162 | population: 4468402, 163 | area: 104656 164 | }, 165 | { 166 | state: 'Louisiana', 167 | code: 'LA', 168 | region: 'South', 169 | division: 'West South Central', 170 | income: 46145, 171 | population: 4659978, 172 | area: 135659 173 | }, 174 | { 175 | state: 'Maine', 176 | code: 'ME', 177 | region: 'Northeast', 178 | division: 'New England', 179 | income: 55277, 180 | population: 1338404, 181 | area: 91633 182 | }, 183 | { 184 | state: 'Maryland', 185 | code: 'MD', 186 | region: 'South', 187 | division: 'South Atlantic', 188 | income: 80776, 189 | population: 6042718, 190 | area: 32131 191 | }, 192 | { 193 | state: 'Massachusetts', 194 | code: 'MA', 195 | region: 'Northeast', 196 | division: 'New England', 197 | income: 77385, 198 | population: 6902149, 199 | area: 27336 200 | }, 201 | { 202 | state: 'Michigan', 203 | code: 'MI', 204 | region: 'Midwest', 205 | division: 'East North Central', 206 | income: 54909, 207 | population: 9995915, 208 | area: 250487 209 | }, 210 | { 211 | state: 'Minnesota', 212 | code: 'MN', 213 | region: 'Midwest', 214 | division: 'West North Central', 215 | income: 68388, 216 | population: 5611179, 217 | area: 225163 218 | }, 219 | { 220 | state: 'Mississippi', 221 | code: 'MS', 222 | region: 'South', 223 | division: 'East South Central', 224 | income: 43529, 225 | population: 2986530, 226 | area: 125438 227 | }, 228 | { 229 | state: 'Missouri', 230 | code: 'MO', 231 | region: 'Midwest', 232 | division: 'West North Central', 233 | income: 53578, 234 | population: 6126452, 235 | area: 180540 236 | }, 237 | { 238 | state: 'Montana', 239 | code: 'MT', 240 | region: 'West', 241 | division: 'Mountain', 242 | income: 53386, 243 | population: 1062305, 244 | area: 380831 245 | }, 246 | { 247 | state: 'Nebraska', 248 | code: 'NE', 249 | region: 'Midwest', 250 | division: 'West North Central', 251 | income: 59970, 252 | population: 1929268, 253 | area: 200330 254 | }, 255 | { 256 | state: 'Nevada', 257 | code: 'NV', 258 | region: 'West', 259 | division: 'Mountain', 260 | income: 58003, 261 | population: 3034392, 262 | area: 286380 263 | }, 264 | { 265 | state: 'New Hampshire', 266 | code: 'NH', 267 | region: 'Northeast', 268 | division: 'New England', 269 | income: 73381, 270 | population: 1356458, 271 | area: 24214 272 | }, 273 | { 274 | state: 'New Jersey', 275 | code: 'NJ', 276 | region: 'Northeast', 277 | division: 'Middle Atlantic', 278 | income: 80088, 279 | population: 8908520, 280 | area: 22591 281 | }, 282 | { 283 | state: 'New Mexico', 284 | code: 'NM', 285 | region: 'West', 286 | division: 'Mountain', 287 | income: 46744, 288 | population: 2095428, 289 | area: 314917 290 | }, 291 | { 292 | state: 'New York', 293 | code: 'NY', 294 | region: 'Northeast', 295 | division: 'Middle Atlantic', 296 | income: 64894, 297 | population: 19542209, 298 | area: 141297 299 | }, 300 | { 301 | state: 'North Carolina', 302 | code: 'NC', 303 | region: 'South', 304 | division: 'South Atlantic', 305 | income: 52752, 306 | population: 10383620, 307 | area: 139391 308 | }, 309 | { 310 | state: 'North Dakota', 311 | code: 'ND', 312 | region: 'Midwest', 313 | division: 'West North Central', 314 | income: 61843, 315 | population: 760077, 316 | area: 183108 317 | }, 318 | { 319 | state: 'Ohio', 320 | code: 'OH', 321 | region: 'Midwest', 322 | division: 'East North Central', 323 | income: 54021, 324 | population: 11689442, 325 | area: 116098 326 | }, 327 | { 328 | state: 'Oklahoma', 329 | code: 'OK', 330 | region: 'South', 331 | division: 'West South Central', 332 | income: 50051, 333 | population: 3943079, 334 | area: 181037 335 | }, 336 | { 337 | state: 'Oregon', 338 | code: 'OR', 339 | region: 'West', 340 | division: 'Pacific', 341 | income: 60212, 342 | population: 4190713, 343 | area: 254799 344 | }, 345 | { 346 | state: 'Pennsylvania', 347 | code: 'PA', 348 | region: 'Northeast', 349 | division: 'Middle Atlantic', 350 | income: 59105, 351 | population: 12807060, 352 | area: 119280 353 | }, 354 | { 355 | state: 'Rhode Island', 356 | code: 'RI', 357 | region: 'Northeast', 358 | division: 'New England', 359 | income: 63870, 360 | population: 1057315, 361 | area: 4001 362 | }, 363 | { 364 | state: 'South Carolina', 365 | code: 'SC', 366 | region: 'South', 367 | division: 'South Atlantic', 368 | income: 50570, 369 | population: 5084127, 370 | area: 82933 371 | }, 372 | { 373 | state: 'South Dakota', 374 | code: 'SD', 375 | region: 'Midwest', 376 | division: 'West North Central', 377 | income: 56521, 378 | population: 882235, 379 | area: 199729 380 | }, 381 | { 382 | state: 'Tennessee', 383 | code: 'TN', 384 | region: 'South', 385 | division: 'East South Central', 386 | income: 51340, 387 | population: 6770010, 388 | area: 109153 389 | }, 390 | { 391 | state: 'Texas', 392 | code: 'TX', 393 | region: 'South', 394 | division: 'West South Central', 395 | income: 59206, 396 | population: 28701845, 397 | area: 695662 398 | }, 399 | { 400 | state: 'Utah', 401 | code: 'UT', 402 | region: 'West', 403 | division: 'Mountain', 404 | income: 65358, 405 | population: 3161105, 406 | area: 219882 407 | }, 408 | { 409 | state: 'Vermont', 410 | code: 'VT', 411 | region: 'Northeast', 412 | division: 'New England', 413 | income: 57513, 414 | population: 626299, 415 | area: 24906 416 | }, 417 | { 418 | state: 'Virginia', 419 | code: 'VA', 420 | region: 'South', 421 | division: 'South Atlantic', 422 | income: 71535, 423 | population: 8517685, 424 | area: 110787 425 | }, 426 | { 427 | state: 'Washington', 428 | code: 'WA', 429 | region: 'West', 430 | division: 'Pacific', 431 | income: 70979, 432 | population: 7535591, 433 | area: 184661 434 | }, 435 | { 436 | state: 'West Virginia', 437 | code: 'WV', 438 | region: 'South', 439 | division: 'South Atlantic', 440 | income: 43469, 441 | population: 1805832, 442 | area: 62756 443 | }, 444 | { 445 | state: 'Wisconsin', 446 | code: 'WI', 447 | region: 'Midwest', 448 | division: 'East North Central', 449 | income: 59305, 450 | population: 5813568, 451 | area: 169635 452 | }, 453 | { 454 | state: 'Wyoming', 455 | code: 'WY', 456 | region: 'West', 457 | division: 'Mountain', 458 | income: 60434, 459 | population: 577737, 460 | area: 253335 461 | } 462 | ]; 463 | 464 | export const objectsTree = { 465 | analytics: { 466 | cluster: { 467 | agglomerative: { 468 | value: 3938 469 | }, 470 | communityStructure: { 471 | value: 3812 472 | }, 473 | hierarchical: { 474 | value: 6714 475 | }, 476 | mergeEdge: { 477 | value: 743 478 | }, 479 | }, 480 | graph: { 481 | betweennessCentrality: { 482 | value: 3534 483 | }, 484 | linkDistance: { 485 | value: 5731 486 | }, 487 | maxFlowMinCut: { 488 | value: 7840 489 | }, 490 | shortestPaths: { 491 | value: 5914 492 | }, 493 | spanningTree: { 494 | value: 3416 495 | }, 496 | }, 497 | optimization: { 498 | aspectRatioBanker: { 499 | value: 7074 500 | } 501 | } 502 | }, 503 | animate: { 504 | easing: { 505 | value: 17010 506 | }, 507 | functionSequence: { 508 | vaue: 5842 509 | }, 510 | interpolate: { 511 | arrayInterpolator: { 512 | value: 1983 513 | }, 514 | colorInterpolator: { 515 | value: 2047 516 | }, 517 | dateInterpolator: { 518 | value: 1375 519 | }, 520 | interpolator: { 521 | value: 8746 522 | }, 523 | matrixInterpolator: { 524 | value: 2202 525 | }, 526 | numberInterpolator: { 527 | value: 1382 528 | }, 529 | objectInterpolator: { 530 | value: 1629 531 | }, 532 | pointInterpolator: { 533 | value: 1675 534 | }, 535 | rectangleInterpolator: { 536 | value: 2042 537 | }, 538 | }, 539 | schedulable: { 540 | value: 1041 541 | } 542 | } 543 | }; 544 | -------------------------------------------------------------------------------- /src/element.js: -------------------------------------------------------------------------------- 1 | import {Element} from 'chart.js'; 2 | import {toFont, isArray, toTRBL, toTRBLCorners, addRoundedRectPath, valueOrDefault, defined, isNumber} from 'chart.js/helpers'; 3 | 4 | const widthCache = new Map(); 5 | 6 | /** 7 | * Helper function to get the bounds of the rect 8 | * @param {TreemapElement} rect the rect 9 | * @param {boolean} [useFinalPosition] 10 | * @return {object} bounds of the rect 11 | * @private 12 | */ 13 | function getBounds(rect, useFinalPosition) { 14 | const {x, y, width, height} = rect.getProps(['x', 'y', 'width', 'height'], useFinalPosition); 15 | return {left: x, top: y, right: x + width, bottom: y + height}; 16 | } 17 | 18 | function limit(value, min, max) { 19 | return Math.max(Math.min(value, max), min); 20 | } 21 | 22 | export function parseBorderWidth(value, maxW, maxH) { 23 | const o = toTRBL(value); 24 | 25 | return { 26 | t: limit(o.top, 0, maxH), 27 | r: limit(o.right, 0, maxW), 28 | b: limit(o.bottom, 0, maxH), 29 | l: limit(o.left, 0, maxW) 30 | }; 31 | } 32 | 33 | function parseBorderRadius(value, maxW, maxH) { 34 | const o = toTRBLCorners(value); 35 | const maxR = Math.min(maxW, maxH); 36 | 37 | return { 38 | topLeft: limit(o.topLeft, 0, maxR), 39 | topRight: limit(o.topRight, 0, maxR), 40 | bottomLeft: limit(o.bottomLeft, 0, maxR), 41 | bottomRight: limit(o.bottomRight, 0, maxR) 42 | }; 43 | } 44 | 45 | function boundingRects(rect) { 46 | const bounds = getBounds(rect); 47 | const width = bounds.right - bounds.left; 48 | const height = bounds.bottom - bounds.top; 49 | const border = parseBorderWidth(rect.options.borderWidth, width / 2, height / 2); 50 | const radius = parseBorderRadius(rect.options.borderRadius, width / 2, height / 2); 51 | const outer = { 52 | x: bounds.left, 53 | y: bounds.top, 54 | w: width, 55 | h: height, 56 | active: rect.active, 57 | radius 58 | }; 59 | 60 | return { 61 | outer, 62 | inner: { 63 | x: outer.x + border.l, 64 | y: outer.y + border.t, 65 | w: outer.w - border.l - border.r, 66 | h: outer.h - border.t - border.b, 67 | active: rect.active, 68 | radius: { 69 | topLeft: Math.max(0, radius.topLeft - Math.max(border.t, border.l)), 70 | topRight: Math.max(0, radius.topRight - Math.max(border.t, border.r)), 71 | bottomLeft: Math.max(0, radius.bottomLeft - Math.max(border.b, border.l)), 72 | bottomRight: Math.max(0, radius.bottomRight - Math.max(border.b, border.r)), 73 | } 74 | } 75 | }; 76 | } 77 | 78 | function inRange(rect, x, y, useFinalPosition) { 79 | const skipX = x === null; 80 | const skipY = y === null; 81 | const bounds = !rect || (skipX && skipY) ? false : getBounds(rect, useFinalPosition); 82 | 83 | return bounds 84 | && (skipX || x >= bounds.left && x <= bounds.right) 85 | && (skipY || y >= bounds.top && y <= bounds.bottom); 86 | } 87 | 88 | function hasRadius(radius) { 89 | return radius.topLeft || radius.topRight || radius.bottomLeft || radius.bottomRight; 90 | } 91 | 92 | /** 93 | * Add a path of a rectangle to the current sub-path 94 | * @param {CanvasRenderingContext2D} ctx Context 95 | * @param {*} rect Bounding rect 96 | */ 97 | function addNormalRectPath(ctx, rect) { 98 | ctx.rect(rect.x, rect.y, rect.w, rect.h); 99 | } 100 | 101 | export function shouldDrawCaption(displayMode, rect, options) { 102 | if (!options || options.display === false) { 103 | return false; 104 | } 105 | if (displayMode === 'headerBoxes') { 106 | return true; 107 | } 108 | const {w, h} = rect; 109 | const font = toFont(options.font); 110 | const min = font.lineHeight; 111 | const padding = limit(valueOrDefault(options.padding, 3) * 2, 0, Math.min(w, h)); 112 | return (w - padding) > min && (h - padding) > min; 113 | } 114 | 115 | export function getCaptionHeight(displayMode, rect, font, padding) { 116 | if (displayMode !== 'headerBoxes') { 117 | return font.lineHeight + padding * 2; 118 | } 119 | const captionHeight = font.lineHeight + padding * 2; 120 | return rect.h < 2 * captionHeight ? rect.h / 3 : captionHeight; 121 | } 122 | 123 | function drawText(ctx, rect, options, item) { 124 | const {captions, labels, displayMode} = options; 125 | ctx.save(); 126 | ctx.beginPath(); 127 | ctx.rect(rect.x, rect.y, rect.w, rect.h); 128 | ctx.clip(); 129 | const isLeaf = item && (!defined(item.l) || item.isLeaf); 130 | if (isLeaf && labels.display) { 131 | drawLabel(ctx, rect, options); 132 | } else if (!isLeaf && shouldDrawCaption(displayMode, rect, captions)) { 133 | drawCaption(ctx, rect, options, item); 134 | } 135 | ctx.restore(); 136 | } 137 | 138 | function drawCaption(ctx, rect, options, item) { 139 | const {captions, spacing, rtl, displayMode} = options; 140 | const {color, hoverColor, font, hoverFont, padding, align, formatter} = captions; 141 | const oColor = (rect.active ? hoverColor : color) || color; 142 | const oAlign = align || (rtl ? 'right' : 'left'); 143 | const optFont = (rect.active ? hoverFont : font) || font; 144 | const oFont = toFont(optFont); 145 | const fonts = [oFont]; 146 | if (oFont.lineHeight > rect.h) { 147 | return; 148 | } 149 | let text = formatter || item.g; 150 | const captionSize = measureLabelSize(ctx, [formatter], fonts); 151 | if (captionSize.width + 2 * padding > rect.w) { 152 | text = sliceTextToFitWidth(ctx, text, rect.w - 2 * padding, fonts); 153 | } 154 | 155 | const lh = oFont.lineHeight / 2; 156 | const x = calculateX(rect, oAlign, padding); 157 | ctx.fillStyle = oColor; 158 | ctx.font = oFont.string; 159 | ctx.textAlign = oAlign; 160 | ctx.textBaseline = 'middle'; 161 | const y = displayMode === 'headerBoxes' ? rect.y + rect.h / 2 : rect.y + padding + spacing + lh; 162 | ctx.fillText(text, x, y); 163 | } 164 | 165 | function sliceTextToFitWidth(ctx, text, width, fonts) { 166 | const ellipsis = '...'; 167 | const ellipsisWidth = measureLabelSize(ctx, [ellipsis], fonts).width; 168 | if (ellipsisWidth >= width) { 169 | return ''; 170 | } 171 | let lowerBoundLen = 1; 172 | let upperBoundLen = text.length; 173 | let currentWidth; 174 | while (lowerBoundLen <= upperBoundLen) { 175 | const currentLen = Math.floor((lowerBoundLen + upperBoundLen) / 2); 176 | const currentText = text.slice(0, currentLen); 177 | currentWidth = measureLabelSize(ctx, [currentText], fonts).width; 178 | if (currentWidth + ellipsisWidth > width) { 179 | upperBoundLen = currentLen - 1; 180 | } else { 181 | lowerBoundLen = currentLen + 1; 182 | } 183 | } 184 | const slicedText = text.slice(0, Math.max(0, lowerBoundLen - 1)); 185 | return slicedText ? slicedText + ellipsis : ''; 186 | } 187 | 188 | function measureLabelSize(ctx, lines, fonts) { 189 | const fontsKey = fonts.reduce(function(prev, item) { 190 | prev += item.string; 191 | return prev; 192 | }, ''); 193 | const mapKey = lines.join() + fontsKey + (ctx._measureText ? '-spriting' : ''); 194 | if (!widthCache.has(mapKey)) { 195 | ctx.save(); 196 | const count = lines.length; 197 | let width = 0; 198 | let height = 0; 199 | for (let i = 0; i < count; i++) { 200 | const font = fonts[Math.min(i, fonts.length - 1)]; 201 | ctx.font = font.string; 202 | const text = lines[i]; 203 | width = Math.max(width, ctx.measureText(text).width); 204 | height += font.lineHeight; 205 | } 206 | ctx.restore(); 207 | widthCache.set(mapKey, {width, height}); 208 | } 209 | return widthCache.get(mapKey); 210 | } 211 | 212 | function toFonts(fonts, fitRatio) { 213 | return fonts.map(function(f) { 214 | f.size = Math.floor(f.size * fitRatio); 215 | f.lineHeight = undefined; 216 | return toFont(f); 217 | }); 218 | } 219 | 220 | function labelToDraw(ctx, rect, options, labelSize) { 221 | const {overflow, padding} = options; 222 | const {width, height} = labelSize; 223 | if (overflow === 'hidden') { 224 | return !((width + padding * 2) > rect.w || (height + padding * 2) > rect.h); 225 | } else if (overflow === 'fit') { 226 | const ratio = Math.min(rect.w / (width + padding * 2), rect.h / (height + padding * 2)); 227 | if (ratio < 1) { 228 | return ratio; 229 | } 230 | } 231 | return true; 232 | } 233 | 234 | function getFontFromOptions(rect, labels) { 235 | const {font, hoverFont} = labels; 236 | const optFont = (rect.active ? hoverFont : font) || font; 237 | return isArray(optFont) ? optFont.map(f => toFont(f)) : [toFont(optFont)]; 238 | } 239 | 240 | function drawLabel(ctx, rect, options) { 241 | const labels = options.labels; 242 | const content = labels.formatter; 243 | if (!content) { 244 | return; 245 | } 246 | const contents = isArray(content) ? content : [content]; 247 | let fonts = getFontFromOptions(rect, labels); 248 | let labelSize = measureLabelSize(ctx, contents, fonts); 249 | const lblToDraw = labelToDraw(ctx, rect, labels, labelSize); 250 | if (!lblToDraw) { 251 | return; 252 | } 253 | if (isNumber(lblToDraw)) { 254 | labelSize = {width: labelSize.width * lblToDraw, height: labelSize.height * lblToDraw}; 255 | fonts = toFonts(fonts, lblToDraw); 256 | } 257 | const {color, hoverColor, align} = labels; 258 | const optColor = (rect.active ? hoverColor : color) || color; 259 | const colors = isArray(optColor) ? optColor : [optColor]; 260 | const xyPoint = calculateXYLabel(rect, labels, labelSize); 261 | ctx.textAlign = align; 262 | ctx.textBaseline = 'middle'; 263 | let lhs = 0; 264 | contents.forEach(function(l, i) { 265 | const c = colors[Math.min(i, colors.length - 1)]; 266 | const f = fonts[Math.min(i, fonts.length - 1)]; 267 | const lh = f.lineHeight; 268 | ctx.font = f.string; 269 | ctx.fillStyle = c; 270 | ctx.fillText(l, xyPoint.x, xyPoint.y + lh / 2 + lhs); 271 | lhs += lh; 272 | }); 273 | } 274 | 275 | function drawDivider(ctx, rect, options, item) { 276 | const dividers = options.dividers; 277 | if (!dividers.display || !item._data.children.length) { 278 | return; 279 | } 280 | const {x, y, w, h} = rect; 281 | const {lineColor, lineCapStyle, lineDash, lineDashOffset, lineWidth} = dividers; 282 | ctx.save(); 283 | ctx.strokeStyle = lineColor; 284 | ctx.lineCap = lineCapStyle; 285 | ctx.setLineDash(lineDash); 286 | ctx.lineDashOffset = lineDashOffset; 287 | ctx.lineWidth = lineWidth; 288 | ctx.beginPath(); 289 | if (w > h) { 290 | const w2 = w / 2; 291 | ctx.moveTo(x + w2, y); 292 | ctx.lineTo(x + w2, y + h); 293 | } else { 294 | const h2 = h / 2; 295 | ctx.moveTo(x, y + h2); 296 | ctx.lineTo(x + w, y + h2); 297 | } 298 | ctx.stroke(); 299 | ctx.restore(); 300 | } 301 | 302 | function calculateXYLabel(rect, options, labelSize) { 303 | const {align, position, padding} = options; 304 | let x, y; 305 | x = calculateX(rect, align, padding); 306 | if (position === 'top') { 307 | y = rect.y + padding; 308 | } else if (position === 'bottom') { 309 | y = rect.y + rect.h - padding - labelSize.height; 310 | } else { 311 | y = rect.y + (rect.h - labelSize.height) / 2 + padding; 312 | } 313 | return {x, y}; 314 | } 315 | 316 | function calculateX(rect, align, padding) { 317 | if (align === 'left') { 318 | return rect.x + padding; 319 | } else if (align === 'right') { 320 | return rect.x + rect.w - padding; 321 | } 322 | return rect.x + rect.w / 2; 323 | } 324 | 325 | export default class TreemapElement extends Element { 326 | 327 | constructor(cfg) { 328 | super(); 329 | 330 | this.options = undefined; 331 | this.width = undefined; 332 | this.height = undefined; 333 | 334 | if (cfg) { 335 | Object.assign(this, cfg); 336 | } 337 | } 338 | 339 | draw(ctx, data) { 340 | if (!data) { 341 | return; 342 | } 343 | const options = this.options; 344 | const {inner, outer} = boundingRects(this); 345 | 346 | const addRectPath = hasRadius(outer.radius) ? addRoundedRectPath : addNormalRectPath; 347 | 348 | ctx.save(); 349 | 350 | if (outer.w !== inner.w || outer.h !== inner.h) { 351 | ctx.beginPath(); 352 | addRectPath(ctx, outer); 353 | ctx.clip(); 354 | addRectPath(ctx, inner); 355 | ctx.fillStyle = options.borderColor; 356 | ctx.fill('evenodd'); 357 | } 358 | 359 | ctx.beginPath(); 360 | addRectPath(ctx, inner); 361 | ctx.fillStyle = options.backgroundColor; 362 | ctx.fill(); 363 | 364 | drawDivider(ctx, inner, options, data); 365 | drawText(ctx, inner, options, data); 366 | ctx.restore(); 367 | } 368 | 369 | inRange(mouseX, mouseY, useFinalPosition) { 370 | return inRange(this, mouseX, mouseY, useFinalPosition); 371 | } 372 | 373 | inXRange(mouseX, useFinalPosition) { 374 | return inRange(this, mouseX, null, useFinalPosition); 375 | } 376 | 377 | inYRange(mouseY, useFinalPosition) { 378 | return inRange(this, null, mouseY, useFinalPosition); 379 | } 380 | 381 | getCenterPoint(useFinalPosition) { 382 | const {x, y, width, height} = this.getProps(['x', 'y', 'width', 'height'], useFinalPosition); 383 | return { 384 | x: x + width / 2, 385 | y: y + height / 2 386 | }; 387 | } 388 | 389 | tooltipPosition() { 390 | return this.getCenterPoint(); 391 | } 392 | 393 | /** 394 | * @todo: remove this unused function in v3 395 | */ 396 | getRange(axis) { 397 | return axis === 'x' ? this.width / 2 : this.height / 2; 398 | } 399 | } 400 | 401 | TreemapElement.id = 'treemap'; 402 | 403 | TreemapElement.defaults = { 404 | borderRadius: 0, 405 | borderWidth: 0, 406 | captions: { 407 | align: undefined, 408 | color: 'black', 409 | display: true, 410 | font: {}, 411 | formatter: (ctx) => ctx.raw.g || ctx.raw._data.label || '', 412 | padding: 3 413 | }, 414 | dividers: { 415 | display: false, 416 | lineCapStyle: 'butt', 417 | lineColor: 'black', 418 | lineDash: [], 419 | lineDashOffset: 0, 420 | lineWidth: 1, 421 | }, 422 | label: undefined, 423 | labels: { 424 | align: 'center', 425 | color: 'black', 426 | display: false, 427 | font: {}, 428 | formatter(ctx) { 429 | if (ctx.raw.g) { 430 | return [ctx.raw.g, ctx.raw.v + '']; 431 | } 432 | return ctx.raw._data.label ? [ctx.raw._data.label, ctx.raw.v + ''] : ctx.raw.v + ''; 433 | }, 434 | overflow: 'cut', 435 | position: 'middle', 436 | padding: 3 437 | }, 438 | rtl: false, 439 | spacing: 0.5, 440 | unsorted: false, 441 | displayMode: 'containerBoxes', 442 | }; 443 | 444 | TreemapElement.descriptors = { 445 | captions: { 446 | _fallback: true 447 | }, 448 | labels: { 449 | _fallback: true 450 | }, 451 | _scriptable: true, 452 | _indexable: false 453 | }; 454 | 455 | TreemapElement.defaultRoutes = { 456 | backgroundColor: 'backgroundColor', 457 | borderColor: 'borderColor' 458 | }; 459 | --------------------------------------------------------------------------------