feat: add functions for parsing scales (#207)

* feat: add more util functions

* feat: add unit test

* feat: define HasToString

* fix: unit test

* fix: update unit tests

* feat: add scale types

* feat: update scale parsing

* fix: enum

* feat: add color scale extraction

* refactor: create scale from config

* feat: parse more scales and add more test

* feat: add tests for band and point

* test: add more unit tests

* refactor: separate applyXXX into multiple files

* feat: parse nice time

* test: add unit tests

* test: make 100% coverage

* fix: complete coverage

* refactor: update type definitions

* fix: address comments

* fix: add comments for date parts

* fix: build issue

* fix: broken tests
This commit is contained in:
Krist Wongsuphasawat 2019-08-28 12:11:49 -07:00 committed by Yongjie Zhao
parent 3138aaa229
commit 024b318dd2
30 changed files with 1648 additions and 19 deletions

View File

@ -40,7 +40,7 @@
],
"license": "Apache-2.0",
"devDependencies": {
"@superset-ui/build-config": "^0.1.1",
"@superset-ui/build-config": "^0.1.3",
"@superset-ui/commit-config": "^0.0.9",
"fast-glob": "^3.0.1",
"fs-extra": "^8.0.1",
@ -90,6 +90,11 @@
]
},
"typescript": {
"compilerOptions": {
"typeRoots": [
"../../node_modules/vega-lite/typings"
]
},
"include": [
"./storybook/**/*"
]

View File

@ -30,13 +30,17 @@ export default function updateTextNode(
textNode.setAttribute('class', className || '');
}
// clear style
STYLE_FIELDS.forEach((field: keyof TextStyle) => {
textNode.style[field] = null;
});
// Clear style
// Note: multi-word property names are hyphenated and not camel-cased.
textNode.style.removeProperty('font');
textNode.style.removeProperty('font-weight');
textNode.style.removeProperty('font-style');
textNode.style.removeProperty('font-size');
textNode.style.removeProperty('font-family');
textNode.style.removeProperty('letter-spacing');
// apply new style
// Note that the font field will auto-populate other font fields when applicable.
// Apply new style
// Note: the font field will auto-populate other font fields when applicable.
STYLE_FIELDS.filter(
(field: keyof TextStyle) => typeof style[field] !== 'undefined' && style[field] !== null,
).forEach((field: keyof TextStyle) => {

View File

@ -28,10 +28,18 @@
"private": true,
"dependencies": {
"lodash": "^4.17.15",
"@types/d3-scale": "^2.1.1",
"@types/d3-interpolate": "^1.3.1",
"@types/d3-time": "^1.0.10",
"d3-scale": "^3.0.1",
"d3-interpolate": "^1.3.2",
"d3-time": "^1.0.11",
"vega": "^5.4.0",
"vega-expression": "^2.6.0",
"vega-lite": "^3.4.0"
},
"peerDependencies": {
"@superset-ui/color": "^0.12.0",
"@superset-ui/number-format": "^0.12.0",
"@superset-ui/time-format": "^0.12.0"
}

View File

@ -0,0 +1,31 @@
import { parse, codegen } from 'vega-expression';
import { dateTimeExpr } from 'vega-lite/build/src/datetime';
import { DateTime } from '../types/VegaLite';
export default function parseDateTime(dateTime: string | number | DateTime) {
if (typeof dateTime === 'number' || typeof dateTime === 'string') {
return new Date(dateTime);
}
const expression = dateTimeExpr(dateTime, true) as string;
const code = codegen({ globalvar: 'window' })(parse(expression)).code as string;
// Technically the "code" here is safe to eval(),
// but we will use more conservative approach and manually parse at the moment.
const isUtc = code.startsWith('Date.UTC');
const dateParts = code
.replace(/^(Date[.]UTC|new[ ]Date)\(/, '')
.replace(/\)$/, '')
.split(',')
.map((chunk: string) => Number(chunk.trim())) as [
number, // year
number, // month
number, // date
number, // hours
number, // minutes
number, // seconds
number, // milliseconds
];
return isUtc ? new Date(Date.UTC(...dateParts)) : new Date(...dateParts);
}

View File

@ -0,0 +1,11 @@
import { Value } from '../../types/VegaLite';
import { ScaleConfig, D3Scale } from '../../types/Scale';
export default function applyAlign<Output extends Value>(
config: ScaleConfig<Output>,
scale: D3Scale<Output>,
) {
if ('align' in config && typeof config.align !== 'undefined' && 'align' in scale) {
scale.align(config.align);
}
}

View File

@ -0,0 +1,11 @@
import { Value } from '../../types/VegaLite';
import { ScaleConfig, D3Scale } from '../../types/Scale';
export default function applyClamp<Output extends Value>(
config: ScaleConfig<Output>,
scale: D3Scale<Output>,
) {
if ('clamp' in config && typeof config.clamp !== 'undefined' && 'clamp' in scale) {
scale.clamp(config.clamp);
}
}

View File

@ -0,0 +1,21 @@
import { Value } from '../../types/VegaLite';
import { ScaleConfig, D3Scale, TimeScaleConfig } from '../../types/Scale';
import parseDateTime from '../parseDateTime';
import inferElementTypeFromUnionOfArrayTypes from '../../utils/inferElementTypeFromUnionOfArrayTypes';
import { isTimeScale } from '../../typeGuards/Scale';
export default function applyDomain<Output extends Value>(
config: ScaleConfig<Output>,
scale: D3Scale<Output>,
) {
const { domain, reverse, type } = config;
if (typeof domain !== 'undefined') {
const processedDomain = reverse ? domain.slice().reverse() : domain;
if (isTimeScale(scale, type)) {
const timeDomain = processedDomain as TimeScaleConfig['domain'];
scale.domain(inferElementTypeFromUnionOfArrayTypes(timeDomain).map(d => parseDateTime(d)));
} else {
scale.domain(processedDomain);
}
}
}

View File

@ -0,0 +1,16 @@
import { Value } from '../../types/VegaLite';
import { ScaleConfig, D3Scale } from '../../types/Scale';
export default function applyInterpolate<Output extends Value>(
config: ScaleConfig<Output>,
scale: D3Scale<Output>,
) {
if (
'interpolate' in config &&
typeof config.interpolate !== 'undefined' &&
'interpolate' in scale
) {
// TODO: Need to convert interpolate string into interpolate function
throw new Error('"scale.interpolate" is not supported yet.');
}
}

View File

@ -0,0 +1,77 @@
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 { Value, ScaleType, NiceTime } from '../../types/VegaLite';
import { ScaleConfig, D3Scale } from '../../types/Scale';
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,
};
// eslint-disable-next-line complexity
export default function applyNice<Output extends Value>(
config: ScaleConfig<Output>,
scale: D3Scale<Output>,
) {
if ('nice' in config && typeof config.nice !== 'undefined' && 'nice' in scale) {
const { nice } = config;
if (typeof nice === 'boolean') {
if (nice === true) {
scale.nice();
}
} else if (typeof nice === 'number') {
scale.nice(nice);
} else {
const timeScale = scale as ScaleTime<Output, Output>;
const { type } = config;
if (typeof nice === 'string') {
timeScale.nice(type === ScaleType.UTC ? utcIntervals[nice] : localTimeIntervals[nice]);
} else {
const { interval, step } = nice;
const parsedInterval = (type === ScaleType.UTC
? utcIntervals[interval]
: localTimeIntervals[interval]
).every(step);
if (parsedInterval !== null) {
timeScale.nice(parsedInterval as CountableTimeInterval);
}
}
}
}
}

View File

@ -0,0 +1,27 @@
import { Value } from '../../types/VegaLite';
import { ScaleConfig, D3Scale } from '../../types/Scale';
export default function applyPadding<Output extends Value>(
config: ScaleConfig<Output>,
scale: D3Scale<Output>,
) {
if ('padding' in config && typeof config.padding !== 'undefined' && 'padding' in scale) {
scale.padding(config.padding);
}
if (
'paddingInner' in config &&
typeof config.paddingInner !== 'undefined' &&
'paddingInner' in scale
) {
scale.paddingInner(config.paddingInner);
}
if (
'paddingOuter' in config &&
typeof config.paddingOuter !== 'undefined' &&
'paddingOuter' in scale
) {
scale.paddingOuter(config.paddingOuter);
}
}

View File

@ -0,0 +1,21 @@
import { getSequentialSchemeRegistry } from '@superset-ui/color';
import { Value } from '../../types/VegaLite';
import { ScaleConfig, D3Scale } from '../../types/Scale';
export default function applyRange<Output extends Value>(
config: ScaleConfig<Output>,
scale: D3Scale<Output>,
) {
const { range } = config;
if (typeof range === 'undefined') {
if ('scheme' in config && typeof config.scheme !== 'undefined') {
const { scheme } = config;
const colorScheme = getSequentialSchemeRegistry().get(scheme);
if (typeof colorScheme !== 'undefined') {
scale.range(colorScheme.colors as Output[]);
}
}
} else {
scale.range(range);
}
}

View File

@ -0,0 +1,22 @@
import { interpolateRound } from 'd3-interpolate';
import { ScalePoint, ScaleBand } from 'd3-scale';
import { Value } from '../../types/VegaLite';
import { ScaleConfig, D3Scale, ContinuousD3Scale } from '../../types/Scale';
import { HasToString } from '../../types/Base';
export default function applyRound<Output extends Value>(
config: ScaleConfig<Output>,
scale: D3Scale<Output>,
) {
if ('round' in config && typeof config.round !== 'undefined') {
const roundableScale = scale as
| ContinuousD3Scale<number>
| ScalePoint<HasToString>
| ScaleBand<HasToString>;
if ('round' in roundableScale) {
roundableScale.round(config.round);
} else {
roundableScale.interpolate(interpolateRound);
}
}
}

View File

@ -0,0 +1,12 @@
import { Value } from '../../types/VegaLite';
import { ScaleConfig, D3Scale, ContinuousD3Scale } from '../../types/Scale';
export default function applyZero<Output extends Value>(
config: ScaleConfig<Output>,
scale: D3Scale<Output>,
) {
if ('zero' in config && typeof config.zero !== 'undefined') {
const [min, max] = (scale as ContinuousD3Scale<Output>).domain() as number[];
scale.domain([Math.min(0, min), Math.max(0, max)]);
}
}

View File

@ -0,0 +1,63 @@
import { CategoricalColorNamespace } from '@superset-ui/color';
import { ScaleType, Value } from '../../types/VegaLite';
import { ScaleConfig } from '../../types/Scale';
import createScaleFromScaleType from './createScaleFromScaleType';
import applyNice from './applyNice';
import applyZero from './applyZero';
import applyInterpolate from './applyInterpolate';
import applyRound from './applyRound';
import applyDomain from './applyDomain';
import applyRange from './applyRange';
import applyPadding from './applyPadding';
import applyAlign from './applyAlign';
import applyClamp from './applyClamp';
export default function createScaleFromScaleConfig<Output extends Value>(
config: ScaleConfig<Output>,
) {
const { domain, range, reverse } = config;
// Handle categorical color scales
// An ordinal scale without specified range
// is assumed to be a color scale.
if (config.type === ScaleType.ORDINAL && typeof range === 'undefined') {
const scheme = 'scheme' in config ? config.scheme : undefined;
const namespace = 'namespace' in config ? config.namespace : undefined;
const colorScale = CategoricalColorNamespace.getScale(scheme, namespace);
// If domain is also provided,
// ensure the nth item is assigned the nth color
if (typeof domain !== 'undefined') {
const { colors } = colorScale;
(reverse ? domain.slice().reverse() : domain).forEach((value: any, index: number) => {
colorScale.setColor(`${value}`, colors[index % colors.length]);
});
}
// Need to manually cast here to make the unioned output types
// considered function.
// Otherwise have to add type guards before using the scale function.
//
// const scaleFn = createScaleFromScaleConfig(...)
// if (isAFunction(scaleFn)) const encodedValue = scaleFn(10)
//
// CategoricalColorScale is actually a function,
// but TypeScript is not smart enough to realize that by itself.
return (colorScale as unknown) as (val?: any) => string;
}
const scale = createScaleFromScaleType(config);
// domain and range apply to all scales
applyDomain(config, scale);
applyRange(config, scale);
// Sort other properties alphabetically.
applyAlign(config, scale);
applyClamp(config, scale);
applyInterpolate(config, scale);
applyNice(config, scale);
applyPadding(config, scale);
applyRound(config, scale);
applyZero(config, scale);
return scale;
}

View File

@ -0,0 +1,62 @@
import {
scaleLinear,
scaleLog,
scalePow,
scaleSqrt,
scaleTime,
scaleUtc,
scaleQuantile,
scaleQuantize,
scaleThreshold,
scaleOrdinal,
scalePoint,
scaleBand,
} from 'd3-scale';
import { HasToString } from '../../types/Base';
import { ScaleConfig } from '../../types/Scale';
import { ScaleType, Value } from '../../types/VegaLite';
// eslint-disable-next-line complexity
export default function createScaleFromScaleType<Output extends Value>(
config: ScaleConfig<Output>,
) {
switch (config.type) {
case ScaleType.LINEAR:
return scaleLinear<Output>();
case ScaleType.LOG:
return typeof config.base === 'undefined'
? scaleLog<Output>()
: scaleLog<Output>().base(config.base);
case ScaleType.POW:
return typeof config.exponent === 'undefined'
? scalePow<Output>()
: scalePow<Output>().exponent(config.exponent);
case ScaleType.SQRT:
return scaleSqrt<Output>();
case ScaleType.TIME:
return scaleTime<Output>();
case ScaleType.UTC:
return scaleUtc<Output>();
case ScaleType.QUANTILE:
return scaleQuantile<Output>();
case ScaleType.QUANTIZE:
return scaleQuantize<Output>();
case ScaleType.THRESHOLD:
return scaleThreshold<number | string | Date, Output>();
case ScaleType.ORDINAL:
return scaleOrdinal<HasToString, Output>();
case ScaleType.POINT:
return scalePoint<HasToString>();
case ScaleType.BAND:
return scaleBand<HasToString>();
case ScaleType.SYMLOG:
// TODO: d3-scale typings does not include scaleSymlog yet
// needs to patch the declaration file before continue.
throw new Error(`"scale.type = ${config.type}" is not supported yet.`);
case ScaleType.BIN_ORDINAL:
// TODO: Pending scale.bins implementation
throw new Error(`"scale.type = ${config.type}" is not supported yet.`);
default:
return scaleLinear<Output>();
}
}

View File

@ -0,0 +1,23 @@
import { ScaleType } from '../../types/VegaLite';
import {
continuousScaleTypesSet,
discreteScaleTypesSet,
discretizingScaleTypesSet,
} from './scaleCategories';
import { ScaleCategory } from '../../types/Scale';
export default function getScaleCategoryFromScaleType(
scaleType: ScaleType,
): ScaleCategory | undefined {
if (continuousScaleTypesSet.has(scaleType)) {
return 'continuous';
}
if (discreteScaleTypesSet.has(scaleType)) {
return 'discrete';
}
if (discretizingScaleTypesSet.has(scaleType)) {
return 'discretizing';
}
return undefined;
}

View File

@ -0,0 +1,63 @@
import { Type, ScaleType } from '../../types/VegaLite';
import { ChannelType } from '../../types/Channel';
/**
* Sometimes scale type is not specified but can be inferred
* from other fields.
* See https://vega.github.io/vega-lite/docs/scale.html
* @param channelType type of the channel
* @param fieldType type of the field
* @param isBinned is value binned
*/
// eslint-disable-next-line complexity
export default function inferScaleType(
channelType: ChannelType,
fieldType?: Type,
isBinned: boolean = false,
): ScaleType | undefined {
if (fieldType === 'nominal' || fieldType === 'ordinal') {
switch (channelType) {
// For positional (x and y) ordinal and ordinal fields,
// "point" is the default scale type for all marks
// except bar and rect marks, which use "band" scales.
// https://vega.github.io/vega-lite/docs/scale.html
case 'XBand':
case 'YBand':
return ScaleType.BAND;
case 'X':
case 'Y':
case 'Numeric':
return ScaleType.POINT;
case 'Color':
case 'Category':
return ScaleType.ORDINAL;
default:
}
} else if (fieldType === 'quantitative') {
switch (channelType) {
case 'XBand':
case 'YBand':
case 'X':
case 'Y':
case 'Numeric':
return ScaleType.LINEAR;
case 'Color':
return isBinned ? ScaleType.BIN_ORDINAL : ScaleType.LINEAR;
default:
}
} else if (fieldType === 'temporal') {
switch (channelType) {
case 'XBand':
case 'YBand':
case 'X':
case 'Y':
case 'Numeric':
return ScaleType.UTC;
case 'Color':
return ScaleType.LINEAR;
default:
}
}
return undefined;
}

View File

@ -0,0 +1,53 @@
import { ScaleType } from '../../types/VegaLite';
import { CombinedScaleConfig } from '../../types/Scale';
import {
allScaleTypesSet,
allScaleTypes,
continuousDomainScaleTypes,
continuousScaleTypes,
continuousScaleTypesSet,
} from './scaleCategories';
const pointOrBand: ScaleType[] = [ScaleType.POINT, ScaleType.BAND];
const pointOrBandSet = new Set(pointOrBand);
const exceptPointOrBand = allScaleTypes.filter(type => !pointOrBandSet.has(type));
const exceptPointOrBandSet = new Set(exceptPointOrBand);
const continuousOrPointOrBandSet = new Set(continuousScaleTypes.concat(pointOrBand));
const zeroSet = new Set(continuousDomainScaleTypes);
// log scale cannot have zero value
zeroSet.delete(ScaleType.LOG);
// zero is not meaningful for time
zeroSet.delete(ScaleType.TIME);
zeroSet.delete(ScaleType.UTC);
// threshold requires custom domain so zero does not matter
zeroSet.delete(ScaleType.THRESHOLD);
// quantile depends on distribution so zero does not matter
zeroSet.delete(ScaleType.QUANTILE);
const supportedScaleTypes: Record<keyof CombinedScaleConfig, Set<ScaleType>> = {
align: pointOrBandSet,
base: new Set([ScaleType.LOG]),
clamp: continuousScaleTypesSet,
constant: new Set([ScaleType.SYMLOG]),
domain: allScaleTypesSet,
exponent: new Set([ScaleType.POW]),
interpolate: exceptPointOrBandSet,
namespace: new Set([ScaleType.ORDINAL]),
nice: new Set(continuousScaleTypes.concat([ScaleType.QUANTIZE, ScaleType.THRESHOLD])),
padding: continuousOrPointOrBandSet,
paddingInner: new Set([ScaleType.BAND]),
paddingOuter: pointOrBandSet,
range: allScaleTypesSet,
reverse: allScaleTypesSet,
round: continuousOrPointOrBandSet,
scheme: exceptPointOrBandSet,
zero: zeroSet,
};
export default function isPropertySupportedByScaleType(
property: keyof CombinedScaleConfig,
scaleType: ScaleType,
) {
return supportedScaleTypes[property].has(scaleType);
}

View File

@ -0,0 +1,75 @@
import { ScaleType } from '../../types/VegaLite';
// Grouped by domain and range
export const continuousToContinuousScaleTypes: ScaleType[] = [
ScaleType.LINEAR,
ScaleType.POW,
ScaleType.SQRT,
ScaleType.SYMLOG,
ScaleType.LOG,
ScaleType.TIME,
ScaleType.UTC,
];
export const continuousToContinuousScaleTypesSet = new Set(continuousToContinuousScaleTypes);
export const continuousToDiscreteScaleTypes: ScaleType[] = [
ScaleType.QUANTILE,
ScaleType.QUANTIZE,
ScaleType.THRESHOLD,
];
export const continuousToDiscreteScaleTypesSet = new Set(continuousToDiscreteScaleTypes);
// Grouped by Domain
export const continuousDomainScaleTypes: ScaleType[] = continuousToContinuousScaleTypes.concat(
continuousToDiscreteScaleTypes,
);
export const continuousDomainScaleTypesSet = new Set(continuousDomainScaleTypes);
export const discreteDomainScaleTypes: ScaleType[] = [
ScaleType.ORDINAL,
ScaleType.BIN_ORDINAL,
ScaleType.POINT,
ScaleType.BAND,
];
export const discreteDomainScaleTypesSet = new Set(discreteDomainScaleTypes);
// Three broad categories
export const continuousScaleTypes: ScaleType[] = continuousToContinuousScaleTypes;
export const continuousScaleTypesSet = continuousToContinuousScaleTypesSet;
export const discreteScaleTypes: ScaleType[] = [ScaleType.BAND, ScaleType.POINT, ScaleType.ORDINAL];
export const discreteScaleTypesSet = new Set(discreteScaleTypes);
export const discretizingScaleTypes: ScaleType[] = [
ScaleType.BIN_ORDINAL,
ScaleType.QUANTILE,
ScaleType.QUANTIZE,
ScaleType.THRESHOLD,
];
export const discretizingScaleTypesSet = new Set(discretizingScaleTypes);
// Others
export const timeScaleTypes: ScaleType[] = [ScaleType.TIME, ScaleType.UTC];
export const timeScaleTypesSet = new Set(timeScaleTypes);
export const allScaleTypes = [
ScaleType.LINEAR,
ScaleType.LOG,
ScaleType.POW,
ScaleType.SQRT,
ScaleType.SYMLOG,
ScaleType.TIME,
ScaleType.UTC,
ScaleType.QUANTILE,
ScaleType.QUANTIZE,
ScaleType.THRESHOLD,
ScaleType.BIN_ORDINAL,
ScaleType.ORDINAL,
ScaleType.POINT,
ScaleType.BAND,
];
export const allScaleTypesSet = new Set(allScaleTypes);

View File

@ -0,0 +1,24 @@
import { CategoricalColorScale } from '@superset-ui/color';
import { ScaleTime } from 'd3-scale';
import { D3Scale } from '../types/Scale';
import { Value, ScaleType } from '../types/VegaLite';
import { timeScaleTypesSet } from '../parsers/scale/scaleCategories';
export function isCategoricalColorScale<Output extends Value = Value>(
scale: D3Scale<Output> | CategoricalColorScale,
): scale is CategoricalColorScale {
return scale instanceof CategoricalColorScale;
}
export function isD3Scale<Output extends Value = Value>(
scale: D3Scale<Output> | CategoricalColorScale,
): scale is D3Scale<Output> {
return !isCategoricalColorScale(scale);
}
export function isTimeScale<Output extends Value = Value>(
scale: D3Scale<Output> | CategoricalColorScale,
scaleType: ScaleType,
): scale is ScaleTime<Output, Output> {
return scale && timeScaleTypesSet.has(scaleType);
}

View File

@ -3,3 +3,6 @@ export type Unarray<T> = T extends Array<infer U> ? U : T;
/** T or an array of T */
export type MayBeArray<T> = T | T[];
/** A value that has .toString() function */
export type HasToString = { toString(): string };

View File

@ -1,19 +1,224 @@
import { Value, DateTime, ScaleType, SchemeParams } from './VegaLite';
import {
ScaleOrdinal,
ScaleLinear,
ScaleLogarithmic,
ScalePower,
ScaleTime,
ScaleQuantile,
ScaleQuantize,
ScaleThreshold,
ScalePoint,
ScaleBand,
} from 'd3-scale';
import { Value, DateTime, NiceTime, ScaleType, Scale as VegaLiteScale } from './VegaLite';
import { HasToString } from './Base';
export interface Scale<Output extends Value = Value> {
type?: ScaleType;
// Pick properties inherited from vega-lite
// and overrides a few properties.
// Then make the specific scales pick
// from this interface to share property documentation
// (which is useful for auto-complete/intellisense)
// and add `type` property as discriminant of union type.
export interface CombinedScaleConfig<Output extends Value = Value>
extends Pick<
VegaLiteScale,
| 'align'
| 'base'
| 'clamp'
| 'constant'
| 'exponent'
| 'interpolate'
| 'padding'
| 'paddingInner'
| 'paddingOuter'
| 'reverse'
| 'round'
| 'zero'
> {
// These fields have different types from original vega-lite
/**
* domain of the scale
*/
domain?: number[] | string[] | boolean[] | DateTime[];
paddingInner?: number;
paddingOuter?: number;
/**
* range of the scale
*/
range?: Output[];
clamp?: boolean;
nice?: boolean;
/** color scheme name */
scheme?: string | SchemeParams;
/** vega-lite does not have this */
/**
* name of the color scheme.
* vega-lite also support SchemeParams object
* but encodeable only accepts string at the moment
*/
scheme?: string;
/**
* color namespace.
* vega-lite does not have this field
*/
namespace?: string;
/**
* Extending the domain so that it starts and ends on nice round values. This method typically modifies the scales 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 };
}
type PickFromCombinedScaleConfig<
Output extends Value,
Fields extends keyof CombinedScaleConfig
> = Pick<CombinedScaleConfig<Output>, 'domain' | 'range' | 'reverse' | Fields>;
export interface LinearScaleConfig<Output extends Value = Value>
extends PickFromCombinedScaleConfig<
Output,
'clamp' | 'interpolate' | 'nice' | 'padding' | 'round' | 'scheme' | 'zero'
> {
type: 'linear';
}
export interface LogScaleConfig<Output extends Value = Value>
extends PickFromCombinedScaleConfig<
Output,
'base' | 'clamp' | 'interpolate' | 'nice' | 'padding' | 'round' | 'scheme' | 'zero'
> {
type: 'log';
}
export interface PowScaleConfig<Output extends Value = Value>
extends PickFromCombinedScaleConfig<
Output,
'clamp' | 'exponent' | 'interpolate' | 'nice' | 'padding' | 'round' | 'scheme'
> {
type: 'pow';
}
export interface SqrtScaleConfig<Output extends Value = Value>
extends PickFromCombinedScaleConfig<
Output,
'clamp' | 'interpolate' | 'nice' | 'padding' | 'round' | 'scheme' | 'zero'
> {
type: 'sqrt';
}
export interface SymlogScaleConfig<Output extends Value = Value>
extends PickFromCombinedScaleConfig<
Output,
'clamp' | 'constant' | 'interpolate' | 'nice' | 'padding' | 'round' | 'scheme' | 'zero'
> {
type: 'symlog';
}
interface BaseTimeScaleConfig<Output extends Value>
extends PickFromCombinedScaleConfig<
Output,
'clamp' | 'interpolate' | 'nice' | 'padding' | 'round' | 'scheme'
> {
domain?: number[] | string[] | DateTime[];
}
export interface TimeScaleConfig<Output extends Value = Value> extends BaseTimeScaleConfig<Output> {
type: 'time';
}
export interface UtcScaleConfig<Output extends Value = Value> extends BaseTimeScaleConfig<Output> {
type: 'utc';
}
export interface QuantileScaleConfig<Output extends Value = Value>
extends PickFromCombinedScaleConfig<Output, 'interpolate' | 'scheme'> {
type: 'quantile';
}
export interface QuantizeScaleConfig<Output extends Value = Value>
extends PickFromCombinedScaleConfig<Output, 'interpolate' | 'nice' | 'scheme' | 'zero'> {
type: 'quantize';
}
export interface ThresholdScaleConfig<Output extends Value = Value>
extends PickFromCombinedScaleConfig<Output, 'interpolate' | 'nice' | 'scheme'> {
type: 'threshold';
}
export interface BinOrdinalScaleConfig<Output extends Value = Value>
extends PickFromCombinedScaleConfig<Output, 'interpolate' | 'scheme'> {
type: 'bin-ordinal';
}
export interface OrdinalScaleConfig<Output extends Value = Value>
extends PickFromCombinedScaleConfig<Output, 'interpolate' | 'scheme' | 'namespace'> {
type: 'ordinal';
}
export interface PointScaleConfig<Output extends Value = Value>
extends PickFromCombinedScaleConfig<Output, 'align' | 'padding' | 'round'> {
type: 'point';
}
export interface BandScaleConfig<Output extends Value = Value>
extends PickFromCombinedScaleConfig<
Output,
'align' | 'padding' | 'paddingInner' | 'paddingOuter' | 'round'
> {
type: 'band';
}
export type ScaleConfig<Output extends Value = Value> =
| LinearScaleConfig<Output>
| LogScaleConfig<Output>
| PowScaleConfig<Output>
| SqrtScaleConfig<Output>
| SymlogScaleConfig<Output>
| TimeScaleConfig<Output>
| UtcScaleConfig<Output>
| QuantileScaleConfig<Output>
| QuantizeScaleConfig<Output>
| ThresholdScaleConfig<Output>
| BinOrdinalScaleConfig<Output>
| OrdinalScaleConfig<Output>
| PointScaleConfig<Output>
| BandScaleConfig<Output>;
export interface WithScale<Output extends Value = Value> {
scale?: Scale<Output>;
scale?: Partial<ScaleConfig<Output>>;
}
/** Each ScaleCategory contains one or more ScaleType */
export type ScaleCategory = 'continuous' | 'discrete' | 'discretizing';
export interface ScaleTypeToD3ScaleType<Output extends Value = Value> {
[ScaleType.LINEAR]: ScaleLinear<Output, Output>;
[ScaleType.LOG]: ScaleLogarithmic<Output, Output>;
[ScaleType.POW]: ScalePower<Output, Output>;
[ScaleType.SQRT]: ScalePower<Output, Output>;
[ScaleType.SYMLOG]: ScaleLogarithmic<Output, Output>;
[ScaleType.TIME]: ScaleTime<Output, Output>;
[ScaleType.UTC]: ScaleTime<Output, Output>;
[ScaleType.QUANTILE]: ScaleQuantile<Output>;
[ScaleType.QUANTIZE]: ScaleQuantize<Output>;
[ScaleType.THRESHOLD]: ScaleThreshold<number | string | Date, Output>;
[ScaleType.BIN_ORDINAL]: ScaleOrdinal<HasToString, Output>;
[ScaleType.ORDINAL]: ScaleOrdinal<HasToString, Output>;
[ScaleType.POINT]: ScalePoint<HasToString>;
[ScaleType.BAND]: ScaleBand<HasToString>;
}
export type ContinuousD3Scale<Output extends Value = Value> =
| ScaleLinear<Output, Output>
| ScaleLogarithmic<Output, Output>
| ScalePower<Output, Output>
| ScaleTime<Output, Output>;
export type D3Scale<Output extends Value = Value> =
| ContinuousD3Scale<Output>
| ScaleQuantile<Output>
| ScaleQuantize<Output>
| ScaleThreshold<number | string | Date, Output>
| ScaleOrdinal<HasToString, Output>
| ScalePoint<HasToString>
| ScaleBand<HasToString>;

View File

@ -2,5 +2,5 @@
export { ValueDef, Value } from 'vega-lite/build/src/channeldef';
export { DateTime } from 'vega-lite/build/src/datetime';
export { SchemeParams, ScaleType } from 'vega-lite/build/src/scale';
export { SchemeParams, ScaleType, Scale, NiceTime } from 'vega-lite/build/src/scale';
export { Type } from 'vega-lite/build/src/type';

View File

@ -0,0 +1,10 @@
type ArrayElement<A> = A extends Array<infer Elem> ? Elem : never;
/**
* Type workaround for https://github.com/Microsoft/TypeScript/issues/7294#issuecomment-465794460
* to avoid error "Cannot invoke an expression whose type lacks a call signature"
* when using array.map
*/
export default function inferElementTypeFromUnionOfArrayTypes<T>(array: T): ArrayElement<T>[] {
return array as any;
}

View File

@ -0,0 +1,29 @@
import parseDateTime from '../../src/parsers/parseDateTime';
describe('parseDateTime(dateTime)', () => {
it('parses number', () => {
expect(parseDateTime(1560384000000)).toEqual(new Date(Date.UTC(2019, 5, 13)));
});
it('parses string', () => {
expect(parseDateTime('2019-01-01')).toEqual(new Date('2019-01-01'));
});
it('parse DateTime object', () => {
expect(
parseDateTime({
year: 2019,
month: 6,
date: 14,
}),
).toEqual(new Date(2019, 5, 14));
});
it('handles utc correctly', () => {
expect(
parseDateTime({
year: 2019,
month: 6,
date: 14,
utc: true,
}),
).toEqual(new Date(Date.UTC(2019, 5, 14)));
});
});

View File

@ -0,0 +1,624 @@
import {
getSequentialSchemeRegistry,
SequentialScheme,
getCategoricalSchemeRegistry,
CategoricalScheme,
} from '@superset-ui/color';
import { ScaleLinear, ScaleTime } from 'd3-scale';
import createScaleFromScaleConfig from '../../../src/parsers/scale/createScaleFromScaleConfig';
describe('createScaleFromScaleConfig(config)', () => {
describe('default', () => {
it('returns linear scale', () => {
// @ts-ignore
const scale = createScaleFromScaleConfig({});
expect(scale(1)).toEqual(1);
});
});
describe('linear scale', () => {
it('basic', () => {
const scale = createScaleFromScaleConfig({
type: 'linear',
domain: [0, 10],
range: [0, 100],
});
expect(scale(10)).toEqual(100);
});
it('with reverse domain', () => {
const scale = createScaleFromScaleConfig({
type: 'linear',
domain: [0, 10],
range: [0, 100],
reverse: true,
});
expect(scale(10)).toEqual(0);
});
it('with color scheme as range', () => {
getSequentialSchemeRegistry().registerValue(
'test-scheme',
new SequentialScheme({
id: 'test-scheme',
colors: ['#ff0000', '#ffff00'],
}),
);
const scale = createScaleFromScaleConfig({
type: 'linear',
domain: [0, 10],
scheme: 'test-scheme',
});
expect(scale(0)).toEqual('rgb(255, 0, 0)');
expect(scale(10)).toEqual('rgb(255, 255, 0)');
getSequentialSchemeRegistry().remove('test-scheme');
});
it('with color scheme as range, but no color scheme available', () => {
getSequentialSchemeRegistry().clearDefaultKey();
const scale = createScaleFromScaleConfig({
type: 'linear',
domain: [0, 10],
scheme: 'test-scheme',
});
expect(scale(0)).toEqual(0);
expect(scale(10)).toEqual(1);
});
it('with nice', () => {
const scale = createScaleFromScaleConfig({
type: 'linear',
domain: [0, 9.9],
range: [0, 100],
nice: true,
});
expect(scale(10)).toEqual(100);
});
it('with nice=false', () => {
const scale = createScaleFromScaleConfig({
type: 'linear',
domain: [0, 9.9],
range: [0, 100],
nice: false,
});
expect(Number(scale(10)).toFixed(2)).toEqual('101.01');
});
it('with nice is number', () => {
const scale = createScaleFromScaleConfig({
type: 'linear',
domain: [0, 9.9],
range: [0, 100],
nice: 3,
});
expect((scale as ScaleLinear<number, number>).ticks(3)).toEqual([0, 5, 10]);
});
it('with clamp', () => {
const scale = createScaleFromScaleConfig({
type: 'linear',
domain: [0, 10],
range: [0, 100],
clamp: true,
});
expect(scale(-10000)).toEqual(0);
expect(scale(10000)).toEqual(100);
});
it('with round', () => {
const scale = createScaleFromScaleConfig({
type: 'linear',
domain: [0, 10],
range: [0, 10],
round: true,
});
expect(scale(9.9)).toEqual(10);
});
it('with zero', () => {
const scale = createScaleFromScaleConfig({
type: 'linear',
domain: [2, 10],
range: [0, 10],
zero: true,
});
expect(scale(5)).toEqual(5);
});
it('with zero (negative domain)', () => {
const scale = createScaleFromScaleConfig({
type: 'linear',
domain: [-10, -2],
range: [0, 10],
zero: true,
});
expect(scale(-5)).toEqual(5);
});
it('with zero (no effect)', () => {
const scale = createScaleFromScaleConfig({
type: 'linear',
domain: [-5, 5],
range: [0, 10],
zero: true,
});
expect(scale(0)).toEqual(5);
});
it('with interpolate', () => {
expect(() =>
createScaleFromScaleConfig({
type: 'linear',
interpolate: 'cubehelix',
}),
).toThrowError('"scale.interpolate" is not supported yet.');
});
});
describe('log scale', () => {
it('basic', () => {
const scale = createScaleFromScaleConfig({
type: 'log',
domain: [1, 100],
range: [1, 10],
});
expect(scale(10)).toEqual(5.5);
expect(scale(100)).toEqual(10);
});
it('with base', () => {
const scale = createScaleFromScaleConfig({
type: 'log',
domain: [1, 16],
base: 2,
});
expect(scale(8)).toEqual(0.75);
});
});
describe('power scale', () => {
it('basic', () => {
const scale = createScaleFromScaleConfig({
type: 'pow',
domain: [0, 100],
});
expect(scale(10)).toEqual(0.1);
expect(scale(100)).toEqual(1);
});
it('with exponent', () => {
const scale = createScaleFromScaleConfig({
type: 'pow',
exponent: 2,
});
expect(scale(3)).toEqual(9);
expect(scale(4)).toEqual(16);
});
});
describe('sqrt scale', () => {
it('basic', () => {
const scale = createScaleFromScaleConfig({
type: 'sqrt',
});
expect(scale(4)).toEqual(2);
expect(scale(9)).toEqual(3);
});
});
describe('symlog scale', () => {
it('is not supported yet', () => {
expect(() => createScaleFromScaleConfig({ type: 'symlog' })).toThrowError(
'"scale.type = symlog" is not supported yet.',
);
});
});
describe('time scale', () => {
it('basic', () => {
const scale = createScaleFromScaleConfig({
type: 'time',
domain: [
{
year: 2019,
month: 7,
date: 1,
},
{
year: 2019,
month: 7,
date: 31,
},
],
range: [0, 100],
});
expect(scale(new Date(2019, 6, 1))).toEqual(0);
expect(scale(new Date(2019, 6, 16))).toEqual(50);
expect(scale(new Date(2019, 6, 31))).toEqual(100);
});
it('with nice is string', () => {
const scale = createScaleFromScaleConfig({
type: 'time',
domain: [
{
year: 2019,
month: 7,
date: 5,
},
{
year: 2019,
month: 7,
date: 30,
},
],
range: [0, 100],
nice: 'month',
});
expect((scale as ScaleTime<number, number>).domain()).toEqual([
new Date(2019, 6, 1),
new Date(2019, 7, 1),
]);
});
it('with nice is interval object', () => {
const scale = createScaleFromScaleConfig({
type: 'time',
domain: [
{
year: 2019,
month: 7,
date: 5,
},
{
year: 2019,
month: 7,
date: 30,
},
],
range: [0, 100],
nice: { interval: 'month', step: 2 },
});
expect((scale as ScaleTime<number, number>).domain()).toEqual([
new Date(2019, 6, 1),
new Date(2019, 8, 1),
]);
});
});
describe('UTC scale', () => {
it('basic', () => {
const scale = createScaleFromScaleConfig({
type: 'utc',
domain: [
{
year: 2019,
month: 7,
date: 1,
utc: true,
},
{
year: 2019,
month: 7,
date: 31,
utc: true,
},
],
range: [0, 100],
});
expect(scale(new Date(Date.UTC(2019, 6, 1)))).toEqual(0);
expect(scale(new Date(Date.UTC(2019, 6, 16)))).toEqual(50);
expect(scale(new Date(Date.UTC(2019, 6, 31)))).toEqual(100);
});
it('with nice is string', () => {
const scale = createScaleFromScaleConfig({
type: 'utc',
domain: [
{
year: 2019,
month: 7,
date: 5,
utc: true,
},
{
year: 2019,
month: 7,
date: 30,
utc: true,
},
],
range: [0, 100],
nice: 'month',
});
expect((scale as ScaleTime<number, number>).domain()).toEqual([
new Date(Date.UTC(2019, 6, 1)),
new Date(Date.UTC(2019, 7, 1)),
]);
});
it('with nice is interval object', () => {
const scale = createScaleFromScaleConfig({
type: 'utc',
domain: [
{
year: 2019,
month: 7,
date: 5,
utc: true,
},
{
year: 2019,
month: 7,
date: 30,
utc: true,
},
],
range: [0, 100],
nice: { interval: 'month', step: 2 },
});
expect((scale as ScaleTime<number, number>).domain()).toEqual([
new Date(Date.UTC(2019, 6, 1)),
new Date(Date.UTC(2019, 8, 1)),
]);
});
it('with nice is interval object that has invalid step', () => {
const scale = createScaleFromScaleConfig({
type: 'utc',
domain: [
{
year: 2019,
month: 7,
date: 5,
utc: true,
},
{
year: 2019,
month: 7,
date: 30,
utc: true,
},
],
range: [0, 100],
nice: { interval: 'month', step: 0.5 },
});
expect((scale as ScaleTime<number, number>).domain()).toEqual([
new Date(Date.UTC(2019, 6, 5)),
new Date(Date.UTC(2019, 6, 30)),
]);
});
});
describe('quantile scale', () => {
it('basic', () => {
const scale = createScaleFromScaleConfig({
type: 'quantile',
domain: [0, 100],
range: [0, 1, 2, 3],
});
expect(scale(0)).toEqual(0);
expect(scale(10)).toEqual(0);
expect(scale(25)).toEqual(1);
expect(scale(50)).toEqual(2);
expect(scale(75)).toEqual(3);
expect(scale(100)).toEqual(3);
});
});
describe('quantize scale', () => {
it('basic', () => {
const scale = createScaleFromScaleConfig({
type: 'quantize',
domain: [10, 100],
range: [1, 2, 4],
});
expect(scale(20)).toEqual(1);
expect(scale(50)).toEqual(2);
expect(scale(80)).toEqual(4);
});
it('with string range', () => {
const scale = createScaleFromScaleConfig({
type: 'quantize',
domain: [0, 1],
range: ['calm-brown', 'shocking-pink'],
});
expect(scale(0.49)).toEqual('calm-brown');
expect(scale(0.51)).toEqual('shocking-pink');
});
});
describe('threshold scale', () => {
it('basic', () => {
const scale = createScaleFromScaleConfig({
type: 'threshold',
domain: [0, 1],
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');
});
});
describe('ordinal scale', () => {
beforeEach(() => {
getCategoricalSchemeRegistry()
.registerValue(
'test-scheme',
new CategoricalScheme({
id: 'test-scheme',
colors: ['red', 'white', 'green'],
}),
)
.registerValue(
'test-scheme2',
new CategoricalScheme({
id: 'test-scheme',
colors: ['pink', 'charcoal', 'orange'],
}),
)
.setDefaultKey('test-scheme');
});
afterEach(() => {
getCategoricalSchemeRegistry()
.remove('test-scheme')
.remove('test-scheme2')
.clearDefaultKey();
});
it('basic', () => {
const scale = createScaleFromScaleConfig({
type: 'ordinal',
});
expect(scale('fish')).toEqual('red');
expect(scale('dinosaur')).toEqual('white');
expect(scale('whale')).toEqual('green');
});
it('with range', () => {
const scale = createScaleFromScaleConfig({
type: 'ordinal',
domain: ['fish', 'dinosaur'],
range: ['red', 'white', 'green'],
});
expect(scale('fish')).toEqual('red');
expect(scale('dinosaur')).toEqual('white');
expect(scale('whale')).toEqual('green');
});
it('with color scheme', () => {
const scale = createScaleFromScaleConfig({
type: 'ordinal',
scheme: 'test-scheme',
});
expect(scale('fish')).toEqual('red');
expect(scale('dinosaur')).toEqual('white');
expect(scale('whale')).toEqual('green');
});
it('with color scheme and domain', () => {
const scale = createScaleFromScaleConfig({
type: 'ordinal',
domain: ['fish', 'dinosaur'],
scheme: 'test-scheme2',
});
expect(scale('fish')).toEqual('pink');
expect(scale('dinosaur')).toEqual('charcoal');
expect(scale('whale')).toEqual('pink');
});
it('with color scheme and reversed domain', () => {
const scale = createScaleFromScaleConfig({
type: 'ordinal',
domain: ['pig', 'panda'],
reverse: true,
scheme: 'test-scheme2',
});
expect(scale('pig')).toEqual('charcoal');
expect(scale('panda')).toEqual('pink');
});
it('with namespace', () => {
const scale1 = createScaleFromScaleConfig({
type: 'ordinal',
namespace: 'abc',
});
const scale2 = createScaleFromScaleConfig({
type: 'ordinal',
namespace: 'def',
});
const scale3 = createScaleFromScaleConfig({
type: 'ordinal',
namespace: 'def',
});
expect(scale1('fish')).toEqual('red');
expect(scale1('dinosaur')).toEqual('white');
expect(scale1('whale')).toEqual('green');
expect(scale2('whale')).toEqual('red');
expect(scale2('dinosaur')).toEqual('white');
expect(scale2('fish')).toEqual('green');
expect(scale3('fish')).toEqual('green');
expect(scale3('dinosaur')).toEqual('white');
expect(scale3('whale')).toEqual('red');
});
});
describe('bin-ordinal scale', () => {
it('is not supported yet', () => {
expect(() => createScaleFromScaleConfig({ type: 'bin-ordinal' })).toThrowError(
'"scale.type = bin-ordinal" is not supported yet.',
);
});
});
describe('point scale', () => {
it('basic', () => {
const scale = createScaleFromScaleConfig({
type: 'point',
domain: ['fish', 'dinosaur', 'whale'],
range: [0, 100],
});
expect(scale('fish')).toEqual(0);
expect(scale('dinosaur')).toEqual(50);
expect(scale('whale')).toEqual(100);
});
it('with padding', () => {
const scale = createScaleFromScaleConfig({
type: 'point',
domain: ['fish', 'dinosaur', 'whale'],
range: [0, 100],
padding: 1,
});
expect(scale('fish')).toEqual(25);
expect(scale('dinosaur')).toEqual(50);
expect(scale('whale')).toEqual(75);
});
it('with round', () => {
const scale = createScaleFromScaleConfig({
type: 'point',
domain: ['fish', 'dinosaur', 'whale'],
range: [0, 100],
padding: 0.5,
round: true,
});
expect(scale('fish')).toEqual(17);
expect(scale('dinosaur')).toEqual(50);
expect(scale('whale')).toEqual(83);
});
});
describe('band scale', () => {
it('basic', () => {
const scale = createScaleFromScaleConfig({
type: 'band',
domain: ['fish', 'dinosaur'],
range: [0, 100],
});
expect(scale('fish')).toEqual(0);
expect(scale('dinosaur')).toEqual(50);
});
it('with paddingInner', () => {
const scale = createScaleFromScaleConfig<number>({
type: 'band',
domain: ['fish', 'dinosaur', 'whale'],
range: [0, 100],
paddingInner: 0.5,
});
expect(scale('fish')).toEqual(0);
expect(scale('dinosaur')).toEqual(40);
expect(scale('whale')).toEqual(80);
});
it('with paddingOuter', () => {
const scale = createScaleFromScaleConfig<number>({
type: 'band',
domain: ['fish', 'dinosaur', 'whale'],
range: [0, 100],
paddingOuter: 0.5,
});
expect(scale('fish')).toEqual(12.5);
expect(scale('dinosaur')).toEqual(37.5);
expect(scale('whale')).toEqual(62.5);
});
it('with align', () => {
const scale = createScaleFromScaleConfig<number>({
type: 'band',
domain: ['fish', 'dinosaur', 'whale'],
range: [0, 100],
align: 0,
paddingOuter: 0.5,
});
expect(scale('fish')).toEqual(0);
expect(scale('dinosaur')).toEqual(25);
expect(scale('whale')).toEqual(50);
});
});
});

View File

@ -0,0 +1,27 @@
import getScaleCategoryFromScaleType from '../../../src/parsers/scale/getScaleCategoryFromScaleType';
describe('getScaleCategoryFromScaleType(scaleType)', () => {
it('handles continuous types', () => {
expect(getScaleCategoryFromScaleType('linear')).toEqual('continuous');
expect(getScaleCategoryFromScaleType('pow')).toEqual('continuous');
expect(getScaleCategoryFromScaleType('sqrt')).toEqual('continuous');
expect(getScaleCategoryFromScaleType('symlog')).toEqual('continuous');
expect(getScaleCategoryFromScaleType('log')).toEqual('continuous');
expect(getScaleCategoryFromScaleType('time')).toEqual('continuous');
expect(getScaleCategoryFromScaleType('utc')).toEqual('continuous');
});
it('handles discrete types', () => {
expect(getScaleCategoryFromScaleType('band')).toEqual('discrete');
expect(getScaleCategoryFromScaleType('point')).toEqual('discrete');
expect(getScaleCategoryFromScaleType('ordinal')).toEqual('discrete');
});
it('handles discretizing types', () => {
expect(getScaleCategoryFromScaleType('bin-ordinal')).toEqual('discretizing');
expect(getScaleCategoryFromScaleType('quantile')).toEqual('discretizing');
expect(getScaleCategoryFromScaleType('threshold')).toEqual('discretizing');
});
it('handles unknown types', () => {
// @ts-ignore
expect(getScaleCategoryFromScaleType('surprise!')).toBeUndefined();
});
});

View File

@ -0,0 +1,58 @@
import inferScaleType from '../../../src/parsers/scale/inferScaleType';
describe('inferScaleType(channelType, fieldType, isBinned)', () => {
describe('for nominal and ordinal fields', () => {
it('returns band when it should', () => {
expect(inferScaleType('XBand', 'nominal')).toEqual('band');
expect(inferScaleType('YBand', 'ordinal')).toEqual('band');
});
it('returns point when it should', () => {
expect(inferScaleType('X', 'nominal')).toEqual('point');
expect(inferScaleType('Y', 'ordinal')).toEqual('point');
});
it('returns ordinal when it should', () => {
expect(inferScaleType('Color', 'nominal')).toEqual('ordinal');
expect(inferScaleType('Category', 'ordinal')).toEqual('ordinal');
});
});
describe('for quantitative fields', () => {
it('returns linear in general', () => {
expect(inferScaleType('XBand', 'quantitative')).toEqual('linear');
expect(inferScaleType('YBand', 'quantitative')).toEqual('linear');
expect(inferScaleType('X', 'quantitative')).toEqual('linear');
expect(inferScaleType('Y', 'quantitative')).toEqual('linear');
expect(inferScaleType('Numeric', 'quantitative')).toEqual('linear');
});
it('return bin-ordinal for binned color', () => {
expect(inferScaleType('Color', 'quantitative', true)).toEqual('bin-ordinal');
});
it('return linear for color', () => {
expect(inferScaleType('Color', 'quantitative')).toEqual('linear');
});
});
describe('for temporal fields', () => {
it('returns UTC time scale in general', () => {
expect(inferScaleType('XBand', 'temporal')).toEqual('utc');
expect(inferScaleType('YBand', 'temporal')).toEqual('utc');
expect(inferScaleType('X', 'temporal')).toEqual('utc');
expect(inferScaleType('Y', 'temporal')).toEqual('utc');
expect(inferScaleType('Numeric', 'temporal')).toEqual('utc');
});
it('returns linear for color', () => {
expect(inferScaleType('Color', 'temporal')).toEqual('linear');
});
});
describe('for other channel types', () => {
it('returns undefined', () => {
expect(inferScaleType('Text', 'quantitative')).toBeUndefined();
expect(inferScaleType('Text', 'nominal')).toBeUndefined();
expect(inferScaleType('Text', 'ordinal')).toBeUndefined();
expect(inferScaleType('Text', 'temporal')).toBeUndefined();
});
});
describe('for undefined fieldType', () => {
it('returns undefined', () => {
expect(inferScaleType('X')).toBeUndefined();
});
});
});

View File

@ -0,0 +1,10 @@
import isPropertySupportedByScaleType from '../../../src/parsers/scale/isPropertySupportedByScaleType';
describe('isPropertySupportedByScaleType(property, scaleType)', () => {
it('returns true for compatible pairs', () => {
expect(isPropertySupportedByScaleType('scheme', 'ordinal')).toBeTruthy();
});
it('returns false otherwise', () => {
expect(isPropertySupportedByScaleType('zero', 'log')).toBeFalsy();
});
});

View File

@ -0,0 +1,34 @@
import { CategoricalColorScale } from '@superset-ui/color';
import { scaleLinear, scaleOrdinal, scaleTime, scaleLog } from 'd3-scale';
import { isD3Scale, isCategoricalColorScale, isTimeScale } from '../../src/typeGuards/Scale';
import { HasToString } from '../../src/types/Base';
describe('type guards', () => {
describe('isD3Scale(scale)', () => {
it('returns true if it is one of D3 scales', () => {
expect(isD3Scale(scaleLinear())).toBeTruthy();
expect(isD3Scale(scaleOrdinal<HasToString, string>())).toBeTruthy();
});
it('returns false otherwise', () => {
expect(isD3Scale(new CategoricalColorScale(['red', 'yellow']))).toBeFalsy();
});
});
describe('isCategoricalColorScale(scale)', () => {
it('returns true if it is CategoricalColorScale', () => {
expect(isCategoricalColorScale(new CategoricalColorScale(['red', 'yellow']))).toBeTruthy();
});
it('returns false otherwise', () => {
expect(isCategoricalColorScale(scaleLinear())).toBeFalsy();
});
});
describe('isTimeScale(scale, type)', () => {
it('returns true if type is one of the time scale types', () => {
expect(isTimeScale(scaleTime(), 'time')).toBeTruthy();
expect(isTimeScale(scaleTime(), 'utc')).toBeTruthy();
});
it('returns false otherwise', () => {
expect(isTimeScale(scaleLinear(), 'linear')).toBeFalsy();
expect(isTimeScale(scaleLog(), 'log')).toBeFalsy();
});
});
});