├── .yarnrc.yml
├── src
├── elements
│ ├── index.ts
│ └── TrapezoidElement.ts
├── controllers
│ ├── index.ts
│ ├── __image_snapshots__
│ │ ├── funnel-controller-spec-ts-funnel-default-1-snap.png
│ │ ├── funnel-controller-spec-ts-funnel-numbers-1-snap.png
│ │ ├── funnel-controller-spec-ts-funnel-align-center-1-snap.png
│ │ ├── funnel-controller-spec-ts-funnel-align-left-1-snap.png
│ │ ├── funnel-controller-spec-ts-funnel-align-right-1-snap.png
│ │ ├── funnel-controller-spec-ts-funnel-horizontal-1-snap.png
│ │ ├── funnel-controller-spec-ts-funnel-shrink-none-1-snap.png
│ │ ├── funnel-controller-spec-ts-funnel-shrink-top-1-snap.png
│ │ ├── funnel-controller-spec-ts-funnel-hor-align-left-1-snap.png
│ │ ├── funnel-controller-spec-ts-funnel-hor-shrink-top-1-snap.png
│ │ ├── funnel-controller-spec-ts-funnel-shrink-bottom-1-snap.png
│ │ ├── funnel-controller-spec-ts-funnel-shrink-middle-1-snap.png
│ │ ├── funnel-controller-spec-ts-funnel-hor-align-center-1-snap.png
│ │ ├── funnel-controller-spec-ts-funnel-hor-align-right-1-snap.png
│ │ ├── funnel-controller-spec-ts-funnel-hor-shrink-bottom-1-snap.png
│ │ ├── funnel-controller-spec-ts-funnel-hor-shrink-middle-1-snap.png
│ │ ├── funnel-controller-spec-ts-funnel-hor-shrink-none-1-snap.png
│ │ ├── funnel-controller-spec-ts-funnel-shrink-fraction-0-1-snap.png
│ │ ├── funnel-controller-spec-ts-funnel-shrink-fraction-1-1-snap.png
│ │ ├── funnel-controller-spec-ts-funnel-shrink-fraction-0-5-1-snap.png
│ │ ├── funnel-controller-spec-ts-funnel-hor-shrink-fraction-0-1-snap.png
│ │ ├── funnel-controller-spec-ts-funnel-hor-shrink-fraction-1-1-snap.png
│ │ ├── funnel-controller-spec-ts-funnel-shrink-fraction-0-25-1-snap.png
│ │ ├── funnel-controller-spec-ts-funnel-shrink-fraction-0-75-1-snap.png
│ │ ├── funnel-controller-spec-ts-funnel-hor-shrink-fraction-0-25-1-snap.png
│ │ ├── funnel-controller-spec-ts-funnel-hor-shrink-fraction-0-5-1-snap.png
│ │ └── funnel-controller-spec-ts-funnel-hor-shrink-fraction-0-75-1-snap.png
│ ├── patchController.ts
│ ├── FunnelController.ts
│ └── FunnelController.spec.ts
├── index.ts
├── index.umd.ts
├── color.ts
└── __tests__
│ └── createChart.ts
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── config.yml
│ ├── feature_request.md
│ ├── question.md
│ └── bug_report.md
├── dependabot.yml
├── release-drafter.yml
└── workflows
│ ├── ci.yml
│ ├── deploy_website.yml
│ ├── create_release.yml
│ └── release_helper.yml
├── .prettierrc.cjs
├── vitest.config.ts
├── tsconfig.c.json
├── docs
├── examples
│ ├── index.md
│ ├── numbers.md
│ ├── gradient.md
│ ├── horizontal.md
│ ├── hierarchical.md
│ ├── horizontal.ts
│ ├── numbers.ts
│ ├── align.md
│ ├── gradient.ts
│ ├── shrink.md
│ ├── hierarchical.ts
│ ├── align.ts
│ ├── basic.ts
│ └── shrink.ts
├── index.md
├── .vitepress
│ ├── theme
│ │ └── index.ts
│ └── config.ts
├── getting-started.md
└── related.md
├── .prettierignore
├── .gitignore
├── typedoc.json
├── .vscode
└── settings.json
├── LICENSE
├── tsconfig.json
├── eslint.config.mjs
├── .gitattributes
├── package.json
├── rollup.config.js
└── README.md
/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | yarnPath: .yarn/releases/yarn-4.10.3.cjs
2 |
--------------------------------------------------------------------------------
/src/elements/index.ts:
--------------------------------------------------------------------------------
1 | export * from './TrapezoidElement';
2 |
--------------------------------------------------------------------------------
/src/controllers/index.ts:
--------------------------------------------------------------------------------
1 | export * from './FunnelController';
2 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [sgratzl]
4 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './elements';
2 | export * from './controllers';
3 | export * from './color';
4 |
--------------------------------------------------------------------------------
/.prettierrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | printWidth: 120,
3 | semi: true,
4 | singleQuote: true,
5 | trailingComma: 'es5',
6 | };
7 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: true
2 | # contact_links:
3 | # - name: Samuel Gratzl
4 | # url: https://www.sgratzl.com
5 | # about: Please ask and answer questions here.
6 |
--------------------------------------------------------------------------------
/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-default-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sgratzl/chartjs-chart-funnel/HEAD/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-default-1-snap.png
--------------------------------------------------------------------------------
/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-numbers-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sgratzl/chartjs-chart-funnel/HEAD/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-numbers-1-snap.png
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { defineConfig } from 'vite';
3 |
4 | export default defineConfig({
5 | test: {
6 | environment: 'jsdom',
7 | root: './src',
8 | },
9 | });
10 |
--------------------------------------------------------------------------------
/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-align-center-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sgratzl/chartjs-chart-funnel/HEAD/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-align-center-1-snap.png
--------------------------------------------------------------------------------
/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-align-left-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sgratzl/chartjs-chart-funnel/HEAD/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-align-left-1-snap.png
--------------------------------------------------------------------------------
/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-align-right-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sgratzl/chartjs-chart-funnel/HEAD/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-align-right-1-snap.png
--------------------------------------------------------------------------------
/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-horizontal-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sgratzl/chartjs-chart-funnel/HEAD/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-horizontal-1-snap.png
--------------------------------------------------------------------------------
/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-shrink-none-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sgratzl/chartjs-chart-funnel/HEAD/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-shrink-none-1-snap.png
--------------------------------------------------------------------------------
/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-shrink-top-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sgratzl/chartjs-chart-funnel/HEAD/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-shrink-top-1-snap.png
--------------------------------------------------------------------------------
/tsconfig.c.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "build",
5 | "declaration": true,
6 | "declarationMap": true,
7 | "noEmit": true,
8 | "composite": true
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-hor-align-left-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sgratzl/chartjs-chart-funnel/HEAD/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-hor-align-left-1-snap.png
--------------------------------------------------------------------------------
/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-hor-shrink-top-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sgratzl/chartjs-chart-funnel/HEAD/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-hor-shrink-top-1-snap.png
--------------------------------------------------------------------------------
/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-shrink-bottom-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sgratzl/chartjs-chart-funnel/HEAD/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-shrink-bottom-1-snap.png
--------------------------------------------------------------------------------
/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-shrink-middle-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sgratzl/chartjs-chart-funnel/HEAD/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-shrink-middle-1-snap.png
--------------------------------------------------------------------------------
/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-hor-align-center-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sgratzl/chartjs-chart-funnel/HEAD/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-hor-align-center-1-snap.png
--------------------------------------------------------------------------------
/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-hor-align-right-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sgratzl/chartjs-chart-funnel/HEAD/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-hor-align-right-1-snap.png
--------------------------------------------------------------------------------
/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-hor-shrink-bottom-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sgratzl/chartjs-chart-funnel/HEAD/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-hor-shrink-bottom-1-snap.png
--------------------------------------------------------------------------------
/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-hor-shrink-middle-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sgratzl/chartjs-chart-funnel/HEAD/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-hor-shrink-middle-1-snap.png
--------------------------------------------------------------------------------
/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-hor-shrink-none-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sgratzl/chartjs-chart-funnel/HEAD/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-hor-shrink-none-1-snap.png
--------------------------------------------------------------------------------
/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-shrink-fraction-0-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sgratzl/chartjs-chart-funnel/HEAD/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-shrink-fraction-0-1-snap.png
--------------------------------------------------------------------------------
/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-shrink-fraction-1-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sgratzl/chartjs-chart-funnel/HEAD/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-shrink-fraction-1-1-snap.png
--------------------------------------------------------------------------------
/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-shrink-fraction-0-5-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sgratzl/chartjs-chart-funnel/HEAD/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-shrink-fraction-0-5-1-snap.png
--------------------------------------------------------------------------------
/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-hor-shrink-fraction-0-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sgratzl/chartjs-chart-funnel/HEAD/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-hor-shrink-fraction-0-1-snap.png
--------------------------------------------------------------------------------
/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-hor-shrink-fraction-1-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sgratzl/chartjs-chart-funnel/HEAD/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-hor-shrink-fraction-1-1-snap.png
--------------------------------------------------------------------------------
/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-shrink-fraction-0-25-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sgratzl/chartjs-chart-funnel/HEAD/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-shrink-fraction-0-25-1-snap.png
--------------------------------------------------------------------------------
/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-shrink-fraction-0-75-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sgratzl/chartjs-chart-funnel/HEAD/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-shrink-fraction-0-75-1-snap.png
--------------------------------------------------------------------------------
/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-hor-shrink-fraction-0-25-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sgratzl/chartjs-chart-funnel/HEAD/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-hor-shrink-fraction-0-25-1-snap.png
--------------------------------------------------------------------------------
/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-hor-shrink-fraction-0-5-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sgratzl/chartjs-chart-funnel/HEAD/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-hor-shrink-fraction-0-5-1-snap.png
--------------------------------------------------------------------------------
/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-hor-shrink-fraction-0-75-1-snap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sgratzl/chartjs-chart-funnel/HEAD/src/controllers/__image_snapshots__/funnel-controller-spec-ts-funnel-hor-shrink-fraction-0-75-1-snap.png
--------------------------------------------------------------------------------
/src/index.umd.ts:
--------------------------------------------------------------------------------
1 | import { registry } from 'chart.js';
2 | import { FunnelController } from './controllers';
3 | import { TrapezoidElement } from './elements';
4 |
5 | export * from '.';
6 |
7 | registry.addControllers(FunnelController);
8 | registry.addElements(TrapezoidElement);
9 |
--------------------------------------------------------------------------------
/docs/examples/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Examples
3 | ---
4 |
5 | # Examples
6 |
7 |
10 |
11 |
15 |
16 | ### Code
17 |
18 | <<< ./basic.ts#config
19 |
--------------------------------------------------------------------------------
/docs/examples/numbers.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Numbers
3 | ---
4 |
5 | # Numbers
6 |
7 |
10 |
11 |
15 |
16 | ### Code
17 |
18 | <<< ./numbers.ts#config
19 |
--------------------------------------------------------------------------------
/docs/examples/gradient.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Gradient
3 | ---
4 |
5 | # Gradient Color
6 |
7 |
10 |
11 |
15 |
16 | ### Code
17 |
18 | <<< ./gradient.ts#config
19 |
--------------------------------------------------------------------------------
/docs/examples/horizontal.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Horizontal Funnel
3 | ---
4 |
5 | # Horizontal Funnel
6 |
7 |
10 |
11 |
15 |
16 | ### Code
17 |
18 | <<< ./horizontal.ts#config
19 |
--------------------------------------------------------------------------------
/docs/examples/hierarchical.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Hierarchical Funnel
3 | ---
4 |
5 | # Hierarchical Funnel
6 |
7 |
10 |
11 |
15 |
16 | ### Code
17 |
18 | <<< ./hierarchical.ts#config
19 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | /.pnp*
2 | /.yarnrc.yml
3 | /.yarn
4 | /build
5 | /docs/.vitepress/cache
6 | /docs/.vitepress/dist
7 | /docs/.vitepress/config.ts.timestamp*
8 | /docs/api
9 | /coverage
10 | /.gitattributes
11 | /.gitignore
12 | /.prettierignore
13 | /LICENSE
14 | /yarn.lock
15 | /.vscode
16 | *.png
17 | *.tgz
18 | *.tsbuildinfo
19 | .eslintcache
20 | .nojekyll
21 |
--------------------------------------------------------------------------------
/src/color.ts:
--------------------------------------------------------------------------------
1 | import chroma from 'chroma-js';
2 |
3 | export function pickForegroundColorToBackgroundColor(
4 | color: string,
5 | blackColor: string = '#000000',
6 | whiteColor: string = '#ffffff'
7 | ): string {
8 | return chroma(color).luminance() > 0.5 ? blackColor : whiteColor;
9 | }
10 |
11 | export function blues(i: number, n: number): string {
12 | return chroma
13 | .scale('Blues')(i / (n - 1))
14 | .hex();
15 | }
16 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | updates:
4 | - package-ecosystem: 'github-actions'
5 | directory: '/'
6 | schedule:
7 | interval: 'monthly'
8 | target-branch: 'dev'
9 | labels:
10 | - 'dependencies'
11 | - 'chore'
12 | - package-ecosystem: 'npm'
13 | directory: '/'
14 | schedule:
15 | interval: 'monthly'
16 | target-branch: 'dev'
17 | labels:
18 | - 'dependencies'
19 | - 'chore'
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | /coverage
7 | /node_modules
8 | .npm
9 | .yarn/*
10 | !.yarn/patches
11 | !.yarn/releases
12 | !.yarn/plugins
13 | !.yarn/versions
14 | .pnp.*
15 |
16 | # Build files
17 | /.tmp
18 | /build
19 |
20 | *.tgz
21 | /.vscode/extensions.json
22 | *.tsbuildinfo
23 | .eslintcache
24 | __diff_output__
25 |
26 | docs/.vitepress/dist
27 | docs/.vitepress/cache
28 | docs/.vitepress/config.ts.timestamp*
29 | docs/api/
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🚀 Feature Request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: 'enhancement'
6 | assignees: ''
7 | ---
8 |
9 |
10 |
11 | It would be great if ...
12 |
13 | **User story**
14 |
15 |
16 |
17 | **Additional context**
18 |
19 |
20 |
--------------------------------------------------------------------------------
/docs/examples/horizontal.ts:
--------------------------------------------------------------------------------
1 | import type { ChartConfiguration } from 'chart.js';
2 | import type {} from '../../src';
3 | import ChartDataLabels from 'chartjs-plugin-datalabels';
4 |
5 | // #region config
6 | export const config: ChartConfiguration<'funnel'> = {
7 | type: 'funnel',
8 | data: {
9 | labels: ['Step 1', 'Step 2', 'Step 3', 'Step 4'],
10 | datasets: [
11 | {
12 | data: [0.7, 0.66, 0.61, 0.01],
13 | shrinkAnchor: 'top',
14 | },
15 | ],
16 | },
17 | plugins: [ChartDataLabels],
18 | };
19 | // #endregion config
20 |
--------------------------------------------------------------------------------
/.github/release-drafter.yml:
--------------------------------------------------------------------------------
1 | name-template: 'v$RESOLVED_VERSION'
2 | tag-template: 'v$RESOLVED_VERSION'
3 | categories:
4 | - title: '🚀 Features'
5 | labels:
6 | - 'enhancement'
7 | - 'feature'
8 | - title: '🐛 Bugs Fixes'
9 | labels:
10 | - 'bug'
11 | - title: 'Documentation'
12 | labels:
13 | - 'documentation'
14 | - title: '🧰 Development'
15 | labels:
16 | - 'chore'
17 | change-template: '- #$NUMBER $TITLE'
18 | change-title-escapes: '\<*_&`#@'
19 | template: |
20 | $CHANGES
21 |
22 | Thanks to $CONTRIBUTORS
23 |
--------------------------------------------------------------------------------
/typedoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://typedoc.org/schema.json",
3 | "entryPoints": ["./src"],
4 | "plugin": ["typedoc-plugin-markdown", "typedoc-vitepress-theme"],
5 | "name": "chartjs-chart-funnel",
6 | "out": "./docs/api",
7 | "docsRoot": "./docs/",
8 | "readme": "none",
9 | "sidebar": {
10 | "pretty": true
11 | },
12 | "theme": "default",
13 | "excludeExternals": true,
14 | "excludeInternal": true,
15 | "excludePrivate": true,
16 | "includeVersion": true,
17 | "categorizeByGroup": true,
18 | "cleanOutputDir": true,
19 | "hideGenerator": true
20 | }
21 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | # https://vitepress.dev/reference/default-theme-home-page
3 | layout: home
4 |
5 | hero:
6 | name: 'chartjs-chart-funnel'
7 | text: 'chart.js plugin'
8 | tagline: Chart.js module for charting funnel charts
9 | actions:
10 | - theme: brand
11 | text: Getting Started
12 | link: /getting-started
13 | - theme: alt
14 | text: Examples
15 | link: /examples/
16 | - theme: alt
17 | text: API
18 | link: /api/
19 | # features:
20 | # - title: Feature A
21 | # details: Lorem ipsum dolor sit amet, consectetur adipiscing elit
22 | ---
23 |
--------------------------------------------------------------------------------
/docs/examples/numbers.ts:
--------------------------------------------------------------------------------
1 | import type { ChartConfiguration } from 'chart.js';
2 | import type {} from '../../src';
3 | import ChartDataLabels from 'chartjs-plugin-datalabels';
4 |
5 | // #region config
6 | export const config: ChartConfiguration<'funnel'> = {
7 | type: 'funnel',
8 | data: {
9 | labels: ['Step 1', 'Step 2', 'Step 3'],
10 | datasets: [
11 | {
12 | data: [2000, 1500, 300],
13 | datalabels: {
14 | formatter: (v) => v.toLocaleString(),
15 | },
16 | },
17 | ],
18 | },
19 | options: {
20 | indexAxis: 'y',
21 | },
22 | plugins: [ChartDataLabels],
23 | };
24 | // #endregion config
25 |
--------------------------------------------------------------------------------
/docs/examples/align.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Funnel Alignment
3 | ---
4 |
5 | # Funnel Alignment
6 |
7 |
10 |
11 | ## Center (default)
12 |
13 |
17 |
18 | ### Code
19 |
20 | <<< ./align.ts#center
21 |
22 | ## Left
23 |
24 |
28 |
29 | ### Code
30 |
31 | <<< ./align.ts#left
32 |
33 | ## Right
34 |
35 |
39 |
40 | ### Code
41 |
42 | <<< ./align.ts#right
43 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/question.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🤗 Question
3 | about: ask question about the library (usage, features,...)
4 | title: ''
5 | labels: 'question'
6 | assignees: ''
7 | ---
8 |
9 |
13 |
14 | I'm having the following question...
15 |
16 | **Screenshots / Sketches**
17 |
18 |
19 |
20 | **Context**
21 |
22 | - Version:
23 | - Browser:
24 |
25 | **Additional context**
26 |
27 |
28 |
--------------------------------------------------------------------------------
/docs/examples/gradient.ts:
--------------------------------------------------------------------------------
1 | import type { ChartConfiguration } from 'chart.js';
2 | import type {} from '../../src';
3 | import ChartDataLabels from 'chartjs-plugin-datalabels';
4 | import chroma from 'chroma-js';
5 |
6 | // #region config
7 | export const config: ChartConfiguration<'funnel'> = {
8 | type: 'funnel',
9 | data: {
10 | labels: ['Step 1', 'Step 2', 'Step 3', 'Step 4'],
11 | datasets: [
12 | {
13 | data: [0.7, 0.66, 0.61, 0.01],
14 | backgroundColor: chroma.scale('Greens').colors(4),
15 | },
16 | ],
17 | },
18 | options: {
19 | indexAxis: 'y',
20 | },
21 | plugins: [ChartDataLabels],
22 | };
23 | // #endregion config
24 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🐛 Bug report
3 | about: If something isn't working as expected 🤔.
4 | title: ''
5 | labels: 'bug'
6 | assignees: ''
7 | ---
8 |
9 |
10 |
11 | When I...
12 |
13 | **To Reproduce**
14 |
15 |
17 |
18 | 1.
19 |
20 | **Expected behavior**
21 |
22 |
23 |
24 | **Screenshots**
25 |
26 |
27 |
28 | **Context**
29 |
30 | - Version:
31 | - Browser:
32 |
33 | **Additional context**
34 |
35 |
36 |
--------------------------------------------------------------------------------
/docs/.vitepress/theme/index.ts:
--------------------------------------------------------------------------------
1 | import Theme from 'vitepress/theme';
2 | import { createTypedChart } from 'vue-chartjs';
3 | import { LinearScale, CategoryScale, Tooltip } from 'chart.js';
4 | import { FunnelController, TrapezoidElement } from '../../../src';
5 | import ChartDataLabels from 'chartjs-plugin-datalabels';
6 | import { HierarchicalScale } from 'chartjs-plugin-hierarchical';
7 |
8 | export default {
9 | ...Theme,
10 | enhanceApp({ app }) {
11 | app.component(
12 | 'FunnelChart',
13 | createTypedChart('funnel', [
14 | LinearScale,
15 | CategoryScale,
16 | FunnelController,
17 | TrapezoidElement,
18 | Tooltip,
19 | ChartDataLabels,
20 | HierarchicalScale,
21 | ])
22 | );
23 | },
24 | };
25 |
--------------------------------------------------------------------------------
/src/controllers/patchController.ts:
--------------------------------------------------------------------------------
1 | import { registry, DatasetControllerChartComponent, ChartComponent } from 'chart.js';
2 |
3 | export default function patchController(
4 | type: TYPE,
5 | config: T,
6 | controller: DatasetControllerChartComponent,
7 | elements: ChartComponent | ChartComponent[] = [],
8 | scales: ChartComponent | ChartComponent[] = []
9 | ): T & { type: TYPE } {
10 | registry.addControllers(controller);
11 | if (Array.isArray(elements)) {
12 | registry.addElements(...elements);
13 | } else {
14 | registry.addElements(elements);
15 | }
16 | if (Array.isArray(scales)) {
17 | registry.addScales(...scales);
18 | } else {
19 | registry.addScales(scales);
20 | }
21 | const c = config as any;
22 | c.type = type;
23 | return c;
24 | }
25 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnSave": true,
3 | "editor.formatOnType": true,
4 | "[javascript]": {
5 | "editor.defaultFormatter": "esbenp.prettier-vscode"
6 | },
7 | "[typescript]": {
8 | "editor.defaultFormatter": "esbenp.prettier-vscode"
9 | },
10 | "[json]": {
11 | "editor.defaultFormatter": "esbenp.prettier-vscode"
12 | },
13 | "[yaml]": {
14 | "editor.defaultFormatter": "esbenp.prettier-vscode"
15 | },
16 | "npm.packageManager": "yarn",
17 | "eslint.nodePath": ".yarn/sdks",
18 | "prettier.prettierPath": ".yarn/sdks/prettier/index.cjs",
19 | "files.eol": "\n",
20 | "typescript.tsdk": ".yarn/sdks/typescript/lib",
21 | "typescript.enablePromptUseWorkspaceTsdk": true,
22 | "editor.detectIndentation": false,
23 | "editor.tabSize": 2,
24 | "search.exclude": {
25 | "**/.yarn": true,
26 | "**/.pnp.*": true
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on:
4 | - push
5 | - pull_request
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v4
12 | - uses: actions/setup-node@v4
13 | with:
14 | node-version: 20
15 | - run: npm i -g yarn
16 | - run: yarn config set checksumBehavior ignore
17 | - name: Cache Node.js modules
18 | uses: actions/cache@v4
19 | with:
20 | path: |
21 | ./.yarn/cache
22 | ./.yarn/unplugged
23 | key: ${{ runner.os }}-yarn2-v5-${{ hashFiles('**/yarn.lock') }}
24 | restore-keys: |
25 | ${{ runner.os }}-yarn2-v5
26 | - run: yarn install
27 | - run: yarn build
28 | - run: yarn lint
29 | - run: yarn test
30 | - uses: actions/upload-artifact@v4
31 | if: failure()
32 | with:
33 | name: diff outputs
34 | path: src/**/__diff_output__/*.png
35 | - run: yarn docs:build
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Samuel Gratzl
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2018",
4 | "module": "ESNext",
5 | "lib": ["DOM", "ES2020"],
6 | "importHelpers": false,
7 | "declaration": false,
8 | "sourceMap": true,
9 | "strict": true,
10 | "removeComments": true,
11 | "verbatimModuleSyntax": false,
12 | "experimentalDecorators": true,
13 | "forceConsistentCasingInFileNames": true,
14 | "strictBindCallApply": true,
15 | "stripInternal": true,
16 | "resolveJsonModule": true,
17 | "noUnusedLocals": true,
18 | "noUnusedParameters": true,
19 | "noImplicitReturns": true,
20 | "noFallthroughCasesInSwitch": true,
21 | "moduleResolution": "Bundler",
22 | "jsx": "react",
23 | "esModuleInterop": true,
24 | "rootDir": "./src",
25 | "baseUrl": "./",
26 | "noEmit": true,
27 | "paths": {
28 | "@": ["./src"],
29 | "*": ["*", "node_modules/*"],
30 | // workaround for: https://github.com/vitest-dev/vitest/issues/4567
31 | "rollup/parseAst": ["./node_modules/rollup/dist/parseAst"]
32 | }
33 | },
34 | "include": ["src/**/*.ts", "src/**/*.tsx", "docs/**/*.tsx"]
35 | }
36 |
--------------------------------------------------------------------------------
/docs/examples/shrink.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Funnel Shrinking
3 | ---
4 |
5 | # Funnel Shrinking
6 |
7 |
10 |
11 | ## Top (default)
12 |
13 |
17 |
18 | ### Code
19 |
20 | <<< ./shrink.ts#top
21 |
22 | ## Top (0.5 fraction)
23 |
24 |
28 |
29 | ### Code
30 |
31 | <<< ./shrink.ts#top5
32 |
33 | ## Top (0.25 fraction)
34 |
35 |
39 |
40 | ### Code
41 |
42 | <<< ./shrink.ts#top25
43 |
44 | ## Middle
45 |
46 |
50 |
51 | ### Code
52 |
53 | <<< ./shrink.ts#middle
54 |
55 | ## Bottom
56 |
57 |
61 |
62 | ### Code
63 |
64 | <<< ./shrink.ts#bottom
65 |
66 | ## None
67 |
68 |
72 |
73 | ### Code
74 |
75 | <<< ./shrink.ts#none
76 |
--------------------------------------------------------------------------------
/docs/getting-started.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Getting Started
3 | ---
4 |
5 | Chart.js module for charting funnel plots. This plugin extends with a new char type `funnel`.
6 |
7 | A Funnel chart is a variant of a bar chart where the bar shrinks on one side to the size of the next bar. In addition, they are usually centered giving the visual impression of a funnel.
8 |
9 | 
10 |
11 | ## Install
12 |
13 | ```sh
14 | npm install chart.js chartjs-chart-funnel
15 | ```
16 |
17 | ## Usage
18 |
19 | see [Examples](./examples/)
20 |
21 | and [CodePen](https://codepen.io/sgratzl/pen/eYjEXQW)
22 |
23 | ## Configuration
24 |
25 | ### Data Structure
26 |
27 | you can either provide percentages (values between 0 and 1) or absolute values (> 1)
28 |
29 | ### Styling
30 |
31 | Trapezoid Elements are Bar elements and provide the same coloring options. In addition, see [TrapezoidElementOptions](/api/interfaces/TrapezoidElementOptions.md) custom option with respect to shrinking behavior.
32 |
33 | In addition, the FunnelController has the following options [FunnelController](/api/interfaces/FunnelChartOptions.md) to customize the alignment of the chart.
34 |
--------------------------------------------------------------------------------
/docs/related.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Related Plugins
3 | ---
4 |
5 | There are several related chart.js plugins providing additional functionality and chart types:
6 |
7 | - [chartjs-chart-boxplot](https://github.com/sgratzl/chartjs-chart-boxplot) for rendering boxplots and violin charts
8 | - [chartjs-chart-error-bars](https://github.com/sgratzl/chartjs-chart-error-bars) for rendering errors bars to bars and line charts
9 | - [chartjs-chart-funnel](https://github.com/sgratzl/chartjs-chart-funnel) for rendering funnel charts
10 | - [chartjs-chart-geo](https://github.com/sgratzl/chartjs-chart-geo) for rendering map, bubble maps, and choropleth charts
11 | - [chartjs-chart-graph](https://github.com/sgratzl/chartjs-chart-graph) for rendering graphs, trees, and networks
12 | - [chartjs-chart-pcp](https://github.com/sgratzl/chartjs-chart-pcp) for rendering parallel coordinate plots
13 | - [chartjs-chart-venn](https://github.com/sgratzl/chartjs-chart-venn) for rendering venn and euler diagrams
14 | - [chartjs-chart-wordcloud](https://github.com/sgratzl/chartjs-chart-wordcloud) for rendering word clouds
15 | - [chartjs-plugin-hierarchical](https://github.com/sgratzl/chartjs-plugin-hierarchical) for rendering hierarchical categorical axes which can be expanded and collapsed
16 |
--------------------------------------------------------------------------------
/docs/examples/hierarchical.ts:
--------------------------------------------------------------------------------
1 | import type { ChartConfiguration } from 'chart.js';
2 | import type {} from '../../src';
3 | import ChartDataLabels from 'chartjs-plugin-datalabels';
4 |
5 | // #region config
6 | export const config: ChartConfiguration<'funnel'> = {
7 | type: 'funnel',
8 | data: {
9 | labels: [
10 | {
11 | label: 'Step 1',
12 | children: ['A', 'B'],
13 | },
14 | 'Step 2',
15 | ],
16 | datasets: [
17 | {
18 | tree: [
19 | {
20 | value: 0.9,
21 | children: [0.9, 0.8],
22 | },
23 | 0.7,
24 | ],
25 | datalabels: {
26 | // formatter: (v) => v.toLocaleString(),
27 | },
28 | } as any,
29 | ],
30 | },
31 | options: {
32 | indexAxis: 'y',
33 | layout: {
34 | padding: {
35 | // add more space at the bottom for the hierarchy
36 | left: 100,
37 | },
38 | },
39 | scales: {
40 | y: {
41 | display: true,
42 | type: 'hierarchical',
43 | padding: 25,
44 | levelPercentage: 1,
45 | },
46 | x: {
47 | // display: true,
48 | },
49 | },
50 | },
51 | plugins: [ChartDataLabels],
52 | };
53 | // #endregion config
54 |
--------------------------------------------------------------------------------
/docs/examples/align.ts:
--------------------------------------------------------------------------------
1 | import type { ChartConfiguration } from 'chart.js';
2 | import type {} from '../../src';
3 | import ChartDataLabels from 'chartjs-plugin-datalabels';
4 |
5 | // #region center
6 | export const center: ChartConfiguration<'funnel'> = {
7 | type: 'funnel',
8 | data: {
9 | labels: ['Step 1', 'Step 2', 'Step 3', 'Step 4'],
10 | datasets: [
11 | {
12 | data: [0.7, 0.66, 0.61, 0.01],
13 | },
14 | ],
15 | },
16 | plugins: [ChartDataLabels],
17 | };
18 | // #endregion center
19 |
20 | // #region left
21 | export const left: ChartConfiguration<'funnel'> = {
22 | type: 'funnel',
23 | data: {
24 | labels: ['Step 1', 'Step 2', 'Step 3', 'Step 4'],
25 | datasets: [
26 | {
27 | data: [0.7, 0.66, 0.61, 0.01],
28 | align: 'left',
29 | },
30 | ],
31 | },
32 | plugins: [ChartDataLabels],
33 | };
34 | // #endregion left
35 |
36 | // #region right
37 | export const right: ChartConfiguration<'funnel'> = {
38 | type: 'funnel',
39 | data: {
40 | labels: ['Step 1', 'Step 2', 'Step 3', 'Step 4'],
41 | datasets: [
42 | {
43 | data: [0.7, 0.66, 0.61, 0.01],
44 | align: 'right',
45 | },
46 | ],
47 | },
48 | plugins: [ChartDataLabels],
49 | };
50 | // #endregion right
51 |
--------------------------------------------------------------------------------
/.github/workflows/deploy_website.yml:
--------------------------------------------------------------------------------
1 | name: Deploy Website
2 | on:
3 | workflow_dispatch: {}
4 | push:
5 | branches:
6 | - main
7 | jobs:
8 | deploy:
9 | runs-on: ubuntu-latest
10 | permissions:
11 | pages: write
12 | id-token: write
13 | environment:
14 | name: github-pages
15 | url: ${{ steps.deployment.outputs.page_url }}
16 | steps:
17 | - uses: actions/checkout@v4
18 | with:
19 | fetch-depth: 0
20 | - uses: actions/setup-node@v4
21 | with:
22 | node-version: 20
23 | cache: npm
24 | - run: npm i -g yarn
25 | - run: yarn config set checksumBehavior ignore
26 | - name: Cache Node.js modules
27 | uses: actions/cache@v4
28 | with:
29 | path: |
30 | ./.yarn/cache
31 | ./.yarn/unplugged
32 | key: ${{ runner.os }}-yarn2-v5-${{ hashFiles('**/yarn.lock') }}
33 | restore-keys: |
34 | ${{ runner.os }}-yarn2-v5
35 | - run: yarn install
36 | - run: yarn docs:build
37 | - uses: actions/configure-pages@v5
38 | - uses: actions/upload-pages-artifact@v3
39 | with:
40 | path: docs/.vitepress/dist
41 | - name: Deploy
42 | id: deployment
43 | uses: actions/deploy-pages@v4
44 |
--------------------------------------------------------------------------------
/docs/examples/basic.ts:
--------------------------------------------------------------------------------
1 | import type { ChartConfiguration } from 'chart.js';
2 | import type {} from '../../src';
3 | import ChartDataLabels from 'chartjs-plugin-datalabels';
4 |
5 | // #region config
6 | export const config: ChartConfiguration<'funnel'> = {
7 | type: 'funnel',
8 | data: {
9 | labels: ['Step 1', 'Step 2', 'Step 3', 'Step 4'],
10 | datasets: [
11 | {
12 | data: [0.7, 0.66, 0.61, 0.01],
13 | datalabels: {
14 | // anchor: (context) => {
15 | // const value = context.dataset.data[context.dataIndex];
16 | // return value < 0.02 ? 'end': 'start';
17 | // },
18 | // align: (context) => {
19 | // const value = context.dataset.data[context.dataIndex];
20 | // return value < 0.02 ? 'end': 'center';
21 | // },
22 | // textAlign: 'center',
23 | // // color: (context) => {
24 | // // return '#FFCE56';
25 | // // },
26 | // font: {
27 | // size: 20,
28 | // },
29 | // formatter: (value, context) => {
30 | // const label = context.chart.data.labels[context.dataIndex];
31 | // return `${label}\n${(value * 100).toLocaleString()}%`;
32 | // }
33 | },
34 | // backgroundColor: ChartFunnel.schemeBlues[9],
35 | },
36 | ],
37 | },
38 | options: {
39 | indexAxis: 'y',
40 | },
41 | plugins: [ChartDataLabels],
42 | };
43 | // #endregion config
44 |
--------------------------------------------------------------------------------
/.github/workflows/create_release.yml:
--------------------------------------------------------------------------------
1 | name: Create Release
2 | on:
3 | workflow_dispatch:
4 | inputs:
5 | versionName:
6 | description: 'Semantic Version Number (i.e., 5.5.0 or patch, minor, major, prepatch, preminor, premajor, prerelease)'
7 | required: true
8 | default: patch
9 | preid:
10 | description: 'Pre Release Identifier (i.e., alpha, beta)'
11 | required: true
12 | default: alpha
13 | jobs:
14 | create_release:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - name: Check out code
18 | uses: actions/checkout@v4
19 | with:
20 | ref: main
21 | ssh-key: ${{ secrets.PRIVATE_SSH_KEY }}
22 | - name: Reset main branch
23 | run: |
24 | git fetch origin dev:dev
25 | git reset --hard origin/dev
26 | - name: Change version number
27 | id: version
28 | run: |
29 | echo "next_tag=$(npm version --no-git-tag-version ${{ github.event.inputs.versionName }} --preid ${{ github.event.inputs.preid }})" >> $GITHUB_OUTPUT
30 | - name: Create pull request into main
31 | uses: peter-evans/create-pull-request@v7
32 | with:
33 | branch: release/${{ steps.version.outputs.next_tag }}
34 | commit-message: 'chore: release ${{ steps.version.outputs.next_tag }}'
35 | base: main
36 | title: Release ${{ steps.version.outputs.next_tag }}
37 | labels: chore
38 | assignees: sgratzl
39 | body: |
40 | Releasing ${{ steps.version.outputs.next_tag }}.
41 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import eslint from '@eslint/js';
4 | import tseslint from 'typescript-eslint';
5 | import prettier from 'eslint-plugin-prettier';
6 |
7 | export default tseslint.config(eslint.configs.recommended, ...tseslint.configs.recommended, {
8 | plugins: { prettier },
9 | rules: {
10 | '@typescript-eslint/no-explicit-any': 'off',
11 | 'max-classes-per-file': 'off',
12 | 'no-underscore-dangle': 'off',
13 | 'import/extensions': 'off',
14 | },
15 | });
16 |
17 | // import path from "node:path";
18 | // import { fileURLToPath } from "node:url";
19 | // import js from "@eslint/js";
20 | // import { FlatCompat } from "@eslint/eslintrc";
21 |
22 | // const __filename = fileURLToPath(import.meta.url);
23 | // const __dirname = path.dirname(__filename);
24 | // const compat = new FlatCompat({
25 | // baseDirectory: __dirname,
26 | // recommendedConfig: js.configs.recommended,
27 | // allConfig: js.configs.all
28 | // });
29 |
30 | // export default [...fixupConfigRules(compat.extends(
31 | // "airbnb-typescript",
32 | // "react-app",
33 | // "plugin:prettier/recommended",
34 | // "prettier",
35 | // )), {
36 | // plugins: {
37 | // prettier: fixupPluginRules(prettier),
38 | // },
39 |
40 | // languageOptions: {
41 | // ecmaVersion: 5,
42 | // sourceType: "script",
43 |
44 | // parserOptions: {
45 | // project: "./tsconfig.eslint.json",
46 | // },
47 | // },
48 |
49 | // settings: {
50 | // react: {
51 | // version: "99.99.99",
52 | // },
53 | // },
54 |
55 | // rules: {
56 | // "@typescript-eslint/no-explicit-any": "off",
57 | // "max-classes-per-file": "off",
58 | // "no-underscore-dangle": "off",
59 | // "import/extensions": "off",
60 | // },
61 | // }];
62 |
--------------------------------------------------------------------------------
/docs/.vitepress/config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitepress';
2 | import { name, description, repository, license, author } from '../../package.json';
3 | import typedocSidebar from '../api/typedoc-sidebar.json';
4 |
5 | const cleanName = name.replace('@sgratzl/', '');
6 |
7 | // https://vitepress.dev/reference/site-config
8 | export default defineConfig({
9 | title: cleanName,
10 | description,
11 | base: `/${cleanName}/`,
12 | useWebFonts: false,
13 | themeConfig: {
14 | // https://vitepress.dev/reference/default-theme-config
15 | nav: [
16 | { text: 'Home', link: '/' },
17 | { text: 'Getting Started', link: '/getting-started' },
18 | { text: 'Examples', link: '/examples/' },
19 | { text: 'API', link: '/api/' },
20 | { text: 'Related Plugins', link: '/related' },
21 | ],
22 |
23 | sidebar: [
24 | {
25 | text: 'Examples',
26 | items: [
27 | { text: 'Basic', link: '/examples/' },
28 | { text: 'Funnel Alignment', link: '/examples/align' },
29 | { text: 'Shrink Options', link: '/examples/shrink' },
30 | { text: 'Gradient', link: '/examples/gradient' },
31 | { text: 'Hierarchical Funnel', link: '/examples/hierarchical' },
32 | { text: 'Horizontal Funnel', link: '/examples/horizontal' },
33 | { text: 'Direct Numbers', link: '/examples/numbers' },
34 | ],
35 | },
36 | {
37 | text: 'API',
38 | collapsed: true,
39 | items: typedocSidebar,
40 | },
41 | ],
42 |
43 | socialLinks: [{ icon: 'github', link: repository.url.replace('.git', '') }],
44 |
45 | footer: {
46 | message: `Released under the ${license} license.`,
50 | copyright: `Copyright © 2019-present ${author.name}`,
51 | },
52 |
53 | editLink: {
54 | pattern: `${repository.url.replace('.git', '')}/edit/main/docs/:path`,
55 | },
56 |
57 | search: {
58 | provider: 'local',
59 | },
60 | },
61 | });
62 |
--------------------------------------------------------------------------------
/src/__tests__/createChart.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import { expect } from 'vitest';
4 | import { Chart, ChartConfiguration, defaults, ChartType, DefaultDataPoint } from 'chart.js';
5 | import { toMatchImageSnapshot, MatchImageSnapshotOptions } from 'jest-image-snapshot';
6 | import 'canvas-5-polyfill';
7 |
8 | expect.extend({ toMatchImageSnapshot });
9 |
10 | function toBuffer(canvas: HTMLCanvasElement) {
11 | return new Promise((resolve) => {
12 | canvas.toBlob((b) => {
13 | const file = new FileReader();
14 | file.onload = () => resolve(Buffer.from(file.result as ArrayBuffer));
15 |
16 | file.readAsArrayBuffer(b!);
17 | });
18 | });
19 | }
20 |
21 | export async function expectMatchSnapshot(canvas: HTMLCanvasElement): Promise {
22 | const image = await toBuffer(canvas);
23 | expect(image).toMatchImageSnapshot();
24 | }
25 |
26 | export interface ChartHelper, LABEL = string> {
27 | chart: Chart;
28 | canvas: HTMLCanvasElement;
29 | ctx: CanvasRenderingContext2D;
30 | toMatchImageSnapshot(options?: MatchImageSnapshotOptions): Promise;
31 | }
32 |
33 | export default function createChart<
34 | TYPE extends ChartType,
35 | DATA extends unknown[] = DefaultDataPoint,
36 | LABEL = string,
37 | >(config: ChartConfiguration, width = 800, height = 600): ChartHelper {
38 | const canvas = document.createElement('canvas');
39 | canvas.width = width;
40 | canvas.height = height;
41 | Object.assign(defaults.font, { family: 'Courier New' });
42 | // defaults.color = 'transparent';
43 |
44 | config.options = {
45 | responsive: false,
46 | animation: {
47 | duration: 1,
48 | },
49 | plugins: {
50 | legend: {
51 | display: false,
52 | },
53 | title: {
54 | display: false,
55 | },
56 | },
57 | ...(config.options || {}),
58 | } as any;
59 |
60 | const ctx = canvas.getContext('2d')!;
61 |
62 | const t = new Chart(ctx, config);
63 |
64 | return {
65 | chart: t,
66 | canvas,
67 | ctx,
68 | async toMatchImageSnapshot(options?: MatchImageSnapshotOptions) {
69 | await new Promise((resolve) => setTimeout(resolve, 100));
70 |
71 | const image = await toBuffer(canvas);
72 | expect(image).toMatchImageSnapshot(options);
73 | },
74 | };
75 | }
76 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # These settings are for any web project
2 |
3 | # Handle line endings automatically for files detected as text
4 | # and leave all files detected as binary untouched.
5 | * text=auto eol=lf
6 |
7 | #
8 | # The above will handle all files NOT found below
9 | #
10 |
11 | #
12 | ## These files are text and should be normalized (Convert crlf => lf)
13 | #
14 |
15 | # source code
16 | *.php text
17 | *.css text
18 | *.sass text
19 | *.scss text
20 | *.less text
21 | *.styl text
22 | *.js text
23 | *.jsx text
24 | *.ts text
25 | *.tsx text
26 | *.coffee text
27 | *.json text
28 | *.htm text
29 | *.html text
30 | *.xml text
31 | *.txt text
32 | *.ini text
33 | *.inc text
34 | *.pl text
35 | *.rb text
36 | *.py text
37 | *.scm text
38 | *.sql text
39 | *.sh text eof=LF
40 | *.bat text
41 |
42 | # templates
43 | *.hbt text
44 | *.jade text
45 | *.haml text
46 | *.hbs text
47 | *.dot text
48 | *.tmpl text
49 | *.phtml text
50 |
51 | # server config
52 | .htaccess text
53 |
54 | # git config
55 | .gitattributes text
56 | .gitignore text
57 |
58 | # code analysis config
59 | .jshintrc text
60 | .jscsrc text
61 | .jshintignore text
62 | .csslintrc text
63 |
64 | # misc config
65 | *.yaml text
66 | *.yml text
67 | .editorconfig text
68 |
69 | # build config
70 | *.npmignore text
71 | *.bowerrc text
72 | Dockerfile text eof=LF
73 |
74 | # Heroku
75 | Procfile text
76 | .slugignore text
77 |
78 | # Documentation
79 | *.md text
80 | LICENSE text
81 | AUTHORS text
82 |
83 |
84 | #
85 | ## These files are binary and should be left untouched
86 | #
87 |
88 | # (binary is a macro for -text -diff)
89 | *.png binary
90 | *.jpg binary
91 | *.jpeg binary
92 | *.gif binary
93 | *.ico binary
94 | *.mov binary
95 | *.mp4 binary
96 | *.mp3 binary
97 | *.flv binary
98 | *.fla binary
99 | *.swf binary
100 | *.gz binary
101 | *.zip binary
102 | *.7z binary
103 | *.ttf binary
104 | *.pyc binary
105 | *.pdf binary
106 |
107 | # Source files
108 | # ============
109 | *.pxd text
110 | *.py text
111 | *.py3 text
112 | *.pyw text
113 | *.pyx text
114 | *.sh text eol=lf
115 | *.json text
116 |
117 | # Binary files
118 | # ============
119 | *.db binary
120 | *.p binary
121 | *.pkl binary
122 | *.pyc binary
123 | *.pyd binary
124 | *.pyo binary
125 |
126 | # Note: .db, .p, and .pkl files are associated
127 | # with the python modules ``pickle``, ``dbm.*``,
128 | # ``shelve``, ``marshal``, ``anydbm``, & ``bsddb``
129 | # (among others).
130 |
--------------------------------------------------------------------------------
/docs/examples/shrink.ts:
--------------------------------------------------------------------------------
1 | import type { ChartConfiguration } from 'chart.js';
2 | import type {} from '../../src';
3 | import ChartDataLabels from 'chartjs-plugin-datalabels';
4 |
5 | // #region top
6 | export const top: ChartConfiguration<'funnel'> = {
7 | type: 'funnel',
8 | data: {
9 | labels: ['Step 1', 'Step 2', 'Step 3', 'Step 4'],
10 | datasets: [
11 | {
12 | data: [0.7, 0.66, 0.61, 0.01],
13 | shrinkAnchor: 'top',
14 | },
15 | ],
16 | },
17 | plugins: [ChartDataLabels],
18 | };
19 | // #endregion top
20 |
21 | // #region top5
22 | export const top5: ChartConfiguration<'funnel'> = {
23 | type: 'funnel',
24 | data: {
25 | labels: ['Step 1', 'Step 2', 'Step 3', 'Step 4'],
26 | datasets: [
27 | {
28 | data: [0.7, 0.66, 0.61, 0.01],
29 | shrinkAnchor: 'top',
30 | shrinkFraction: 0.5,
31 | },
32 | ],
33 | },
34 | plugins: [ChartDataLabels],
35 | };
36 | // #endregion top5
37 |
38 | // #region top25
39 | export const top25: ChartConfiguration<'funnel'> = {
40 | type: 'funnel',
41 | data: {
42 | labels: ['Step 1', 'Step 2', 'Step 3', 'Step 4'],
43 | datasets: [
44 | {
45 | data: [0.7, 0.66, 0.61, 0.01],
46 | shrinkAnchor: 'top',
47 | shrinkFraction: 0.25,
48 | },
49 | ],
50 | },
51 | plugins: [ChartDataLabels],
52 | };
53 | // #endregion top25
54 |
55 | // #region middle
56 | export const middle: ChartConfiguration<'funnel'> = {
57 | type: 'funnel',
58 | data: {
59 | labels: ['Step 1', 'Step 2', 'Step 3', 'Step 4'],
60 | datasets: [
61 | {
62 | data: [0.7, 0.66, 0.61, 0.01],
63 | shrinkAnchor: 'middle',
64 | },
65 | ],
66 | },
67 | plugins: [ChartDataLabels],
68 | };
69 | // #endregion middle
70 |
71 | // #region bottom
72 | export const bottom: ChartConfiguration<'funnel'> = {
73 | type: 'funnel',
74 | data: {
75 | labels: ['Step 1', 'Step 2', 'Step 3', 'Step 4'],
76 | datasets: [
77 | {
78 | data: [0.7, 0.66, 0.61, 0.01],
79 | shrinkAnchor: 'bottom',
80 | },
81 | ],
82 | },
83 | plugins: [ChartDataLabels],
84 | };
85 | // #endregion bottom
86 |
87 | // #region none
88 | export const none: ChartConfiguration<'funnel'> = {
89 | type: 'funnel',
90 | data: {
91 | labels: ['Step 1', 'Step 2', 'Step 3', 'Step 4'],
92 | datasets: [
93 | {
94 | data: [0.7, 0.66, 0.61, 0.01],
95 | shrinkAnchor: 'none',
96 | },
97 | ],
98 | },
99 | plugins: [ChartDataLabels],
100 | };
101 | // #endregion none
102 |
--------------------------------------------------------------------------------
/.github/workflows/release_helper.yml:
--------------------------------------------------------------------------------
1 | name: Release Helper
2 | on:
3 | push:
4 | branches:
5 | - main
6 |
7 | jobs:
8 | correct_repository:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: fail on fork
12 | if: github.repository_owner != 'sgratzl'
13 | run: exit 1
14 |
15 | create_release:
16 | needs: correct_repository
17 | runs-on: ubuntu-latest
18 | steps:
19 | - name: Check out code
20 | uses: actions/checkout@v4
21 | - uses: actions/setup-node@v4
22 | with:
23 | node-version: 20
24 | - name: Extract version
25 | id: extract_version
26 | run: |
27 | node -pe "'version=' + require('./package.json').version" >> $GITHUB_OUTPUT
28 | node -pe "'npm_tag=' + (require('./package.json').version.includes('-') ? 'next' : 'latest')" >> $GITHUB_OUTPUT
29 | - name: Print version
30 | run: |
31 | echo "releasing ${{ steps.extract_version.outputs.version }} with tag ${{ steps.extract_version.outputs.npm_tag }}"
32 | - name: Create Release
33 | id: create_release
34 | uses: release-drafter/release-drafter@v6
35 | env:
36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
37 | with:
38 | name: v${{ steps.extract_version.outputs.version }}
39 | tag: v${{ steps.extract_version.outputs.version }}
40 | version: ${{ steps.extract_version.outputs.version }}
41 | prerelease: ${{ needs.create_release.outputs.tag_name == 'next' }}
42 | publish: true
43 | outputs:
44 | version: ${{ steps.extract_version.outputs.version }}
45 | npm_tag: ${{ steps.extract_version.outputs.npm_tag }}
46 | upload_url: ${{ steps.create_release.outputs.upload_url }}
47 | tag_name: ${{ steps.create_release.outputs.tag_name }}
48 |
49 | build_assets:
50 | needs: create_release
51 | runs-on: ubuntu-latest
52 | steps:
53 | - name: Check out code
54 | uses: actions/checkout@v4
55 | - uses: actions/setup-node@v4
56 | with:
57 | node-version: 20
58 | - run: npm i -g yarn
59 | - run: yarn config set checksumBehavior ignore
60 | - name: Cache Node.js modules
61 | uses: actions/cache@v4
62 | with:
63 | path: |
64 | ./.yarn/cache
65 | ./.yarn/unplugged
66 | key: ${{ runner.os }}-yarn2-v5-${{ hashFiles('**/yarn.lock') }}
67 | restore-keys: |
68 | ${{ runner.os }}-yarn2-v5
69 | - run: yarn install
70 | - run: yarn build
71 | - run: yarn pack
72 | - name: Upload Release Asset
73 | uses: AButler/upload-release-assets@v3.0
74 | with:
75 | files: 'package.tgz'
76 | repo-token: ${{ secrets.GITHUB_TOKEN }}
77 | release-tag: ${{ needs.create_release.outputs.tag_name }}
78 | - name: Pack Publish
79 | run: |
80 | yarn config set npmAuthToken "${{ secrets.NPM_TOKEN }}"
81 | yarn pack
82 | yarn npm publish --tag "${{ needs.create_release.outputs.npm_tag }}"
83 |
84 | sync_dev:
85 | needs: correct_repository
86 | runs-on: ubuntu-latest
87 | steps:
88 | - name: Check out code
89 | uses: actions/checkout@v4
90 | with:
91 | ref: dev
92 | ssh-key: ${{ secrets.PRIVATE_SSH_KEY }}
93 | - name: Reset dev branch
94 | run: |
95 | git fetch origin main:main
96 | git merge main
97 | git push
98 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chartjs-chart-funnel",
3 | "description": "Chart.js module for charting funnel charts",
4 | "version": "4.2.5",
5 | "author": {
6 | "name": "Samuel Gratzl",
7 | "email": "samu@sgratzl.com",
8 | "url": "https://www.sgratzl.com"
9 | },
10 | "license": "MIT",
11 | "homepage": "https://github.com/sgratzl/chartjs-chart-funnel",
12 | "bugs": {
13 | "url": "https://github.com/sgratzl/chartjs-chart-funnel/issues"
14 | },
15 | "keywords": [
16 | "chart.js",
17 | "funnel"
18 | ],
19 | "repository": {
20 | "type": "git",
21 | "url": "https://github.com/sgratzl/chartjs-chart-funnel.git"
22 | },
23 | "global": "ChartFunnel",
24 | "type": "module",
25 | "main": "build/index.js",
26 | "module": "build/index.js",
27 | "require": "build/index.cjs",
28 | "umd": "build/index.umd.js",
29 | "unpkg": "build/index.umd.min.js",
30 | "jsdelivr": "build/index.umd.min.js",
31 | "types": "build/index.d.ts",
32 | "exports": {
33 | ".": {
34 | "import": "./build/index.js",
35 | "require": "./build/index.cjs",
36 | "scripts": "./build/index.umd.min.js",
37 | "types": "./build/index.d.ts"
38 | }
39 | },
40 | "sideEffects": false,
41 | "files": [
42 | "build",
43 | "src/**/*.ts",
44 | "c"
45 | ],
46 | "peerDependencies": {
47 | "chart.js": ">=3.7.0"
48 | },
49 | "browserslist": [
50 | "Firefox ESR",
51 | "last 2 Chrome versions",
52 | "last 2 Firefox versions"
53 | ],
54 | "devDependencies": {
55 | "@chiogen/rollup-plugin-terser": "^7.1.3",
56 | "@eslint/js": "^9.37.0",
57 | "@rollup/plugin-commonjs": "^28.0.6",
58 | "@rollup/plugin-node-resolve": "^16.0.2",
59 | "@rollup/plugin-replace": "^6.0.2",
60 | "@rollup/plugin-typescript": "^12.1.4",
61 | "@types/jest-image-snapshot": "^6.4.0",
62 | "@types/node": "^24.6.2",
63 | "@yarnpkg/sdks": "^3.2.3",
64 | "canvas": "^3.2.0",
65 | "canvas-5-polyfill": "^0.1.5",
66 | "chart.js": "^4.5.0",
67 | "chartjs-plugin-datalabels": "^2.2.0",
68 | "chartjs-plugin-hierarchical": "^4.4.4",
69 | "eslint": "^9.37.0",
70 | "eslint-plugin-prettier": "^5.5.4",
71 | "jest-image-snapshot": "^6.5.1",
72 | "jsdom": "^27.0.0",
73 | "prettier": "^3.6.2",
74 | "rimraf": "^6.0.1",
75 | "rollup": "^4.52.4",
76 | "rollup-plugin-cleanup": "^3.2.1",
77 | "rollup-plugin-dts": "^6.2.3",
78 | "ts-jest": "^29.4.4",
79 | "tslib": "^2.8.1",
80 | "typedoc": "^0.28.13",
81 | "typedoc-plugin-markdown": "^4.9.0",
82 | "typedoc-vitepress-theme": "^1.1.2",
83 | "typescript": "^5.9.3",
84 | "typescript-eslint": "^8.45.0",
85 | "vite": "^7.1.9",
86 | "vitepress": "^1.6.4",
87 | "vitest": "^3.2.4",
88 | "vue": "^3.5.22",
89 | "vue-chartjs": "^5.3.2"
90 | },
91 | "scripts": {
92 | "clean": "rimraf --glob build node_modules \"*.tgz\" \"*.tsbuildinfo\"",
93 | "compile": "tsc -b tsconfig.c.json",
94 | "start": "yarn run watch",
95 | "watch": "rollup -c -w",
96 | "build": "rollup -c",
97 | "test": "vitest --passWithNoTests",
98 | "test:watch": "yarn run test --watch",
99 | "test:coverage": "yarn run test --coverage",
100 | "lint": "yarn run eslint && yarn run prettier",
101 | "fix": "yarn run eslint:fix && yarn run prettier:write",
102 | "prettier:write": "prettier \"*\" \"*/**\" --write",
103 | "prettier": "prettier \"*\" \"*/**\" --check",
104 | "eslint": "eslint src --cache",
105 | "eslint:fix": "yarn run eslint --fix",
106 | "prepare": "yarn run build",
107 | "docs:api": "typedoc --options typedoc.json",
108 | "docs:dev": "vitepress dev docs",
109 | "docs:build": "yarn run docs:api && vitepress build docs",
110 | "docs:preview": "vitepress preview docs"
111 | },
112 | "packageManager": "yarn@4.10.3",
113 | "dependencies": {
114 | "@types/chroma-js": "^3.1.1",
115 | "chroma-js": "^3.1.2"
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import commonjs from '@rollup/plugin-commonjs';
2 | import resolve from '@rollup/plugin-node-resolve';
3 | import cleanup from 'rollup-plugin-cleanup';
4 | import dts from 'rollup-plugin-dts';
5 | import typescript from '@rollup/plugin-typescript';
6 | import { terser } from '@chiogen/rollup-plugin-terser';
7 | import replace from '@rollup/plugin-replace';
8 |
9 | import fs from 'fs';
10 |
11 | const pkg = JSON.parse(fs.readFileSync('./package.json'));
12 |
13 | function resolveYear() {
14 | // Extract copyrights from the LICENSE.
15 | const license = fs.readFileSync('./LICENSE', 'utf-8').toString();
16 | const matches = Array.from(license.matchAll(/\(c\) (\d+-\d+)/gm));
17 | if (!matches || matches.length === 0) {
18 | return 2021;
19 | }
20 | return matches[matches.length - 1][1];
21 | }
22 | const year = resolveYear();
23 |
24 | const banner = `/**
25 | * ${pkg.name}
26 | * ${pkg.homepage}
27 | *
28 | * Copyright (c) ${year} ${pkg.author.name} <${pkg.author.email}>
29 | */
30 | `;
31 |
32 | /**
33 | * defines which formats (umd, esm, cjs, types) should be built when watching
34 | */
35 | const watchOnly = ['umd'];
36 |
37 | const isDependency = (v) => Object.keys(pkg.dependencies || {}).some((e) => e === v || v.startsWith(`${e}/`));
38 | const isPeerDependency = (v) => Object.keys(pkg.peerDependencies || {}).some((e) => e === v || v.startsWith(`${e}/`));
39 |
40 | export default function Config(options) {
41 | const buildFormat = (format) => {
42 | return !options.watch || watchOnly.includes(format);
43 | };
44 | const commonOutput = {
45 | sourcemap: true,
46 | banner,
47 | globals: {
48 | 'chart.js': 'Chart',
49 | 'chart.js/helpers': 'Chart.helpers',
50 | 'chroma-js': 'chroma',
51 | },
52 | };
53 |
54 | const base = {
55 | input: './src/index.ts',
56 | external: (v) => isDependency(v) || isPeerDependency(v),
57 | plugins: [
58 | typescript(),
59 | resolve({
60 | mainFields: ['module', 'main'],
61 | extensions: ['.mjs', '.cjs', '.js', '.jsx', '.json', '.node'],
62 | // modulesOnly: true,
63 | }),
64 | commonjs(),
65 | replace({
66 | preventAssignment: true,
67 | values: {
68 | // eslint-disable-next-line no-undef
69 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) || 'production',
70 | __VERSION__: JSON.stringify(pkg.version),
71 | },
72 | }),
73 | cleanup({
74 | comments: ['some', 'ts', 'ts3s'],
75 | extensions: ['ts', 'tsx', 'js', 'jsx'],
76 | include: './src/**/*',
77 | }),
78 | ],
79 | };
80 | return [
81 | buildFormat('esm') && {
82 | ...base,
83 | output: {
84 | ...commonOutput,
85 | file: pkg.module,
86 | format: 'esm',
87 | },
88 | },
89 | buildFormat('cjs') && {
90 | ...base,
91 | output: {
92 | ...commonOutput,
93 | file: pkg.require,
94 | format: 'cjs',
95 | },
96 | external: (v) => (isDependency(v) || isPeerDependency(v)) && ['d3-'].every((di) => !v.includes(di)),
97 | },
98 | (buildFormat('umd') || buildFormat('umd-min')) && {
99 | ...base,
100 | input: './src/index.umd.ts',
101 | output: [
102 | buildFormat('umd') && {
103 | ...commonOutput,
104 | file: pkg.umd,
105 | format: 'umd',
106 | name: pkg.global,
107 | },
108 | buildFormat('umd-min') && {
109 | ...commonOutput,
110 | file: pkg.unpkg,
111 | format: 'umd',
112 | name: pkg.global,
113 | plugins: [terser()],
114 | },
115 | ].filter(Boolean),
116 | external: (v) => isPeerDependency(v),
117 | },
118 | buildFormat('types') && {
119 | ...base,
120 | output: {
121 | ...commonOutput,
122 | file: pkg.types,
123 | format: 'es',
124 | },
125 | plugins: [
126 | dts({
127 | respectExternal: true,
128 | compilerOptions: {
129 | skipLibCheck: true,
130 | skipDefaultLibCheck: true,
131 | },
132 | }),
133 | ],
134 | },
135 | ].filter(Boolean);
136 | }
137 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Chart.js Funnel
2 |
3 | [![License: MIT][mit-image]][mit-url] [![NPM Package][npm-image]][npm-url] [![Github Actions][github-actions-image]][github-actions-url]
4 |
5 | Chart.js module for charting funnel plots. This plugin extends with a new char type `funnel`.
6 |
7 | A Funnel chart is a variant of a bar chart where the bar shrinks on one side to the size of the next bar. In addition, they are usually centered giving the visual impression of a funnel.
8 |
9 | 
10 |
11 | Works great with https://github.com/chartjs/chartjs-plugin-datalabels
12 |
13 | 
14 |
15 | ## Related Plugins
16 |
17 | Check out also my other chart.js plugins:
18 |
19 | - [chartjs-chart-boxplot](https://github.com/sgratzl/chartjs-chart-boxplot) for rendering boxplots and violin plots
20 | - [chartjs-chart-error-bars](https://github.com/sgratzl/chartjs-chart-error-bars) for rendering errors bars to bars and line charts
21 | - [chartjs-chart-geo](https://github.com/sgratzl/chartjs-chart-geo) for rendering map, bubble maps, and choropleth charts
22 | - [chartjs-chart-graph](https://github.com/sgratzl/chartjs-chart-graph) for rendering graphs, trees, and networks
23 | - [chartjs-chart-pcp](https://github.com/sgratzl/chartjs-chart-pcp) for rendering parallel coordinate plots
24 | - [chartjs-chart-venn](https://github.com/sgratzl/chartjs-chart-venn) for rendering venn and euler diagrams
25 | - [chartjs-chart-wordcloud](https://github.com/sgratzl/chartjs-chart-wordcloud) for rendering word clouds
26 | - [chartjs-plugin-hierarchical](https://github.com/sgratzl/chartjs-plugin-hierarchical) for rendering hierarchical categorical axes which can be expanded and collapsed
27 |
28 | ## Install
29 |
30 | ```bash
31 | npm install chart.js chartjs-chart-funnel
32 | ```
33 |
34 | ## Usage
35 |
36 | see [Examples](https://www.sgratzl.com/chartjs-chart-funnel/examples/)
37 |
38 | and [![Open in CodePen][codepen]](https://codepen.io/sgratzl/pen/eYjEXQW)
39 |
40 | ## Styling
41 |
42 | Trapezoid Elements are Bar elements and provide the same coloring options. In addition, see [TrapezoidElementOptions](https://github.com/sgratzl/chartjs-chart-funnel/blob/main/src/elements/TrapezoidElement.tjs#L11-L27) custom option with respect to shrinking behavior.
43 |
44 | In addition, the FunnelController has the following options [FunnelController](https://github.com/sgratzl/chartjs-chart-funnel/blob/main/src/controllers/FunnelController.tjs#L24-L30) to customize the alignment of the chart.
45 |
46 | ### ESM and Tree Shaking
47 |
48 | The ESM build of the library supports tree shaking thus having no side effects. As a consequence the chart.js library won't be automatically manipulated nor new controllers automatically registered. One has to manually import and register them.
49 |
50 | Variant A:
51 |
52 | ```js
53 | import Chart, { LinearScale, CategoryScale } from 'chart.js';
54 | import { FunnelController, TrapezoidElement } from 'chartjs-chart-funnel';
55 |
56 | // register controller in chart.js and ensure the defaults are set
57 | Chart.register(FunnelController, TrapezoidElement, LinearScale, CategoryScale);
58 |
59 | const chart = new Chart(document.getElementById('canvas').getContext('2d'), {
60 | type: 'funnel',
61 | data: {
62 | labels: ['Step 1', 'Step 2', 'Step 3', 'Step 4'],
63 | datasets: [
64 | {
65 | data: [0.7, 0.66, 0.61, 0.01],
66 | },
67 | ],
68 | },
69 | });
70 | ```
71 |
72 | Variant B:
73 |
74 | ```js
75 | import { FunnelChart } from 'chartjs-chart-funnel';
76 |
77 | const chart = new FunnelChart(document.getElementById('canvas').getContext('2d'), {
78 | data: {
79 | //...
80 | },
81 | });
82 | ```
83 |
84 | ## Development Environment
85 |
86 | ```sh
87 | npm i -g yarn
88 | yarn install
89 | yarn sdks vscode
90 | ```
91 |
92 | ### Building
93 |
94 | ```sh
95 | yarn install
96 | yarn build
97 | ```
98 |
99 | [mit-image]: https://img.shields.io/badge/License-MIT-yellow.svg
100 | [mit-url]: https://opensource.org/licenses/MIT
101 | [npm-image]: https://badge.fury.io/js/chartjs-chart-funnel.svg
102 | [npm-url]: https://npmjs.org/package/chartjs-chart-funnel
103 | [github-actions-image]: https://github.com/sgratzl/chartjs-chart-funnel/workflows/ci/badge.svg
104 | [github-actions-url]: https://github.com/sgratzl/chartjs-chart-funnel/actions
105 | [codepen]: https://img.shields.io/badge/CodePen-open-blue?logo=codepen
106 |
--------------------------------------------------------------------------------
/src/controllers/FunnelController.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Chart,
3 | ChartItem,
4 | ScriptableAndArrayOptions,
5 | ControllerDatasetOptions,
6 | CommonHoverOptions,
7 | UpdateMode,
8 | ChartConfiguration,
9 | Scale,
10 | ScriptableContext,
11 | CategoryScale,
12 | BarController,
13 | CoreChartOptions,
14 | CartesianScaleTypeRegistry,
15 | LinearScale,
16 | } from 'chart.js';
17 | import { merge } from 'chart.js/helpers';
18 | import { pickForegroundColorToBackgroundColor, blues } from '../color';
19 | import { TrapezoidElement, TrapezoidElementOptions } from '../elements';
20 | import patchController from './patchController';
21 |
22 | export interface FunnelChartOptions {
23 | /**
24 | * alignment of the elements one of center, left, right
25 | * @default center
26 | */
27 | align: 'center' | 'left' | 'right';
28 | }
29 |
30 | export class FunnelController extends BarController {
31 | /**
32 | * @hidden
33 | */
34 | declare options: FunnelChartOptions;
35 |
36 | static readonly id: string = 'funnel';
37 |
38 | /**
39 | * @hidden
40 | */
41 | static readonly defaults: any = /* #__PURE__ */ merge({}, [
42 | BarController.defaults,
43 | {
44 | dataElementType: TrapezoidElement.id,
45 | barPercentage: 1,
46 | align: 'center',
47 | categoryPercentage: 0.98,
48 | },
49 | ]);
50 |
51 | /**
52 | * @hidden
53 | */
54 | static readonly overrides: any = /* #__PURE__ */ merge({}, [
55 | (BarController as any).overrides,
56 | {
57 | plugins: {
58 | legend: {
59 | display: false,
60 | },
61 | colors: {
62 | enabled: false,
63 | },
64 | datalabels: {
65 | anchor: 'start',
66 | // align: 'start',
67 | textAlign: 'center',
68 | font: {
69 | size: 20,
70 | },
71 | color: (context: {
72 | chart: Chart<'funnel'>;
73 | dataset: Chart['data']['datasets'][0];
74 | dataIndex: number;
75 | datasetIndex: number;
76 | }) => {
77 | // auto pick color based on background color
78 | const bgColor = context.chart.getDatasetMeta(context.datasetIndex).data[context.dataIndex].options
79 | .backgroundColor as string;
80 | return pickForegroundColorToBackgroundColor(bgColor, Chart.defaults.color as string, '#ffffff');
81 | },
82 | formatter: (value: number, context: { chart: Chart<'funnel'>; dataIndex: number }) => {
83 | const label = context.chart.data.labels?.[context.dataIndex] ?? '';
84 | return `${label}\n${(value * 100).toLocaleString()}%`;
85 | },
86 | },
87 | },
88 | scales: {
89 | _index_: {
90 | display: false,
91 | padding: 10,
92 | grid: {
93 | display: false,
94 | },
95 | },
96 | _value_: {
97 | display: false,
98 | beginAtZero: false,
99 | grace: 0,
100 | grid: {
101 | display: false,
102 | },
103 | },
104 | },
105 | elements: {
106 | trapezoid: {
107 | backgroundColor(context: ScriptableContext<'funnel'>) {
108 | const nData = context.chart.data.datasets[context.datasetIndex].data.length;
109 | return blues(context.dataIndex, nData);
110 | },
111 | },
112 | },
113 | },
114 | ]);
115 |
116 | /**
117 | * @hidden
118 | */
119 | getMinMax(scale: Scale, canStack?: boolean | undefined): { min: number; max: number } {
120 | const { max } = super.getMinMax(scale, canStack);
121 | const r = {
122 | // fake mirroring the scale
123 | center: { min: -max, max },
124 | left: { min: 0, max },
125 | right: { min: -max, max: 0 },
126 | }[this.options.align];
127 | return r;
128 | }
129 |
130 | /**
131 | * @hidden
132 | */
133 | update(mode: UpdateMode): void {
134 | super.update(mode);
135 | const meta = this._cachedMeta;
136 | // create a link
137 | const elements = (meta.data || []) as unknown as TrapezoidElement[];
138 | for (let i = 0; i < elements.length; i++) {
139 | elements[i].align = this.options.align;
140 | elements[i].next = elements[i + 1];
141 | elements[i].previous = elements[i - 1];
142 | }
143 | }
144 | }
145 |
146 | export interface FunnelControllerDatasetOptions
147 | extends ControllerDatasetOptions,
148 | ScriptableAndArrayOptions>,
149 | ScriptableAndArrayOptions> {}
150 |
151 | declare module 'chart.js' {
152 | interface ChartTypeRegistry {
153 | funnel: {
154 | chartOptions: FunnelChartOptions & CoreChartOptions<'funnel'>;
155 | datasetOptions: FunnelControllerDatasetOptions;
156 | defaultDataPoint: number;
157 | metaExtensions: Record;
158 | parsedDataType: { x: number; y: number };
159 | scales: keyof CartesianScaleTypeRegistry;
160 | };
161 | }
162 | }
163 |
164 | export class FunnelChart extends Chart<'funnel', DATA, LABEL> {
165 | static id = FunnelController.id as 'funnel';
166 |
167 | constructor(item: ChartItem, config: Omit, 'type'>) {
168 | super(item, patchController('funnel', config, FunnelController, TrapezoidElement, [CategoryScale, LinearScale]));
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/src/controllers/FunnelController.spec.ts:
--------------------------------------------------------------------------------
1 | import { CategoryScale, LinearScale, registry } from 'chart.js';
2 | import createChart from '../__tests__/createChart';
3 | import { FunnelController } from './FunnelController';
4 | import { TrapezoidElement } from '../elements';
5 | import { describe, beforeAll, test } from 'vitest';
6 | describe('funnel', () => {
7 | beforeAll(() => {
8 | registry.addControllers(FunnelController);
9 | registry.addElements(TrapezoidElement);
10 | registry.addScales(LinearScale, CategoryScale);
11 | });
12 | test('default', () => {
13 | const chart = createChart(
14 | {
15 | type: 'funnel',
16 | data: {
17 | labels: ['Step 1', 'Step 2', 'Step 3'],
18 | datasets: [
19 | {
20 | data: [1, 0.75, 0.5],
21 | },
22 | ],
23 | },
24 | options: {
25 | indexAxis: 'y',
26 | },
27 | },
28 | 500,
29 | 200
30 | );
31 | return chart.toMatchImageSnapshot();
32 | });
33 | test('horizontal', () => {
34 | const chart = createChart(
35 | {
36 | type: 'funnel',
37 | data: {
38 | labels: ['Step 1', 'Step 2', 'Step 3'],
39 | datasets: [
40 | {
41 | data: [1, 0.75, 0.5],
42 | },
43 | ],
44 | },
45 | options: {
46 | indexAxis: 'x',
47 | },
48 | },
49 | 500,
50 | 200
51 | );
52 | return chart.toMatchImageSnapshot();
53 | });
54 | test('numbers', () => {
55 | const chart = createChart(
56 | {
57 | type: 'funnel',
58 | data: {
59 | labels: ['Step 1', 'Step 2', 'Step 3'],
60 | datasets: [
61 | {
62 | data: [100, 75, 50],
63 | },
64 | ],
65 | },
66 | options: {
67 | indexAxis: 'y',
68 | },
69 | },
70 | 500,
71 | 200
72 | );
73 | return chart.toMatchImageSnapshot();
74 | });
75 | test.each(['top', 'bottom', 'none', 'middle'])('shrink-%s', (shrinkAnchor: string) => {
76 | const chart = createChart(
77 | {
78 | type: 'funnel',
79 | data: {
80 | labels: ['Step 1', 'Step 2', 'Step 3'],
81 | datasets: [
82 | {
83 | data: [1, 0.75, 0.5],
84 | shrinkAnchor: shrinkAnchor as 'top' | 'bottom' | 'none' | 'middle',
85 | },
86 | ],
87 | },
88 | options: {
89 | indexAxis: 'y',
90 | },
91 | },
92 | 500,
93 | 200
94 | );
95 | return chart.toMatchImageSnapshot();
96 | });
97 | test.each(['top', 'bottom', 'none', 'middle'])('hor-shrink-%s', (shrinkAnchor: string) => {
98 | const chart = createChart(
99 | {
100 | type: 'funnel',
101 | data: {
102 | labels: ['Step 1', 'Step 2', 'Step 3'],
103 | datasets: [
104 | {
105 | data: [1, 0.75, 0.5],
106 | shrinkAnchor: shrinkAnchor as 'top' | 'bottom' | 'none' | 'middle',
107 | },
108 | ],
109 | },
110 | options: {
111 | indexAxis: 'x',
112 | },
113 | },
114 | 500,
115 | 200
116 | );
117 | return chart.toMatchImageSnapshot();
118 | });
119 | test.each(['0', '0.25', '0.5', '0.75', '1'])('shrink-fraction-%s', (shrinkFraction: string) => {
120 | const chart = createChart(
121 | {
122 | type: 'funnel',
123 | data: {
124 | labels: ['Step 1', 'Step 2', 'Step 3'],
125 | datasets: [
126 | {
127 | data: [1, 0.75, 0.5],
128 | shrinkFraction: +shrinkFraction,
129 | },
130 | ],
131 | },
132 | options: {
133 | indexAxis: 'y',
134 | },
135 | },
136 | 500,
137 | 200
138 | );
139 | return chart.toMatchImageSnapshot();
140 | });
141 | test.each(['0', '0.25', '0.5', '0.75', '1'])('hor-shrink-fraction-%s', (shrinkFraction: string) => {
142 | const chart = createChart(
143 | {
144 | type: 'funnel',
145 | data: {
146 | labels: ['Step 1', 'Step 2', 'Step 3'],
147 | datasets: [
148 | {
149 | data: [1, 0.75, 0.5],
150 | shrinkFraction: +shrinkFraction,
151 | },
152 | ],
153 | },
154 | options: {
155 | indexAxis: 'x',
156 | },
157 | },
158 | 500,
159 | 200
160 | );
161 | return chart.toMatchImageSnapshot();
162 | });
163 |
164 | test.each(['left', 'right', 'center'])('align-%s', (align: string) => {
165 | const chart = createChart(
166 | {
167 | type: 'funnel',
168 | data: {
169 | labels: ['Step 1', 'Step 2', 'Step 3'],
170 | datasets: [
171 | {
172 | data: [1, 0.75, 0.5],
173 | align: align as 'left' | 'right' | 'center',
174 | },
175 | ],
176 | },
177 | options: {
178 | indexAxis: 'y',
179 | },
180 | },
181 | 500,
182 | 200
183 | );
184 | return chart.toMatchImageSnapshot();
185 | });
186 | test.each(['left', 'right', 'center'])('hor-align-%s', (align: string) => {
187 | const chart = createChart(
188 | {
189 | type: 'funnel',
190 | data: {
191 | labels: ['Step 1', 'Step 2', 'Step 3'],
192 | datasets: [
193 | {
194 | data: [1, 0.75, 0.5],
195 | align: align as 'left' | 'right' | 'center',
196 | },
197 | ],
198 | },
199 | options: {
200 | indexAxis: 'x',
201 | },
202 | },
203 | 500,
204 | 200
205 | );
206 | return chart.toMatchImageSnapshot();
207 | });
208 | });
209 |
--------------------------------------------------------------------------------
/src/elements/TrapezoidElement.ts:
--------------------------------------------------------------------------------
1 | import {
2 | BarElement,
3 | CommonElementOptions,
4 | BarOptions,
5 | ScriptableAndArrayOptions,
6 | ChartType,
7 | CommonHoverOptions,
8 | ScriptableContext,
9 | } from 'chart.js';
10 |
11 | export interface TrapezoidElementOptions extends CommonElementOptions, Record {
12 | /**
13 | * Width of the border
14 | * @default 0
15 | */
16 | borderWidth: number;
17 | /**
18 | * which side of the bar to shrink: top, right, middle, none
19 | * @default top
20 | */
21 | shrinkAnchor: 'middle' | 'top' | 'bottom' | 'none';
22 | /**
23 | * fraction (0-1) of the bar that shrinks shrink. 1 all of the bar, 0 none
24 | * @default 1
25 | */
26 | shrinkFraction: number;
27 | }
28 |
29 | export interface TrapezoidElementProps {
30 | x: number;
31 | y: number;
32 | width: number;
33 | height: number;
34 | base: number;
35 | horizontal: boolean;
36 | }
37 |
38 | function inBetween(v: number, min: number, max: number, delta = 10e-6) {
39 | return v >= Math.min(min, max) - delta && v <= Math.max(min, max) + delta;
40 | }
41 |
42 | function transpose(m: { horizontal: boolean; left: number; top: number; right: number; bottom: number }) {
43 | return {
44 | left: m.top,
45 | right: m.bottom,
46 | top: m.left,
47 | bottom: m.right,
48 | horizontal: !m.horizontal,
49 | };
50 | }
51 |
52 | // need to make it a bar element for proper data label support
53 | export class TrapezoidElement extends BarElement {
54 | // implements VisualElement {
55 | static readonly id = 'trapezoid';
56 |
57 | /**
58 | * @hidden
59 | */
60 | declare options: BarOptions & TrapezoidElementOptions;
61 |
62 | /**
63 | * @hidden
64 | */
65 | static readonly defaults = /* #__PURE__ */ {
66 | ...BarElement.defaults,
67 | shrinkAnchor: 'top',
68 | shrinkFraction: 1,
69 | };
70 |
71 | /**
72 | * @hidden
73 | */
74 | static readonly defaultRoutes = /* #__PURE__ */ BarElement.defaultRoutes;
75 |
76 | /**
77 | * @hidden
78 | */
79 | align: 'left' | 'right' | 'center' = 'center';
80 |
81 | /**
82 | * @hidden
83 | */
84 | next: TrapezoidElement | undefined = undefined;
85 |
86 | /**
87 | * @hidden
88 | */
89 | previous: TrapezoidElement | undefined = undefined;
90 |
91 | private getBounds(useFinalPosition = false) {
92 | // x ... center not start
93 | const { x, y, base, width, height, horizontal } = this.getProps(
94 | ['x', 'y', 'base', 'width', 'height', 'horizontal'],
95 | useFinalPosition
96 | );
97 | if (horizontal) {
98 | const w = Math.abs(x - base);
99 | const left = base - (this.align !== 'left' ? w : 0);
100 | const right = base + (this.align !== 'right' ? w : 0);
101 | const half = height / 2;
102 | const top = y - half;
103 | const bottom = y + half;
104 | return { left, top, right, bottom, horizontal };
105 | } else {
106 | const h = Math.abs(y - base);
107 | const half = width / 2;
108 | const left = x - half;
109 | const right = x + half;
110 | const top = base - (this.align !== 'right' ? h : 0);
111 | const bottom = base + (this.align !== 'left' ? h : 0);
112 | return { left, top, right, bottom, horizontal };
113 | }
114 | }
115 |
116 | /**
117 | * @hidden
118 | */
119 | inRange(mouseX: number | null, mouseY: number | null, useFinalPosition: boolean) {
120 | const bb = this.getBounds(useFinalPosition);
121 | const inX = mouseX == null || inBetween(mouseX, bb.left, bb.right);
122 | const inY = mouseY == null || inBetween(mouseY, bb.top, bb.bottom);
123 | return inX && inY;
124 | }
125 |
126 | /**
127 | * @hidden
128 | */
129 | inXRange(mouseX: number, useFinalPosition: boolean) {
130 | return this.inRange(mouseX, null, useFinalPosition);
131 | }
132 |
133 | /**
134 | * @hidden
135 | */
136 | inYRange(mouseY: number, useFinalPosition: boolean) {
137 | return this.inRange(null, mouseY, useFinalPosition);
138 | }
139 |
140 | /**
141 | * @hidden
142 | */
143 | getCenterPoint(useFinalPosition: boolean) {
144 | const { x, y, base, horizontal } = this.getProps(['x', 'y', 'base', 'horizontal'], useFinalPosition);
145 | const r = {
146 | center: {
147 | x: horizontal ? base : x,
148 | y: horizontal ? y : base,
149 | },
150 | left: {
151 | x: horizontal ? (base + x) / 2 : x,
152 | y: horizontal ? y : (base + y) / 2,
153 | },
154 | right: {
155 | x: horizontal ? base - (x - base) / 2 : x,
156 | y: horizontal ? y : base - (y + base) / 2,
157 | },
158 | }[this.align];
159 | return r;
160 | }
161 |
162 | /**
163 | * @hidden
164 | */
165 | tooltipPosition(useFinalPosition: boolean): { x: number; y: number } {
166 | return this.getCenterPoint(useFinalPosition);
167 | }
168 |
169 | /**
170 | * @hidden
171 | */
172 | getRange(axis: string) {
173 | const { width, height } = this.getProps(['width', 'height']);
174 | // const factor;
175 | return axis === 'x' ? width : height;
176 | }
177 |
178 | private computeWayPoints(useFinalPosition = false): [number, number][] {
179 | let dir = this.options.shrinkAnchor;
180 | let shrinkFraction = Math.max(Math.min(this.options.shrinkFraction, 1), 0);
181 |
182 | if (shrinkFraction === 0) {
183 | dir = 'none';
184 | shrinkFraction = 1;
185 | }
186 |
187 | let bounds = this.getBounds(useFinalPosition);
188 | const hor = bounds.horizontal;
189 | let nextBounds = this.next && (dir === 'top' || dir === 'middle') ? this.next.getBounds(useFinalPosition) : bounds;
190 | let prevBounds =
191 | this.previous && (dir === 'bottom' || dir === 'middle') ? this.previous.getBounds(useFinalPosition) : bounds;
192 |
193 | if (!hor) {
194 | bounds = transpose(bounds);
195 | nextBounds = transpose(nextBounds);
196 | prevBounds = transpose(prevBounds);
197 | }
198 |
199 | const hi = Math.floor((bounds.bottom - bounds.top) * (1 - shrinkFraction));
200 | const hiRest = Math.floor((bounds.bottom - bounds.top - hi) / 2);
201 |
202 | const points: [number, number][] = [];
203 | const rPoints: [number, number][] = [];
204 | if (dir === 'none' || dir === 'top') {
205 | points.push([bounds.left, bounds.top], [bounds.right, bounds.top]);
206 | } else {
207 | let pFraction = 1;
208 | if (dir === 'middle') {
209 | const pHiRest = Math.floor((prevBounds.bottom - prevBounds.top) * shrinkFraction * 0.5);
210 | pFraction = hiRest / (pHiRest + hiRest);
211 | }
212 | points.push(
213 | [bounds.left + (prevBounds.left - bounds.left) * pFraction, bounds.top],
214 | [bounds.right + (prevBounds.right - bounds.right) * pFraction, bounds.top]
215 | );
216 | }
217 |
218 | if (dir === 'middle') {
219 | points.push([bounds.right, bounds.top + hiRest]);
220 | points.push([bounds.right, bounds.bottom - hiRest]);
221 | rPoints.push([bounds.left, bounds.top + hiRest]);
222 | rPoints.push([bounds.left, bounds.bottom - hiRest]);
223 | } else if (dir === 'top' && shrinkFraction < 1) {
224 | points.push([bounds.right, bounds.top + hi]);
225 | rPoints.push([bounds.left, bounds.top + hi]);
226 | } else if (dir === 'bottom' && shrinkFraction < 1) {
227 | points.push([bounds.right, bounds.bottom - hi]);
228 | rPoints.push([bounds.left, bounds.bottom - hi]);
229 | }
230 |
231 | if (dir === 'none' || dir === 'bottom') {
232 | points.push([bounds.right, bounds.bottom], [bounds.left, bounds.bottom]);
233 | } else {
234 | let nFraction = 1;
235 | if (dir === 'middle') {
236 | const nHiRest = Math.floor((nextBounds.bottom - nextBounds.top) * shrinkFraction * 0.5);
237 | nFraction = hiRest / (nHiRest + hiRest);
238 | }
239 | points.push(
240 | [bounds.right + (nextBounds.right - bounds.right) * nFraction, bounds.bottom],
241 | [bounds.left + (nextBounds.left - bounds.left) * nFraction, bounds.bottom]
242 | );
243 | }
244 |
245 | points.push(...rPoints.reverse());
246 |
247 | if (!hor) {
248 | return points.map(([x, y]) => [y, x]);
249 | }
250 | return points;
251 | }
252 |
253 | /**
254 | * @hidden
255 | */
256 | draw(ctx: CanvasRenderingContext2D): void {
257 | const { options } = this;
258 | ctx.save();
259 | ctx.beginPath();
260 | const points = this.computeWayPoints();
261 | ctx.moveTo(points[0][0], points[0][1]);
262 | for (const p of points.slice(1)) {
263 | ctx.lineTo(p[0], p[1]);
264 | }
265 | if (options.backgroundColor) {
266 | ctx.fillStyle = options.backgroundColor;
267 | ctx.fill();
268 | }
269 | if (options.borderColor) {
270 | ctx.strokeStyle = options.borderColor;
271 | ctx.lineWidth = options.borderWidth as number;
272 | ctx.stroke();
273 | }
274 | ctx.restore();
275 | }
276 | }
277 |
278 | declare module 'chart.js' {
279 | export interface ElementOptionsByType {
280 | trapezoid: ScriptableAndArrayOptions>;
281 | }
282 | }
283 |
--------------------------------------------------------------------------------