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 |
--------------------------------------------------------------------------------