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

feat: add functions for parsing scales #207

Merged
merged 23 commits into from
Aug 28, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -90,6 +90,11 @@
]
},
"typescript": {
"compilerOptions": {
"typeRoots": [
"../../node_modules/vega-lite/typings"
]
},
"include": [
"./storybook/**/*"
]
Expand Down
16 changes: 10 additions & 6 deletions packages/superset-ui-dimension/src/svg/updateTextNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
8 changes: 8 additions & 0 deletions packages/superset-ui-encodeable/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
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, // year
number, // month
number, // date
number, // hours
number, // minutes
number, // seconds
number, // milliseconds
];

return isUtc ? new Date(Date.UTC(...dateParts)) : new Date(...dateParts);
}
11 changes: 11 additions & 0 deletions packages/superset-ui-encodeable/src/parsers/scale/applyAlign.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
11 changes: 11 additions & 0 deletions packages/superset-ui-encodeable/src/parsers/scale/applyClamp.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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.');
}
}
77 changes: 77 additions & 0 deletions packages/superset-ui-encodeable/src/parsers/scale/applyNice.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
21 changes: 21 additions & 0 deletions packages/superset-ui-encodeable/src/parsers/scale/applyRange.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
22 changes: 22 additions & 0 deletions packages/superset-ui-encodeable/src/parsers/scale/applyRound.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
12 changes: 12 additions & 0 deletions packages/superset-ui-encodeable/src/parsers/scale/applyZero.ts
Original file line number Diff line number Diff line change
@@ -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)]);
}
}
Original file line number Diff line number Diff line change
@@ -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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah this is a tricky one

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