Skip to content

Commit

Permalink
Simplifying API to always expect an array of thresholds and (for isob…
Browse files Browse the repository at this point in the history
…ands) bandwidths. Updating test cases and README to match.
  • Loading branch information
smallsaucepan committed Nov 5, 2024
1 parent b0e9320 commit 9ef9cd1
Show file tree
Hide file tree
Showing 19 changed files with 393 additions and 386 deletions.
94 changes: 39 additions & 55 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ The implementation computes _iso lines_ (_iso contours_) or _iso bands_ for rect
## Table of contents

1. [Availability](#availability)
2. [Installation](#installation)
3. [Usage](#usage)
4. [API Description](#api-description)
5. [Examples](#examples)
6. [License](#license)
1. [Installation](#installation)
1. [Usage](#usage)
1. [API](#api)
1. [Examples](#examples)
1. [License](#license)

## Availability

Expand Down Expand Up @@ -65,7 +65,7 @@ const thresholds = [1, 2];
const lines = isoLines(data, thresholds);
```

This should yield you the data of two lines, which if displayed graphically would look something like this:
This will yield the data of two lines, which if displayed graphically would look something like this:

Next - iso bands.

Expand All @@ -85,7 +85,7 @@ const lines = isoBands(data, lowerBounds, bandWidths);

```

This should yield you the data of two bands, which might look like this if displayed visually:
This will yield the data of two bands, which might look like this if displayed visually:

### Optimisations

Expand Down Expand Up @@ -119,26 +119,20 @@ const lines2 = isoLines(tree, thresholds2); // faster :)
### Iso Lines

```javascript
function isoLines(data, threshold, options)
function isoLines(data, thresholds, options)
```

Compute _iso lines_ and _iso contours_ for a 2-dimensional scalar field and a (list of) thresholds.
Compute _iso lines_ for a 2-dimensional scalar field and a list of thresholds.

| Parameter | Description |

| ----------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |

| `data` | 2-dimensional input data, i.e. the scalar field (must be array of arrays, or pre-processed data object obtained from `new QuadTree()`). This parameter is **mandatory**. |

| `threshold` | A constant numerical value (or array of numerical values) defining the curve function for the _iso line(s)_. This parameter is **mandatory** |

| `options` | An object with attributes allowing for changes in the behavior of this function (See below). This parameter is **optional** |
| Parameter | Description |
| ------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `data` | 2-dimensional input data, i.e. the scalar field (must be array of arrays, or pre-processed data object obtained from `new QuadTree()`). This parameter is **mandatory**. |
| `thresholds` | An array of numerical values defining the curve function for the _iso line(s)_. This parameter is **mandatory** |
| `options` | An object with attributes allowing for changes in the behavior of this function (See below). This parameter is **optional** |

#### Returns:

1. If `threshold` is a _single scalar_, an array of paths representing the _iso lines_ for the given `threshold` and input `data`.

2. If `threshold` is an _array of scalars_, an additional array layer wraps the individual arrays of paths for each threshold value.
An array of arrays of paths representing the _iso lines_ for the given `thresholds` and input `data`. Each element of the top level array represents the results for a single value in `thresholds`. Three threshold values in, three arrays of paths out.

A single path is an array of coordinates where each coordinate, again, is an array with two entries `[ x, y ]` denoting the `x` and `y` position, respectively.

Expand All @@ -149,61 +143,51 @@ Furthermore, if all values at the border of the input data are below the thresho
### Iso Bands

```javascript
function isoBands(data, lowerBound, bandWidth, options)
function isoBands(data, thresholds, bandwidths, options)
```

Compute _iso bands_ for a 2-dimensional scalar field, a (list of) lowerBound(s), and a (list of) bandWidth(s).

| Parameter | Description |

| Parameter | Description |
| ------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |

| `data` | 2-dimensional input data, i.e. the scalar field (must be array of arrays, or pre-processed data object obtained from `new QuadTree()`). This parameter is **mandatory**. |

| `lowerBound` | A constant numerical value (or array of numerical values) that define(s) the lower bound of the _iso band_. This parameter is **mandatory**. |

| `bandWidth` | A constant numerical value (or array of numerical values) that defines the width(s) the _iso band_, i.e. the range of values. This parameter is **mandatory**. |

| `options` | An object with attributes allowing for changes in the behavior of this function (See below). This parameter is **optional**. |
| `data` | 2-dimensional input data, i.e. the scalar field (must be array of arrays, or pre-processed data object obtained from `new QuadTree()`). This parameter is **mandatory**. |
| `thresholds` | An array of numerical values that define the lower bounds of the _iso bands_. This parameter is **mandatory**. |
| `bandwidths` | An array of numerical values that define the widths of the _iso bands_. This parameter is **mandatory**. |
| `options` | An object with attributes allowing for changes in the behavior of this function (See below). This parameter is **optional**. |

#### Returns:

1. If `lowerBound` is a _single scalar_, an array of paths representing the _iso lines_ which enclose the _iso band_ of size `bandWidth`.

2. If `lowerBound` is an _array of scalars_, an additional array layer wraps the individual arrays of paths for each `threshold`-`bandWidth` pair. Note, that if `bandWidth` is a _scalar_ it will be applied to all entries in the `lowerBound` array.
An array of arrays of paths representing the _iso lines_ which enclose the _iso bands_ of size `bandWidths`. Each element of the top level array represents the results for a single value in `bandwidths`. Three bandwidth values in, three arrays of paths out.

A single path is an array of coordinates where each coordinate, again, is an array with two entries `[ x, y ]` denoting the `x` and `y` position, respectively.

Note, that the paths resemble _linear Rings_ by default, i.e. they are closed and have identical first and last coordinates. (see the `options` parameter to change the output)

### Options

The following options can be passed to either `isoLines` or `isoBands`.

| Property | Type | Description | Default value |

| ------------------------- | :--------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |

| `options.successCallback` | _function_ | A function called at the end of each _iso line_ / _iso band_ computation. It will be passed the `path array` and the corresponding limit(s) (`threshold` or `lowerBound, bandWidth`) as first and second (third) arguments, respectively. | `null` |
The following options can be passed to either `isoLines` or `isoBands` as properties on an options object.

| `options.verbose` | _bool_ | Create `console.log()` info messages before each major step of the algorithm | `false` |

| `options.polygons` | _bool_ | If `true` the function returns a list of path coordinates for individual polygons within each grid cell, if `false` returns a list of path coordinates representing the outline of connected polygons. | `false` |

| `options.linearRing` | _bool_ | If `true`, the polygon paths are returned as linear rings, i.e. the first and last coordinate are identical indicating a closed path. Note, that for the `IsoLines` implementation a value of `false` reduces the output to _iso lines_ that are not necessarily closed paths. | `true` |

| `options.noQuadTree` | _bool_ | If `true`, Quad-Tree optimization is deactivated no matter what the input is. Otherwise, the implementations make use of Quad-Tree optimization if the input demands for _multiple_ iso lines/bands. | `false` |
```javascript
const lines = isoLines(data, thresholds, { verbose: true, noFrame: true });
```

| `options.noFrame` | _bool_ | If `true`, the _iso line_ / _iso contour_ algorithm omits the enclosing rectangular outer frame when all data points along the boundary of the scalar field are below the threshold. Otherwise, if necessary, the enclosing frame will be included for each threshold level as the very first returned path. | `false` |
| Property | Type | Description | Default value |
| ----------------- | :--------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| `successCallback` | _function_ | A function called at the end of each _iso line_ / _iso band_ computation. It will be passed the `path array` and the corresponding limit(s) (`threshold` or `lowerBound, bandWidth`) as first and second (third) arguments, respectively. | `null` |
| `verbose` | _bool_ | Create `console.log()` info messages before each major step of the algorithm | `false` |
| `polygons` | _bool_ | If `true` the function returns a list of path coordinates for individual polygons within each grid cell, if `false` returns a list of path coordinates representing the outline of connected polygons. | `false` |
| `linearRing` | _bool_ | If `true`, the polygon paths are returned as linear rings, i.e. the first and last coordinate are identical indicating a closed path. Note, that for the `IsoLines` implementation a value of `false` reduces the output to _iso lines_ that are not necessarily closed paths. | `true` |
| `noQuadTree` | _bool_ | If `true`, Quad-Tree optimization is deactivated no matter what the input is. Otherwise, the implementations make use of Quad-Tree optimization if the input demands for _multiple_ iso lines/bands. | `false` |
| `noFrame` | _bool_ | If `true`, the _iso line_ / _iso contour_ algorithm omits the enclosing rectangular outer frame when all data points along the boundary of the scalar field are below the threshold. Otherwise, if necessary, the enclosing frame will be included for each threshold level as the very first returned path. | `false` |

## Examples

The _iso band_ shown below should contain all values greater than or equal to 2 and _less than_ 3.
The _iso band_ shown below will contain all values greater than or equal to 2 and _less than_ 3.

```javascript
const lowerBounds = [2];
const thresholds = [2];

const bandWidths = [1];
const bandwidths = [1];

const data = [
[18, 13, 10, 9, 10, 13, 18],
Expand All @@ -215,10 +199,10 @@ const data = [
[18, 13, 10, 9, 10, 13, 18],
];

const bands = isoBands(data, lowerBounds, bandWidths);
const bands = isoBands(data, thresholds, bandwidths);
```

The return value, `bands`, is an array of an array of closed polygons which includes all the points of the grid meeting the criteria.
The return value, `bands`, is an array of arrays of closed polygons which includes all the points of the grid meeting the criteria.

You can find more examples in the [example/](example/) directory.

Expand Down
6 changes: 3 additions & 3 deletions src/common.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
type SuccessCallback = (
pathArray: (Ring | Ring[])[],
thresholdOrlowerBound: number,
bandWidth?: number
pathArray: Ring[][],
threshold: number,
bandwidth?: number
) => void;

type Coord = [number, number];
Expand Down
87 changes: 30 additions & 57 deletions src/isobands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1771,8 +1771,8 @@ const shapeCoordinates = {
*/
function isoBands(
input: number[][] | QuadTree,
minV: number | number[],
bandWidth: number | number[],
thresholds: number[],
bandwidths: number[],
options?: Options
) {
let i: number,
Expand All @@ -1783,26 +1783,20 @@ function isoBands(
root = null,
data = null,
cellGrid: BandCellGrid = [],
multiBand = false,
bw = [],
bandPolygons: Ring[],
ret: (Ring | Ring[])[] = [];
ret: Ring[][] = [];

/* Defaults for optional args */
options = options ?? {};

/* basic input validation */
if (!input) throw new Error("data is required");
if (minV === undefined || minV === null)
throw new Error("lowerBound is required");
if (bandWidth === undefined || bandWidth === null)
throw new Error("bandWidth is required");
if (!!options && typeof options !== "object")
throw new Error("options must be an object");

settings = isoBandOptions(options);

/* check for input data */
if (!input) throw new Error("data is required");
if (input instanceof QuadTree) {
tree = input;
root = input.root;
Expand All @@ -1816,44 +1810,32 @@ function isoBands(
);
}

/* check and prepare input thresholds */
if (Array.isArray(minV)) {
multiBand = true;

/* activate QuadTree optimization if not explicitly forbidden by user settings */
if (!settings.noQuadTree) useQuadTree = true;
if (thresholds === undefined || thresholds === null)
throw new Error("thresholds is required");
if (!Array.isArray(thresholds))
throw new Error("thresholds must be an array");
if (bandwidths === undefined || bandwidths === null)
throw new Error("bandwidths is required");
if (!Array.isArray(bandwidths))
throw new Error("bandwidths must be an array");

/* check if all minV are numbers */
for (i = 0; i < minV.length; i++)
if (isNaN(+minV[i]))
throw new Error("lowerBound[" + i + "] is not a number");

if (Array.isArray(bandWidth)) {
if (minV.length !== bandWidth.length)
throw new Error("lowerBound and bandWidth have unequal lengths");

/* check bandwidth values */
for (i = 0; i < bandWidth.length; i++)
if (isNaN(+bandWidth[i]))
throw new Error("bandWidth[" + i + "] is not a number");
} else {
if (isNaN(+bandWidth)) throw new Error("bandWidth must be a number");
/* check and prepare input thresholds */

bw = [];
for (i = 0; i < minV.length; i++) {
bw.push(bandWidth);
}
bandWidth = bw;
}
} else {
if (isNaN(+minV)) throw new Error("lowerBound must be a number");
/* activate QuadTree optimization if not explicitly forbidden by user settings */
if (!settings.noQuadTree) useQuadTree = true;

minV = [minV];
/* check if all thresholds are numbers */
for (i = 0; i < thresholds.length; i++)
if (isNaN(+thresholds[i]))
throw new Error("thresholds[" + i + "] is not a number");

if (isNaN(+bandWidth)) throw new Error("bandWidth must be a number");
if (thresholds.length !== bandwidths.length)
throw new Error("threshold and bandwidth arrays have unequal lengths");

bandWidth = [bandWidth as number]; // singular value (not an array)
}
/* check bandwidth values */
for (i = 0; i < bandwidths.length; i++)
if (isNaN(+bandwidths[i]))
throw new Error("bandwidths[" + i + "] is not a number");

/* create QuadTree root node if not already present */
if (useQuadTree && !root) {
Expand All @@ -1866,29 +1848,24 @@ function isoBands(
if (settings.polygons)
console.log("isoBands: returning single polygons for each grid cell");
else console.log("isoBands: returning polygon paths for entire data grid");

if (multiBand)
console.log(
"isoBands: multiple bands requested, returning array of band polygons instead of polygons for a single band"
);
}

/* Done with all input validation, now let's start computing stuff */

/* loop over all minV values */
minV.forEach(function (lowerBound, b) {
thresholds.forEach(function (lowerBound, b) {
bandPolygons = [];

/* store bounds for current computation in settings object */
settings.minV = lowerBound;
settings.maxV = lowerBound + bandWidth[b];
settings.maxV = lowerBound + bandwidths[b];

if (settings.verbose)
console.log(
"isoBands: computing isobands for [" +
lowerBound +
":" +
(lowerBound + bandWidth[b]) +
(lowerBound + bandwidths[b]) +
"]"
);

Expand Down Expand Up @@ -1945,14 +1922,10 @@ function isoBands(
}

/* finally, add polygons to output array */
if (multiBand) {
ret.push(bandPolygons);
} else {
ret = bandPolygons;
}
ret.push(bandPolygons);

if (typeof settings.successCallback === "function")
settings.successCallback(ret, lowerBound, bandWidth[b]);
settings.successCallback(ret, lowerBound, bandwidths[b]);
});

return ret;
Expand Down
Loading

0 comments on commit 9ef9cd1

Please sign in to comment.