-
Notifications
You must be signed in to change notification settings - Fork 57
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
255 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import scaleGenerator from '../theme-scale-generator'; | ||
|
||
describe('Theme scale generator', () => { | ||
const input = ['#ffffff', '#000000']; | ||
const base8 = ['#ffffff', '#d4d4d4', '#aaaaaa', '#7f7f7f', '#545454', '#2a2a2a', '#000000']; | ||
|
||
beforeEach(() => {}); | ||
|
||
it('Should generate a pyramid', () => { | ||
const scales = [{ type: 'class', scale: input }]; | ||
scaleGenerator(scales); | ||
expect(scales).to.have.length(1); | ||
const { scale, type } = scales[0]; | ||
|
||
expect(type).to.equals('class-pyramid'); | ||
expect(scale.length).to.equal(8); | ||
expect(scale[scale.length - 1].length).to.equal(7); | ||
}); | ||
|
||
it('Should generate a correct base of colors', () => { | ||
const scales = [{ type: 'class', scale: input }]; | ||
scaleGenerator(scales); | ||
const { scale } = scales[0]; | ||
|
||
const colors = scale[scale.length - 1]; | ||
expect(colors).to.deep.equal(base8); | ||
}); | ||
|
||
it('Should work correctly on a scale from the sense theme', () => { | ||
const senseDivergentScale = [ | ||
'#ae1c3e', | ||
'#d24d3e', | ||
'#ed875e', | ||
'#f9bd7e', | ||
'#ffe3aa', | ||
'#e6f5fe', | ||
'#b4ddf7', | ||
'#77b7e5', | ||
'#3a89c9', | ||
'#3d52a1', | ||
]; | ||
const scales = [{ type: 'class', scale: senseDivergentScale }]; | ||
scaleGenerator(scales); | ||
const { scale } = scales[0]; | ||
expect(scale).to.deep.equal([ | ||
null, | ||
['#e6f5fe'], | ||
['#ed875e', '#3a89c9'], | ||
['#d24d3e', '#ffe3aa', '#3a89c9'], | ||
['#d24d3e', '#f9bd7e', '#b4ddf7', '#3a89c9'], | ||
['#d24d3e', '#f9bd7e', '#e6f5fe', '#b4ddf7', '#3a89c9'], | ||
['#d24d3e', '#ed875e', '#ffe3aa', '#e6f5fe', '#77b7e5', '#3d52a1'], | ||
['#ae1c3e', '#ed875e', '#f9bd7e', '#e6f5fe', '#b4ddf7', '#77b7e5', '#3d52a1'], | ||
['#ae1c3e', '#d24d3e', '#f9bd7e', '#ffe3aa', '#e6f5fe', '#b4ddf7', '#3a89c9', '#3d52a1'], | ||
['#ae1c3e', '#d24d3e', '#ed875e', '#f9bd7e', '#e6f5fe', '#b4ddf7', '#77b7e5', '#3a89c9', '#3d52a1'], | ||
['#ae1c3e', '#d24d3e', '#ed875e', '#f9bd7e', '#ffe3aa', '#e6f5fe', '#b4ddf7', '#77b7e5', '#3a89c9', '#3d52a1'], | ||
]); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
import { color, rgb } from 'd3-color'; | ||
|
||
/** | ||
* Gets this mapping between the scaled value and the color parts | ||
* @param {Number} scaledValue - A value between 0 and 1 representing a value in the data scaled between the max and min boundaries of the data. Values are clamped to 0 and 1. | ||
* @param {Number} numEdges - Number of parts that makes up this scale | ||
*/ | ||
function limitFunction(scaledValue, numParts) { | ||
/* | ||
* Color-Scale doesn't calculate exact color blends based of the scaled value. It instead shifts the value inwards to achieve | ||
* a better color representation at the edges. Primarily this is done to allow setting custom limits to where each color begins | ||
* and ends. If a color begins and ends at 1, it should not be visible. The simplest way to achive this is to remove 1 and 0 | ||
* from the possible numbers that can be used. Colors that are not equal to 1 or 0 should not be affected. | ||
*/ | ||
|
||
// The following is done to keep the scaled value above 0 and below 1. This shifts values that hits an exact boundary upwards. | ||
// eslint-disable-next-line no-param-reassign | ||
scaledValue = Math.min(Math.max(scaledValue, 0.000000000001), 0.999999999999); | ||
return numParts - scaledValue * numParts; | ||
} | ||
|
||
function getLevel(scale, level) { | ||
return Math.min(level || scale.startLevel, scale.colorParts.length - 1); | ||
} | ||
|
||
function blend(c1, c2, t) { | ||
const r = Math.floor(c1.r + (c2.r - c1.r) * t); | ||
const g = Math.floor(c1.g + (c2.g - c1.g) * t); | ||
const b = Math.floor(c1.b + (c2.b - c1.b) * t); | ||
const a = Math.floor(c1.opacity + (c2.opacity - c1.opacity) * t); | ||
return rgb(r, g, b, a); | ||
} | ||
|
||
class ColorScale { | ||
constructor(nanColor) { | ||
this.colorParts = []; | ||
this.startLevel = 0; | ||
this.max = 1; | ||
this.min = 0; | ||
this.nanColor = color(nanColor); | ||
} | ||
|
||
/** | ||
* Adds a part to this color scale. The input colors span one part of the gradient, colors between them are interpolated. Input two equal colors for a solid scale part. | ||
* @param {String|Number} color1 - First color to be used, in formats defined by Color | ||
* @param {String|Number} color2 - Second color to be used, in formats defined by Color | ||
* @param {Number} level - Which level of the color pyramid to add this part to. | ||
*/ | ||
addColorPart(color1, color2, level) { | ||
// eslint-disable-next-line no-param-reassign | ||
level = level || 0; | ||
this.startLevel = Math.max(level, this.startLevel); | ||
if (!this.colorParts[level]) { | ||
this.colorParts[level] = []; | ||
} | ||
this.colorParts[level].push([color(color1), color(color2)]); | ||
} | ||
|
||
/** | ||
* Gets the color which represents the input value | ||
* @param {Number} scaledValue - A value between 0 and 1 representing a value in the data scaled between the max and min boundaries of the data. Values are clamped to 0 and 1. | ||
*/ | ||
getColor(value, level) { | ||
const scaledValue = value - this.min; | ||
if (Number.isNaN(+value) || Number.isNaN(+scaledValue)) { | ||
return this.nanColor; | ||
} | ||
// eslint-disable-next-line no-param-reassign | ||
level = getLevel(this, level); | ||
const k = limitFunction(scaledValue, this.colorParts[level].length); | ||
let f = Math.floor(k); | ||
f = f === k ? f - 1 : f; // To fulfill equal or greater than: 329-<330 | ||
const part = this.colorParts[level][f]; | ||
const c1 = part[0]; | ||
const c2 = part[1]; | ||
|
||
// For absolute edges we return the colors at the limit | ||
if (value === this.min) { | ||
return c2; | ||
} | ||
if (value === this.max) { | ||
return c1; | ||
} | ||
|
||
const t = k - f; | ||
const uc = blend(c1, c2, t); | ||
return uc; | ||
} | ||
} | ||
|
||
export default ColorScale; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import { color } from 'd3-color'; | ||
import ColorScale from './color-scale'; | ||
|
||
/* Calculates a value that expands from 0.5 out to 0 and 1 | ||
* Ex for size 8: | ||
* current -> percent | ||
* 0 -> 0.0625 4 -> 0.3125 | ||
* 1 -> 0.125 5 -> 0.375 | ||
* 2 -> 0.1875 6 -> 0.4375 | ||
* 3 -> 0.25 7 -> 0.5 | ||
*/ | ||
|
||
function getScaleValue(value, current, size) { | ||
const percent = 0.25 + ((current + 1) / size) * 0.25; | ||
const min = 0.5 - percent; | ||
const max = 0.5 + percent; | ||
const span = max - min; | ||
return min + (value / 1) * span; | ||
} | ||
|
||
function setupColorScale(colors, nanColor, gradient) { | ||
const newColors = []; | ||
const cs = new ColorScale(nanColor); | ||
newColors.push(colors[0]); | ||
|
||
if (!gradient) { | ||
newColors.push(colors[0]); | ||
} | ||
let i = 1; | ||
for (; i < colors.length - 1; i++) { | ||
newColors.push(colors[i]); | ||
newColors.push(colors[i]); | ||
} | ||
|
||
newColors.push(colors[i]); | ||
if (!gradient) { | ||
newColors.push(colors[i]); | ||
} | ||
|
||
for (let j = 0; j < newColors.length; j += 2) { | ||
cs.addColorPart(newColors[j], newColors[j + 1]); | ||
} | ||
|
||
return cs; | ||
} | ||
|
||
function generateLevel(scale, current, size) { | ||
const level = []; | ||
for (let j = 0; j < current + 1; j++) { | ||
let c; | ||
switch (current) { | ||
case 0: | ||
c = scale.getColor(0.5); | ||
break; | ||
default: { | ||
const scaled = getScaleValue((1 / current) * j, current, size); | ||
|
||
c = scale.getColor(scaled); | ||
break; | ||
} | ||
} | ||
level.push(color(c).formatHex()); | ||
} | ||
return level; | ||
} | ||
|
||
/** | ||
* Generates a pyramid of colors from a minimum of 2 colors | ||
* | ||
* @internal | ||
* @param {Array} colors an array of colors to generate from | ||
* @param {Int} size the size of the base of the pyramid | ||
* @returns {Array} A 2 dimensional array containing the levels of the color pyramid | ||
*/ | ||
function createPyramidFromColors(colors, size, nanColor) { | ||
const gradientScale = setupColorScale(colors, nanColor, true); | ||
const baseLevel = generateLevel(gradientScale, size - 1, size); | ||
const scale = setupColorScale(baseLevel, nanColor, false); | ||
const pyramid = [null]; | ||
for (let i = 0; i < size; i++) { | ||
pyramid.push(generateLevel(scale, i, size)); | ||
} | ||
return pyramid; | ||
} | ||
|
||
export default function generateOrdinalScales(scalesDef, nanColor = '#d2d2d2') { | ||
scalesDef.forEach((def) => { | ||
if (def.type === 'class') { | ||
// generate pyramid | ||
const pyramid = createPyramidFromColors(def.scale, Math.max(def.scale.length, 7), nanColor); | ||
// eslint-disable-next-line no-param-reassign | ||
def.scale = pyramid; | ||
// eslint-disable-next-line no-param-reassign | ||
def.type = 'class-pyramid'; | ||
} | ||
}); | ||
} |