Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(colors): add support for continuous color scales
- Loading branch information
Showing
8 changed files
with
272 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
export * from './schemes' | ||
export * from './inheritedColor' | ||
export * from './motion' | ||
export * from './ordinalColorScale' | ||
export * from './props' | ||
export * from './scales' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)}`, | ||
})) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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]) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export * from './continuousColorScale' | ||
export * from './divergingColorScale' | ||
export * from './ordinalColorScale' | ||
export * from './quantizeColorScale' | ||
export * from './sequentialColorScale' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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]) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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]) |