├── .prettierrc ├── src ├── index.ts ├── tools.ts ├── regression-plugin.ts ├── MetaData.ts ├── types.ts └── MetaSection.ts ├── tslint.json ├── tsconfig.json ├── bin └── setversion.js ├── .gitignore ├── LICENSE ├── webpack.config.js ├── demo ├── demo.css ├── index.html ├── demoHelpers.js ├── demo.js └── demoData.js ├── package.json ├── README.md └── dist ├── chartjs-plugin-regression-0.2.1.js ├── chartjs-plugin-regression-0.1.1.js └── chartjs-plugin-regression-0.2.0.js /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "trailingComma": "none", 4 | "singleQuote": true, 5 | "arrowParens": "avoid" 6 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export * from './MetaData'; 3 | export * from './MetaSection'; 4 | export * from './regression-plugin'; 5 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-config-prettier"], 3 | "rules": { 4 | "triple-equals": false, 5 | "one-variable-per-declaration": false, 6 | "no-bitwise": false 7 | } 8 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["ES2016", "DOM"], 5 | "module": "commonjs", 6 | "declaration": true, 7 | "outDir": "./lib", 8 | "strict": true, 9 | "sourceMap": true 10 | }, 11 | "include": ["src"], 12 | "exclude": ["node_modules", "**/__tests__/*"] 13 | } -------------------------------------------------------------------------------- /bin/setversion.js: -------------------------------------------------------------------------------- 1 | #!/bin/env node 2 | 3 | const 4 | pck = require('../package.json'), 5 | fs = require('fs'), 6 | files = ['./demo/index.html', './README.md']; 7 | 8 | files.forEach(replaceVersion); 9 | 10 | function replaceVersion(file) { 11 | var index = fs.readFileSync(file, 'utf8'); 12 | index = index.replace(/(dist\/chartjs-plugin-regression-).*?\.js/, `$1${pck.version}.js`); 13 | fs.writeFileSync(file, index, 'utf8'); 14 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Maps 7 | *.map 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # Dependency directories 26 | node_modules/ 27 | 28 | # TypeScript v1 declaration files 29 | typings/ 30 | 31 | # TypeScript cache 32 | *.tsbuildinfo 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional eslint cache 38 | .eslintcache 39 | 40 | # Output of 'npm pack' 41 | *.tgz 42 | 43 | # build / generate output 44 | /lib 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2020, Pomgui Informatica Ltda. 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const pck = require('./package.json'); 3 | 4 | module.exports = (env, argv) => { 5 | const config = { 6 | entry: './lib/index.js', 7 | mode: 'development', 8 | devtool: 'source-map', 9 | output: { 10 | path: path.resolve(__dirname, 'dist'), 11 | filename: 'chartjs-plugin-regression-' + pck.version + '.js' 12 | } 13 | }; 14 | 15 | if (argv.mode == 'production') { 16 | Object.assign(config, { 17 | mode: 'production', 18 | devtool: undefined, 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.js$/, 23 | exclude: /node_modules/, 24 | use: ['source-map-loader'], 25 | enforce: 'pre' 26 | } 27 | ] 28 | } 29 | }); 30 | } 31 | 32 | return config; 33 | }; 34 | -------------------------------------------------------------------------------- /src/tools.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Makes a deep copy of the object including a copy of all the children and so on. 3 | * @param source source object. It can be any type 4 | * @param fields list of root fields that should be copied. If not sent, it will copy all the fields. 5 | * Note: the fields only filters the children of the source not the grandchildren. 6 | */ 7 | export function deepCopy(source: T, fields?: string[]): T { 8 | let target: T = source; 9 | if (source && (typeof source === 'object' || Array.isArray(source))) { 10 | if (source instanceof Date) 11 | return (new Date(source.getTime()) as unknown) as T; 12 | target = (Array.isArray(source) ? [] : {}) as any; 13 | if (!fields) 14 | for (const field in source) target[field] = deepCopy(source[field]); 15 | else 16 | fields.forEach( 17 | field => ((target as any)[field] = deepCopy((source as any)[field])) 18 | ); 19 | } 20 | return target; 21 | } 22 | -------------------------------------------------------------------------------- /demo/demo.css: -------------------------------------------------------------------------------- 1 | body * { 2 | box-sizing: border-box; 3 | } 4 | 5 | h1, 6 | h2, 7 | h3 { 8 | margin-bottom: 0; 9 | } 10 | 11 | .sample>.row { 12 | height: 300px; 13 | } 14 | 15 | .row::after { 16 | content: ''; 17 | display: table; 18 | clear : both; 19 | } 20 | 21 | .cntr { 22 | position: relative; 23 | padding : 10px; 24 | height : 100%; 25 | } 26 | 27 | .cntr>* { 28 | position: absolute; 29 | left : 0; 30 | top : 0; 31 | right : 0; 32 | bottom : 0; 33 | overflow: auto; 34 | border : 1px solid #aaa; 35 | padding : 5px; 36 | margin : 5px; 37 | } 38 | 39 | .cntr.chart { 40 | width: 70%; 41 | float: left; 42 | } 43 | 44 | .cntr.params { 45 | width: 30%; 46 | float: right; 47 | } 48 | 49 | pi-chart canvas { 50 | width : 100%; 51 | height: 100%; 52 | } 53 | 54 | pi-chart>.info { 55 | font : 16px Arial, helvetica, sans-serif; 56 | position: absolute; 57 | left : 52px; 58 | top : 48px; 59 | } 60 | 61 | .info, 62 | .info td { 63 | border : 1px solid #aaa; 64 | background-color: #fff8; 65 | } 66 | 67 | button { 68 | position: absolute; 69 | right : 0; 70 | top : 0; 71 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chartjs-plugin-regression", 3 | "version": "0.2.1", 4 | "description": "Plugin to draw a regression line", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "build": "tsc && webpack --mode development", 8 | "format": "prettier --write \"src/**/*.ts\" \"demo/**/*.js\"", 9 | "lint": "tslint -p tsconfig.json", 10 | "test": "echo \"Warning: no test specified\"", 11 | "preversion": "npm test", 12 | "version": "node bin/setversion.js && npm run prepare && git add -A", 13 | "postversion": "git push && git push --tags", 14 | "prepare": "find . -name '*.map' -delete; tsc && webpack --mode production" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/pomgui/chartjs-plugin-regression.git" 19 | }, 20 | "keywords": [ 21 | "charts", 22 | "chartjs", 23 | "regression", 24 | "data", 25 | "fiting", 26 | "analysis" 27 | ], 28 | "bugs": { 29 | "url": "https://github.com/pomgui/chartjs-plugin-regression/issues" 30 | }, 31 | "homepage": "https://github.com/pomgui/chartjs-plugin-regression#readme", 32 | "author": "Wilfredo Pomier (wpomier@pomgui.com)", 33 | "license": "ISC", 34 | "devDependencies": { 35 | "@types/chart.js": "^2.9.22", 36 | "@types/regression": "^2.0.0", 37 | "prettier": "^2.0.5", 38 | "source-map-loader": "^1.0.1", 39 | "tslint": "^6.1.2", 40 | "tslint-config-prettier": "^1.18.0", 41 | "typescript": "^3.9.6" 42 | }, 43 | "dependencies": { 44 | "regression": "^2.0.1" 45 | }, 46 | "peerDependencies": { 47 | "chart.js": "^2.6.0" 48 | }, 49 | "files": [ 50 | "lib/**/*" 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | chartjs-plugin-regression 7 | 9 | 10 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |

23 |
24 |

25 |
26 |
27 | 28 | 29 |
30 |
31 |
32 |
33 |

Global Options Config

34 |

36 |                             
37 |
38 |

Dataset Config

39 |

41 |                             
42 |
43 |
44 |
45 |
46 |
47 |
48 | 49 | 50 | -------------------------------------------------------------------------------- /demo/demoHelpers.js: -------------------------------------------------------------------------------- 1 | var helpers = new (function () { 2 | this.generateRandomData = function (numElems, type, prediction) { 3 | var types = { linear, logarithmic, power, exponential }; 4 | var fn = (types[type] || types.exponential)(); 5 | var delta = 0; 6 | return new Array(numElems).fill(0).map(function (v, i) { 7 | return !i || (prediction == 'last' && i >= demo.NUM_NORMAL_ELEMS) 8 | ? null 9 | : fn(i) + helpers.rnd(delta); 10 | }); 11 | }; 12 | 13 | function linear() { 14 | var A = helpers.rnd(10) + 10, 15 | B = helpers.rnd(100), 16 | rnd = f(demo.NUM_ELEMS_4); 17 | function f(x) { 18 | return A * x + B; 19 | } 20 | return function (x) { 21 | return f(x) + Math.random() * rnd; 22 | }; 23 | } 24 | function exponential() { 25 | var A = Math.random() * 10, 26 | B = Math.random() * 0.2 + 1e-2, 27 | rnd = f(demo.NUM_ELEMS_2); 28 | function f(x) { 29 | return A * Math.exp(x * B); 30 | } 31 | return function (x) { 32 | return f(x) + Math.random() * rnd; 33 | }; 34 | } 35 | function logarithmic() { 36 | var A = helpers.rnd(200), 37 | B = helpers.rnd(50) + 1, 38 | rnd = f(0.5); 39 | function f(x) { 40 | return A + B * Math.log(x); 41 | } 42 | return function (x) { 43 | return f(x) + Math.random() * rnd; 44 | }; 45 | } 46 | function power() { 47 | var A = Math.random() * 2 + 0.2, 48 | B = Math.random() * 2 + 0.2, 49 | rnd = f(demo.NUM_ELEMS_4); 50 | function f(x) { 51 | return A * Math.pow(x, B); 52 | } 53 | return function (x) { 54 | return f(x) + Math.random() * rnd; 55 | }; 56 | } 57 | 58 | this.generateColors = function (numElems, id, alpha) { 59 | var normal = this.color(id, alpha); 60 | var predicted = 'rgba(136,136,136,' + alpha + ')'; 61 | var colors = new Array(numElems); 62 | for (var i = 0; i < numElems; i++) 63 | colors[i] = i < demo.NUM_NORMAL_ELEMS ? normal : predicted; 64 | return colors; 65 | }; 66 | 67 | this.rnd = function (max) { 68 | return Math.trunc(max * Math.random()); 69 | }; 70 | 71 | this.formatJson = function (cfg) { 72 | var s = JSON.stringify(cfg, jsonReplacer, 2) 73 | .replace(/"(\w+)":/g, '$1:') 74 | .replace(/\{[^{}}]+\}/gm, strReplacer) 75 | .replace(/\[[^\[\]]+\]/gm, strReplacer); 76 | return s; 77 | function jsonReplacer(k, v) { 78 | return k == '$$hashKey' ? undefined : v; 79 | } 80 | function strReplacer(g) { 81 | return g.length < 80 ? g.replace(/\s+/g, ' ') : g; 82 | } 83 | }; 84 | 85 | this.color = function (id, alpha) { 86 | const c = new Color({ h: id * 16, s: 100, l: 50 }); 87 | c.alpha(alpha); 88 | return c.rgbaString(); 89 | } 90 | })(); 91 | -------------------------------------------------------------------------------- /src/regression-plugin.ts: -------------------------------------------------------------------------------- 1 | import { MetaDataSet } from './MetaData'; 2 | import { 3 | PluginServiceGlobalRegistration, 4 | PluginServiceRegistrationOptions, 5 | Easing 6 | } from 'chart.js'; 7 | import { MetaSection } from './MetaSection'; 8 | import { ChartDataSetsEx, OptionsConfig } from './types'; 9 | 10 | // Cache for all plugins' metadata 11 | var _metadataMap: any = {}; 12 | var _chartId = 0; 13 | 14 | interface ChartEx extends Chart { 15 | $$id: number; 16 | } 17 | 18 | class Plugin 19 | implements PluginServiceGlobalRegistration, PluginServiceRegistrationOptions { 20 | id = 'regressions'; 21 | 22 | beforeInit(chart: any) { 23 | chart.$$id = ++_chartId; 24 | } 25 | 26 | /** 27 | * Called after update (when the chart is created and when chart.update() is called) 28 | * @param chart 29 | */ 30 | beforeUpdate?(chart: Chart, options?: any): void { 31 | let o, p, r: OptionsConfig; 32 | const onComplete = 33 | (o = chart.config.options) && 34 | (p = o.plugins) && 35 | (r = p.regressions) && 36 | r.onCompleteCalculation; 37 | 38 | forEach(chart, (ds, meta, datasetIndex) => { 39 | meta = new MetaDataSet(chart, ds); 40 | const id = (chart as ChartEx).$$id * 1000 + datasetIndex; 41 | _metadataMap[id] = meta; 42 | }); 43 | if (onComplete) onComplete(chart); 44 | } 45 | 46 | /** 47 | * It's called once before all the drawing 48 | * @param chart 49 | */ 50 | beforeRender(chart: Chart, options?: any): void { 51 | forEach(chart, (ds, meta) => meta.adjustScales()); 52 | } 53 | 54 | /** Draws the vertical lines before the datasets are drawn */ 55 | beforeDatasetsDraw(chart: Chart, easing: Easing, options?: any): void { 56 | forEach(chart, (ds, meta) => meta.drawRightBorders()); 57 | } 58 | 59 | /** Draws the regression lines */ 60 | afterDatasetsDraw(chart: Chart, easing: Easing, options?: any): void { 61 | forEach(chart, (ds, meta) => meta.drawRegressions()); 62 | } 63 | 64 | destroy(chart: Chart): void { 65 | Object.keys(_metadataMap) 66 | .filter((k: any) => (k / 1000) >> 0 == (chart as ChartEx).$$id) 67 | .forEach(k => delete _metadataMap[k]); 68 | } 69 | 70 | /** Get dataset's meta data */ 71 | getDataset(chart: ChartEx, datasetIndex: number): MetaDataSet { 72 | const id = chart.$$id * 1000 + datasetIndex; 73 | return _metadataMap[id]; 74 | } 75 | 76 | /** Get dataset's meta sections */ 77 | getSections(chart: ChartEx, datasetIndex: number): MetaSection[] { 78 | const ds = this.getDataset(chart, datasetIndex); 79 | return ds && ds.sections; 80 | } 81 | } 82 | 83 | function forEach( 84 | chart: any, 85 | fn: (ds: ChartDataSetsEx, meta: MetaDataSet, datasetIndex: number) => void 86 | ) { 87 | chart.data.datasets.forEach((ds: ChartDataSetsEx, i: number) => { 88 | if (ds.regressions && chart.isDatasetVisible(i)) { 89 | const meta = ChartRegressions.getDataset(chart, i); 90 | fn(ds, meta, i); 91 | } 92 | }); 93 | } 94 | 95 | export const ChartRegressions = new Plugin(); 96 | 97 | declare var window: any; 98 | window.ChartRegressions = ChartRegressions; 99 | -------------------------------------------------------------------------------- /src/MetaData.ts: -------------------------------------------------------------------------------- 1 | import { Options, DataPoint } from 'regression'; 2 | import { Section, LineOptions, ChartDataSetsEx, DatasetConfig } from './types'; 3 | import { Point } from 'chart.js'; 4 | import { MetaSection } from './MetaSection'; 5 | 6 | type GetXY = (x: number, y: number) => Point; 7 | 8 | export class MetaDataSet { 9 | chart: Chart; 10 | dataset: ChartDataSetsEx; 11 | 12 | /** Sections */ 13 | sections: MetaSection[]; 14 | 15 | /** Scales wil be initialized in beforeDraw hook */ 16 | getXY: GetXY = undefined as any; 17 | topY?: number; 18 | bottomY?: number; 19 | 20 | /** Normalized dataset's data */ 21 | normalizedData: DataPoint[]; 22 | /** Is the dataset's data an array of {x,y}? */ 23 | isXY = false; 24 | 25 | constructor(chart: Chart, ds: ChartDataSetsEx) { 26 | const cfg = ds.regressions; 27 | this.chart = chart; 28 | this.dataset = ds; 29 | this.normalizedData = this._normalizeData(ds.data!); 30 | this.sections = this._createMetaSections(cfg); 31 | this._calculate(); 32 | } 33 | 34 | /** 35 | * Normalize data to DataPoint[] 36 | * Only supports number[] and {x:number,y:number} 37 | */ 38 | _normalizeData(data: any[]): DataPoint[] { 39 | return data.map((value: any, index: number) => { 40 | let p: DataPoint; 41 | if (typeof value == 'number' || value == null || value === undefined) { 42 | p = [index, value]; 43 | } else { 44 | this.isXY = true; 45 | p = [value.x as number, value.y as number]; 46 | } 47 | return p; 48 | }); 49 | } 50 | 51 | /** @private */ 52 | _createMetaSections(cfg: DatasetConfig): MetaSection[] { 53 | const source = cfg.sections || [ 54 | { startIndex: 0, endIndex: this.dataset.data!.length - 1 } 55 | ]; 56 | return source.map((s: Section) => new MetaSection(s, this)); 57 | } 58 | 59 | /** @private */ 60 | _calculate() { 61 | this.sections.forEach(section => section.calculate()); // Calculate Section Results 62 | } 63 | 64 | adjustScales() { 65 | if (this.topY !== undefined) return; 66 | let xScale: any; 67 | let yScale: any; 68 | const scales = (this.chart as any).scales; 69 | Object.keys(scales).forEach( 70 | k => (k[0] == 'x' && (xScale = scales[k])) || (yScale = scales[k]) 71 | ); 72 | this.topY = yScale.top; 73 | this.bottomY = yScale.bottom; 74 | this.getXY = (x: number, y: number) => ({ 75 | x: xScale.getPixelForValue(x, undefined, undefined, true), 76 | y: yScale.getPixelForValue(y) 77 | }); 78 | } 79 | 80 | drawRegressions() { 81 | const ctx: CanvasRenderingContext2D = (this.chart as any).chart.ctx; 82 | ctx.save(); 83 | try { 84 | this.sections.forEach(section => section.drawRegressions(ctx)); 85 | } finally { 86 | ctx.restore(); 87 | } 88 | } 89 | 90 | drawRightBorders() { 91 | const ctx: CanvasRenderingContext2D = (this.chart as any).chart.ctx; 92 | ctx.save(); 93 | try { 94 | for (let i = 0; i < this.sections.length - 1; i++) 95 | this.sections[i].drawRightBorder(ctx); 96 | } finally { 97 | ctx.restore(); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { ChartDataSets } from 'chart.js'; 2 | import * as regression from 'regression'; 3 | 4 | export type Type = 5 | | 'copy' 6 | | 'linear' 7 | | 'exponential' 8 | | 'power' 9 | | 'polynomial' 10 | | 'polynomial3' 11 | | 'polynomial4' 12 | | 'logarithmic'; 13 | export type OverwritingType = 'none' | 'all' | 'empty' | 'last'; 14 | 15 | export interface LineOptions { 16 | width?: number; 17 | color?: string; 18 | dash?: number[]; 19 | } 20 | 21 | export type CalculationOptions = regression.Options; 22 | 23 | export interface CopyOptions { 24 | /** 25 | * none - No data will be overwriten (default) 26 | * all - All data will be overwriten. 27 | * empty- Only zero, undefined, or null data will be overwriten. 28 | * last - Only the last item (data[endIndex]) will be overwriten. 29 | */ 30 | overwriteData?: OverwritingType; 31 | /** Minimum value that the predicted value can be written into the data */ 32 | minValue?: number; 33 | /** Maximum value that the predicted value can be written into the data */ 34 | maxValue?: number; 35 | } 36 | 37 | export interface BasicOptions { 38 | /** Type of regression to be calculated for all the sections unless they define their own type */ 39 | type?: Type | Type[]; 40 | /** Line configuration for all the sections unless they define their own line */ 41 | line?: LineOptions; 42 | /** Precision and polynomial order of the values returned by the regression calculations */ 43 | calculation?: CalculationOptions; 44 | /** Previous sections predictions for the current section will be drawed as dashed lines */ 45 | extendPredictions?: boolean; 46 | /** Only if type=='copy' */ 47 | copy?: CopyOptions; 48 | } 49 | 50 | export interface CopyOptionsEx extends CopyOptions { 51 | /** Copy the predictions calculated by the section with index fromSectionIndex */ 52 | fromSectionIndex?: number; 53 | } 54 | 55 | export interface Section extends BasicOptions { 56 | /** Start index on dataset's data. Default: 0 */ 57 | startIndex?: number; 58 | /** End index on dataset's data. Default: data.length-1 */ 59 | endIndex?: number; 60 | /** If type=='copy' the section can be configured with this extended options for copy */ 61 | copy?: CopyOptionsEx; 62 | /** Label that will be drawn in the top of the right border line. Default: xaxis' label */ 63 | label?: string; 64 | } 65 | 66 | export interface ChartDataSetsEx extends ChartDataSets { 67 | regressions: DatasetConfig; 68 | } 69 | 70 | export interface DatasetConfig extends BasicOptions { 71 | /** Sections of the data that shall draw a regression. If not specified it's assumed {start:0,end:data.length-1} */ 72 | sections?: Section[]; 73 | } 74 | 75 | /** 76 | * The options.plugin.regressions have a global configuration that can be override by 77 | * the Dataset or Section configuration. 78 | * Example: If In options is configured a precision, all the datasets and sections will 79 | * use it, unless they set their own precision. 80 | * This hierarchical configuration avoids repetitive configurations 81 | */ 82 | export interface OptionsConfig extends BasicOptions { 83 | /** 84 | * Callback called when the regressions for all the datasets in 85 | * a chart have been calculated 86 | */ 87 | onCompleteCalculation?: (chart: Chart) => void; 88 | } 89 | -------------------------------------------------------------------------------- /demo/demo.js: -------------------------------------------------------------------------------- 1 | var app = angular.module('demoApp', []); 2 | app.controller('DemoController', function () { 3 | this.groups = demo.groups; 4 | this.format = helpers.formatJson; 5 | }); 6 | 7 | app.directive('piChart', function () { 8 | var me = this; 9 | return { 10 | restrict: 'E', 11 | template: 12 | '' + 13 | '' + 14 | '' + 15 | ' ' + 16 | ' ' + 17 | ' ' + 18 | ' ' + 19 | ' ' + 20 | '
{{s.type}}R² = {{s.r2}}{{s.string}}
', 21 | scope: { 22 | sample: '<' 23 | }, 24 | link: piChart_LinkFn 25 | }; 26 | }); 27 | 28 | function piChart_LinkFn($scope, element) { 29 | $scope.shuffle = shuffle; 30 | var chart, chartType, isBarChart; 31 | var numElems = $scope.sample.prediction 32 | ? demo.NUM_ELEMS_PREDICT 33 | : demo.NUM_NORMAL_ELEMS; 34 | var chartConfig = createChartConfiguration(); 35 | shuffle(); 36 | return; 37 | 38 | function createChartConfiguration() { 39 | chartType = $scope.sample.chartType; 40 | chartType = chartType || ['bar', 'line'][helpers.rnd(2)]; 41 | isBarChart = chartType == 'bar'; 42 | 43 | // Chart.js configuration: 44 | return { 45 | type: chartType, 46 | data: { 47 | labels: new Array(numElems).fill().map((v, i) => i), 48 | // helpers.generateLabels(numElems, $scope.sample.prediction), 49 | datasets: generateDatasets( 50 | $scope.sample.numDatasets || 1, 51 | $scope.sample.datasetCfg 52 | ) 53 | }, 54 | plugins: [ChartRegressions], 55 | options: { 56 | plugins: { 57 | // Global configuration of the plugin for all the datasets 58 | regressions: Object.assign({ 59 | onCompleteCalculation: showRegressionResults 60 | }, 61 | $scope.sample.optionsCfg 62 | ) 63 | }, 64 | responsive: true, 65 | maintainAspectRatio: false 66 | } 67 | }; 68 | } 69 | 70 | /** Change the data, so the regression results may change as well */ 71 | function shuffle() { 72 | $scope.btnShuffle = 'wait...'; 73 | var s = $scope.sample; 74 | var dsType = s.datasetCfg.type; 75 | var opType = s.optionsCfg && s.optionsCfg.type; 76 | chartConfig.data.datasets.forEach(function (ds) { 77 | ds.data = helpers.generateRandomData( 78 | numElems, 79 | dsType || opType, 80 | s.prediction 81 | ); 82 | }); 83 | 84 | // Update or create the chart 85 | if (chart) chart.update(); 86 | else { 87 | var canvas = element[0].firstChild; 88 | chart = new Chart(canvas, chartConfig); 89 | } 90 | } 91 | 92 | function generateDatasets(numDatasets, datasetCfg) { 93 | var ds = []; 94 | for (var i = 0; i < numDatasets; i++) { 95 | var color = helpers.rnd(23); 96 | ds.push({ 97 | label: 'data#' + (i + 1), 98 | data: undefined, // settled in shuffle() function 99 | borderColor: helpers.generateColors(numElems, color, 1), 100 | backgroundColor: !isBarChart 101 | ? helpers.color(color, 0.5) 102 | : helpers.generateColors(numElems, color, 0.5), 103 | showLine: isBarChart, 104 | // Configuration of the plugin for the dataset: 105 | regressions: datasetCfg 106 | }); 107 | } 108 | return ds; 109 | } 110 | 111 | function showRegressionResults(chart2) { 112 | var results = []; 113 | for (var i = 0; i < ($scope.sample.numDatasets || 1); i++) { 114 | var sections = ChartRegressions.getSections(chart2, i); 115 | sections.forEach(function (s) { 116 | if (s.result.r2) { 117 | s = Object.assign({ line: s.line }, s.result); 118 | s.r2 = Math.round(s.r2 * 1000) / 10 + '%'; 119 | results.push(s); 120 | } 121 | }); 122 | } 123 | $scope.results = results; 124 | $scope.btnShuffle = 'shuffle'; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/MetaSection.ts: -------------------------------------------------------------------------------- 1 | import * as regression from 'regression'; 2 | import { 3 | Section, 4 | Type, 5 | LineOptions, 6 | BasicOptions, 7 | CalculationOptions, 8 | CopyOptionsEx 9 | } from './types'; 10 | import { MetaDataSet } from './MetaData'; 11 | 12 | const defaultConfig: BasicOptions = { 13 | type: 'linear', 14 | calculation: { 15 | precision: 2, 16 | order: 2 17 | }, 18 | line: { 19 | width: 2, 20 | color: '#000', 21 | dash: [] 22 | }, 23 | extendPredictions: false, 24 | copy: { 25 | overwriteData: 'none' 26 | } 27 | }; 28 | 29 | export interface Result extends regression.Result { 30 | type: Type; 31 | } 32 | 33 | export class MetaSection implements Section, BasicOptions { 34 | type: Type[]; 35 | startIndex: number; 36 | endIndex: number; 37 | line: LineOptions; 38 | extendPredictions: boolean; 39 | result?: regression.Result; 40 | copy: CopyOptionsEx; 41 | calculation: CalculationOptions; 42 | label: string; 43 | 44 | constructor(sec: Section, private _meta: MetaDataSet) { 45 | const chart = _meta.chart; 46 | const ds = _meta.dataset; 47 | const cfg = getConfig([ 48 | 'type', 49 | 'calculation', 50 | 'line', 51 | 'extendPredictions', 52 | 'copy' 53 | ]); 54 | this.startIndex = sec.startIndex || 0; 55 | this.endIndex = sec.endIndex || ds.data!.length - 1; 56 | this.type = Array.isArray(cfg.type) ? cfg.type : [cfg.type]; 57 | this.line = cfg.line; 58 | this.calculation = cfg.calculation; 59 | this.extendPredictions = cfg.extendPredictions; 60 | this.copy = cfg.copy; 61 | this.label = 62 | sec.label || (this._meta.chart.data.labels![this.endIndex] as string); 63 | this._validateType(); 64 | 65 | // --- constructor helpers 66 | 67 | /** 68 | * Calculate the inherited configuration from defaultConfig, globalConfig, 69 | * dataset config, and section config (in that order) 70 | */ 71 | function getConfig(fields: string[]): any { 72 | let o, p; 73 | const globalConfig = 74 | ((o = chart.config.options) && (p = o.plugins) && p.regressions) || {}; 75 | return configMerge( 76 | fields, 77 | defaultConfig, 78 | globalConfig, 79 | ds.regressions, 80 | sec 81 | ); 82 | 83 | /** merge the config objects */ 84 | function configMerge(fields: string[], ...cfgList: any[]): any { 85 | const dstConfig: any = {}; 86 | fields.forEach(f => { 87 | cfgList.forEach(srcConfig => { 88 | const o = srcConfig[f]; 89 | const t = typeof o; 90 | if (t != 'undefined') { 91 | if (Array.isArray(o) || t != 'object' || o == null) 92 | dstConfig[f] = o; 93 | else 94 | dstConfig[f] = Object.assign( 95 | {}, 96 | dstConfig[f], 97 | configMerge(Object.keys(o), o) 98 | ); 99 | } 100 | }); 101 | }); 102 | return dstConfig; 103 | } 104 | } 105 | } 106 | /** Validates the type to avoid inconsistences */ 107 | _validateType() { 108 | if (this.type.length > 1 && this.type.includes('copy')) 109 | throw Error( 110 | 'Invalid regression type:' + 111 | this.type + 112 | '. "none" cannot be combined with other type!' 113 | ); 114 | } 115 | 116 | /** Calculates the regression(s) and sets the result objects */ 117 | calculate() { 118 | const sectionData = this._meta.normalizedData.slice( 119 | this.startIndex, 120 | this.endIndex + 1 121 | ); 122 | if (this.type[0] == 'copy') this._calculateCopySection(sectionData); 123 | else this._calculateBestR2(sectionData); 124 | } 125 | 126 | private _calculateBestR2(sectionData: regression.DataPoint[]) { 127 | this.result = this.type.reduce((max: any, type) => { 128 | let calculation = Object.assign({}, this.calculation); 129 | let realType = type; 130 | if (/polynomial[34]$/.test(type)) { 131 | calculation.order = parseInt(type.substr(10)); 132 | realType = type.substr(0, 10) as Type; 133 | } 134 | const r: Result = (regression as any)[realType](sectionData, calculation); 135 | r.type = type; 136 | return !max || max.r2 < r.r2 ? r : max; 137 | }, null); 138 | } 139 | 140 | private _calculateCopySection(sectionData: regression.DataPoint[]) { 141 | const from = this._meta.sections[this.copy.fromSectionIndex!], 142 | r = (this.result = Object.assign({}, from.result)), 143 | overwrite = this.copy.overwriteData, 144 | data = this._meta.normalizedData; 145 | r.points = sectionData.map(p => r.predict(p[0])) as any; 146 | delete r.r2; 147 | if (overwrite != 'none') { 148 | const dsdata = this._meta.dataset.data!, 149 | isXY = this._meta.isXY; 150 | r.points.forEach(([x, y], i) => { 151 | const index = i + this.startIndex; 152 | if ( 153 | (index < from.startIndex || index > from.endIndex) && 154 | (overwrite == 'all' || 155 | (overwrite == 'last' && index == this.endIndex) || 156 | (overwrite == 'empty' && !data[index])) 157 | ) { 158 | if (this.copy.maxValue) y = Math.min(this.copy.maxValue, y); 159 | if (this.copy.minValue !== undefined) 160 | y = Math.max(this.copy.minValue, y); 161 | dsdata[index] = isXY ? { x, y } : y; 162 | } 163 | }); 164 | } 165 | } 166 | 167 | drawRightBorder(ctx: CanvasRenderingContext2D): void { 168 | ctx.beginPath(); 169 | this._setLineAttrs(ctx); 170 | ctx.setLineDash([10, 2]); 171 | ctx.lineWidth = 2; 172 | // Print vertical line 173 | const p = this._meta.getXY(this.endIndex, 0); 174 | ctx.moveTo(p.x, this._meta.topY!); 175 | ctx.lineTo(p.x, this._meta.bottomY!); 176 | ctx.fillStyle = this.line.color!; 177 | ctx.fillText(this.label, p.x, this._meta.topY!); 178 | ctx.stroke(); 179 | } 180 | 181 | drawRegressions(ctx: CanvasRenderingContext2D): void { 182 | for (let i = 0, len = this._meta.sections.length; i < len; i++) { 183 | const section = this._meta.sections[i]; 184 | const isMe = section == this; 185 | if ( 186 | (isMe && this.type[0] != 'copy') || 187 | (!isMe && this.extendPredictions) 188 | ) { 189 | section.drawRange(ctx, this.startIndex, this.endIndex, !isMe); 190 | } 191 | if (isMe) break; 192 | } 193 | } 194 | 195 | drawRange( 196 | ctx: CanvasRenderingContext2D, 197 | startIndex: number, 198 | endIndex: number, 199 | forceDash: boolean 200 | ): void { 201 | ctx.beginPath(); 202 | this._setLineAttrs(ctx); 203 | if (forceDash) ctx.setLineDash([5, 5]); 204 | const predict = this.result!.predict; 205 | const f = (x: number) => this._meta.getXY(x, predict(x)[1]); 206 | let p = f(startIndex); 207 | ctx.moveTo(p.x, p.y); 208 | for (let x = startIndex + 1; x <= endIndex; x++) { 209 | p = f(x); 210 | ctx.lineTo(p.x, p.y); 211 | } 212 | ctx.stroke(); 213 | } 214 | 215 | private _setLineAttrs(ctx: CanvasRenderingContext2D) { 216 | if (this.line.width) ctx.lineWidth = this.line.width; 217 | if (this.line.color) ctx.strokeStyle = this.line.color; 218 | if (this.line.dash) ctx.setLineDash(this.line.dash); 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /demo/demoData.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var demo = new (function () { 4 | this.NUM_NORMAL_ELEMS = 50; 5 | this.NUM_ELEMS_2 = (this.NUM_NORMAL_ELEMS / 2) | 0; 6 | this.NUM_ELEMS_4 = (this.NUM_NORMAL_ELEMS / 4) | 0; 7 | this.NUM_ELEMS_34 = ((3 * this.NUM_NORMAL_ELEMS) / 4) | 0; 8 | this.NUM_ELEMS_PREDICT = this.NUM_NORMAL_ELEMS + 10; 9 | 10 | this.groups = [ 11 | { 12 | title: 'Single Regression Type', 13 | samples: [ 14 | { 15 | subtitle: 'Linear regression', 16 | // optionsCfg: {}, 17 | datasetCfg: { type: 'linear', line: { color: 'red' } } 18 | }, 19 | { 20 | subtitle: 'Exponential regression', 21 | // optionsCfg: {}, 22 | datasetCfg: { 23 | type: 'exponential', 24 | line: { color: '#0000ff', width: 3 } 25 | } 26 | }, 27 | { 28 | subtitle: 'Polynomial regression', 29 | // optionsCfg: {}, 30 | datasetCfg: { 31 | type: 'polynomial', 32 | line: { color: '#0000ff', width: 3 }, 33 | calculation: { precision: 10, order: 4 } 34 | } 35 | }, 36 | { 37 | subtitle: 'Power regression', 38 | // optionsCfg: {}, 39 | datasetCfg: { type: 'power', line: { color: '#0000ff', width: 3 } } 40 | }, 41 | { 42 | subtitle: 'Logarithmic', 43 | // optionsCfg: {}, 44 | datasetCfg: { 45 | type: 'logarithmic', 46 | calculation: { precision: 10 }, 47 | line: { color: '#0000ff', width: 3 } 48 | } 49 | } 50 | ] 51 | }, 52 | { 53 | title: 'Drawing the best regression type', 54 | samples: [ 55 | { 56 | subtitle: 'Best R² between Exponential and Polynomial', 57 | // optionsCfg: {}, 58 | datasetCfg: { 59 | type: ['exponential', 'polynomial'], 60 | line: { color: '#0000ff', width: 3 }, 61 | calculation: { order: 3 } 62 | } 63 | } 64 | ] 65 | }, 66 | { 67 | title: 'Sections', 68 | samples: [ 69 | { 70 | subtitle: 'Single section of the data', 71 | optionsCfg: { 72 | line: { color: 'red', width: 3 } 73 | }, 74 | datasetCfg: { 75 | type: ['linear', 'exponential', 'polynomial'], 76 | sections: [ 77 | { startIndex: this.NUM_ELEMS_4, endIndex: this.NUM_ELEMS_34 } 78 | ] 79 | } 80 | }, 81 | { 82 | subtitle: 'Multiple sections extending previous predictions', 83 | optionsCfg: { 84 | line: { color: 'blue', width: 3 } 85 | }, 86 | datasetCfg: { 87 | type: ['linear', 'exponential', 'polynomial'], 88 | extendPredictions: true, 89 | sections: [ 90 | { endIndex: this.NUM_ELEMS_2, line: { color: 'red' } }, 91 | { 92 | startIndex: this.NUM_ELEMS_2, 93 | endIndex: this.NUM_NORMAL_ELEMS - 1 94 | } 95 | ] 96 | } 97 | }, 98 | { 99 | subtitle: 'Multiple sections with custom label on right border', 100 | optionsCfg: { 101 | line: { color: 'blue', width: 3 } 102 | }, 103 | datasetCfg: { 104 | type: ['linear', 'exponential', 'polynomial'], 105 | extendPredictions: true, 106 | sections: [ 107 | { 108 | endIndex: this.NUM_ELEMS_2, 109 | line: { color: 'red' }, 110 | label: 'custom text' 111 | }, 112 | { 113 | startIndex: this.NUM_ELEMS_2, 114 | endIndex: this.NUM_NORMAL_ELEMS - 1 115 | } 116 | ] 117 | } 118 | } 119 | ] 120 | }, 121 | { 122 | title: 'Multiple regressions', 123 | samples: [ 124 | { 125 | subtitle: 'One dataset with 3 different regressions', 126 | chartType: 'line', 127 | // optionsCfg: {}, 128 | datasetCfg: { 129 | line: { width: 3, color: 'blue' }, 130 | sections: [ 131 | { type: 'linear', line: { width: 1.5 } }, 132 | { type: 'polynomial', line: { color: 'orange', dash: [8, 2] } }, 133 | { type: 'exponential', line: { color: 'red' } } 134 | ] 135 | } 136 | }, 137 | { 138 | subtitle: 'One dataset with 3 polynomial with different order', 139 | chartType: 'line', 140 | optionsCfg: { type: 'polynomial', line: { width: 3, color: 'blue' } }, 141 | datasetCfg: { 142 | sections: [ 143 | { calculation: { order: 2 }, line: { width: 1.5 } }, 144 | { 145 | calculation: { order: 3 }, 146 | line: { color: 'orange', dash: [8, 2] } 147 | }, 148 | { calculation: { order: 4 }, line: { color: 'red' } } 149 | ] 150 | } 151 | } 152 | ] 153 | }, 154 | { 155 | title: "Drawing predictions using other section's regression", 156 | samples: [ 157 | { 158 | prediction: 'none', 159 | subtitle: "Without overwriting the last section's data", 160 | chartType: 'bar', 161 | // optionsCfg: {}, 162 | datasetCfg: { 163 | type: ['linear', 'exponential', 'polynomial'], 164 | line: { color: 'blue', width: 3 }, 165 | extendPredictions: true, 166 | sections: [ 167 | { endIndex: this.NUM_ELEMS_2 }, 168 | { 169 | startIndex: this.NUM_ELEMS_2, 170 | endIndex: this.NUM_NORMAL_ELEMS - 1, 171 | line: { color: 'red' } 172 | }, 173 | { 174 | type: 'copy', 175 | copy: { fromSectionIndex: 1, overwriteData: 'none' }, 176 | startIndex: this.NUM_NORMAL_ELEMS - 1 177 | } 178 | ] 179 | } 180 | }, 181 | { 182 | prediction: 'all', 183 | subtitle: "Overwrites the last section's data", 184 | chartType: 'bar', 185 | // optionsCfg: {}, 186 | datasetCfg: { 187 | type: ['linear', 'exponential', 'polynomial'], 188 | line: { color: 'blue', width: 3 }, 189 | extendPredictions: true, 190 | sections: [ 191 | { endIndex: this.NUM_ELEMS_2 }, 192 | { 193 | startIndex: this.NUM_ELEMS_2, 194 | endIndex: this.NUM_NORMAL_ELEMS - 1, 195 | line: { color: 'red' } 196 | }, 197 | { 198 | type: 'copy', 199 | copy: { fromSectionIndex: 1, overwriteData: 'all' }, 200 | startIndex: this.NUM_NORMAL_ELEMS - 1 201 | } 202 | ] 203 | } 204 | }, 205 | { 206 | prediction: 'last', 207 | subtitle: 'Overwrites only the last data item', 208 | chartType: 'bar', 209 | // optionsCfg: {}, 210 | datasetCfg: { 211 | type: ['linear', 'exponential', 'polynomial'], 212 | line: { color: 'blue', width: 3 }, 213 | extendPredictions: true, 214 | sections: [ 215 | { endIndex: this.NUM_ELEMS_2 }, 216 | { 217 | startIndex: this.NUM_ELEMS_2, 218 | endIndex: this.NUM_NORMAL_ELEMS - 1, 219 | line: { color: 'red' } 220 | }, 221 | { 222 | type: 'copy', 223 | copy: { fromSectionIndex: 1, overwriteData: 'last' }, 224 | startIndex: this.NUM_NORMAL_ELEMS - 1 225 | } 226 | ] 227 | } 228 | } 229 | ] 230 | }, 231 | { 232 | title: 'Configuration inheritance', 233 | samples: [ 234 | { 235 | subtitle: 'Inherits calculation: precision', 236 | optionsCfg: { calculation: { precision: 5 } }, 237 | datasetCfg: { type: 'linear' } 238 | }, 239 | { 240 | subtitle: 'Inherits all in two different datasets', 241 | numDatasets: 2, 242 | chartType: 'line', 243 | optionsCfg: { type: 'linear', calculation: { precision: 4 } }, 244 | datasetCfg: {} 245 | }, 246 | { 247 | subtitle: 248 | 'Inherits line and polynomial order, but overrides type and precision', 249 | optionsCfg: { 250 | type: 'linear', 251 | line: { width: 3, dash: [2, 2], color: '#f00' }, 252 | calculation: { precision: 5, order: 4 } 253 | }, 254 | datasetCfg: { type: 'polynomial', calculation: { precision: 3 } } 255 | } 256 | ] 257 | } 258 | ]; 259 | })(); 260 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![NPM version](https://img.shields.io/npm/v/chartjs-plugin-regression.svg)](https://npmjs.org/package/chartjs-plugin-regression) 2 | [![License](http://img.shields.io/badge/license-ISC-blue.svg)](https://github.com/pomgui/chartjs-plugin-regression/blob/master/LICENSE) 3 | [![NPM downloads](https://img.shields.io/npm/dm/chartjs-plugin-regression.svg)](https://npmjs.org/package/chartjs-plugin-regression) 4 | 5 | # chartjs-plugin-regression 6 | Chart.js plugin to calculate and draw statistical linear, exponential, power, 7 | logarithmic, and polynomial regressions using chart datasets data. 8 | 9 | The plugin, at the current version, uses the [regression](https://www.npmjs.com/package/regression) 10 | npm package as its calculation engine. 11 | 12 | ### Important 13 | - Only `bar`, `line`, and `scatter` chart types are supported. 14 | - The plugin works just fine with chart.js@2.5.0, however that version may have some problems handling certain color configuration (not related with the plugin). No problems have been found with chart.js@^2.6.0. 15 | - The plugin does not work with chart.js@3.x 16 | 17 | ## Demo 18 | For a better understanding of the capabilities of this plugin, please see this 19 | [Live Demo](https://pomgui.github.io/chartjs-plugin-regression/demo/). 20 | 21 | ## Download 22 | The [compressed](https://pomgui.github.io/chartjs-plugin-regression/dist/chartjs-plugin-regression-0.2.1.js) 23 | version includes the regression package. 24 | 25 | ## Installation 26 | 27 | npm install --save chartjs-plugin-regression 28 | 29 | ## Usage 30 | 31 | For a single chart, it needs to be listed in plugins section. 32 | 33 | ### Example: 34 | 35 | ```javascript 36 | new Chart(ctx, { 37 | type: 'bar', 38 | plugins: [ 39 | // This chart will use the plugin 40 | ChartRegressions 41 | ], 42 | data: { 43 | ... 44 | datasets: [ 45 | { 46 | ... 47 | // Configuration of the plugin per dataset (only will be drawn the datasets with this property) 48 | regressions: { 49 | type: 'linear', 50 | line: { color: 'red', width: 3}, 51 | ... 52 | } 53 | } 54 | ] 55 | ... 56 | } 57 | }); 58 | ``` 59 | 60 | Also, it's possible to register the plugin for all the charts: 61 | 62 | ```javascript 63 | Chart.plugins.register(ChartRegressions); 64 | ``` 65 | 66 | ## Configuration 67 | 68 | The plugin has three levels of configuration: 69 | 70 | - global (for all the datasets in a chart) 71 | - Per dataset 72 | - Per section 73 | 74 | There are common properties that the three levels share, and the priority of them are: section, dataset, and global. 75 | 76 | ### Common properties 77 | 78 | Common to the three levels of configuration. 79 | 80 | | Property | Description | 81 | |---|---| 82 | | type | Type of regression to be calculated. It can be 'copy', 'linear', 'exponential', 'power', 'polynomial', 'polynomial3', 'polynomial4', or 'logarithmic'. It also can be an array with a combination of these types, in which case the regression type with the best [_R²_](https://en.wikipedia.org/wiki/Coefficient_of_determination) will be drawn. | 83 | | line | Line configuration for drawing the regression. It has the following properties: `{width, color, dash}` | 84 | | calculation | Precision and polynomial order of the values returned by the regression calculations | 85 | | extendPredictions | Previous sections predictions for the current section will be drawed as dashed lines | 86 | | copy | Only if type=='copy'. Behavior of sections that copy other section's calculation | 87 | 88 | Some considerations: 89 | 90 | - `type`: polynomial3 and polynomial4 are pseudo-types added for convenience, they allow combinationss where the plugin will draw the regression with bigger R². Example: 91 | 92 | ```javascript 93 | { 94 | type: ['polynomial', 'polynomial3', 'polynomial4'], 95 | calculation: {order 2} 96 | } 97 | ``` 98 | 99 | - `calculation` has the following properties: 100 | 101 | | Property | Description | 102 | |---|---| 103 | | `precision` | Determines how many decimals will have the results (default: 2). | 104 | | `order` | Only for `polynomial` regression type, i.e. `polynomial3` and `polynomial4` are not affected by this property. Example: _ax² + bx + c_ has order 2. | 105 | 106 | - `copy` has the following properties: 107 | 108 | | Property | Description | 109 | |---|---| 110 | | `overwriteData` | Possible values: 'none', 'all', 'empty', or 'last'. Default: 'none'. It determines how the dataset's data will be overwritten in this section (empty: Only zero, undefined, or null data will be overwriten). **Obs.** the plugin is only prepared to overwrite numerical data arrays, e.g. `[1,2,3,...]`, scatter charts use xy data arrays, e.g. `[{x:1,y:1}, {x:2,y:2},...]`, with them the behavior is undetermined. In these cases it's better use `overwriteData: 'none'`. | 111 | | `minValue` | Minimum value that the predicted value can be written into the data. | 112 | | `maxValue` | Maximum value that the predicted value can be written into the data. | 113 | 114 | ### Global 115 | 116 | The global configuration affects all the regressions calculated for all the datasets in the chart. It contains all the common properties and the following properties: 117 | 118 | | Property | Description | 119 | |---|---| 120 | | onCompleteCalculation | Callback called when the regressions for all the datasets in a chart have been calculated | 121 | 122 | Example: 123 | 124 | ```javascript 125 | options: { 126 | plugins: { 127 | regressions: { 128 | type: ['linear', 'polynomial'], 129 | line: { color: 'blue', width: 3 }, 130 | onCompleteCalculation: function callback(chart){ ... } 131 | } 132 | } 133 | } 134 | ``` 135 | 136 | ### Per Dataset 137 | 138 | It's possible to configure the regressions per dataset. The configuration will contain all the common properties and the following properties: 139 | 140 | | Property | Description | 141 | |---|---| 142 | | sections | Array of sections of the data that shall be drawn. If not specified it's assumed `[{start:0,end:data.length-1}]` | 143 | 144 | Example: 145 | 146 | ```javascript 147 | datasets: [ 148 | { 149 | ... 150 | // Configuration of the plugin per dataset (only will be drawn the datasets with this property) 151 | regressions: { 152 | type: ['linear','exponential'], 153 | line: { color: '#ff0', width: 3}, 154 | calculation: { precision: 5 }, 155 | sections: [{startIndex: 10, endIndex: 50}], 156 | ... 157 | } 158 | } 159 | ] 160 | ``` 161 | 162 | ### Per Section 163 | 164 | Each section can be configured independently using all the common properties and the following properties: 165 | 166 | | Property | Description | 167 | |---|---| 168 | | startIndex | Start index on dataset's data. Default: 0 | 169 | | endIndex | End index on dataset's data. Default: data.length-1 | 170 | | label | Label that will be drawn in the top of the right border line. Default: xaxis' label | 171 | | copy.fromSectionIndex | Copy the predictions calculated by other section (the one with index fromSectionIndex) | 172 | 173 | Example: 174 | 175 | ```javascript 176 | datasets: [ 177 | { 178 | ... 179 | // Configuration of the plugin per dataset (only will be drawn the datasets with this property) 180 | regressions: { 181 | line: { width: 3 }, 182 | calculation: { precision: 5 }, 183 | sections: [ 184 | { 185 | type: ['linear','exponential'], 186 | line: { color: 'red' }, 187 | startIndex: 10, 188 | endIndex: 50 189 | }, 190 | { 191 | type: 'polynomial', 192 | line: { color: 'green' }, 193 | startIndex: 50, 194 | endIndex: 80, 195 | calculation: { order: 4 } 196 | }, 197 | ] 198 | } 199 | } 200 | ... 201 | ] 202 | ``` 203 | 204 | ## API 205 | 206 | ### .getDataset(chart, datasetIndex) 207 | 208 | Returns the metadata associated to one dataset used internally by the plugin to work. 209 | 210 | ```javascript 211 | var meta = ChartRegressions.getDataset(chart, datasetIndex); 212 | ``` 213 | 214 | This object provides the following information: 215 | 216 | | Property | Description | 217 | |---|---| 218 | | `sections` | array of sections for each dataset (it will contain at least 1 section) | 219 | | `getXY(x, y)` | Returns the canvas coordinates {x,y} for the data point `x, y`. | 220 | | `topY` | Minimum y coordinate in the canvas. | 221 | | `bottomY` | Maximum y coordinate in the canvas. | 222 | 223 | ### .getSections(chart, datasetIndex) 224 | 225 | Returns the sections with all the properties calculated (some with default values, or inherited from dataset's plugin configuration or the global configuration in options). 226 | 227 | This object provides the following information: 228 | 229 | | Property | Description | 230 | |---|---| 231 | | `type` | array of regression types used to calculate and draw the section. | 232 | | `startIndex` | Index of the dataset's data. | 233 | | `endIndex` | Index of the dataset's data. | 234 | | `line` | Configuration used to draw the lines {color, width, dash}. | 235 | | `result` | Regression calculation result (see [demo](https://pomgui.github.io/chartjs-plugin-regression/demo/)) to see how to use this information. | 236 | 237 | ## Events 238 | 239 | ### onCompleteCalculation(chart) 240 | 241 | The plugin provides one single event to inform when the calculation of all the regresions for a chart have been conmpleted. 242 | 243 | This callback should be configured in the chart options. 244 | 245 | Example: 246 | 247 | ```javascript 248 | options: { 249 | plugins: { 250 | regressions: { 251 | onCompleteCalculation: function callback(chart){ ... } 252 | } 253 | } 254 | } 255 | ``` 256 | 257 | ## License 258 | The project is released under the [ISC license](https://github.com/pomgui/chartjs-plugin-regression/blob/master/LICENSE). 259 | -------------------------------------------------------------------------------- /dist/chartjs-plugin-regression-0.2.1.js: -------------------------------------------------------------------------------- 1 | !function(t){var e={};function n(r){if(e[r])return e[r].exports;var i=e[r]={i:r,l:!1,exports:{}};return t[r].call(i.exports,i,i.exports,n),i.l=!0,i.exports}n.m=t,n.c=e,n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:r})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var i in t)n.d(r,i,function(e){return t[e]}.bind(null,i));return r},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=2)}([function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.MetaDataSet=void 0;var r=n(1),i=function(){function t(t,e){this.getXY=void 0,this.isXY=!1;var n=e.regressions;this.chart=t,this.dataset=e,this.normalizedData=this._normalizeData(e.data),this.sections=this._createMetaSections(n),this._calculate()}return t.prototype._normalizeData=function(t){var e=this;return t.map((function(t,n){var r;return"number"==typeof t||null==t||void 0===t?r=[n,t]:(e.isXY=!0,r=[t.x,t.y]),r}))},t.prototype._createMetaSections=function(t){var e=this;return(t.sections||[{startIndex:0,endIndex:this.dataset.data.length-1}]).map((function(t){return new r.MetaSection(t,e)}))},t.prototype._calculate=function(){this.sections.forEach((function(t){return t.calculate()}))},t.prototype.adjustScales=function(){if(void 0===this.topY){var t,e,n=this.chart.scales;Object.keys(n).forEach((function(r){return"x"==r[0]&&(t=n[r])||(e=n[r])})),this.topY=e.top,this.bottomY=e.bottom,this.getXY=function(n,r){return{x:t.getPixelForValue(n,void 0,void 0,!0),y:e.getPixelForValue(r)}}}},t.prototype.drawRegressions=function(){var t=this.chart.chart.ctx;t.save();try{this.sections.forEach((function(e){return e.drawRegressions(t)}))}finally{t.restore()}},t.prototype.drawRightBorders=function(){var t=this.chart.chart.ctx;t.save();try{for(var e=0;e1&&this.type.includes("copy"))throw Error("Invalid regression type:"+this.type+'. "none" cannot be combined with other type!')},t.prototype.calculate=function(){var t=this._meta.normalizedData.slice(this.startIndex,this.endIndex+1);"copy"==this.type[0]?this._calculateCopySection(t):this._calculateBestR2(t)},t.prototype._calculateBestR2=function(t){var e=this;this.result=this.type.reduce((function(n,i){var o=Object.assign({},e.calculation),a=i;/polynomial[34]$/.test(i)&&(o.order=parseInt(i.substr(10)),a=i.substr(0,10));var s=r[a](t,o);return s.type=i,!n||n.r2n.endIndex)&&("all"==i||"last"==i&&l==e.endIndex||"empty"==i&&!o[l])&&(e.copy.maxValue&&(u=Math.min(e.copy.maxValue,u)),void 0!==e.copy.minValue&&(u=Math.max(e.copy.minValue,u)),a[l]=s?{x:c,y:u}:u)}))}},t.prototype.drawRightBorder=function(t){t.beginPath(),this._setLineAttrs(t),t.setLineDash([10,2]),t.lineWidth=2;var e=this._meta.getXY(this.endIndex,0);t.moveTo(e.x,this._meta.topY),t.lineTo(e.x,this._meta.bottomY),t.fillStyle=this.line.color,t.fillText(this.label,e.x,this._meta.topY),t.stroke()},t.prototype.drawRegressions=function(t){for(var e=0,n=this._meta.sections.length;eMath.abs(n[o][a])&&(a=s);for(var c=o;c=o;p--)n[p][l]-=n[p][o]*n[o][l]/n[o][o]}for(var h=r-1;h>=0;h--){for(var f=0,d=h+1;d=0;m--)b+=m>1?v[m]+"x^"+m+" + ":1===m?v[m]+"x + ":v[m];return{string:b,points:x,predict:g,equation:[].concat(n(v)).reverse(),r2:o(i(t,x),e.precision)}}};t.exports=Object.keys(a).reduce((function(t,n){return e({_round:o},t,(c=function(t,i){return a[n](t,e({},r,i))},(s=n)in(i={})?Object.defineProperty(i,s,{value:c,enumerable:!0,configurable:!0,writable:!0}):i[s]=c,i));var i,s,c}),{})})?r.apply(e,i):r)||(t.exports=o)},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ChartRegressions=void 0;var r=n(0),i={},o=0,a=function(){function t(){this.id="regressions"}return t.prototype.beforeInit=function(t){t.$$id=++o},t.prototype.beforeUpdate=function(t,e){var n,o,a,c=(n=t.config.options)&&(o=n.plugins)&&(a=o.regressions)&&a.onCompleteCalculation;s(t,(function(e,n,o){n=new r.MetaDataSet(t,e);var a=1e3*t.$$id+o;i[a]=n})),c&&c(t)},t.prototype.beforeRender=function(t,e){s(t,(function(t,e){return e.adjustScales()}))},t.prototype.beforeDatasetsDraw=function(t,e,n){s(t,(function(t,e){return e.drawRightBorders()}))},t.prototype.afterDatasetsDraw=function(t,e,n){s(t,(function(t,e){return e.drawRegressions()}))},t.prototype.destroy=function(t){Object.keys(i).filter((function(e){return e/1e3>>0==t.$$id})).forEach((function(t){return delete i[t]}))},t.prototype.getDataset=function(t,e){var n=1e3*t.$$id+e;return i[n]},t.prototype.getSections=function(t,e){var n=this.getDataset(t,e);return n&&n.sections},t}();function s(t,n){t.data.datasets.forEach((function(r,i){if(r.regressions&&t.isDatasetVisible(i)){var o=e.ChartRegressions.getDataset(t,i);n(r,o,i)}}))}e.ChartRegressions=new a,window.ChartRegressions=e.ChartRegressions}]); -------------------------------------------------------------------------------- /dist/chartjs-plugin-regression-0.1.1.js: -------------------------------------------------------------------------------- 1 | /******/ (function(modules) { // webpackBootstrap 2 | /******/ // The module cache 3 | /******/ var installedModules = {}; 4 | /******/ 5 | /******/ // The require function 6 | /******/ function __webpack_require__(moduleId) { 7 | /******/ 8 | /******/ // Check if module is in cache 9 | /******/ if(installedModules[moduleId]) { 10 | /******/ return installedModules[moduleId].exports; 11 | /******/ } 12 | /******/ // Create a new module (and put it into the cache) 13 | /******/ var module = installedModules[moduleId] = { 14 | /******/ i: moduleId, 15 | /******/ l: false, 16 | /******/ exports: {} 17 | /******/ }; 18 | /******/ 19 | /******/ // Execute the module function 20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 21 | /******/ 22 | /******/ // Flag the module as loaded 23 | /******/ module.l = true; 24 | /******/ 25 | /******/ // Return the exports of the module 26 | /******/ return module.exports; 27 | /******/ } 28 | /******/ 29 | /******/ 30 | /******/ // expose the modules object (__webpack_modules__) 31 | /******/ __webpack_require__.m = modules; 32 | /******/ 33 | /******/ // expose the module cache 34 | /******/ __webpack_require__.c = installedModules; 35 | /******/ 36 | /******/ // define getter function for harmony exports 37 | /******/ __webpack_require__.d = function(exports, name, getter) { 38 | /******/ if(!__webpack_require__.o(exports, name)) { 39 | /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); 40 | /******/ } 41 | /******/ }; 42 | /******/ 43 | /******/ // define __esModule on exports 44 | /******/ __webpack_require__.r = function(exports) { 45 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { 46 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); 47 | /******/ } 48 | /******/ Object.defineProperty(exports, '__esModule', { value: true }); 49 | /******/ }; 50 | /******/ 51 | /******/ // create a fake namespace object 52 | /******/ // mode & 1: value is a module id, require it 53 | /******/ // mode & 2: merge all properties of value into the ns 54 | /******/ // mode & 4: return value when already ns object 55 | /******/ // mode & 8|1: behave like require 56 | /******/ __webpack_require__.t = function(value, mode) { 57 | /******/ if(mode & 1) value = __webpack_require__(value); 58 | /******/ if(mode & 8) return value; 59 | /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; 60 | /******/ var ns = Object.create(null); 61 | /******/ __webpack_require__.r(ns); 62 | /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); 63 | /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); 64 | /******/ return ns; 65 | /******/ }; 66 | /******/ 67 | /******/ // getDefaultExport function for compatibility with non-harmony modules 68 | /******/ __webpack_require__.n = function(module) { 69 | /******/ var getter = module && module.__esModule ? 70 | /******/ function getDefault() { return module['default']; } : 71 | /******/ function getModuleExports() { return module; }; 72 | /******/ __webpack_require__.d(getter, 'a', getter); 73 | /******/ return getter; 74 | /******/ }; 75 | /******/ 76 | /******/ // Object.prototype.hasOwnProperty.call 77 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 78 | /******/ 79 | /******/ // __webpack_public_path__ 80 | /******/ __webpack_require__.p = ""; 81 | /******/ 82 | /******/ 83 | /******/ // Load entry module and return exports 84 | /******/ return __webpack_require__(__webpack_require__.s = "./lib/index.js"); 85 | /******/ }) 86 | /************************************************************************/ 87 | /******/ ({ 88 | 89 | /***/ "./lib/MetaData.js": 90 | /*!*************************!*\ 91 | !*** ./lib/MetaData.js ***! 92 | \*************************/ 93 | /*! no static exports found */ 94 | /***/ (function(module, exports, __webpack_require__) { 95 | 96 | "use strict"; 97 | 98 | Object.defineProperty(exports, "__esModule", { value: true }); 99 | exports.MetaDataSet = void 0; 100 | var MetaSection_1 = __webpack_require__(/*! ./MetaSection */ "./lib/MetaSection.js"); 101 | var MetaDataSet = /** @class */ (function () { 102 | function MetaDataSet(chart, ds) { 103 | /** Scales wil be initialized in beforeDraw hook */ 104 | this.getXY = undefined; 105 | /** Is the dataset's data an array of {x,y}? */ 106 | this.isXY = false; 107 | var cfg = ds.regressions; 108 | this.chart = chart; 109 | this.dataset = ds; 110 | this.normalizedData = this._normalizeData(ds.data); 111 | this.sections = this._createMetaSections(cfg); 112 | this._calculate(); 113 | } 114 | /** 115 | * Normalize data to DataPoint[] 116 | * Only supports number[] and {x:number,y:number} 117 | */ 118 | MetaDataSet.prototype._normalizeData = function (data) { 119 | var _this = this; 120 | return data.map(function (value, index) { 121 | var p; 122 | if (typeof value == 'number' || value == null || value === undefined) { 123 | p = [index, value]; 124 | } 125 | else { 126 | _this.isXY = true; 127 | p = [value.x, value.y]; 128 | } 129 | return p; 130 | }); 131 | }; 132 | /** @private */ 133 | MetaDataSet.prototype._createMetaSections = function (cfg) { 134 | var _this = this; 135 | var source = cfg.sections || [{ startIndex: 0, endIndex: this.dataset.data.length - 1 }]; 136 | return source.map(function (s) { return new MetaSection_1.MetaSection(s, _this); }); 137 | }; 138 | /** @private */ 139 | MetaDataSet.prototype._calculate = function () { 140 | this.sections.forEach(function (section) { return section.calculate(); }); // Calculate Section Results 141 | }; 142 | MetaDataSet.prototype.adjustScales = function () { 143 | if (this.topY !== undefined) 144 | return; 145 | var xScale; 146 | var yScale; 147 | var scales = this.chart.scales; 148 | Object.keys(scales).forEach(function (k) { return k[0] == 'x' && (xScale = scales[k]) || (yScale = scales[k]); }); 149 | this.topY = yScale.top; 150 | this.bottomY = yScale.bottom; 151 | this.getXY = 152 | function (x, y) { 153 | return ({ 154 | x: xScale.getPixelForValue(x, undefined, undefined, true), 155 | y: yScale.getPixelForValue(y) 156 | }); 157 | }; 158 | }; 159 | MetaDataSet.prototype.draw = function () { 160 | var ctx = this.chart.chart.ctx; 161 | ctx.save(); 162 | try { 163 | this.sections.forEach(function (section) { return section.drawRegressions(ctx); }); 164 | } 165 | finally { 166 | ctx.restore(); 167 | } 168 | }; 169 | return MetaDataSet; 170 | }()); 171 | exports.MetaDataSet = MetaDataSet; 172 | //# sourceMappingURL=MetaData.js.map 173 | 174 | /***/ }), 175 | 176 | /***/ "./lib/MetaSection.js": 177 | /*!****************************!*\ 178 | !*** ./lib/MetaSection.js ***! 179 | \****************************/ 180 | /*! no static exports found */ 181 | /***/ (function(module, exports, __webpack_require__) { 182 | 183 | "use strict"; 184 | 185 | Object.defineProperty(exports, "__esModule", { value: true }); 186 | exports.MetaSection = void 0; 187 | var regression = __webpack_require__(/*! regression */ "./node_modules/regression/dist/regression.js"); 188 | var defaultConfig = { 189 | type: "linear", 190 | calculation: { 191 | precision: 2, 192 | order: 2 193 | }, 194 | line: { 195 | width: 2, 196 | color: '#000', 197 | dash: [] 198 | }, 199 | extendPredictions: false, 200 | copy: { 201 | overwriteData: 'none' 202 | } 203 | }; 204 | var MetaSection = /** @class */ (function () { 205 | function MetaSection(sec, _meta) { 206 | this._meta = _meta; 207 | var chart = _meta.chart; 208 | var ds = _meta.dataset; 209 | var cfg = getConfig(['type', 'calculation', 'line', 'extendPredictions', 'copy']); 210 | this.startIndex = sec.startIndex || 0; 211 | this.endIndex = sec.endIndex || ds.data.length - 1; 212 | this.type = Array.isArray(cfg.type) ? cfg.type : [cfg.type]; 213 | this.line = cfg.line; 214 | this.calculation = cfg.calculation; 215 | this.extendPredictions = cfg.extendPredictions; 216 | this.copy = cfg.copy; 217 | this.label = sec.label || this._meta.chart.data.labels[this.endIndex]; 218 | this._validateType(); 219 | // --- constructor helpers 220 | /** 221 | * Calculate the inherited configuration from defaultConfig, globalConfig, 222 | * dataset config, and section config (in that order) 223 | */ 224 | function getConfig(fields) { 225 | var o, p; 226 | var globalConfig = (o = chart.config.options) && (p = o.plugins) && p.regressions || {}; 227 | return configMerge(fields, defaultConfig, globalConfig, ds.regressions, sec); 228 | /** merge the config objects */ 229 | function configMerge(fields) { 230 | var cfgList = []; 231 | for (var _i = 1; _i < arguments.length; _i++) { 232 | cfgList[_i - 1] = arguments[_i]; 233 | } 234 | var dstConfig = {}; 235 | fields.forEach(function (f) { 236 | cfgList.forEach(function (srcConfig) { 237 | var o = srcConfig[f]; 238 | var t = typeof o; 239 | if (t != 'undefined') { 240 | if (Array.isArray(o) || t != 'object' || o == null) 241 | dstConfig[f] = o; 242 | else 243 | dstConfig[f] = Object.assign({}, dstConfig[f], configMerge(Object.keys(o), o)); 244 | } 245 | }); 246 | }); 247 | return dstConfig; 248 | } 249 | } 250 | } 251 | /** Validates the type to avoid inconsistences */ 252 | MetaSection.prototype._validateType = function () { 253 | if (this.type.length > 1 && this.type.includes('copy')) 254 | throw Error('Invalid regression type:' + this.type + '. "none" cannot be combined with other type!'); 255 | }; 256 | /** Calculates the regression(s) and sets the result objects */ 257 | MetaSection.prototype.calculate = function () { 258 | var sectionData = this._meta.normalizedData.slice(this.startIndex, this.endIndex + 1); 259 | if (this.type[0] == 'copy') 260 | this._calculateCopySection(sectionData); 261 | else 262 | this._calculateBestR2(sectionData); 263 | }; 264 | MetaSection.prototype._calculateBestR2 = function (sectionData) { 265 | var _this = this; 266 | this.result = this.type.reduce(function (max, type) { 267 | var calculation = Object.assign({}, _this.calculation); 268 | var realType = type; 269 | if (/polynomial[34]$/.test(type)) { 270 | calculation.order = parseInt(type.substr(10)); 271 | realType = type.substr(0, 10); 272 | } 273 | var r = regression[realType](sectionData, calculation); 274 | r.type = type; 275 | return (!max || max.r2 < r.r2) ? r : max; 276 | }, null); 277 | }; 278 | MetaSection.prototype._calculateCopySection = function (sectionData) { 279 | var _this = this; 280 | var from = this._meta.sections[this.copy.fromSectionIndex], r = this.result = Object.assign({}, from.result), overwrite = this.copy.overwriteData, data = this._meta.normalizedData; 281 | r.points = sectionData.map(function (p) { return r.predict(p[0]); }); 282 | delete r.r2; 283 | if (overwrite != 'none') { 284 | var dsdata_1 = this._meta.dataset.data, isXY_1 = this._meta.isXY; 285 | r.points.forEach(function (_a, i) { 286 | var x = _a[0], y = _a[1]; 287 | var index = i + _this.startIndex; 288 | if ((index < from.startIndex || index > from.endIndex) && 289 | (overwrite == 'all' || 290 | overwrite == 'last' && index == _this.endIndex || 291 | overwrite == 'empty' && !data[index])) { 292 | if (_this.copy.maxValue) 293 | y = Math.min(_this.copy.maxValue, y); 294 | if (_this.copy.minValue !== undefined) 295 | y = Math.max(_this.copy.minValue, y); 296 | dsdata_1[index] = isXY_1 ? { x: x, y: y } : y; 297 | } 298 | }); 299 | } 300 | }; 301 | MetaSection.prototype.drawRightBorder = function (ctx) { 302 | ctx.beginPath(); 303 | this._setLineAttrs(ctx); 304 | ctx.setLineDash([10, 2]); 305 | ctx.lineWidth = 2; 306 | // Print vertical line 307 | var p = this._meta.getXY(this.endIndex, 0); 308 | ctx.moveTo(p.x, this._meta.topY); 309 | ctx.lineTo(p.x, this._meta.bottomY); 310 | ctx.fillStyle = this.line.color; 311 | ctx.fillText(this.label, p.x, this._meta.topY); 312 | ctx.stroke(); 313 | }; 314 | MetaSection.prototype.drawRegressions = function (ctx) { 315 | for (var i = 0, len = this._meta.sections.length; i < len; i++) { 316 | var section = this._meta.sections[i]; 317 | var isMe = section == this; 318 | if (isMe && this.type[0] != 'copy' || !isMe && this.extendPredictions) { 319 | section.drawRange(ctx, this.startIndex, this.endIndex, !isMe); 320 | } 321 | if (isMe) 322 | break; 323 | } 324 | }; 325 | MetaSection.prototype.drawRange = function (ctx, startIndex, endIndex, forceDash) { 326 | var _this = this; 327 | ctx.beginPath(); 328 | this._setLineAttrs(ctx); 329 | if (forceDash) 330 | ctx.setLineDash([5, 5]); 331 | var predict = this.result.predict; 332 | var f = function (x) { return _this._meta.getXY(x, predict(x)[1]); }; 333 | var p = f(startIndex); 334 | ctx.moveTo(p.x, p.y); 335 | for (var x = startIndex + 1; x <= endIndex; x++) { 336 | p = f(x); 337 | ctx.lineTo(p.x, p.y); 338 | } 339 | ctx.stroke(); 340 | }; 341 | MetaSection.prototype._setLineAttrs = function (ctx) { 342 | if (this.line.width) 343 | ctx.lineWidth = this.line.width; 344 | if (this.line.color) 345 | ctx.strokeStyle = this.line.color; 346 | if (this.line.dash) 347 | ctx.setLineDash(this.line.dash); 348 | }; 349 | return MetaSection; 350 | }()); 351 | exports.MetaSection = MetaSection; 352 | //# sourceMappingURL=MetaSection.js.map 353 | 354 | /***/ }), 355 | 356 | /***/ "./lib/index.js": 357 | /*!**********************!*\ 358 | !*** ./lib/index.js ***! 359 | \**********************/ 360 | /*! no static exports found */ 361 | /***/ (function(module, exports, __webpack_require__) { 362 | 363 | "use strict"; 364 | 365 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 366 | if (k2 === undefined) k2 = k; 367 | Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); 368 | }) : (function(o, m, k, k2) { 369 | if (k2 === undefined) k2 = k; 370 | o[k2] = m[k]; 371 | })); 372 | var __exportStar = (this && this.__exportStar) || function(m, exports) { 373 | for (var p in m) if (p !== "default" && !exports.hasOwnProperty(p)) __createBinding(exports, m, p); 374 | }; 375 | Object.defineProperty(exports, "__esModule", { value: true }); 376 | __exportStar(__webpack_require__(/*! ./types */ "./lib/types.js"), exports); 377 | __exportStar(__webpack_require__(/*! ./MetaData */ "./lib/MetaData.js"), exports); 378 | __exportStar(__webpack_require__(/*! ./MetaSection */ "./lib/MetaSection.js"), exports); 379 | __exportStar(__webpack_require__(/*! ./regression-plugin */ "./lib/regression-plugin.js"), exports); 380 | //# sourceMappingURL=index.js.map 381 | 382 | /***/ }), 383 | 384 | /***/ "./lib/regression-plugin.js": 385 | /*!**********************************!*\ 386 | !*** ./lib/regression-plugin.js ***! 387 | \**********************************/ 388 | /*! no static exports found */ 389 | /***/ (function(module, exports, __webpack_require__) { 390 | 391 | "use strict"; 392 | 393 | Object.defineProperty(exports, "__esModule", { value: true }); 394 | exports.ChartRegressions = void 0; 395 | var MetaData_1 = __webpack_require__(/*! ./MetaData */ "./lib/MetaData.js"); 396 | // Cache for all plugins' metadata 397 | var _metadataMap = {}; 398 | var _chartId = 0; 399 | var Plugin = /** @class */ (function () { 400 | function Plugin() { 401 | this.id = 'regressions'; 402 | } 403 | Plugin.prototype.beforeInit = function (chart) { 404 | chart.$$id = ++_chartId; 405 | }; 406 | /** 407 | * Called after update (when the chart is created and when chart.update() is called) 408 | * @param chart 409 | */ 410 | Plugin.prototype.beforeUpdate = function (chart, options) { 411 | var o, p, r; 412 | var onComplete = (o = chart.config.options) && (p = o.plugins) 413 | && (r = p.regressions) && r.onCompleteCalculation; 414 | forEach(chart, function (ds, meta, datasetIndex) { 415 | meta = new MetaData_1.MetaDataSet(chart, ds); 416 | var id = chart.$$id * 1000 + datasetIndex; 417 | _metadataMap[id] = meta; 418 | }); 419 | if (onComplete) 420 | onComplete(chart); 421 | }; 422 | /** 423 | * It's called once before all the drawing 424 | * @param chart 425 | */ 426 | Plugin.prototype.beforeRender = function (chart, options) { 427 | forEach(chart, function (ds, meta) { return meta.adjustScales(); }); 428 | }; 429 | /** Draws the vertical lines before the datasets are drawn */ 430 | Plugin.prototype.beforeDatasetsDraw = function (chart, easing, options) { 431 | forEach(chart, function (ds, meta) { 432 | var ctx = chart.ctx; 433 | ctx.save(); 434 | try { 435 | for (var i = 0; i < meta.sections.length - 1; i++) 436 | meta.sections[i].drawRightBorder(ctx); 437 | } 438 | finally { 439 | ctx.restore(); 440 | } 441 | ; 442 | }); 443 | }; 444 | /** Draws the regression lines */ 445 | Plugin.prototype.afterDatasetsDraw = function (chart, easing, options) { 446 | forEach(chart, function (ds, meta) { return meta.draw(); }); 447 | }; 448 | Plugin.prototype.destroy = function (chart) { 449 | Object.keys(_metadataMap) 450 | .filter(function (k) { return (k / 1000) >> 0 == chart.$$id; }) 451 | .forEach(function (k) { return delete _metadataMap[k]; }); 452 | }; 453 | /** Get dataset's meta data */ 454 | Plugin.prototype.getDataset = function (chart, datasetIndex) { 455 | var id = chart.$$id * 1000 + datasetIndex; 456 | return _metadataMap[id]; 457 | }; 458 | /** Get dataset's meta sections */ 459 | Plugin.prototype.getSections = function (chart, datasetIndex) { 460 | var ds = this.getDataset(chart, datasetIndex); 461 | return ds && ds.sections; 462 | }; 463 | return Plugin; 464 | }()); 465 | function forEach(chart, fn) { 466 | chart.data.datasets.forEach(function (ds, i) { 467 | if (ds.regressions && chart.isDatasetVisible(i)) { 468 | var meta = exports.ChartRegressions.getDataset(chart, i); 469 | fn(ds, meta, i); 470 | } 471 | }); 472 | } 473 | exports.ChartRegressions = new Plugin(); 474 | window.ChartRegressions = exports.ChartRegressions; 475 | //# sourceMappingURL=regression-plugin.js.map 476 | 477 | /***/ }), 478 | 479 | /***/ "./lib/types.js": 480 | /*!**********************!*\ 481 | !*** ./lib/types.js ***! 482 | \**********************/ 483 | /*! no static exports found */ 484 | /***/ (function(module, exports, __webpack_require__) { 485 | 486 | "use strict"; 487 | 488 | Object.defineProperty(exports, "__esModule", { value: true }); 489 | //# sourceMappingURL=types.js.map 490 | 491 | /***/ }), 492 | 493 | /***/ "./node_modules/regression/dist/regression.js": 494 | /*!****************************************************!*\ 495 | !*** ./node_modules/regression/dist/regression.js ***! 496 | \****************************************************/ 497 | /*! no static exports found */ 498 | /***/ (function(module, exports, __webpack_require__) { 499 | 500 | var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;(function (global, factory) { 501 | if (true) { 502 | !(__WEBPACK_AMD_DEFINE_ARRAY__ = [module], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), 503 | __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? 504 | (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), 505 | __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); 506 | } else { var mod; } 507 | })(this, function (module) { 508 | 'use strict'; 509 | 510 | function _defineProperty(obj, key, value) { 511 | if (key in obj) { 512 | Object.defineProperty(obj, key, { 513 | value: value, 514 | enumerable: true, 515 | configurable: true, 516 | writable: true 517 | }); 518 | } else { 519 | obj[key] = value; 520 | } 521 | 522 | return obj; 523 | } 524 | 525 | var _extends = Object.assign || function (target) { 526 | for (var i = 1; i < arguments.length; i++) { 527 | var source = arguments[i]; 528 | 529 | for (var key in source) { 530 | if (Object.prototype.hasOwnProperty.call(source, key)) { 531 | target[key] = source[key]; 532 | } 533 | } 534 | } 535 | 536 | return target; 537 | }; 538 | 539 | function _toConsumableArray(arr) { 540 | if (Array.isArray(arr)) { 541 | for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { 542 | arr2[i] = arr[i]; 543 | } 544 | 545 | return arr2; 546 | } else { 547 | return Array.from(arr); 548 | } 549 | } 550 | 551 | var DEFAULT_OPTIONS = { order: 2, precision: 2, period: null }; 552 | 553 | /** 554 | * Determine the coefficient of determination (r^2) of a fit from the observations 555 | * and predictions. 556 | * 557 | * @param {Array>} data - Pairs of observed x-y values 558 | * @param {Array>} results - Pairs of observed predicted x-y values 559 | * 560 | * @return {number} - The r^2 value, or NaN if one cannot be calculated. 561 | */ 562 | function determinationCoefficient(data, results) { 563 | var predictions = []; 564 | var observations = []; 565 | 566 | data.forEach(function (d, i) { 567 | if (d[1] !== null) { 568 | observations.push(d); 569 | predictions.push(results[i]); 570 | } 571 | }); 572 | 573 | var sum = observations.reduce(function (a, observation) { 574 | return a + observation[1]; 575 | }, 0); 576 | var mean = sum / observations.length; 577 | 578 | var ssyy = observations.reduce(function (a, observation) { 579 | var difference = observation[1] - mean; 580 | return a + difference * difference; 581 | }, 0); 582 | 583 | var sse = observations.reduce(function (accum, observation, index) { 584 | var prediction = predictions[index]; 585 | var residual = observation[1] - prediction[1]; 586 | return accum + residual * residual; 587 | }, 0); 588 | 589 | return 1 - sse / ssyy; 590 | } 591 | 592 | /** 593 | * Determine the solution of a system of linear equations A * x = b using 594 | * Gaussian elimination. 595 | * 596 | * @param {Array>} input - A 2-d matrix of data in row-major form [ A | b ] 597 | * @param {number} order - How many degrees to solve for 598 | * 599 | * @return {Array} - Vector of normalized solution coefficients matrix (x) 600 | */ 601 | function gaussianElimination(input, order) { 602 | var matrix = input; 603 | var n = input.length - 1; 604 | var coefficients = [order]; 605 | 606 | for (var i = 0; i < n; i++) { 607 | var maxrow = i; 608 | for (var j = i + 1; j < n; j++) { 609 | if (Math.abs(matrix[i][j]) > Math.abs(matrix[i][maxrow])) { 610 | maxrow = j; 611 | } 612 | } 613 | 614 | for (var k = i; k < n + 1; k++) { 615 | var tmp = matrix[k][i]; 616 | matrix[k][i] = matrix[k][maxrow]; 617 | matrix[k][maxrow] = tmp; 618 | } 619 | 620 | for (var _j = i + 1; _j < n; _j++) { 621 | for (var _k = n; _k >= i; _k--) { 622 | matrix[_k][_j] -= matrix[_k][i] * matrix[i][_j] / matrix[i][i]; 623 | } 624 | } 625 | } 626 | 627 | for (var _j2 = n - 1; _j2 >= 0; _j2--) { 628 | var total = 0; 629 | for (var _k2 = _j2 + 1; _k2 < n; _k2++) { 630 | total += matrix[_k2][_j2] * coefficients[_k2]; 631 | } 632 | 633 | coefficients[_j2] = (matrix[n][_j2] - total) / matrix[_j2][_j2]; 634 | } 635 | 636 | return coefficients; 637 | } 638 | 639 | /** 640 | * Round a number to a precision, specificed in number of decimal places 641 | * 642 | * @param {number} number - The number to round 643 | * @param {number} precision - The number of decimal places to round to: 644 | * > 0 means decimals, < 0 means powers of 10 645 | * 646 | * 647 | * @return {numbr} - The number, rounded 648 | */ 649 | function round(number, precision) { 650 | var factor = Math.pow(10, precision); 651 | return Math.round(number * factor) / factor; 652 | } 653 | 654 | /** 655 | * The set of all fitting methods 656 | * 657 | * @namespace 658 | */ 659 | var methods = { 660 | linear: function linear(data, options) { 661 | var sum = [0, 0, 0, 0, 0]; 662 | var len = 0; 663 | 664 | for (var n = 0; n < data.length; n++) { 665 | if (data[n][1] !== null) { 666 | len++; 667 | sum[0] += data[n][0]; 668 | sum[1] += data[n][1]; 669 | sum[2] += data[n][0] * data[n][0]; 670 | sum[3] += data[n][0] * data[n][1]; 671 | sum[4] += data[n][1] * data[n][1]; 672 | } 673 | } 674 | 675 | var run = len * sum[2] - sum[0] * sum[0]; 676 | var rise = len * sum[3] - sum[0] * sum[1]; 677 | var gradient = run === 0 ? 0 : round(rise / run, options.precision); 678 | var intercept = round(sum[1] / len - gradient * sum[0] / len, options.precision); 679 | 680 | var predict = function predict(x) { 681 | return [round(x, options.precision), round(gradient * x + intercept, options.precision)]; 682 | }; 683 | 684 | var points = data.map(function (point) { 685 | return predict(point[0]); 686 | }); 687 | 688 | return { 689 | points: points, 690 | predict: predict, 691 | equation: [gradient, intercept], 692 | r2: round(determinationCoefficient(data, points), options.precision), 693 | string: intercept === 0 ? 'y = ' + gradient + 'x' : 'y = ' + gradient + 'x + ' + intercept 694 | }; 695 | }, 696 | exponential: function exponential(data, options) { 697 | var sum = [0, 0, 0, 0, 0, 0]; 698 | 699 | for (var n = 0; n < data.length; n++) { 700 | if (data[n][1] !== null) { 701 | sum[0] += data[n][0]; 702 | sum[1] += data[n][1]; 703 | sum[2] += data[n][0] * data[n][0] * data[n][1]; 704 | sum[3] += data[n][1] * Math.log(data[n][1]); 705 | sum[4] += data[n][0] * data[n][1] * Math.log(data[n][1]); 706 | sum[5] += data[n][0] * data[n][1]; 707 | } 708 | } 709 | 710 | var denominator = sum[1] * sum[2] - sum[5] * sum[5]; 711 | var a = Math.exp((sum[2] * sum[3] - sum[5] * sum[4]) / denominator); 712 | var b = (sum[1] * sum[4] - sum[5] * sum[3]) / denominator; 713 | var coeffA = round(a, options.precision); 714 | var coeffB = round(b, options.precision); 715 | var predict = function predict(x) { 716 | return [round(x, options.precision), round(coeffA * Math.exp(coeffB * x), options.precision)]; 717 | }; 718 | 719 | var points = data.map(function (point) { 720 | return predict(point[0]); 721 | }); 722 | 723 | return { 724 | points: points, 725 | predict: predict, 726 | equation: [coeffA, coeffB], 727 | string: 'y = ' + coeffA + 'e^(' + coeffB + 'x)', 728 | r2: round(determinationCoefficient(data, points), options.precision) 729 | }; 730 | }, 731 | logarithmic: function logarithmic(data, options) { 732 | var sum = [0, 0, 0, 0]; 733 | var len = data.length; 734 | 735 | for (var n = 0; n < len; n++) { 736 | if (data[n][1] !== null) { 737 | sum[0] += Math.log(data[n][0]); 738 | sum[1] += data[n][1] * Math.log(data[n][0]); 739 | sum[2] += data[n][1]; 740 | sum[3] += Math.pow(Math.log(data[n][0]), 2); 741 | } 742 | } 743 | 744 | var a = (len * sum[1] - sum[2] * sum[0]) / (len * sum[3] - sum[0] * sum[0]); 745 | var coeffB = round(a, options.precision); 746 | var coeffA = round((sum[2] - coeffB * sum[0]) / len, options.precision); 747 | 748 | var predict = function predict(x) { 749 | return [round(x, options.precision), round(round(coeffA + coeffB * Math.log(x), options.precision), options.precision)]; 750 | }; 751 | 752 | var points = data.map(function (point) { 753 | return predict(point[0]); 754 | }); 755 | 756 | return { 757 | points: points, 758 | predict: predict, 759 | equation: [coeffA, coeffB], 760 | string: 'y = ' + coeffA + ' + ' + coeffB + ' ln(x)', 761 | r2: round(determinationCoefficient(data, points), options.precision) 762 | }; 763 | }, 764 | power: function power(data, options) { 765 | var sum = [0, 0, 0, 0, 0]; 766 | var len = data.length; 767 | 768 | for (var n = 0; n < len; n++) { 769 | if (data[n][1] !== null) { 770 | sum[0] += Math.log(data[n][0]); 771 | sum[1] += Math.log(data[n][1]) * Math.log(data[n][0]); 772 | sum[2] += Math.log(data[n][1]); 773 | sum[3] += Math.pow(Math.log(data[n][0]), 2); 774 | } 775 | } 776 | 777 | var b = (len * sum[1] - sum[0] * sum[2]) / (len * sum[3] - Math.pow(sum[0], 2)); 778 | var a = (sum[2] - b * sum[0]) / len; 779 | var coeffA = round(Math.exp(a), options.precision); 780 | var coeffB = round(b, options.precision); 781 | 782 | var predict = function predict(x) { 783 | return [round(x, options.precision), round(round(coeffA * Math.pow(x, coeffB), options.precision), options.precision)]; 784 | }; 785 | 786 | var points = data.map(function (point) { 787 | return predict(point[0]); 788 | }); 789 | 790 | return { 791 | points: points, 792 | predict: predict, 793 | equation: [coeffA, coeffB], 794 | string: 'y = ' + coeffA + 'x^' + coeffB, 795 | r2: round(determinationCoefficient(data, points), options.precision) 796 | }; 797 | }, 798 | polynomial: function polynomial(data, options) { 799 | var lhs = []; 800 | var rhs = []; 801 | var a = 0; 802 | var b = 0; 803 | var len = data.length; 804 | var k = options.order + 1; 805 | 806 | for (var i = 0; i < k; i++) { 807 | for (var l = 0; l < len; l++) { 808 | if (data[l][1] !== null) { 809 | a += Math.pow(data[l][0], i) * data[l][1]; 810 | } 811 | } 812 | 813 | lhs.push(a); 814 | a = 0; 815 | 816 | var c = []; 817 | for (var j = 0; j < k; j++) { 818 | for (var _l = 0; _l < len; _l++) { 819 | if (data[_l][1] !== null) { 820 | b += Math.pow(data[_l][0], i + j); 821 | } 822 | } 823 | c.push(b); 824 | b = 0; 825 | } 826 | rhs.push(c); 827 | } 828 | rhs.push(lhs); 829 | 830 | var coefficients = gaussianElimination(rhs, k).map(function (v) { 831 | return round(v, options.precision); 832 | }); 833 | 834 | var predict = function predict(x) { 835 | return [round(x, options.precision), round(coefficients.reduce(function (sum, coeff, power) { 836 | return sum + coeff * Math.pow(x, power); 837 | }, 0), options.precision)]; 838 | }; 839 | 840 | var points = data.map(function (point) { 841 | return predict(point[0]); 842 | }); 843 | 844 | var string = 'y = '; 845 | for (var _i = coefficients.length - 1; _i >= 0; _i--) { 846 | if (_i > 1) { 847 | string += coefficients[_i] + 'x^' + _i + ' + '; 848 | } else if (_i === 1) { 849 | string += coefficients[_i] + 'x + '; 850 | } else { 851 | string += coefficients[_i]; 852 | } 853 | } 854 | 855 | return { 856 | string: string, 857 | points: points, 858 | predict: predict, 859 | equation: [].concat(_toConsumableArray(coefficients)).reverse(), 860 | r2: round(determinationCoefficient(data, points), options.precision) 861 | }; 862 | } 863 | }; 864 | 865 | function createWrapper() { 866 | var reduce = function reduce(accumulator, name) { 867 | return _extends({ 868 | _round: round 869 | }, accumulator, _defineProperty({}, name, function (data, supplied) { 870 | return methods[name](data, _extends({}, DEFAULT_OPTIONS, supplied)); 871 | })); 872 | }; 873 | 874 | return Object.keys(methods).reduce(reduce, {}); 875 | } 876 | 877 | module.exports = createWrapper(); 878 | }); 879 | 880 | 881 | /***/ }) 882 | 883 | /******/ }); 884 | //# sourceMappingURL=chartjs-plugin-regression-0.1.1.js.map -------------------------------------------------------------------------------- /dist/chartjs-plugin-regression-0.2.0.js: -------------------------------------------------------------------------------- 1 | /******/ (function(modules) { // webpackBootstrap 2 | /******/ // The module cache 3 | /******/ var installedModules = {}; 4 | /******/ 5 | /******/ // The require function 6 | /******/ function __webpack_require__(moduleId) { 7 | /******/ 8 | /******/ // Check if module is in cache 9 | /******/ if(installedModules[moduleId]) { 10 | /******/ return installedModules[moduleId].exports; 11 | /******/ } 12 | /******/ // Create a new module (and put it into the cache) 13 | /******/ var module = installedModules[moduleId] = { 14 | /******/ i: moduleId, 15 | /******/ l: false, 16 | /******/ exports: {} 17 | /******/ }; 18 | /******/ 19 | /******/ // Execute the module function 20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 21 | /******/ 22 | /******/ // Flag the module as loaded 23 | /******/ module.l = true; 24 | /******/ 25 | /******/ // Return the exports of the module 26 | /******/ return module.exports; 27 | /******/ } 28 | /******/ 29 | /******/ 30 | /******/ // expose the modules object (__webpack_modules__) 31 | /******/ __webpack_require__.m = modules; 32 | /******/ 33 | /******/ // expose the module cache 34 | /******/ __webpack_require__.c = installedModules; 35 | /******/ 36 | /******/ // define getter function for harmony exports 37 | /******/ __webpack_require__.d = function(exports, name, getter) { 38 | /******/ if(!__webpack_require__.o(exports, name)) { 39 | /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); 40 | /******/ } 41 | /******/ }; 42 | /******/ 43 | /******/ // define __esModule on exports 44 | /******/ __webpack_require__.r = function(exports) { 45 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { 46 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); 47 | /******/ } 48 | /******/ Object.defineProperty(exports, '__esModule', { value: true }); 49 | /******/ }; 50 | /******/ 51 | /******/ // create a fake namespace object 52 | /******/ // mode & 1: value is a module id, require it 53 | /******/ // mode & 2: merge all properties of value into the ns 54 | /******/ // mode & 4: return value when already ns object 55 | /******/ // mode & 8|1: behave like require 56 | /******/ __webpack_require__.t = function(value, mode) { 57 | /******/ if(mode & 1) value = __webpack_require__(value); 58 | /******/ if(mode & 8) return value; 59 | /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; 60 | /******/ var ns = Object.create(null); 61 | /******/ __webpack_require__.r(ns); 62 | /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); 63 | /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); 64 | /******/ return ns; 65 | /******/ }; 66 | /******/ 67 | /******/ // getDefaultExport function for compatibility with non-harmony modules 68 | /******/ __webpack_require__.n = function(module) { 69 | /******/ var getter = module && module.__esModule ? 70 | /******/ function getDefault() { return module['default']; } : 71 | /******/ function getModuleExports() { return module; }; 72 | /******/ __webpack_require__.d(getter, 'a', getter); 73 | /******/ return getter; 74 | /******/ }; 75 | /******/ 76 | /******/ // Object.prototype.hasOwnProperty.call 77 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 78 | /******/ 79 | /******/ // __webpack_public_path__ 80 | /******/ __webpack_require__.p = ""; 81 | /******/ 82 | /******/ 83 | /******/ // Load entry module and return exports 84 | /******/ return __webpack_require__(__webpack_require__.s = "./lib/index.js"); 85 | /******/ }) 86 | /************************************************************************/ 87 | /******/ ({ 88 | 89 | /***/ "./lib/MetaData.js": 90 | /*!*************************!*\ 91 | !*** ./lib/MetaData.js ***! 92 | \*************************/ 93 | /*! no static exports found */ 94 | /***/ (function(module, exports, __webpack_require__) { 95 | 96 | "use strict"; 97 | 98 | Object.defineProperty(exports, "__esModule", { value: true }); 99 | exports.MetaDataSet = void 0; 100 | var MetaSection_1 = __webpack_require__(/*! ./MetaSection */ "./lib/MetaSection.js"); 101 | var MetaDataSet = /** @class */ (function () { 102 | function MetaDataSet(chart, ds) { 103 | /** Scales wil be initialized in beforeDraw hook */ 104 | this.getXY = undefined; 105 | /** Is the dataset's data an array of {x,y}? */ 106 | this.isXY = false; 107 | var cfg = ds.regressions; 108 | this.chart = chart; 109 | this.dataset = ds; 110 | this.normalizedData = this._normalizeData(ds.data); 111 | this.sections = this._createMetaSections(cfg); 112 | this._calculate(); 113 | } 114 | /** 115 | * Normalize data to DataPoint[] 116 | * Only supports number[] and {x:number,y:number} 117 | */ 118 | MetaDataSet.prototype._normalizeData = function (data) { 119 | var _this = this; 120 | return data.map(function (value, index) { 121 | var p; 122 | if (typeof value == 'number' || value == null || value === undefined) { 123 | p = [index, value]; 124 | } 125 | else { 126 | _this.isXY = true; 127 | p = [value.x, value.y]; 128 | } 129 | return p; 130 | }); 131 | }; 132 | /** @private */ 133 | MetaDataSet.prototype._createMetaSections = function (cfg) { 134 | var _this = this; 135 | var source = cfg.sections || [ 136 | { startIndex: 0, endIndex: this.dataset.data.length - 1 } 137 | ]; 138 | return source.map(function (s) { return new MetaSection_1.MetaSection(s, _this); }); 139 | }; 140 | /** @private */ 141 | MetaDataSet.prototype._calculate = function () { 142 | this.sections.forEach(function (section) { return section.calculate(); }); // Calculate Section Results 143 | }; 144 | MetaDataSet.prototype.adjustScales = function () { 145 | if (this.topY !== undefined) 146 | return; 147 | var xScale; 148 | var yScale; 149 | var scales = this.chart.scales; 150 | Object.keys(scales).forEach(function (k) { return (k[0] == 'x' && (xScale = scales[k])) || (yScale = scales[k]); }); 151 | this.topY = yScale.top; 152 | this.bottomY = yScale.bottom; 153 | this.getXY = function (x, y) { return ({ 154 | x: xScale.getPixelForValue(x, undefined, undefined, true), 155 | y: yScale.getPixelForValue(y) 156 | }); }; 157 | }; 158 | MetaDataSet.prototype.drawRegressions = function () { 159 | var ctx = this.chart.chart.ctx; 160 | ctx.save(); 161 | try { 162 | this.sections.forEach(function (section) { return section.drawRegressions(ctx); }); 163 | } 164 | finally { 165 | ctx.restore(); 166 | } 167 | }; 168 | MetaDataSet.prototype.drawRightBorders = function () { 169 | var ctx = this.chart.chart.ctx; 170 | ctx.save(); 171 | try { 172 | for (var i = 0; i < this.sections.length - 1; i++) 173 | this.sections[i].drawRightBorder(ctx); 174 | } 175 | finally { 176 | ctx.restore(); 177 | } 178 | }; 179 | return MetaDataSet; 180 | }()); 181 | exports.MetaDataSet = MetaDataSet; 182 | //# sourceMappingURL=MetaData.js.map 183 | 184 | /***/ }), 185 | 186 | /***/ "./lib/MetaSection.js": 187 | /*!****************************!*\ 188 | !*** ./lib/MetaSection.js ***! 189 | \****************************/ 190 | /*! no static exports found */ 191 | /***/ (function(module, exports, __webpack_require__) { 192 | 193 | "use strict"; 194 | 195 | Object.defineProperty(exports, "__esModule", { value: true }); 196 | exports.MetaSection = void 0; 197 | var regression = __webpack_require__(/*! regression */ "./node_modules/regression/dist/regression.js"); 198 | var defaultConfig = { 199 | type: 'linear', 200 | calculation: { 201 | precision: 2, 202 | order: 2 203 | }, 204 | line: { 205 | width: 2, 206 | color: '#000', 207 | dash: [] 208 | }, 209 | extendPredictions: false, 210 | copy: { 211 | overwriteData: 'none' 212 | } 213 | }; 214 | var MetaSection = /** @class */ (function () { 215 | function MetaSection(sec, _meta) { 216 | this._meta = _meta; 217 | var chart = _meta.chart; 218 | var ds = _meta.dataset; 219 | var cfg = getConfig([ 220 | 'type', 221 | 'calculation', 222 | 'line', 223 | 'extendPredictions', 224 | 'copy' 225 | ]); 226 | this.startIndex = sec.startIndex || 0; 227 | this.endIndex = sec.endIndex || ds.data.length - 1; 228 | this.type = Array.isArray(cfg.type) ? cfg.type : [cfg.type]; 229 | this.line = cfg.line; 230 | this.calculation = cfg.calculation; 231 | this.extendPredictions = cfg.extendPredictions; 232 | this.copy = cfg.copy; 233 | this.label = 234 | sec.label || this._meta.chart.data.labels[this.endIndex]; 235 | this._validateType(); 236 | // --- constructor helpers 237 | /** 238 | * Calculate the inherited configuration from defaultConfig, globalConfig, 239 | * dataset config, and section config (in that order) 240 | */ 241 | function getConfig(fields) { 242 | var o, p; 243 | var globalConfig = ((o = chart.config.options) && (p = o.plugins) && p.regressions) || {}; 244 | return configMerge(fields, defaultConfig, globalConfig, ds.regressions, sec); 245 | /** merge the config objects */ 246 | function configMerge(fields) { 247 | var cfgList = []; 248 | for (var _i = 1; _i < arguments.length; _i++) { 249 | cfgList[_i - 1] = arguments[_i]; 250 | } 251 | var dstConfig = {}; 252 | fields.forEach(function (f) { 253 | cfgList.forEach(function (srcConfig) { 254 | var o = srcConfig[f]; 255 | var t = typeof o; 256 | if (t != 'undefined') { 257 | if (Array.isArray(o) || t != 'object' || o == null) 258 | dstConfig[f] = o; 259 | else 260 | dstConfig[f] = Object.assign({}, dstConfig[f], configMerge(Object.keys(o), o)); 261 | } 262 | }); 263 | }); 264 | return dstConfig; 265 | } 266 | } 267 | } 268 | /** Validates the type to avoid inconsistences */ 269 | MetaSection.prototype._validateType = function () { 270 | if (this.type.length > 1 && this.type.includes('copy')) 271 | throw Error('Invalid regression type:' + 272 | this.type + 273 | '. "none" cannot be combined with other type!'); 274 | }; 275 | /** Calculates the regression(s) and sets the result objects */ 276 | MetaSection.prototype.calculate = function () { 277 | var sectionData = this._meta.normalizedData.slice(this.startIndex, this.endIndex + 1); 278 | if (this.type[0] == 'copy') 279 | this._calculateCopySection(sectionData); 280 | else 281 | this._calculateBestR2(sectionData); 282 | }; 283 | MetaSection.prototype._calculateBestR2 = function (sectionData) { 284 | var _this = this; 285 | this.result = this.type.reduce(function (max, type) { 286 | var calculation = Object.assign({}, _this.calculation); 287 | var realType = type; 288 | if (/polynomial[34]$/.test(type)) { 289 | calculation.order = parseInt(type.substr(10)); 290 | realType = type.substr(0, 10); 291 | } 292 | var r = regression[realType](sectionData, calculation); 293 | r.type = type; 294 | return !max || max.r2 < r.r2 ? r : max; 295 | }, null); 296 | }; 297 | MetaSection.prototype._calculateCopySection = function (sectionData) { 298 | var _this = this; 299 | var from = this._meta.sections[this.copy.fromSectionIndex], r = (this.result = Object.assign({}, from.result)), overwrite = this.copy.overwriteData, data = this._meta.normalizedData; 300 | r.points = sectionData.map(function (p) { return r.predict(p[0]); }); 301 | delete r.r2; 302 | if (overwrite != 'none') { 303 | var dsdata_1 = this._meta.dataset.data, isXY_1 = this._meta.isXY; 304 | r.points.forEach(function (_a, i) { 305 | var x = _a[0], y = _a[1]; 306 | var index = i + _this.startIndex; 307 | if ((index < from.startIndex || index > from.endIndex) && 308 | (overwrite == 'all' || 309 | (overwrite == 'last' && index == _this.endIndex) || 310 | (overwrite == 'empty' && !data[index]))) { 311 | if (_this.copy.maxValue) 312 | y = Math.min(_this.copy.maxValue, y); 313 | if (_this.copy.minValue !== undefined) 314 | y = Math.max(_this.copy.minValue, y); 315 | dsdata_1[index] = isXY_1 ? { x: x, y: y } : y; 316 | } 317 | }); 318 | } 319 | }; 320 | MetaSection.prototype.drawRightBorder = function (ctx) { 321 | ctx.beginPath(); 322 | this._setLineAttrs(ctx); 323 | ctx.setLineDash([10, 2]); 324 | ctx.lineWidth = 2; 325 | // Print vertical line 326 | var p = this._meta.getXY(this.endIndex, 0); 327 | ctx.moveTo(p.x, this._meta.topY); 328 | ctx.lineTo(p.x, this._meta.bottomY); 329 | ctx.fillStyle = this.line.color; 330 | ctx.fillText(this.label, p.x, this._meta.topY); 331 | ctx.stroke(); 332 | }; 333 | MetaSection.prototype.drawRegressions = function (ctx) { 334 | for (var i = 0, len = this._meta.sections.length; i < len; i++) { 335 | var section = this._meta.sections[i]; 336 | var isMe = section == this; 337 | if ((isMe && this.type[0] != 'copy') || 338 | (!isMe && this.extendPredictions)) { 339 | section.drawRange(ctx, this.startIndex, this.endIndex, !isMe); 340 | } 341 | if (isMe) 342 | break; 343 | } 344 | }; 345 | MetaSection.prototype.drawRange = function (ctx, startIndex, endIndex, forceDash) { 346 | var _this = this; 347 | ctx.beginPath(); 348 | this._setLineAttrs(ctx); 349 | if (forceDash) 350 | ctx.setLineDash([5, 5]); 351 | var predict = this.result.predict; 352 | var f = function (x) { return _this._meta.getXY(x, predict(x)[1]); }; 353 | var p = f(startIndex); 354 | ctx.moveTo(p.x, p.y); 355 | for (var x = startIndex + 1; x <= endIndex; x++) { 356 | p = f(x); 357 | ctx.lineTo(p.x, p.y); 358 | } 359 | ctx.stroke(); 360 | }; 361 | MetaSection.prototype._setLineAttrs = function (ctx) { 362 | if (this.line.width) 363 | ctx.lineWidth = this.line.width; 364 | if (this.line.color) 365 | ctx.strokeStyle = this.line.color; 366 | if (this.line.dash) 367 | ctx.setLineDash(this.line.dash); 368 | }; 369 | return MetaSection; 370 | }()); 371 | exports.MetaSection = MetaSection; 372 | //# sourceMappingURL=MetaSection.js.map 373 | 374 | /***/ }), 375 | 376 | /***/ "./lib/index.js": 377 | /*!**********************!*\ 378 | !*** ./lib/index.js ***! 379 | \**********************/ 380 | /*! no static exports found */ 381 | /***/ (function(module, exports, __webpack_require__) { 382 | 383 | "use strict"; 384 | 385 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 386 | if (k2 === undefined) k2 = k; 387 | Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); 388 | }) : (function(o, m, k, k2) { 389 | if (k2 === undefined) k2 = k; 390 | o[k2] = m[k]; 391 | })); 392 | var __exportStar = (this && this.__exportStar) || function(m, exports) { 393 | for (var p in m) if (p !== "default" && !exports.hasOwnProperty(p)) __createBinding(exports, m, p); 394 | }; 395 | Object.defineProperty(exports, "__esModule", { value: true }); 396 | __exportStar(__webpack_require__(/*! ./types */ "./lib/types.js"), exports); 397 | __exportStar(__webpack_require__(/*! ./MetaData */ "./lib/MetaData.js"), exports); 398 | __exportStar(__webpack_require__(/*! ./MetaSection */ "./lib/MetaSection.js"), exports); 399 | __exportStar(__webpack_require__(/*! ./regression-plugin */ "./lib/regression-plugin.js"), exports); 400 | //# sourceMappingURL=index.js.map 401 | 402 | /***/ }), 403 | 404 | /***/ "./lib/regression-plugin.js": 405 | /*!**********************************!*\ 406 | !*** ./lib/regression-plugin.js ***! 407 | \**********************************/ 408 | /*! no static exports found */ 409 | /***/ (function(module, exports, __webpack_require__) { 410 | 411 | "use strict"; 412 | 413 | Object.defineProperty(exports, "__esModule", { value: true }); 414 | exports.ChartRegressions = void 0; 415 | var MetaData_1 = __webpack_require__(/*! ./MetaData */ "./lib/MetaData.js"); 416 | // Cache for all plugins' metadata 417 | var _metadataMap = {}; 418 | var _chartId = 0; 419 | var Plugin = /** @class */ (function () { 420 | function Plugin() { 421 | this.id = 'regressions'; 422 | } 423 | Plugin.prototype.beforeInit = function (chart) { 424 | chart.$$id = ++_chartId; 425 | }; 426 | /** 427 | * Called after update (when the chart is created and when chart.update() is called) 428 | * @param chart 429 | */ 430 | Plugin.prototype.beforeUpdate = function (chart, options) { 431 | var o, p, r; 432 | var onComplete = (o = chart.config.options) && 433 | (p = o.plugins) && 434 | (r = p.regressions) && 435 | r.onCompleteCalculation; 436 | forEach(chart, function (ds, meta, datasetIndex) { 437 | meta = new MetaData_1.MetaDataSet(chart, ds); 438 | var id = chart.$$id * 1000 + datasetIndex; 439 | _metadataMap[id] = meta; 440 | }); 441 | if (onComplete) 442 | onComplete(chart); 443 | }; 444 | /** 445 | * It's called once before all the drawing 446 | * @param chart 447 | */ 448 | Plugin.prototype.beforeRender = function (chart, options) { 449 | forEach(chart, function (ds, meta) { return meta.adjustScales(); }); 450 | }; 451 | /** Draws the vertical lines before the datasets are drawn */ 452 | Plugin.prototype.beforeDatasetsDraw = function (chart, easing, options) { 453 | forEach(chart, function (ds, meta) { return meta.drawRightBorders(); }); 454 | }; 455 | /** Draws the regression lines */ 456 | Plugin.prototype.afterDatasetsDraw = function (chart, easing, options) { 457 | forEach(chart, function (ds, meta) { return meta.drawRegressions(); }); 458 | }; 459 | Plugin.prototype.destroy = function (chart) { 460 | Object.keys(_metadataMap) 461 | .filter(function (k) { return (k / 1000) >> 0 == chart.$$id; }) 462 | .forEach(function (k) { return delete _metadataMap[k]; }); 463 | }; 464 | /** Get dataset's meta data */ 465 | Plugin.prototype.getDataset = function (chart, datasetIndex) { 466 | var id = chart.$$id * 1000 + datasetIndex; 467 | return _metadataMap[id]; 468 | }; 469 | /** Get dataset's meta sections */ 470 | Plugin.prototype.getSections = function (chart, datasetIndex) { 471 | var ds = this.getDataset(chart, datasetIndex); 472 | return ds && ds.sections; 473 | }; 474 | return Plugin; 475 | }()); 476 | function forEach(chart, fn) { 477 | chart.data.datasets.forEach(function (ds, i) { 478 | if (ds.regressions && chart.isDatasetVisible(i)) { 479 | var meta = exports.ChartRegressions.getDataset(chart, i); 480 | fn(ds, meta, i); 481 | } 482 | }); 483 | } 484 | exports.ChartRegressions = new Plugin(); 485 | window.ChartRegressions = exports.ChartRegressions; 486 | //# sourceMappingURL=regression-plugin.js.map 487 | 488 | /***/ }), 489 | 490 | /***/ "./lib/types.js": 491 | /*!**********************!*\ 492 | !*** ./lib/types.js ***! 493 | \**********************/ 494 | /*! no static exports found */ 495 | /***/ (function(module, exports, __webpack_require__) { 496 | 497 | "use strict"; 498 | 499 | Object.defineProperty(exports, "__esModule", { value: true }); 500 | //# sourceMappingURL=types.js.map 501 | 502 | /***/ }), 503 | 504 | /***/ "./node_modules/regression/dist/regression.js": 505 | /*!****************************************************!*\ 506 | !*** ./node_modules/regression/dist/regression.js ***! 507 | \****************************************************/ 508 | /*! no static exports found */ 509 | /***/ (function(module, exports, __webpack_require__) { 510 | 511 | var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;(function (global, factory) { 512 | if (true) { 513 | !(__WEBPACK_AMD_DEFINE_ARRAY__ = [module], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory), 514 | __WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ? 515 | (__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__), 516 | __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); 517 | } else { var mod; } 518 | })(this, function (module) { 519 | 'use strict'; 520 | 521 | function _defineProperty(obj, key, value) { 522 | if (key in obj) { 523 | Object.defineProperty(obj, key, { 524 | value: value, 525 | enumerable: true, 526 | configurable: true, 527 | writable: true 528 | }); 529 | } else { 530 | obj[key] = value; 531 | } 532 | 533 | return obj; 534 | } 535 | 536 | var _extends = Object.assign || function (target) { 537 | for (var i = 1; i < arguments.length; i++) { 538 | var source = arguments[i]; 539 | 540 | for (var key in source) { 541 | if (Object.prototype.hasOwnProperty.call(source, key)) { 542 | target[key] = source[key]; 543 | } 544 | } 545 | } 546 | 547 | return target; 548 | }; 549 | 550 | function _toConsumableArray(arr) { 551 | if (Array.isArray(arr)) { 552 | for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { 553 | arr2[i] = arr[i]; 554 | } 555 | 556 | return arr2; 557 | } else { 558 | return Array.from(arr); 559 | } 560 | } 561 | 562 | var DEFAULT_OPTIONS = { order: 2, precision: 2, period: null }; 563 | 564 | /** 565 | * Determine the coefficient of determination (r^2) of a fit from the observations 566 | * and predictions. 567 | * 568 | * @param {Array>} data - Pairs of observed x-y values 569 | * @param {Array>} results - Pairs of observed predicted x-y values 570 | * 571 | * @return {number} - The r^2 value, or NaN if one cannot be calculated. 572 | */ 573 | function determinationCoefficient(data, results) { 574 | var predictions = []; 575 | var observations = []; 576 | 577 | data.forEach(function (d, i) { 578 | if (d[1] !== null) { 579 | observations.push(d); 580 | predictions.push(results[i]); 581 | } 582 | }); 583 | 584 | var sum = observations.reduce(function (a, observation) { 585 | return a + observation[1]; 586 | }, 0); 587 | var mean = sum / observations.length; 588 | 589 | var ssyy = observations.reduce(function (a, observation) { 590 | var difference = observation[1] - mean; 591 | return a + difference * difference; 592 | }, 0); 593 | 594 | var sse = observations.reduce(function (accum, observation, index) { 595 | var prediction = predictions[index]; 596 | var residual = observation[1] - prediction[1]; 597 | return accum + residual * residual; 598 | }, 0); 599 | 600 | return 1 - sse / ssyy; 601 | } 602 | 603 | /** 604 | * Determine the solution of a system of linear equations A * x = b using 605 | * Gaussian elimination. 606 | * 607 | * @param {Array>} input - A 2-d matrix of data in row-major form [ A | b ] 608 | * @param {number} order - How many degrees to solve for 609 | * 610 | * @return {Array} - Vector of normalized solution coefficients matrix (x) 611 | */ 612 | function gaussianElimination(input, order) { 613 | var matrix = input; 614 | var n = input.length - 1; 615 | var coefficients = [order]; 616 | 617 | for (var i = 0; i < n; i++) { 618 | var maxrow = i; 619 | for (var j = i + 1; j < n; j++) { 620 | if (Math.abs(matrix[i][j]) > Math.abs(matrix[i][maxrow])) { 621 | maxrow = j; 622 | } 623 | } 624 | 625 | for (var k = i; k < n + 1; k++) { 626 | var tmp = matrix[k][i]; 627 | matrix[k][i] = matrix[k][maxrow]; 628 | matrix[k][maxrow] = tmp; 629 | } 630 | 631 | for (var _j = i + 1; _j < n; _j++) { 632 | for (var _k = n; _k >= i; _k--) { 633 | matrix[_k][_j] -= matrix[_k][i] * matrix[i][_j] / matrix[i][i]; 634 | } 635 | } 636 | } 637 | 638 | for (var _j2 = n - 1; _j2 >= 0; _j2--) { 639 | var total = 0; 640 | for (var _k2 = _j2 + 1; _k2 < n; _k2++) { 641 | total += matrix[_k2][_j2] * coefficients[_k2]; 642 | } 643 | 644 | coefficients[_j2] = (matrix[n][_j2] - total) / matrix[_j2][_j2]; 645 | } 646 | 647 | return coefficients; 648 | } 649 | 650 | /** 651 | * Round a number to a precision, specificed in number of decimal places 652 | * 653 | * @param {number} number - The number to round 654 | * @param {number} precision - The number of decimal places to round to: 655 | * > 0 means decimals, < 0 means powers of 10 656 | * 657 | * 658 | * @return {numbr} - The number, rounded 659 | */ 660 | function round(number, precision) { 661 | var factor = Math.pow(10, precision); 662 | return Math.round(number * factor) / factor; 663 | } 664 | 665 | /** 666 | * The set of all fitting methods 667 | * 668 | * @namespace 669 | */ 670 | var methods = { 671 | linear: function linear(data, options) { 672 | var sum = [0, 0, 0, 0, 0]; 673 | var len = 0; 674 | 675 | for (var n = 0; n < data.length; n++) { 676 | if (data[n][1] !== null) { 677 | len++; 678 | sum[0] += data[n][0]; 679 | sum[1] += data[n][1]; 680 | sum[2] += data[n][0] * data[n][0]; 681 | sum[3] += data[n][0] * data[n][1]; 682 | sum[4] += data[n][1] * data[n][1]; 683 | } 684 | } 685 | 686 | var run = len * sum[2] - sum[0] * sum[0]; 687 | var rise = len * sum[3] - sum[0] * sum[1]; 688 | var gradient = run === 0 ? 0 : round(rise / run, options.precision); 689 | var intercept = round(sum[1] / len - gradient * sum[0] / len, options.precision); 690 | 691 | var predict = function predict(x) { 692 | return [round(x, options.precision), round(gradient * x + intercept, options.precision)]; 693 | }; 694 | 695 | var points = data.map(function (point) { 696 | return predict(point[0]); 697 | }); 698 | 699 | return { 700 | points: points, 701 | predict: predict, 702 | equation: [gradient, intercept], 703 | r2: round(determinationCoefficient(data, points), options.precision), 704 | string: intercept === 0 ? 'y = ' + gradient + 'x' : 'y = ' + gradient + 'x + ' + intercept 705 | }; 706 | }, 707 | exponential: function exponential(data, options) { 708 | var sum = [0, 0, 0, 0, 0, 0]; 709 | 710 | for (var n = 0; n < data.length; n++) { 711 | if (data[n][1] !== null) { 712 | sum[0] += data[n][0]; 713 | sum[1] += data[n][1]; 714 | sum[2] += data[n][0] * data[n][0] * data[n][1]; 715 | sum[3] += data[n][1] * Math.log(data[n][1]); 716 | sum[4] += data[n][0] * data[n][1] * Math.log(data[n][1]); 717 | sum[5] += data[n][0] * data[n][1]; 718 | } 719 | } 720 | 721 | var denominator = sum[1] * sum[2] - sum[5] * sum[5]; 722 | var a = Math.exp((sum[2] * sum[3] - sum[5] * sum[4]) / denominator); 723 | var b = (sum[1] * sum[4] - sum[5] * sum[3]) / denominator; 724 | var coeffA = round(a, options.precision); 725 | var coeffB = round(b, options.precision); 726 | var predict = function predict(x) { 727 | return [round(x, options.precision), round(coeffA * Math.exp(coeffB * x), options.precision)]; 728 | }; 729 | 730 | var points = data.map(function (point) { 731 | return predict(point[0]); 732 | }); 733 | 734 | return { 735 | points: points, 736 | predict: predict, 737 | equation: [coeffA, coeffB], 738 | string: 'y = ' + coeffA + 'e^(' + coeffB + 'x)', 739 | r2: round(determinationCoefficient(data, points), options.precision) 740 | }; 741 | }, 742 | logarithmic: function logarithmic(data, options) { 743 | var sum = [0, 0, 0, 0]; 744 | var len = data.length; 745 | 746 | for (var n = 0; n < len; n++) { 747 | if (data[n][1] !== null) { 748 | sum[0] += Math.log(data[n][0]); 749 | sum[1] += data[n][1] * Math.log(data[n][0]); 750 | sum[2] += data[n][1]; 751 | sum[3] += Math.pow(Math.log(data[n][0]), 2); 752 | } 753 | } 754 | 755 | var a = (len * sum[1] - sum[2] * sum[0]) / (len * sum[3] - sum[0] * sum[0]); 756 | var coeffB = round(a, options.precision); 757 | var coeffA = round((sum[2] - coeffB * sum[0]) / len, options.precision); 758 | 759 | var predict = function predict(x) { 760 | return [round(x, options.precision), round(round(coeffA + coeffB * Math.log(x), options.precision), options.precision)]; 761 | }; 762 | 763 | var points = data.map(function (point) { 764 | return predict(point[0]); 765 | }); 766 | 767 | return { 768 | points: points, 769 | predict: predict, 770 | equation: [coeffA, coeffB], 771 | string: 'y = ' + coeffA + ' + ' + coeffB + ' ln(x)', 772 | r2: round(determinationCoefficient(data, points), options.precision) 773 | }; 774 | }, 775 | power: function power(data, options) { 776 | var sum = [0, 0, 0, 0, 0]; 777 | var len = data.length; 778 | 779 | for (var n = 0; n < len; n++) { 780 | if (data[n][1] !== null) { 781 | sum[0] += Math.log(data[n][0]); 782 | sum[1] += Math.log(data[n][1]) * Math.log(data[n][0]); 783 | sum[2] += Math.log(data[n][1]); 784 | sum[3] += Math.pow(Math.log(data[n][0]), 2); 785 | } 786 | } 787 | 788 | var b = (len * sum[1] - sum[0] * sum[2]) / (len * sum[3] - Math.pow(sum[0], 2)); 789 | var a = (sum[2] - b * sum[0]) / len; 790 | var coeffA = round(Math.exp(a), options.precision); 791 | var coeffB = round(b, options.precision); 792 | 793 | var predict = function predict(x) { 794 | return [round(x, options.precision), round(round(coeffA * Math.pow(x, coeffB), options.precision), options.precision)]; 795 | }; 796 | 797 | var points = data.map(function (point) { 798 | return predict(point[0]); 799 | }); 800 | 801 | return { 802 | points: points, 803 | predict: predict, 804 | equation: [coeffA, coeffB], 805 | string: 'y = ' + coeffA + 'x^' + coeffB, 806 | r2: round(determinationCoefficient(data, points), options.precision) 807 | }; 808 | }, 809 | polynomial: function polynomial(data, options) { 810 | var lhs = []; 811 | var rhs = []; 812 | var a = 0; 813 | var b = 0; 814 | var len = data.length; 815 | var k = options.order + 1; 816 | 817 | for (var i = 0; i < k; i++) { 818 | for (var l = 0; l < len; l++) { 819 | if (data[l][1] !== null) { 820 | a += Math.pow(data[l][0], i) * data[l][1]; 821 | } 822 | } 823 | 824 | lhs.push(a); 825 | a = 0; 826 | 827 | var c = []; 828 | for (var j = 0; j < k; j++) { 829 | for (var _l = 0; _l < len; _l++) { 830 | if (data[_l][1] !== null) { 831 | b += Math.pow(data[_l][0], i + j); 832 | } 833 | } 834 | c.push(b); 835 | b = 0; 836 | } 837 | rhs.push(c); 838 | } 839 | rhs.push(lhs); 840 | 841 | var coefficients = gaussianElimination(rhs, k).map(function (v) { 842 | return round(v, options.precision); 843 | }); 844 | 845 | var predict = function predict(x) { 846 | return [round(x, options.precision), round(coefficients.reduce(function (sum, coeff, power) { 847 | return sum + coeff * Math.pow(x, power); 848 | }, 0), options.precision)]; 849 | }; 850 | 851 | var points = data.map(function (point) { 852 | return predict(point[0]); 853 | }); 854 | 855 | var string = 'y = '; 856 | for (var _i = coefficients.length - 1; _i >= 0; _i--) { 857 | if (_i > 1) { 858 | string += coefficients[_i] + 'x^' + _i + ' + '; 859 | } else if (_i === 1) { 860 | string += coefficients[_i] + 'x + '; 861 | } else { 862 | string += coefficients[_i]; 863 | } 864 | } 865 | 866 | return { 867 | string: string, 868 | points: points, 869 | predict: predict, 870 | equation: [].concat(_toConsumableArray(coefficients)).reverse(), 871 | r2: round(determinationCoefficient(data, points), options.precision) 872 | }; 873 | } 874 | }; 875 | 876 | function createWrapper() { 877 | var reduce = function reduce(accumulator, name) { 878 | return _extends({ 879 | _round: round 880 | }, accumulator, _defineProperty({}, name, function (data, supplied) { 881 | return methods[name](data, _extends({}, DEFAULT_OPTIONS, supplied)); 882 | })); 883 | }; 884 | 885 | return Object.keys(methods).reduce(reduce, {}); 886 | } 887 | 888 | module.exports = createWrapper(); 889 | }); 890 | 891 | 892 | /***/ }) 893 | 894 | /******/ }); 895 | //# sourceMappingURL=chartjs-plugin-regression-0.2.0.js.map --------------------------------------------------------------------------------