diff --git a/README.md b/README.md index 88196af0c..e2eb9678b 100644 --- a/README.md +++ b/README.md @@ -103,12 +103,14 @@ const y = d => +d.frequency * 100; // And then scale the graph by our data const xScale = scaleBand({ - rangeRound: [0, xMax], + range: [0, xMax], + round: true, domain: data.map(x), padding: 0.4, }); const yScale = scaleLinear({ - rangeRound: [yMax, 0], + range: [yMax, 0], + round: true, domain: [0, Math.max(...data.map(y))], }); diff --git a/package.json b/package.json index 668268fdd..9e173c299 100644 --- a/package.json +++ b/package.json @@ -53,13 +53,15 @@ "enzyme-to-json": "^3.4.0", "fs-jetpack": "^1.3.0", "husky": "^3.0.0", + "jest-mock-console": "^1.0.1", "lerna": "^3.15.0", "marked": "^0.7.0", "raf": "^3.4.0", "react": "^15.0.0-0 || ^16.0.0-0", "react-dom": "^15.0.0-0 || ^16.0.0-0", "react-test-renderer": "^16.8.6", - "regenerator-runtime": "^0.10.5" + "regenerator-runtime": "^0.10.5", + "timezone-mock": "^1.1.0" }, "workspaces": [ "./packages/*" diff --git a/packages/vx-axis/test/Axis.test.tsx b/packages/vx-axis/test/Axis.test.tsx index fc96501f3..40f49b5ca 100644 --- a/packages/vx-axis/test/Axis.test.tsx +++ b/packages/vx-axis/test/Axis.test.tsx @@ -10,7 +10,8 @@ import { GenericScale } from '../src/types'; const axisProps = { orientation: 'left' as const, scale: scaleLinear({ - rangeRound: [10, 0], + range: [10, 0], + round: true, domain: [0, 10], }) as GenericScale, label: 'test axis', @@ -199,7 +200,8 @@ describe('', () => { const overrideAxisProps = { orientation: 'bottom' as const, scale: scaleBand({ - rangeRound: [10, 0], + range: [10, 0], + round: true, domain: ['a', 'b'], }), }; diff --git a/packages/vx-axis/test/AxisBottom.test.tsx b/packages/vx-axis/test/AxisBottom.test.tsx index e5c8066d3..1a4752716 100644 --- a/packages/vx-axis/test/AxisBottom.test.tsx +++ b/packages/vx-axis/test/AxisBottom.test.tsx @@ -7,7 +7,8 @@ import { GenericScale } from '../src/types'; const axisProps = { scale: scaleLinear({ - rangeRound: [10, 0], + range: [10, 0], + round: true, domain: [0, 10], }) as GenericScale, }; diff --git a/packages/vx-axis/test/AxisLeft.test.tsx b/packages/vx-axis/test/AxisLeft.test.tsx index f9256a954..e943d3f97 100644 --- a/packages/vx-axis/test/AxisLeft.test.tsx +++ b/packages/vx-axis/test/AxisLeft.test.tsx @@ -7,7 +7,8 @@ import { GenericScale } from '../src/types'; const axisProps = { scale: scaleLinear({ - rangeRound: [10, 0], + range: [10, 0], + round: true, domain: [0, 10], }) as GenericScale, }; diff --git a/packages/vx-axis/test/AxisRight.test.tsx b/packages/vx-axis/test/AxisRight.test.tsx index 07b04aa7f..50dc26d75 100644 --- a/packages/vx-axis/test/AxisRight.test.tsx +++ b/packages/vx-axis/test/AxisRight.test.tsx @@ -7,7 +7,8 @@ import { GenericScale } from '../src/types'; const axisProps = { scale: scaleLinear({ - rangeRound: [10, 0], + range: [10, 0], + round: true, domain: [0, 10], }) as GenericScale, }; diff --git a/packages/vx-axis/test/AxisTop.test.tsx b/packages/vx-axis/test/AxisTop.test.tsx index b26285cf0..74a6b4698 100644 --- a/packages/vx-axis/test/AxisTop.test.tsx +++ b/packages/vx-axis/test/AxisTop.test.tsx @@ -7,7 +7,8 @@ import { GenericScale } from '../src/types'; const axisProps = { scale: scaleLinear({ - rangeRound: [10, 0], + range: [10, 0], + round: true, domain: [0, 10], }) as GenericScale, }; diff --git a/packages/vx-axis/test/scales.test.tsx b/packages/vx-axis/test/scales.test.tsx index 0ce6ba82e..979d95c9b 100644 --- a/packages/vx-axis/test/scales.test.tsx +++ b/packages/vx-axis/test/scales.test.tsx @@ -31,7 +31,8 @@ describe('Axis scales', () => { expect( setup( scaleBand({ - rangeRound: [10, 0], + range: [10, 0], + round: true, domain: ['a', 'b', 'c'], }) as GenericScale, ), @@ -42,7 +43,8 @@ describe('Axis scales', () => { expect( setup( scaleLinear({ - rangeRound: [10, 0], + range: [10, 0], + round: true, domain: [0, 10], }) as GenericScale, ), @@ -53,7 +55,8 @@ describe('Axis scales', () => { expect( setup( scaleLog({ - rangeRound: [10, 0], + range: [10, 0], + round: true, domain: [1, 10, 100, 1000], }) as GenericScale, ), @@ -75,7 +78,8 @@ describe('Axis scales', () => { expect( setup( scalePoint({ - rangeRound: [0, 10], + range: [0, 10], + round: true, domain: ['a', 'b', 'c'], }) as GenericScale, ), diff --git a/packages/vx-demo/src/sandboxes/vx-bars/Example.tsx b/packages/vx-demo/src/sandboxes/vx-bars/Example.tsx index 829449e7f..239ead683 100644 --- a/packages/vx-demo/src/sandboxes/vx-bars/Example.tsx +++ b/packages/vx-demo/src/sandboxes/vx-bars/Example.tsx @@ -27,7 +27,8 @@ export default function Example({ width, height, events = false }: BarsProps) { const xScale = useMemo( () => scaleBand({ - rangeRound: [0, xMax], + range: [0, xMax], + round: true, domain: data.map(getLetter), padding: 0.4, }), @@ -36,7 +37,8 @@ export default function Example({ width, height, events = false }: BarsProps) { const yScale = useMemo( () => scaleLinear({ - rangeRound: [yMax, 0], + range: [yMax, 0], + round: true, domain: [0, Math.max(...data.map(getLetterFrequency))], }), [yMax], diff --git a/packages/vx-demo/src/sandboxes/vx-stats/Example.tsx b/packages/vx-demo/src/sandboxes/vx-stats/Example.tsx index 3eac9d72a..aba10eea7 100644 --- a/packages/vx-demo/src/sandboxes/vx-stats/Example.tsx +++ b/packages/vx-demo/src/sandboxes/vx-stats/Example.tsx @@ -50,7 +50,8 @@ export default withTooltip( // scales const xScale = scaleBand({ - rangeRound: [0, xMax], + range: [0, xMax], + round: true, domain: data.map(x), padding: 0.4, }); @@ -63,7 +64,8 @@ export default withTooltip( const maxYValue = Math.max(...values); const yScale = scaleLinear({ - rangeRound: [yMax, 0], + range: [yMax, 0], + round: true, domain: [minYValue, maxYValue], }); diff --git a/packages/vx-legend/test/Legend.test.tsx b/packages/vx-legend/test/Legend.test.tsx index d47391ea2..8b4dd2211 100644 --- a/packages/vx-legend/test/Legend.test.tsx +++ b/packages/vx-legend/test/Legend.test.tsx @@ -6,7 +6,8 @@ import { Legend, LegendLabel } from '../src'; const defaultProps = { scale: scaleLinear({ - rangeRound: [10, 0], + range: [10, 0], + round: true, domain: [0, 10], }), }; diff --git a/packages/vx-scale/Readme.md b/packages/vx-scale/Readme.md index e3db38533..c1dbfe58c 100644 --- a/packages/vx-scale/Readme.md +++ b/packages/vx-scale/Readme.md @@ -33,14 +33,16 @@ const [minY, maxY] = getYMinAndMax(); const xScale = Scale.scaleLinear({ domain: [minX, maxX], // x-coordinate data values - rangeRound: [0, graphWidth], // svg x-coordinates, svg x-coordinates increase left to right + range: [0, graphWidth], // svg x-coordinates, svg x-coordinates increase left to right + round: true, }); const yScale = Scale.scaleLinear({ domain: [minY, maxY], // y-coordinate data values // svg y-coordinates, these increase from top to bottom so we reverse the order // so that minY in data space maps to graphHeight in svg y-coordinate space - rangeRound: [graphHeight, 0], + range: [graphHeight, 0], + round: true, }); // ... @@ -63,7 +65,7 @@ Example: const scale = Scale.scaleBand({ /* range, - rangeRound, + round, domain, padding, nice = false @@ -81,7 +83,7 @@ Example: const scale = Scale.scaleLinear({ /* range, - rangeRound, + round, domain, nice = false, clamp = false, @@ -99,7 +101,7 @@ Example: const scale = Scale.scaleLog({ /* range, - rangeRound, + round, domain, base, nice = false, @@ -134,7 +136,7 @@ Example: const scale = Scale.scalePoint({ /* range, - rangeRound, + round, domain, padding, align, @@ -153,7 +155,7 @@ Example: const scale = Scale.scalePower({ /* range, - rangeRound, + round, domain, exponent, nice = false, @@ -173,7 +175,7 @@ Example: const scale = Scale.scaleSqrt({ /* range, - rangeRound, + round, domain, nice = false, clamp = false, @@ -191,7 +193,7 @@ Example: const scale = Scale.scaleTime({ /* range, - rangeRound, + round, domain, nice = false, clamp = false, @@ -207,7 +209,7 @@ Example: const scale = Scale.scaleUtc({ /* range, - rangeRound, + round, domain, nice = false, clamp = false, diff --git a/packages/vx-scale/package.json b/packages/vx-scale/package.json index 24268ff86..139abfb3c 100644 --- a/packages/vx-scale/package.json +++ b/packages/vx-scale/package.json @@ -21,15 +21,19 @@ "visualizations", "charts" ], - "author": "@hshoff", + "authors": ["@hshoff", "@kristw"], "license": "MIT", "bugs": { "url": "https://github.com/hshoff/vx/issues" }, "homepage": "https://github.com/hshoff/vx#readme", "dependencies": { + "@types/d3-interpolate": "^1.3.1", "@types/d3-scale": "^2.1.1", - "d3-scale": "^2.2.2" + "@types/d3-time": "^1.0.10", + "d3-interpolate": "^1.4.0", + "d3-scale": "^3.0.1", + "d3-time": "^1.1.0" }, "publishConfig": { "access": "public" diff --git a/packages/vx-scale/src/createScale.ts b/packages/vx-scale/src/createScale.ts new file mode 100644 index 000000000..1713ddb22 --- /dev/null +++ b/packages/vx-scale/src/createScale.ts @@ -0,0 +1,157 @@ +import { ScaleConfig, PickScaleConfigWithoutType, PickScaleConfig } from './types/ScaleConfig'; +import { DefaultThresholdInput, PickD3Scale } from './types/Scale'; +import { StringLike, DefaultOutput } from './types/Base'; +import createLinearScale from './scales/linear'; +import createLogScale from './scales/log'; +import createPowScale from './scales/power'; +import createSqrtScale from './scales/squareRoot'; +import createSymlogScale from './scales/symlog'; +import createTimeScale from './scales/time'; +import createUtcScale from './scales/utc'; +import createQuantileScale from './scales/quantile'; +import createQuantizeScale from './scales/quantize'; +import createThresholdScale from './scales/threshold'; +import createOrdinalScale from './scales/ordinal'; +import createPointScale from './scales/point'; +import createBandScale from './scales/band'; + +// Overload function for more strict typing, e.g., +// If the config is a linear config then a ScaleLinear will be returned +// instead of a union type of all scales. + +function createScale< + Output = DefaultOutput, + DiscreteInput extends StringLike = StringLike, + ThresholdInput extends DefaultThresholdInput = DefaultThresholdInput +>( + config?: PickScaleConfig<'linear', Output> | PickScaleConfigWithoutType<'linear', Output>, +): PickD3Scale<'linear', Output>; + +function createScale< + Output = DefaultOutput, + DiscreteInput extends StringLike = StringLike, + ThresholdInput extends DefaultThresholdInput = DefaultThresholdInput +>(config: PickScaleConfig<'log', Output>): PickD3Scale<'log', Output>; + +function createScale< + Output = DefaultOutput, + DiscreteInput extends StringLike = StringLike, + ThresholdInput extends DefaultThresholdInput = DefaultThresholdInput +>(config: PickScaleConfig<'pow', Output>): PickD3Scale<'pow', Output>; + +function createScale< + Output = DefaultOutput, + DiscreteInput extends StringLike = StringLike, + ThresholdInput extends DefaultThresholdInput = DefaultThresholdInput +>(config: PickScaleConfig<'sqrt', Output>): PickD3Scale<'sqrt', Output>; + +function createScale< + Output = DefaultOutput, + DiscreteInput extends StringLike = StringLike, + ThresholdInput extends DefaultThresholdInput = DefaultThresholdInput +>(config: PickScaleConfig<'symlog', Output>): PickD3Scale<'symlog', Output>; + +function createScale< + Output = DefaultOutput, + DiscreteInput extends StringLike = StringLike, + ThresholdInput extends DefaultThresholdInput = DefaultThresholdInput +>(config: PickScaleConfig<'time', Output>): PickD3Scale<'time', Output>; + +function createScale< + Output = DefaultOutput, + DiscreteInput extends StringLike = StringLike, + ThresholdInput extends DefaultThresholdInput = DefaultThresholdInput +>(config: PickScaleConfig<'utc', Output>): PickD3Scale<'utc', Output>; + +function createScale< + Output = DefaultOutput, + DiscreteInput extends StringLike = StringLike, + ThresholdInput extends DefaultThresholdInput = DefaultThresholdInput +>(config: PickScaleConfig<'quantile', Output>): PickD3Scale<'quantile', Output>; + +function createScale< + Output = DefaultOutput, + DiscreteInput extends StringLike = StringLike, + ThresholdInput extends DefaultThresholdInput = DefaultThresholdInput +>(config: PickScaleConfig<'quantize', Output>): PickD3Scale<'quantize', Output>; + +function createScale< + Output = DefaultOutput, + DiscreteInput extends StringLike = StringLike, + ThresholdInput extends DefaultThresholdInput = DefaultThresholdInput +>( + config: PickScaleConfig<'threshold', Output, StringLike, ThresholdInput>, +): PickD3Scale<'threshold', Output, StringLike, ThresholdInput>; + +function createScale< + Output = DefaultOutput, + DiscreteInput extends StringLike = StringLike, + ThresholdInput extends DefaultThresholdInput = DefaultThresholdInput +>( + config: PickScaleConfig<'ordinal', Output, DiscreteInput>, +): PickD3Scale<'ordinal', Output, DiscreteInput>; + +function createScale< + Output = DefaultOutput, + DiscreteInput extends StringLike = StringLike, + ThresholdInput extends DefaultThresholdInput = DefaultThresholdInput +>( + config: PickScaleConfig<'point', Output, DiscreteInput>, +): PickD3Scale<'point', Output, DiscreteInput>; + +function createScale< + Output = DefaultOutput, + DiscreteInput extends StringLike = StringLike, + ThresholdInput extends DefaultThresholdInput = DefaultThresholdInput +>( + config: PickScaleConfig<'band', Output, DiscreteInput>, +): PickD3Scale<'band', Output, DiscreteInput>; + +// Actual implementation + +function createScale< + Output, + DiscreteInput extends StringLike, + ThresholdInput extends DefaultThresholdInput +>( + config?: + | ScaleConfig + | PickScaleConfigWithoutType<'linear', Output>, +) { + if (typeof config !== 'undefined' && 'type' in config) { + switch (config.type) { + case 'linear': + return createLinearScale(config); + case 'log': + return createLogScale(config); + case 'pow': + return createPowScale(config); + case 'sqrt': + return createSqrtScale(config); + case 'symlog': + return createSymlogScale(config); + case 'time': + return createTimeScale(config); + case 'utc': + return createUtcScale(config); + case 'quantile': + return createQuantileScale(config); + case 'quantize': + return createQuantizeScale(config); + case 'threshold': + return createThresholdScale(config); + case 'ordinal': + return createOrdinalScale(config); + case 'point': + return createPointScale(config); + case 'band': + return createBandScale(config); + default: + } + } + + // If type is not specified, fallback to linear scale + return createLinearScale(config); +} + +export default createScale; diff --git a/packages/vx-scale/src/index.ts b/packages/vx-scale/src/index.ts index d3b4fe895..aa0a70da5 100644 --- a/packages/vx-scale/src/index.ts +++ b/packages/vx-scale/src/index.ts @@ -10,5 +10,15 @@ export { default as scaleQuantize } from './scales/quantize'; export { default as scaleQuantile } from './scales/quantile'; export { default as scaleSymlog } from './scales/symlog'; export { default as scaleThreshold } from './scales/threshold'; -export { default as updateScale } from './util/updateScale'; export { default as scaleSqrt } from './scales/squareRoot'; + +export { default as createScale } from './createScale'; +export { default as updateScale } from './updateScale'; +export { default as inferScaleType } from './utils/inferScaleType'; + +// export types +export * from './types/Base'; +export * from './types/Nice'; +export * from './types/Scale'; +export * from './types/ScaleConfig'; +export * from './types/ScaleInterpolate'; diff --git a/packages/vx-scale/src/operators/align.ts b/packages/vx-scale/src/operators/align.ts new file mode 100644 index 000000000..2d810c9de --- /dev/null +++ b/packages/vx-scale/src/operators/align.ts @@ -0,0 +1,16 @@ +import { DefaultThresholdInput, D3Scale } from '../types/Scale'; +import { StringLike } from '../types/Base'; +import { ScaleConfigWithoutType } from '../types/ScaleConfig'; + +export default function applyAlign< + Output, + DiscreteInput extends StringLike, + ThresholdInput extends DefaultThresholdInput +>( + scale: D3Scale, + config: ScaleConfigWithoutType, +) { + if ('align' in scale && 'align' in config && typeof config.align !== 'undefined') { + scale.align(config.align); + } +} diff --git a/packages/vx-scale/src/operators/base.ts b/packages/vx-scale/src/operators/base.ts new file mode 100644 index 000000000..c0507356c --- /dev/null +++ b/packages/vx-scale/src/operators/base.ts @@ -0,0 +1,16 @@ +import { DefaultThresholdInput, D3Scale } from '../types/Scale'; +import { StringLike } from '../types/Base'; +import { ScaleConfigWithoutType } from '../types/ScaleConfig'; + +export default function applyBase< + Output, + DiscreteInput extends StringLike, + ThresholdInput extends DefaultThresholdInput +>( + scale: D3Scale, + config: ScaleConfigWithoutType, +) { + if ('base' in scale && 'base' in config && typeof config.base !== 'undefined') { + scale.base(config.base); + } +} diff --git a/packages/vx-scale/src/operators/clamp.ts b/packages/vx-scale/src/operators/clamp.ts new file mode 100644 index 000000000..3b738c8fb --- /dev/null +++ b/packages/vx-scale/src/operators/clamp.ts @@ -0,0 +1,16 @@ +import { DefaultThresholdInput, D3Scale } from '../types/Scale'; +import { StringLike } from '../types/Base'; +import { ScaleConfigWithoutType } from '../types/ScaleConfig'; + +export default function applyClamp< + Output, + DiscreteInput extends StringLike, + ThresholdInput extends DefaultThresholdInput +>( + scale: D3Scale, + config: ScaleConfigWithoutType, +) { + if ('clamp' in scale && 'clamp' in config && typeof config.clamp !== 'undefined') { + scale.clamp(config.clamp); + } +} diff --git a/packages/vx-scale/src/operators/constant.ts b/packages/vx-scale/src/operators/constant.ts new file mode 100644 index 000000000..7a730cac3 --- /dev/null +++ b/packages/vx-scale/src/operators/constant.ts @@ -0,0 +1,16 @@ +import { DefaultThresholdInput, D3Scale } from '../types/Scale'; +import { StringLike } from '../types/Base'; +import { ScaleConfigWithoutType } from '../types/ScaleConfig'; + +export default function applyConstant< + Output, + DiscreteInput extends StringLike, + ThresholdInput extends DefaultThresholdInput +>( + scale: D3Scale, + config: ScaleConfigWithoutType, +) { + if ('constant' in scale && 'constant' in config && typeof config.constant !== 'undefined') { + scale.constant(config.constant); + } +} diff --git a/packages/vx-scale/src/operators/domain.ts b/packages/vx-scale/src/operators/domain.ts new file mode 100644 index 000000000..78d5a6b47 --- /dev/null +++ b/packages/vx-scale/src/operators/domain.ts @@ -0,0 +1,25 @@ +import { DefaultThresholdInput, D3Scale } from '../types/Scale'; +import { StringLike } from '../types/Base'; +import { ScaleConfigWithoutType } from '../types/ScaleConfig'; + +export default function applyDomain< + Output, + DiscreteInput extends StringLike, + ThresholdInput extends DefaultThresholdInput +>( + scale: D3Scale, + config: ScaleConfigWithoutType, +) { + if (config.domain) { + if ('nice' in scale || 'quantiles' in scale) { + // continuous input scales + scale.domain(config.domain as number[] | Date[]); + } else if ('padding' in scale) { + // point and band scales + scale.domain(config.domain as DiscreteInput[]); + } else { + // ordinal and threshold scale + scale.domain(config.domain as ThresholdInput[]); + } + } +} diff --git a/packages/vx-scale/src/operators/exponent.ts b/packages/vx-scale/src/operators/exponent.ts new file mode 100644 index 000000000..f786cfd0e --- /dev/null +++ b/packages/vx-scale/src/operators/exponent.ts @@ -0,0 +1,16 @@ +import { DefaultThresholdInput, D3Scale } from '../types/Scale'; +import { StringLike } from '../types/Base'; +import { ScaleConfigWithoutType } from '../types/ScaleConfig'; + +export default function applyExponent< + Output, + DiscreteInput extends StringLike, + ThresholdInput extends DefaultThresholdInput +>( + scale: D3Scale, + config: ScaleConfigWithoutType, +) { + if ('exponent' in scale && 'exponent' in config && typeof config.exponent !== 'undefined') { + scale.exponent(config.exponent); + } +} diff --git a/packages/vx-scale/src/operators/interpolate.ts b/packages/vx-scale/src/operators/interpolate.ts new file mode 100644 index 000000000..0d80ff62b --- /dev/null +++ b/packages/vx-scale/src/operators/interpolate.ts @@ -0,0 +1,23 @@ +import { InterpolatorFactory } from 'd3-scale'; +import { StringLike } from '../types/Base'; +import { D3Scale, DefaultThresholdInput } from '../types/Scale'; +import createColorInterpolator from '../utils/createColorInterpolator'; +import { ScaleConfigWithoutType } from '../types/ScaleConfig'; + +export default function applyInterpolate< + Output, + DiscreteInput extends StringLike, + ThresholdInput extends DefaultThresholdInput +>( + scale: D3Scale, + config: ScaleConfigWithoutType, +) { + if ( + 'interpolate' in config && + 'interpolate' in scale && + typeof config.interpolate !== 'undefined' + ) { + const interpolator = createColorInterpolator(config.interpolate); + scale.interpolate((interpolator as unknown) as InterpolatorFactory); + } +} diff --git a/packages/vx-scale/src/operators/nice.ts b/packages/vx-scale/src/operators/nice.ts new file mode 100644 index 000000000..8bd81c788 --- /dev/null +++ b/packages/vx-scale/src/operators/nice.ts @@ -0,0 +1,82 @@ +import { + timeSecond, + timeMinute, + timeHour, + timeDay, + timeYear, + timeMonth, + timeWeek, + utcSecond, + utcMinute, + utcHour, + utcDay, + utcWeek, + utcMonth, + utcYear, + CountableTimeInterval, +} from 'd3-time'; +import { ScaleTime } from 'd3-scale'; +import { StringLike } from '../types/Base'; +import { DefaultThresholdInput, D3Scale } from '../types/Scale'; +import { ScaleConfigWithoutType } from '../types/ScaleConfig'; +import { NiceTime } from '../types/Nice'; +import isUtcScale from '../utils/isUtcScale'; + +const localTimeIntervals: { + [key in NiceTime]: CountableTimeInterval; +} = { + day: timeDay, + hour: timeHour, + minute: timeMinute, + month: timeMonth, + second: timeSecond, + week: timeWeek, + year: timeYear, +}; + +const utcIntervals: { + [key in NiceTime]: CountableTimeInterval; +} = { + day: utcDay, + hour: utcHour, + minute: utcMinute, + month: utcMonth, + second: utcSecond, + week: utcWeek, + year: utcYear, +}; + +export default function applyNice< + Output, + DiscreteInput extends StringLike, + ThresholdInput extends DefaultThresholdInput +>( + scale: D3Scale, + config: ScaleConfigWithoutType, +) { + if ('nice' in config && typeof config.nice !== 'undefined' && 'nice' in scale) { + const { nice } = config; + if (typeof nice === 'boolean') { + if (nice) { + scale.nice(); + } + } else if (typeof nice === 'number') { + scale.nice(nice); + } else { + const timeScale = scale as ScaleTime; + const isUtc = isUtcScale(timeScale); + if (typeof nice === 'string') { + timeScale.nice(isUtc ? utcIntervals[nice] : localTimeIntervals[nice]); + } else { + const { interval, step } = nice; + const parsedInterval = (isUtc + ? utcIntervals[interval] + : localTimeIntervals[interval] + ).every(step); + if (parsedInterval != null) { + timeScale.nice(parsedInterval as CountableTimeInterval); + } + } + } + } +} diff --git a/packages/vx-scale/src/operators/padding.ts b/packages/vx-scale/src/operators/padding.ts new file mode 100644 index 000000000..2aaeec559 --- /dev/null +++ b/packages/vx-scale/src/operators/padding.ts @@ -0,0 +1,30 @@ +import { DefaultThresholdInput, D3Scale } from '../types/Scale'; +import { StringLike } from '../types/Base'; +import { ScaleConfigWithoutType } from '../types/ScaleConfig'; + +export default function applyPadding< + Output, + DiscreteInput extends StringLike, + ThresholdInput extends DefaultThresholdInput +>( + scale: D3Scale, + config: ScaleConfigWithoutType, +) { + if ('padding' in scale && 'padding' in config && typeof config.padding !== 'undefined') { + scale.padding(config.padding); + } + if ( + 'paddingInner' in scale && + 'paddingInner' in config && + typeof config.paddingInner !== 'undefined' + ) { + scale.paddingInner(config.paddingInner); + } + if ( + 'paddingOuter' in scale && + 'paddingOuter' in config && + typeof config.paddingOuter !== 'undefined' + ) { + scale.paddingOuter(config.paddingOuter); + } +} diff --git a/packages/vx-scale/src/operators/range.ts b/packages/vx-scale/src/operators/range.ts new file mode 100644 index 000000000..b3b39eee6 --- /dev/null +++ b/packages/vx-scale/src/operators/range.ts @@ -0,0 +1,22 @@ +import { DefaultThresholdInput, D3Scale } from '../types/Scale'; +import { StringLike } from '../types/Base'; +import { ScaleConfigWithoutType } from '../types/ScaleConfig'; + +export default function applyRange< + Output, + DiscreteInput extends StringLike, + ThresholdInput extends DefaultThresholdInput +>( + scale: D3Scale, + config: ScaleConfigWithoutType, +) { + if (config.range) { + if ('padding' in scale) { + // point and band scales + scale.range(config.range as [number, number]); + } else { + // the rest + scale.range(config.range as Output[]); + } + } +} diff --git a/packages/vx-scale/src/operators/round.ts b/packages/vx-scale/src/operators/round.ts new file mode 100644 index 000000000..2703aac10 --- /dev/null +++ b/packages/vx-scale/src/operators/round.ts @@ -0,0 +1,32 @@ +import { interpolateRound } from 'd3-interpolate'; +import { InterpolatorFactory } from 'd3-scale'; +import { StringLike } from '../types/Base'; +import { D3Scale, DefaultThresholdInput } from '../types/Scale'; +import { ScaleConfigWithoutType } from '../types/ScaleConfig'; + +export default function applyRound< + Output, + DiscreteInput extends StringLike, + ThresholdInput extends DefaultThresholdInput +>( + scale: D3Scale, + config: ScaleConfigWithoutType, +) { + if ('round' in config && typeof config.round !== 'undefined') { + if (config.round && 'interpolate' in config && typeof config.interpolate !== 'undefined') { + console.warn( + `[vx/scale/applyRound] ignoring round: scale config contains round and interpolate. only applying interpolate. config:`, + config, + ); + } else if ('round' in scale) { + // for point and band scales + scale.round(config.round); + } else if ('interpolate' in scale && config.round) { + // for continuous output scales + // setting config.round = true + // is actually setting interpolator to interpolateRound + // as these scales do not have scale.round() function + scale.interpolate((interpolateRound as unknown) as InterpolatorFactory); + } + } +} diff --git a/packages/vx-scale/src/operators/scaleOperator.ts b/packages/vx-scale/src/operators/scaleOperator.ts new file mode 100644 index 000000000..96a057371 --- /dev/null +++ b/packages/vx-scale/src/operators/scaleOperator.ts @@ -0,0 +1,81 @@ +import { DefaultThresholdInput, PickD3Scale } from '../types/Scale'; +import { ScaleType, PickScaleConfigWithoutType } from '../types/ScaleConfig'; +import { DefaultOutput, StringLike } from '../types/Base'; +import domain from './domain'; +import range from './range'; +import align from './align'; +import base from './base'; +import clamp from './clamp'; +import constant from './constant'; +import exponent from './exponent'; +import interpolate from './interpolate'; +import nice from './nice'; +import padding from './padding'; +import round from './round'; +import unknown from './unknown'; +import zero from './zero'; + +/** + * List of all operators, in order of execution + */ +export const ALL_OPERATORS = [ + // domain => nice => zero + 'domain', + 'nice', + 'zero', + + // interpolate before round + 'interpolate', + 'round', + + // Order does not matter for these operators + 'align', + 'base', + 'clamp', + 'constant', + 'exponent', + 'padding', + 'range', + 'unknown', +] as const; + +type OperatorType = typeof ALL_OPERATORS[number]; + +// Use Record to enforce that all keys in OperatorType must exist. +const operators: Record = { + domain, + nice, + zero, + interpolate, + round, + align, + base, + clamp, + constant, + exponent, + padding, + range, + unknown, +}; + +export default function scaleOperator(...ops: OperatorType[]) { + const selection = new Set(ops); + const selectedOps = ALL_OPERATORS.filter(o => selection.has(o)); + + return function applyOperators< + Output = DefaultOutput, + DiscreteInput extends StringLike = StringLike, + ThresholdInput extends DefaultThresholdInput = DefaultThresholdInput + >( + scale: PickD3Scale, + config?: PickScaleConfigWithoutType, + ) { + if (typeof config !== 'undefined') { + selectedOps.forEach(op => { + operators[op](scale, config); + }); + } + + return scale; + }; +} diff --git a/packages/vx-scale/src/operators/unknown.ts b/packages/vx-scale/src/operators/unknown.ts new file mode 100644 index 000000000..df6a467c6 --- /dev/null +++ b/packages/vx-scale/src/operators/unknown.ts @@ -0,0 +1,16 @@ +import { DefaultThresholdInput, D3Scale } from '../types/Scale'; +import { StringLike } from '../types/Base'; +import { ScaleConfigWithoutType } from '../types/ScaleConfig'; + +export default function applyUnknown< + Output, + DiscreteInput extends StringLike, + ThresholdInput extends DefaultThresholdInput +>( + scale: D3Scale, + config: ScaleConfigWithoutType, +) { + if ('unknown' in scale && 'unknown' in config && typeof config.unknown !== 'undefined') { + scale.unknown(config.unknown); + } +} diff --git a/packages/vx-scale/src/operators/zero.ts b/packages/vx-scale/src/operators/zero.ts new file mode 100644 index 000000000..f832289af --- /dev/null +++ b/packages/vx-scale/src/operators/zero.ts @@ -0,0 +1,21 @@ +import { StringLike } from '../types/Base'; +import { DefaultThresholdInput, D3Scale } from '../types/Scale'; +import { ScaleConfigWithoutType } from '../types/ScaleConfig'; + +export default function applyZero< + Output, + DiscreteInput extends StringLike, + ThresholdInput extends DefaultThresholdInput +>( + scale: D3Scale, + config: ScaleConfigWithoutType, +) { + if ('zero' in config && config.zero === true) { + const domain = scale.domain() as number[]; + const [a, b] = domain; + const isDescending = b < a; + const [min, max] = isDescending ? [b, a] : [a, b]; + const domainWithZero = [Math.min(0, min), Math.max(0, max)]; + scale.domain(isDescending ? domainWithZero.reverse() : domainWithZero); + } +} diff --git a/packages/vx-scale/src/scales/band.ts b/packages/vx-scale/src/scales/band.ts index 34de5f8c4..bdb0d1089 100644 --- a/packages/vx-scale/src/scales/band.ts +++ b/packages/vx-scale/src/scales/band.ts @@ -1,51 +1,18 @@ import { scaleBand } from 'd3-scale'; +import { DefaultOutput, StringLike } from '../types/Base'; +import { PickScaleConfigWithoutType } from '../types/ScaleConfig'; +import scaleOperator from '../operators/scaleOperator'; -type StringLike = string | { toString(): string }; -type Numeric = number | { valueOf(): number }; +export const updateBandScale = scaleOperator<'band'>( + 'domain', + 'range', + 'align', + 'padding', + 'round', +); -export type BandConfig = { - /** Sets the output values of the scale, which are numbers for band scales. */ - range?: [Numeric, Numeric]; - /** Sets the output values of the scale while setting its interpolator to round. If the elements are not numbers, they will be coerced to numbers. */ - rangeRound?: [Numeric, Numeric]; - /** Sets the input values of the scale, which are strings for band scales. */ - domain?: Datum[]; - /** 0-1, determines how any leftover unused space in the range is distributed. 0.5 distributes it equally left and right. */ - align?: number; - /** 0-1, determines the ratio of the range that is reserved for blank space before the first point and after the last. */ - padding?: number; - /** 0-1, determines the ratio of the range that is reserved for blank space _between_ bands. */ - paddingInner?: number; - /** 0-1, determines the ratio of the range that is reserved for blank space before the first band and after the last band. */ - paddingOuter?: number; - tickFormat?: unknown; -}; - -export default function bandScale({ - range, - rangeRound, - domain, - padding, - paddingInner, - paddingOuter, - align, - tickFormat, -}: BandConfig) { - const scale = scaleBand(); - - if (range) scale.range(range); - if (rangeRound) scale.rangeRound(rangeRound); - if (domain) scale.domain(domain); - if (padding) scale.padding(padding); - if (paddingInner) scale.paddingInner(paddingInner); - if (paddingOuter) scale.paddingOuter(paddingOuter); - if (align) scale.align(align); - - // @TODO should likely get rid of these. - // @ts-ignore - if (tickFormat) scale.tickFormat = tickFormat; - // @ts-ignore - scale.type = 'band'; - - return scale; +export default function createBandScale( + config?: PickScaleConfigWithoutType<'band', DefaultOutput, DiscreteInput>, +) { + return updateBandScale(scaleBand(), config); } diff --git a/packages/vx-scale/src/scales/linear.ts b/packages/vx-scale/src/scales/linear.ts index e127f8d26..d56f690a7 100644 --- a/packages/vx-scale/src/scales/linear.ts +++ b/packages/vx-scale/src/scales/linear.ts @@ -1,35 +1,20 @@ import { scaleLinear } from 'd3-scale'; +import { DefaultOutput } from '../types/Base'; +import { PickScaleConfigWithoutType } from '../types/ScaleConfig'; +import scaleOperator from '../operators/scaleOperator'; -export type LinearConfig = { - /** Sets the input values of the scale, which are numbers for a linear scale. */ - domain?: number[]; - /** Sets the output values of the scale. */ - range?: Output[]; - /** Sets the output values of the scale while setting its interpolator to round. If the elements are not numbers, they will be coerced to numbers. */ - rangeRound?: number[]; - /** Extends the domain so that it starts and ends on nice round values. */ - nice?: boolean; - /** Whether the scale should clamp values to within the range. */ - clamp?: boolean; -}; +export const updateLinearScale = scaleOperator<'linear'>( + 'domain', + 'range', + 'clamp', + 'interpolate', + 'nice', + 'round', + 'zero', +); -export default function linearScale({ - range, - rangeRound, - domain, - nice = false, - clamp = false, -}: LinearConfig) { - const scale = scaleLinear(); - - if (range) scale.range(range); - if (rangeRound) scale.rangeRound(rangeRound); - if (domain) scale.domain(domain); - if (nice) scale.nice(); - if (clamp) scale.clamp(true); - - // @ts-ignore - scale.type = 'linear'; - - return scale; +export default function createLinearScale( + config?: PickScaleConfigWithoutType<'linear', Output>, +) { + return updateLinearScale(scaleLinear(), config); } diff --git a/packages/vx-scale/src/scales/log.ts b/packages/vx-scale/src/scales/log.ts index 1d770bd8d..6e68139c7 100644 --- a/packages/vx-scale/src/scales/log.ts +++ b/packages/vx-scale/src/scales/log.ts @@ -1,39 +1,20 @@ import { scaleLog } from 'd3-scale'; +import { DefaultOutput } from '../types/Base'; +import { PickScaleConfigWithoutType } from '../types/ScaleConfig'; +import scaleOperator from '../operators/scaleOperator'; -export type LogConfig = { - /** Sets the input values of the scale, which are numbers for a log scale. */ - domain?: number[]; - /** Sets the output values of the scale. */ - range?: Output[]; - /** Sets the output values of the scale while setting its interpolator to round. If the elements are not numbers, they will be coerced to numbers. */ - rangeRound?: number[]; - /** Sets the base for this logarithmic scale (defaults to 10). */ - base?: number; - /** Extends the domain so that it starts and ends on nice round values. */ - nice?: boolean; - /** Whether the scale should clamp values to within the range. */ - clamp?: boolean; -}; +export const updateLogScale = scaleOperator<'log'>( + 'domain', + 'range', + 'base', + 'clamp', + 'interpolate', + 'nice', + 'round', +); -export default function logScale({ - range, - rangeRound, - domain, - base, - nice = false, - clamp = false, -}: LogConfig) { - const scale = scaleLog(); - - if (range) scale.range(range); - if (rangeRound) scale.rangeRound(rangeRound); - if (domain) scale.domain(domain); - if (nice) scale.nice(); - if (clamp) scale.clamp(true); - if (base) scale.base(base); - - // @ts-ignore - scale.type = 'log'; - - return scale; +export default function createLogScale( + config?: PickScaleConfigWithoutType<'log', Output>, +) { + return updateLogScale(scaleLog(), config); } diff --git a/packages/vx-scale/src/scales/ordinal.ts b/packages/vx-scale/src/scales/ordinal.ts index b190cda05..ea91e0310 100644 --- a/packages/vx-scale/src/scales/ordinal.ts +++ b/packages/vx-scale/src/scales/ordinal.ts @@ -1,27 +1,13 @@ import { scaleOrdinal } from 'd3-scale'; +import { DefaultOutput, StringLike } from '../types/Base'; +import { PickScaleConfigWithoutType } from '../types/ScaleConfig'; +import scaleOperator from '../operators/scaleOperator'; -export type OrdinalConfig = { - /** Sets the input values of the scale, which are strings for an ordinal scale. */ - domain?: Input[]; - /** Sets the output values of the scale. */ - range?: Output[]; - /** Sets the output value of the scale for unknown input values. */ - unknown?: Output | { name: 'implicit' }; -}; +export const updateOrdinalScale = scaleOperator<'ordinal'>('domain', 'range', 'unknown'); -export default function ordinalScale({ - range, - domain, - unknown, -}: OrdinalConfig) { - const scale = scaleOrdinal(); - - if (range) scale.range(range); - if (domain) scale.domain(domain); - if (unknown) scale.unknown(unknown); - - // @ts-ignore - scale.type = 'ordinal'; - - return scale; +export default function createOrdinalScale< + DiscreteInput extends StringLike = StringLike, + Output = DefaultOutput +>(config?: PickScaleConfigWithoutType<'ordinal', Output, DiscreteInput>) { + return updateOrdinalScale(scaleOrdinal(), config); } diff --git a/packages/vx-scale/src/scales/point.ts b/packages/vx-scale/src/scales/point.ts index 66fe6854a..9ab16a39e 100644 --- a/packages/vx-scale/src/scales/point.ts +++ b/packages/vx-scale/src/scales/point.ts @@ -1,35 +1,18 @@ import { scalePoint } from 'd3-scale'; +import { DefaultOutput, StringLike } from '../types/Base'; +import { PickScaleConfigWithoutType } from '../types/ScaleConfig'; +import scaleOperator from '../operators/scaleOperator'; -export type PointConfig = { - /** Sets the output values of the scale, which are numbers for point scales. */ - range?: [number, number]; - /** Sets the output values of the scale while setting its interpolator to round. */ - rangeRound?: [number, number]; - /** Sets the input values of the scale. */ - domain?: Input[]; - /** 0-1, determines the ratio of the range that is reserved for blank space before the first point and after the last. */ - padding?: number; - /** 0-1, determines how any leftover unused space in the range is distributed. 0.5 distributes it equally left and right. */ - align?: number; -}; +export const updatePointScale = scaleOperator<'point'>( + 'domain', + 'range', + 'align', + 'padding', + 'round', +); -export default function pointScale({ - range, - rangeRound, - domain, - padding, - align, -}: PointConfig) { - const scale = scalePoint(); - - if (range) scale.range(range); - if (rangeRound) scale.rangeRound(rangeRound); - if (domain) scale.domain(domain); - if (padding) scale.padding(padding); - if (align) scale.align(align); - - // @ts-ignore - scale.type = 'point'; - - return scale; +export default function createPointScale( + config?: PickScaleConfigWithoutType<'point', DefaultOutput, DiscreteInput>, +) { + return updatePointScale(scalePoint(), config); } diff --git a/packages/vx-scale/src/scales/power.ts b/packages/vx-scale/src/scales/power.ts index 5c40c46ea..fd5fb2596 100644 --- a/packages/vx-scale/src/scales/power.ts +++ b/packages/vx-scale/src/scales/power.ts @@ -1,39 +1,21 @@ import { scalePow } from 'd3-scale'; +import { DefaultOutput } from '../types/Base'; +import { PickScaleConfigWithoutType } from '../types/ScaleConfig'; +import scaleOperator from '../operators/scaleOperator'; -export type PowerConfig = { - /** Sets the output values of the scale. */ - range?: Output[]; - /** Sets the output values of the scale while setting its interpolator to round. They need not be numbers, though numbers are required for invert. */ - rangeRound?: number[]; - /** Sets the input values of the scalem which are numbers for a power scale. */ - domain?: number[]; - /** Sets the scale's exponent to the given number, defaults to 1. This is effectively a linear scale until you set a different exponent. */ - exponent?: number; - /** Extends the domain so that it starts and ends on nice round values. */ - nice?: boolean; - /** Whether the scale should clamp values to within the range. */ - clamp?: boolean; -}; +export const updatePowScale = scaleOperator<'pow'>( + 'domain', + 'range', + 'clamp', + 'exponent', + 'interpolate', + 'nice', + 'round', + 'zero', +); -export default function powerScale({ - range, - rangeRound, - domain, - exponent, - nice = false, - clamp = false, -}: PowerConfig) { - const scale = scalePow(); - - if (range) scale.range(range); - if (rangeRound) scale.rangeRound(rangeRound); - if (domain) scale.domain(domain); - if (nice) scale.nice(); - if (clamp) scale.clamp(true); - if (exponent) scale.exponent(exponent); - - // @ts-ignore - scale.type = 'power'; - - return scale; +export default function createPowScale( + config?: PickScaleConfigWithoutType<'pow', Output>, +) { + return updatePowScale(scalePow(), config); } diff --git a/packages/vx-scale/src/scales/quantile.ts b/packages/vx-scale/src/scales/quantile.ts index 1bbcfb471..27b73d3eb 100644 --- a/packages/vx-scale/src/scales/quantile.ts +++ b/packages/vx-scale/src/scales/quantile.ts @@ -1,20 +1,12 @@ import { scaleQuantile } from 'd3-scale'; +import { DefaultOutput } from '../types/Base'; +import { PickScaleConfigWithoutType } from '../types/ScaleConfig'; +import scaleOperator from '../operators/scaleOperator'; -export type QuantileConfig = { - /** Sets the output values of the scale. */ - range?: Output[]; - /** Sets the input values of the scale. */ - domain?: (number | null | undefined)[]; -}; +export const updateQuantileScale = scaleOperator<'quantile'>('domain', 'range'); -export default function quantileScale({ range, domain }: QuantileConfig) { - const scale = scaleQuantile(); - - if (range) scale.range(range); - if (domain) scale.domain(domain); - - // @ts-ignore - scale.type = 'quantile'; - - return scale; +export default function createQuantileScale( + config?: PickScaleConfigWithoutType<'quantile', Output>, +) { + return updateQuantileScale(scaleQuantile(), config); } diff --git a/packages/vx-scale/src/scales/quantize.ts b/packages/vx-scale/src/scales/quantize.ts index 25ef98975..42d21dd73 100644 --- a/packages/vx-scale/src/scales/quantize.ts +++ b/packages/vx-scale/src/scales/quantize.ts @@ -1,35 +1,12 @@ import { scaleQuantize } from 'd3-scale'; +import { DefaultOutput } from '../types/Base'; +import { PickScaleConfigWithoutType } from '../types/ScaleConfig'; +import scaleOperator from '../operators/scaleOperator'; -export type QuantizeConfig = { - /** Sets the output values of the scale, which are numbers for point scales. */ - range?: Output[]; - /** Sets the input values of the scale. */ - domain?: [number, number]; - /** Extends the domain so that it starts and ends on nice round values. */ - nice?: boolean; - /** Optional approximate number of ticks to be returned. */ - ticks?: number; - /** Specifies an approximate tick count and valid format specifier string. */ - tickFormat?: [number, string]; -}; +export const updateQuantizeScale = scaleOperator<'quantize'>('domain', 'range', 'nice', 'zero'); -export default function quantizeScale({ - range, - domain, - ticks, - tickFormat, - nice = false, -}: QuantizeConfig) { - const scale = scaleQuantize(); - - if (range) scale.range(range); - if (domain) scale.domain(domain); - if (nice) scale.nice(); - if (ticks) scale.ticks(ticks); - if (tickFormat) scale.tickFormat(...tickFormat); - - // @ts-ignore - scale.type = 'quantize'; - - return scale; +export default function createQuantizeScale( + config?: PickScaleConfigWithoutType<'quantize', Output>, +) { + return updateQuantizeScale(scaleQuantize(), config); } diff --git a/packages/vx-scale/src/scales/squareRoot.ts b/packages/vx-scale/src/scales/squareRoot.ts index e08c09e00..187ba9be1 100644 --- a/packages/vx-scale/src/scales/squareRoot.ts +++ b/packages/vx-scale/src/scales/squareRoot.ts @@ -1,12 +1,20 @@ -import powerScale, { PowerConfig } from './power'; +import { scaleSqrt } from 'd3-scale'; +import { DefaultOutput } from '../types/Base'; +import { PickScaleConfigWithoutType } from '../types/ScaleConfig'; +import scaleOperator from '../operators/scaleOperator'; -export type SquareRootConfig = Omit, 'exponent'>; +export const updateSqrtScale = scaleOperator<'sqrt'>( + 'domain', + 'range', + 'clamp', + 'interpolate', + 'nice', + 'round', + 'zero', +); -export default function squareRootScale(scaleConfig: SquareRootConfig) { - const scale = powerScale({ ...scaleConfig, exponent: 0.5 }); - - // @ts-ignore - scale.type = 'squareRoot'; - - return scale; +export default function createSqrtScale( + config?: PickScaleConfigWithoutType<'sqrt', Output>, +) { + return updateSqrtScale(scaleSqrt(), config); } diff --git a/packages/vx-scale/src/scales/symlog.ts b/packages/vx-scale/src/scales/symlog.ts index 921fa74ba..bf47a2ff4 100644 --- a/packages/vx-scale/src/scales/symlog.ts +++ b/packages/vx-scale/src/scales/symlog.ts @@ -1,24 +1,19 @@ -// @ts-ignore no type defs for symlog import { scaleSymlog } from 'd3-scale'; +import { DefaultOutput } from '../types/Base'; +import { PickScaleConfigWithoutType } from '../types/ScaleConfig'; +import scaleOperator from '../operators/scaleOperator'; -export type SymlogConfig = { - /** Sets the output values of the scale. */ - range?: any[]; - /** Sets the input values of the scale. */ - domain?: any[]; - /** Sets the symlog constant to the specified number, defaults to 1. */ - constant?: number; -}; +export const updateSymlogScale = scaleOperator<'symlog'>( + 'domain', + 'range', + 'clamp', + 'constant', + 'nice', + 'zero', +); -export default function symLogScale({ range, domain, constant }: SymlogConfig) { - const scale = scaleSymlog(); - - if (range) scale.range(range); - if (domain) scale.domain(domain); - if (constant) scale.constant(constant); - - // @ts-ignore - scale.type = 'symlog'; - - return scale; +export default function createSymlogScale( + config?: PickScaleConfigWithoutType<'symlog', Output>, +) { + return updateSymlogScale(scaleSymlog(), config); } diff --git a/packages/vx-scale/src/scales/threshold.ts b/packages/vx-scale/src/scales/threshold.ts index a5c4a399b..8023f8d51 100644 --- a/packages/vx-scale/src/scales/threshold.ts +++ b/packages/vx-scale/src/scales/threshold.ts @@ -1,23 +1,14 @@ import { scaleThreshold } from 'd3-scale'; - -export type ThresholdConfig = { - /** Sets the output values of the scale. */ - range?: Output[]; - /** Sets the input values of the scale. */ - domain?: Input[]; -}; - -export default function thresholdScale({ - range, - domain, -}: ThresholdConfig) { - const scale = scaleThreshold(); - - if (range) scale.range(range); - if (domain) scale.domain(domain); - - // @ts-ignore - scale.type = 'threshold'; - - return scale; +import { DefaultOutput, StringLike } from '../types/Base'; +import { PickScaleConfigWithoutType } from '../types/ScaleConfig'; +import { DefaultThresholdInput } from '../types/Scale'; +import scaleOperator from '../operators/scaleOperator'; + +export const updateThresholdScale = scaleOperator<'threshold'>('domain', 'range'); + +export default function createThresholdScale< + ThresholdInput extends DefaultThresholdInput = DefaultThresholdInput, + Output = DefaultOutput +>(config?: PickScaleConfigWithoutType<'threshold', Output, StringLike, ThresholdInput>) { + return updateThresholdScale(scaleThreshold(), config); } diff --git a/packages/vx-scale/src/scales/time.ts b/packages/vx-scale/src/scales/time.ts index 8ac26f819..ae9937489 100644 --- a/packages/vx-scale/src/scales/time.ts +++ b/packages/vx-scale/src/scales/time.ts @@ -1,35 +1,19 @@ import { scaleTime } from 'd3-scale'; +import { DefaultOutput } from '../types/Base'; +import { PickScaleConfigWithoutType } from '../types/ScaleConfig'; +import scaleOperator from '../operators/scaleOperator'; -export type TimeConfig = { - /** Sets the input values of the scale, which are Dates or coercible to numbers for time scales. */ - domain?: (Date | number | { valueOf(): number })[]; - /** Sets the output values of the scale. */ - range?: Output[]; - /** Sets the output values of the scale while setting its interpolator to round. If the elements are not numbers, they will be coerced to numbers. */ - rangeRound?: number[]; - /** Extends the domain so that it starts and ends on nice round values. */ - nice?: boolean; - /** Whether the scale should clamp values to within the range. */ - clamp?: boolean; -}; +export const updateTimeScale = scaleOperator<'time'>( + 'domain', + 'range', + 'clamp', + 'interpolate', + 'nice', + 'round', +); -export default function timeScale({ - range, - rangeRound, - domain, - nice = false, - clamp = false, -}: TimeConfig) { - const scale = scaleTime(); - - if (range) scale.range(range); - if (rangeRound) scale.rangeRound(rangeRound); - if (domain) scale.domain(domain); - if (nice) scale.nice(); - if (clamp) scale.clamp(true); - - // @ts-ignore - scale.type = 'time'; - - return scale; +export default function createTimeScale( + config?: PickScaleConfigWithoutType<'time', Output>, +) { + return updateTimeScale(scaleTime(), config); } diff --git a/packages/vx-scale/src/scales/utc.ts b/packages/vx-scale/src/scales/utc.ts index 2b4b77735..846c11a19 100644 --- a/packages/vx-scale/src/scales/utc.ts +++ b/packages/vx-scale/src/scales/utc.ts @@ -1,35 +1,19 @@ import { scaleUtc } from 'd3-scale'; +import { DefaultOutput } from '../types/Base'; +import { PickScaleConfigWithoutType } from '../types/ScaleConfig'; +import scaleOperator from '../operators/scaleOperator'; -export type UtcConfig = { - /** Sets the input values of the scale, which are Dates or coercible to numbers for UTC time scales. */ - domain?: (Date | number | { valueOf(): number })[]; - /** Sets the output values of the scale. */ - range?: Output[]; - /** Sets the output values of the scale while setting its interpolator to round. If the elements are not numbers, they will be coerced to numbers. */ - rangeRound?: number[]; - /** Extends the domain so that it starts and ends on nice round values. */ - nice?: boolean; - /** Whether the scale should clamp values to within the range. */ - clamp?: boolean; -}; +export const updateUtcScale = scaleOperator<'utc'>( + 'domain', + 'range', + 'clamp', + 'interpolate', + 'nice', + 'round', +); -export default function timeScale({ - range, - rangeRound, - domain, - nice = false, - clamp = false, -}: UtcConfig) { - const scale = scaleUtc(); - - if (range) scale.range(range); - if (rangeRound) scale.rangeRound(rangeRound); - if (domain) scale.domain(domain); - if (nice) scale.nice(); - if (clamp) scale.clamp(true); - - // @ts-ignore - scale.type = 'utc'; - - return scale; +export default function createUtcScale( + config?: PickScaleConfigWithoutType<'utc', Output>, +) { + return updateUtcScale(scaleUtc(), config); } diff --git a/packages/vx-scale/src/types/Base.ts b/packages/vx-scale/src/types/Base.ts new file mode 100644 index 000000000..bd721bf85 --- /dev/null +++ b/packages/vx-scale/src/types/Base.ts @@ -0,0 +1,14 @@ +/** A value that has .valueOf() function */ +export type NumberLike = { valueOf(): number }; + +/** A value that has .toString() function */ +export type StringLike = { toString(): string }; + +/** Default output type */ +export type DefaultOutput = number | string | boolean | null; + +/** Union types of all values from a map type */ +export type ValueOf = T[keyof T]; + +/** Extract generic type from array */ +export type Unarray = T extends Array ? U : T; diff --git a/packages/vx-scale/src/types/BaseScaleConfig.ts b/packages/vx-scale/src/types/BaseScaleConfig.ts new file mode 100644 index 000000000..319eca6ea --- /dev/null +++ b/packages/vx-scale/src/types/BaseScaleConfig.ts @@ -0,0 +1,123 @@ +import { Unarray } from './Base'; +import { ScaleInterpolate, ScaleInterpolateParams } from './ScaleInterpolate'; + +export interface BaseScaleConfig { + type: T; + + /** + * The domain of the scale. + */ + domain?: D; + + /** + * The range of the scale. + */ + range?: R; + + /** + * The alignment of the steps within the scale range. + * + * This value must lie in the range `[0,1]`. A value of `0.5` indicates that the steps should be centered within the range. A value of `0` or `1` may be used to shift the bands to one side, say to position them adjacent to an axis. + * + * __Default value:__ `0.5` + */ + align?: number; + + /** + * The logarithm base of the `log` scale (default `10`). + */ + base?: number; + + /** + * If `true`, values that exceed the data domain are clamped to either the minimum or maximum range value + * + * __Default value:__ `false`. + */ + clamp?: boolean; + + /** + * A constant determining the slope of the symlog function around zero. Only used for `symlog` scales. + * + * __Default value:__ `1` + */ + constant?: number; + + /** + * The exponent of the `pow` scale. + */ + exponent?: number; + + /** + * The interpolation method for range values. + * By default, a general interpolator for numbers, dates, strings and colors (in HCL space) is used. + * For color ranges, this property allows interpolation in alternative color spaces. Legal values include `rgb`, `hsl`, `hsl-long`, `lab`, `hcl`, `hcl-long`, `cubehelix` and `cubehelix-long` ('-long' variants use longer paths in polar coordinate spaces). If object-valued, this property accepts an object with a string-valued _type_ property and an optional numeric _gamma_ property applicable to rgb and cubehelix interpolators. For more, see the [d3-interpolate documentation](https://github.com/d3/d3-interpolate). + * + */ + interpolate?: ScaleInterpolate | ScaleInterpolateParams; + + /** + * Extending the domain so that it starts and ends on nice round values. This method typically modifies the scale’s domain, and may only extend the bounds to the nearest round value. Nicing is useful if the domain is computed from data and may be irregular. For example, for a domain of _[0.201479…, 0.996679…]_, a nice domain might be _[0.2, 1.0]_. + * + * For quantitative scales such as linear, `nice` can be either a boolean flag or a number. If `nice` is a number, it will represent a desired tick count. This allows greater control over the step size used to extend the bounds, guaranteeing that the returned ticks will exactly cover the domain. + * + * __Default value:__ `true` for _quantitative_ fields; `false` otherwise. + * + */ + nice?: boolean | number; + + /** + * For band scale, shortcut for setting `paddingInner` and `paddingOuter` to the same value. + * + * For point scale, the outer padding (spacing) at the ends of the range. + * This is similar to band scale's `paddingOuter`. + * + * @minimum 0 + */ + padding?: number; + + /** + * The inner padding (spacing) within each band step of band scales, as a fraction of the step size. This value must lie in the range [0,1]. + * + * @minimum 0 + * @maximum 1 + */ + paddingInner?: number; + + /** + * The outer padding (spacing) at the ends of the range of band and point scales, + * as a fraction of the step size. This value must lie in the range [0,1]. + * + * @minimum 0 + * @maximum 1 + */ + paddingOuter?: number; + + /** + * If true, reverses the order of the scale range. + * __Default value:__ `false`. + * + * @hidden + */ + reverse?: boolean; + + /** + * If `true`, rounds numeric output values to integers. This can be helpful for snapping to the pixel grid. + * + * __Default value:__ `false`. + */ + round?: boolean; + + /** + * Sets the output value of the scale for unknown input values. + */ + unknown?: Unarray | { name: 'implicit' }; + + /** + * If `true`, ensures that a zero baseline value is included in the scale domain. + * + * __Default value:__ `false` + * + * __Note:__ Log, time, and utc scales do not support `zero`. + */ + zero?: boolean; +} diff --git a/packages/vx-scale/src/types/Nice.ts b/packages/vx-scale/src/types/Nice.ts new file mode 100644 index 000000000..9a76dbedf --- /dev/null +++ b/packages/vx-scale/src/types/Nice.ts @@ -0,0 +1 @@ +export type NiceTime = 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year'; diff --git a/packages/vx-scale/src/types/Scale.ts b/packages/vx-scale/src/types/Scale.ts new file mode 100644 index 000000000..c57221196 --- /dev/null +++ b/packages/vx-scale/src/types/Scale.ts @@ -0,0 +1,64 @@ +import { + ScaleOrdinal, + ScaleLinear, + ScaleLogarithmic, + ScalePower, + ScaleTime, + ScaleQuantile, + ScaleQuantize, + ScaleThreshold, + ScalePoint, + ScaleBand, + ScaleSymLog, +} from 'd3-scale'; +import { StringLike, DefaultOutput, ValueOf } from './Base'; + +export type DefaultThresholdInput = number | string | Date; + +/** + * Map scale type to D3Scale type + * @type `Output`: Output type of all scales except point and band + * @type `ThresholdInput`: Input type for threshold scale + * @type `DiscreteInput`: Input type for ordinal, point and band scales + */ +export interface ScaleTypeToD3Scale< + Output = DefaultOutput, + DiscreteInput extends StringLike = StringLike, + ThresholdInput extends DefaultThresholdInput = DefaultThresholdInput +> { + // Input of these continuous scales are `number | { valueOf(): number }` + // and cannot be customized via generic type. + linear: ScaleLinear; + log: ScaleLogarithmic; + pow: ScalePower; + sqrt: ScalePower; + symlog: ScaleSymLog; + // Input of time scales are `Date | number | { valueOf(): number }` + // and cannot be customized via generic type. + time: ScaleTime; + utc: ScaleTime; + // Input of these discretizing scales are `number | { valueOf(): number }` + // and cannot be customized via generic type. + quantile: ScaleQuantile; + quantize: ScaleQuantize; + // Threshold scale has its own Input generic type. + threshold: ScaleThreshold; + // Ordinal scale can customize both Input and Output types. + ordinal: ScaleOrdinal; + // Output of these two scales are always number while Input can be customized. + point: ScalePoint; + band: ScaleBand; +} + +export type PickD3Scale< + T extends keyof ScaleTypeToD3Scale, + Output = DefaultOutput, + DiscreteInput extends StringLike = StringLike, + ThresholdInput extends DefaultThresholdInput = DefaultThresholdInput +> = ValueOf, T>>; + +export type D3Scale< + Output = DefaultOutput, + DiscreteInput extends StringLike = StringLike, + ThresholdInput extends DefaultThresholdInput = DefaultThresholdInput +> = ValueOf>; diff --git a/packages/vx-scale/src/types/ScaleConfig.ts b/packages/vx-scale/src/types/ScaleConfig.ts new file mode 100644 index 000000000..f528187b5 --- /dev/null +++ b/packages/vx-scale/src/types/ScaleConfig.ts @@ -0,0 +1,177 @@ +import { BaseScaleConfig } from './BaseScaleConfig'; +import { StringLike, DefaultOutput, ValueOf, NumberLike } from './Base'; +import { NiceTime } from './Nice'; +import { DefaultThresholdInput, ScaleTypeToD3Scale } from './Scale'; + +type Numeric = number | NumberLike; + +export type TimeInput = number | Date; +export type ContinuousInput = number | Date; + +export type TimeDomain = TimeInput[]; +export type ContinuousDomain = ContinuousInput[]; + +// Make the specific scales pick +// from same base type to share property documentation +// (which is useful for auto-complete/intellisense) +// and add `type` property as discriminant of union type. +type CreateScaleConfig = 'type'> = Pick< + BaseScaleConfig, + 'type' | 'domain' | 'range' | Fields +>; + +export type LinearScaleConfig = CreateScaleConfig< + 'linear', + ContinuousDomain, + Output[], + 'clamp' | 'interpolate' | 'nice' | 'round' | 'zero' +>; + +export type LogScaleConfig = CreateScaleConfig< + 'log', + ContinuousDomain, + Output[], + 'base' | 'clamp' | 'interpolate' | 'nice' | 'round' +>; + +export type PowScaleConfig = CreateScaleConfig< + 'pow', + ContinuousDomain, + Output[], + 'clamp' | 'exponent' | 'interpolate' | 'nice' | 'round' | 'zero' +>; + +export type SqrtScaleConfig = CreateScaleConfig< + 'sqrt', + ContinuousDomain, + Output[], + 'clamp' | 'interpolate' | 'nice' | 'round' | 'zero' +>; + +export type SymlogScaleConfig = CreateScaleConfig< + 'symlog', + ContinuousDomain, + Output[], + 'clamp' | 'constant' | 'nice' | 'zero' +>; + +export type QuantileScaleConfig = CreateScaleConfig< + 'quantile', + ContinuousDomain, + Output[] +>; + +export type QuantizeScaleConfig = CreateScaleConfig< + 'quantize', + [ContinuousInput, ContinuousInput], + Output[], + 'nice' | 'zero' +>; + +export type ThresholdScaleConfig< + ThresholdInput extends DefaultThresholdInput = DefaultThresholdInput, + Output = DefaultOutput +> = CreateScaleConfig<'threshold', ThresholdInput[], Output[]>; + +export type OrdinalScaleConfig< + DiscreteInput extends StringLike = StringLike, + Output = DefaultOutput +> = CreateScaleConfig<'ordinal', DiscreteInput[], Output[], 'unknown'>; + +export type PointScaleConfig = CreateScaleConfig< + 'point', + DiscreteInput[], + [Numeric, Numeric], + 'align' | 'padding' | 'round' +>; + +export type BandScaleConfig = CreateScaleConfig< + 'band', + DiscreteInput[], + [Numeric, Numeric], + 'align' | 'padding' | 'paddingInner' | 'paddingOuter' | 'round' +>; + +interface TemporalScaleConfig + extends CreateScaleConfig { + /** + * Extending the domain so that it starts and ends on nice round values. This method typically modifies the scale’s domain, and may only extend the bounds to the nearest round value. Nicing is useful if the domain is computed from data and may be irregular. For example, for a domain of _[0.201479…, 0.996679…]_, a nice domain might be _[0.2, 1.0]_. + * + * For quantitative scales such as linear, `nice` can be either a boolean flag or a number. If `nice` is a number, it will represent a desired tick count. This allows greater control over the step size used to extend the bounds, guaranteeing that the returned ticks will exactly cover the domain. + * + * For temporal fields with time and utc scales, the `nice` value can be a string indicating the desired time interval. Legal values are `"millisecond"`, `"second"`, `"minute"`, `"hour"`, `"day"`, `"week"`, `"month"`, and `"year"`. Alternatively, `time` and `utc` scales can accept an object-valued interval specifier of the form `{"interval": "month", "step": 3}`, which includes a desired number of interval steps. Here, the domain would snap to quarter (Jan, Apr, Jul, Oct) boundaries. + * + * __Default value:__ `true` for unbinned _quantitative_ fields; `false` otherwise. + * + */ + nice?: boolean | number | NiceTime | { interval: NiceTime; step: number }; +} + +export type TimeScaleConfig = TemporalScaleConfig<'time', Output>; + +export type UtcScaleConfig = TemporalScaleConfig<'utc', Output>; + +/** + * Map scale type to D3Scale type + * @type `Output`: Output type of all scales except point and band + * @type `ThresholdInput`: Input type for threshold scale + * @type `DiscreteInput`: Input type for ordinal, point and band scales + */ +export interface ScaleTypeToScaleConfig< + Output = DefaultOutput, + DiscreteInput extends StringLike = StringLike, + ThresholdInput extends DefaultThresholdInput = DefaultThresholdInput +> { + linear: LinearScaleConfig; + log: LogScaleConfig; + pow: PowScaleConfig; + sqrt: SqrtScaleConfig; + symlog: SymlogScaleConfig; + time: TimeScaleConfig; + utc: UtcScaleConfig; + quantile: QuantileScaleConfig; + quantize: QuantizeScaleConfig; + threshold: ThresholdScaleConfig; + ordinal: OrdinalScaleConfig; + point: PointScaleConfig; + band: BandScaleConfig; +} + +export type ScaleType = keyof ScaleTypeToScaleConfig; + +export type PickScaleConfig< + T extends ScaleType, + Output = DefaultOutput, + DiscreteInput extends StringLike = StringLike, + ThresholdInput extends DefaultThresholdInput = DefaultThresholdInput +> = ValueOf, T>>; + +type OmitType = { + [key in keyof T]: Omit; +}; + +export type PickScaleConfigWithoutType< + T extends ScaleType, + Output = DefaultOutput, + DiscreteInput extends StringLike = StringLike, + ThresholdInput extends DefaultThresholdInput = DefaultThresholdInput +> = ValueOf>, T>>; + +export type ScaleConfig< + Output = DefaultOutput, + DiscreteInput extends StringLike = StringLike, + ThresholdInput extends DefaultThresholdInput = DefaultThresholdInput +> = ValueOf>; + +export type ScaleConfigWithoutType< + Output = DefaultOutput, + DiscreteInput extends StringLike = StringLike, + ThresholdInput extends DefaultThresholdInput = DefaultThresholdInput +> = PickScaleConfigWithoutType; + +export type ScaleConfigToD3Scale< + Config extends ScaleConfig, + Output = DefaultOutput, + DiscreteInput extends StringLike = StringLike, + ThresholdInput extends DefaultThresholdInput = DefaultThresholdInput +> = ScaleTypeToD3Scale[Config['type']]; diff --git a/packages/vx-scale/src/types/ScaleInterpolate.ts b/packages/vx-scale/src/types/ScaleInterpolate.ts new file mode 100644 index 000000000..8d72fe443 --- /dev/null +++ b/packages/vx-scale/src/types/ScaleInterpolate.ts @@ -0,0 +1,14 @@ +export type ScaleInterpolate = + | 'rgb' + | 'lab' + | 'hcl' + | 'hsl' + | 'hsl-long' + | 'hcl-long' + | 'cubehelix' + | 'cubehelix-long'; + +export interface ScaleInterpolateParams { + type: 'rgb' | 'cubehelix' | 'cubehelix-long'; + gamma?: number; +} diff --git a/packages/vx-scale/src/updateScale.ts b/packages/vx-scale/src/updateScale.ts new file mode 100644 index 000000000..2a212fb54 --- /dev/null +++ b/packages/vx-scale/src/updateScale.ts @@ -0,0 +1,152 @@ +import { PickScaleConfigWithoutType, ScaleConfigWithoutType } from './types/ScaleConfig'; +import { DefaultThresholdInput, D3Scale, PickD3Scale } from './types/Scale'; +import { StringLike, DefaultOutput } from './types/Base'; +import scaleOperator, { ALL_OPERATORS } from './operators/scaleOperator'; + +const applyAllOperators = scaleOperator(...ALL_OPERATORS); + +// Overload function signature for more strict typing, e.g., +// If the scale is a ScaleLinear, the config is a linear config. + +function updateScale< + Output = DefaultOutput, + DiscreteInput extends StringLike = StringLike, + ThresholdInput extends DefaultThresholdInput = DefaultThresholdInput +>( + scale: PickD3Scale<'linear', Output>, + config: PickScaleConfigWithoutType<'linear', Output>, +): PickD3Scale<'linear', Output>; + +function updateScale< + Output = DefaultOutput, + DiscreteInput extends StringLike = StringLike, + ThresholdInput extends DefaultThresholdInput = DefaultThresholdInput +>( + scale: PickD3Scale<'log', Output>, + config: PickScaleConfigWithoutType<'log', Output>, +): PickD3Scale<'log', Output>; + +function updateScale< + Output = DefaultOutput, + DiscreteInput extends StringLike = StringLike, + ThresholdInput extends DefaultThresholdInput = DefaultThresholdInput +>( + scale: PickD3Scale<'pow', Output>, + config: PickScaleConfigWithoutType<'pow', Output>, +): PickD3Scale<'pow', Output>; + +function updateScale< + Output = DefaultOutput, + DiscreteInput extends StringLike = StringLike, + ThresholdInput extends DefaultThresholdInput = DefaultThresholdInput +>( + scale: PickD3Scale<'sqrt', Output>, + config: PickScaleConfigWithoutType<'sqrt', Output>, +): PickD3Scale<'sqrt', Output>; + +function updateScale< + Output = DefaultOutput, + DiscreteInput extends StringLike = StringLike, + ThresholdInput extends DefaultThresholdInput = DefaultThresholdInput +>( + scale: PickD3Scale<'symlog', Output>, + config: PickScaleConfigWithoutType<'symlog', Output>, +): PickD3Scale<'symlog', Output>; + +function updateScale< + Output = DefaultOutput, + DiscreteInput extends StringLike = StringLike, + ThresholdInput extends DefaultThresholdInput = DefaultThresholdInput +>( + scale: PickD3Scale<'time', Output>, + config: PickScaleConfigWithoutType<'time', Output>, +): PickD3Scale<'time', Output>; + +function updateScale< + Output = DefaultOutput, + DiscreteInput extends StringLike = StringLike, + ThresholdInput extends DefaultThresholdInput = DefaultThresholdInput +>( + scale: PickD3Scale<'utc', Output>, + config: PickScaleConfigWithoutType<'utc', Output>, +): PickD3Scale<'utc', Output>; + +function updateScale< + Output = DefaultOutput, + DiscreteInput extends StringLike = StringLike, + ThresholdInput extends DefaultThresholdInput = DefaultThresholdInput +>( + scale: PickD3Scale<'quantile', Output>, + config: PickScaleConfigWithoutType<'quantile', Output>, +): PickD3Scale<'quantile', Output>; + +function updateScale< + Output = DefaultOutput, + DiscreteInput extends StringLike = StringLike, + ThresholdInput extends DefaultThresholdInput = DefaultThresholdInput +>( + scale: PickD3Scale<'quantize', Output>, + config: PickScaleConfigWithoutType<'quantize', Output>, +): PickD3Scale<'quantize', Output>; + +function updateScale< + Output = DefaultOutput, + DiscreteInput extends StringLike = StringLike, + ThresholdInput extends DefaultThresholdInput = DefaultThresholdInput +>( + scale: PickD3Scale<'threshold', Output, StringLike, ThresholdInput>, + config: PickScaleConfigWithoutType<'threshold', Output, StringLike, ThresholdInput>, +): PickD3Scale<'threshold', Output, StringLike, ThresholdInput>; + +function updateScale< + Output = DefaultOutput, + DiscreteInput extends StringLike = StringLike, + ThresholdInput extends DefaultThresholdInput = DefaultThresholdInput +>( + scale: PickD3Scale<'ordinal', Output, DiscreteInput>, + config: PickScaleConfigWithoutType<'ordinal', Output, DiscreteInput>, +): PickD3Scale<'ordinal', Output, DiscreteInput>; + +function updateScale< + Output = DefaultOutput, + DiscreteInput extends StringLike = StringLike, + ThresholdInput extends DefaultThresholdInput = DefaultThresholdInput +>( + scale: PickD3Scale<'point', Output, DiscreteInput>, + config: PickScaleConfigWithoutType<'point', Output, DiscreteInput>, +): PickD3Scale<'point', Output, DiscreteInput>; + +function updateScale< + Output = DefaultOutput, + DiscreteInput extends StringLike = StringLike, + ThresholdInput extends DefaultThresholdInput = DefaultThresholdInput +>( + scale: PickD3Scale<'band', Output, DiscreteInput>, + config: PickScaleConfigWithoutType<'band', Output, DiscreteInput>, +): PickD3Scale<'band', Output, DiscreteInput>; + +function updateScale< + Output = DefaultOutput, + DiscreteInput extends StringLike = StringLike, + ThresholdInput extends DefaultThresholdInput = DefaultThresholdInput, + Scale extends D3Scale = D3Scale< + Output, + DiscreteInput, + ThresholdInput + > +>(scale: Scale, config?: undefined): Scale; + +// Actual implementation + +function updateScale< + Output, + DiscreteInput extends StringLike, + ThresholdInput extends DefaultThresholdInput +>( + scale: D3Scale, + config?: ScaleConfigWithoutType, +) { + return applyAllOperators(scale.copy(), config); +} + +export default updateScale; diff --git a/packages/vx-scale/src/util/updateScale.ts b/packages/vx-scale/src/util/updateScale.ts deleted file mode 100644 index 62c08e152..000000000 --- a/packages/vx-scale/src/util/updateScale.ts +++ /dev/null @@ -1,9 +0,0 @@ -const has = Object.prototype.hasOwnProperty; - -export default function updateScale(scale: any, { ...args }: any = {}) { - const nextScale = scale.copy(); - Object.keys(args).forEach(key => { - if (has.call(nextScale, key)) nextScale[key](args[key]); - }); - return nextScale; -} diff --git a/packages/vx-scale/src/utils/createColorInterpolator.ts b/packages/vx-scale/src/utils/createColorInterpolator.ts new file mode 100644 index 000000000..c3f482e85 --- /dev/null +++ b/packages/vx-scale/src/utils/createColorInterpolator.ts @@ -0,0 +1,43 @@ +import { + interpolateRgb, + interpolateLab, + interpolateHcl, + interpolateHclLong, + interpolateHsl, + interpolateHslLong, + interpolateCubehelix, + interpolateCubehelixLong, +} from 'd3-interpolate'; +import { ScaleInterpolateParams, ScaleInterpolate } from '../types/ScaleInterpolate'; + +const interpolatorMap = { + lab: interpolateLab, + hcl: interpolateHcl, + 'hcl-long': interpolateHclLong, + hsl: interpolateHsl, + 'hsl-long': interpolateHslLong, + cubehelix: interpolateCubehelix, + 'cubehelix-long': interpolateCubehelixLong, + rgb: interpolateRgb, +} as const; + +export default function createColorInterpolator( + interpolate: ScaleInterpolate | ScaleInterpolateParams, +) { + switch (interpolate) { + case 'lab': + case 'hcl': + case 'hcl-long': + case 'hsl': + case 'hsl-long': + case 'cubehelix': + case 'cubehelix-long': + case 'rgb': + return interpolatorMap[interpolate]; + default: + } + + const { type, gamma } = interpolate; + const interpolator = interpolatorMap[type]; + return typeof gamma === 'undefined' ? interpolator : interpolator.gamma(gamma); +} diff --git a/packages/vx-scale/src/utils/inferScaleType.ts b/packages/vx-scale/src/utils/inferScaleType.ts new file mode 100644 index 000000000..146e0aa16 --- /dev/null +++ b/packages/vx-scale/src/utils/inferScaleType.ts @@ -0,0 +1,55 @@ +import { ScaleTime } from 'd3-scale'; +import { StringLike } from '../types/Base'; +import { DefaultThresholdInput, D3Scale } from '../types/Scale'; +import { ScaleType } from '../types/ScaleConfig'; +import isUtcScale from './isUtcScale'; + +export default function inferScaleType< + Output, + DiscreteInput extends StringLike, + ThresholdInput extends DefaultThresholdInput +>(scale: D3Scale): ScaleType { + // Try a sequence of typeguards to figure out the scale type + + if ('paddingInner' in scale) { + return 'band'; + } + + if ('padding' in scale) { + return 'point'; + } + + if ('quantiles' in scale) { + return 'quantile'; + } + + if ('base' in scale) { + return 'log'; + } + + if ('exponent' in scale) { + return scale.exponent() === 0.5 ? 'sqrt' : 'pow'; + } + + if ('constant' in scale) { + return 'symlog'; + } + + if ('clamp' in scale) { + // Linear, Time or Utc scales + if (scale.ticks()[0] instanceof Date) { + return isUtcScale(scale as ScaleTime) ? 'utc' : 'time'; + } + return 'linear'; + } + + if ('nice' in scale) { + return 'quantize'; + } + + if ('invertExtent' in scale) { + return 'threshold'; + } + + return 'ordinal'; +} diff --git a/packages/vx-scale/src/utils/isUtcScale.ts b/packages/vx-scale/src/utils/isUtcScale.ts new file mode 100644 index 000000000..6c4a187d2 --- /dev/null +++ b/packages/vx-scale/src/utils/isUtcScale.ts @@ -0,0 +1,16 @@ +import { ScaleTime } from 'd3-scale'; + +const TEST_TIME = new Date(Date.UTC(2020, 1, 2, 3, 4, 5)); +const TEST_FORMAT = '%Y-%m-%d %H:%M'; + +/** + * Check if the scale is UTC or Time scale + * When local time is equal to UTC, always return true + * @param scale time or utc scale + */ +export default function isUtcScale(scale: ScaleTime) { + // The only difference between time and utc scale is + // whether the tick format function is utcFormat or timeFormat + const output = scale.tickFormat(1, TEST_FORMAT)(TEST_TIME); + return output === '2020-02-02 03:04'; +} diff --git a/packages/vx-scale/test/createScale.test.ts b/packages/vx-scale/test/createScale.test.ts new file mode 100644 index 000000000..012d84f62 --- /dev/null +++ b/packages/vx-scale/test/createScale.test.ts @@ -0,0 +1,101 @@ +import { createScale } from '../src'; + +describe('createScale()', () => { + it('linear', () => { + const scale = createScale({ type: 'linear', domain: [0, 10], range: [2, 4] }); + expect(scale(5)).toEqual(3); + }); + it('fallbacks to linear if type is not defined', () => { + const scale = createScale({ domain: [0, 10], range: [2, 4] }); + expect(scale(5)).toEqual(3); + }); + it('log', () => { + const scale = createScale({ + type: 'log', + base: 2, + domain: [2, 8], + range: [1, 3], + }); + expect(scale(4).toFixed(2)).toEqual('2.00'); + }); + it('pow', () => { + const scale = createScale({ type: 'pow', exponent: 2, domain: [1, 3], range: [2, 18] }); + expect(scale(2)).toEqual(8); + }); + it('sqrt', () => { + const scale = createScale({ type: 'sqrt', domain: [1, 9], range: [1, 3] }); + expect(scale(4)).toEqual(2); + }); + it('symlog', () => { + const scale = createScale({ type: 'symlog', domain: [1, 9], range: [1, 3], constant: 2 }); + expect(scale(4).toFixed(2)).toEqual('2.07'); + }); + it('time', () => { + const scale = createScale({ + type: 'time', + domain: [new Date(2020, 0, 1), new Date(2020, 0, 10)], + range: [1, 10], + }); + expect(scale(new Date(2020, 0, 4))).toEqual(4); + }); + it('utc', () => { + const scale = createScale({ + type: 'utc', + domain: [new Date(Date.UTC(2020, 0, 1)), new Date(Date.UTC(2020, 0, 10))], + range: [1, 10], + }); + expect(scale(new Date(Date.UTC(2020, 0, 4)))).toEqual(4); + }); + it('quantile', () => { + const scale = createScale({ type: 'quantile', domain: [1, 3, 5, 7], range: [0, 10] }); + expect(scale(2)).toEqual(0); + }); + it('quantize', () => { + const scale = createScale({ type: 'quantize', domain: [1, 10], range: ['red', 'green'] }); + expect(scale(2)).toEqual('red'); + expect(scale(6)).toEqual('green'); + }); + it('threshold', () => { + const scale = createScale({ + type: 'threshold', + domain: [0, 1] as number[], + range: ['red', 'white', 'green'], + }); + expect(scale(-1)).toEqual('red'); + expect(scale(0)).toEqual('white'); + expect(scale(0.5)).toEqual('white'); + expect(scale(1)).toEqual('green'); + expect(scale(1000)).toEqual('green'); + }); + it('ordinal', () => { + const scale = createScale({ type: 'ordinal', domain: ['pig', 'cat'], range: ['red', 'green'] }); + expect(scale('pig')).toEqual('red'); + expect(scale('cat')).toEqual('green'); + }); + it('point', () => { + const scale = createScale({ + type: 'point', + domain: ['a', 'b', 'c'], + range: [1.1, 3.5], + round: true, + }); + expect(scale('a')).toEqual(1); + expect(scale('b')).toEqual(2); + expect(scale('c')).toEqual(3); + }); + it('band', () => { + const scale = createScale({ + type: 'band', + domain: ['a', 'b', 'c'], + range: [1.1, 3.5], + round: false, + }); + expect(scale('a')).toEqual(1.1); + expect(scale('b')).toEqual(1.9); + expect(scale('c')).toEqual(2.7); + }); + it('invalid type', () => { + // @ts-ignore + expect(createScale({ type: 'invalid' })).toBeDefined(); + }); +}); diff --git a/packages/vx-scale/test/scaleBand.test.ts b/packages/vx-scale/test/scaleBand.test.ts index 19d183e83..0fcd48bcf 100644 --- a/packages/vx-scale/test/scaleBand.test.ts +++ b/packages/vx-scale/test/scaleBand.test.ts @@ -1,34 +1,42 @@ import { scaleBand } from '../src'; describe('scaleBand', () => { - test('it should be defined', () => { + it('should be defined', () => { expect(scaleBand).toBeDefined(); }); - - test('range param should set scale range', () => { - const range = [2, 3] as [number, number]; - const scale = scaleBand({ range }); - expect(scale.range()).toEqual(range); - }); - - test('rangeRound param should set scale range', () => { - const rangeRound = [2, 3] as [number, number]; - const scale = scaleBand({ rangeRound }); - expect(scale.range()).toEqual(rangeRound); - }); - - test('domain param should set scale domain', () => { - const domain = ['a', 'b']; + it('set domain', () => { + const domain = [0, 350]; const scale = scaleBand({ domain }); expect(scale.domain()).toEqual(domain); }); - - test('padding param should set scale padding inner & outer', () => { - const padding = 0.75; - const range = [0, 1] as [number, number]; - const domain = ['a', 'b']; - const scale = scaleBand({ padding, range, domain }); - expect(scale.paddingInner()).toEqual(padding); - expect(scale.paddingOuter()).toEqual(padding); + it('set range', () => { + const scale = scaleBand({ range: [2, 3] }); + expect(scale.range()).toEqual([2, 3]); + }); + it('set align', () => { + expect(scaleBand({ align: 0.5 }).align()).toEqual(0.5); + }); + it('set padding', () => { + expect(scaleBand({ padding: 0.3 }).padding()).toEqual(0.3); + }); + it('set paddingInner', () => { + expect(scaleBand({ paddingInner: 0.7 }).paddingInner()).toEqual(0.7); + }); + it('set paddingOuter', () => { + expect(scaleBand({ paddingOuter: 0.7 }).paddingOuter()).toEqual(0.7); + }); + describe('set round', () => { + it('true', () => { + const scale = scaleBand({ domain: ['a', 'b', 'c'], range: [1.1, 3.5], round: true }); + expect(scale('a')).toEqual(2); + expect(scale('b')).toEqual(2); + expect(scale('c')).toEqual(2); + }); + it('false', () => { + const scale = scaleBand({ domain: ['a', 'b', 'c'], range: [1.1, 3.5], round: false }); + expect(scale('a')).toEqual(1.1); + expect(scale('b')).toEqual(1.9); + expect(scale('c')).toEqual(2.7); + }); }); }); diff --git a/packages/vx-scale/test/scaleLinear.test.ts b/packages/vx-scale/test/scaleLinear.test.ts index 8c9b87a39..39cb53f2c 100644 --- a/packages/vx-scale/test/scaleLinear.test.ts +++ b/packages/vx-scale/test/scaleLinear.test.ts @@ -1,7 +1,103 @@ +import mockConsole from 'jest-mock-console'; import { scaleLinear } from '../src'; -describe('scaleLinear', () => { - test('it should be defined', () => { +describe('scaleLinear()', () => { + it('should be defined', () => { expect(scaleLinear).toBeDefined(); }); + it('set domain', () => { + const domain = [1, 2]; + expect(scaleLinear({ domain: [1, 2] }).domain()).toEqual(domain); + }); + it('set range', () => { + const range = [1, 2]; + expect(scaleLinear({ range: [1, 2] }).range()).toEqual(range); + }); + describe('set clamp', () => { + it('true', () => { + const scale = scaleLinear({ clamp: true }); + expect(scale(10)).toEqual(1); + }); + it('false', () => { + const scale = scaleLinear({ clamp: false }); + expect(scale(10)).toEqual(10); + }); + }); + describe('set (color) interpolate', () => { + it('string', () => { + const scale = scaleLinear({ + domain: [0, 10], + range: ['#ff0000', '#000000'], + interpolate: 'lab', + }); + expect(scale(5)).toEqual('rgb(122, 27, 11)'); + }); + it('config object', () => { + const scale = scaleLinear({ + domain: [0, 10], + range: ['#ff0000', '#000000'], + interpolate: { + type: 'rgb', + }, + }); + expect(scale(5)).toEqual('rgb(128, 0, 0)'); + }); + it('config object with gamma', () => { + const scale = scaleLinear({ + domain: [0, 10], + range: ['#ff0000', '#000000'], + interpolate: { + type: 'rgb', + gamma: 0.9, + }, + }); + expect(scale(5)).toEqual('rgb(118, 0, 0)'); + }); + }); + describe('set nice', () => { + it('true', () => { + const scale = scaleLinear({ domain: [0.1, 0.91], nice: true }); + expect(scale.domain()).toEqual([0.1, 1]); + }); + it('false', () => { + const scale = scaleLinear({ domain: [0.1, 0.91], nice: false }); + expect(scale.domain()).toEqual([0.1, 0.91]); + }); + }); + describe('set round', () => { + it('true', () => { + const scale = scaleLinear({ domain: [0, 10], range: [0, 10], round: true }); + expect(scale(2.2)).toEqual(2); + expect(scale(2.6)).toEqual(3); + }); + it('false', () => { + const scale = scaleLinear({ domain: [0, 10], range: [0, 10], round: false }); + expect(scale(2.2)).toEqual(2.2); + expect(scale(2.6)).toEqual(2.6); + }); + it('warns if do both interpolate and round', () => { + const restoreConsole = mockConsole(); + scaleLinear({ + domain: [0, 10], + range: [0, 10], + interpolate: 'hsl', + round: true, + }); + expect(console.warn).toHaveBeenCalledTimes(1); + restoreConsole(); + }); + }); + describe('set zero', () => { + it('true', () => { + expect(scaleLinear({ domain: [1, 2], zero: true }).domain()).toEqual([0, 2]); + expect(scaleLinear({ domain: [-2, -1], zero: true }).domain()).toEqual([-2, 0]); + expect(scaleLinear({ domain: [1, -2], zero: true }).domain()).toEqual([1, -2]); + expect(scaleLinear({ domain: [-2, 3], zero: true }).domain()).toEqual([-2, 3]); + }); + it('false', () => { + expect(scaleLinear({ domain: [1, 2], zero: false }).domain()).toEqual([1, 2]); + expect(scaleLinear({ domain: [-2, -1], zero: false }).domain()).toEqual([-2, -1]); + expect(scaleLinear({ domain: [-2, 3], zero: false }).domain()).toEqual([-2, 3]); + }); + }); }); diff --git a/packages/vx-scale/test/scaleLog.test.ts b/packages/vx-scale/test/scaleLog.test.ts index 838e36b76..52a33d18f 100644 --- a/packages/vx-scale/test/scaleLog.test.ts +++ b/packages/vx-scale/test/scaleLog.test.ts @@ -1,7 +1,56 @@ import { scaleLog } from '../src'; -describe('scaleLog', () => { - test('it should be defined', () => { +describe('scaleLog()', () => { + it('should be defined', () => { expect(scaleLog).toBeDefined(); }); + it('set domain', () => { + const domain = [1, 2]; + expect(scaleLog({ domain: [1, 2] }).domain()).toEqual(domain); + }); + it('set range', () => { + const range = [1, 2]; + expect(scaleLog({ range: [1, 2] }).range()).toEqual(range); + }); + it('set base', () => { + expect(scaleLog({ base: 2 }).base()).toEqual(2); + }); + describe('set clamp', () => { + it('true', () => { + const scale = scaleLog({ range: [1, 2], clamp: true }); + expect(scale(100)).toEqual(2); + }); + it('false', () => { + const scale = scaleLog({ range: [1, 2], clamp: false }); + expect(scale(100)).toEqual(3); + }); + }); + it('set (color) interpolate', () => { + const scale = scaleLog({ + domain: [1, 100], + range: ['#ff0000', '#000000'], + interpolate: 'lab', + }); + expect(scale(10)).toEqual('rgb(122, 27, 11)'); + }); + describe('set nice', () => { + it('true', () => { + const scale = scaleLog({ domain: [0.1, 0.91], nice: true }); + expect(scale.domain()).toEqual([0.1, 1]); + }); + it('false', () => { + const scale = scaleLog({ domain: [0.1, 0.91], nice: false }); + expect(scale.domain()).toEqual([0.1, 0.91]); + }); + }); + describe('set round', () => { + it('true', () => { + const scale = scaleLog({ domain: [1, 10], range: [1, 10], round: true }); + expect(scale(2.2)).toEqual(4); + }); + it('false', () => { + const scale = scaleLog({ domain: [1, 10], range: [1, 10], round: false }); + expect(scale(5).toFixed(2)).toEqual('7.29'); + }); + }); }); diff --git a/packages/vx-scale/test/scaleOrdinal.test.ts b/packages/vx-scale/test/scaleOrdinal.test.ts index 3752a55e7..892906be9 100644 --- a/packages/vx-scale/test/scaleOrdinal.test.ts +++ b/packages/vx-scale/test/scaleOrdinal.test.ts @@ -1,7 +1,21 @@ import { scaleOrdinal } from '../src'; describe('scaleOrdinal', () => { - test('it should be defined', () => { + it('should be defined', () => { expect(scaleOrdinal).toBeDefined(); }); + it('set domain', () => { + const domain = ['noodle', 'burger']; + const scale = scaleOrdinal({ domain }); + expect(scale.domain()).toEqual(domain); + }); + it('set range', () => { + const range = ['red', 'green']; + const scale = scaleOrdinal({ range }); + expect(scale.range()).toEqual(range); + }); + it('set unknown', () => { + const scale = scaleOrdinal({ domain: ['noodle', 'burger'], unknown: 'green' }); + expect(scale('sandwich')).toEqual('green'); + }); }); diff --git a/packages/vx-scale/test/scalePoint.test.ts b/packages/vx-scale/test/scalePoint.test.ts index 8c5580428..3a239a442 100644 --- a/packages/vx-scale/test/scalePoint.test.ts +++ b/packages/vx-scale/test/scalePoint.test.ts @@ -1,7 +1,36 @@ import { scalePoint } from '../src'; describe('scalePoint', () => { - test('it should be defined', () => { + it('should be defined', () => { expect(scalePoint).toBeDefined(); }); + it('set domain', () => { + const domain = [0, 350]; + const scale = scalePoint({ domain }); + expect(scale.domain()).toEqual(domain); + }); + it('set range', () => { + const scale = scalePoint({ range: [2, 3] }); + expect(scale.range()).toEqual([2, 3]); + }); + it('set align', () => { + expect(scalePoint({ align: 0.5 }).align()).toEqual(0.5); + }); + it('set padding', () => { + expect(scalePoint({ padding: 0.5 }).padding()).toEqual(0.5); + }); + describe('set round', () => { + it('true', () => { + const scale = scalePoint({ domain: ['a', 'b', 'c'], range: [1.1, 3.5], round: true }); + expect(scale('a')).toEqual(1); + expect(scale('b')).toEqual(2); + expect(scale('c')).toEqual(3); + }); + it('false', () => { + const scale = scalePoint({ domain: ['a', 'b', 'c'], range: [1.1, 3.5], round: false }); + expect(scale('a')).toEqual(1.1); + expect(scale('b')).toEqual(2.3); + expect(scale('c')).toEqual(3.5); + }); + }); }); diff --git a/packages/vx-scale/test/scalePower.test.ts b/packages/vx-scale/test/scalePower.test.ts index f748d6c98..303c720db 100644 --- a/packages/vx-scale/test/scalePower.test.ts +++ b/packages/vx-scale/test/scalePower.test.ts @@ -1,25 +1,70 @@ import { scalePower } from '../src'; -describe('scalePower', () => { - test('it should be defined', () => { +describe('scalePower()', () => { + it('should be defined', () => { expect(scalePower).toBeDefined(); }); - - test('exponent param should set scale exponent', () => { - const exponent = 2; - const scale = scalePower({ exponent }); - expect(scale.exponent()).toEqual(exponent); + it('set domain', () => { + const domain = [1, 2]; + expect(scalePower({ domain: [1, 2] }).domain()).toEqual(domain); }); - - test('range param should set scale range', () => { - const range = [2, 3]; - const scale = scalePower({ range }); - expect(scale.range()).toEqual(range); + it('set range', () => { + const range = [1, 2]; + expect(scalePower({ range: [1, 2] }).range()).toEqual(range); }); - - test('domain param should set scale domain', () => { - const domain = [0, 350]; - const scale = scalePower({ domain }); - expect(scale.domain()).toEqual(domain); + it('set exponent', () => { + expect(scalePower({ exponent: 3 }).exponent()).toEqual(3); + }); + describe('set clamp', () => { + it('true', () => { + const scale = scalePower({ clamp: true }); + expect(scale(10)).toEqual(1); + }); + it('false', () => { + const scale = scalePower({ clamp: false }); + expect(scale(10)).toEqual(10); + }); + }); + it('set (color) interpolate', () => { + const scale = scalePower({ + domain: [0, 10], + range: ['#ff0000', '#000000'], + interpolate: 'lab', + }); + expect(scale(5)).toEqual('rgb(122, 27, 11)'); + }); + describe('set nice', () => { + it('true', () => { + const scale = scalePower({ domain: [0.1, 0.91], nice: true }); + expect(scale.domain()).toEqual([0.1, 1]); + }); + it('false', () => { + const scale = scalePower({ domain: [0.1, 0.91], nice: false }); + expect(scale.domain()).toEqual([0.1, 0.91]); + }); + }); + describe('set round', () => { + it('true', () => { + const scale = scalePower({ domain: [0, 10], range: [0, 10], round: true }); + expect(scale(2.2)).toEqual(2); + expect(scale(2.6)).toEqual(3); + }); + it('false', () => { + const scale = scalePower({ domain: [0, 10], range: [0, 10], round: false }); + expect(scale(2.2)).toEqual(2.2); + expect(scale(2.6)).toEqual(2.6); + }); + }); + describe('set zero', () => { + it('true', () => { + expect(scalePower({ domain: [1, 2], zero: true }).domain()).toEqual([0, 2]); + expect(scalePower({ domain: [-2, -1], zero: true }).domain()).toEqual([-2, 0]); + expect(scalePower({ domain: [-2, 3], zero: true }).domain()).toEqual([-2, 3]); + }); + it('false', () => { + expect(scalePower({ domain: [1, 2], zero: false }).domain()).toEqual([1, 2]); + expect(scalePower({ domain: [-2, -1], zero: false }).domain()).toEqual([-2, -1]); + expect(scalePower({ domain: [-2, 3], zero: false }).domain()).toEqual([-2, 3]); + }); }); }); diff --git a/packages/vx-scale/test/scaleQuantile.test.ts b/packages/vx-scale/test/scaleQuantile.test.ts index e518fd510..ac943567d 100644 --- a/packages/vx-scale/test/scaleQuantile.test.ts +++ b/packages/vx-scale/test/scaleQuantile.test.ts @@ -1,19 +1,17 @@ import { scaleQuantile } from '../src'; describe('scaleQuantile', () => { - test('it should be defined', () => { + it('should be defined', () => { expect(scaleQuantile).toBeDefined(); }); - - test('range param should set scale range', () => { - const range = [2, 3]; - const scale = scaleQuantile({ range }); - expect(scale.range()).toEqual(range); - }); - - test('domain param should set scale domain', () => { + it('set domain', () => { const domain = [0, 350]; const scale = scaleQuantile({ domain }); expect(scale.domain()).toEqual(domain); }); + it('set range', () => { + const range = [2, 3]; + const scale = scaleQuantile({ range }); + expect(scale.range()).toEqual(range); + }); }); diff --git a/packages/vx-scale/test/scaleQuantize.test.ts b/packages/vx-scale/test/scaleQuantize.test.ts index aca992c7e..826c6bf91 100644 --- a/packages/vx-scale/test/scaleQuantize.test.ts +++ b/packages/vx-scale/test/scaleQuantize.test.ts @@ -1,19 +1,37 @@ import { scaleQuantize } from '../src'; describe('scaleQuantize', () => { - test('it should be defined', () => { + it('should be defined', () => { expect(scaleQuantize).toBeDefined(); }); - - test('range param should set scale range', () => { - const range = [2, 3]; - const scale = scaleQuantize({ range }); - expect(scale.range()).toEqual(range); + it('set domain', () => { + const domain = [1, 2]; + expect(scaleQuantize({ domain: [1, 2] }).domain()).toEqual(domain); }); - - test('domain param should set scale domain', () => { - const domain = [0, 350] as [number, number]; - const scale = scaleQuantize({ domain }); - expect(scale.domain()).toEqual(domain); + it('set range', () => { + const range = [1, 2]; + expect(scaleQuantize({ range: [1, 2] }).range()).toEqual(range); + }); + describe('set nice', () => { + it('true', () => { + const scale = scaleQuantize({ domain: [0.1, 0.91], nice: true }); + expect(scale.domain()).toEqual([0.1, 1]); + }); + it('false', () => { + const scale = scaleQuantize({ domain: [0.1, 0.91], nice: false }); + expect(scale.domain()).toEqual([0.1, 0.91]); + }); + }); + describe('set zero', () => { + it('true', () => { + expect(scaleQuantize({ domain: [1, 2], zero: true }).domain()).toEqual([0, 2]); + expect(scaleQuantize({ domain: [-2, -1], zero: true }).domain()).toEqual([-2, 0]); + expect(scaleQuantize({ domain: [-2, 3], zero: true }).domain()).toEqual([-2, 3]); + }); + it('false', () => { + expect(scaleQuantize({ domain: [1, 2], zero: false }).domain()).toEqual([1, 2]); + expect(scaleQuantize({ domain: [-2, -1], zero: false }).domain()).toEqual([-2, -1]); + expect(scaleQuantize({ domain: [-2, 3], zero: false }).domain()).toEqual([-2, 3]); + }); }); }); diff --git a/packages/vx-scale/test/scaleSqrt.test.ts b/packages/vx-scale/test/scaleSqrt.test.ts index 9c1b0491a..3cdf88c76 100644 --- a/packages/vx-scale/test/scaleSqrt.test.ts +++ b/packages/vx-scale/test/scaleSqrt.test.ts @@ -1,24 +1,68 @@ import { scaleSqrt } from '../src'; -describe('scaleSqrt', () => { - test('it should be defined', () => { +describe('scaleSqrt()', () => { + it('should be defined', () => { expect(scaleSqrt).toBeDefined(); }); - - test('exponent param should be 0.5', () => { - const scale = scaleSqrt({}); - expect(scale.exponent()).toEqual(0.5); + it('set domain', () => { + const domain = [1, 2]; + expect(scaleSqrt({ domain: [1, 2] }).domain()).toEqual(domain); }); - - test('range param should set scale range', () => { - const range = [2, 3]; - const scale = scaleSqrt({ range }); - expect(scale.range()).toEqual(range); + it('set range', () => { + const range = [1, 2]; + expect(scaleSqrt({ range: [1, 2] }).range()).toEqual(range); }); - - test('domain param should set scasle domain', () => { - const domain = [0, 350]; - const scale = scaleSqrt({ domain }); - expect(scale.domain()).toEqual(domain); + it('exponent is 0.5', () => { + expect(scaleSqrt({}).exponent()).toEqual(0.5); + }); + describe('set clamp', () => { + it('true', () => { + const scale = scaleSqrt({ clamp: true }); + expect(scale(10)).toEqual(1); + }); + it('false', () => { + const scale = scaleSqrt({ clamp: false }); + expect(scale(10).toFixed(2)).toEqual('3.16'); + }); + }); + it('set (color) interpolate', () => { + const scale = scaleSqrt({ + domain: [0, 10], + range: ['#ff0000', '#000000'], + interpolate: 'lab', + }); + expect(scale(5)).toEqual('rgb(73, 23, 9)'); + }); + describe('set nice', () => { + it('true', () => { + const scale = scaleSqrt({ domain: [0.1, 0.91], nice: true }); + expect(scale.domain()).toEqual([0.1, 1]); + }); + it('false', () => { + const scale = scaleSqrt({ domain: [0.1, 0.91], nice: false }); + expect(scale.domain()).toEqual([0.1, 0.91]); + }); + }); + describe('set round', () => { + it('true', () => { + const scale = scaleSqrt({ domain: [0, 4], range: [0, 2], round: true }); + expect(scale(3)).toEqual(2); + }); + it('false', () => { + const scale = scaleSqrt({ domain: [0, 4], range: [0, 2], round: false }); + expect(scale(3).toFixed(2)).toEqual('1.73'); + }); + }); + describe('set zero', () => { + it('true', () => { + expect(scaleSqrt({ domain: [1, 2], zero: true }).domain()).toEqual([0, 2]); + expect(scaleSqrt({ domain: [-2, -1], zero: true }).domain()).toEqual([-2, 0]); + expect(scaleSqrt({ domain: [-2, 3], zero: true }).domain()).toEqual([-2, 3]); + }); + it('false', () => { + expect(scaleSqrt({ domain: [1, 2], zero: false }).domain()).toEqual([1, 2]); + expect(scaleSqrt({ domain: [-2, -1], zero: false }).domain()).toEqual([-2, -1]); + expect(scaleSqrt({ domain: [-2, 3], zero: false }).domain()).toEqual([-2, 3]); + }); }); }); diff --git a/packages/vx-scale/test/scaleSymlog.test.ts b/packages/vx-scale/test/scaleSymlog.test.ts index a2f176a65..017f2dbb6 100644 --- a/packages/vx-scale/test/scaleSymlog.test.ts +++ b/packages/vx-scale/test/scaleSymlog.test.ts @@ -1,19 +1,52 @@ import { scaleSymlog } from '../src'; describe('scaleSymlog', () => { - test('it should be defined', () => { + it('should be defined', () => { expect(scaleSymlog).toBeDefined(); }); - - test('range param should set scale range', () => { - const range = [2, 3]; - const scale = scaleSymlog({ range }); - expect(scale.range()).toEqual(range); + it('set domain', () => { + const domain = [1, 2]; + expect(scaleSymlog({ domain: [1, 2] }).domain()).toEqual(domain); }); - - test('constant param should set scale constant', () => { + it('set range', () => { + const range = [1, 2]; + expect(scaleSymlog({ range: [1, 2] }).range()).toEqual(range); + }); + describe('set clamp', () => { + it('true', () => { + const scale = scaleSymlog({ clamp: true }); + expect(scale(10)).toEqual(1); + }); + it('false', () => { + const scale = scaleSymlog({ clamp: false }); + expect(scale(10).toFixed(2)).toEqual('3.46'); + }); + }); + it('set constant', () => { const constant = 2; const scale = scaleSymlog({ constant }); expect(scale.constant()).toEqual(constant); }); + describe('set nice', () => { + it('true', () => { + const scale = scaleSymlog({ domain: [0.1, 0.91], nice: true }); + expect(scale.domain()).toEqual([0.1, 1]); + }); + it('false', () => { + const scale = scaleSymlog({ domain: [0.1, 0.91], nice: false }); + expect(scale.domain()).toEqual([0.1, 0.91]); + }); + }); + describe('set zero', () => { + it('true', () => { + expect(scaleSymlog({ domain: [1, 2], zero: true }).domain()).toEqual([0, 2]); + expect(scaleSymlog({ domain: [-2, -1], zero: true }).domain()).toEqual([-2, 0]); + expect(scaleSymlog({ domain: [-2, 3], zero: true }).domain()).toEqual([-2, 3]); + }); + it('false', () => { + expect(scaleSymlog({ domain: [1, 2], zero: false }).domain()).toEqual([1, 2]); + expect(scaleSymlog({ domain: [-2, -1], zero: false }).domain()).toEqual([-2, -1]); + expect(scaleSymlog({ domain: [-2, 3], zero: false }).domain()).toEqual([-2, 3]); + }); + }); }); diff --git a/packages/vx-scale/test/scaleThreshold.test.ts b/packages/vx-scale/test/scaleThreshold.test.ts index 19783ff90..44c2c70b0 100644 --- a/packages/vx-scale/test/scaleThreshold.test.ts +++ b/packages/vx-scale/test/scaleThreshold.test.ts @@ -1,19 +1,17 @@ import { scaleThreshold } from '../src'; describe('scaleThreshold', () => { - test('it should be defined', () => { + it('should be defined', () => { expect(scaleThreshold).toBeDefined(); }); - - test('range param should set scale range', () => { - const range = [2, 3]; - const scale = scaleThreshold({ range }); - expect(scale.range()).toEqual(range); - }); - - test('domain param should set scale domain', () => { + it('set domain', () => { const domain = [0, 350]; const scale = scaleThreshold({ domain }); expect(scale.domain()).toEqual(domain); }); + it('set range', () => { + const range = [2, 3]; + const scale = scaleThreshold({ range }); + expect(scale.range()).toEqual(range); + }); }); diff --git a/packages/vx-scale/test/scaleTime.test.ts b/packages/vx-scale/test/scaleTime.test.ts index 209cf23cb..71673bf01 100644 --- a/packages/vx-scale/test/scaleTime.test.ts +++ b/packages/vx-scale/test/scaleTime.test.ts @@ -1,7 +1,93 @@ +import TimezoneMock from 'timezone-mock'; import { scaleTime } from '../src'; -describe('scaleTime', () => { - test('it should be defined', () => { +describe('scaleTime()', () => { + let domain: [Date, Date]; + let unniceDomain: [Date, Date]; + + beforeAll(() => { + TimezoneMock.register('US/Pacific'); + domain = [new Date(2020, 0, 1), new Date(2020, 0, 10)]; + unniceDomain = [new Date(2020, 0, 1), new Date(2020, 0, 9, 20)]; + }); + + afterAll(() => { + TimezoneMock.unregister(); + }); + + it('should be defined', () => { expect(scaleTime).toBeDefined(); }); + it('set domain', () => { + expect(scaleTime({ domain }).domain()).toEqual(domain); + }); + it('set range', () => { + const range = [1, 2]; + expect(scaleTime({ range: [1, 2] }).range()).toEqual(range); + }); + describe('set clamp', () => { + it('true', () => { + const scale = scaleTime({ domain, range: [0, 10], clamp: true }); + expect(scale(new Date(2019, 11, 31))).toEqual(0); + }); + it('false', () => { + const scale = scaleTime({ domain, range: [0, 10], clamp: false }); + expect(scale(new Date(2019, 11, 31)).toFixed(2)).toEqual('-1.11'); + }); + }); + it('set (color) interpolate', () => { + const scale = scaleTime({ + domain, + range: ['#ff0000', '#000000'], + interpolate: 'lab', + }); + expect(scale(new Date(2020, 0, 5))).toEqual('rgb(136, 28, 11)'); + }); + describe('set nice', () => { + it('true', () => { + const scale = scaleTime({ + domain: unniceDomain, + nice: true, + }); + expect(scale.domain()).toEqual(domain); + }); + it('false', () => { + const scale = scaleTime({ domain: unniceDomain, nice: false }); + expect(scale.domain()).toEqual(unniceDomain); + }); + it('number', () => { + const scale = scaleTime({ domain: unniceDomain, nice: 5 }); + expect(scale.domain()).toEqual([new Date(2020, 0, 1), new Date(2020, 0, 11)]); + }); + it('time unit string', () => { + const scale = scaleTime({ domain: unniceDomain, nice: 'hour' }); + expect(scale.domain()).toEqual(unniceDomain); + }); + it('nice object', () => { + const scale = scaleTime({ domain: unniceDomain, nice: { interval: 'hour', step: 3 } }); + expect(scale.domain()).toEqual([new Date(2020, 0, 1), new Date(2020, 0, 9, 21)]); + }); + it('invalid nice object', () => { + const scale = scaleTime({ domain: unniceDomain, nice: { interval: 'hour', step: NaN } }); + expect(scale.domain()).toEqual(unniceDomain); + }); + }); + describe('set round', () => { + it('true', () => { + const scale = scaleTime({ + domain, + range: [1, 5], + round: true, + }); + expect(scale(new Date(2020, 0, 5))).toEqual(3); + }); + it('false', () => { + const scale = scaleTime({ + domain, + range: [1, 5], + round: false, + }); + expect(scale(new Date(2020, 0, 5)).toFixed(2)).toEqual('2.78'); + }); + }); }); diff --git a/packages/vx-scale/test/scaleUtc.test.ts b/packages/vx-scale/test/scaleUtc.test.ts index bc91fc620..c3062579a 100644 --- a/packages/vx-scale/test/scaleUtc.test.ts +++ b/packages/vx-scale/test/scaleUtc.test.ts @@ -1,7 +1,95 @@ +import TimezoneMock from 'timezone-mock'; import { scaleUtc } from '../src'; -describe('scaleUtc', () => { - test('it should be defined', () => { +describe('scaleUtc()', () => { + let domain: [Date, Date]; + let unniceDomain: [Date, Date]; + + beforeAll(() => { + TimezoneMock.register('US/Pacific'); + domain = [new Date(Date.UTC(2020, 0, 1)), new Date(Date.UTC(2020, 0, 10))]; + unniceDomain = [new Date(Date.UTC(2020, 0, 1)), new Date(Date.UTC(2020, 0, 9, 20))]; + }); + + afterAll(() => { + TimezoneMock.unregister(); + }); + + it('should be defined', () => { expect(scaleUtc).toBeDefined(); }); + it('set domain', () => { + expect(scaleUtc({ domain }).domain()).toEqual(domain); + }); + it('set range', () => { + const range = [1, 2]; + expect(scaleUtc({ range: [1, 2] }).range()).toEqual(range); + }); + describe('set clamp', () => { + it('true', () => { + const scale = scaleUtc({ domain, range: [0, 10], clamp: true }); + expect(scale(new Date(Date.UTC(2019, 11, 31)))).toEqual(0); + }); + it('false', () => { + const scale = scaleUtc({ domain, range: [0, 10], clamp: false }); + expect(scale(new Date(Date.UTC(2019, 11, 31))).toFixed(2)).toEqual('-1.11'); + }); + }); + it('set (color) interpolate', () => { + const scale = scaleUtc({ + domain, + range: ['#ff0000', '#000000'], + interpolate: 'lab', + }); + expect(scale(new Date(Date.UTC(2020, 0, 5)))).toEqual('rgb(136, 28, 11)'); + }); + describe('set nice', () => { + it('true', () => { + const scale = scaleUtc({ + domain: unniceDomain, + nice: true, + }); + expect(scale.domain()).toEqual(domain); + }); + it('false', () => { + const scale = scaleUtc({ domain: unniceDomain, nice: false }); + expect(scale.domain()).toEqual(unniceDomain); + }); + it('number', () => { + const scale = scaleUtc({ domain: unniceDomain, nice: 5 }); + expect(scale.domain()).toEqual([ + new Date(Date.UTC(2020, 0, 1)), + new Date(Date.UTC(2020, 0, 11)), + ]); + }); + it('time unit string', () => { + const scale = scaleUtc({ domain: unniceDomain, nice: 'hour' }); + expect(scale.domain()).toEqual(unniceDomain); + }); + it('nice object', () => { + const scale = scaleUtc({ domain: unniceDomain, nice: { interval: 'hour', step: 3 } }); + expect(scale.domain()).toEqual([ + new Date(Date.UTC(2020, 0, 1)), + new Date(Date.UTC(2020, 0, 9, 21)), + ]); + }); + }); + describe('set round', () => { + it('true', () => { + const scale = scaleUtc({ + domain, + range: [1, 5], + round: true, + }); + expect(scale(new Date(Date.UTC(2020, 0, 5)))).toEqual(3); + }); + it('false', () => { + const scale = scaleUtc({ + domain, + range: [1, 5], + round: false, + }); + expect(scale(new Date(Date.UTC(2020, 0, 5))).toFixed(2)).toEqual('2.78'); + }); + }); }); diff --git a/packages/vx-scale/test/updateScale.test.ts b/packages/vx-scale/test/updateScale.test.ts index 6745e9619..c81307d83 100644 --- a/packages/vx-scale/test/updateScale.test.ts +++ b/packages/vx-scale/test/updateScale.test.ts @@ -1,28 +1,117 @@ -import { updateScale, scaleLinear } from '../src'; +import TimezoneMock from 'timezone-mock'; +import { + updateScale, + scaleLinear, + scaleLog, + scalePower, + scaleSqrt, + scaleSymlog, + scaleTime, + scaleUtc, + scaleQuantile, + scaleOrdinal, + scalePoint, + scaleBand, + scaleQuantize, + scaleThreshold, +} from '../src'; describe('updateScale', () => { - test('it should be defined', () => { + it('should be defined', () => { expect(updateScale).toBeDefined(); }); - - test('it should return a new copy of the scale', () => { - const domain = [0, 350]; - const range = [0, 2]; - const scale = scaleLinear({ range, domain }); + it('should return a new copy of the scale', () => { + const scale = scaleLinear(); const nextScale = updateScale(scale); expect(scale).not.toBe(nextScale); }); - - test('it should update the new copy of the scale', () => { - const domain = [0, 350]; - const newDomain = [200, 300]; - const range = [0, 2]; - const scale = scaleLinear({ range, domain }); - const nextScale = updateScale(scale, { - domain: newDomain, + it('linear', () => { + const scale = updateScale(scaleLinear(), { domain: [0, 10], range: [2, 4] }); + expect(scale(5)).toEqual(3); + }); + it('log', () => { + const scale = updateScale(scaleLog(), { + base: 2, + domain: [2, 8], + range: [1, 3], }); - expect(scale).not.toBe(nextScale); - expect(nextScale.domain()).toEqual(newDomain); - expect(scale.domain()).toEqual(domain); + expect(scale(4).toFixed(2)).toEqual('2.00'); + }); + it('pow', () => { + const scale = updateScale(scalePower(), { exponent: 2, domain: [1, 3], range: [2, 18] }); + expect(scale(2)).toEqual(8); + }); + it('sqrt', () => { + const scale = updateScale(scaleSqrt(), { domain: [1, 9], range: [1, 3] }); + expect(scale(4)).toEqual(2); + }); + it('symlog', () => { + const scale = updateScale(scaleSymlog(), { domain: [1, 9], range: [1, 3], constant: 2 }); + expect(scale(4).toFixed(2)).toEqual('2.07'); + }); + it('time', () => { + TimezoneMock.register('US/Pacific'); + const scale = updateScale(scaleTime(), { + domain: [new Date(2020, 0, 1), new Date(2020, 0, 10)], + range: [1, 10], + }); + expect(scale(new Date(2020, 0, 4))).toEqual(4); + TimezoneMock.unregister(); + }); + it('utc', () => { + const scale = updateScale(scaleUtc(), { + domain: [new Date(Date.UTC(2020, 0, 1)), new Date(Date.UTC(2020, 0, 10))], + range: [1, 10], + }); + expect(scale(new Date(Date.UTC(2020, 0, 4)))).toEqual(4); + }); + it('quantile', () => { + const scale = updateScale(scaleQuantile(), { domain: [1, 3, 5, 7], range: [0, 10] }); + expect(scale(2)).toEqual(0); + }); + it('quantize', () => { + const scale = updateScale(scaleQuantize(), { domain: [1, 10], range: ['red', 'green'] }); + expect(scale(2)).toEqual('red'); + expect(scale(6)).toEqual('green'); + }); + it('threshold', () => { + const scale = updateScale(scaleThreshold(), { + domain: [0, 1] as number[], + range: ['red', 'white', 'green'], + }); + expect(scale(-1)).toEqual('red'); + expect(scale(0)).toEqual('white'); + expect(scale(0.5)).toEqual('white'); + expect(scale(1)).toEqual('green'); + expect(scale(1000)).toEqual('green'); + }); + it('ordinal', () => { + const scale = updateScale(scaleOrdinal(), { domain: ['pig', 'cat'], range: ['red', 'green'] }); + expect(scale('pig')).toEqual('red'); + expect(scale('cat')).toEqual('green'); + }); + it('point', () => { + const scale = updateScale(scalePoint(), { + domain: ['a', 'b', 'c'], + range: [1.1, 3.5], + round: true, + }); + expect(scale('a')).toEqual(1); + expect(scale('b')).toEqual(2); + expect(scale('c')).toEqual(3); + }); + it('band', () => { + const scale = updateScale(scaleBand(), { + domain: ['a', 'b', 'c'], + range: [1.1, 3.5], + round: false, + }); + expect(scale('a')).toEqual(1.1); + expect(scale('b')).toEqual(1.9); + expect(scale('c')).toEqual(2.7); + }); + it('invalid type', () => { + // @ts-ignore + expect(updateScale(scaleLinear(), { type: 'invalid' })).toBeDefined(); }); }); diff --git a/packages/vx-scale/test/utils/inferScaleType.test.ts b/packages/vx-scale/test/utils/inferScaleType.test.ts new file mode 100644 index 000000000..3d5c441ac --- /dev/null +++ b/packages/vx-scale/test/utils/inferScaleType.test.ts @@ -0,0 +1,68 @@ +import { + scaleLinear, + scaleLog, + scalePow, + scaleSqrt, + scaleSymlog, + scaleTime, + scaleUtc, + scaleQuantile, + scaleQuantize, + scaleThreshold, + scaleOrdinal, + scalePoint, + scaleBand, +} from 'd3-scale'; +import TimezoneMock from 'timezone-mock'; +import inferScaleType from '../../src/utils/inferScaleType'; + +describe('inferScaleType(scale)', () => { + it('linear scale', () => { + expect(inferScaleType(scaleLinear())).toEqual('linear'); + }); + it('log scale', () => { + expect(inferScaleType(scaleLog())).toEqual('log'); + }); + it('pow scale', () => { + expect(inferScaleType(scalePow())).toEqual('pow'); + }); + it('sqrt scale', () => { + expect(inferScaleType(scaleSqrt())).toEqual('sqrt'); + }); + it('symlog scale', () => { + expect(inferScaleType(scaleSymlog())).toEqual('symlog'); + }); + describe('time scale', () => { + it('returns time when local time is not UTC', () => { + TimezoneMock.register('US/Pacific'); + expect(inferScaleType(scaleTime())).toEqual('time'); + TimezoneMock.unregister(); + }); + it('returns utc when local time is UTC', () => { + TimezoneMock.register('UTC'); + expect(inferScaleType(scaleTime())).toEqual('utc'); + TimezoneMock.unregister(); + }); + }); + it('utc scale', () => { + expect(inferScaleType(scaleUtc())).toEqual('utc'); + }); + it('quantile scale', () => { + expect(inferScaleType(scaleQuantile())).toEqual('quantile'); + }); + it('quantize scale', () => { + expect(inferScaleType(scaleQuantize())).toEqual('quantize'); + }); + it('threshold scale', () => { + expect(inferScaleType(scaleThreshold())).toEqual('threshold'); + }); + it('ordinal scale', () => { + expect(inferScaleType(scaleOrdinal())).toEqual('ordinal'); + }); + it('point scale', () => { + expect(inferScaleType(scalePoint())).toEqual('point'); + }); + it('band scale', () => { + expect(inferScaleType(scaleBand())).toEqual('band'); + }); +}); diff --git a/packages/vx-scale/test/utils/isUtcScale.test.ts b/packages/vx-scale/test/utils/isUtcScale.test.ts new file mode 100644 index 000000000..bc1e3f335 --- /dev/null +++ b/packages/vx-scale/test/utils/isUtcScale.test.ts @@ -0,0 +1,21 @@ +import { scaleUtc, scaleTime } from 'd3-scale'; +import TimezoneMock from 'timezone-mock'; +import isUtcScale from '../../src/utils/isUtcScale'; + +describe('isUtcScale(scale)', () => { + it('returns true for utc scale', () => { + expect(isUtcScale(scaleUtc())).toEqual(true); + }); + describe('for time scale', () => { + it('returns false when local time is not UTC', () => { + TimezoneMock.register('US/Pacific'); + expect(isUtcScale(scaleTime())).toEqual(false); + TimezoneMock.unregister(); + }); + it('returns true when local time is UTC', () => { + TimezoneMock.register('UTC'); + expect(isUtcScale(scaleTime())).toEqual(true); + TimezoneMock.unregister(); + }); + }); +}); diff --git a/packages/vx-stats/src/ViolinPlot.tsx b/packages/vx-stats/src/ViolinPlot.tsx index 0b3c0ebe2..fa10fb0cc 100644 --- a/packages/vx-stats/src/ViolinPlot.tsx +++ b/packages/vx-stats/src/ViolinPlot.tsx @@ -25,8 +25,8 @@ export default function ViolinPlot({ className, data, width = 10, - count = (d: any) => (d && d.count) || 0, - value = (d: any) => (d && d.value) || 0, + count = (d?: { count?: number }) => d?.count || 0, + value = (d?: { value?: number }) => d?.value || 0, valueScale, horizontal, children, @@ -36,7 +36,8 @@ export default function ViolinPlot({ const center = (horizontal ? top : left) + width / 2; const binCounts = data.map(bin => count(bin)); const widthScale = scaleLinear({ - rangeRound: [0, width / 2], + range: [0, width / 2], + round: true, domain: [0, Math.max(...binCounts)], }); diff --git a/packages/vx-stats/test/BoxPlot.test.tsx b/packages/vx-stats/test/BoxPlot.test.tsx index 9dd207ccf..666674957 100644 --- a/packages/vx-stats/test/BoxPlot.test.tsx +++ b/packages/vx-stats/test/BoxPlot.test.tsx @@ -10,7 +10,8 @@ const { boxPlot: boxPlotData } = computeStats(data); const { min, firstQuartile, median, thirdQuartile, max, outliers } = boxPlotData; const valueScale = scaleLinear({ - rangeRound: [10, 0], + range: [10, 0], + round: true, domain: [0, 10], }); diff --git a/packages/vx-stats/test/ViolinPlot.test.tsx b/packages/vx-stats/test/ViolinPlot.test.tsx index 6d5f501c2..5bcb3a2ff 100644 --- a/packages/vx-stats/test/ViolinPlot.test.tsx +++ b/packages/vx-stats/test/ViolinPlot.test.tsx @@ -9,7 +9,8 @@ const data = [1, 2, 3, 4, 5, 6, 6, 7, 8, 9, 1]; const { binData } = computeStats(data); const valueScale = scaleLinear({ - rangeRound: [10, 0], + range: [10, 0], + round: true, domain: [0, 10], }); diff --git a/yarn.lock b/yarn.lock index b8bae5166..20959cc91 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2678,6 +2678,11 @@ resolved "https://registry.yarnpkg.com/@types/d3-chord/-/d3-chord-1.0.9.tgz#ccc5de03ff079025491b7aa6b750670a140b45ae" integrity sha512-UA6lI9CVW5cT5Ku/RV4hxoFn4mKySHm7HEgodtfRthAj1lt9rKZEPon58vyYfk+HIAm33DtJJgZwMXy2QgyPXw== +"@types/d3-color@*": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-1.2.2.tgz#80cf7cfff7401587b8f89307ba36fe4a576bc7cf" + integrity sha512-6pBxzJ8ZP3dYEQ4YjQ+NVbQaOflfgXq/JbDiS99oLobM2o72uAST4q6yPxHv6FOTCRC/n35ktuo8pvw/S4M7sw== + "@types/d3-format@^1.3.1": version "1.3.1" resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-1.3.1.tgz#35bf88264bd6bcda39251165bb827f67879c4384" @@ -2695,6 +2700,13 @@ resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-1.1.6.tgz#4c017521900813ea524c9ecb8d7985ec26a9ad9a" integrity sha512-vvSaIDf/Ov0o3KwMT+1M8+WbnnlRiGjlGD5uvk83a1mPCTd/E5x12bUJ/oP55+wUY/4Kb5kc67rVpVGJ2KUHxg== +"@types/d3-interpolate@^1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-1.3.1.tgz#1c280511f622de9b0b47d463fa55f9a4fd6f5fc8" + integrity sha512-z8Zmi08XVwe8e62vP6wcA+CNuRhpuUU5XPEfqpG0hRypDE5BWNthQHB1UNWWDB7ojCbGaN4qBdsWp5kWxhT1IQ== + dependencies: + "@types/d3-color" "*" + "@types/d3-path@*", "@types/d3-path@^1.0.8": version "1.0.8" resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-1.0.8.tgz#48e6945a8ff43ee0a1ce85c8cfa2337de85c7c79" @@ -2729,7 +2741,7 @@ resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-2.1.0.tgz#011e0fb7937be34a9a8f580ae1e2f2f1336a8a22" integrity sha512-/myT3I7EwlukNOX2xVdMzb8FRgNzRMpsZddwst9Ld/VFe6LyJyRp0s32l/V9XoUzk+Gqu56F/oGk6507+8BxrA== -"@types/d3-time@*": +"@types/d3-time@*", "@types/d3-time@^1.0.10": version "1.0.10" resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-1.0.10.tgz#d338c7feac93a98a32aac875d1100f92c7b61f4f" integrity sha512-aKf62rRQafDQmSiv1NylKhIMmznsjRN+MnXRXTqHoqm0U/UZzVpdrtRnSIfdiLS616OuC1soYeX1dBg2n1u8Xw== @@ -5219,6 +5231,11 @@ d3-array@1, d3-array@^1.1.1, d3-array@^1.2.0: resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f" integrity sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw== +"d3-array@1.2.0 - 2": + version "2.4.0" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-2.4.0.tgz#87f8b9ad11088769c82b5ea846bcb1cc9393f242" + integrity sha512-KQ41bAF2BMakf/HdKT865ALd4cgND6VcIztVQZUTt0+BH3RWy6ZYnHghVXf6NFjt2ritLr8H1T8LreAAlfiNcw== + d3-chord@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/d3-chord/-/d3-chord-1.0.6.tgz#309157e3f2db2c752f0280fedd35f2067ccbb15f" @@ -5254,7 +5271,7 @@ d3-hierarchy@^1.1.4, d3-hierarchy@^1.1.8: resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz#2f6bee24caaea43f8dc37545fa01628559647a83" integrity sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ== -d3-interpolate@1, d3-interpolate@^1.1.5: +d3-interpolate@1, d3-interpolate@^1.1.5, d3-interpolate@^1.2.0, d3-interpolate@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.4.0.tgz#526e79e2d80daa383f9e0c1c1c7dcc0f0583e987" integrity sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA== @@ -5292,15 +5309,14 @@ d3-scale@^1.0.6: d3-time "1" d3-time-format "2" -d3-scale@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-2.2.2.tgz#4e880e0b2745acaaddd3ede26a9e908a9e17b81f" - integrity sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw== +d3-scale@^3.0.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-3.2.1.tgz#da1684adce7261b4bc7a76fe193d887f0e909e69" + integrity sha512-huz5byJO/6MPpz6Q8d4lg7GgSpTjIZW/l+1MQkzKfu2u8P6hjaXaStOpmyrD6ymKoW87d2QVFCKvSjLwjzx/rA== dependencies: - d3-array "^1.2.0" - d3-collection "1" + d3-array "1.2.0 - 2" d3-format "1" - d3-interpolate "1" + d3-interpolate "^1.2.0" d3-time "1" d3-time-format "2" @@ -5318,7 +5334,7 @@ d3-time-format@2, d3-time-format@^2.0.5: dependencies: d3-time "1" -d3-time@1: +d3-time@1, d3-time@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.1.0.tgz#b1e19d307dae9c900b7e5b25ffc5dcc249a8a0f1" integrity sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA== @@ -8276,6 +8292,11 @@ jest-message-util@^25.5.0: slash "^3.0.0" stack-utils "^1.0.1" +jest-mock-console@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/jest-mock-console/-/jest-mock-console-1.0.1.tgz#07978047735a782d0d4172d1afcabd82f6de9b08" + integrity sha512-Bn+Of/cvz9LOEEeEg5IX5Lsf8D2BscXa3Zl5+vSVJl37yiT8gMAPPKfE09jJOwwu1zbagL11QTrH+L/Gn8udOg== + jest-mock@^25.5.0: version "25.5.0" resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-25.5.0.tgz#a91a54dabd14e37ecd61665d6b6e06360a55387a" @@ -12953,6 +12974,11 @@ timers-browserify@^2.0.4: dependencies: setimmediate "^1.0.4" +timezone-mock@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/timezone-mock/-/timezone-mock-1.1.0.tgz#ac402345b42721538131368876cc17cfbb849da3" + integrity sha512-YBUMlri3qt6yh07Q+zEr7boXLaumRnUA96Y/NJxaapW7BcR9GRp39nyg5XrRm1+h2q0L5LIOzHt78uHeBntdaA== + timsort@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"