mirror of
https://github.com/apache/superset.git
synced 2024-09-19 20:19:37 -04:00
feat(color): support better color interpolation for sequential schemes (#547)
* feat: update functions * test: add unit tests * fix: unit tests * fix: address comments and use piecewise
This commit is contained in:
parent
970f632e15
commit
f2a053b034
@ -27,7 +27,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/d3-scale": "^2.1.1",
|
||||
"d3-scale": "^3.0.0"
|
||||
"@types/d3-interpolate": "^1.3.1",
|
||||
"d3-scale": "^3.0.0",
|
||||
"d3-interpolate": "^1.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@superset-ui/core": "^0.13.0"
|
||||
|
@ -1,15 +1,7 @@
|
||||
import { scaleLinear } from 'd3-scale';
|
||||
import { interpolateHcl, interpolateNumber, piecewise, quantize } from 'd3-interpolate';
|
||||
import ColorScheme, { ColorSchemeConfig } from './ColorScheme';
|
||||
|
||||
function range(count: number) {
|
||||
const values: number[] = [];
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
values.push(i);
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
export interface SequentialSchemeConfig extends ColorSchemeConfig {
|
||||
isDiverging?: boolean;
|
||||
}
|
||||
@ -23,24 +15,43 @@ export default class SequentialScheme extends ColorScheme {
|
||||
this.isDiverging = isDiverging;
|
||||
}
|
||||
|
||||
createLinearScale(extent: number[] = [0, 1]) {
|
||||
// Create matching domain
|
||||
// because D3 continuous scale uses piecewise mapping
|
||||
// between domain and range.
|
||||
const valueScale = scaleLinear().range(extent);
|
||||
const denominator = this.colors.length - 1;
|
||||
const domain = range(this.colors.length).map(i => valueScale(i / denominator));
|
||||
/**
|
||||
* Return a linear scale with a new domain interpolated from the input domain
|
||||
* to match the number of elements in the color scheme
|
||||
* because D3 continuous scale uses piecewise mapping between domain and range.
|
||||
* This is a common use-case when the domain is [min, max]
|
||||
* and the palette has more than two colors.
|
||||
*
|
||||
* @param domain domain of the scale
|
||||
* @param modifyRange Set this to true if you don't want to modify the domain and
|
||||
* want to interpolate range to have the same number of elements with domain instead.
|
||||
*/
|
||||
createLinearScale(domain: number[] = [0, 1], modifyRange = false) {
|
||||
const scale = scaleLinear<string>().interpolate(interpolateHcl).clamp(true);
|
||||
|
||||
return scaleLinear<string>().domain(domain).range(this.colors).clamp(true);
|
||||
return modifyRange || domain.length === this.colors.length
|
||||
? scale.domain(domain).range(this.getColors(domain.length))
|
||||
: scale
|
||||
.domain(quantize(piecewise(interpolateNumber, domain), this.colors.length))
|
||||
.range(this.colors);
|
||||
}
|
||||
|
||||
getColors(numColors: number = this.colors.length): string[] {
|
||||
if (numColors === this.colors.length) {
|
||||
/**
|
||||
* Get colors from this scheme
|
||||
* @param numColors number of colors to return.
|
||||
* Will interpolate the current scheme to match the number of colors requested
|
||||
* @param extent The extent of the color range to use.
|
||||
* For example [0.2, 1] will rescale the color scheme
|
||||
* such that color values in the range [0, 0.2) are excluded from the scheme.
|
||||
*/
|
||||
getColors(numColors: number = this.colors.length, extent: number[] = [0, 1]): string[] {
|
||||
if (numColors === this.colors.length && extent[0] === 0 && extent[1] === 1) {
|
||||
return this.colors;
|
||||
}
|
||||
const colorScale = this.createLinearScale();
|
||||
const denominator = numColors - 1;
|
||||
|
||||
return range(numColors).map(i => colorScale(i / denominator));
|
||||
const piecewiseScale: (t: number) => string = piecewise(interpolateHcl, this.colors);
|
||||
const adjustExtent = scaleLinear().range(extent).clamp(true);
|
||||
|
||||
return quantize<string>(t => piecewiseScale(adjustExtent(t)), numColors);
|
||||
}
|
||||
}
|
||||
|
@ -17,25 +17,50 @@ describe('SequentialScheme', () => {
|
||||
expect(scheme2).toBeInstanceOf(SequentialScheme);
|
||||
});
|
||||
});
|
||||
describe('.createLinearScale(extent)', () => {
|
||||
it('returns a linear scale for the given extent', () => {
|
||||
describe('.createLinearScale(domain, modifyRange)', () => {
|
||||
it('returns a piecewise scale', () => {
|
||||
const scale = scheme.createLinearScale([10, 100]);
|
||||
expect(scale.domain()).toHaveLength(scale.range().length);
|
||||
const scale2 = scheme.createLinearScale([0, 10, 100]);
|
||||
expect(scale2.domain()).toHaveLength(scale2.range().length);
|
||||
});
|
||||
describe('domain', () => {
|
||||
it('returns a linear scale for the given domain', () => {
|
||||
const scale = scheme.createLinearScale([10, 100]);
|
||||
expect(scale(1)).toEqual('rgb(255, 255, 255)');
|
||||
expect(scale(10)).toEqual('rgb(255, 255, 255)');
|
||||
expect(scale(55)).toEqual('rgb(128, 128, 128)');
|
||||
expect(scale(55)).toEqual('rgb(119, 119, 119)');
|
||||
expect(scale(100)).toEqual('rgb(0, 0, 0)');
|
||||
expect(scale(1000)).toEqual('rgb(0, 0, 0)');
|
||||
});
|
||||
it('uses [0, 1] as extent if not specified', () => {
|
||||
it('uses [0, 1] as domain if not specified', () => {
|
||||
const scale = scheme.createLinearScale();
|
||||
expect(scale(-1)).toEqual('rgb(255, 255, 255)');
|
||||
expect(scale(0)).toEqual('rgb(255, 255, 255)');
|
||||
expect(scale(0.5)).toEqual('rgb(128, 128, 128)');
|
||||
expect(scale(0.5)).toEqual('rgb(119, 119, 119)');
|
||||
expect(scale(1)).toEqual('rgb(0, 0, 0)');
|
||||
expect(scale(2)).toEqual('rgb(0, 0, 0)');
|
||||
});
|
||||
});
|
||||
describe('.getColors(numColors)', () => {
|
||||
describe('modifyRange', () => {
|
||||
const scheme3 = new SequentialScheme({
|
||||
id: 'test-scheme3',
|
||||
colors: ['#fee087', '#fa5c2e', '#800026'],
|
||||
});
|
||||
it('modifies domain by default', () => {
|
||||
const scale = scheme3.createLinearScale([0, 100]);
|
||||
expect(scale.domain()).toEqual([0, 50, 100]);
|
||||
expect(scale.range()).toEqual(['#fee087', '#fa5c2e', '#800026']);
|
||||
});
|
||||
it('modifies range instead of domain if set to true', () => {
|
||||
const scale = scheme3.createLinearScale([0, 100], true);
|
||||
expect(scale.domain()).toEqual([0, 100]);
|
||||
expect(scale.range()).toEqual(['rgb(254, 224, 135)', 'rgb(128, 0, 38)']);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('.getColors(numColors, extent)', () => {
|
||||
describe('numColors', () => {
|
||||
it('returns the original colors if numColors is not specified', () => {
|
||||
expect(scheme.getColors()).toEqual(['#fff', '#000']);
|
||||
});
|
||||
@ -43,15 +68,22 @@ describe('SequentialScheme', () => {
|
||||
expect(scheme.getColors(2)).toEqual(['#fff', '#000']);
|
||||
expect(scheme.getColors(3)).toEqual([
|
||||
'rgb(255, 255, 255)',
|
||||
'rgb(128, 128, 128)',
|
||||
'rgb(119, 119, 119)',
|
||||
'rgb(0, 0, 0)',
|
||||
]);
|
||||
expect(scheme.getColors(4)).toEqual([
|
||||
'rgb(255, 255, 255)',
|
||||
'rgb(170, 170, 170)',
|
||||
'rgb(85, 85, 85)',
|
||||
'rgb(162, 162, 162)',
|
||||
'rgb(78, 78, 78)',
|
||||
'rgb(0, 0, 0)',
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('extent', () => {
|
||||
it('adjust the range if extent is specified', () => {
|
||||
expect(scheme.getColors(2, [0, 0.5])).toEqual(['rgb(255, 255, 255)', 'rgb(119, 119, 119)']);
|
||||
expect(scheme.getColors(2, [0.5, 1])).toEqual(['rgb(119, 119, 119)', 'rgb(0, 0, 0)']);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user