feat: add controls from other projects

This commit is contained in:
natoverse 2021-05-05 11:11:43 -07:00
Родитель 3802930b3a
Коммит b84f1ff024
16 изменённых файлов: 1072 добавлений и 0 удалений

Просмотреть файл

@ -0,0 +1,448 @@
/*!
* Copyright (c) Microsoft. All rights reserved.
* Licensed under the MIT license. See LICENSE file in the project.
*/
/* eslint-disable @typescript-eslint/no-var-requires */
import { Params, Scheme } from '../interfaces'
const chroma = require('chroma-js')
const hsluv = require('hsluv')
const lightTextLuminance = 95
const darkTextLuminance = 20
const lightScaleLuminance = 70
const darkScaleLuminance = 70
const maxSaturation = 100
const maxLightLuminanceOffset = 5
const maxDarkLuminanceOffset = 10
const maxBackgroundChroma = 4 // absolute
const lightScaleBackgroundLuminanceShift = 10
const darkScaleBackgroundLuminanceShift = 3
const analogousRange = 60
const complementaryRange = 60
const nominalSaturation = 90
const nominalLighterShift = 15
const nominalDarkerShift = 30
const boldSaturationShift = 10
const mutedSaturationShift = 55
const minNominalSaturation = 10
const minNominalLuminance = 50
const linearExponent = 1
const polynomialExponent = 1.5
const lowContrastBackgroundShift = 20
const darkestGrey = 20
const lightestGrey = 90
const offsetBackgroundLuminanceShift = 1
export interface NominalHueSequence {
bold: [number, number, number][]
std: [number, number, number][]
muted: [number, number, number][]
hues: number[]
size: number
}
export function polynomial_scale(
exp: number,
start: number,
end: number,
size: number,
): number[] {
const base = 1 / (size - 1)
const interval = end - start
const result: number[] = []
for (let i = 0; i < size; i++) {
result.push(start + interval * (i * base) ** exp)
}
return result
}
export type HSLVector = [number, number, number]
export function polynomial_hsl_scale(
exp: number,
[sh, ss, sl]: HSLVector,
[eh, es, el]: HSLVector,
size: number,
): HSLVector[] {
const hues = polynomial_scale(exp, sh, eh, size)
const saturations = polynomial_scale(exp, ss, es, size)
const luminances = polynomial_scale(exp, sl, el, size)
const hexvalues: HSLVector[] = []
for (let i = 0; i < size; i++) {
hexvalues.push([hues[i], saturations[i], luminances[i]])
}
return hexvalues
}
// Based on a maximum hue shift of 100 (from the slider)
function getOffsetHue([h, s, l]: HSLVector, hueShift: number) {
if (hueShift >= 25 && hueShift <= 75) {
// analogous range
const delta = (analogousRange * (hueShift - 50)) / 25
return (h + delta) % 360
} else if (hueShift > 75) {
// clockwise complementary range
const delta = 180 - (complementaryRange * (100 - hueShift)) / 25
return (h + delta) % 360
} else {
// anticlockwise complementary range
const delta = 180 + (complementaryRange * hueShift) / 25
return (h + delta) % 360
}
}
function getBackgroundSaturationaAndLuminance(
hue: number,
backgroundLevel: number,
light: boolean,
): [number, number] {
function normalizeSaturation(h: number, l: number) {
let satGivingMaxChroma = 100
let c = chroma(hsluv.hsluvToHex([hue, 100, l])).hcl()[1]
while (c > maxBackgroundChroma && satGivingMaxChroma >= 0) {
satGivingMaxChroma -= 1
c = chroma(hsluv.hsluvToHex([hue, satGivingMaxChroma, l])).hcl()[1]
}
return satGivingMaxChroma
}
if (light) {
if (backgroundLevel < 50) {
const l = 100 - maxLightLuminanceOffset
return [(backgroundLevel * normalizeSaturation(hue, l)) / 50, l]
} else {
const l =
100 - maxLightLuminanceOffset * (1 - (backgroundLevel - 50) / 50)
return [normalizeSaturation(hue, l), l]
}
} else {
if (backgroundLevel < 50) {
const l = maxDarkLuminanceOffset
return [(backgroundLevel * normalizeSaturation(hue, l)) / 50, l]
} else {
const l = maxDarkLuminanceOffset * (1 - (backgroundLevel - 50) / 50)
return [normalizeSaturation(hue, l), l]
}
}
}
export class ColorMaker {
public readonly textLuminance: number
public readonly scaleLuminance: number
public readonly backgroundHsl: [number, number, number]
public readonly offsetbackgroundHsl: [number, number, number]
public readonly foregroundHsl: [number, number, number]
public readonly faintAnnotationHsl: [number, number, number]
public readonly lowContrastAnnotationHsl: [number, number, number]
public readonly midContrastAnnotationHsl: [number, number, number]
public readonly highContrastAnnotationHsl: [number, number, number]
public readonly greyLuminance: number
private cachedNominalSequence: NominalHueSequence | undefined
public constructor(
public readonly accentHsl: [number, number, number],
public readonly backgroundLevel: number,
public readonly backgroundHueShift: number,
public readonly nominalHueStep: number,
public readonly light: boolean,
) {
this.textLuminance = light ? darkTextLuminance : lightTextLuminance
this.scaleLuminance = light ? darkScaleLuminance : lightScaleLuminance
const h = getOffsetHue(this.accentHsl, this.backgroundHueShift)
const [s, l] = getBackgroundSaturationaAndLuminance(
h,
this.backgroundLevel,
this.light,
)
this.backgroundHsl = [h, s, l]
const offsetBackgroundLuminance = light
? Math.min(100, l + offsetBackgroundLuminanceShift)
: Math.max(0, l - offsetBackgroundLuminanceShift)
this.offsetbackgroundHsl = [h, s, offsetBackgroundLuminance]
this.foregroundHsl = [0, 0, this.textLuminance]
this.greyLuminance = this.light
? this.backgroundHsl[2] - darkScaleBackgroundLuminanceShift
: this.backgroundHsl[2] + lightScaleBackgroundLuminanceShift
const mutedGreyLuminance = this.light
? this.backgroundHsl[2] - lowContrastBackgroundShift
: this.backgroundHsl[2] + lowContrastBackgroundShift
const boldGreyLuminance = this.light ? darkestGrey : lightestGrey
const greys = this.explicitGrey(3, mutedGreyLuminance, boldGreyLuminance)
this.lowContrastAnnotationHsl = greys[0]
this.midContrastAnnotationHsl = greys[1]
this.highContrastAnnotationHsl = greys[2]
// halfway to the background from the darkest/lightest
const faintLuminance = this.light
? (100 - lightestGrey) / 2 + lightestGrey
: darkestGrey / 2
this.faintAnnotationHsl = [this.accentHsl[0], 0, faintLuminance]
}
public grey(size: number): HSLVector[] {
const boldGreyLuminance = this.light ? darkestGrey : lightestGrey
return this.explicitGrey(size, this.greyLuminance, boldGreyLuminance)
}
public nominal(size: number): NominalHueSequence {
if (
!this.cachedNominalSequence ||
this.cachedNominalSequence.size !== size
) {
const {
nominalBold,
nominal,
nominalMuted,
nominalHues,
} = this.getNominalHueSequences(size)
this.cachedNominalSequence = {
bold: nominalBold,
std: nominal,
muted: nominalMuted,
hues: nominalHues,
size: nominal.length,
}
}
return this.cachedNominalSequence
}
private explicitGrey(
size: number,
startLuminance: number,
endLuminance: number,
) {
const greyFrom: HSLVector = [this.accentHsl[0], 0, startLuminance]
const greyTo: HSLVector = [this.accentHsl[0], 0, endLuminance]
return polynomial_hsl_scale(linearExponent, greyFrom, greyTo, size)
}
private getAccentsAndComplements(
size: number,
offset: number,
): {
greyAccent: HSLVector
greyComplement: HSLVector
maxSaturationAccent: HSLVector
maxSaturationComplement: HSLVector
} {
const { hues: nominalHues } = this.nominal(size)
const accentHue = nominalHues[offset % nominalHues.length]
const colorLuminance = this.accentHsl[2]
const greyLuminance = this.greyLuminance
const complementHue = (accentHue + 90) % 360
const greyAccent: HSLVector = [accentHue, 0, greyLuminance]
const greyComplement: HSLVector = [complementHue, 0, greyLuminance]
const maxSaturationAccent: HSLVector = [
accentHue,
maxSaturation,
colorLuminance,
]
const maxSaturationComplement: HSLVector = [
complementHue,
maxSaturation,
colorLuminance,
]
return {
greyAccent,
greyComplement,
maxSaturationAccent,
maxSaturationComplement,
}
}
public sequential(size: number, offset = 0): HSLVector[] {
const { greyAccent, maxSaturationAccent } = this.getAccentsAndComplements(
size,
offset,
)
return polynomial_hsl_scale(
linearExponent,
greyAccent,
maxSaturationAccent,
size,
)
}
public sequentialComplement(size: number, offset = 0): HSLVector[] {
const {
greyAccent,
maxSaturationComplement,
} = this.getAccentsAndComplements(size, offset)
return polynomial_hsl_scale(
linearExponent,
greyAccent,
maxSaturationComplement,
size,
)
}
public diverging(size: number, offset = 0): HSLVector[] {
const {
greyAccent,
greyComplement,
maxSaturationAccent,
maxSaturationComplement,
} = this.getAccentsAndComplements(size, offset)
const half = Math.round(size / 2)
const cut = size % 2 === 0 ? half + 1 : half
let diverging = polynomial_hsl_scale(
polynomialExponent,
greyComplement,
maxSaturationComplement,
cut,
)
.slice(1)
.reverse()
let toAdd = polynomial_hsl_scale(
polynomialExponent,
greyAccent,
maxSaturationAccent,
cut,
)
if (size % 2 === 0) {
toAdd = toAdd.slice(1, cut)
}
diverging = diverging.concat(toAdd)
return diverging
}
private getNominalHueSequences(size: number) {
const nominal: HSLVector[] = []
const nominalBold: HSLVector[] = []
const nominalMuted: HSLVector[] = []
const nominalHues: number[] = []
let hues: number[] = []
const h = this.accentHsl[0]
const initialHues =
this.nominalHueStep <= 10
? 13 - this.nominalHueStep
: this.nominalHueStep - 8
const hueDirection = this.nominalHueStep <= 10 ? -1 : 1
let hueStep = 360.0 / initialHues
for (let i = 0; i < initialHues; i += 1) {
hues.push((h + i * hueDirection * hueStep) % 360)
}
while (hues.length < size) {
hueStep = hueStep / 2
const newHues: number[] = []
for (const existingHue of hues) {
newHues.push((existingHue + hueDirection * hueStep) % 360)
if (hues.length + newHues.length === size) {
break
}
}
hues = hues.concat(newHues)
}
// trim the core hues to match requested size
// this acounts for initialHues calculation
// which can result in minimum counts higher than requested
hues = hues.slice(0, size)
let baseSaturation = nominalSaturation
let baseLuminance = this.scaleLuminance
const addHue = (hue: number) => {
nominal.push([hue, baseSaturation, baseLuminance])
nominalBold.push([
hue,
0.5 * (nominalSaturation + baseSaturation) + boldSaturationShift,
0.5 * (this.scaleLuminance + baseLuminance) - nominalDarkerShift,
])
nominalMuted.push([
hue,
0.5 * (nominalSaturation + baseSaturation) - mutedSaturationShift,
0.5 * (this.scaleLuminance + baseLuminance) + nominalLighterShift,
])
nominalHues.push(hue)
if (nominal.length % 3 === 0) {
baseSaturation = Math.max(baseSaturation - 1, minNominalSaturation)
}
if (nominal.length % 6 === 0) {
baseLuminance = Math.max(baseLuminance - 1, minNominalLuminance)
}
}
for (const finalHue of hues) {
addHue(finalHue)
}
return {
nominal,
nominalBold,
nominalMuted,
nominalHues,
}
}
}
export function hsluv_to_hex(values: HSLVector[]): string[] {
return values.map(c => hsluv.hsluvToHex(c))
}
// TODO: this should probably throw errors if out-of-bounds values are submitted OR, wrap around the geometry if that's always valid
export function getScheme(
params: Params,
nominalItemCount: number,
sequentialItemCount: number,
light: boolean,
): Scheme {
const {
accentHue,
accentSaturation,
accentLuminance,
backgroundLevel,
backgroundHueShift,
nominalHueStep,
} = params
const colorMaker = new ColorMaker(
[accentHue, accentSaturation, accentLuminance],
backgroundLevel,
backgroundHueShift,
nominalHueStep,
light,
)
const nominalSequences = colorMaker.nominal(nominalItemCount)
return {
background: hsluv.hsluvToHex(colorMaker.backgroundHsl),
offsetBackground: hsluv.hsluvToHex(colorMaker.offsetbackgroundHsl),
foreground: hsluv.hsluvToHex(colorMaker.foregroundHsl),
accent: hsluv.hsluvToHex(colorMaker.accentHsl),
lowContrastAnnotation: hsluv.hsluvToHex(
colorMaker.lowContrastAnnotationHsl,
),
midContrastAnnotation: hsluv.hsluvToHex(
colorMaker.midContrastAnnotationHsl,
),
highContrastAnnotation: hsluv.hsluvToHex(
colorMaker.highContrastAnnotationHsl,
),
faintAnnotation: hsluv.hsluvToHex(colorMaker.faintAnnotationHsl),
sequential: hsluv_to_hex(colorMaker.sequential(sequentialItemCount, 0)),
sequential2: hsluv_to_hex(colorMaker.sequential(sequentialItemCount, 1)),
diverging: hsluv_to_hex(colorMaker.diverging(sequentialItemCount, 0)),
diverging2: hsluv_to_hex(colorMaker.diverging(sequentialItemCount, 1)),
nominalBold: hsluv_to_hex(nominalSequences.bold),
nominal: hsluv_to_hex(nominalSequences.std),
nominalMuted: hsluv_to_hex(nominalSequences.muted),
greys: hsluv_to_hex(colorMaker.grey(sequentialItemCount)),
warning: '#ff8c00',
error: '#d13438',
}
}

Просмотреть файл

@ -0,0 +1,34 @@
/*!
* Copyright (c) Microsoft. All rights reserved.
* Licensed under the MIT license. See LICENSE file in the project.
*/
import { Color } from '../Color'
import { Scheme } from '../interfaces'
/**
* Extracts a thematic Color using its scheme "path".
* @param scheme
* @param path
*/
export function getNamedSchemeColor(scheme: Scheme, path?: string): Color {
if (!path || path === 'none') {
return new Color('none')
}
const indexed = indexedTest(path)
if (indexed) {
return new Color((scheme as any)[indexed.name][indexed.index])
}
return new Color((scheme as any)[path])
}
function indexedTest(path: string) {
const indexedName = path.match(/(\w+)\[/)
const indexedIndex = path.match(/\[(\d{1,2})\]/)
if (indexedName && indexedIndex) {
return {
name: indexedName[1],
index: +indexedIndex[1],
}
}
}

Просмотреть файл

@ -0,0 +1,5 @@
/*!
* Copyright (c) Microsoft. All rights reserved.
* Licensed under the MIT license. See LICENSE file in the project.
*/
export * from './HsluvColorLogic'

Просмотреть файл

@ -0,0 +1,7 @@
/*!
* Copyright (c) Microsoft. All rights reserved.
* Licensed under the MIT license. See LICENSE file in the project.
*/
export { getScheme } from './getScheme'
export * from './getNamedSchemeColor'
export * from './isNominal'

Просмотреть файл

@ -0,0 +1,7 @@
/*!
* Copyright (c) Microsoft. All rights reserved.
* Licensed under the MIT license. See LICENSE file in the project.
*/
export function isNominal(name?: string): boolean {
return name === 'nominal' || name === 'nominalBold' || name === 'nominalMuted'
}

Просмотреть файл

@ -0,0 +1,42 @@
/*!
* Copyright (c) Microsoft. All rights reserved.
* Licensed under the MIT license. See LICENSE file in the project.
*/
import { scaleLinear } from 'd3-scale'
import React, { useMemo } from 'react'
import { ChipsProps } from './types'
export const ColorChips: React.FC<ChipsProps> = ({
scale,
width = 200,
height = 10,
maxItems = 10,
}) => {
const blocks = useMemo(() => {
if (height <= 1) {
return []
}
const r = height / 2 - 1
const x = scaleLinear()
.domain([0, maxItems - 1])
.range([r, width - r])
return scale
.toArray(maxItems)
.map((color, i) => (
<circle
key={`color-chips-${color}-${i}`}
fill={color}
stroke={'none'}
cx={x(i)}
cy={r}
r={r}
height={height}
/>
))
}, [scale, width, height, maxItems])
return (
<svg width={width} height={height}>
{blocks}
</svg>
)
}

Просмотреть файл

@ -0,0 +1,32 @@
/*!
* Copyright (c) Microsoft. All rights reserved.
* Licensed under the MIT license. See LICENSE file in the project.
*/
import React, { useMemo } from 'react'
import { ChipsProps } from './types'
export const ContinuousBand: React.FC<ChipsProps> = ({
scale,
width,
height,
}) => {
const blocks = useMemo(() => {
return scale
.toArray(width)
.map((color, i) => (
<rect
key={`continuous-band-${color}-${i}`}
fill={color}
stroke={'none'}
x={i}
width={2}
height={height}
/>
))
}, [scale, width, height])
return (
<svg width={width} height={height}>
{blocks}
</svg>
)
}

Просмотреть файл

@ -0,0 +1,72 @@
/*!
* Copyright (c) Microsoft. All rights reserved.
* Licensed under the MIT license. See LICENSE file in the project.
*/
import { Dropdown } from '@fluentui/react'
import React, { useCallback, useRef } from 'react'
import styled from 'styled-components'
import { ScaleDropdownItem } from './ScaleDropdownItem'
import {
usePaletteWidth,
usePaletteHeight,
useItemStyle,
useThematicScaleOptions,
} from './hooks/theme'
import { useSafeDimensions } from './hooks/useSafeDimensions'
import { ScaleDropdownProps } from './types'
/**
* Represents a Fluent dropdown of Thematic scale options.
* The scale names can be accompanied by a visual rendering of the scale colors.
* This bascially extends Dropdown, overriding the options and item rendering.
* TODO: move to @thematic/fluent
*/
export const ScaleDropdown: React.FC<ScaleDropdownProps> = props => {
const ref = useRef(null)
const { width, height } = useSafeDimensions(ref)
const paletteWidth = usePaletteWidth(width)
const paletteHeight = usePaletteHeight(height, props.label)
const itemStyle = useItemStyle(width)
const options = useThematicScaleOptions()
const handleRenderTitle = useCallback(
([option]) => {
return (
<ScaleDropdownItem
paletteWidth={paletteWidth}
paletteHeight={paletteHeight}
option={option}
/>
)
},
[paletteWidth, paletteHeight],
)
const handleRenderOption = useCallback(
option => {
return (
<ScaleDropdownItem
key={`scale-dropdown-item-${option.key}`}
paletteWidth={paletteWidth}
paletteHeight={paletteHeight}
option={option}
style={itemStyle}
/>
)
},
[paletteWidth, paletteHeight, itemStyle],
)
return (
<Container ref={ref}>
<Dropdown
{...props}
options={options}
onRenderTitle={handleRenderTitle}
onRenderOption={handleRenderOption}
/>
</Container>
)
}
const Container = styled.div``

Просмотреть файл

@ -0,0 +1,39 @@
/*!
* Copyright (c) Microsoft. All rights reserved.
* Licensed under the MIT license. See LICENSE file in the project.
*/
import React from 'react'
import styled from 'styled-components'
import { useSafeCollapseDimensions } from './hooks/useSafeDimensions'
import { ScaleDropdownItemProps } from './types'
import { selectColorPalette, useScale } from './util'
export const ScaleDropdownItem: React.FC<ScaleDropdownItemProps> = ({
option,
paletteWidth,
paletteHeight,
style,
}) => {
const { key } = option
const Palette = selectColorPalette(key)
const [width, height] = useSafeCollapseDimensions(paletteWidth, paletteHeight)
const scale = useScale(option.key, paletteWidth)
return (
<Container style={style}>
<Label>{option.text}</Label>
<Palette scale={scale} width={width} height={height} />
</Container>
)
}
const Container = styled.div`
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
min-width: 10;
`
const Label = styled.div`
width: 74px;
`

Просмотреть файл

@ -0,0 +1,65 @@
/*!
* Copyright (c) Microsoft. All rights reserved.
* Licensed under the MIT license. See LICENSE file in the project.
*/
import { IDropdownOption } from '@fluentui/react'
import { useMemo } from 'react'
import { useThematic } from '@thematic/react'
const ITEM_LEFT_PADDING = 8 // defined default in fluent dropdown
const CARET_PADDING = 30 // defined default in fluent dropdown
const TEXT_WIDTH = 80 // TODO: adjust this based on font size/max measured
const LABEL_HEIGHT = 29 // defined default in fluent dropdown
// we may want this to be an optional prop once extracted
// visually we'll keep it lowercase in this app for visual consistency
const TITLE_CASE = false
export function usePaletteWidth(width: number): number {
// subtract space for the caret, left pad, and text
return width - CARET_PADDING - ITEM_LEFT_PADDING - TEXT_WIDTH
}
export function usePaletteHeight(height: number, label?: string): number {
// the measured component dimensions will include the label if present
const root = label ? height - LABEL_HEIGHT : height
return root / 2
}
/**
* This provides unique style overrides for the dropdown items,
* NOT the title. The paddings here are to align the item
* contents visually with the title, so it has a seamless
* appearance whether expanded or not.
*/
export function useItemStyle(width: number): React.CSSProperties {
return useMemo(
() => ({
width: width - CARET_PADDING,
paddingLeft: ITEM_LEFT_PADDING,
paddingRight: CARET_PADDING,
}),
[width],
)
}
// TODO: it would be helpful to provide a filter function so scales can be
// constrained to categorical or sequential when data is string versus numeric
export function useThematicScaleOptions(): IDropdownOption[] {
const theme = useThematic()
return useMemo(() => {
const keys = Object.keys(theme.scales())
return keys.map(key => {
// pretty print the scale names
// (1) uppercase first letter
// (2) use +/- for bold/muted
const text = key
.replace(/^(\w{1})/, c => (TITLE_CASE ? c.toLocaleUpperCase() : c))
.replace('Bold', '+')
.replace('Muted', '-')
return {
key,
text,
}
})
}, [theme])
}

Просмотреть файл

@ -0,0 +1,32 @@
/*!
* Copyright (c) Microsoft. All rights reserved.
* Licensed under the MIT license. See LICENSE file in the project.
*/
import { Dimensions, useDimensions } from '@essex-js-toolkit/hooks'
import React from 'react'
const DEFAULT_WIDTH = 200
const DEFAULT_HEIGHT = 32
export function useSafeDimensions(
ref: React.RefObject<HTMLDivElement>,
): Dimensions {
const dimensions = useDimensions(ref)
return (
dimensions || {
width: DEFAULT_WIDTH,
height: DEFAULT_HEIGHT,
}
)
}
/**
* Retrieve a non-zero width/height, because collapsible panels can force invalid color arrays
* @param width
* @param height
*/
export function useSafeCollapseDimensions(
width: number,
height: number,
): [number, number] {
return [width <= 0 ? 1 : width, height <= 0 ? 1 : height]
}

Просмотреть файл

@ -0,0 +1,5 @@
/*!
* Copyright (c) Microsoft. All rights reserved.
* Licensed under the MIT license. See LICENSE file in the project.
*/
export * from './ScaleDropdown'

Просмотреть файл

@ -0,0 +1,25 @@
/*!
* Copyright (c) Microsoft. All rights reserved.
* Licensed under the MIT license. See LICENSE file in the project.
*/
import { IDropdownProps } from '@fluentui/react'
import {
ContinuousColorScaleFunction,
NominalColorScaleFunction,
} from '@thematic/core'
export type ScaleDropdownProps = Omit<IDropdownProps, 'options'>
export interface ScaleDropdownItemProps {
option: any
paletteWidth: number
paletteHeight: number
style?: React.CSSProperties
}
export interface ChipsProps {
scale: ContinuousColorScaleFunction | NominalColorScaleFunction
width?: number
height?: number
maxItems?: number
}

Просмотреть файл

@ -0,0 +1,62 @@
/*!
* Copyright (c) Microsoft. All rights reserved.
* Licensed under the MIT license. See LICENSE file in the project.
*/
import { useMemo } from 'react'
import { ColorChips } from './ColorChips'
import { ContinuousBand } from './ContinuousBand'
import { ChipsProps } from './types'
import {
Theme,
NominalColorScaleFunction,
ContinuousColorScaleFunction,
} from '@thematic/core'
import { useThematic } from '@thematic/react'
export function useScale(
key: string,
width: number,
): NominalColorScaleFunction | ContinuousColorScaleFunction {
const theme = useThematic()
const scale = useMemo(() => chooseScale(theme, key, width), [
theme,
key,
width,
])
return scale
}
export function chooseScale(
theme: Theme,
key: string,
width: number,
): NominalColorScaleFunction | ContinuousColorScaleFunction {
const scales = theme.scales()
const domain = [0, width]
switch (key) {
case 'sequential':
return scales.sequential(domain)
case 'sequential2':
return scales.sequential2(domain)
case 'diverging':
return scales.diverging(domain)
case 'diverging2':
return scales.diverging2(domain)
case 'greys':
return scales.greys(domain)
case 'nominalMuted':
return scales.nominalMuted()
case 'nominalBold':
return scales.nominalBold()
case 'nominal':
default:
return scales.nominal()
}
}
export function selectColorPalette(key: string): React.FC<ChipsProps> {
if (key === 'nominal' || key === 'nominalMuted' || key === 'nominalBold') {
return ColorChips
}
return ContinuousBand
}

Просмотреть файл

@ -0,0 +1,82 @@
/*!
* Copyright (c) Microsoft. All rights reserved.
* Licensed under the MIT license. See LICENSE file in the project.
*/
import {
ChoiceGroup,
IChoiceGroupOption,
IChoiceGroupStyles,
} from '@fluentui/react'
import React, { useCallback, useMemo } from 'react'
import { ScaleType } from '@thematic/core'
interface ScaleTypeChoiceGroupProps {
selectedType: ScaleType
onChange?: (scaleType: ScaleType) => void
label?: string
suppressQuantile?: boolean
styles?: IChoiceGroupStyles
}
const CHOICE_STYLE = {
flexContainer: { display: 'flex', justifyContent: 'space-around' },
}
/**
* Represents a strongly typed ChoiceGroup for selecting thematic ScaleTypes.
*/
export const ScaleTypeChoiceGroup: React.FC<ScaleTypeChoiceGroupProps> = ({
selectedType,
onChange,
label,
suppressQuantile = false,
styles,
}) => {
const style = useMemo(
() => ({
...CHOICE_STYLE,
...styles,
}),
[styles],
)
const typeOptions = useTypeOptions(suppressQuantile)
const handleTypeChange = useCallback(
(_, option) => {
onChange && onChange(option.key)
},
[onChange],
)
return (
<ChoiceGroup
styles={style}
label={label}
options={typeOptions}
selectedKey={selectedType}
onChange={handleTypeChange}
/>
)
}
function useTypeOptions(suppressQuantile = false): IChoiceGroupOption[] {
return useMemo(() => {
const options = [
{
key: 'linear',
text: 'Linear',
},
{
key: 'log',
text: 'Log',
},
]
if (!suppressQuantile) {
options.push({
key: 'quantile',
text: 'Quantile',
})
}
return options
}, [suppressQuantile])
}

Просмотреть файл

@ -0,0 +1,115 @@
/*!
* Copyright (c) Microsoft. All rights reserved.
* Licensed under the MIT license. See LICENSE file in the project.
*/
import React, { useState, useCallback } from 'react'
import { connect } from 'react-redux'
import styled from 'styled-components'
import { themeLoaded } from '../../state/actions'
import { ScaleType, Theme } from '@thematic/core'
import {
ScaleDropdown,
ScaleTypeChoiceGroup,
ColorPicker,
ColorPickerButton,
} from '@thematic/fluent'
import { useThematic } from '@thematic/react'
interface FluentControlsComponentProps {
themeLoaded: (theme: Theme) => void
}
const FluentControlsComponent: React.FC<FluentControlsComponentProps> = ({
themeLoaded,
}) => {
const theme = useThematic()
const [scale, setScale] = useState<string>('<none>')
const handleScaleChange = useCallback((e, option) => setScale(option.key), [])
const [scaleType, setScaleType] = useState<ScaleType>(ScaleType.Linear)
const handleScaleTypeChange = useCallback(type => setScaleType(type), [])
const handlePickerChange = useCallback(t => themeLoaded(t), [themeLoaded])
return (
<Container>
<Description>
The @thematic/fluent package contains a few custom Fluent controls you
can use in your applications to allow Thematic-specific interactions.
</Description>
<Controls>
<Control>
<Description>
<Label>ScaleDropdown:</Label> a Dropdown that pre-loads Thematic
scale options.
</Description>
<ScaleDropdown
placeholder={'Choose scale'}
onChange={handleScaleChange}
/>
<Action> onChange: {scale}</Action>
</Control>
<Control>
<Description>
<Label>ScaleTypeChoiceGroup:</Label> a ChoiceGroup that pre-loads
Thematic scale types.
</Description>
<ScaleTypeChoiceGroup
selectedType={scaleType}
onChange={handleScaleTypeChange}
/>
<Action> onChange: {scaleType}</Action>
</Control>
<Control>
<Description>
<Label>ColorPicker:</Label> a ColorPicker that emits Thematic
parameters.
</Description>
<ColorPicker theme={theme} onChange={handlePickerChange} />
<Action> onChange: {theme.application().accent().hex()}</Action>
</Control>
<Control>
<Description>
<Label>ColorPickerButton:</Label> a DropdownButton that hosts a
Thematic ColorPicker.
</Description>
<ColorPickerButton theme={theme} onChange={handlePickerChange} />
<Action> onChange: {theme.application().accent().hex()}</Action>
</Control>
</Controls>
</Container>
)
}
export const FluentControls = connect(null, {
themeLoaded,
})(FluentControlsComponent)
const Container = styled.div`
font-size: 14px;
overflow-y: scroll;
`
const Description = styled.p``
const Controls = styled.div`
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-around;
`
const Control = styled.div`
width: 320px;
padding: 8px;
margin: 8px;
//border: 1px solid ${({ theme }) => theme.application().border().hex()};
`
const Label = styled.span`
font-weight: bold;
color: ${({ theme }) => theme.application().accent().hex()};
`
const Action = styled.p`
font-size: 12px;
font-family: monospace;
color: ${({ theme }) => theme.application().warning().hex()};
`