Skip to content

Commit f086791

Browse files
committedJan 12, 2022
feat(colors): add support for continuous color scales
1 parent 079770c commit f086791

8 files changed

+272
-4
lines changed
 

‎packages/colors/src/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export * from './schemes'
22
export * from './inheritedColor'
33
export * from './motion'
4-
export * from './ordinalColorScale'
54
export * from './props'
5+
export * from './scales'

‎packages/colors/src/props.ts

-2
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ export const ordinalColorsPropType = PropTypes.oneOfType([
1414
PropTypes.string,
1515
])
1616

17-
export const colorPropertyAccessorPropType = PropTypes.oneOfType([PropTypes.func, PropTypes.string])
18-
1917
export const inheritedColorPropType = PropTypes.oneOfType([
2018
PropTypes.string,
2119
PropTypes.func,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { useMemo } from 'react'
2+
import { ScaleDiverging, ScaleQuantize, ScaleSequential, scaleLinear } from 'd3-scale'
3+
import {
4+
SequentialColorScaleConfig,
5+
SequentialColorScaleValues,
6+
getSequentialColorScale,
7+
} from './sequentialColorScale'
8+
import {
9+
DivergingColorScaleConfig,
10+
DivergingColorScaleValues,
11+
getDivergingColorScale,
12+
} from './divergingColorScale'
13+
import {
14+
QuantizeColorScaleConfig,
15+
QuantizeColorScaleValues,
16+
getQuantizeColorScale,
17+
} from './quantizeColorScale'
18+
19+
export type ContinuousColorScaleConfig =
20+
| SequentialColorScaleConfig
21+
| DivergingColorScaleConfig
22+
| QuantizeColorScaleConfig
23+
24+
export type ContinuousColorScaleValues =
25+
| SequentialColorScaleValues
26+
| DivergingColorScaleValues
27+
| QuantizeColorScaleValues
28+
29+
const isSequentialColorScaleConfig = (
30+
config: ContinuousColorScaleConfig
31+
): config is SequentialColorScaleConfig => config.type === 'sequential'
32+
33+
const isDivergingColorScaleConfig = (
34+
config: ContinuousColorScaleConfig
35+
): config is DivergingColorScaleConfig => config.type === 'diverging'
36+
37+
const isQuantizeColorScaleConfig = (
38+
config: ContinuousColorScaleConfig
39+
): config is QuantizeColorScaleConfig => config.type === 'quantize'
40+
41+
export const getContinuousColorScale = <Config extends ContinuousColorScaleConfig>(
42+
config: Config,
43+
values: ContinuousColorScaleValues
44+
) => {
45+
if (isSequentialColorScaleConfig(config)) {
46+
return getSequentialColorScale(config, values)
47+
}
48+
49+
if (isDivergingColorScaleConfig(config)) {
50+
return getDivergingColorScale(config, values)
51+
}
52+
53+
if (isQuantizeColorScaleConfig(config)) {
54+
return getQuantizeColorScale(config, values)
55+
}
56+
57+
throw new Error('Invalid continuous color scale config')
58+
}
59+
60+
export const useContinuousColorScale = (
61+
config: ContinuousColorScaleConfig,
62+
values: ContinuousColorScaleValues
63+
) => useMemo(() => getContinuousColorScale(config, values), [config, values])
64+
65+
export const computeContinuousColorScaleColorStops = (
66+
scale: ScaleSequential<string> | ScaleDiverging<string> | ScaleQuantize<string>,
67+
steps = 16
68+
) => {
69+
const domain = scale.domain()
70+
71+
// quantize
72+
if ('thresholds' in scale) {
73+
const stops: {
74+
key: string
75+
offset: string
76+
stopColor: string
77+
}[] = []
78+
79+
const normalizedScale = scaleLinear().domain(domain).range([0, 1])
80+
scale.range().forEach((color, index) => {
81+
const [start, end] = scale.invertExtent(color)
82+
83+
stops.push({
84+
key: `${index}.0`,
85+
offset: `${Math.round(normalizedScale(start) * 100)}%`,
86+
stopColor: color,
87+
})
88+
stops.push({
89+
key: `${index}.1`,
90+
offset: `${Math.round(normalizedScale(end) * 100)}%`,
91+
stopColor: color,
92+
})
93+
})
94+
95+
return stops
96+
}
97+
98+
const colorStopsScale = scale.copy()
99+
if (domain.length === 2) {
100+
// sequential
101+
colorStopsScale.domain([0, 1])
102+
} else if (domain.length === 3) {
103+
// diverging
104+
colorStopsScale.domain([0, 0.5, 1])
105+
}
106+
107+
return ((colorStopsScale as any).ticks(steps) as number[]).map((value: number) => ({
108+
key: `${value}`,
109+
offset: `${Math.round(value * 100)}%`,
110+
stopColor: `${colorStopsScale(value)}`,
111+
}))
112+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { useMemo } from 'react'
2+
import { scaleDiverging } from 'd3-scale'
3+
import { colorInterpolators, ColorInterpolatorId } from '../schemes'
4+
5+
export interface DivergingColorScaleConfig {
6+
type: 'diverging'
7+
scheme?: ColorInterpolatorId
8+
minValue?: number
9+
maxValue?: number
10+
divergeAt?: number
11+
}
12+
13+
export interface DivergingColorScaleValues {
14+
min: number
15+
max: number
16+
}
17+
18+
export const divergingColorScaleDefaults: {
19+
scheme: ColorInterpolatorId
20+
divergeAt: number
21+
} = {
22+
scheme: 'red_yellow_blue',
23+
divergeAt: 0.5,
24+
}
25+
26+
export const getDivergingColorScale = (
27+
{
28+
scheme = divergingColorScaleDefaults.scheme,
29+
divergeAt = divergingColorScaleDefaults.divergeAt,
30+
minValue,
31+
maxValue,
32+
}: DivergingColorScaleConfig,
33+
values: DivergingColorScaleValues
34+
) => {
35+
const min = minValue !== undefined ? minValue : values.min
36+
const max = maxValue !== undefined ? maxValue : values.max
37+
const domain = [min, min + (max - min) / 2, max]
38+
39+
const interpolator = colorInterpolators[scheme]
40+
const offset = 0.5 - divergeAt
41+
const offsetInterpolator = (t: number) => interpolator(t + offset)
42+
43+
return scaleDiverging(offsetInterpolator).domain(domain).clamp(true)
44+
}
45+
46+
export const useDivergingColorScale = (
47+
config: DivergingColorScaleConfig,
48+
values: DivergingColorScaleValues
49+
) => useMemo(() => getDivergingColorScale(config, values), [config, values])

‎packages/colors/src/scales/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export * from './continuousColorScale'
2+
export * from './divergingColorScale'
3+
export * from './ordinalColorScale'
4+
export * from './quantizeColorScale'
5+
export * from './sequentialColorScale'

‎packages/colors/src/ordinalColorScale.ts ‎packages/colors/src/scales/ordinalColorScale.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
isCategoricalColorScheme,
88
isSequentialColorScheme,
99
isDivergingColorScheme,
10-
} from './schemes'
10+
} from '../schemes'
1111

1212
/**
1313
* Static color.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { useMemo } from 'react'
2+
import { scaleQuantize } from 'd3-scale'
3+
import { colorInterpolators, ColorInterpolatorId } from '../schemes'
4+
5+
// colors from a scheme
6+
export interface QuantizeColorScaleSchemeConfig {
7+
type: 'quantize'
8+
domain?: [number, number]
9+
scheme?: ColorInterpolatorId
10+
steps?: number
11+
}
12+
13+
// explicit colors
14+
export interface QuantizeColorScaleColorsConfig {
15+
type: 'quantize'
16+
domain?: [number, number]
17+
colors: string[]
18+
}
19+
20+
export type QuantizeColorScaleConfig =
21+
| QuantizeColorScaleSchemeConfig
22+
| QuantizeColorScaleColorsConfig
23+
24+
export interface QuantizeColorScaleValues {
25+
min: number
26+
max: number
27+
}
28+
29+
export const quantizeColorScaleDefaults: {
30+
scheme: ColorInterpolatorId
31+
steps: NonNullable<QuantizeColorScaleSchemeConfig['steps']>
32+
} = {
33+
scheme: 'turbo',
34+
steps: 7,
35+
}
36+
37+
export const getQuantizeColorScale = (
38+
config: QuantizeColorScaleConfig,
39+
values: QuantizeColorScaleValues
40+
) => {
41+
const colorScale = scaleQuantize<string>()
42+
.domain(config.domain || [values.min, values.max])
43+
.nice()
44+
45+
if ('colors' in config) {
46+
colorScale.range(config.colors)
47+
} else {
48+
const scheme = config.scheme || quantizeColorScaleDefaults.scheme
49+
const steps = config.steps === undefined ? quantizeColorScaleDefaults.steps : config.steps
50+
const interpolator = colorInterpolators[scheme]
51+
const colors = Array.from({ length: steps }).map((_, step) =>
52+
interpolator(step * (1 / (steps - 1)))
53+
)
54+
55+
colorScale.range(colors)
56+
}
57+
58+
return colorScale
59+
}
60+
61+
export const useQuantizeColorScale = (
62+
config: QuantizeColorScaleConfig,
63+
values: QuantizeColorScaleValues
64+
) => useMemo(() => getQuantizeColorScale(config, values), [config, values])
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { useMemo } from 'react'
2+
import { scaleSequential } from 'd3-scale'
3+
import { colorInterpolators, ColorInterpolatorId } from '../schemes'
4+
5+
export interface SequentialColorScaleConfig {
6+
type: 'sequential'
7+
scheme?: ColorInterpolatorId
8+
minValue?: number
9+
maxValue?: number
10+
}
11+
12+
export interface SequentialColorScaleValues {
13+
min: number
14+
max: number
15+
}
16+
17+
export const sequentialColorScaleDefaults: {
18+
scheme: ColorInterpolatorId
19+
} = {
20+
scheme: 'turbo',
21+
}
22+
23+
export const getSequentialColorScale = (
24+
{
25+
scheme = sequentialColorScaleDefaults.scheme,
26+
minValue,
27+
maxValue,
28+
}: SequentialColorScaleConfig,
29+
values: SequentialColorScaleValues
30+
) => {
31+
const min = minValue !== undefined ? minValue : values.min
32+
const max = maxValue !== undefined ? maxValue : values.max
33+
34+
return scaleSequential().domain([min, max]).interpolator(colorInterpolators[scheme])
35+
}
36+
37+
export const useSequentialColorScale = (
38+
config: SequentialColorScaleConfig,
39+
values: SequentialColorScaleValues
40+
) => useMemo(() => getSequentialColorScale(config, values), [config, values])

0 commit comments

Comments
 (0)
Please sign in to comment.