├── config.jshintrc
├── .gitignore
├── spec
├── support
│ └── jasmine.json
├── controllers.funnel.spec.js
├── MockCanvasContext.js
└── element.trapezium.spec.js
├── src
├── chart.funnel.js
├── elements
│ └── element.trapezium.js
└── controllers
│ └── controller.funnel.js
├── bower.json
├── .travis.yml
├── LICENSE.md
├── example
├── funnel.html
├── reserve-funnel.html
└── keep-funnel.html
├── package.json
├── karma.conf.js
├── karma.conf.ci.js
├── gulpfile.js
├── README.md
└── dist
├── chart.funnel.min.js
├── chart.funnel.js
└── chart.funnel.min.js.map
/config.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "node": true,
3 | "predef": [ "require", "module" ]
4 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | .DS_Store
3 | Chart.Funnel.js.iml
4 |
5 | node_modules/*
6 | custom/*
7 |
8 | docs/index.md
9 |
10 | bower_components/
11 |
12 | coverage/*
13 | .idea
14 | nbproject/*
15 |
16 | npm-debug.log
17 | nbproject/
18 |
--------------------------------------------------------------------------------
/spec/support/jasmine.json:
--------------------------------------------------------------------------------
1 | {
2 | "spec_dir": "spec",
3 | "spec_files": [
4 | "**/*[sS]pec.js"
5 | ],
6 | "helpers": [
7 | "helpers/**/*.js"
8 | ],
9 | "stopSpecOnExpectationFailure": false,
10 | "random": false
11 | }
12 |
--------------------------------------------------------------------------------
/src/chart.funnel.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * Main file of Chart.Funnel.js
4 | * @author Jone Casper
5 | * @email xu.chenhui@live.com
6 | *
7 | */
8 |
9 | /* global window */
10 | "use strict";
11 |
12 | var Chart = require('chart.js');
13 | Chart = typeof(Chart) === 'function' ? Chart : window.Chart;
14 |
15 | require('./elements/element.trapezium.js')(Chart);
16 | require("./controllers/controller.funnel.js")(Chart);
17 |
18 | module.exports = Chart;
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chartjs-plugin-funnel",
3 | "description": "The funnel plugin for Chart.js",
4 | "main": "dist/chart.funnel.js",
5 | "authors": [
6 | "Jone Casper & YetiForce"
7 | ],
8 | "license": "MIT",
9 | "keywords": [
10 | "Chart.js",
11 | "Chartjs",
12 | "funnel",
13 | "plugin"
14 | ],
15 | "homepage": "https://github.com/YetiForceCompany/chartjs-plugin-funnel",
16 | "ignore": [
17 | "**/.*",
18 | "node_modules",
19 | "bower_components",
20 | "test",
21 | "tests"
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: required
2 | dist: trusty
3 |
4 | language: node_js
5 | node_js:
6 | - "6"
7 | - "6.1"
8 | - "5.11"
9 |
10 | addons:
11 | firefox: latest
12 | apt:
13 | sources:
14 | - google-chrome
15 | packages:
16 | - google-chrome-stable
17 |
18 | before_install:
19 | - "export CHROME_BIN=/usr/bin/google-chrome"
20 | # starting a GUI to run tests, per https://docs.travis-ci.com/user/gui-and-headless-browsers/#Using-xvfb-to-Run-Tests-That-Require-a-GUI
21 | - export DISPLAY=:99.0
22 | - sh -e /etc/init.d/xvfb start
23 |
24 | before_script:
25 | - npm install -g gulp
26 | - npm install
27 |
28 | script:
29 | - gulp test.ci
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | Copyright (c) 2016 Jone Casper
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5 |
6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7 |
8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
9 |
--------------------------------------------------------------------------------
/example/funnel.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Funnel Chart
6 |
7 |
8 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
65 |
66 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chartjs-plugin-funnel",
3 | "version": "1.1.5",
4 | "description": "The funnel plugin for Chart.js",
5 | "main": "dist/chart.funnel.js",
6 | "directories": {
7 | "example": "example"
8 | },
9 | "dependencies": {
10 | "chart.js": ">2.7.0"
11 | },
12 | "devDependencies": {
13 | "browserify": "^13.0.1",
14 | "bundle-collapser": "^1.2.1",
15 | "gulp": "^3.9.1",
16 | "gulp-browserify": "^0.5.1",
17 | "gulp-buble": "^0.9.0",
18 | "gulp-concat": "^2.6.0",
19 | "gulp-insert": "^0.5.0",
20 | "gulp-jshint": "^2.0.1",
21 | "gulp-plumber": "^1.1.0",
22 | "gulp-rename": "^1.2.2",
23 | "gulp-replace": "^0.5.4",
24 | "gulp-sourcemaps": "^2.6.5",
25 | "gulp-streamify": "^1.0.2",
26 | "gulp-uglifyes": "^0.1.3",
27 | "jasmine": "^2.4.1",
28 | "jasmine-core": "^2.4.1",
29 | "jshint": "^2.9.2",
30 | "jshint-stylish": "^2.2.0",
31 | "karma": "^1.1.0",
32 | "karma-browserify": "^5.0.5",
33 | "karma-chrome-launcher": "^1.0.1",
34 | "karma-firefox-launcher": "^1.0.0",
35 | "karma-jasmine": "^1.0.2",
36 | "merge-stream": "^1.0.0",
37 | "vinyl-source-stream": "^1.1.0",
38 | "watchify": "^3.7.0"
39 | },
40 | "scripts": {
41 | "test": "echo \"Error: no test specified\" && exit 1"
42 | },
43 | "repository": {
44 | "type": "git",
45 | "url": "https://github.com/YetiForceCompany/chartjs-plugin-funnel"
46 | },
47 | "keywords": [
48 | "Chart.js",
49 | "Chartjs",
50 | "funnel",
51 | "plugin"
52 | ],
53 | "author": "Jone Casper & YetiForce",
54 | "license": "MIT",
55 | "bugs": {
56 | "url": "https://github.com/YetiForceCompany/chartjs-plugin-funnel/issues"
57 | },
58 | "homepage": "https://github.com/YetiForceCompany/chartjs-plugin-funnel"
59 | }
60 |
--------------------------------------------------------------------------------
/example/reserve-funnel.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Funnel Chart
6 |
7 |
8 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
66 |
67 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration
2 | // Generated on Wed Jun 29 2016 00:12:17 GMT+0800 (中国标准时间)
3 |
4 | module.exports = function(config) {
5 | config.set({
6 |
7 | // base path that will be used to resolve all patterns (eg. files, exclude)
8 | basePath: '',
9 |
10 |
11 | // frameworks to use
12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
13 | frameworks: ['browserify', 'jasmine'],
14 |
15 |
16 | // list of files / patterns to load in the browser
17 | files: [
18 | 'spec/*[sS]pec.js'
19 | ],
20 |
21 |
22 | // list of files to exclude
23 | exclude: [
24 | ],
25 |
26 |
27 | // preprocess matching files before serving them to the browser
28 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
29 | preprocessors: {
30 | 'src/**/*.js': ['browserify'],
31 | 'spec/*[sS]pec.js': ['browserify']
32 | },
33 |
34 | browserify: {
35 | debug: true
36 | },
37 |
38 | // test results reporter to use
39 | // possible values: 'dots', 'progress'
40 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter
41 | reporters: ['progress'],
42 |
43 |
44 | // web server port
45 | port: 9876,
46 |
47 |
48 | // enable / disable colors in the output (reporters and logs)
49 | colors: true,
50 |
51 |
52 | // level of logging
53 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
54 | logLevel: config.LOG_INFO,
55 |
56 |
57 | // enable / disable watching file and executing tests whenever any file changes
58 | autoWatch: true,
59 |
60 |
61 | // start these browsers
62 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
63 | browsers: ['Chrome', 'Firefox'],
64 |
65 |
66 | // Continuous Integration mode
67 | // if true, Karma captures browsers, runs the tests and exits
68 | singleRun: false,
69 |
70 | // Concurrency level
71 | // how many browser should be started simultaneous
72 | concurrency: Infinity
73 | })
74 | }
75 |
--------------------------------------------------------------------------------
/example/keep-funnel.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Funnel Chart
6 |
7 |
8 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
72 |
73 |
--------------------------------------------------------------------------------
/karma.conf.ci.js:
--------------------------------------------------------------------------------
1 | // Karma configuration
2 | // Generated on Wed Jun 29 2016 00:12:17 GMT+0800 (中国标准时间)
3 |
4 | module.exports = function(config) {
5 | var configuration = {
6 |
7 | // base path that will be used to resolve all patterns (eg. files, exclude)
8 | basePath: '',
9 |
10 |
11 | // frameworks to use
12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
13 | frameworks: ['browserify', 'jasmine'],
14 |
15 |
16 | // list of files / patterns to load in the browser
17 | files: [
18 | 'spec/*[sS]pec.js'
19 | ],
20 |
21 |
22 | // list of files to exclude
23 | exclude: [
24 | ],
25 |
26 |
27 | // preprocess matching files before serving them to the browser
28 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
29 | preprocessors: {
30 | 'src/**/*.js': ['browserify'],
31 | 'spec/*[sS]pec.js': ['browserify']
32 | },
33 |
34 | browserify: {
35 | debug: true
36 | },
37 |
38 | // test results reporter to use
39 | // possible values: 'dots', 'progress'
40 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter
41 | reporters: ['progress'],
42 |
43 |
44 | // web server port
45 | //port: 9876,
46 |
47 |
48 | // enable / disable colors in the output (reporters and logs)
49 | colors: true,
50 |
51 |
52 | // level of logging
53 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
54 | logLevel: config.LOG_INFO,
55 |
56 |
57 | // enable / disable watching file and executing tests whenever any file changes
58 | autoWatch: false,
59 |
60 |
61 | // start these browsers
62 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
63 | browsers: ['Firefox'],
64 | customLaunchers: {
65 | Chrome_travis_ci: {
66 | base: 'Chrome',
67 | flags: ['--no-sandbox']
68 | }
69 | },
70 |
71 | // Continuous Integration mode
72 | // if true, Karma captures browsers, runs the tests and exits
73 | singleRun: false,
74 |
75 | // Concurrency level
76 | // how many browser should be started simultaneous
77 | concurrency: Infinity
78 | };
79 |
80 | if (process.env.TRAVIS) {
81 | configuration.browsers.push('Chrome_travis_ci');
82 | }
83 |
84 | config.set(configuration)
85 | };
86 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | var gulp = require("gulp");
2 | var rename = require("gulp-rename");
3 | var jshint = require("gulp-jshint");
4 | var replace = require("gulp-replace");
5 | var insert = require("gulp-insert");
6 | var streamify = require("gulp-streamify");
7 | var browserify = require("browserify");
8 | var uglify = require("gulp-uglifyes");
9 | var concat = require("gulp-concat");
10 | var source = require("vinyl-source-stream");
11 | var merge = require("merge-stream");
12 | var collapse = require("bundle-collapser/plugin");
13 | var Server = require("karma").Server;
14 | var buble = require("gulp-buble");
15 | var package = require("./package.json");
16 | var sourcemaps = require("gulp-sourcemaps");
17 |
18 | var header =
19 | "/*!\n" +
20 | " * Chart.Funnel.js\n" +
21 | " * A funnel plugin for Chart.js(http://chartjs.org/)\n" +
22 | " * Version: {{ version }}\n" +
23 | " *\n" +
24 | " * Copyright 2016 Jone Casaper & YetiForce\n" +
25 | " * Released under the MIT license\n" +
26 | " * https://github.com/xch89820/Chart.Funnel.js/blob/master/LICENSE.md\n" +
27 | " */\n";
28 |
29 | gulp.task("js", function() {
30 | // Bundled library
31 | var bundled = browserify("./src/chart.funnel.js", {
32 | standalone: "Chart.Funnel"
33 | })
34 | .plugin(collapse)
35 | .bundle()
36 | .pipe(source("chart.funnel.bundled.js"))
37 | .pipe(insert.prepend(header))
38 | .pipe(streamify(replace("{{ version }}", package.version)))
39 | .pipe(streamify(jshint()))
40 | .pipe(jshint.reporter("default"))
41 | .pipe(gulp.dest("dist"))
42 | .pipe(streamify(uglify()))
43 | .pipe(insert.prepend(header))
44 | .pipe(streamify(replace("{{ version }}", package.version)))
45 | .pipe(streamify(concat("chart.funnel.bundled.min.js")))
46 | .pipe(gulp.dest("dist"));
47 |
48 | var nonBundled = browserify("./src/chart.funnel.js", {
49 | standalone: "Chart.Funnel"
50 | })
51 | .ignore("chart.js")
52 | .plugin(collapse)
53 | .bundle()
54 | .pipe(source("chart.funnel.js"))
55 | .pipe(insert.prepend(header))
56 | .pipe(streamify(replace("{{ version }}", package.version)))
57 | .pipe(streamify(jshint()))
58 | .pipe(jshint.reporter("default"))
59 | .pipe(gulp.dest("dist"))
60 | .pipe(streamify(uglify()))
61 | .pipe(insert.prepend(header))
62 | .pipe(streamify(replace("{{ version }}", package.version)))
63 | .pipe(streamify(concat("chart.funnel.min.js")))
64 | .pipe(gulp.dest("dist"));
65 |
66 | return merge(bundled, nonBundled);
67 | });
68 |
69 | gulp.task("jshint", function() {
70 | return gulp
71 | .src("src/**/*.js")
72 | .pipe(jshint("config.jshintrc"))
73 | .pipe(jshint.reporter("jshint-stylish"))
74 | .pipe(jshint.reporter("fail"));
75 | });
76 |
77 | gulp.task("compile-min", () =>
78 | gulp
79 | .src("dist/chart.funnel.js")
80 | .pipe(sourcemaps.init())
81 | .pipe(
82 | rename({
83 | suffix: ".min"
84 | })
85 | )
86 | .pipe(uglify())
87 | .pipe(buble())
88 | .pipe(sourcemaps.write("."))
89 | .pipe(gulp.dest("dist"))
90 | );
91 |
92 | // For CI
93 | gulp.task("unittest.ci", function(done) {
94 | new Server(
95 | {
96 | configFile: __dirname + "/karma.conf.ci.js",
97 | singleRun: true
98 | },
99 | done
100 | ).start();
101 | });
102 |
103 | gulp.task("test.ci", ["jshint", "unittest.ci"]);
104 |
105 | gulp.task("default", function() {
106 | gulp.watch("src/**/*.js", ["js"]);
107 | });
108 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [![NPM version][npm-version-image]][npm-url]
2 | [![NPM downloads][npm-downloads-image]][npm-url]
3 | [![MIT License][license-image]][license-url]
4 | [](https://travis-ci.org/xch89820/Chart.Funnel.js)
5 |
6 | # chartjs-plugin-funnel
7 | The funnel plugin for Chart.js > 2.7
8 |
9 | ## Installation
10 |
11 | To download a zip, go to the chartjs-plugin-funnel on Github
12 |
13 | To install via npm / bower:
14 |
15 | ```bash
16 | npm install chartjs-plugin-funnel --save
17 | ```
18 |
19 | ## Preview
20 |
21 | 
22 |
23 |
24 | 
25 |
26 |
27 | 
28 |
29 |
30 | 
31 |
32 |
33 | 
34 |
35 |
36 | ## Usage
37 |
38 | To configure the funnel plugin, you can simply set chart type to `funnel`.
39 |
40 | Simple example:
41 | ```js
42 | var config = {
43 | type: 'funnel',
44 | data: {
45 | datasets: [{
46 | data: [30, 60, 90],
47 | backgroundColor: [
48 | "#FF6384",
49 | "#36A2EB",
50 | "#FFCE56"
51 | ],
52 | hoverBackgroundColor: [
53 | "#FF6384",
54 | "#36A2EB",
55 | "#FFCE56"
56 | ]
57 | }],
58 | labels: [
59 | "Red",
60 | "Blue",
61 | "Yellow"
62 | ]
63 | }
64 | }
65 | ```
66 |
67 | Funnel chart support upside-down drawing or against left or right side drawing.
68 |
69 | This plugin works with datalabels plugin.
70 |
71 |
72 | You can find documentation for Chart.js at [www.chartjs.org/docs](http://www.chartjs.org/docs).
73 |
74 | ## Options
75 |
76 | #### sort
77 | Reverse or not, you can set 'desc' to draw an upside-down funnel.
78 |
79 | default is 'asc'.
80 |
81 | #### gap
82 | The gap between to trapezium in our funnel chart. The unit is px.
83 |
84 | default is 2
85 |
86 | #### keep
87 | Draw element against left or right side.
88 |
89 | default is 'auto'.
90 |
91 | #### topWidth
92 | The top-width of funnel chart, defualt is 0
93 |
94 | #### bottomWidth
95 | The bottom-width of funnel chart, default use the width of canvas.
96 |
97 | #### tooltips
98 | The tooltips option is a special option for funnel chart, you should be careful if you want to rewrite the option.
99 |
100 | The default option is
101 | ```js
102 | {
103 | callbacks: {
104 | title: function (tooltipItem, data) {
105 | return '';
106 | },
107 | label: function (tooltipItem, data) {
108 | return data.labels[tooltipItem.index] + ': ' + data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index];
109 | }
110 | }
111 | }
112 | ```
113 | ## License
114 |
115 | Chart.Funnel.js is available under the [MIT license](http://opensource.org/licenses/MIT).
116 |
117 | [license-image]: http://img.shields.io/badge/license-MIT-blue.svg?style=flat
118 | [license-url]: http://opensource.org/licenses/MIT
119 |
120 | [npm-url]: https://www.npmjs.com/package/chartjs-plugin-funnel
121 | [npm-version-image]: http://img.shields.io/npm/v/chartjs-plugin-funnel.svg?style=flat
122 |
123 | [npm-downloads-image]: http://img.shields.io/npm/dm/chartjs-plugin-funnel.svg?style=flat
124 |
--------------------------------------------------------------------------------
/src/elements/element.trapezium.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * Extend trapezium element
4 | * @author Jone Casper
5 | * @email xu.chenhui@live.com
6 | *
7 | */
8 |
9 | "use strict";
10 |
11 | module.exports = function (Chart) {
12 | var helpers = Chart.helpers,
13 | globalOpts = Chart.defaults.global;
14 |
15 | globalOpts.elements.trapezium = {
16 | backgroundColor: globalOpts.defaultColor,
17 | borderWidth: 0,
18 | borderColor: globalOpts.defaultColor,
19 | borderSkipped: 'bottom',
20 | type: 'isosceles' // isosceles, scalene
21 | };
22 |
23 | // Thanks for https://github.com/substack/point-in-polygon
24 | var pointInPolygon = function (point, vs) {
25 | // ray-casting algorithm based on
26 | // http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
27 |
28 | var x = point[0], y = point[1];
29 |
30 | var inside = false;
31 | for (var i = 0, j = vs.length - 1; i < vs.length; j = i++) {
32 | var xi = vs[i][0], yi = vs[i][1];
33 | var xj = vs[j][0], yj = vs[j][1];
34 |
35 | var intersect = ((yi > y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
36 | if (intersect)
37 | inside = !inside;
38 | }
39 |
40 | return inside;
41 | };
42 |
43 | Chart.elements.Trapezium = Chart.elements.Rectangle.extend({
44 | getCorners: function () {
45 | var vm = this._view;
46 | var globalOptionTrapeziumElements = globalOpts.elements.trapezium;
47 |
48 | var corners = [],
49 | type = vm.type || globalOptionTrapeziumElements.type,
50 | top = vm.y,
51 | borderWidth = vm.borderWidth || globalOptionTrapeziumElements.borderWidth,
52 | upHalfWidth = vm.upperWidth / 2,
53 | botHalfWidth = vm.bottomWidth / 2,
54 | halfStroke = borderWidth / 2;
55 |
56 | halfStroke = halfStroke < 0 ? 0 : halfStroke;
57 |
58 | // An isosceles trapezium
59 | if (type == 'isosceles') {
60 | var x = vm.x;
61 |
62 | // Corner points, from bottom-left to bottom-right clockwise
63 | // | 1 2 |
64 | // | 0 3 |
65 | corners = [
66 | [x - botHalfWidth + halfStroke, vm.base],
67 | [x - upHalfWidth + halfStroke, top + halfStroke],
68 | [x + upHalfWidth - halfStroke, top + halfStroke],
69 | [x + botHalfWidth - halfStroke, vm.base]
70 | ];
71 | } else if (type == 'scalene') {
72 | var x1 = vm.x1,
73 | x2 = vm.x2;
74 |
75 | corners = [
76 | [x2 - botHalfWidth + halfStroke, vm.base],
77 | [x1 - upHalfWidth + halfStroke, top + halfStroke],
78 | [x1 + upHalfWidth - halfStroke, top + halfStroke],
79 | [x2 + botHalfWidth - halfStroke, vm.base]
80 | ];
81 | }
82 |
83 |
84 | return corners;
85 | },
86 | draw: function () {
87 | var ctx = this._chart.ctx;
88 | var vm = this._view;
89 | var globalOptionTrapeziumElements = globalOpts.elements.trapezium;
90 |
91 | var corners = this.getCorners();
92 | this._cornersCache = corners;
93 |
94 | ctx.beginPath();
95 | ctx.fillStyle = vm.backgroundColor || globalOptionTrapeziumElements.backgroundColor;
96 | ctx.strokeStyle = vm.borderColor || globalOptionTrapeziumElements.borderColor;
97 | ctx.lineWidth = vm.borderWidth || globalOptionTrapeziumElements.borderWidth;
98 |
99 | // Find first (starting) corner with fallback to 'bottom'
100 | var borders = ['bottom', 'left', 'top', 'right'];
101 | var startCorner = borders.indexOf(
102 | vm.borderSkipped || globalOptionTrapeziumElements.borderSkipped,
103 | 0);
104 | if (startCorner === -1)
105 | startCorner = 0;
106 |
107 | function cornerAt(index) {
108 | return corners[(startCorner + index) % 4];
109 | }
110 |
111 | // Draw rectangle from 'startCorner'
112 | ctx.moveTo.apply(ctx, cornerAt(0));
113 | for (var i = 1; i < 4; i++)
114 | ctx.lineTo.apply(ctx, cornerAt(i));
115 |
116 | ctx.fill();
117 | if (vm.borderWidth) {
118 | ctx.stroke();
119 | }
120 | },
121 | height: function () {
122 | var vm = this._view;
123 | if (!vm) {
124 | return 0;
125 | }
126 |
127 | return vm.base - vm.y;
128 | },
129 | inRange: function (mouseX, mouseY) {
130 | var vm = this._view;
131 | if (!vm) {
132 | return false;
133 | }
134 | var corners = this._cornersCache ? this._cornersCache : this.getCorners();
135 | return pointInPolygon([mouseX, mouseY], corners);
136 | },
137 | inLabelRange: function (mouseX) {
138 | var x,
139 | vm = this._view;
140 |
141 | if (!vm) {
142 | return false;
143 | }
144 |
145 | if (vm.type == 'scalene') {
146 | if (vm.x1 > vm.x2) {
147 | return mouseX >= vm.x2 - vm.bottomWidth / 2 && mouseX <= vm.x1 + vm.upperWidth / 2;
148 | } else {
149 | return mouseX <= vm.x2 + vm.bottomWidth / 2 && mouseX >= vm.x1 - vm.upperWidth / 2;
150 | }
151 | }
152 |
153 | var maxWidth = Math.max(vm.upperWidth, vm.bottomWidth);
154 | return mouseX >= vm.x - maxWidth / 2 && mouseX <= vm.x + maxWidth / 2;
155 | },
156 | tooltipPosition: function () {
157 | var vm = this._view;
158 | return {
159 | x: vm.x || vm.x2,
160 | y: vm.base - (vm.base - vm.y) / 2
161 | };
162 | },
163 | getArea: function () {
164 | var vm = this._view;
165 | var total = 0;
166 | var corners = this._cornersCache ? this._cornersCache : this.getCorners();
167 | for (var i = 0, l = corners.length; i < l; i++) {
168 | var addX = corners[i][0];
169 | var addY = corners[i == corners.length - 1 ? 0 : i + 1][1];
170 | var subX = corners[i == corners.length - 1 ? 0 : i + 1][0];
171 | var subY = corners[i][1];
172 | total += (addX * addY * 0.5);
173 | total -= (subX * subY * 0.5);
174 | }
175 | return Math.abs(total);
176 | },
177 | getCenterPoint: function () {
178 | var corners = this._cornersCache ? this._cornersCache : this.getCorners();
179 | var vm = this._view;
180 | var x = 0, y = 0, i, j, f, point1, point2;
181 | for (i = 0, j = corners.length - 1; i < corners.length; j = i, i++) {
182 | point1 = corners[i];
183 | point2 = corners[j];
184 | f = point1[0] * point2[1] - point2[0] * point1[1];
185 | x += (point1[0] + point2[0]) * f;
186 | y += (point1[1] + point2[1]) * f;
187 | }
188 | f = this.getArea() * 6;
189 | return {x: x / f, y: y / f};
190 | },
191 | });
192 | };
193 |
--------------------------------------------------------------------------------
/dist/chart.funnel.min.js:
--------------------------------------------------------------------------------
1 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module){ module.exports=e(); }else if("function"==typeof define&&define.amd){ define([],e); }else{var t;((t="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).Chart||(t.Chart={})).Funnel=e()}}(function(){return function(){return function e(t,r,o){function a(i,l){if(!r[i]){if(!t[i]){var s="function"==typeof require&&require;if(!l&&s){ return s(i,!0); }if(n){ return n(i,!0); }var d=new Error("Cannot find module '"+i+"'");throw d.code="MODULE_NOT_FOUND",d}var u=r[i]={exports:{}};t[i][0].call(u.exports,function(e){return a(t[i][1][e]||e)},u,u.exports,e,t,r,o)}return r[i].exports}for(var n="function"==typeof require&&require,i=0;i');var r=e.data,o=r.datasets,a=r.labels;if(o.length){ for(var n=0;n'),a[n]&&t.push(a[n]),t.push(""); } }return t.push(""),t.join("")},legend:{labels:{generateLabels:function(e){var r=e.data;return r.labels.length&&r.datasets.length?r.labels.map(function(o,a){var n=e.getDatasetMeta(0),i=r.datasets[0],l=n.data[a],s=l.custom||{},d=t.getValueAtIndexOrDefault,u=e.options.elements.trapezium;return{text:o,fillStyle:s.backgroundColor?s.backgroundColor:d(i.backgroundColor,a,u.backgroundColor),strokeStyle:s.borderColor?s.borderColor:d(i.borderColor,a,u.borderColor),lineWidth:s.borderWidth?s.borderWidth:d(i.borderWidth,a,u.borderWidth),hidden:isNaN(i.data[a])||n.data[a].hidden,index:a,_index:l._index}}):[]}},onClick:function(e,t){var r,o,a,n=t.index,i=this.chart;for(r=0,o=(i.data.datasets||[]).length;rb?e:b)});var g=u/b,x=0;t.each(f,function(e,t){e._viewIndex=e.hidden?-1:x++});var v=n.gap||0,y=(d-(p-1)*v)/p;r.topWidth=h,r.dwRatio=g,r.elHeight=y,r.valAndLabels=f,t.each(i,function(t,o){r.updateElement(t,o,e)},r)},updateElement:function(e,r,o){var a,n,i,l,s,d,u=this,h=u.chart,c=h.chartArea,f=h.options,p=f.sort,b=u.dwRatio,g=u.elHeight,x=f.gap||0,v=f.elements.borderWidth||0,y="isosceles",m=u.valAndLabels[r],C=m._viewIndex<0?r:m._viewIndex,W=c.top+(C+1)*(g+x)-x,k=u.getMeta();if(e._xScale=u.getScaleForId(k.xAxisID),"asc"!==p&&"data-asc"!==p&&p){if("desc"===p||"data-desc"===p){var A=t.findNextWhere(u.valAndLabels,function(e){return!e.hidden},r);s=m.val*b,d=A?A.val*b:u.topWidth}}else{var _=t.findPreviousWhere(u.valAndLabels,function(e){return!e.hidden},r);s=_?_.val*b:u.topWidth,d=m.val*b}n=c.top+m.orgIndex*(g+x),"left"===f.keep?(y="scalene",i=c.left+s/2,l=c.left+d/2,a=i):"right"===f.keep?(y="scalene",i=c.right-s/2,l=c.right-d/2,a=i):a=(c.left+c.right)/2,t.extend(e,{_datasetIndex:u.index,_index:m.orgIndex,_model:{type:y,y:n,base:W>c.bottom?c.bottom:W,x:a,x1:i,x2:l,upperWidth:o||m.hidden?0:s,bottomWidth:o||m.hidden?0:d,borderWidth:v,backgroundColor:m&&m.backgroundColor,borderColor:m&&m.borderColor,label:m&&m.label}}),e.pivot()},removeHoverStyle:function(t){e.DatasetController.prototype.removeHoverStyle.call(this,t,this.chart.options.elements.trapezium)}})}},{}],4:[function(e,t,r){"use strict";t.exports=function(e){e.helpers;var t=e.defaults.global;t.elements.trapezium={backgroundColor:t.defaultColor,borderWidth:0,borderColor:t.defaultColor,borderSkipped:"bottom",type:"isosceles"};e.elements.Trapezium=e.elements.Rectangle.extend({getCorners:function(){var e=this._view,r=t.elements.trapezium,o=[],a=e.type||r.type,n=e.y,i=e.borderWidth||r.borderWidth,l=e.upperWidth/2,s=e.bottomWidth/2,d=i/2;if(d=d<0?0:d,"isosceles"==a){var u=e.x;o=[[u-s+d,e.base],[u-l+d,n+d],[u+l-d,n+d],[u+s-d,e.base]]}else if("scalene"==a){var h=e.x1,c=e.x2;o=[[c-s+d,e.base],[h-l+d,n+d],[h+l-d,n+d],[c+s-d,e.base]]}return o},draw:function(){var e=this._chart.ctx,r=this._view,o=t.elements.trapezium,a=this.getCorners();this._cornersCache=a,e.beginPath(),e.fillStyle=r.backgroundColor||o.backgroundColor,e.strokeStyle=r.borderColor||o.borderColor,e.lineWidth=r.borderWidth||o.borderWidth;var n=["bottom","left","top","right"].indexOf(r.borderSkipped||o.borderSkipped,0);function i(e){return a[(n+e)%4]}-1===n&&(n=0),e.moveTo.apply(e,i(0));for(var l=1;l<4;l++){ e.lineTo.apply(e,i(l)); }e.fill(),r.borderWidth&&e.stroke()},height:function(){var e=this._view;return e?e.base-e.y:0},inRange:function(e,t){return!!this._view&&function(e,t){for(var r=e[0],o=e[1],a=!1,n=0,i=t.length-1;no!=u>o&&r<(d-l)*(o-s)/(u-s)+l&&(a=!a)}return a}([e,t],this._cornersCache?this._cornersCache:this.getCorners())},inLabelRange:function(e){var t=this._view;if(!t){ return!1; }if("scalene"==t.type){ return t.x1>t.x2?e>=t.x2-t.bottomWidth/2&&e<=t.x1+t.upperWidth/2:e<=t.x2+t.bottomWidth/2&&e>=t.x1-t.upperWidth/2; }var r=Math.max(t.upperWidth,t.bottomWidth);return e>=t.x-r/2&&e<=t.x+r/2},tooltipPosition:function(){var e=this._view;return{x:e.x||e.x2,y:e.base-(e.base-e.y)/2}},getArea:function(){this._view;for(var e=0,t=this._cornersCache?this._cornersCache:this.getCorners(),r=0,o=t.length;r B ? A : B) * percentDiff) || diff < 2; // 2 pixels is fine
167 | }
168 |
169 | return {pass: result};
170 | }
171 | }
172 | };
173 |
174 | function toEqualOneOf() {
175 | return {
176 | compare: function (actual, expecteds) {
177 | var result = false;
178 | for (var i = 0, l = expecteds.length; i < l; i++) {
179 | if (actual === expecteds[i]) {
180 | result = true;
181 | break;
182 | }
183 | }
184 | return {
185 | pass: result
186 | };
187 | }
188 | };
189 | }
190 |
191 | window.addDefaultMatchers = function (jasmine) {
192 | jasmine.addMatchers({
193 | toBeCloseToPixel: toBeCloseToPixel,
194 | toEqualOneOf: toEqualOneOf
195 | });
196 | }
197 |
198 | // Canvas injection helpers
199 | var charts = {};
200 |
201 | function acquireChart(config, style) {
202 | var wrapper = document.createElement("div");
203 | var canvas = document.createElement("canvas");
204 | wrapper.className = 'chartjs-wrapper';
205 |
206 | style = style || {height: '512px', width: '512px'};
207 | for (var k in style) {
208 | wrapper.style[k] = style[k];
209 | canvas.style[k] = style[k];
210 | }
211 |
212 | canvas.height = canvas.style.height && parseInt(canvas.style.height);
213 | canvas.width = canvas.style.width && parseInt(canvas.style.width);
214 |
215 | // by default, remove chart animation and auto resize
216 | var options = config.options = config.options || {};
217 | options.animation = options.animation === undefined ? false : options.animation;
218 | options.responsive = options.responsive === undefined ? false : options.responsive;
219 | options.defaultFontFamily = options.defaultFontFamily || 'Arial';
220 |
221 | wrapper.appendChild(canvas);
222 | window.document.body.appendChild(wrapper);
223 | var chart = new Chart(canvas.getContext("2d"), config);
224 | charts[chart.id] = chart;
225 | return chart;
226 | }
227 |
228 | function releaseChart(chart) {
229 | chart.chart.canvas.parentNode.remove();
230 | delete charts[chart.id];
231 | delete chart;
232 | }
233 |
234 | function releaseAllCharts(scope) {
235 | for (var id in charts) {
236 | var chart = charts[id];
237 | releaseChart(chart);
238 | }
239 | }
240 |
241 | function injectCSS(css) {
242 | // http://stackoverflow.com/q/3922139
243 | var head = document.getElementsByTagName('head')[0];
244 | var style = document.createElement('style');
245 | style.setAttribute('type', 'text/css');
246 | if (style.styleSheet) { // IE
247 | style.styleSheet.cssText = css;
248 | } else {
249 | style.appendChild(document.createTextNode(css));
250 | }
251 | head.appendChild(style);
252 | }
253 |
254 | window.acquireChart = acquireChart;
255 | window.releaseChart = releaseChart;
256 | window.releaseAllCharts = releaseAllCharts;
257 |
258 | // some style initialization to limit differences between browsers across different plateforms.
259 | injectCSS(
260 | '.chartjs-wrapper, .chartjs-wrapper canvas {' +
261 | 'border: 0;' +
262 | 'margin: 0;' +
263 | 'padding: 0;' +
264 | '}' +
265 | '.chartjs-wrapper {' +
266 | 'position: absolute' +
267 | '}');
268 |
269 | module.exports = Context;
270 |
--------------------------------------------------------------------------------
/spec/element.trapezium.spec.js:
--------------------------------------------------------------------------------
1 | // Test the trapezium element
2 |
3 | /** globals Chart **/
4 | var Chart = require('chart.js');
5 | var MockContext = require('./MockCanvasContext.js');
6 | require('../src/chart.funnel.js');
7 |
8 | describe('Trapezium element tests', function() {
9 | it ('Should be constructed', function() {
10 | var trapezium = new Chart.elements.Trapezium({
11 | _datasetIndex: 2,
12 | _index: 1
13 | });
14 |
15 | expect(trapezium).not.toBe(undefined);
16 | expect(trapezium._datasetIndex).toBe(2);
17 | expect(trapezium._index).toBe(1);
18 | });
19 |
20 | it ('Isosceles trapezium, should determine if in range', function() {
21 | var trapezium = new Chart.elements.Trapezium({
22 | _datasetIndex: 2,
23 | _index: 1
24 | });
25 |
26 | // Make sure we can run these before the view is added
27 | expect(trapezium.inRange(2, 2)).toBe(false);
28 | expect(trapezium.inLabelRange(2)).toBe(false);
29 |
30 | // Mock out the view as if the controller put it there
31 | // An 'zero' test
32 | trapezium._view = {
33 | y: 0,
34 | base: 0,
35 | x: 0,
36 | upperWidth: 0,
37 | bottomWidth: 0
38 | };
39 |
40 | expect(trapezium.inRange(2, 2)).toBe(false);
41 | expect(trapezium.inLabelRange(2)).toBe(false);
42 |
43 | // x,y in positive number
44 | trapezium._view = {
45 | y: 0,
46 | base: 5,
47 | x: 2,
48 | upperWidth: 2,
49 | bottomWidth: 4
50 | };
51 | expect(trapezium.inRange(1, 0)).toBe(true);
52 | expect(trapezium.inRange(0, 0)).toBe(false);
53 | expect(trapezium.inRange(2, 3)).toBe(true);
54 | expect(trapezium.inLabelRange(1)).toBe(true);
55 | expect(trapezium.inLabelRange(2)).toBe(true);
56 | expect(trapezium.inLabelRange(5)).toBe(false);
57 |
58 | // upper axis not in positive number
59 | trapezium._view = {
60 | y: 0,
61 | base: 5,
62 | x: 2,
63 | upperWidth: 10,
64 | bottomWidth: 4
65 | };
66 | expect(trapezium.inRange(0, 0)).toBe(true);
67 | expect(trapezium.inRange(2, 3)).toBe(true);
68 | expect(trapezium.inRange(10, 3)).toBe(false);
69 | expect(trapezium.inLabelRange(7)).toBe(true);
70 | expect(trapezium.inLabelRange(2)).toBe(true);
71 | expect(trapezium.inLabelRange(10)).toBe(false);
72 | });
73 |
74 | it ('Isosceles trapezium, should get the correct height', function() {
75 | var trapezium = new Chart.elements.Trapezium({
76 | _datasetIndex: 2,
77 | _index: 1
78 | });
79 |
80 | expect(trapezium.height()).toEqual(0);
81 |
82 | trapezium._view = {
83 | y: 0,
84 | base: 10,
85 | x: 4,
86 | upperWidth: 2,
87 | bottomWidth: 5
88 | };
89 |
90 | expect(trapezium.height()).toEqual(10);
91 | });
92 |
93 | it ('Scalene trapezium, should determine if in range', function() {
94 | var trapezium = new Chart.elements.Trapezium({
95 | _datasetIndex: 2,
96 | _index: 1
97 | });
98 |
99 | // Make sure we can run these before the view is added
100 | expect(trapezium.inRange(2, 2)).toBe(false);
101 | expect(trapezium.inLabelRange(2)).toBe(false);
102 |
103 | // Mock out the view as if the controller put it there
104 | // An 'zero' test
105 | trapezium._view = {
106 | y: 0,
107 | base: 0,
108 | x1: 0,
109 | x2: 0,
110 | upperWidth: 0,
111 | bottomWidth: 0,
112 | type :'scalene'
113 | };
114 |
115 | expect(trapezium.inRange(2, 2)).toBe(false);
116 | expect(trapezium.inLabelRange(2)).toBe(false);
117 |
118 | // x1 == x2 is an isosceles trapezium
119 | trapezium._view = {
120 | y: 0,
121 | base: 5,
122 | x1: 2,
123 | x2: 2,
124 | upperWidth: 2,
125 | bottomWidth: 4,
126 | type :'scalene'
127 | };
128 | expect(trapezium.inRange(1, 0)).toBe(true);
129 | expect(trapezium.inRange(0, 0)).toBe(false);
130 | expect(trapezium.inRange(2, 3)).toBe(true);
131 | expect(trapezium.inLabelRange(1)).toBe(true);
132 | expect(trapezium.inLabelRange(2)).toBe(true);
133 | expect(trapezium.inLabelRange(5)).toBe(false);
134 |
135 | // x1=1 x2=3
136 | trapezium._view = {
137 | y: 0,
138 | base: 5,
139 | x1: 1,
140 | x2: 3,
141 | upperWidth: 2,
142 | bottomWidth: 4,
143 | type :'scalene'
144 | };
145 |
146 | expect(trapezium.inRange(0, 0)).toBe(true);
147 | expect(trapezium.inRange(0, 1)).toBe(false);
148 | expect(trapezium.inRange(2, 2)).toBe(true);
149 | expect(trapezium.inLabelRange(0)).toBe(true);
150 | expect(trapezium.inLabelRange(2)).toBe(true);
151 | expect(trapezium.inLabelRange(5)).toBe(true);
152 | expect(trapezium.inLabelRange(6)).toBe(false);
153 | });
154 |
155 | it ('Scalene trapezium, should get the correct height', function() {
156 | var trapezium = new Chart.elements.Trapezium({
157 | _datasetIndex: 2,
158 | _index: 1
159 | });
160 |
161 | expect(trapezium.height()).toEqual(0);
162 |
163 | trapezium._view = {
164 | y: 0,
165 | base: 10,
166 | x1: 1,
167 | x2: 3,
168 | upperWidth: 2,
169 | bottomWidth: 5,
170 | type :'scalene'
171 | };
172 |
173 | expect(trapezium.height()).toEqual(10);
174 | });
175 |
176 | it ('should draw correctly, no border', function() {
177 | var mockContext = new MockContext();
178 | var trapezium = new Chart.elements.Trapezium({
179 | _datasetIndex: 2,
180 | _index: 1,
181 | _chart: {
182 | ctx: mockContext
183 | }
184 | });
185 |
186 | trapezium._view = {
187 | y: 0,
188 | base: 5,
189 | x: 2,
190 | upperWidth: 2,
191 | bottomWidth: 4
192 | };
193 |
194 | trapezium.draw();
195 |
196 | expect(mockContext.getCalls()).toEqual([{
197 | name: 'beginPath',
198 | args: []
199 | }, {
200 | name: 'setFillStyle',
201 | args: ["rgba(0,0,0,0.1)"]
202 | }, {
203 | name: 'setStrokeStyle',
204 | args: ["rgba(0,0,0,0.1)"]
205 | }, {
206 | name: 'setLineWidth',
207 | args: [0]
208 | }, {
209 | name: 'moveTo',
210 | args: [0,5]
211 | }, {
212 | name: 'lineTo',
213 | args: [1,0]
214 | }, {
215 | name: 'lineTo',
216 | args: [3,0]
217 | }, {
218 | name: 'lineTo',
219 | args: [4,5]
220 | }, {
221 | name: 'fill',
222 | args: []
223 | }]);
224 | });
225 |
226 | it ('should draw correctly with backgroundColor', function() {
227 | var mockContext = new MockContext();
228 | var trapezium = new Chart.elements.Trapezium({
229 | _datasetIndex: 2,
230 | _index: 1,
231 | _chart: {
232 | ctx: mockContext
233 | }
234 | });
235 |
236 | trapezium._view = {
237 | y: 0,
238 | base: 5,
239 | x: 2,
240 | upperWidth: 2,
241 | bottomWidth: 4,
242 |
243 | backgroundColor: "rgba(23,45,223)"
244 | };
245 |
246 | trapezium.draw();
247 |
248 | expect(mockContext.getCalls()).toEqual([{
249 | name: 'beginPath',
250 | args: []
251 | }, {
252 | name: 'setFillStyle',
253 | args: ["rgba(23,45,223)"]
254 | }, {
255 | name: 'setStrokeStyle',
256 | args: ["rgba(0,0,0,0.1)"]
257 | }, {
258 | name: 'setLineWidth',
259 | args: [0]
260 | }, {
261 | name: 'moveTo',
262 | args: [0,5]
263 | }, {
264 | name: 'lineTo',
265 | args: [1,0]
266 | }, {
267 | name: 'lineTo',
268 | args: [3,0]
269 | }, {
270 | name: 'lineTo',
271 | args: [4,5]
272 | }, {
273 | name: 'fill',
274 | args: []
275 | }]);
276 | })
277 |
278 | it ('should draw correctly with border', function() {
279 | var mockContext = new MockContext();
280 | var trapezium = new Chart.elements.Trapezium({
281 | _datasetIndex: 2,
282 | _index: 1,
283 | _chart: {
284 | ctx: mockContext
285 | }
286 | });
287 |
288 | trapezium._view = {
289 | y: 0,
290 | base: 5,
291 | x: 2,
292 | upperWidth: 2,
293 | bottomWidth: 4,
294 |
295 | borderWidth: 3,
296 | backgroundColor: "rgba(23,45,223)"
297 | };
298 |
299 | trapezium.draw();
300 |
301 | expect(mockContext.getCalls()).toEqual([{
302 | name: 'beginPath',
303 | args: []
304 | }, {
305 | name: 'setFillStyle',
306 | args: ["rgba(23,45,223)"]
307 | }, {
308 | name: 'setStrokeStyle',
309 | args: ["rgba(0,0,0,0.1)"]
310 | }, {
311 | name: 'setLineWidth',
312 | args: [3]
313 | }, {
314 | name: 'moveTo',
315 | args: [1.5,5]
316 | }, {
317 | name: 'lineTo',
318 | args: [2.5,1.5]
319 | }, {
320 | name: 'lineTo',
321 | args: [1.5,1.5]
322 | }, {
323 | name: 'lineTo',
324 | args: [2.5,5]
325 | }, {
326 | name: 'fill',
327 | args: []
328 | }, {
329 | name: 'stroke',
330 | args: []
331 | }]);
332 | })
333 | });
--------------------------------------------------------------------------------
/src/controllers/controller.funnel.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * Extend funnel Charts
4 | * @author Jone Casper
5 | * @email xu.chenhui@live.com
6 | *
7 | * @example
8 | * var data = {
9 | * labels: ["A", "B", "C"],
10 | * datasets: [{
11 | * data: [300, 50, 100],
12 | * backgroundColor: [
13 | * "#FF6384",
14 | * "#36A2EB",
15 | * "#FFCE56"
16 | * ],
17 | * hoverBackgroundColor: [
18 | * "#FF6384",
19 | * "#36A2EB",
20 | * "#FFCE56"
21 | * ]
22 | * }]
23 | * }
24 | *
25 | */
26 |
27 | "use strict";
28 |
29 | module.exports = function (Chart) {
30 | var helpers = Chart.helpers;
31 |
32 | Chart.defaults.funnel = {
33 | hover: {
34 | mode: "label"
35 | },
36 | sort: 'asc', // sort options: 'asc', 'desc'
37 | gap: 0,
38 | bottomWidth: null, // the bottom width of funnel
39 | topWidth: 0, // the top width of funnel
40 | keep: 'auto', // Keep left or right
41 | elements: {
42 | borderWidth: 0
43 | },
44 | tooltips: {
45 | callbacks: {
46 | title: function (tooltipItem, data) {
47 | return '';
48 | },
49 | label: function (tooltipItem, data) {
50 | return data.labels[tooltipItem.index] + ': ' + data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index];
51 | }
52 | }
53 | },
54 | legendCallback: function (chart) {
55 | var text = [];
56 | text.push('');
57 |
58 | var data = chart.data;
59 | var datasets = data.datasets;
60 | var labels = data.labels;
61 |
62 | if (datasets.length) {
63 | for (var i = 0; i < datasets[0].data.length; ++i) {
64 | text.push('- ');
65 | if (labels[i]) {
66 | text.push(labels[i]);
67 | }
68 | text.push('
');
69 | }
70 | }
71 |
72 | text.push('
');
73 | return text.join("");
74 | },
75 | legend: {
76 | labels: {
77 | generateLabels: function (chart) {
78 | var data = chart.data;
79 | if (data.labels.length && data.datasets.length) {
80 | return data.labels.map(function (label, i) {
81 | var meta = chart.getDatasetMeta(0);
82 | var ds = data.datasets[0];
83 | var trap = meta.data[i];
84 | var custom = trap.custom || {};
85 | var getValueAtIndexOrDefault = helpers.getValueAtIndexOrDefault;
86 | var trapeziumOpts = chart.options.elements.trapezium;
87 | var fill = custom.backgroundColor ? custom.backgroundColor : getValueAtIndexOrDefault(ds.backgroundColor, i, trapeziumOpts.backgroundColor);
88 | var stroke = custom.borderColor ? custom.borderColor : getValueAtIndexOrDefault(ds.borderColor, i, trapeziumOpts.borderColor);
89 | var bw = custom.borderWidth ? custom.borderWidth : getValueAtIndexOrDefault(ds.borderWidth, i, trapeziumOpts.borderWidth);
90 |
91 | return {
92 | text: label,
93 | fillStyle: fill,
94 | strokeStyle: stroke,
95 | lineWidth: bw,
96 | hidden: isNaN(ds.data[i]) || meta.data[i].hidden,
97 |
98 | // Extra data used for toggling the correct item
99 | index: i,
100 | // Original Index
101 | _index: trap._index
102 | };
103 | });
104 | } else {
105 | return [];
106 | }
107 | }
108 | },
109 |
110 | onClick: function (e, legendItem) {
111 | var index = legendItem.index;
112 | var chart = this.chart;
113 | var i, ilen, meta;
114 |
115 | for (i = 0, ilen = (chart.data.datasets || []).length; i < ilen; ++i) {
116 | meta = chart.getDatasetMeta(i);
117 | meta.data[index].hidden = !meta.data[index].hidden;
118 | }
119 |
120 | chart.update();
121 | }
122 | },
123 | scales: {
124 | xAxes: [{
125 | position: 'left',
126 | type: 'category',
127 | display: false,
128 | // Specific to Horizontal Bar Controller
129 | categoryPercentage: 0.8,
130 | barPercentage: 0.9,
131 |
132 | // offset settings
133 | offset: true,
134 |
135 | // grid line settings
136 | gridLines: {
137 | offsetGridLines: true
138 | }
139 | }],
140 | yAxes: [{
141 | position: 'left',
142 | type: 'category',
143 | display: false,
144 | // Specific to Horizontal Bar Controller
145 | categoryPercentage: 0.8,
146 | barPercentage: 0.9,
147 |
148 | // offset settings
149 | offset: true,
150 |
151 | // grid line settings
152 | gridLines: {
153 | offsetGridLines: true
154 | }
155 | }]
156 | },
157 | };
158 |
159 | Chart.controllers.funnel = Chart.DatasetController.extend({
160 |
161 | dataElementType: Chart.elements.Trapezium,
162 |
163 | initialize: function (chart, datasetIndex) {
164 | Chart.controllers.bar.prototype.initialize.call(this, chart, datasetIndex);
165 | // sort arrays
166 | if (typeof chart.options !== 'undefined' && typeof chart.options.sort !== 'undefined' && chart.options.sort.substr(0, 4) !== 'data') {
167 | var dataset = chart.data.datasets[datasetIndex];
168 | var dataPositions = [];
169 | var originalData = dataset.data.slice();
170 | helpers.each(dataset.data, function (item, index) {
171 | dataPositions.push({index, value: item});
172 | });
173 | dataPositions.sort(function (a, b) {
174 | return chart.options.sort === 'asc' ? a.value - b.value : b.value - a.value;
175 | });
176 | // sort labels in the same manner as data sort order
177 | var labels = chart.data.labels.map((value, index) => {
178 | return chart.data.labels[ dataPositions[index].index ];
179 | });
180 | chart.data.labels = labels;
181 | // sort other arrays inside datasets
182 | var keys = Object.keys(dataset);
183 | for (var i = 0, len = keys.length; i < len; i++) {
184 | var key = keys[i];
185 | var arr = dataset[key];
186 | if (dataset.hasOwnProperty(key) && Array.isArray(arr)) {
187 | var sortedArr = arr.map((item, index) => {
188 | return arr[dataPositions[index].index];
189 | });
190 | dataset[key] = sortedArr;
191 | }
192 | }
193 | }
194 | },
195 |
196 | linkScales: function () {
197 | var me = this;
198 | var meta = me.getMeta();
199 | var dataset = me.getDataset();
200 | if (meta.yAxisID === null || !(meta.yAxisID in me.chart.scales)) {
201 | meta.yAxisID = dataset.yAxisID || me.chart.options.scales.yAxes[0].id;
202 | }
203 | if (meta.xAxisID === null || !(meta.xAxisID in me.chart.scales)) {
204 | meta.xAxisID = dataset.xAxisID || me.chart.options.scales.xAxes[0].id;
205 | }
206 | },
207 |
208 | update: function update(reset) {
209 | var me = this;
210 | var chart = me.chart,
211 | chartArea = chart.chartArea,
212 | opts = chart.options,
213 | meta = me.getMeta(),
214 | elements = meta.data,
215 | borderWidth = opts.elements.borderWidth || 0,
216 | availableWidth = chartArea.right - chartArea.left - borderWidth * 2,
217 | availableHeight = chartArea.bottom - chartArea.top - borderWidth * 2;
218 |
219 | // top and bottom width
220 | var bottomWidth = availableWidth,
221 | topWidth = (opts.topWidth < availableWidth ? opts.topWidth : availableWidth) || 0;
222 | if (opts.bottomWidth) {
223 | bottomWidth = opts.bottomWidth < availableWidth ? opts.bottomWidth : availableWidth;
224 | }
225 |
226 | // percentage calculation and sort data
227 | var dataset = me.getDataset(),
228 | valAndLabels = [],
229 | visiableNum = 0,
230 | dMax = 0;
231 | helpers.each(dataset.data, function (val, index) {
232 | var backgroundColor = helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index),
233 | hidden = elements[index].hidden;
234 | valAndLabels.push({
235 | hidden: hidden,
236 | orgIndex: index,
237 | val: val,
238 | backgroundColor: backgroundColor,
239 | borderColor: helpers.getValueAtIndexOrDefault(dataset.borderColor, index, backgroundColor),
240 | label: helpers.getValueAtIndexOrDefault(dataset.label, index, chart.data.labels[index])
241 | });
242 | if (!elements[index].hidden) {
243 | visiableNum++;
244 | dMax = val > dMax ? val : dMax;
245 | }
246 | });
247 | var dwRatio = bottomWidth / dMax;
248 | // For render hidden view
249 | var _viewIndex = 0;
250 | helpers.each(valAndLabels, function (dal, index) {
251 | dal._viewIndex = !dal.hidden ? _viewIndex++ : -1;
252 | });
253 | // Elements height calculation
254 | var gap = opts.gap || 0,
255 | elHeight = (availableHeight - ((visiableNum - 1) * gap)) / visiableNum;
256 |
257 | // save
258 | me.topWidth = topWidth;
259 | me.dwRatio = dwRatio;
260 | me.elHeight = elHeight;
261 | me.valAndLabels = valAndLabels;
262 | helpers.each(elements, function (trapezium, index) {
263 | me.updateElement(trapezium, index, reset);
264 | }, me);
265 | },
266 |
267 | // update elements
268 | updateElement: function updateElement(trapezium, index, reset) {
269 | var me = this,
270 | chart = me.chart,
271 | chartArea = chart.chartArea,
272 | opts = chart.options,
273 | sort = opts.sort,
274 | dwRatio = me.dwRatio,
275 | elHeight = me.elHeight,
276 | gap = opts.gap || 0,
277 | borderWidth = opts.elements.borderWidth || 0;
278 |
279 | // calculate x,y,base, width,etc.
280 | var x, y, x1, x2,
281 | elementType = 'isosceles',
282 | elementData = me.valAndLabels[index], upperWidth, bottomWidth,
283 | viewIndex = elementData._viewIndex < 0 ? index : elementData._viewIndex,
284 | base = chartArea.top + (viewIndex + 1) * (elHeight + gap) - gap;
285 |
286 | var meta = me.getMeta();
287 | trapezium._xScale = me.getScaleForId(meta.xAxisID);
288 |
289 | if (sort === 'asc' || sort === 'data-asc' || !sort) {
290 | // Find previous element which is visible
291 | var previousElement = helpers.findPreviousWhere(me.valAndLabels,
292 | function (el) {
293 | return !el.hidden;
294 | },
295 | index
296 | );
297 | upperWidth = previousElement ? previousElement.val * dwRatio : me.topWidth;
298 | bottomWidth = elementData.val * dwRatio;
299 | } else if (sort === 'desc' || sort === 'data-desc') {
300 | var nextElement = helpers.findNextWhere(me.valAndLabels,
301 | function (el) {
302 | return !el.hidden;
303 | },
304 | index
305 | );
306 | upperWidth = elementData.val * dwRatio;
307 | bottomWidth = nextElement ? nextElement.val * dwRatio : me.topWidth;
308 | }
309 |
310 | y = chartArea.top + elementData.orgIndex * (elHeight + gap);
311 | if (opts.keep === 'left') {
312 | elementType = 'scalene';
313 | x1 = chartArea.left + upperWidth / 2;
314 | x2 = chartArea.left + bottomWidth / 2;
315 | x = x1;
316 | } else if (opts.keep === 'right') {
317 | elementType = 'scalene';
318 | x1 = chartArea.right - upperWidth / 2;
319 | x2 = chartArea.right - bottomWidth / 2;
320 | x = x1;
321 | } else {
322 | x = (chartArea.left + chartArea.right) / 2;
323 | }
324 |
325 | helpers.extend(trapezium, {
326 | // Utility
327 | _datasetIndex: me.index,
328 | _index: elementData.orgIndex,
329 |
330 | // Desired view properties
331 | _model: {
332 | type: elementType,
333 | y: y,
334 | base: base > chartArea.bottom ? chartArea.bottom : base,
335 | x: x,
336 | x1: x1,
337 | x2: x2,
338 | upperWidth: (reset || !!elementData.hidden) ? 0 : upperWidth,
339 | bottomWidth: (reset || !!elementData.hidden) ? 0 : bottomWidth,
340 | borderWidth: borderWidth,
341 | backgroundColor: elementData && elementData.backgroundColor,
342 | borderColor: elementData && elementData.borderColor,
343 | label: elementData && elementData.label
344 | }
345 | });
346 | trapezium.pivot();
347 | },
348 | removeHoverStyle: function (trapezium) {
349 | Chart.DatasetController.prototype.removeHoverStyle.call(this, trapezium, this.chart.options.elements.trapezium);
350 | }
351 | });
352 | };
353 |
--------------------------------------------------------------------------------
/dist/chart.funnel.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * Chart.Funnel.js
3 | * A funnel plugin for Chart.js(http://chartjs.org/)
4 | * Version: 1.1.5
5 | *
6 | * Copyright 2016 Jone Casaper & YetiForce
7 | * Released under the MIT license
8 | * https://github.com/xch89820/Chart.Funnel.js/blob/master/LICENSE.md
9 | */
10 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}(g.Chart || (g.Chart = {})).Funnel = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i');
88 |
89 | var data = chart.data;
90 | var datasets = data.datasets;
91 | var labels = data.labels;
92 |
93 | if (datasets.length) {
94 | for (var i = 0; i < datasets[0].data.length; ++i) {
95 | text.push('');
96 | if (labels[i]) {
97 | text.push(labels[i]);
98 | }
99 | text.push('');
100 | }
101 | }
102 |
103 | text.push('');
104 | return text.join("");
105 | },
106 | legend: {
107 | labels: {
108 | generateLabels: function (chart) {
109 | var data = chart.data;
110 | if (data.labels.length && data.datasets.length) {
111 | return data.labels.map(function (label, i) {
112 | var meta = chart.getDatasetMeta(0);
113 | var ds = data.datasets[0];
114 | var trap = meta.data[i];
115 | var custom = trap.custom || {};
116 | var getValueAtIndexOrDefault = helpers.getValueAtIndexOrDefault;
117 | var trapeziumOpts = chart.options.elements.trapezium;
118 | var fill = custom.backgroundColor ? custom.backgroundColor : getValueAtIndexOrDefault(ds.backgroundColor, i, trapeziumOpts.backgroundColor);
119 | var stroke = custom.borderColor ? custom.borderColor : getValueAtIndexOrDefault(ds.borderColor, i, trapeziumOpts.borderColor);
120 | var bw = custom.borderWidth ? custom.borderWidth : getValueAtIndexOrDefault(ds.borderWidth, i, trapeziumOpts.borderWidth);
121 |
122 | return {
123 | text: label,
124 | fillStyle: fill,
125 | strokeStyle: stroke,
126 | lineWidth: bw,
127 | hidden: isNaN(ds.data[i]) || meta.data[i].hidden,
128 |
129 | // Extra data used for toggling the correct item
130 | index: i,
131 | // Original Index
132 | _index: trap._index
133 | };
134 | });
135 | } else {
136 | return [];
137 | }
138 | }
139 | },
140 |
141 | onClick: function (e, legendItem) {
142 | var index = legendItem.index;
143 | var chart = this.chart;
144 | var i, ilen, meta;
145 |
146 | for (i = 0, ilen = (chart.data.datasets || []).length; i < ilen; ++i) {
147 | meta = chart.getDatasetMeta(i);
148 | meta.data[index].hidden = !meta.data[index].hidden;
149 | }
150 |
151 | chart.update();
152 | }
153 | },
154 | scales: {
155 | xAxes: [{
156 | position: 'left',
157 | type: 'category',
158 | display: false,
159 | // Specific to Horizontal Bar Controller
160 | categoryPercentage: 0.8,
161 | barPercentage: 0.9,
162 |
163 | // offset settings
164 | offset: true,
165 |
166 | // grid line settings
167 | gridLines: {
168 | offsetGridLines: true
169 | }
170 | }],
171 | yAxes: [{
172 | position: 'left',
173 | type: 'category',
174 | display: false,
175 | // Specific to Horizontal Bar Controller
176 | categoryPercentage: 0.8,
177 | barPercentage: 0.9,
178 |
179 | // offset settings
180 | offset: true,
181 |
182 | // grid line settings
183 | gridLines: {
184 | offsetGridLines: true
185 | }
186 | }]
187 | },
188 | };
189 |
190 | Chart.controllers.funnel = Chart.DatasetController.extend({
191 |
192 | dataElementType: Chart.elements.Trapezium,
193 |
194 | initialize: function (chart, datasetIndex) {
195 | Chart.controllers.bar.prototype.initialize.call(this, chart, datasetIndex);
196 | // sort arrays
197 | if (typeof chart.options !== 'undefined' && typeof chart.options.sort !== 'undefined' && chart.options.sort.substr(0, 4) !== 'data') {
198 | var dataset = chart.data.datasets[datasetIndex];
199 | var dataPositions = [];
200 | var originalData = dataset.data.slice();
201 | helpers.each(dataset.data, function (item, index) {
202 | dataPositions.push({index, value: item});
203 | });
204 | dataPositions.sort(function (a, b) {
205 | return chart.options.sort === 'asc' ? a.value - b.value : b.value - a.value;
206 | });
207 | // sort labels in the same manner as data sort order
208 | var labels = chart.data.labels.map((value, index) => {
209 | return chart.data.labels[ dataPositions[index].index ];
210 | });
211 | chart.data.labels = labels;
212 | // sort other arrays inside datasets
213 | var keys = Object.keys(dataset);
214 | for (var i = 0, len = keys.length; i < len; i++) {
215 | var key = keys[i];
216 | var arr = dataset[key];
217 | if (dataset.hasOwnProperty(key) && Array.isArray(arr)) {
218 | var sortedArr = arr.map((item, index) => {
219 | return arr[dataPositions[index].index];
220 | });
221 | dataset[key] = sortedArr;
222 | }
223 | }
224 | }
225 | },
226 |
227 | linkScales: function () {
228 | var me = this;
229 | var meta = me.getMeta();
230 | var dataset = me.getDataset();
231 | if (meta.yAxisID === null || !(meta.yAxisID in me.chart.scales)) {
232 | meta.yAxisID = dataset.yAxisID || me.chart.options.scales.yAxes[0].id;
233 | }
234 | if (meta.xAxisID === null || !(meta.xAxisID in me.chart.scales)) {
235 | meta.xAxisID = dataset.xAxisID || me.chart.options.scales.xAxes[0].id;
236 | }
237 | },
238 |
239 | update: function update(reset) {
240 | var me = this;
241 | var chart = me.chart,
242 | chartArea = chart.chartArea,
243 | opts = chart.options,
244 | meta = me.getMeta(),
245 | elements = meta.data,
246 | borderWidth = opts.elements.borderWidth || 0,
247 | availableWidth = chartArea.right - chartArea.left - borderWidth * 2,
248 | availableHeight = chartArea.bottom - chartArea.top - borderWidth * 2;
249 |
250 | // top and bottom width
251 | var bottomWidth = availableWidth,
252 | topWidth = (opts.topWidth < availableWidth ? opts.topWidth : availableWidth) || 0;
253 | if (opts.bottomWidth) {
254 | bottomWidth = opts.bottomWidth < availableWidth ? opts.bottomWidth : availableWidth;
255 | }
256 |
257 | // percentage calculation and sort data
258 | var dataset = me.getDataset(),
259 | valAndLabels = [],
260 | visiableNum = 0,
261 | dMax = 0;
262 | helpers.each(dataset.data, function (val, index) {
263 | var backgroundColor = helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index),
264 | hidden = elements[index].hidden;
265 | valAndLabels.push({
266 | hidden: hidden,
267 | orgIndex: index,
268 | val: val,
269 | backgroundColor: backgroundColor,
270 | borderColor: helpers.getValueAtIndexOrDefault(dataset.borderColor, index, backgroundColor),
271 | label: helpers.getValueAtIndexOrDefault(dataset.label, index, chart.data.labels[index])
272 | });
273 | if (!elements[index].hidden) {
274 | visiableNum++;
275 | dMax = val > dMax ? val : dMax;
276 | }
277 | });
278 | var dwRatio = bottomWidth / dMax;
279 | // For render hidden view
280 | var _viewIndex = 0;
281 | helpers.each(valAndLabels, function (dal, index) {
282 | dal._viewIndex = !dal.hidden ? _viewIndex++ : -1;
283 | });
284 | // Elements height calculation
285 | var gap = opts.gap || 0,
286 | elHeight = (availableHeight - ((visiableNum - 1) * gap)) / visiableNum;
287 |
288 | // save
289 | me.topWidth = topWidth;
290 | me.dwRatio = dwRatio;
291 | me.elHeight = elHeight;
292 | me.valAndLabels = valAndLabels;
293 | helpers.each(elements, function (trapezium, index) {
294 | me.updateElement(trapezium, index, reset);
295 | }, me);
296 | },
297 |
298 | // update elements
299 | updateElement: function updateElement(trapezium, index, reset) {
300 | var me = this,
301 | chart = me.chart,
302 | chartArea = chart.chartArea,
303 | opts = chart.options,
304 | sort = opts.sort,
305 | dwRatio = me.dwRatio,
306 | elHeight = me.elHeight,
307 | gap = opts.gap || 0,
308 | borderWidth = opts.elements.borderWidth || 0;
309 |
310 | // calculate x,y,base, width,etc.
311 | var x, y, x1, x2,
312 | elementType = 'isosceles',
313 | elementData = me.valAndLabels[index], upperWidth, bottomWidth,
314 | viewIndex = elementData._viewIndex < 0 ? index : elementData._viewIndex,
315 | base = chartArea.top + (viewIndex + 1) * (elHeight + gap) - gap;
316 |
317 | var meta = me.getMeta();
318 | trapezium._xScale = me.getScaleForId(meta.xAxisID);
319 |
320 | if (sort === 'asc' || sort === 'data-asc' || !sort) {
321 | // Find previous element which is visible
322 | var previousElement = helpers.findPreviousWhere(me.valAndLabels,
323 | function (el) {
324 | return !el.hidden;
325 | },
326 | index
327 | );
328 | upperWidth = previousElement ? previousElement.val * dwRatio : me.topWidth;
329 | bottomWidth = elementData.val * dwRatio;
330 | } else if (sort === 'desc' || sort === 'data-desc') {
331 | var nextElement = helpers.findNextWhere(me.valAndLabels,
332 | function (el) {
333 | return !el.hidden;
334 | },
335 | index
336 | );
337 | upperWidth = elementData.val * dwRatio;
338 | bottomWidth = nextElement ? nextElement.val * dwRatio : me.topWidth;
339 | }
340 |
341 | y = chartArea.top + elementData.orgIndex * (elHeight + gap);
342 | if (opts.keep === 'left') {
343 | elementType = 'scalene';
344 | x1 = chartArea.left + upperWidth / 2;
345 | x2 = chartArea.left + bottomWidth / 2;
346 | x = x1;
347 | } else if (opts.keep === 'right') {
348 | elementType = 'scalene';
349 | x1 = chartArea.right - upperWidth / 2;
350 | x2 = chartArea.right - bottomWidth / 2;
351 | x = x1;
352 | } else {
353 | x = (chartArea.left + chartArea.right) / 2;
354 | }
355 |
356 | helpers.extend(trapezium, {
357 | // Utility
358 | _datasetIndex: me.index,
359 | _index: elementData.orgIndex,
360 |
361 | // Desired view properties
362 | _model: {
363 | type: elementType,
364 | y: y,
365 | base: base > chartArea.bottom ? chartArea.bottom : base,
366 | x: x,
367 | x1: x1,
368 | x2: x2,
369 | upperWidth: (reset || !!elementData.hidden) ? 0 : upperWidth,
370 | bottomWidth: (reset || !!elementData.hidden) ? 0 : bottomWidth,
371 | borderWidth: borderWidth,
372 | backgroundColor: elementData && elementData.backgroundColor,
373 | borderColor: elementData && elementData.borderColor,
374 | label: elementData && elementData.label
375 | }
376 | });
377 | trapezium.pivot();
378 | },
379 | removeHoverStyle: function (trapezium) {
380 | Chart.DatasetController.prototype.removeHoverStyle.call(this, trapezium, this.chart.options.elements.trapezium);
381 | }
382 | });
383 | };
384 |
385 | },{}],4:[function(require,module,exports){
386 | /**
387 | *
388 | * Extend trapezium element
389 | * @author Jone Casper
390 | * @email xu.chenhui@live.com
391 | *
392 | */
393 |
394 | "use strict";
395 |
396 | module.exports = function (Chart) {
397 | var helpers = Chart.helpers,
398 | globalOpts = Chart.defaults.global;
399 |
400 | globalOpts.elements.trapezium = {
401 | backgroundColor: globalOpts.defaultColor,
402 | borderWidth: 0,
403 | borderColor: globalOpts.defaultColor,
404 | borderSkipped: 'bottom',
405 | type: 'isosceles' // isosceles, scalene
406 | };
407 |
408 | // Thanks for https://github.com/substack/point-in-polygon
409 | var pointInPolygon = function (point, vs) {
410 | // ray-casting algorithm based on
411 | // http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
412 |
413 | var x = point[0], y = point[1];
414 |
415 | var inside = false;
416 | for (var i = 0, j = vs.length - 1; i < vs.length; j = i++) {
417 | var xi = vs[i][0], yi = vs[i][1];
418 | var xj = vs[j][0], yj = vs[j][1];
419 |
420 | var intersect = ((yi > y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
421 | if (intersect)
422 | inside = !inside;
423 | }
424 |
425 | return inside;
426 | };
427 |
428 | Chart.elements.Trapezium = Chart.elements.Rectangle.extend({
429 | getCorners: function () {
430 | var vm = this._view;
431 | var globalOptionTrapeziumElements = globalOpts.elements.trapezium;
432 |
433 | var corners = [],
434 | type = vm.type || globalOptionTrapeziumElements.type,
435 | top = vm.y,
436 | borderWidth = vm.borderWidth || globalOptionTrapeziumElements.borderWidth,
437 | upHalfWidth = vm.upperWidth / 2,
438 | botHalfWidth = vm.bottomWidth / 2,
439 | halfStroke = borderWidth / 2;
440 |
441 | halfStroke = halfStroke < 0 ? 0 : halfStroke;
442 |
443 | // An isosceles trapezium
444 | if (type == 'isosceles') {
445 | var x = vm.x;
446 |
447 | // Corner points, from bottom-left to bottom-right clockwise
448 | // | 1 2 |
449 | // | 0 3 |
450 | corners = [
451 | [x - botHalfWidth + halfStroke, vm.base],
452 | [x - upHalfWidth + halfStroke, top + halfStroke],
453 | [x + upHalfWidth - halfStroke, top + halfStroke],
454 | [x + botHalfWidth - halfStroke, vm.base]
455 | ];
456 | } else if (type == 'scalene') {
457 | var x1 = vm.x1,
458 | x2 = vm.x2;
459 |
460 | corners = [
461 | [x2 - botHalfWidth + halfStroke, vm.base],
462 | [x1 - upHalfWidth + halfStroke, top + halfStroke],
463 | [x1 + upHalfWidth - halfStroke, top + halfStroke],
464 | [x2 + botHalfWidth - halfStroke, vm.base]
465 | ];
466 | }
467 |
468 |
469 | return corners;
470 | },
471 | draw: function () {
472 | var ctx = this._chart.ctx;
473 | var vm = this._view;
474 | var globalOptionTrapeziumElements = globalOpts.elements.trapezium;
475 |
476 | var corners = this.getCorners();
477 | this._cornersCache = corners;
478 |
479 | ctx.beginPath();
480 | ctx.fillStyle = vm.backgroundColor || globalOptionTrapeziumElements.backgroundColor;
481 | ctx.strokeStyle = vm.borderColor || globalOptionTrapeziumElements.borderColor;
482 | ctx.lineWidth = vm.borderWidth || globalOptionTrapeziumElements.borderWidth;
483 |
484 | // Find first (starting) corner with fallback to 'bottom'
485 | var borders = ['bottom', 'left', 'top', 'right'];
486 | var startCorner = borders.indexOf(
487 | vm.borderSkipped || globalOptionTrapeziumElements.borderSkipped,
488 | 0);
489 | if (startCorner === -1)
490 | startCorner = 0;
491 |
492 | function cornerAt(index) {
493 | return corners[(startCorner + index) % 4];
494 | }
495 |
496 | // Draw rectangle from 'startCorner'
497 | ctx.moveTo.apply(ctx, cornerAt(0));
498 | for (var i = 1; i < 4; i++)
499 | ctx.lineTo.apply(ctx, cornerAt(i));
500 |
501 | ctx.fill();
502 | if (vm.borderWidth) {
503 | ctx.stroke();
504 | }
505 | },
506 | height: function () {
507 | var vm = this._view;
508 | if (!vm) {
509 | return 0;
510 | }
511 |
512 | return vm.base - vm.y;
513 | },
514 | inRange: function (mouseX, mouseY) {
515 | var vm = this._view;
516 | if (!vm) {
517 | return false;
518 | }
519 | var corners = this._cornersCache ? this._cornersCache : this.getCorners();
520 | return pointInPolygon([mouseX, mouseY], corners);
521 | },
522 | inLabelRange: function (mouseX) {
523 | var x,
524 | vm = this._view;
525 |
526 | if (!vm) {
527 | return false;
528 | }
529 |
530 | if (vm.type == 'scalene') {
531 | if (vm.x1 > vm.x2) {
532 | return mouseX >= vm.x2 - vm.bottomWidth / 2 && mouseX <= vm.x1 + vm.upperWidth / 2;
533 | } else {
534 | return mouseX <= vm.x2 + vm.bottomWidth / 2 && mouseX >= vm.x1 - vm.upperWidth / 2;
535 | }
536 | }
537 |
538 | var maxWidth = Math.max(vm.upperWidth, vm.bottomWidth);
539 | return mouseX >= vm.x - maxWidth / 2 && mouseX <= vm.x + maxWidth / 2;
540 | },
541 | tooltipPosition: function () {
542 | var vm = this._view;
543 | return {
544 | x: vm.x || vm.x2,
545 | y: vm.base - (vm.base - vm.y) / 2
546 | };
547 | },
548 | getArea: function () {
549 | var vm = this._view;
550 | var total = 0;
551 | var corners = this._cornersCache ? this._cornersCache : this.getCorners();
552 | for (var i = 0, l = corners.length; i < l; i++) {
553 | var addX = corners[i][0];
554 | var addY = corners[i == corners.length - 1 ? 0 : i + 1][1];
555 | var subX = corners[i == corners.length - 1 ? 0 : i + 1][0];
556 | var subY = corners[i][1];
557 | total += (addX * addY * 0.5);
558 | total -= (subX * subY * 0.5);
559 | }
560 | return Math.abs(total);
561 | },
562 | getCenterPoint: function () {
563 | var corners = this._cornersCache ? this._cornersCache : this.getCorners();
564 | var vm = this._view;
565 | var x = 0, y = 0, i, j, f, point1, point2;
566 | for (i = 0, j = corners.length - 1; i < corners.length; j = i, i++) {
567 | point1 = corners[i];
568 | point2 = corners[j];
569 | f = point1[0] * point2[1] - point2[0] * point1[1];
570 | x += (point1[0] + point2[0]) * f;
571 | y += (point1[1] + point2[1]) * f;
572 | }
573 | f = this.getArea() * 6;
574 | return {x: x / f, y: y / f};
575 | },
576 | });
577 | };
578 |
579 | },{}]},{},[2])(2)
580 | });
581 |
--------------------------------------------------------------------------------
/dist/chart.funnel.min.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"chart.funnel.min.js","sources":["chart.funnel.min.js"],"sourcesContent":["!function(e){if(\"object\"==typeof exports&&\"undefined\"!=typeof module)module.exports=e();else if(\"function\"==typeof define&&define.amd)define([],e);else{var t;((t=\"undefined\"!=typeof window?window:\"undefined\"!=typeof global?global:\"undefined\"!=typeof self?self:this).Chart||(t.Chart={})).Funnel=e()}}(function(){return function(){return function e(t,r,o){function a(i,l){if(!r[i]){if(!t[i]){var s=\"function\"==typeof require&&require;if(!l&&s)return s(i,!0);if(n)return n(i,!0);var d=new Error(\"Cannot find module '\"+i+\"'\");throw d.code=\"MODULE_NOT_FOUND\",d}var u=r[i]={exports:{}};t[i][0].call(u.exports,function(e){return a(t[i][1][e]||e)},u,u.exports,e,t,r,o)}return r[i].exports}for(var n=\"function\"==typeof require&&require,i=0;i');var r=e.data,o=r.datasets,a=r.labels;if(o.length)for(var n=0;n'),a[n]&&t.push(a[n]),t.push(\"\");return t.push(\"\"),t.join(\"\")},legend:{labels:{generateLabels:function(e){var r=e.data;return r.labels.length&&r.datasets.length?r.labels.map(function(o,a){var n=e.getDatasetMeta(0),i=r.datasets[0],l=n.data[a],s=l.custom||{},d=t.getValueAtIndexOrDefault,u=e.options.elements.trapezium;return{text:o,fillStyle:s.backgroundColor?s.backgroundColor:d(i.backgroundColor,a,u.backgroundColor),strokeStyle:s.borderColor?s.borderColor:d(i.borderColor,a,u.borderColor),lineWidth:s.borderWidth?s.borderWidth:d(i.borderWidth,a,u.borderWidth),hidden:isNaN(i.data[a])||n.data[a].hidden,index:a,_index:l._index}}):[]}},onClick:function(e,t){var r,o,a,n=t.index,i=this.chart;for(r=0,o=(i.data.datasets||[]).length;rr.data.labels[n[t].index]);r.data.labels=i;for(var l=Object.keys(a),s=0,d=l.length;sh[n[t].index]);a[u]=c}}}},linkScales:function(){var e=this,t=e.getMeta(),r=e.getDataset();null!==t.yAxisID&&t.yAxisID in e.chart.scales||(t.yAxisID=r.yAxisID||e.chart.options.scales.yAxes[0].id),null!==t.xAxisID&&t.xAxisID in e.chart.scales||(t.xAxisID=r.xAxisID||e.chart.options.scales.xAxes[0].id)},update:function(e){var r=this,o=r.chart,a=o.chartArea,n=o.options,i=r.getMeta().data,l=n.elements.borderWidth||0,s=a.right-a.left-2*l,d=a.bottom-a.top-2*l,u=s,h=(n.topWidthb?e:b)});var g=u/b,x=0;t.each(f,function(e,t){e._viewIndex=e.hidden?-1:x++});var v=n.gap||0,y=(d-(p-1)*v)/p;r.topWidth=h,r.dwRatio=g,r.elHeight=y,r.valAndLabels=f,t.each(i,function(t,o){r.updateElement(t,o,e)},r)},updateElement:function(e,r,o){var a,n,i,l,s,d,u=this,h=u.chart,c=h.chartArea,f=h.options,p=f.sort,b=u.dwRatio,g=u.elHeight,x=f.gap||0,v=f.elements.borderWidth||0,y=\"isosceles\",m=u.valAndLabels[r],C=m._viewIndex<0?r:m._viewIndex,W=c.top+(C+1)*(g+x)-x,k=u.getMeta();if(e._xScale=u.getScaleForId(k.xAxisID),\"asc\"!==p&&\"data-asc\"!==p&&p){if(\"desc\"===p||\"data-desc\"===p){var A=t.findNextWhere(u.valAndLabels,function(e){return!e.hidden},r);s=m.val*b,d=A?A.val*b:u.topWidth}}else{var _=t.findPreviousWhere(u.valAndLabels,function(e){return!e.hidden},r);s=_?_.val*b:u.topWidth,d=m.val*b}n=c.top+m.orgIndex*(g+x),\"left\"===f.keep?(y=\"scalene\",i=c.left+s/2,l=c.left+d/2,a=i):\"right\"===f.keep?(y=\"scalene\",i=c.right-s/2,l=c.right-d/2,a=i):a=(c.left+c.right)/2,t.extend(e,{_datasetIndex:u.index,_index:m.orgIndex,_model:{type:y,y:n,base:W>c.bottom?c.bottom:W,x:a,x1:i,x2:l,upperWidth:o||m.hidden?0:s,bottomWidth:o||m.hidden?0:d,borderWidth:v,backgroundColor:m&&m.backgroundColor,borderColor:m&&m.borderColor,label:m&&m.label}}),e.pivot()},removeHoverStyle:function(t){e.DatasetController.prototype.removeHoverStyle.call(this,t,this.chart.options.elements.trapezium)}})}},{}],4:[function(e,t,r){\"use strict\";t.exports=function(e){e.helpers;var t=e.defaults.global;t.elements.trapezium={backgroundColor:t.defaultColor,borderWidth:0,borderColor:t.defaultColor,borderSkipped:\"bottom\",type:\"isosceles\"};e.elements.Trapezium=e.elements.Rectangle.extend({getCorners:function(){var e=this._view,r=t.elements.trapezium,o=[],a=e.type||r.type,n=e.y,i=e.borderWidth||r.borderWidth,l=e.upperWidth/2,s=e.bottomWidth/2,d=i/2;if(d=d<0?0:d,\"isosceles\"==a){var u=e.x;o=[[u-s+d,e.base],[u-l+d,n+d],[u+l-d,n+d],[u+s-d,e.base]]}else if(\"scalene\"==a){var h=e.x1,c=e.x2;o=[[c-s+d,e.base],[h-l+d,n+d],[h+l-d,n+d],[c+s-d,e.base]]}return o},draw:function(){var e=this._chart.ctx,r=this._view,o=t.elements.trapezium,a=this.getCorners();this._cornersCache=a,e.beginPath(),e.fillStyle=r.backgroundColor||o.backgroundColor,e.strokeStyle=r.borderColor||o.borderColor,e.lineWidth=r.borderWidth||o.borderWidth;var n=[\"bottom\",\"left\",\"top\",\"right\"].indexOf(r.borderSkipped||o.borderSkipped,0);function i(e){return a[(n+e)%4]}-1===n&&(n=0),e.moveTo.apply(e,i(0));for(var l=1;l<4;l++)e.lineTo.apply(e,i(l));e.fill(),r.borderWidth&&e.stroke()},height:function(){var e=this._view;return e?e.base-e.y:0},inRange:function(e,t){return!!this._view&&function(e,t){for(var r=e[0],o=e[1],a=!1,n=0,i=t.length-1;no!=u>o&&r<(d-l)*(o-s)/(u-s)+l&&(a=!a)}return a}([e,t],this._cornersCache?this._cornersCache:this.getCorners())},inLabelRange:function(e){var t=this._view;if(!t)return!1;if(\"scalene\"==t.type)return t.x1>t.x2?e>=t.x2-t.bottomWidth/2&&e<=t.x1+t.upperWidth/2:e<=t.x2+t.bottomWidth/2&&e>=t.x1-t.upperWidth/2;var r=Math.max(t.upperWidth,t.bottomWidth);return e>=t.x-r/2&&e<=t.x+r/2},tooltipPosition:function(){var e=this._view;return{x:e.x||e.x2,y:e.base-(e.base-e.y)/2}},getArea:function(){this._view;for(var e=0,t=this._cornersCache?this._cornersCache:this.getCorners(),r=0,o=t.length;r