diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-color/package.json b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-color/package.json index dff510802da9..00667aad82b8 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-color/package.json +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-color/package.json @@ -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" diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-color/src/SequentialScheme.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-color/src/SequentialScheme.ts index a25b6f05af9c..8b04d31d8019 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-color/src/SequentialScheme.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-color/src/SequentialScheme.ts @@ -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 scaleLinear().domain(domain).range(this.colors).clamp(true); + /** + * 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().interpolate(interpolateHcl).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(t => piecewiseScale(adjustExtent(t)), numColors); } } diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-color/test/SequentialScheme.test.ts b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-color/test/SequentialScheme.test.ts index c05ab1dd8c98..22f9d351c904 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-color/test/SequentialScheme.test.ts +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-color/test/SequentialScheme.test.ts @@ -17,41 +17,73 @@ 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(1)).toEqual('rgb(255, 255, 255)'); - expect(scale(10)).toEqual('rgb(255, 255, 255)'); - expect(scale(55)).toEqual('rgb(128, 128, 128)'); - expect(scale(100)).toEqual('rgb(0, 0, 0)'); - expect(scale(1000)).toEqual('rgb(0, 0, 0)'); + expect(scale.domain()).toHaveLength(scale.range().length); + const scale2 = scheme.createLinearScale([0, 10, 100]); + expect(scale2.domain()).toHaveLength(scale2.range().length); }); - it('uses [0, 1] as extent 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(1)).toEqual('rgb(0, 0, 0)'); - expect(scale(2)).toEqual('rgb(0, 0, 0)'); + 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(119, 119, 119)'); + expect(scale(100)).toEqual('rgb(0, 0, 0)'); + expect(scale(1000)).toEqual('rgb(0, 0, 0)'); + }); + 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(119, 119, 119)'); + expect(scale(1)).toEqual('rgb(0, 0, 0)'); + expect(scale(2)).toEqual('rgb(0, 0, 0)'); + }); + }); + 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)', () => { - it('returns the original colors if numColors is not specified', () => { - expect(scheme.getColors()).toEqual(['#fff', '#000']); + describe('.getColors(numColors, extent)', () => { + describe('numColors', () => { + it('returns the original colors if numColors is not specified', () => { + expect(scheme.getColors()).toEqual(['#fff', '#000']); + }); + it('returns the exact number of colors if numColors is specified', () => { + expect(scheme.getColors(2)).toEqual(['#fff', '#000']); + expect(scheme.getColors(3)).toEqual([ + 'rgb(255, 255, 255)', + 'rgb(119, 119, 119)', + 'rgb(0, 0, 0)', + ]); + expect(scheme.getColors(4)).toEqual([ + 'rgb(255, 255, 255)', + 'rgb(162, 162, 162)', + 'rgb(78, 78, 78)', + 'rgb(0, 0, 0)', + ]); + }); }); - it('returns the exact number of colors if numColors is specified', () => { - expect(scheme.getColors(2)).toEqual(['#fff', '#000']); - expect(scheme.getColors(3)).toEqual([ - 'rgb(255, 255, 255)', - 'rgb(128, 128, 128)', - 'rgb(0, 0, 0)', - ]); - expect(scheme.getColors(4)).toEqual([ - 'rgb(255, 255, 255)', - 'rgb(170, 170, 170)', - 'rgb(85, 85, 85)', - '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)']); + }); }); }); });