Skip to content

Commit

Permalink
feat: Add timeFormatType field (vega#8320)
Browse files Browse the repository at this point in the history
Co-authored-by: GitHub Actions Bot <vega-actions-bot@users.noreply.github.com>
  • Loading branch information
2 people authored and BradyJ27 committed Oct 19, 2023
1 parent a770fb1 commit 70600c7
Show file tree
Hide file tree
Showing 9 changed files with 339 additions and 135 deletions.
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) {
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) {
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({});
});
});
});

0 comments on commit 70600c7

Please sign in to comment.