Skip to content
This repository has been archived by the owner on Dec 10, 2021. It is now read-only.

Commit

Permalink
feat: parse more scales and add more test
Browse files Browse the repository at this point in the history
  • Loading branch information
kristw committed Aug 27, 2019
1 parent f391f5f commit f027bcb
Show file tree
Hide file tree
Showing 10 changed files with 428 additions and 98 deletions.
3 changes: 3 additions & 0 deletions packages/superset-ui-encodeable/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,11 @@
"dependencies": {
"lodash": "^4.17.15",
"@types/d3-scale": "^2.1.1",
"@types/d3-interpolate": "^1.3.1",
"d3-scale": "^3.0.1",
"d3-interpolate": "^1.3.2",
"vega": "^5.4.0",
"vega-expression": "2.6.0",
"vega-lite": "^3.4.0"
},
"peerDependencies": {
Expand Down
31 changes: 31 additions & 0 deletions packages/superset-ui-encodeable/src/parsers/parseDateTime.ts
Original file line number Diff line number Diff line change
@@ -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,
number,
number,
number,
number,
number,
number,
];

return isUtc ? new Date(Date.UTC(...dateParts)) : new Date(...dateParts);
}
Original file line number Diff line number Diff line change
@@ -1,29 +1,40 @@
import {
scaleLinear,
scaleLog,
scalePow,
scaleSqrt,
scaleTime,
scaleUtc,
scaleQuantile,
scaleQuantize,
scaleThreshold,
scaleOrdinal,
scalePoint,
scaleBand,
} from 'd3-scale';
import { interpolateRound } from 'd3-interpolate';
import { getSequentialSchemeRegistry, CategoricalColorNamespace } from '@superset-ui/color';
import { ScaleType, Value } from '../../types/VegaLite';
import { HasToString } from '../../types/Base';
import { ScaleConfig, D3Scale } from '../../types/Scale';
import createScaleFromScaleType from './createScaleFromScaleType';
import parseDateTime from '../parseDateTime';
import inferElementTypeFromUnionOfArrayTypes from '../../utils/inferElementTypeFromUnionOfArrayTypes';
import { isTimeScale } from '../../typeGuards/Scale';

function applySequentialScheme<Output extends Value>(config: ScaleConfig<Output>, scale: D3Scale) {
if ('scheme' in config && typeof config.scheme !== 'undefined') {
const { scheme } = config;
const colorScheme = getSequentialSchemeRegistry().get(scheme);
if (typeof colorScheme !== 'undefined') {
scale.range(colorScheme.colors);
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)) {
scale.domain(
inferElementTypeFromUnionOfArrayTypes(processedDomain).map(d =>
typeof d === 'boolean' ? new Date() : parseDateTime(d),
),
);
} else {
scale.domain(processedDomain);
}
}
}

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);
}
}

Expand All @@ -33,7 +44,7 @@ function applyAlign<Output extends Value>(config: ScaleConfig<Output>, scale: D3
}
}

function applyBins<Output extends Value>(config: ScaleConfig<Output>, scale: D3Scale<Output>) {
function applyBins<Output extends Value>(config: ScaleConfig<Output>) {
if ('bins' in config && typeof config.bins !== 'undefined') {
throw new Error('"scale.bins" is not implemented yet.');
}
Expand Down Expand Up @@ -99,8 +110,13 @@ function applyPadding<Output extends Value>(config: ScaleConfig<Output>, scale:
}

function applyRound<Output extends Value>(config: ScaleConfig<Output>, scale: D3Scale<Output>) {
if ('round' in config && typeof config.round !== 'undefined' && 'round' in scale) {
scale.round(config.round);
if ('round' in config && typeof config.round !== 'undefined') {
const roundableScale = scale as D3Scale<number>;
if ('round' in roundableScale) {
roundableScale.round(config.round);
} else if ('interpolate' in roundableScale) {
roundableScale.interpolate(interpolateRound);
}
}
}

Expand All @@ -113,46 +129,6 @@ function applyZero<Output extends Value>(config: ScaleConfig<Output>, scale: D3S
}
}

// eslint-disable-next-line complexity
function createScaleFromScaleType<Output extends Value>(config: ScaleConfig<Output>) {
switch (config.type) {
default:
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.SYMLOG:
// TODO: d3-scale typings does not include scaleSymlog yet
// needs to patch the declaration file before continue.
throw new Error('"scale.type = symlog" is not implemented yet.');
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.BIN_ORDINAL:
case ScaleType.ORDINAL:
return scaleOrdinal<HasToString, Output>();
case ScaleType.POINT:
return scalePoint<HasToString>();
case ScaleType.BAND:
return scaleBand<HasToString>();
}
}

export default function createScaleFromScaleConfig<Output extends Value>(
config: ScaleConfig<Output>,
) {
Expand All @@ -175,28 +151,26 @@ export default function createScaleFromScaleConfig<Output extends Value>(
});
}

return colorScale;
// 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);

if (typeof domain !== 'undefined') {
scale.domain(reverse ? domain.slice().reverse() : domain);
}

if (typeof range === 'undefined') {
applySequentialScheme(config, scale);
} else {
scale.range(range);
}

// domain and range apply to all scales
applyDomain(config, scale);
applyRange(config, scale);
// Sort other properties alphabetically.
applyAlign(config, scale);
applyBins(config, scale);
applyBins(config);
applyClamp(config, scale);

// TODO: Add support for config.constant
// once symlog is implemented

applyInterpolate(config, scale);
applyNice(config, scale);
applyPadding(config, scale);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
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) {
default:
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.SYMLOG:
// TODO: d3-scale typings does not include scaleSymlog yet
// needs to patch the declaration file before continue.
throw new Error('"scale.type = symlog" is not implemented yet.');
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.BIN_ORDINAL:
case ScaleType.ORDINAL:
return scaleOrdinal<HasToString, Output>();
case ScaleType.POINT:
return scalePoint<HasToString>();
case ScaleType.BAND:
return scaleBand<HasToString>();
}
}
24 changes: 24 additions & 0 deletions packages/superset-ui-encodeable/src/typeGuards/Scale.ts
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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;
}
29 changes: 29 additions & 0 deletions packages/superset-ui-encodeable/test/parsers/parseDateTime.test.ts
Original file line number Diff line number Diff line change
@@ -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)));
});
});
Loading

0 comments on commit f027bcb

Please sign in to comment.