├── 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 | [![Build Status](https://travis-ci.org/xch89820/Chart.Funnel.js.svg?branch=master)](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 | ![chartjs-plugin-funnel](https://user-images.githubusercontent.com/36499752/37270921-0021a42c-25d1-11e8-8823-926758ad0061.jpg) 22 | 23 | 24 | ![chartjs-plugin-funnel](https://user-images.githubusercontent.com/36499752/37270922-003b924c-25d1-11e8-9795-11a6dc35c68a.jpg) 25 | 26 | 27 | ![chartjs-plugin-funnel](https://user-images.githubusercontent.com/36499752/37270924-00574fd2-25d1-11e8-933d-1a07ee16862d.jpg) 28 | 29 | 30 | ![chartjs-plugin-funnel](https://user-images.githubusercontent.com/36499752/37288346-514d2f0c-2607-11e8-8bcb-eacabd470d8f.jpg) 31 | 32 | 33 | ![chartjs-plugin-funnel](https://user-images.githubusercontent.com/36499752/37288348-521abcb0-2607-11e8-87cb-b52fc5575f44.jpg) 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