Skip to content

Commit

Permalink
feat(colors): add support for continuous color scales
Browse files Browse the repository at this point in the history
  • Loading branch information
plouc committed Jan 12, 2022
1 parent 079770c commit f086791
Show file tree
Hide file tree
Showing 8 changed files with 272 additions and 4 deletions.
2 changes: 1 addition & 1 deletion 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'
2 changes: 0 additions & 2 deletions packages/colors/src/props.ts
Expand Up @@ -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,
Expand Down
112 changes: 112 additions & 0 deletions 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 extends ContinuousColorScaleConfig>(
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<string> | ScaleDiverging<string> | ScaleQuantize<string>,
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)}`,
}))
}
49 changes: 49 additions & 0 deletions 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])
5 changes: 5 additions & 0 deletions 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'
Expand Up @@ -7,7 +7,7 @@ import {
isCategoricalColorScheme,
isSequentialColorScheme,
isDivergingColorScheme,
} from './schemes'
} from '../schemes'

/**
* Static color.
Expand Down
64 changes: 64 additions & 0 deletions 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<QuantizeColorScaleSchemeConfig['steps']>
} = {
scheme: 'turbo',
steps: 7,
}

export const getQuantizeColorScale = (
config: QuantizeColorScaleConfig,
values: QuantizeColorScaleValues
) => {
const colorScale = scaleQuantize<string>()
.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])
40 changes: 40 additions & 0 deletions 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])

0 comments on commit f086791

Please sign in to comment.