Skip to content

Commit

Permalink
Converted turf-standard-deviational-ellipse to Typescript (#2649)
Browse files Browse the repository at this point in the history
* Converting turf-standard-deviational-ellipse to Typescript.
  • Loading branch information
smallsaucepan authored Jul 9, 2024
1 parent 5940110 commit fbfb882
Show file tree
Hide file tree
Showing 8 changed files with 135 additions and 113 deletions.
54 changes: 26 additions & 28 deletions packages/turf-standard-deviational-ellipse/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,70 +4,68 @@

## standardDeviationalEllipse

Takes a [FeatureCollection][1] and returns a standard deviational ellipse,
Takes a collection of features and returns a standard deviational ellipse,
also known as a “directional distribution.” The standard deviational ellipse
aims to show the direction and the distribution of a dataset by drawing
an ellipse that contains about one standard deviation’s worth (~ 70%) of the
data.

This module mirrors the functionality of [Directional Distribution][2]
in ArcGIS and the [QGIS Standard Deviational Ellipse Plugin][3]
This module mirrors the functionality of [Directional Distribution][1]
in ArcGIS and the [QGIS Standard Deviational Ellipse Plugin][2]

**Bibliography**

• Robert S. Yuill, “The Standard Deviational Ellipse; An Updated Tool for
Spatial Description,” *Geografiska Annaler* 53, no. 1 (1971): 28–39,
doi:{@link [https://doi.org/10.2307/490885|10.2307/490885}][4].
doi:{@link [https://doi.org/10.2307/490885|10.2307/490885}][3].

• Paul Hanly Furfey, “A Note on Lefever’s “Standard Deviational Ellipse,”
*American Journal of Sociology* 33, no. 1 (1927): 94—98,
doi:{@link [https://doi.org/10.1086/214336|10.1086/214336}][5].
doi:{@link [https://doi.org/10.1086/214336|10.1086/214336}][4].

### Parameters

* `points` **[FeatureCollection][6]<[Point][7]>** GeoJSON points
* `options` **[Object][8]** Optional parameters (optional, default `{}`)
* `points` **[FeatureCollection][5]<[Point][6]>** GeoJSON points
* `options` **[Object][7]** Optional parameters (optional, default `{}`)

* `options.weight` **[string][9]?** the property name used to weight the center
* `options.steps` **[number][10]** number of steps for the polygon (optional, default `64`)
* `options.properties` **[Object][8]** properties to pass to the resulting ellipse (optional, default `{}`)
* `options.weight` **[string][8]?** the property name used to weight the center
* `options.steps` **[number][9]** number of steps for the polygon (optional, default `64`)
* `options.properties` **[Object][7]** properties to pass to the resulting ellipse (optional, default `{}`)

### Examples

```javascript
var bbox = [-74, 40.72, -73.98, 40.74];
var points = turf.randomPoint(400, {bbox: bbox});
var sdEllipse = turf.standardDeviationalEllipse(points);
const bbox = [-74, 40.72, -73.98, 40.74];
const points = turf.randomPoint(400, {bbox: bbox});
const sdEllipse = turf.standardDeviationalEllipse(points);

//addToMap
var addToMap = [points, sdEllipse];
const addToMap = [points, sdEllipse];
```

Returns **[Feature][11]<[Polygon][12]>** an elliptical Polygon that includes approximately 1 SD of the dataset within it.
Returns **[Feature][10]<[Polygon][11]>** an elliptical Polygon that includes approximately 1 SD of the dataset within it.

[1]: https://tools.ietf.org/html/rfc7946#section-3.3
[1]: http://desktop.arcgis.com/en/arcmap/10.3/tools/spatial-statistics-toolbox/directional-distribution.htm

[2]: http://desktop.arcgis.com/en/arcmap/10.3/tools/spatial-statistics-toolbox/directional-distribution.htm
[2]: http://arken.nmbu.no/~havatv/gis/qgisplugins/SDEllipse/

[3]: http://arken.nmbu.no/~havatv/gis/qgisplugins/SDEllipse/
[3]: https://doi.org/10.2307/490885|10.2307/490885}

[4]: https://doi.org/10.2307/490885|10.2307/490885}
[4]: https://doi.org/10.1086/214336|10.1086/214336}

[5]: https://doi.org/10.1086/214336|10.1086/214336}
[5]: https://tools.ietf.org/html/rfc7946#section-3.3

[6]: https://tools.ietf.org/html/rfc7946#section-3.3
[6]: https://tools.ietf.org/html/rfc7946#section-3.1.2

[7]: https://tools.ietf.org/html/rfc7946#section-3.1.2
[7]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object

[8]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[8]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String

[9]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[9]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number

[10]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number
[10]: https://tools.ietf.org/html/rfc7946#section-3.2

[11]: https://tools.ietf.org/html/rfc7946#section-3.2

[12]: https://tools.ietf.org/html/rfc7946#section-3.1.6
[11]: https://tools.ietf.org/html/rfc7946#section-3.1.6

<!-- This file is automatically generated. Please don't edit it directly. If you find an error, edit the source file of the module in question (likely index.js or index.ts), and re-run "yarn docs" from the root of the turf project. -->

Expand Down
7 changes: 4 additions & 3 deletions packages/turf-standard-deviational-ellipse/bench.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { BBox } from "geojson";
import { randomPoint } from "@turf/random";
import { standardDeviationalEllipse } from "./index.js";
import Benchmark from "benchmark";
import Benchmark, { Event } from "benchmark";

/**
* Benchmark Results
Expand All @@ -10,7 +11,7 @@ import Benchmark from "benchmark";
* turf-standard-deviational-ellipse - 600 points x 574 ops/sec ±1.14% (92 runs sampled)
*/
const suite = new Benchmark.Suite("turf-standard-deviational-ellipse");
const properties = { bbox: [-10, -10, 10, 10] };
const properties: { bbox: BBox } = { bbox: [-10, -10, 10, 10] };
suite
.add("turf-standard-deviational-ellipse - 150 points", () =>
standardDeviationalEllipse(randomPoint(150, properties))
Expand All @@ -21,5 +22,5 @@ suite
.add("turf-standard-deviational-ellipse - 600 points", () =>
standardDeviationalEllipse(randomPoint(600, properties))
)
.on("cycle", (e) => console.log(String(e.target)))
.on("cycle", (e: Event) => console.log(String(e.target)))
.run();
40 changes: 0 additions & 40 deletions packages/turf-standard-deviational-ellipse/index.d.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,19 +1,43 @@
import {
FeatureCollection,
Feature,
Position,
Polygon,
GeoJsonProperties,
Point,
} from "geojson";
import { coordAll, featureEach } from "@turf/meta";
import { getCoords } from "@turf/invariant";
import { featureCollection, isObject, isNumber } from "@turf/helpers";
import { centerMean } from "@turf/center-mean";
import { pointsWithinPolygon } from "@turf/points-within-polygon";
import { ellipse } from "@turf/ellipse";

declare interface SDEProps {
meanCenterCoordinates: Position;
semiMajorAxis: number;
semiMinorAxis: number;
numberOfFeatures: number;
angle: number;
percentageWithinEllipse: number;
}

declare interface StandardDeviationalEllipse extends Feature<Polygon> {
properties: {
standardDeviationalEllipse: SDEProps;
[key: string]: any;
} | null;
}

/**
* Takes a {@link FeatureCollection} and returns a standard deviational ellipse,
* Takes a collection of features and returns a standard deviational ellipse,
* also known as a “directional distribution.” The standard deviational ellipse
* aims to show the direction and the distribution of a dataset by drawing
* an ellipse that contains about one standard deviation’s worth (~ 70%) of the
* data.
*
* This module mirrors the functionality of [Directional Distribution](http://desktop.arcgis.com/en/arcmap/10.3/tools/spatial-statistics-toolbox/directional-distribution.htm)
* in ArcGIS and the [QGIS Standard Deviational Ellipse Plugin](http://arken.nmbu.no/~havatv/gis/qgisplugins/SDEllipse/)
* This module mirrors the functionality of {@link http://desktop.arcgis.com/en/arcmap/10.3/tools/spatial-statistics-toolbox/directional-distribution.htm|Directional Distribution}
* in ArcGIS and the {@link http://arken.nmbu.no/~havatv/gis/qgisplugins/SDEllipse/|QGIS Standard Deviational Ellipse Plugin}
*
* **Bibliography**
*
Expand All @@ -35,29 +59,36 @@ import { ellipse } from "@turf/ellipse";
* @returns {Feature<Polygon>} an elliptical Polygon that includes approximately 1 SD of the dataset within it.
* @example
*
* var bbox = [-74, 40.72, -73.98, 40.74];
* var points = turf.randomPoint(400, {bbox: bbox});
* var sdEllipse = turf.standardDeviationalEllipse(points);
* const bbox = [-74, 40.72, -73.98, 40.74];
* const points = turf.randomPoint(400, {bbox: bbox});
* const sdEllipse = turf.standardDeviationalEllipse(points);
*
* //addToMap
* var addToMap = [points, sdEllipse];
* const addToMap = [points, sdEllipse];
*
*/
function standardDeviationalEllipse(points, options) {
function standardDeviationalEllipse(
points: FeatureCollection<Point>,
options?: {
properties?: GeoJsonProperties;
weight?: string;
steps?: number;
}
): StandardDeviationalEllipse {
// Optional params
options = options || {};
if (!isObject(options)) throw new Error("options is invalid");
var steps = options.steps || 64;
var weightTerm = options.weight;
var properties = options.properties || {};
const steps = options.steps || 64;
const weightTerm = options.weight;
const properties = options.properties || {};

// Validation:
if (!isNumber(steps)) throw new Error("steps must be a number");
if (!isObject(properties)) throw new Error("properties must be a number");

// Calculate mean center & number of features:
var numberOfFeatures = coordAll(points).length;
var meanCenter = centerMean(points, { weight: weightTerm });
const numberOfFeatures = coordAll(points).length;
const meanCenter = centerMean(points, { weight: weightTerm });

// Calculate angle of rotation:
// [X, Y] = mean center of all [x, y].
Expand All @@ -66,33 +97,35 @@ function standardDeviationalEllipse(points, options) {
// B = sqrt(A^2 + 4(sum((x - X)(y - Y))^2))
// C = 2(sum((x - X)(y - Y)))

var xDeviationSquaredSum = 0;
var yDeviationSquaredSum = 0;
var xyDeviationSum = 0;
let xDeviationSquaredSum = 0;
let yDeviationSquaredSum = 0;
let xyDeviationSum = 0;

featureEach(points, function (point) {
var weight = point.properties[weightTerm] || 1;
var deviation = getDeviations(getCoords(point), getCoords(meanCenter));
// weightTerm or point.properties might be undefined, hence this check.
const weight = weightTerm ? point.properties?.[weightTerm] || 1 : 1;
const deviation = getDeviations(getCoords(point), getCoords(meanCenter));
xDeviationSquaredSum += Math.pow(deviation.x, 2) * weight;
yDeviationSquaredSum += Math.pow(deviation.y, 2) * weight;
xyDeviationSum += deviation.x * deviation.y * weight;
});

var bigA = xDeviationSquaredSum - yDeviationSquaredSum;
var bigB = Math.sqrt(Math.pow(bigA, 2) + 4 * Math.pow(xyDeviationSum, 2));
var bigC = 2 * xyDeviationSum;
var theta = Math.atan((bigA + bigB) / bigC);
var thetaDeg = (theta * 180) / Math.PI;
const bigA = xDeviationSquaredSum - yDeviationSquaredSum;
const bigB = Math.sqrt(Math.pow(bigA, 2) + 4 * Math.pow(xyDeviationSum, 2));
const bigC = 2 * xyDeviationSum;
const theta = Math.atan((bigA + bigB) / bigC);
const thetaDeg = (theta * 180) / Math.PI;

// Calculate axes:
// sigmaX = sqrt((1 / n - 2) * sum((((x - X) * cos(theta)) - ((y - Y) * sin(theta)))^2))
// sigmaY = sqrt((1 / n - 2) * sum((((x - X) * sin(theta)) - ((y - Y) * cos(theta)))^2))
var sigmaXsum = 0;
var sigmaYsum = 0;
var weightsum = 0;
let sigmaXsum = 0;
let sigmaYsum = 0;
let weightsum = 0;
featureEach(points, function (point) {
var weight = point.properties[weightTerm] || 1;
var deviation = getDeviations(getCoords(point), getCoords(meanCenter));
// weightTerm or point.properties might be undefined, hence this check.
const weight = weightTerm ? point.properties?.[weightTerm] || 1 : 1;
const deviation = getDeviations(getCoords(point), getCoords(meanCenter));
sigmaXsum +=
Math.pow(
deviation.x * Math.cos(theta) - deviation.y * Math.sin(theta),
Expand All @@ -106,20 +139,20 @@ function standardDeviationalEllipse(points, options) {
weightsum += weight;
});

var sigmaX = Math.sqrt((2 * sigmaXsum) / weightsum);
var sigmaY = Math.sqrt((2 * sigmaYsum) / weightsum);
const sigmaX = Math.sqrt((2 * sigmaXsum) / weightsum);
const sigmaY = Math.sqrt((2 * sigmaYsum) / weightsum);

var theEllipse = ellipse(meanCenter, sigmaX, sigmaY, {
const theEllipse: Feature<Polygon> = ellipse(meanCenter, sigmaX, sigmaY, {
units: "degrees",
angle: thetaDeg,
steps: steps,
properties: properties,
});
var pointsWithinEllipse = pointsWithinPolygon(
const pointsWithinEllipse = pointsWithinPolygon(
points,
featureCollection([theEllipse])
);
var standardDeviationalEllipseProperties = {
const standardDeviationalEllipseProperties = {
meanCenterCoordinates: getCoords(meanCenter),
semiMajorAxis: sigmaX,
semiMinorAxis: sigmaY,
Expand All @@ -128,26 +161,30 @@ function standardDeviationalEllipse(points, options) {
percentageWithinEllipse:
(100 * coordAll(pointsWithinEllipse).length) / numberOfFeatures,
};
// Make sure properties object exists.
theEllipse.properties = theEllipse.properties ?? {};
theEllipse.properties.standardDeviationalEllipse =
standardDeviationalEllipseProperties;

return theEllipse;
// We have added the StandardDeviationalEllipse specific properties, so
// confirm this to Typescript with a cast.
return theEllipse as StandardDeviationalEllipse;
}

/**
* Get x_i - X and y_i - Y
*
* @private
* @param {Array} coordinates Array of [x_i, y_i]
* @param {Array} center Array of [X, Y]
* @param {Position} coordinates Array of [x_i, y_i]
* @param {Position} center Array of [X, Y]
* @returns {Object} { x: n, y: m }
*/
function getDeviations(coordinates, center) {
function getDeviations(coordinates: Position, center: Position) {
return {
x: coordinates[0] - center[0],
y: coordinates[1] - center[1],
};
}

export { standardDeviationalEllipse };
export { standardDeviationalEllipse, SDEProps, StandardDeviationalEllipse };
export default standardDeviationalEllipse;
5 changes: 4 additions & 1 deletion packages/turf-standard-deviational-ellipse/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"tape": "^5.7.2",
"tsup": "^8.0.1",
"tsx": "^4.6.2",
"typescript": "^5.2.2",
"write-json-file": "^5.0.0"
},
"dependencies": {
Expand All @@ -73,6 +74,8 @@
"@turf/helpers": "workspace:^",
"@turf/invariant": "workspace:^",
"@turf/meta": "workspace:^",
"@turf/points-within-polygon": "workspace:^"
"@turf/points-within-polygon": "workspace:^",
"@types/geojson": "7946.0.8",
"tslib": "^2.6.2"
}
}
Loading

0 comments on commit fbfb882

Please sign in to comment.