├── tsconfig.json ├── .gitignore ├── README.md ├── package.json ├── LICENSE ├── test.js └── index.ts /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "lib": ["es2018"], 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "declaration": true, 8 | "isolatedModules": true, 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "strict": true, 12 | "skipLibCheck": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | # Compiled output 31 | index.js 32 | index.d.ts -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cubic-spline 2 | 3 | A slight modification of [Ivan Kuckir's cubic spline implementation](http://blog.ivank.net/interpolation-with-cubic-splines.html), cubic-spline guesses the value of y for any x value on a line. This is helpful for smoothing line graphs. 4 | 5 | ## installation 6 | 7 | ```sh 8 | npm install cubic-spline 9 | ``` 10 | 11 | ## usage 12 | 13 | ```js 14 | const Spline = require('cubic-spline'); 15 | 16 | const xs = [1, 2, 3, 4, 5]; 17 | const ys = [9, 3, 6, 2, 4]; 18 | 19 | // new a Spline object 20 | const spline = new Spline(xs, ys); 21 | 22 | // get Y at arbitrary X 23 | console.log(spline.at(1.4)); 24 | 25 | // interpolate a line at a higher resolution 26 | for (let i = 0; i < 50; i++) { 27 | console.log(spline.at(i * 0.1)); 28 | } 29 | ``` 30 | 31 | ## test 32 | 33 | ```sh 34 | npm test 35 | ``` 36 | 37 | ## lint 38 | 39 | ```sh 40 | npm run lint 41 | ``` 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cubic-spline", 3 | "version": "3.0.3", 4 | "description": "spline interpolation", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "scripts": { 8 | "build": "tsc", 9 | "build:watch": "tsc --watch", 10 | "test": "node test.js", 11 | "lint": "prettier **/*.js **/*.json --write", 12 | "prepublishOnly": "npm run build" 13 | }, 14 | "files": [ 15 | "index.js", 16 | "index.d.ts" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/morganherlocker/cubic-spline.git" 21 | }, 22 | "keywords": [ 23 | "cubic", 24 | "spline", 25 | "interpolation", 26 | "line", 27 | "smooth", 28 | "smoothing" 29 | ], 30 | "author": "morganherlocker", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/morganherlocker/cubic-spline/issues" 34 | }, 35 | "homepage": "https://github.com/morganherlocker/cubic-spline", 36 | "devDependencies": { 37 | "prettier": "2.7.1", 38 | "tape": "^5.6.1", 39 | "typescript": "~4.8.4" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Morgan Herlocker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const Spline = require("./"); 2 | const test = require("tape"); 3 | 4 | test("spline", function (t) { 5 | const xs = [1, 2, 3, 4, 5]; 6 | const ys = [9, 3, 6, 2, 4]; 7 | 8 | // new a Spline object 9 | const spline = new Spline(xs, ys); 10 | 11 | // get Y at arbitrary X 12 | t.equal(spline.at(1.4), 5.586); 13 | 14 | // interpolate a line at a higher resolution 15 | for (var i = 0; i < 50; i++) { 16 | t.equal(typeof spline.at(i * 0.1), "number"); 17 | } 18 | t.end(); 19 | }); 20 | 21 | test("extrapolate outside bounds", function (t) { 22 | const xs = [0, 1]; 23 | const ys = [0, 1]; 24 | 25 | const spline = new Spline(xs, ys); 26 | 27 | // In practice you would only want to do this slightly outside the bounds 28 | t.equal(spline.at(-1), -1); 29 | t.equal(spline.at(2), 2); 30 | 31 | t.end(); 32 | }); 33 | 34 | test("speed", function (t) { 35 | let arraySizes = [10, 100, 1000, 2000, 4000]; 36 | let arraySpeed = []; 37 | let arrayInitialization = []; 38 | let multiplicationFactor = 10; // array.length * multiplicationFactor = number of times the .at()-function is executed on the same array 39 | for (let i = 0; i < arraySizes.length; i++) { 40 | // generate x-y-set 41 | let setX = []; 42 | let setY = []; 43 | let x = 0; 44 | let y = 0; 45 | for (let j = 0; j < arraySizes[i]; j++) { 46 | x += 0.05; 47 | y = Math.sin(x); 48 | setX.push(x.toFixed(2)); 49 | setY.push(y.toFixed(4)); 50 | } 51 | // run cubic-spline-interpolation 52 | let initializationStart = new Date(); 53 | const spline = new Spline(setX, setY); 54 | let initializationEnd = new Date(); 55 | // interpolate spline at 10x resolution 56 | let atStart = new Date(); 57 | for (let j = 0; j < arraySizes[i] * multiplicationFactor; j++) { 58 | spline.at(j * 0.05 + 0.01); // create a small offset to the actual data set 59 | } 60 | let atEnd = new Date(); 61 | arrayInitialization[i] = initializationEnd - initializationStart; 62 | arraySpeed[i] = atEnd - atStart; 63 | } 64 | // print result 65 | console.log(`elements in | times .at() | initialization | execution time`); 66 | console.log(`dataset | is used | time of spline | of all .at()'s`); 67 | console.log(`------------|-------------|----------------|---------------`); 68 | for (let i = 0; i < arraySizes.length; i++) { 69 | let size = arraySizes[i].toString().padEnd(11); 70 | let at = (arraySizes[i] * multiplicationFactor).toString().padStart(11); 71 | let initializationTime = arrayInitialization[i].toString().padStart(12); 72 | let atTime = arraySpeed[i].toString().padStart(12); 73 | console.log(`${size} | ${at} | ${initializationTime}ms | ${atTime}ms`); 74 | } 75 | t.end(); 76 | }); 77 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | class Spline { 2 | readonly xs: number[]; 3 | readonly ys: number[]; 4 | readonly ks: Float64Array; 5 | 6 | constructor(xs: number[], ys: number[]) { 7 | this.xs = xs; 8 | this.ys = ys; 9 | this.ks = this.getNaturalKs(new Float64Array(this.xs.length)); 10 | } 11 | 12 | getNaturalKs(ks: Float64Array): Float64Array { 13 | const n = this.xs.length - 1; 14 | const A = zerosMat(n + 1, n + 2); 15 | 16 | for ( 17 | let i = 1; 18 | i < n; 19 | i++ // rows 20 | ) { 21 | A[i][i - 1] = 1 / (this.xs[i] - this.xs[i - 1]); 22 | A[i][i] = 23 | 2 * 24 | (1 / (this.xs[i] - this.xs[i - 1]) + 1 / (this.xs[i + 1] - this.xs[i])); 25 | A[i][i + 1] = 1 / (this.xs[i + 1] - this.xs[i]); 26 | A[i][n + 1] = 27 | 3 * 28 | ((this.ys[i] - this.ys[i - 1]) / 29 | ((this.xs[i] - this.xs[i - 1]) * (this.xs[i] - this.xs[i - 1])) + 30 | (this.ys[i + 1] - this.ys[i]) / 31 | ((this.xs[i + 1] - this.xs[i]) * (this.xs[i + 1] - this.xs[i]))); 32 | } 33 | 34 | A[0][0] = 2 / (this.xs[1] - this.xs[0]); 35 | A[0][1] = 1 / (this.xs[1] - this.xs[0]); 36 | A[0][n + 1] = 37 | (3 * (this.ys[1] - this.ys[0])) / 38 | ((this.xs[1] - this.xs[0]) * (this.xs[1] - this.xs[0])); 39 | 40 | A[n][n - 1] = 1 / (this.xs[n] - this.xs[n - 1]); 41 | A[n][n] = 2 / (this.xs[n] - this.xs[n - 1]); 42 | A[n][n + 1] = 43 | (3 * (this.ys[n] - this.ys[n - 1])) / 44 | ((this.xs[n] - this.xs[n - 1]) * (this.xs[n] - this.xs[n - 1])); 45 | 46 | return solve(A, ks); 47 | } 48 | 49 | /** 50 | * inspired by https://stackoverflow.com/a/40850313/4417327 51 | */ 52 | getIndexBefore(target: number): number { 53 | let low = 0; 54 | let high = this.xs.length; 55 | let mid = 0; 56 | while (low < high) { 57 | mid = Math.floor((low + high) / 2); 58 | if (this.xs[mid] < target && mid !== low) { 59 | low = mid; 60 | } else if (this.xs[mid] >= target && mid !== high) { 61 | high = mid; 62 | } else { 63 | high = low; 64 | } 65 | } 66 | 67 | if (low === this.xs.length - 1) { 68 | return this.xs.length - 1; 69 | } 70 | 71 | return low + 1; 72 | } 73 | 74 | at(x: number): number { 75 | let i = this.getIndexBefore(x); 76 | const t = (x - this.xs[i - 1]) / (this.xs[i] - this.xs[i - 1]); 77 | const a = 78 | this.ks[i - 1] * (this.xs[i] - this.xs[i - 1]) - 79 | (this.ys[i] - this.ys[i - 1]); 80 | const b = 81 | -this.ks[i] * (this.xs[i] - this.xs[i - 1]) + 82 | (this.ys[i] - this.ys[i - 1]); 83 | const q = 84 | (1 - t) * this.ys[i - 1] + 85 | t * this.ys[i] + 86 | t * (1 - t) * (a * (1 - t) + b * t); 87 | return q; 88 | } 89 | } 90 | 91 | export = Spline; 92 | 93 | function solve(A: Float64Array[], ks: Float64Array): Float64Array { 94 | const m = A.length; 95 | let h = 0; 96 | let k = 0; 97 | while (h < m && k <= m) { 98 | let i_max = 0; 99 | let max = -Infinity; 100 | for (let i = h; i < m; i++) { 101 | const v = Math.abs(A[i][k]); 102 | if (v > max) { 103 | i_max = i; 104 | max = v; 105 | } 106 | } 107 | 108 | if (A[i_max][k] === 0) { 109 | k++; 110 | } else { 111 | swapRows(A, h, i_max); 112 | for (let i = h + 1; i < m; i++) { 113 | const f = A[i][k] / A[h][k]; 114 | A[i][k] = 0; 115 | for (let j = k + 1; j <= m; j++) A[i][j] -= A[h][j] * f; 116 | } 117 | h++; 118 | k++; 119 | } 120 | } 121 | 122 | for ( 123 | let i = m - 1; 124 | i >= 0; 125 | i-- // rows = columns 126 | ) { 127 | var v = 0; 128 | if (A[i][i]) { 129 | v = A[i][m] / A[i][i]; 130 | } 131 | ks[i] = v; 132 | for ( 133 | let j = i - 1; 134 | j >= 0; 135 | j-- // rows 136 | ) { 137 | A[j][m] -= A[j][i] * v; 138 | A[j][i] = 0; 139 | } 140 | } 141 | return ks; 142 | } 143 | 144 | function zerosMat(r: number, c: number): Float64Array[] { 145 | const A = []; 146 | for (let i = 0; i < r; i++) A.push(new Float64Array(c)); 147 | return A; 148 | } 149 | 150 | function swapRows(m: Float64Array[], k: number, l: number): void { 151 | let p = m[k]; 152 | m[k] = m[l]; 153 | m[l] = p; 154 | } 155 | --------------------------------------------------------------------------------