From f086791480c2742dde49ea80d1f0a7e671bb3aea Mon Sep 17 00:00:00 2001 From: plouc Date: Fri, 7 Jan 2022 06:34:58 +0900 Subject: [PATCH] feat(colors): add support for continuous color scales --- packages/colors/src/index.ts | 2 +- packages/colors/src/props.ts | 2 - .../colors/src/scales/continuousColorScale.ts | 112 ++++++++++++++++++ .../colors/src/scales/divergingColorScale.ts | 49 ++++++++ packages/colors/src/scales/index.ts | 5 + .../src/{ => scales}/ordinalColorScale.ts | 2 +- .../colors/src/scales/quantizeColorScale.ts | 64 ++++++++++ .../colors/src/scales/sequentialColorScale.ts | 40 +++++++ 8 files changed, 272 insertions(+), 4 deletions(-) create mode 100644 packages/colors/src/scales/continuousColorScale.ts create mode 100644 packages/colors/src/scales/divergingColorScale.ts create mode 100644 packages/colors/src/scales/index.ts rename packages/colors/src/{ => scales}/ordinalColorScale.ts (99%) create mode 100644 packages/colors/src/scales/quantizeColorScale.ts create mode 100644 packages/colors/src/scales/sequentialColorScale.ts diff --git a/packages/colors/src/index.ts b/packages/colors/src/index.ts index 9ad2b7e42c..c399882db9 100644 --- a/packages/colors/src/index.ts +++ b/packages/colors/src/index.ts @@ -1,5 +1,5 @@ export * from './schemes' export * from './inheritedColor' export * from './motion' -export * from './ordinalColorScale' export * from './props' +export * from './scales' diff --git a/packages/colors/src/props.ts b/packages/colors/src/props.ts index 0676abc9be..b533d08669 100644 --- a/packages/colors/src/props.ts +++ b/packages/colors/src/props.ts @@ -14,8 +14,6 @@ export const ordinalColorsPropType = PropTypes.oneOfType([ PropTypes.string, ]) -export const colorPropertyAccessorPropType = PropTypes.oneOfType([PropTypes.func, PropTypes.string]) - export const inheritedColorPropType = PropTypes.oneOfType([ PropTypes.string, PropTypes.func, diff --git a/packages/colors/src/scales/continuousColorScale.ts b/packages/colors/src/scales/continuousColorScale.ts new file mode 100644 index 0000000000..51e69bb49e --- /dev/null +++ b/packages/colors/src/scales/continuousColorScale.ts @@ -0,0 +1,112 @@ +import { useMemo } from 'react' +import { ScaleDiverging, ScaleQuantize, ScaleSequential, scaleLinear } from 'd3-scale' +import { + SequentialColorScaleConfig, + SequentialColorScaleValues, + getSequentialColorScale, +} from './sequentialColorScale' +import { + DivergingColorScaleConfig, + DivergingColorScaleValues, + getDivergingColorScale, +} from './divergingColorScale' +import { + QuantizeColorScaleConfig, + QuantizeColorScaleValues, + getQuantizeColorScale, +} from './quantizeColorScale' + +export type ContinuousColorScaleConfig = + | SequentialColorScaleConfig + | DivergingColorScaleConfig + | QuantizeColorScaleConfig + +export type ContinuousColorScaleValues = + | SequentialColorScaleValues + | DivergingColorScaleValues + | QuantizeColorScaleValues + +const isSequentialColorScaleConfig = ( + config: ContinuousColorScaleConfig +): config is SequentialColorScaleConfig => config.type === 'sequential' + +const isDivergingColorScaleConfig = ( + config: ContinuousColorScaleConfig +): config is DivergingColorScaleConfig => config.type === 'diverging' + +const isQuantizeColorScaleConfig = ( + config: ContinuousColorScaleConfig +): config is QuantizeColorScaleConfig => config.type === 'quantize' + +export const getContinuousColorScale = ( + config: Config, + values: ContinuousColorScaleValues +) => { + if (isSequentialColorScaleConfig(config)) { + return getSequentialColorScale(config, values) + } + + if (isDivergingColorScaleConfig(config)) { + return getDivergingColorScale(config, values) + } + + if (isQuantizeColorScaleConfig(config)) { + return getQuantizeColorScale(config, values) + } + + throw new Error('Invalid continuous color scale config') +} + +export const useContinuousColorScale = ( + config: ContinuousColorScaleConfig, + values: ContinuousColorScaleValues +) => useMemo(() => getContinuousColorScale(config, values), [config, values]) + +export const computeContinuousColorScaleColorStops = ( + scale: ScaleSequential | ScaleDiverging | ScaleQuantize, + steps = 16 +) => { + const domain = scale.domain() + + // quantize + if ('thresholds' in scale) { + const stops: { + key: string + offset: string + stopColor: string + }[] = [] + + const normalizedScale = scaleLinear().domain(domain).range([0, 1]) + scale.range().forEach((color, index) => { + const [start, end] = scale.invertExtent(color) + + stops.push({ + key: `${index}.0`, + offset: `${Math.round(normalizedScale(start) * 100)}%`, + stopColor: color, + }) + stops.push({ + key: `${index}.1`, + offset: `${Math.round(normalizedScale(end) * 100)}%`, + stopColor: color, + }) + }) + + return stops + } + + const colorStopsScale = scale.copy() + if (domain.length === 2) { + // sequential + colorStopsScale.domain([0, 1]) + } else if (domain.length === 3) { + // diverging + colorStopsScale.domain([0, 0.5, 1]) + } + + return ((colorStopsScale as any).ticks(steps) as number[]).map((value: number) => ({ + key: `${value}`, + offset: `${Math.round(value * 100)}%`, + stopColor: `${colorStopsScale(value)}`, + })) +} diff --git a/packages/colors/src/scales/divergingColorScale.ts b/packages/colors/src/scales/divergingColorScale.ts new file mode 100644 index 0000000000..6eae1b5e51 --- /dev/null +++ b/packages/colors/src/scales/divergingColorScale.ts @@ -0,0 +1,49 @@ +import { useMemo } from 'react' +import { scaleDiverging } from 'd3-scale' +import { colorInterpolators, ColorInterpolatorId } from '../schemes' + +export interface DivergingColorScaleConfig { + type: 'diverging' + scheme?: ColorInterpolatorId + minValue?: number + maxValue?: number + divergeAt?: number +} + +export interface DivergingColorScaleValues { + min: number + max: number +} + +export const divergingColorScaleDefaults: { + scheme: ColorInterpolatorId + divergeAt: number +} = { + scheme: 'red_yellow_blue', + divergeAt: 0.5, +} + +export const getDivergingColorScale = ( + { + scheme = divergingColorScaleDefaults.scheme, + divergeAt = divergingColorScaleDefaults.divergeAt, + minValue, + maxValue, + }: DivergingColorScaleConfig, + values: DivergingColorScaleValues +) => { + const min = minValue !== undefined ? minValue : values.min + const max = maxValue !== undefined ? maxValue : values.max + const domain = [min, min + (max - min) / 2, max] + + const interpolator = colorInterpolators[scheme] + const offset = 0.5 - divergeAt + const offsetInterpolator = (t: number) => interpolator(t + offset) + + return scaleDiverging(offsetInterpolator).domain(domain).clamp(true) +} + +export const useDivergingColorScale = ( + config: DivergingColorScaleConfig, + values: DivergingColorScaleValues +) => useMemo(() => getDivergingColorScale(config, values), [config, values]) diff --git a/packages/colors/src/scales/index.ts b/packages/colors/src/scales/index.ts new file mode 100644 index 0000000000..a3bd9f7a1a --- /dev/null +++ b/packages/colors/src/scales/index.ts @@ -0,0 +1,5 @@ +export * from './continuousColorScale' +export * from './divergingColorScale' +export * from './ordinalColorScale' +export * from './quantizeColorScale' +export * from './sequentialColorScale' diff --git a/packages/colors/src/ordinalColorScale.ts b/packages/colors/src/scales/ordinalColorScale.ts similarity index 99% rename from packages/colors/src/ordinalColorScale.ts rename to packages/colors/src/scales/ordinalColorScale.ts index e038e6a248..f0fb682b1a 100644 --- a/packages/colors/src/ordinalColorScale.ts +++ b/packages/colors/src/scales/ordinalColorScale.ts @@ -7,7 +7,7 @@ import { isCategoricalColorScheme, isSequentialColorScheme, isDivergingColorScheme, -} from './schemes' +} from '../schemes' /** * Static color. diff --git a/packages/colors/src/scales/quantizeColorScale.ts b/packages/colors/src/scales/quantizeColorScale.ts new file mode 100644 index 0000000000..3aa264e9c8 --- /dev/null +++ b/packages/colors/src/scales/quantizeColorScale.ts @@ -0,0 +1,64 @@ +import { useMemo } from 'react' +import { scaleQuantize } from 'd3-scale' +import { colorInterpolators, ColorInterpolatorId } from '../schemes' + +// colors from a scheme +export interface QuantizeColorScaleSchemeConfig { + type: 'quantize' + domain?: [number, number] + scheme?: ColorInterpolatorId + steps?: number +} + +// explicit colors +export interface QuantizeColorScaleColorsConfig { + type: 'quantize' + domain?: [number, number] + colors: string[] +} + +export type QuantizeColorScaleConfig = + | QuantizeColorScaleSchemeConfig + | QuantizeColorScaleColorsConfig + +export interface QuantizeColorScaleValues { + min: number + max: number +} + +export const quantizeColorScaleDefaults: { + scheme: ColorInterpolatorId + steps: NonNullable +} = { + scheme: 'turbo', + steps: 7, +} + +export const getQuantizeColorScale = ( + config: QuantizeColorScaleConfig, + values: QuantizeColorScaleValues +) => { + const colorScale = scaleQuantize() + .domain(config.domain || [values.min, values.max]) + .nice() + + if ('colors' in config) { + colorScale.range(config.colors) + } else { + const scheme = config.scheme || quantizeColorScaleDefaults.scheme + const steps = config.steps === undefined ? quantizeColorScaleDefaults.steps : config.steps + const interpolator = colorInterpolators[scheme] + const colors = Array.from({ length: steps }).map((_, step) => + interpolator(step * (1 / (steps - 1))) + ) + + colorScale.range(colors) + } + + return colorScale +} + +export const useQuantizeColorScale = ( + config: QuantizeColorScaleConfig, + values: QuantizeColorScaleValues +) => useMemo(() => getQuantizeColorScale(config, values), [config, values]) diff --git a/packages/colors/src/scales/sequentialColorScale.ts b/packages/colors/src/scales/sequentialColorScale.ts new file mode 100644 index 0000000000..30377c248c --- /dev/null +++ b/packages/colors/src/scales/sequentialColorScale.ts @@ -0,0 +1,40 @@ +import { useMemo } from 'react' +import { scaleSequential } from 'd3-scale' +import { colorInterpolators, ColorInterpolatorId } from '../schemes' + +export interface SequentialColorScaleConfig { + type: 'sequential' + scheme?: ColorInterpolatorId + minValue?: number + maxValue?: number +} + +export interface SequentialColorScaleValues { + min: number + max: number +} + +export const sequentialColorScaleDefaults: { + scheme: ColorInterpolatorId +} = { + scheme: 'turbo', +} + +export const getSequentialColorScale = ( + { + scheme = sequentialColorScaleDefaults.scheme, + minValue, + maxValue, + }: SequentialColorScaleConfig, + values: SequentialColorScaleValues +) => { + const min = minValue !== undefined ? minValue : values.min + const max = maxValue !== undefined ? maxValue : values.max + + return scaleSequential().domain([min, max]).interpolator(colorInterpolators[scheme]) +} + +export const useSequentialColorScale = ( + config: SequentialColorScaleConfig, + values: SequentialColorScaleValues +) => useMemo(() => getSequentialColorScale(config, values), [config, values])