@d3plus/math 3.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +216 -0
- package/es/index.js +15 -0
- package/es/src/ckmeans.js +204 -0
- package/es/src/closest.js +19 -0
- package/es/src/largestRect.js +324 -0
- package/es/src/lineIntersection.js +22 -0
- package/es/src/path2polygon.js +23 -0
- package/es/src/pointDistance.js +10 -0
- package/es/src/pointDistanceSquared.js +10 -0
- package/es/src/pointRotate.js +18 -0
- package/es/src/polygonInside.js +26 -0
- package/es/src/polygonRayCast.js +101 -0
- package/es/src/polygonRotate.js +17 -0
- package/es/src/segmentBoxContains.js +57 -0
- package/es/src/segmentsIntersect.js +15 -0
- package/es/src/shapeEdgePoint.js +45 -0
- package/es/src/simplify.js +96 -0
- package/package.json +40 -0
- package/umd/d3plus-math.full.js +1024 -0
- package/umd/d3plus-math.full.js.map +1 -0
- package/umd/d3plus-math.full.min.js +289 -0
- package/umd/d3plus-math.js +939 -0
- package/umd/d3plus-math.js.map +1 -0
- package/umd/d3plus-math.min.js +289 -0
package/README.md
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# @d3plus/math
|
|
2
|
+
|
|
3
|
+
Mathematical functions to aid in calculating visualizations.
|
|
4
|
+
|
|
5
|
+
## Installing
|
|
6
|
+
|
|
7
|
+
If using npm, `npm install @d3plus/math`. Otherwise, you can download the [latest release from GitHub](https://github.com/d3plus/d3plus/releases/latest) or load from a [CDN](https://cdn.jsdelivr.net/npm/@d3plus/math@3.0.0-alpha.0/+esm).
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
import modules from "@d3plus/math";
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
In vanilla JavaScript, a `d3plus` global is exported from the pre-bundled version:
|
|
14
|
+
|
|
15
|
+
```html
|
|
16
|
+
<script src="https://cdn.jsdelivr.net/npm/@d3plus/math@3.0.0-alpha.0"></script>
|
|
17
|
+
<script>
|
|
18
|
+
console.log(d3plus);
|
|
19
|
+
</script>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Examples
|
|
23
|
+
|
|
24
|
+
Live examples can be found on [d3plus.org](https://d3plus.org/), which includes a collection of example visualizations using @d3plus/react.
|
|
25
|
+
|
|
26
|
+
## API Reference
|
|
27
|
+
|
|
28
|
+
#####
|
|
29
|
+
* [closest](#closest) - Finds the closest numeric value in an array.
|
|
30
|
+
* [largestRect](#largestRect) - An angle of zero means that the longer side of the polygon (the width) will be aligned with the x axis. An angle of 90 and/or -90 means that the longer side of the polygon (the width) will be aligned with the y axis. The value can be a number between -90 and 90 specifying the angle of rotation of the polygon, a string which is parsed to a number, or an array of numbers specifying the possible rotations of the polygon.
|
|
31
|
+
* [lineIntersection](#lineIntersection) - Finds the intersection point (if there is one) of the lines p1q1 and p2q2.
|
|
32
|
+
* [path2polygon](#path2polygon) - Transforms a path string into an Array of points.
|
|
33
|
+
* [pointDistance](#pointDistance) - Calculates the pixel distance between two points.
|
|
34
|
+
* [pointDistanceSquared](#pointDistanceSquared) - Returns the squared euclidean distance between two points.
|
|
35
|
+
* [pointRotate](#pointRotate) - Rotates a point around a given origin.
|
|
36
|
+
* [polygonInside](#polygonInside) - Checks if one polygon is inside another polygon.
|
|
37
|
+
* [polygonRayCast](#polygonRayCast) - Gives the two closest intersection points between a ray cast from a point inside a polygon. The two points should lie on opposite sides of the origin.
|
|
38
|
+
* [polygonRotate](#polygonRotate) - Rotates a point around a given origin.
|
|
39
|
+
* [segmentBoxContains](#segmentBoxContains) - Checks whether a point is inside the bounding box of a line segment.
|
|
40
|
+
* [segmentsIntersect](#segmentsIntersect) - Checks whether the line segments p1q1 && p2q2 intersect.
|
|
41
|
+
* [shapeEdgePoint](#shapeEdgePoint) - Calculates the x/y position of a point at the edge of a shape, from the center of the shape, given a specified pixel distance and radian angle.
|
|
42
|
+
* [largestRect](#largestRect) - Simplifies the points of a polygon using both the Ramer-Douglas-Peucker algorithm and basic distance-based simplification. Adapted to an ES6 module from the excellent [Simplify.js](http://mourner.github.io/simplify-js/).
|
|
43
|
+
|
|
44
|
+
#####
|
|
45
|
+
* [LargestRect](#LargestRect) - The returned Object of the largestRect function.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
<a name="closest"></a>
|
|
50
|
+
#### d3plus.**closest**(n, arr) [<>](https://github.com/d3plus/d3plus/blob/main/packages/math/src/closest.js#L1)
|
|
51
|
+
|
|
52
|
+
Finds the closest numeric value in an array.
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
This is a global function
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
<a name="largestRect"></a>
|
|
60
|
+
#### d3plus.**largestRect**(poly, [options]) [<>](https://github.com/d3plus/d3plus/blob/main/packages/math/src/largestRect.js#L28)
|
|
61
|
+
|
|
62
|
+
An angle of zero means that the longer side of the polygon (the width) will be aligned with the x axis. An angle of 90 and/or -90 means that the longer side of the polygon (the width) will be aligned with the y axis. The value can be a number between -90 and 90 specifying the angle of rotation of the polygon, a string which is parsed to a number, or an array of numbers specifying the possible rotations of the polygon.
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
This is a global function
|
|
66
|
+
**Author**: Daniel Smilkov [dsmilkov@gmail.com]
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
<a name="lineIntersection"></a>
|
|
71
|
+
#### d3plus.**lineIntersection**(p1, q1, p2, q2) [<>](https://github.com/d3plus/d3plus/blob/main/packages/math/src/lineIntersection.js#L1)
|
|
72
|
+
|
|
73
|
+
Finds the intersection point (if there is one) of the lines p1q1 and p2q2.
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
This is a global function
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
<a name="path2polygon"></a>
|
|
81
|
+
#### d3plus.**path2polygon**(path, [segmentLength]) [<>](https://github.com/d3plus/d3plus/blob/main/packages/math/src/path2polygon.js#L1)
|
|
82
|
+
|
|
83
|
+
Transforms a path string into an Array of points.
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
This is a global function
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
<a name="pointDistance"></a>
|
|
91
|
+
#### d3plus.**pointDistance**(p1, p2) [<>](https://github.com/d3plus/d3plus/blob/main/packages/math/src/pointDistance.js#L3)
|
|
92
|
+
|
|
93
|
+
Calculates the pixel distance between two points.
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
This is a global function
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
<a name="pointDistanceSquared"></a>
|
|
101
|
+
#### d3plus.**pointDistanceSquared**(p1, p2) [<>](https://github.com/d3plus/d3plus/blob/main/packages/math/src/pointDistanceSquared.js#L1)
|
|
102
|
+
|
|
103
|
+
Returns the squared euclidean distance between two points.
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
This is a global function
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
<a name="pointRotate"></a>
|
|
111
|
+
#### d3plus.**pointRotate**(p, alpha, [origin]) [<>](https://github.com/d3plus/d3plus/blob/main/packages/math/src/pointRotate.js#L1)
|
|
112
|
+
|
|
113
|
+
Rotates a point around a given origin.
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
This is a global function
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
<a name="polygonInside"></a>
|
|
121
|
+
#### d3plus.**polygonInside**(polyA, polyB) [<>](https://github.com/d3plus/d3plus/blob/main/packages/math/src/polygonInside.js#L5)
|
|
122
|
+
|
|
123
|
+
Checks if one polygon is inside another polygon.
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
This is a global function
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
<a name="polygonRayCast"></a>
|
|
131
|
+
#### d3plus.**polygonRayCast**(poly, origin, [alpha]) [<>](https://github.com/d3plus/d3plus/blob/main/packages/math/src/polygonRayCast.js#L5)
|
|
132
|
+
|
|
133
|
+
Gives the two closest intersection points between a ray cast from a point inside a polygon. The two points should lie on opposite sides of the origin.
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
This is a global function
|
|
137
|
+
**Returns**: <code>Array</code> - An array containing two values, the closest point on the left and the closest point on the right. If either point cannot be found, that value will be `null`.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
<a name="polygonRotate"></a>
|
|
142
|
+
#### d3plus.**polygonRotate**(poly, alpha, [origin]) [<>](https://github.com/d3plus/d3plus/blob/main/packages/math/src/polygonRotate.js#L3)
|
|
143
|
+
|
|
144
|
+
Rotates a point around a given origin.
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
This is a global function
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
<a name="segmentBoxContains"></a>
|
|
152
|
+
#### d3plus.**segmentBoxContains**(s1, s2, p) [<>](https://github.com/d3plus/d3plus/blob/main/packages/math/src/segmentBoxContains.js#L1)
|
|
153
|
+
|
|
154
|
+
Checks whether a point is inside the bounding box of a line segment.
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
This is a global function
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
<a name="segmentsIntersect"></a>
|
|
162
|
+
#### d3plus.**segmentsIntersect**(p1, q1, p2, q2) [<>](https://github.com/d3plus/d3plus/blob/main/packages/math/src/segmentsIntersect.js#L4)
|
|
163
|
+
|
|
164
|
+
Checks whether the line segments p1q1 && p2q2 intersect.
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
This is a global function
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
<a name="shapeEdgePoint"></a>
|
|
172
|
+
#### d3plus.**shapeEdgePoint**(angle, distance) [<>](https://github.com/d3plus/d3plus/blob/main/packages/math/src/shapeEdgePoint.js#L3)
|
|
173
|
+
|
|
174
|
+
Calculates the x/y position of a point at the edge of a shape, from the center of the shape, given a specified pixel distance and radian angle.
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
This is a global function
|
|
178
|
+
**Returns**: <code>String</code> - [shape = "circle"] The type of shape, which can be either "circle" or "square".
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
<a name="largestRect"></a>
|
|
183
|
+
#### d3plus.**largestRect**(poly, [tolerance], [highestQuality]) [<>](https://github.com/d3plus/d3plus/blob/main/packages/math/src/simplify.js#L112)
|
|
184
|
+
|
|
185
|
+
Simplifies the points of a polygon using both the Ramer-Douglas-Peucker algorithm and basic distance-based simplification. Adapted to an ES6 module from the excellent [Simplify.js](http://mourner.github.io/simplify-js/).
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
This is a global function
|
|
189
|
+
**Author**: Vladimir Agafonkin
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
<a name="LargestRect"></a>
|
|
194
|
+
#### **LargestRect** [<>](https://github.com/d3plus/d3plus/blob/main/packages/math/src/largestRect.js#L16)
|
|
195
|
+
|
|
196
|
+
The returned Object of the largestRect function.
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
This is a global typedef
|
|
200
|
+
**Properties**
|
|
201
|
+
|
|
202
|
+
| Name | Type | Description |
|
|
203
|
+
| --- | --- | --- |
|
|
204
|
+
| width | <code>Number</code> | The width of the rectangle |
|
|
205
|
+
| height | <code>Number</code> | The height of the rectangle |
|
|
206
|
+
| cx | <code>Number</code> | The x coordinate of the rectangle's center |
|
|
207
|
+
| cy | <code>Number</code> | The y coordinate of the rectangle's center |
|
|
208
|
+
| angle | <code>Number</code> | The rotation angle of the rectangle in degrees. The anchor of rotation is the center point. |
|
|
209
|
+
| area | <code>Number</code> | The area of the largest rectangle. |
|
|
210
|
+
| points | <code>Array</code> | An array of x/y coordinates for each point in the rectangle, useful for rendering paths. |
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
###### <sub>Documentation generated on Thu, 13 Mar 2025 19:58:29 GMT</sub>
|
package/es/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { default as ckmeans } from "./src/ckmeans.js";
|
|
2
|
+
export { default as closest } from "./src/closest.js";
|
|
3
|
+
export { default as largestRect } from "./src/largestRect.js";
|
|
4
|
+
export { default as lineIntersection } from "./src/lineIntersection.js";
|
|
5
|
+
export { default as path2polygon } from "./src/path2polygon.js";
|
|
6
|
+
export { default as pointDistance } from "./src/pointDistance.js";
|
|
7
|
+
export { default as pointDistanceSquared } from "./src/pointDistanceSquared.js";
|
|
8
|
+
export { default as pointRotate } from "./src/pointRotate.js";
|
|
9
|
+
export { default as polygonInside } from "./src/polygonInside.js";
|
|
10
|
+
export { default as polygonRayCast } from "./src/polygonRayCast.js";
|
|
11
|
+
export { default as polygonRotate } from "./src/polygonRotate.js";
|
|
12
|
+
export { default as segmentBoxContains } from "./src/segmentBoxContains.js";
|
|
13
|
+
export { default as segmentsIntersect } from "./src/segmentsIntersect.js";
|
|
14
|
+
export { default as shapeEdgePoint } from "./src/shapeEdgePoint.js";
|
|
15
|
+
export { default as simplify } from "./src/simplify.js";
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
@desc Sort an array of numbers by their numeric value, ensuring that the array is not changed in place.
|
|
3
|
+
|
|
4
|
+
This is necessary because the default behavior of .sort in JavaScript is to sort arrays as string values
|
|
5
|
+
|
|
6
|
+
[1, 10, 12, 102, 20].sort()
|
|
7
|
+
// output
|
|
8
|
+
[1, 10, 102, 12, 20]
|
|
9
|
+
|
|
10
|
+
@param {Array<number>} array input array
|
|
11
|
+
@return {Array<number>} sorted array
|
|
12
|
+
@private
|
|
13
|
+
@example
|
|
14
|
+
numericSort([3, 2, 1]) // => [1, 2, 3]
|
|
15
|
+
*/ function numericSort(array) {
|
|
16
|
+
return array.slice().sort(function(a, b) {
|
|
17
|
+
return a - b;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
For a sorted input, counting the number of unique values is possible in constant time and constant memory. This is a simple implementation of the algorithm.
|
|
22
|
+
|
|
23
|
+
Values are compared with `===`, so objects and non-primitive objects are not handled in any special way.
|
|
24
|
+
@private
|
|
25
|
+
@param {Array} input an array of primitive values.
|
|
26
|
+
@returns {number} count of unique values
|
|
27
|
+
@example
|
|
28
|
+
uniqueCountSorted([1, 2, 3]); // => 3
|
|
29
|
+
uniqueCountSorted([1, 1, 1]); // => 1
|
|
30
|
+
*/ function uniqueCountSorted(input) {
|
|
31
|
+
var lastSeenValue, uniqueValueCount = 0;
|
|
32
|
+
for(var i = 0; i < input.length; i++){
|
|
33
|
+
if (i === 0 || input[i] !== lastSeenValue) {
|
|
34
|
+
lastSeenValue = input[i];
|
|
35
|
+
uniqueValueCount++;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return uniqueValueCount;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
Create a new column x row matrix.
|
|
42
|
+
@private
|
|
43
|
+
@param {number} columns
|
|
44
|
+
@param {number} rows
|
|
45
|
+
@return {Array<Array<number>>} matrix
|
|
46
|
+
@example
|
|
47
|
+
makeMatrix(10, 10);
|
|
48
|
+
*/ function makeMatrix(columns, rows) {
|
|
49
|
+
var matrix = [];
|
|
50
|
+
for(var i = 0; i < columns; i++){
|
|
51
|
+
var column = [];
|
|
52
|
+
for(var j = 0; j < rows; j++)column.push(0);
|
|
53
|
+
matrix.push(column);
|
|
54
|
+
}
|
|
55
|
+
return matrix;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
Generates incrementally computed values based on the sums and sums of squares for the data array
|
|
59
|
+
@private
|
|
60
|
+
@param {number} j
|
|
61
|
+
@param {number} i
|
|
62
|
+
@param {Array<number>} sums
|
|
63
|
+
@param {Array<number>} sumsOfSquares
|
|
64
|
+
@return {number}
|
|
65
|
+
@example
|
|
66
|
+
ssq(0, 1, [-1, 0, 2], [1, 1, 5]);
|
|
67
|
+
*/ function ssq(j, i, sums, sumsOfSquares) {
|
|
68
|
+
var sji; // s(j, i)
|
|
69
|
+
if (j > 0) {
|
|
70
|
+
var muji = (sums[i] - sums[j - 1]) / (i - j + 1); // mu(j, i)
|
|
71
|
+
sji = sumsOfSquares[i] - sumsOfSquares[j - 1] - (i - j + 1) * muji * muji;
|
|
72
|
+
} else sji = sumsOfSquares[i] - sums[i] * sums[i] / (i + 1);
|
|
73
|
+
if (sji < 0) return 0;
|
|
74
|
+
return sji;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
Function that recursively divides and conquers computations for cluster j
|
|
78
|
+
@private
|
|
79
|
+
@param {number} iMin Minimum index in cluster to be computed
|
|
80
|
+
@param {number} iMax Maximum index in cluster to be computed
|
|
81
|
+
@param {number} cluster Index of the cluster currently being computed
|
|
82
|
+
@param {Array<Array<number>>} matrix
|
|
83
|
+
@param {Array<Array<number>>} backtrackMatrix
|
|
84
|
+
@param {Array<number>} sums
|
|
85
|
+
@param {Array<number>} sumsOfSquares
|
|
86
|
+
*/ function fillMatrixColumn(iMin, iMax, cluster, matrix, backtrackMatrix, sums, sumsOfSquares) {
|
|
87
|
+
if (iMin > iMax) return;
|
|
88
|
+
// Start at midpoint between iMin and iMax
|
|
89
|
+
var i = Math.floor((iMin + iMax) / 2);
|
|
90
|
+
matrix[cluster][i] = matrix[cluster - 1][i - 1];
|
|
91
|
+
backtrackMatrix[cluster][i] = i;
|
|
92
|
+
var jlow = cluster; // the lower end for j
|
|
93
|
+
if (iMin > cluster) jlow = Math.max(jlow, backtrackMatrix[cluster][iMin - 1] || 0);
|
|
94
|
+
jlow = Math.max(jlow, backtrackMatrix[cluster - 1][i] || 0);
|
|
95
|
+
var jhigh = i - 1; // the upper end for j
|
|
96
|
+
if (iMax < matrix.length - 1) jhigh = Math.min(jhigh, backtrackMatrix[cluster][iMax + 1] || 0);
|
|
97
|
+
for(var j = jhigh; j >= jlow; --j){
|
|
98
|
+
var sji = ssq(j, i, sums, sumsOfSquares);
|
|
99
|
+
if (sji + matrix[cluster - 1][jlow - 1] >= matrix[cluster][i]) break;
|
|
100
|
+
// Examine the lower bound of the cluster border
|
|
101
|
+
var sjlowi = ssq(jlow, i, sums, sumsOfSquares);
|
|
102
|
+
var ssqjlow = sjlowi + matrix[cluster - 1][jlow - 1];
|
|
103
|
+
if (ssqjlow < matrix[cluster][i]) {
|
|
104
|
+
// Shrink the lower bound
|
|
105
|
+
matrix[cluster][i] = ssqjlow;
|
|
106
|
+
backtrackMatrix[cluster][i] = jlow;
|
|
107
|
+
}
|
|
108
|
+
jlow++;
|
|
109
|
+
var ssqj = sji + matrix[cluster - 1][j - 1];
|
|
110
|
+
if (ssqj < matrix[cluster][i]) {
|
|
111
|
+
matrix[cluster][i] = ssqj;
|
|
112
|
+
backtrackMatrix[cluster][i] = j;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
fillMatrixColumn(iMin, i - 1, cluster, matrix, backtrackMatrix, sums, sumsOfSquares);
|
|
116
|
+
fillMatrixColumn(i + 1, iMax, cluster, matrix, backtrackMatrix, sums, sumsOfSquares);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
Initializes the main matrices used in Ckmeans and kicks off the divide and conquer cluster computation strategy
|
|
120
|
+
@private
|
|
121
|
+
@param {Array<number>} data sorted array of values
|
|
122
|
+
@param {Array<Array<number>>} matrix
|
|
123
|
+
@param {Array<Array<number>>} backtrackMatrix
|
|
124
|
+
*/ function fillMatrices(data, matrix, backtrackMatrix) {
|
|
125
|
+
var nValues = matrix[0] ? matrix[0].length : 0;
|
|
126
|
+
// Shift values by the median to improve numeric stability
|
|
127
|
+
var shift = data[Math.floor(nValues / 2)];
|
|
128
|
+
// Cumulative sum and cumulative sum of squares for all values in data array
|
|
129
|
+
var sums = [];
|
|
130
|
+
var sumsOfSquares = [];
|
|
131
|
+
// Initialize first column in matrix & backtrackMatrix
|
|
132
|
+
for(var i = 0, shiftedValue = void 0; i < nValues; ++i){
|
|
133
|
+
shiftedValue = data[i] - shift;
|
|
134
|
+
if (i === 0) {
|
|
135
|
+
sums.push(shiftedValue);
|
|
136
|
+
sumsOfSquares.push(shiftedValue * shiftedValue);
|
|
137
|
+
} else {
|
|
138
|
+
sums.push(sums[i - 1] + shiftedValue);
|
|
139
|
+
sumsOfSquares.push(sumsOfSquares[i - 1] + shiftedValue * shiftedValue);
|
|
140
|
+
}
|
|
141
|
+
// Initialize for cluster = 0
|
|
142
|
+
matrix[0][i] = ssq(0, i, sums, sumsOfSquares);
|
|
143
|
+
backtrackMatrix[0][i] = 0;
|
|
144
|
+
}
|
|
145
|
+
// Initialize the rest of the columns
|
|
146
|
+
for(var cluster = 1; cluster < matrix.length; ++cluster){
|
|
147
|
+
var iMin = nValues - 1;
|
|
148
|
+
if (cluster < matrix.length - 1) iMin = cluster;
|
|
149
|
+
fillMatrixColumn(iMin, nValues - 1, cluster, matrix, backtrackMatrix, sums, sumsOfSquares);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
@module ckmeans
|
|
154
|
+
@desc Ported to ES6 from the excellent [simple-statistics](https://github.com/simple-statistics/simple-statistics) packages.
|
|
155
|
+
|
|
156
|
+
Ckmeans clustering is an improvement on heuristic-based clustering approaches like Jenks. The algorithm was developed in [Haizhou Wang and Mingzhou Song](http://journal.r-project.org/archive/2011-2/RJournal_2011-2_Wang+Song.pdf) as a [dynamic programming](https://en.wikipedia.org/wiki/Dynamic_programming) approach to the problem of clustering numeric data into groups with the least within-group sum-of-squared-deviations.
|
|
157
|
+
|
|
158
|
+
Minimizing the difference within groups - what Wang & Song refer to as `withinss`, or within sum-of-squares, means that groups are optimally homogenous within and the data is split into representative groups. This is very useful for visualization, where you may want to represent a continuous variable in discrete color or style groups. This function can provide groups that emphasize differences between data.
|
|
159
|
+
|
|
160
|
+
Being a dynamic approach, this algorithm is based on two matrices that store incrementally-computed values for squared deviations and backtracking indexes.
|
|
161
|
+
|
|
162
|
+
This implementation is based on Ckmeans 3.4.6, which introduced a new divide and conquer approach that improved runtime from O(kn^2) to O(kn log(n)).
|
|
163
|
+
|
|
164
|
+
Unlike the [original implementation](https://cran.r-project.org/web/packages/Ckmeans.1d.dp/index.html), this implementation does not include any code to automatically determine the optimal number of clusters: this information needs to be explicitly provided.
|
|
165
|
+
|
|
166
|
+
### References
|
|
167
|
+
_Ckmeans.1d.dp: Optimal k-means Clustering in One Dimension by Dynamic
|
|
168
|
+
Programming_ Haizhou Wang and Mingzhou Song ISSN 2073-4859 from The R Journal Vol. 3/2, December 2011
|
|
169
|
+
@param {Array<number>} data input data, as an array of number values
|
|
170
|
+
@param {number} nClusters number of desired classes. This cannot be greater than the number of values in the data array.
|
|
171
|
+
@returns {Array<Array<number>>} clustered input
|
|
172
|
+
@private
|
|
173
|
+
@example
|
|
174
|
+
ckmeans([-1, 2, -1, 2, 4, 5, 6, -1, 2, -1], 3);
|
|
175
|
+
// The input, clustered into groups of similar numbers.
|
|
176
|
+
//= [[-1, -1, -1, -1], [2, 2, 2], [4, 5, 6]]);
|
|
177
|
+
*/ export default function(data, nClusters) {
|
|
178
|
+
if (nClusters > data.length) {
|
|
179
|
+
throw new Error("Cannot generate more classes than there are data values");
|
|
180
|
+
}
|
|
181
|
+
var sorted = numericSort(data);
|
|
182
|
+
// we'll use this as the maximum number of clusters
|
|
183
|
+
var uniqueCount = uniqueCountSorted(sorted);
|
|
184
|
+
// if all of the input values are identical, there's one cluster with all of the input in it.
|
|
185
|
+
if (uniqueCount === 1) {
|
|
186
|
+
return [
|
|
187
|
+
sorted
|
|
188
|
+
];
|
|
189
|
+
}
|
|
190
|
+
var backtrackMatrix = makeMatrix(nClusters, sorted.length), matrix = makeMatrix(nClusters, sorted.length);
|
|
191
|
+
// This is a dynamic programming way to solve the problem of minimizing within-cluster sum of squares. It's similar to linear regression in this way, and this calculation incrementally computes the sum of squares that are later read.
|
|
192
|
+
fillMatrices(sorted, matrix, backtrackMatrix);
|
|
193
|
+
// The real work of Ckmeans clustering happens in the matrix generation: the generated matrices encode all possible clustering combinations, and once they're generated we can solve for the best clustering groups very quickly.
|
|
194
|
+
var clusterRight = backtrackMatrix[0] ? backtrackMatrix[0].length - 1 : 0;
|
|
195
|
+
var clusters = [];
|
|
196
|
+
// Backtrack the clusters from the dynamic programming matrix. This starts at the bottom-right corner of the matrix (if the top-left is 0, 0), and moves the cluster target with the loop.
|
|
197
|
+
for(var cluster = backtrackMatrix.length - 1; cluster >= 0; cluster--){
|
|
198
|
+
var clusterLeft = backtrackMatrix[cluster][clusterRight];
|
|
199
|
+
// fill the cluster from the sorted input by taking a slice of the array. the backtrack matrix makes this easy - it stores the indexes where the cluster should start and end.
|
|
200
|
+
clusters[cluster] = sorted.slice(clusterLeft, clusterRight + 1);
|
|
201
|
+
if (cluster > 0) clusterRight = clusterLeft - 1;
|
|
202
|
+
}
|
|
203
|
+
return clusters;
|
|
204
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
@function closest
|
|
3
|
+
@desc Finds the closest numeric value in an array.
|
|
4
|
+
@param {Number} n The number value to use when searching the array.
|
|
5
|
+
@param {Array} arr The array of values to test against.
|
|
6
|
+
*/ function _instanceof(left, right) {
|
|
7
|
+
if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) {
|
|
8
|
+
return !!right[Symbol.hasInstance](left);
|
|
9
|
+
} else {
|
|
10
|
+
return left instanceof right;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export default function(n) {
|
|
14
|
+
var arr = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : [];
|
|
15
|
+
if (!arr || !_instanceof(arr, Array) || !arr.length) return undefined;
|
|
16
|
+
return arr.reduce(function(prev, curr) {
|
|
17
|
+
return Math.abs(curr - n) < Math.abs(prev - n) ? curr : prev;
|
|
18
|
+
});
|
|
19
|
+
}
|