Skip to content

Commit

Permalink
fix: make text formatting preserve invalid values by coercing them to…
Browse files Browse the repository at this point in the history
… string by default
  • Loading branch information
kanitw committed May 20, 2024
1 parent 9212d87 commit 41f3c37
Show file tree
Hide file tree
Showing 11 changed files with 138 additions and 80 deletions.
2 changes: 1 addition & 1 deletion site/docs/invaliddata.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ title: Modes for Handling Invalid Data
permalink: /docs/invalid-data.html
---

This page discusses modes in Vega-Lite for handling invalid data (`null` and `NaN` in continuous scales).
This page discusses modes in Vega-Lite for handling invalid data (`null` and `NaN` in continuous scales *without* defined output for invalid values in `config.scale.invalid`).

The main configurations are [`mark.invalid`](#mark) and [`config.scale.invalid`](#scale). In addition, you can use [other Vega-Lite features including conditional encodings, layering, or window transform to handle invalid and missing data](#other).

Expand Down
6 changes: 3 additions & 3 deletions src/channeldef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ export interface DatumDef<
// `F extends RepeatRef` probably should be `RepeatRef extends F` but there is likely a bug in TS.
}

export interface FormatMixins {
export interface FormatMixins<FT extends string = 'number' | 'time' | string> {
/**
* When used with the default `"number"` and `"time"` format type, the text formatting pattern for labels of guides (axes, legends, headers) and text marks.
*
Expand All @@ -420,7 +420,7 @@ export interface FormatMixins {
* - `"time"` for temporal fields and ordinal and nominal fields with `timeUnit`.
* - `"number"` for quantitative fields as well as ordinal and nominal fields without `timeUnit`.
*/
formatType?: 'number' | 'time' | string;
formatType?: FT;
}

export type StringDatumDef<F extends Field = string> = DatumDef<F> & FormatMixins;
Expand Down Expand Up @@ -974,7 +974,7 @@ export function defaultTitle(fieldDef: FieldDefBase<string>, config: Config) {
return titleFormatter(fieldDef, config);
}

export function getFormatMixins(fieldDef: TypedFieldDef<string> | DatumDef) {
export function getFormatMixins(fieldDef: TypedFieldDef<string> | DatumDef): FormatMixins {
if (isStringFieldOrDatumDef(fieldDef)) {
const {format, formatType} = fieldDef;
return {format, formatType};
Expand Down
21 changes: 13 additions & 8 deletions src/compile/axis/encode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ export function labels(model: UnitModel, channel: PositionScaleChannel, specifie
text: formatCustomType({
fieldOrDatumDef,
field: 'datum.value',
format,
formatType,
formatMixins: {format, formatType},
config
}),
...specifiedLabelsSpec
Expand All @@ -33,8 +32,10 @@ export function labels(model: UnitModel, channel: PositionScaleChannel, specifie
text: formatCustomType({
fieldOrDatumDef,
field: 'datum.value',
format: config.normalizedNumberFormat,
formatType: config.normalizedNumberFormatType,
formatMixins: {
format: config.normalizedNumberFormat,
formatType: config.normalizedNumberFormatType
},
config
}),
...specifiedLabelsSpec
Expand All @@ -44,8 +45,10 @@ export function labels(model: UnitModel, channel: PositionScaleChannel, specifie
text: formatCustomType({
fieldOrDatumDef,
field: 'datum.value',
format: config.numberFormat,
formatType: config.numberFormatType,
formatMixins: {
format: config.numberFormat,
formatType: config.numberFormatType
},
config
}),
...specifiedLabelsSpec
Expand All @@ -62,8 +65,10 @@ export function labels(model: UnitModel, channel: PositionScaleChannel, specifie
text: formatCustomType({
fieldOrDatumDef,
field: 'datum.value',
format: config.timeFormat,
formatType: config.timeFormatType,
formatMixins: {
format: config.timeFormat,
formatType: config.timeFormatType
},
config
}),
...specifiedLabelsSpec
Expand Down
2 changes: 1 addition & 1 deletion src/compile/data/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ function rangeFormula(model: ModelWithField, fieldDef: TypedFieldDef<string>, ch

return {
formulaAs: vgField(fieldDef, {binSuffix: 'range', forAs: true}),
formula: binFormatExpression(startField, endField, guide.format, guide.formatType, config)
formula: binFormatExpression(startField, endField, guide, config)
};
}
return {};
Expand Down
110 changes: 76 additions & 34 deletions src/compile/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
channelDefType,
DatumDef,
FieldDef,
FormatMixins,
isFieldDef,
isFieldOrDatumDefForTimeFormat,
isPositionFieldOrDatumDef,
Expand All @@ -21,7 +22,9 @@ import {isSignalRef} from '../vega.schema';
import {TimeUnit} from './../timeunit';
import {datumDefToExpr} from './mark/encode/valueref';

export function isCustomFormatType(formatType: string) {
export type CustomFormatType = Exclude<string, 'number' | 'time'>;

export function isCustomFormatType(formatType: string): formatType is CustomFormatType {
return formatType && formatType !== 'number' && formatType !== 'time';
}

Expand All @@ -33,24 +36,23 @@ export const BIN_RANGE_DELIMITER = ' \u2013 ';

export function formatSignalRef({
fieldOrDatumDef,
format,
formatType,
formatMixins,
expr,
normalizeStack,
config
}: {
fieldOrDatumDef: FieldDef<string> | DatumDef<string>;
format: string | Dict<unknown>;
formatType: string;
formatMixins: FormatMixins;
expr?: 'datum' | 'parent' | 'datum.datum';
normalizeStack?: boolean;
config: Config;
}) {
const {formatType} = formatMixins;
let {format} = formatMixins;
if (isCustomFormatType(formatType)) {
return formatCustomType({
fieldOrDatumDef,
format,
formatType,
formatMixins: {...formatMixins, formatType},
expr,
config
});
Expand All @@ -64,16 +66,20 @@ export function formatSignalRef({
if (normalizeStack && config.normalizedNumberFormatType)
return formatCustomType({
fieldOrDatumDef,
format: config.normalizedNumberFormat,
formatType: config.normalizedNumberFormatType,
formatMixins: {
format: config.normalizedNumberFormat,
formatType: config.normalizedNumberFormatType
},
expr,
config
});
if (config.numberFormatType) {
return formatCustomType({
fieldOrDatumDef,
format: config.numberFormat,
formatType: config.numberFormatType,
formatMixins: {
format: config.numberFormat,
formatType: config.numberFormatType
},
expr,
config
});
Expand All @@ -87,41 +93,51 @@ export function formatSignalRef({
) {
return formatCustomType({
fieldOrDatumDef,
format: config.timeFormat,
formatType: config.timeFormatType,
formatMixins,
expr,
config
});
}
}

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

const signal = wrapFormatExprToHandleInvalidValues({
mainFormatExpr,
fieldExpr: field
});
return signal ? {signal} : undefined;
}

format = numberFormat({type, specifiedFormat: format, config, normalizeStack});
if (isFieldDef(fieldOrDatumDef) && isBinning(fieldOrDatumDef.bin)) {
const endField = vgField(fieldOrDatumDef, {expr, binSuffix: 'end'});
return {
signal: binFormatExpression(field, endField, format, formatType, config)
signal: binFormatExpression(field, endField, {format, formatType}, config)
};
} else if (format || channelDefType(fieldOrDatumDef) === 'quantitative') {
return {
signal: `${formatExpr(field, format)}`
};
const signal = wrapFormatExprToHandleInvalidValues({
mainFormatExpr: builtInFormatExpr(field, format),
fieldExpr: field
});
return {signal};
} else {
return {signal: `isValid(${field}) ? ${field} : ""+${field}`};
return {signal: toStringExpr(field)};
}
}

function toStringExpr(expr: string) {
return `"" + ${expr}`;
}

function fieldToFormat(
fieldOrDatumDef: FieldDef<string> | DatumDef<string>,
expr: 'datum' | 'parent' | 'datum.datum',
Expand All @@ -143,16 +159,14 @@ function fieldToFormat(

export function formatCustomType({
fieldOrDatumDef,
format,
formatType,
expr,
normalizeStack,
config,
formatMixins,
field
}: {
fieldOrDatumDef: FieldDef<string> | DatumDef<string>;
format: string | Dict<unknown>;
formatType: string;
formatMixins: FormatMixins<CustomFormatType>;
expr?: 'datum' | 'parent' | 'datum.datum';
normalizeStack?: boolean;
config: Config;
Expand All @@ -167,10 +181,15 @@ export function formatCustomType({
) {
const endField = vgField(fieldOrDatumDef, {expr, binSuffix: 'end'});
return {
signal: binFormatExpression(field, endField, format, formatType, config)
signal: binFormatExpression(field, endField, formatMixins, config)
};
}
return {signal: customFormatExpr(formatType, field, format)};
const {format, formatType} = formatMixins;
const signal = wrapFormatExprToHandleInvalidValues({
mainFormatExpr: customFormatExpr(formatType, field, format),
fieldExpr: field
});
return {signal};
}

export function guideFormat(
Expand Down Expand Up @@ -289,31 +308,54 @@ export function timeFormat({
return omitTimeFormatConfig ? undefined : config.timeFormat;
}

function formatExpr(field: string, format: string) {
function builtInFormatExpr(field: string, format: string) {
return `format(${field}, "${format || ''}")`;
}

function binNumberFormatExpr(field: string, format: string | Dict<unknown>, formatType: string, config: Config) {
/**
* return format expr for number bin's start/end
*/
function binExtentFormatExpr(field: string, format: string | Dict<unknown>, formatType: string, config: Config) {
if (isCustomFormatType(formatType)) {
return customFormatExpr(formatType, field, format);
}
format = (isString(format) ? format : undefined) ?? config.numberFormat;
return builtInFormatExpr(field, format);
}

return formatExpr(field, (isString(format) ? format : undefined) ?? config.numberFormat);
function wrapFormatExprToHandleInvalidValues({
fieldExpr,
mainFormatExpr
}: {
mainFormatExpr: string;
fieldExpr: string;
}): string {
return `${fieldValidPredicate(fieldExpr, false)} ? ${toStringExpr(fieldExpr)} : ${mainFormatExpr}`;
}

export function binFormatExpression(
startField: string,
endField: string,
format: string | Dict<unknown>,
formatType: string,
{format, formatType}: FormatMixins,
config: Config
): string {
if (format === undefined && formatType === undefined && config.customFormatTypes && config.numberFormatType) {
return binFormatExpression(startField, endField, config.numberFormat, config.numberFormatType, config);
return binFormatExpression(
startField,
endField,
{
format: config.numberFormat,
formatType: config.numberFormatType
},
config
);
}
const start = binNumberFormatExpr(startField, format, formatType, config);
const end = binNumberFormatExpr(endField, format, formatType, config);
return `${fieldValidPredicate(startField, false)} ? "null" : ${start} + "${BIN_RANGE_DELIMITER}" + ${end}`;
const start = binExtentFormatExpr(startField, format, formatType, config);
const end = binExtentFormatExpr(endField, format, formatType, config);
return wrapFormatExprToHandleInvalidValues({
fieldExpr: startField,
mainFormatExpr: `${start} + "${BIN_RANGE_DELIMITER}" + ${end}`
});
}

/**
Expand Down
3 changes: 1 addition & 2 deletions src/compile/header/assemble.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,7 @@ export function assembleLabelTitle(

const titleTextExpr = formatSignalRef({
fieldOrDatumDef: facetFieldDef,
format,
formatType,
formatMixins: {format, formatType},
expr: 'parent',
config
}).signal;
Expand Down
15 changes: 9 additions & 6 deletions src/compile/legend/encode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,17 +156,18 @@ export function labels(specifiedlabelsSpec: any, {fieldOrDatumDef, model, channe
text = formatCustomType({
fieldOrDatumDef,
field: 'datum.value',
format,
formatType,
formatMixins: {format, formatType},
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,
formatMixins: {
format: config.numberFormat,
formatType: config.numberFormatType
},
config
});
} else if (
Expand All @@ -178,8 +179,10 @@ export function labels(specifiedlabelsSpec: any, {fieldOrDatumDef, model, channe
text = formatCustomType({
fieldOrDatumDef,
field: 'datum.value',
format: config.timeFormat,
formatType: config.timeFormatType,
formatMixins: {
format: config.timeFormat,
formatType: config.timeFormatType
},
config
});
}
Expand Down
4 changes: 2 additions & 2 deletions src/compile/mark/encode/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ export function textRef(
return signalOrValueRef(channelDef.value);
}
if (isFieldOrDatumDef(channelDef)) {
const {format, formatType} = getFormatMixins(channelDef);
return formatSignalRef({fieldOrDatumDef: channelDef, format, formatType, expr, config});
const formatMixins = getFormatMixins(channelDef);
return formatSignalRef({fieldOrDatumDef: channelDef, formatMixins, expr, config});
}
}
return undefined;
Expand Down
Loading

0 comments on commit 41f3c37

Please sign in to comment.