Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add timeFormatType field #8320

Merged
merged 16 commits into from Aug 4, 2022
4 changes: 4 additions & 0 deletions build/vega-lite-schema.json
Expand Up @@ -7725,6 +7725,10 @@
"description": "Default time format for raw time values (without time units) in text marks, legend labels and header labels.\n\n__Default value:__ `\"%b %d, %Y\"` __Note:__ Axes automatically determine the format for each label automatically so this config does not affect axes.",
"type": "string"
},
"timeFormatType": {
"description": "[Custom format type](https://vega.github.io/vega-lite/docs/config.html#custom-format-type) for `config.timeFormat`.\n\n__Default value:__ `undefined` -- This is equilvalent to call D3-time-format, which is exposed as [`timeFormat` in Vega-Expression](https://vega.github.io/vega/docs/expressions/#timeFormat). __Note:__ You must also set `customFormatTypes` to `true` and there must *not* be a `timeUnit` defined to use this feature.",
"type": "string"
},
"title": {
"$ref": "#/definitions/TitleConfig",
"description": "Title configuration, which determines default properties for all [titles](https://vega.github.io/vega-lite/docs/title.html). For a full list of title configuration options, please see the [corresponding section of the title documentation](https://vega.github.io/vega-lite/docs/title.html#config)."
Expand Down
4 changes: 2 additions & 2 deletions site/docs/config.md
Expand Up @@ -50,9 +50,9 @@ A Vega-Lite `config` object can have the following top-level properties:

## Format Configuration

These two config properties define the default number and time formats for text marks as well as axes, headers, and legends:
These config properties define the default number and time formats for text marks as well as axes, headers, and legends:

{% include table.html props="numberFormat,numberFormatType,timeFormat,customFormatTypes" source="Config" %}
{% include table.html props="numberFormat,numberFormatType,normalizedNumberFormat,normalizedNumberFormatType,timeFormat,timeFormatType,customFormatTypes" source="Config" %}

{:#custom-format-type}

Expand Down
60 changes: 37 additions & 23 deletions src/compile/axis/encode.ts
@@ -1,5 +1,5 @@
import {getSecondaryRangeChannel, PositionScaleChannel} from '../../channel';
import {channelDefType, getFieldOrDatumDef, isPositionFieldOrDatumDef} from '../../channeldef';
import {channelDefType, getFieldOrDatumDef, isFieldDef, isPositionFieldOrDatumDef} from '../../channeldef';
import {formatCustomType, isCustomFormatType} from '../format';
import {UnitModel} from '../unit';

Expand All @@ -22,34 +22,48 @@ export function labels(model: UnitModel, channel: PositionScaleChannel, specifie
}),
...specifiedLabelsSpec
};
} else if (
format === undefined &&
formatType === undefined &&
channelDefType(fieldOrDatumDef) === 'quantitative' &&
config.customFormatTypes
) {
} else if (format === undefined && formatType === undefined && config.customFormatTypes) {
if (channelDefType(fieldOrDatumDef) === 'quantitative') {
if (
isPositionFieldOrDatumDef(fieldOrDatumDef) &&
fieldOrDatumDef.stack === 'normalize' &&
config.normalizedNumberFormatType
) {
return {
text: formatCustomType({
fieldOrDatumDef,
field: 'datum.value',
format: config.normalizedNumberFormat,
formatType: config.normalizedNumberFormatType,
config
}),
...specifiedLabelsSpec
};
} else if (config.numberFormatType) {
return {
text: formatCustomType({
fieldOrDatumDef,
field: 'datum.value',
format: config.numberFormat,
formatType: config.numberFormatType,
config
}),
...specifiedLabelsSpec
};
}
}
if (
isPositionFieldOrDatumDef(fieldOrDatumDef) &&
fieldOrDatumDef.stack === 'normalize' &&
config.normalizedNumberFormatType
channelDefType(fieldOrDatumDef) === 'temporal' &&
config.timeFormatType &&
isFieldDef(fieldOrDatumDef) &&
!fieldOrDatumDef.timeUnit
) {
return {
text: formatCustomType({
fieldOrDatumDef,
field: 'datum.value',
format: config.normalizedNumberFormat,
formatType: config.normalizedNumberFormatType,
config
}),
...specifiedLabelsSpec
};
} else if (config.numberFormatType) {
return {
text: formatCustomType({
fieldOrDatumDef,
field: 'datum.value',
format: config.numberFormat,
formatType: config.numberFormatType,
format: config.timeFormat,
formatType: config.timeFormatType,
config
}),
...specifiedLabelsSpec
Expand Down
145 changes: 85 additions & 60 deletions src/compile/format.ts
Expand Up @@ -59,47 +59,51 @@ export function formatSignalRef({
const field = fieldToFormat(fieldOrDatumDef, expr, normalizeStack);
const type = channelDefType(fieldOrDatumDef);

if (
normalizeStack &&
type === 'quantitative' &&
format === undefined &&
formatType === undefined &&
config.customFormatTypes &&
config.normalizedNumberFormatType
) {
return formatCustomType({
fieldOrDatumDef,
format: config.normalizedNumberFormat,
formatType: config.normalizedNumberFormatType,
expr,
config
});
}

if (
type === 'quantitative' &&
format === undefined &&
formatType === undefined &&
config.customFormatTypes &&
config.numberFormatType
) {
return formatCustomType({
fieldOrDatumDef,
format: config.numberFormat,
formatType: config.numberFormatType,
expr,
config
});
if (format === undefined && formatType === undefined && config.customFormatTypes) {
if (type === 'quantitative') {
if (normalizeStack && config.normalizedNumberFormatType)
return formatCustomType({
fieldOrDatumDef,
format: config.normalizedNumberFormat,
formatType: config.normalizedNumberFormatType,
expr,
config
});
if (config.numberFormatType) {
return formatCustomType({
fieldOrDatumDef,
format: config.numberFormat,
formatType: config.numberFormatType,
expr,
config
});
}
}
if (
type === 'temporal' &&
config.timeFormatType &&
isFieldDef(fieldOrDatumDef) &&
fieldOrDatumDef.timeUnit === undefined
) {
return formatCustomType({
fieldOrDatumDef,
format: config.timeFormat,
formatType: config.timeFormatType,
expr,
config
});
}
}

if (isFieldOrDatumDefForTimeFormat(fieldOrDatumDef)) {
const signal = timeFormatExpression(
const signal = timeFormatExpression({
field,
isFieldDef(fieldOrDatumDef) ? normalizeTimeUnit(fieldOrDatumDef.timeUnit)?.unit : undefined,
timeUnit: isFieldDef(fieldOrDatumDef) ? normalizeTimeUnit(fieldOrDatumDef.timeUnit)?.unit : undefined,
format,
config.timeFormat,
isScaleFieldDef(fieldOrDatumDef) && fieldOrDatumDef.scale?.type === ScaleType.UTC
);
formatType: config.timeFormatType,
rawTimeFormat: config.timeFormat,
isUTCScale: isScaleFieldDef(fieldOrDatumDef) && fieldOrDatumDef.scale?.type === ScaleType.UTC
});
return signal ? {signal} : undefined;
}

Expand Down Expand Up @@ -179,21 +183,18 @@ export function guideFormat(
) {
if (isCustomFormatType(formatType)) {
return undefined; // handled in encode block
} else if (
format === undefined &&
formatType === undefined &&
config.customFormatTypes &&
channelDefType(fieldOrDatumDef) === 'quantitative'
) {
if (
config.normalizedNumberFormatType &&
isPositionFieldOrDatumDef(fieldOrDatumDef) &&
fieldOrDatumDef.stack === 'normalize'
) {
return undefined; // handled in encode block
}
if (config.numberFormatType) {
lsh marked this conversation as resolved.
Show resolved Hide resolved
return undefined; // handled in encode block
} else if (format === undefined && formatType === undefined && config.customFormatTypes) {
if (channelDefType(fieldOrDatumDef) === 'quantitative') {
if (
config.normalizedNumberFormatType &&
isPositionFieldOrDatumDef(fieldOrDatumDef) &&
fieldOrDatumDef.stack === 'normalize'
) {
return undefined; // handled in encode block
}
if (config.numberFormatType) {
return undefined; // handled in encode block
}
}
}

Expand All @@ -211,8 +212,11 @@ export function guideFormat(

if (isFieldOrDatumDefForTimeFormat(fieldOrDatumDef)) {
const timeUnit = isFieldDef(fieldOrDatumDef) ? normalizeTimeUnit(fieldOrDatumDef.timeUnit)?.unit : undefined;
if (timeUnit === undefined && config.customFormatTypes && config.timeFormatType) {
lsh marked this conversation as resolved.
Show resolved Hide resolved
return undefined; // hanlded in encode block
}

return timeFormat(format as string, timeUnit, config, omitTimeFormatConfig);
return timeFormat({specifiedFormat: format as string, timeUnit, config, omitTimeFormatConfig});
}

return numberFormat({type, specifiedFormat: format, config});
Expand Down Expand Up @@ -261,7 +265,17 @@ export function numberFormat({
/**
* Returns time format for a fieldDef for use in guides.
*/
export function timeFormat(specifiedFormat: string, timeUnit: TimeUnit, config: Config, omitTimeFormatConfig: boolean) {
export function timeFormat({
specifiedFormat,
timeUnit,
config,
omitTimeFormatConfig
}: {
specifiedFormat?: string;
timeUnit?: TimeUnit;
config: Config;
omitTimeFormatConfig?: boolean;
}) {
if (specifiedFormat) {
return specifiedFormat;
}
Expand Down Expand Up @@ -305,15 +319,26 @@ export function binFormatExpression(
/**
* Returns the time expression used for axis/legend labels or text mark for a temporal field
*/
export function timeFormatExpression(
field: string,
timeUnit: TimeUnit,
format: string | Dict<unknown>,
rawTimeFormat: string, // should be provided only for actual text and headers, not axis/legend labels
isUTCScale: boolean
): string {
export function timeFormatExpression({
field,
timeUnit,
format,
formatType,
rawTimeFormat,
isUTCScale
}: {
field: string;
timeUnit?: TimeUnit;
format?: string | Dict<unknown>;
formatType?: string;
rawTimeFormat?: string; // should be provided only for actual text and headers, not axis/legend labels
isUTCScale?: boolean;
}): string {
if (!timeUnit || format) {
// If there is no time unit, or if user explicitly specifies format for axis/legend/text.
if (!timeUnit && formatType) {
return `${formatType}(${field}, '${format}')`;
}
format = isString(format) ? format : rawTimeFormat; // only use provided timeFormat if there is no timeUnit.
return `${isUTCScale ? 'utc' : 'time'}Format(${field}, '${format}')`;
} else {
Expand Down
37 changes: 23 additions & 14 deletions src/compile/legend/encode.ts
Expand Up @@ -160,20 +160,29 @@ export function labels(specifiedlabelsSpec: any, {fieldOrDatumDef, model, channe
formatType,
config
});
} else if (
fieldOrDatumDef.type === 'quantitative' &&
format === undefined &&
formatType === undefined &&
config.customFormatTypes &&
config.numberFormatType
) {
text = formatCustomType({
fieldOrDatumDef,
field: 'datum.value',
format: config.numberFormat,
formatType: config.numberFormatType,
config
});
} else if (format === undefined && formatType === undefined && config.customFormatTypes) {
if (fieldOrDatumDef.type === 'quantitative' && config.numberFormatType) {
text = formatCustomType({
fieldOrDatumDef,
field: 'datum.value',
format: config.numberFormat,
formatType: config.numberFormatType,
config
});
} else if (
fieldOrDatumDef.type === 'temporal' &&
config.timeFormatType &&
isFieldDef(fieldOrDatumDef) &&
fieldOrDatumDef.timeUnit === undefined
) {
text = formatCustomType({
fieldOrDatumDef,
field: 'datum.value',
format: config.timeFormat,
formatType: config.timeFormatType,
config
});
}
}

const labelsSpec = {
Expand Down
9 changes: 9 additions & 0 deletions src/config.ts
Expand Up @@ -189,6 +189,15 @@ export interface VLOnlyConfig<ES extends ExprRef | SignalRef> {
*/
timeFormat?: string;

/**
* [Custom format type](https://vega.github.io/vega-lite/docs/config.html#custom-format-type)
* for `config.timeFormat`.
*
* __Default value:__ `undefined` -- This is equilvalent to call D3-time-format, which is exposed as [`timeFormat` in Vega-Expression](https://vega.github.io/vega/docs/expressions/#timeFormat).
* __Note:__ You must also set `customFormatTypes` to `true` and there must *not* be a `timeUnit` defined to use this feature.
*/
timeFormatType?: string;

/**
* Allow the `formatType` property for text marks and guides to accept a custom formatter function [registered as a Vega expression](https://vega.github.io/vega-lite/usage/compile.html#format-type).
*/
Expand Down
24 changes: 24 additions & 0 deletions test/compile/axis/encode.test.ts
Expand Up @@ -77,5 +77,29 @@ describe('compile/axis/encode', () => {
const labels = encode.labels(model, 'x', {});
expect(labels.text.signal).toBe('customNumberFormat(datum.value, "abc")');
});

it('applies custom timeFormatType from config', () => {
const model = parseUnitModelWithScale({
mark: 'point',
encoding: {
x: {field: 'a', type: 'temporal'}
},
config: {customFormatTypes: true, timeFormat: 'abc', timeFormatType: 'customTimeFormat'}
});
const labels = encode.labels(model, 'x', {});
expect(labels.text.signal).toBe('customTimeFormat(datum.value, "abc")');
});

it('prefers timeUnit over timeFormatType from config', () => {
const model = parseUnitModelWithScale({
mark: 'point',
encoding: {
x: {field: 'a', type: 'temporal', timeUnit: 'date'}
},
config: {customFormatTypes: true, timeFormat: 'abc', timeFormatType: 'customTimeFormat'}
});
const labels = encode.labels(model, 'x', {});
expect(labels).toEqual({});
});
});
});