Skip to content

Commit

Permalink
feat: Interpolate color threshold stops (kalkih#596)
Browse files Browse the repository at this point in the history
It's no longer necessary to specify all the stop points in color thresholds (gradients). This paves the way for two more improvements (which are not featured in this PR):

Implicitly assuming the first and last stops are the min/max values in the graph (or some other reasonable logic), thereby making all stop points completely optional
Allowing for named color patterns (hardcoded into the project), like "rainbow", "cubehelix", "rocket", "mako", etc. (adopting common color patterns already established in other FOSS data visualization projects)
  • Loading branch information
acshef authored and jlsjonas committed Jan 22, 2022
1 parent 53d923a commit 3826c0d
Show file tree
Hide file tree
Showing 2 changed files with 121 additions and 5 deletions.
40 changes: 39 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,47 @@ See [dynamic line color](#dynamic-line-color) for example usage.

| Name | Type | Default | Description |
|------|:----:|:-------:|-------------|
| value ***(required)*** | number | | The threshold for the color stop.
| value ***(required [except in interpolation (see below)](#line-color-interpolation-of-stop-values))*** | number | | The threshold for the color stop.
| color ***(required)*** | string | | Color in 6 digit hex format (e.g. `#008080`).

##### Line color interpolation of stop values
As long as the first and last threshold stops have `value` properties, intermediate stops can exclude `value`; they will be interpolated linearly. For example, given stops like:

```yaml
color_thresholds:
- value: 0
color: "#ff0000"
- color: "#ffff00"
- color: "#00ff00"
- value: 4
color: "#0000ff"
```

The values will be interpolated as:

```yaml
color_thresholds:
- value: 0
color: "#ff0000"
- value: 1.333333
color: "#ffff00"
- value: 2.666667
color: "#00ff00"
- value: 4
color: "#0000ff"
```

As a shorthand, you can just use a color string for the stops that you want interpolated:

```yaml
- value: 0
color: "#ff0000"
- "#ffff00"
- "#00ff00"
- value: 4
color: "#0000ff"
```

#### Action object options
| Name | Type | Default | Options | Description |
|------|:----:|:-------:|:-----------:|-------------|
Expand Down
86 changes: 82 additions & 4 deletions src/buildConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,93 @@ import {
DEFAULT_SHOW,
} from './const';

/**
* Starting from the given index, increment the index until an array element with a
* "value" property is found
*
* @param {Array} stops
* @param {number} startIndex
* @returns {number}
*/
const findFirstValuedIndex = (stops, startIndex) => {
for (let i = startIndex, l = stops.length; i < l; i += 1) {
if (stops[i].value != null) {
return i;
}
}
throw new Error(
'Error in threshold interpolation: could not find right-nearest valued stop. '
+ 'Do the first and last thresholds have a set "value"?',
);
};

/**
* Interpolates the "value" of each stop. Each stop can be a color string or an object of type
* ```
* {
* color: string
* value?: number | null
* }
* ```
* And the values will be interpolated by the nearest valued stops.
*
* For example, given values `[ 0, null, null, 4, null, 3]`,
* the interpolation will output `[ 0, 1.3333, 2.6667, 4, 3.5, 3 ]`
*
* Note that values will be interpolated ascending and descending.
* All that's necessary is that the first and the last elements have values.
*
* @param {Array} stops
* @returns {Array<{ color: string, value: number }>}
*/
const interpolateStops = (stops) => {
if (!stops || !stops.length) {
return stops;
}
if (stops[0].value == null || stops[stops.length - 1].value == null) {
throw new Error(`The first and last thresholds must have a set "value".\n See ${URL_DOCS}`);
}

let leftValuedIndex = 0;
let rightValuedIndex = null;

return stops.map((stop, stopIndex) => {
if (stop.value != null) {
return { ...stop };
}

if (rightValuedIndex == null) {
rightValuedIndex = findFirstValuedIndex(stops, stopIndex);
} else if (stopIndex > rightValuedIndex) {
leftValuedIndex = rightValuedIndex;
rightValuedIndex = findFirstValuedIndex(stops, stopIndex);
}

// y = mx + b
// m = dY/dX
// x = index in question
// b = left value

const leftValue = stops[leftValuedIndex].value;
const rightValue = stops[rightValuedIndex].value;
const m = (rightValue - leftValue) / (rightValuedIndex - leftValuedIndex);
return {
color: typeof stop === 'string' ? stop : stop.color,
value: m * stopIndex + leftValue,
};
});
};

const computeThresholds = (stops, type) => {
stops.sort((a, b) => b.value - a.value);
const valuedStops = interpolateStops(stops);
valuedStops.sort((a, b) => b.value - a.value);

if (type === 'smooth') {
return stops;
return valuedStops;
} else {
const rect = [].concat(...stops.map((stop, i) => ([stop, {
const rect = [].concat(...valuedStops.map((stop, i) => ([stop, {
value: stop.value - 0.0001,
color: stops[i + 1] ? stops[i + 1].color : stop.color,
color: valuedStops[i + 1] ? valuedStops[i + 1].color : stop.color,
}])));
return rect;
}
Expand Down

0 comments on commit 3826c0d

Please sign in to comment.