├── .npmignore ├── .gitignore ├── test ├── index.js ├── 01-distance.js └── 10-kmpp.js ├── .travis.yml ├── lib ├── distance │ ├── l1.js │ ├── sqrt.js │ ├── l2.js │ ├── l.js │ └── index.js ├── initialize-naive.js ├── iterate.js ├── index.js └── initialize-kmpp.js ├── CHANGELOG.md ├── package.json ├── README.md └── example └── index.js /.npmignore: -------------------------------------------------------------------------------- 1 | examples 2 | test 3 | www 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | *.log 3 | lib-cov 4 | node_modules 5 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | require('./01-distance'); 2 | require('./10-kmpp'); 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "12" 4 | - "11" 5 | - "10" 6 | - "9" 7 | -------------------------------------------------------------------------------- /lib/distance/l1.js: -------------------------------------------------------------------------------- 1 | module.exports = function L1Distance (a, b) { 2 | var i, sum; 3 | for (i = a.length - 1, sum = 0; i >= 0; i--) { 4 | sum += Math.abs(a[i] - b[i]); 5 | } 6 | return sum; 7 | }; 8 | -------------------------------------------------------------------------------- /lib/distance/sqrt.js: -------------------------------------------------------------------------------- 1 | module.exports = function LSqrtDistance (a, b) { 2 | var i, sum; 3 | for (i = a.length - 1, sum = 0; i >= 0; i--) { 4 | sum += Math.sqrt(Math.abs(a[i] - b[i])); 5 | } 6 | return sum * sum; 7 | }; 8 | -------------------------------------------------------------------------------- /lib/distance/l2.js: -------------------------------------------------------------------------------- 1 | module.exports = function L2Distance (a, b) { 2 | var i, sum, dx; 3 | for (i = a.length - 1, sum = 0; i >= 0; i--) { 4 | dx = a[i] - b[i]; 5 | sum += dx * dx; 6 | } 7 | return Math.sqrt(sum); 8 | }; 9 | -------------------------------------------------------------------------------- /lib/distance/l.js: -------------------------------------------------------------------------------- 1 | module.exports = function (L) { 2 | return function LDistance (a, b) { 3 | var i, sum; 4 | for (i = a.length - 1, sum = 0; i >= 0; i--) { 5 | sum += Math.pow(Math.abs(a[i] - b[i]), L); 6 | } 7 | return Math.pow(sum, 1 / L); 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /test/01-distance.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | var distanceMetric = require('../lib/distance'); 3 | 4 | test('is a function', function (t) { 5 | t.equal(typeof distanceMetric, 'function'); 6 | t.end(); 7 | }); 8 | 9 | test('L1 norm', function (t) { 10 | var distance = distanceMetric(1); 11 | t.equal(distance([2], [5]), 3); 12 | t.equal(distance([-2], [-5]), 3); 13 | t.end(); 14 | }); 15 | -------------------------------------------------------------------------------- /lib/initialize-naive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = initializeNaive; 4 | 5 | function initializeNaive (k, points, state) { 6 | var i, j, l; 7 | var dim = points[0].length; 8 | for (i = 0; i < k; i++) { 9 | j = ~~(Math.random() * points.length); 10 | for (l = 0; l < dim; l++) { 11 | state.centroids[i][l] = points[j][l]; 12 | } 13 | state.assignments[i] = j; 14 | state.counts[i] = 1; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/distance/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var LSqrtDistance = require('./sqrt'); 4 | var L1Distance = require('./l1'); 5 | var L2Distance = require('./l2'); 6 | var LDistance = require('./l'); 7 | 8 | module.exports = function (L) { 9 | if (L === 0.5) { 10 | return LSqrtDistance; 11 | } 12 | if (L === 1) { 13 | return L1Distance; 14 | } 15 | if (L === 2) { 16 | return L2Distance; 17 | } 18 | return LDistance(L); 19 | }; 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | 0.1.2 - 2019/10/31 4 | 5 | - Adressing vulnerabilities: 6 | - Bump lodash from 4.17.11 to 4.17.15. 7 | - Bump eslint-utils from 1.3.1 to 1.4.3. 8 | - Updating all dependencies to most recent versions. 9 | 10 | 0.1.1 - 2019/06/12 11 | 12 | - Updating [budo](https://github.com/mattdesl/budo) to recent version due to potential security vulnerabilities. 13 | - Updating all dependencies to most recent versions. 14 | 15 | 0.1.0 - 2019/06/03 16 | 17 | - refactored API thanks to [rreuser](https://github.com/rresuer) 18 | 19 | 0.0.34 - 2015/10/19 20 | 21 | - renamed to "kmpp" and released on the NPM 22 | 23 | - performance optimizations 24 | 25 | - using Manhattan distance instead of euclean distance 26 | - reducing function calls 27 | - random initialization by choosing from exising points, thus maxWidth 28 | and maxHeight are not required anymore 29 | 30 | 0.0.2 - 2012/12/05 31 | 32 | - Removed unnecessary dependency on _/Lo-Dash 33 | 34 | 0.0.1 - 2012/08/15 35 | 36 | - Initial release -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kmpp", 3 | "version": "0.1.2", 4 | "description": "k-means with k-means++-initialization", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "node test", 8 | "start": "budo --open --live --host localhost example/index.js", 9 | "build": "browserify -g es2040 example/index.js | uglifyjs -cm | indexhtmlify | html-inject-github-corner > docs/index.html", 10 | "lint": "semistandard", 11 | "lint-fix": "semistandard --fix" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git://github.com/cmtt/kmpp.git" 16 | }, 17 | "keywords": [ 18 | "k-means", 19 | "k-means++", 20 | "clustering", 21 | "data", 22 | "partition", 23 | "algorithm", 24 | "kmeans", 25 | "browser" 26 | ], 27 | "author": "Matthias Thoemmes ", 28 | "contributors": [ 29 | { 30 | "name": "Matthias Thoemmes", 31 | "email": "thoemmes@gmail.com" 32 | }, 33 | { 34 | "name": "Ricky Reusser", 35 | "email": "rsreusser@gmail.com" 36 | } 37 | ], 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/cmtt/kmpp/issues" 41 | }, 42 | "homepage": "https://github.com/cmtt/kmpp", 43 | "devDependencies": { 44 | "assert": "^2.0.0", 45 | "budo": "^11.6.3", 46 | "control-panel": "^1.3.4", 47 | "create-html": "^4.1.0", 48 | "es2040": "^1.2.6", 49 | "fail-nicely": "^2.0.0", 50 | "float-hsl2rgb": "^1.0.2", 51 | "h": "^1.0.0", 52 | "html-inject-github-corner": "^2.1.4", 53 | "indexhtmlify": "^2.0.0", 54 | "insert-css": "^2.0.0", 55 | "regl": "^1.3.13", 56 | "semistandard": "^14.2.0", 57 | "tape": "^4.11.0", 58 | "uglify-js": "^3.6.5" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/iterate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = iterate; 4 | 5 | function iterate (k, points, state, distance) { 6 | var i, j, p, c, cnt, minDist, dist; 7 | var converged = true; 8 | 9 | // Unpack the state: 10 | var centroids = state.centroids; 11 | var counts = state.counts; 12 | var assignments = state.assignments; 13 | 14 | var n = points.length; 15 | var dim = points[0].length - 1; 16 | 17 | // Zero the arrays of sums and counts for this iteration 18 | for (i = 0; i < k; i++) { 19 | counts[i] = 0; 20 | } 21 | 22 | // Find the closest centroid for each point 23 | for (i = 0; i < n; i++) { 24 | // Finds the centroid with the closest distance to the current point 25 | c = 0; 26 | minDist = distance(centroids[0], points[i]); 27 | 28 | for (j = 1; j < k; j++) { 29 | dist = distance(centroids[j], points[i]); 30 | if (dist < minDist) { 31 | minDist = dist; 32 | c = j; 33 | } 34 | } 35 | 36 | // If the result has changed, then has not converged: 37 | if (assignments[i] === undefined || assignments[i] !== c) { 38 | converged = false; 39 | } 40 | 41 | // Assign the point to the centroid 42 | assignments[i] = c; 43 | counts[c]++; 44 | } 45 | 46 | // Zero out the centroids: 47 | for (i = 0; i < k; i++) { 48 | for (j = dim; j >= 0; j--) { 49 | centroids[i][j] = 0; 50 | } 51 | } 52 | 53 | // Add the contribution of each centroid member: 54 | for (i = 0; i < n; i++) { 55 | c = centroids[assignments[i]]; 56 | p = points[i]; 57 | for (j = dim; j >= 0; j--) { 58 | c[j] += p[j]; 59 | } 60 | } 61 | 62 | // Once accumulated, average 63 | for (i = 0; i < k; i++) { 64 | c = centroids[i]; 65 | cnt = counts[i]; 66 | for (j = dim; j >= 0; j--) { 67 | c[j] /= cnt; 68 | } 69 | } 70 | 71 | return converged; 72 | } 73 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = kmeans; 4 | 5 | var initializeKmpp = require('./initialize-kmpp'); 6 | var initializeNaive = require('./initialize-naive'); 7 | var iterate = require('./iterate'); 8 | var distanceMetric = require('./distance'); 9 | 10 | function kmeans (points, opts) { 11 | var i, k, n, dim, iter, c, initializer, initialize; 12 | 13 | opts = opts || {}; 14 | var kmpp = opts.kmpp === undefined ? true : !!opts.kmpp; 15 | var norm = opts.norm === undefined ? 2 : opts.norm; 16 | var distance = opts.distance === undefined ? distanceMetric(norm) : opts.distance; 17 | var maxIterations = opts.maxIterations === undefined ? 100 : opts.maxIterations; 18 | 19 | n = points.length; 20 | dim = points[0].length; 21 | 22 | if (opts.k === undefined) { 23 | k = ~~(Math.sqrt(n * 0.5)); 24 | } else { 25 | k = opts.k; 26 | } 27 | 28 | var out = {}; 29 | if (Array.isArray(opts.centroids) && opts.centroids.length === k) { 30 | initialize = opts.initialize === undefined ? false : opts.initialize; 31 | out.centroids = opts.centroids; 32 | } else { 33 | initialize = opts.initialize === undefined ? true : opts.initialize; 34 | out.centroids = new Array(k); 35 | } 36 | 37 | if (Array.isArray(opts.counts) && opts.counts.length === k) { 38 | out.counts = opts.counts; 39 | } else { 40 | out.counts = new Array(k); 41 | } 42 | 43 | if (Array.isArray(opts.assignments) && opts.assignments.length === n) { 44 | out.assignments = opts.assignments; 45 | } else { 46 | out.assignments = new Array(k); 47 | } 48 | 49 | // Initialize the components of the centroids if they don't look right: 50 | for (i = 0; i < k; i++) { 51 | c = out.centroids[i]; 52 | if (!Array.isArray(c) || out.centroids[i].length !== dim) { 53 | out.centroids[i] = []; 54 | } 55 | } 56 | 57 | if (initialize) { 58 | initializer = kmpp ? initializeKmpp : initializeNaive; 59 | 60 | initializer(k, points, out, distance); 61 | } 62 | 63 | out.converged = out.converged || false; 64 | iter = 0; 65 | 66 | while (!out.converged && ++iter <= maxIterations) { 67 | out.converged = iterate(k, points, out, distance); 68 | } 69 | 70 | out.iterations = iter; 71 | 72 | return out; 73 | } 74 | -------------------------------------------------------------------------------- /lib/initialize-kmpp.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = initializeKmpp; 4 | 5 | function initializeKmpp (k, points, state, distance) { 6 | var i, j, l, m, cmp1, cmp2, Dsum, tmpDsum, D, ntries, bestIdx; 7 | var p, bestDsum, rndVal, tmpD; 8 | var n = points.length; 9 | var dim = points[0].length; 10 | 11 | var centroids = state.centroids; 12 | var counts = state.counts; 13 | var assignments = state.assignments; 14 | 15 | // K-Means++ initialization 16 | 17 | // determine the amount of tries 18 | D = []; 19 | ntries = 2 + Math.round(Math.log(k)); 20 | 21 | // 1. Choose one center uniformly at random from the data points. 22 | p = points[~~(Math.random() * n)]; 23 | 24 | for (i = dim - 1; i >= 0; i--) { 25 | centroids[0][i] = p[i]; 26 | } 27 | assignments[0] = 0; 28 | counts[0] = 1; 29 | 30 | // 2. For each data point x, compute D(x), the distance between x and 31 | // the nearest center that has already been chosen. 32 | for (i = 0, Dsum = 0; i < n; ++i) { 33 | D[i] = distance(p, points[i]); 34 | Dsum += D[i]; 35 | } 36 | 37 | // 3. Choose one new data point at random as a new center, using a 38 | // weighted probability distribution where a point x is chosen with 39 | // probability proportional to D(x)^2. (Repeated until k centers 40 | // have been chosen.) 41 | for (i = 1; i < k; ++i) { 42 | bestDsum = Infinity; 43 | bestIdx = -1; 44 | 45 | for (j = 0; j < ntries; ++j) { 46 | rndVal = Math.random() * Dsum; 47 | for (l = 0; l < n; ++l) { 48 | if (rndVal <= D[l]) { 49 | break; 50 | } else { 51 | rndVal -= D[l]; 52 | } 53 | } 54 | 55 | tmpD = []; 56 | for (m = 0, tmpDsum = 0; m < n; ++m) { 57 | cmp1 = D[m]; 58 | cmp2 = distance(points[m], points[l]); 59 | tmpD[m] = cmp1 > cmp2 ? cmp2 : cmp1; 60 | tmpDsum += tmpD[m]; 61 | } 62 | 63 | if (tmpDsum < bestDsum) { 64 | bestDsum = tmpDsum; 65 | bestIdx = l; 66 | } 67 | } 68 | 69 | Dsum = bestDsum; 70 | 71 | p = points[bestIdx]; 72 | for (j = dim - 1; j >= 0; j--) { 73 | centroids[i][j] = p[j]; 74 | } 75 | centroids[i] = points[bestIdx].slice(0); 76 | assignments[i] = i; 77 | counts[i] = 1; 78 | 79 | for (j = 0; j < n; j++) { 80 | cmp1 = D[j]; 81 | cmp2 = distance(points[bestIdx], points[j]); 82 | D[j] = cmp1 > cmp2 ? cmp2 : cmp1; 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /test/10-kmpp.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var kmpp = require('../lib'); 3 | var test = require('tape'); 4 | 5 | test('effectively never fails given km++ initilaization', function (t) { 6 | var i, run, k, n, x, clusterOffset, 7 | runs; 8 | 9 | k = 3; 10 | n = 20; 11 | clusterOffset = 1e6; 12 | runs = 10000; 13 | 14 | // Fill with points, roughly speaking: 15 | // 16 | // 0, 1, 2, 3, 100000, 100001, 100002, 100003, 200001, 200002, 200003 17 | // 18 | // so we can easily sanity-check the output. 19 | // 20 | for (i = 0, x = []; i < n; i++) { 21 | x[i] = [Math.floor(i * k / n) * 1e6 + i]; 22 | } 23 | 24 | for (run = 0; run < runs; run++) { 25 | var out = kmpp(x, { k: k, kmpp: true }); 26 | 27 | for (i = 0; i < k; i++) { 28 | t.assert(out.centroids[i][0] % clusterOffset < n, 'mean ' + i + ' % ' + clusterOffset + ' < ' + n + ' (got ' + (out.centroids[i] % clusterOffset) + ')'); 29 | } 30 | 31 | var sum = 0; 32 | for (i = 0; i < k; i++) { 33 | sum += out.counts[i]; 34 | } 35 | 36 | t.equal(sum, n, 'All points are assigned'); 37 | } 38 | 39 | t.end(); 40 | }); 41 | 42 | test('runs successfully without km++ initialization', function (t) { 43 | var i, k, n, x, run; 44 | var runs = 2; 45 | 46 | k = 3; 47 | n = 20; 48 | 49 | for (i = 0, x = []; i < n; i++) { 50 | x[i] = [Math.floor(i * k / n) * 1e6 + i]; 51 | } 52 | 53 | for (run = 0; run < runs; run++) { 54 | var out = kmpp(x, { k: k, kmpp: false }); 55 | 56 | var sum = 0; 57 | for (i = 0; i < k; i++) { 58 | sum += out.counts[i]; 59 | } 60 | 61 | t.equal(sum, n, 'All points are assigned'); 62 | } 63 | 64 | t.end(); 65 | }); 66 | 67 | test('continues iteration', function (t) { 68 | var i, x; 69 | 70 | var n = 30; 71 | var k = 3; 72 | 73 | for (i = 0, x = []; i < n; i++) { 74 | x[i] = [i]; 75 | } 76 | 77 | var out1 = kmpp(x, { k: k }); 78 | 79 | t.equal(out1.centroids.length, k, 'has the right number of centroids'); 80 | t.equal(out1.assignments.length, n, 'assignments is the right length'); 81 | t.ok(out1.iterations >= 1, 'iterated'); 82 | 83 | // So basically just deep clone 84 | var c1 = JSON.stringify(out1.centroids); 85 | var a1 = out1.assignments.slice(); 86 | 87 | var out2 = kmpp(x, { assignments: out1.assignments, centroids: out1.centroids, k: k }); 88 | 89 | t.deepEqual(out2.assignments, a1, 'assignments unchanged on subsequent runs'); 90 | t.equal(JSON.stringify(out2.centroids), c1, 'assignments unchanged on subsequent runs'); 91 | t.equal(out2.iterations, 1, 'Only one iteration on subsequent runs'); 92 | 93 | var out3 = kmpp(x, { assignments: out1.assignments, centroids: out1.centroids, k: k }); 94 | t.ok(out3.iterations >= 1, 'Restarts'); 95 | 96 | t.end(); 97 | }); 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kmpp 2 | 3 | [![Travis CI](https://travis-ci.org/cmtt/kmpp.svg)](https://travis-ci.org/cmtt/kmpp) 4 | 5 | When dealing with lots of data points, clustering algorithms may be used to group them. The k-means algorithm partitions _n_ data points into _k_ clusters and finds the centroids of these clusters incrementally. 6 | 7 | The algorithm assigns data points to the closest cluster, and the centroids of each cluster are re-calculated. These steps are repeated until the centroids do not changing anymore. 8 | 9 | The basic k-means algorithm is initialized with _k_ centroids at random positions. This implementation addresses some disadvantages of the arbitrary initialization method with the k-means++ algorithm (see "Further reading" at the end). 10 | 11 | ## Installation 12 | 13 | ## Installing via npm 14 | 15 | Install kmpp as Node.js module via NPM: 16 | ````bash 17 | $ npm install kmpp 18 | ```` 19 | 20 | ## Example 21 | 22 | ```javascript 23 | var kmpp = require('kmpp'); 24 | 25 | kmpp([ 26 | [x1, y1, ...], 27 | [x2, y2, ...], 28 | [x3, y3, ...], 29 | ... 30 | ], { 31 | k: 4 32 | }); 33 | 34 | // => 35 | // { converged: true, 36 | // centroids: [[xm1, ym1, ...], [xm2, ym2, ...], [xm3, ym3, ...]], 37 | // counts: [ 7, 6, 7 ], 38 | // assignments: [ 2, 2, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1 ] 39 | // } 40 | ``` 41 | 42 | ## API 43 | 44 | ### `kmpp(points[, opts)` 45 | 46 | Exectes the k-means++ algorithm on `points`. 47 | 48 | Arguments: 49 | - `points` (`Array`): An array-of-arrays containing the points in format `[[x1, y1, ...], [x2, y2, ...], [x3, y3, ...], ...]` 50 | - `opts`: object containing configuration parameters. Parameters are 51 | - `distance` (`function`): Optional function that takes two points and returns the distance between them. 52 | - `initialize` (`Boolean`): Perform initialization. If false, uses the initial state provided in `centroids` and `assignments`. Otherwise discards any initial state and performs initialization. 53 | - `k` (`Number`): number of centroids. If not provided, `sqrt(n / 2)` is used, where `n` is the number of points. 54 | - `kmpp` (`Boolean`, default: `true`): If true, uses k-means++ initialization. Otherwise uses naive random assignment. 55 | - `maxIterations` (`Number`, default: `100`): Maximum allowed number of iterations. 56 | - `norm` (`Number`, default: `2`): L-norm used for distance computation. `1` is Manhattan norm, `2` is Euclidean norm. Ignored if `distance` function is provided. 57 | - `centroids` (`Array`): An array of centroids. If `initialize` is false, used as initialization for the algorithm, otherwise overwritten in-place if of the correct size. 58 | - `assignments` (`Array`): An array of assignments. Used for initialization, otherwise overwritten. 59 | - `counts` (`Array`): An output array used to avoid extra allocation. Values are discarded and overwritten. 60 | 61 | Returns an object containing information about the centroids and point assignments. Values are: 62 | - `converged`: `true` if the algorithm converged successfully 63 | - `centroids`: a list of centroids 64 | - `counts`: the number of points assigned to each respective centroid 65 | - `assignments`: a list of integer assignments of each point to the respective centroid 66 | - `iterations`: number of iterations used 67 | 68 | # Credits 69 | 70 | * [Jared Harkins](https://github.com/hDeraj) improved the performance by 71 | reducing the amount of function calls, reverting to Manhattan distance 72 | for measurements and improved the random initialization by choosing from 73 | points 74 | 75 | * [Ricky Reusser](https://github.com/rreusser) refactored API 76 | 77 | # Further reading 78 | 79 | * [Wikipedia: k-means clustering](https://en.wikipedia.org/wiki/K-means_clustering) 80 | * [Wikipedia: Determining the number of clusters in a data set](https://en.wikipedia.org/wiki/Determining_the_number_of_clusters_in_a_data_set) 81 | * [k-means++: The advantages of careful seeding, Arthur Vassilvitskii](http://ilpubs.stanford.edu:8090/778/1/2006-13.pdf) 82 | * [k-means++: The advantages of careful seeding, Presentation by Arthur Vassilvitskii (Presentation)](http://theory.stanford.edu/~sergei/slides/BATS-Means.pdf) 83 | 84 | # License 85 | 86 | © 2017-2019. MIT License. 87 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | var regl = require('regl'); 2 | var failNicely = require('fail-nicely'); 3 | var panel = require('control-panel'); 4 | var hsl = require('float-hsl2rgb'); 5 | var css = require('insert-css'); 6 | var kmpp = require('../'); 7 | var h = require('h'); 8 | 9 | css(` 10 | .control-panel { 11 | z-index: 100; 12 | position: relative; 13 | } 14 | 15 | .progress { 16 | background: rgba(255, 255, 255, 0.8); 17 | position: absolute; 18 | bottom: 10px; 19 | left: 10px; 20 | z-index: 1; 21 | } 22 | `); 23 | 24 | var progress = h('pre.progress'); 25 | document.body.appendChild(progress); 26 | 27 | regl({ 28 | onDone: failNicely((regl) => { 29 | var pointBuf = regl.buffer(); 30 | var pointColorBuf = regl.buffer(); 31 | var centroidBuf = regl.buffer(); 32 | var centroidColorBuf = regl.buffer(); 33 | 34 | var x, iteration; 35 | var km = {}; 36 | var settings = { 37 | norm: 2, 38 | points: 5000, 39 | k: 0, 40 | kmpp: true, 41 | uniformity: 0.5, 42 | periodicity: 4 43 | }; 44 | 45 | function distribution (x, y) { 46 | var w = Math.PI * settings.periodicity; 47 | return Math.abs( 48 | Math.cos(x * w) * 49 | Math.sin((y - x / 2) * w) * 50 | Math.sin((y + x / 2) * w) 51 | ); 52 | } 53 | 54 | function restart () { 55 | iteration = 0; 56 | progress.textContent = ''; 57 | delete km.assignments; 58 | delete km.centroids; 59 | km.converged = false; 60 | } 61 | 62 | function initialize () { 63 | var ar = window.innerWidth / window.innerHeight; 64 | var i = 0; 65 | x = []; 66 | while (i < settings.points) { 67 | // Random points; we'll scale these to the viewport: 68 | var xp = 2.0 * (Math.random() - 0.5) * (ar > 1 ? ar : 1); 69 | var yp = 2.0 * (Math.random() - 0.5) * (ar > 1 ? 1 : 1 / ar); 70 | 71 | if (Math.pow(distribution(xp, yp), (1.0 - settings.uniformity) * 2.0) > Math.random()) { 72 | x[i++] = [xp, yp]; 73 | } 74 | } 75 | restart(); 76 | } 77 | 78 | var drawPoints = regl({ 79 | vert: ` 80 | precision mediump float; 81 | attribute vec2 xy; 82 | attribute vec3 color; 83 | uniform float size; 84 | uniform vec2 aspect; 85 | varying vec3 col; 86 | void main () { 87 | col = color; 88 | gl_Position = vec4(xy * aspect, 0, 1); 89 | gl_PointSize = size; 90 | } 91 | `, 92 | frag: ` 93 | precision mediump float; 94 | uniform float alpha; 95 | varying vec3 col; 96 | uniform float size; 97 | void main () { 98 | vec2 uv = gl_PointCoord - 0.5; 99 | float r = length(uv) * size * 2.0; 100 | 101 | gl_FragColor = vec4(col, alpha * smoothstep(size, size - 2.0, r)); 102 | } 103 | `, 104 | depth: { enable: false }, 105 | blend: { 106 | enable: true, 107 | func: { srcRGB: 'src alpha', srcAlpha: 1, dstRGB: 1, dstAlpha: 1 }, 108 | equation: { rgb: 'reverse subtract', alpha: 'add' } 109 | }, 110 | attributes: { 111 | xy: regl.prop('xy'), 112 | color: regl.prop('color') 113 | }, 114 | uniforms: { 115 | size: (ctx, props) => ctx.pixelRatio * props.size, 116 | alpha: regl.prop('alpha'), 117 | aspect: ctx => { 118 | var w = ctx.viewportWidth; 119 | var h = ctx.viewportHeight; 120 | return w / h > 1 ? [h / w, 1] : [1, w / h]; 121 | } 122 | }, 123 | primitive: 'points', 124 | count: (ctx, props) => props.xy._buffer.byteLength / 8 125 | }); 126 | 127 | panel([ 128 | { label: 'norm', type: 'range', min: 0.5, max: 4, step: 0.5, initial: settings.norm }, 129 | { label: 'k', type: 'range', min: 0, max: 100, step: 1, initial: settings.k }, 130 | { label: 'points', type: 'range', min: 1000, max: 20000, step: 100, initial: settings.points }, 131 | { label: 'uniformity', type: 'range', min: 0, max: 1, step: 0.1, initial: settings.uniformity }, 132 | { label: 'periodicity', type: 'range', min: 1, max: 10, step: 0.5, initial: settings.periodicity }, 133 | { label: 'kmpp', type: 'checkbox', initial: settings.kmpp }, 134 | { label: 'restart', type: 'button', action: restart } 135 | ], { position: 'top-left', width: 350 }).on('input', (data) => { 136 | var needsInitialize = false; 137 | var needsRestart = false; 138 | if ((data.points !== settings.points) || (data.uniformity !== settings.uniformity) || (data.periodicity !== settings.periodicity)) { 139 | needsInitialize = true; 140 | } else if ((data.k !== settings.k) || (data.kmpp !== settings.kmpp)) { 141 | needsRestart = true; 142 | } else if (data.norm !== settings.norm) { 143 | km.converged = false; 144 | } 145 | Object.assign(settings, data); 146 | if (needsRestart) restart(); 147 | if (needsInitialize) initialize(); 148 | }); 149 | 150 | initialize(); 151 | 152 | window.addEventListener('resize', initialize, false); 153 | 154 | iteration = 0; 155 | regl.frame(({ tick }) => { 156 | if (km.converged) return; 157 | 158 | iteration++; 159 | 160 | km = kmpp(x, Object.assign({ maxIterations: 1, 161 | norm: settings.norm, 162 | k: settings.k === 0 ? undefined : settings.k, 163 | kmpp: settings.kmpp 164 | }, km)); 165 | 166 | progress.textContent = km.converged ? ('converged after ' + iteration + ' iterations') : ('iteration: ' + iteration); 167 | 168 | var colorList = new Array(km.centroids.length).fill(0).map((d, i) => hsl([i / km.centroids.length, 0.5, 0.5])); 169 | 170 | pointColorBuf({ data: km.assignments.map(i => colorList[i]) }); 171 | centroidColorBuf({ data: colorList }); 172 | pointBuf({ data: x }); 173 | centroidBuf({ data: km.centroids }); 174 | 175 | regl.clear({ color: [1, 1, 1, 1] }); 176 | 177 | drawPoints({ 178 | xy: pointBuf, 179 | size: 5, 180 | color: pointColorBuf, 181 | alpha: 0.25 * Math.sqrt(5000 / settings.points * window.innerWidth * window.innerHeight / 600 / 600) 182 | }); 183 | 184 | drawPoints({ 185 | xy: centroidBuf, 186 | size: 15, 187 | color: centroidColorBuf, 188 | alpha: 1.0 189 | }); 190 | }); 191 | }) 192 | }); 193 | --------------------------------------------------------------------------------